@mathers101/react-native-render-rich-text 0.1.9 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/README.md +140 -140
- package/ReactNativeRenderRichText.podspec +23 -23
- package/android/build.gradle +86 -86
- package/android/consumer-rules.pro +3 -3
- package/android/src/main/AndroidManifest.xml +2 -2
- package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextLayoutManager.kt +24 -24
- package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextPackage.kt +13 -13
- package/android/src/main/java/com/renderrichtext/MeasurementStore.kt +644 -644
- package/android/src/main/java/com/renderrichtext/accessibility/AccessibilityLabels.kt +17 -17
- package/android/src/main/java/com/renderrichtext/accessibility/AccessibleTiptapTextView.kt +112 -112
- package/android/src/main/java/com/renderrichtext/accessibility/TiptapAccessibilityHelper.kt +576 -576
- package/android/src/main/java/com/renderrichtext/events/CodeBlockPressEvent.kt +24 -24
- package/android/src/main/java/com/renderrichtext/events/ContextMenuItemPressEvent.kt +28 -28
- package/android/src/main/java/com/renderrichtext/events/CopyPressEvent.kt +24 -24
- package/android/src/main/java/com/renderrichtext/events/ImagePressEvent.kt +25 -25
- package/android/src/main/java/com/renderrichtext/events/LatexErrorEvent.kt +29 -29
- package/android/src/main/java/com/renderrichtext/events/LinkLongPressEvent.kt +23 -23
- package/android/src/main/java/com/renderrichtext/events/LinkPressEvent.kt +23 -23
- package/android/src/main/java/com/renderrichtext/events/MentionLongPressEvent.kt +25 -25
- package/android/src/main/java/com/renderrichtext/events/MentionPressEvent.kt +25 -25
- package/android/src/main/java/com/renderrichtext/events/TaskListItemPressEvent.kt +26 -26
- package/android/src/main/java/com/renderrichtext/math/LatexErrorReporter.kt +9 -9
- package/android/src/main/java/com/renderrichtext/parser/Parser.kt +59 -59
- package/android/src/main/java/com/renderrichtext/parser/TextLinkMatching.kt +35 -35
- package/android/src/main/java/com/renderrichtext/renderer/BaselineShiftRenderer.kt +45 -45
- package/android/src/main/java/com/renderrichtext/renderer/BlankLineRenderer.kt +39 -39
- package/android/src/main/java/com/renderrichtext/renderer/BlockquoteRenderer.kt +243 -243
- package/android/src/main/java/com/renderrichtext/renderer/BlockquoteTextRenderer.kt +56 -56
- package/android/src/main/java/com/renderrichtext/renderer/CodeBlockRenderer.kt +135 -135
- package/android/src/main/java/com/renderrichtext/renderer/CodeRenderer.kt +37 -37
- package/android/src/main/java/com/renderrichtext/renderer/EmphasisRenderer.kt +28 -28
- package/android/src/main/java/com/renderrichtext/renderer/HeadingRenderer.kt +81 -81
- package/android/src/main/java/com/renderrichtext/renderer/HighlightRenderer.kt +26 -26
- package/android/src/main/java/com/renderrichtext/renderer/LineBreakRenderer.kt +17 -17
- package/android/src/main/java/com/renderrichtext/renderer/LinkRenderer.kt +90 -90
- package/android/src/main/java/com/renderrichtext/renderer/ListContextManager.kt +109 -109
- package/android/src/main/java/com/renderrichtext/renderer/ListItemRenderer.kt +211 -211
- package/android/src/main/java/com/renderrichtext/renderer/ListRenderer.kt +133 -133
- package/android/src/main/java/com/renderrichtext/renderer/MentionRenderer.kt +85 -85
- package/android/src/main/java/com/renderrichtext/renderer/ParagraphRenderer.kt +91 -91
- package/android/src/main/java/com/renderrichtext/renderer/Renderer.kt +148 -148
- package/android/src/main/java/com/renderrichtext/renderer/SoftBreakRenderer.kt +17 -17
- package/android/src/main/java/com/renderrichtext/renderer/SpanStyleCache.kt +143 -143
- package/android/src/main/java/com/renderrichtext/renderer/StrikethroughRenderer.kt +28 -28
- package/android/src/main/java/com/renderrichtext/renderer/StrongRenderer.kt +28 -28
- package/android/src/main/java/com/renderrichtext/renderer/TextRenderer.kt +31 -31
- package/android/src/main/java/com/renderrichtext/renderer/ThematicBreakRenderer.kt +46 -46
- package/android/src/main/java/com/renderrichtext/renderer/UnderlineRenderer.kt +28 -28
- package/android/src/main/java/com/renderrichtext/spans/BaseListSpan.kt +164 -164
- package/android/src/main/java/com/renderrichtext/spans/BaselineShiftSpan.kt +27 -27
- package/android/src/main/java/com/renderrichtext/spans/BlockquoteSpan.kt +302 -302
- package/android/src/main/java/com/renderrichtext/spans/CodeBackgroundSpan.kt +205 -205
- package/android/src/main/java/com/renderrichtext/spans/CodeBlockSpan.kt +216 -216
- package/android/src/main/java/com/renderrichtext/spans/CodeSpan.kt +32 -32
- package/android/src/main/java/com/renderrichtext/spans/EmphasisSpan.kt +42 -42
- package/android/src/main/java/com/renderrichtext/spans/FadeInSpan.kt +29 -29
- package/android/src/main/java/com/renderrichtext/spans/HeadingSpan.kt +38 -38
- package/android/src/main/java/com/renderrichtext/spans/HighlightSpan.kt +80 -80
- package/android/src/main/java/com/renderrichtext/spans/ImageSpan.kt +481 -481
- package/android/src/main/java/com/renderrichtext/spans/InlineBackgroundGeometry.kt +42 -42
- package/android/src/main/java/com/renderrichtext/spans/LineHeightSpan.kt +39 -39
- package/android/src/main/java/com/renderrichtext/spans/LinkPillIconCache.kt +154 -154
- package/android/src/main/java/com/renderrichtext/spans/LinkPillSpan.kt +381 -381
- package/android/src/main/java/com/renderrichtext/spans/LinkSpan.kt +68 -68
- package/android/src/main/java/com/renderrichtext/spans/ListItemSpacingSpan.kt +33 -33
- package/android/src/main/java/com/renderrichtext/spans/ListMarkerAnchorSpan.kt +38 -38
- package/android/src/main/java/com/renderrichtext/spans/MarginBottomSpan.kt +76 -76
- package/android/src/main/java/com/renderrichtext/spans/MentionChipSpan.kt +132 -132
- package/android/src/main/java/com/renderrichtext/spans/MentionSpan.kt +46 -46
- package/android/src/main/java/com/renderrichtext/spans/OrderedListSpan.kt +127 -127
- package/android/src/main/java/com/renderrichtext/spans/SpoilerSpan.kt +33 -33
- package/android/src/main/java/com/renderrichtext/spans/StrikethroughSpan.kt +12 -12
- package/android/src/main/java/com/renderrichtext/spans/StrongSpan.kt +39 -39
- package/android/src/main/java/com/renderrichtext/spans/TaskListSpan.kt +143 -143
- package/android/src/main/java/com/renderrichtext/spans/TextSpan.kt +26 -26
- package/android/src/main/java/com/renderrichtext/spans/ThematicBreakSpan.kt +69 -69
- package/android/src/main/java/com/renderrichtext/spans/UnorderedListSpan.kt +96 -96
- package/android/src/main/java/com/renderrichtext/spoiler/SpoilerStubs.kt +46 -46
- package/android/src/main/java/com/renderrichtext/styles/BaseBlockStyle.kt +11 -11
- package/android/src/main/java/com/renderrichtext/styles/BlockquoteStyle.kt +83 -83
- package/android/src/main/java/com/renderrichtext/styles/CodeBlockStyle.kt +80 -80
- package/android/src/main/java/com/renderrichtext/styles/CodeStyle.kt +26 -26
- package/android/src/main/java/com/renderrichtext/styles/EmphasisStyle.kt +21 -21
- package/android/src/main/java/com/renderrichtext/styles/FactoidStyle.kt +21 -21
- package/android/src/main/java/com/renderrichtext/styles/HeadingStyle.kt +33 -33
- package/android/src/main/java/com/renderrichtext/styles/HighlightStyle.kt +19 -19
- package/android/src/main/java/com/renderrichtext/styles/ImageStyle.kt +29 -29
- package/android/src/main/java/com/renderrichtext/styles/InlineImageStyle.kt +17 -17
- package/android/src/main/java/com/renderrichtext/styles/InlineMathStyle.kt +17 -17
- package/android/src/main/java/com/renderrichtext/styles/LinkPillContent.kt +36 -36
- package/android/src/main/java/com/renderrichtext/styles/LinkPillStyle.kt +44 -44
- package/android/src/main/java/com/renderrichtext/styles/LinkStyle.kt +23 -23
- package/android/src/main/java/com/renderrichtext/styles/LinkVariantEntry.kt +34 -34
- package/android/src/main/java/com/renderrichtext/styles/ListStyle.kt +65 -65
- package/android/src/main/java/com/renderrichtext/styles/MathStyle.kt +29 -29
- package/android/src/main/java/com/renderrichtext/styles/MentionStyle.kt +19 -19
- package/android/src/main/java/com/renderrichtext/styles/ParagraphStyle.kt +33 -33
- package/android/src/main/java/com/renderrichtext/styles/SpoilerStyle.kt +27 -27
- package/android/src/main/java/com/renderrichtext/styles/StrikethroughStyle.kt +17 -17
- package/android/src/main/java/com/renderrichtext/styles/StrongStyle.kt +21 -21
- package/android/src/main/java/com/renderrichtext/styles/StyleConfig.kt +411 -411
- package/android/src/main/java/com/renderrichtext/styles/StyleParser.kt +92 -92
- package/android/src/main/java/com/renderrichtext/styles/SubscriptStyle.kt +16 -16
- package/android/src/main/java/com/renderrichtext/styles/SuperscriptStyle.kt +16 -16
- package/android/src/main/java/com/renderrichtext/styles/TableStyle.kt +75 -75
- package/android/src/main/java/com/renderrichtext/styles/TaskListStyle.kt +29 -29
- package/android/src/main/java/com/renderrichtext/styles/TextAlignment.kt +32 -32
- package/android/src/main/java/com/renderrichtext/styles/ThematicBreakStyle.kt +23 -23
- package/android/src/main/java/com/renderrichtext/styles/UnderlineStyle.kt +17 -17
- package/android/src/main/java/com/renderrichtext/styles/VideoStyle.kt +31 -31
- package/android/src/main/java/com/renderrichtext/utils/common/BorderStyleUtils.kt +86 -86
- package/android/src/main/java/com/renderrichtext/utils/common/BreakStrategyUtils.kt +32 -32
- package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockHighlighter.kt +36 -36
- package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockNode.kt +42 -42
- package/android/src/main/java/com/renderrichtext/utils/common/EllipsizeUtils.kt +27 -27
- package/android/src/main/java/com/renderrichtext/utils/common/EnrichedTiptapAncestorUtils.kt +10 -10
- package/android/src/main/java/com/renderrichtext/utils/common/FeatureFlags.kt +9 -9
- package/android/src/main/java/com/renderrichtext/utils/common/LinkRecognitionProps.kt +31 -31
- package/android/src/main/java/com/renderrichtext/utils/common/LinkRegexConfig.kt +56 -56
- package/android/src/main/java/com/renderrichtext/utils/common/MotionPreferences.kt +18 -18
- package/android/src/main/java/com/renderrichtext/utils/common/ReadableMapExtensions.kt +28 -28
- package/android/src/main/java/com/renderrichtext/utils/common/layout/RTLUtils.kt +6 -6
- package/android/src/main/java/com/renderrichtext/utils/common/serialization/TiptapASTSerializer.kt +368 -368
- package/android/src/main/java/com/renderrichtext/utils/text/ImageCache.kt +88 -88
- package/android/src/main/java/com/renderrichtext/utils/text/ImageDownloader.kt +203 -203
- package/android/src/main/java/com/renderrichtext/utils/text/LocalImageLoader.kt +167 -167
- package/android/src/main/java/com/renderrichtext/utils/text/TailFadeInAnimator.kt +84 -84
- package/android/src/main/java/com/renderrichtext/utils/text/conversion/HTMLGenerator.kt +1105 -1105
- package/android/src/main/java/com/renderrichtext/utils/text/conversion/TiptapExtractor.kt +423 -423
- package/android/src/main/java/com/renderrichtext/utils/text/extensions/ParagraphUtils.kt +9 -9
- package/android/src/main/java/com/renderrichtext/utils/text/extensions/SpannableExtensions.kt +9 -9
- package/android/src/main/java/com/renderrichtext/utils/text/extensions/TextPaintExtensions.kt +76 -76
- package/android/src/main/java/com/renderrichtext/utils/text/interaction/CheckboxTouchHelper.kt +61 -61
- package/android/src/main/java/com/renderrichtext/utils/text/interaction/TaskListTapUtils.kt +286 -286
- package/android/src/main/java/com/renderrichtext/utils/text/span/ApplyLineHeight.kt +39 -39
- package/android/src/main/java/com/renderrichtext/utils/text/span/BlockSpacing.kt +142 -142
- package/android/src/main/java/com/renderrichtext/utils/text/span/SpanFlags.kt +43 -43
- package/android/src/main/java/com/renderrichtext/utils/text/span/WidthAwareSpans.kt +30 -30
- package/android/src/main/java/com/renderrichtext/utils/text/view/CodeBlockPressHost.kt +15 -15
- package/android/src/main/java/com/renderrichtext/utils/text/view/ImagePressHost.kt +19 -19
- package/android/src/main/java/com/renderrichtext/utils/text/view/SelectionActionMode.kt +235 -235
- package/android/src/main/java/com/renderrichtext/utils/text/view/TextSelectionColors.kt +48 -48
- package/android/src/main/java/com/renderrichtext/utils/text/view/TextViewSetup.kt +119 -119
- package/android/src/main/java/com/renderrichtext/views/ContextMenuPopup.kt +217 -217
- package/android/src/main/java/com/renderrichtext/views/CopyGlyph.kt +101 -101
- package/android/src/main/java/com/renderrichtext/views/WrapGlyph.kt +40 -40
- package/android/src/main/jni/CMakeLists.txt +52 -52
- package/android/src/main/jni/react/renderer/components/RichTextSpec/ComponentDescriptors.h +30 -30
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextComponentDescriptor.h +29 -29
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.cpp +45 -45
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.h +21 -21
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.cpp +38 -38
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.h +53 -53
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.cpp +9 -9
- package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.h +24 -24
- package/android/src/main/jni/react/renderer/components/RichTextSpec/conversions.h +40 -40
- package/cpp/parser/TiptapParser.hpp +13 -13
- package/cpp/vendor/nlohmann/json.hpp +24765 -24765
- package/ios/EnrichedTiptapText.h +27 -27
- package/ios/attachments/ENRMImageAttachment.h +51 -51
- package/ios/attachments/ENRMImageAttachment.m +398 -398
- package/ios/attachments/ENRMLinkPillAttachment.h +47 -47
- package/ios/attachments/ENRMLinkPillAttachment.m +324 -324
- package/ios/attachments/ENRMLinkPillIconCache.h +28 -28
- package/ios/attachments/ENRMLinkPillIconCache.m +181 -181
- package/ios/attachments/ENRMLinkPillText.h +27 -27
- package/ios/attachments/ENRMLinkPillText.m +118 -118
- package/ios/attachments/ENRMMathInlineAttachment.h +7 -7
- package/ios/attachments/ENRMMathInlineAttachment.m +4 -4
- package/ios/attachments/ThematicBreakAttachment.h +15 -15
- package/ios/attachments/ThematicBreakAttachment.m +33 -33
- package/ios/internals/ENRMViewFreeMeasurement.h +208 -208
- package/ios/internals/EnrichedTiptapTextComponentDescriptor.h +19 -19
- package/ios/internals/EnrichedTiptapTextShadowNode.h +43 -43
- package/ios/internals/EnrichedTiptapTextShadowNode.mm +69 -69
- package/ios/internals/EnrichedTiptapTextState.h +24 -24
- package/ios/internals/MeasurementCache.h +308 -308
- package/ios/internals/ShadowMeasurementUtils.h +145 -145
- package/ios/parser/ENRMTextLinkAttributes.h +7 -7
- package/ios/parser/ENRMTiptapParser.h +33 -33
- package/ios/parser/ENRMTiptapParser.mm +73 -73
- package/ios/parser/TiptapASTNode.m +52 -52
- package/ios/renderer/AttributedRenderer.h +23 -23
- package/ios/renderer/AttributedRenderer.m +229 -229
- package/ios/renderer/BaseRenderer.h +33 -33
- package/ios/renderer/BaseRenderer.m +46 -46
- package/ios/renderer/BaselineShiftTextAttributes.h +10 -10
- package/ios/renderer/BaselineShiftTextAttributes.m +26 -26
- package/ios/renderer/BlankLineRenderer.h +4 -4
- package/ios/renderer/BlankLineRenderer.m +51 -51
- package/ios/renderer/BlockquoteRenderer.h +6 -6
- package/ios/renderer/BlockquoteRenderer.m +258 -258
- package/ios/renderer/CodeBlockRenderer.h +9 -9
- package/ios/renderer/CodeBlockRenderer.m +99 -99
- package/ios/renderer/CodeRenderer.h +9 -9
- package/ios/renderer/CodeRenderer.m +69 -69
- package/ios/renderer/ENRMBlockquoteTextRenderer.h +25 -25
- package/ios/renderer/ENRMBlockquoteTextRenderer.m +48 -48
- package/ios/renderer/EmphasisRenderer.h +5 -5
- package/ios/renderer/EmphasisRenderer.m +99 -99
- package/ios/renderer/HeadingRenderer.h +6 -6
- package/ios/renderer/HeadingRenderer.m +102 -102
- package/ios/renderer/HighlightRenderer.h +7 -7
- package/ios/renderer/HighlightRenderer.m +33 -33
- package/ios/renderer/LinkRenderer.h +6 -6
- package/ios/renderer/LinkRenderer.m +157 -157
- package/ios/renderer/ListItemRenderer.h +24 -24
- package/ios/renderer/ListItemRenderer.m +283 -283
- package/ios/renderer/ListRenderer.h +13 -13
- package/ios/renderer/ListRenderer.m +126 -126
- package/ios/renderer/MentionRenderer.h +9 -9
- package/ios/renderer/MentionRenderer.m +96 -96
- package/ios/renderer/NodeRenderer.h +8 -8
- package/ios/renderer/ParagraphRenderer.h +6 -6
- package/ios/renderer/ParagraphRenderer.m +85 -85
- package/ios/renderer/RenderContext.h +145 -145
- package/ios/renderer/RenderContext.m +402 -402
- package/ios/renderer/RendererFactory.h +12 -12
- package/ios/renderer/StrikethroughRenderer.h +5 -5
- package/ios/renderer/StrikethroughRenderer.m +30 -30
- package/ios/renderer/StrongRenderer.h +5 -5
- package/ios/renderer/StrongRenderer.m +87 -87
- package/ios/renderer/TextRenderer.h +6 -6
- package/ios/renderer/TextRenderer.m +18 -18
- package/ios/renderer/ThematicBreakRenderer.h +4 -4
- package/ios/renderer/ThematicBreakRenderer.m +41 -41
- package/ios/renderer/UnderlineRenderer.h +4 -4
- package/ios/renderer/UnderlineRenderer.m +41 -41
- package/ios/styles/ENRMFontSlot.h +12 -12
- package/ios/styles/ENRMFontSlot.m +24 -24
- package/ios/styles/StyleConfig.h +488 -488
- package/ios/styles/StyleConfig.mm +2820 -2820
- package/ios/utils/AccessibilityInfo.h +35 -35
- package/ios/utils/AccessibilityInfo.m +24 -24
- package/ios/utils/BlockquoteBorder.h +26 -26
- package/ios/utils/BlockquoteBorder.m +294 -294
- package/ios/utils/CodeBackground.h +19 -19
- package/ios/utils/CodeBackground.m +196 -196
- package/ios/utils/CodeBlockBackground.h +17 -17
- package/ios/utils/CodeBlockBackground.m +102 -102
- package/ios/utils/ContextMenuUtils.h +76 -76
- package/ios/utils/ContextMenuUtils.mm +75 -75
- package/ios/utils/ENRMAccessibilityLabels.h +40 -40
- package/ios/utils/ENRMAccessibilityLabels.m +5 -5
- package/ios/utils/ENRMAsyncRenderCoordinator.h +24 -24
- package/ios/utils/ENRMAsyncRenderCoordinator.m +37 -37
- package/ios/utils/ENRMAtomicSize.h +65 -65
- package/ios/utils/ENRMCodeBlockContent.h +37 -37
- package/ios/utils/ENRMCodeBlockContent.mm +89 -89
- package/ios/utils/ENRMFeatureFlags.h +25 -25
- package/ios/utils/ENRMImageDownloader.h +30 -30
- package/ios/utils/ENRMImageDownloader.m +168 -168
- package/ios/utils/ENRMLatexErrorCoordinator.h +27 -27
- package/ios/utils/ENRMLatexErrorCoordinator.mm +75 -75
- package/ios/utils/ENRMLatexErrorReporting.h +13 -13
- package/ios/utils/ENRMLinkContextMenus.h +93 -93
- package/ios/utils/ENRMLinkContextMenus.m +178 -178
- package/ios/utils/ENRMLinkRegexConfig.h +30 -30
- package/ios/utils/ENRMLinkRegexConfig.mm +104 -104
- package/ios/utils/ENRMLinkRegexProps.h +23 -23
- package/ios/utils/ENRMLocalImageLoader.h +35 -35
- package/ios/utils/ENRMLocalImageLoader.m +111 -111
- package/ios/utils/ENRMMenuAction.h +27 -27
- package/ios/utils/ENRMMenuAction.m +33 -33
- package/ios/utils/ENRMSpoilerOverlayManager.h +15 -15
- package/ios/utils/ENRMSpoilerOverlayManager.m +19 -19
- package/ios/utils/ENRMSpoilerTapUtils.h +6 -6
- package/ios/utils/ENRMSpoilerTapUtils.m +11 -11
- package/ios/utils/ENRMTailFadeInAnimator.h +16 -16
- package/ios/utils/ENRMTailFadeInAnimator.m +161 -161
- package/ios/utils/ENRMTextHitTest.h +23 -23
- package/ios/utils/ENRMTextHitTest.m +20 -20
- package/ios/utils/ENRMTextInteractionUtils.h +20 -20
- package/ios/utils/ENRMTextInteractionUtils.m +15 -15
- package/ios/utils/ENRMTextRenderer.h +38 -38
- package/ios/utils/ENRMTextRenderer.m +54 -54
- package/ios/utils/ENRMTextViewSetup.h +132 -132
- package/ios/utils/ENRMUIKit.h +429 -429
- package/ios/utils/EditMenuUtils.h +105 -105
- package/ios/utils/EditMenuUtils.m +143 -143
- package/ios/utils/FontScaleObserver.h +10 -10
- package/ios/utils/FontScaleObserver.m +55 -55
- package/ios/utils/FontUtils.h +31 -31
- package/ios/utils/FontUtils.m +63 -63
- package/ios/utils/HTMLGenerator.h +25 -25
- package/ios/utils/HTMLGenerator.m +1041 -1041
- package/ios/utils/HeightUpdateUtils.h +25 -25
- package/ios/utils/ImageRequestHeaderUtils.h +35 -35
- package/ios/utils/LastElementUtils.h +97 -97
- package/ios/utils/LinkContextMenuUtils.h +55 -55
- package/ios/utils/LinkPillContentUtils.h +46 -46
- package/ios/utils/ListMarkerDrawer.h +15 -15
- package/ios/utils/ListMarkerDrawer.m +239 -239
- package/ios/utils/MentionBackground.h +26 -26
- package/ios/utils/MentionBackground.m +86 -86
- package/ios/utils/ParagraphStyleUtils.h +87 -87
- package/ios/utils/ParagraphStyleUtils.m +484 -484
- package/ios/utils/PasteboardUtils.h +48 -48
- package/ios/utils/PasteboardUtils.m +166 -166
- package/ios/utils/RTFExportUtils.h +24 -24
- package/ios/utils/RTFExportUtils.m +299 -299
- package/ios/utils/SelectionColorUtils.h +14 -14
- package/ios/utils/StylePropsUtils.h +1340 -1340
- package/ios/utils/TaskListTapUtils.h +6 -6
- package/ios/utils/TaskListTapUtils.m +14 -14
- package/ios/utils/TextViewLayoutManager.h +14 -14
- package/ios/utils/TiptapAccessibilityElementBuilder.h +43 -43
- package/ios/utils/TiptapAccessibilityElementBuilder.m +432 -432
- package/ios/utils/TiptapExtractor.h +17 -17
- package/ios/utils/TiptapExtractor.m +367 -367
- package/ios/views/ENRMTiptapTextView.h +25 -25
- package/ios/views/ENRMTiptapTextView.m +247 -247
- package/package.json +65 -70
- package/react-native.config.js +10 -10
- package/src/admonitionDefaults.ts +58 -58
- package/src/darkModeTextStyle.ts +99 -99
- package/src/headingDefaults.ts +21 -21
- package/src/index.tsx +4 -4
- package/src/linkVariantUtils.ts +120 -120
- package/src/normalizeTiptapStyle.ts +397 -397
- package/src/rn-globals.d.ts +1 -1
- package/src/styleUtils.ts +107 -107
- package/src/types/TiptapStyle.ts +532 -532
- package/src/types/TiptapStyleInternal.ts +299 -299
- package/src/types/linkPill.ts +9 -9
- package/src/utils/isDev.ts +4 -4
|
@@ -1,1105 +1,1105 @@
|
|
|
1
|
-
package com.renderrichtext.utils.text.conversion
|
|
2
|
-
|
|
3
|
-
import android.graphics.Typeface
|
|
4
|
-
import android.text.Layout
|
|
5
|
-
import android.text.Spannable
|
|
6
|
-
import android.text.style.StyleSpan
|
|
7
|
-
import android.text.style.UnderlineSpan
|
|
8
|
-
import com.renderrichtext.spans.BaseListSpan
|
|
9
|
-
import com.renderrichtext.spans.BlockquoteSpan
|
|
10
|
-
import com.renderrichtext.spans.CodeBlockSpan
|
|
11
|
-
import com.renderrichtext.spans.CodeSpan
|
|
12
|
-
import com.renderrichtext.spans.EmphasisSpan
|
|
13
|
-
import com.renderrichtext.spans.FactoidPaddingSpan
|
|
14
|
-
import com.renderrichtext.spans.HeadingSpan
|
|
15
|
-
import com.renderrichtext.spans.HighlightSpan
|
|
16
|
-
import com.renderrichtext.spans.ImageSpan
|
|
17
|
-
import com.renderrichtext.spans.LinkSpan
|
|
18
|
-
import com.renderrichtext.spans.OrderedListSpan
|
|
19
|
-
import com.renderrichtext.spans.StrikethroughSpan
|
|
20
|
-
import com.renderrichtext.spans.StrongSpan
|
|
21
|
-
import com.renderrichtext.spans.TaskListSpan
|
|
22
|
-
import com.renderrichtext.spans.UnorderedListSpan
|
|
23
|
-
import com.renderrichtext.styles.StyleConfig
|
|
24
|
-
|
|
25
|
-
/** Generates semantic HTML with inline styles from Spannable text. */
|
|
26
|
-
object HTMLGenerator {
|
|
27
|
-
private const val OBJECT_REPLACEMENT_CHAR = '\uFFFC'
|
|
28
|
-
|
|
29
|
-
/** Pre-computed styles to avoid repeated StyleConfig method calls. */
|
|
30
|
-
private class CachedStyles(
|
|
31
|
-
style: StyleConfig,
|
|
32
|
-
private val fontDensity: Float,
|
|
33
|
-
private val dimDensity: Float,
|
|
34
|
-
) {
|
|
35
|
-
// Convert device pixels back to CSS pixels
|
|
36
|
-
private fun fontPx(px: Float) = (px / fontDensity).toInt()
|
|
37
|
-
|
|
38
|
-
private fun dimPx(px: Float) = (px / dimDensity).toInt()
|
|
39
|
-
|
|
40
|
-
// Paragraph
|
|
41
|
-
val paragraphColor: String
|
|
42
|
-
val paragraphFontSize: Int
|
|
43
|
-
val paragraphMarginBottom: Int
|
|
44
|
-
|
|
45
|
-
// Code block
|
|
46
|
-
val codeBlockColor: String
|
|
47
|
-
val codeBlockBgColor: String
|
|
48
|
-
val codeBlockFontSize: Int
|
|
49
|
-
val codeBlockPadding: Int
|
|
50
|
-
val codeBlockBorderRadius: Int
|
|
51
|
-
val codeBlockMarginBottom: Int
|
|
52
|
-
|
|
53
|
-
// Inline code
|
|
54
|
-
val codeColor: String
|
|
55
|
-
val codeBgColor: String
|
|
56
|
-
|
|
57
|
-
// Blockquote
|
|
58
|
-
val blockquoteColor: String
|
|
59
|
-
val blockquoteBgColor: String
|
|
60
|
-
val blockquoteBorderColor: String
|
|
61
|
-
val blockquoteBorderWidth: Int
|
|
62
|
-
val blockquoteGapWidth: Int
|
|
63
|
-
val blockquoteMarginBottom: Int
|
|
64
|
-
val blockquoteFontSize: Int
|
|
65
|
-
|
|
66
|
-
// List
|
|
67
|
-
val listColor: String
|
|
68
|
-
val listFontSize: Int
|
|
69
|
-
val listMarginBottom: Int
|
|
70
|
-
val listMarginLeft: Int
|
|
71
|
-
|
|
72
|
-
// Task list checkbox
|
|
73
|
-
val taskCheckedColor: String
|
|
74
|
-
val taskBorderColor: String
|
|
75
|
-
val taskCheckmarkColor: String
|
|
76
|
-
val taskCheckboxSize: Int
|
|
77
|
-
val taskCheckboxBorderRadius: Int
|
|
78
|
-
|
|
79
|
-
// Link
|
|
80
|
-
val linkFontFamily: String
|
|
81
|
-
val linkColor: String
|
|
82
|
-
val linkUnderline: Boolean
|
|
83
|
-
|
|
84
|
-
// Strong/Emphasis/Strikethrough/Underline
|
|
85
|
-
val strongColor: String?
|
|
86
|
-
val emphasisColor: String?
|
|
87
|
-
val strikethroughColor: String?
|
|
88
|
-
val underlineColor: String?
|
|
89
|
-
val highlightColor: String
|
|
90
|
-
val highlightBackgroundColor: String
|
|
91
|
-
|
|
92
|
-
// Image
|
|
93
|
-
val imageMarginBottom: Int
|
|
94
|
-
val imageBorderRadius: Int
|
|
95
|
-
|
|
96
|
-
// Fixed HTML values (not from StyleConfig)
|
|
97
|
-
val blockquotePaddingVertical = "8px"
|
|
98
|
-
val blockquoteBorderRadiusCorners = "border-start-end-radius: 8px; border-end-end-radius: 8px"
|
|
99
|
-
val blockquoteNestedMargin = "8px 0 0 0"
|
|
100
|
-
val blockquoteParagraphMargin = "0 0 4px 0"
|
|
101
|
-
val inlineImageHeight = "1.2em"
|
|
102
|
-
val inlineImageVerticalAlign = "-0.2em"
|
|
103
|
-
val codePadding = "2px 4px"
|
|
104
|
-
val codeBorderRadius = "4px"
|
|
105
|
-
val codeFontSize = "1em"
|
|
106
|
-
|
|
107
|
-
// Headings (array for O(1) lookup)
|
|
108
|
-
val headingFontSizes: IntArray
|
|
109
|
-
val headingFontWeights: Array<String>
|
|
110
|
-
val headingColors: Array<String>
|
|
111
|
-
val headingMarginBottoms: IntArray
|
|
112
|
-
|
|
113
|
-
init {
|
|
114
|
-
// Paragraph
|
|
115
|
-
val pStyle = style.paragraphStyle
|
|
116
|
-
paragraphColor = colorToCSS(pStyle.color)
|
|
117
|
-
paragraphFontSize = fontPx(pStyle.fontSize)
|
|
118
|
-
paragraphMarginBottom = dimPx(pStyle.marginBottom)
|
|
119
|
-
|
|
120
|
-
// Code block
|
|
121
|
-
val cbStyle = style.codeBlockStyle
|
|
122
|
-
codeBlockColor = colorToCSS(cbStyle.color)
|
|
123
|
-
codeBlockBgColor = colorToCSS(cbStyle.backgroundColor)
|
|
124
|
-
codeBlockFontSize = fontPx(cbStyle.fontSize)
|
|
125
|
-
codeBlockPadding = dimPx(cbStyle.padding)
|
|
126
|
-
codeBlockBorderRadius = dimPx(cbStyle.borderRadius)
|
|
127
|
-
codeBlockMarginBottom = dimPx(cbStyle.marginBottom)
|
|
128
|
-
|
|
129
|
-
// Inline code
|
|
130
|
-
val cStyle = style.codeStyle
|
|
131
|
-
codeColor = colorToCSS(cStyle.color)
|
|
132
|
-
codeBgColor = colorToCSS(cStyle.backgroundColor)
|
|
133
|
-
|
|
134
|
-
// Blockquote
|
|
135
|
-
val bqStyle = style.blockquoteStyle
|
|
136
|
-
blockquoteColor = colorToCSS(bqStyle.color)
|
|
137
|
-
blockquoteBgColor = colorToCSS(bqStyle.backgroundColor ?: 0)
|
|
138
|
-
blockquoteBorderColor = colorToCSS(bqStyle.borderColor)
|
|
139
|
-
blockquoteBorderWidth = dimPx(bqStyle.borderWidth)
|
|
140
|
-
blockquoteGapWidth = dimPx(bqStyle.gapWidth)
|
|
141
|
-
blockquoteMarginBottom = dimPx(bqStyle.marginBottom)
|
|
142
|
-
blockquoteFontSize = fontPx(bqStyle.fontSize)
|
|
143
|
-
|
|
144
|
-
// List
|
|
145
|
-
val lStyle = style.listStyle
|
|
146
|
-
listColor = colorToCSS(lStyle.color)
|
|
147
|
-
listFontSize = fontPx(lStyle.fontSize)
|
|
148
|
-
listMarginBottom = dimPx(lStyle.marginBottom)
|
|
149
|
-
listMarginLeft = dimPx(lStyle.marginLeft)
|
|
150
|
-
|
|
151
|
-
// Task list checkbox
|
|
152
|
-
val tlStyle = style.taskListStyle
|
|
153
|
-
taskCheckedColor = colorToCSS(tlStyle.checkedColor)
|
|
154
|
-
taskBorderColor = colorToCSS(tlStyle.borderColor)
|
|
155
|
-
taskCheckmarkColor = colorToCSS(tlStyle.checkmarkColor)
|
|
156
|
-
taskCheckboxSize = dimPx(tlStyle.checkboxSize)
|
|
157
|
-
taskCheckboxBorderRadius = dimPx(tlStyle.checkboxBorderRadius)
|
|
158
|
-
|
|
159
|
-
// Link
|
|
160
|
-
linkFontFamily = style.linkStyle.fontFamily
|
|
161
|
-
linkColor = colorToCSS(style.linkStyle.color)
|
|
162
|
-
linkUnderline = style.linkStyle.underline
|
|
163
|
-
|
|
164
|
-
// Strong/Emphasis/Strikethrough/Underline (nullable for inherit)
|
|
165
|
-
val sc = style.strongStyle.color
|
|
166
|
-
strongColor = if (sc != null && sc != 0) colorToCSS(sc) else null
|
|
167
|
-
val ec = style.emphasisStyle.color
|
|
168
|
-
emphasisColor = if (ec != null && ec != 0) colorToCSS(ec) else null
|
|
169
|
-
val strikeColor = style.strikethroughStyle.color
|
|
170
|
-
strikethroughColor = if (strikeColor != 0) colorToCSS(strikeColor) else null
|
|
171
|
-
val underline = style.underlineStyle.color
|
|
172
|
-
underlineColor = if (underline != 0) colorToCSS(underline) else null
|
|
173
|
-
highlightColor = colorToCSS(style.highlightStyle.color)
|
|
174
|
-
highlightBackgroundColor = colorToCSS(style.highlightStyle.backgroundColor)
|
|
175
|
-
|
|
176
|
-
// Image
|
|
177
|
-
val imgStyle = style.imageStyle
|
|
178
|
-
imageMarginBottom = dimPx(imgStyle.marginBottom)
|
|
179
|
-
imageBorderRadius = dimPx(imgStyle.borderRadius)
|
|
180
|
-
|
|
181
|
-
// Headings (1-6, index 0-5)
|
|
182
|
-
headingFontSizes = IntArray(6)
|
|
183
|
-
headingFontWeights = Array(6) { "" }
|
|
184
|
-
headingColors = Array(6) { "" }
|
|
185
|
-
headingMarginBottoms = IntArray(6)
|
|
186
|
-
|
|
187
|
-
for (level in 1..6) {
|
|
188
|
-
val hStyle = style.headingStyles[level]!!
|
|
189
|
-
val idx = level - 1
|
|
190
|
-
headingFontSizes[idx] = fontPx(hStyle.fontSize)
|
|
191
|
-
headingFontWeights[idx] = fontWeightToCSS(hStyle.fontWeight)
|
|
192
|
-
headingColors[idx] = colorToCSS(hStyle.color)
|
|
193
|
-
headingMarginBottoms[idx] = dimPx(hStyle.marginBottom)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
companion object {
|
|
198
|
-
private fun colorToCSS(color: Int): String {
|
|
199
|
-
if (color == 0) return "inherit"
|
|
200
|
-
val alpha = (color shr 24) and 0xFF
|
|
201
|
-
val red = (color shr 16) and 0xFF
|
|
202
|
-
val green = (color shr 8) and 0xFF
|
|
203
|
-
val blue = color and 0xFF
|
|
204
|
-
|
|
205
|
-
return if (alpha < 255) {
|
|
206
|
-
"rgba($red, $green, $blue, ${alpha / 255f})"
|
|
207
|
-
} else {
|
|
208
|
-
String.format("#%02X%02X%02X", red, green, blue)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
fun fontWeightToCSS(fontWeight: String): String =
|
|
213
|
-
when {
|
|
214
|
-
fontWeight.equals("bold", ignoreCase = true) -> "700"
|
|
215
|
-
fontWeight.equals("semibold", ignoreCase = true) -> "600"
|
|
216
|
-
fontWeight.equals("medium", ignoreCase = true) -> "500"
|
|
217
|
-
fontWeight.isEmpty() || fontWeight.equals("normal", ignoreCase = true) -> "normal"
|
|
218
|
-
else -> fontWeight
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private class GeneratorState {
|
|
224
|
-
var inCodeBlock = false
|
|
225
|
-
var previousWasCodeBlock = false
|
|
226
|
-
val codeBlockLines = ArrayList<String>(16) // Pre-sized
|
|
227
|
-
|
|
228
|
-
var inBlockquote = false
|
|
229
|
-
var blockquoteDepth = -1
|
|
230
|
-
var previousWasBlockquote = false
|
|
231
|
-
|
|
232
|
-
var listDepth = -1
|
|
233
|
-
val openListTypes = ArrayList<Boolean>(4) // true = ol, false = ul
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Paragraph type constants (H1-H6 = 1-6 directly from HeadingSpan.level)
|
|
237
|
-
private const val TYPE_NORMAL = 0
|
|
238
|
-
private const val TYPE_H1 = 1
|
|
239
|
-
private const val TYPE_H6 = 6
|
|
240
|
-
private const val TYPE_CODE_BLOCK = 7
|
|
241
|
-
private const val TYPE_BLOCKQUOTE = 8
|
|
242
|
-
private const val TYPE_ORDERED_LIST = 9
|
|
243
|
-
private const val TYPE_UNORDERED_LIST = 10
|
|
244
|
-
private const val TYPE_TASK_LIST = 11
|
|
245
|
-
|
|
246
|
-
private data class ParagraphInfo(
|
|
247
|
-
val start: Int,
|
|
248
|
-
val end: Int,
|
|
249
|
-
val type: Int,
|
|
250
|
-
val depth: Int = 0,
|
|
251
|
-
val isTaskChecked: Boolean = false,
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
fun generateHTML(
|
|
255
|
-
text: Spannable,
|
|
256
|
-
style: StyleConfig,
|
|
257
|
-
scaledDensity: Float = 1f,
|
|
258
|
-
density: Float = 1f,
|
|
259
|
-
isRTL: Boolean = false,
|
|
260
|
-
): String {
|
|
261
|
-
if (text.isEmpty()) return "<html></html>"
|
|
262
|
-
|
|
263
|
-
// Pre-cache all styles (single allocation)
|
|
264
|
-
val styles = CachedStyles(style, scaledDensity, density)
|
|
265
|
-
val state = GeneratorState()
|
|
266
|
-
|
|
267
|
-
// Estimate capacity (average 2x text length)
|
|
268
|
-
val html = StringBuilder(text.length * 2)
|
|
269
|
-
if (isRTL) {
|
|
270
|
-
html.append("<html dir=\"rtl\">")
|
|
271
|
-
html.append("<div dir=\"rtl\" style=\"direction: rtl; text-align: right;\">")
|
|
272
|
-
} else {
|
|
273
|
-
html.append("<html>")
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Collect paragraphs
|
|
277
|
-
val paragraphs = collectParagraphs(text)
|
|
278
|
-
|
|
279
|
-
for (para in paragraphs) {
|
|
280
|
-
processParagraph(html, text, para, styles, state)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
closeRemainingContainers(html, state, styles)
|
|
284
|
-
if (isRTL) {
|
|
285
|
-
html.append("</div>")
|
|
286
|
-
}
|
|
287
|
-
html.append("</html>")
|
|
288
|
-
|
|
289
|
-
return html.toString()
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private fun processParagraph(
|
|
293
|
-
html: StringBuilder,
|
|
294
|
-
text: Spannable,
|
|
295
|
-
para: ParagraphInfo,
|
|
296
|
-
styles: CachedStyles,
|
|
297
|
-
state: GeneratorState,
|
|
298
|
-
) {
|
|
299
|
-
val paraText = text.subSequence(para.start, para.end).toString().trimEnd('\n')
|
|
300
|
-
|
|
301
|
-
// Handle empty paragraphs
|
|
302
|
-
if (paraText.isEmpty() && para.type == TYPE_NORMAL) {
|
|
303
|
-
closeAllBlockquotes(html, state)
|
|
304
|
-
state.previousWasBlockquote = false
|
|
305
|
-
return
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Get content range (trim trailing newline)
|
|
309
|
-
val contentEnd = if (para.end > para.start && text[para.end - 1] == '\n') para.end - 1 else para.end
|
|
310
|
-
val isCodeBlock = para.type == TYPE_CODE_BLOCK
|
|
311
|
-
val inlineContent = generateInlineHTML(text, para.start, contentEnd, styles, isCodeBlock)
|
|
312
|
-
|
|
313
|
-
// Handle different paragraph types
|
|
314
|
-
when (para.type) {
|
|
315
|
-
TYPE_CODE_BLOCK -> handleCodeBlock(inlineContent, state)
|
|
316
|
-
TYPE_BLOCKQUOTE -> handleBlockquote(html, inlineContent, para, styles, state)
|
|
317
|
-
TYPE_ORDERED_LIST, TYPE_UNORDERED_LIST, TYPE_TASK_LIST -> handleList(html, inlineContent, para, styles, state)
|
|
318
|
-
in TYPE_H1..TYPE_H6 -> handleHeading(html, inlineContent, para.type, styles, state)
|
|
319
|
-
else -> handleNormalParagraph(html, inlineContent, styles, state)
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
private fun handleCodeBlock(
|
|
324
|
-
content: String,
|
|
325
|
-
state: GeneratorState,
|
|
326
|
-
) {
|
|
327
|
-
if (!state.inCodeBlock) {
|
|
328
|
-
state.inCodeBlock = true
|
|
329
|
-
state.codeBlockLines.clear()
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
state.codeBlockLines.add(content.trimStart())
|
|
333
|
-
state.previousWasCodeBlock = true
|
|
334
|
-
state.previousWasBlockquote = false
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
private fun outputCodeBlock(
|
|
338
|
-
html: StringBuilder,
|
|
339
|
-
lines: List<String>,
|
|
340
|
-
styles: CachedStyles,
|
|
341
|
-
) {
|
|
342
|
-
if (lines.isEmpty()) return
|
|
343
|
-
|
|
344
|
-
html
|
|
345
|
-
.append("<pre dir=\"ltr\" style=\"background-color: ")
|
|
346
|
-
.append(styles.codeBlockBgColor)
|
|
347
|
-
.append("; padding: ")
|
|
348
|
-
.append(styles.codeBlockPadding)
|
|
349
|
-
.append("px; border-radius: ")
|
|
350
|
-
.append(styles.codeBlockBorderRadius)
|
|
351
|
-
.append("px; margin: 0 0 ")
|
|
352
|
-
.append(styles.codeBlockMarginBottom)
|
|
353
|
-
.append("px 0; overflow-x: auto; text-align: left;\"><code style=\"font-family: Menlo, Monaco, Consolas, monospace; font-size: ")
|
|
354
|
-
.append(styles.codeBlockFontSize)
|
|
355
|
-
.append("px; color: ")
|
|
356
|
-
.append(styles.codeBlockColor)
|
|
357
|
-
.append(";\">")
|
|
358
|
-
|
|
359
|
-
for (i in lines.indices) {
|
|
360
|
-
if (i > 0) html.append('\n')
|
|
361
|
-
html.append(lines[i])
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
html.append("</code></pre>")
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
private fun handleBlockquote(
|
|
368
|
-
html: StringBuilder,
|
|
369
|
-
content: String,
|
|
370
|
-
para: ParagraphInfo,
|
|
371
|
-
styles: CachedStyles,
|
|
372
|
-
state: GeneratorState,
|
|
373
|
-
) {
|
|
374
|
-
closeCodeBlockIfOpen(html, state, styles)
|
|
375
|
-
|
|
376
|
-
val depth = para.depth
|
|
377
|
-
|
|
378
|
-
// Reset if coming from non-blockquote content
|
|
379
|
-
if (!state.previousWasBlockquote && state.inBlockquote) {
|
|
380
|
-
closeAllBlockquotes(html, state)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
while (state.blockquoteDepth > depth) {
|
|
384
|
-
html.append("</blockquote>")
|
|
385
|
-
state.blockquoteDepth--
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
while (state.blockquoteDepth < depth) {
|
|
389
|
-
state.blockquoteDepth++
|
|
390
|
-
state.inBlockquote = true
|
|
391
|
-
|
|
392
|
-
if (state.blockquoteDepth == 0) {
|
|
393
|
-
html
|
|
394
|
-
.append("<blockquote style=\"background-color: ")
|
|
395
|
-
.append(styles.blockquoteBgColor)
|
|
396
|
-
.append("; border-inline-start: ")
|
|
397
|
-
.append(styles.blockquoteBorderWidth)
|
|
398
|
-
.append("px solid ")
|
|
399
|
-
.append(styles.blockquoteBorderColor)
|
|
400
|
-
.append("; padding: ")
|
|
401
|
-
.append(styles.blockquotePaddingVertical)
|
|
402
|
-
.append(" ")
|
|
403
|
-
.append(styles.blockquoteGapWidth)
|
|
404
|
-
.append("px; margin: 0 0 ")
|
|
405
|
-
.append(styles.blockquoteMarginBottom)
|
|
406
|
-
.append("px 0; ")
|
|
407
|
-
.append(styles.blockquoteBorderRadiusCorners)
|
|
408
|
-
.append(";\">")
|
|
409
|
-
} else {
|
|
410
|
-
html
|
|
411
|
-
.append("<blockquote style=\"border-inline-start: ")
|
|
412
|
-
.append(styles.blockquoteBorderWidth)
|
|
413
|
-
.append("px solid ")
|
|
414
|
-
.append(styles.blockquoteBorderColor)
|
|
415
|
-
.append("; padding-inline-start: ")
|
|
416
|
-
.append(styles.blockquoteGapWidth)
|
|
417
|
-
.append("px; margin: ")
|
|
418
|
-
.append(styles.blockquoteNestedMargin)
|
|
419
|
-
.append(";\">")
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
html
|
|
424
|
-
.append("<p style=\"margin: ")
|
|
425
|
-
.append(styles.blockquoteParagraphMargin)
|
|
426
|
-
.append("; color: ")
|
|
427
|
-
.append(styles.blockquoteColor)
|
|
428
|
-
.append("; font-size: ")
|
|
429
|
-
.append(styles.blockquoteFontSize)
|
|
430
|
-
.append("px;\">")
|
|
431
|
-
.append(content)
|
|
432
|
-
.append("</p>")
|
|
433
|
-
|
|
434
|
-
state.previousWasBlockquote = true
|
|
435
|
-
state.previousWasCodeBlock = false
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
private fun handleList(
|
|
439
|
-
html: StringBuilder,
|
|
440
|
-
content: String,
|
|
441
|
-
para: ParagraphInfo,
|
|
442
|
-
styles: CachedStyles,
|
|
443
|
-
state: GeneratorState,
|
|
444
|
-
) {
|
|
445
|
-
closeCodeBlockIfOpen(html, state, styles)
|
|
446
|
-
closeAllBlockquotes(html, state)
|
|
447
|
-
|
|
448
|
-
val depth = para.depth
|
|
449
|
-
val isOrdered = para.type == TYPE_ORDERED_LIST
|
|
450
|
-
val isTask = para.type == TYPE_TASK_LIST
|
|
451
|
-
|
|
452
|
-
// Close lists to shallower depth
|
|
453
|
-
while (state.listDepth > depth) {
|
|
454
|
-
html.append(if (state.openListTypes.lastOrNull() == true) "</ol>" else "</ul>")
|
|
455
|
-
if (state.openListTypes.isNotEmpty()) state.openListTypes.removeAt(state.openListTypes.lastIndex)
|
|
456
|
-
state.listDepth--
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Handle list type change at same depth (ul <-> ol)
|
|
460
|
-
if (state.listDepth == depth && state.openListTypes.isNotEmpty()) {
|
|
461
|
-
if (state.openListTypes.last() != isOrdered) {
|
|
462
|
-
html.append(if (state.openListTypes.last()) "</ol>" else "</ul>")
|
|
463
|
-
state.openListTypes.removeAt(state.openListTypes.lastIndex)
|
|
464
|
-
state.listDepth--
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Open lists to deeper depth
|
|
469
|
-
while (state.listDepth < depth) {
|
|
470
|
-
state.listDepth++
|
|
471
|
-
val marginStyle =
|
|
472
|
-
if (state.listDepth == 0) {
|
|
473
|
-
"margin: 0 0 ${styles.paragraphMarginBottom}px 0; "
|
|
474
|
-
} else {
|
|
475
|
-
"margin: 0; "
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (isOrdered) {
|
|
479
|
-
html
|
|
480
|
-
.append("<ol style=\"")
|
|
481
|
-
.append(marginStyle)
|
|
482
|
-
.append("padding-inline-start: ")
|
|
483
|
-
.append(styles.listMarginLeft)
|
|
484
|
-
.append("px;\">")
|
|
485
|
-
state.openListTypes.add(true)
|
|
486
|
-
} else {
|
|
487
|
-
html
|
|
488
|
-
.append("<ul style=\"")
|
|
489
|
-
.append(marginStyle)
|
|
490
|
-
.append("padding-inline-start: ")
|
|
491
|
-
.append(styles.listMarginLeft)
|
|
492
|
-
.append("px; list-style-type: disc;\">")
|
|
493
|
-
state.openListTypes.add(false)
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
html
|
|
498
|
-
.append("<li style=\"margin-bottom: ")
|
|
499
|
-
.append(styles.listMarginBottom)
|
|
500
|
-
.append("px; color: ")
|
|
501
|
-
.append(styles.listColor)
|
|
502
|
-
.append("; font-size: ")
|
|
503
|
-
.append(styles.listFontSize)
|
|
504
|
-
.append("px;")
|
|
505
|
-
|
|
506
|
-
if (isTask) {
|
|
507
|
-
html.append(" list-style-type: none;")
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
html.append("\">")
|
|
511
|
-
|
|
512
|
-
if (isTask) {
|
|
513
|
-
val size = styles.taskCheckboxSize
|
|
514
|
-
val radius = styles.taskCheckboxBorderRadius
|
|
515
|
-
if (para.isTaskChecked) {
|
|
516
|
-
html
|
|
517
|
-
.append("<span style=\"display: inline-block; width: ")
|
|
518
|
-
.append(size)
|
|
519
|
-
.append("px; height: ")
|
|
520
|
-
.append(size)
|
|
521
|
-
.append("px; border-radius: ")
|
|
522
|
-
.append(radius)
|
|
523
|
-
.append("px; background-color: ")
|
|
524
|
-
.append(styles.taskCheckedColor)
|
|
525
|
-
.append("; color: ")
|
|
526
|
-
.append(styles.taskCheckmarkColor)
|
|
527
|
-
.append("; font-size: ")
|
|
528
|
-
.append(size - 2)
|
|
529
|
-
.append("px; line-height: ")
|
|
530
|
-
.append(size)
|
|
531
|
-
.append("px; text-align: center; vertical-align: middle; margin-inline-end: 4px;\">✓</span> ")
|
|
532
|
-
} else {
|
|
533
|
-
html
|
|
534
|
-
.append("<span style=\"display: inline-block; width: ")
|
|
535
|
-
.append(size)
|
|
536
|
-
.append("px; height: ")
|
|
537
|
-
.append(size)
|
|
538
|
-
.append("px; border-radius: ")
|
|
539
|
-
.append(radius)
|
|
540
|
-
.append("px; border: 1.5px solid ")
|
|
541
|
-
.append(styles.taskBorderColor)
|
|
542
|
-
.append("; vertical-align: middle; margin-inline-end: 4px;\"></span> ")
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
html
|
|
547
|
-
.append(content)
|
|
548
|
-
.append("</li>")
|
|
549
|
-
|
|
550
|
-
state.previousWasBlockquote = false
|
|
551
|
-
state.previousWasCodeBlock = false
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
private fun handleHeading(
|
|
555
|
-
html: StringBuilder,
|
|
556
|
-
content: String,
|
|
557
|
-
type: Int,
|
|
558
|
-
styles: CachedStyles,
|
|
559
|
-
state: GeneratorState,
|
|
560
|
-
) {
|
|
561
|
-
closeCodeBlockIfOpen(html, state, styles)
|
|
562
|
-
closeAllBlockquotes(html, state)
|
|
563
|
-
closeListsIfOpen(html, state)
|
|
564
|
-
|
|
565
|
-
val level = type // TYPE_H1..TYPE_H6 = 1..6
|
|
566
|
-
val idx = level - 1
|
|
567
|
-
|
|
568
|
-
html
|
|
569
|
-
.append("<h")
|
|
570
|
-
.append(level)
|
|
571
|
-
.append(" style=\"font-size: ")
|
|
572
|
-
.append(styles.headingFontSizes[idx])
|
|
573
|
-
.append("px; font-weight: ")
|
|
574
|
-
.append(styles.headingFontWeights[idx])
|
|
575
|
-
.append("; color: ")
|
|
576
|
-
.append(styles.headingColors[idx])
|
|
577
|
-
.append("; margin: 0 0 ")
|
|
578
|
-
.append(styles.headingMarginBottoms[idx])
|
|
579
|
-
.append("px 0;\">")
|
|
580
|
-
.append(content)
|
|
581
|
-
.append("</h")
|
|
582
|
-
.append(level)
|
|
583
|
-
.append('>')
|
|
584
|
-
|
|
585
|
-
state.previousWasBlockquote = false
|
|
586
|
-
state.previousWasCodeBlock = false
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
private fun handleNormalParagraph(
|
|
590
|
-
html: StringBuilder,
|
|
591
|
-
content: String,
|
|
592
|
-
styles: CachedStyles,
|
|
593
|
-
state: GeneratorState,
|
|
594
|
-
) {
|
|
595
|
-
closeCodeBlockIfOpen(html, state, styles)
|
|
596
|
-
closeAllBlockquotes(html, state)
|
|
597
|
-
closeListsIfOpen(html, state)
|
|
598
|
-
|
|
599
|
-
html
|
|
600
|
-
.append("<p style=\"margin: 0 0 ")
|
|
601
|
-
.append(styles.paragraphMarginBottom)
|
|
602
|
-
.append("px 0; color: ")
|
|
603
|
-
.append(styles.paragraphColor)
|
|
604
|
-
.append("; font-size: ")
|
|
605
|
-
.append(styles.paragraphFontSize)
|
|
606
|
-
.append("px;\">")
|
|
607
|
-
.append(content)
|
|
608
|
-
.append("</p>")
|
|
609
|
-
|
|
610
|
-
state.previousWasBlockquote = false
|
|
611
|
-
state.previousWasCodeBlock = false
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
private fun closeCodeBlockIfOpen(
|
|
615
|
-
html: StringBuilder,
|
|
616
|
-
state: GeneratorState,
|
|
617
|
-
styles: CachedStyles,
|
|
618
|
-
) {
|
|
619
|
-
if (state.inCodeBlock) {
|
|
620
|
-
state.inCodeBlock = false
|
|
621
|
-
outputCodeBlock(html, state.codeBlockLines, styles)
|
|
622
|
-
state.codeBlockLines.clear()
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
private fun closeAllBlockquotes(
|
|
627
|
-
html: StringBuilder,
|
|
628
|
-
state: GeneratorState,
|
|
629
|
-
) {
|
|
630
|
-
while (state.blockquoteDepth >= 0) {
|
|
631
|
-
html.append("</blockquote>")
|
|
632
|
-
state.blockquoteDepth--
|
|
633
|
-
}
|
|
634
|
-
state.inBlockquote = false
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
private fun closeListsIfOpen(
|
|
638
|
-
html: StringBuilder,
|
|
639
|
-
state: GeneratorState,
|
|
640
|
-
) {
|
|
641
|
-
while (state.openListTypes.isNotEmpty()) {
|
|
642
|
-
html.append(if (state.openListTypes.last()) "</ol>" else "</ul>")
|
|
643
|
-
state.openListTypes.removeAt(state.openListTypes.lastIndex)
|
|
644
|
-
}
|
|
645
|
-
state.listDepth = -1
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
private fun closeRemainingContainers(
|
|
649
|
-
html: StringBuilder,
|
|
650
|
-
state: GeneratorState,
|
|
651
|
-
styles: CachedStyles,
|
|
652
|
-
) {
|
|
653
|
-
closeCodeBlockIfOpen(html, state, styles)
|
|
654
|
-
closeAllBlockquotes(html, state)
|
|
655
|
-
closeListsIfOpen(html, state)
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
private fun generateInlineHTML(
|
|
659
|
-
text: Spannable,
|
|
660
|
-
start: Int,
|
|
661
|
-
end: Int,
|
|
662
|
-
styles: CachedStyles,
|
|
663
|
-
isCodeBlock: Boolean,
|
|
664
|
-
): String {
|
|
665
|
-
val html = StringBuilder(end - start + 32)
|
|
666
|
-
var i = start
|
|
667
|
-
|
|
668
|
-
while (i < end) {
|
|
669
|
-
val char = text[i]
|
|
670
|
-
|
|
671
|
-
when {
|
|
672
|
-
char == FactoidPaddingSpan.MARK &&
|
|
673
|
-
text.getSpans(i, i + 1, FactoidPaddingSpan::class.java).isNotEmpty() -> {
|
|
674
|
-
i++
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
char == '\n' && i == end - 1 -> {
|
|
678
|
-
i++
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
char == OBJECT_REPLACEMENT_CHAR -> {
|
|
682
|
-
appendReplacementChar(html, text, i, styles)
|
|
683
|
-
i++
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
else -> {
|
|
687
|
-
val segmentEnd = minOf(text.nextSpanTransition(i, end, Any::class.java), end)
|
|
688
|
-
val segmentText = text.subSequence(i, segmentEnd).toString()
|
|
689
|
-
|
|
690
|
-
if (segmentText.isNotEmpty() && segmentText != "\n") {
|
|
691
|
-
appendStyledSegment(html, text, i, segmentEnd, segmentText, styles, isCodeBlock)
|
|
692
|
-
}
|
|
693
|
-
i = segmentEnd
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return html.toString()
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
private fun appendReplacementChar(
|
|
702
|
-
html: StringBuilder,
|
|
703
|
-
text: Spannable,
|
|
704
|
-
pos: Int,
|
|
705
|
-
styles: CachedStyles,
|
|
706
|
-
) {
|
|
707
|
-
val imageSpans = text.getSpans(pos, pos + 1, ImageSpan::class.java)
|
|
708
|
-
if (imageSpans.isNotEmpty()) {
|
|
709
|
-
val imgSpan = imageSpans[0]
|
|
710
|
-
|
|
711
|
-
if (imgSpan.isInline) {
|
|
712
|
-
html.append("<img src=\"")
|
|
713
|
-
escapeHTMLTo(html, imgSpan.imageUrl)
|
|
714
|
-
html
|
|
715
|
-
.append("\" style=\"height: ")
|
|
716
|
-
.append(styles.inlineImageHeight)
|
|
717
|
-
.append("; width: auto; vertical-align: ")
|
|
718
|
-
.append(styles.inlineImageVerticalAlign)
|
|
719
|
-
.append(";\">")
|
|
720
|
-
} else {
|
|
721
|
-
html
|
|
722
|
-
.append("</p><div style=\"margin-bottom: ")
|
|
723
|
-
.append(styles.imageMarginBottom)
|
|
724
|
-
.append("px;\"><img src=\"")
|
|
725
|
-
escapeHTMLTo(html, imgSpan.imageUrl)
|
|
726
|
-
html
|
|
727
|
-
.append("\" style=\"max-width: 100%; border-radius: ")
|
|
728
|
-
.append(styles.imageBorderRadius)
|
|
729
|
-
.append("px;\"></div><p>")
|
|
730
|
-
}
|
|
731
|
-
return
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
val mathLatex = extractMathLatex(text, pos, pos + 1)
|
|
735
|
-
if (mathLatex != null) {
|
|
736
|
-
html
|
|
737
|
-
.append("<code style=\"background-color: ")
|
|
738
|
-
.append(styles.codeBgColor)
|
|
739
|
-
.append("; color: ")
|
|
740
|
-
.append(styles.codeColor)
|
|
741
|
-
.append("; padding: ")
|
|
742
|
-
.append(styles.codePadding)
|
|
743
|
-
.append("; border-radius: ")
|
|
744
|
-
.append(styles.codeBorderRadius)
|
|
745
|
-
.append("; font-size: ")
|
|
746
|
-
.append(styles.codeFontSize)
|
|
747
|
-
.append("; font-family: Menlo, Monaco, Consolas, monospace;\">")
|
|
748
|
-
escapeHTMLTo(html, mathLatex)
|
|
749
|
-
html.append("</code>")
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
private fun appendStyledSegment(
|
|
754
|
-
html: StringBuilder,
|
|
755
|
-
text: Spannable,
|
|
756
|
-
start: Int,
|
|
757
|
-
end: Int,
|
|
758
|
-
content: String,
|
|
759
|
-
styles: CachedStyles,
|
|
760
|
-
isCodeBlock: Boolean,
|
|
761
|
-
) {
|
|
762
|
-
val strongSpans = text.getSpans(start, end, StrongSpan::class.java)
|
|
763
|
-
val styleSpans = text.getSpans(start, end, StyleSpan::class.java)
|
|
764
|
-
val emphasisSpans = text.getSpans(start, end, EmphasisSpan::class.java)
|
|
765
|
-
val underlineSpans = text.getSpans(start, end, UnderlineSpan::class.java)
|
|
766
|
-
val strikethroughSpans = text.getSpans(start, end, StrikethroughSpan::class.java)
|
|
767
|
-
val linkSpans = text.getSpans(start, end, LinkSpan::class.java)
|
|
768
|
-
val codeSpans = text.getSpans(start, end, CodeSpan::class.java)
|
|
769
|
-
val highlightSpans = text.getSpans(start, end, HighlightSpan::class.java)
|
|
770
|
-
|
|
771
|
-
val isBold =
|
|
772
|
-
strongSpans.isNotEmpty() ||
|
|
773
|
-
styleSpans.any { it.style == Typeface.BOLD || it.style == Typeface.BOLD_ITALIC }
|
|
774
|
-
val isItalic =
|
|
775
|
-
emphasisSpans.isNotEmpty() ||
|
|
776
|
-
styleSpans.any { it.style == Typeface.ITALIC || it.style == Typeface.BOLD_ITALIC }
|
|
777
|
-
val isUnderline = underlineSpans.isNotEmpty()
|
|
778
|
-
val isStrikethrough = strikethroughSpans.isNotEmpty()
|
|
779
|
-
val link = linkSpans.firstOrNull()
|
|
780
|
-
val isCode = codeSpans.isNotEmpty() && !isCodeBlock
|
|
781
|
-
val isHighlight = highlightSpans.isNotEmpty()
|
|
782
|
-
|
|
783
|
-
if (isHighlight) {
|
|
784
|
-
html
|
|
785
|
-
.append("<mark style=\"background-color: ")
|
|
786
|
-
.append(styles.highlightBackgroundColor)
|
|
787
|
-
if (styles.highlightColor != styles.paragraphColor) {
|
|
788
|
-
html
|
|
789
|
-
.append("; color: ")
|
|
790
|
-
.append(styles.highlightColor)
|
|
791
|
-
} else {
|
|
792
|
-
html.append("; color: inherit")
|
|
793
|
-
}
|
|
794
|
-
html.append(";\">")
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
link?.let {
|
|
798
|
-
html.append("<a href=\"")
|
|
799
|
-
escapeHTMLTo(html, it.url)
|
|
800
|
-
html
|
|
801
|
-
.append("\" style=\"color: ")
|
|
802
|
-
.append(styles.linkColor)
|
|
803
|
-
.append("; text-decoration: ")
|
|
804
|
-
.append(if (styles.linkUnderline) "underline" else "none")
|
|
805
|
-
if (styles.linkFontFamily.isNotEmpty()) {
|
|
806
|
-
html.append("; font-family: '").append(styles.linkFontFamily).append("'")
|
|
807
|
-
}
|
|
808
|
-
html.append(";\">")
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
if (isCode) {
|
|
812
|
-
html
|
|
813
|
-
.append("<code style=\"background-color: ")
|
|
814
|
-
.append(styles.codeBgColor)
|
|
815
|
-
.append("; color: ")
|
|
816
|
-
.append(styles.codeColor)
|
|
817
|
-
.append("; padding: ")
|
|
818
|
-
.append(styles.codePadding)
|
|
819
|
-
.append("; border-radius: ")
|
|
820
|
-
.append(styles.codeBorderRadius)
|
|
821
|
-
.append("; font-size: ")
|
|
822
|
-
.append(styles.codeFontSize)
|
|
823
|
-
.append("; font-family: Menlo, Monaco, Consolas, monospace;\">")
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
if (isBold) {
|
|
827
|
-
if (styles.strongColor != null) {
|
|
828
|
-
html.append("<strong style=\"color: ").append(styles.strongColor).append(";\">")
|
|
829
|
-
} else {
|
|
830
|
-
html.append("<strong>")
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if (isItalic) {
|
|
835
|
-
if (styles.emphasisColor != null) {
|
|
836
|
-
html.append("<em style=\"color: ").append(styles.emphasisColor).append(";\">")
|
|
837
|
-
} else {
|
|
838
|
-
html.append("<em>")
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (isStrikethrough) {
|
|
843
|
-
if (styles.strikethroughColor != null) {
|
|
844
|
-
html.append("<s style=\"text-decoration-color: ").append(styles.strikethroughColor).append(";\">")
|
|
845
|
-
} else {
|
|
846
|
-
html.append("<s>")
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
if (isUnderline && link == null) {
|
|
850
|
-
if (styles.underlineColor != null) {
|
|
851
|
-
html.append("<u style=\"text-decoration-color: ").append(styles.underlineColor).append(";\">")
|
|
852
|
-
} else {
|
|
853
|
-
html.append("<u>")
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
escapeHTMLTo(html, content.trimEnd('\n'), preserveLineBreaks = !isCodeBlock)
|
|
858
|
-
|
|
859
|
-
if (isUnderline && link == null) html.append("</u>")
|
|
860
|
-
if (isStrikethrough) html.append("</s>")
|
|
861
|
-
if (isItalic) html.append("</em>")
|
|
862
|
-
if (isBold) html.append("</strong>")
|
|
863
|
-
if (isCode) html.append("</code>")
|
|
864
|
-
if (link != null) html.append("</a>")
|
|
865
|
-
if (isHighlight) html.append("</mark>")
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
private fun collectParagraphs(text: Spannable): ArrayList<ParagraphInfo> {
|
|
869
|
-
val string = text.toString()
|
|
870
|
-
val paragraphs = ArrayList<ParagraphInfo>(string.length / 40 + 1) // Estimate
|
|
871
|
-
var currentIndex = 0
|
|
872
|
-
|
|
873
|
-
while (currentIndex < string.length) {
|
|
874
|
-
var lineEnd = string.indexOf('\n', currentIndex)
|
|
875
|
-
if (lineEnd == -1) lineEnd = string.length else lineEnd++
|
|
876
|
-
|
|
877
|
-
val listSpan = innermostListSpan(text, currentIndex)
|
|
878
|
-
val type = getParagraphType(text, currentIndex, listSpan)
|
|
879
|
-
val depth = getDepthForType(text, currentIndex, type, listSpan)
|
|
880
|
-
val isTaskChecked = type == TYPE_TASK_LIST && (listSpan as? TaskListSpan)?.isChecked == true
|
|
881
|
-
|
|
882
|
-
paragraphs.add(ParagraphInfo(currentIndex, lineEnd, type, depth, isTaskChecked))
|
|
883
|
-
currentIndex = lineEnd
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
return paragraphs
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* The list span that owns the line starting at [start]. A list item's span covers its nested
|
|
891
|
-
* children, so every ancestor is returned here as well - the innermost span is the item that
|
|
892
|
-
* actually starts on this line.
|
|
893
|
-
*/
|
|
894
|
-
private fun innermostListSpan(
|
|
895
|
-
text: Spannable,
|
|
896
|
-
start: Int,
|
|
897
|
-
): BaseListSpan? =
|
|
898
|
-
text
|
|
899
|
-
.getSpans(start, minOf(start + 1, text.length), BaseListSpan::class.java)
|
|
900
|
-
.maxByOrNull { it.depth }
|
|
901
|
-
|
|
902
|
-
private fun getParagraphType(
|
|
903
|
-
text: Spannable,
|
|
904
|
-
start: Int,
|
|
905
|
-
listSpan: BaseListSpan?,
|
|
906
|
-
): Int {
|
|
907
|
-
val end = minOf(start + 1, text.length)
|
|
908
|
-
|
|
909
|
-
if (text.getSpans(start, end, CodeBlockSpan::class.java).isNotEmpty()) return TYPE_CODE_BLOCK
|
|
910
|
-
|
|
911
|
-
val headingSpans = text.getSpans(start, end, HeadingSpan::class.java)
|
|
912
|
-
if (headingSpans.isNotEmpty()) return headingSpans[0].level.coerceIn(1, 6)
|
|
913
|
-
|
|
914
|
-
if (text.getSpans(start, end, BlockquoteSpan::class.java).isNotEmpty()) return TYPE_BLOCKQUOTE
|
|
915
|
-
|
|
916
|
-
return when (listSpan) {
|
|
917
|
-
is TaskListSpan -> TYPE_TASK_LIST
|
|
918
|
-
is OrderedListSpan -> TYPE_ORDERED_LIST
|
|
919
|
-
is UnorderedListSpan -> TYPE_UNORDERED_LIST
|
|
920
|
-
else -> TYPE_NORMAL
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
private fun getDepthForType(
|
|
925
|
-
text: Spannable,
|
|
926
|
-
start: Int,
|
|
927
|
-
type: Int,
|
|
928
|
-
listSpan: BaseListSpan?,
|
|
929
|
-
): Int =
|
|
930
|
-
when (type) {
|
|
931
|
-
TYPE_BLOCKQUOTE -> {
|
|
932
|
-
text.getSpans(start, start + 1, BlockquoteSpan::class.java).maxOfOrNull { it.depth } ?: 0
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
TYPE_ORDERED_LIST, TYPE_UNORDERED_LIST, TYPE_TASK_LIST -> {
|
|
936
|
-
listSpan?.depth ?: 0
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
else -> {
|
|
940
|
-
0
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
fun generateTableHTML(
|
|
945
|
-
rows: List<List<Triple<CharSequence, Boolean, Layout.Alignment>>>,
|
|
946
|
-
style: StyleConfig,
|
|
947
|
-
scaledDensity: Float = 1f,
|
|
948
|
-
density: Float = 1f,
|
|
949
|
-
): String {
|
|
950
|
-
if (rows.isEmpty()) return ""
|
|
951
|
-
|
|
952
|
-
val tableStyle = style.tableStyle
|
|
953
|
-
val cachedStyles = CachedStyles(style, scaledDensity, density)
|
|
954
|
-
|
|
955
|
-
val borderColorCSS = colorToCSS(tableStyle.borderColor)
|
|
956
|
-
val borderWidthPx = tableStyle.borderWidth / density
|
|
957
|
-
val borderRadiusPx = tableStyle.borderRadius / density
|
|
958
|
-
val fontSizePx = tableStyle.fontSize / scaledDensity
|
|
959
|
-
val cellPaddingVerticalPx = tableStyle.cellPaddingVertical / density
|
|
960
|
-
val cellPaddingHorizontalPx = tableStyle.cellPaddingHorizontal / density
|
|
961
|
-
val bodyFontWeight = CachedStyles.fontWeightToCSS(tableStyle.fontWeight)
|
|
962
|
-
|
|
963
|
-
return buildString(rows.size * 250) {
|
|
964
|
-
append("<table style=\"border-collapse: separate; border-spacing: 0; ")
|
|
965
|
-
append("border: ${borderWidthPx.toInt()}px solid $borderColorCSS; ")
|
|
966
|
-
append("border-radius: ${borderRadiusPx.toInt()}px; ")
|
|
967
|
-
append("overflow: hidden; font-size: ${fontSizePx.toInt()}px;\">")
|
|
968
|
-
|
|
969
|
-
var hasOpenedBody = false
|
|
970
|
-
var bodyRowIndex = 0
|
|
971
|
-
|
|
972
|
-
for (row in rows) {
|
|
973
|
-
if (row.isEmpty()) continue
|
|
974
|
-
|
|
975
|
-
val isHeaderRow = row.first().second
|
|
976
|
-
|
|
977
|
-
if (isHeaderRow) {
|
|
978
|
-
append("<thead>")
|
|
979
|
-
} else if (!hasOpenedBody) {
|
|
980
|
-
append("<tbody>")
|
|
981
|
-
hasOpenedBody = true
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
append("<tr>")
|
|
985
|
-
|
|
986
|
-
for ((cellText, isHeader, alignment) in row) {
|
|
987
|
-
val backgroundColor =
|
|
988
|
-
when {
|
|
989
|
-
isHeader -> colorToCSS(tableStyle.headerBackgroundColor)
|
|
990
|
-
bodyRowIndex % 2 == 0 -> colorToCSS(tableStyle.rowEvenBackgroundColor)
|
|
991
|
-
else -> colorToCSS(tableStyle.rowOddBackgroundColor)
|
|
992
|
-
}
|
|
993
|
-
val textColor = if (isHeader) colorToCSS(tableStyle.headerTextColor) else colorToCSS(tableStyle.color)
|
|
994
|
-
val textAlignment = alignmentToCSS(alignment)
|
|
995
|
-
val fontWeight = if (isHeader) "bold" else bodyFontWeight
|
|
996
|
-
|
|
997
|
-
val htmlTag = if (isHeader) "th" else "td"
|
|
998
|
-
append("<$htmlTag style=\"")
|
|
999
|
-
append("padding: ${cellPaddingVerticalPx.toInt()}px ${cellPaddingHorizontalPx.toInt()}px; ")
|
|
1000
|
-
append("text-align: $textAlignment; ")
|
|
1001
|
-
append("background-color: $backgroundColor; ")
|
|
1002
|
-
append("color: $textColor; ")
|
|
1003
|
-
append("border: ${borderWidthPx.toInt()}px solid $borderColorCSS; ")
|
|
1004
|
-
append("font-weight: $fontWeight;\">")
|
|
1005
|
-
|
|
1006
|
-
if (cellText.isNotEmpty()) {
|
|
1007
|
-
if (cellText is Spannable) {
|
|
1008
|
-
append(generateInlineHTML(cellText, 0, cellText.length, cachedStyles, false))
|
|
1009
|
-
} else {
|
|
1010
|
-
escapeHTMLTo(this, cellText.toString(), preserveLineBreaks = true)
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
append("</$htmlTag>")
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
append("</tr>")
|
|
1018
|
-
|
|
1019
|
-
if (isHeaderRow) {
|
|
1020
|
-
append("</thead>")
|
|
1021
|
-
} else {
|
|
1022
|
-
bodyRowIndex++
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
if (hasOpenedBody) append("</tbody>")
|
|
1027
|
-
append("</table>")
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
private fun alignmentToCSS(alignment: Layout.Alignment): String =
|
|
1032
|
-
when (alignment) {
|
|
1033
|
-
Layout.Alignment.ALIGN_CENTER -> "center"
|
|
1034
|
-
Layout.Alignment.ALIGN_OPPOSITE -> "right"
|
|
1035
|
-
else -> "left"
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
private fun colorToCSS(color: Int): String {
|
|
1039
|
-
if (color == 0) return "inherit"
|
|
1040
|
-
val alpha = (color shr 24) and 0xFF
|
|
1041
|
-
val red = (color shr 16) and 0xFF
|
|
1042
|
-
val green = (color shr 8) and 0xFF
|
|
1043
|
-
val blue = color and 0xFF
|
|
1044
|
-
return if (alpha < 255) "rgba($red, $green, $blue, ${alpha / 255f})" else String.format("#%02X%02X%02X", red, green, blue)
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
/** Fast HTML escape using single-pass character scanning. */
|
|
1048
|
-
private fun escapeHTMLTo(
|
|
1049
|
-
output: StringBuilder,
|
|
1050
|
-
text: String,
|
|
1051
|
-
preserveLineBreaks: Boolean = false,
|
|
1052
|
-
) {
|
|
1053
|
-
val len = text.length
|
|
1054
|
-
|
|
1055
|
-
// Fast path: skip if no escaping needed
|
|
1056
|
-
var needsEscape = false
|
|
1057
|
-
for (i in 0 until len) {
|
|
1058
|
-
val c = text[i]
|
|
1059
|
-
if (c == '&' || c == '<' || c == '>' || c == '"' || c == '\'' ||
|
|
1060
|
-
(preserveLineBreaks && (c == '\r' || c == '\n' || c == '\u2028' || c == '\u2029'))
|
|
1061
|
-
) {
|
|
1062
|
-
needsEscape = true
|
|
1063
|
-
break
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
if (!needsEscape) {
|
|
1067
|
-
output.append(text)
|
|
1068
|
-
return
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
var index = 0
|
|
1072
|
-
while (index < len) {
|
|
1073
|
-
val c = text[index++]
|
|
1074
|
-
if (preserveLineBreaks && (c == '\r' || c == '\n' || c == '\u2028' || c == '\u2029')) {
|
|
1075
|
-
if (c == '\r' && index < len && text[index] == '\n') index++
|
|
1076
|
-
output.append("<br>")
|
|
1077
|
-
continue
|
|
1078
|
-
}
|
|
1079
|
-
when (c) {
|
|
1080
|
-
'&' -> output.append("&")
|
|
1081
|
-
'<' -> output.append("<")
|
|
1082
|
-
'>' -> output.append(">")
|
|
1083
|
-
'"' -> output.append(""")
|
|
1084
|
-
'\'' -> output.append("'")
|
|
1085
|
-
else -> output.append(c)
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
private fun extractMathLatex(
|
|
1091
|
-
text: android.text.Spannable,
|
|
1092
|
-
start: Int,
|
|
1093
|
-
end: Int,
|
|
1094
|
-
): String? {
|
|
1095
|
-
return try {
|
|
1096
|
-
val mathInlineSpanClass = Class.forName("com.renderrichtext.spans.MathInlineSpan")
|
|
1097
|
-
val spans = text.getSpans(start, end, mathInlineSpanClass)
|
|
1098
|
-
if (spans.isEmpty()) return null
|
|
1099
|
-
val latexField = mathInlineSpanClass.getDeclaredField("latex").apply { isAccessible = true }
|
|
1100
|
-
latexField.get(spans[0]) as? String
|
|
1101
|
-
} catch (_: Exception) {
|
|
1102
|
-
null
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1
|
+
package com.renderrichtext.utils.text.conversion
|
|
2
|
+
|
|
3
|
+
import android.graphics.Typeface
|
|
4
|
+
import android.text.Layout
|
|
5
|
+
import android.text.Spannable
|
|
6
|
+
import android.text.style.StyleSpan
|
|
7
|
+
import android.text.style.UnderlineSpan
|
|
8
|
+
import com.renderrichtext.spans.BaseListSpan
|
|
9
|
+
import com.renderrichtext.spans.BlockquoteSpan
|
|
10
|
+
import com.renderrichtext.spans.CodeBlockSpan
|
|
11
|
+
import com.renderrichtext.spans.CodeSpan
|
|
12
|
+
import com.renderrichtext.spans.EmphasisSpan
|
|
13
|
+
import com.renderrichtext.spans.FactoidPaddingSpan
|
|
14
|
+
import com.renderrichtext.spans.HeadingSpan
|
|
15
|
+
import com.renderrichtext.spans.HighlightSpan
|
|
16
|
+
import com.renderrichtext.spans.ImageSpan
|
|
17
|
+
import com.renderrichtext.spans.LinkSpan
|
|
18
|
+
import com.renderrichtext.spans.OrderedListSpan
|
|
19
|
+
import com.renderrichtext.spans.StrikethroughSpan
|
|
20
|
+
import com.renderrichtext.spans.StrongSpan
|
|
21
|
+
import com.renderrichtext.spans.TaskListSpan
|
|
22
|
+
import com.renderrichtext.spans.UnorderedListSpan
|
|
23
|
+
import com.renderrichtext.styles.StyleConfig
|
|
24
|
+
|
|
25
|
+
/** Generates semantic HTML with inline styles from Spannable text. */
|
|
26
|
+
object HTMLGenerator {
|
|
27
|
+
private const val OBJECT_REPLACEMENT_CHAR = '\uFFFC'
|
|
28
|
+
|
|
29
|
+
/** Pre-computed styles to avoid repeated StyleConfig method calls. */
|
|
30
|
+
private class CachedStyles(
|
|
31
|
+
style: StyleConfig,
|
|
32
|
+
private val fontDensity: Float,
|
|
33
|
+
private val dimDensity: Float,
|
|
34
|
+
) {
|
|
35
|
+
// Convert device pixels back to CSS pixels
|
|
36
|
+
private fun fontPx(px: Float) = (px / fontDensity).toInt()
|
|
37
|
+
|
|
38
|
+
private fun dimPx(px: Float) = (px / dimDensity).toInt()
|
|
39
|
+
|
|
40
|
+
// Paragraph
|
|
41
|
+
val paragraphColor: String
|
|
42
|
+
val paragraphFontSize: Int
|
|
43
|
+
val paragraphMarginBottom: Int
|
|
44
|
+
|
|
45
|
+
// Code block
|
|
46
|
+
val codeBlockColor: String
|
|
47
|
+
val codeBlockBgColor: String
|
|
48
|
+
val codeBlockFontSize: Int
|
|
49
|
+
val codeBlockPadding: Int
|
|
50
|
+
val codeBlockBorderRadius: Int
|
|
51
|
+
val codeBlockMarginBottom: Int
|
|
52
|
+
|
|
53
|
+
// Inline code
|
|
54
|
+
val codeColor: String
|
|
55
|
+
val codeBgColor: String
|
|
56
|
+
|
|
57
|
+
// Blockquote
|
|
58
|
+
val blockquoteColor: String
|
|
59
|
+
val blockquoteBgColor: String
|
|
60
|
+
val blockquoteBorderColor: String
|
|
61
|
+
val blockquoteBorderWidth: Int
|
|
62
|
+
val blockquoteGapWidth: Int
|
|
63
|
+
val blockquoteMarginBottom: Int
|
|
64
|
+
val blockquoteFontSize: Int
|
|
65
|
+
|
|
66
|
+
// List
|
|
67
|
+
val listColor: String
|
|
68
|
+
val listFontSize: Int
|
|
69
|
+
val listMarginBottom: Int
|
|
70
|
+
val listMarginLeft: Int
|
|
71
|
+
|
|
72
|
+
// Task list checkbox
|
|
73
|
+
val taskCheckedColor: String
|
|
74
|
+
val taskBorderColor: String
|
|
75
|
+
val taskCheckmarkColor: String
|
|
76
|
+
val taskCheckboxSize: Int
|
|
77
|
+
val taskCheckboxBorderRadius: Int
|
|
78
|
+
|
|
79
|
+
// Link
|
|
80
|
+
val linkFontFamily: String
|
|
81
|
+
val linkColor: String
|
|
82
|
+
val linkUnderline: Boolean
|
|
83
|
+
|
|
84
|
+
// Strong/Emphasis/Strikethrough/Underline
|
|
85
|
+
val strongColor: String?
|
|
86
|
+
val emphasisColor: String?
|
|
87
|
+
val strikethroughColor: String?
|
|
88
|
+
val underlineColor: String?
|
|
89
|
+
val highlightColor: String
|
|
90
|
+
val highlightBackgroundColor: String
|
|
91
|
+
|
|
92
|
+
// Image
|
|
93
|
+
val imageMarginBottom: Int
|
|
94
|
+
val imageBorderRadius: Int
|
|
95
|
+
|
|
96
|
+
// Fixed HTML values (not from StyleConfig)
|
|
97
|
+
val blockquotePaddingVertical = "8px"
|
|
98
|
+
val blockquoteBorderRadiusCorners = "border-start-end-radius: 8px; border-end-end-radius: 8px"
|
|
99
|
+
val blockquoteNestedMargin = "8px 0 0 0"
|
|
100
|
+
val blockquoteParagraphMargin = "0 0 4px 0"
|
|
101
|
+
val inlineImageHeight = "1.2em"
|
|
102
|
+
val inlineImageVerticalAlign = "-0.2em"
|
|
103
|
+
val codePadding = "2px 4px"
|
|
104
|
+
val codeBorderRadius = "4px"
|
|
105
|
+
val codeFontSize = "1em"
|
|
106
|
+
|
|
107
|
+
// Headings (array for O(1) lookup)
|
|
108
|
+
val headingFontSizes: IntArray
|
|
109
|
+
val headingFontWeights: Array<String>
|
|
110
|
+
val headingColors: Array<String>
|
|
111
|
+
val headingMarginBottoms: IntArray
|
|
112
|
+
|
|
113
|
+
init {
|
|
114
|
+
// Paragraph
|
|
115
|
+
val pStyle = style.paragraphStyle
|
|
116
|
+
paragraphColor = colorToCSS(pStyle.color)
|
|
117
|
+
paragraphFontSize = fontPx(pStyle.fontSize)
|
|
118
|
+
paragraphMarginBottom = dimPx(pStyle.marginBottom)
|
|
119
|
+
|
|
120
|
+
// Code block
|
|
121
|
+
val cbStyle = style.codeBlockStyle
|
|
122
|
+
codeBlockColor = colorToCSS(cbStyle.color)
|
|
123
|
+
codeBlockBgColor = colorToCSS(cbStyle.backgroundColor)
|
|
124
|
+
codeBlockFontSize = fontPx(cbStyle.fontSize)
|
|
125
|
+
codeBlockPadding = dimPx(cbStyle.padding)
|
|
126
|
+
codeBlockBorderRadius = dimPx(cbStyle.borderRadius)
|
|
127
|
+
codeBlockMarginBottom = dimPx(cbStyle.marginBottom)
|
|
128
|
+
|
|
129
|
+
// Inline code
|
|
130
|
+
val cStyle = style.codeStyle
|
|
131
|
+
codeColor = colorToCSS(cStyle.color)
|
|
132
|
+
codeBgColor = colorToCSS(cStyle.backgroundColor)
|
|
133
|
+
|
|
134
|
+
// Blockquote
|
|
135
|
+
val bqStyle = style.blockquoteStyle
|
|
136
|
+
blockquoteColor = colorToCSS(bqStyle.color)
|
|
137
|
+
blockquoteBgColor = colorToCSS(bqStyle.backgroundColor ?: 0)
|
|
138
|
+
blockquoteBorderColor = colorToCSS(bqStyle.borderColor)
|
|
139
|
+
blockquoteBorderWidth = dimPx(bqStyle.borderWidth)
|
|
140
|
+
blockquoteGapWidth = dimPx(bqStyle.gapWidth)
|
|
141
|
+
blockquoteMarginBottom = dimPx(bqStyle.marginBottom)
|
|
142
|
+
blockquoteFontSize = fontPx(bqStyle.fontSize)
|
|
143
|
+
|
|
144
|
+
// List
|
|
145
|
+
val lStyle = style.listStyle
|
|
146
|
+
listColor = colorToCSS(lStyle.color)
|
|
147
|
+
listFontSize = fontPx(lStyle.fontSize)
|
|
148
|
+
listMarginBottom = dimPx(lStyle.marginBottom)
|
|
149
|
+
listMarginLeft = dimPx(lStyle.marginLeft)
|
|
150
|
+
|
|
151
|
+
// Task list checkbox
|
|
152
|
+
val tlStyle = style.taskListStyle
|
|
153
|
+
taskCheckedColor = colorToCSS(tlStyle.checkedColor)
|
|
154
|
+
taskBorderColor = colorToCSS(tlStyle.borderColor)
|
|
155
|
+
taskCheckmarkColor = colorToCSS(tlStyle.checkmarkColor)
|
|
156
|
+
taskCheckboxSize = dimPx(tlStyle.checkboxSize)
|
|
157
|
+
taskCheckboxBorderRadius = dimPx(tlStyle.checkboxBorderRadius)
|
|
158
|
+
|
|
159
|
+
// Link
|
|
160
|
+
linkFontFamily = style.linkStyle.fontFamily
|
|
161
|
+
linkColor = colorToCSS(style.linkStyle.color)
|
|
162
|
+
linkUnderline = style.linkStyle.underline
|
|
163
|
+
|
|
164
|
+
// Strong/Emphasis/Strikethrough/Underline (nullable for inherit)
|
|
165
|
+
val sc = style.strongStyle.color
|
|
166
|
+
strongColor = if (sc != null && sc != 0) colorToCSS(sc) else null
|
|
167
|
+
val ec = style.emphasisStyle.color
|
|
168
|
+
emphasisColor = if (ec != null && ec != 0) colorToCSS(ec) else null
|
|
169
|
+
val strikeColor = style.strikethroughStyle.color
|
|
170
|
+
strikethroughColor = if (strikeColor != 0) colorToCSS(strikeColor) else null
|
|
171
|
+
val underline = style.underlineStyle.color
|
|
172
|
+
underlineColor = if (underline != 0) colorToCSS(underline) else null
|
|
173
|
+
highlightColor = colorToCSS(style.highlightStyle.color)
|
|
174
|
+
highlightBackgroundColor = colorToCSS(style.highlightStyle.backgroundColor)
|
|
175
|
+
|
|
176
|
+
// Image
|
|
177
|
+
val imgStyle = style.imageStyle
|
|
178
|
+
imageMarginBottom = dimPx(imgStyle.marginBottom)
|
|
179
|
+
imageBorderRadius = dimPx(imgStyle.borderRadius)
|
|
180
|
+
|
|
181
|
+
// Headings (1-6, index 0-5)
|
|
182
|
+
headingFontSizes = IntArray(6)
|
|
183
|
+
headingFontWeights = Array(6) { "" }
|
|
184
|
+
headingColors = Array(6) { "" }
|
|
185
|
+
headingMarginBottoms = IntArray(6)
|
|
186
|
+
|
|
187
|
+
for (level in 1..6) {
|
|
188
|
+
val hStyle = style.headingStyles[level]!!
|
|
189
|
+
val idx = level - 1
|
|
190
|
+
headingFontSizes[idx] = fontPx(hStyle.fontSize)
|
|
191
|
+
headingFontWeights[idx] = fontWeightToCSS(hStyle.fontWeight)
|
|
192
|
+
headingColors[idx] = colorToCSS(hStyle.color)
|
|
193
|
+
headingMarginBottoms[idx] = dimPx(hStyle.marginBottom)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
companion object {
|
|
198
|
+
private fun colorToCSS(color: Int): String {
|
|
199
|
+
if (color == 0) return "inherit"
|
|
200
|
+
val alpha = (color shr 24) and 0xFF
|
|
201
|
+
val red = (color shr 16) and 0xFF
|
|
202
|
+
val green = (color shr 8) and 0xFF
|
|
203
|
+
val blue = color and 0xFF
|
|
204
|
+
|
|
205
|
+
return if (alpha < 255) {
|
|
206
|
+
"rgba($red, $green, $blue, ${alpha / 255f})"
|
|
207
|
+
} else {
|
|
208
|
+
String.format("#%02X%02X%02X", red, green, blue)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fun fontWeightToCSS(fontWeight: String): String =
|
|
213
|
+
when {
|
|
214
|
+
fontWeight.equals("bold", ignoreCase = true) -> "700"
|
|
215
|
+
fontWeight.equals("semibold", ignoreCase = true) -> "600"
|
|
216
|
+
fontWeight.equals("medium", ignoreCase = true) -> "500"
|
|
217
|
+
fontWeight.isEmpty() || fontWeight.equals("normal", ignoreCase = true) -> "normal"
|
|
218
|
+
else -> fontWeight
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private class GeneratorState {
|
|
224
|
+
var inCodeBlock = false
|
|
225
|
+
var previousWasCodeBlock = false
|
|
226
|
+
val codeBlockLines = ArrayList<String>(16) // Pre-sized
|
|
227
|
+
|
|
228
|
+
var inBlockquote = false
|
|
229
|
+
var blockquoteDepth = -1
|
|
230
|
+
var previousWasBlockquote = false
|
|
231
|
+
|
|
232
|
+
var listDepth = -1
|
|
233
|
+
val openListTypes = ArrayList<Boolean>(4) // true = ol, false = ul
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Paragraph type constants (H1-H6 = 1-6 directly from HeadingSpan.level)
|
|
237
|
+
private const val TYPE_NORMAL = 0
|
|
238
|
+
private const val TYPE_H1 = 1
|
|
239
|
+
private const val TYPE_H6 = 6
|
|
240
|
+
private const val TYPE_CODE_BLOCK = 7
|
|
241
|
+
private const val TYPE_BLOCKQUOTE = 8
|
|
242
|
+
private const val TYPE_ORDERED_LIST = 9
|
|
243
|
+
private const val TYPE_UNORDERED_LIST = 10
|
|
244
|
+
private const val TYPE_TASK_LIST = 11
|
|
245
|
+
|
|
246
|
+
private data class ParagraphInfo(
|
|
247
|
+
val start: Int,
|
|
248
|
+
val end: Int,
|
|
249
|
+
val type: Int,
|
|
250
|
+
val depth: Int = 0,
|
|
251
|
+
val isTaskChecked: Boolean = false,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
fun generateHTML(
|
|
255
|
+
text: Spannable,
|
|
256
|
+
style: StyleConfig,
|
|
257
|
+
scaledDensity: Float = 1f,
|
|
258
|
+
density: Float = 1f,
|
|
259
|
+
isRTL: Boolean = false,
|
|
260
|
+
): String {
|
|
261
|
+
if (text.isEmpty()) return "<html></html>"
|
|
262
|
+
|
|
263
|
+
// Pre-cache all styles (single allocation)
|
|
264
|
+
val styles = CachedStyles(style, scaledDensity, density)
|
|
265
|
+
val state = GeneratorState()
|
|
266
|
+
|
|
267
|
+
// Estimate capacity (average 2x text length)
|
|
268
|
+
val html = StringBuilder(text.length * 2)
|
|
269
|
+
if (isRTL) {
|
|
270
|
+
html.append("<html dir=\"rtl\">")
|
|
271
|
+
html.append("<div dir=\"rtl\" style=\"direction: rtl; text-align: right;\">")
|
|
272
|
+
} else {
|
|
273
|
+
html.append("<html>")
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Collect paragraphs
|
|
277
|
+
val paragraphs = collectParagraphs(text)
|
|
278
|
+
|
|
279
|
+
for (para in paragraphs) {
|
|
280
|
+
processParagraph(html, text, para, styles, state)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
closeRemainingContainers(html, state, styles)
|
|
284
|
+
if (isRTL) {
|
|
285
|
+
html.append("</div>")
|
|
286
|
+
}
|
|
287
|
+
html.append("</html>")
|
|
288
|
+
|
|
289
|
+
return html.toString()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun processParagraph(
|
|
293
|
+
html: StringBuilder,
|
|
294
|
+
text: Spannable,
|
|
295
|
+
para: ParagraphInfo,
|
|
296
|
+
styles: CachedStyles,
|
|
297
|
+
state: GeneratorState,
|
|
298
|
+
) {
|
|
299
|
+
val paraText = text.subSequence(para.start, para.end).toString().trimEnd('\n')
|
|
300
|
+
|
|
301
|
+
// Handle empty paragraphs
|
|
302
|
+
if (paraText.isEmpty() && para.type == TYPE_NORMAL) {
|
|
303
|
+
closeAllBlockquotes(html, state)
|
|
304
|
+
state.previousWasBlockquote = false
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Get content range (trim trailing newline)
|
|
309
|
+
val contentEnd = if (para.end > para.start && text[para.end - 1] == '\n') para.end - 1 else para.end
|
|
310
|
+
val isCodeBlock = para.type == TYPE_CODE_BLOCK
|
|
311
|
+
val inlineContent = generateInlineHTML(text, para.start, contentEnd, styles, isCodeBlock)
|
|
312
|
+
|
|
313
|
+
// Handle different paragraph types
|
|
314
|
+
when (para.type) {
|
|
315
|
+
TYPE_CODE_BLOCK -> handleCodeBlock(inlineContent, state)
|
|
316
|
+
TYPE_BLOCKQUOTE -> handleBlockquote(html, inlineContent, para, styles, state)
|
|
317
|
+
TYPE_ORDERED_LIST, TYPE_UNORDERED_LIST, TYPE_TASK_LIST -> handleList(html, inlineContent, para, styles, state)
|
|
318
|
+
in TYPE_H1..TYPE_H6 -> handleHeading(html, inlineContent, para.type, styles, state)
|
|
319
|
+
else -> handleNormalParagraph(html, inlineContent, styles, state)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private fun handleCodeBlock(
|
|
324
|
+
content: String,
|
|
325
|
+
state: GeneratorState,
|
|
326
|
+
) {
|
|
327
|
+
if (!state.inCodeBlock) {
|
|
328
|
+
state.inCodeBlock = true
|
|
329
|
+
state.codeBlockLines.clear()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
state.codeBlockLines.add(content.trimStart())
|
|
333
|
+
state.previousWasCodeBlock = true
|
|
334
|
+
state.previousWasBlockquote = false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private fun outputCodeBlock(
|
|
338
|
+
html: StringBuilder,
|
|
339
|
+
lines: List<String>,
|
|
340
|
+
styles: CachedStyles,
|
|
341
|
+
) {
|
|
342
|
+
if (lines.isEmpty()) return
|
|
343
|
+
|
|
344
|
+
html
|
|
345
|
+
.append("<pre dir=\"ltr\" style=\"background-color: ")
|
|
346
|
+
.append(styles.codeBlockBgColor)
|
|
347
|
+
.append("; padding: ")
|
|
348
|
+
.append(styles.codeBlockPadding)
|
|
349
|
+
.append("px; border-radius: ")
|
|
350
|
+
.append(styles.codeBlockBorderRadius)
|
|
351
|
+
.append("px; margin: 0 0 ")
|
|
352
|
+
.append(styles.codeBlockMarginBottom)
|
|
353
|
+
.append("px 0; overflow-x: auto; text-align: left;\"><code style=\"font-family: Menlo, Monaco, Consolas, monospace; font-size: ")
|
|
354
|
+
.append(styles.codeBlockFontSize)
|
|
355
|
+
.append("px; color: ")
|
|
356
|
+
.append(styles.codeBlockColor)
|
|
357
|
+
.append(";\">")
|
|
358
|
+
|
|
359
|
+
for (i in lines.indices) {
|
|
360
|
+
if (i > 0) html.append('\n')
|
|
361
|
+
html.append(lines[i])
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
html.append("</code></pre>")
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private fun handleBlockquote(
|
|
368
|
+
html: StringBuilder,
|
|
369
|
+
content: String,
|
|
370
|
+
para: ParagraphInfo,
|
|
371
|
+
styles: CachedStyles,
|
|
372
|
+
state: GeneratorState,
|
|
373
|
+
) {
|
|
374
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
375
|
+
|
|
376
|
+
val depth = para.depth
|
|
377
|
+
|
|
378
|
+
// Reset if coming from non-blockquote content
|
|
379
|
+
if (!state.previousWasBlockquote && state.inBlockquote) {
|
|
380
|
+
closeAllBlockquotes(html, state)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
while (state.blockquoteDepth > depth) {
|
|
384
|
+
html.append("</blockquote>")
|
|
385
|
+
state.blockquoteDepth--
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
while (state.blockquoteDepth < depth) {
|
|
389
|
+
state.blockquoteDepth++
|
|
390
|
+
state.inBlockquote = true
|
|
391
|
+
|
|
392
|
+
if (state.blockquoteDepth == 0) {
|
|
393
|
+
html
|
|
394
|
+
.append("<blockquote style=\"background-color: ")
|
|
395
|
+
.append(styles.blockquoteBgColor)
|
|
396
|
+
.append("; border-inline-start: ")
|
|
397
|
+
.append(styles.blockquoteBorderWidth)
|
|
398
|
+
.append("px solid ")
|
|
399
|
+
.append(styles.blockquoteBorderColor)
|
|
400
|
+
.append("; padding: ")
|
|
401
|
+
.append(styles.blockquotePaddingVertical)
|
|
402
|
+
.append(" ")
|
|
403
|
+
.append(styles.blockquoteGapWidth)
|
|
404
|
+
.append("px; margin: 0 0 ")
|
|
405
|
+
.append(styles.blockquoteMarginBottom)
|
|
406
|
+
.append("px 0; ")
|
|
407
|
+
.append(styles.blockquoteBorderRadiusCorners)
|
|
408
|
+
.append(";\">")
|
|
409
|
+
} else {
|
|
410
|
+
html
|
|
411
|
+
.append("<blockquote style=\"border-inline-start: ")
|
|
412
|
+
.append(styles.blockquoteBorderWidth)
|
|
413
|
+
.append("px solid ")
|
|
414
|
+
.append(styles.blockquoteBorderColor)
|
|
415
|
+
.append("; padding-inline-start: ")
|
|
416
|
+
.append(styles.blockquoteGapWidth)
|
|
417
|
+
.append("px; margin: ")
|
|
418
|
+
.append(styles.blockquoteNestedMargin)
|
|
419
|
+
.append(";\">")
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
html
|
|
424
|
+
.append("<p style=\"margin: ")
|
|
425
|
+
.append(styles.blockquoteParagraphMargin)
|
|
426
|
+
.append("; color: ")
|
|
427
|
+
.append(styles.blockquoteColor)
|
|
428
|
+
.append("; font-size: ")
|
|
429
|
+
.append(styles.blockquoteFontSize)
|
|
430
|
+
.append("px;\">")
|
|
431
|
+
.append(content)
|
|
432
|
+
.append("</p>")
|
|
433
|
+
|
|
434
|
+
state.previousWasBlockquote = true
|
|
435
|
+
state.previousWasCodeBlock = false
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private fun handleList(
|
|
439
|
+
html: StringBuilder,
|
|
440
|
+
content: String,
|
|
441
|
+
para: ParagraphInfo,
|
|
442
|
+
styles: CachedStyles,
|
|
443
|
+
state: GeneratorState,
|
|
444
|
+
) {
|
|
445
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
446
|
+
closeAllBlockquotes(html, state)
|
|
447
|
+
|
|
448
|
+
val depth = para.depth
|
|
449
|
+
val isOrdered = para.type == TYPE_ORDERED_LIST
|
|
450
|
+
val isTask = para.type == TYPE_TASK_LIST
|
|
451
|
+
|
|
452
|
+
// Close lists to shallower depth
|
|
453
|
+
while (state.listDepth > depth) {
|
|
454
|
+
html.append(if (state.openListTypes.lastOrNull() == true) "</ol>" else "</ul>")
|
|
455
|
+
if (state.openListTypes.isNotEmpty()) state.openListTypes.removeAt(state.openListTypes.lastIndex)
|
|
456
|
+
state.listDepth--
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Handle list type change at same depth (ul <-> ol)
|
|
460
|
+
if (state.listDepth == depth && state.openListTypes.isNotEmpty()) {
|
|
461
|
+
if (state.openListTypes.last() != isOrdered) {
|
|
462
|
+
html.append(if (state.openListTypes.last()) "</ol>" else "</ul>")
|
|
463
|
+
state.openListTypes.removeAt(state.openListTypes.lastIndex)
|
|
464
|
+
state.listDepth--
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Open lists to deeper depth
|
|
469
|
+
while (state.listDepth < depth) {
|
|
470
|
+
state.listDepth++
|
|
471
|
+
val marginStyle =
|
|
472
|
+
if (state.listDepth == 0) {
|
|
473
|
+
"margin: 0 0 ${styles.paragraphMarginBottom}px 0; "
|
|
474
|
+
} else {
|
|
475
|
+
"margin: 0; "
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (isOrdered) {
|
|
479
|
+
html
|
|
480
|
+
.append("<ol style=\"")
|
|
481
|
+
.append(marginStyle)
|
|
482
|
+
.append("padding-inline-start: ")
|
|
483
|
+
.append(styles.listMarginLeft)
|
|
484
|
+
.append("px;\">")
|
|
485
|
+
state.openListTypes.add(true)
|
|
486
|
+
} else {
|
|
487
|
+
html
|
|
488
|
+
.append("<ul style=\"")
|
|
489
|
+
.append(marginStyle)
|
|
490
|
+
.append("padding-inline-start: ")
|
|
491
|
+
.append(styles.listMarginLeft)
|
|
492
|
+
.append("px; list-style-type: disc;\">")
|
|
493
|
+
state.openListTypes.add(false)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
html
|
|
498
|
+
.append("<li style=\"margin-bottom: ")
|
|
499
|
+
.append(styles.listMarginBottom)
|
|
500
|
+
.append("px; color: ")
|
|
501
|
+
.append(styles.listColor)
|
|
502
|
+
.append("; font-size: ")
|
|
503
|
+
.append(styles.listFontSize)
|
|
504
|
+
.append("px;")
|
|
505
|
+
|
|
506
|
+
if (isTask) {
|
|
507
|
+
html.append(" list-style-type: none;")
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
html.append("\">")
|
|
511
|
+
|
|
512
|
+
if (isTask) {
|
|
513
|
+
val size = styles.taskCheckboxSize
|
|
514
|
+
val radius = styles.taskCheckboxBorderRadius
|
|
515
|
+
if (para.isTaskChecked) {
|
|
516
|
+
html
|
|
517
|
+
.append("<span style=\"display: inline-block; width: ")
|
|
518
|
+
.append(size)
|
|
519
|
+
.append("px; height: ")
|
|
520
|
+
.append(size)
|
|
521
|
+
.append("px; border-radius: ")
|
|
522
|
+
.append(radius)
|
|
523
|
+
.append("px; background-color: ")
|
|
524
|
+
.append(styles.taskCheckedColor)
|
|
525
|
+
.append("; color: ")
|
|
526
|
+
.append(styles.taskCheckmarkColor)
|
|
527
|
+
.append("; font-size: ")
|
|
528
|
+
.append(size - 2)
|
|
529
|
+
.append("px; line-height: ")
|
|
530
|
+
.append(size)
|
|
531
|
+
.append("px; text-align: center; vertical-align: middle; margin-inline-end: 4px;\">✓</span> ")
|
|
532
|
+
} else {
|
|
533
|
+
html
|
|
534
|
+
.append("<span style=\"display: inline-block; width: ")
|
|
535
|
+
.append(size)
|
|
536
|
+
.append("px; height: ")
|
|
537
|
+
.append(size)
|
|
538
|
+
.append("px; border-radius: ")
|
|
539
|
+
.append(radius)
|
|
540
|
+
.append("px; border: 1.5px solid ")
|
|
541
|
+
.append(styles.taskBorderColor)
|
|
542
|
+
.append("; vertical-align: middle; margin-inline-end: 4px;\"></span> ")
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
html
|
|
547
|
+
.append(content)
|
|
548
|
+
.append("</li>")
|
|
549
|
+
|
|
550
|
+
state.previousWasBlockquote = false
|
|
551
|
+
state.previousWasCodeBlock = false
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private fun handleHeading(
|
|
555
|
+
html: StringBuilder,
|
|
556
|
+
content: String,
|
|
557
|
+
type: Int,
|
|
558
|
+
styles: CachedStyles,
|
|
559
|
+
state: GeneratorState,
|
|
560
|
+
) {
|
|
561
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
562
|
+
closeAllBlockquotes(html, state)
|
|
563
|
+
closeListsIfOpen(html, state)
|
|
564
|
+
|
|
565
|
+
val level = type // TYPE_H1..TYPE_H6 = 1..6
|
|
566
|
+
val idx = level - 1
|
|
567
|
+
|
|
568
|
+
html
|
|
569
|
+
.append("<h")
|
|
570
|
+
.append(level)
|
|
571
|
+
.append(" style=\"font-size: ")
|
|
572
|
+
.append(styles.headingFontSizes[idx])
|
|
573
|
+
.append("px; font-weight: ")
|
|
574
|
+
.append(styles.headingFontWeights[idx])
|
|
575
|
+
.append("; color: ")
|
|
576
|
+
.append(styles.headingColors[idx])
|
|
577
|
+
.append("; margin: 0 0 ")
|
|
578
|
+
.append(styles.headingMarginBottoms[idx])
|
|
579
|
+
.append("px 0;\">")
|
|
580
|
+
.append(content)
|
|
581
|
+
.append("</h")
|
|
582
|
+
.append(level)
|
|
583
|
+
.append('>')
|
|
584
|
+
|
|
585
|
+
state.previousWasBlockquote = false
|
|
586
|
+
state.previousWasCodeBlock = false
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private fun handleNormalParagraph(
|
|
590
|
+
html: StringBuilder,
|
|
591
|
+
content: String,
|
|
592
|
+
styles: CachedStyles,
|
|
593
|
+
state: GeneratorState,
|
|
594
|
+
) {
|
|
595
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
596
|
+
closeAllBlockquotes(html, state)
|
|
597
|
+
closeListsIfOpen(html, state)
|
|
598
|
+
|
|
599
|
+
html
|
|
600
|
+
.append("<p style=\"margin: 0 0 ")
|
|
601
|
+
.append(styles.paragraphMarginBottom)
|
|
602
|
+
.append("px 0; color: ")
|
|
603
|
+
.append(styles.paragraphColor)
|
|
604
|
+
.append("; font-size: ")
|
|
605
|
+
.append(styles.paragraphFontSize)
|
|
606
|
+
.append("px;\">")
|
|
607
|
+
.append(content)
|
|
608
|
+
.append("</p>")
|
|
609
|
+
|
|
610
|
+
state.previousWasBlockquote = false
|
|
611
|
+
state.previousWasCodeBlock = false
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private fun closeCodeBlockIfOpen(
|
|
615
|
+
html: StringBuilder,
|
|
616
|
+
state: GeneratorState,
|
|
617
|
+
styles: CachedStyles,
|
|
618
|
+
) {
|
|
619
|
+
if (state.inCodeBlock) {
|
|
620
|
+
state.inCodeBlock = false
|
|
621
|
+
outputCodeBlock(html, state.codeBlockLines, styles)
|
|
622
|
+
state.codeBlockLines.clear()
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private fun closeAllBlockquotes(
|
|
627
|
+
html: StringBuilder,
|
|
628
|
+
state: GeneratorState,
|
|
629
|
+
) {
|
|
630
|
+
while (state.blockquoteDepth >= 0) {
|
|
631
|
+
html.append("</blockquote>")
|
|
632
|
+
state.blockquoteDepth--
|
|
633
|
+
}
|
|
634
|
+
state.inBlockquote = false
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private fun closeListsIfOpen(
|
|
638
|
+
html: StringBuilder,
|
|
639
|
+
state: GeneratorState,
|
|
640
|
+
) {
|
|
641
|
+
while (state.openListTypes.isNotEmpty()) {
|
|
642
|
+
html.append(if (state.openListTypes.last()) "</ol>" else "</ul>")
|
|
643
|
+
state.openListTypes.removeAt(state.openListTypes.lastIndex)
|
|
644
|
+
}
|
|
645
|
+
state.listDepth = -1
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private fun closeRemainingContainers(
|
|
649
|
+
html: StringBuilder,
|
|
650
|
+
state: GeneratorState,
|
|
651
|
+
styles: CachedStyles,
|
|
652
|
+
) {
|
|
653
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
654
|
+
closeAllBlockquotes(html, state)
|
|
655
|
+
closeListsIfOpen(html, state)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private fun generateInlineHTML(
|
|
659
|
+
text: Spannable,
|
|
660
|
+
start: Int,
|
|
661
|
+
end: Int,
|
|
662
|
+
styles: CachedStyles,
|
|
663
|
+
isCodeBlock: Boolean,
|
|
664
|
+
): String {
|
|
665
|
+
val html = StringBuilder(end - start + 32)
|
|
666
|
+
var i = start
|
|
667
|
+
|
|
668
|
+
while (i < end) {
|
|
669
|
+
val char = text[i]
|
|
670
|
+
|
|
671
|
+
when {
|
|
672
|
+
char == FactoidPaddingSpan.MARK &&
|
|
673
|
+
text.getSpans(i, i + 1, FactoidPaddingSpan::class.java).isNotEmpty() -> {
|
|
674
|
+
i++
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
char == '\n' && i == end - 1 -> {
|
|
678
|
+
i++
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
char == OBJECT_REPLACEMENT_CHAR -> {
|
|
682
|
+
appendReplacementChar(html, text, i, styles)
|
|
683
|
+
i++
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
else -> {
|
|
687
|
+
val segmentEnd = minOf(text.nextSpanTransition(i, end, Any::class.java), end)
|
|
688
|
+
val segmentText = text.subSequence(i, segmentEnd).toString()
|
|
689
|
+
|
|
690
|
+
if (segmentText.isNotEmpty() && segmentText != "\n") {
|
|
691
|
+
appendStyledSegment(html, text, i, segmentEnd, segmentText, styles, isCodeBlock)
|
|
692
|
+
}
|
|
693
|
+
i = segmentEnd
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return html.toString()
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private fun appendReplacementChar(
|
|
702
|
+
html: StringBuilder,
|
|
703
|
+
text: Spannable,
|
|
704
|
+
pos: Int,
|
|
705
|
+
styles: CachedStyles,
|
|
706
|
+
) {
|
|
707
|
+
val imageSpans = text.getSpans(pos, pos + 1, ImageSpan::class.java)
|
|
708
|
+
if (imageSpans.isNotEmpty()) {
|
|
709
|
+
val imgSpan = imageSpans[0]
|
|
710
|
+
|
|
711
|
+
if (imgSpan.isInline) {
|
|
712
|
+
html.append("<img src=\"")
|
|
713
|
+
escapeHTMLTo(html, imgSpan.imageUrl)
|
|
714
|
+
html
|
|
715
|
+
.append("\" style=\"height: ")
|
|
716
|
+
.append(styles.inlineImageHeight)
|
|
717
|
+
.append("; width: auto; vertical-align: ")
|
|
718
|
+
.append(styles.inlineImageVerticalAlign)
|
|
719
|
+
.append(";\">")
|
|
720
|
+
} else {
|
|
721
|
+
html
|
|
722
|
+
.append("</p><div style=\"margin-bottom: ")
|
|
723
|
+
.append(styles.imageMarginBottom)
|
|
724
|
+
.append("px;\"><img src=\"")
|
|
725
|
+
escapeHTMLTo(html, imgSpan.imageUrl)
|
|
726
|
+
html
|
|
727
|
+
.append("\" style=\"max-width: 100%; border-radius: ")
|
|
728
|
+
.append(styles.imageBorderRadius)
|
|
729
|
+
.append("px;\"></div><p>")
|
|
730
|
+
}
|
|
731
|
+
return
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
val mathLatex = extractMathLatex(text, pos, pos + 1)
|
|
735
|
+
if (mathLatex != null) {
|
|
736
|
+
html
|
|
737
|
+
.append("<code style=\"background-color: ")
|
|
738
|
+
.append(styles.codeBgColor)
|
|
739
|
+
.append("; color: ")
|
|
740
|
+
.append(styles.codeColor)
|
|
741
|
+
.append("; padding: ")
|
|
742
|
+
.append(styles.codePadding)
|
|
743
|
+
.append("; border-radius: ")
|
|
744
|
+
.append(styles.codeBorderRadius)
|
|
745
|
+
.append("; font-size: ")
|
|
746
|
+
.append(styles.codeFontSize)
|
|
747
|
+
.append("; font-family: Menlo, Monaco, Consolas, monospace;\">")
|
|
748
|
+
escapeHTMLTo(html, mathLatex)
|
|
749
|
+
html.append("</code>")
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private fun appendStyledSegment(
|
|
754
|
+
html: StringBuilder,
|
|
755
|
+
text: Spannable,
|
|
756
|
+
start: Int,
|
|
757
|
+
end: Int,
|
|
758
|
+
content: String,
|
|
759
|
+
styles: CachedStyles,
|
|
760
|
+
isCodeBlock: Boolean,
|
|
761
|
+
) {
|
|
762
|
+
val strongSpans = text.getSpans(start, end, StrongSpan::class.java)
|
|
763
|
+
val styleSpans = text.getSpans(start, end, StyleSpan::class.java)
|
|
764
|
+
val emphasisSpans = text.getSpans(start, end, EmphasisSpan::class.java)
|
|
765
|
+
val underlineSpans = text.getSpans(start, end, UnderlineSpan::class.java)
|
|
766
|
+
val strikethroughSpans = text.getSpans(start, end, StrikethroughSpan::class.java)
|
|
767
|
+
val linkSpans = text.getSpans(start, end, LinkSpan::class.java)
|
|
768
|
+
val codeSpans = text.getSpans(start, end, CodeSpan::class.java)
|
|
769
|
+
val highlightSpans = text.getSpans(start, end, HighlightSpan::class.java)
|
|
770
|
+
|
|
771
|
+
val isBold =
|
|
772
|
+
strongSpans.isNotEmpty() ||
|
|
773
|
+
styleSpans.any { it.style == Typeface.BOLD || it.style == Typeface.BOLD_ITALIC }
|
|
774
|
+
val isItalic =
|
|
775
|
+
emphasisSpans.isNotEmpty() ||
|
|
776
|
+
styleSpans.any { it.style == Typeface.ITALIC || it.style == Typeface.BOLD_ITALIC }
|
|
777
|
+
val isUnderline = underlineSpans.isNotEmpty()
|
|
778
|
+
val isStrikethrough = strikethroughSpans.isNotEmpty()
|
|
779
|
+
val link = linkSpans.firstOrNull()
|
|
780
|
+
val isCode = codeSpans.isNotEmpty() && !isCodeBlock
|
|
781
|
+
val isHighlight = highlightSpans.isNotEmpty()
|
|
782
|
+
|
|
783
|
+
if (isHighlight) {
|
|
784
|
+
html
|
|
785
|
+
.append("<mark style=\"background-color: ")
|
|
786
|
+
.append(styles.highlightBackgroundColor)
|
|
787
|
+
if (styles.highlightColor != styles.paragraphColor) {
|
|
788
|
+
html
|
|
789
|
+
.append("; color: ")
|
|
790
|
+
.append(styles.highlightColor)
|
|
791
|
+
} else {
|
|
792
|
+
html.append("; color: inherit")
|
|
793
|
+
}
|
|
794
|
+
html.append(";\">")
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
link?.let {
|
|
798
|
+
html.append("<a href=\"")
|
|
799
|
+
escapeHTMLTo(html, it.url)
|
|
800
|
+
html
|
|
801
|
+
.append("\" style=\"color: ")
|
|
802
|
+
.append(styles.linkColor)
|
|
803
|
+
.append("; text-decoration: ")
|
|
804
|
+
.append(if (styles.linkUnderline) "underline" else "none")
|
|
805
|
+
if (styles.linkFontFamily.isNotEmpty()) {
|
|
806
|
+
html.append("; font-family: '").append(styles.linkFontFamily).append("'")
|
|
807
|
+
}
|
|
808
|
+
html.append(";\">")
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (isCode) {
|
|
812
|
+
html
|
|
813
|
+
.append("<code style=\"background-color: ")
|
|
814
|
+
.append(styles.codeBgColor)
|
|
815
|
+
.append("; color: ")
|
|
816
|
+
.append(styles.codeColor)
|
|
817
|
+
.append("; padding: ")
|
|
818
|
+
.append(styles.codePadding)
|
|
819
|
+
.append("; border-radius: ")
|
|
820
|
+
.append(styles.codeBorderRadius)
|
|
821
|
+
.append("; font-size: ")
|
|
822
|
+
.append(styles.codeFontSize)
|
|
823
|
+
.append("; font-family: Menlo, Monaco, Consolas, monospace;\">")
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (isBold) {
|
|
827
|
+
if (styles.strongColor != null) {
|
|
828
|
+
html.append("<strong style=\"color: ").append(styles.strongColor).append(";\">")
|
|
829
|
+
} else {
|
|
830
|
+
html.append("<strong>")
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (isItalic) {
|
|
835
|
+
if (styles.emphasisColor != null) {
|
|
836
|
+
html.append("<em style=\"color: ").append(styles.emphasisColor).append(";\">")
|
|
837
|
+
} else {
|
|
838
|
+
html.append("<em>")
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (isStrikethrough) {
|
|
843
|
+
if (styles.strikethroughColor != null) {
|
|
844
|
+
html.append("<s style=\"text-decoration-color: ").append(styles.strikethroughColor).append(";\">")
|
|
845
|
+
} else {
|
|
846
|
+
html.append("<s>")
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (isUnderline && link == null) {
|
|
850
|
+
if (styles.underlineColor != null) {
|
|
851
|
+
html.append("<u style=\"text-decoration-color: ").append(styles.underlineColor).append(";\">")
|
|
852
|
+
} else {
|
|
853
|
+
html.append("<u>")
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
escapeHTMLTo(html, content.trimEnd('\n'), preserveLineBreaks = !isCodeBlock)
|
|
858
|
+
|
|
859
|
+
if (isUnderline && link == null) html.append("</u>")
|
|
860
|
+
if (isStrikethrough) html.append("</s>")
|
|
861
|
+
if (isItalic) html.append("</em>")
|
|
862
|
+
if (isBold) html.append("</strong>")
|
|
863
|
+
if (isCode) html.append("</code>")
|
|
864
|
+
if (link != null) html.append("</a>")
|
|
865
|
+
if (isHighlight) html.append("</mark>")
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
private fun collectParagraphs(text: Spannable): ArrayList<ParagraphInfo> {
|
|
869
|
+
val string = text.toString()
|
|
870
|
+
val paragraphs = ArrayList<ParagraphInfo>(string.length / 40 + 1) // Estimate
|
|
871
|
+
var currentIndex = 0
|
|
872
|
+
|
|
873
|
+
while (currentIndex < string.length) {
|
|
874
|
+
var lineEnd = string.indexOf('\n', currentIndex)
|
|
875
|
+
if (lineEnd == -1) lineEnd = string.length else lineEnd++
|
|
876
|
+
|
|
877
|
+
val listSpan = innermostListSpan(text, currentIndex)
|
|
878
|
+
val type = getParagraphType(text, currentIndex, listSpan)
|
|
879
|
+
val depth = getDepthForType(text, currentIndex, type, listSpan)
|
|
880
|
+
val isTaskChecked = type == TYPE_TASK_LIST && (listSpan as? TaskListSpan)?.isChecked == true
|
|
881
|
+
|
|
882
|
+
paragraphs.add(ParagraphInfo(currentIndex, lineEnd, type, depth, isTaskChecked))
|
|
883
|
+
currentIndex = lineEnd
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return paragraphs
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The list span that owns the line starting at [start]. A list item's span covers its nested
|
|
891
|
+
* children, so every ancestor is returned here as well - the innermost span is the item that
|
|
892
|
+
* actually starts on this line.
|
|
893
|
+
*/
|
|
894
|
+
private fun innermostListSpan(
|
|
895
|
+
text: Spannable,
|
|
896
|
+
start: Int,
|
|
897
|
+
): BaseListSpan? =
|
|
898
|
+
text
|
|
899
|
+
.getSpans(start, minOf(start + 1, text.length), BaseListSpan::class.java)
|
|
900
|
+
.maxByOrNull { it.depth }
|
|
901
|
+
|
|
902
|
+
private fun getParagraphType(
|
|
903
|
+
text: Spannable,
|
|
904
|
+
start: Int,
|
|
905
|
+
listSpan: BaseListSpan?,
|
|
906
|
+
): Int {
|
|
907
|
+
val end = minOf(start + 1, text.length)
|
|
908
|
+
|
|
909
|
+
if (text.getSpans(start, end, CodeBlockSpan::class.java).isNotEmpty()) return TYPE_CODE_BLOCK
|
|
910
|
+
|
|
911
|
+
val headingSpans = text.getSpans(start, end, HeadingSpan::class.java)
|
|
912
|
+
if (headingSpans.isNotEmpty()) return headingSpans[0].level.coerceIn(1, 6)
|
|
913
|
+
|
|
914
|
+
if (text.getSpans(start, end, BlockquoteSpan::class.java).isNotEmpty()) return TYPE_BLOCKQUOTE
|
|
915
|
+
|
|
916
|
+
return when (listSpan) {
|
|
917
|
+
is TaskListSpan -> TYPE_TASK_LIST
|
|
918
|
+
is OrderedListSpan -> TYPE_ORDERED_LIST
|
|
919
|
+
is UnorderedListSpan -> TYPE_UNORDERED_LIST
|
|
920
|
+
else -> TYPE_NORMAL
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private fun getDepthForType(
|
|
925
|
+
text: Spannable,
|
|
926
|
+
start: Int,
|
|
927
|
+
type: Int,
|
|
928
|
+
listSpan: BaseListSpan?,
|
|
929
|
+
): Int =
|
|
930
|
+
when (type) {
|
|
931
|
+
TYPE_BLOCKQUOTE -> {
|
|
932
|
+
text.getSpans(start, start + 1, BlockquoteSpan::class.java).maxOfOrNull { it.depth } ?: 0
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
TYPE_ORDERED_LIST, TYPE_UNORDERED_LIST, TYPE_TASK_LIST -> {
|
|
936
|
+
listSpan?.depth ?: 0
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
else -> {
|
|
940
|
+
0
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
fun generateTableHTML(
|
|
945
|
+
rows: List<List<Triple<CharSequence, Boolean, Layout.Alignment>>>,
|
|
946
|
+
style: StyleConfig,
|
|
947
|
+
scaledDensity: Float = 1f,
|
|
948
|
+
density: Float = 1f,
|
|
949
|
+
): String {
|
|
950
|
+
if (rows.isEmpty()) return ""
|
|
951
|
+
|
|
952
|
+
val tableStyle = style.tableStyle
|
|
953
|
+
val cachedStyles = CachedStyles(style, scaledDensity, density)
|
|
954
|
+
|
|
955
|
+
val borderColorCSS = colorToCSS(tableStyle.borderColor)
|
|
956
|
+
val borderWidthPx = tableStyle.borderWidth / density
|
|
957
|
+
val borderRadiusPx = tableStyle.borderRadius / density
|
|
958
|
+
val fontSizePx = tableStyle.fontSize / scaledDensity
|
|
959
|
+
val cellPaddingVerticalPx = tableStyle.cellPaddingVertical / density
|
|
960
|
+
val cellPaddingHorizontalPx = tableStyle.cellPaddingHorizontal / density
|
|
961
|
+
val bodyFontWeight = CachedStyles.fontWeightToCSS(tableStyle.fontWeight)
|
|
962
|
+
|
|
963
|
+
return buildString(rows.size * 250) {
|
|
964
|
+
append("<table style=\"border-collapse: separate; border-spacing: 0; ")
|
|
965
|
+
append("border: ${borderWidthPx.toInt()}px solid $borderColorCSS; ")
|
|
966
|
+
append("border-radius: ${borderRadiusPx.toInt()}px; ")
|
|
967
|
+
append("overflow: hidden; font-size: ${fontSizePx.toInt()}px;\">")
|
|
968
|
+
|
|
969
|
+
var hasOpenedBody = false
|
|
970
|
+
var bodyRowIndex = 0
|
|
971
|
+
|
|
972
|
+
for (row in rows) {
|
|
973
|
+
if (row.isEmpty()) continue
|
|
974
|
+
|
|
975
|
+
val isHeaderRow = row.first().second
|
|
976
|
+
|
|
977
|
+
if (isHeaderRow) {
|
|
978
|
+
append("<thead>")
|
|
979
|
+
} else if (!hasOpenedBody) {
|
|
980
|
+
append("<tbody>")
|
|
981
|
+
hasOpenedBody = true
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
append("<tr>")
|
|
985
|
+
|
|
986
|
+
for ((cellText, isHeader, alignment) in row) {
|
|
987
|
+
val backgroundColor =
|
|
988
|
+
when {
|
|
989
|
+
isHeader -> colorToCSS(tableStyle.headerBackgroundColor)
|
|
990
|
+
bodyRowIndex % 2 == 0 -> colorToCSS(tableStyle.rowEvenBackgroundColor)
|
|
991
|
+
else -> colorToCSS(tableStyle.rowOddBackgroundColor)
|
|
992
|
+
}
|
|
993
|
+
val textColor = if (isHeader) colorToCSS(tableStyle.headerTextColor) else colorToCSS(tableStyle.color)
|
|
994
|
+
val textAlignment = alignmentToCSS(alignment)
|
|
995
|
+
val fontWeight = if (isHeader) "bold" else bodyFontWeight
|
|
996
|
+
|
|
997
|
+
val htmlTag = if (isHeader) "th" else "td"
|
|
998
|
+
append("<$htmlTag style=\"")
|
|
999
|
+
append("padding: ${cellPaddingVerticalPx.toInt()}px ${cellPaddingHorizontalPx.toInt()}px; ")
|
|
1000
|
+
append("text-align: $textAlignment; ")
|
|
1001
|
+
append("background-color: $backgroundColor; ")
|
|
1002
|
+
append("color: $textColor; ")
|
|
1003
|
+
append("border: ${borderWidthPx.toInt()}px solid $borderColorCSS; ")
|
|
1004
|
+
append("font-weight: $fontWeight;\">")
|
|
1005
|
+
|
|
1006
|
+
if (cellText.isNotEmpty()) {
|
|
1007
|
+
if (cellText is Spannable) {
|
|
1008
|
+
append(generateInlineHTML(cellText, 0, cellText.length, cachedStyles, false))
|
|
1009
|
+
} else {
|
|
1010
|
+
escapeHTMLTo(this, cellText.toString(), preserveLineBreaks = true)
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
append("</$htmlTag>")
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
append("</tr>")
|
|
1018
|
+
|
|
1019
|
+
if (isHeaderRow) {
|
|
1020
|
+
append("</thead>")
|
|
1021
|
+
} else {
|
|
1022
|
+
bodyRowIndex++
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (hasOpenedBody) append("</tbody>")
|
|
1027
|
+
append("</table>")
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private fun alignmentToCSS(alignment: Layout.Alignment): String =
|
|
1032
|
+
when (alignment) {
|
|
1033
|
+
Layout.Alignment.ALIGN_CENTER -> "center"
|
|
1034
|
+
Layout.Alignment.ALIGN_OPPOSITE -> "right"
|
|
1035
|
+
else -> "left"
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private fun colorToCSS(color: Int): String {
|
|
1039
|
+
if (color == 0) return "inherit"
|
|
1040
|
+
val alpha = (color shr 24) and 0xFF
|
|
1041
|
+
val red = (color shr 16) and 0xFF
|
|
1042
|
+
val green = (color shr 8) and 0xFF
|
|
1043
|
+
val blue = color and 0xFF
|
|
1044
|
+
return if (alpha < 255) "rgba($red, $green, $blue, ${alpha / 255f})" else String.format("#%02X%02X%02X", red, green, blue)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/** Fast HTML escape using single-pass character scanning. */
|
|
1048
|
+
private fun escapeHTMLTo(
|
|
1049
|
+
output: StringBuilder,
|
|
1050
|
+
text: String,
|
|
1051
|
+
preserveLineBreaks: Boolean = false,
|
|
1052
|
+
) {
|
|
1053
|
+
val len = text.length
|
|
1054
|
+
|
|
1055
|
+
// Fast path: skip if no escaping needed
|
|
1056
|
+
var needsEscape = false
|
|
1057
|
+
for (i in 0 until len) {
|
|
1058
|
+
val c = text[i]
|
|
1059
|
+
if (c == '&' || c == '<' || c == '>' || c == '"' || c == '\'' ||
|
|
1060
|
+
(preserveLineBreaks && (c == '\r' || c == '\n' || c == '\u2028' || c == '\u2029'))
|
|
1061
|
+
) {
|
|
1062
|
+
needsEscape = true
|
|
1063
|
+
break
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (!needsEscape) {
|
|
1067
|
+
output.append(text)
|
|
1068
|
+
return
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
var index = 0
|
|
1072
|
+
while (index < len) {
|
|
1073
|
+
val c = text[index++]
|
|
1074
|
+
if (preserveLineBreaks && (c == '\r' || c == '\n' || c == '\u2028' || c == '\u2029')) {
|
|
1075
|
+
if (c == '\r' && index < len && text[index] == '\n') index++
|
|
1076
|
+
output.append("<br>")
|
|
1077
|
+
continue
|
|
1078
|
+
}
|
|
1079
|
+
when (c) {
|
|
1080
|
+
'&' -> output.append("&")
|
|
1081
|
+
'<' -> output.append("<")
|
|
1082
|
+
'>' -> output.append(">")
|
|
1083
|
+
'"' -> output.append(""")
|
|
1084
|
+
'\'' -> output.append("'")
|
|
1085
|
+
else -> output.append(c)
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private fun extractMathLatex(
|
|
1091
|
+
text: android.text.Spannable,
|
|
1092
|
+
start: Int,
|
|
1093
|
+
end: Int,
|
|
1094
|
+
): String? {
|
|
1095
|
+
return try {
|
|
1096
|
+
val mathInlineSpanClass = Class.forName("com.renderrichtext.spans.MathInlineSpan")
|
|
1097
|
+
val spans = text.getSpans(start, end, mathInlineSpanClass)
|
|
1098
|
+
if (spans.isEmpty()) return null
|
|
1099
|
+
val latexField = mathInlineSpanClass.getDeclaredField("latex").apply { isAccessible = true }
|
|
1100
|
+
latexField.get(spans[0]) as? String
|
|
1101
|
+
} catch (_: Exception) {
|
|
1102
|
+
null
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|