react-native-enriched-markdown 0.1.0 → 0.2.0
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 +551 -0
- package/ReactNativeEnrichedMarkdown.podspec +27 -0
- package/android/build.gradle +101 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +54 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +26 -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 +33 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +82 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1388 -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 +220 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +270 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +173 -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 +385 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +279 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLongPressEvent.kt +23 -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 +31 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +62 -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 +84 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +104 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +70 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +68 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +29 -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 +59 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +76 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +103 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +80 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +109 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +86 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +45 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +27 -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 +321 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +51 -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/StrikethroughSpan.kt +12 -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 +11 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +51 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +54 -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 +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +23 -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 +57 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -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 +211 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +92 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/UnderlineStyle.kt +17 -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 +827 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt +121 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +375 -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 +183 -0
- package/android/src/main/jni/CMakeLists.txt +70 -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 +20 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +37 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +22 -0
- package/cpp/md4c/md4c.c +6492 -0
- package/cpp/md4c/md4c.h +402 -0
- package/cpp/parser/MD4CParser.cpp +327 -0
- package/cpp/parser/MD4CParser.hpp +27 -0
- package/cpp/parser/MarkdownASTNode.hpp +51 -0
- package/ios/EnrichedMarkdownText.h +18 -0
- package/ios/EnrichedMarkdownText.mm +1401 -0
- package/ios/attachments/EnrichedMarkdownImageAttachment.h +23 -0
- package/ios/attachments/EnrichedMarkdownImageAttachment.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 +33 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +82 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1388 -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 +35 -0
- package/ios/parser/MarkdownASTNode.m +32 -0
- package/ios/parser/MarkdownParser.h +17 -0
- package/ios/parser/MarkdownParser.mm +42 -0
- package/ios/parser/MarkdownParserBridge.mm +120 -0
- package/ios/renderer/AttributedRenderer.h +11 -0
- package/ios/renderer/AttributedRenderer.m +152 -0
- package/ios/renderer/BlockquoteRenderer.h +7 -0
- package/ios/renderer/BlockquoteRenderer.m +160 -0
- package/ios/renderer/CodeBlockRenderer.h +10 -0
- package/ios/renderer/CodeBlockRenderer.m +90 -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 +105 -0
- package/ios/renderer/ImageRenderer.h +12 -0
- package/ios/renderer/ImageRenderer.m +83 -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 +103 -0
- package/ios/renderer/ListRenderer.h +13 -0
- package/ios/renderer/ListRenderer.m +70 -0
- package/ios/renderer/NodeRenderer.h +8 -0
- package/ios/renderer/ParagraphRenderer.h +7 -0
- package/ios/renderer/ParagraphRenderer.m +80 -0
- package/ios/renderer/RenderContext.h +105 -0
- package/ios/renderer/RenderContext.m +312 -0
- package/ios/renderer/RendererFactory.h +12 -0
- package/ios/renderer/RendererFactory.m +116 -0
- package/ios/renderer/StrikethroughRenderer.h +6 -0
- package/ios/renderer/StrikethroughRenderer.m +40 -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/renderer/UnderlineRenderer.h +6 -0
- package/ios/renderer/UnderlineRenderer.m +39 -0
- package/ios/styles/StyleConfig.h +274 -0
- package/ios/styles/StyleConfig.mm +1806 -0
- package/ios/utils/AccessibilityInfo.h +35 -0
- package/ios/utils/AccessibilityInfo.m +24 -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 +82 -0
- package/ios/utils/EditMenuUtils.h +22 -0
- package/ios/utils/EditMenuUtils.m +118 -0
- package/ios/utils/FontUtils.h +25 -0
- package/ios/utils/FontUtils.m +27 -0
- package/ios/utils/HTMLGenerator.h +20 -0
- package/ios/utils/HTMLGenerator.m +793 -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/MarkdownAccessibilityElementBuilder.h +45 -0
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
- package/ios/utils/MarkdownExtractor.h +17 -0
- package/ios/utils/MarkdownExtractor.m +308 -0
- package/ios/utils/ParagraphStyleUtils.h +21 -0
- package/ios/utils/ParagraphStyleUtils.m +111 -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 +65 -0
- package/lib/module/EnrichedMarkdownText.js.map +1 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +210 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalizeMarkdownStyle.js +384 -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 +183 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +185 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -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 +280 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +210 -0
- package/src/index.tsx +10 -0
- package/src/normalizeMarkdownStyle.ts +423 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.accessibility
|
|
2
|
+
|
|
3
|
+
import android.graphics.Rect
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import android.text.Spanned
|
|
6
|
+
import android.widget.TextView
|
|
7
|
+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
|
8
|
+
import androidx.customview.widget.ExploreByTouchHelper
|
|
9
|
+
import com.swmansion.enriched.markdown.spans.BaseListSpan
|
|
10
|
+
import com.swmansion.enriched.markdown.spans.HeadingSpan
|
|
11
|
+
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
12
|
+
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
13
|
+
import com.swmansion.enriched.markdown.spans.OrderedListSpan
|
|
14
|
+
import com.swmansion.enriched.markdown.spans.UnorderedListSpan
|
|
15
|
+
|
|
16
|
+
class MarkdownAccessibilityHelper(
|
|
17
|
+
private val textView: TextView,
|
|
18
|
+
) : ExploreByTouchHelper(textView) {
|
|
19
|
+
private var accessibilityItems: List<AccessibilityItem> = emptyList()
|
|
20
|
+
private var needsRebuild = false
|
|
21
|
+
private var lastLayoutHashCode = 0
|
|
22
|
+
|
|
23
|
+
data class AccessibilityItem(
|
|
24
|
+
val id: Int,
|
|
25
|
+
val text: String,
|
|
26
|
+
val start: Int,
|
|
27
|
+
val end: Int,
|
|
28
|
+
val headingLevel: Int = 0,
|
|
29
|
+
val linkUrl: String? = null,
|
|
30
|
+
val listInfo: ListItemInfo? = null,
|
|
31
|
+
val imageAltText: String? = null,
|
|
32
|
+
) {
|
|
33
|
+
val isHeading get() = headingLevel > 0
|
|
34
|
+
val isLink get() = linkUrl != null
|
|
35
|
+
val isListItem get() = listInfo != null
|
|
36
|
+
val isImage get() = imageAltText != null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
data class ListItemInfo(
|
|
40
|
+
val isOrdered: Boolean,
|
|
41
|
+
val itemNumber: Int,
|
|
42
|
+
val depth: Int,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
private data class SpanRange(
|
|
46
|
+
val start: Int,
|
|
47
|
+
val end: Int,
|
|
48
|
+
val headingLevel: Int = 0,
|
|
49
|
+
val linkUrl: String? = null,
|
|
50
|
+
val imageAltText: String? = null,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
fun invalidateAccessibilityItems() {
|
|
54
|
+
needsRebuild = true
|
|
55
|
+
rebuildIfNeeded()
|
|
56
|
+
invalidateRoot()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private fun rebuildIfNeeded() {
|
|
60
|
+
val layout = textView.layout ?: return
|
|
61
|
+
if (needsRebuild || lastLayoutHashCode != layout.hashCode()) {
|
|
62
|
+
accessibilityItems = buildAccessibilityItems()
|
|
63
|
+
needsRebuild = false
|
|
64
|
+
lastLayoutHashCode = layout.hashCode()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun buildAccessibilityItems(): List<AccessibilityItem> {
|
|
69
|
+
val spanned = textView.text as? Spanned ?: return emptyList()
|
|
70
|
+
if (spanned.isEmpty()) return emptyList()
|
|
71
|
+
|
|
72
|
+
val items = mutableListOf<AccessibilityItem>()
|
|
73
|
+
var nextId = 0
|
|
74
|
+
|
|
75
|
+
// Consolidated span collection using functional mapping
|
|
76
|
+
val semanticSpans =
|
|
77
|
+
(
|
|
78
|
+
spanned.getSpans(0, spanned.length, HeadingSpan::class.java).map {
|
|
79
|
+
SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), headingLevel = it.level)
|
|
80
|
+
} +
|
|
81
|
+
spanned.getSpans(0, spanned.length, LinkSpan::class.java).map {
|
|
82
|
+
SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), linkUrl = it.url)
|
|
83
|
+
} +
|
|
84
|
+
spanned.getSpans(0, spanned.length, ImageSpan::class.java).map {
|
|
85
|
+
SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), imageAltText = it.altText)
|
|
86
|
+
}
|
|
87
|
+
).sortedBy { it.start }
|
|
88
|
+
|
|
89
|
+
var currentPos = 0
|
|
90
|
+
for (span in semanticSpans) {
|
|
91
|
+
if (span.start < currentPos) continue
|
|
92
|
+
|
|
93
|
+
if (currentPos < span.start) {
|
|
94
|
+
nextId = addTextSegments(items, spanned, currentPos, span.start, nextId)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
val content = span.imageAltText?.ifEmpty { "Image" } ?: spanned.substring(span.start, span.end).trim()
|
|
98
|
+
|
|
99
|
+
if (content.isNotEmpty()) {
|
|
100
|
+
val listContext =
|
|
101
|
+
if (span.headingLevel > 0 || span.imageAltText != null) {
|
|
102
|
+
null
|
|
103
|
+
} else {
|
|
104
|
+
getListInfoAt(spanned, span.start, span.linkUrl == null)
|
|
105
|
+
}
|
|
106
|
+
items.add(
|
|
107
|
+
AccessibilityItem(
|
|
108
|
+
nextId++,
|
|
109
|
+
content,
|
|
110
|
+
span.start,
|
|
111
|
+
span.end,
|
|
112
|
+
span.headingLevel,
|
|
113
|
+
span.linkUrl,
|
|
114
|
+
listContext,
|
|
115
|
+
span.imageAltText,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
currentPos = span.end
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (currentPos < spanned.length) addTextSegments(items, spanned, currentPos, spanned.length, nextId)
|
|
123
|
+
return items.ifEmpty { listOf(AccessibilityItem(0, spanned.toString().trim(), 0, spanned.length)) }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fun getListInfoAt(
|
|
127
|
+
spanned: Spanned,
|
|
128
|
+
position: Int,
|
|
129
|
+
requireStart: Boolean,
|
|
130
|
+
): ListItemInfo? {
|
|
131
|
+
val deepest = spanned.getSpans(position, position + 1, BaseListSpan::class.java).maxByOrNull { it.depth } ?: return null
|
|
132
|
+
if (requireStart) {
|
|
133
|
+
val start = spanned.getSpanStart(deepest)
|
|
134
|
+
val firstChar = (start until minOf(start + 10, spanned.length)).firstOrNull { !spanned[it].isWhitespace() } ?: start
|
|
135
|
+
if (position > firstChar + 1) return null
|
|
136
|
+
}
|
|
137
|
+
return ListItemInfo(deepest is OrderedListSpan, (deepest as? OrderedListSpan)?.itemNumber ?: 0, deepest.depth)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun addTextSegments(
|
|
141
|
+
items: MutableList<AccessibilityItem>,
|
|
142
|
+
spanned: Spanned,
|
|
143
|
+
start: Int,
|
|
144
|
+
end: Int,
|
|
145
|
+
startId: Int,
|
|
146
|
+
): Int {
|
|
147
|
+
var cid = startId
|
|
148
|
+
val layout = textView.layout ?: return cid
|
|
149
|
+
for (line in layout.getLineForOffset(start)..layout.getLineForOffset(end)) {
|
|
150
|
+
val s = maxOf(start, layout.getLineStart(line))
|
|
151
|
+
val e = minOf(end, layout.getLineEnd(line))
|
|
152
|
+
if (s >= e) continue
|
|
153
|
+
|
|
154
|
+
val raw = spanned.substring(s, e)
|
|
155
|
+
val first = raw.indexOfFirst { !it.isWhitespace() }
|
|
156
|
+
if (first != -1) {
|
|
157
|
+
val last = raw.indexOfLast { !it.isWhitespace() }
|
|
158
|
+
val absoluteStart = s + first
|
|
159
|
+
items.add(
|
|
160
|
+
AccessibilityItem(cid++, raw.trim(), absoluteStart, s + last + 1, listInfo = getListInfoAt(spanned, absoluteStart, true)),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return cid
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override fun getVirtualViewAt(
|
|
168
|
+
x: Float,
|
|
169
|
+
y: Float,
|
|
170
|
+
): Int {
|
|
171
|
+
rebuildIfNeeded()
|
|
172
|
+
val offset = getOffsetForPosition(x, y)
|
|
173
|
+
return accessibilityItems
|
|
174
|
+
.filter { offset in it.start until it.end }
|
|
175
|
+
.minByOrNull {
|
|
176
|
+
when {
|
|
177
|
+
it.isLink -> 0
|
|
178
|
+
it.isImage -> 1
|
|
179
|
+
it.isHeading -> 2
|
|
180
|
+
it.isListItem -> 3
|
|
181
|
+
else -> 4
|
|
182
|
+
}
|
|
183
|
+
}?.id ?: HOST_ID
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
override fun getVisibleVirtualViews(ids: MutableList<Int>) {
|
|
187
|
+
rebuildIfNeeded()
|
|
188
|
+
accessibilityItems.forEach { ids.add(it.id) }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
override fun onPopulateNodeForVirtualView(
|
|
192
|
+
id: Int,
|
|
193
|
+
node: AccessibilityNodeInfoCompat,
|
|
194
|
+
) {
|
|
195
|
+
val item = accessibilityItems.find { it.id == id } ?: return
|
|
196
|
+
node.apply {
|
|
197
|
+
text = item.text
|
|
198
|
+
contentDescription = item.text
|
|
199
|
+
isFocusable = true
|
|
200
|
+
isScreenReaderFocusable = true
|
|
201
|
+
setBoundsInParent(getBoundsForRange(item.start, item.end))
|
|
202
|
+
|
|
203
|
+
item.listInfo?.let { info ->
|
|
204
|
+
setCollectionItemInfo(
|
|
205
|
+
AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(info.itemNumber - 1, 1, 0, 1, false, false),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
val prefix = if (item.listInfo?.depth ?: 0 > 0) "nested " else ""
|
|
210
|
+
val listText = if (item.listInfo?.isOrdered == true) "list item ${item.listInfo.itemNumber}" else "bullet point"
|
|
211
|
+
|
|
212
|
+
when {
|
|
213
|
+
item.isHeading -> {
|
|
214
|
+
isHeading = true
|
|
215
|
+
contentDescription = "${item.text}, heading level ${item.headingLevel}"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
item.isImage -> {
|
|
219
|
+
roleDescription = "image"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
item.isLink -> {
|
|
223
|
+
isClickable = true
|
|
224
|
+
addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK)
|
|
225
|
+
roleDescription = item.listInfo?.let { "link, $prefix$listText" } ?: "link"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
item.isListItem -> {
|
|
229
|
+
roleDescription = "$prefix$listText"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
override fun onPerformActionForVirtualView(
|
|
236
|
+
id: Int,
|
|
237
|
+
action: Int,
|
|
238
|
+
args: Bundle?,
|
|
239
|
+
): Boolean {
|
|
240
|
+
val item = accessibilityItems.find { it.id == id } ?: return false
|
|
241
|
+
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && item.isLink) {
|
|
242
|
+
(textView.text as? Spanned)?.getSpans(item.start, item.end, LinkSpan::class.java)?.firstOrNull()?.onClick(textView)
|
|
243
|
+
?: return false
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private fun getOffsetForPosition(
|
|
250
|
+
x: Float,
|
|
251
|
+
y: Float,
|
|
252
|
+
): Int {
|
|
253
|
+
val layout = textView.layout ?: return 0
|
|
254
|
+
return layout.getOffsetForHorizontal(layout.getLineForVertical(y.toInt()).coerceIn(0, layout.lineCount - 1), x)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private fun getBoundsForRange(
|
|
258
|
+
start: Int,
|
|
259
|
+
end: Int,
|
|
260
|
+
): Rect {
|
|
261
|
+
val layout = textView.layout ?: return Rect()
|
|
262
|
+
val line = layout.getLineForOffset(start)
|
|
263
|
+
val left = layout.getPrimaryHorizontal(start).toInt() + textView.paddingLeft
|
|
264
|
+
val right =
|
|
265
|
+
if (layout.getPrimaryHorizontal(end) <=
|
|
266
|
+
layout.getPrimaryHorizontal(start)
|
|
267
|
+
) {
|
|
268
|
+
layout.getLineRight(line).toInt() + textView.paddingLeft
|
|
269
|
+
} else {
|
|
270
|
+
layout.getPrimaryHorizontal(end).toInt() + textView.paddingLeft
|
|
271
|
+
}
|
|
272
|
+
return Rect(
|
|
273
|
+
left,
|
|
274
|
+
layout.getLineTop(line) + textView.paddingTop,
|
|
275
|
+
right,
|
|
276
|
+
layout.getLineBottom(layout.getLineForOffset(end)) + textView.paddingTop,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
|
|
7
|
+
class LinkLongPressEvent(
|
|
8
|
+
surfaceId: Int,
|
|
9
|
+
viewId: Int,
|
|
10
|
+
private val url: String,
|
|
11
|
+
) : Event<LinkLongPressEvent>(surfaceId, viewId) {
|
|
12
|
+
override fun getEventName(): String = EVENT_NAME
|
|
13
|
+
|
|
14
|
+
override fun getEventData(): WritableMap {
|
|
15
|
+
val eventData: WritableMap = Arguments.createMap()
|
|
16
|
+
eventData.putString("url", url)
|
|
17
|
+
return eventData
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
const val EVENT_NAME: String = "onLinkLongPress"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
|
|
7
|
+
class LinkPressEvent(
|
|
8
|
+
surfaceId: Int,
|
|
9
|
+
viewId: Int,
|
|
10
|
+
private val url: String,
|
|
11
|
+
) : Event<LinkPressEvent>(surfaceId, viewId) {
|
|
12
|
+
override fun getEventName(): String = EVENT_NAME
|
|
13
|
+
|
|
14
|
+
override fun getEventData(): WritableMap {
|
|
15
|
+
val eventData: WritableMap = Arguments.createMap()
|
|
16
|
+
eventData.putString("url", url)
|
|
17
|
+
return eventData
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
const val EVENT_NAME: String = "onLinkPress"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.parser
|
|
2
|
+
|
|
3
|
+
data class MarkdownASTNode(
|
|
4
|
+
val type: NodeType,
|
|
5
|
+
val content: String = "",
|
|
6
|
+
val attributes: Map<String, String> = emptyMap(),
|
|
7
|
+
val children: List<MarkdownASTNode> = emptyList(),
|
|
8
|
+
) {
|
|
9
|
+
enum class NodeType {
|
|
10
|
+
Document,
|
|
11
|
+
Paragraph,
|
|
12
|
+
Text,
|
|
13
|
+
Link,
|
|
14
|
+
Heading,
|
|
15
|
+
LineBreak,
|
|
16
|
+
Strong,
|
|
17
|
+
Emphasis,
|
|
18
|
+
Strikethrough,
|
|
19
|
+
Underline,
|
|
20
|
+
Code,
|
|
21
|
+
Image,
|
|
22
|
+
Blockquote,
|
|
23
|
+
UnorderedList,
|
|
24
|
+
OrderedList,
|
|
25
|
+
ListItem,
|
|
26
|
+
CodeBlock,
|
|
27
|
+
ThematicBreak,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun getAttribute(key: String): String? = attributes[key]
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.parser
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
data class Md4cFlags(
|
|
6
|
+
val underline: Boolean = false,
|
|
7
|
+
) {
|
|
8
|
+
companion object {
|
|
9
|
+
val DEFAULT = Md4cFlags()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class Parser {
|
|
14
|
+
companion object {
|
|
15
|
+
init {
|
|
16
|
+
try {
|
|
17
|
+
// Library name must follow react_codegen_<ComponentName> convention
|
|
18
|
+
// required by React Native Fabric's CMake build system.
|
|
19
|
+
// md4c parser is bundled in the same shared library to avoid
|
|
20
|
+
// multiple library loading and complex linking dependencies.
|
|
21
|
+
System.loadLibrary("react_codegen_EnrichedMarkdownTextSpec")
|
|
22
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
23
|
+
Log.e("MarkdownParser", "Failed to load native library", e)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@JvmStatic
|
|
28
|
+
private external fun nativeParseMarkdown(
|
|
29
|
+
markdown: String,
|
|
30
|
+
flags: Md4cFlags,
|
|
31
|
+
): MarkdownASTNode?
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shared parser instance. Parser is stateless and thread-safe, so it can be reused
|
|
35
|
+
* across all EnrichedMarkdownText instances to avoid unnecessary allocations.
|
|
36
|
+
*/
|
|
37
|
+
val shared: Parser = Parser()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fun parseMarkdown(
|
|
41
|
+
markdown: String,
|
|
42
|
+
flags: Md4cFlags = Md4cFlags.DEFAULT,
|
|
43
|
+
): MarkdownASTNode? {
|
|
44
|
+
if (markdown.isBlank()) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
val ast = nativeParseMarkdown(markdown, flags)
|
|
50
|
+
|
|
51
|
+
if (ast != null) {
|
|
52
|
+
return ast
|
|
53
|
+
} else {
|
|
54
|
+
Log.w("MarkdownParser", "Native parser returned null")
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
} catch (e: Exception) {
|
|
58
|
+
Log.e("MarkdownParser", "MD4C parsing failed: ${e.message}", e)
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import com.swmansion.enriched.markdown.styles.BaseBlockStyle
|
|
4
|
+
import com.swmansion.enriched.markdown.styles.BlockquoteStyle
|
|
5
|
+
import com.swmansion.enriched.markdown.styles.CodeBlockStyle
|
|
6
|
+
import com.swmansion.enriched.markdown.styles.HeadingStyle
|
|
7
|
+
import com.swmansion.enriched.markdown.styles.ListStyle
|
|
8
|
+
import com.swmansion.enriched.markdown.styles.ParagraphStyle
|
|
9
|
+
|
|
10
|
+
enum class BlockType {
|
|
11
|
+
NONE,
|
|
12
|
+
PARAGRAPH,
|
|
13
|
+
HEADING,
|
|
14
|
+
BLOCKQUOTE,
|
|
15
|
+
UNORDERED_LIST,
|
|
16
|
+
ORDERED_LIST,
|
|
17
|
+
CODE_BLOCK,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
data class BlockStyle(
|
|
21
|
+
val fontSize: Float,
|
|
22
|
+
val fontFamily: String,
|
|
23
|
+
val fontWeight: String,
|
|
24
|
+
val color: Int,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
private class MutableBlockStyle {
|
|
28
|
+
var fontSize: Float = 0f
|
|
29
|
+
var fontFamily: String = ""
|
|
30
|
+
var fontWeight: String = ""
|
|
31
|
+
var color: Int = 0
|
|
32
|
+
var isDirty: Boolean = false
|
|
33
|
+
|
|
34
|
+
fun updateFrom(style: BaseBlockStyle) {
|
|
35
|
+
fontSize = style.fontSize
|
|
36
|
+
fontFamily = style.fontFamily
|
|
37
|
+
fontWeight = style.fontWeight
|
|
38
|
+
color = style.color
|
|
39
|
+
isDirty = true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fun toImmutable(): BlockStyle = BlockStyle(fontSize, fontFamily, fontWeight, color)
|
|
43
|
+
|
|
44
|
+
fun clear() {
|
|
45
|
+
isDirty = false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class BlockStyleContext {
|
|
50
|
+
var currentBlockType = BlockType.NONE
|
|
51
|
+
private set
|
|
52
|
+
|
|
53
|
+
private val mutableBlockStyle = MutableBlockStyle()
|
|
54
|
+
private var cachedBlockStyle: BlockStyle? = null
|
|
55
|
+
private var currentHeadingLevel = 0
|
|
56
|
+
|
|
57
|
+
var blockquoteDepth = 0
|
|
58
|
+
var listDepth = 0
|
|
59
|
+
var listType: ListType? = null
|
|
60
|
+
var listItemNumber = 0
|
|
61
|
+
|
|
62
|
+
// Optimization: ArrayDeque is more efficient for stack operations than MutableList
|
|
63
|
+
private val orderedListItemNumbers = ArrayDeque<Int>()
|
|
64
|
+
|
|
65
|
+
enum class ListType { UNORDERED, ORDERED }
|
|
66
|
+
|
|
67
|
+
private fun updateBlockStyle(
|
|
68
|
+
type: BlockType,
|
|
69
|
+
style: BaseBlockStyle,
|
|
70
|
+
headingLevel: Int = 0,
|
|
71
|
+
) {
|
|
72
|
+
currentBlockType = type
|
|
73
|
+
currentHeadingLevel = headingLevel
|
|
74
|
+
// Update mutable style in place - no allocation here
|
|
75
|
+
mutableBlockStyle.updateFrom(style)
|
|
76
|
+
// Invalidate cached immutable copy
|
|
77
|
+
cachedBlockStyle = null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Unified Setters
|
|
81
|
+
fun setParagraphStyle(style: ParagraphStyle) = updateBlockStyle(BlockType.PARAGRAPH, style)
|
|
82
|
+
|
|
83
|
+
fun setHeadingStyle(
|
|
84
|
+
style: HeadingStyle,
|
|
85
|
+
level: Int,
|
|
86
|
+
) = updateBlockStyle(BlockType.HEADING, style, level)
|
|
87
|
+
|
|
88
|
+
fun setBlockquoteStyle(style: BlockquoteStyle) = updateBlockStyle(BlockType.BLOCKQUOTE, style)
|
|
89
|
+
|
|
90
|
+
fun setUnorderedListStyle(style: ListStyle) {
|
|
91
|
+
listType = ListType.UNORDERED
|
|
92
|
+
updateBlockStyle(BlockType.UNORDERED_LIST, style)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fun setOrderedListStyle(style: ListStyle) {
|
|
96
|
+
listType = ListType.ORDERED
|
|
97
|
+
updateBlockStyle(BlockType.ORDERED_LIST, style)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun setCodeBlockStyle(style: CodeBlockStyle) = updateBlockStyle(BlockType.CODE_BLOCK, style)
|
|
101
|
+
|
|
102
|
+
// List State Management
|
|
103
|
+
fun isInsideBlockElement(): Boolean = blockquoteDepth > 0 || listDepth > 0
|
|
104
|
+
|
|
105
|
+
fun incrementListItemNumber() {
|
|
106
|
+
listItemNumber++
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fun resetListItemNumber() {
|
|
110
|
+
listItemNumber = 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Using ArrayDeque methods for clarity: addLast/removeLast
|
|
114
|
+
fun pushOrderedListItemNumber() {
|
|
115
|
+
orderedListItemNumbers.addLast(listItemNumber)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fun popOrderedListItemNumber() {
|
|
119
|
+
if (orderedListItemNumbers.isNotEmpty()) {
|
|
120
|
+
listItemNumber = orderedListItemNumbers.removeLast()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fun clearListStyle() {
|
|
125
|
+
// Only trigger full reset when we have completely exited all nested lists
|
|
126
|
+
if (listDepth == 0) {
|
|
127
|
+
reset()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun reset() {
|
|
132
|
+
clearBlockStyle()
|
|
133
|
+
listType = null
|
|
134
|
+
listItemNumber = 0
|
|
135
|
+
orderedListItemNumbers.clear()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun requireBlockStyle(): BlockStyle {
|
|
139
|
+
if (!mutableBlockStyle.isDirty) {
|
|
140
|
+
throw IllegalStateException(
|
|
141
|
+
"BlockStyle is null. Inline renderers must be used within a block context.",
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
// Create immutable copy only when needed, cache for reuse within same block
|
|
145
|
+
return cachedBlockStyle ?: mutableBlockStyle.toImmutable().also { cachedBlockStyle = it }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun clearBlockStyle() {
|
|
149
|
+
currentBlockType = BlockType.NONE
|
|
150
|
+
mutableBlockStyle.clear()
|
|
151
|
+
cachedBlockStyle = null
|
|
152
|
+
currentHeadingLevel = 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fun resetForNewRender() {
|
|
156
|
+
currentBlockType = BlockType.NONE
|
|
157
|
+
mutableBlockStyle.clear()
|
|
158
|
+
cachedBlockStyle = null
|
|
159
|
+
currentHeadingLevel = 0
|
|
160
|
+
blockquoteDepth = 0
|
|
161
|
+
listDepth = 0
|
|
162
|
+
listType = null
|
|
163
|
+
listItemNumber = 0
|
|
164
|
+
orderedListItemNumbers.clear()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.text.SpannableStringBuilder
|
|
4
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
5
|
+
import com.swmansion.enriched.markdown.spans.BlockquoteSpan
|
|
6
|
+
import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
|
|
7
|
+
import com.swmansion.enriched.markdown.utils.applyMarginBottom
|
|
8
|
+
import com.swmansion.enriched.markdown.utils.applyMarginTop
|
|
9
|
+
import com.swmansion.enriched.markdown.utils.createLineHeightSpan
|
|
10
|
+
|
|
11
|
+
class BlockquoteRenderer(
|
|
12
|
+
private val config: RendererConfig,
|
|
13
|
+
) : NodeRenderer {
|
|
14
|
+
override fun render(
|
|
15
|
+
node: MarkdownASTNode,
|
|
16
|
+
builder: SpannableStringBuilder,
|
|
17
|
+
onLinkPress: ((String) -> Unit)?,
|
|
18
|
+
onLinkLongPress: ((String) -> Unit)?,
|
|
19
|
+
factory: RendererFactory,
|
|
20
|
+
) {
|
|
21
|
+
val start = builder.length
|
|
22
|
+
val style = config.style.blockquoteStyle
|
|
23
|
+
val context = factory.blockStyleContext
|
|
24
|
+
val depth = context.blockquoteDepth
|
|
25
|
+
|
|
26
|
+
// Track depth to handle nested indentation levels
|
|
27
|
+
context.blockquoteDepth = depth + 1
|
|
28
|
+
context.setBlockquoteStyle(style)
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
|
|
32
|
+
} finally {
|
|
33
|
+
context.clearBlockStyle()
|
|
34
|
+
context.blockquoteDepth = depth
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (builder.length == start) return
|
|
38
|
+
val end = builder.length
|
|
39
|
+
|
|
40
|
+
// Find immediately nested quotes to exclude them from this level's line-height/margins
|
|
41
|
+
val nestedRanges =
|
|
42
|
+
builder
|
|
43
|
+
.getSpans(start, end, BlockquoteSpan::class.java)
|
|
44
|
+
.filter { it.depth == depth + 1 }
|
|
45
|
+
.map { builder.getSpanStart(it) to builder.getSpanEnd(it) }
|
|
46
|
+
.sortedBy { it.first }
|
|
47
|
+
|
|
48
|
+
// The Accent Bar Span covers the full range for visual continuity
|
|
49
|
+
builder.setSpan(
|
|
50
|
+
BlockquoteSpan(style, depth, factory.context, factory.styleCache),
|
|
51
|
+
start,
|
|
52
|
+
end,
|
|
53
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Apply styling only to segments that are NOT nested quotes
|
|
57
|
+
applySpansExcludingNested(builder, nestedRanges, start, end, createLineHeightSpan(style.lineHeight))
|
|
58
|
+
|
|
59
|
+
// Margins are only applied by the outermost (root) quote
|
|
60
|
+
if (depth == 0) {
|
|
61
|
+
applyMarginTop(builder, start, style.marginTop)
|
|
62
|
+
applyMarginBottom(builder, style.marginBottom)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun applySpansExcludingNested(
|
|
67
|
+
builder: SpannableStringBuilder,
|
|
68
|
+
nestedRanges: List<Pair<Int, Int>>,
|
|
69
|
+
start: Int,
|
|
70
|
+
end: Int,
|
|
71
|
+
span: Any,
|
|
72
|
+
) {
|
|
73
|
+
var currentPos = start
|
|
74
|
+
for ((nestedStart, nestedEnd) in nestedRanges) {
|
|
75
|
+
if (currentPos < nestedStart) {
|
|
76
|
+
builder.setSpan(span, currentPos, nestedStart, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
|
|
77
|
+
}
|
|
78
|
+
currentPos = nestedEnd
|
|
79
|
+
}
|
|
80
|
+
if (currentPos < end) {
|
|
81
|
+
builder.setSpan(span, currentPos, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|