@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.
- package/LICENSE +20 -20
- package/README.md +140 -140
- package/ReactNativeRenderRichText.podspec +23 -23
- package/android/build.gradle +86 -86
- package/android/consumer-rules.pro +3 -3
- package/android/src/main/AndroidManifest.xml +2 -2
- package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextLayoutManager.kt +24 -24
- package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextPackage.kt +13 -13
- package/android/src/main/java/com/renderrichtext/MeasurementStore.kt +644 -644
- package/android/src/main/java/com/renderrichtext/accessibility/AccessibilityLabels.kt +17 -17
- package/android/src/main/java/com/renderrichtext/accessibility/AccessibleTiptapTextView.kt +112 -112
- package/android/src/main/java/com/renderrichtext/accessibility/TiptapAccessibilityHelper.kt +576 -576
- package/android/src/main/java/com/renderrichtext/events/CodeBlockPressEvent.kt +24 -24
- package/android/src/main/java/com/renderrichtext/events/ContextMenuItemPressEvent.kt +28 -28
- package/android/src/main/java/com/renderrichtext/events/CopyPressEvent.kt +24 -24
- package/android/src/main/java/com/renderrichtext/events/ImagePressEvent.kt +25 -25
- package/android/src/main/java/com/renderrichtext/events/LatexErrorEvent.kt +29 -29
- package/android/src/main/java/com/renderrichtext/events/LinkLongPressEvent.kt +23 -23
- package/android/src/main/java/com/renderrichtext/events/LinkPressEvent.kt +23 -23
- package/android/src/main/java/com/renderrichtext/events/MentionLongPressEvent.kt +25 -25
- package/android/src/main/java/com/renderrichtext/events/MentionPressEvent.kt +25 -25
- package/android/src/main/java/com/renderrichtext/events/TaskListItemPressEvent.kt +26 -26
- package/android/src/main/java/com/renderrichtext/math/LatexErrorReporter.kt +9 -9
- package/android/src/main/java/com/renderrichtext/parser/Parser.kt +59 -59
- package/android/src/main/java/com/renderrichtext/parser/TextLinkMatching.kt +35 -35
- package/android/src/main/java/com/renderrichtext/renderer/BaselineShiftRenderer.kt +45 -45
- package/android/src/main/java/com/renderrichtext/renderer/BlankLineRenderer.kt +39 -39
- package/android/src/main/java/com/renderrichtext/renderer/BlockquoteRenderer.kt +243 -243
- package/android/src/main/java/com/renderrichtext/renderer/BlockquoteTextRenderer.kt +56 -56
- package/android/src/main/java/com/renderrichtext/renderer/CodeBlockRenderer.kt +135 -135
- package/android/src/main/java/com/renderrichtext/renderer/CodeRenderer.kt +37 -37
- package/android/src/main/java/com/renderrichtext/renderer/EmphasisRenderer.kt +28 -28
- package/android/src/main/java/com/renderrichtext/renderer/HeadingRenderer.kt +81 -81
- package/android/src/main/java/com/renderrichtext/renderer/HighlightRenderer.kt +26 -26
- package/android/src/main/java/com/renderrichtext/renderer/LineBreakRenderer.kt +17 -17
- package/android/src/main/java/com/renderrichtext/renderer/LinkRenderer.kt +90 -90
- package/android/src/main/java/com/renderrichtext/renderer/ListContextManager.kt +109 -109
- package/android/src/main/java/com/renderrichtext/renderer/ListItemRenderer.kt +211 -211
- package/android/src/main/java/com/renderrichtext/renderer/ListRenderer.kt +133 -133
- package/android/src/main/java/com/renderrichtext/renderer/MentionRenderer.kt +85 -85
- package/android/src/main/java/com/renderrichtext/renderer/ParagraphRenderer.kt +91 -91
- package/android/src/main/java/com/renderrichtext/renderer/Renderer.kt +148 -148
- package/android/src/main/java/com/renderrichtext/renderer/SoftBreakRenderer.kt +17 -17
- package/android/src/main/java/com/renderrichtext/renderer/SpanStyleCache.kt +143 -143
- package/android/src/main/java/com/renderrichtext/renderer/StrikethroughRenderer.kt +28 -28
- package/android/src/main/java/com/renderrichtext/renderer/StrongRenderer.kt +28 -28
- package/android/src/main/java/com/renderrichtext/renderer/TextRenderer.kt +31 -31
- package/android/src/main/java/com/renderrichtext/renderer/ThematicBreakRenderer.kt +46 -46
- package/android/src/main/java/com/renderrichtext/renderer/UnderlineRenderer.kt +28 -28
- package/android/src/main/java/com/renderrichtext/spans/BaseListSpan.kt +164 -164
- package/android/src/main/java/com/renderrichtext/spans/BaselineShiftSpan.kt +27 -27
- package/android/src/main/java/com/renderrichtext/spans/BlockquoteSpan.kt +302 -302
- package/android/src/main/java/com/renderrichtext/spans/CodeBackgroundSpan.kt +205 -205
- package/android/src/main/java/com/renderrichtext/spans/CodeBlockSpan.kt +216 -216
- package/android/src/main/java/com/renderrichtext/spans/CodeSpan.kt +32 -32
- package/android/src/main/java/com/renderrichtext/spans/EmphasisSpan.kt +42 -42
- package/android/src/main/java/com/renderrichtext/spans/FadeInSpan.kt +29 -29
- package/android/src/main/java/com/renderrichtext/spans/HeadingSpan.kt +38 -38
- package/android/src/main/java/com/renderrichtext/spans/HighlightSpan.kt +80 -80
- package/android/src/main/java/com/renderrichtext/spans/ImageSpan.kt +481 -481
- package/android/src/main/java/com/renderrichtext/spans/InlineBackgroundGeometry.kt +42 -42
- package/android/src/main/java/com/renderrichtext/spans/LineHeightSpan.kt +39 -39
- package/android/src/main/java/com/renderrichtext/spans/LinkPillIconCache.kt +154 -154
- package/android/src/main/java/com/renderrichtext/spans/LinkPillSpan.kt +381 -381
- package/android/src/main/java/com/renderrichtext/spans/LinkSpan.kt +68 -68
- package/android/src/main/java/com/renderrichtext/spans/ListItemSpacingSpan.kt +33 -33
- package/android/src/main/java/com/renderrichtext/spans/ListMarkerAnchorSpan.kt +38 -38
- package/android/src/main/java/com/renderrichtext/spans/MarginBottomSpan.kt +76 -76
- package/android/src/main/java/com/renderrichtext/spans/MentionChipSpan.kt +132 -132
- package/android/src/main/java/com/renderrichtext/spans/MentionSpan.kt +46 -46
- package/android/src/main/java/com/renderrichtext/spans/OrderedListSpan.kt +127 -127
- package/android/src/main/java/com/renderrichtext/spans/SpoilerSpan.kt +33 -33
- package/android/src/main/java/com/renderrichtext/spans/StrikethroughSpan.kt +12 -12
- package/android/src/main/java/com/renderrichtext/spans/StrongSpan.kt +39 -39
- package/android/src/main/java/com/renderrichtext/spans/TaskListSpan.kt +143 -143
- package/android/src/main/java/com/renderrichtext/spans/TextSpan.kt +26 -26
- package/android/src/main/java/com/renderrichtext/spans/ThematicBreakSpan.kt +69 -69
- package/android/src/main/java/com/renderrichtext/spans/UnorderedListSpan.kt +96 -96
- package/android/src/main/java/com/renderrichtext/spoiler/SpoilerStubs.kt +46 -46
- package/android/src/main/java/com/renderrichtext/styles/BaseBlockStyle.kt +11 -11
- package/android/src/main/java/com/renderrichtext/styles/BlockquoteStyle.kt +83 -83
- package/android/src/main/java/com/renderrichtext/styles/CodeBlockStyle.kt +80 -80
- package/android/src/main/java/com/renderrichtext/styles/CodeStyle.kt +26 -26
- package/android/src/main/java/com/renderrichtext/styles/EmphasisStyle.kt +21 -21
- package/android/src/main/java/com/renderrichtext/styles/FactoidStyle.kt +21 -21
- package/android/src/main/java/com/renderrichtext/styles/HeadingStyle.kt +33 -33
- package/android/src/main/java/com/renderrichtext/styles/HighlightStyle.kt +19 -19
- package/android/src/main/java/com/renderrichtext/styles/ImageStyle.kt +29 -29
- package/android/src/main/java/com/renderrichtext/styles/InlineImageStyle.kt +17 -17
- package/android/src/main/java/com/renderrichtext/styles/InlineMathStyle.kt +17 -17
- package/android/src/main/java/com/renderrichtext/styles/LinkPillContent.kt +36 -36
- package/android/src/main/java/com/renderrichtext/styles/LinkPillStyle.kt +44 -44
- package/android/src/main/java/com/renderrichtext/styles/LinkStyle.kt +23 -23
- package/android/src/main/java/com/renderrichtext/styles/LinkVariantEntry.kt +34 -34
- package/android/src/main/java/com/renderrichtext/styles/ListStyle.kt +65 -65
- package/android/src/main/java/com/renderrichtext/styles/MathStyle.kt +29 -29
- package/android/src/main/java/com/renderrichtext/styles/MentionStyle.kt +19 -19
- package/android/src/main/java/com/renderrichtext/styles/ParagraphStyle.kt +33 -33
- package/android/src/main/java/com/renderrichtext/styles/SpoilerStyle.kt +27 -27
- package/android/src/main/java/com/renderrichtext/styles/StrikethroughStyle.kt +17 -17
- package/android/src/main/java/com/renderrichtext/styles/StrongStyle.kt +21 -21
- package/android/src/main/java/com/renderrichtext/styles/StyleConfig.kt +411 -411
- package/android/src/main/java/com/renderrichtext/styles/StyleParser.kt +92 -92
- package/android/src/main/java/com/renderrichtext/styles/SubscriptStyle.kt +16 -16
- package/android/src/main/java/com/renderrichtext/styles/SuperscriptStyle.kt +16 -16
- package/android/src/main/java/com/renderrichtext/styles/TableStyle.kt +75 -75
- package/android/src/main/java/com/renderrichtext/styles/TaskListStyle.kt +29 -29
- package/android/src/main/java/com/renderrichtext/styles/TextAlignment.kt +32 -32
- package/android/src/main/java/com/renderrichtext/styles/ThematicBreakStyle.kt +23 -23
- package/android/src/main/java/com/renderrichtext/styles/UnderlineStyle.kt +17 -17
- package/android/src/main/java/com/renderrichtext/styles/VideoStyle.kt +31 -31
- package/android/src/main/java/com/renderrichtext/utils/common/BorderStyleUtils.kt +86 -86
- package/android/src/main/java/com/renderrichtext/utils/common/BreakStrategyUtils.kt +32 -32
- package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockHighlighter.kt +36 -36
- package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockNode.kt +42 -42
- package/android/src/main/java/com/renderrichtext/utils/common/EllipsizeUtils.kt +27 -27
- package/android/src/main/java/com/renderrichtext/utils/common/EnrichedTiptapAncestorUtils.kt +10 -10
- package/android/src/main/java/com/renderrichtext/utils/common/FeatureFlags.kt +9 -9
- package/android/src/main/java/com/renderrichtext/utils/common/LinkRecognitionProps.kt +31 -31
- package/android/src/main/java/com/renderrichtext/utils/common/LinkRegexConfig.kt +56 -56
- package/android/src/main/java/com/renderrichtext/utils/common/MotionPreferences.kt +18 -18
- package/android/src/main/java/com/renderrichtext/utils/common/ReadableMapExtensions.kt +28 -28
- package/android/src/main/java/com/renderrichtext/utils/common/layout/RTLUtils.kt +6 -6
- package/android/src/main/java/com/renderrichtext/utils/common/serialization/TiptapASTSerializer.kt +368 -368
- package/android/src/main/java/com/renderrichtext/utils/text/ImageCache.kt +88 -88
- package/android/src/main/java/com/renderrichtext/utils/text/ImageDownloader.kt +203 -203
- package/android/src/main/java/com/renderrichtext/utils/text/LocalImageLoader.kt +167 -167
- package/android/src/main/java/com/renderrichtext/utils/text/TailFadeInAnimator.kt +84 -84
- package/android/src/main/java/com/renderrichtext/utils/text/conversion/HTMLGenerator.kt +1105 -1105
- package/android/src/main/java/com/renderrichtext/utils/text/conversion/TiptapExtractor.kt +423 -423
- package/android/src/main/java/com/renderrichtext/utils/text/extensions/ParagraphUtils.kt +9 -9
- package/android/src/main/java/com/renderrichtext/utils/text/extensions/SpannableExtensions.kt +9 -9
- package/android/src/main/java/com/renderrichtext/utils/text/extensions/TextPaintExtensions.kt +76 -76
- package/android/src/main/java/com/renderrichtext/utils/text/interaction/CheckboxTouchHelper.kt +61 -61
- package/android/src/main/java/com/renderrichtext/utils/text/interaction/TaskListTapUtils.kt +286 -286
- package/android/src/main/java/com/renderrichtext/utils/text/span/ApplyLineHeight.kt +39 -39
- package/android/src/main/java/com/renderrichtext/utils/text/span/BlockSpacing.kt +142 -142
- package/android/src/main/java/com/renderrichtext/utils/text/span/SpanFlags.kt +43 -43
- package/android/src/main/java/com/renderrichtext/utils/text/span/WidthAwareSpans.kt +30 -30
- package/android/src/main/java/com/renderrichtext/utils/text/view/CodeBlockPressHost.kt +15 -15
- package/android/src/main/java/com/renderrichtext/utils/text/view/ImagePressHost.kt +19 -19
- package/android/src/main/java/com/renderrichtext/utils/text/view/SelectionActionMode.kt +235 -235
- package/android/src/main/java/com/renderrichtext/utils/text/view/TextSelectionColors.kt +48 -48
- package/android/src/main/java/com/renderrichtext/utils/text/view/TextViewSetup.kt +119 -119
- package/android/src/main/java/com/renderrichtext/views/ContextMenuPopup.kt +217 -217
- package/android/src/main/java/com/renderrichtext/views/CopyGlyph.kt +101 -101
- package/android/src/main/java/com/renderrichtext/views/WrapGlyph.kt +40 -40
- package/android/src/main/jni/CMakeLists.txt +52 -52
- package/android/src/main/jni/react/renderer/components/RichTextSpec/ComponentDescriptors.h +30 -30
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextComponentDescriptor.h +29 -29
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.cpp +45 -45
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.h +21 -21
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.cpp +38 -38
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.h +53 -53
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.cpp +9 -9
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.h +24 -24
- package/android/src/main/jni/react/renderer/components/RichTextSpec/conversions.h +40 -40
- package/cpp/parser/TiptapParser.hpp +13 -13
- package/cpp/vendor/nlohmann/json.hpp +24765 -24765
- package/ios/EnrichedTiptapText.h +27 -27
- package/ios/attachments/ENRMImageAttachment.h +51 -51
- package/ios/attachments/ENRMImageAttachment.m +398 -398
- package/ios/attachments/ENRMLinkPillAttachment.h +47 -47
- package/ios/attachments/ENRMLinkPillAttachment.m +324 -324
- package/ios/attachments/ENRMLinkPillIconCache.h +28 -28
- package/ios/attachments/ENRMLinkPillIconCache.m +181 -181
- package/ios/attachments/ENRMLinkPillText.h +27 -27
- package/ios/attachments/ENRMLinkPillText.m +118 -118
- package/ios/attachments/ENRMMathInlineAttachment.h +7 -7
- package/ios/attachments/ENRMMathInlineAttachment.m +4 -4
- package/ios/attachments/ThematicBreakAttachment.h +15 -15
- package/ios/attachments/ThematicBreakAttachment.m +33 -33
- package/ios/internals/ENRMViewFreeMeasurement.h +208 -208
- package/ios/internals/EnrichedTiptapTextComponentDescriptor.h +19 -19
- package/ios/internals/EnrichedTiptapTextShadowNode.h +43 -43
- package/ios/internals/EnrichedTiptapTextShadowNode.mm +69 -69
- package/ios/internals/EnrichedTiptapTextState.h +24 -24
- package/ios/internals/MeasurementCache.h +308 -308
- package/ios/internals/ShadowMeasurementUtils.h +145 -145
- package/ios/parser/ENRMTextLinkAttributes.h +7 -7
- package/ios/parser/ENRMTiptapParser.h +33 -33
- package/ios/parser/ENRMTiptapParser.mm +73 -73
- package/ios/parser/TiptapASTNode.m +52 -52
- package/ios/renderer/AttributedRenderer.h +23 -23
- package/ios/renderer/AttributedRenderer.m +229 -229
- package/ios/renderer/BaseRenderer.h +33 -33
- package/ios/renderer/BaseRenderer.m +46 -46
- package/ios/renderer/BaselineShiftTextAttributes.h +10 -10
- package/ios/renderer/BaselineShiftTextAttributes.m +26 -26
- package/ios/renderer/BlankLineRenderer.h +4 -4
- package/ios/renderer/BlankLineRenderer.m +51 -51
- package/ios/renderer/BlockquoteRenderer.h +6 -6
- package/ios/renderer/BlockquoteRenderer.m +258 -258
- package/ios/renderer/CodeBlockRenderer.h +9 -9
- package/ios/renderer/CodeBlockRenderer.m +99 -99
- package/ios/renderer/CodeRenderer.h +9 -9
- package/ios/renderer/CodeRenderer.m +69 -69
- package/ios/renderer/ENRMBlockquoteTextRenderer.h +25 -25
- package/ios/renderer/ENRMBlockquoteTextRenderer.m +48 -48
- package/ios/renderer/EmphasisRenderer.h +5 -5
- package/ios/renderer/EmphasisRenderer.m +99 -99
- package/ios/renderer/HeadingRenderer.h +6 -6
- package/ios/renderer/HeadingRenderer.m +102 -102
- package/ios/renderer/HighlightRenderer.h +7 -7
- package/ios/renderer/HighlightRenderer.m +33 -33
- package/ios/renderer/LinkRenderer.h +6 -6
- package/ios/renderer/LinkRenderer.m +157 -157
- package/ios/renderer/ListItemRenderer.h +24 -24
- package/ios/renderer/ListItemRenderer.m +283 -283
- package/ios/renderer/ListRenderer.h +13 -13
- package/ios/renderer/ListRenderer.m +126 -126
- package/ios/renderer/MentionRenderer.h +9 -9
- package/ios/renderer/MentionRenderer.m +96 -96
- package/ios/renderer/NodeRenderer.h +8 -8
- package/ios/renderer/ParagraphRenderer.h +6 -6
- package/ios/renderer/ParagraphRenderer.m +85 -85
- package/ios/renderer/RenderContext.h +145 -145
- package/ios/renderer/RenderContext.m +402 -402
- package/ios/renderer/RendererFactory.h +12 -12
- package/ios/renderer/StrikethroughRenderer.h +5 -5
- package/ios/renderer/StrikethroughRenderer.m +30 -30
- package/ios/renderer/StrongRenderer.h +5 -5
- package/ios/renderer/StrongRenderer.m +87 -87
- package/ios/renderer/TextRenderer.h +6 -6
- package/ios/renderer/TextRenderer.m +18 -18
- package/ios/renderer/ThematicBreakRenderer.h +4 -4
- package/ios/renderer/ThematicBreakRenderer.m +41 -41
- package/ios/renderer/UnderlineRenderer.h +4 -4
- package/ios/renderer/UnderlineRenderer.m +41 -41
- package/ios/styles/ENRMFontSlot.h +12 -12
- package/ios/styles/ENRMFontSlot.m +24 -24
- package/ios/styles/StyleConfig.h +488 -488
- package/ios/styles/StyleConfig.mm +2820 -2820
- package/ios/utils/AccessibilityInfo.h +35 -35
- package/ios/utils/AccessibilityInfo.m +24 -24
- package/ios/utils/BlockquoteBorder.h +26 -26
- package/ios/utils/BlockquoteBorder.m +294 -294
- package/ios/utils/CodeBackground.h +19 -19
- package/ios/utils/CodeBackground.m +196 -196
- package/ios/utils/CodeBlockBackground.h +17 -17
- package/ios/utils/CodeBlockBackground.m +102 -102
- package/ios/utils/ContextMenuUtils.h +76 -76
- package/ios/utils/ContextMenuUtils.mm +75 -75
- package/ios/utils/ENRMAccessibilityLabels.h +40 -40
- package/ios/utils/ENRMAccessibilityLabels.m +5 -5
- package/ios/utils/ENRMAsyncRenderCoordinator.h +24 -24
- package/ios/utils/ENRMAsyncRenderCoordinator.m +37 -37
- package/ios/utils/ENRMAtomicSize.h +65 -65
- package/ios/utils/ENRMCodeBlockContent.h +37 -37
- package/ios/utils/ENRMCodeBlockContent.mm +89 -89
- package/ios/utils/ENRMFeatureFlags.h +25 -25
- package/ios/utils/ENRMImageDownloader.h +30 -30
- package/ios/utils/ENRMImageDownloader.m +168 -168
- package/ios/utils/ENRMLatexErrorCoordinator.h +27 -27
- package/ios/utils/ENRMLatexErrorCoordinator.mm +75 -75
- package/ios/utils/ENRMLatexErrorReporting.h +13 -13
- package/ios/utils/ENRMLinkContextMenus.h +93 -93
- package/ios/utils/ENRMLinkContextMenus.m +178 -178
- package/ios/utils/ENRMLinkRegexConfig.h +30 -30
- package/ios/utils/ENRMLinkRegexConfig.mm +104 -104
- package/ios/utils/ENRMLinkRegexProps.h +23 -23
- package/ios/utils/ENRMLocalImageLoader.h +35 -35
- package/ios/utils/ENRMLocalImageLoader.m +111 -111
- package/ios/utils/ENRMMenuAction.h +27 -27
- package/ios/utils/ENRMMenuAction.m +33 -33
- package/ios/utils/ENRMSpoilerOverlayManager.h +15 -15
- package/ios/utils/ENRMSpoilerOverlayManager.m +19 -19
- package/ios/utils/ENRMSpoilerTapUtils.h +6 -6
- package/ios/utils/ENRMSpoilerTapUtils.m +11 -11
- package/ios/utils/ENRMTailFadeInAnimator.h +16 -16
- package/ios/utils/ENRMTailFadeInAnimator.m +161 -161
- package/ios/utils/ENRMTextHitTest.h +23 -23
- package/ios/utils/ENRMTextHitTest.m +20 -20
- package/ios/utils/ENRMTextInteractionUtils.h +20 -20
- package/ios/utils/ENRMTextInteractionUtils.m +15 -15
- package/ios/utils/ENRMTextRenderer.h +38 -38
- package/ios/utils/ENRMTextRenderer.m +54 -54
- package/ios/utils/ENRMTextViewSetup.h +132 -132
- package/ios/utils/ENRMUIKit.h +429 -429
- package/ios/utils/EditMenuUtils.h +105 -105
- package/ios/utils/EditMenuUtils.m +143 -143
- package/ios/utils/FontScaleObserver.h +10 -10
- package/ios/utils/FontScaleObserver.m +55 -55
- package/ios/utils/FontUtils.h +31 -31
- package/ios/utils/FontUtils.m +63 -63
- package/ios/utils/HTMLGenerator.h +25 -25
- package/ios/utils/HTMLGenerator.m +1041 -1041
- package/ios/utils/HeightUpdateUtils.h +25 -25
- package/ios/utils/ImageRequestHeaderUtils.h +35 -35
- package/ios/utils/LastElementUtils.h +97 -97
- package/ios/utils/LinkContextMenuUtils.h +55 -55
- package/ios/utils/LinkPillContentUtils.h +46 -46
- package/ios/utils/ListMarkerDrawer.h +15 -15
- package/ios/utils/ListMarkerDrawer.m +239 -239
- package/ios/utils/MentionBackground.h +26 -26
- package/ios/utils/MentionBackground.m +86 -86
- package/ios/utils/ParagraphStyleUtils.h +87 -87
- package/ios/utils/ParagraphStyleUtils.m +484 -484
- package/ios/utils/PasteboardUtils.h +48 -48
- package/ios/utils/PasteboardUtils.m +166 -166
- package/ios/utils/RTFExportUtils.h +24 -24
- package/ios/utils/RTFExportUtils.m +299 -299
- package/ios/utils/SelectionColorUtils.h +14 -14
- package/ios/utils/StylePropsUtils.h +1340 -1340
- package/ios/utils/TaskListTapUtils.h +6 -6
- package/ios/utils/TaskListTapUtils.m +14 -14
- package/ios/utils/TextViewLayoutManager.h +14 -14
- package/ios/utils/TiptapAccessibilityElementBuilder.h +43 -43
- package/ios/utils/TiptapAccessibilityElementBuilder.m +432 -432
- package/ios/utils/TiptapExtractor.h +17 -17
- package/ios/utils/TiptapExtractor.m +367 -367
- package/ios/views/ENRMTiptapTextView.h +25 -25
- package/ios/views/ENRMTiptapTextView.m +247 -247
- package/package.json +65 -70
- package/react-native.config.js +10 -10
- package/src/admonitionDefaults.ts +58 -58
- package/src/darkModeTextStyle.ts +99 -99
- package/src/headingDefaults.ts +21 -21
- package/src/index.tsx +4 -4
- package/src/linkVariantUtils.ts +120 -120
- package/src/normalizeTiptapStyle.ts +397 -397
- package/src/rn-globals.d.ts +1 -1
- package/src/styleUtils.ts +107 -107
- package/src/types/TiptapStyle.ts +532 -532
- package/src/types/TiptapStyleInternal.ts +299 -299
- package/src/types/linkPill.ts +9 -9
- 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
|
+
}
|