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.
Files changed (211) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +479 -0
  3. package/ReactNativeEnrichedMarkdown.podspec +27 -0
  4. package/android/build.gradle +101 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +39 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +21 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
  10. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
  11. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +57 -0
  12. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1164 -0
  13. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
  14. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
  15. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.cpp +16 -0
  16. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.h +20 -0
  17. package/android/gradle.properties +5 -0
  18. package/android/src/main/AndroidManifest.xml +2 -0
  19. package/android/src/main/baseline-prof.txt +65 -0
  20. package/android/src/main/cpp/jni-adapter.cpp +203 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +153 -0
  22. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +30 -0
  23. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +119 -0
  24. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextPackage.kt +17 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +165 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +23 -0
  27. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +29 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +48 -0
  29. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +166 -0
  30. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +89 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +105 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +35 -0
  33. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +15 -0
  34. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +26 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +54 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +52 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +15 -0
  38. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +28 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +105 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +58 -0
  41. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +69 -0
  42. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +99 -0
  43. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +66 -0
  44. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +95 -0
  45. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +85 -0
  46. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +26 -0
  47. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +29 -0
  48. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +44 -0
  49. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +136 -0
  50. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +135 -0
  51. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +180 -0
  52. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +196 -0
  53. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +27 -0
  54. package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +34 -0
  55. package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +38 -0
  56. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +320 -0
  57. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +36 -0
  58. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +37 -0
  59. package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +76 -0
  60. package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +87 -0
  61. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +37 -0
  62. package/android/src/main/java/com/swmansion/enriched/markdown/spans/TextSpan.kt +26 -0
  63. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ThematicBreakSpan.kt +69 -0
  64. package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +69 -0
  65. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +10 -0
  66. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +48 -0
  67. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +51 -0
  68. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeStyle.kt +21 -0
  69. package/android/src/main/java/com/swmansion/enriched/markdown/styles/EmphasisStyle.kt +17 -0
  70. package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +29 -0
  71. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +21 -0
  72. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineImageStyle.kt +17 -0
  73. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +19 -0
  74. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +54 -0
  75. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +29 -0
  76. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrongStyle.kt +17 -0
  77. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +180 -0
  78. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +75 -0
  79. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -0
  80. package/android/src/main/java/com/swmansion/enriched/markdown/utils/AsyncDrawable.kt +91 -0
  81. package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +809 -0
  82. package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +365 -0
  83. package/android/src/main/java/com/swmansion/enriched/markdown/utils/SelectionActionMode.kt +139 -0
  84. package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +181 -0
  85. package/android/src/main/jni/CMakeLists.txt +82 -0
  86. package/android/src/main/jni/EnrichedMarkdownTextSpec.cpp +21 -0
  87. package/android/src/main/jni/EnrichedMarkdownTextSpec.h +25 -0
  88. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextComponentDescriptor.h +29 -0
  89. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.cpp +45 -0
  90. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.h +21 -0
  91. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +33 -0
  92. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +49 -0
  93. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
  94. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +25 -0
  95. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +19 -0
  96. package/cpp/md4c/md4c.c +6492 -0
  97. package/cpp/md4c/md4c.h +402 -0
  98. package/cpp/parser/MD4CParser.cpp +314 -0
  99. package/cpp/parser/MD4CParser.hpp +23 -0
  100. package/cpp/parser/MarkdownASTNode.hpp +49 -0
  101. package/ios/EnrichedMarkdownText.h +18 -0
  102. package/ios/EnrichedMarkdownText.mm +1074 -0
  103. package/ios/attachments/ImageAttachment.h +23 -0
  104. package/ios/attachments/ImageAttachment.m +185 -0
  105. package/ios/attachments/ThematicBreakAttachment.h +15 -0
  106. package/ios/attachments/ThematicBreakAttachment.m +33 -0
  107. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
  108. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
  109. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
  110. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
  111. package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +57 -0
  112. package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1164 -0
  113. package/ios/generated/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +20 -0
  114. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
  115. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
  116. package/ios/generated/EnrichedMarkdownTextSpec/States.cpp +16 -0
  117. package/ios/generated/EnrichedMarkdownTextSpec/States.h +20 -0
  118. package/ios/internals/EnrichedMarkdownTextComponentDescriptor.h +19 -0
  119. package/ios/internals/EnrichedMarkdownTextShadowNode.h +43 -0
  120. package/ios/internals/EnrichedMarkdownTextShadowNode.mm +85 -0
  121. package/ios/internals/EnrichedMarkdownTextState.h +24 -0
  122. package/ios/parser/MarkdownASTNode.h +33 -0
  123. package/ios/parser/MarkdownASTNode.m +32 -0
  124. package/ios/parser/MarkdownParser.h +8 -0
  125. package/ios/parser/MarkdownParser.mm +13 -0
  126. package/ios/parser/MarkdownParserBridge.mm +110 -0
  127. package/ios/renderer/AttributedRenderer.h +9 -0
  128. package/ios/renderer/AttributedRenderer.m +119 -0
  129. package/ios/renderer/BlockquoteRenderer.h +7 -0
  130. package/ios/renderer/BlockquoteRenderer.m +159 -0
  131. package/ios/renderer/CodeBlockRenderer.h +10 -0
  132. package/ios/renderer/CodeBlockRenderer.m +89 -0
  133. package/ios/renderer/CodeRenderer.h +10 -0
  134. package/ios/renderer/CodeRenderer.m +60 -0
  135. package/ios/renderer/EmphasisRenderer.h +6 -0
  136. package/ios/renderer/EmphasisRenderer.m +96 -0
  137. package/ios/renderer/HeadingRenderer.h +7 -0
  138. package/ios/renderer/HeadingRenderer.m +98 -0
  139. package/ios/renderer/ImageRenderer.h +12 -0
  140. package/ios/renderer/ImageRenderer.m +62 -0
  141. package/ios/renderer/LinkRenderer.h +7 -0
  142. package/ios/renderer/LinkRenderer.m +69 -0
  143. package/ios/renderer/ListItemRenderer.h +16 -0
  144. package/ios/renderer/ListItemRenderer.m +91 -0
  145. package/ios/renderer/ListRenderer.h +13 -0
  146. package/ios/renderer/ListRenderer.m +67 -0
  147. package/ios/renderer/NodeRenderer.h +8 -0
  148. package/ios/renderer/ParagraphRenderer.h +7 -0
  149. package/ios/renderer/ParagraphRenderer.m +69 -0
  150. package/ios/renderer/RenderContext.h +88 -0
  151. package/ios/renderer/RenderContext.m +248 -0
  152. package/ios/renderer/RendererFactory.h +12 -0
  153. package/ios/renderer/RendererFactory.m +110 -0
  154. package/ios/renderer/StrongRenderer.h +6 -0
  155. package/ios/renderer/StrongRenderer.m +83 -0
  156. package/ios/renderer/TextRenderer.h +6 -0
  157. package/ios/renderer/TextRenderer.m +16 -0
  158. package/ios/renderer/ThematicBreakRenderer.h +5 -0
  159. package/ios/renderer/ThematicBreakRenderer.m +53 -0
  160. package/ios/styles/StyleConfig.h +228 -0
  161. package/ios/styles/StyleConfig.mm +1467 -0
  162. package/ios/utils/BlockquoteBorder.h +20 -0
  163. package/ios/utils/BlockquoteBorder.m +92 -0
  164. package/ios/utils/CodeBackground.h +19 -0
  165. package/ios/utils/CodeBackground.m +191 -0
  166. package/ios/utils/CodeBlockBackground.h +17 -0
  167. package/ios/utils/CodeBlockBackground.m +87 -0
  168. package/ios/utils/EditMenuUtils.h +22 -0
  169. package/ios/utils/EditMenuUtils.m +118 -0
  170. package/ios/utils/FontUtils.h +20 -0
  171. package/ios/utils/FontUtils.m +13 -0
  172. package/ios/utils/HTMLGenerator.h +20 -0
  173. package/ios/utils/HTMLGenerator.m +779 -0
  174. package/ios/utils/LastElementUtils.h +53 -0
  175. package/ios/utils/ListMarkerDrawer.h +15 -0
  176. package/ios/utils/ListMarkerDrawer.m +127 -0
  177. package/ios/utils/MarkdownExtractor.h +17 -0
  178. package/ios/utils/MarkdownExtractor.m +295 -0
  179. package/ios/utils/ParagraphStyleUtils.h +13 -0
  180. package/ios/utils/ParagraphStyleUtils.m +56 -0
  181. package/ios/utils/PasteboardUtils.h +36 -0
  182. package/ios/utils/PasteboardUtils.m +134 -0
  183. package/ios/utils/RTFExportUtils.h +24 -0
  184. package/ios/utils/RTFExportUtils.m +297 -0
  185. package/ios/utils/RuntimeKeys.h +38 -0
  186. package/ios/utils/RuntimeKeys.m +11 -0
  187. package/ios/utils/TextViewLayoutManager.h +14 -0
  188. package/ios/utils/TextViewLayoutManager.mm +113 -0
  189. package/lib/module/EnrichedMarkdownText.js +34 -0
  190. package/lib/module/EnrichedMarkdownText.js.map +1 -0
  191. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +130 -0
  192. package/lib/module/index.js +5 -0
  193. package/lib/module/index.js.map +1 -0
  194. package/lib/module/normalizeMarkdownStyle.js +340 -0
  195. package/lib/module/normalizeMarkdownStyle.js.map +1 -0
  196. package/lib/module/package.json +1 -0
  197. package/lib/typescript/package.json +1 -0
  198. package/lib/typescript/src/EnrichedMarkdownText.d.ts +101 -0
  199. package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
  200. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +111 -0
  201. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
  202. package/lib/typescript/src/index.d.ts +5 -0
  203. package/lib/typescript/src/index.d.ts.map +1 -0
  204. package/lib/typescript/src/normalizeMarkdownStyle.d.ts +6 -0
  205. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -0
  206. package/package.json +186 -1
  207. package/react-native.config.js +13 -0
  208. package/src/EnrichedMarkdownText.tsx +152 -0
  209. package/src/EnrichedMarkdownTextNativeComponent.ts +130 -0
  210. package/src/index.tsx +7 -0
  211. 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("&amp;")
801
+ '<' -> output.append("&lt;")
802
+ '>' -> output.append("&gt;")
803
+ '"' -> output.append("&quot;")
804
+ '\'' -> output.append("&#39;")
805
+ else -> output.append(c)
806
+ }
807
+ }
808
+ }
809
+ }