Skip to content

Commit c159f16

Browse files
committed
feat: update version to 1.10.1 and improve markdown renderer line break handling
1 parent e9f1257 commit c159f16

9 files changed

Lines changed: 169 additions & 72 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ android {
5656
minSdk = 26
5757
targetSdk = 34
5858
versionCode = 41
59-
versionName = "1.10.0+15"
59+
versionName = "1.10.1"
6060

6161
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
6262
vectorDrawables {

app/src/main/java/com/ai/assistance/operit/ui/common/markdown/StreamMarkdownRenderer.kt

Lines changed: 98 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import ru.noties.jlatexmath.JLatexMathDrawable
7676
private const val TAG = "MarkdownRenderer"
7777
private const val RENDER_INTERVAL_MS = 200L // 渲染间隔 0.2 秒
7878
private const val FADE_IN_DURATION_MS = 800 // 淡入动画持续时间
79+
private const val MAX_CONSECUTIVE_RENDERED_NEWLINES = 2
7980

8081
internal enum class MarkdownRenderMode {
8182
STREAMING,
@@ -84,47 +85,101 @@ internal enum class MarkdownRenderMode {
8485

8586
internal val LocalMarkdownRenderMode = compositionLocalOf { MarkdownRenderMode.STATIC }
8687

88+
private data class PendingLineBreakState(
89+
val count: Int = 0,
90+
val lastWasCarriageReturn: Boolean = false,
91+
)
92+
93+
private fun appendPendingLineBreaks(
94+
parentNode: MarkdownNode,
95+
childNode: MarkdownNode,
96+
count: Int,
97+
) {
98+
if (parentNode.content.isEmpty()) {
99+
return
100+
}
101+
102+
repeat(count.coerceIn(0, MAX_CONSECUTIVE_RENDERED_NEWLINES)) {
103+
parentNode.content + '\n'
104+
childNode.content + '\n'
105+
}
106+
}
107+
108+
private fun accumulatePendingLineBreak(
109+
state: PendingLineBreakState,
110+
char: Char,
111+
): PendingLineBreakState {
112+
val normalizedCount = state.count.coerceIn(0, MAX_CONSECUTIVE_RENDERED_NEWLINES)
113+
if (char == '\n' && state.lastWasCarriageReturn && normalizedCount > 0) {
114+
return PendingLineBreakState(
115+
count = normalizedCount,
116+
lastWasCarriageReturn = false,
117+
)
118+
}
119+
120+
return PendingLineBreakState(
121+
count = (normalizedCount + 1).coerceAtMost(MAX_CONSECUTIVE_RENDERED_NEWLINES),
122+
lastWasCarriageReturn = char == '\r',
123+
)
124+
}
125+
87126
private fun appendInlineChunk(
88127
parentNode: MarkdownNode,
89128
getOrCreateChildNode: () -> MarkdownNode,
90129
chunk: String,
91-
lastCharWasNewline: Boolean,
92-
): Boolean {
93-
var pendingNewline = lastCharWasNewline
130+
pendingLineBreakState: PendingLineBreakState,
131+
): PendingLineBreakState {
132+
var state =
133+
PendingLineBreakState(
134+
count = pendingLineBreakState.count.coerceIn(0, MAX_CONSECUTIVE_RENDERED_NEWLINES),
135+
lastWasCarriageReturn =
136+
pendingLineBreakState.lastWasCarriageReturn && pendingLineBreakState.count > 0,
137+
)
94138

95139
for (char in chunk) {
96140
val isCurrentCharNewline = char == '\n' || char == '\r'
97141
if (isCurrentCharNewline) {
98-
pendingNewline = true
142+
state = accumulatePendingLineBreak(state, char)
99143
continue
100144
}
101145

102146
val childNode = getOrCreateChildNode()
103-
if (pendingNewline) {
104-
if (parentNode.content.isNotEmpty()) {
105-
parentNode.content + '\n'
106-
childNode.content + '\n'
107-
}
108-
pendingNewline = false
147+
if (state.count > 0) {
148+
appendPendingLineBreaks(
149+
parentNode = parentNode,
150+
childNode = childNode,
151+
count = state.count,
152+
)
153+
state = PendingLineBreakState()
109154
}
110155

111156
parentNode.content + char
112157
childNode.content + char
113158
}
114159

115-
return pendingNewline
160+
return state
116161
}
117162

118163
private fun canMergeWithHtmlBreak(node: MarkdownNode?): Boolean {
119164
return node?.type == MarkdownProcessorType.PLAIN_TEXT
120165
}
121166

122-
private fun appendHtmlBreakNode(nodes: SnapshotStateList<MarkdownNode>) {
123-
nodes.add(MarkdownNode(type = MarkdownProcessorType.HTML_BREAK, initialContent = "\n"))
167+
private fun appendHtmlBreakNode(
168+
nodes: SnapshotStateList<MarkdownNode>,
169+
count: Int = 1,
170+
) {
171+
repeat(count.coerceIn(0, MAX_CONSECUTIVE_RENDERED_NEWLINES)) {
172+
nodes.add(MarkdownNode(type = MarkdownProcessorType.HTML_BREAK, initialContent = "\n"))
173+
}
124174
}
125175

126-
private fun appendHtmlBreakNode(nodes: MutableList<MarkdownNode>) {
127-
nodes.add(MarkdownNode(type = MarkdownProcessorType.HTML_BREAK, initialContent = "\n"))
176+
private fun appendHtmlBreakNode(
177+
nodes: MutableList<MarkdownNode>,
178+
count: Int = 1,
179+
) {
180+
repeat(count.coerceIn(0, MAX_CONSECUTIVE_RENDERED_NEWLINES)) {
181+
nodes.add(MarkdownNode(type = MarkdownProcessorType.HTML_BREAK, initialContent = "\n"))
182+
}
128183
}
129184

130185
/**
@@ -380,25 +435,26 @@ fun StreamMarkdownRenderer(
380435
rendererState.streamParsingCompletedSuccessfully = false
381436

382437
try {
383-
var pendingHtmlBreak = false
438+
var pendingHtmlBreakCount = 0
384439
interceptedStream.nativeMarkdownSplitByBlock(flushIntervalMs = RENDER_INTERVAL_MS).collect { blockGroup ->
385440
val blockType = blockGroup.tag ?: MarkdownProcessorType.PLAIN_TEXT
386441

387442
if (blockType == MarkdownProcessorType.HTML_BREAK) {
388443
if (canMergeWithHtmlBreak(nodes.lastOrNull())) {
389-
pendingHtmlBreak = true
444+
pendingHtmlBreakCount =
445+
(pendingHtmlBreakCount + 1).coerceAtMost(MAX_CONSECUTIVE_RENDERED_NEWLINES)
390446
} else {
391447
appendHtmlBreakNode(nodes)
392448
}
393449
return@collect
394450
}
395451

396452
if (blockType == MarkdownProcessorType.HORIZONTAL_RULE) {
397-
if (pendingHtmlBreak) {
398-
appendHtmlBreakNode(nodes)
453+
if (pendingHtmlBreakCount > 0) {
454+
appendHtmlBreakNode(nodes, pendingHtmlBreakCount)
399455
}
400456
nodes.add(MarkdownNode(type = blockType, initialContent = "---"))
401-
pendingHtmlBreak = false
457+
pendingHtmlBreakCount = 0
402458
return@collect
403459
}
404460

@@ -413,13 +469,13 @@ fun StreamMarkdownRenderer(
413469
tempBlockType != MarkdownProcessorType.XML_BLOCK
414470

415471
val mergeWithPrevious =
416-
pendingHtmlBreak &&
472+
pendingHtmlBreakCount > 0 &&
417473
tempBlockType == MarkdownProcessorType.PLAIN_TEXT &&
418474
canMergeWithHtmlBreak(nodes.lastOrNull())
419475

420-
if (pendingHtmlBreak && !mergeWithPrevious) {
421-
appendHtmlBreakNode(nodes)
422-
pendingHtmlBreak = false
476+
if (pendingHtmlBreakCount > 0 && !mergeWithPrevious) {
477+
appendHtmlBreakNode(nodes, pendingHtmlBreakCount)
478+
pendingHtmlBreakCount = 0
423479
}
424480

425481
val newNode =
@@ -441,7 +497,7 @@ fun StreamMarkdownRenderer(
441497
}
442498

443499
if (isInlineContainer) {
444-
var lastCharWasNewline = pendingHtmlBreak
500+
var pendingLineBreakState = PendingLineBreakState(count = pendingHtmlBreakCount)
445501

446502
blockStream.nativeMarkdownSplitByInline(flushIntervalMs = RENDER_INTERVAL_MS).collect { inlineGroup ->
447503
val originalInlineType = inlineGroup.tag ?: MarkdownProcessorType.PLAIN_TEXT
@@ -452,7 +508,7 @@ fun StreamMarkdownRenderer(
452508
var childNode: MarkdownNode? = null
453509

454510
inlineGroup.stream.collect { chunk ->
455-
lastCharWasNewline =
511+
pendingLineBreakState =
456512
appendInlineChunk(
457513
parentNode = newNode,
458514
getOrCreateChildNode = {
@@ -471,7 +527,7 @@ fun StreamMarkdownRenderer(
471527
}
472528
},
473529
chunk = chunk,
474-
lastCharWasNewline = lastCharWasNewline,
530+
pendingLineBreakState = pendingLineBreakState,
475531
)
476532
}
477533

@@ -508,7 +564,7 @@ fun StreamMarkdownRenderer(
508564
nodes[nodeIndex] = latexNode
509565
}
510566

511-
pendingHtmlBreak = false
567+
pendingHtmlBreakCount = 0
512568
}
513569
rendererState.streamParsingCompletedSuccessfully = true
514570
} catch (e: CancellationException) {
@@ -681,25 +737,26 @@ fun StreamMarkdownRenderer(
681737
launch(Dispatchers.IO) {
682738
try {
683739
val parsedNodes = mutableListOf<MarkdownNode>()
684-
var pendingHtmlBreak = false
740+
var pendingHtmlBreakCount = 0
685741
stream { emit(content) }.nativeMarkdownSplitByBlock().collect { blockGroup ->
686742
val blockType = blockGroup.tag ?: MarkdownProcessorType.PLAIN_TEXT
687743

688744
if (blockType == MarkdownProcessorType.HTML_BREAK) {
689745
if (canMergeWithHtmlBreak(parsedNodes.lastOrNull())) {
690-
pendingHtmlBreak = true
746+
pendingHtmlBreakCount =
747+
(pendingHtmlBreakCount + 1).coerceAtMost(MAX_CONSECUTIVE_RENDERED_NEWLINES)
691748
} else {
692749
appendHtmlBreakNode(parsedNodes)
693750
}
694751
return@collect
695752
}
696753

697754
if (blockType == MarkdownProcessorType.HORIZONTAL_RULE) {
698-
if (pendingHtmlBreak) {
699-
appendHtmlBreakNode(parsedNodes)
755+
if (pendingHtmlBreakCount > 0) {
756+
appendHtmlBreakNode(parsedNodes, pendingHtmlBreakCount)
700757
}
701758
parsedNodes.add(MarkdownNode(type = blockType, initialContent = "---"))
702-
pendingHtmlBreak = false
759+
pendingHtmlBreakCount = 0
703760
return@collect
704761
}
705762

@@ -714,13 +771,13 @@ fun StreamMarkdownRenderer(
714771
tempBlockType != MarkdownProcessorType.XML_BLOCK
715772

716773
val mergeWithPrevious =
717-
pendingHtmlBreak &&
774+
pendingHtmlBreakCount > 0 &&
718775
tempBlockType == MarkdownProcessorType.PLAIN_TEXT &&
719776
canMergeWithHtmlBreak(parsedNodes.lastOrNull())
720777

721-
if (pendingHtmlBreak && !mergeWithPrevious) {
722-
appendHtmlBreakNode(parsedNodes)
723-
pendingHtmlBreak = false
778+
if (pendingHtmlBreakCount > 0 && !mergeWithPrevious) {
779+
appendHtmlBreakNode(parsedNodes, pendingHtmlBreakCount)
780+
pendingHtmlBreakCount = 0
724781
}
725782

726783
val newNode =
@@ -738,7 +795,7 @@ fun StreamMarkdownRenderer(
738795
}
739796
val blockText = blockTextBuilder.toString()
740797

741-
var lastCharWasNewline = pendingHtmlBreak
798+
var pendingLineBreakState = PendingLineBreakState(count = pendingHtmlBreakCount)
742799

743800
stream { emit(blockText) }.nativeMarkdownSplitByInline().collect { inlineGroup ->
744801
val originalInlineType = inlineGroup.tag ?: MarkdownProcessorType.PLAIN_TEXT
@@ -749,7 +806,7 @@ fun StreamMarkdownRenderer(
749806
var childNode: MarkdownNode? = null
750807

751808
inlineGroup.stream.collect { chunk ->
752-
lastCharWasNewline =
809+
pendingLineBreakState =
753810
appendInlineChunk(
754811
parentNode = newNode,
755812
getOrCreateChildNode = {
@@ -768,7 +825,7 @@ fun StreamMarkdownRenderer(
768825
}
769826
},
770827
chunk = chunk,
771-
lastCharWasNewline = lastCharWasNewline,
828+
pendingLineBreakState = pendingLineBreakState,
772829
)
773830
}
774831

@@ -805,7 +862,7 @@ fun StreamMarkdownRenderer(
805862
parsedNodes[nodeIndex] = latexNode
806863
}
807864

808-
pendingHtmlBreak = false
865+
pendingHtmlBreakCount = 0
809866
}
810867

811868
// 将解析完成的节点添加到节点列表,并更新动画状态

0 commit comments

Comments
 (0)