@@ -76,6 +76,7 @@ import ru.noties.jlatexmath.JLatexMathDrawable
7676private const val TAG = " MarkdownRenderer"
7777private const val RENDER_INTERVAL_MS = 200L // 渲染间隔 0.2 秒
7878private const val FADE_IN_DURATION_MS = 800 // 淡入动画持续时间
79+ private const val MAX_CONSECUTIVE_RENDERED_NEWLINES = 2
7980
8081internal enum class MarkdownRenderMode {
8182 STREAMING ,
@@ -84,47 +85,101 @@ internal enum class MarkdownRenderMode {
8485
8586internal 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+
87126private 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
118163private 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