@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,449 @@
1
+ package com.renderrichtext.utils.text.view
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.text.Spannable
6
+ import android.text.method.ArrowKeyMovementMethod
7
+ import android.view.MotionEvent
8
+ import android.view.View
9
+ import android.view.ViewConfiguration
10
+ import android.view.ViewParent
11
+ import android.widget.TextView
12
+ import com.renderrichtext.spans.CodeBlockSpan
13
+ import com.renderrichtext.spans.FactoidSpan
14
+ import com.renderrichtext.spans.ImageSpan
15
+ import com.renderrichtext.spans.LinkSpan
16
+ import com.renderrichtext.spans.MentionChipSpan
17
+ import com.renderrichtext.spans.MentionSpan
18
+ import com.renderrichtext.spans.SpoilerSpan
19
+ import com.renderrichtext.spoiler.SpoilerCapable
20
+ import kotlin.math.abs
21
+
22
+ /**
23
+ * Movement method that adds link tap / long-press and spoiler tap handling on
24
+ * top of [ArrowKeyMovementMethod], the method [setTextIsSelectable] installs.
25
+ *
26
+ * Must never mutate the buffer's Selection spans — the platform Editor
27
+ * manages them during long-press gestures, and removing or overwriting
28
+ * them mid-gesture crashes on some OEM skins.
29
+ */
30
+ class LinkLongPressMovementMethod : ArrowKeyMovementMethod() {
31
+ private val handler = Handler(Looper.getMainLooper())
32
+ private var longPressRunnable: Runnable? = null
33
+
34
+ private var startX = 0f
35
+ private var startY = 0f
36
+ private var pressedLink: LinkSpan? = null
37
+ private var pressedImage: ImageSpan? = null
38
+ private var pressedFactoid: FactoidSpan? = null
39
+ private var pressedMention: MentionSpan? = null
40
+ private var pressedCodeBlock: CodeBlockSpan? = null
41
+
42
+ var isLinkTouchActive: Boolean = false
43
+ private set
44
+ private var isTouchWithinTextBounds: Boolean = true
45
+ private var disallowsParentIntercept = false
46
+
47
+ override fun onTouchEvent(
48
+ widget: TextView,
49
+ buffer: Spannable,
50
+ event: MotionEvent,
51
+ ): Boolean {
52
+ when (event.action) {
53
+ MotionEvent.ACTION_DOWN -> {
54
+ startX = event.x
55
+ startY = event.y
56
+
57
+ pressedLink = findLinkSpan(widget, buffer, event)
58
+ pressedImage =
59
+ if (pressedLink == null && imagePressHost(widget)?.imagePressEnabled == true) {
60
+ findImageSpan(widget, buffer, event)
61
+ } else {
62
+ null
63
+ }
64
+ pressedFactoid =
65
+ if (pressedLink == null && pressedImage == null) {
66
+ findFactoidSpan(widget, buffer, event)
67
+ } else {
68
+ null
69
+ }
70
+ pressedMention =
71
+ if (pressedLink == null && pressedImage == null && pressedFactoid == null) {
72
+ findMentionSpan(widget, buffer, event)
73
+ } else {
74
+ null
75
+ }
76
+ pressedCodeBlock =
77
+ if (pressedLink == null &&
78
+ pressedImage == null &&
79
+ pressedFactoid == null &&
80
+ pressedMention == null &&
81
+ // Skip while text is selected so the tap clears the selection
82
+ // instead of firing the press (matches the web guard).
83
+ !widget.hasSelection() &&
84
+ codeBlockPressHost(widget)?.codeBlockPressEnabled == true
85
+ ) {
86
+ findCodeBlockSpan(widget, buffer, event)
87
+ } else {
88
+ null
89
+ }
90
+ isLinkTouchActive =
91
+ pressedLink != null || pressedImage != null || pressedFactoid != null ||
92
+ pressedMention != null || pressedCodeBlock != null
93
+ setFactoidPressed(widget, pressedFactoid, pressed = true)
94
+ setMentionPressed(widget, pressedMention, pressed = true)
95
+ pressedLink?.let { scheduleLongPress(widget, it) }
96
+ pressedMention?.let { scheduleMentionLongPress(widget, it) }
97
+ // Keeps RNGH Pressable and scrolling parents from stealing the press before ACTION_UP.
98
+ disallowsParentIntercept = isLinkTouchActive
99
+ if (isLinkTouchActive) widget.parent?.requestDisallowInterceptTouchEvent(true)
100
+ }
101
+
102
+ MotionEvent.ACTION_MOVE -> {
103
+ val config = ViewConfiguration.get(widget.context)
104
+ if (abs(event.x - startX) > config.scaledTouchSlop ||
105
+ abs(event.y - startY) > config.scaledTouchSlop
106
+ ) {
107
+ cancelLongPress()
108
+ isLinkTouchActive = false
109
+ setFactoidPressed(widget, pressedFactoid, pressed = false)
110
+ setMentionPressed(widget, pressedMention, pressed = false)
111
+ pressedLink = null
112
+ pressedImage = null
113
+ pressedFactoid = null
114
+ pressedMention = null
115
+ pressedCodeBlock = null
116
+ releaseParentIntercept(widget)
117
+ } else if (pressedFactoid != null) {
118
+ setFactoidPressed(widget, pressedFactoid, findFactoidSpan(widget, buffer, event) === pressedFactoid)
119
+ } else if (pressedMention != null) {
120
+ setMentionPressed(widget, pressedMention, findMentionSpan(widget, buffer, event) === pressedMention)
121
+ }
122
+ }
123
+
124
+ MotionEvent.ACTION_UP -> {
125
+ cancelLongPress()
126
+ val tappedLink = pressedLink
127
+ val tappedImage = pressedImage
128
+ val tappedFactoid = pressedFactoid
129
+ val tappedMention = pressedMention
130
+ val tappedCodeBlock = pressedCodeBlock
131
+ isLinkTouchActive = false
132
+ setFactoidPressed(widget, tappedFactoid, pressed = false)
133
+ setMentionPressed(widget, tappedMention, pressed = false)
134
+ pressedLink = null
135
+ pressedImage = null
136
+ pressedFactoid = null
137
+ pressedMention = null
138
+ pressedCodeBlock = null
139
+ releaseParentIntercept(widget)
140
+
141
+ if (handleSpoilerTap(widget, buffer, event)) {
142
+ return true
143
+ }
144
+
145
+ // LinkSpan.onClick itself swallows the click that follows a completed
146
+ // long-press (and resets its internal flag), so it is always invoked
147
+ // for a tap that started and ended on the same link.
148
+ if (tappedLink != null && findLinkSpan(widget, buffer, event) === tappedLink) {
149
+ tappedLink.onClick(widget)
150
+ return true
151
+ }
152
+
153
+ if (tappedImage != null && findImageSpan(widget, buffer, event) === tappedImage) {
154
+ imagePressHost(widget)?.emitOnImagePress(tappedImage.imageUrl, tappedImage.altText)
155
+ return true
156
+ }
157
+
158
+ if (tappedFactoid != null && findFactoidSpan(widget, buffer, event) === tappedFactoid) {
159
+ tappedFactoid.onClick(widget)
160
+ return true
161
+ }
162
+
163
+ if (tappedMention != null && findMentionSpan(widget, buffer, event) === tappedMention) {
164
+ tappedMention.onClick(widget)
165
+ return true
166
+ }
167
+
168
+ if (tappedCodeBlock != null && findCodeBlockSpan(widget, buffer, event) === tappedCodeBlock) {
169
+ codeBlockPressHost(widget)?.emitOnCodeBlockPress(tappedCodeBlock.code, tappedCodeBlock.language)
170
+ return true
171
+ }
172
+ }
173
+
174
+ MotionEvent.ACTION_CANCEL -> {
175
+ cancelLongPress()
176
+ isLinkTouchActive = false
177
+ setFactoidPressed(widget, pressedFactoid, pressed = false)
178
+ setMentionPressed(widget, pressedMention, pressed = false)
179
+ pressedLink = null
180
+ pressedImage = null
181
+ pressedCodeBlock = null
182
+ pressedFactoid = null
183
+ pressedMention = null
184
+ releaseParentIntercept(widget)
185
+ }
186
+ }
187
+
188
+ // getOffsetForHorizontal snaps to the nearest character, so without this
189
+ // guard taps in empty space past the end of a line would still be treated
190
+ // as text touches. Let them fall through to the parent (e.g. RNGH
191
+ // Pressable) instead.
192
+ if (!isTouchWithinTextBounds) {
193
+ return false
194
+ }
195
+
196
+ return super.onTouchEvent(widget, buffer, event)
197
+ }
198
+
199
+ private fun setFactoidPressed(
200
+ widget: TextView,
201
+ span: FactoidSpan?,
202
+ pressed: Boolean,
203
+ ) {
204
+ if (span == null || span.pressed == pressed) return
205
+ span.pressed = pressed
206
+ widget.invalidate()
207
+ }
208
+
209
+ private fun setMentionPressed(
210
+ widget: TextView,
211
+ span: MentionSpan?,
212
+ pressed: Boolean,
213
+ ) {
214
+ if (span == null || span.pressed == pressed) return
215
+ span.pressed = pressed
216
+ val text = widget.text as? Spannable
217
+ val start = text?.getSpanStart(span) ?: -1
218
+ val end = if (text != null && start >= 0) text.getSpanEnd(span) else -1
219
+ val chip =
220
+ if (text != null && end > start) {
221
+ text.getSpans(start, end, MentionChipSpan::class.java).firstOrNull { candidate ->
222
+ text.getSpanStart(candidate) == start && text.getSpanEnd(candidate) == end
223
+ }
224
+ } else {
225
+ null
226
+ }
227
+ // A factoid fill is a line background, so invalidate() draws it again. The mention
228
+ // is a replacement run, and a selectable TextView keeps that run in a display list
229
+ // that invalidate() does not rebuild. Re-setting the chip records the darker fill.
230
+ if (text != null && chip != null) {
231
+ chip.pressed = pressed
232
+ text.setSpan(chip, start, end, text.getSpanFlags(chip))
233
+ } else {
234
+ widget.invalidate()
235
+ }
236
+ }
237
+
238
+ private fun scheduleLongPress(
239
+ widget: TextView,
240
+ span: LinkSpan,
241
+ ) {
242
+ cancelLongPress()
243
+ // The view's own long-press races the runnable below; cancel it so only the link fires.
244
+ widget.cancelLongPress()
245
+
246
+ longPressRunnable =
247
+ Runnable {
248
+ // Execute the long click logic on the span
249
+ if (span.onLongClick(widget)) {
250
+ // If consumed, cancel the system's own long-press logic (like context menus)
251
+ widget.cancelLongPress()
252
+ }
253
+ longPressRunnable = null
254
+ }.also {
255
+ handler.postDelayed(it, ViewConfiguration.getLongPressTimeout().toLong())
256
+ }
257
+ }
258
+
259
+ private fun scheduleMentionLongPress(
260
+ widget: TextView,
261
+ span: MentionSpan,
262
+ ) {
263
+ cancelLongPress()
264
+ widget.cancelLongPress()
265
+
266
+ longPressRunnable =
267
+ Runnable {
268
+ if (span.onLongClick(widget)) {
269
+ widget.cancelLongPress()
270
+ }
271
+ longPressRunnable = null
272
+ }.also {
273
+ handler.postDelayed(it, ViewConfiguration.getLongPressTimeout().toLong())
274
+ }
275
+ }
276
+
277
+ private fun cancelLongPress() {
278
+ longPressRunnable?.let(handler::removeCallbacks)
279
+ longPressRunnable = null
280
+ }
281
+
282
+ // Undo only our own request: a long-press drag selection relies on the Editor's
283
+ // request staying in place until ACTION_UP, or a scrolling parent steals the drag.
284
+ private fun releaseParentIntercept(widget: TextView) {
285
+ if (!disallowsParentIntercept) return
286
+ disallowsParentIntercept = false
287
+ widget.parent?.requestDisallowInterceptTouchEvent(false)
288
+ }
289
+
290
+ private fun charOffsetAt(
291
+ widget: TextView,
292
+ event: MotionEvent,
293
+ ): Int? {
294
+ val x = event.x - widget.totalPaddingLeft + widget.scrollX
295
+ val y = event.y - widget.totalPaddingTop + widget.scrollY
296
+ val layout = widget.layout ?: return null
297
+
298
+ // getLineForVertical clamps to the first/last line for out-of-range
299
+ // values, so taps in vertical padding would silently map to a real
300
+ // line. Reject them before proceeding.
301
+ if (y < 0f || y > layout.height) {
302
+ return null
303
+ }
304
+
305
+ val line = layout.getLineForVertical(y.toInt())
306
+
307
+ // getOffsetForHorizontal snaps to the nearest character even when the
308
+ // tap is outside the actual text content (e.g. empty space after the
309
+ // last word on a line). Guard against that by checking the tap falls
310
+ // within the line's text bounds.
311
+ if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)) {
312
+ return null
313
+ }
314
+
315
+ return layout.getOffsetForHorizontal(line, x)
316
+ }
317
+
318
+ private fun findLinkSpan(
319
+ widget: TextView,
320
+ buffer: Spannable,
321
+ event: MotionEvent,
322
+ ): LinkSpan? {
323
+ val offset = charOffsetAt(widget, event) ?: return null
324
+ return buffer.getSpans(offset, offset, LinkSpan::class.java).firstOrNull()
325
+ }
326
+
327
+ private fun findMentionSpan(
328
+ widget: TextView,
329
+ buffer: Spannable,
330
+ event: MotionEvent,
331
+ ): MentionSpan? {
332
+ val offset = charOffsetAt(widget, event) ?: return null
333
+ return buffer
334
+ .getSpans(offset, offset, MentionSpan::class.java)
335
+ .minByOrNull { buffer.getSpanEnd(it) - buffer.getSpanStart(it) }
336
+ }
337
+
338
+ private fun findFactoidSpan(
339
+ widget: TextView,
340
+ buffer: Spannable,
341
+ event: MotionEvent,
342
+ ): FactoidSpan? {
343
+ val offset = charOffsetAt(widget, event) ?: return null
344
+ return buffer
345
+ .getSpans(offset, offset, FactoidSpan::class.java)
346
+ .minByOrNull { buffer.getSpanEnd(it) - buffer.getSpanStart(it) }
347
+ }
348
+
349
+ private fun findImageSpan(
350
+ widget: TextView,
351
+ buffer: Spannable,
352
+ event: MotionEvent,
353
+ ): ImageSpan? {
354
+ val offset = charOffsetAt(widget, event) ?: return null
355
+ return buffer.getSpans(offset, offset, ImageSpan::class.java).firstOrNull()
356
+ }
357
+
358
+ private fun imagePressHost(widget: View): ImagePressHost? {
359
+ if (widget is ImagePressHost) return widget
360
+ var parent: ViewParent? = widget.parent
361
+ while (parent != null && parent !is ImagePressHost) {
362
+ parent = parent.parent
363
+ }
364
+ return parent as? ImagePressHost
365
+ }
366
+
367
+ // A code block is a whole-line box, so this matches a tap anywhere on the line
368
+ // (ignoring x), unlike charOffsetAt which needs an exact glyph.
369
+ private fun findCodeBlockSpan(
370
+ widget: TextView,
371
+ buffer: Spannable,
372
+ event: MotionEvent,
373
+ ): CodeBlockSpan? {
374
+ val layout = widget.layout ?: return null
375
+ val y = event.y - widget.totalPaddingTop + widget.scrollY
376
+ if (y < 0f || y > layout.height) return null
377
+ val line = layout.getLineForVertical(y.toInt())
378
+ val lineStart = layout.getLineStart(line)
379
+ val lineEnd = layout.getLineEnd(line)
380
+ return buffer.getSpans(lineStart, lineEnd, CodeBlockSpan::class.java).firstOrNull()
381
+ }
382
+
383
+ private fun codeBlockPressHost(widget: View): CodeBlockPressHost? {
384
+ if (widget is CodeBlockPressHost) return widget
385
+ var parent: ViewParent? = widget.parent
386
+ while (parent != null && parent !is CodeBlockPressHost) {
387
+ parent = parent.parent
388
+ }
389
+ return parent as? CodeBlockPressHost
390
+ }
391
+
392
+ private fun handleSpoilerTap(
393
+ widget: TextView,
394
+ buffer: Spannable,
395
+ event: MotionEvent,
396
+ ): Boolean {
397
+ val offset = charOffsetAt(widget, event) ?: return false
398
+ val tappedSpan =
399
+ buffer
400
+ .getSpans(offset, offset, SpoilerSpan::class.java)
401
+ .firstOrNull { !it.revealed && !it.revealing } ?: return false
402
+
403
+ val drawer = (widget as? SpoilerCapable)?.spoilerOverlayDrawer ?: return false
404
+ val spans = expandContiguousSpoilers(buffer, tappedSpan)
405
+ val remaining = intArrayOf(spans.size)
406
+
407
+ for (span in spans) {
408
+ drawer.revealSpan(span) {
409
+ remaining[0]--
410
+ if (remaining[0] <= 0) widget.invalidate()
411
+ }
412
+ }
413
+ widget.invalidate()
414
+ return true
415
+ }
416
+
417
+ private fun expandContiguousSpoilers(
418
+ buffer: Spannable,
419
+ seed: SpoilerSpan,
420
+ ): List<SpoilerSpan> {
421
+ val allSpans = buffer.getSpans(0, buffer.length, SpoilerSpan::class.java)
422
+ if (allSpans.size <= 1) return listOf(seed)
423
+
424
+ val result = mutableSetOf(seed)
425
+ var rangeStart = buffer.getSpanStart(seed)
426
+ var rangeEnd = buffer.getSpanEnd(seed)
427
+ var changed = true
428
+ while (changed) {
429
+ changed = false
430
+ for (span in allSpans) {
431
+ if (span in result) continue
432
+ val spanStart = buffer.getSpanStart(span)
433
+ val spanEnd = buffer.getSpanEnd(span)
434
+ if (spanEnd >= rangeStart && spanStart <= rangeEnd) {
435
+ result.add(span)
436
+ if (spanStart < rangeStart) rangeStart = spanStart
437
+ if (spanEnd > rangeEnd) rangeEnd = spanEnd
438
+ changed = true
439
+ }
440
+ }
441
+ }
442
+ return result.sortedBy { buffer.getSpanStart(it) }
443
+ }
444
+
445
+ companion object {
446
+ @JvmStatic
447
+ fun createInstance(): LinkLongPressMovementMethod = LinkLongPressMovementMethod()
448
+ }
449
+ }
@@ -0,0 +1,235 @@
1
+ package com.renderrichtext.utils.text.view
2
+
3
+ import android.content.ClipData
4
+ import android.content.ClipboardManager
5
+ import android.content.Context
6
+ import android.text.Spannable
7
+ import android.view.ActionMode
8
+ import android.view.Menu
9
+ import android.view.MenuItem
10
+ import android.view.ViewParent
11
+ import android.widget.TextView
12
+ import com.renderrichtext.EnrichedTiptapText
13
+ import com.renderrichtext.spans.ImageSpan
14
+ import com.renderrichtext.spans.stripFactoidPadding
15
+ import com.renderrichtext.styles.StyleConfig
16
+ import com.renderrichtext.utils.common.layout.isLayoutRTL
17
+ import com.renderrichtext.utils.text.conversion.HTMLGenerator
18
+ import com.renderrichtext.utils.text.conversion.TiptapExtractor
19
+
20
+ private const val MENU_ITEM_COPY_TIPTAP = 1000
21
+ private const val MENU_ITEM_COPY_IMAGE_URL = 1001
22
+ private const val MENU_ITEM_CUSTOM_BASE = 2000
23
+ private const val MENU_ITEM_CUSTOM_GROUP = 2001
24
+
25
+ data class SelectionMenuConfig(
26
+ val copyAsTiptap: Boolean = true,
27
+ val copyImageUrl: Boolean = true,
28
+ val copyLabel: String = "",
29
+ val copyAsTiptapLabel: String = "",
30
+ val copyImageUrlLabel: String = "",
31
+ val copyImageUrlsLabel: String = "",
32
+ // Plural templates indexed by image count (0..100). Empty when no pluralLabels
33
+ // are set; counts > 100 use copyImageUrlsLabel (the "other" form).
34
+ val copyImageUrlPluralTemplates: List<String> = emptyList(),
35
+ )
36
+
37
+ /**
38
+ * Resolves the "Copy Image URL(s)" menu title for the given image count. Uses the
39
+ * precomputed plural templates when present (counts > 100 use the "other" form),
40
+ * otherwise the singular/`{count}` templates. All labels are resolved JS-side.
41
+ */
42
+ private fun SelectionMenuConfig.imageUrlsTitle(count: Int): String {
43
+ val template =
44
+ when {
45
+ copyImageUrlPluralTemplates.isNotEmpty() && count in 0..100 -> copyImageUrlPluralTemplates[count]
46
+ count == 1 -> copyImageUrlLabel
47
+ else -> copyImageUrlsLabel
48
+ }
49
+ return template.replace("{count}", count.toString())
50
+ }
51
+
52
+ /**
53
+ * Creates an ActionMode.Callback that adds custom copy options and
54
+ * overrides the default "Copy" action to include HTML for rich text support.
55
+ */
56
+ fun createSelectionActionModeCallback(
57
+ textView: TextView,
58
+ getCustomItemTexts: () -> List<String> = { emptyList() },
59
+ getSelectionMenuConfig: () -> SelectionMenuConfig = { SelectionMenuConfig() },
60
+ onCustomItemPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit =
61
+ { _, _, _, _ -> },
62
+ ): ActionMode.Callback =
63
+ object : ActionMode.Callback {
64
+ override fun onCreateActionMode(
65
+ mode: ActionMode?,
66
+ menu: Menu?,
67
+ ): Boolean = true
68
+
69
+ override fun onPrepareActionMode(
70
+ mode: ActionMode?,
71
+ menu: Menu?,
72
+ ): Boolean {
73
+ if (menu == null) return false
74
+
75
+ menu.removeItem(MENU_ITEM_COPY_TIPTAP)
76
+ menu.removeItem(MENU_ITEM_COPY_IMAGE_URL)
77
+ menu.removeGroup(MENU_ITEM_CUSTOM_GROUP)
78
+
79
+ val selectionMenuConfig = getSelectionMenuConfig()
80
+
81
+ // The system Copy item is added by the platform; relabel it so it
82
+ // matches the rest of the localized menu (iOS rebuilds the menu so
83
+ // gets this for free; Android only intercepts the click).
84
+ if (selectionMenuConfig.copyLabel.isNotEmpty()) {
85
+ menu.findItem(android.R.id.copy)?.title = selectionMenuConfig.copyLabel
86
+ }
87
+
88
+ if (
89
+ selectionMenuConfig.copyAsTiptap &&
90
+ textView.selectionStart >= 0 &&
91
+ textView.selectionEnd > textView.selectionStart
92
+ ) {
93
+ menu.add(
94
+ Menu.NONE,
95
+ MENU_ITEM_COPY_TIPTAP,
96
+ Menu.NONE,
97
+ selectionMenuConfig.copyAsTiptapLabel,
98
+ )
99
+ }
100
+
101
+ if (textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart) {
102
+ val customItems = getCustomItemTexts()
103
+ customItems.forEachIndexed { index, text ->
104
+ menu
105
+ .add(MENU_ITEM_CUSTOM_GROUP, MENU_ITEM_CUSTOM_BASE + index, index, text)
106
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
107
+ }
108
+ }
109
+
110
+ val imageUrls =
111
+ if (selectionMenuConfig.copyImageUrl) {
112
+ textView.getImageUrlsInSelection()
113
+ } else {
114
+ emptyList()
115
+ }
116
+ if (imageUrls.isNotEmpty()) {
117
+ val title = selectionMenuConfig.imageUrlsTitle(imageUrls.size)
118
+ menu.add(Menu.NONE, MENU_ITEM_COPY_IMAGE_URL, Menu.NONE, title)
119
+ }
120
+
121
+ return true
122
+ }
123
+
124
+ override fun onActionItemClicked(
125
+ mode: ActionMode?,
126
+ item: MenuItem?,
127
+ ): Boolean {
128
+ val itemId = item?.itemId ?: return false
129
+
130
+ when (itemId) {
131
+ android.R.id.copy -> {
132
+ textView.copyWithHTML()
133
+ mode?.finish()
134
+ return true
135
+ }
136
+
137
+ MENU_ITEM_COPY_TIPTAP -> {
138
+ textView.copyTiptapToClipboard()
139
+ mode?.finish()
140
+ return true
141
+ }
142
+
143
+ MENU_ITEM_COPY_IMAGE_URL -> {
144
+ textView.copyImageUrlsToClipboard()
145
+ mode?.finish()
146
+ return true
147
+ }
148
+ }
149
+
150
+ val customItems = getCustomItemTexts()
151
+ val customIndex = itemId - MENU_ITEM_CUSTOM_BASE
152
+ if (customIndex in customItems.indices) {
153
+ val start = textView.selectionStart
154
+ val end = textView.selectionEnd
155
+ val selectedText =
156
+ if (start >= 0 && end > start) textView.text.subSequence(start, end).stripFactoidPadding() else ""
157
+ onCustomItemPress(customItems[customIndex], selectedText, start, end)
158
+ mode?.finish()
159
+ return true
160
+ }
161
+
162
+ return false
163
+ }
164
+
165
+ override fun onDestroyActionMode(mode: ActionMode?) {}
166
+ }
167
+
168
+ /** Copies selection as both plain text and HTML with inline styles. */
169
+ private fun TextView.copyWithHTML() {
170
+ val start = selectionStart
171
+ val end = selectionEnd
172
+ if (start < 0 || end < 0 || start >= end) return
173
+
174
+ val spannable = text as? Spannable ?: return
175
+ val selectedText = spannable.subSequence(start, end)
176
+ val plainText = selectedText.stripFactoidPadding()
177
+
178
+ val styleConfig =
179
+ (this as? EnrichedTiptapText)?.tiptapStyle
180
+ ?: findParentTiptapStyle()
181
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
182
+
183
+ if (styleConfig != null && selectedText is Spannable) {
184
+ // Density values convert device pixels back to CSS pixels
185
+ val displayMetrics = context.resources.displayMetrics
186
+ val isRTL = context.resources.isLayoutRTL()
187
+ val html =
188
+ HTMLGenerator.generateHTML(
189
+ selectedText,
190
+ styleConfig,
191
+ displayMetrics.scaledDensity,
192
+ displayMetrics.density,
193
+ isRTL,
194
+ )
195
+ clipboard.setPrimaryClip(ClipData.newHtmlText("EnrichedTiptap", plainText, html))
196
+ } else {
197
+ clipboard.setPrimaryClip(ClipData.newPlainText("Text", plainText))
198
+ }
199
+ }
200
+
201
+ private fun TextView.copyTiptapToClipboard() {
202
+ val tiptap = TiptapExtractor.getTiptapForSelection(this) ?: return
203
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
204
+ clipboard.setPrimaryClip(ClipData.newPlainText("Tiptap", tiptap))
205
+ }
206
+
207
+ /** Returns remote image URLs (http/https only) from the current selection. */
208
+ private fun TextView.getImageUrlsInSelection(): List<String> {
209
+ val start = selectionStart
210
+ val end = selectionEnd
211
+ if (start < 0 || end < 0 || start >= end) return emptyList()
212
+
213
+ val spannable = text as? Spannable ?: return emptyList()
214
+ return spannable
215
+ .getSpans(start, end, ImageSpan::class.java)
216
+ .mapNotNull { it.imageUrl }
217
+ .filter { it.startsWith("http://") || it.startsWith("https://") }
218
+ }
219
+
220
+ private fun TextView.copyImageUrlsToClipboard() {
221
+ val urls = getImageUrlsInSelection()
222
+ if (urls.isEmpty()) return
223
+
224
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
225
+ clipboard.setPrimaryClip(ClipData.newPlainText("Image URLs", urls.joinToString("\n")))
226
+ }
227
+
228
+ private fun TextView.findParentTiptapStyle(): StyleConfig? {
229
+ var current: ViewParent? = parent
230
+ while (current != null) {
231
+ if (current is EnrichedTiptapText) return current.tiptapStyle
232
+ current = current.parent
233
+ }
234
+ return null
235
+ }