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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +140 -3
  3. package/ReactNativeRenderRichText.podspec +23 -0
  4. package/android/build.gradle +86 -0
  5. package/android/consumer-rules.pro +3 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/jni-adapter.cpp +191 -0
  8. package/android/src/main/java/com/renderrichtext/EnrichedTiptapText.kt +673 -0
  9. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextLayoutManager.kt +24 -0
  10. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextManager.kt +405 -0
  11. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextPackage.kt +13 -0
  12. package/android/src/main/java/com/renderrichtext/MeasurementStore.kt +644 -0
  13. package/android/src/main/java/com/renderrichtext/accessibility/AccessibilityLabels.kt +17 -0
  14. package/android/src/main/java/com/renderrichtext/accessibility/AccessibleTiptapTextView.kt +112 -0
  15. package/android/src/main/java/com/renderrichtext/accessibility/TiptapAccessibilityHelper.kt +576 -0
  16. package/android/src/main/java/com/renderrichtext/events/CodeBlockPressEvent.kt +24 -0
  17. package/android/src/main/java/com/renderrichtext/events/ContextMenuItemPressEvent.kt +28 -0
  18. package/android/src/main/java/com/renderrichtext/events/CopyPressEvent.kt +24 -0
  19. package/android/src/main/java/com/renderrichtext/events/FactoidPressEvent.kt +23 -0
  20. package/android/src/main/java/com/renderrichtext/events/ImagePressEvent.kt +25 -0
  21. package/android/src/main/java/com/renderrichtext/events/LatexErrorEvent.kt +29 -0
  22. package/android/src/main/java/com/renderrichtext/events/LinkLongPressEvent.kt +23 -0
  23. package/android/src/main/java/com/renderrichtext/events/LinkPressEvent.kt +23 -0
  24. package/android/src/main/java/com/renderrichtext/events/MentionLongPressEvent.kt +25 -0
  25. package/android/src/main/java/com/renderrichtext/events/MentionPressEvent.kt +25 -0
  26. package/android/src/main/java/com/renderrichtext/events/TaskListItemPressEvent.kt +26 -0
  27. package/android/src/main/java/com/renderrichtext/math/LatexErrorReporter.kt +9 -0
  28. package/android/src/main/java/com/renderrichtext/parser/Parser.kt +59 -0
  29. package/android/src/main/java/com/renderrichtext/parser/TextLinkMatching.kt +35 -0
  30. package/android/src/main/java/com/renderrichtext/parser/TiptapASTNode.kt +69 -0
  31. package/android/src/main/java/com/renderrichtext/renderer/BaselineShiftRenderer.kt +45 -0
  32. package/android/src/main/java/com/renderrichtext/renderer/BlankLineRenderer.kt +39 -0
  33. package/android/src/main/java/com/renderrichtext/renderer/BlockStyleContext.kt +181 -0
  34. package/android/src/main/java/com/renderrichtext/renderer/BlockquoteRenderer.kt +243 -0
  35. package/android/src/main/java/com/renderrichtext/renderer/BlockquoteTextRenderer.kt +56 -0
  36. package/android/src/main/java/com/renderrichtext/renderer/CodeBlockRenderer.kt +135 -0
  37. package/android/src/main/java/com/renderrichtext/renderer/CodeRenderer.kt +37 -0
  38. package/android/src/main/java/com/renderrichtext/renderer/EmphasisRenderer.kt +28 -0
  39. package/android/src/main/java/com/renderrichtext/renderer/FactoidRenderer.kt +97 -0
  40. package/android/src/main/java/com/renderrichtext/renderer/HeadingRenderer.kt +81 -0
  41. package/android/src/main/java/com/renderrichtext/renderer/HighlightRenderer.kt +26 -0
  42. package/android/src/main/java/com/renderrichtext/renderer/LineBreakRenderer.kt +17 -0
  43. package/android/src/main/java/com/renderrichtext/renderer/LinkRenderer.kt +90 -0
  44. package/android/src/main/java/com/renderrichtext/renderer/ListContextManager.kt +109 -0
  45. package/android/src/main/java/com/renderrichtext/renderer/ListItemRenderer.kt +211 -0
  46. package/android/src/main/java/com/renderrichtext/renderer/ListRenderer.kt +133 -0
  47. package/android/src/main/java/com/renderrichtext/renderer/MentionRenderer.kt +85 -0
  48. package/android/src/main/java/com/renderrichtext/renderer/NodeRenderer.kt +207 -0
  49. package/android/src/main/java/com/renderrichtext/renderer/ParagraphRenderer.kt +91 -0
  50. package/android/src/main/java/com/renderrichtext/renderer/Renderer.kt +148 -0
  51. package/android/src/main/java/com/renderrichtext/renderer/SoftBreakRenderer.kt +17 -0
  52. package/android/src/main/java/com/renderrichtext/renderer/SpanStyleCache.kt +143 -0
  53. package/android/src/main/java/com/renderrichtext/renderer/StrikethroughRenderer.kt +28 -0
  54. package/android/src/main/java/com/renderrichtext/renderer/StrongRenderer.kt +28 -0
  55. package/android/src/main/java/com/renderrichtext/renderer/TextRenderer.kt +31 -0
  56. package/android/src/main/java/com/renderrichtext/renderer/ThematicBreakRenderer.kt +46 -0
  57. package/android/src/main/java/com/renderrichtext/renderer/UnderlineRenderer.kt +28 -0
  58. package/android/src/main/java/com/renderrichtext/spans/BaseListSpan.kt +164 -0
  59. package/android/src/main/java/com/renderrichtext/spans/BaselineShiftSpan.kt +27 -0
  60. package/android/src/main/java/com/renderrichtext/spans/BlockquoteSpan.kt +302 -0
  61. package/android/src/main/java/com/renderrichtext/spans/CodeBackgroundSpan.kt +205 -0
  62. package/android/src/main/java/com/renderrichtext/spans/CodeBlockSpan.kt +216 -0
  63. package/android/src/main/java/com/renderrichtext/spans/CodeSpan.kt +32 -0
  64. package/android/src/main/java/com/renderrichtext/spans/EmphasisSpan.kt +42 -0
  65. package/android/src/main/java/com/renderrichtext/spans/FactoidBackgroundSpan.kt +265 -0
  66. package/android/src/main/java/com/renderrichtext/spans/FactoidPaddingSpan.kt +258 -0
  67. package/android/src/main/java/com/renderrichtext/spans/FactoidSpan.kt +32 -0
  68. package/android/src/main/java/com/renderrichtext/spans/FadeInSpan.kt +29 -0
  69. package/android/src/main/java/com/renderrichtext/spans/HeadingSpan.kt +38 -0
  70. package/android/src/main/java/com/renderrichtext/spans/HighlightSpan.kt +80 -0
  71. package/android/src/main/java/com/renderrichtext/spans/ImageSpan.kt +481 -0
  72. package/android/src/main/java/com/renderrichtext/spans/InlineBackgroundGeometry.kt +42 -0
  73. package/android/src/main/java/com/renderrichtext/spans/LineHeightSpan.kt +39 -0
  74. package/android/src/main/java/com/renderrichtext/spans/LinkPillIconCache.kt +154 -0
  75. package/android/src/main/java/com/renderrichtext/spans/LinkPillSpan.kt +381 -0
  76. package/android/src/main/java/com/renderrichtext/spans/LinkSpan.kt +68 -0
  77. package/android/src/main/java/com/renderrichtext/spans/ListItemSpacingSpan.kt +33 -0
  78. package/android/src/main/java/com/renderrichtext/spans/ListMarkerAnchorSpan.kt +38 -0
  79. package/android/src/main/java/com/renderrichtext/spans/MarginBottomSpan.kt +76 -0
  80. package/android/src/main/java/com/renderrichtext/spans/MentionChipSpan.kt +132 -0
  81. package/android/src/main/java/com/renderrichtext/spans/MentionSpan.kt +46 -0
  82. package/android/src/main/java/com/renderrichtext/spans/OrderedListSpan.kt +127 -0
  83. package/android/src/main/java/com/renderrichtext/spans/SpoilerSpan.kt +33 -0
  84. package/android/src/main/java/com/renderrichtext/spans/StrikethroughSpan.kt +12 -0
  85. package/android/src/main/java/com/renderrichtext/spans/StrongSpan.kt +39 -0
  86. package/android/src/main/java/com/renderrichtext/spans/TaskListSpan.kt +143 -0
  87. package/android/src/main/java/com/renderrichtext/spans/TextSpan.kt +26 -0
  88. package/android/src/main/java/com/renderrichtext/spans/ThematicBreakSpan.kt +69 -0
  89. package/android/src/main/java/com/renderrichtext/spans/UnorderedListSpan.kt +96 -0
  90. package/android/src/main/java/com/renderrichtext/spoiler/SpoilerStubs.kt +46 -0
  91. package/android/src/main/java/com/renderrichtext/styles/BaseBlockStyle.kt +11 -0
  92. package/android/src/main/java/com/renderrichtext/styles/BlockquoteStyle.kt +83 -0
  93. package/android/src/main/java/com/renderrichtext/styles/CodeBlockStyle.kt +80 -0
  94. package/android/src/main/java/com/renderrichtext/styles/CodeStyle.kt +26 -0
  95. package/android/src/main/java/com/renderrichtext/styles/EmphasisStyle.kt +21 -0
  96. package/android/src/main/java/com/renderrichtext/styles/FactoidStyle.kt +21 -0
  97. package/android/src/main/java/com/renderrichtext/styles/HeadingStyle.kt +33 -0
  98. package/android/src/main/java/com/renderrichtext/styles/HighlightStyle.kt +19 -0
  99. package/android/src/main/java/com/renderrichtext/styles/ImageStyle.kt +29 -0
  100. package/android/src/main/java/com/renderrichtext/styles/InlineImageStyle.kt +17 -0
  101. package/android/src/main/java/com/renderrichtext/styles/InlineMathStyle.kt +17 -0
  102. package/android/src/main/java/com/renderrichtext/styles/LinkPillContent.kt +36 -0
  103. package/android/src/main/java/com/renderrichtext/styles/LinkPillStyle.kt +44 -0
  104. package/android/src/main/java/com/renderrichtext/styles/LinkStyle.kt +23 -0
  105. package/android/src/main/java/com/renderrichtext/styles/LinkVariantEntry.kt +34 -0
  106. package/android/src/main/java/com/renderrichtext/styles/ListStyle.kt +65 -0
  107. package/android/src/main/java/com/renderrichtext/styles/MathStyle.kt +29 -0
  108. package/android/src/main/java/com/renderrichtext/styles/MentionStyle.kt +19 -0
  109. package/android/src/main/java/com/renderrichtext/styles/ParagraphStyle.kt +33 -0
  110. package/android/src/main/java/com/renderrichtext/styles/SpoilerStyle.kt +27 -0
  111. package/android/src/main/java/com/renderrichtext/styles/StrikethroughStyle.kt +17 -0
  112. package/android/src/main/java/com/renderrichtext/styles/StrongStyle.kt +21 -0
  113. package/android/src/main/java/com/renderrichtext/styles/StyleConfig.kt +411 -0
  114. package/android/src/main/java/com/renderrichtext/styles/StyleParser.kt +92 -0
  115. package/android/src/main/java/com/renderrichtext/styles/SubscriptStyle.kt +16 -0
  116. package/android/src/main/java/com/renderrichtext/styles/SuperscriptStyle.kt +16 -0
  117. package/android/src/main/java/com/renderrichtext/styles/TableStyle.kt +75 -0
  118. package/android/src/main/java/com/renderrichtext/styles/TaskListStyle.kt +29 -0
  119. package/android/src/main/java/com/renderrichtext/styles/TextAlignment.kt +32 -0
  120. package/android/src/main/java/com/renderrichtext/styles/ThematicBreakStyle.kt +23 -0
  121. package/android/src/main/java/com/renderrichtext/styles/UnderlineStyle.kt +17 -0
  122. package/android/src/main/java/com/renderrichtext/styles/VideoStyle.kt +31 -0
  123. package/android/src/main/java/com/renderrichtext/utils/common/BorderStyleUtils.kt +86 -0
  124. package/android/src/main/java/com/renderrichtext/utils/common/BreakStrategyUtils.kt +32 -0
  125. package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockHighlighter.kt +36 -0
  126. package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockNode.kt +42 -0
  127. package/android/src/main/java/com/renderrichtext/utils/common/EllipsizeUtils.kt +27 -0
  128. package/android/src/main/java/com/renderrichtext/utils/common/EnrichedTiptapAncestorUtils.kt +10 -0
  129. package/android/src/main/java/com/renderrichtext/utils/common/FeatureFlags.kt +9 -0
  130. package/android/src/main/java/com/renderrichtext/utils/common/LinkRecognitionProps.kt +31 -0
  131. package/android/src/main/java/com/renderrichtext/utils/common/LinkRegexConfig.kt +56 -0
  132. package/android/src/main/java/com/renderrichtext/utils/common/MotionPreferences.kt +18 -0
  133. package/android/src/main/java/com/renderrichtext/utils/common/ReadableMapExtensions.kt +28 -0
  134. package/android/src/main/java/com/renderrichtext/utils/common/TiptapViewManagerUtils.kt +242 -0
  135. package/android/src/main/java/com/renderrichtext/utils/common/layout/RTLUtils.kt +6 -0
  136. package/android/src/main/java/com/renderrichtext/utils/common/serialization/TiptapASTSerializer.kt +368 -0
  137. package/android/src/main/java/com/renderrichtext/utils/text/ImageCache.kt +88 -0
  138. package/android/src/main/java/com/renderrichtext/utils/text/ImageDownloader.kt +203 -0
  139. package/android/src/main/java/com/renderrichtext/utils/text/LocalImageLoader.kt +167 -0
  140. package/android/src/main/java/com/renderrichtext/utils/text/TailFadeInAnimator.kt +84 -0
  141. package/android/src/main/java/com/renderrichtext/utils/text/conversion/HTMLGenerator.kt +1105 -0
  142. package/android/src/main/java/com/renderrichtext/utils/text/conversion/TiptapExtractor.kt +423 -0
  143. package/android/src/main/java/com/renderrichtext/utils/text/extensions/ParagraphUtils.kt +9 -0
  144. package/android/src/main/java/com/renderrichtext/utils/text/extensions/SpannableExtensions.kt +9 -0
  145. package/android/src/main/java/com/renderrichtext/utils/text/extensions/TextPaintExtensions.kt +76 -0
  146. package/android/src/main/java/com/renderrichtext/utils/text/interaction/CheckboxTouchHelper.kt +61 -0
  147. package/android/src/main/java/com/renderrichtext/utils/text/interaction/TaskListTapUtils.kt +286 -0
  148. package/android/src/main/java/com/renderrichtext/utils/text/span/ApplyLineHeight.kt +39 -0
  149. package/android/src/main/java/com/renderrichtext/utils/text/span/BlockSpacing.kt +142 -0
  150. package/android/src/main/java/com/renderrichtext/utils/text/span/SpanFlags.kt +43 -0
  151. package/android/src/main/java/com/renderrichtext/utils/text/span/WidthAwareSpans.kt +30 -0
  152. package/android/src/main/java/com/renderrichtext/utils/text/view/CodeBlockPressHost.kt +15 -0
  153. package/android/src/main/java/com/renderrichtext/utils/text/view/ImagePressHost.kt +19 -0
  154. package/android/src/main/java/com/renderrichtext/utils/text/view/LinkEvents.kt +88 -0
  155. package/android/src/main/java/com/renderrichtext/utils/text/view/LinkLongPressMovementMethod.kt +449 -0
  156. package/android/src/main/java/com/renderrichtext/utils/text/view/SelectionActionMode.kt +235 -0
  157. package/android/src/main/java/com/renderrichtext/utils/text/view/TextSelectionColors.kt +48 -0
  158. package/android/src/main/java/com/renderrichtext/utils/text/view/TextViewSetup.kt +119 -0
  159. package/android/src/main/java/com/renderrichtext/views/ContextMenuPopup.kt +217 -0
  160. package/android/src/main/java/com/renderrichtext/views/CopyGlyph.kt +101 -0
  161. package/android/src/main/java/com/renderrichtext/views/WrapGlyph.kt +40 -0
  162. package/android/src/main/jni/CMakeLists.txt +52 -0
  163. package/android/src/main/jni/react/renderer/components/RichTextSpec/ComponentDescriptors.h +30 -0
  164. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextComponentDescriptor.h +29 -0
  165. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.cpp +45 -0
  166. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.h +21 -0
  167. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.cpp +38 -0
  168. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.h +53 -0
  169. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.cpp +9 -0
  170. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.h +24 -0
  171. package/android/src/main/jni/react/renderer/components/RichTextSpec/conversions.h +40 -0
  172. package/cpp/parser/TiptapASTNode.hpp +68 -0
  173. package/cpp/parser/TiptapParser.cpp +233 -0
  174. package/cpp/parser/TiptapParser.hpp +13 -0
  175. package/cpp/vendor/nlohmann/json.hpp +24765 -0
  176. package/ios/EnrichedTiptapText.h +27 -0
  177. package/ios/EnrichedTiptapText.mm +1222 -0
  178. package/ios/attachments/ENRMImageAttachment.h +51 -0
  179. package/ios/attachments/ENRMImageAttachment.m +398 -0
  180. package/ios/attachments/ENRMLinkPillAttachment.h +47 -0
  181. package/ios/attachments/ENRMLinkPillAttachment.m +324 -0
  182. package/ios/attachments/ENRMLinkPillIconCache.h +28 -0
  183. package/ios/attachments/ENRMLinkPillIconCache.m +181 -0
  184. package/ios/attachments/ENRMLinkPillText.h +27 -0
  185. package/ios/attachments/ENRMLinkPillText.m +118 -0
  186. package/ios/attachments/ENRMMathInlineAttachment.h +7 -0
  187. package/ios/attachments/ENRMMathInlineAttachment.m +4 -0
  188. package/ios/attachments/ThematicBreakAttachment.h +15 -0
  189. package/ios/attachments/ThematicBreakAttachment.m +33 -0
  190. package/ios/internals/ENRMViewFreeMeasurement.h +208 -0
  191. package/ios/internals/EnrichedTiptapTextComponentDescriptor.h +19 -0
  192. package/ios/internals/EnrichedTiptapTextShadowNode.h +43 -0
  193. package/ios/internals/EnrichedTiptapTextShadowNode.mm +69 -0
  194. package/ios/internals/EnrichedTiptapTextState.h +24 -0
  195. package/ios/internals/MeasurementCache.h +308 -0
  196. package/ios/internals/ShadowMeasurementUtils.h +145 -0
  197. package/ios/parser/ENRMTextLinkAttributes.h +7 -0
  198. package/ios/parser/ENRMTiptapParser.h +33 -0
  199. package/ios/parser/ENRMTiptapParser.mm +73 -0
  200. package/ios/parser/TiptapASTNode.h +57 -0
  201. package/ios/parser/TiptapASTNode.m +52 -0
  202. package/ios/parser/TiptapParserBridge.mm +179 -0
  203. package/ios/renderer/AttributedRenderer.h +23 -0
  204. package/ios/renderer/AttributedRenderer.m +229 -0
  205. package/ios/renderer/BaseRenderer.h +33 -0
  206. package/ios/renderer/BaseRenderer.m +46 -0
  207. package/ios/renderer/BaselineShiftTextAttributes.h +10 -0
  208. package/ios/renderer/BaselineShiftTextAttributes.m +26 -0
  209. package/ios/renderer/BlankLineRenderer.h +4 -0
  210. package/ios/renderer/BlankLineRenderer.m +51 -0
  211. package/ios/renderer/BlockquoteRenderer.h +6 -0
  212. package/ios/renderer/BlockquoteRenderer.m +258 -0
  213. package/ios/renderer/CodeBlockRenderer.h +9 -0
  214. package/ios/renderer/CodeBlockRenderer.m +99 -0
  215. package/ios/renderer/CodeRenderer.h +9 -0
  216. package/ios/renderer/CodeRenderer.m +69 -0
  217. package/ios/renderer/ENRMBlockquoteTextRenderer.h +25 -0
  218. package/ios/renderer/ENRMBlockquoteTextRenderer.m +48 -0
  219. package/ios/renderer/EmphasisRenderer.h +5 -0
  220. package/ios/renderer/EmphasisRenderer.m +100 -0
  221. package/ios/renderer/FactoidRenderer.h +9 -0
  222. package/ios/renderer/FactoidRenderer.m +130 -0
  223. package/ios/renderer/HeadingRenderer.h +6 -0
  224. package/ios/renderer/HeadingRenderer.m +103 -0
  225. package/ios/renderer/HighlightRenderer.h +7 -0
  226. package/ios/renderer/HighlightRenderer.m +33 -0
  227. package/ios/renderer/LinkRenderer.h +6 -0
  228. package/ios/renderer/LinkRenderer.m +158 -0
  229. package/ios/renderer/ListItemRenderer.h +24 -0
  230. package/ios/renderer/ListItemRenderer.m +283 -0
  231. package/ios/renderer/ListRenderer.h +13 -0
  232. package/ios/renderer/ListRenderer.m +126 -0
  233. package/ios/renderer/MentionRenderer.h +9 -0
  234. package/ios/renderer/MentionRenderer.m +96 -0
  235. package/ios/renderer/NodeRenderer.h +8 -0
  236. package/ios/renderer/ParagraphRenderer.h +6 -0
  237. package/ios/renderer/ParagraphRenderer.m +85 -0
  238. package/ios/renderer/RenderContext.h +145 -0
  239. package/ios/renderer/RenderContext.m +403 -0
  240. package/ios/renderer/RendererFactory.h +12 -0
  241. package/ios/renderer/RendererFactory.m +158 -0
  242. package/ios/renderer/StrikethroughRenderer.h +5 -0
  243. package/ios/renderer/StrikethroughRenderer.m +30 -0
  244. package/ios/renderer/StrongRenderer.h +5 -0
  245. package/ios/renderer/StrongRenderer.m +88 -0
  246. package/ios/renderer/TextRenderer.h +6 -0
  247. package/ios/renderer/TextRenderer.m +18 -0
  248. package/ios/renderer/ThematicBreakRenderer.h +4 -0
  249. package/ios/renderer/ThematicBreakRenderer.m +42 -0
  250. package/ios/renderer/UnderlineRenderer.h +4 -0
  251. package/ios/renderer/UnderlineRenderer.m +41 -0
  252. package/ios/styles/ENRMFontSlot.h +12 -0
  253. package/ios/styles/ENRMFontSlot.m +24 -0
  254. package/ios/styles/StyleConfig.h +488 -0
  255. package/ios/styles/StyleConfig.mm +2820 -0
  256. package/ios/utils/AccessibilityInfo.h +35 -0
  257. package/ios/utils/AccessibilityInfo.m +24 -0
  258. package/ios/utils/BlockquoteBorder.h +26 -0
  259. package/ios/utils/BlockquoteBorder.m +294 -0
  260. package/ios/utils/CodeBackground.h +19 -0
  261. package/ios/utils/CodeBackground.m +197 -0
  262. package/ios/utils/CodeBlockBackground.h +17 -0
  263. package/ios/utils/CodeBlockBackground.m +103 -0
  264. package/ios/utils/ContextMenuUtils.h +76 -0
  265. package/ios/utils/ContextMenuUtils.mm +75 -0
  266. package/ios/utils/ENRMAccessibilityLabels.h +40 -0
  267. package/ios/utils/ENRMAccessibilityLabels.m +5 -0
  268. package/ios/utils/ENRMAsyncRenderCoordinator.h +24 -0
  269. package/ios/utils/ENRMAsyncRenderCoordinator.m +37 -0
  270. package/ios/utils/ENRMAtomicSize.h +65 -0
  271. package/ios/utils/ENRMCodeBlockContent.h +37 -0
  272. package/ios/utils/ENRMCodeBlockContent.mm +89 -0
  273. package/ios/utils/ENRMFeatureFlags.h +25 -0
  274. package/ios/utils/ENRMImageDownloader.h +30 -0
  275. package/ios/utils/ENRMImageDownloader.m +168 -0
  276. package/ios/utils/ENRMLatexErrorCoordinator.h +27 -0
  277. package/ios/utils/ENRMLatexErrorCoordinator.mm +75 -0
  278. package/ios/utils/ENRMLatexErrorReporting.h +13 -0
  279. package/ios/utils/ENRMLinkContextMenus.h +93 -0
  280. package/ios/utils/ENRMLinkContextMenus.m +178 -0
  281. package/ios/utils/ENRMLinkRegexConfig.h +30 -0
  282. package/ios/utils/ENRMLinkRegexConfig.mm +104 -0
  283. package/ios/utils/ENRMLinkRegexProps.h +23 -0
  284. package/ios/utils/ENRMLocalImageLoader.h +35 -0
  285. package/ios/utils/ENRMLocalImageLoader.m +111 -0
  286. package/ios/utils/ENRMMenuAction.h +27 -0
  287. package/ios/utils/ENRMMenuAction.m +33 -0
  288. package/ios/utils/ENRMSpoilerOverlayManager.h +15 -0
  289. package/ios/utils/ENRMSpoilerOverlayManager.m +19 -0
  290. package/ios/utils/ENRMSpoilerTapUtils.h +6 -0
  291. package/ios/utils/ENRMSpoilerTapUtils.m +11 -0
  292. package/ios/utils/ENRMTailFadeInAnimator.h +16 -0
  293. package/ios/utils/ENRMTailFadeInAnimator.m +161 -0
  294. package/ios/utils/ENRMTextHitTest.h +23 -0
  295. package/ios/utils/ENRMTextHitTest.m +20 -0
  296. package/ios/utils/ENRMTextInteractionUtils.h +20 -0
  297. package/ios/utils/ENRMTextInteractionUtils.m +15 -0
  298. package/ios/utils/ENRMTextRenderer.h +38 -0
  299. package/ios/utils/ENRMTextRenderer.m +54 -0
  300. package/ios/utils/ENRMTextViewSetup.h +132 -0
  301. package/ios/utils/ENRMUIKit.h +429 -0
  302. package/ios/utils/EditMenuUtils.h +105 -0
  303. package/ios/utils/EditMenuUtils.m +143 -0
  304. package/ios/utils/FactoidBackground.h +20 -0
  305. package/ios/utils/FactoidBackground.m +161 -0
  306. package/ios/utils/FontScaleObserver.h +10 -0
  307. package/ios/utils/FontScaleObserver.m +55 -0
  308. package/ios/utils/FontUtils.h +31 -0
  309. package/ios/utils/FontUtils.m +63 -0
  310. package/ios/utils/HTMLGenerator.h +25 -0
  311. package/ios/utils/HTMLGenerator.m +1041 -0
  312. package/ios/utils/HeightUpdateUtils.h +25 -0
  313. package/ios/utils/ImageRequestHeaderUtils.h +35 -0
  314. package/ios/utils/LastElementUtils.h +97 -0
  315. package/ios/utils/LinkContextMenuUtils.h +55 -0
  316. package/ios/utils/LinkPillContentUtils.h +46 -0
  317. package/ios/utils/LinkTapUtils.h +56 -0
  318. package/ios/utils/LinkTapUtils.m +139 -0
  319. package/ios/utils/ListMarkerDrawer.h +15 -0
  320. package/ios/utils/ListMarkerDrawer.m +239 -0
  321. package/ios/utils/MentionBackground.h +26 -0
  322. package/ios/utils/MentionBackground.m +86 -0
  323. package/ios/utils/ParagraphStyleUtils.h +87 -0
  324. package/ios/utils/ParagraphStyleUtils.m +484 -0
  325. package/ios/utils/PasteboardUtils.h +48 -0
  326. package/ios/utils/PasteboardUtils.m +166 -0
  327. package/ios/utils/RTFExportUtils.h +24 -0
  328. package/ios/utils/RTFExportUtils.m +299 -0
  329. package/ios/utils/RuntimeKeys.h +44 -0
  330. package/ios/utils/RuntimeKeys.m +13 -0
  331. package/ios/utils/SelectionColorUtils.h +14 -0
  332. package/ios/utils/StylePropsUtils.h +1340 -0
  333. package/ios/utils/TaskListTapUtils.h +6 -0
  334. package/ios/utils/TaskListTapUtils.m +14 -0
  335. package/ios/utils/TextViewLayoutManager.h +14 -0
  336. package/ios/utils/TextViewLayoutManager.mm +198 -0
  337. package/ios/utils/TiptapAccessibilityElementBuilder.h +43 -0
  338. package/ios/utils/TiptapAccessibilityElementBuilder.m +432 -0
  339. package/ios/utils/TiptapExtractor.h +17 -0
  340. package/ios/utils/TiptapExtractor.m +367 -0
  341. package/ios/views/ENRMTiptapTextView.h +25 -0
  342. package/ios/views/ENRMTiptapTextView.m +247 -0
  343. package/package.json +70 -6
  344. package/react-native.config.js +10 -0
  345. package/src/EnrichedTiptapTextNativeComponent.ts +739 -0
  346. package/src/RichText.tsx +164 -0
  347. package/src/admonitionDefaults.ts +58 -0
  348. package/src/darkModeTextStyle.ts +99 -0
  349. package/src/headingDefaults.ts +21 -0
  350. package/src/index.tsx +4 -0
  351. package/src/linkVariantUtils.ts +120 -0
  352. package/src/normalizeTiptapStyle.ts +397 -0
  353. package/src/rn-globals.d.ts +1 -0
  354. package/src/styleUtils.ts +107 -0
  355. package/src/types/TiptapStyle.ts +532 -0
  356. package/src/types/TiptapStyleInternal.ts +299 -0
  357. package/src/types/linkPill.ts +9 -0
  358. package/src/utils/isDev.ts +4 -0
@@ -0,0 +1,154 @@
1
+ package com.renderrichtext.spans
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.net.Uri
6
+ import android.os.SystemClock
7
+ import android.util.LruCache
8
+ import androidx.annotation.VisibleForTesting
9
+ import androidx.core.graphics.scale
10
+ import com.renderrichtext.utils.text.ImageCache
11
+ import com.renderrichtext.utils.text.ImageDownloader
12
+ import com.renderrichtext.utils.text.LocalImageLoader
13
+ import java.io.File
14
+ import kotlin.math.max
15
+
16
+ /**
17
+ * Original-color pill icon thumbnails, bounded by count and decoded bytes. Local
18
+ * sources decode synchronously through the library's local resolver; remote ones go
19
+ * through the shared image downloader and arrive later.
20
+ */
21
+ internal object LinkPillIconCache {
22
+ // Icons are drawn at the label's font size, so they need few pixels. Local sources
23
+ // are sampled down by the decoder in powers of two; downloads are scaled once.
24
+ private const val LOCAL_MAX_DIMENSION = 512
25
+ private const val REMOTE_MAX_DIMENSION = 256
26
+
27
+ // Every render asks for the same icons again. A local source is trusted for this
28
+ // long before its file is stat'ed (or a failed decode retried); a failed download
29
+ // is not retried sooner than the remote window.
30
+ private const val LOCAL_REVALIDATE_MS = 1_000L
31
+ private const val REMOTE_RETRY_MS = 30_000L
32
+
33
+ private val images = ImageCache.bitmapLruCache(8 * 1024 * 1024, 64)
34
+ private val sources = LruCache<String, Source>(256)
35
+
36
+ private class Source(
37
+ val imageKey: String,
38
+ val checkedAt: Long,
39
+ val failed: Boolean,
40
+ val isFile: Boolean = false,
41
+ )
42
+
43
+ /** Replaceable so unit tests never touch the network. */
44
+ @VisibleForTesting
45
+ internal var remoteLoader: (Context, String, Map<String, String>, (Bitmap?) -> Unit) -> Unit = ImageDownloader::download
46
+
47
+ @VisibleForTesting
48
+ internal var clock: () -> Long = SystemClock::uptimeMillis
49
+
50
+ fun isRemote(iconUri: String): Boolean =
51
+ iconUri.startsWith("http://", ignoreCase = true) || iconUri.startsWith("https://", ignoreCase = true)
52
+
53
+ /** Synchronously resolves a local icon; null for remote, empty and unreadable sources. */
54
+ @Synchronized
55
+ fun load(
56
+ context: Context,
57
+ iconUri: String,
58
+ ): Bitmap? {
59
+ if (iconUri.isEmpty() || isRemote(iconUri)) return null
60
+ val key = localKey(context, iconUri)
61
+ val now = clock()
62
+ val known = sources.get(key)
63
+ // File-backed and failed sources are re-checked once the window passes.
64
+ if (known != null && ((!known.isFile && !known.failed) || now - known.checkedAt < LOCAL_REVALIDATE_MS)) {
65
+ if (known.failed) return null
66
+ images.get(known.imageKey)?.let { return it }
67
+ }
68
+
69
+ // Only a real file can change underneath its URI; asset, resource, content and
70
+ // data sources keep one entry.
71
+ val file = localFile(iconUri)?.takeIf { it.isFile }
72
+ val imageKey = if (file != null) "$key|${file.lastModified()}|${file.length()}" else key
73
+ val bitmap =
74
+ images.get(imageKey)
75
+ ?: runCatching { LocalImageLoader.load(context, iconUri, maxDimension = LOCAL_MAX_DIMENSION) }
76
+ .getOrNull()
77
+ ?.also { images.put(imageKey, it) }
78
+ sources.put(key, Source(imageKey, now, failed = bitmap == null, isFile = file != null))
79
+ return bitmap
80
+ }
81
+
82
+ fun hasFailedRecently(
83
+ iconUri: String,
84
+ headers: Map<String, String>,
85
+ ): Boolean =
86
+ synchronized(this) {
87
+ val known = sources.get(remoteKey(iconUri, headers))
88
+ known != null && known.failed && clock() - known.checkedAt < REMOTE_RETRY_MS
89
+ }
90
+
91
+ private fun remoteKey(
92
+ iconUri: String,
93
+ headers: Map<String, String>,
94
+ ) = "remote|" + ImageCache.requestKey(iconUri, headers)
95
+
96
+ /**
97
+ * Returns the cached remote icon, or null after starting a download that reports
98
+ * through [onLoaded] on the main thread, with null when the download fails. [onLoaded]
99
+ * can also run before this returns when the downloader already holds the image.
100
+ */
101
+ fun loadRemote(
102
+ context: Context,
103
+ iconUri: String,
104
+ headers: Map<String, String>,
105
+ onLoaded: (Bitmap?) -> Unit,
106
+ ): Bitmap? {
107
+ val key = remoteKey(iconUri, headers)
108
+ synchronized(this) {
109
+ images.get(key)?.let { return it }
110
+ val known = sources.get(key)
111
+ if (known != null && known.failed && clock() - known.checkedAt < REMOTE_RETRY_MS) return null
112
+ }
113
+ remoteLoader(context, iconUri, headers) { downloaded ->
114
+ val icon =
115
+ synchronized(this) {
116
+ // Every pill waiting for this download gets the same thumbnail.
117
+ val shared = images.get(key) ?: downloaded?.let { thumbnail(it) }?.also { images.put(key, it) }
118
+ sources.put(key, Source(key, clock(), failed = shared == null))
119
+ shared
120
+ }
121
+ onLoaded(icon)
122
+ }
123
+ return null
124
+ }
125
+
126
+ private fun thumbnail(bitmap: Bitmap): Bitmap {
127
+ val longest = max(bitmap.width, bitmap.height)
128
+ if (longest <= REMOTE_MAX_DIMENSION) return bitmap
129
+ val ratio = REMOTE_MAX_DIMENSION.toFloat() / longest
130
+ return bitmap.scale((bitmap.width * ratio).toInt().coerceAtLeast(1), (bitmap.height * ratio).toInt().coerceAtLeast(1))
131
+ }
132
+
133
+ // Data URIs can be large: key them by a cheap fingerprint instead of retaining
134
+ // (or digesting) the whole payload on every render.
135
+ private fun localKey(
136
+ context: Context,
137
+ iconUri: String,
138
+ ): String {
139
+ val source =
140
+ if (iconUri.startsWith("data:", ignoreCase = true)) {
141
+ "data|${iconUri.length}|${iconUri.hashCode()}|${iconUri.takeLast(32)}"
142
+ } else {
143
+ iconUri
144
+ }
145
+ return "${context.packageName}|$source"
146
+ }
147
+
148
+ private fun localFile(iconUri: String): File? =
149
+ when {
150
+ iconUri.startsWith('/') -> File(iconUri)
151
+ iconUri.startsWith("file:", ignoreCase = true) -> Uri.parse(iconUri).path?.let { File(it) }
152
+ else -> null
153
+ }
154
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,68 @@
1
+ package com.renderrichtext.spans
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.text.TextPaint
6
+ import android.text.style.ClickableSpan
7
+ import android.view.View
8
+ import com.renderrichtext.EnrichedTiptapText
9
+ import com.renderrichtext.renderer.BlockStyle
10
+ import com.renderrichtext.renderer.SpanStyleCache
11
+ import com.renderrichtext.utils.text.extensions.applyBlockStyleFont
12
+
13
+ class LinkSpan(
14
+ val url: String,
15
+ private val onLinkPress: ((String) -> Unit)?,
16
+ private val onLinkLongPress: ((String) -> Unit)?,
17
+ private val styleCache: SpanStyleCache,
18
+ private val blockStyle: BlockStyle,
19
+ private val context: Context,
20
+ val recognizedLink: Boolean = false,
21
+ // A recognized code span keeps its code font unless a link or variant font is set.
22
+ private val keepsCodeFont: Boolean = false,
23
+ ) : ClickableSpan() {
24
+ @Volatile
25
+ private var longPressTriggered = false
26
+
27
+ override fun onClick(widget: View) {
28
+ if (longPressTriggered) {
29
+ longPressTriggered = false
30
+ return
31
+ }
32
+
33
+ onLinkPress?.invoke(url) ?: (widget as? EnrichedTiptapText)?.emitOnLinkPress(url)
34
+ }
35
+
36
+ fun onLongClick(widget: View): Boolean {
37
+ longPressTriggered = true
38
+
39
+ (widget as? EnrichedTiptapText)?.emitOnLinkLongPress(url)
40
+
41
+ onLinkLongPress?.invoke(url)
42
+
43
+ return true
44
+ }
45
+
46
+ override fun updateDrawState(textPaint: TextPaint) {
47
+ super.updateDrawState(textPaint)
48
+
49
+ val variant = styleCache.resolvedVariantForUrl(url)
50
+
51
+ val fontFamily = variant?.fontFamily?.takeIf { it.isNotEmpty() } ?: styleCache.linkFontFamily
52
+ if (fontFamily.isNotEmpty()) {
53
+ textPaint.textSize = blockStyle.fontSize
54
+ textPaint.applyBlockStyleFont(blockStyle.copy(fontFamily = fontFamily), context)
55
+ } else if (!keepsCodeFont) {
56
+ textPaint.textSize = blockStyle.fontSize
57
+ textPaint.applyBlockStyleFont(blockStyle, context)
58
+ }
59
+
60
+ textPaint.color = variant?.color ?: styleCache.linkColor
61
+ textPaint.isUnderlineText = variant?.underline ?: styleCache.linkUnderline
62
+
63
+ val backgroundColor = variant?.backgroundColor ?: styleCache.linkBackgroundColor
64
+ if (Color.alpha(backgroundColor) > 0) {
65
+ textPaint.bgColor = backgroundColor
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,33 @@
1
+ package com.renderrichtext.spans
2
+
3
+ import android.graphics.Paint
4
+ import android.text.Spanned
5
+ import android.text.style.LineHeightSpan
6
+
7
+ /**
8
+ * Adds vertical spacing between list items using LineHeightSpan.
9
+ *
10
+ * Attached over the newline that separates two items; extends only the line
11
+ * ending at that newline so the gap appears below the preceding item.
12
+ *
13
+ * @param spacing The spacing in pixels to add below the line (0 = no spacing)
14
+ */
15
+ class ListItemSpacingSpan(
16
+ val spacing: Float,
17
+ ) : LineHeightSpan {
18
+ override fun chooseHeight(
19
+ text: CharSequence,
20
+ start: Int,
21
+ end: Int,
22
+ spanstartv: Int,
23
+ lineHeight: Int,
24
+ fm: Paint.FontMetricsInt,
25
+ ) {
26
+ val spanEnd = (text as? Spanned)?.getSpanEnd(this) ?: return
27
+ if (end != spanEnd) return
28
+
29
+ val spacingPixels = spacing.toInt()
30
+ fm.descent += spacingPixels
31
+ fm.bottom += spacingPixels
32
+ }
33
+ }
@@ -0,0 +1,38 @@
1
+ package com.renderrichtext.spans
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.Paint
5
+ import android.text.Layout
6
+ import android.text.Spanned
7
+ import android.text.style.LeadingMarginSpan
8
+
9
+ // Draws a list marker on a line owned by another block (code block, nested
10
+ // sublist) without contributing any margin of its own.
11
+ class ListMarkerAnchorSpan(
12
+ internal val marker: BaseListSpan,
13
+ ) : LeadingMarginSpan {
14
+ override fun getLeadingMargin(first: Boolean): Int = 0
15
+
16
+ override fun drawLeadingMargin(
17
+ c: Canvas,
18
+ p: Paint,
19
+ x: Int,
20
+ dir: Int,
21
+ top: Int,
22
+ baseline: Int,
23
+ bottom: Int,
24
+ text: CharSequence?,
25
+ start: Int,
26
+ end: Int,
27
+ first: Boolean,
28
+ layout: Layout?,
29
+ ) {
30
+ if (!first) return
31
+ if (text is Spanned && text.getSpanStart(this) != start) return
32
+
33
+ // Pass the paragraph origin: drawMarker reconstructs its offset from depth,
34
+ // matching the x its own span would receive on a plain text line.
35
+ val originX = if (dir > 0) 0 else layout?.width ?: 0
36
+ marker.drawMarkerAt(c, p, originX, dir, top, baseline, bottom, layout, start)
37
+ }
38
+ }