Repository: Kotlin/dokka (kotlin)

Issue:
'greater than' symbol is not rendered in large code blocks
**Describe the bug**
When using three-backticks block in the documentation, sign `>` is not rendered properly:

```
 * It is also an established pattern to validate input in user's classes in the following manner:
 * ```
 * @Serializable
 * class User(val age: Int, val name: String) {
 *     init {
 *         require(age > 0) { ... }
 *         require(name.isNotBlank()) { ... }
 *     }
 * }
 * ```
 * While clearly being serialization error (when compromised data was deserialized),
 * 
```

<img width="1207" alt="image" src="https://github.com/user-attachments/assets/e1d65b08-5c06-4027-914c-0ff55ceff22c">

I also tried `&gt;`, but it is rendered literally as `&gt;` and not as `>`.

**Expected behaviour**
Sign `>` should be visible in the screenshot between `age` and `0`

**To Reproduce**
Reproducer also available here: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serialization-exception/ (source: https://github.com/Kotlin/kotlinx.serialization/blob/79ecba6e813cc0d59758c9d3f229288b6bd90e8a/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt#L27)

**Dokka configuration**
Configuration of dokka used to reproduce the bug

```Kotlin
dokka = "1.9.20"
gradlePlugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka"}
```

Pull request:
Fixes #3775

Additionally, it could improve the performance (and memory pressure) of Markdown parsing a bit, as we don't create three `Regex`s anymore on each text processing

-----

Initially, I tested that code block and thought that the problem was inside `prism.js`, so we couldn't do anything.
But after additional checks, I was wrong, and the problem was inside our smart markdown parser, which had wrong regex replacements.
Specifically, `Regex(" >+ +")` was meant to replace only block quotes, like this one:
```
 > Blockquotes are very handy in email to emulate reply text.
 > This line is part of the same quote.
```
But in reality, it was replacing all `>` signs :)

Patch:
diff --git a/dokka-subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownParser.kt b/dokka-subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownParser.kt
index b108326f8e..de944ba564 100644
--- a/dokka-subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownParser.kt
+++ b/dokka-subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownParser.kt
@@ -133,12 +133,21 @@ public open class MarkdownParser(
     private fun List<ASTNode>.evaluateChildrenWithDroppedEnclosingTokens(count: Int) =
         drop(count).dropLast(count).evaluateChildren()
 
-    private fun blockquotesHandler(node: ASTNode) =
-        DocTagsFromIElementFactory.getInstance(
+    /**
+     * Tracks if we are currently processing elements inside of [MarkdownElementTypes.BLOCK_QUOTE] element.
+     * More info on the specifics and why we need it, in the documentation of [preprocessTextElement]
+     */
+    private var isInsideBlockquote = false
+    private fun blockquotesHandler(node: ASTNode): List<DocTag> {
+        isInsideBlockquote = true
+        val result = DocTagsFromIElementFactory.getInstance(
             node.type, children = node.children
                 .filterIsInstance<CompositeASTNode>()
                 .evaluateChildren()
         )
+        isInsideBlockquote = false
+        return result
+    }
 
     private fun listsHandler(node: ASTNode): List<DocTag> {
 
@@ -281,7 +290,10 @@ public open class MarkdownParser(
 
     private fun textHandler(node: ASTNode, keepAllFormatting: Boolean) = DocTagsFromIElementFactory.getInstance(
         MarkdownTokenTypes.TEXT,
-        body = text.substring(node.startOffset, node.endOffset).transform(),
+        body = preprocessTextElement(
+            text = text.substring(node.startOffset, node.endOffset),
+            isInsideBlockquotesHandler = isInsideBlockquote
+        ),
         keepFormatting = keepAllFormatting
     )
 
@@ -524,11 +536,6 @@ public open class MarkdownParser(
         return LeafASTNode(type, startOffset, endOffset)
     }
 
-    private fun String.transform() = this
-        .replace(Regex("\n\n+"), "")        // Squashing new lines between paragraphs
-        .replace(Regex("\n"), " ")
-        .replace(Regex(" >+ +"), " ")      // Replacement used in blockquotes, get rid of garbage
-
     private fun detailedException(baseMessage: String, node: ASTNode) =
         IllegalStateException(
             baseMessage + " in ${kdocLocation ?: "unspecified location"}, element starts from offset ${node.startOffset} and ends ${node.endOffset}: ${
@@ -549,6 +556,41 @@ public open class MarkdownParser(
                 .joinToString(separator = ".")
                 .takeIf { it.isNotBlank() }
         }
+
+        // Note: Regex creation is a complex operation, and so it should be created once
+        private val blockquoteNewLineRegex = Regex("\n>+ ")
+
+        /**
+         * Performs preprocessing of the Markdown text element.
+         *
+         * First replacement: blockquotes' handling, executed if [isInsideBlockquotesHandler] is `true`
+         * Markdown parser,
+         * when handling blockquotes will handle (remove) `>` symbols only for the first line
+         * and will do nothing for further lines.
+         * So we need to do it manually, for example, when parsing the following Markdown:
+         * ```text
+         * > First line
+         * > Second line
+         * ```
+         * Text element will contain: `First line\n> Second line`.
+         *
+         * Second replacement: combining multiple lines into a single paragraph
+         * When parsing the following Markdown:
+         * ```text
+         * First line
+         * Second line
+         * ```
+         * Text element will contain: `First line\n Second line`.
+         * But it's still a single paragraph that should be concatenated into a single one.
+         *
+         * @param isInsideBlockquotesHandler `true` if the text is inside of blockquotes, and so additional blockquotes preprocessing is required.
+         */
+        private fun preprocessTextElement(text: String, isInsideBlockquotesHandler: Boolean): String {
+            // Note: we need to first do blockquote replacement so that the next lines will be concatenated into a single paragraph.
+            return when {
+                isInsideBlockquotesHandler -> text.replace(blockquoteNewLineRegex, "\n")
+                else -> text
+            }.replace('\n', ' ')
+        }
     }
 }
-


Test patch:
diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/markdown/ParserTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/ParserTest.kt
index 95b7e341f5..6c1b453afb 100644
--- a/dokka-subprojects/plugin-base/src/test/kotlin/markdown/ParserTest.kt
+++ b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/ParserTest.kt
@@ -18,7 +18,7 @@ import kotlin.test.assertTrue
 class ParserTest : KDocTest() {
 
     private fun parseMarkdownToDocNode(text: String) =
-        MarkdownParser( { null }, "").parseStringToDocNode(text)
+        MarkdownParser({ null }, "").parseStringToDocNode(text)
 
     @Test
     fun `Simple text`() {
@@ -113,6 +113,39 @@ class ParserTest : KDocTest() {
         executeTest(kdoc, expectedDocumentationNode)
     }
 
+    @Test
+    fun `CodeBlock with angle brackets`() {
+        val kdoc = """
+        | Here is the code block:
+        | ```
+        | x > y
+        | and
+        | y < x
+        | ```
+        """.trimMargin()
+
+        val expectedDocumentationNode = DocumentationNode(
+            children = listOf(
+                Description(
+                    root = CustomDocTag(
+                        children = listOf(
+                            P(listOf(Text("Here is the code block:"))),
+                            CodeBlock(
+                                children = listOf(
+                                    Text("x > y"), Br,
+                                    Text("and"), Br,
+                                    Text("y < x")
+                                )
+                            )
+                        ),
+                        name = MARKDOWN_ELEMENT_FILE_NAME
+                    )
+                )
+            )
+        )
+        executeTest(kdoc, expectedDocumentationNode)
+    }
+
     @Test
     fun `Multilined text`() {
         val kdoc = """
@@ -682,7 +715,6 @@ class ParserTest : KDocTest() {
         executeTest(kdoc, expectedDocumentationNode)
     }
 
-    @Ignore //TODO: ATX_2 to ATX_6 and sometimes ATX_1 from jetbrains parser consumes white space. Need to handle it in their library
     @Test
     fun `All headers`() {
         val kdoc = """
@@ -868,7 +900,59 @@ class ParserTest : KDocTest() {
         executeTest(kdoc, expectedDocumentationNode)
     }
 
-    @Ignore //TODO: Again ATX_1 consumes white space
+    @Test
+    fun `Blockquote right after text`() {
+        val kdoc = """
+        | text
+        | > quote
+         """.trimMargin()
+
+        val expectedDocumentationNode = DocumentationNode(
+            listOf(
+                Description(
+                    CustomDocTag(
+                        listOf(
+                            P(listOf(Text("text"))),
+                            BlockQuote(listOf(P(listOf(Text("quote")))))
+                        ), name = MARKDOWN_ELEMENT_FILE_NAME
+                    )
+                )
+            )
+        )
+        executeTest(kdoc, expectedDocumentationNode)
+    }
+
+    @Test
+    fun `Blockquote right after text inside codeblock`() {
+        val kdoc = """
+        | ```md
+        | text
+        | > quote
+        | > quote
+        | ```
+         """.trimMargin()
+
+        val expectedDocumentationNode = DocumentationNode(
+            listOf(
+                Description(
+                    CustomDocTag(
+                        listOf(
+                            CodeBlock(
+                                listOf(
+                                    Text("text"), Br,
+                                    Text("> quote"), Br,
+                                    Text("> quote")
+                                ),
+                                params = mapOf("lang" to "md")
+                            ),
+                        ), name = MARKDOWN_ELEMENT_FILE_NAME
+                    )
+                )
+            )
+        )
+        executeTest(kdoc, expectedDocumentationNode)
+    }
+
     @Test
     fun `Blockquote nested with fancy text enhancement`() {
         val kdoc = """
@@ -895,7 +979,7 @@ class ParserTest : KDocTest() {
                                         listOf(
                                             Text("text "),
                                             B(listOf(Text("1"))),
-                                            Text("\ntext 2")
+                                            Text(" text 2")
                                         )
                                     ),
                                     BlockQuote(