@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,112 @@
1
+ package com.renderrichtext.accessibility
2
+
3
+ import android.content.Context
4
+ import android.graphics.Rect
5
+ import android.util.AttributeSet
6
+ import android.view.KeyEvent
7
+ import android.view.MotionEvent
8
+ import androidx.appcompat.widget.AppCompatTextView
9
+ import com.renderrichtext.spans.LinkPillSpan
10
+ import com.renderrichtext.utils.text.span.prepareWidthAwareSpans
11
+
12
+ /** AppCompatTextView with built-in TalkBack support via TiptapAccessibilityHelper. */
13
+ abstract class AccessibleTiptapTextView
14
+ @JvmOverloads
15
+ constructor(
16
+ context: Context,
17
+ attrs: AttributeSet? = null,
18
+ defStyleAttr: Int = 0,
19
+ ) : AppCompatTextView(context, attrs, defStyleAttr) {
20
+ val accessibilityHelper = TiptapAccessibilityHelper(this)
21
+
22
+ /**
23
+ * True only while [requestFocus] is on the stack. A tap focuses this view, and the
24
+ * surrounding scroll view then brings the focused child on screen by its drawing
25
+ * rect. This view is as tall as its text, so that scroll aligns the text with the
26
+ * top of the viewport and the tapped line jumps upward. For that one call, the
27
+ * drawing rect is the part already on screen, and the scroll delta is zero.
28
+ * Dragging a selection still scrolls: that path asks for the caret's rectangle,
29
+ * not this drawing rect.
30
+ */
31
+ private var reportingVisibleFocusRect = false
32
+
33
+ override fun setText(
34
+ text: CharSequence?,
35
+ type: BufferType?,
36
+ ) {
37
+ // Prime pills before the layout for the new text is built, and register for icons
38
+ // that load later. Runs from the TextView constructor too: use nothing initialized after it.
39
+ val contentWidth = width - compoundPaddingLeft - compoundPaddingRight
40
+ if (contentWidth > 0) prepareWidthAwareSpans(text, contentWidth, includeImages = false)
41
+ LinkPillSpan.registerView(text, this)
42
+ super.setText(text, type)
43
+ }
44
+
45
+ override fun onMeasure(
46
+ widthMeasureSpec: Int,
47
+ heightMeasureSpec: Int,
48
+ ) {
49
+ if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
50
+ val width = (MeasureSpec.getSize(widthMeasureSpec) - compoundPaddingLeft - compoundPaddingRight).coerceAtLeast(1)
51
+ // TextView keeps its layout when the new width still fits the old lines.
52
+ if (prepareWidthAwareSpans(text, width, includeImages = false) && layout != null) dropTextLayout()
53
+ }
54
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
55
+ }
56
+
57
+ /**
58
+ * Makes the next measure build a fresh text layout. TextView exposes no direct way:
59
+ * re-setting the text would copy it and clear the selection, while setting the break
60
+ * strategy, even to its current value, discards the layout and nothing else.
61
+ */
62
+ private fun dropTextLayout() {
63
+ breakStrategy = breakStrategy
64
+ }
65
+
66
+ override fun dispatchHoverEvent(event: MotionEvent): Boolean =
67
+ accessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event)
68
+
69
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean = accessibilityHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
70
+
71
+ override fun requestFocus(
72
+ direction: Int,
73
+ previouslyFocusedRect: Rect?,
74
+ ): Boolean {
75
+ reportingVisibleFocusRect = true
76
+ return try {
77
+ super.requestFocus(direction, previouslyFocusedRect)
78
+ } finally {
79
+ reportingVisibleFocusRect = false
80
+ }
81
+ }
82
+
83
+ override fun getDrawingRect(outRect: Rect) {
84
+ super.getDrawingRect(outRect)
85
+ if (!reportingVisibleFocusRect) return
86
+ val visibleOnScreen = Rect()
87
+ if (!getGlobalVisibleRect(visibleOnScreen)) return
88
+ val screenOrigin = IntArray(2)
89
+ getLocationOnScreen(screenOrigin)
90
+ visibleOnScreen.offset(-screenOrigin[0] + scrollX, -screenOrigin[1] + scrollY)
91
+ if (!outRect.intersect(visibleOnScreen)) super.getDrawingRect(outRect)
92
+ }
93
+
94
+ override fun onFocusChanged(
95
+ gainFocus: Boolean,
96
+ direction: Int,
97
+ previouslyFocusedRect: Rect?,
98
+ ) {
99
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
100
+ accessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
101
+ }
102
+
103
+ override fun onDetachedFromWindow() {
104
+ // Before super, and here rather than in a ViewManager's
105
+ // onDropViewInstance: getViewTreeObserver() returns the WINDOW's observer
106
+ // only while the view is attached. Once detached it hands back the view's
107
+ // own floating observer, so a removal then would target the wrong
108
+ // observer and leave the listener on the window.
109
+ accessibilityHelper.cleanup()
110
+ super.onDetachedFromWindow()
111
+ }
112
+ }
@@ -0,0 +1,576 @@
1
+ package com.renderrichtext.accessibility
2
+
3
+ import android.graphics.Rect
4
+ import android.os.Bundle
5
+ import android.text.Layout
6
+ import android.text.Spanned
7
+ import android.view.View
8
+ import android.view.ViewTreeObserver
9
+ import android.widget.TextView
10
+ import androidx.core.view.ViewCompat
11
+ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
12
+ import androidx.customview.widget.ExploreByTouchHelper
13
+ import com.renderrichtext.spans.BaseListSpan
14
+ import com.renderrichtext.spans.BlockquoteSpan
15
+ import com.renderrichtext.spans.HeadingSpan
16
+ import com.renderrichtext.spans.ImageSpan
17
+ import com.renderrichtext.spans.LinkPillSpan
18
+ import com.renderrichtext.spans.LinkSpan
19
+ import com.renderrichtext.spans.OrderedListSpan
20
+
21
+ class TiptapAccessibilityHelper(
22
+ private val textView: TextView,
23
+ ) : ExploreByTouchHelper(textView) {
24
+ private var items: List<AccessibilityItem> = emptyList()
25
+ private var needsRebuild = false
26
+ private var lastLayoutHashCode = 0
27
+ private var pendingLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
28
+
29
+ var labels: AccessibilityLabels = AccessibilityLabels()
30
+ set(value) {
31
+ if (field == value) return
32
+ field = value
33
+ invalidateAccessibilityItems()
34
+ }
35
+
36
+ data class AccessibilityItem(
37
+ val id: Int,
38
+ val text: String,
39
+ /** Full character range — used for hit-testing so there are no gaps between items. */
40
+ val start: Int,
41
+ val end: Int,
42
+ /** Trimmed character range — used for bounds calculation to avoid whitespace offsets. */
43
+ val visibleStart: Int = start,
44
+ val visibleEnd: Int = end,
45
+ val headingLevel: Int = 0,
46
+ val linkUrl: String? = null,
47
+ val listInfo: ListItemInfo? = null,
48
+ val imageAltText: String? = null,
49
+ ) {
50
+ val isHeading get() = headingLevel > 0
51
+ val isLink get() = linkUrl != null
52
+ val isListItem get() = listInfo != null
53
+ val isImage get() = imageAltText != null
54
+ }
55
+
56
+ data class ListItemInfo(
57
+ val isOrdered: Boolean,
58
+ val itemNumber: Int,
59
+ val depth: Int,
60
+ )
61
+
62
+ private data class SpanRange(
63
+ val start: Int,
64
+ val end: Int,
65
+ val headingLevel: Int = 0,
66
+ val linkUrl: String? = null,
67
+ val imageAltText: String? = null,
68
+ )
69
+
70
+ fun invalidateAccessibilityItems() {
71
+ needsRebuild = true
72
+ if (textView.layout != null) {
73
+ rebuildIfNeeded()
74
+ invalidateRoot()
75
+ } else {
76
+ schedulePostLayoutRebuild()
77
+ }
78
+ }
79
+
80
+ private fun schedulePostLayoutRebuild() {
81
+ if (pendingLayoutListener != null) return
82
+ val observer = textView.viewTreeObserver
83
+ if (!observer.isAlive) return
84
+ val listener =
85
+ ViewTreeObserver.OnGlobalLayoutListener {
86
+ removePendingLayoutListener()
87
+ if (needsRebuild) {
88
+ rebuildIfNeeded()
89
+ invalidateRoot()
90
+ }
91
+ }
92
+ pendingLayoutListener = listener
93
+ observer.addOnGlobalLayoutListener(listener)
94
+ }
95
+
96
+ /**
97
+ * Drops the pending global-layout listener, if any is still registered.
98
+ *
99
+ * Must run while the view is attached: `textView.viewTreeObserver` returns
100
+ * the window's observer only until detach, and the listener was merged into
101
+ * it from the view's floating observer when the view attached.
102
+ */
103
+ fun cleanup() {
104
+ removePendingLayoutListener()
105
+ }
106
+
107
+ private fun removePendingLayoutListener() {
108
+ val listener = pendingLayoutListener ?: return
109
+ pendingLayoutListener = null
110
+ val observer = textView.viewTreeObserver
111
+ if (observer.isAlive) {
112
+ observer.removeOnGlobalLayoutListener(listener)
113
+ }
114
+ }
115
+
116
+ /**
117
+ * When virtual children exist, prevent the host TextView from appearing as a
118
+ * standalone focusable element in TalkBack's swipe order. Without this,
119
+ * `setTextIsSelectable(true)` keeps the view focusable and TalkBack reads
120
+ * the entire text as one element before entering the virtual hierarchy.
121
+ */
122
+ private fun updateHostFocusability() {
123
+ val hasVirtualChildren = items.isNotEmpty()
124
+ ViewCompat.setScreenReaderFocusable(textView, !hasVirtualChildren)
125
+ textView.importantForAccessibility =
126
+ if (hasVirtualChildren) View.IMPORTANT_FOR_ACCESSIBILITY_YES else View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
127
+ }
128
+
129
+ private fun rebuildIfNeeded() {
130
+ val layout = textView.layout ?: return
131
+ if (needsRebuild || lastLayoutHashCode != layout.hashCode()) {
132
+ items = buildItems()
133
+ needsRebuild = false
134
+ lastLayoutHashCode = layout.hashCode()
135
+ updateHostFocusability()
136
+ }
137
+ }
138
+
139
+ private fun buildItems(): List<AccessibilityItem> {
140
+ val spanned = textView.text as? Spanned ?: return emptyList()
141
+ if (spanned.isEmpty()) return emptyList()
142
+
143
+ val text = spanned.toString()
144
+ val endLimit = visibleTextLength(text.length)
145
+ val result = mutableListOf<AccessibilityItem>()
146
+ var nextId = 0
147
+ val semanticSpans = collectSemanticSpans(spanned)
148
+
149
+ var paraStart = 0
150
+ while (paraStart < endLimit) {
151
+ val newlineIdx = text.indexOf('\n', paraStart)
152
+ val paraEnd = minOf(if (newlineIdx == -1) text.length else newlineIdx + 1, endLimit)
153
+ val trimmed = text.substring(paraStart, paraEnd).trim()
154
+
155
+ if (trimmed.isNotEmpty()) {
156
+ val spansInParagraph =
157
+ semanticSpans
158
+ .filter { it.start < paraEnd && it.end > paraStart }
159
+ .map { it.copy(end = minOf(it.end, endLimit)) }
160
+
161
+ if (spansInParagraph.isEmpty()) {
162
+ result.add(
163
+ createTextItem(nextId++, trimmed, paraStart, paraEnd, text, spanned),
164
+ )
165
+ } else {
166
+ nextId = addSegmentedItems(result, spanned, text, paraStart, paraEnd, spansInParagraph, nextId)
167
+ }
168
+ }
169
+ paraStart = paraEnd
170
+ }
171
+
172
+ return result.ifEmpty {
173
+ val visibleText = text.substring(0, endLimit).trim()
174
+ if (visibleText.isEmpty()) emptyList() else listOf(AccessibilityItem(0, visibleText, 0, endLimit))
175
+ }
176
+ }
177
+
178
+ /** Last line that is visible; less than lineCount - 1 when height-capped (e.g. `clip`). */
179
+ private fun lastVisibleLineIndex(layout: Layout): Int {
180
+ val maxLines = textView.maxLines.coerceAtLeast(1)
181
+ return (minOf(layout.lineCount, maxLines) - 1).coerceIn(0, layout.lineCount - 1)
182
+ }
183
+
184
+ /** Ellipsis-aware end offset for a visible line. */
185
+ private fun visibleLineEnd(
186
+ layout: Layout,
187
+ line: Int,
188
+ ): Int {
189
+ val lineEnd = layout.getLineEnd(line)
190
+ val count = layout.getEllipsisCount(line)
191
+ if (count == 0) return lineEnd
192
+ // Only a tail ellipsis hides text up to the end of the line. Head/middle hide a prefix or an
193
+ // inner range, so the line end is still laid out and must stay reachable.
194
+ val hiddenStart = layout.getLineStart(line) + layout.getEllipsisStart(line)
195
+ return if (hiddenStart + count >= lineEnd) hiddenStart else lineEnd
196
+ }
197
+
198
+ /** End offset of the laid-out (visible) text; less than the full length when truncated. */
199
+ private fun visibleTextLength(fullLength: Int): Int {
200
+ val layout = textView.layout ?: return fullLength
201
+ if (layout.lineCount == 0) return fullLength
202
+ return minOf(fullLength, maxOf(0, visibleLineEnd(layout, lastVisibleLineIndex(layout))))
203
+ }
204
+
205
+ private fun collectSemanticSpans(spanned: Spanned): List<SpanRange> =
206
+ (
207
+ spanned.getSpans(0, spanned.length, HeadingSpan::class.java).map {
208
+ SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), headingLevel = it.level)
209
+ } +
210
+ spanned.getSpans(0, spanned.length, LinkSpan::class.java).map {
211
+ SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), linkUrl = it.url)
212
+ } +
213
+ spanned.getSpans(0, spanned.length, ImageSpan::class.java).map {
214
+ SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), imageAltText = it.altText)
215
+ }
216
+ ).sortedBy { it.start }
217
+
218
+ private fun addSegmentedItems(
219
+ items: MutableList<AccessibilityItem>,
220
+ spanned: Spanned,
221
+ text: String,
222
+ paraStart: Int,
223
+ paraEnd: Int,
224
+ spans: List<SpanRange>,
225
+ startId: Int,
226
+ ): Int {
227
+ var nextId = startId
228
+ var segmentPos = paraStart
229
+
230
+ for (span in spans) {
231
+ if (span.start < segmentPos) continue
232
+
233
+ // Text before the span
234
+ if (segmentPos < span.start) {
235
+ val beforeText = text.substring(segmentPos, span.start).trim()
236
+ if (beforeText.isNotEmpty() && beforeText.any { it.isLetterOrDigit() }) {
237
+ items.add(createTextItem(nextId++, beforeText, segmentPos, span.start, text, spanned))
238
+ }
239
+ }
240
+
241
+ // The semantic span itself
242
+ val pill =
243
+ if (span.linkUrl != null) {
244
+ spanned.getSpans(span.start, span.end, LinkPillSpan::class.java).firstOrNull {
245
+ spanned.getSpanStart(it) == span.start && spanned.getSpanEnd(it) == span.end
246
+ }
247
+ } else {
248
+ null
249
+ }
250
+ val content = span.imageAltText ?: pill?.accessibilityText ?: spanned.substring(span.start, span.end).trim()
251
+ if (content.isNotEmpty()) {
252
+ items.add(createSpanItem(nextId++, content, span, spanned))
253
+ }
254
+ segmentPos = span.end
255
+ }
256
+
257
+ // Text after the last span
258
+ if (segmentPos < paraEnd) {
259
+ val afterText = text.substring(segmentPos, paraEnd).trim()
260
+ if (afterText.isNotEmpty() && afterText.any { it.isLetterOrDigit() }) {
261
+ items.add(createTextItem(nextId++, afterText, segmentPos, paraEnd, text, spanned))
262
+ }
263
+ }
264
+
265
+ return nextId
266
+ }
267
+
268
+ private fun createTextItem(
269
+ id: Int,
270
+ label: String,
271
+ start: Int,
272
+ end: Int,
273
+ text: String,
274
+ spanned: Spanned,
275
+ ) = AccessibilityItem(
276
+ id = id,
277
+ text = label,
278
+ start = start,
279
+ end = end,
280
+ visibleStart = text.findFirstNonWhitespace(start, end),
281
+ visibleEnd = text.findLastNonWhitespace(start, end),
282
+ listInfo = getListInfoAt(spanned, start, requireStart = true),
283
+ )
284
+
285
+ private fun createSpanItem(
286
+ id: Int,
287
+ content: String,
288
+ span: SpanRange,
289
+ spanned: Spanned,
290
+ ): AccessibilityItem {
291
+ val listContext =
292
+ if (span.headingLevel > 0 || span.imageAltText != null) {
293
+ null
294
+ } else {
295
+ getListInfoAt(spanned, span.start, requireStart = span.linkUrl == null)
296
+ }
297
+ return AccessibilityItem(
298
+ id = id,
299
+ text = content,
300
+ start = span.start,
301
+ end = span.end,
302
+ headingLevel = span.headingLevel,
303
+ linkUrl = span.linkUrl,
304
+ listInfo = listContext,
305
+ imageAltText = span.imageAltText,
306
+ )
307
+ }
308
+
309
+ override fun getVirtualViewAt(
310
+ x: Float,
311
+ y: Float,
312
+ ): Int {
313
+ rebuildIfNeeded()
314
+ if (items.isEmpty()) return HOST_ID
315
+
316
+ val offset = getCharOffsetAt(x, y)
317
+
318
+ val exact =
319
+ items
320
+ .filter { offset in it.start until it.end }
321
+ .minByOrNull { it.hitTestPriority }
322
+ if (exact != null) return exact.id
323
+
324
+ return items.minByOrNull { it.distanceTo(offset) }?.id ?: HOST_ID
325
+ }
326
+
327
+ override fun getVisibleVirtualViews(ids: MutableList<Int>) {
328
+ rebuildIfNeeded()
329
+ items.forEach { ids.add(it.id) }
330
+ }
331
+
332
+ override fun onPopulateNodeForHost(host: AccessibilityNodeInfoCompat) {
333
+ super.onPopulateNodeForHost(host)
334
+ rebuildIfNeeded()
335
+ if (items.isNotEmpty()) {
336
+ host.isScreenReaderFocusable = false
337
+ host.isFocusable = false
338
+ // Prevent TalkBack from reading the full text when swiping onto the host.
339
+ // Without this, the host node retains the TextView's text and TalkBack
340
+ // announces it as a single element before entering the virtual children.
341
+ host.text = null
342
+ host.contentDescription = null
343
+ }
344
+ }
345
+
346
+ override fun onPopulateNodeForVirtualView(
347
+ id: Int,
348
+ node: AccessibilityNodeInfoCompat,
349
+ ) {
350
+ val item = items.getOrNull(id)
351
+ if (item == null) {
352
+ node.contentDescription = ""
353
+ node.setBoundsInParent(Rect())
354
+ return
355
+ }
356
+ node.apply {
357
+ text = item.text
358
+ contentDescription = item.text
359
+ isFocusable = true
360
+ isScreenReaderFocusable = true
361
+ setBoundsInParent(boundsForItem(item))
362
+ applySemantics(item)
363
+ }
364
+ }
365
+
366
+ override fun onPerformActionForVirtualView(
367
+ id: Int,
368
+ action: Int,
369
+ args: Bundle?,
370
+ ): Boolean {
371
+ val item = items.getOrNull(id) ?: return false
372
+ if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && item.isLink) {
373
+ val spanned = textView.text as? Spanned ?: return false
374
+ val linkSpan = spanned.getSpans(item.start, item.end, LinkSpan::class.java).firstOrNull() ?: return false
375
+ linkSpan.onClick(textView)
376
+ return true
377
+ }
378
+ return false
379
+ }
380
+
381
+ private fun AccessibilityNodeInfoCompat.applySemantics(item: AccessibilityItem) {
382
+ item.listInfo?.let { info ->
383
+ setCollectionItemInfo(
384
+ AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(info.itemNumber - 1, 1, 0, 1, false, false),
385
+ )
386
+ }
387
+
388
+ val blockquoteAnnouncement = blockquoteAnnouncementFor(item)
389
+ when {
390
+ item.isHeading -> {
391
+ isHeading = true
392
+ if (blockquoteAnnouncement != null) {
393
+ roleDescription = blockquoteAnnouncement
394
+ }
395
+ }
396
+
397
+ item.isImage -> {
398
+ roleDescription = appendBlockquote("image", blockquoteAnnouncement)
399
+ }
400
+
401
+ item.isLink -> {
402
+ isClickable = true
403
+ addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK)
404
+ val base = item.listInfo?.let { "link, ${it.listAnnouncement}" } ?: "link"
405
+ roleDescription = appendBlockquote(base, blockquoteAnnouncement)
406
+ }
407
+
408
+ item.isListItem -> {
409
+ roleDescription = appendBlockquote(item.listInfo!!.listAnnouncement, blockquoteAnnouncement)
410
+ }
411
+
412
+ blockquoteAnnouncement != null -> {
413
+ roleDescription = blockquoteAnnouncement
414
+ }
415
+ }
416
+ }
417
+
418
+ private fun appendBlockquote(
419
+ base: String,
420
+ blockquoteAnnouncement: String?,
421
+ ): String = if (blockquoteAnnouncement != null) "$base, $blockquoteAnnouncement" else base
422
+
423
+ private fun blockquoteAnnouncementFor(item: AccessibilityItem): String? {
424
+ val spanned = textView.text as? Spanned ?: return null
425
+ if (spanned.isEmpty()) return null
426
+ val pos = item.start.coerceIn(0, spanned.length - 1).coerceAtLeast(0)
427
+ if (pos >= spanned.length) return null
428
+ val spans = spanned.getSpans(pos, pos + 1, BlockquoteSpan::class.java)
429
+ if (spans.isEmpty()) return null
430
+ val maxDepth = spans.maxOf { it.depth }
431
+ return if (maxDepth >= 1) labels.nestedBlockquote else labels.blockquote
432
+ }
433
+
434
+ private val ListItemInfo.listAnnouncement: String
435
+ get() {
436
+ val nested = depth > 0
437
+ return if (isOrdered) {
438
+ val template = if (nested) labels.nestedOrderedItem else labels.orderedItem
439
+ template.replace("{n}", itemNumber.toString())
440
+ } else {
441
+ if (nested) labels.nestedBulletPoint else labels.bulletPoint
442
+ }
443
+ }
444
+
445
+ private fun boundsForItem(item: AccessibilityItem): Rect {
446
+ val layout = textView.layout ?: return Rect()
447
+ if (layout.lineCount == 0) return Rect()
448
+ val maxOffset = visibleLineEnd(layout, lastVisibleLineIndex(layout))
449
+ val vs = item.visibleStart.coerceIn(0, maxOffset)
450
+ val ve = item.visibleEnd.coerceIn(vs, maxOffset)
451
+
452
+ val startLine = layout.getLineForOffset(vs)
453
+ val endLine = layout.getLineForOffset(maxOf(vs, ve - 1))
454
+
455
+ if (startLine == endLine) {
456
+ return singleLineBounds(vs, ve, startLine)
457
+ }
458
+
459
+ return multiLineBounds(vs, startLine, endLine)
460
+ }
461
+
462
+ private fun singleLineBounds(
463
+ start: Int,
464
+ end: Int,
465
+ line: Int,
466
+ ): Rect {
467
+ val layout = textView.layout
468
+ val left = layout.getPrimaryHorizontal(start).toInt() + textView.paddingLeft
469
+ val rawRight = layout.getPrimaryHorizontal(end).toInt() + textView.paddingLeft
470
+ val right = if (rawRight <= left) layout.getLineRight(line).toInt() + textView.paddingLeft else rawRight
471
+
472
+ return Rect(
473
+ left,
474
+ layout.getLineTop(line) + textView.paddingTop,
475
+ right,
476
+ layout.getLineBottom(line) + textView.paddingTop,
477
+ )
478
+ }
479
+
480
+ private fun multiLineBounds(
481
+ visibleStart: Int,
482
+ startLine: Int,
483
+ endLine: Int,
484
+ ): Rect {
485
+ val layout = textView.layout
486
+ val padLeft = textView.paddingLeft
487
+
488
+ val firstLineLeft = layout.getPrimaryHorizontal(visibleStart).toInt() + padLeft
489
+ val firstLineRight = layout.getLineRight(startLine).toInt() + padLeft
490
+
491
+ var minLeft = firstLineLeft
492
+ var maxRight = firstLineRight
493
+ for (line in (startLine + 1)..endLine) {
494
+ minLeft = minOf(minLeft, layout.getLineLeft(line).toInt() + padLeft)
495
+ maxRight = maxOf(maxRight, layout.getLineRight(line).toInt() + padLeft)
496
+ }
497
+
498
+ return Rect(
499
+ minOf(firstLineLeft, minLeft),
500
+ layout.getLineTop(startLine) + textView.paddingTop,
501
+ maxRight,
502
+ layout.getLineBottom(endLine) + textView.paddingTop,
503
+ )
504
+ }
505
+
506
+ private fun getCharOffsetAt(
507
+ x: Float,
508
+ y: Float,
509
+ ): Int {
510
+ val layout = textView.layout ?: return 0
511
+ val line = layout.getLineForVertical(y.toInt()).coerceIn(0, layout.lineCount - 1)
512
+ return layout.getOffsetForHorizontal(line, x)
513
+ }
514
+
515
+ private val AccessibilityItem.hitTestPriority: Int
516
+ get() =
517
+ when {
518
+ isLink -> 0
519
+ isImage -> 1
520
+ isHeading -> 2
521
+ isListItem -> 3
522
+ else -> 4
523
+ }
524
+
525
+ private fun AccessibilityItem.distanceTo(offset: Int): Int =
526
+ when {
527
+ offset < start -> start - offset
528
+ offset >= end -> offset - end + 1
529
+ else -> 0
530
+ }
531
+
532
+ private fun getListInfoAt(
533
+ spanned: Spanned,
534
+ position: Int,
535
+ requireStart: Boolean,
536
+ ): ListItemInfo? {
537
+ val deepest =
538
+ spanned
539
+ .getSpans(position, position + 1, BaseListSpan::class.java)
540
+ .maxByOrNull { it.depth } ?: return null
541
+
542
+ if (requireStart) {
543
+ val spanStart = spanned.getSpanStart(deepest)
544
+ val firstChar =
545
+ (spanStart until minOf(spanStart + 10, spanned.length))
546
+ .firstOrNull { !spanned[it].isWhitespace() } ?: spanStart
547
+ if (position > firstChar + 1) return null
548
+ }
549
+
550
+ return ListItemInfo(
551
+ isOrdered = deepest is OrderedListSpan,
552
+ itemNumber = (deepest as? OrderedListSpan)?.itemNumber ?: 0,
553
+ depth = deepest.depth,
554
+ )
555
+ }
556
+ }
557
+
558
+ private fun String.findFirstNonWhitespace(
559
+ from: Int,
560
+ to: Int,
561
+ ): Int {
562
+ for (i in from until to) {
563
+ if (!this[i].isWhitespace()) return i
564
+ }
565
+ return from
566
+ }
567
+
568
+ private fun String.findLastNonWhitespace(
569
+ from: Int,
570
+ to: Int,
571
+ ): Int {
572
+ for (i in (to - 1) downTo from) {
573
+ if (!this[i].isWhitespace()) return i + 1
574
+ }
575
+ return to
576
+ }
@@ -0,0 +1,24 @@
1
+ package com.renderrichtext.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class CodeBlockPressEvent(
8
+ surfaceId: Int,
9
+ viewId: Int,
10
+ private val code: String,
11
+ private val language: String,
12
+ ) : Event<CodeBlockPressEvent>(surfaceId, viewId) {
13
+ override fun getEventName(): String = EVENT_NAME
14
+
15
+ override fun getEventData(): WritableMap =
16
+ Arguments.createMap().apply {
17
+ putString("code", code)
18
+ putString("language", language)
19
+ }
20
+
21
+ companion object {
22
+ const val EVENT_NAME: String = "onCodeBlockPress"
23
+ }
24
+ }