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