react-native-enriched-markdown 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +479 -0
- package/ReactNativeEnrichedMarkdown.podspec +27 -0
- package/android/build.gradle +101 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +39 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +21 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +57 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1164 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.h +20 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/baseline-prof.txt +65 -0
- package/android/src/main/cpp/jni-adapter.cpp +203 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +153 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +119 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextPackage.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +165 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +48 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +166 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +89 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +105 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +52 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +28 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +105 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +58 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +99 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +66 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +95 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +85 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +44 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +136 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +135 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +180 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +196 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +34 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +320 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +76 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +87 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/TextSpan.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ThematicBreakSpan.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +48 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +51 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeStyle.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/EmphasisStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineImageStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrongStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +180 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +75 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/AsyncDrawable.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +809 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +365 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/SelectionActionMode.kt +139 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +181 -0
- package/android/src/main/jni/CMakeLists.txt +82 -0
- package/android/src/main/jni/EnrichedMarkdownTextSpec.cpp +21 -0
- package/android/src/main/jni/EnrichedMarkdownTextSpec.h +25 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextComponentDescriptor.h +29 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.cpp +45 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.h +21 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +33 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +49 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +25 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +19 -0
- package/cpp/md4c/md4c.c +6492 -0
- package/cpp/md4c/md4c.h +402 -0
- package/cpp/parser/MD4CParser.cpp +314 -0
- package/cpp/parser/MD4CParser.hpp +23 -0
- package/cpp/parser/MarkdownASTNode.hpp +49 -0
- package/ios/EnrichedMarkdownText.h +18 -0
- package/ios/EnrichedMarkdownText.mm +1074 -0
- package/ios/attachments/ImageAttachment.h +23 -0
- package/ios/attachments/ImageAttachment.m +185 -0
- package/ios/attachments/ThematicBreakAttachment.h +15 -0
- package/ios/attachments/ThematicBreakAttachment.m +33 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +57 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1164 -0
- package/ios/generated/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +20 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
- package/ios/generated/EnrichedMarkdownTextSpec/States.cpp +16 -0
- package/ios/generated/EnrichedMarkdownTextSpec/States.h +20 -0
- package/ios/internals/EnrichedMarkdownTextComponentDescriptor.h +19 -0
- package/ios/internals/EnrichedMarkdownTextShadowNode.h +43 -0
- package/ios/internals/EnrichedMarkdownTextShadowNode.mm +85 -0
- package/ios/internals/EnrichedMarkdownTextState.h +24 -0
- package/ios/parser/MarkdownASTNode.h +33 -0
- package/ios/parser/MarkdownASTNode.m +32 -0
- package/ios/parser/MarkdownParser.h +8 -0
- package/ios/parser/MarkdownParser.mm +13 -0
- package/ios/parser/MarkdownParserBridge.mm +110 -0
- package/ios/renderer/AttributedRenderer.h +9 -0
- package/ios/renderer/AttributedRenderer.m +119 -0
- package/ios/renderer/BlockquoteRenderer.h +7 -0
- package/ios/renderer/BlockquoteRenderer.m +159 -0
- package/ios/renderer/CodeBlockRenderer.h +10 -0
- package/ios/renderer/CodeBlockRenderer.m +89 -0
- package/ios/renderer/CodeRenderer.h +10 -0
- package/ios/renderer/CodeRenderer.m +60 -0
- package/ios/renderer/EmphasisRenderer.h +6 -0
- package/ios/renderer/EmphasisRenderer.m +96 -0
- package/ios/renderer/HeadingRenderer.h +7 -0
- package/ios/renderer/HeadingRenderer.m +98 -0
- package/ios/renderer/ImageRenderer.h +12 -0
- package/ios/renderer/ImageRenderer.m +62 -0
- package/ios/renderer/LinkRenderer.h +7 -0
- package/ios/renderer/LinkRenderer.m +69 -0
- package/ios/renderer/ListItemRenderer.h +16 -0
- package/ios/renderer/ListItemRenderer.m +91 -0
- package/ios/renderer/ListRenderer.h +13 -0
- package/ios/renderer/ListRenderer.m +67 -0
- package/ios/renderer/NodeRenderer.h +8 -0
- package/ios/renderer/ParagraphRenderer.h +7 -0
- package/ios/renderer/ParagraphRenderer.m +69 -0
- package/ios/renderer/RenderContext.h +88 -0
- package/ios/renderer/RenderContext.m +248 -0
- package/ios/renderer/RendererFactory.h +12 -0
- package/ios/renderer/RendererFactory.m +110 -0
- package/ios/renderer/StrongRenderer.h +6 -0
- package/ios/renderer/StrongRenderer.m +83 -0
- package/ios/renderer/TextRenderer.h +6 -0
- package/ios/renderer/TextRenderer.m +16 -0
- package/ios/renderer/ThematicBreakRenderer.h +5 -0
- package/ios/renderer/ThematicBreakRenderer.m +53 -0
- package/ios/styles/StyleConfig.h +228 -0
- package/ios/styles/StyleConfig.mm +1467 -0
- package/ios/utils/BlockquoteBorder.h +20 -0
- package/ios/utils/BlockquoteBorder.m +92 -0
- package/ios/utils/CodeBackground.h +19 -0
- package/ios/utils/CodeBackground.m +191 -0
- package/ios/utils/CodeBlockBackground.h +17 -0
- package/ios/utils/CodeBlockBackground.m +87 -0
- package/ios/utils/EditMenuUtils.h +22 -0
- package/ios/utils/EditMenuUtils.m +118 -0
- package/ios/utils/FontUtils.h +20 -0
- package/ios/utils/FontUtils.m +13 -0
- package/ios/utils/HTMLGenerator.h +20 -0
- package/ios/utils/HTMLGenerator.m +779 -0
- package/ios/utils/LastElementUtils.h +53 -0
- package/ios/utils/ListMarkerDrawer.h +15 -0
- package/ios/utils/ListMarkerDrawer.m +127 -0
- package/ios/utils/MarkdownExtractor.h +17 -0
- package/ios/utils/MarkdownExtractor.m +295 -0
- package/ios/utils/ParagraphStyleUtils.h +13 -0
- package/ios/utils/ParagraphStyleUtils.m +56 -0
- package/ios/utils/PasteboardUtils.h +36 -0
- package/ios/utils/PasteboardUtils.m +134 -0
- package/ios/utils/RTFExportUtils.h +24 -0
- package/ios/utils/RTFExportUtils.m +297 -0
- package/ios/utils/RuntimeKeys.h +38 -0
- package/ios/utils/RuntimeKeys.m +11 -0
- package/ios/utils/TextViewLayoutManager.h +14 -0
- package/ios/utils/TextViewLayoutManager.mm +113 -0
- package/lib/module/EnrichedMarkdownText.js +34 -0
- package/lib/module/EnrichedMarkdownText.js.map +1 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +130 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalizeMarkdownStyle.js +340 -0
- package/lib/module/normalizeMarkdownStyle.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts +101 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +111 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts +6 -0
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -0
- package/package.json +186 -1
- package/react-native.config.js +13 -0
- package/src/EnrichedMarkdownText.tsx +152 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +130 -0
- package/src/index.tsx +7 -0
- package/src/normalizeMarkdownStyle.ts +377 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils
|
|
2
|
+
|
|
3
|
+
import android.graphics.Typeface
|
|
4
|
+
import android.text.Spannable
|
|
5
|
+
import android.text.style.StrikethroughSpan
|
|
6
|
+
import android.text.style.StyleSpan
|
|
7
|
+
import android.text.style.UnderlineSpan
|
|
8
|
+
import com.swmansion.enriched.markdown.spans.BlockquoteSpan
|
|
9
|
+
import com.swmansion.enriched.markdown.spans.CodeBlockSpan
|
|
10
|
+
import com.swmansion.enriched.markdown.spans.CodeSpan
|
|
11
|
+
import com.swmansion.enriched.markdown.spans.EmphasisSpan
|
|
12
|
+
import com.swmansion.enriched.markdown.spans.HeadingSpan
|
|
13
|
+
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
14
|
+
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
15
|
+
import com.swmansion.enriched.markdown.spans.OrderedListSpan
|
|
16
|
+
import com.swmansion.enriched.markdown.spans.StrongSpan
|
|
17
|
+
import com.swmansion.enriched.markdown.spans.UnorderedListSpan
|
|
18
|
+
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
19
|
+
|
|
20
|
+
/** Generates semantic HTML with inline styles from Spannable text. */
|
|
21
|
+
object HTMLGenerator {
|
|
22
|
+
private const val OBJECT_REPLACEMENT_CHAR = '\uFFFC'
|
|
23
|
+
|
|
24
|
+
/** Pre-computed styles to avoid repeated StyleConfig method calls. */
|
|
25
|
+
private class CachedStyles(
|
|
26
|
+
style: StyleConfig,
|
|
27
|
+
private val fontDensity: Float,
|
|
28
|
+
private val dimDensity: Float,
|
|
29
|
+
) {
|
|
30
|
+
// Convert device pixels back to CSS pixels
|
|
31
|
+
private fun fontPx(px: Float) = (px / fontDensity).toInt()
|
|
32
|
+
|
|
33
|
+
private fun dimPx(px: Float) = (px / dimDensity).toInt()
|
|
34
|
+
|
|
35
|
+
// Paragraph
|
|
36
|
+
val paragraphColor: String
|
|
37
|
+
val paragraphFontSize: Int
|
|
38
|
+
val paragraphMarginBottom: Int
|
|
39
|
+
|
|
40
|
+
// Code block
|
|
41
|
+
val codeBlockColor: String
|
|
42
|
+
val codeBlockBgColor: String
|
|
43
|
+
val codeBlockFontSize: Int
|
|
44
|
+
val codeBlockPadding: Int
|
|
45
|
+
val codeBlockBorderRadius: Int
|
|
46
|
+
val codeBlockMarginBottom: Int
|
|
47
|
+
|
|
48
|
+
// Inline code
|
|
49
|
+
val codeColor: String
|
|
50
|
+
val codeBgColor: String
|
|
51
|
+
|
|
52
|
+
// Blockquote
|
|
53
|
+
val blockquoteColor: String
|
|
54
|
+
val blockquoteBgColor: String
|
|
55
|
+
val blockquoteBorderColor: String
|
|
56
|
+
val blockquoteBorderWidth: Int
|
|
57
|
+
val blockquoteGapWidth: Int
|
|
58
|
+
val blockquoteMarginBottom: Int
|
|
59
|
+
val blockquoteFontSize: Int
|
|
60
|
+
|
|
61
|
+
// List
|
|
62
|
+
val listColor: String
|
|
63
|
+
val listFontSize: Int
|
|
64
|
+
val listMarginBottom: Int
|
|
65
|
+
val listMarginLeft: Int
|
|
66
|
+
|
|
67
|
+
// Link
|
|
68
|
+
val linkColor: String
|
|
69
|
+
val linkUnderline: Boolean
|
|
70
|
+
|
|
71
|
+
// Strong/Emphasis
|
|
72
|
+
val strongColor: String?
|
|
73
|
+
val emphasisColor: String?
|
|
74
|
+
|
|
75
|
+
// Image
|
|
76
|
+
val imageMarginBottom: Int
|
|
77
|
+
val imageBorderRadius: Int
|
|
78
|
+
|
|
79
|
+
// Fixed HTML values (not from StyleConfig)
|
|
80
|
+
val blockquotePaddingVertical = "8px"
|
|
81
|
+
val blockquoteBorderRadiusCorners = "0 8px 8px 0"
|
|
82
|
+
val blockquoteNestedMargin = "8px 0 0 0"
|
|
83
|
+
val blockquoteParagraphMargin = "0 0 4px 0"
|
|
84
|
+
val inlineImageHeight = "1.2em"
|
|
85
|
+
val inlineImageVerticalAlign = "-0.2em"
|
|
86
|
+
val codePadding = "2px 4px"
|
|
87
|
+
val codeBorderRadius = "4px"
|
|
88
|
+
val codeFontSize = "0.7em"
|
|
89
|
+
|
|
90
|
+
// Headings (array for O(1) lookup)
|
|
91
|
+
val headingFontSizes: IntArray
|
|
92
|
+
val headingFontWeights: Array<String>
|
|
93
|
+
val headingColors: Array<String>
|
|
94
|
+
val headingMarginBottoms: IntArray
|
|
95
|
+
|
|
96
|
+
init {
|
|
97
|
+
// Paragraph
|
|
98
|
+
val pStyle = style.paragraphStyle
|
|
99
|
+
paragraphColor = colorToCSS(pStyle.color)
|
|
100
|
+
paragraphFontSize = fontPx(pStyle.fontSize)
|
|
101
|
+
paragraphMarginBottom = dimPx(pStyle.marginBottom)
|
|
102
|
+
|
|
103
|
+
// Code block
|
|
104
|
+
val cbStyle = style.codeBlockStyle
|
|
105
|
+
codeBlockColor = colorToCSS(cbStyle.color)
|
|
106
|
+
codeBlockBgColor = colorToCSS(cbStyle.backgroundColor)
|
|
107
|
+
codeBlockFontSize = fontPx(cbStyle.fontSize)
|
|
108
|
+
codeBlockPadding = dimPx(cbStyle.padding)
|
|
109
|
+
codeBlockBorderRadius = dimPx(cbStyle.borderRadius)
|
|
110
|
+
codeBlockMarginBottom = dimPx(cbStyle.marginBottom)
|
|
111
|
+
|
|
112
|
+
// Inline code
|
|
113
|
+
val cStyle = style.codeStyle
|
|
114
|
+
codeColor = colorToCSS(cStyle.color)
|
|
115
|
+
codeBgColor = colorToCSS(cStyle.backgroundColor)
|
|
116
|
+
|
|
117
|
+
// Blockquote
|
|
118
|
+
val bqStyle = style.blockquoteStyle
|
|
119
|
+
blockquoteColor = colorToCSS(bqStyle.color)
|
|
120
|
+
blockquoteBgColor = colorToCSS(bqStyle.backgroundColor ?: 0)
|
|
121
|
+
blockquoteBorderColor = colorToCSS(bqStyle.borderColor)
|
|
122
|
+
blockquoteBorderWidth = dimPx(bqStyle.borderWidth)
|
|
123
|
+
blockquoteGapWidth = dimPx(bqStyle.gapWidth)
|
|
124
|
+
blockquoteMarginBottom = dimPx(bqStyle.marginBottom)
|
|
125
|
+
blockquoteFontSize = fontPx(bqStyle.fontSize)
|
|
126
|
+
|
|
127
|
+
// List
|
|
128
|
+
val lStyle = style.listStyle
|
|
129
|
+
listColor = colorToCSS(lStyle.color)
|
|
130
|
+
listFontSize = fontPx(lStyle.fontSize)
|
|
131
|
+
listMarginBottom = dimPx(lStyle.marginBottom)
|
|
132
|
+
listMarginLeft = dimPx(lStyle.marginLeft)
|
|
133
|
+
|
|
134
|
+
// Link
|
|
135
|
+
linkColor = colorToCSS(style.linkStyle.color)
|
|
136
|
+
linkUnderline = style.linkStyle.underline
|
|
137
|
+
|
|
138
|
+
// Strong/Emphasis (nullable for inherit)
|
|
139
|
+
val sc = style.strongStyle.color
|
|
140
|
+
strongColor = if (sc != null && sc != 0) colorToCSS(sc) else null
|
|
141
|
+
val ec = style.emphasisStyle.color
|
|
142
|
+
emphasisColor = if (ec != null && ec != 0) colorToCSS(ec) else null
|
|
143
|
+
|
|
144
|
+
// Image
|
|
145
|
+
val imgStyle = style.imageStyle
|
|
146
|
+
imageMarginBottom = dimPx(imgStyle.marginBottom)
|
|
147
|
+
imageBorderRadius = dimPx(imgStyle.borderRadius)
|
|
148
|
+
|
|
149
|
+
// Headings (1-6, index 0-5)
|
|
150
|
+
headingFontSizes = IntArray(6)
|
|
151
|
+
headingFontWeights = Array(6) { "" }
|
|
152
|
+
headingColors = Array(6) { "" }
|
|
153
|
+
headingMarginBottoms = IntArray(6)
|
|
154
|
+
|
|
155
|
+
for (level in 1..6) {
|
|
156
|
+
val hStyle = style.headingStyles[level]!!
|
|
157
|
+
val idx = level - 1
|
|
158
|
+
headingFontSizes[idx] = fontPx(hStyle.fontSize)
|
|
159
|
+
headingFontWeights[idx] = fontWeightToCSS(hStyle.fontWeight)
|
|
160
|
+
headingColors[idx] = colorToCSS(hStyle.color)
|
|
161
|
+
headingMarginBottoms[idx] = dimPx(hStyle.marginBottom)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
companion object {
|
|
166
|
+
private fun colorToCSS(color: Int): String {
|
|
167
|
+
if (color == 0) return "inherit"
|
|
168
|
+
val alpha = (color shr 24) and 0xFF
|
|
169
|
+
val red = (color shr 16) and 0xFF
|
|
170
|
+
val green = (color shr 8) and 0xFF
|
|
171
|
+
val blue = color and 0xFF
|
|
172
|
+
|
|
173
|
+
return if (alpha < 255) {
|
|
174
|
+
"rgba($red, $green, $blue, ${alpha / 255f})"
|
|
175
|
+
} else {
|
|
176
|
+
String.format("#%02X%02X%02X", red, green, blue)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private fun fontWeightToCSS(fontWeight: String): String =
|
|
181
|
+
when {
|
|
182
|
+
fontWeight.equals("bold", ignoreCase = true) -> "700"
|
|
183
|
+
fontWeight.equals("semibold", ignoreCase = true) -> "600"
|
|
184
|
+
fontWeight.equals("medium", ignoreCase = true) -> "500"
|
|
185
|
+
fontWeight.isEmpty() || fontWeight.equals("normal", ignoreCase = true) -> "normal"
|
|
186
|
+
else -> fontWeight
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private class GeneratorState {
|
|
192
|
+
var inCodeBlock = false
|
|
193
|
+
var previousWasCodeBlock = false
|
|
194
|
+
val codeBlockLines = ArrayList<String>(16) // Pre-sized
|
|
195
|
+
|
|
196
|
+
var inBlockquote = false
|
|
197
|
+
var blockquoteDepth = -1
|
|
198
|
+
var previousWasBlockquote = false
|
|
199
|
+
|
|
200
|
+
var listDepth = -1
|
|
201
|
+
val openListTypes = ArrayList<Boolean>(4) // true = ol, false = ul
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Paragraph type constants (H1-H6 = 1-6 directly from HeadingSpan.level)
|
|
205
|
+
private const val TYPE_NORMAL = 0
|
|
206
|
+
private const val TYPE_H1 = 1
|
|
207
|
+
private const val TYPE_H6 = 6
|
|
208
|
+
private const val TYPE_CODE_BLOCK = 7
|
|
209
|
+
private const val TYPE_BLOCKQUOTE = 8
|
|
210
|
+
private const val TYPE_ORDERED_LIST = 9
|
|
211
|
+
private const val TYPE_UNORDERED_LIST = 10
|
|
212
|
+
|
|
213
|
+
private data class ParagraphInfo(
|
|
214
|
+
val start: Int,
|
|
215
|
+
val end: Int,
|
|
216
|
+
val type: Int,
|
|
217
|
+
val depth: Int = 0,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
fun generateHTML(
|
|
221
|
+
text: Spannable,
|
|
222
|
+
style: StyleConfig,
|
|
223
|
+
scaledDensity: Float = 1f,
|
|
224
|
+
density: Float = 1f,
|
|
225
|
+
): String {
|
|
226
|
+
if (text.isEmpty()) return "<html></html>"
|
|
227
|
+
|
|
228
|
+
// Pre-cache all styles (single allocation)
|
|
229
|
+
val styles = CachedStyles(style, scaledDensity, density)
|
|
230
|
+
val state = GeneratorState()
|
|
231
|
+
|
|
232
|
+
// Estimate capacity (average 2x text length)
|
|
233
|
+
val html = StringBuilder(text.length * 2)
|
|
234
|
+
html.append("<html>")
|
|
235
|
+
|
|
236
|
+
// Collect paragraphs
|
|
237
|
+
val paragraphs = collectParagraphs(text)
|
|
238
|
+
|
|
239
|
+
for (para in paragraphs) {
|
|
240
|
+
processParagraph(html, text, para, styles, state)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
closeRemainingContainers(html, state, styles)
|
|
244
|
+
html.append("</html>")
|
|
245
|
+
|
|
246
|
+
return html.toString()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private fun processParagraph(
|
|
250
|
+
html: StringBuilder,
|
|
251
|
+
text: Spannable,
|
|
252
|
+
para: ParagraphInfo,
|
|
253
|
+
styles: CachedStyles,
|
|
254
|
+
state: GeneratorState,
|
|
255
|
+
) {
|
|
256
|
+
val paraText = text.subSequence(para.start, para.end).toString().trimEnd('\n')
|
|
257
|
+
|
|
258
|
+
// Handle empty paragraphs
|
|
259
|
+
if (paraText.isEmpty() && para.type == TYPE_NORMAL) {
|
|
260
|
+
closeAllBlockquotes(html, state)
|
|
261
|
+
state.previousWasBlockquote = false
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Get content range (trim trailing newline)
|
|
266
|
+
val contentEnd = if (para.end > para.start && text[para.end - 1] == '\n') para.end - 1 else para.end
|
|
267
|
+
val isCodeBlock = para.type == TYPE_CODE_BLOCK
|
|
268
|
+
val inlineContent = generateInlineHTML(text, para.start, contentEnd, styles, isCodeBlock)
|
|
269
|
+
|
|
270
|
+
// Handle different paragraph types
|
|
271
|
+
when (para.type) {
|
|
272
|
+
TYPE_CODE_BLOCK -> handleCodeBlock(inlineContent, state)
|
|
273
|
+
TYPE_BLOCKQUOTE -> handleBlockquote(html, inlineContent, para, styles, state)
|
|
274
|
+
TYPE_ORDERED_LIST, TYPE_UNORDERED_LIST -> handleList(html, inlineContent, para, styles, state)
|
|
275
|
+
in TYPE_H1..TYPE_H6 -> handleHeading(html, inlineContent, para.type, styles, state)
|
|
276
|
+
else -> handleNormalParagraph(html, inlineContent, styles, state)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private fun handleCodeBlock(
|
|
281
|
+
content: String,
|
|
282
|
+
state: GeneratorState,
|
|
283
|
+
) {
|
|
284
|
+
if (!state.inCodeBlock) {
|
|
285
|
+
state.inCodeBlock = true
|
|
286
|
+
state.codeBlockLines.clear()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
state.codeBlockLines.add(content.trimStart())
|
|
290
|
+
state.previousWasCodeBlock = true
|
|
291
|
+
state.previousWasBlockquote = false
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private fun outputCodeBlock(
|
|
295
|
+
html: StringBuilder,
|
|
296
|
+
lines: List<String>,
|
|
297
|
+
styles: CachedStyles,
|
|
298
|
+
) {
|
|
299
|
+
if (lines.isEmpty()) return
|
|
300
|
+
|
|
301
|
+
html
|
|
302
|
+
.append("<pre style=\"background-color: ")
|
|
303
|
+
.append(styles.codeBlockBgColor)
|
|
304
|
+
.append("; padding: ")
|
|
305
|
+
.append(styles.codeBlockPadding)
|
|
306
|
+
.append("px; border-radius: ")
|
|
307
|
+
.append(styles.codeBlockBorderRadius)
|
|
308
|
+
.append("px; margin: 0 0 ")
|
|
309
|
+
.append(styles.codeBlockMarginBottom)
|
|
310
|
+
.append("px 0; overflow-x: auto;\"><code style=\"font-family: Menlo, Monaco, Consolas, monospace; font-size: ")
|
|
311
|
+
.append(styles.codeBlockFontSize)
|
|
312
|
+
.append("px; color: ")
|
|
313
|
+
.append(styles.codeBlockColor)
|
|
314
|
+
.append(";\">")
|
|
315
|
+
|
|
316
|
+
for (i in lines.indices) {
|
|
317
|
+
if (i > 0) html.append('\n')
|
|
318
|
+
html.append(lines[i])
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
html.append("</code></pre>")
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private fun handleBlockquote(
|
|
325
|
+
html: StringBuilder,
|
|
326
|
+
content: String,
|
|
327
|
+
para: ParagraphInfo,
|
|
328
|
+
styles: CachedStyles,
|
|
329
|
+
state: GeneratorState,
|
|
330
|
+
) {
|
|
331
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
332
|
+
|
|
333
|
+
val depth = para.depth
|
|
334
|
+
|
|
335
|
+
// Reset if coming from non-blockquote content
|
|
336
|
+
if (!state.previousWasBlockquote && state.inBlockquote) {
|
|
337
|
+
closeAllBlockquotes(html, state)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
while (state.blockquoteDepth > depth) {
|
|
341
|
+
html.append("</blockquote>")
|
|
342
|
+
state.blockquoteDepth--
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
while (state.blockquoteDepth < depth) {
|
|
346
|
+
state.blockquoteDepth++
|
|
347
|
+
state.inBlockquote = true
|
|
348
|
+
|
|
349
|
+
if (state.blockquoteDepth == 0) {
|
|
350
|
+
html
|
|
351
|
+
.append("<blockquote style=\"background-color: ")
|
|
352
|
+
.append(styles.blockquoteBgColor)
|
|
353
|
+
.append("; border-left: ")
|
|
354
|
+
.append(styles.blockquoteBorderWidth)
|
|
355
|
+
.append("px solid ")
|
|
356
|
+
.append(styles.blockquoteBorderColor)
|
|
357
|
+
.append("; padding: ")
|
|
358
|
+
.append(styles.blockquotePaddingVertical)
|
|
359
|
+
.append(" ")
|
|
360
|
+
.append(styles.blockquoteGapWidth)
|
|
361
|
+
.append("px; margin: 0 0 ")
|
|
362
|
+
.append(styles.blockquoteMarginBottom)
|
|
363
|
+
.append("px 0; border-radius: ")
|
|
364
|
+
.append(styles.blockquoteBorderRadiusCorners)
|
|
365
|
+
.append(";\">")
|
|
366
|
+
} else {
|
|
367
|
+
html
|
|
368
|
+
.append("<blockquote style=\"border-left: ")
|
|
369
|
+
.append(styles.blockquoteBorderWidth)
|
|
370
|
+
.append("px solid ")
|
|
371
|
+
.append(styles.blockquoteBorderColor)
|
|
372
|
+
.append("; padding-left: ")
|
|
373
|
+
.append(styles.blockquoteGapWidth)
|
|
374
|
+
.append("px; margin: ")
|
|
375
|
+
.append(styles.blockquoteNestedMargin)
|
|
376
|
+
.append(";\">")
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
html
|
|
381
|
+
.append("<p style=\"margin: ")
|
|
382
|
+
.append(styles.blockquoteParagraphMargin)
|
|
383
|
+
.append("; color: ")
|
|
384
|
+
.append(styles.blockquoteColor)
|
|
385
|
+
.append("; font-size: ")
|
|
386
|
+
.append(styles.blockquoteFontSize)
|
|
387
|
+
.append("px;\">")
|
|
388
|
+
.append(content)
|
|
389
|
+
.append("</p>")
|
|
390
|
+
|
|
391
|
+
state.previousWasBlockquote = true
|
|
392
|
+
state.previousWasCodeBlock = false
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private fun handleList(
|
|
396
|
+
html: StringBuilder,
|
|
397
|
+
content: String,
|
|
398
|
+
para: ParagraphInfo,
|
|
399
|
+
styles: CachedStyles,
|
|
400
|
+
state: GeneratorState,
|
|
401
|
+
) {
|
|
402
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
403
|
+
closeAllBlockquotes(html, state)
|
|
404
|
+
|
|
405
|
+
val depth = para.depth
|
|
406
|
+
val isOrdered = para.type == TYPE_ORDERED_LIST
|
|
407
|
+
|
|
408
|
+
// Close lists to shallower depth
|
|
409
|
+
while (state.listDepth > depth) {
|
|
410
|
+
html.append(if (state.openListTypes.lastOrNull() == true) "</ol>" else "</ul>")
|
|
411
|
+
if (state.openListTypes.isNotEmpty()) state.openListTypes.removeAt(state.openListTypes.lastIndex)
|
|
412
|
+
state.listDepth--
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Handle list type change at same depth (ul <-> ol)
|
|
416
|
+
if (state.listDepth == depth && state.openListTypes.isNotEmpty()) {
|
|
417
|
+
if (state.openListTypes.last() != isOrdered) {
|
|
418
|
+
html.append(if (state.openListTypes.last()) "</ol>" else "</ul>")
|
|
419
|
+
state.openListTypes.removeAt(state.openListTypes.lastIndex)
|
|
420
|
+
state.listDepth--
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Open lists to deeper depth
|
|
425
|
+
while (state.listDepth < depth) {
|
|
426
|
+
state.listDepth++
|
|
427
|
+
val marginStyle =
|
|
428
|
+
if (state.listDepth == 0) {
|
|
429
|
+
"margin: 0 0 ${styles.paragraphMarginBottom}px 0; "
|
|
430
|
+
} else {
|
|
431
|
+
"margin: 0; "
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (isOrdered) {
|
|
435
|
+
html
|
|
436
|
+
.append("<ol style=\"")
|
|
437
|
+
.append(marginStyle)
|
|
438
|
+
.append("padding-left: ")
|
|
439
|
+
.append(styles.listMarginLeft)
|
|
440
|
+
.append("px;\">")
|
|
441
|
+
state.openListTypes.add(true)
|
|
442
|
+
} else {
|
|
443
|
+
html
|
|
444
|
+
.append("<ul style=\"")
|
|
445
|
+
.append(marginStyle)
|
|
446
|
+
.append("padding-left: ")
|
|
447
|
+
.append(styles.listMarginLeft)
|
|
448
|
+
.append("px; list-style-type: disc;\">")
|
|
449
|
+
state.openListTypes.add(false)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
html
|
|
454
|
+
.append("<li style=\"margin-bottom: ")
|
|
455
|
+
.append(styles.listMarginBottom)
|
|
456
|
+
.append("px; color: ")
|
|
457
|
+
.append(styles.listColor)
|
|
458
|
+
.append("; font-size: ")
|
|
459
|
+
.append(styles.listFontSize)
|
|
460
|
+
.append("px;\">")
|
|
461
|
+
.append(content)
|
|
462
|
+
.append("</li>")
|
|
463
|
+
|
|
464
|
+
state.previousWasBlockquote = false
|
|
465
|
+
state.previousWasCodeBlock = false
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private fun handleHeading(
|
|
469
|
+
html: StringBuilder,
|
|
470
|
+
content: String,
|
|
471
|
+
type: Int,
|
|
472
|
+
styles: CachedStyles,
|
|
473
|
+
state: GeneratorState,
|
|
474
|
+
) {
|
|
475
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
476
|
+
closeAllBlockquotes(html, state)
|
|
477
|
+
closeListsIfOpen(html, state)
|
|
478
|
+
|
|
479
|
+
val level = type // TYPE_H1..TYPE_H6 = 1..6
|
|
480
|
+
val idx = level - 1
|
|
481
|
+
|
|
482
|
+
html
|
|
483
|
+
.append("<h")
|
|
484
|
+
.append(level)
|
|
485
|
+
.append(" style=\"font-size: ")
|
|
486
|
+
.append(styles.headingFontSizes[idx])
|
|
487
|
+
.append("px; font-weight: ")
|
|
488
|
+
.append(styles.headingFontWeights[idx])
|
|
489
|
+
.append("; color: ")
|
|
490
|
+
.append(styles.headingColors[idx])
|
|
491
|
+
.append("; margin: 0 0 ")
|
|
492
|
+
.append(styles.headingMarginBottoms[idx])
|
|
493
|
+
.append("px 0;\">")
|
|
494
|
+
.append(content)
|
|
495
|
+
.append("</h")
|
|
496
|
+
.append(level)
|
|
497
|
+
.append('>')
|
|
498
|
+
|
|
499
|
+
state.previousWasBlockquote = false
|
|
500
|
+
state.previousWasCodeBlock = false
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private fun handleNormalParagraph(
|
|
504
|
+
html: StringBuilder,
|
|
505
|
+
content: String,
|
|
506
|
+
styles: CachedStyles,
|
|
507
|
+
state: GeneratorState,
|
|
508
|
+
) {
|
|
509
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
510
|
+
closeAllBlockquotes(html, state)
|
|
511
|
+
closeListsIfOpen(html, state)
|
|
512
|
+
|
|
513
|
+
html
|
|
514
|
+
.append("<p style=\"margin: 0 0 ")
|
|
515
|
+
.append(styles.paragraphMarginBottom)
|
|
516
|
+
.append("px 0; color: ")
|
|
517
|
+
.append(styles.paragraphColor)
|
|
518
|
+
.append("; font-size: ")
|
|
519
|
+
.append(styles.paragraphFontSize)
|
|
520
|
+
.append("px;\">")
|
|
521
|
+
.append(content)
|
|
522
|
+
.append("</p>")
|
|
523
|
+
|
|
524
|
+
state.previousWasBlockquote = false
|
|
525
|
+
state.previousWasCodeBlock = false
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private fun closeCodeBlockIfOpen(
|
|
529
|
+
html: StringBuilder,
|
|
530
|
+
state: GeneratorState,
|
|
531
|
+
styles: CachedStyles,
|
|
532
|
+
) {
|
|
533
|
+
if (state.inCodeBlock) {
|
|
534
|
+
state.inCodeBlock = false
|
|
535
|
+
outputCodeBlock(html, state.codeBlockLines, styles)
|
|
536
|
+
state.codeBlockLines.clear()
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private fun closeAllBlockquotes(
|
|
541
|
+
html: StringBuilder,
|
|
542
|
+
state: GeneratorState,
|
|
543
|
+
) {
|
|
544
|
+
while (state.blockquoteDepth >= 0) {
|
|
545
|
+
html.append("</blockquote>")
|
|
546
|
+
state.blockquoteDepth--
|
|
547
|
+
}
|
|
548
|
+
state.inBlockquote = false
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private fun closeListsIfOpen(
|
|
552
|
+
html: StringBuilder,
|
|
553
|
+
state: GeneratorState,
|
|
554
|
+
) {
|
|
555
|
+
while (state.openListTypes.isNotEmpty()) {
|
|
556
|
+
html.append(if (state.openListTypes.last()) "</ol>" else "</ul>")
|
|
557
|
+
state.openListTypes.removeAt(state.openListTypes.lastIndex)
|
|
558
|
+
}
|
|
559
|
+
state.listDepth = -1
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private fun closeRemainingContainers(
|
|
563
|
+
html: StringBuilder,
|
|
564
|
+
state: GeneratorState,
|
|
565
|
+
styles: CachedStyles,
|
|
566
|
+
) {
|
|
567
|
+
closeCodeBlockIfOpen(html, state, styles)
|
|
568
|
+
closeAllBlockquotes(html, state)
|
|
569
|
+
closeListsIfOpen(html, state)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private fun generateInlineHTML(
|
|
573
|
+
text: Spannable,
|
|
574
|
+
start: Int,
|
|
575
|
+
end: Int,
|
|
576
|
+
styles: CachedStyles,
|
|
577
|
+
isCodeBlock: Boolean,
|
|
578
|
+
): String {
|
|
579
|
+
val html = StringBuilder(end - start + 32)
|
|
580
|
+
var i = start
|
|
581
|
+
|
|
582
|
+
while (i < end) {
|
|
583
|
+
val char = text[i]
|
|
584
|
+
|
|
585
|
+
when {
|
|
586
|
+
char == '\n' && i == end - 1 -> {
|
|
587
|
+
i++
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
char == OBJECT_REPLACEMENT_CHAR -> {
|
|
591
|
+
appendImageIfPresent(html, text, i, styles)
|
|
592
|
+
i++
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
else -> {
|
|
596
|
+
val segmentEnd = minOf(text.nextSpanTransition(i, end, Any::class.java), end)
|
|
597
|
+
val segmentText = text.subSequence(i, segmentEnd).toString()
|
|
598
|
+
|
|
599
|
+
if (segmentText.isNotEmpty() && segmentText != "\n") {
|
|
600
|
+
appendStyledSegment(html, text, i, segmentEnd, segmentText, styles, isCodeBlock)
|
|
601
|
+
}
|
|
602
|
+
i = segmentEnd
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return html.toString()
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private fun appendImageIfPresent(
|
|
611
|
+
html: StringBuilder,
|
|
612
|
+
text: Spannable,
|
|
613
|
+
pos: Int,
|
|
614
|
+
styles: CachedStyles,
|
|
615
|
+
) {
|
|
616
|
+
val imageSpans = text.getSpans(pos, pos + 1, ImageSpan::class.java)
|
|
617
|
+
if (imageSpans.isEmpty()) return
|
|
618
|
+
|
|
619
|
+
val imgSpan = imageSpans[0]
|
|
620
|
+
|
|
621
|
+
if (imgSpan.isInline) {
|
|
622
|
+
html.append("<img src=\"")
|
|
623
|
+
escapeHTMLTo(html, imgSpan.imageUrl)
|
|
624
|
+
html
|
|
625
|
+
.append("\" style=\"height: ")
|
|
626
|
+
.append(styles.inlineImageHeight)
|
|
627
|
+
.append("; width: auto; vertical-align: ")
|
|
628
|
+
.append(styles.inlineImageVerticalAlign)
|
|
629
|
+
.append(";\">")
|
|
630
|
+
} else {
|
|
631
|
+
html
|
|
632
|
+
.append("</p><div style=\"margin-bottom: ")
|
|
633
|
+
.append(styles.imageMarginBottom)
|
|
634
|
+
.append("px;\"><img src=\"")
|
|
635
|
+
escapeHTMLTo(html, imgSpan.imageUrl)
|
|
636
|
+
html
|
|
637
|
+
.append("\" style=\"max-width: 100%; border-radius: ")
|
|
638
|
+
.append(styles.imageBorderRadius)
|
|
639
|
+
.append("px;\"></div><p>")
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private fun appendStyledSegment(
|
|
644
|
+
html: StringBuilder,
|
|
645
|
+
text: Spannable,
|
|
646
|
+
start: Int,
|
|
647
|
+
end: Int,
|
|
648
|
+
content: String,
|
|
649
|
+
styles: CachedStyles,
|
|
650
|
+
isCodeBlock: Boolean,
|
|
651
|
+
) {
|
|
652
|
+
val strongSpans = text.getSpans(start, end, StrongSpan::class.java)
|
|
653
|
+
val styleSpans = text.getSpans(start, end, StyleSpan::class.java)
|
|
654
|
+
val emphasisSpans = text.getSpans(start, end, EmphasisSpan::class.java)
|
|
655
|
+
val underlineSpans = text.getSpans(start, end, UnderlineSpan::class.java)
|
|
656
|
+
val strikethroughSpans = text.getSpans(start, end, StrikethroughSpan::class.java)
|
|
657
|
+
val linkSpans = text.getSpans(start, end, LinkSpan::class.java)
|
|
658
|
+
val codeSpans = text.getSpans(start, end, CodeSpan::class.java)
|
|
659
|
+
|
|
660
|
+
val isBold =
|
|
661
|
+
strongSpans.isNotEmpty() ||
|
|
662
|
+
styleSpans.any { it.style == Typeface.BOLD || it.style == Typeface.BOLD_ITALIC }
|
|
663
|
+
val isItalic =
|
|
664
|
+
emphasisSpans.isNotEmpty() ||
|
|
665
|
+
styleSpans.any { it.style == Typeface.ITALIC || it.style == Typeface.BOLD_ITALIC }
|
|
666
|
+
val isUnderline = underlineSpans.isNotEmpty()
|
|
667
|
+
val isStrikethrough = strikethroughSpans.isNotEmpty()
|
|
668
|
+
val link = linkSpans.firstOrNull()
|
|
669
|
+
val isCode = codeSpans.isNotEmpty() && !isCodeBlock
|
|
670
|
+
|
|
671
|
+
link?.let {
|
|
672
|
+
html.append("<a href=\"")
|
|
673
|
+
escapeHTMLTo(html, it.url)
|
|
674
|
+
html
|
|
675
|
+
.append("\" style=\"color: ")
|
|
676
|
+
.append(styles.linkColor)
|
|
677
|
+
.append("; text-decoration: ")
|
|
678
|
+
.append(if (styles.linkUnderline) "underline" else "none")
|
|
679
|
+
.append(";\">")
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (isCode) {
|
|
683
|
+
html
|
|
684
|
+
.append("<code style=\"background-color: ")
|
|
685
|
+
.append(styles.codeBgColor)
|
|
686
|
+
.append("; color: ")
|
|
687
|
+
.append(styles.codeColor)
|
|
688
|
+
.append("; padding: ")
|
|
689
|
+
.append(styles.codePadding)
|
|
690
|
+
.append("; border-radius: ")
|
|
691
|
+
.append(styles.codeBorderRadius)
|
|
692
|
+
.append("; font-size: ")
|
|
693
|
+
.append(styles.codeFontSize)
|
|
694
|
+
.append("; font-family: Menlo, Monaco, Consolas, monospace;\">")
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (isBold) {
|
|
698
|
+
if (styles.strongColor != null) {
|
|
699
|
+
html.append("<strong style=\"color: ").append(styles.strongColor).append(";\">")
|
|
700
|
+
} else {
|
|
701
|
+
html.append("<strong>")
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (isItalic) {
|
|
706
|
+
if (styles.emphasisColor != null) {
|
|
707
|
+
html.append("<em style=\"color: ").append(styles.emphasisColor).append(";\">")
|
|
708
|
+
} else {
|
|
709
|
+
html.append("<em>")
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (isStrikethrough) html.append("<s>")
|
|
714
|
+
if (isUnderline && link == null) html.append("<u>")
|
|
715
|
+
|
|
716
|
+
escapeHTMLTo(html, content.trimEnd('\n'))
|
|
717
|
+
|
|
718
|
+
if (isUnderline && link == null) html.append("</u>")
|
|
719
|
+
if (isStrikethrough) html.append("</s>")
|
|
720
|
+
if (isItalic) html.append("</em>")
|
|
721
|
+
if (isBold) html.append("</strong>")
|
|
722
|
+
if (isCode) html.append("</code>")
|
|
723
|
+
if (link != null) html.append("</a>")
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private fun collectParagraphs(text: Spannable): ArrayList<ParagraphInfo> {
|
|
727
|
+
val string = text.toString()
|
|
728
|
+
val paragraphs = ArrayList<ParagraphInfo>(string.length / 40 + 1) // Estimate
|
|
729
|
+
var currentIndex = 0
|
|
730
|
+
|
|
731
|
+
while (currentIndex < string.length) {
|
|
732
|
+
var lineEnd = string.indexOf('\n', currentIndex)
|
|
733
|
+
if (lineEnd == -1) lineEnd = string.length else lineEnd++
|
|
734
|
+
|
|
735
|
+
val type = getParagraphType(text, currentIndex)
|
|
736
|
+
val depth = getDepthForType(text, currentIndex, type)
|
|
737
|
+
|
|
738
|
+
paragraphs.add(ParagraphInfo(currentIndex, lineEnd, type, depth))
|
|
739
|
+
currentIndex = lineEnd
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return paragraphs
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private fun getParagraphType(
|
|
746
|
+
text: Spannable,
|
|
747
|
+
start: Int,
|
|
748
|
+
): Int {
|
|
749
|
+
val end = minOf(start + 1, text.length)
|
|
750
|
+
|
|
751
|
+
if (text.getSpans(start, end, CodeBlockSpan::class.java).isNotEmpty()) return TYPE_CODE_BLOCK
|
|
752
|
+
|
|
753
|
+
val headingSpans = text.getSpans(start, end, HeadingSpan::class.java)
|
|
754
|
+
if (headingSpans.isNotEmpty()) return headingSpans[0].level.coerceIn(1, 6)
|
|
755
|
+
|
|
756
|
+
if (text.getSpans(start, end, BlockquoteSpan::class.java).isNotEmpty()) return TYPE_BLOCKQUOTE
|
|
757
|
+
if (text.getSpans(start, end, OrderedListSpan::class.java).isNotEmpty()) return TYPE_ORDERED_LIST
|
|
758
|
+
if (text.getSpans(start, end, UnorderedListSpan::class.java).isNotEmpty()) return TYPE_UNORDERED_LIST
|
|
759
|
+
|
|
760
|
+
return TYPE_NORMAL
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private fun getDepthForType(
|
|
764
|
+
text: Spannable,
|
|
765
|
+
start: Int,
|
|
766
|
+
type: Int,
|
|
767
|
+
): Int {
|
|
768
|
+
val end = start + 1
|
|
769
|
+
return when (type) {
|
|
770
|
+
TYPE_BLOCKQUOTE -> text.getSpans(start, end, BlockquoteSpan::class.java).maxOfOrNull { it.depth } ?: 0
|
|
771
|
+
TYPE_ORDERED_LIST -> text.getSpans(start, end, OrderedListSpan::class.java).maxOfOrNull { it.depth } ?: 0
|
|
772
|
+
TYPE_UNORDERED_LIST -> text.getSpans(start, end, UnorderedListSpan::class.java).maxOfOrNull { it.depth } ?: 0
|
|
773
|
+
else -> 0
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** Fast HTML escape using single-pass character scanning. */
|
|
778
|
+
private fun escapeHTMLTo(
|
|
779
|
+
output: StringBuilder,
|
|
780
|
+
text: String,
|
|
781
|
+
) {
|
|
782
|
+
val len = text.length
|
|
783
|
+
|
|
784
|
+
// Fast path: skip if no escaping needed
|
|
785
|
+
var needsEscape = false
|
|
786
|
+
for (i in 0 until len) {
|
|
787
|
+
val c = text[i]
|
|
788
|
+
if (c == '&' || c == '<' || c == '>' || c == '"' || c == '\'') {
|
|
789
|
+
needsEscape = true
|
|
790
|
+
break
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!needsEscape) {
|
|
794
|
+
output.append(text)
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
for (i in 0 until len) {
|
|
799
|
+
when (val c = text[i]) {
|
|
800
|
+
'&' -> output.append("&")
|
|
801
|
+
'<' -> output.append("<")
|
|
802
|
+
'>' -> output.append(">")
|
|
803
|
+
'"' -> output.append(""")
|
|
804
|
+
'\'' -> output.append("'")
|
|
805
|
+
else -> output.append(c)
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|