@mathers101/react-native-render-rich-text 0.0.0-stage → 0.1.10

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 (358) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +140 -3
  3. package/ReactNativeRenderRichText.podspec +23 -0
  4. package/android/build.gradle +86 -0
  5. package/android/consumer-rules.pro +3 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/jni-adapter.cpp +191 -0
  8. package/android/src/main/java/com/renderrichtext/EnrichedTiptapText.kt +673 -0
  9. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextLayoutManager.kt +24 -0
  10. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextManager.kt +405 -0
  11. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextPackage.kt +13 -0
  12. package/android/src/main/java/com/renderrichtext/MeasurementStore.kt +644 -0
  13. package/android/src/main/java/com/renderrichtext/accessibility/AccessibilityLabels.kt +17 -0
  14. package/android/src/main/java/com/renderrichtext/accessibility/AccessibleTiptapTextView.kt +112 -0
  15. package/android/src/main/java/com/renderrichtext/accessibility/TiptapAccessibilityHelper.kt +576 -0
  16. package/android/src/main/java/com/renderrichtext/events/CodeBlockPressEvent.kt +24 -0
  17. package/android/src/main/java/com/renderrichtext/events/ContextMenuItemPressEvent.kt +28 -0
  18. package/android/src/main/java/com/renderrichtext/events/CopyPressEvent.kt +24 -0
  19. package/android/src/main/java/com/renderrichtext/events/FactoidPressEvent.kt +23 -0
  20. package/android/src/main/java/com/renderrichtext/events/ImagePressEvent.kt +25 -0
  21. package/android/src/main/java/com/renderrichtext/events/LatexErrorEvent.kt +29 -0
  22. package/android/src/main/java/com/renderrichtext/events/LinkLongPressEvent.kt +23 -0
  23. package/android/src/main/java/com/renderrichtext/events/LinkPressEvent.kt +23 -0
  24. package/android/src/main/java/com/renderrichtext/events/MentionLongPressEvent.kt +25 -0
  25. package/android/src/main/java/com/renderrichtext/events/MentionPressEvent.kt +25 -0
  26. package/android/src/main/java/com/renderrichtext/events/TaskListItemPressEvent.kt +26 -0
  27. package/android/src/main/java/com/renderrichtext/math/LatexErrorReporter.kt +9 -0
  28. package/android/src/main/java/com/renderrichtext/parser/Parser.kt +59 -0
  29. package/android/src/main/java/com/renderrichtext/parser/TextLinkMatching.kt +35 -0
  30. package/android/src/main/java/com/renderrichtext/parser/TiptapASTNode.kt +69 -0
  31. package/android/src/main/java/com/renderrichtext/renderer/BaselineShiftRenderer.kt +45 -0
  32. package/android/src/main/java/com/renderrichtext/renderer/BlankLineRenderer.kt +39 -0
  33. package/android/src/main/java/com/renderrichtext/renderer/BlockStyleContext.kt +181 -0
  34. package/android/src/main/java/com/renderrichtext/renderer/BlockquoteRenderer.kt +243 -0
  35. package/android/src/main/java/com/renderrichtext/renderer/BlockquoteTextRenderer.kt +56 -0
  36. package/android/src/main/java/com/renderrichtext/renderer/CodeBlockRenderer.kt +135 -0
  37. package/android/src/main/java/com/renderrichtext/renderer/CodeRenderer.kt +37 -0
  38. package/android/src/main/java/com/renderrichtext/renderer/EmphasisRenderer.kt +28 -0
  39. package/android/src/main/java/com/renderrichtext/renderer/FactoidRenderer.kt +97 -0
  40. package/android/src/main/java/com/renderrichtext/renderer/HeadingRenderer.kt +81 -0
  41. package/android/src/main/java/com/renderrichtext/renderer/HighlightRenderer.kt +26 -0
  42. package/android/src/main/java/com/renderrichtext/renderer/LineBreakRenderer.kt +17 -0
  43. package/android/src/main/java/com/renderrichtext/renderer/LinkRenderer.kt +90 -0
  44. package/android/src/main/java/com/renderrichtext/renderer/ListContextManager.kt +109 -0
  45. package/android/src/main/java/com/renderrichtext/renderer/ListItemRenderer.kt +211 -0
  46. package/android/src/main/java/com/renderrichtext/renderer/ListRenderer.kt +133 -0
  47. package/android/src/main/java/com/renderrichtext/renderer/MentionRenderer.kt +85 -0
  48. package/android/src/main/java/com/renderrichtext/renderer/NodeRenderer.kt +207 -0
  49. package/android/src/main/java/com/renderrichtext/renderer/ParagraphRenderer.kt +91 -0
  50. package/android/src/main/java/com/renderrichtext/renderer/Renderer.kt +148 -0
  51. package/android/src/main/java/com/renderrichtext/renderer/SoftBreakRenderer.kt +17 -0
  52. package/android/src/main/java/com/renderrichtext/renderer/SpanStyleCache.kt +143 -0
  53. package/android/src/main/java/com/renderrichtext/renderer/StrikethroughRenderer.kt +28 -0
  54. package/android/src/main/java/com/renderrichtext/renderer/StrongRenderer.kt +28 -0
  55. package/android/src/main/java/com/renderrichtext/renderer/TextRenderer.kt +31 -0
  56. package/android/src/main/java/com/renderrichtext/renderer/ThematicBreakRenderer.kt +46 -0
  57. package/android/src/main/java/com/renderrichtext/renderer/UnderlineRenderer.kt +28 -0
  58. package/android/src/main/java/com/renderrichtext/spans/BaseListSpan.kt +164 -0
  59. package/android/src/main/java/com/renderrichtext/spans/BaselineShiftSpan.kt +27 -0
  60. package/android/src/main/java/com/renderrichtext/spans/BlockquoteSpan.kt +302 -0
  61. package/android/src/main/java/com/renderrichtext/spans/CodeBackgroundSpan.kt +205 -0
  62. package/android/src/main/java/com/renderrichtext/spans/CodeBlockSpan.kt +216 -0
  63. package/android/src/main/java/com/renderrichtext/spans/CodeSpan.kt +32 -0
  64. package/android/src/main/java/com/renderrichtext/spans/EmphasisSpan.kt +42 -0
  65. package/android/src/main/java/com/renderrichtext/spans/FactoidBackgroundSpan.kt +265 -0
  66. package/android/src/main/java/com/renderrichtext/spans/FactoidPaddingSpan.kt +258 -0
  67. package/android/src/main/java/com/renderrichtext/spans/FactoidSpan.kt +32 -0
  68. package/android/src/main/java/com/renderrichtext/spans/FadeInSpan.kt +29 -0
  69. package/android/src/main/java/com/renderrichtext/spans/HeadingSpan.kt +38 -0
  70. package/android/src/main/java/com/renderrichtext/spans/HighlightSpan.kt +80 -0
  71. package/android/src/main/java/com/renderrichtext/spans/ImageSpan.kt +481 -0
  72. package/android/src/main/java/com/renderrichtext/spans/InlineBackgroundGeometry.kt +42 -0
  73. package/android/src/main/java/com/renderrichtext/spans/LineHeightSpan.kt +39 -0
  74. package/android/src/main/java/com/renderrichtext/spans/LinkPillIconCache.kt +154 -0
  75. package/android/src/main/java/com/renderrichtext/spans/LinkPillSpan.kt +381 -0
  76. package/android/src/main/java/com/renderrichtext/spans/LinkSpan.kt +68 -0
  77. package/android/src/main/java/com/renderrichtext/spans/ListItemSpacingSpan.kt +33 -0
  78. package/android/src/main/java/com/renderrichtext/spans/ListMarkerAnchorSpan.kt +38 -0
  79. package/android/src/main/java/com/renderrichtext/spans/MarginBottomSpan.kt +76 -0
  80. package/android/src/main/java/com/renderrichtext/spans/MentionChipSpan.kt +132 -0
  81. package/android/src/main/java/com/renderrichtext/spans/MentionSpan.kt +46 -0
  82. package/android/src/main/java/com/renderrichtext/spans/OrderedListSpan.kt +127 -0
  83. package/android/src/main/java/com/renderrichtext/spans/SpoilerSpan.kt +33 -0
  84. package/android/src/main/java/com/renderrichtext/spans/StrikethroughSpan.kt +12 -0
  85. package/android/src/main/java/com/renderrichtext/spans/StrongSpan.kt +39 -0
  86. package/android/src/main/java/com/renderrichtext/spans/TaskListSpan.kt +143 -0
  87. package/android/src/main/java/com/renderrichtext/spans/TextSpan.kt +26 -0
  88. package/android/src/main/java/com/renderrichtext/spans/ThematicBreakSpan.kt +69 -0
  89. package/android/src/main/java/com/renderrichtext/spans/UnorderedListSpan.kt +96 -0
  90. package/android/src/main/java/com/renderrichtext/spoiler/SpoilerStubs.kt +46 -0
  91. package/android/src/main/java/com/renderrichtext/styles/BaseBlockStyle.kt +11 -0
  92. package/android/src/main/java/com/renderrichtext/styles/BlockquoteStyle.kt +83 -0
  93. package/android/src/main/java/com/renderrichtext/styles/CodeBlockStyle.kt +80 -0
  94. package/android/src/main/java/com/renderrichtext/styles/CodeStyle.kt +26 -0
  95. package/android/src/main/java/com/renderrichtext/styles/EmphasisStyle.kt +21 -0
  96. package/android/src/main/java/com/renderrichtext/styles/FactoidStyle.kt +21 -0
  97. package/android/src/main/java/com/renderrichtext/styles/HeadingStyle.kt +33 -0
  98. package/android/src/main/java/com/renderrichtext/styles/HighlightStyle.kt +19 -0
  99. package/android/src/main/java/com/renderrichtext/styles/ImageStyle.kt +29 -0
  100. package/android/src/main/java/com/renderrichtext/styles/InlineImageStyle.kt +17 -0
  101. package/android/src/main/java/com/renderrichtext/styles/InlineMathStyle.kt +17 -0
  102. package/android/src/main/java/com/renderrichtext/styles/LinkPillContent.kt +36 -0
  103. package/android/src/main/java/com/renderrichtext/styles/LinkPillStyle.kt +44 -0
  104. package/android/src/main/java/com/renderrichtext/styles/LinkStyle.kt +23 -0
  105. package/android/src/main/java/com/renderrichtext/styles/LinkVariantEntry.kt +34 -0
  106. package/android/src/main/java/com/renderrichtext/styles/ListStyle.kt +65 -0
  107. package/android/src/main/java/com/renderrichtext/styles/MathStyle.kt +29 -0
  108. package/android/src/main/java/com/renderrichtext/styles/MentionStyle.kt +19 -0
  109. package/android/src/main/java/com/renderrichtext/styles/ParagraphStyle.kt +33 -0
  110. package/android/src/main/java/com/renderrichtext/styles/SpoilerStyle.kt +27 -0
  111. package/android/src/main/java/com/renderrichtext/styles/StrikethroughStyle.kt +17 -0
  112. package/android/src/main/java/com/renderrichtext/styles/StrongStyle.kt +21 -0
  113. package/android/src/main/java/com/renderrichtext/styles/StyleConfig.kt +411 -0
  114. package/android/src/main/java/com/renderrichtext/styles/StyleParser.kt +92 -0
  115. package/android/src/main/java/com/renderrichtext/styles/SubscriptStyle.kt +16 -0
  116. package/android/src/main/java/com/renderrichtext/styles/SuperscriptStyle.kt +16 -0
  117. package/android/src/main/java/com/renderrichtext/styles/TableStyle.kt +75 -0
  118. package/android/src/main/java/com/renderrichtext/styles/TaskListStyle.kt +29 -0
  119. package/android/src/main/java/com/renderrichtext/styles/TextAlignment.kt +32 -0
  120. package/android/src/main/java/com/renderrichtext/styles/ThematicBreakStyle.kt +23 -0
  121. package/android/src/main/java/com/renderrichtext/styles/UnderlineStyle.kt +17 -0
  122. package/android/src/main/java/com/renderrichtext/styles/VideoStyle.kt +31 -0
  123. package/android/src/main/java/com/renderrichtext/utils/common/BorderStyleUtils.kt +86 -0
  124. package/android/src/main/java/com/renderrichtext/utils/common/BreakStrategyUtils.kt +32 -0
  125. package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockHighlighter.kt +36 -0
  126. package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockNode.kt +42 -0
  127. package/android/src/main/java/com/renderrichtext/utils/common/EllipsizeUtils.kt +27 -0
  128. package/android/src/main/java/com/renderrichtext/utils/common/EnrichedTiptapAncestorUtils.kt +10 -0
  129. package/android/src/main/java/com/renderrichtext/utils/common/FeatureFlags.kt +9 -0
  130. package/android/src/main/java/com/renderrichtext/utils/common/LinkRecognitionProps.kt +31 -0
  131. package/android/src/main/java/com/renderrichtext/utils/common/LinkRegexConfig.kt +56 -0
  132. package/android/src/main/java/com/renderrichtext/utils/common/MotionPreferences.kt +18 -0
  133. package/android/src/main/java/com/renderrichtext/utils/common/ReadableMapExtensions.kt +28 -0
  134. package/android/src/main/java/com/renderrichtext/utils/common/TiptapViewManagerUtils.kt +242 -0
  135. package/android/src/main/java/com/renderrichtext/utils/common/layout/RTLUtils.kt +6 -0
  136. package/android/src/main/java/com/renderrichtext/utils/common/serialization/TiptapASTSerializer.kt +368 -0
  137. package/android/src/main/java/com/renderrichtext/utils/text/ImageCache.kt +88 -0
  138. package/android/src/main/java/com/renderrichtext/utils/text/ImageDownloader.kt +203 -0
  139. package/android/src/main/java/com/renderrichtext/utils/text/LocalImageLoader.kt +167 -0
  140. package/android/src/main/java/com/renderrichtext/utils/text/TailFadeInAnimator.kt +84 -0
  141. package/android/src/main/java/com/renderrichtext/utils/text/conversion/HTMLGenerator.kt +1105 -0
  142. package/android/src/main/java/com/renderrichtext/utils/text/conversion/TiptapExtractor.kt +423 -0
  143. package/android/src/main/java/com/renderrichtext/utils/text/extensions/ParagraphUtils.kt +9 -0
  144. package/android/src/main/java/com/renderrichtext/utils/text/extensions/SpannableExtensions.kt +9 -0
  145. package/android/src/main/java/com/renderrichtext/utils/text/extensions/TextPaintExtensions.kt +76 -0
  146. package/android/src/main/java/com/renderrichtext/utils/text/interaction/CheckboxTouchHelper.kt +61 -0
  147. package/android/src/main/java/com/renderrichtext/utils/text/interaction/TaskListTapUtils.kt +286 -0
  148. package/android/src/main/java/com/renderrichtext/utils/text/span/ApplyLineHeight.kt +39 -0
  149. package/android/src/main/java/com/renderrichtext/utils/text/span/BlockSpacing.kt +142 -0
  150. package/android/src/main/java/com/renderrichtext/utils/text/span/SpanFlags.kt +43 -0
  151. package/android/src/main/java/com/renderrichtext/utils/text/span/WidthAwareSpans.kt +30 -0
  152. package/android/src/main/java/com/renderrichtext/utils/text/view/CodeBlockPressHost.kt +15 -0
  153. package/android/src/main/java/com/renderrichtext/utils/text/view/ImagePressHost.kt +19 -0
  154. package/android/src/main/java/com/renderrichtext/utils/text/view/LinkEvents.kt +88 -0
  155. package/android/src/main/java/com/renderrichtext/utils/text/view/LinkLongPressMovementMethod.kt +449 -0
  156. package/android/src/main/java/com/renderrichtext/utils/text/view/SelectionActionMode.kt +235 -0
  157. package/android/src/main/java/com/renderrichtext/utils/text/view/TextSelectionColors.kt +48 -0
  158. package/android/src/main/java/com/renderrichtext/utils/text/view/TextViewSetup.kt +119 -0
  159. package/android/src/main/java/com/renderrichtext/views/ContextMenuPopup.kt +217 -0
  160. package/android/src/main/java/com/renderrichtext/views/CopyGlyph.kt +101 -0
  161. package/android/src/main/java/com/renderrichtext/views/WrapGlyph.kt +40 -0
  162. package/android/src/main/jni/CMakeLists.txt +52 -0
  163. package/android/src/main/jni/react/renderer/components/RichTextSpec/ComponentDescriptors.h +30 -0
  164. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextComponentDescriptor.h +29 -0
  165. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.cpp +45 -0
  166. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.h +21 -0
  167. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.cpp +38 -0
  168. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.h +53 -0
  169. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.cpp +9 -0
  170. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.h +24 -0
  171. package/android/src/main/jni/react/renderer/components/RichTextSpec/conversions.h +40 -0
  172. package/cpp/parser/TiptapASTNode.hpp +68 -0
  173. package/cpp/parser/TiptapParser.cpp +233 -0
  174. package/cpp/parser/TiptapParser.hpp +13 -0
  175. package/cpp/vendor/nlohmann/json.hpp +24765 -0
  176. package/ios/EnrichedTiptapText.h +27 -0
  177. package/ios/EnrichedTiptapText.mm +1222 -0
  178. package/ios/attachments/ENRMImageAttachment.h +51 -0
  179. package/ios/attachments/ENRMImageAttachment.m +398 -0
  180. package/ios/attachments/ENRMLinkPillAttachment.h +47 -0
  181. package/ios/attachments/ENRMLinkPillAttachment.m +324 -0
  182. package/ios/attachments/ENRMLinkPillIconCache.h +28 -0
  183. package/ios/attachments/ENRMLinkPillIconCache.m +181 -0
  184. package/ios/attachments/ENRMLinkPillText.h +27 -0
  185. package/ios/attachments/ENRMLinkPillText.m +118 -0
  186. package/ios/attachments/ENRMMathInlineAttachment.h +7 -0
  187. package/ios/attachments/ENRMMathInlineAttachment.m +4 -0
  188. package/ios/attachments/ThematicBreakAttachment.h +15 -0
  189. package/ios/attachments/ThematicBreakAttachment.m +33 -0
  190. package/ios/internals/ENRMViewFreeMeasurement.h +208 -0
  191. package/ios/internals/EnrichedTiptapTextComponentDescriptor.h +19 -0
  192. package/ios/internals/EnrichedTiptapTextShadowNode.h +43 -0
  193. package/ios/internals/EnrichedTiptapTextShadowNode.mm +69 -0
  194. package/ios/internals/EnrichedTiptapTextState.h +24 -0
  195. package/ios/internals/MeasurementCache.h +308 -0
  196. package/ios/internals/ShadowMeasurementUtils.h +145 -0
  197. package/ios/parser/ENRMTextLinkAttributes.h +7 -0
  198. package/ios/parser/ENRMTiptapParser.h +33 -0
  199. package/ios/parser/ENRMTiptapParser.mm +73 -0
  200. package/ios/parser/TiptapASTNode.h +57 -0
  201. package/ios/parser/TiptapASTNode.m +52 -0
  202. package/ios/parser/TiptapParserBridge.mm +179 -0
  203. package/ios/renderer/AttributedRenderer.h +23 -0
  204. package/ios/renderer/AttributedRenderer.m +229 -0
  205. package/ios/renderer/BaseRenderer.h +33 -0
  206. package/ios/renderer/BaseRenderer.m +46 -0
  207. package/ios/renderer/BaselineShiftTextAttributes.h +10 -0
  208. package/ios/renderer/BaselineShiftTextAttributes.m +26 -0
  209. package/ios/renderer/BlankLineRenderer.h +4 -0
  210. package/ios/renderer/BlankLineRenderer.m +51 -0
  211. package/ios/renderer/BlockquoteRenderer.h +6 -0
  212. package/ios/renderer/BlockquoteRenderer.m +258 -0
  213. package/ios/renderer/CodeBlockRenderer.h +9 -0
  214. package/ios/renderer/CodeBlockRenderer.m +99 -0
  215. package/ios/renderer/CodeRenderer.h +9 -0
  216. package/ios/renderer/CodeRenderer.m +69 -0
  217. package/ios/renderer/ENRMBlockquoteTextRenderer.h +25 -0
  218. package/ios/renderer/ENRMBlockquoteTextRenderer.m +48 -0
  219. package/ios/renderer/EmphasisRenderer.h +5 -0
  220. package/ios/renderer/EmphasisRenderer.m +100 -0
  221. package/ios/renderer/FactoidRenderer.h +9 -0
  222. package/ios/renderer/FactoidRenderer.m +130 -0
  223. package/ios/renderer/HeadingRenderer.h +6 -0
  224. package/ios/renderer/HeadingRenderer.m +103 -0
  225. package/ios/renderer/HighlightRenderer.h +7 -0
  226. package/ios/renderer/HighlightRenderer.m +33 -0
  227. package/ios/renderer/LinkRenderer.h +6 -0
  228. package/ios/renderer/LinkRenderer.m +158 -0
  229. package/ios/renderer/ListItemRenderer.h +24 -0
  230. package/ios/renderer/ListItemRenderer.m +283 -0
  231. package/ios/renderer/ListRenderer.h +13 -0
  232. package/ios/renderer/ListRenderer.m +126 -0
  233. package/ios/renderer/MentionRenderer.h +9 -0
  234. package/ios/renderer/MentionRenderer.m +96 -0
  235. package/ios/renderer/NodeRenderer.h +8 -0
  236. package/ios/renderer/ParagraphRenderer.h +6 -0
  237. package/ios/renderer/ParagraphRenderer.m +85 -0
  238. package/ios/renderer/RenderContext.h +145 -0
  239. package/ios/renderer/RenderContext.m +403 -0
  240. package/ios/renderer/RendererFactory.h +12 -0
  241. package/ios/renderer/RendererFactory.m +158 -0
  242. package/ios/renderer/StrikethroughRenderer.h +5 -0
  243. package/ios/renderer/StrikethroughRenderer.m +30 -0
  244. package/ios/renderer/StrongRenderer.h +5 -0
  245. package/ios/renderer/StrongRenderer.m +88 -0
  246. package/ios/renderer/TextRenderer.h +6 -0
  247. package/ios/renderer/TextRenderer.m +18 -0
  248. package/ios/renderer/ThematicBreakRenderer.h +4 -0
  249. package/ios/renderer/ThematicBreakRenderer.m +42 -0
  250. package/ios/renderer/UnderlineRenderer.h +4 -0
  251. package/ios/renderer/UnderlineRenderer.m +41 -0
  252. package/ios/styles/ENRMFontSlot.h +12 -0
  253. package/ios/styles/ENRMFontSlot.m +24 -0
  254. package/ios/styles/StyleConfig.h +488 -0
  255. package/ios/styles/StyleConfig.mm +2820 -0
  256. package/ios/utils/AccessibilityInfo.h +35 -0
  257. package/ios/utils/AccessibilityInfo.m +24 -0
  258. package/ios/utils/BlockquoteBorder.h +26 -0
  259. package/ios/utils/BlockquoteBorder.m +294 -0
  260. package/ios/utils/CodeBackground.h +19 -0
  261. package/ios/utils/CodeBackground.m +197 -0
  262. package/ios/utils/CodeBlockBackground.h +17 -0
  263. package/ios/utils/CodeBlockBackground.m +103 -0
  264. package/ios/utils/ContextMenuUtils.h +76 -0
  265. package/ios/utils/ContextMenuUtils.mm +75 -0
  266. package/ios/utils/ENRMAccessibilityLabels.h +40 -0
  267. package/ios/utils/ENRMAccessibilityLabels.m +5 -0
  268. package/ios/utils/ENRMAsyncRenderCoordinator.h +24 -0
  269. package/ios/utils/ENRMAsyncRenderCoordinator.m +37 -0
  270. package/ios/utils/ENRMAtomicSize.h +65 -0
  271. package/ios/utils/ENRMCodeBlockContent.h +37 -0
  272. package/ios/utils/ENRMCodeBlockContent.mm +89 -0
  273. package/ios/utils/ENRMFeatureFlags.h +25 -0
  274. package/ios/utils/ENRMImageDownloader.h +30 -0
  275. package/ios/utils/ENRMImageDownloader.m +168 -0
  276. package/ios/utils/ENRMLatexErrorCoordinator.h +27 -0
  277. package/ios/utils/ENRMLatexErrorCoordinator.mm +75 -0
  278. package/ios/utils/ENRMLatexErrorReporting.h +13 -0
  279. package/ios/utils/ENRMLinkContextMenus.h +93 -0
  280. package/ios/utils/ENRMLinkContextMenus.m +178 -0
  281. package/ios/utils/ENRMLinkRegexConfig.h +30 -0
  282. package/ios/utils/ENRMLinkRegexConfig.mm +104 -0
  283. package/ios/utils/ENRMLinkRegexProps.h +23 -0
  284. package/ios/utils/ENRMLocalImageLoader.h +35 -0
  285. package/ios/utils/ENRMLocalImageLoader.m +111 -0
  286. package/ios/utils/ENRMMenuAction.h +27 -0
  287. package/ios/utils/ENRMMenuAction.m +33 -0
  288. package/ios/utils/ENRMSpoilerOverlayManager.h +15 -0
  289. package/ios/utils/ENRMSpoilerOverlayManager.m +19 -0
  290. package/ios/utils/ENRMSpoilerTapUtils.h +6 -0
  291. package/ios/utils/ENRMSpoilerTapUtils.m +11 -0
  292. package/ios/utils/ENRMTailFadeInAnimator.h +16 -0
  293. package/ios/utils/ENRMTailFadeInAnimator.m +161 -0
  294. package/ios/utils/ENRMTextHitTest.h +23 -0
  295. package/ios/utils/ENRMTextHitTest.m +20 -0
  296. package/ios/utils/ENRMTextInteractionUtils.h +20 -0
  297. package/ios/utils/ENRMTextInteractionUtils.m +15 -0
  298. package/ios/utils/ENRMTextRenderer.h +38 -0
  299. package/ios/utils/ENRMTextRenderer.m +54 -0
  300. package/ios/utils/ENRMTextViewSetup.h +132 -0
  301. package/ios/utils/ENRMUIKit.h +429 -0
  302. package/ios/utils/EditMenuUtils.h +105 -0
  303. package/ios/utils/EditMenuUtils.m +143 -0
  304. package/ios/utils/FactoidBackground.h +20 -0
  305. package/ios/utils/FactoidBackground.m +161 -0
  306. package/ios/utils/FontScaleObserver.h +10 -0
  307. package/ios/utils/FontScaleObserver.m +55 -0
  308. package/ios/utils/FontUtils.h +31 -0
  309. package/ios/utils/FontUtils.m +63 -0
  310. package/ios/utils/HTMLGenerator.h +25 -0
  311. package/ios/utils/HTMLGenerator.m +1041 -0
  312. package/ios/utils/HeightUpdateUtils.h +25 -0
  313. package/ios/utils/ImageRequestHeaderUtils.h +35 -0
  314. package/ios/utils/LastElementUtils.h +97 -0
  315. package/ios/utils/LinkContextMenuUtils.h +55 -0
  316. package/ios/utils/LinkPillContentUtils.h +46 -0
  317. package/ios/utils/LinkTapUtils.h +56 -0
  318. package/ios/utils/LinkTapUtils.m +139 -0
  319. package/ios/utils/ListMarkerDrawer.h +15 -0
  320. package/ios/utils/ListMarkerDrawer.m +239 -0
  321. package/ios/utils/MentionBackground.h +26 -0
  322. package/ios/utils/MentionBackground.m +86 -0
  323. package/ios/utils/ParagraphStyleUtils.h +87 -0
  324. package/ios/utils/ParagraphStyleUtils.m +484 -0
  325. package/ios/utils/PasteboardUtils.h +48 -0
  326. package/ios/utils/PasteboardUtils.m +166 -0
  327. package/ios/utils/RTFExportUtils.h +24 -0
  328. package/ios/utils/RTFExportUtils.m +299 -0
  329. package/ios/utils/RuntimeKeys.h +44 -0
  330. package/ios/utils/RuntimeKeys.m +13 -0
  331. package/ios/utils/SelectionColorUtils.h +14 -0
  332. package/ios/utils/StylePropsUtils.h +1340 -0
  333. package/ios/utils/TaskListTapUtils.h +6 -0
  334. package/ios/utils/TaskListTapUtils.m +14 -0
  335. package/ios/utils/TextViewLayoutManager.h +14 -0
  336. package/ios/utils/TextViewLayoutManager.mm +198 -0
  337. package/ios/utils/TiptapAccessibilityElementBuilder.h +43 -0
  338. package/ios/utils/TiptapAccessibilityElementBuilder.m +432 -0
  339. package/ios/utils/TiptapExtractor.h +17 -0
  340. package/ios/utils/TiptapExtractor.m +367 -0
  341. package/ios/views/ENRMTiptapTextView.h +25 -0
  342. package/ios/views/ENRMTiptapTextView.m +247 -0
  343. package/package.json +70 -6
  344. package/react-native.config.js +10 -0
  345. package/src/EnrichedTiptapTextNativeComponent.ts +739 -0
  346. package/src/RichText.tsx +164 -0
  347. package/src/admonitionDefaults.ts +58 -0
  348. package/src/darkModeTextStyle.ts +99 -0
  349. package/src/headingDefaults.ts +21 -0
  350. package/src/index.tsx +4 -0
  351. package/src/linkVariantUtils.ts +120 -0
  352. package/src/normalizeTiptapStyle.ts +397 -0
  353. package/src/rn-globals.d.ts +1 -0
  354. package/src/styleUtils.ts +107 -0
  355. package/src/types/TiptapStyle.ts +532 -0
  356. package/src/types/TiptapStyleInternal.ts +299 -0
  357. package/src/types/linkPill.ts +9 -0
  358. package/src/utils/isDev.ts +4 -0
@@ -0,0 +1,1222 @@
1
+ #import "EnrichedTiptapText.h"
2
+ #import "CodeBlockBackground.h"
3
+ #import "ContextMenuUtils.h"
4
+ #import "ENRMAccessibilityLabels.h"
5
+ #import "ENRMAsyncRenderCoordinator.h"
6
+ #import "ENRMAtomicSize.h"
7
+ #if TARGET_OS_OSX
8
+ #import "ENRMContextMenuTextView+macOS.h"
9
+ #endif
10
+ #import "ENRMImageAttachment.h"
11
+ #import "ENRMLatexErrorCoordinator.h"
12
+ #import "ENRMLinkContextMenus.h"
13
+ #import "ENRMLinkRegexProps.h"
14
+ #import "ENRMTiptapParser.h"
15
+ #import "ENRMTiptapTextView.h"
16
+ #import "ENRMSpoilerOverlayManager.h"
17
+ #import "ENRMSpoilerTapUtils.h"
18
+ #import "ENRMTailFadeInAnimator.h"
19
+ #import "ENRMTextInteractionUtils.h"
20
+ #import "ENRMTextRenderer.h"
21
+ #import "ENRMTextViewSetup.h"
22
+ #import "ENRMUIKit.h"
23
+ #import "EditMenuUtils.h"
24
+ #import "FontScaleObserver.h"
25
+ #import "FontUtils.h"
26
+ #import "HeightUpdateUtils.h"
27
+ #import "ImageRequestHeaderUtils.h"
28
+ #import "LinkContextMenuUtils.h"
29
+ #import "LinkPillContentUtils.h"
30
+ #import "LinkTapUtils.h"
31
+ #import "TiptapASTNode.h"
32
+ #import "TiptapAccessibilityElementBuilder.h"
33
+ #import "TiptapExtractor.h"
34
+ #import "MeasurementCache.h"
35
+ #import "ParagraphStyleUtils.h"
36
+ #import "PasteboardUtils.h"
37
+ #import "RenderContext.h"
38
+ #import "RuntimeKeys.h"
39
+ #import "SelectionColorUtils.h"
40
+ #import "StylePropsUtils.h"
41
+ #import "TaskListTapUtils.h"
42
+
43
+ #import <ReactNativeRenderRichText/EnrichedTiptapTextComponentDescriptor.h>
44
+ #import <ReactNativeRenderRichText/EventEmitters.h>
45
+ #import <ReactNativeRenderRichText/Props.h>
46
+ #import <ReactNativeRenderRichText/RCTComponentViewHelpers.h>
47
+
48
+ #import "RCTFabricComponentsPlugins.h"
49
+ #import <React/RCTConversions.h>
50
+ #import <React/RCTFont.h>
51
+ #import <React/RCTI18nUtil.h>
52
+
53
+ using namespace facebook::react;
54
+
55
+ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
56
+ ENRMDirtyNone = 0,
57
+ ENRMDirtyRender = 1 << 0,
58
+ };
59
+
60
+ @interface EnrichedTiptapText () <RCTEnrichedTiptapTextViewProtocol, UITextViewDelegate, ENRMImageLayoutObserver
61
+ #if !TARGET_OS_OSX
62
+ ,
63
+ UIGestureRecognizerDelegate
64
+ #endif
65
+ >
66
+ + (ENRMMd4cFlags *)flagsFromProps:(const EnrichedTiptapTextMd4cFlagsStruct &)props;
67
+ - (void)setupTextView;
68
+ - (void)renderTiptapContent:(NSString *)tiptapString;
69
+ - (void)applyRenderedText:(NSMutableAttributedString *)attributedText;
70
+ - (void)textTapped:(ENRMTapRecognizer *)recognizer;
71
+ - (void)setupLayoutManager;
72
+ - (void)emitLinkPress:(NSString *)url;
73
+ - (void)emitFactoidPress:(NSString *)factoidId;
74
+ - (void)emitMentionPress:(NSString *)mentionId label:(NSString *)label;
75
+ - (void)emitMentionLongPress:(NSString *)mentionId label:(NSString *)label;
76
+ - (void)emitLinkLongPress:(NSString *)url;
77
+ - (void)emitLinkContextMenuItemPress:(NSString *)itemText pattern:(NSString *)pattern url:(NSString *)url;
78
+ - (void)emitImagePress:(NSString *)url altText:(NSString *)altText;
79
+ - (void)emitCodeBlockPress:(NSString *)code language:(NSString *)language;
80
+ - (void)emitTaskListItemPress:(NSInteger)index checked:(BOOL)checked text:(NSString *)text;
81
+ - (void)emitContextMenuItemPress:(NSString *)itemText
82
+ selectedText:(NSString *)selectedText
83
+ selectionStart:(NSUInteger)selectionStart
84
+ selectionEnd:(NSUInteger)selectionEnd;
85
+ @end
86
+
87
+ #if !TARGET_OS_OSX
88
+ @interface EnrichedTiptapText () <ENRMLinkContextMenuSource>
89
+ @end
90
+ #endif
91
+
92
+ @implementation EnrichedTiptapText {
93
+ ENRMPlatformTextView *_textView;
94
+ #if !TARGET_OS_OSX
95
+ UILongPressGestureRecognizer *_mentionLongPress;
96
+ #endif
97
+ ENRMTiptapParser *_parser;
98
+ NSString *_cachedTiptap;
99
+ NSString *_renderedTiptap;
100
+ StyleConfig *_config;
101
+ ENRMMd4cFlags *_md4cFlags;
102
+ BOOL _isGFM;
103
+ ENRMLinkRegexConfig *_linkRegex;
104
+ ENRMLinkRegexConfig *_inlineCodeLinkRegex;
105
+
106
+ ENRMAsyncRenderCoordinator *_renderCoordinator;
107
+
108
+ EnrichedTiptapTextShadowNode::ConcreteState::Shared _state;
109
+ int _heightUpdateCounter;
110
+
111
+ FontScaleObserver *_fontScaleObserver;
112
+ CGFloat _maxFontSizeMultiplier;
113
+
114
+ CGFloat _lastElementMarginBottom;
115
+ BOOL _allowTrailingMargin;
116
+ BOOL _enableLinkPreview;
117
+ BOOL _enableTaskListItemToggle;
118
+ BOOL _enableImagePress;
119
+ BOOL _enableCodeBlockPress;
120
+ BOOL _streamingAnimation;
121
+ BOOL _forceHeightUpdateOnNextRender;
122
+
123
+ size_t _renderedStyleFingerprint;
124
+ size_t _pendingStyleFingerprint;
125
+
126
+ NSUInteger _previousTextLength;
127
+ ENRMTailFadeInAnimator *_fadeAnimator;
128
+
129
+ AccessibilityInfo *_accessibilityInfo;
130
+ ENRMAccessibilityLabels *_accessibilityLabels;
131
+ #if !TARGET_OS_OSX
132
+ NSMutableArray<UIAccessibilityElement *> *_accessibilityElements;
133
+ #else
134
+ NSMutableArray *_accessibilityElements;
135
+ #endif
136
+ BOOL _accessibilityNeedsRebuild;
137
+
138
+ ENRMLinkContextMenus *_linkContextMenus;
139
+ NSArray<NSString *> *_contextMenuItemTexts;
140
+ NSArray<NSString *> *_contextMenuItemIcons;
141
+ ENRMSelectionMenuConfig _selectionMenuConfig;
142
+ ENRMSelectionMenuLabels _selectionMenuLabels;
143
+
144
+ ENRMSpoilerOverlayManager *_spoilerManager;
145
+
146
+ NSLineBreakStrategy _lineBreakStrategy;
147
+
148
+ NSInteger _numberOfLines;
149
+ NSLineBreakMode _ellipsizeLineBreakMode;
150
+
151
+ ENRMWritingDirectionMode _writingDirectionMode;
152
+ NSWritingDirection _resolvedLayoutDirection;
153
+
154
+ ENRMDirtyFlags _dirtyFlags;
155
+
156
+ ENRMAtomicSize _lastCommittedSize;
157
+
158
+ ENRMLatexErrorCoordinator *_latexErrorCoordinator;
159
+ }
160
+
161
+ + (ComponentDescriptorProvider)componentDescriptorProvider
162
+ {
163
+ return concreteComponentDescriptorProvider<EnrichedTiptapTextComponentDescriptor>();
164
+ }
165
+
166
+ + (ENRMMd4cFlags *)flagsFromProps:(const EnrichedTiptapTextMd4cFlagsStruct &)props
167
+ {
168
+ ENRMMd4cFlags *flags = [ENRMMd4cFlags defaultFlags];
169
+ flags.underline = props.underline;
170
+ flags.superscript = props.superscript;
171
+ flags.subscript = props.subscript;
172
+ flags.latexMath = props.latexMath;
173
+ flags.highlight = props.highlight;
174
+ flags.hardSoftBreaks = props.hardSoftBreaks;
175
+ flags.preserveBlankLines = props.preserveBlankLines;
176
+ flags.admonitions = props.admonitions;
177
+ return flags;
178
+ }
179
+
180
+ #pragma mark - Measuring and State
181
+
182
+ /// The box the text actually lives in: `self.bounds` inset by the
183
+ /// `containerStyle` border and padding.
184
+ ///
185
+ /// Yoga hands `measureContent` an available size that already excludes those
186
+ /// insets, and `RCTViewComponentView` sizes `contentView` (our text view) to
187
+ /// `layoutMetrics.getContentFrame()`, so the shadow node's height and the text
188
+ /// view's frame are both content-box quantities. Measuring at `self.bounds`
189
+ /// instead overshoots by the horizontal insets, and because
190
+ /// `ENRMMeasureTextLayout` measures by resizing the live display container and
191
+ /// leaves it there (UIKit never re-syncs it - `widthTracksTextView` does not
192
+ /// apply with `scrollEnabled = NO`), the overshoot becomes the layout the user
193
+ /// sees: glyphs spill past the text view and get clipped, while the taller
194
+ /// content-width height Yoga committed shows up as blank space at the bottom.
195
+ ///
196
+ /// The text view's frame is authoritative, so this is zero-sized both before the
197
+ /// first layout metrics arrive and when the insets consume the whole component
198
+ /// width. Neither may fall back to `self.bounds`: that is the padded border box,
199
+ /// and in the second case it is a real layout the text would then be drawn
200
+ /// across. Callers skip measuring and laying out at a zero width instead.
201
+ ///
202
+ /// The frame, not the bounds: a `UITextView` is a scroll view, so its
203
+ /// `bounds.origin` is the content offset rather than the inset the segments of
204
+ /// `EnrichedTiptap` get from the matching method.
205
+ - (CGRect)contentBounds
206
+ {
207
+ return _textView.frame;
208
+ }
209
+
210
+ - (CGSize)measureSize:(CGFloat)maxWidth
211
+ {
212
+ CGSize size = ENRMMeasureTiptapText(_textView, maxWidth, _config, _allowTrailingMargin, _lastElementMarginBottom);
213
+ if (CGSizeEqualToSize(size, CGSizeZero)) {
214
+ CGFloat defaultHeight = UIFontLineHeight([UIFont systemFontOfSize:16.0]);
215
+ return CGSizeMake(maxWidth, defaultHeight);
216
+ }
217
+ return size;
218
+ }
219
+
220
+ - (CGSize)lastCommittedLayoutSize
221
+ {
222
+ return _lastCommittedSize.load();
223
+ }
224
+
225
+ - (BOOL)hasRenderedTiptap:(NSString *)tiptap
226
+ {
227
+ return _renderedTiptap != nil && [_renderedTiptap isEqualToString:tiptap];
228
+ }
229
+
230
+ - (BOOL)hasRenderedWithStyleFingerprint:(size_t)fingerprint
231
+ {
232
+ return _renderedStyleFingerprint == fingerprint;
233
+ }
234
+
235
+ - (void)updateState:(const facebook::react::State::Shared &)state
236
+ oldState:(const facebook::react::State::Shared &)oldState
237
+ {
238
+ _state = std::static_pointer_cast<const EnrichedTiptapTextShadowNode::ConcreteState>(state);
239
+
240
+ if (oldState == nullptr) {
241
+ [self requestHeightUpdate];
242
+ }
243
+ }
244
+
245
+ /// Publishes the committed size to the streaming fast path's mailbox, and picks
246
+ /// up the Yoga-resolved direction inherited from any ancestor `direction` style.
247
+ /// In FirstStrong mode the direction feeds the neutral-paragraph fallback, so a
248
+ /// change requires a re-render of the cached tiptap.
249
+ ///
250
+ /// The mailbox holds a content size, not the border box: `measureContent`
251
+ /// returns it straight to Yoga, which adds the `containerStyle` insets back on
252
+ /// top of whatever it gets, so storing `frame.size` would double-count them.
253
+ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
254
+ oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
255
+ {
256
+ [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
257
+
258
+ const auto contentSize = layoutMetrics.getContentFrame().size;
259
+ _lastCommittedSize.store(CGSizeMake(contentSize.width, contentSize.height));
260
+
261
+ NSWritingDirection resolved = _resolvedLayoutDirection;
262
+ if (layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
263
+ resolved = NSWritingDirectionRightToLeft;
264
+ } else if (layoutMetrics.layoutDirection == LayoutDirection::LeftToRight) {
265
+ resolved = NSWritingDirectionLeftToRight;
266
+ }
267
+
268
+ if (resolved != _resolvedLayoutDirection) {
269
+ _resolvedLayoutDirection = resolved;
270
+ if (_writingDirectionMode == ENRMWritingDirectionModeFirstStrong && _cachedTiptap.length > 0) {
271
+ [self renderTiptapContent:_cachedTiptap];
272
+ }
273
+ }
274
+ }
275
+
276
+ - (void)requestHeightUpdate
277
+ {
278
+ ENRMRequestHeightUpdate<EnrichedTiptapTextState>(_state, _heightUpdateCounter, self);
279
+ }
280
+
281
+ // A block image resolved its box height after loading (maxHeight/aspectRatio
282
+ // sizing). The shadow node measured — and cached — this tiptap with the
283
+ // pre-load fallback height, so drop those entries and re-measure.
284
+ - (void)imageAttachmentDidResolveLayout
285
+ {
286
+ // The text around the attachment moved, and the spoiler overlays with it.
287
+ [_spoilerManager setNeedsUpdate];
288
+ [_spoilerManager updateIfNeeded];
289
+
290
+ if (_renderedTiptap.length > 0) {
291
+ facebook::react::MeasurementCache::shared().removeMatchingTiptap(std::string(_renderedTiptap.UTF8String));
292
+ }
293
+
294
+ CGRect contentBounds = [self contentBounds];
295
+ if (contentBounds.size.width > 0) {
296
+ CGSize measured = [self measureSize:contentBounds.size.width];
297
+ if (needsHeightUpdate(measured, contentBounds)) {
298
+ [self requestHeightUpdate];
299
+ }
300
+ }
301
+ }
302
+
303
+ - (instancetype)initWithFrame:(CGRect)frame
304
+ {
305
+ if (self = [super initWithFrame:frame]) {
306
+ static const auto defaultProps = std::make_shared<const EnrichedTiptapTextProps>();
307
+ _props = defaultProps;
308
+
309
+ self.backgroundColor = [RCTUIColor clearColor];
310
+ _parser = [[ENRMTiptapParser alloc] init];
311
+ _linkContextMenus = [[ENRMLinkContextMenus alloc] init];
312
+ __weak EnrichedTiptapText *weakMenuSelf = self;
313
+ _linkContextMenus.onPress = ^(NSString *url, NSString *pattern, NSString *itemText) {
314
+ [weakMenuSelf emitLinkContextMenuItemPress:itemText pattern:pattern url:url];
315
+ };
316
+ _md4cFlags = [EnrichedTiptapText flagsFromProps:defaultProps->md4cFlags];
317
+ _isGFM = defaultProps->isGFM;
318
+ _linkRegex = ENRMLinkRegexConfigFromProps(defaultProps->linkRecognition.text);
319
+ _inlineCodeLinkRegex = ENRMLinkRegexConfigFromProps(defaultProps->linkRecognition.inlineCode);
320
+
321
+ _renderCoordinator =
322
+ [[ENRMAsyncRenderCoordinator alloc] initWithQueueLabel:"com.swmansion.enriched.tiptap.render"];
323
+
324
+ __weak __typeof(self) weakLatexSelf = self;
325
+ _latexErrorCoordinator =
326
+ [[ENRMLatexErrorCoordinator alloc] initWithEmit:^BOOL(NSString *source, NSString *message, BOOL displayMode) {
327
+ __typeof(self) strongSelf = weakLatexSelf;
328
+ if (!strongSelf)
329
+ return NO;
330
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(strongSelf->_eventEmitter);
331
+ if (!emitter)
332
+ return NO;
333
+ emitter->onLatexError({
334
+ .source = std::string(source.UTF8String ?: ""),
335
+ .message = std::string(message.UTF8String ?: ""),
336
+ .displayMode = displayMode ? true : false,
337
+ });
338
+ return YES;
339
+ }];
340
+ _maxFontSizeMultiplier = 0;
341
+ _allowTrailingMargin = NO;
342
+ _enableLinkPreview = YES;
343
+ _enableTaskListItemToggle = YES;
344
+ _enableImagePress = NO;
345
+ _enableCodeBlockPress = NO;
346
+ _forceHeightUpdateOnNextRender = NO;
347
+ _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsTiptap = YES, .copyImageURL = YES};
348
+ _lineBreakStrategy = NSLineBreakStrategyNone;
349
+ _numberOfLines = 0;
350
+ _ellipsizeLineBreakMode = NSLineBreakByTruncatingTail;
351
+ _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
352
+ _resolvedLayoutDirection =
353
+ [[RCTI18nUtil sharedInstance] isRTL] ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight;
354
+ _dirtyFlags = ENRMDirtyNone;
355
+
356
+ _fontScaleObserver = [[FontScaleObserver alloc] init];
357
+ __weak EnrichedTiptapText *weakSelf = self;
358
+ _fontScaleObserver.onChange = ^{
359
+ EnrichedTiptapText *strongSelf = weakSelf;
360
+ if (!strongSelf)
361
+ return;
362
+ if (strongSelf->_config != nil) {
363
+ [strongSelf->_config setFontScaleMultiplier:strongSelf->_fontScaleObserver.effectiveFontScale];
364
+ }
365
+ if (strongSelf->_cachedTiptap != nil && strongSelf->_cachedTiptap.length > 0) {
366
+ strongSelf->_forceHeightUpdateOnNextRender = YES;
367
+ [strongSelf renderTiptapContent:strongSelf->_cachedTiptap];
368
+ }
369
+ };
370
+
371
+ [self setupTextView];
372
+ }
373
+
374
+ return self;
375
+ }
376
+
377
+ - (void)setupTextView
378
+ {
379
+ #if !TARGET_OS_OSX
380
+ ENRMTiptapTextView *tiptapTextView = [[ENRMTiptapTextView alloc] init];
381
+ __weak EnrichedTiptapText *weakCopyOwner = self;
382
+ tiptapTextView.copySelectionHandler = ^(NSRange range) {
383
+ EnrichedTiptapText *owner = weakCopyOwner;
384
+ if (!owner)
385
+ return;
386
+ NSAttributedString *text = owner->_textView.textStorage;
387
+ copyAttributedStringToPasteboard([text attributedSubstringFromRange:range],
388
+ tiptapForRange(text, range, owner->_cachedTiptap), owner -> _config);
389
+ };
390
+ _textView = tiptapTextView;
391
+ _textView.text = @"";
392
+ #else
393
+ _textView = [[ENRMContextMenuTextView alloc] init];
394
+ _textView.string = @"";
395
+ #endif
396
+ ENRMConfigureTiptapTextView(_textView);
397
+ _textView.delegate = self;
398
+ _textView.hidden = YES;
399
+
400
+ #if TARGET_OS_OSX
401
+ __weak EnrichedTiptapText *weakSelf = self;
402
+ ((ENRMContextMenuTextView *)_textView).contextMenuProvider =
403
+ ^NSMenu *_Nullable(NSMenu *baseMenu, NSTextView *textView)
404
+ {
405
+ EnrichedTiptapText *strongSelf = weakSelf;
406
+ if (!strongSelf) {
407
+ return baseMenu;
408
+ }
409
+ NSArray<NSMenuItem *> *customItems = ENRMBuildContextMenuItems(
410
+ strongSelf->_contextMenuItemTexts, strongSelf->_contextMenuItemIcons, textView,
411
+ ^(NSString *itemText, NSString *selectedText, NSUInteger selectionStart, NSUInteger selectionEnd) {
412
+ [strongSelf emitContextMenuItemPress:itemText
413
+ selectedText:selectedText
414
+ selectionStart:selectionStart
415
+ selectionEnd:selectionEnd];
416
+ });
417
+ return buildEditMenuForSelection(textView.textStorage, textView.selectedRange, strongSelf->_cachedTiptap,
418
+ strongSelf->_config, @[ baseMenu ], customItems,
419
+ strongSelf -> _selectionMenuConfig);
420
+ };
421
+ #endif
422
+
423
+ ENRMTapRecognizer *tapRecognizer = [[ENRMTapRecognizer alloc] initWithTarget:self action:@selector(textTapped:)];
424
+ #if !TARGET_OS_OSX
425
+ // Fails immediately unless the touch is on a mention, so text selection is unchanged
426
+ // everywhere else. A mention long-press wins over the text view's selection gesture.
427
+ _mentionLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(mentionLongPressed:)];
428
+ _mentionLongPress.delegate = self;
429
+ [_textView addGestureRecognizer:_mentionLongPress];
430
+ [tapRecognizer requireGestureRecognizerToFail:_mentionLongPress];
431
+ #endif
432
+ [_textView addGestureRecognizer:tapRecognizer];
433
+
434
+ self.contentView = _textView;
435
+ }
436
+
437
+ - (void)didAddSubview:(RCTUIView *)subview
438
+ {
439
+ [super didAddSubview:subview];
440
+
441
+ if (subview == _textView) {
442
+ [self setupLayoutManager];
443
+ }
444
+ }
445
+
446
+ - (void)willRemoveSubview:(RCTUIView *)subview
447
+ {
448
+ if (subview == _textView) {
449
+ ENRMDetachLayoutManager(_textView);
450
+ }
451
+ [super willRemoveSubview:subview];
452
+ }
453
+
454
+ - (void)setupLayoutManager
455
+ {
456
+ ENRMAttachLayoutManager(_textView, _config);
457
+ }
458
+
459
+ - (void)renderTiptapContent:(NSString *)tiptapString
460
+ {
461
+ if (_renderCoordinator.blockAsyncRender)
462
+ return;
463
+
464
+ _cachedTiptap = [tiptapString copy];
465
+
466
+ StyleConfig *config = [_config copy];
467
+ ENRMTiptapParser *parser = _parser;
468
+ ENRMMd4cFlags *md4cFlags = [_md4cFlags copy];
469
+ BOOL isGFM = _isGFM;
470
+ ENRMLinkRegexConfig *linkRegex = _linkRegex;
471
+ ENRMLinkRegexConfig *inlineCodeLinkRegex = _inlineCodeLinkRegex;
472
+
473
+ BOOL allowFontScaling = _fontScaleObserver.allowFontScaling;
474
+ CGFloat maxFontSizeMultiplier = _maxFontSizeMultiplier;
475
+ BOOL allowTrailingMargin = _allowTrailingMargin;
476
+ NSLineBreakStrategy lineBreakStrategy = _lineBreakStrategy;
477
+ ENRMWritingDirectionMode writingDirectionMode = _writingDirectionMode;
478
+ NSWritingDirection resolvedLayoutDirection = _resolvedLayoutDirection;
479
+
480
+ __block ENRMRenderResult *result = nil;
481
+
482
+ [_renderCoordinator
483
+ scheduleRender:^BOOL {
484
+ TiptapASTNode *ast = [parser parseTiptap:tiptapString
485
+ flags:md4cFlags
486
+ isGFM:isGFM
487
+ linkRegex:linkRegex
488
+ inlineCodeLinkRegex:inlineCodeLinkRegex];
489
+ if (!ast)
490
+ return NO;
491
+
492
+ result = ENRMRenderASTNodes(ast.children, config, allowTrailingMargin, allowFontScaling, maxFontSizeMultiplier,
493
+ lineBreakStrategy);
494
+ ENRMApplyWritingDirectionMode(result.attributedText, writingDirectionMode, resolvedLayoutDirection);
495
+ return YES;
496
+ }
497
+ apply:^{
498
+ self->_lastElementMarginBottom = result.lastElementMarginBottom;
499
+ self->_accessibilityInfo = result.accessibilityInfo;
500
+ self->_renderedStyleFingerprint = self->_pendingStyleFingerprint;
501
+ [self->_latexErrorCoordinator wireReporters:result.context.mathReporters];
502
+ [self applyRenderedText:result.attributedText];
503
+ }];
504
+ }
505
+
506
+ - (NSMutableAttributedString *)parseAndRenderTiptap:(NSString *)tiptapString
507
+ {
508
+ TiptapASTNode *ast = [_parser parseTiptap:tiptapString
509
+ flags:_md4cFlags
510
+ isGFM:_isGFM
511
+ linkRegex:_linkRegex
512
+ inlineCodeLinkRegex:_inlineCodeLinkRegex];
513
+ if (!ast) {
514
+ return nil;
515
+ }
516
+
517
+ ENRMRenderResult *result =
518
+ ENRMRenderASTNodes(ast.children, _config, _allowTrailingMargin, _fontScaleObserver.allowFontScaling,
519
+ _maxFontSizeMultiplier, _lineBreakStrategy);
520
+ ENRMApplyWritingDirectionMode(result.attributedText, _writingDirectionMode, _resolvedLayoutDirection);
521
+
522
+ _lastElementMarginBottom = result.lastElementMarginBottom;
523
+ _accessibilityInfo = result.accessibilityInfo;
524
+
525
+ return result.attributedText;
526
+ }
527
+
528
+ /// Synchronous rendering for mock view measurement (no UI updates needed).
529
+ - (void)renderTiptapSynchronously:(NSString *)tiptapString
530
+ {
531
+ if (!tiptapString || tiptapString.length == 0) {
532
+ return;
533
+ }
534
+
535
+ _renderCoordinator.blockAsyncRender = YES;
536
+ _cachedTiptap = [tiptapString copy];
537
+
538
+ NSMutableAttributedString *attributedText = [self parseAndRenderTiptap:tiptapString];
539
+ if (!attributedText) {
540
+ return;
541
+ }
542
+
543
+ _textView.attributedText = attributedText;
544
+ _renderedTiptap = [_cachedTiptap copy];
545
+ _renderedStyleFingerprint = _pendingStyleFingerprint;
546
+ }
547
+
548
+ // Kept in sync with the view-free measurement so the rendered line count matches
549
+ // the measured height: both clamp a layout performed at the content width (see
550
+ // contentBounds), so the truncation falls on the same word.
551
+ // numberOfLines == 0 restores the unlimited default.
552
+ - (void)applyLineClampToTextContainer
553
+ {
554
+ if (_numberOfLines > 0) {
555
+ _textView.textContainer.maximumNumberOfLines = _numberOfLines;
556
+ _textView.textContainer.lineBreakMode = _ellipsizeLineBreakMode;
557
+ } else {
558
+ _textView.textContainer.maximumNumberOfLines = 0;
559
+ _textView.textContainer.lineBreakMode = NSLineBreakByWordWrapping;
560
+ }
561
+ }
562
+
563
+ - (void)applyRenderedText:(NSMutableAttributedString *)attributedText
564
+ {
565
+ NSUInteger tailStart = _previousTextLength;
566
+
567
+ NSLayoutManager *layoutManager = _textView.layoutManager;
568
+ if ([layoutManager isKindOfClass:[TextViewLayoutManager class]]) {
569
+ [layoutManager setValue:_config forKey:@"config"];
570
+ }
571
+
572
+ objc_setAssociatedObject(_textView.textContainer, kTextViewKey, _textView, OBJC_ASSOCIATION_ASSIGN);
573
+
574
+ // Ensure the text container has unlimited height before setting content.
575
+ // updateLayoutMetrics may have shrunk the frame (and thus the text container)
576
+ // from a previous layout pass, which would clip the new attributed text.
577
+ CGRect contentBounds = [self contentBounds];
578
+ CGFloat containerWidth = contentBounds.size.width;
579
+ if (containerWidth <= 0) {
580
+ containerWidth = _textView.textContainer.size.width;
581
+ }
582
+ _textView.textContainer.size = CGSizeMake(containerWidth, CGFLOAT_MAX);
583
+ [self applyLineClampToTextContainer];
584
+
585
+ _accessibilityElements = nil;
586
+ _accessibilityNeedsRebuild = YES;
587
+
588
+ _textView.attributedText = attributedText;
589
+ _renderedTiptap = [_cachedTiptap copy];
590
+
591
+ [_textView.layoutManager invalidateLayoutForCharacterRange:NSMakeRange(0, attributedText.length)
592
+ actualCharacterRange:NULL];
593
+
594
+ // When bounds width is zero (recycled view not yet laid out), skip layout
595
+ // and measurement — layoutSubviews will handle it once the view has real
596
+ // bounds. Measuring with width=0 produces a bogus single-line measurement
597
+ // that corrupts the height sent to Yoga.
598
+ [_spoilerManager setNeedsUpdate];
599
+
600
+ if (contentBounds.size.width > 0) {
601
+ // Font/style changes can produce the same measured size before UIKit has
602
+ // fully refreshed layout, so force one Yoga update after those renders.
603
+ BOOL forceHeightUpdate = _forceHeightUpdateOnNextRender;
604
+ _forceHeightUpdateOnNextRender = NO;
605
+
606
+ [_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer];
607
+ ENRMSetNeedsDisplay(_textView);
608
+ #if !TARGET_OS_OSX
609
+ [self setNeedsLayout];
610
+ #endif
611
+
612
+ [_spoilerManager updateIfNeeded];
613
+
614
+ CGSize measured = [self measureSize:contentBounds.size.width];
615
+ if (forceHeightUpdate || needsHeightUpdate(measured, contentBounds)) {
616
+ [self requestHeightUpdate];
617
+ }
618
+ }
619
+
620
+ if (_textView.hidden) {
621
+ dispatch_async(dispatch_get_main_queue(), ^{ self->_textView.hidden = NO; });
622
+ }
623
+
624
+ if (_streamingAnimation) {
625
+ if (!_fadeAnimator) {
626
+ _fadeAnimator = [[ENRMTailFadeInAnimator alloc] initWithTextView:_textView];
627
+ }
628
+ [_fadeAnimator animateFrom:tailStart to:attributedText.length];
629
+ _previousTextLength = attributedText.length;
630
+ }
631
+ }
632
+
633
+ #if !TARGET_OS_OSX
634
+ - (void)layoutSubviews
635
+ {
636
+ [super layoutSubviews];
637
+ [_spoilerManager updateIfNeeded];
638
+ }
639
+ #endif
640
+
641
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
642
+ {
643
+ const auto &oldViewProps = *std::static_pointer_cast<EnrichedTiptapTextProps const>(_props);
644
+ const auto &newViewProps = *std::static_pointer_cast<EnrichedTiptapTextProps const>(props);
645
+
646
+ if (oldViewProps.document != newViewProps.document) {
647
+ _dirtyFlags |= ENRMDirtyRender;
648
+ }
649
+
650
+ if (_config == nil) {
651
+ _config = [[StyleConfig alloc] init];
652
+ [_config setFontScaleMultiplier:_fontScaleObserver.effectiveFontScale];
653
+ _spoilerManager = [[ENRMSpoilerOverlayManager alloc] initWithTextView:_textView config:_config];
654
+ }
655
+
656
+ if (applyTiptapStyleToConfig(_config, newViewProps.tiptapStyle, oldViewProps.tiptapStyle)) {
657
+ _forceHeightUpdateOnNextRender = YES;
658
+ _dirtyFlags |= ENRMDirtyRender;
659
+ }
660
+
661
+ if (ENRMImageRequestHeadersChanged(oldViewProps.imageRequestHeaders, newViewProps.imageRequestHeaders)) {
662
+ [_config setImageRequestHeaders:ENRMImageRequestHeadersFromProps(newViewProps.imageRequestHeaders)];
663
+ _dirtyFlags |= ENRMDirtyRender;
664
+ }
665
+
666
+ // Pill labels change layout, so treat new content like a style change.
667
+ if (ENRMLinkPillContentChanged(oldViewProps.linkPillContent, newViewProps.linkPillContent)) {
668
+ [_config setLinkPillContent:ENRMLinkPillContentFromProps(newViewProps.linkPillContent)];
669
+ _forceHeightUpdateOnNextRender = YES;
670
+ _dirtyFlags |= ENRMDirtyRender;
671
+ }
672
+
673
+ NSLayoutManager *layoutManager = _textView.layoutManager;
674
+ if ([layoutManager isKindOfClass:[TextViewLayoutManager class]]) {
675
+ StyleConfig *currentConfig = [layoutManager valueForKey:@"config"];
676
+ if (currentConfig != _config) {
677
+ [layoutManager setValue:_config forKey:@"config"];
678
+ }
679
+ }
680
+
681
+ if (_textView.selectable != newViewProps.selectable) {
682
+ _textView.selectable = newViewProps.selectable;
683
+ }
684
+
685
+ if (newViewProps.selectionColor != oldViewProps.selectionColor) {
686
+ ENRMApplySelectionColor(_textView, newViewProps.selectionColor);
687
+ }
688
+
689
+ if (newViewProps.allowFontScaling != oldViewProps.allowFontScaling) {
690
+ _fontScaleObserver.allowFontScaling = newViewProps.allowFontScaling;
691
+
692
+ if (_config != nil) {
693
+ [_config setFontScaleMultiplier:_fontScaleObserver.effectiveFontScale];
694
+ }
695
+
696
+ _forceHeightUpdateOnNextRender = YES;
697
+ _dirtyFlags |= ENRMDirtyRender;
698
+ }
699
+
700
+ if (newViewProps.maxFontSizeMultiplier != oldViewProps.maxFontSizeMultiplier) {
701
+ _maxFontSizeMultiplier = newViewProps.maxFontSizeMultiplier;
702
+
703
+ if (_config != nil) {
704
+ [_config setMaxFontSizeMultiplier:_maxFontSizeMultiplier];
705
+ }
706
+
707
+ _forceHeightUpdateOnNextRender = YES;
708
+ _dirtyFlags |= ENRMDirtyRender;
709
+ }
710
+
711
+ if (newViewProps.allowTrailingMargin != oldViewProps.allowTrailingMargin) {
712
+ _allowTrailingMargin = newViewProps.allowTrailingMargin;
713
+ _forceHeightUpdateOnNextRender = YES;
714
+ _dirtyFlags |= ENRMDirtyRender;
715
+ }
716
+
717
+ if (!ENRMLinkRegexPropsEqual(oldViewProps.linkRecognition.text, newViewProps.linkRecognition.text)) {
718
+ _linkRegex = ENRMLinkRegexConfigFromProps(newViewProps.linkRecognition.text);
719
+ _dirtyFlags |= ENRMDirtyRender;
720
+ _forceHeightUpdateOnNextRender = YES;
721
+ }
722
+ if (!ENRMLinkRegexPropsEqual(oldViewProps.linkRecognition.inlineCode, newViewProps.linkRecognition.inlineCode)) {
723
+ _inlineCodeLinkRegex = ENRMLinkRegexConfigFromProps(newViewProps.linkRecognition.inlineCode);
724
+ _dirtyFlags |= ENRMDirtyRender;
725
+ _forceHeightUpdateOnNextRender = YES;
726
+ }
727
+
728
+ if (newViewProps.md4cFlags.underline != oldViewProps.md4cFlags.underline ||
729
+ newViewProps.md4cFlags.superscript != oldViewProps.md4cFlags.superscript ||
730
+ newViewProps.md4cFlags.subscript != oldViewProps.md4cFlags.subscript ||
731
+ newViewProps.md4cFlags.latexMath != oldViewProps.md4cFlags.latexMath ||
732
+ newViewProps.md4cFlags.highlight != oldViewProps.md4cFlags.highlight ||
733
+ newViewProps.md4cFlags.hardSoftBreaks != oldViewProps.md4cFlags.hardSoftBreaks ||
734
+ newViewProps.md4cFlags.preserveBlankLines != oldViewProps.md4cFlags.preserveBlankLines ||
735
+ newViewProps.md4cFlags.admonitions != oldViewProps.md4cFlags.admonitions) {
736
+ _md4cFlags = [EnrichedTiptapText flagsFromProps:newViewProps.md4cFlags];
737
+ _forceHeightUpdateOnNextRender = YES;
738
+ _dirtyFlags |= ENRMDirtyRender;
739
+ }
740
+
741
+ if (newViewProps.isGFM != oldViewProps.isGFM) {
742
+ _isGFM = newViewProps.isGFM;
743
+ _forceHeightUpdateOnNextRender = YES;
744
+ _dirtyFlags |= ENRMDirtyRender;
745
+ }
746
+
747
+ _enableLinkPreview = newViewProps.enableLinkPreview;
748
+ _enableTaskListItemToggle = newViewProps.enableTaskListItemToggle;
749
+ _enableImagePress = newViewProps.enableImagePress;
750
+ _enableCodeBlockPress = newViewProps.enableCodeBlockPress;
751
+
752
+ if (ENRMLinkContextMenuItemsChanged(oldViewProps.linkContextMenuItems, newViewProps.linkContextMenuItems)) {
753
+ _linkContextMenus.entries = ENRMLinkContextMenuEntriesFromProps(newViewProps.linkContextMenuItems);
754
+ }
755
+
756
+ if (ENRMContextMenuItemsChanged(oldViewProps.contextMenuItems, newViewProps.contextMenuItems)) {
757
+ _contextMenuItemTexts = ENRMContextMenuTextsFromItems(newViewProps.contextMenuItems);
758
+ _contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems);
759
+ }
760
+
761
+ _selectionMenuLabels = ENRMParseSelectionMenuLabels(newViewProps.selectionMenuConfig);
762
+ _selectionMenuConfig =
763
+ ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig.copyAsTiptap,
764
+ newViewProps.selectionMenuConfig.copyImageUrl);
765
+
766
+ if (ENRMAccessibilityLabelsChanged(oldViewProps.accessibilityLabels, newViewProps.accessibilityLabels)) {
767
+ _accessibilityLabels = [[ENRMAccessibilityLabels alloc] init];
768
+ _accessibilityLabels.bulletPoint =
769
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.list.bulletPoint.c_str()];
770
+ _accessibilityLabels.nestedBulletPoint =
771
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.list.nestedBulletPoint.c_str()];
772
+ _accessibilityLabels.orderedItem =
773
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.list.orderedItem.c_str()];
774
+ _accessibilityLabels.nestedOrderedItem =
775
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.list.nestedOrderedItem.c_str()];
776
+ _accessibilityLabels.blockquote =
777
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.blockquote.quote.c_str()];
778
+ _accessibilityLabels.nestedBlockquote =
779
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.blockquote.nestedQuote.c_str()];
780
+ _accessibilityLabels.tableRow =
781
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.table.row.c_str()];
782
+ _accessibilityLabels.mathEquation =
783
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.math.equation.c_str()];
784
+ _accessibilityLabels.codeBlockWrap =
785
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.codeBlock.wrap.c_str()];
786
+ _accessibilityLabels.codeBlockUnwrap =
787
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.codeBlock.unwrap.c_str()];
788
+ _accessibilityLabels.rotorHeadings =
789
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.rotor.headings.c_str()];
790
+ _accessibilityLabels.rotorLinks =
791
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.rotor.links.c_str()];
792
+ _accessibilityLabels.rotorImages =
793
+ [[NSString alloc] initWithUTF8String:newViewProps.accessibilityLabels.rotor.images.c_str()];
794
+ _accessibilityNeedsRebuild = YES;
795
+ }
796
+
797
+ if (newViewProps.streamingAnimation != oldViewProps.streamingAnimation) {
798
+ _streamingAnimation = newViewProps.streamingAnimation;
799
+ if (_streamingAnimation) {
800
+ _previousTextLength = ENRMGetAttributedText(_textView).length;
801
+ } else {
802
+ [_fadeAnimator cancel];
803
+ _fadeAnimator = nil;
804
+ _previousTextLength = 0;
805
+ }
806
+ }
807
+
808
+ if (newViewProps.spoilerOverlay != oldViewProps.spoilerOverlay) {
809
+ NSString *modeStr = [[NSString alloc] initWithUTF8String:newViewProps.spoilerOverlay.c_str()];
810
+ _spoilerManager.spoilerOverlay = ENRMSpoilerOverlayFromString(modeStr);
811
+ }
812
+
813
+ if (newViewProps.lineBreakStrategyIOS != oldViewProps.lineBreakStrategyIOS) {
814
+ NSString *strategy = [[NSString alloc] initWithUTF8String:newViewProps.lineBreakStrategyIOS.c_str()];
815
+ _lineBreakStrategy = ENRMResolveLineBreakStrategy(strategy);
816
+ _forceHeightUpdateOnNextRender = YES;
817
+ _dirtyFlags |= ENRMDirtyRender;
818
+ }
819
+
820
+ if (newViewProps.writingDirection != oldViewProps.writingDirection) {
821
+ NSString *value = [[NSString alloc] initWithUTF8String:newViewProps.writingDirection.c_str()];
822
+ _writingDirectionMode = ENRMResolveWritingDirectionMode(value);
823
+ _forceHeightUpdateOnNextRender = YES;
824
+ _dirtyFlags |= ENRMDirtyRender;
825
+ }
826
+
827
+ if (newViewProps.numberOfLines != oldViewProps.numberOfLines) {
828
+ _numberOfLines = (NSInteger)newViewProps.numberOfLines;
829
+ [self applyLineClampToTextContainer];
830
+ _forceHeightUpdateOnNextRender = YES;
831
+ _dirtyFlags |= ENRMDirtyRender;
832
+ }
833
+
834
+ if (newViewProps.ellipsizeMode != oldViewProps.ellipsizeMode) {
835
+ _ellipsizeLineBreakMode =
836
+ ENRMResolveEllipsizeLineBreakMode([[NSString alloc] initWithUTF8String:newViewProps.ellipsizeMode.c_str()]);
837
+ [self applyLineClampToTextContainer];
838
+ _forceHeightUpdateOnNextRender = YES;
839
+ _dirtyFlags |= ENRMDirtyRender;
840
+ }
841
+
842
+ if (_dirtyFlags & ENRMDirtyRender) {
843
+ _pendingStyleFingerprint =
844
+ computeStyleFingerprint(newViewProps.tiptapStyle) ^ std::hash<bool>{}(newViewProps.allowTrailingMargin);
845
+ NSString *tiptapString = [[NSString alloc] initWithUTF8String:newViewProps.document.c_str()];
846
+ [self renderTiptapContent:tiptapString];
847
+ _dirtyFlags &= ~ENRMDirtyRender;
848
+ }
849
+
850
+ [super updateProps:props oldProps:oldProps];
851
+ }
852
+
853
+ - (void)didMoveToWindow
854
+ {
855
+ [super didMoveToWindow];
856
+
857
+ if (self.window && _renderedTiptap != nil) {
858
+ _textView.hidden = NO;
859
+ // Refresh in place: RCTViewComponentView owns the text view's frame and has
860
+ // already set it to the content frame, so re-assigning self.bounds here would
861
+ // drop the containerStyle insets (see contentBounds).
862
+ ENRMRefreshTextViewLayout(_textView);
863
+
864
+ [_spoilerManager setNeedsUpdate];
865
+ [_spoilerManager updateIfNeeded];
866
+
867
+ CGRect contentBounds = [self contentBounds];
868
+ if (contentBounds.size.width > 0) {
869
+ CGSize measured = [self measureSize:contentBounds.size.width];
870
+ if (needsHeightUpdate(measured, contentBounds)) {
871
+ [self requestHeightUpdate];
872
+ }
873
+ }
874
+ }
875
+ }
876
+
877
+ - (void)prepareForRecycle
878
+ {
879
+ const auto resetProps = std::make_shared<const EnrichedTiptapTextProps>();
880
+ _props = resetProps;
881
+ [_renderCoordinator invalidate];
882
+
883
+ [_fadeAnimator cancel];
884
+ _fadeAnimator = nil;
885
+ _previousTextLength = 0;
886
+ _streamingAnimation = NO;
887
+ _forceHeightUpdateOnNextRender = NO;
888
+ _cachedTiptap = nil;
889
+ _renderedTiptap = nil;
890
+ _config = nil;
891
+ _md4cFlags = [EnrichedTiptapText flagsFromProps:resetProps->md4cFlags];
892
+ _isGFM = resetProps->isGFM;
893
+ _linkRegex = ENRMLinkRegexConfigFromProps(resetProps->linkRecognition.text);
894
+ _inlineCodeLinkRegex = ENRMLinkRegexConfigFromProps(resetProps->linkRecognition.inlineCode);
895
+ _maxFontSizeMultiplier = 0;
896
+ _lastElementMarginBottom = 0;
897
+ _allowTrailingMargin = NO;
898
+ _lineBreakStrategy = NSLineBreakStrategyNone;
899
+ _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
900
+ _numberOfLines = 0;
901
+ _ellipsizeLineBreakMode = NSLineBreakByTruncatingTail;
902
+ [self applyLineClampToTextContainer];
903
+ _renderedStyleFingerprint = 0;
904
+ _pendingStyleFingerprint = 0;
905
+ _linkContextMenus.entries = @[];
906
+ _contextMenuItemTexts = nil;
907
+ _contextMenuItemIcons = nil;
908
+ _fontScaleObserver.allowFontScaling = resetProps->allowFontScaling;
909
+ _accessibilityLabels = nil;
910
+ _accessibilityElements = nil;
911
+ _accessibilityInfo = nil;
912
+ _accessibilityNeedsRebuild = NO;
913
+ [_spoilerManager removeAllOverlays];
914
+ _spoilerManager = nil;
915
+ if (_textView != nil) {
916
+ ENRMSetAttributedText(_textView, [[NSAttributedString alloc] initWithString:@""]);
917
+ _textView.hidden = YES;
918
+ }
919
+
920
+ [super prepareForRecycle];
921
+ }
922
+
923
+ Class<RCTComponentViewProtocol> EnrichedTiptapTextCls(void)
924
+ {
925
+ return EnrichedTiptapText.class;
926
+ }
927
+
928
+ - (facebook::react::SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
929
+ {
930
+ if (_textView) {
931
+ CGPoint textViewPoint = [self convertPoint:point toView:_textView];
932
+ if (isPointOnInteractiveElement(_textView, textViewPoint, _enableImagePress, _enableCodeBlockPress)) {
933
+ return nil;
934
+ }
935
+ }
936
+
937
+ return [super touchEventEmitterAtPoint:point];
938
+ }
939
+
940
+ - (void)emitLinkPress:(NSString *)url
941
+ {
942
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(_eventEmitter);
943
+ if (emitter)
944
+ emitter->onLinkPress({.url = std::string(url.UTF8String)});
945
+ }
946
+
947
+ - (void)emitMentionPress:(NSString *)mentionId label:(NSString *)label
948
+ {
949
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(_eventEmitter);
950
+ if (emitter)
951
+ emitter->onMentionPress(
952
+ {.mentionId = std::string(mentionId.UTF8String ?: ""), .label = std::string(label.UTF8String ?: "")});
953
+ }
954
+
955
+ - (void)emitMentionLongPress:(NSString *)mentionId label:(NSString *)label
956
+ {
957
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(_eventEmitter);
958
+ if (emitter)
959
+ emitter->onMentionLongPress(
960
+ {.mentionId = std::string(mentionId.UTF8String ?: ""), .label = std::string(label.UTF8String ?: "")});
961
+ }
962
+
963
+ - (void)emitLinkLongPress:(NSString *)url
964
+ {
965
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(_eventEmitter);
966
+ if (emitter)
967
+ emitter->onLinkLongPress({.url = std::string(url.UTF8String)});
968
+ }
969
+
970
+ - (void)emitLinkContextMenuItemPress:(NSString *)itemText pattern:(NSString *)pattern url:(NSString *)url
971
+ {
972
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(_eventEmitter);
973
+ if (emitter) {
974
+ emitter->onLinkContextMenuItemPress({.url = std::string(url.UTF8String),
975
+ .pattern = std::string(pattern.UTF8String),
976
+ .itemText = std::string(itemText.UTF8String)});
977
+ }
978
+ }
979
+
980
+ - (void)emitImagePress:(NSString *)url altText:(NSString *)altText
981
+ {
982
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(_eventEmitter);
983
+ if (emitter)
984
+ emitter->onImagePress({.url = std::string(url.UTF8String ?: ""), .altText = std::string(altText.UTF8String ?: "")});
985
+ }
986
+
987
+ - (void)emitCodeBlockPress:(NSString *)code language:(NSString *)language
988
+ {
989
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(_eventEmitter);
990
+ if (emitter)
991
+ emitter->onCodeBlockPress(
992
+ {.code = std::string(code.UTF8String ?: ""), .language = std::string(language.UTF8String ?: "")});
993
+ }
994
+
995
+ - (void)emitTaskListItemPress:(NSInteger)index checked:(BOOL)checked text:(NSString *)text
996
+ {
997
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(_eventEmitter);
998
+ if (emitter)
999
+ emitter->onTaskListItemPress({.index = (int)index, .checked = checked, .text = std::string(text.UTF8String ?: "")});
1000
+ }
1001
+
1002
+ - (void)emitContextMenuItemPress:(NSString *)itemText
1003
+ selectedText:(NSString *)selectedText
1004
+ selectionStart:(NSUInteger)selectionStart
1005
+ selectionEnd:(NSUInteger)selectionEnd
1006
+ {
1007
+ auto emitter = std::static_pointer_cast<EnrichedTiptapTextEventEmitter const>(_eventEmitter);
1008
+ if (emitter)
1009
+ emitter->onContextMenuItemPress({
1010
+ .itemText = std::string(itemText.UTF8String),
1011
+ .selectedText = std::string(selectedText.UTF8String),
1012
+ .selectionStart = (int)selectionStart,
1013
+ .selectionEnd = (int)selectionEnd,
1014
+ });
1015
+ }
1016
+
1017
+ - (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
1018
+ {
1019
+ [super updateEventEmitter:eventEmitter];
1020
+ [_latexErrorCoordinator flushPending];
1021
+ }
1022
+
1023
+ - (void)textTapped:(ENRMTapRecognizer *)recognizer
1024
+ {
1025
+ ENRMPlatformTextView *textView = (ENRMPlatformTextView *)recognizer.view;
1026
+
1027
+ if (_enableTaskListItemToggle &&
1028
+ handleTaskListTapWithSharedLogic(
1029
+ textView, recognizer, &self->_cachedTiptap, self->_config,
1030
+ ^(NSInteger index, BOOL checked, NSString *itemText) {
1031
+ [self emitTaskListItemPress:index checked:checked text:itemText];
1032
+ },
1033
+ ^(NSString *updatedTiptap) { [self renderTiptapContent:updatedTiptap]; })) {
1034
+ return;
1035
+ }
1036
+
1037
+ if (handleSpoilerTap(textView, recognizer, _spoilerManager)) {
1038
+ return;
1039
+ }
1040
+
1041
+ if (ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url) { [self emitLinkPress:url]; })) {
1042
+ return;
1043
+ }
1044
+
1045
+ NSDictionary<NSString *, NSString *> *mention = mentionAtTapLocation(textView, recognizer);
1046
+ if (mention) {
1047
+ [self emitMentionPress:mention[@"id"] label:mention[@"label"]];
1048
+ return;
1049
+ }
1050
+
1051
+ if (_enableImagePress) {
1052
+ NSDictionary<NSString *, NSString *> *image = imageAtTapLocation(textView, recognizer);
1053
+ if (image) {
1054
+ [self emitImagePress:image[@"url"] altText:image[@"altText"]];
1055
+ return;
1056
+ }
1057
+ }
1058
+
1059
+ // Skip while text is selected so the tap clears the selection instead of
1060
+ // firing the press (matches the web guard).
1061
+ if (_enableCodeBlockPress && textView.selectedRange.length == 0) {
1062
+ NSDictionary<NSString *, NSString *> *codeBlock = codeBlockAtTapLocation(textView, recognizer);
1063
+ if (codeBlock) {
1064
+ [self emitCodeBlockPress:codeBlock[@"code"] language:codeBlock[@"language"]];
1065
+ return;
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ #if !TARGET_OS_OSX
1071
+ - (void)mentionLongPressed:(UILongPressGestureRecognizer *)recognizer
1072
+ {
1073
+ if (recognizer.state != UIGestureRecognizerStateBegan)
1074
+ return;
1075
+ NSDictionary<NSString *, NSString *> *mention = mentionAtPoint(_textView, [recognizer locationInView:_textView]);
1076
+ if (!mention)
1077
+ return;
1078
+ [self emitMentionLongPress:mention[@"id"] label:mention[@"label"]];
1079
+ }
1080
+
1081
+ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
1082
+ {
1083
+ if (gestureRecognizer != _mentionLongPress)
1084
+ return YES;
1085
+ return mentionAtPoint(_textView, [_mentionLongPress locationInView:_textView]) != nil;
1086
+ }
1087
+
1088
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
1089
+ shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
1090
+ {
1091
+ if (gestureRecognizer == _mentionLongPress &&
1092
+ [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]])
1093
+ return YES;
1094
+ return NO;
1095
+ }
1096
+ #endif
1097
+
1098
+ #pragma mark - UITextViewDelegate (Link Interaction)
1099
+
1100
+ #if !TARGET_OS_OSX
1101
+ - (UITextItemMenuConfiguration *)textView:(UITextView *)textView
1102
+ menuConfigurationForTextItem:(UITextItem *)textItem
1103
+ defaultMenu:(UIMenu *)defaultMenu API_AVAILABLE(ios(17.0))
1104
+ {
1105
+ __weak EnrichedTiptapText *weakSelf = self;
1106
+ return ENRMLinkMenuConfigurationForTextItem(textView, textItem, defaultMenu, _linkContextMenus, _enableLinkPreview,
1107
+ ^(NSString *url) { [weakSelf emitLinkLongPress:url]; });
1108
+ }
1109
+
1110
+ - (ENRMLinkContextMenus *)linkContextMenusForTextView:(UITextView *)textView
1111
+ {
1112
+ return _linkContextMenus;
1113
+ }
1114
+
1115
+ - (BOOL)textView:(ENRMPlatformTextView *)textView
1116
+ shouldInteractWithURL:(NSURL *)URL
1117
+ inRange:(NSRange)characterRange
1118
+ interaction:(UITextItemInteraction)interaction
1119
+ {
1120
+ if (interaction != UITextItemInteractionPresentActions) {
1121
+ return YES;
1122
+ }
1123
+
1124
+ NSString *urlString = linkURLAtRange(textView, characterRange);
1125
+
1126
+ // A link with a menu is presented by the iOS 17 callback above and must not be vetoed here.
1127
+ if ([_linkContextMenus hasMenuForURL:urlString])
1128
+ return YES;
1129
+
1130
+ if (!urlString || _enableLinkPreview) {
1131
+ return YES;
1132
+ }
1133
+
1134
+ [self emitLinkLongPress:urlString];
1135
+ return NO;
1136
+ }
1137
+
1138
+ #pragma mark - UITextViewDelegate (Edit Menu)
1139
+
1140
+ // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16.
1141
+ - (UIMenu *)textView:(ENRMPlatformTextView *)textView
1142
+ editMenuForTextInRange:(NSRange)range
1143
+ suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
1144
+ {
1145
+ __weak EnrichedTiptapText *weakSelf = self;
1146
+ ENRMContextMenuPressHandler handler =
1147
+ ^(NSString *itemText, NSString *selectedText, NSUInteger selectionStart, NSUInteger selectionEnd) {
1148
+ EnrichedTiptapText *strongSelf = weakSelf;
1149
+ if (strongSelf)
1150
+ [strongSelf emitContextMenuItemPress:itemText
1151
+ selectedText:selectedText
1152
+ selectionStart:selectionStart
1153
+ selectionEnd:selectionEnd];
1154
+ };
1155
+ NSMutableArray<UIAction *> *customActions =
1156
+ ENRMBuildContextMenuActions(_contextMenuItemTexts, _contextMenuItemIcons, textView, range, handler);
1157
+
1158
+ return buildEditMenuForSelection(textView.attributedText, range, _cachedTiptap, _config, suggestedActions,
1159
+ customActions, _selectionMenuConfig);
1160
+ }
1161
+ #endif
1162
+
1163
+ #pragma mark - Accessibility (VoiceOver Navigation)
1164
+
1165
+ - (void)rebuildAccessibilityElementsIfNeeded
1166
+ {
1167
+ if (!_accessibilityNeedsRebuild) {
1168
+ return;
1169
+ }
1170
+ _accessibilityNeedsRebuild = NO;
1171
+ #if !TARGET_OS_OSX
1172
+ _accessibilityElements = [TiptapAccessibilityElementBuilder buildElementsForTextView:_textView
1173
+ info:_accessibilityInfo
1174
+ labels:_accessibilityLabels
1175
+ container:self];
1176
+ #else
1177
+ _accessibilityElements = [NSMutableArray array];
1178
+ #endif
1179
+ }
1180
+
1181
+ - (BOOL)isAccessibilityElement
1182
+ {
1183
+ return NO;
1184
+ }
1185
+
1186
+ - (NSInteger)accessibilityElementCount
1187
+ {
1188
+ [self rebuildAccessibilityElementsIfNeeded];
1189
+ return _accessibilityElements.count;
1190
+ }
1191
+
1192
+ - (id)accessibilityElementAtIndex:(NSInteger)index
1193
+ {
1194
+ [self rebuildAccessibilityElementsIfNeeded];
1195
+ if (index < 0 || index >= (NSInteger)_accessibilityElements.count) {
1196
+ return nil;
1197
+ }
1198
+ return _accessibilityElements[index];
1199
+ }
1200
+
1201
+ - (NSInteger)indexOfAccessibilityElement:(id)element
1202
+ {
1203
+ [self rebuildAccessibilityElementsIfNeeded];
1204
+ return [_accessibilityElements indexOfObject:element];
1205
+ }
1206
+
1207
+ - (NSArray *)accessibilityElements
1208
+ {
1209
+ [self rebuildAccessibilityElementsIfNeeded];
1210
+ return _accessibilityElements;
1211
+ }
1212
+
1213
+ #if !TARGET_OS_OSX
1214
+ - (NSArray<UIAccessibilityCustomRotor *> *)accessibilityCustomRotors
1215
+ {
1216
+ [self rebuildAccessibilityElementsIfNeeded];
1217
+ return [TiptapAccessibilityElementBuilder buildRotorsFromElements:_accessibilityElements
1218
+ labels:_accessibilityLabels];
1219
+ }
1220
+ #endif
1221
+
1222
+ @end