@mathers101/react-native-render-rich-text 0.1.9 → 0.1.14

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 (327) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +140 -140
  3. package/ReactNativeRenderRichText.podspec +23 -23
  4. package/android/build.gradle +86 -86
  5. package/android/consumer-rules.pro +3 -3
  6. package/android/src/main/AndroidManifest.xml +2 -2
  7. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextLayoutManager.kt +24 -24
  8. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextPackage.kt +13 -13
  9. package/android/src/main/java/com/renderrichtext/MeasurementStore.kt +644 -644
  10. package/android/src/main/java/com/renderrichtext/accessibility/AccessibilityLabels.kt +17 -17
  11. package/android/src/main/java/com/renderrichtext/accessibility/AccessibleTiptapTextView.kt +112 -112
  12. package/android/src/main/java/com/renderrichtext/accessibility/TiptapAccessibilityHelper.kt +576 -576
  13. package/android/src/main/java/com/renderrichtext/events/CodeBlockPressEvent.kt +24 -24
  14. package/android/src/main/java/com/renderrichtext/events/ContextMenuItemPressEvent.kt +28 -28
  15. package/android/src/main/java/com/renderrichtext/events/CopyPressEvent.kt +24 -24
  16. package/android/src/main/java/com/renderrichtext/events/ImagePressEvent.kt +25 -25
  17. package/android/src/main/java/com/renderrichtext/events/LatexErrorEvent.kt +29 -29
  18. package/android/src/main/java/com/renderrichtext/events/LinkLongPressEvent.kt +23 -23
  19. package/android/src/main/java/com/renderrichtext/events/LinkPressEvent.kt +23 -23
  20. package/android/src/main/java/com/renderrichtext/events/MentionLongPressEvent.kt +25 -25
  21. package/android/src/main/java/com/renderrichtext/events/MentionPressEvent.kt +25 -25
  22. package/android/src/main/java/com/renderrichtext/events/TaskListItemPressEvent.kt +26 -26
  23. package/android/src/main/java/com/renderrichtext/math/LatexErrorReporter.kt +9 -9
  24. package/android/src/main/java/com/renderrichtext/parser/Parser.kt +59 -59
  25. package/android/src/main/java/com/renderrichtext/parser/TextLinkMatching.kt +35 -35
  26. package/android/src/main/java/com/renderrichtext/renderer/BaselineShiftRenderer.kt +45 -45
  27. package/android/src/main/java/com/renderrichtext/renderer/BlankLineRenderer.kt +39 -39
  28. package/android/src/main/java/com/renderrichtext/renderer/BlockquoteRenderer.kt +243 -243
  29. package/android/src/main/java/com/renderrichtext/renderer/BlockquoteTextRenderer.kt +56 -56
  30. package/android/src/main/java/com/renderrichtext/renderer/CodeBlockRenderer.kt +135 -135
  31. package/android/src/main/java/com/renderrichtext/renderer/CodeRenderer.kt +37 -37
  32. package/android/src/main/java/com/renderrichtext/renderer/EmphasisRenderer.kt +28 -28
  33. package/android/src/main/java/com/renderrichtext/renderer/HeadingRenderer.kt +81 -81
  34. package/android/src/main/java/com/renderrichtext/renderer/HighlightRenderer.kt +26 -26
  35. package/android/src/main/java/com/renderrichtext/renderer/LineBreakRenderer.kt +17 -17
  36. package/android/src/main/java/com/renderrichtext/renderer/LinkRenderer.kt +90 -90
  37. package/android/src/main/java/com/renderrichtext/renderer/ListContextManager.kt +109 -109
  38. package/android/src/main/java/com/renderrichtext/renderer/ListItemRenderer.kt +211 -211
  39. package/android/src/main/java/com/renderrichtext/renderer/ListRenderer.kt +133 -133
  40. package/android/src/main/java/com/renderrichtext/renderer/MentionRenderer.kt +85 -85
  41. package/android/src/main/java/com/renderrichtext/renderer/ParagraphRenderer.kt +91 -91
  42. package/android/src/main/java/com/renderrichtext/renderer/Renderer.kt +148 -148
  43. package/android/src/main/java/com/renderrichtext/renderer/SoftBreakRenderer.kt +17 -17
  44. package/android/src/main/java/com/renderrichtext/renderer/SpanStyleCache.kt +143 -143
  45. package/android/src/main/java/com/renderrichtext/renderer/StrikethroughRenderer.kt +28 -28
  46. package/android/src/main/java/com/renderrichtext/renderer/StrongRenderer.kt +28 -28
  47. package/android/src/main/java/com/renderrichtext/renderer/TextRenderer.kt +31 -31
  48. package/android/src/main/java/com/renderrichtext/renderer/ThematicBreakRenderer.kt +46 -46
  49. package/android/src/main/java/com/renderrichtext/renderer/UnderlineRenderer.kt +28 -28
  50. package/android/src/main/java/com/renderrichtext/spans/BaseListSpan.kt +164 -164
  51. package/android/src/main/java/com/renderrichtext/spans/BaselineShiftSpan.kt +27 -27
  52. package/android/src/main/java/com/renderrichtext/spans/BlockquoteSpan.kt +302 -302
  53. package/android/src/main/java/com/renderrichtext/spans/CodeBackgroundSpan.kt +205 -205
  54. package/android/src/main/java/com/renderrichtext/spans/CodeBlockSpan.kt +216 -216
  55. package/android/src/main/java/com/renderrichtext/spans/CodeSpan.kt +32 -32
  56. package/android/src/main/java/com/renderrichtext/spans/EmphasisSpan.kt +42 -42
  57. package/android/src/main/java/com/renderrichtext/spans/FadeInSpan.kt +29 -29
  58. package/android/src/main/java/com/renderrichtext/spans/HeadingSpan.kt +38 -38
  59. package/android/src/main/java/com/renderrichtext/spans/HighlightSpan.kt +80 -80
  60. package/android/src/main/java/com/renderrichtext/spans/ImageSpan.kt +481 -481
  61. package/android/src/main/java/com/renderrichtext/spans/InlineBackgroundGeometry.kt +42 -42
  62. package/android/src/main/java/com/renderrichtext/spans/LineHeightSpan.kt +39 -39
  63. package/android/src/main/java/com/renderrichtext/spans/LinkPillIconCache.kt +154 -154
  64. package/android/src/main/java/com/renderrichtext/spans/LinkPillSpan.kt +381 -381
  65. package/android/src/main/java/com/renderrichtext/spans/LinkSpan.kt +68 -68
  66. package/android/src/main/java/com/renderrichtext/spans/ListItemSpacingSpan.kt +33 -33
  67. package/android/src/main/java/com/renderrichtext/spans/ListMarkerAnchorSpan.kt +38 -38
  68. package/android/src/main/java/com/renderrichtext/spans/MarginBottomSpan.kt +76 -76
  69. package/android/src/main/java/com/renderrichtext/spans/MentionChipSpan.kt +132 -132
  70. package/android/src/main/java/com/renderrichtext/spans/MentionSpan.kt +46 -46
  71. package/android/src/main/java/com/renderrichtext/spans/OrderedListSpan.kt +127 -127
  72. package/android/src/main/java/com/renderrichtext/spans/SpoilerSpan.kt +33 -33
  73. package/android/src/main/java/com/renderrichtext/spans/StrikethroughSpan.kt +12 -12
  74. package/android/src/main/java/com/renderrichtext/spans/StrongSpan.kt +39 -39
  75. package/android/src/main/java/com/renderrichtext/spans/TaskListSpan.kt +143 -143
  76. package/android/src/main/java/com/renderrichtext/spans/TextSpan.kt +26 -26
  77. package/android/src/main/java/com/renderrichtext/spans/ThematicBreakSpan.kt +69 -69
  78. package/android/src/main/java/com/renderrichtext/spans/UnorderedListSpan.kt +96 -96
  79. package/android/src/main/java/com/renderrichtext/spoiler/SpoilerStubs.kt +46 -46
  80. package/android/src/main/java/com/renderrichtext/styles/BaseBlockStyle.kt +11 -11
  81. package/android/src/main/java/com/renderrichtext/styles/BlockquoteStyle.kt +83 -83
  82. package/android/src/main/java/com/renderrichtext/styles/CodeBlockStyle.kt +80 -80
  83. package/android/src/main/java/com/renderrichtext/styles/CodeStyle.kt +26 -26
  84. package/android/src/main/java/com/renderrichtext/styles/EmphasisStyle.kt +21 -21
  85. package/android/src/main/java/com/renderrichtext/styles/FactoidStyle.kt +21 -21
  86. package/android/src/main/java/com/renderrichtext/styles/HeadingStyle.kt +33 -33
  87. package/android/src/main/java/com/renderrichtext/styles/HighlightStyle.kt +19 -19
  88. package/android/src/main/java/com/renderrichtext/styles/ImageStyle.kt +29 -29
  89. package/android/src/main/java/com/renderrichtext/styles/InlineImageStyle.kt +17 -17
  90. package/android/src/main/java/com/renderrichtext/styles/InlineMathStyle.kt +17 -17
  91. package/android/src/main/java/com/renderrichtext/styles/LinkPillContent.kt +36 -36
  92. package/android/src/main/java/com/renderrichtext/styles/LinkPillStyle.kt +44 -44
  93. package/android/src/main/java/com/renderrichtext/styles/LinkStyle.kt +23 -23
  94. package/android/src/main/java/com/renderrichtext/styles/LinkVariantEntry.kt +34 -34
  95. package/android/src/main/java/com/renderrichtext/styles/ListStyle.kt +65 -65
  96. package/android/src/main/java/com/renderrichtext/styles/MathStyle.kt +29 -29
  97. package/android/src/main/java/com/renderrichtext/styles/MentionStyle.kt +19 -19
  98. package/android/src/main/java/com/renderrichtext/styles/ParagraphStyle.kt +33 -33
  99. package/android/src/main/java/com/renderrichtext/styles/SpoilerStyle.kt +27 -27
  100. package/android/src/main/java/com/renderrichtext/styles/StrikethroughStyle.kt +17 -17
  101. package/android/src/main/java/com/renderrichtext/styles/StrongStyle.kt +21 -21
  102. package/android/src/main/java/com/renderrichtext/styles/StyleConfig.kt +411 -411
  103. package/android/src/main/java/com/renderrichtext/styles/StyleParser.kt +92 -92
  104. package/android/src/main/java/com/renderrichtext/styles/SubscriptStyle.kt +16 -16
  105. package/android/src/main/java/com/renderrichtext/styles/SuperscriptStyle.kt +16 -16
  106. package/android/src/main/java/com/renderrichtext/styles/TableStyle.kt +75 -75
  107. package/android/src/main/java/com/renderrichtext/styles/TaskListStyle.kt +29 -29
  108. package/android/src/main/java/com/renderrichtext/styles/TextAlignment.kt +32 -32
  109. package/android/src/main/java/com/renderrichtext/styles/ThematicBreakStyle.kt +23 -23
  110. package/android/src/main/java/com/renderrichtext/styles/UnderlineStyle.kt +17 -17
  111. package/android/src/main/java/com/renderrichtext/styles/VideoStyle.kt +31 -31
  112. package/android/src/main/java/com/renderrichtext/utils/common/BorderStyleUtils.kt +86 -86
  113. package/android/src/main/java/com/renderrichtext/utils/common/BreakStrategyUtils.kt +32 -32
  114. package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockHighlighter.kt +36 -36
  115. package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockNode.kt +42 -42
  116. package/android/src/main/java/com/renderrichtext/utils/common/EllipsizeUtils.kt +27 -27
  117. package/android/src/main/java/com/renderrichtext/utils/common/EnrichedTiptapAncestorUtils.kt +10 -10
  118. package/android/src/main/java/com/renderrichtext/utils/common/FeatureFlags.kt +9 -9
  119. package/android/src/main/java/com/renderrichtext/utils/common/LinkRecognitionProps.kt +31 -31
  120. package/android/src/main/java/com/renderrichtext/utils/common/LinkRegexConfig.kt +56 -56
  121. package/android/src/main/java/com/renderrichtext/utils/common/MotionPreferences.kt +18 -18
  122. package/android/src/main/java/com/renderrichtext/utils/common/ReadableMapExtensions.kt +28 -28
  123. package/android/src/main/java/com/renderrichtext/utils/common/layout/RTLUtils.kt +6 -6
  124. package/android/src/main/java/com/renderrichtext/utils/common/serialization/TiptapASTSerializer.kt +368 -368
  125. package/android/src/main/java/com/renderrichtext/utils/text/ImageCache.kt +88 -88
  126. package/android/src/main/java/com/renderrichtext/utils/text/ImageDownloader.kt +203 -203
  127. package/android/src/main/java/com/renderrichtext/utils/text/LocalImageLoader.kt +167 -167
  128. package/android/src/main/java/com/renderrichtext/utils/text/TailFadeInAnimator.kt +84 -84
  129. package/android/src/main/java/com/renderrichtext/utils/text/conversion/HTMLGenerator.kt +1105 -1105
  130. package/android/src/main/java/com/renderrichtext/utils/text/conversion/TiptapExtractor.kt +423 -423
  131. package/android/src/main/java/com/renderrichtext/utils/text/extensions/ParagraphUtils.kt +9 -9
  132. package/android/src/main/java/com/renderrichtext/utils/text/extensions/SpannableExtensions.kt +9 -9
  133. package/android/src/main/java/com/renderrichtext/utils/text/extensions/TextPaintExtensions.kt +76 -76
  134. package/android/src/main/java/com/renderrichtext/utils/text/interaction/CheckboxTouchHelper.kt +61 -61
  135. package/android/src/main/java/com/renderrichtext/utils/text/interaction/TaskListTapUtils.kt +286 -286
  136. package/android/src/main/java/com/renderrichtext/utils/text/span/ApplyLineHeight.kt +39 -39
  137. package/android/src/main/java/com/renderrichtext/utils/text/span/BlockSpacing.kt +142 -142
  138. package/android/src/main/java/com/renderrichtext/utils/text/span/SpanFlags.kt +43 -43
  139. package/android/src/main/java/com/renderrichtext/utils/text/span/WidthAwareSpans.kt +30 -30
  140. package/android/src/main/java/com/renderrichtext/utils/text/view/CodeBlockPressHost.kt +15 -15
  141. package/android/src/main/java/com/renderrichtext/utils/text/view/ImagePressHost.kt +19 -19
  142. package/android/src/main/java/com/renderrichtext/utils/text/view/SelectionActionMode.kt +235 -235
  143. package/android/src/main/java/com/renderrichtext/utils/text/view/TextSelectionColors.kt +48 -48
  144. package/android/src/main/java/com/renderrichtext/utils/text/view/TextViewSetup.kt +119 -119
  145. package/android/src/main/java/com/renderrichtext/views/ContextMenuPopup.kt +217 -217
  146. package/android/src/main/java/com/renderrichtext/views/CopyGlyph.kt +101 -101
  147. package/android/src/main/java/com/renderrichtext/views/WrapGlyph.kt +40 -40
  148. package/android/src/main/jni/CMakeLists.txt +52 -52
  149. package/android/src/main/jni/react/renderer/components/RichTextSpec/ComponentDescriptors.h +30 -30
  150. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextComponentDescriptor.h +29 -29
  151. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.cpp +45 -45
  152. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.h +21 -21
  153. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.cpp +38 -38
  154. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.h +53 -53
  155. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.cpp +9 -9
  156. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.h +24 -24
  157. package/android/src/main/jni/react/renderer/components/RichTextSpec/conversions.h +40 -40
  158. package/cpp/parser/TiptapParser.hpp +13 -13
  159. package/cpp/vendor/nlohmann/json.hpp +24765 -24765
  160. package/ios/EnrichedTiptapText.h +27 -27
  161. package/ios/attachments/ENRMImageAttachment.h +51 -51
  162. package/ios/attachments/ENRMImageAttachment.m +398 -398
  163. package/ios/attachments/ENRMLinkPillAttachment.h +47 -47
  164. package/ios/attachments/ENRMLinkPillAttachment.m +324 -324
  165. package/ios/attachments/ENRMLinkPillIconCache.h +28 -28
  166. package/ios/attachments/ENRMLinkPillIconCache.m +181 -181
  167. package/ios/attachments/ENRMLinkPillText.h +27 -27
  168. package/ios/attachments/ENRMLinkPillText.m +118 -118
  169. package/ios/attachments/ENRMMathInlineAttachment.h +7 -7
  170. package/ios/attachments/ENRMMathInlineAttachment.m +4 -4
  171. package/ios/attachments/ThematicBreakAttachment.h +15 -15
  172. package/ios/attachments/ThematicBreakAttachment.m +33 -33
  173. package/ios/internals/ENRMViewFreeMeasurement.h +208 -208
  174. package/ios/internals/EnrichedTiptapTextComponentDescriptor.h +19 -19
  175. package/ios/internals/EnrichedTiptapTextShadowNode.h +43 -43
  176. package/ios/internals/EnrichedTiptapTextShadowNode.mm +69 -69
  177. package/ios/internals/EnrichedTiptapTextState.h +24 -24
  178. package/ios/internals/MeasurementCache.h +308 -308
  179. package/ios/internals/ShadowMeasurementUtils.h +145 -145
  180. package/ios/parser/ENRMTextLinkAttributes.h +7 -7
  181. package/ios/parser/ENRMTiptapParser.h +33 -33
  182. package/ios/parser/ENRMTiptapParser.mm +73 -73
  183. package/ios/parser/TiptapASTNode.m +52 -52
  184. package/ios/renderer/AttributedRenderer.h +23 -23
  185. package/ios/renderer/AttributedRenderer.m +229 -229
  186. package/ios/renderer/BaseRenderer.h +33 -33
  187. package/ios/renderer/BaseRenderer.m +46 -46
  188. package/ios/renderer/BaselineShiftTextAttributes.h +10 -10
  189. package/ios/renderer/BaselineShiftTextAttributes.m +26 -26
  190. package/ios/renderer/BlankLineRenderer.h +4 -4
  191. package/ios/renderer/BlankLineRenderer.m +51 -51
  192. package/ios/renderer/BlockquoteRenderer.h +6 -6
  193. package/ios/renderer/BlockquoteRenderer.m +258 -258
  194. package/ios/renderer/CodeBlockRenderer.h +9 -9
  195. package/ios/renderer/CodeBlockRenderer.m +99 -99
  196. package/ios/renderer/CodeRenderer.h +9 -9
  197. package/ios/renderer/CodeRenderer.m +69 -69
  198. package/ios/renderer/ENRMBlockquoteTextRenderer.h +25 -25
  199. package/ios/renderer/ENRMBlockquoteTextRenderer.m +48 -48
  200. package/ios/renderer/EmphasisRenderer.h +5 -5
  201. package/ios/renderer/EmphasisRenderer.m +99 -99
  202. package/ios/renderer/HeadingRenderer.h +6 -6
  203. package/ios/renderer/HeadingRenderer.m +102 -102
  204. package/ios/renderer/HighlightRenderer.h +7 -7
  205. package/ios/renderer/HighlightRenderer.m +33 -33
  206. package/ios/renderer/LinkRenderer.h +6 -6
  207. package/ios/renderer/LinkRenderer.m +157 -157
  208. package/ios/renderer/ListItemRenderer.h +24 -24
  209. package/ios/renderer/ListItemRenderer.m +283 -283
  210. package/ios/renderer/ListRenderer.h +13 -13
  211. package/ios/renderer/ListRenderer.m +126 -126
  212. package/ios/renderer/MentionRenderer.h +9 -9
  213. package/ios/renderer/MentionRenderer.m +96 -96
  214. package/ios/renderer/NodeRenderer.h +8 -8
  215. package/ios/renderer/ParagraphRenderer.h +6 -6
  216. package/ios/renderer/ParagraphRenderer.m +85 -85
  217. package/ios/renderer/RenderContext.h +145 -145
  218. package/ios/renderer/RenderContext.m +402 -402
  219. package/ios/renderer/RendererFactory.h +12 -12
  220. package/ios/renderer/StrikethroughRenderer.h +5 -5
  221. package/ios/renderer/StrikethroughRenderer.m +30 -30
  222. package/ios/renderer/StrongRenderer.h +5 -5
  223. package/ios/renderer/StrongRenderer.m +87 -87
  224. package/ios/renderer/TextRenderer.h +6 -6
  225. package/ios/renderer/TextRenderer.m +18 -18
  226. package/ios/renderer/ThematicBreakRenderer.h +4 -4
  227. package/ios/renderer/ThematicBreakRenderer.m +41 -41
  228. package/ios/renderer/UnderlineRenderer.h +4 -4
  229. package/ios/renderer/UnderlineRenderer.m +41 -41
  230. package/ios/styles/ENRMFontSlot.h +12 -12
  231. package/ios/styles/ENRMFontSlot.m +24 -24
  232. package/ios/styles/StyleConfig.h +488 -488
  233. package/ios/styles/StyleConfig.mm +2820 -2820
  234. package/ios/utils/AccessibilityInfo.h +35 -35
  235. package/ios/utils/AccessibilityInfo.m +24 -24
  236. package/ios/utils/BlockquoteBorder.h +26 -26
  237. package/ios/utils/BlockquoteBorder.m +294 -294
  238. package/ios/utils/CodeBackground.h +19 -19
  239. package/ios/utils/CodeBackground.m +196 -196
  240. package/ios/utils/CodeBlockBackground.h +17 -17
  241. package/ios/utils/CodeBlockBackground.m +102 -102
  242. package/ios/utils/ContextMenuUtils.h +76 -76
  243. package/ios/utils/ContextMenuUtils.mm +75 -75
  244. package/ios/utils/ENRMAccessibilityLabels.h +40 -40
  245. package/ios/utils/ENRMAccessibilityLabels.m +5 -5
  246. package/ios/utils/ENRMAsyncRenderCoordinator.h +24 -24
  247. package/ios/utils/ENRMAsyncRenderCoordinator.m +37 -37
  248. package/ios/utils/ENRMAtomicSize.h +65 -65
  249. package/ios/utils/ENRMCodeBlockContent.h +37 -37
  250. package/ios/utils/ENRMCodeBlockContent.mm +89 -89
  251. package/ios/utils/ENRMFeatureFlags.h +25 -25
  252. package/ios/utils/ENRMImageDownloader.h +30 -30
  253. package/ios/utils/ENRMImageDownloader.m +168 -168
  254. package/ios/utils/ENRMLatexErrorCoordinator.h +27 -27
  255. package/ios/utils/ENRMLatexErrorCoordinator.mm +75 -75
  256. package/ios/utils/ENRMLatexErrorReporting.h +13 -13
  257. package/ios/utils/ENRMLinkContextMenus.h +93 -93
  258. package/ios/utils/ENRMLinkContextMenus.m +178 -178
  259. package/ios/utils/ENRMLinkRegexConfig.h +30 -30
  260. package/ios/utils/ENRMLinkRegexConfig.mm +104 -104
  261. package/ios/utils/ENRMLinkRegexProps.h +23 -23
  262. package/ios/utils/ENRMLocalImageLoader.h +35 -35
  263. package/ios/utils/ENRMLocalImageLoader.m +111 -111
  264. package/ios/utils/ENRMMenuAction.h +27 -27
  265. package/ios/utils/ENRMMenuAction.m +33 -33
  266. package/ios/utils/ENRMSpoilerOverlayManager.h +15 -15
  267. package/ios/utils/ENRMSpoilerOverlayManager.m +19 -19
  268. package/ios/utils/ENRMSpoilerTapUtils.h +6 -6
  269. package/ios/utils/ENRMSpoilerTapUtils.m +11 -11
  270. package/ios/utils/ENRMTailFadeInAnimator.h +16 -16
  271. package/ios/utils/ENRMTailFadeInAnimator.m +161 -161
  272. package/ios/utils/ENRMTextHitTest.h +23 -23
  273. package/ios/utils/ENRMTextHitTest.m +20 -20
  274. package/ios/utils/ENRMTextInteractionUtils.h +20 -20
  275. package/ios/utils/ENRMTextInteractionUtils.m +15 -15
  276. package/ios/utils/ENRMTextRenderer.h +38 -38
  277. package/ios/utils/ENRMTextRenderer.m +54 -54
  278. package/ios/utils/ENRMTextViewSetup.h +132 -132
  279. package/ios/utils/ENRMUIKit.h +429 -429
  280. package/ios/utils/EditMenuUtils.h +105 -105
  281. package/ios/utils/EditMenuUtils.m +143 -143
  282. package/ios/utils/FontScaleObserver.h +10 -10
  283. package/ios/utils/FontScaleObserver.m +55 -55
  284. package/ios/utils/FontUtils.h +31 -31
  285. package/ios/utils/FontUtils.m +63 -63
  286. package/ios/utils/HTMLGenerator.h +25 -25
  287. package/ios/utils/HTMLGenerator.m +1041 -1041
  288. package/ios/utils/HeightUpdateUtils.h +25 -25
  289. package/ios/utils/ImageRequestHeaderUtils.h +35 -35
  290. package/ios/utils/LastElementUtils.h +97 -97
  291. package/ios/utils/LinkContextMenuUtils.h +55 -55
  292. package/ios/utils/LinkPillContentUtils.h +46 -46
  293. package/ios/utils/ListMarkerDrawer.h +15 -15
  294. package/ios/utils/ListMarkerDrawer.m +239 -239
  295. package/ios/utils/MentionBackground.h +26 -26
  296. package/ios/utils/MentionBackground.m +86 -86
  297. package/ios/utils/ParagraphStyleUtils.h +87 -87
  298. package/ios/utils/ParagraphStyleUtils.m +484 -484
  299. package/ios/utils/PasteboardUtils.h +48 -48
  300. package/ios/utils/PasteboardUtils.m +166 -166
  301. package/ios/utils/RTFExportUtils.h +24 -24
  302. package/ios/utils/RTFExportUtils.m +299 -299
  303. package/ios/utils/SelectionColorUtils.h +14 -14
  304. package/ios/utils/StylePropsUtils.h +1340 -1340
  305. package/ios/utils/TaskListTapUtils.h +6 -6
  306. package/ios/utils/TaskListTapUtils.m +14 -14
  307. package/ios/utils/TextViewLayoutManager.h +14 -14
  308. package/ios/utils/TiptapAccessibilityElementBuilder.h +43 -43
  309. package/ios/utils/TiptapAccessibilityElementBuilder.m +432 -432
  310. package/ios/utils/TiptapExtractor.h +17 -17
  311. package/ios/utils/TiptapExtractor.m +367 -367
  312. package/ios/views/ENRMTiptapTextView.h +25 -25
  313. package/ios/views/ENRMTiptapTextView.m +247 -247
  314. package/package.json +65 -70
  315. package/react-native.config.js +10 -10
  316. package/src/admonitionDefaults.ts +58 -58
  317. package/src/darkModeTextStyle.ts +99 -99
  318. package/src/headingDefaults.ts +21 -21
  319. package/src/index.tsx +4 -4
  320. package/src/linkVariantUtils.ts +120 -120
  321. package/src/normalizeTiptapStyle.ts +397 -397
  322. package/src/rn-globals.d.ts +1 -1
  323. package/src/styleUtils.ts +107 -107
  324. package/src/types/TiptapStyle.ts +532 -532
  325. package/src/types/TiptapStyleInternal.ts +299 -299
  326. package/src/types/linkPill.ts +9 -9
  327. package/src/utils/isDev.ts +4 -4
@@ -1,481 +1,481 @@
1
- package com.renderrichtext.spans
2
-
3
- import android.content.Context
4
- import android.graphics.Bitmap
5
- import android.graphics.Canvas
6
- import android.graphics.Color
7
- import android.graphics.Paint
8
- import android.graphics.Path
9
- import android.graphics.drawable.Drawable
10
- import android.net.Uri
11
- import android.os.Build
12
- import android.text.Spannable
13
- import android.text.Spanned
14
- import android.text.style.LeadingMarginSpan
15
- import android.util.Log
16
- import android.widget.TextView
17
- import androidx.core.graphics.createBitmap
18
- import androidx.core.graphics.drawable.toDrawable
19
- import androidx.core.graphics.withClip
20
- import androidx.core.graphics.withSave
21
- import com.renderrichtext.EnrichedTiptapText
22
- import com.renderrichtext.styles.StyleConfig
23
- import com.renderrichtext.utils.common.findEnrichedTiptapTextAncestor
24
- import com.renderrichtext.utils.text.ImageCache
25
- import com.renderrichtext.utils.text.ImageDownloader
26
- import com.renderrichtext.utils.text.LocalImageLoader
27
- import java.lang.ref.WeakReference
28
- import java.util.concurrent.Executors
29
- import android.text.style.ImageSpan as AndroidImageSpan
30
- import android.text.style.LineHeightSpan as AndroidLineHeightSpan
31
-
32
- class ImageSpan(
33
- private val context: Context,
34
- val imageUrl: String,
35
- styleConfig: StyleConfig,
36
- val isInline: Boolean = false,
37
- val altText: String = "",
38
- ) : AndroidImageSpan(
39
- Color.TRANSPARENT.toDrawable(),
40
- imageUrl,
41
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ALIGN_CENTER else ALIGN_BASELINE,
42
- ),
43
- AndroidLineHeightSpan {
44
- private var loadedDrawable: Drawable? = null
45
- private val height: Int = if (isInline) styleConfig.inlineImageStyle.size.toInt() else styleConfig.imageStyle.height.toInt()
46
-
47
- private val borderRadiusPx: Int = styleConfig.imageStyle.borderRadius.toInt()
48
- private val maxHeightPx: Int = if (isInline) 0 else styleConfig.imageStyle.maxHeight.toInt()
49
- private val aspectRatio: Float = if (isInline) 0f else styleConfig.imageStyle.aspectRatio
50
- private val resizeMode: String = if (isInline) "" else styleConfig.imageStyle.resizeMode
51
-
52
- // so legacy always implies a fixed height box.
53
- private val legacySizing: Boolean = resizeMode.isEmpty()
54
-
55
- private val dynamicBoxHeight: Boolean = !isInline && (maxHeightPx > 0 || aspectRatio > 0f)
56
-
57
- private var boxHeight: Int =
58
- when {
59
- isInline -> height
60
- maxHeightPx > 0 -> maxHeightPx
61
- else -> height
62
- }
63
-
64
- private val requestHeaders: Map<String, String> = styleConfig.imageRequestHeaders
65
- private val requestKey: String = ImageCache.requestKey(imageUrl, requestHeaders)
66
-
67
- private var cachedWidth: Int = 0
68
- private var viewRef: WeakReference<TextView>? = null
69
- private var sourceDrawable: Drawable? = null
70
- private var onBoxHeightChanged: (() -> Unit)? = null
71
-
72
- private fun intrinsicImageSize(): Pair<Int, Int> {
73
- sourceDrawable?.let { return it.intrinsicWidth to it.intrinsicHeight }
74
- val cached = ImageCache.getOriginal(imageUrl) ?: return 0 to 0
75
- return cached.width to cached.height
76
- }
77
-
78
- // Sizing precedence: aspectRatio > maxHeight > height. Returns the legacy fixed
79
- // height when no new knob is set.
80
- private fun resolveBoxHeight(targetWidth: Int): Int {
81
- if (isInline) return height
82
- if (aspectRatio > 0f && targetWidth > 0) {
83
- return (targetWidth / aspectRatio).toInt().coerceAtLeast(1)
84
- }
85
- if (maxHeightPx > 0) {
86
- val (intrinsicWidth, intrinsicHeight) = intrinsicImageSize()
87
- if (targetWidth > 0 && intrinsicWidth > 0 && intrinsicHeight > 0) {
88
- val fitted = targetWidth.toFloat() * intrinsicHeight / intrinsicWidth
89
- return minOf(maxHeightPx.toFloat(), fitted).toInt().coerceAtLeast(1)
90
- }
91
- return maxHeightPx
92
- }
93
- return height
94
- }
95
-
96
- fun prepareForMeasurement(
97
- text: Spanned,
98
- widthPx: Int,
99
- ) {
100
- if (!dynamicBoxHeight || widthPx <= 0) return
101
- val available = availableWidthIn(text, widthPx)
102
- if (available > 0) boxHeight = resolveBoxHeight(available)
103
- }
104
-
105
- init {
106
- loadImage()
107
- }
108
-
109
- private fun loadImage() {
110
- val scheme = Uri.parse(imageUrl).scheme?.lowercase()
111
- if (scheme == "http" || scheme == "https") {
112
- ImageDownloader.download(context, imageUrl, requestHeaders) { bitmap ->
113
- if (bitmap != null) {
114
- sourceDrawable = bitmap.toDrawable(context.resources)
115
- wrapAndAssignDrawable()
116
- }
117
- }
118
- } else {
119
- val cached = ImageCache.getOriginal(imageUrl)
120
- val bitmap = cached ?: LocalImageLoader.load(context, imageUrl)
121
- if (bitmap != null) {
122
- if (cached == null) ImageCache.putOriginal(imageUrl, bitmap)
123
- sourceDrawable = bitmap.toDrawable(context.resources)
124
- wrapAndAssignDrawable()
125
- }
126
- }
127
- }
128
-
129
- private fun wrapAndAssignDrawable() {
130
- val base = sourceDrawable ?: return
131
- val targetWidth =
132
- if (isInline) {
133
- height
134
- } else {
135
- val available = viewRef?.get()?.let { getAvailableWidth(it) } ?: cachedWidth
136
- available.coerceAtLeast(0)
137
- }
138
-
139
- boxHeight = resolveBoxHeight(targetWidth)
140
-
141
- val cachedBitmap = ImageCache.getProcessed(requestKey, targetWidth, boxHeight, borderRadiusPx, resizeMode)
142
-
143
- if (cachedBitmap != null) {
144
- loadedDrawable =
145
- cachedBitmap.toDrawable(context.resources).apply {
146
- setBounds(0, 0, targetWidth, boxHeight)
147
- }
148
- } else {
149
- loadedDrawable =
150
- ScaledImageDrawable(
151
- imageDrawable = base,
152
- targetWidth = targetWidth,
153
- targetHeight = boxHeight,
154
- borderRadius = borderRadiusPx,
155
- isBlockImage = !isInline,
156
- resizeMode = resizeMode,
157
- legacySizing = legacySizing,
158
- cacheKey = CacheKey(requestKey, targetWidth, boxHeight, borderRadiusPx, resizeMode),
159
- )
160
- }
161
- requestReflow()
162
- }
163
-
164
- private fun requestReflow() {
165
- val view = viewRef?.get() ?: return
166
- val text = view.text
167
- if (text is Spannable) {
168
- val start = text.getSpanStart(this)
169
- val end = text.getSpanEnd(this)
170
- if (start != -1 && end != -1) {
171
- text.setSpan(this, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
172
- }
173
- } else {
174
- view.invalidate()
175
- view.requestLayout()
176
- }
177
- notifyBoxHeightMayHaveChanged(view)
178
- }
179
-
180
- // With maxHeight/aspectRatio the box height can change after the image loads
181
- // (maxHeight fitted, aspectRatio at a late width). The React container height
182
- // was measured with the pre-load value, so ask the owning component to
183
- // re-measure; it only propagates when the stored height actually changed.
184
- private fun notifyBoxHeightMayHaveChanged(view: TextView) {
185
- if (!dynamicBoxHeight) return
186
- // A self-measuring host (e.g. a table cell) sizes the image box itself and drives
187
- // its own re-layout, so bubbling up to the component would churn it and orphan this
188
- // freshly registered view. Let the host re-measure locally instead.
189
- onBoxHeightChanged?.let {
190
- it()
191
- return
192
- }
193
- if (view is EnrichedTiptapText) {
194
- view.layoutManager.invalidateLayout()
195
- return
196
- }
197
- view.findEnrichedTiptapTextAncestor()?.layoutManager?.invalidateLayout()
198
- }
199
-
200
- fun registerTextView(
201
- view: TextView,
202
- onBoxHeightChanged: (() -> Unit)? = null,
203
- ) {
204
- this.onBoxHeightChanged = onBoxHeightChanged
205
- viewRef = WeakReference(view)
206
- if (!isInline) {
207
- val availableWidth = getAvailableWidth(view)
208
- if (availableWidth > 0) {
209
- updateWidthAndRecreate(availableWidth)
210
- }
211
- view.post {
212
- val postWidth = getAvailableWidth(view)
213
- if (postWidth != cachedWidth) updateWidthAndRecreate(postWidth)
214
- }
215
- }
216
- }
217
-
218
- private fun updateWidthAndRecreate(newWidth: Int) {
219
- if (newWidth <= 0 || cachedWidth == newWidth) return
220
- cachedWidth = newWidth
221
- if (sourceDrawable != null) {
222
- wrapAndAssignDrawable()
223
- return
224
- }
225
- if (dynamicBoxHeight) {
226
- val newBoxHeight = resolveBoxHeight(newWidth)
227
- if (newBoxHeight != boxHeight) {
228
- boxHeight = newBoxHeight
229
- requestReflow()
230
- }
231
- }
232
- }
233
-
234
- private fun getAvailableWidth(view: TextView): Int {
235
- val baseWidth = (view.layout?.width ?: view.width).coerceAtLeast(0)
236
- val text = view.text as? Spanned ?: return baseWidth
237
- return availableWidthIn(text, baseWidth)
238
- }
239
-
240
- private fun availableWidthIn(
241
- text: Spanned,
242
- baseWidth: Int,
243
- ): Int {
244
- val start = text.getSpanStart(this).takeIf { it >= 0 } ?: return baseWidth
245
- val end = text.getSpanEnd(this).coerceAtLeast(start)
246
- // Subtract leading margins (list bullet/indent, blockquote bar) that consume
247
- // horizontal space on the image's line. first=true is correct because a block
248
- // image sits on its own line — the "first line" from the layout's perspective.
249
- val totalMargin =
250
- text
251
- .getSpans(start, end, LeadingMarginSpan::class.java)
252
- .sumOf { it.getLeadingMargin(true) }
253
- return (baseWidth - totalMargin).coerceAtLeast(0)
254
- }
255
-
256
- override fun getDrawable(): Drawable {
257
- val drawable = loadedDrawable ?: transparentDrawable
258
- if (drawable !is ScaledImageDrawable) {
259
- val drawableWidth = if (isInline) height else cachedWidth.takeIf { it > 0 } ?: drawable.intrinsicWidth
260
- drawable.setBounds(0, 0, drawableWidth.coerceAtLeast(0), boxHeight.coerceAtLeast(0))
261
- }
262
- return drawable
263
- }
264
-
265
- override fun getSize(
266
- paint: Paint,
267
- text: CharSequence?,
268
- start: Int,
269
- end: Int,
270
- fm: Paint.FontMetricsInt?,
271
- ): Int = getDrawable().bounds.right
272
-
273
- override fun chooseHeight(
274
- text: CharSequence?,
275
- start: Int,
276
- end: Int,
277
- spanstartv: Int,
278
- lineHeight: Int,
279
- fm: Paint.FontMetricsInt?,
280
- ) {
281
- if (fm == null || isInline) return
282
- val currentLineHeight = fm.descent - fm.ascent
283
- if (boxHeight > currentLineHeight) {
284
- val extraHeight = boxHeight - currentLineHeight
285
- fm.descent += extraHeight
286
- fm.bottom += extraHeight
287
- }
288
- }
289
-
290
- override fun draw(
291
- canvas: Canvas,
292
- text: CharSequence?,
293
- start: Int,
294
- end: Int,
295
- x: Float,
296
- top: Int,
297
- y: Int,
298
- bottom: Int,
299
- paint: Paint,
300
- ) {
301
- val drawable = getDrawable()
302
- canvas.withSave {
303
- if (isInline) {
304
- val imageHeight = drawable.bounds.height()
305
- translate(x, (y - imageHeight + (imageHeight * 0.1f)))
306
- } else {
307
- translate(x, top.toFloat())
308
- }
309
- drawable.draw(this)
310
- }
311
- }
312
-
313
- private data class CacheKey(
314
- val url: String,
315
- val width: Int,
316
- val height: Int,
317
- val borderRadius: Int,
318
- val resizeMode: String,
319
- )
320
-
321
- private class ScaledImageDrawable(
322
- private val imageDrawable: Drawable,
323
- private val targetWidth: Int,
324
- private val targetHeight: Int,
325
- private val borderRadius: Int,
326
- isBlockImage: Boolean,
327
- private val resizeMode: String = "",
328
- private val legacySizing: Boolean = true,
329
- private val cacheKey: CacheKey? = null,
330
- ) : Drawable() {
331
- private val clipPath: Path?
332
- private var hasCached = false
333
-
334
- init {
335
- setBounds(0, 0, targetWidth, targetHeight)
336
- val intrinsicWidth = imageDrawable.intrinsicWidth
337
- val intrinsicHeight = imageDrawable.intrinsicHeight
338
-
339
- // New sizing needs a clip to the box so cropping
340
- // modes don't spill over neighbouring lines.
341
- val clipToBox = isBlockImage && !legacySizing
342
-
343
- val (scaledWidth, scaledHeight) =
344
- if (intrinsicWidth > 0 && intrinsicHeight > 0) {
345
- when {
346
- !isBlockImage -> {
347
- val scale = minOf(targetWidth.toFloat() / intrinsicWidth, targetHeight.toFloat() / intrinsicHeight)
348
- (intrinsicWidth * scale).toInt() to (intrinsicHeight * scale).toInt()
349
- }
350
-
351
- legacySizing -> {
352
- val scale = targetWidth.toFloat() / intrinsicWidth
353
- targetWidth to (intrinsicHeight * scale).toInt()
354
- }
355
-
356
- else -> {
357
- resizeModeSize(intrinsicWidth, intrinsicHeight)
358
- }
359
- }
360
- } else {
361
- targetWidth to targetHeight
362
- }
363
-
364
- val left = (targetWidth - scaledWidth) / 2
365
- val top = (targetHeight - scaledHeight) / 2
366
- imageDrawable.setBounds(left, top, left + scaledWidth, top + scaledHeight)
367
-
368
- val clipLeft = maxOf(0, left).toFloat()
369
- val clipTop = maxOf(0, top).toFloat()
370
- val clipRight = minOf(targetWidth, left + scaledWidth).toFloat()
371
- val clipBottom = minOf(targetHeight, top + scaledHeight).toFloat()
372
- clipPath =
373
- when {
374
- borderRadius > 0 -> {
375
- Path().apply {
376
- addRoundRect(
377
- clipLeft,
378
- clipTop,
379
- clipRight,
380
- clipBottom,
381
- borderRadius.toFloat(),
382
- borderRadius.toFloat(),
383
- Path.Direction.CW,
384
- )
385
- }
386
- }
387
-
388
- clipToBox -> {
389
- Path().apply {
390
- addRect(clipLeft, clipTop, clipRight, clipBottom, Path.Direction.CW)
391
- }
392
- }
393
-
394
- else -> {
395
- null
396
- }
397
- }
398
- }
399
-
400
- private fun resizeModeSize(
401
- intrinsicWidth: Int,
402
- intrinsicHeight: Int,
403
- ): Pair<Int, Int> {
404
- if (resizeMode == "stretch") return targetWidth to targetHeight
405
- val widthScale = targetWidth.toFloat() / intrinsicWidth
406
- val heightScale = targetHeight.toFloat() / intrinsicHeight
407
- val scale =
408
- when (resizeMode) {
409
- "contain" -> minOf(widthScale, heightScale)
410
- "center" -> minOf(1f, minOf(widthScale, heightScale))
411
- "none" -> 1f
412
- else -> maxOf(widthScale, heightScale) // cover (default)
413
- }
414
- return (intrinsicWidth * scale).toInt() to (intrinsicHeight * scale).toInt()
415
- }
416
-
417
- override fun draw(canvas: Canvas) {
418
- if (clipPath != null) {
419
- canvas.withSave {
420
- clipPath(clipPath)
421
- imageDrawable.draw(canvas)
422
- }
423
- } else {
424
- imageDrawable.draw(canvas)
425
- }
426
- scheduleCacheBitmap()
427
- }
428
-
429
- private fun scheduleCacheBitmap() {
430
- if (hasCached || cacheKey == null || targetWidth <= 0 || targetHeight <= 0) return
431
- hasCached = true
432
- val cachedKey = cacheKey
433
- val width = targetWidth
434
- val height = targetHeight
435
- val path = clipPath
436
- val source = imageDrawable
437
- cacheExecutor.execute {
438
- try {
439
- val bitmap = createBitmap(width, height)
440
- val offscreen = Canvas(bitmap)
441
- if (path != null) {
442
- offscreen.withClip(path) { source.draw(this) }
443
- } else {
444
- source.draw(offscreen)
445
- }
446
- ImageCache.putProcessed(
447
- cachedKey.url,
448
- cachedKey.width,
449
- cachedKey.height,
450
- cachedKey.borderRadius,
451
- cachedKey.resizeMode,
452
- bitmap,
453
- )
454
- } catch (_: OutOfMemoryError) {
455
- Log.e("ScaledImageDrawable", "OOM caching bitmap ${width}x$height")
456
- }
457
- }
458
- }
459
-
460
- override fun setAlpha(alpha: Int) {
461
- imageDrawable.alpha = alpha
462
- }
463
-
464
- override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {
465
- imageDrawable.colorFilter = colorFilter
466
- }
467
-
468
- @Suppress("DEPRECATION")
469
- @Deprecated("Deprecated in Java")
470
- override fun getOpacity(): Int = imageDrawable.opacity
471
-
472
- override fun getIntrinsicWidth(): Int = targetWidth
473
-
474
- override fun getIntrinsicHeight(): Int = targetHeight
475
- }
476
-
477
- companion object {
478
- private val transparentDrawable by lazy { Color.TRANSPARENT.toDrawable() }
479
- private val cacheExecutor = Executors.newSingleThreadExecutor()
480
- }
481
- }
1
+ package com.renderrichtext.spans
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Paint
8
+ import android.graphics.Path
9
+ import android.graphics.drawable.Drawable
10
+ import android.net.Uri
11
+ import android.os.Build
12
+ import android.text.Spannable
13
+ import android.text.Spanned
14
+ import android.text.style.LeadingMarginSpan
15
+ import android.util.Log
16
+ import android.widget.TextView
17
+ import androidx.core.graphics.createBitmap
18
+ import androidx.core.graphics.drawable.toDrawable
19
+ import androidx.core.graphics.withClip
20
+ import androidx.core.graphics.withSave
21
+ import com.renderrichtext.EnrichedTiptapText
22
+ import com.renderrichtext.styles.StyleConfig
23
+ import com.renderrichtext.utils.common.findEnrichedTiptapTextAncestor
24
+ import com.renderrichtext.utils.text.ImageCache
25
+ import com.renderrichtext.utils.text.ImageDownloader
26
+ import com.renderrichtext.utils.text.LocalImageLoader
27
+ import java.lang.ref.WeakReference
28
+ import java.util.concurrent.Executors
29
+ import android.text.style.ImageSpan as AndroidImageSpan
30
+ import android.text.style.LineHeightSpan as AndroidLineHeightSpan
31
+
32
+ class ImageSpan(
33
+ private val context: Context,
34
+ val imageUrl: String,
35
+ styleConfig: StyleConfig,
36
+ val isInline: Boolean = false,
37
+ val altText: String = "",
38
+ ) : AndroidImageSpan(
39
+ Color.TRANSPARENT.toDrawable(),
40
+ imageUrl,
41
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ALIGN_CENTER else ALIGN_BASELINE,
42
+ ),
43
+ AndroidLineHeightSpan {
44
+ private var loadedDrawable: Drawable? = null
45
+ private val height: Int = if (isInline) styleConfig.inlineImageStyle.size.toInt() else styleConfig.imageStyle.height.toInt()
46
+
47
+ private val borderRadiusPx: Int = styleConfig.imageStyle.borderRadius.toInt()
48
+ private val maxHeightPx: Int = if (isInline) 0 else styleConfig.imageStyle.maxHeight.toInt()
49
+ private val aspectRatio: Float = if (isInline) 0f else styleConfig.imageStyle.aspectRatio
50
+ private val resizeMode: String = if (isInline) "" else styleConfig.imageStyle.resizeMode
51
+
52
+ // so legacy always implies a fixed height box.
53
+ private val legacySizing: Boolean = resizeMode.isEmpty()
54
+
55
+ private val dynamicBoxHeight: Boolean = !isInline && (maxHeightPx > 0 || aspectRatio > 0f)
56
+
57
+ private var boxHeight: Int =
58
+ when {
59
+ isInline -> height
60
+ maxHeightPx > 0 -> maxHeightPx
61
+ else -> height
62
+ }
63
+
64
+ private val requestHeaders: Map<String, String> = styleConfig.imageRequestHeaders
65
+ private val requestKey: String = ImageCache.requestKey(imageUrl, requestHeaders)
66
+
67
+ private var cachedWidth: Int = 0
68
+ private var viewRef: WeakReference<TextView>? = null
69
+ private var sourceDrawable: Drawable? = null
70
+ private var onBoxHeightChanged: (() -> Unit)? = null
71
+
72
+ private fun intrinsicImageSize(): Pair<Int, Int> {
73
+ sourceDrawable?.let { return it.intrinsicWidth to it.intrinsicHeight }
74
+ val cached = ImageCache.getOriginal(imageUrl) ?: return 0 to 0
75
+ return cached.width to cached.height
76
+ }
77
+
78
+ // Sizing precedence: aspectRatio > maxHeight > height. Returns the legacy fixed
79
+ // height when no new knob is set.
80
+ private fun resolveBoxHeight(targetWidth: Int): Int {
81
+ if (isInline) return height
82
+ if (aspectRatio > 0f && targetWidth > 0) {
83
+ return (targetWidth / aspectRatio).toInt().coerceAtLeast(1)
84
+ }
85
+ if (maxHeightPx > 0) {
86
+ val (intrinsicWidth, intrinsicHeight) = intrinsicImageSize()
87
+ if (targetWidth > 0 && intrinsicWidth > 0 && intrinsicHeight > 0) {
88
+ val fitted = targetWidth.toFloat() * intrinsicHeight / intrinsicWidth
89
+ return minOf(maxHeightPx.toFloat(), fitted).toInt().coerceAtLeast(1)
90
+ }
91
+ return maxHeightPx
92
+ }
93
+ return height
94
+ }
95
+
96
+ fun prepareForMeasurement(
97
+ text: Spanned,
98
+ widthPx: Int,
99
+ ) {
100
+ if (!dynamicBoxHeight || widthPx <= 0) return
101
+ val available = availableWidthIn(text, widthPx)
102
+ if (available > 0) boxHeight = resolveBoxHeight(available)
103
+ }
104
+
105
+ init {
106
+ loadImage()
107
+ }
108
+
109
+ private fun loadImage() {
110
+ val scheme = Uri.parse(imageUrl).scheme?.lowercase()
111
+ if (scheme == "http" || scheme == "https") {
112
+ ImageDownloader.download(context, imageUrl, requestHeaders) { bitmap ->
113
+ if (bitmap != null) {
114
+ sourceDrawable = bitmap.toDrawable(context.resources)
115
+ wrapAndAssignDrawable()
116
+ }
117
+ }
118
+ } else {
119
+ val cached = ImageCache.getOriginal(imageUrl)
120
+ val bitmap = cached ?: LocalImageLoader.load(context, imageUrl)
121
+ if (bitmap != null) {
122
+ if (cached == null) ImageCache.putOriginal(imageUrl, bitmap)
123
+ sourceDrawable = bitmap.toDrawable(context.resources)
124
+ wrapAndAssignDrawable()
125
+ }
126
+ }
127
+ }
128
+
129
+ private fun wrapAndAssignDrawable() {
130
+ val base = sourceDrawable ?: return
131
+ val targetWidth =
132
+ if (isInline) {
133
+ height
134
+ } else {
135
+ val available = viewRef?.get()?.let { getAvailableWidth(it) } ?: cachedWidth
136
+ available.coerceAtLeast(0)
137
+ }
138
+
139
+ boxHeight = resolveBoxHeight(targetWidth)
140
+
141
+ val cachedBitmap = ImageCache.getProcessed(requestKey, targetWidth, boxHeight, borderRadiusPx, resizeMode)
142
+
143
+ if (cachedBitmap != null) {
144
+ loadedDrawable =
145
+ cachedBitmap.toDrawable(context.resources).apply {
146
+ setBounds(0, 0, targetWidth, boxHeight)
147
+ }
148
+ } else {
149
+ loadedDrawable =
150
+ ScaledImageDrawable(
151
+ imageDrawable = base,
152
+ targetWidth = targetWidth,
153
+ targetHeight = boxHeight,
154
+ borderRadius = borderRadiusPx,
155
+ isBlockImage = !isInline,
156
+ resizeMode = resizeMode,
157
+ legacySizing = legacySizing,
158
+ cacheKey = CacheKey(requestKey, targetWidth, boxHeight, borderRadiusPx, resizeMode),
159
+ )
160
+ }
161
+ requestReflow()
162
+ }
163
+
164
+ private fun requestReflow() {
165
+ val view = viewRef?.get() ?: return
166
+ val text = view.text
167
+ if (text is Spannable) {
168
+ val start = text.getSpanStart(this)
169
+ val end = text.getSpanEnd(this)
170
+ if (start != -1 && end != -1) {
171
+ text.setSpan(this, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
172
+ }
173
+ } else {
174
+ view.invalidate()
175
+ view.requestLayout()
176
+ }
177
+ notifyBoxHeightMayHaveChanged(view)
178
+ }
179
+
180
+ // With maxHeight/aspectRatio the box height can change after the image loads
181
+ // (maxHeight fitted, aspectRatio at a late width). The React container height
182
+ // was measured with the pre-load value, so ask the owning component to
183
+ // re-measure; it only propagates when the stored height actually changed.
184
+ private fun notifyBoxHeightMayHaveChanged(view: TextView) {
185
+ if (!dynamicBoxHeight) return
186
+ // A self-measuring host (e.g. a table cell) sizes the image box itself and drives
187
+ // its own re-layout, so bubbling up to the component would churn it and orphan this
188
+ // freshly registered view. Let the host re-measure locally instead.
189
+ onBoxHeightChanged?.let {
190
+ it()
191
+ return
192
+ }
193
+ if (view is EnrichedTiptapText) {
194
+ view.layoutManager.invalidateLayout()
195
+ return
196
+ }
197
+ view.findEnrichedTiptapTextAncestor()?.layoutManager?.invalidateLayout()
198
+ }
199
+
200
+ fun registerTextView(
201
+ view: TextView,
202
+ onBoxHeightChanged: (() -> Unit)? = null,
203
+ ) {
204
+ this.onBoxHeightChanged = onBoxHeightChanged
205
+ viewRef = WeakReference(view)
206
+ if (!isInline) {
207
+ val availableWidth = getAvailableWidth(view)
208
+ if (availableWidth > 0) {
209
+ updateWidthAndRecreate(availableWidth)
210
+ }
211
+ view.post {
212
+ val postWidth = getAvailableWidth(view)
213
+ if (postWidth != cachedWidth) updateWidthAndRecreate(postWidth)
214
+ }
215
+ }
216
+ }
217
+
218
+ private fun updateWidthAndRecreate(newWidth: Int) {
219
+ if (newWidth <= 0 || cachedWidth == newWidth) return
220
+ cachedWidth = newWidth
221
+ if (sourceDrawable != null) {
222
+ wrapAndAssignDrawable()
223
+ return
224
+ }
225
+ if (dynamicBoxHeight) {
226
+ val newBoxHeight = resolveBoxHeight(newWidth)
227
+ if (newBoxHeight != boxHeight) {
228
+ boxHeight = newBoxHeight
229
+ requestReflow()
230
+ }
231
+ }
232
+ }
233
+
234
+ private fun getAvailableWidth(view: TextView): Int {
235
+ val baseWidth = (view.layout?.width ?: view.width).coerceAtLeast(0)
236
+ val text = view.text as? Spanned ?: return baseWidth
237
+ return availableWidthIn(text, baseWidth)
238
+ }
239
+
240
+ private fun availableWidthIn(
241
+ text: Spanned,
242
+ baseWidth: Int,
243
+ ): Int {
244
+ val start = text.getSpanStart(this).takeIf { it >= 0 } ?: return baseWidth
245
+ val end = text.getSpanEnd(this).coerceAtLeast(start)
246
+ // Subtract leading margins (list bullet/indent, blockquote bar) that consume
247
+ // horizontal space on the image's line. first=true is correct because a block
248
+ // image sits on its own line — the "first line" from the layout's perspective.
249
+ val totalMargin =
250
+ text
251
+ .getSpans(start, end, LeadingMarginSpan::class.java)
252
+ .sumOf { it.getLeadingMargin(true) }
253
+ return (baseWidth - totalMargin).coerceAtLeast(0)
254
+ }
255
+
256
+ override fun getDrawable(): Drawable {
257
+ val drawable = loadedDrawable ?: transparentDrawable
258
+ if (drawable !is ScaledImageDrawable) {
259
+ val drawableWidth = if (isInline) height else cachedWidth.takeIf { it > 0 } ?: drawable.intrinsicWidth
260
+ drawable.setBounds(0, 0, drawableWidth.coerceAtLeast(0), boxHeight.coerceAtLeast(0))
261
+ }
262
+ return drawable
263
+ }
264
+
265
+ override fun getSize(
266
+ paint: Paint,
267
+ text: CharSequence?,
268
+ start: Int,
269
+ end: Int,
270
+ fm: Paint.FontMetricsInt?,
271
+ ): Int = getDrawable().bounds.right
272
+
273
+ override fun chooseHeight(
274
+ text: CharSequence?,
275
+ start: Int,
276
+ end: Int,
277
+ spanstartv: Int,
278
+ lineHeight: Int,
279
+ fm: Paint.FontMetricsInt?,
280
+ ) {
281
+ if (fm == null || isInline) return
282
+ val currentLineHeight = fm.descent - fm.ascent
283
+ if (boxHeight > currentLineHeight) {
284
+ val extraHeight = boxHeight - currentLineHeight
285
+ fm.descent += extraHeight
286
+ fm.bottom += extraHeight
287
+ }
288
+ }
289
+
290
+ override fun draw(
291
+ canvas: Canvas,
292
+ text: CharSequence?,
293
+ start: Int,
294
+ end: Int,
295
+ x: Float,
296
+ top: Int,
297
+ y: Int,
298
+ bottom: Int,
299
+ paint: Paint,
300
+ ) {
301
+ val drawable = getDrawable()
302
+ canvas.withSave {
303
+ if (isInline) {
304
+ val imageHeight = drawable.bounds.height()
305
+ translate(x, (y - imageHeight + (imageHeight * 0.1f)))
306
+ } else {
307
+ translate(x, top.toFloat())
308
+ }
309
+ drawable.draw(this)
310
+ }
311
+ }
312
+
313
+ private data class CacheKey(
314
+ val url: String,
315
+ val width: Int,
316
+ val height: Int,
317
+ val borderRadius: Int,
318
+ val resizeMode: String,
319
+ )
320
+
321
+ private class ScaledImageDrawable(
322
+ private val imageDrawable: Drawable,
323
+ private val targetWidth: Int,
324
+ private val targetHeight: Int,
325
+ private val borderRadius: Int,
326
+ isBlockImage: Boolean,
327
+ private val resizeMode: String = "",
328
+ private val legacySizing: Boolean = true,
329
+ private val cacheKey: CacheKey? = null,
330
+ ) : Drawable() {
331
+ private val clipPath: Path?
332
+ private var hasCached = false
333
+
334
+ init {
335
+ setBounds(0, 0, targetWidth, targetHeight)
336
+ val intrinsicWidth = imageDrawable.intrinsicWidth
337
+ val intrinsicHeight = imageDrawable.intrinsicHeight
338
+
339
+ // New sizing needs a clip to the box so cropping
340
+ // modes don't spill over neighbouring lines.
341
+ val clipToBox = isBlockImage && !legacySizing
342
+
343
+ val (scaledWidth, scaledHeight) =
344
+ if (intrinsicWidth > 0 && intrinsicHeight > 0) {
345
+ when {
346
+ !isBlockImage -> {
347
+ val scale = minOf(targetWidth.toFloat() / intrinsicWidth, targetHeight.toFloat() / intrinsicHeight)
348
+ (intrinsicWidth * scale).toInt() to (intrinsicHeight * scale).toInt()
349
+ }
350
+
351
+ legacySizing -> {
352
+ val scale = targetWidth.toFloat() / intrinsicWidth
353
+ targetWidth to (intrinsicHeight * scale).toInt()
354
+ }
355
+
356
+ else -> {
357
+ resizeModeSize(intrinsicWidth, intrinsicHeight)
358
+ }
359
+ }
360
+ } else {
361
+ targetWidth to targetHeight
362
+ }
363
+
364
+ val left = (targetWidth - scaledWidth) / 2
365
+ val top = (targetHeight - scaledHeight) / 2
366
+ imageDrawable.setBounds(left, top, left + scaledWidth, top + scaledHeight)
367
+
368
+ val clipLeft = maxOf(0, left).toFloat()
369
+ val clipTop = maxOf(0, top).toFloat()
370
+ val clipRight = minOf(targetWidth, left + scaledWidth).toFloat()
371
+ val clipBottom = minOf(targetHeight, top + scaledHeight).toFloat()
372
+ clipPath =
373
+ when {
374
+ borderRadius > 0 -> {
375
+ Path().apply {
376
+ addRoundRect(
377
+ clipLeft,
378
+ clipTop,
379
+ clipRight,
380
+ clipBottom,
381
+ borderRadius.toFloat(),
382
+ borderRadius.toFloat(),
383
+ Path.Direction.CW,
384
+ )
385
+ }
386
+ }
387
+
388
+ clipToBox -> {
389
+ Path().apply {
390
+ addRect(clipLeft, clipTop, clipRight, clipBottom, Path.Direction.CW)
391
+ }
392
+ }
393
+
394
+ else -> {
395
+ null
396
+ }
397
+ }
398
+ }
399
+
400
+ private fun resizeModeSize(
401
+ intrinsicWidth: Int,
402
+ intrinsicHeight: Int,
403
+ ): Pair<Int, Int> {
404
+ if (resizeMode == "stretch") return targetWidth to targetHeight
405
+ val widthScale = targetWidth.toFloat() / intrinsicWidth
406
+ val heightScale = targetHeight.toFloat() / intrinsicHeight
407
+ val scale =
408
+ when (resizeMode) {
409
+ "contain" -> minOf(widthScale, heightScale)
410
+ "center" -> minOf(1f, minOf(widthScale, heightScale))
411
+ "none" -> 1f
412
+ else -> maxOf(widthScale, heightScale) // cover (default)
413
+ }
414
+ return (intrinsicWidth * scale).toInt() to (intrinsicHeight * scale).toInt()
415
+ }
416
+
417
+ override fun draw(canvas: Canvas) {
418
+ if (clipPath != null) {
419
+ canvas.withSave {
420
+ clipPath(clipPath)
421
+ imageDrawable.draw(canvas)
422
+ }
423
+ } else {
424
+ imageDrawable.draw(canvas)
425
+ }
426
+ scheduleCacheBitmap()
427
+ }
428
+
429
+ private fun scheduleCacheBitmap() {
430
+ if (hasCached || cacheKey == null || targetWidth <= 0 || targetHeight <= 0) return
431
+ hasCached = true
432
+ val cachedKey = cacheKey
433
+ val width = targetWidth
434
+ val height = targetHeight
435
+ val path = clipPath
436
+ val source = imageDrawable
437
+ cacheExecutor.execute {
438
+ try {
439
+ val bitmap = createBitmap(width, height)
440
+ val offscreen = Canvas(bitmap)
441
+ if (path != null) {
442
+ offscreen.withClip(path) { source.draw(this) }
443
+ } else {
444
+ source.draw(offscreen)
445
+ }
446
+ ImageCache.putProcessed(
447
+ cachedKey.url,
448
+ cachedKey.width,
449
+ cachedKey.height,
450
+ cachedKey.borderRadius,
451
+ cachedKey.resizeMode,
452
+ bitmap,
453
+ )
454
+ } catch (_: OutOfMemoryError) {
455
+ Log.e("ScaledImageDrawable", "OOM caching bitmap ${width}x$height")
456
+ }
457
+ }
458
+ }
459
+
460
+ override fun setAlpha(alpha: Int) {
461
+ imageDrawable.alpha = alpha
462
+ }
463
+
464
+ override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {
465
+ imageDrawable.colorFilter = colorFilter
466
+ }
467
+
468
+ @Suppress("DEPRECATION")
469
+ @Deprecated("Deprecated in Java")
470
+ override fun getOpacity(): Int = imageDrawable.opacity
471
+
472
+ override fun getIntrinsicWidth(): Int = targetWidth
473
+
474
+ override fun getIntrinsicHeight(): Int = targetHeight
475
+ }
476
+
477
+ companion object {
478
+ private val transparentDrawable by lazy { Color.TRANSPARENT.toDrawable() }
479
+ private val cacheExecutor = Executors.newSingleThreadExecutor()
480
+ }
481
+ }