@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,381 +1,381 @@
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.ColorFilter
7
- import android.graphics.Paint
8
- import android.graphics.PorterDuff
9
- import android.graphics.PorterDuffColorFilter
10
- import android.graphics.RectF
11
- import android.graphics.Typeface
12
- import android.os.Build
13
- import android.os.Looper
14
- import android.text.Spannable
15
- import android.text.Spanned
16
- import android.text.TextPaint
17
- import android.text.TextUtils
18
- import android.text.style.LeadingMarginSpan
19
- import android.text.style.ReplacementSpan
20
- import android.view.View
21
- import android.widget.TextView
22
- import com.renderrichtext.EnrichedTiptapText
23
- import com.renderrichtext.spoiler.isConcealedBySpoiler
24
- import com.renderrichtext.styles.LinkPillContent
25
- import com.renderrichtext.styles.LinkPillStyle
26
- import com.renderrichtext.styles.LinkVariantEntry
27
- import com.renderrichtext.utils.common.findEnrichedTiptapTextAncestor
28
- import java.lang.ref.WeakReference
29
- import kotlin.math.ceil
30
- import kotlin.math.max
31
- import kotlin.math.min
32
-
33
- /**
34
- * Draws an atomic visual over the original link text, without changing stored characters.
35
- *
36
- * Presentation comes from the [variant]; [content] is what this particular link shows
37
- * and wins over the variant's label and icon, which win over the link text.
38
- */
39
- class LinkPillSpan(
40
- private val variant: LinkVariantEntry,
41
- private val typeface: Typeface,
42
- private val fontSize: Float,
43
- originalLinkText: String,
44
- context: Context,
45
- content: LinkPillContent? = null,
46
- requestHeaders: Map<String, String> = emptyMap(),
47
- // False when the link style names a font family, which then wins over the run's font.
48
- private val followsRunTypeface: Boolean = false,
49
- ) : ReplacementSpan() {
50
- private val pill = variant.pill ?: LinkPillStyle()
51
- private val resolved = (content ?: LinkPillContent()).orElse(pill.content)
52
- private val label =
53
- resolved.label
54
- .ifEmpty { originalLinkText }
55
- .replace('\n', ' ')
56
- .replace('\r', ' ')
57
-
58
- /** Width limit given to a layout, and the box that layout reserved for the pill. */
59
- private class LayoutState {
60
- var availableWidth = Float.MAX_VALUE
61
- var reservedWidth = 0
62
- }
63
-
64
- // The view's text is also re-measured by Yoga off the main thread, at widths that may
65
- // never be committed. Each side keeps its own state so that pass cannot change what
66
- // the visible layout reserved and draws.
67
- private val uiState = LayoutState()
68
- private val backgroundState = LayoutState()
69
- private val state: LayoutState
70
- get() = if (Looper.myLooper() == Looper.getMainLooper()) uiState else backgroundState
71
-
72
- // Indentation of the enclosing blocks; -1 until resolved.
73
- @Volatile
74
- private var leadingMargin = -1
75
-
76
- @Volatile
77
- private var icon: Bitmap? = null
78
-
79
- // A remote icon keeps its slot while it loads, so its arrival needs a redraw, never a
80
- // relayout. A failed load gives the slot up, which reflows the text and re-measures.
81
- @Volatile
82
- private var reservesIconSlot: Boolean
83
- private val views = ArrayList<WeakReference<View>>()
84
-
85
- // A self-measuring host (a table cell) re-measures itself instead of the component.
86
- private var onSlotReleased: (() -> Unit)? = null
87
-
88
- private var ellipsizedLabel = label
89
- private var ellipsizedForWidth = -1f
90
-
91
- val accessibilityText = if (label == originalLinkText) originalLinkText else "$label, $originalLinkText"
92
-
93
- /** The minimum line height the pill asks of the block that holds it; 0 for none. */
94
- val lineHeight: Float = pill.lineHeight
95
-
96
- // A tint set for the link wins. The variant's tint is for the variant's own icon, not
97
- // for an icon supplied per link (an avatar would become a silhouette).
98
- private val iconTint: ColorFilter? =
99
- content?.iconTintColor?.let { PorterDuffColorFilter(it, PorterDuff.Mode.SRC_IN) }
100
- ?: pill.iconTint.takeIf { content?.iconUri.isNullOrEmpty() }
101
-
102
- init {
103
- val iconUri = resolved.iconUri
104
- if (LinkPillIconCache.isRemote(iconUri)) {
105
- // A source that failed a moment ago gets no slot and no request until it may be retried.
106
- reservesIconSlot = !LinkPillIconCache.hasFailedRecently(iconUri, requestHeaders)
107
- if (reservesIconSlot) {
108
- LinkPillIconCache
109
- .loadRemote(context, iconUri, requestHeaders) { loaded ->
110
- if (loaded == null) {
111
- releaseIconSlot()
112
- } else {
113
- icon = loaded
114
- invalidateViews()
115
- }
116
- }?.let { icon = it }
117
- }
118
- } else {
119
- icon = LinkPillIconCache.load(context, iconUri)
120
- reservesIconSlot = icon != null
121
- }
122
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) contentDescription = accessibilityText
123
- }
124
-
125
- /**
126
- * Records how much the enclosing blocks (lists, quotes) indent this pill's line. Done
127
- * once when the text is complete; looking it up on every layout would scan all spans.
128
- */
129
- fun resolveLeadingMargin(
130
- text: Spanned,
131
- start: Int,
132
- end: Int,
133
- ) {
134
- leadingMargin =
135
- text
136
- .getSpans(start, end, LeadingMarginSpan::class.java)
137
- .sumOf { max(it.getLeadingMargin(true), it.getLeadingMargin(false)) }
138
- }
139
-
140
- fun prepareForMeasurement(width: Int): Boolean {
141
- val next = width.coerceAtLeast(1).toFloat()
142
- val current = state
143
- if (current.availableWidth == next) return false
144
- current.availableWidth = next
145
- return true
146
- }
147
-
148
- // A link's text can hold several styled runs (bold next to regular, inline code), and
149
- // a layout measures and draws a replacement once per run. The pill belongs to the
150
- // first run; the others take no room and draw nothing.
151
- private fun startsPill(
152
- text: CharSequence,
153
- start: Int,
154
- ): Boolean {
155
- val spanStart = (text as? Spanned)?.getSpanStart(this) ?: -1
156
- return spanStart < 0 || start <= spanStart
157
- }
158
-
159
- /** Lets an icon that arrives after layout redraw [view], and a failed one reflow it. */
160
- fun registerView(
161
- view: View,
162
- onSlotReleased: (() -> Unit)? = null,
163
- ) {
164
- if (!reservesIconSlot || icon != null) return
165
- this.onSlotReleased = onSlotReleased
166
- synchronized(views) {
167
- views.removeAll { it.get() == null }
168
- if (views.none { it.get() === view }) views.add(WeakReference(view))
169
- }
170
- }
171
-
172
- private fun releaseIconSlot() {
173
- reservesIconSlot = false
174
- withHosts { hosts ->
175
- val host = hosts.filter { reflowIn(it) }.firstOrNull() ?: return@withHosts
176
- onSlotReleased?.let {
177
- it()
178
- return@withHosts
179
- }
180
- if (host is EnrichedTiptapText) {
181
- host.layoutManager.invalidateLayout()
182
- } else {
183
- host.findEnrichedTiptapTextAncestor()?.layoutManager?.invalidateLayout() ?: host.invalidate()
184
- }
185
- }
186
- }
187
-
188
- private fun invalidateViews() {
189
- withHosts { hosts -> hosts.forEach { reflowIn(it) } }
190
- }
191
-
192
- /** Hands the registered views, once, to [action] on the main thread. */
193
- private fun withHosts(action: (List<View>) -> Unit) {
194
- val hosts = synchronized(views) { views.mapNotNull { it.get() }.also { views.clear() } }
195
- if (hosts.isEmpty()) return
196
- if (Looper.myLooper() == Looper.getMainLooper()) action(hosts) else hosts.first().post { action(hosts) }
197
- }
198
-
199
- /**
200
- * A span change makes a TextView lay the pill out again and re-record the display list
201
- * a selectable one draws from, which invalidating alone does not. False when [view] no
202
- * longer shows this span.
203
- */
204
- private fun reflowIn(view: View): Boolean {
205
- val text = (view as? TextView)?.text as? Spannable
206
- val start = text?.getSpanStart(this) ?: -1
207
- if (text != null && start >= 0) {
208
- text.setSpan(this, start, text.getSpanEnd(this), text.getSpanFlags(this))
209
- return true
210
- }
211
- view.invalidate()
212
- return false
213
- }
214
-
215
- private fun TextPaint.applyLabelStyle(source: Paint): TextPaint {
216
- set(source)
217
- // The other spans on this run (strong, emphasis, inline code) have already styled the
218
- // paint. Use that font, as ordinary link text would; an explicit link font family keeps
219
- // its face and only takes the run's weight and slant.
220
- val base = this@LinkPillSpan.typeface
221
- val run = source.typeface
222
- typeface =
223
- if (followsRunTypeface && run != null) {
224
- run
225
- } else {
226
- val style = base.style or ((run?.style ?: Typeface.NORMAL) and Typeface.BOLD_ITALIC)
227
- if (style == base.style) base else Typeface.create(base, style)
228
- }
229
- textSize = fontSize
230
- color = variant.color
231
- bgColor = 0
232
- isUnderlineText = variant.underline
233
- isStrikeThruText = false
234
- return this
235
- }
236
-
237
- private fun width(paint: Paint): Float {
238
- val iconWidth = if (reservesIconSlot) fontSize * ICON_SLOT_RATIO else 0f
239
- val natural = paint.measureText(label) + iconWidth + 2 * (pill.paddingHorizontal + pill.borderWidth)
240
- val availableWidth = state.availableWidth
241
- val limit = if (pill.maxWidth > 0) min(availableWidth, pill.maxWidth) else availableWidth
242
- return min(ceil(natural), limit).coerceAtLeast(1f)
243
- }
244
-
245
- override fun getSize(
246
- paint: Paint,
247
- text: CharSequence,
248
- start: Int,
249
- end: Int,
250
- fm: Paint.FontMetricsInt?,
251
- ): Int {
252
- if (!startsPill(text, start)) return 0
253
- // Measurement can run off the main thread, so it keeps its own paint.
254
- val labelPaint = TextPaint().applyLabelStyle(paint)
255
- val metrics = labelPaint.fontMetricsInt
256
- val inset = ceil(pill.paddingVertical + pill.borderWidth).toInt()
257
- fm?.let {
258
- it.ascent = min(it.ascent, metrics.ascent - inset)
259
- it.descent = max(it.descent, metrics.descent + inset)
260
- it.top = min(it.top, it.ascent)
261
- it.bottom = max(it.bottom, it.descent)
262
- }
263
- return ceil(width(labelPaint)).toInt().also { state.reservedWidth = it }
264
- }
265
-
266
- override fun draw(
267
- canvas: Canvas,
268
- text: CharSequence,
269
- start: Int,
270
- end: Int,
271
- x: Float,
272
- top: Int,
273
- y: Int,
274
- bottom: Int,
275
- paint: Paint,
276
- ) {
277
- // The spoiler overlay only covers the text's glyph box, so a pill drawn under it
278
- // would outline the hidden link with its padding and border.
279
- if (text is Spanned && text.isConcealedBySpoiler(start, end)) return
280
- if (!startsPill(text, start)) return
281
-
282
- val labelPaint = drawLabelPaint.applyLabelStyle(paint)
283
- val metrics = drawMetrics.also { labelPaint.getFontMetrics(it) }
284
- val inset = pill.paddingVertical + pill.borderWidth
285
- val reservedWidth = uiState.reservedWidth
286
- val pillWidth = if (reservedWidth > 0) reservedWidth.toFloat() else width(labelPaint)
287
- rect.set(x, y + metrics.ascent - inset, x + pillWidth, y + metrics.descent + inset)
288
- shapePaint.style = Paint.Style.FILL
289
- shapePaint.color = variant.backgroundColor
290
- canvas.drawRoundRect(rect, pill.borderRadius, pill.borderRadius, shapePaint)
291
- if (pill.borderWidth > 0) {
292
- shapePaint.color = pill.borderColor
293
- shapePaint.style = Paint.Style.STROKE
294
- shapePaint.strokeWidth = pill.borderWidth
295
- innerRect.set(rect)
296
- innerRect.inset(pill.borderWidth / 2, pill.borderWidth / 2)
297
- canvas.drawRoundRect(innerRect, pill.borderRadius, pill.borderRadius, shapePaint)
298
- }
299
- val save = canvas.save()
300
- canvas.clipRect(rect)
301
- var left = x + pill.paddingHorizontal + pill.borderWidth
302
- val right = rect.right - pill.paddingHorizontal - pill.borderWidth
303
- if (reservesIconSlot && right - left >= fontSize) {
304
- icon?.let {
305
- val iconTop = y + (metrics.ascent + metrics.descent - fontSize) / 2
306
- val scale = fontSize / max(it.width, it.height)
307
- innerRect.set(left, iconTop, left + it.width * scale, iconTop + it.height * scale)
308
- innerRect.offset((fontSize - innerRect.width()) / 2, (fontSize - innerRect.height()) / 2)
309
- // The paint is shared by all pills, so the tint is set for every icon, tinted or not.
310
- iconPaint.colorFilter = iconTint
311
- canvas.drawBitmap(it, null, innerRect, iconPaint)
312
- }
313
- left += fontSize * ICON_SLOT_RATIO
314
- }
315
- canvas.drawText(displayedLabel(labelPaint, max(0f, right - left)), left, y.toFloat(), labelPaint)
316
- canvas.restoreToCount(save)
317
- }
318
-
319
- private fun displayedLabel(
320
- paint: TextPaint,
321
- width: Float,
322
- ): String {
323
- if (width != ellipsizedForWidth) {
324
- ellipsizedLabel = TextUtils.ellipsize(label, paint, width, TextUtils.TruncateAt.END).toString()
325
- ellipsizedForWidth = width
326
- }
327
- return ellipsizedLabel
328
- }
329
-
330
- companion object {
331
- // The icon occupies a square of the label's font size plus a quarter of it as a gap.
332
- private const val ICON_SLOT_RATIO = 1.25f
333
-
334
- // Drawing happens on the main thread only, so all pills share these scratch objects
335
- // instead of allocating native paints per pill.
336
- private val iconPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
337
- private val shapePaint = Paint(Paint.ANTI_ALIAS_FLAG)
338
- private val drawLabelPaint = TextPaint()
339
- private val drawMetrics = Paint.FontMetrics()
340
- private val rect = RectF()
341
- private val innerRect = RectF()
342
-
343
- /**
344
- * Gives every pill in [text] its width limit. Layout sites reach this through
345
- * `prepareWidthAwareSpans`; returns true when a limit changed.
346
- */
347
- fun prepareForMeasurement(
348
- text: CharSequence?,
349
- width: Int,
350
- ): Boolean {
351
- val spanned = text as? Spanned ?: return false
352
- var changed = false
353
- spanned.getSpans(0, spanned.length, LinkPillSpan::class.java).forEach { pill ->
354
- if (pill.leadingMargin < 0) {
355
- pill.resolveLeadingMargin(spanned, spanned.getSpanStart(pill), spanned.getSpanEnd(pill))
356
- }
357
- changed = pill.prepareForMeasurement(width - pill.leadingMargin) || changed
358
- }
359
- return changed
360
- }
361
-
362
- fun registerView(
363
- text: CharSequence?,
364
- view: View,
365
- onSlotReleased: (() -> Unit)? = null,
366
- ) {
367
- val spanned = text as? Spanned ?: return
368
- spanned.getSpans(0, spanned.length, LinkPillSpan::class.java).forEach { it.registerView(view, onSlotReleased) }
369
- }
370
-
371
- /** Redraws the pills in `[start, end)`, which skipped drawing while a spoiler concealed them. */
372
- fun redraw(
373
- view: TextView,
374
- start: Int,
375
- end: Int,
376
- ) {
377
- val spanned = view.text as? Spanned ?: return
378
- spanned.getSpans(start, end, LinkPillSpan::class.java).forEach { it.reflowIn(view) }
379
- }
380
- }
381
- }
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.ColorFilter
7
+ import android.graphics.Paint
8
+ import android.graphics.PorterDuff
9
+ import android.graphics.PorterDuffColorFilter
10
+ import android.graphics.RectF
11
+ import android.graphics.Typeface
12
+ import android.os.Build
13
+ import android.os.Looper
14
+ import android.text.Spannable
15
+ import android.text.Spanned
16
+ import android.text.TextPaint
17
+ import android.text.TextUtils
18
+ import android.text.style.LeadingMarginSpan
19
+ import android.text.style.ReplacementSpan
20
+ import android.view.View
21
+ import android.widget.TextView
22
+ import com.renderrichtext.EnrichedTiptapText
23
+ import com.renderrichtext.spoiler.isConcealedBySpoiler
24
+ import com.renderrichtext.styles.LinkPillContent
25
+ import com.renderrichtext.styles.LinkPillStyle
26
+ import com.renderrichtext.styles.LinkVariantEntry
27
+ import com.renderrichtext.utils.common.findEnrichedTiptapTextAncestor
28
+ import java.lang.ref.WeakReference
29
+ import kotlin.math.ceil
30
+ import kotlin.math.max
31
+ import kotlin.math.min
32
+
33
+ /**
34
+ * Draws an atomic visual over the original link text, without changing stored characters.
35
+ *
36
+ * Presentation comes from the [variant]; [content] is what this particular link shows
37
+ * and wins over the variant's label and icon, which win over the link text.
38
+ */
39
+ class LinkPillSpan(
40
+ private val variant: LinkVariantEntry,
41
+ private val typeface: Typeface,
42
+ private val fontSize: Float,
43
+ originalLinkText: String,
44
+ context: Context,
45
+ content: LinkPillContent? = null,
46
+ requestHeaders: Map<String, String> = emptyMap(),
47
+ // False when the link style names a font family, which then wins over the run's font.
48
+ private val followsRunTypeface: Boolean = false,
49
+ ) : ReplacementSpan() {
50
+ private val pill = variant.pill ?: LinkPillStyle()
51
+ private val resolved = (content ?: LinkPillContent()).orElse(pill.content)
52
+ private val label =
53
+ resolved.label
54
+ .ifEmpty { originalLinkText }
55
+ .replace('\n', ' ')
56
+ .replace('\r', ' ')
57
+
58
+ /** Width limit given to a layout, and the box that layout reserved for the pill. */
59
+ private class LayoutState {
60
+ var availableWidth = Float.MAX_VALUE
61
+ var reservedWidth = 0
62
+ }
63
+
64
+ // The view's text is also re-measured by Yoga off the main thread, at widths that may
65
+ // never be committed. Each side keeps its own state so that pass cannot change what
66
+ // the visible layout reserved and draws.
67
+ private val uiState = LayoutState()
68
+ private val backgroundState = LayoutState()
69
+ private val state: LayoutState
70
+ get() = if (Looper.myLooper() == Looper.getMainLooper()) uiState else backgroundState
71
+
72
+ // Indentation of the enclosing blocks; -1 until resolved.
73
+ @Volatile
74
+ private var leadingMargin = -1
75
+
76
+ @Volatile
77
+ private var icon: Bitmap? = null
78
+
79
+ // A remote icon keeps its slot while it loads, so its arrival needs a redraw, never a
80
+ // relayout. A failed load gives the slot up, which reflows the text and re-measures.
81
+ @Volatile
82
+ private var reservesIconSlot: Boolean
83
+ private val views = ArrayList<WeakReference<View>>()
84
+
85
+ // A self-measuring host (a table cell) re-measures itself instead of the component.
86
+ private var onSlotReleased: (() -> Unit)? = null
87
+
88
+ private var ellipsizedLabel = label
89
+ private var ellipsizedForWidth = -1f
90
+
91
+ val accessibilityText = if (label == originalLinkText) originalLinkText else "$label, $originalLinkText"
92
+
93
+ /** The minimum line height the pill asks of the block that holds it; 0 for none. */
94
+ val lineHeight: Float = pill.lineHeight
95
+
96
+ // A tint set for the link wins. The variant's tint is for the variant's own icon, not
97
+ // for an icon supplied per link (an avatar would become a silhouette).
98
+ private val iconTint: ColorFilter? =
99
+ content?.iconTintColor?.let { PorterDuffColorFilter(it, PorterDuff.Mode.SRC_IN) }
100
+ ?: pill.iconTint.takeIf { content?.iconUri.isNullOrEmpty() }
101
+
102
+ init {
103
+ val iconUri = resolved.iconUri
104
+ if (LinkPillIconCache.isRemote(iconUri)) {
105
+ // A source that failed a moment ago gets no slot and no request until it may be retried.
106
+ reservesIconSlot = !LinkPillIconCache.hasFailedRecently(iconUri, requestHeaders)
107
+ if (reservesIconSlot) {
108
+ LinkPillIconCache
109
+ .loadRemote(context, iconUri, requestHeaders) { loaded ->
110
+ if (loaded == null) {
111
+ releaseIconSlot()
112
+ } else {
113
+ icon = loaded
114
+ invalidateViews()
115
+ }
116
+ }?.let { icon = it }
117
+ }
118
+ } else {
119
+ icon = LinkPillIconCache.load(context, iconUri)
120
+ reservesIconSlot = icon != null
121
+ }
122
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) contentDescription = accessibilityText
123
+ }
124
+
125
+ /**
126
+ * Records how much the enclosing blocks (lists, quotes) indent this pill's line. Done
127
+ * once when the text is complete; looking it up on every layout would scan all spans.
128
+ */
129
+ fun resolveLeadingMargin(
130
+ text: Spanned,
131
+ start: Int,
132
+ end: Int,
133
+ ) {
134
+ leadingMargin =
135
+ text
136
+ .getSpans(start, end, LeadingMarginSpan::class.java)
137
+ .sumOf { max(it.getLeadingMargin(true), it.getLeadingMargin(false)) }
138
+ }
139
+
140
+ fun prepareForMeasurement(width: Int): Boolean {
141
+ val next = width.coerceAtLeast(1).toFloat()
142
+ val current = state
143
+ if (current.availableWidth == next) return false
144
+ current.availableWidth = next
145
+ return true
146
+ }
147
+
148
+ // A link's text can hold several styled runs (bold next to regular, inline code), and
149
+ // a layout measures and draws a replacement once per run. The pill belongs to the
150
+ // first run; the others take no room and draw nothing.
151
+ private fun startsPill(
152
+ text: CharSequence,
153
+ start: Int,
154
+ ): Boolean {
155
+ val spanStart = (text as? Spanned)?.getSpanStart(this) ?: -1
156
+ return spanStart < 0 || start <= spanStart
157
+ }
158
+
159
+ /** Lets an icon that arrives after layout redraw [view], and a failed one reflow it. */
160
+ fun registerView(
161
+ view: View,
162
+ onSlotReleased: (() -> Unit)? = null,
163
+ ) {
164
+ if (!reservesIconSlot || icon != null) return
165
+ this.onSlotReleased = onSlotReleased
166
+ synchronized(views) {
167
+ views.removeAll { it.get() == null }
168
+ if (views.none { it.get() === view }) views.add(WeakReference(view))
169
+ }
170
+ }
171
+
172
+ private fun releaseIconSlot() {
173
+ reservesIconSlot = false
174
+ withHosts { hosts ->
175
+ val host = hosts.filter { reflowIn(it) }.firstOrNull() ?: return@withHosts
176
+ onSlotReleased?.let {
177
+ it()
178
+ return@withHosts
179
+ }
180
+ if (host is EnrichedTiptapText) {
181
+ host.layoutManager.invalidateLayout()
182
+ } else {
183
+ host.findEnrichedTiptapTextAncestor()?.layoutManager?.invalidateLayout() ?: host.invalidate()
184
+ }
185
+ }
186
+ }
187
+
188
+ private fun invalidateViews() {
189
+ withHosts { hosts -> hosts.forEach { reflowIn(it) } }
190
+ }
191
+
192
+ /** Hands the registered views, once, to [action] on the main thread. */
193
+ private fun withHosts(action: (List<View>) -> Unit) {
194
+ val hosts = synchronized(views) { views.mapNotNull { it.get() }.also { views.clear() } }
195
+ if (hosts.isEmpty()) return
196
+ if (Looper.myLooper() == Looper.getMainLooper()) action(hosts) else hosts.first().post { action(hosts) }
197
+ }
198
+
199
+ /**
200
+ * A span change makes a TextView lay the pill out again and re-record the display list
201
+ * a selectable one draws from, which invalidating alone does not. False when [view] no
202
+ * longer shows this span.
203
+ */
204
+ private fun reflowIn(view: View): Boolean {
205
+ val text = (view as? TextView)?.text as? Spannable
206
+ val start = text?.getSpanStart(this) ?: -1
207
+ if (text != null && start >= 0) {
208
+ text.setSpan(this, start, text.getSpanEnd(this), text.getSpanFlags(this))
209
+ return true
210
+ }
211
+ view.invalidate()
212
+ return false
213
+ }
214
+
215
+ private fun TextPaint.applyLabelStyle(source: Paint): TextPaint {
216
+ set(source)
217
+ // The other spans on this run (strong, emphasis, inline code) have already styled the
218
+ // paint. Use that font, as ordinary link text would; an explicit link font family keeps
219
+ // its face and only takes the run's weight and slant.
220
+ val base = this@LinkPillSpan.typeface
221
+ val run = source.typeface
222
+ typeface =
223
+ if (followsRunTypeface && run != null) {
224
+ run
225
+ } else {
226
+ val style = base.style or ((run?.style ?: Typeface.NORMAL) and Typeface.BOLD_ITALIC)
227
+ if (style == base.style) base else Typeface.create(base, style)
228
+ }
229
+ textSize = fontSize
230
+ color = variant.color
231
+ bgColor = 0
232
+ isUnderlineText = variant.underline
233
+ isStrikeThruText = false
234
+ return this
235
+ }
236
+
237
+ private fun width(paint: Paint): Float {
238
+ val iconWidth = if (reservesIconSlot) fontSize * ICON_SLOT_RATIO else 0f
239
+ val natural = paint.measureText(label) + iconWidth + 2 * (pill.paddingHorizontal + pill.borderWidth)
240
+ val availableWidth = state.availableWidth
241
+ val limit = if (pill.maxWidth > 0) min(availableWidth, pill.maxWidth) else availableWidth
242
+ return min(ceil(natural), limit).coerceAtLeast(1f)
243
+ }
244
+
245
+ override fun getSize(
246
+ paint: Paint,
247
+ text: CharSequence,
248
+ start: Int,
249
+ end: Int,
250
+ fm: Paint.FontMetricsInt?,
251
+ ): Int {
252
+ if (!startsPill(text, start)) return 0
253
+ // Measurement can run off the main thread, so it keeps its own paint.
254
+ val labelPaint = TextPaint().applyLabelStyle(paint)
255
+ val metrics = labelPaint.fontMetricsInt
256
+ val inset = ceil(pill.paddingVertical + pill.borderWidth).toInt()
257
+ fm?.let {
258
+ it.ascent = min(it.ascent, metrics.ascent - inset)
259
+ it.descent = max(it.descent, metrics.descent + inset)
260
+ it.top = min(it.top, it.ascent)
261
+ it.bottom = max(it.bottom, it.descent)
262
+ }
263
+ return ceil(width(labelPaint)).toInt().also { state.reservedWidth = it }
264
+ }
265
+
266
+ override fun draw(
267
+ canvas: Canvas,
268
+ text: CharSequence,
269
+ start: Int,
270
+ end: Int,
271
+ x: Float,
272
+ top: Int,
273
+ y: Int,
274
+ bottom: Int,
275
+ paint: Paint,
276
+ ) {
277
+ // The spoiler overlay only covers the text's glyph box, so a pill drawn under it
278
+ // would outline the hidden link with its padding and border.
279
+ if (text is Spanned && text.isConcealedBySpoiler(start, end)) return
280
+ if (!startsPill(text, start)) return
281
+
282
+ val labelPaint = drawLabelPaint.applyLabelStyle(paint)
283
+ val metrics = drawMetrics.also { labelPaint.getFontMetrics(it) }
284
+ val inset = pill.paddingVertical + pill.borderWidth
285
+ val reservedWidth = uiState.reservedWidth
286
+ val pillWidth = if (reservedWidth > 0) reservedWidth.toFloat() else width(labelPaint)
287
+ rect.set(x, y + metrics.ascent - inset, x + pillWidth, y + metrics.descent + inset)
288
+ shapePaint.style = Paint.Style.FILL
289
+ shapePaint.color = variant.backgroundColor
290
+ canvas.drawRoundRect(rect, pill.borderRadius, pill.borderRadius, shapePaint)
291
+ if (pill.borderWidth > 0) {
292
+ shapePaint.color = pill.borderColor
293
+ shapePaint.style = Paint.Style.STROKE
294
+ shapePaint.strokeWidth = pill.borderWidth
295
+ innerRect.set(rect)
296
+ innerRect.inset(pill.borderWidth / 2, pill.borderWidth / 2)
297
+ canvas.drawRoundRect(innerRect, pill.borderRadius, pill.borderRadius, shapePaint)
298
+ }
299
+ val save = canvas.save()
300
+ canvas.clipRect(rect)
301
+ var left = x + pill.paddingHorizontal + pill.borderWidth
302
+ val right = rect.right - pill.paddingHorizontal - pill.borderWidth
303
+ if (reservesIconSlot && right - left >= fontSize) {
304
+ icon?.let {
305
+ val iconTop = y + (metrics.ascent + metrics.descent - fontSize) / 2
306
+ val scale = fontSize / max(it.width, it.height)
307
+ innerRect.set(left, iconTop, left + it.width * scale, iconTop + it.height * scale)
308
+ innerRect.offset((fontSize - innerRect.width()) / 2, (fontSize - innerRect.height()) / 2)
309
+ // The paint is shared by all pills, so the tint is set for every icon, tinted or not.
310
+ iconPaint.colorFilter = iconTint
311
+ canvas.drawBitmap(it, null, innerRect, iconPaint)
312
+ }
313
+ left += fontSize * ICON_SLOT_RATIO
314
+ }
315
+ canvas.drawText(displayedLabel(labelPaint, max(0f, right - left)), left, y.toFloat(), labelPaint)
316
+ canvas.restoreToCount(save)
317
+ }
318
+
319
+ private fun displayedLabel(
320
+ paint: TextPaint,
321
+ width: Float,
322
+ ): String {
323
+ if (width != ellipsizedForWidth) {
324
+ ellipsizedLabel = TextUtils.ellipsize(label, paint, width, TextUtils.TruncateAt.END).toString()
325
+ ellipsizedForWidth = width
326
+ }
327
+ return ellipsizedLabel
328
+ }
329
+
330
+ companion object {
331
+ // The icon occupies a square of the label's font size plus a quarter of it as a gap.
332
+ private const val ICON_SLOT_RATIO = 1.25f
333
+
334
+ // Drawing happens on the main thread only, so all pills share these scratch objects
335
+ // instead of allocating native paints per pill.
336
+ private val iconPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
337
+ private val shapePaint = Paint(Paint.ANTI_ALIAS_FLAG)
338
+ private val drawLabelPaint = TextPaint()
339
+ private val drawMetrics = Paint.FontMetrics()
340
+ private val rect = RectF()
341
+ private val innerRect = RectF()
342
+
343
+ /**
344
+ * Gives every pill in [text] its width limit. Layout sites reach this through
345
+ * `prepareWidthAwareSpans`; returns true when a limit changed.
346
+ */
347
+ fun prepareForMeasurement(
348
+ text: CharSequence?,
349
+ width: Int,
350
+ ): Boolean {
351
+ val spanned = text as? Spanned ?: return false
352
+ var changed = false
353
+ spanned.getSpans(0, spanned.length, LinkPillSpan::class.java).forEach { pill ->
354
+ if (pill.leadingMargin < 0) {
355
+ pill.resolveLeadingMargin(spanned, spanned.getSpanStart(pill), spanned.getSpanEnd(pill))
356
+ }
357
+ changed = pill.prepareForMeasurement(width - pill.leadingMargin) || changed
358
+ }
359
+ return changed
360
+ }
361
+
362
+ fun registerView(
363
+ text: CharSequence?,
364
+ view: View,
365
+ onSlotReleased: (() -> Unit)? = null,
366
+ ) {
367
+ val spanned = text as? Spanned ?: return
368
+ spanned.getSpans(0, spanned.length, LinkPillSpan::class.java).forEach { it.registerView(view, onSlotReleased) }
369
+ }
370
+
371
+ /** Redraws the pills in `[start, end)`, which skipped drawing while a spoiler concealed them. */
372
+ fun redraw(
373
+ view: TextView,
374
+ start: Int,
375
+ end: Int,
376
+ ) {
377
+ val spanned = view.text as? Spanned ?: return
378
+ spanned.getSpans(start, end, LinkPillSpan::class.java).forEach { it.reflowIn(view) }
379
+ }
380
+ }
381
+ }