@mathers101/react-native-render-rich-text 0.1.10 → 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,644 +1,644 @@
|
|
|
1
|
-
package com.renderrichtext
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.graphics.Typeface
|
|
5
|
-
import android.os.Build
|
|
6
|
-
import android.text.Spannable
|
|
7
|
-
import android.text.Spanned
|
|
8
|
-
import android.text.StaticLayout
|
|
9
|
-
import android.text.TextPaint
|
|
10
|
-
import android.text.TextUtils.TruncateAt
|
|
11
|
-
import android.util.Log
|
|
12
|
-
import com.facebook.react.bridge.ReadableMap
|
|
13
|
-
import com.facebook.react.uimanager.PixelUtil
|
|
14
|
-
import com.facebook.yoga.YogaMeasureMode
|
|
15
|
-
import com.facebook.yoga.YogaMeasureOutput
|
|
16
|
-
import com.renderrichtext.parser.Md4cFlags
|
|
17
|
-
import com.renderrichtext.parser.Parser
|
|
18
|
-
import com.renderrichtext.renderer.Renderer
|
|
19
|
-
import com.renderrichtext.spans.MarginBottomSpan
|
|
20
|
-
import com.renderrichtext.styles.LinkPillContent
|
|
21
|
-
import com.renderrichtext.styles.StyleConfig
|
|
22
|
-
import com.renderrichtext.utils.common.BreakStrategyUtils
|
|
23
|
-
import com.renderrichtext.utils.common.EllipsizeUtils
|
|
24
|
-
import com.renderrichtext.utils.common.LinkRegexConfig
|
|
25
|
-
import com.renderrichtext.utils.common.getArrayOrNull
|
|
26
|
-
import com.renderrichtext.utils.common.getBooleanOrDefault
|
|
27
|
-
import com.renderrichtext.utils.common.getIntOrDefault
|
|
28
|
-
import com.renderrichtext.utils.common.getMapOrNull
|
|
29
|
-
import com.renderrichtext.utils.common.getStringOrDefault
|
|
30
|
-
import com.renderrichtext.utils.common.parseImageRequestHeaders
|
|
31
|
-
import com.renderrichtext.utils.common.parseLinkPillContent
|
|
32
|
-
import com.renderrichtext.utils.common.parseLinkRecognition
|
|
33
|
-
import com.renderrichtext.utils.text.span.prepareWidthAwareSpans
|
|
34
|
-
import java.util.concurrent.ConcurrentHashMap
|
|
35
|
-
import kotlin.math.ceil
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Manages text measurements for ShadowNode layout.
|
|
39
|
-
* Parses and renders tiptap to Spannable at measure time for accurate height calculation.
|
|
40
|
-
*/
|
|
41
|
-
object MeasurementStore {
|
|
42
|
-
private const val TAG = "MeasurementStore"
|
|
43
|
-
|
|
44
|
-
private data class PaintParams(
|
|
45
|
-
val typeface: Typeface,
|
|
46
|
-
val fontSize: Float,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
private data class MeasurementParams(
|
|
50
|
-
val cachedWidth: Float,
|
|
51
|
-
val cachedSize: Long,
|
|
52
|
-
val spannable: CharSequence?,
|
|
53
|
-
val paintParams: PaintParams,
|
|
54
|
-
val tiptapHash: Int,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
private val data = ConcurrentHashMap<Int, MeasurementParams>()
|
|
58
|
-
|
|
59
|
-
// Store font scaling settings per view ID
|
|
60
|
-
private data class FontScalingSettings(
|
|
61
|
-
val allowFontScaling: Boolean = true,
|
|
62
|
-
val maxFontSizeMultiplier: Float = 0f,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
private val fontScalingSettings = ConcurrentHashMap<Int, FontScalingSettings>()
|
|
66
|
-
|
|
67
|
-
private val breakStrategies = ConcurrentHashMap<Int, String>()
|
|
68
|
-
|
|
69
|
-
// The measure path and the display TextView resolve the same clamp. 0 means unlimited.
|
|
70
|
-
private val numberOfLinesByViewId = ConcurrentHashMap<Int, Int>()
|
|
71
|
-
|
|
72
|
-
private val ellipsizeModeByViewId = ConcurrentHashMap<Int, String>()
|
|
73
|
-
|
|
74
|
-
// Code blocks the user wrapped, by document-order index across nesting (see CodeBlockWrapState).
|
|
75
|
-
private val wrappedCodeBlocks = ConcurrentHashMap<Int, Set<Int>>()
|
|
76
|
-
|
|
77
|
-
private fun resolveFontScalingSettings(
|
|
78
|
-
viewId: Int?,
|
|
79
|
-
props: ReadableMap?,
|
|
80
|
-
): FontScalingSettings {
|
|
81
|
-
val stored = viewId?.let { fontScalingSettings[it] }
|
|
82
|
-
return FontScalingSettings(
|
|
83
|
-
allowFontScaling =
|
|
84
|
-
props?.takeIf { it.hasKey("allowFontScaling") }?.getBoolean("allowFontScaling")
|
|
85
|
-
?: stored?.allowFontScaling
|
|
86
|
-
?: true,
|
|
87
|
-
maxFontSizeMultiplier =
|
|
88
|
-
props?.takeIf { it.hasKey("maxFontSizeMultiplier") }?.getDouble("maxFontSizeMultiplier")?.toFloat()
|
|
89
|
-
?: stored?.maxFontSizeMultiplier
|
|
90
|
-
?: 0f,
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private val measurePaint = TextPaint()
|
|
95
|
-
private val measureRenderer = Renderer()
|
|
96
|
-
|
|
97
|
-
@Volatile
|
|
98
|
-
private var lastKnownFontScale: Float = 1.0f
|
|
99
|
-
|
|
100
|
-
/** Updates measurement with rendered Spannable. Returns true if height changed. */
|
|
101
|
-
fun store(
|
|
102
|
-
id: Int,
|
|
103
|
-
spannable: CharSequence?,
|
|
104
|
-
paint: TextPaint,
|
|
105
|
-
trailingMarginBottomPx: Float = 0f,
|
|
106
|
-
): Boolean {
|
|
107
|
-
val cached = data[id]
|
|
108
|
-
val width = cached?.cachedWidth ?: 0f
|
|
109
|
-
val oldSize = cached?.cachedSize ?: 0L
|
|
110
|
-
val existingHash = cached?.tiptapHash ?: 0
|
|
111
|
-
val paintParams = PaintParams(paint.typeface ?: Typeface.DEFAULT, paint.textSize)
|
|
112
|
-
|
|
113
|
-
var newSize = measure(width, spannable, paint, id)
|
|
114
|
-
if (trailingMarginBottomPx > 0f) {
|
|
115
|
-
newSize =
|
|
116
|
-
YogaMeasureOutput.make(
|
|
117
|
-
YogaMeasureOutput.getWidth(newSize),
|
|
118
|
-
YogaMeasureOutput.getHeight(newSize) + PixelUtil.toDIPFromPixel(trailingMarginBottomPx),
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
data[id] = MeasurementParams(width, newSize, spannable, paintParams, existingHash)
|
|
122
|
-
return oldSize != newSize
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
fun release(id: Int) {
|
|
126
|
-
data.remove(id)
|
|
127
|
-
numberOfLinesByViewId.remove(id)
|
|
128
|
-
ellipsizeModeByViewId.remove(id)
|
|
129
|
-
wrappedCodeBlocks.remove(id)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
fun updateWrappedCodeBlocks(
|
|
133
|
-
viewId: Int,
|
|
134
|
-
indices: Set<Int>,
|
|
135
|
-
) {
|
|
136
|
-
if (indices.isEmpty()) wrappedCodeBlocks.remove(viewId) else wrappedCodeBlocks[viewId] = indices
|
|
137
|
-
data.remove(viewId)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
fun invalidate(id: Int) {
|
|
141
|
-
data.remove(id)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Content-box width (px) the measure pass last wrapped at; the display view insets to match (#805).
|
|
145
|
-
fun contentBoxWidthPx(id: Int): Float? = data[id]?.cachedWidth
|
|
146
|
-
|
|
147
|
-
/** Main entry point for ShadowNode measurement. */
|
|
148
|
-
fun getMeasureById(
|
|
149
|
-
context: Context,
|
|
150
|
-
id: Int?,
|
|
151
|
-
width: Float,
|
|
152
|
-
widthMode: YogaMeasureMode?,
|
|
153
|
-
height: Float,
|
|
154
|
-
heightMode: YogaMeasureMode?,
|
|
155
|
-
props: ReadableMap?,
|
|
156
|
-
): Long {
|
|
157
|
-
// Early exit for empty tiptap. Honor widthMode here too: empty content has
|
|
158
|
-
// zero intrinsic width, so only fill when the width is EXACTLY assigned -
|
|
159
|
-
// otherwise an empty view would claim all available horizontal space.
|
|
160
|
-
val tiptap = props.getStringOrDefault("document", "")
|
|
161
|
-
if (tiptap.isEmpty()) {
|
|
162
|
-
val emptyWidth = if (widthMode === YogaMeasureMode.EXACTLY) PixelUtil.toDIPFromPixel(width) else 0f
|
|
163
|
-
return YogaMeasureOutput.make(emptyWidth, 0f)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Resolve the line clamp from props here so the (background) measure pass and
|
|
167
|
-
// the (main-thread) display TextView never race on the per-viewId cache. Only
|
|
168
|
-
// overwrite when the key is present so we never clobber the view's value.
|
|
169
|
-
if (id != null && props != null) {
|
|
170
|
-
if (props.hasKey("numberOfLines")) {
|
|
171
|
-
numberOfLinesByViewId[id] = props.getDouble("numberOfLines").toInt()
|
|
172
|
-
}
|
|
173
|
-
if (props.hasKey("ellipsizeMode")) {
|
|
174
|
-
ellipsizeModeByViewId[id] = props.getString("ellipsizeMode") ?: EllipsizeUtils.DEFAULT_MODE
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
val size = getMeasureByIdInternal(context, id, width, props)
|
|
179
|
-
|
|
180
|
-
// Honor the Yoga width contract. When the parent assigns an exact width
|
|
181
|
-
// (flex child, alignItems: stretch, explicit width / width: "100%"), fill it
|
|
182
|
-
// instead of reporting the content's widest line, which would make the node
|
|
183
|
-
// hug its content and wrap early. Mirrors RN's TextLayoutManager.calculateWidth;
|
|
184
|
-
// iOS already does this via ENRMClampMeasuredSize.
|
|
185
|
-
val resultWidth =
|
|
186
|
-
if (widthMode === YogaMeasureMode.EXACTLY) {
|
|
187
|
-
PixelUtil.toDIPFromPixel(width)
|
|
188
|
-
} else {
|
|
189
|
-
YogaMeasureOutput.getWidth(size)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
var resultHeight = YogaMeasureOutput.getHeight(size)
|
|
193
|
-
if (heightMode === YogaMeasureMode.AT_MOST) {
|
|
194
|
-
resultHeight = resultHeight.coerceAtMost(PixelUtil.toDIPFromPixel(height))
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return YogaMeasureOutput.make(resultWidth, resultHeight)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
fun updateFontScalingSettings(
|
|
201
|
-
viewId: Int,
|
|
202
|
-
allowFontScaling: Boolean,
|
|
203
|
-
maxFontSizeMultiplier: Float,
|
|
204
|
-
) {
|
|
205
|
-
val previous = fontScalingSettings[viewId]
|
|
206
|
-
fontScalingSettings[viewId] = FontScalingSettings(allowFontScaling, maxFontSizeMultiplier)
|
|
207
|
-
if (previous != null && (previous.allowFontScaling != allowFontScaling || previous.maxFontSizeMultiplier != maxFontSizeMultiplier)) {
|
|
208
|
-
data.remove(viewId)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
fun clearFontScalingSettings(viewId: Int) {
|
|
213
|
-
fontScalingSettings.remove(viewId)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
fun updateBreakStrategy(
|
|
217
|
-
viewId: Int,
|
|
218
|
-
strategy: String,
|
|
219
|
-
) {
|
|
220
|
-
breakStrategies[viewId] = strategy
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
fun clearBreakStrategy(viewId: Int) {
|
|
224
|
-
breakStrategies.remove(viewId)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private fun resolveBreakStrategy(viewId: Int?): Int = BreakStrategyUtils.resolveBreakStrategy(viewId?.let { breakStrategies[it] })
|
|
228
|
-
|
|
229
|
-
fun updateNumberOfLines(
|
|
230
|
-
viewId: Int,
|
|
231
|
-
numberOfLines: Int,
|
|
232
|
-
) {
|
|
233
|
-
numberOfLinesByViewId[viewId] = numberOfLines
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
fun updateEllipsizeMode(
|
|
237
|
-
viewId: Int,
|
|
238
|
-
mode: String,
|
|
239
|
-
) {
|
|
240
|
-
ellipsizeModeByViewId[viewId] = mode
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
private fun resolveMaxLines(viewId: Int?): Int = viewId?.let { numberOfLinesByViewId[it] } ?: 0
|
|
244
|
-
|
|
245
|
-
private fun resolveEllipsize(viewId: Int?): TruncateAt? =
|
|
246
|
-
EllipsizeUtils.resolveTruncateAt(viewId?.let { ellipsizeModeByViewId[it] } ?: EllipsizeUtils.DEFAULT_MODE)
|
|
247
|
-
|
|
248
|
-
private fun getMeasureByIdInternal(
|
|
249
|
-
context: Context,
|
|
250
|
-
id: Int?,
|
|
251
|
-
width: Float,
|
|
252
|
-
props: ReadableMap?,
|
|
253
|
-
): Long {
|
|
254
|
-
val (allowFontScaling, maxFontSizeMultiplier) = resolveFontScalingSettings(id, props)
|
|
255
|
-
|
|
256
|
-
val fontScale = checkAndUpdateFontScale(context, allowFontScaling, maxFontSizeMultiplier)
|
|
257
|
-
|
|
258
|
-
val safeId = id ?: return measureAndCache(context, null, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
259
|
-
val cached = data[safeId] ?: return measureAndCache(context, safeId, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
260
|
-
|
|
261
|
-
val currentHash = computePropsHash(props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
262
|
-
|
|
263
|
-
if (cached.tiptapHash != currentHash) {
|
|
264
|
-
return measureAndCache(context, safeId, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Width changed - re-measure with cached spannable
|
|
268
|
-
if (cached.cachedWidth != width) {
|
|
269
|
-
val newSize = measure(width, cached.spannable, cached.paintParams, safeId)
|
|
270
|
-
data[safeId] = cached.copy(cachedWidth = width, cachedSize = newSize)
|
|
271
|
-
return newSize
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return cached.cachedSize
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
private fun computePropsHash(
|
|
278
|
-
props: ReadableMap?,
|
|
279
|
-
allowFontScaling: Boolean,
|
|
280
|
-
fontScale: Float,
|
|
281
|
-
maxFontSizeMultiplier: Float,
|
|
282
|
-
): Int {
|
|
283
|
-
val tiptap = props.getStringOrDefault("document", "")
|
|
284
|
-
return computePropsHashForTiptap(tiptap, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private fun computePropsHashForTiptap(
|
|
288
|
-
tiptap: String,
|
|
289
|
-
props: ReadableMap?,
|
|
290
|
-
allowFontScaling: Boolean,
|
|
291
|
-
fontScale: Float,
|
|
292
|
-
maxFontSizeMultiplier: Float,
|
|
293
|
-
): Int {
|
|
294
|
-
val styleMap = props.getMapOrNull("tiptapStyle")
|
|
295
|
-
val md4cFlagsMap = props.getMapOrNull("md4cFlags")
|
|
296
|
-
val allowTrailingMargin = props.getBooleanOrDefault("allowTrailingMargin", false)
|
|
297
|
-
val imageRequestHeaders = parseImageRequestHeaders(props.getArrayOrNull("imageRequestHeaders"))
|
|
298
|
-
val linkPillContent = parseLinkPillContent(props.getArrayOrNull("linkPillContent"))
|
|
299
|
-
var result = tiptap.hashCode()
|
|
300
|
-
result = 31 * result + (styleMap?.hashCode() ?: 0)
|
|
301
|
-
result = 31 * result + (md4cFlagsMap?.hashCode() ?: 0)
|
|
302
|
-
result = 31 * result + parseLinkRecognition(props.getMapOrNull("linkRecognition")).hashCode()
|
|
303
|
-
result = 31 * result + fontScale.toBits()
|
|
304
|
-
result = 31 * result + allowFontScaling.hashCode()
|
|
305
|
-
result = 31 * result + maxFontSizeMultiplier.toBits()
|
|
306
|
-
result = 31 * result + allowTrailingMargin.hashCode()
|
|
307
|
-
result = 31 * result + imageRequestHeaders.hashCode()
|
|
308
|
-
result = 31 * result + linkPillContent.hashCode()
|
|
309
|
-
result = 31 * result + props.getIntOrDefault("numberOfLines", 0)
|
|
310
|
-
result = 31 * result + props.getStringOrDefault("ellipsizeMode", EllipsizeUtils.DEFAULT_MODE).hashCode()
|
|
311
|
-
return result
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
private fun checkAndUpdateFontScale(
|
|
315
|
-
context: Context,
|
|
316
|
-
allowFontScaling: Boolean,
|
|
317
|
-
maxFontSizeMultiplier: Float,
|
|
318
|
-
): Float {
|
|
319
|
-
if (!allowFontScaling) {
|
|
320
|
-
// Clear cache if we switched from scaling to non-scaling
|
|
321
|
-
if (lastKnownFontScale != 1.0f) {
|
|
322
|
-
lastKnownFontScale = 1.0f
|
|
323
|
-
data.clear()
|
|
324
|
-
}
|
|
325
|
-
return 1.0f
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
var currentFontScale = context.resources.configuration.fontScale
|
|
329
|
-
|
|
330
|
-
if (maxFontSizeMultiplier >= 1.0f && currentFontScale > maxFontSizeMultiplier) {
|
|
331
|
-
currentFontScale = maxFontSizeMultiplier
|
|
332
|
-
}
|
|
333
|
-
if (currentFontScale != lastKnownFontScale) {
|
|
334
|
-
lastKnownFontScale = currentFontScale
|
|
335
|
-
data.clear()
|
|
336
|
-
}
|
|
337
|
-
return currentFontScale
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private fun measureAndCache(
|
|
341
|
-
context: Context,
|
|
342
|
-
id: Int?,
|
|
343
|
-
width: Float,
|
|
344
|
-
props: ReadableMap?,
|
|
345
|
-
allowFontScaling: Boolean,
|
|
346
|
-
fontScale: Float,
|
|
347
|
-
maxFontSizeMultiplier: Float,
|
|
348
|
-
): Long {
|
|
349
|
-
// 1. Extract Props & Setup
|
|
350
|
-
val tiptap = props.getStringOrDefault("document", "")
|
|
351
|
-
val styleMap = props.getMapOrNull("tiptapStyle")
|
|
352
|
-
val md4cFlags =
|
|
353
|
-
Md4cFlags(
|
|
354
|
-
underline = props.getMapOrNull("md4cFlags").getBooleanOrDefault("underline", false),
|
|
355
|
-
latexMath = false,
|
|
356
|
-
superscript = props.getMapOrNull("md4cFlags").getBooleanOrDefault("superscript", false),
|
|
357
|
-
subscript = props.getMapOrNull("md4cFlags").getBooleanOrDefault("subscript", false),
|
|
358
|
-
highlight = props.getMapOrNull("md4cFlags").getBooleanOrDefault("highlight", false),
|
|
359
|
-
hardSoftBreaks = props.getMapOrNull("md4cFlags").getBooleanOrDefault("hardSoftBreaks", false),
|
|
360
|
-
preserveBlankLines = props.getMapOrNull("md4cFlags").getBooleanOrDefault("preserveBlankLines", false),
|
|
361
|
-
admonitions = props.getMapOrNull("md4cFlags").getBooleanOrDefault("admonitions", true),
|
|
362
|
-
)
|
|
363
|
-
val isGFM = props.getBooleanOrDefault("isGFM", false)
|
|
364
|
-
|
|
365
|
-
val fontSize = getInitialFontSize(styleMap, context, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
366
|
-
val propsHash = computePropsHash(props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
367
|
-
|
|
368
|
-
// 2. Render & Measure
|
|
369
|
-
measurePaint.textSize = fontSize
|
|
370
|
-
val imageRequestHeaders = parseImageRequestHeaders(props.getArrayOrNull("imageRequestHeaders"))
|
|
371
|
-
val linkRecognition = parseLinkRecognition(props.getMapOrNull("linkRecognition"))
|
|
372
|
-
val spannable =
|
|
373
|
-
tryRenderTiptap(
|
|
374
|
-
tiptap,
|
|
375
|
-
styleMap,
|
|
376
|
-
context,
|
|
377
|
-
md4cFlags,
|
|
378
|
-
isGFM,
|
|
379
|
-
allowFontScaling,
|
|
380
|
-
maxFontSizeMultiplier,
|
|
381
|
-
imageRequestHeaders,
|
|
382
|
-
parseLinkPillContent(props.getArrayOrNull("linkPillContent")),
|
|
383
|
-
linkRecognition.text,
|
|
384
|
-
linkRecognition.inlineCode,
|
|
385
|
-
)
|
|
386
|
-
val textToMeasure = spannable ?: tiptap
|
|
387
|
-
val (size, _) = measureWithLayout(width, textToMeasure, measurePaint, id)
|
|
388
|
-
|
|
389
|
-
// 3. Calculate Margin
|
|
390
|
-
val allowTrailingMargin = props.getBooleanOrDefault("allowTrailingMargin", false)
|
|
391
|
-
val marginBottom =
|
|
392
|
-
if (allowTrailingMargin && spannable != null) {
|
|
393
|
-
PixelUtil.toDIPFromPixel(measureRenderer.getLastElementMarginBottom())
|
|
394
|
-
} else {
|
|
395
|
-
0f
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// 4. Finalize Height
|
|
399
|
-
val currentWidth = YogaMeasureOutput.getWidth(size)
|
|
400
|
-
val currentHeight = YogaMeasureOutput.getHeight(size)
|
|
401
|
-
val adjustedSize = YogaMeasureOutput.make(currentWidth, currentHeight + marginBottom)
|
|
402
|
-
|
|
403
|
-
if (id != null) {
|
|
404
|
-
data[id] = MeasurementParams(width, adjustedSize, textToMeasure, PaintParams(Typeface.DEFAULT, fontSize), propsHash)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return adjustedSize
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
private fun createStaticLayout(
|
|
411
|
-
text: CharSequence,
|
|
412
|
-
fontSize: Float,
|
|
413
|
-
widthPx: Int,
|
|
414
|
-
viewId: Int?,
|
|
415
|
-
): StaticLayout {
|
|
416
|
-
measurePaint.textSize = fontSize
|
|
417
|
-
prepareWidthAwareSpans(text, widthPx)
|
|
418
|
-
return StaticLayout.Builder
|
|
419
|
-
.obtain(text, 0, text.length, measurePaint, widthPx)
|
|
420
|
-
.setIncludePad(false)
|
|
421
|
-
.setLineSpacing(0f, 1f)
|
|
422
|
-
.apply {
|
|
423
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
424
|
-
@Suppress("WrongConstant")
|
|
425
|
-
setBreakStrategy(resolveBreakStrategy(viewId))
|
|
426
|
-
}
|
|
427
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
428
|
-
setUseLineSpacingFromFallbacks(true)
|
|
429
|
-
}
|
|
430
|
-
}.build()
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
private fun tryRenderTiptap(
|
|
434
|
-
tiptap: String,
|
|
435
|
-
styleMap: ReadableMap?,
|
|
436
|
-
context: Context,
|
|
437
|
-
md4cFlags: Md4cFlags,
|
|
438
|
-
isGFM: Boolean,
|
|
439
|
-
allowFontScaling: Boolean,
|
|
440
|
-
maxFontSizeMultiplier: Float,
|
|
441
|
-
imageRequestHeaders: Map<String, String> = emptyMap(),
|
|
442
|
-
linkPillContent: Map<String, LinkPillContent> = emptyMap(),
|
|
443
|
-
linkRegex: LinkRegexConfig? = null,
|
|
444
|
-
inlineCodeLinkRegex: LinkRegexConfig? = null,
|
|
445
|
-
): Spannable? {
|
|
446
|
-
if (styleMap == null) return null
|
|
447
|
-
|
|
448
|
-
return try {
|
|
449
|
-
val ast = Parser.shared.parseTiptap(tiptap, md4cFlags, isGFM, linkRegex, inlineCodeLinkRegex) ?: return null
|
|
450
|
-
val style = StyleConfig(styleMap, context, allowFontScaling, maxFontSizeMultiplier)
|
|
451
|
-
style.imageRequestHeaders = imageRequestHeaders
|
|
452
|
-
style.linkPillContent = linkPillContent
|
|
453
|
-
measureRenderer.configure(style, context)
|
|
454
|
-
measureRenderer.renderDocument(ast, null)
|
|
455
|
-
} catch (e: Exception) {
|
|
456
|
-
Log.w(TAG, "Failed to render tiptap for measurement, falling back to raw text", e)
|
|
457
|
-
null
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
private fun getInitialFontSize(
|
|
462
|
-
styleMap: ReadableMap?,
|
|
463
|
-
context: Context,
|
|
464
|
-
allowFontScaling: Boolean,
|
|
465
|
-
fontScale: Float,
|
|
466
|
-
maxFontSizeMultiplier: Float,
|
|
467
|
-
): Float {
|
|
468
|
-
val fontSizeSp = styleMap?.getMap("paragraph")?.getDouble("fontSize")?.toFloat() ?: 16f
|
|
469
|
-
val density = context.resources.displayMetrics.density
|
|
470
|
-
|
|
471
|
-
if (!allowFontScaling) {
|
|
472
|
-
return ceil(fontSizeSp * density)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
val cappedFontScale =
|
|
476
|
-
if (maxFontSizeMultiplier >= 1.0f && fontScale > maxFontSizeMultiplier) {
|
|
477
|
-
maxFontSizeMultiplier
|
|
478
|
-
} else {
|
|
479
|
-
fontScale
|
|
480
|
-
}
|
|
481
|
-
return ceil(fontSizeSp * cappedFontScale * density)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
private fun measure(
|
|
485
|
-
maxWidth: Float,
|
|
486
|
-
text: CharSequence?,
|
|
487
|
-
paintParams: PaintParams,
|
|
488
|
-
viewId: Int?,
|
|
489
|
-
): Long {
|
|
490
|
-
measurePaint.reset()
|
|
491
|
-
measurePaint.typeface = paintParams.typeface
|
|
492
|
-
measurePaint.textSize = paintParams.fontSize
|
|
493
|
-
return measure(maxWidth, text, measurePaint, viewId)
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
private fun measure(
|
|
497
|
-
maxWidth: Float,
|
|
498
|
-
text: CharSequence?,
|
|
499
|
-
paint: TextPaint,
|
|
500
|
-
viewId: Int?,
|
|
501
|
-
): Long {
|
|
502
|
-
val content = text ?: ""
|
|
503
|
-
val safeWidth = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
504
|
-
prepareWidthAwareSpans(content, safeWidth)
|
|
505
|
-
|
|
506
|
-
val builder =
|
|
507
|
-
StaticLayout.Builder
|
|
508
|
-
.obtain(content, 0, content.length, paint, safeWidth)
|
|
509
|
-
.setIncludePad(false)
|
|
510
|
-
.setLineSpacing(0f, 1f)
|
|
511
|
-
|
|
512
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
513
|
-
@Suppress("WrongConstant")
|
|
514
|
-
builder.setBreakStrategy(resolveBreakStrategy(viewId))
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
518
|
-
builder.setUseLineSpacingFromFallbacks(true)
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
applyLineLimit(builder, viewId)
|
|
522
|
-
|
|
523
|
-
val layout = builder.build()
|
|
524
|
-
val visibleLineCount = visibleLineCount(layout, viewId)
|
|
525
|
-
val measuredHeight = truncatedHeight(layout, visibleLineCount, content)
|
|
526
|
-
|
|
527
|
-
// Calculate actual content width (widest visible line)
|
|
528
|
-
val measuredWidth = (0 until visibleLineCount).maxOfOrNull { layout.getLineWidth(it) } ?: 0f
|
|
529
|
-
|
|
530
|
-
return YogaMeasureOutput.make(
|
|
531
|
-
PixelUtil.toDIPFromPixel(ceil(measuredWidth)),
|
|
532
|
-
PixelUtil.toDIPFromPixel(measuredHeight),
|
|
533
|
-
)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Applies the per-view numberOfLines / ellipsizeMode clamp to a builder, matching
|
|
537
|
-
// RN's TextLayoutManager.buildLayout: only when a line limit is set.
|
|
538
|
-
private fun applyLineLimit(
|
|
539
|
-
builder: StaticLayout.Builder,
|
|
540
|
-
viewId: Int?,
|
|
541
|
-
) {
|
|
542
|
-
val maxLines = resolveMaxLines(viewId)
|
|
543
|
-
if (maxLines > 0) {
|
|
544
|
-
builder.setMaxLines(maxLines).setEllipsize(resolveEllipsize(viewId))
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
private fun visibleLineCount(
|
|
549
|
-
layout: StaticLayout,
|
|
550
|
-
viewId: Int?,
|
|
551
|
-
): Int {
|
|
552
|
-
val maxLines = resolveMaxLines(viewId)
|
|
553
|
-
return if (maxLines > 0) minOf(maxLines, layout.lineCount) else layout.lineCount
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
private fun truncatedHeight(
|
|
557
|
-
layout: StaticLayout,
|
|
558
|
-
visibleLineCount: Int,
|
|
559
|
-
text: CharSequence,
|
|
560
|
-
): Float {
|
|
561
|
-
if (visibleLineCount <= 0) return 0f
|
|
562
|
-
val lastLine = visibleLineCount - 1
|
|
563
|
-
val bottom = layout.getLineBottom(lastLine).toFloat()
|
|
564
|
-
return (bottom - measureLeakedTrailingMargin(layout, lastLine, text)).coerceAtLeast(0f)
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// When we clamp the text mid-paragraph, drop the trailing block margin that Android
|
|
568
|
-
// bakes into the truncated last line (only when content follows, like MarginBottomSpan).
|
|
569
|
-
private fun measureLeakedTrailingMargin(
|
|
570
|
-
layout: StaticLayout,
|
|
571
|
-
lastLine: Int,
|
|
572
|
-
text: CharSequence,
|
|
573
|
-
): Float {
|
|
574
|
-
val spanned = text as? Spanned ?: return 0f
|
|
575
|
-
val lineStart = layout.getLineStart(lastLine)
|
|
576
|
-
val lineEnd = layout.getLineEnd(lastLine)
|
|
577
|
-
if (lineEnd <= lineStart || lineEnd < 1 || text[lineEnd - 1] != '\n') return 0f
|
|
578
|
-
if (!hasContentAfter(text, lineEnd)) return 0f
|
|
579
|
-
val trailing =
|
|
580
|
-
spanned
|
|
581
|
-
.getSpans(lineStart, lineEnd, MarginBottomSpan::class.java)
|
|
582
|
-
.firstOrNull { spanned.getSpanEnd(it) == lineEnd } ?: return 0f
|
|
583
|
-
return trailing.marginBottom.toInt().toFloat()
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
private fun hasContentAfter(
|
|
587
|
-
text: CharSequence,
|
|
588
|
-
pos: Int,
|
|
589
|
-
): Boolean {
|
|
590
|
-
if (pos >= text.length) return false
|
|
591
|
-
if (text[pos] == '\n') {
|
|
592
|
-
val nextPos = pos + 1
|
|
593
|
-
if (nextPos >= text.length) return false
|
|
594
|
-
return text[nextPos] != '\n'
|
|
595
|
-
}
|
|
596
|
-
return true
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Measures text and returns both the size and the layout for calculating last line descent.
|
|
601
|
-
*/
|
|
602
|
-
private fun measureWithLayout(
|
|
603
|
-
maxWidth: Float,
|
|
604
|
-
text: CharSequence?,
|
|
605
|
-
paint: TextPaint,
|
|
606
|
-
viewId: Int?,
|
|
607
|
-
): Pair<Long, StaticLayout> {
|
|
608
|
-
val content = text ?: ""
|
|
609
|
-
val widthPx = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
610
|
-
prepareWidthAwareSpans(content, widthPx)
|
|
611
|
-
|
|
612
|
-
val layout =
|
|
613
|
-
StaticLayout.Builder
|
|
614
|
-
.obtain(content, 0, content.length, paint, widthPx)
|
|
615
|
-
.setIncludePad(false)
|
|
616
|
-
.setLineSpacing(0f, 1f)
|
|
617
|
-
.apply {
|
|
618
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
619
|
-
@Suppress("WrongConstant")
|
|
620
|
-
setBreakStrategy(resolveBreakStrategy(viewId))
|
|
621
|
-
}
|
|
622
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
623
|
-
setUseLineSpacingFromFallbacks(true)
|
|
624
|
-
}
|
|
625
|
-
applyLineLimit(this, viewId)
|
|
626
|
-
}.build()
|
|
627
|
-
|
|
628
|
-
val visibleLineCount = visibleLineCount(layout, viewId)
|
|
629
|
-
|
|
630
|
-
// Find the widest visible line to get the actual content width
|
|
631
|
-
val maxLineWidth =
|
|
632
|
-
(0 until visibleLineCount)
|
|
633
|
-
.maxOfOrNull { layout.getLineWidth(it) } ?: 0f
|
|
634
|
-
|
|
635
|
-
val size =
|
|
636
|
-
YogaMeasureOutput.make(
|
|
637
|
-
PixelUtil.toDIPFromPixel(ceil(maxLineWidth)),
|
|
638
|
-
PixelUtil.toDIPFromPixel(truncatedHeight(layout, visibleLineCount, content)),
|
|
639
|
-
)
|
|
640
|
-
|
|
641
|
-
return size to layout
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
}
|
|
1
|
+
package com.renderrichtext
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Typeface
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.text.Spannable
|
|
7
|
+
import android.text.Spanned
|
|
8
|
+
import android.text.StaticLayout
|
|
9
|
+
import android.text.TextPaint
|
|
10
|
+
import android.text.TextUtils.TruncateAt
|
|
11
|
+
import android.util.Log
|
|
12
|
+
import com.facebook.react.bridge.ReadableMap
|
|
13
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
14
|
+
import com.facebook.yoga.YogaMeasureMode
|
|
15
|
+
import com.facebook.yoga.YogaMeasureOutput
|
|
16
|
+
import com.renderrichtext.parser.Md4cFlags
|
|
17
|
+
import com.renderrichtext.parser.Parser
|
|
18
|
+
import com.renderrichtext.renderer.Renderer
|
|
19
|
+
import com.renderrichtext.spans.MarginBottomSpan
|
|
20
|
+
import com.renderrichtext.styles.LinkPillContent
|
|
21
|
+
import com.renderrichtext.styles.StyleConfig
|
|
22
|
+
import com.renderrichtext.utils.common.BreakStrategyUtils
|
|
23
|
+
import com.renderrichtext.utils.common.EllipsizeUtils
|
|
24
|
+
import com.renderrichtext.utils.common.LinkRegexConfig
|
|
25
|
+
import com.renderrichtext.utils.common.getArrayOrNull
|
|
26
|
+
import com.renderrichtext.utils.common.getBooleanOrDefault
|
|
27
|
+
import com.renderrichtext.utils.common.getIntOrDefault
|
|
28
|
+
import com.renderrichtext.utils.common.getMapOrNull
|
|
29
|
+
import com.renderrichtext.utils.common.getStringOrDefault
|
|
30
|
+
import com.renderrichtext.utils.common.parseImageRequestHeaders
|
|
31
|
+
import com.renderrichtext.utils.common.parseLinkPillContent
|
|
32
|
+
import com.renderrichtext.utils.common.parseLinkRecognition
|
|
33
|
+
import com.renderrichtext.utils.text.span.prepareWidthAwareSpans
|
|
34
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
35
|
+
import kotlin.math.ceil
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Manages text measurements for ShadowNode layout.
|
|
39
|
+
* Parses and renders tiptap to Spannable at measure time for accurate height calculation.
|
|
40
|
+
*/
|
|
41
|
+
object MeasurementStore {
|
|
42
|
+
private const val TAG = "MeasurementStore"
|
|
43
|
+
|
|
44
|
+
private data class PaintParams(
|
|
45
|
+
val typeface: Typeface,
|
|
46
|
+
val fontSize: Float,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
private data class MeasurementParams(
|
|
50
|
+
val cachedWidth: Float,
|
|
51
|
+
val cachedSize: Long,
|
|
52
|
+
val spannable: CharSequence?,
|
|
53
|
+
val paintParams: PaintParams,
|
|
54
|
+
val tiptapHash: Int,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
private val data = ConcurrentHashMap<Int, MeasurementParams>()
|
|
58
|
+
|
|
59
|
+
// Store font scaling settings per view ID
|
|
60
|
+
private data class FontScalingSettings(
|
|
61
|
+
val allowFontScaling: Boolean = true,
|
|
62
|
+
val maxFontSizeMultiplier: Float = 0f,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
private val fontScalingSettings = ConcurrentHashMap<Int, FontScalingSettings>()
|
|
66
|
+
|
|
67
|
+
private val breakStrategies = ConcurrentHashMap<Int, String>()
|
|
68
|
+
|
|
69
|
+
// The measure path and the display TextView resolve the same clamp. 0 means unlimited.
|
|
70
|
+
private val numberOfLinesByViewId = ConcurrentHashMap<Int, Int>()
|
|
71
|
+
|
|
72
|
+
private val ellipsizeModeByViewId = ConcurrentHashMap<Int, String>()
|
|
73
|
+
|
|
74
|
+
// Code blocks the user wrapped, by document-order index across nesting (see CodeBlockWrapState).
|
|
75
|
+
private val wrappedCodeBlocks = ConcurrentHashMap<Int, Set<Int>>()
|
|
76
|
+
|
|
77
|
+
private fun resolveFontScalingSettings(
|
|
78
|
+
viewId: Int?,
|
|
79
|
+
props: ReadableMap?,
|
|
80
|
+
): FontScalingSettings {
|
|
81
|
+
val stored = viewId?.let { fontScalingSettings[it] }
|
|
82
|
+
return FontScalingSettings(
|
|
83
|
+
allowFontScaling =
|
|
84
|
+
props?.takeIf { it.hasKey("allowFontScaling") }?.getBoolean("allowFontScaling")
|
|
85
|
+
?: stored?.allowFontScaling
|
|
86
|
+
?: true,
|
|
87
|
+
maxFontSizeMultiplier =
|
|
88
|
+
props?.takeIf { it.hasKey("maxFontSizeMultiplier") }?.getDouble("maxFontSizeMultiplier")?.toFloat()
|
|
89
|
+
?: stored?.maxFontSizeMultiplier
|
|
90
|
+
?: 0f,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private val measurePaint = TextPaint()
|
|
95
|
+
private val measureRenderer = Renderer()
|
|
96
|
+
|
|
97
|
+
@Volatile
|
|
98
|
+
private var lastKnownFontScale: Float = 1.0f
|
|
99
|
+
|
|
100
|
+
/** Updates measurement with rendered Spannable. Returns true if height changed. */
|
|
101
|
+
fun store(
|
|
102
|
+
id: Int,
|
|
103
|
+
spannable: CharSequence?,
|
|
104
|
+
paint: TextPaint,
|
|
105
|
+
trailingMarginBottomPx: Float = 0f,
|
|
106
|
+
): Boolean {
|
|
107
|
+
val cached = data[id]
|
|
108
|
+
val width = cached?.cachedWidth ?: 0f
|
|
109
|
+
val oldSize = cached?.cachedSize ?: 0L
|
|
110
|
+
val existingHash = cached?.tiptapHash ?: 0
|
|
111
|
+
val paintParams = PaintParams(paint.typeface ?: Typeface.DEFAULT, paint.textSize)
|
|
112
|
+
|
|
113
|
+
var newSize = measure(width, spannable, paint, id)
|
|
114
|
+
if (trailingMarginBottomPx > 0f) {
|
|
115
|
+
newSize =
|
|
116
|
+
YogaMeasureOutput.make(
|
|
117
|
+
YogaMeasureOutput.getWidth(newSize),
|
|
118
|
+
YogaMeasureOutput.getHeight(newSize) + PixelUtil.toDIPFromPixel(trailingMarginBottomPx),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
data[id] = MeasurementParams(width, newSize, spannable, paintParams, existingHash)
|
|
122
|
+
return oldSize != newSize
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fun release(id: Int) {
|
|
126
|
+
data.remove(id)
|
|
127
|
+
numberOfLinesByViewId.remove(id)
|
|
128
|
+
ellipsizeModeByViewId.remove(id)
|
|
129
|
+
wrappedCodeBlocks.remove(id)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fun updateWrappedCodeBlocks(
|
|
133
|
+
viewId: Int,
|
|
134
|
+
indices: Set<Int>,
|
|
135
|
+
) {
|
|
136
|
+
if (indices.isEmpty()) wrappedCodeBlocks.remove(viewId) else wrappedCodeBlocks[viewId] = indices
|
|
137
|
+
data.remove(viewId)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fun invalidate(id: Int) {
|
|
141
|
+
data.remove(id)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Content-box width (px) the measure pass last wrapped at; the display view insets to match (#805).
|
|
145
|
+
fun contentBoxWidthPx(id: Int): Float? = data[id]?.cachedWidth
|
|
146
|
+
|
|
147
|
+
/** Main entry point for ShadowNode measurement. */
|
|
148
|
+
fun getMeasureById(
|
|
149
|
+
context: Context,
|
|
150
|
+
id: Int?,
|
|
151
|
+
width: Float,
|
|
152
|
+
widthMode: YogaMeasureMode?,
|
|
153
|
+
height: Float,
|
|
154
|
+
heightMode: YogaMeasureMode?,
|
|
155
|
+
props: ReadableMap?,
|
|
156
|
+
): Long {
|
|
157
|
+
// Early exit for empty tiptap. Honor widthMode here too: empty content has
|
|
158
|
+
// zero intrinsic width, so only fill when the width is EXACTLY assigned -
|
|
159
|
+
// otherwise an empty view would claim all available horizontal space.
|
|
160
|
+
val tiptap = props.getStringOrDefault("document", "")
|
|
161
|
+
if (tiptap.isEmpty()) {
|
|
162
|
+
val emptyWidth = if (widthMode === YogaMeasureMode.EXACTLY) PixelUtil.toDIPFromPixel(width) else 0f
|
|
163
|
+
return YogaMeasureOutput.make(emptyWidth, 0f)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Resolve the line clamp from props here so the (background) measure pass and
|
|
167
|
+
// the (main-thread) display TextView never race on the per-viewId cache. Only
|
|
168
|
+
// overwrite when the key is present so we never clobber the view's value.
|
|
169
|
+
if (id != null && props != null) {
|
|
170
|
+
if (props.hasKey("numberOfLines")) {
|
|
171
|
+
numberOfLinesByViewId[id] = props.getDouble("numberOfLines").toInt()
|
|
172
|
+
}
|
|
173
|
+
if (props.hasKey("ellipsizeMode")) {
|
|
174
|
+
ellipsizeModeByViewId[id] = props.getString("ellipsizeMode") ?: EllipsizeUtils.DEFAULT_MODE
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
val size = getMeasureByIdInternal(context, id, width, props)
|
|
179
|
+
|
|
180
|
+
// Honor the Yoga width contract. When the parent assigns an exact width
|
|
181
|
+
// (flex child, alignItems: stretch, explicit width / width: "100%"), fill it
|
|
182
|
+
// instead of reporting the content's widest line, which would make the node
|
|
183
|
+
// hug its content and wrap early. Mirrors RN's TextLayoutManager.calculateWidth;
|
|
184
|
+
// iOS already does this via ENRMClampMeasuredSize.
|
|
185
|
+
val resultWidth =
|
|
186
|
+
if (widthMode === YogaMeasureMode.EXACTLY) {
|
|
187
|
+
PixelUtil.toDIPFromPixel(width)
|
|
188
|
+
} else {
|
|
189
|
+
YogaMeasureOutput.getWidth(size)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
var resultHeight = YogaMeasureOutput.getHeight(size)
|
|
193
|
+
if (heightMode === YogaMeasureMode.AT_MOST) {
|
|
194
|
+
resultHeight = resultHeight.coerceAtMost(PixelUtil.toDIPFromPixel(height))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return YogaMeasureOutput.make(resultWidth, resultHeight)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fun updateFontScalingSettings(
|
|
201
|
+
viewId: Int,
|
|
202
|
+
allowFontScaling: Boolean,
|
|
203
|
+
maxFontSizeMultiplier: Float,
|
|
204
|
+
) {
|
|
205
|
+
val previous = fontScalingSettings[viewId]
|
|
206
|
+
fontScalingSettings[viewId] = FontScalingSettings(allowFontScaling, maxFontSizeMultiplier)
|
|
207
|
+
if (previous != null && (previous.allowFontScaling != allowFontScaling || previous.maxFontSizeMultiplier != maxFontSizeMultiplier)) {
|
|
208
|
+
data.remove(viewId)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fun clearFontScalingSettings(viewId: Int) {
|
|
213
|
+
fontScalingSettings.remove(viewId)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fun updateBreakStrategy(
|
|
217
|
+
viewId: Int,
|
|
218
|
+
strategy: String,
|
|
219
|
+
) {
|
|
220
|
+
breakStrategies[viewId] = strategy
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fun clearBreakStrategy(viewId: Int) {
|
|
224
|
+
breakStrategies.remove(viewId)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private fun resolveBreakStrategy(viewId: Int?): Int = BreakStrategyUtils.resolveBreakStrategy(viewId?.let { breakStrategies[it] })
|
|
228
|
+
|
|
229
|
+
fun updateNumberOfLines(
|
|
230
|
+
viewId: Int,
|
|
231
|
+
numberOfLines: Int,
|
|
232
|
+
) {
|
|
233
|
+
numberOfLinesByViewId[viewId] = numberOfLines
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fun updateEllipsizeMode(
|
|
237
|
+
viewId: Int,
|
|
238
|
+
mode: String,
|
|
239
|
+
) {
|
|
240
|
+
ellipsizeModeByViewId[viewId] = mode
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private fun resolveMaxLines(viewId: Int?): Int = viewId?.let { numberOfLinesByViewId[it] } ?: 0
|
|
244
|
+
|
|
245
|
+
private fun resolveEllipsize(viewId: Int?): TruncateAt? =
|
|
246
|
+
EllipsizeUtils.resolveTruncateAt(viewId?.let { ellipsizeModeByViewId[it] } ?: EllipsizeUtils.DEFAULT_MODE)
|
|
247
|
+
|
|
248
|
+
private fun getMeasureByIdInternal(
|
|
249
|
+
context: Context,
|
|
250
|
+
id: Int?,
|
|
251
|
+
width: Float,
|
|
252
|
+
props: ReadableMap?,
|
|
253
|
+
): Long {
|
|
254
|
+
val (allowFontScaling, maxFontSizeMultiplier) = resolveFontScalingSettings(id, props)
|
|
255
|
+
|
|
256
|
+
val fontScale = checkAndUpdateFontScale(context, allowFontScaling, maxFontSizeMultiplier)
|
|
257
|
+
|
|
258
|
+
val safeId = id ?: return measureAndCache(context, null, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
259
|
+
val cached = data[safeId] ?: return measureAndCache(context, safeId, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
260
|
+
|
|
261
|
+
val currentHash = computePropsHash(props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
262
|
+
|
|
263
|
+
if (cached.tiptapHash != currentHash) {
|
|
264
|
+
return measureAndCache(context, safeId, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Width changed - re-measure with cached spannable
|
|
268
|
+
if (cached.cachedWidth != width) {
|
|
269
|
+
val newSize = measure(width, cached.spannable, cached.paintParams, safeId)
|
|
270
|
+
data[safeId] = cached.copy(cachedWidth = width, cachedSize = newSize)
|
|
271
|
+
return newSize
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return cached.cachedSize
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private fun computePropsHash(
|
|
278
|
+
props: ReadableMap?,
|
|
279
|
+
allowFontScaling: Boolean,
|
|
280
|
+
fontScale: Float,
|
|
281
|
+
maxFontSizeMultiplier: Float,
|
|
282
|
+
): Int {
|
|
283
|
+
val tiptap = props.getStringOrDefault("document", "")
|
|
284
|
+
return computePropsHashForTiptap(tiptap, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private fun computePropsHashForTiptap(
|
|
288
|
+
tiptap: String,
|
|
289
|
+
props: ReadableMap?,
|
|
290
|
+
allowFontScaling: Boolean,
|
|
291
|
+
fontScale: Float,
|
|
292
|
+
maxFontSizeMultiplier: Float,
|
|
293
|
+
): Int {
|
|
294
|
+
val styleMap = props.getMapOrNull("tiptapStyle")
|
|
295
|
+
val md4cFlagsMap = props.getMapOrNull("md4cFlags")
|
|
296
|
+
val allowTrailingMargin = props.getBooleanOrDefault("allowTrailingMargin", false)
|
|
297
|
+
val imageRequestHeaders = parseImageRequestHeaders(props.getArrayOrNull("imageRequestHeaders"))
|
|
298
|
+
val linkPillContent = parseLinkPillContent(props.getArrayOrNull("linkPillContent"))
|
|
299
|
+
var result = tiptap.hashCode()
|
|
300
|
+
result = 31 * result + (styleMap?.hashCode() ?: 0)
|
|
301
|
+
result = 31 * result + (md4cFlagsMap?.hashCode() ?: 0)
|
|
302
|
+
result = 31 * result + parseLinkRecognition(props.getMapOrNull("linkRecognition")).hashCode()
|
|
303
|
+
result = 31 * result + fontScale.toBits()
|
|
304
|
+
result = 31 * result + allowFontScaling.hashCode()
|
|
305
|
+
result = 31 * result + maxFontSizeMultiplier.toBits()
|
|
306
|
+
result = 31 * result + allowTrailingMargin.hashCode()
|
|
307
|
+
result = 31 * result + imageRequestHeaders.hashCode()
|
|
308
|
+
result = 31 * result + linkPillContent.hashCode()
|
|
309
|
+
result = 31 * result + props.getIntOrDefault("numberOfLines", 0)
|
|
310
|
+
result = 31 * result + props.getStringOrDefault("ellipsizeMode", EllipsizeUtils.DEFAULT_MODE).hashCode()
|
|
311
|
+
return result
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private fun checkAndUpdateFontScale(
|
|
315
|
+
context: Context,
|
|
316
|
+
allowFontScaling: Boolean,
|
|
317
|
+
maxFontSizeMultiplier: Float,
|
|
318
|
+
): Float {
|
|
319
|
+
if (!allowFontScaling) {
|
|
320
|
+
// Clear cache if we switched from scaling to non-scaling
|
|
321
|
+
if (lastKnownFontScale != 1.0f) {
|
|
322
|
+
lastKnownFontScale = 1.0f
|
|
323
|
+
data.clear()
|
|
324
|
+
}
|
|
325
|
+
return 1.0f
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
var currentFontScale = context.resources.configuration.fontScale
|
|
329
|
+
|
|
330
|
+
if (maxFontSizeMultiplier >= 1.0f && currentFontScale > maxFontSizeMultiplier) {
|
|
331
|
+
currentFontScale = maxFontSizeMultiplier
|
|
332
|
+
}
|
|
333
|
+
if (currentFontScale != lastKnownFontScale) {
|
|
334
|
+
lastKnownFontScale = currentFontScale
|
|
335
|
+
data.clear()
|
|
336
|
+
}
|
|
337
|
+
return currentFontScale
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private fun measureAndCache(
|
|
341
|
+
context: Context,
|
|
342
|
+
id: Int?,
|
|
343
|
+
width: Float,
|
|
344
|
+
props: ReadableMap?,
|
|
345
|
+
allowFontScaling: Boolean,
|
|
346
|
+
fontScale: Float,
|
|
347
|
+
maxFontSizeMultiplier: Float,
|
|
348
|
+
): Long {
|
|
349
|
+
// 1. Extract Props & Setup
|
|
350
|
+
val tiptap = props.getStringOrDefault("document", "")
|
|
351
|
+
val styleMap = props.getMapOrNull("tiptapStyle")
|
|
352
|
+
val md4cFlags =
|
|
353
|
+
Md4cFlags(
|
|
354
|
+
underline = props.getMapOrNull("md4cFlags").getBooleanOrDefault("underline", false),
|
|
355
|
+
latexMath = false,
|
|
356
|
+
superscript = props.getMapOrNull("md4cFlags").getBooleanOrDefault("superscript", false),
|
|
357
|
+
subscript = props.getMapOrNull("md4cFlags").getBooleanOrDefault("subscript", false),
|
|
358
|
+
highlight = props.getMapOrNull("md4cFlags").getBooleanOrDefault("highlight", false),
|
|
359
|
+
hardSoftBreaks = props.getMapOrNull("md4cFlags").getBooleanOrDefault("hardSoftBreaks", false),
|
|
360
|
+
preserveBlankLines = props.getMapOrNull("md4cFlags").getBooleanOrDefault("preserveBlankLines", false),
|
|
361
|
+
admonitions = props.getMapOrNull("md4cFlags").getBooleanOrDefault("admonitions", true),
|
|
362
|
+
)
|
|
363
|
+
val isGFM = props.getBooleanOrDefault("isGFM", false)
|
|
364
|
+
|
|
365
|
+
val fontSize = getInitialFontSize(styleMap, context, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
366
|
+
val propsHash = computePropsHash(props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
367
|
+
|
|
368
|
+
// 2. Render & Measure
|
|
369
|
+
measurePaint.textSize = fontSize
|
|
370
|
+
val imageRequestHeaders = parseImageRequestHeaders(props.getArrayOrNull("imageRequestHeaders"))
|
|
371
|
+
val linkRecognition = parseLinkRecognition(props.getMapOrNull("linkRecognition"))
|
|
372
|
+
val spannable =
|
|
373
|
+
tryRenderTiptap(
|
|
374
|
+
tiptap,
|
|
375
|
+
styleMap,
|
|
376
|
+
context,
|
|
377
|
+
md4cFlags,
|
|
378
|
+
isGFM,
|
|
379
|
+
allowFontScaling,
|
|
380
|
+
maxFontSizeMultiplier,
|
|
381
|
+
imageRequestHeaders,
|
|
382
|
+
parseLinkPillContent(props.getArrayOrNull("linkPillContent")),
|
|
383
|
+
linkRecognition.text,
|
|
384
|
+
linkRecognition.inlineCode,
|
|
385
|
+
)
|
|
386
|
+
val textToMeasure = spannable ?: tiptap
|
|
387
|
+
val (size, _) = measureWithLayout(width, textToMeasure, measurePaint, id)
|
|
388
|
+
|
|
389
|
+
// 3. Calculate Margin
|
|
390
|
+
val allowTrailingMargin = props.getBooleanOrDefault("allowTrailingMargin", false)
|
|
391
|
+
val marginBottom =
|
|
392
|
+
if (allowTrailingMargin && spannable != null) {
|
|
393
|
+
PixelUtil.toDIPFromPixel(measureRenderer.getLastElementMarginBottom())
|
|
394
|
+
} else {
|
|
395
|
+
0f
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 4. Finalize Height
|
|
399
|
+
val currentWidth = YogaMeasureOutput.getWidth(size)
|
|
400
|
+
val currentHeight = YogaMeasureOutput.getHeight(size)
|
|
401
|
+
val adjustedSize = YogaMeasureOutput.make(currentWidth, currentHeight + marginBottom)
|
|
402
|
+
|
|
403
|
+
if (id != null) {
|
|
404
|
+
data[id] = MeasurementParams(width, adjustedSize, textToMeasure, PaintParams(Typeface.DEFAULT, fontSize), propsHash)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return adjustedSize
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private fun createStaticLayout(
|
|
411
|
+
text: CharSequence,
|
|
412
|
+
fontSize: Float,
|
|
413
|
+
widthPx: Int,
|
|
414
|
+
viewId: Int?,
|
|
415
|
+
): StaticLayout {
|
|
416
|
+
measurePaint.textSize = fontSize
|
|
417
|
+
prepareWidthAwareSpans(text, widthPx)
|
|
418
|
+
return StaticLayout.Builder
|
|
419
|
+
.obtain(text, 0, text.length, measurePaint, widthPx)
|
|
420
|
+
.setIncludePad(false)
|
|
421
|
+
.setLineSpacing(0f, 1f)
|
|
422
|
+
.apply {
|
|
423
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
424
|
+
@Suppress("WrongConstant")
|
|
425
|
+
setBreakStrategy(resolveBreakStrategy(viewId))
|
|
426
|
+
}
|
|
427
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
428
|
+
setUseLineSpacingFromFallbacks(true)
|
|
429
|
+
}
|
|
430
|
+
}.build()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private fun tryRenderTiptap(
|
|
434
|
+
tiptap: String,
|
|
435
|
+
styleMap: ReadableMap?,
|
|
436
|
+
context: Context,
|
|
437
|
+
md4cFlags: Md4cFlags,
|
|
438
|
+
isGFM: Boolean,
|
|
439
|
+
allowFontScaling: Boolean,
|
|
440
|
+
maxFontSizeMultiplier: Float,
|
|
441
|
+
imageRequestHeaders: Map<String, String> = emptyMap(),
|
|
442
|
+
linkPillContent: Map<String, LinkPillContent> = emptyMap(),
|
|
443
|
+
linkRegex: LinkRegexConfig? = null,
|
|
444
|
+
inlineCodeLinkRegex: LinkRegexConfig? = null,
|
|
445
|
+
): Spannable? {
|
|
446
|
+
if (styleMap == null) return null
|
|
447
|
+
|
|
448
|
+
return try {
|
|
449
|
+
val ast = Parser.shared.parseTiptap(tiptap, md4cFlags, isGFM, linkRegex, inlineCodeLinkRegex) ?: return null
|
|
450
|
+
val style = StyleConfig(styleMap, context, allowFontScaling, maxFontSizeMultiplier)
|
|
451
|
+
style.imageRequestHeaders = imageRequestHeaders
|
|
452
|
+
style.linkPillContent = linkPillContent
|
|
453
|
+
measureRenderer.configure(style, context)
|
|
454
|
+
measureRenderer.renderDocument(ast, null)
|
|
455
|
+
} catch (e: Exception) {
|
|
456
|
+
Log.w(TAG, "Failed to render tiptap for measurement, falling back to raw text", e)
|
|
457
|
+
null
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private fun getInitialFontSize(
|
|
462
|
+
styleMap: ReadableMap?,
|
|
463
|
+
context: Context,
|
|
464
|
+
allowFontScaling: Boolean,
|
|
465
|
+
fontScale: Float,
|
|
466
|
+
maxFontSizeMultiplier: Float,
|
|
467
|
+
): Float {
|
|
468
|
+
val fontSizeSp = styleMap?.getMap("paragraph")?.getDouble("fontSize")?.toFloat() ?: 16f
|
|
469
|
+
val density = context.resources.displayMetrics.density
|
|
470
|
+
|
|
471
|
+
if (!allowFontScaling) {
|
|
472
|
+
return ceil(fontSizeSp * density)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
val cappedFontScale =
|
|
476
|
+
if (maxFontSizeMultiplier >= 1.0f && fontScale > maxFontSizeMultiplier) {
|
|
477
|
+
maxFontSizeMultiplier
|
|
478
|
+
} else {
|
|
479
|
+
fontScale
|
|
480
|
+
}
|
|
481
|
+
return ceil(fontSizeSp * cappedFontScale * density)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private fun measure(
|
|
485
|
+
maxWidth: Float,
|
|
486
|
+
text: CharSequence?,
|
|
487
|
+
paintParams: PaintParams,
|
|
488
|
+
viewId: Int?,
|
|
489
|
+
): Long {
|
|
490
|
+
measurePaint.reset()
|
|
491
|
+
measurePaint.typeface = paintParams.typeface
|
|
492
|
+
measurePaint.textSize = paintParams.fontSize
|
|
493
|
+
return measure(maxWidth, text, measurePaint, viewId)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private fun measure(
|
|
497
|
+
maxWidth: Float,
|
|
498
|
+
text: CharSequence?,
|
|
499
|
+
paint: TextPaint,
|
|
500
|
+
viewId: Int?,
|
|
501
|
+
): Long {
|
|
502
|
+
val content = text ?: ""
|
|
503
|
+
val safeWidth = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
504
|
+
prepareWidthAwareSpans(content, safeWidth)
|
|
505
|
+
|
|
506
|
+
val builder =
|
|
507
|
+
StaticLayout.Builder
|
|
508
|
+
.obtain(content, 0, content.length, paint, safeWidth)
|
|
509
|
+
.setIncludePad(false)
|
|
510
|
+
.setLineSpacing(0f, 1f)
|
|
511
|
+
|
|
512
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
513
|
+
@Suppress("WrongConstant")
|
|
514
|
+
builder.setBreakStrategy(resolveBreakStrategy(viewId))
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
518
|
+
builder.setUseLineSpacingFromFallbacks(true)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
applyLineLimit(builder, viewId)
|
|
522
|
+
|
|
523
|
+
val layout = builder.build()
|
|
524
|
+
val visibleLineCount = visibleLineCount(layout, viewId)
|
|
525
|
+
val measuredHeight = truncatedHeight(layout, visibleLineCount, content)
|
|
526
|
+
|
|
527
|
+
// Calculate actual content width (widest visible line)
|
|
528
|
+
val measuredWidth = (0 until visibleLineCount).maxOfOrNull { layout.getLineWidth(it) } ?: 0f
|
|
529
|
+
|
|
530
|
+
return YogaMeasureOutput.make(
|
|
531
|
+
PixelUtil.toDIPFromPixel(ceil(measuredWidth)),
|
|
532
|
+
PixelUtil.toDIPFromPixel(measuredHeight),
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Applies the per-view numberOfLines / ellipsizeMode clamp to a builder, matching
|
|
537
|
+
// RN's TextLayoutManager.buildLayout: only when a line limit is set.
|
|
538
|
+
private fun applyLineLimit(
|
|
539
|
+
builder: StaticLayout.Builder,
|
|
540
|
+
viewId: Int?,
|
|
541
|
+
) {
|
|
542
|
+
val maxLines = resolveMaxLines(viewId)
|
|
543
|
+
if (maxLines > 0) {
|
|
544
|
+
builder.setMaxLines(maxLines).setEllipsize(resolveEllipsize(viewId))
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private fun visibleLineCount(
|
|
549
|
+
layout: StaticLayout,
|
|
550
|
+
viewId: Int?,
|
|
551
|
+
): Int {
|
|
552
|
+
val maxLines = resolveMaxLines(viewId)
|
|
553
|
+
return if (maxLines > 0) minOf(maxLines, layout.lineCount) else layout.lineCount
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private fun truncatedHeight(
|
|
557
|
+
layout: StaticLayout,
|
|
558
|
+
visibleLineCount: Int,
|
|
559
|
+
text: CharSequence,
|
|
560
|
+
): Float {
|
|
561
|
+
if (visibleLineCount <= 0) return 0f
|
|
562
|
+
val lastLine = visibleLineCount - 1
|
|
563
|
+
val bottom = layout.getLineBottom(lastLine).toFloat()
|
|
564
|
+
return (bottom - measureLeakedTrailingMargin(layout, lastLine, text)).coerceAtLeast(0f)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// When we clamp the text mid-paragraph, drop the trailing block margin that Android
|
|
568
|
+
// bakes into the truncated last line (only when content follows, like MarginBottomSpan).
|
|
569
|
+
private fun measureLeakedTrailingMargin(
|
|
570
|
+
layout: StaticLayout,
|
|
571
|
+
lastLine: Int,
|
|
572
|
+
text: CharSequence,
|
|
573
|
+
): Float {
|
|
574
|
+
val spanned = text as? Spanned ?: return 0f
|
|
575
|
+
val lineStart = layout.getLineStart(lastLine)
|
|
576
|
+
val lineEnd = layout.getLineEnd(lastLine)
|
|
577
|
+
if (lineEnd <= lineStart || lineEnd < 1 || text[lineEnd - 1] != '\n') return 0f
|
|
578
|
+
if (!hasContentAfter(text, lineEnd)) return 0f
|
|
579
|
+
val trailing =
|
|
580
|
+
spanned
|
|
581
|
+
.getSpans(lineStart, lineEnd, MarginBottomSpan::class.java)
|
|
582
|
+
.firstOrNull { spanned.getSpanEnd(it) == lineEnd } ?: return 0f
|
|
583
|
+
return trailing.marginBottom.toInt().toFloat()
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private fun hasContentAfter(
|
|
587
|
+
text: CharSequence,
|
|
588
|
+
pos: Int,
|
|
589
|
+
): Boolean {
|
|
590
|
+
if (pos >= text.length) return false
|
|
591
|
+
if (text[pos] == '\n') {
|
|
592
|
+
val nextPos = pos + 1
|
|
593
|
+
if (nextPos >= text.length) return false
|
|
594
|
+
return text[nextPos] != '\n'
|
|
595
|
+
}
|
|
596
|
+
return true
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Measures text and returns both the size and the layout for calculating last line descent.
|
|
601
|
+
*/
|
|
602
|
+
private fun measureWithLayout(
|
|
603
|
+
maxWidth: Float,
|
|
604
|
+
text: CharSequence?,
|
|
605
|
+
paint: TextPaint,
|
|
606
|
+
viewId: Int?,
|
|
607
|
+
): Pair<Long, StaticLayout> {
|
|
608
|
+
val content = text ?: ""
|
|
609
|
+
val widthPx = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
610
|
+
prepareWidthAwareSpans(content, widthPx)
|
|
611
|
+
|
|
612
|
+
val layout =
|
|
613
|
+
StaticLayout.Builder
|
|
614
|
+
.obtain(content, 0, content.length, paint, widthPx)
|
|
615
|
+
.setIncludePad(false)
|
|
616
|
+
.setLineSpacing(0f, 1f)
|
|
617
|
+
.apply {
|
|
618
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
619
|
+
@Suppress("WrongConstant")
|
|
620
|
+
setBreakStrategy(resolveBreakStrategy(viewId))
|
|
621
|
+
}
|
|
622
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
623
|
+
setUseLineSpacingFromFallbacks(true)
|
|
624
|
+
}
|
|
625
|
+
applyLineLimit(this, viewId)
|
|
626
|
+
}.build()
|
|
627
|
+
|
|
628
|
+
val visibleLineCount = visibleLineCount(layout, viewId)
|
|
629
|
+
|
|
630
|
+
// Find the widest visible line to get the actual content width
|
|
631
|
+
val maxLineWidth =
|
|
632
|
+
(0 until visibleLineCount)
|
|
633
|
+
.maxOfOrNull { layout.getLineWidth(it) } ?: 0f
|
|
634
|
+
|
|
635
|
+
val size =
|
|
636
|
+
YogaMeasureOutput.make(
|
|
637
|
+
PixelUtil.toDIPFromPixel(ceil(maxLineWidth)),
|
|
638
|
+
PixelUtil.toDIPFromPixel(truncatedHeight(layout, visibleLineCount, content)),
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
return size to layout
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
}
|