Skip to content

Commit

Permalink
Fix using @ symbol inside code block (#2418)
Browse files Browse the repository at this point in the history
  • Loading branch information
vmishenev committed Apr 11, 2022
1 parent 2f0a259 commit c59d6ef
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 9 deletions.
81 changes: 77 additions & 4 deletions plugins/base/src/main/kotlin/parsers/Parser.kt
@@ -1,6 +1,8 @@
package org.jetbrains.dokka.base.parsers

import org.jetbrains.dokka.model.doc.*
import org.jetbrains.dokka.model.doc.Deprecated
import org.jetbrains.dokka.model.doc.Suppress

abstract class Parser {

Expand All @@ -9,7 +11,7 @@ abstract class Parser {
abstract fun preparse(text: String): String

open fun parse(text: String): DocumentationNode =
DocumentationNode(jkdocToListOfPairs(preparse(text)).map { (tag, content) -> parseTagWithBody(tag, content) })
DocumentationNode(extractTagsToListOfPairs(preparse(text)).map { (tag, content) -> parseTagWithBody(tag, content) })

open fun parseTagWithBody(tagName: String, content: String): TagWrapper =
when (tagName) {
Expand Down Expand Up @@ -47,12 +49,83 @@ abstract class Parser {
else -> CustomTagWrapper(parseStringToDocNode(content), tagName)
}

private fun jkdocToListOfPairs(javadoc: String): List<Pair<String, String>> =
"description $javadoc"
.split("\n@")
/**
* KDoc parser from Kotlin compiler relies on a comment asterisk
* So there is a mini parser here
* TODO: at least to adapt [org.jetbrains.kotlin.kdoc.lexer.KDocLexer] to analyze KDoc without the asterisks and use it here
*/
private fun extractTagsToListOfPairs(text: String): List<Pair<String, String>> =
"description $text"
.extractKDocSections()
.map { content ->
val contentWithEscapedAts = content.replace("\\@", "@")
val (tag, body) = contentWithEscapedAts.split(" ", limit = 2)
tag to body
}

/**
* Ignore a doc tag inside code spans and blocks
* @see org.jetbrains.kotlin.kdoc.psi.impl.KDocSection
*/
private fun CharSequence.extractKDocSections(delimiter: String = "\n@"): List<String> {
var countOfBackticks = 0
var countOfTildes = 0
var countOfBackticksInOpeningFence = 0
var countOfTildesInOpeningFence = 0

var isInCode = false
val result = mutableListOf<String>()
var rangeStart = 0
var rangeEnd = 0
var currentOffset = 0
while (currentOffset < length) {

when (get(currentOffset)) {
'`' -> {
countOfBackticks++
countOfTildes = 0
}
'~' -> {
countOfTildes++
countOfBackticks = 0
}
else -> {
if (isInCode) {
// The closing code fence must be at least as long as the opening fence
if(countOfBackticks >= countOfBackticksInOpeningFence
|| countOfTildes >= countOfTildesInOpeningFence)
isInCode = false
} else {
// as per CommonMark spec, there can be any number of backticks for a code span, not only one or three
if (countOfBackticks > 0) {
isInCode = true
countOfBackticksInOpeningFence = countOfBackticks
countOfTildesInOpeningFence = Int.MAX_VALUE
}
// tildes are only for a code block, not code span
if (countOfTildes >= 3) {
isInCode = true
countOfTildesInOpeningFence = countOfTildes
countOfBackticksInOpeningFence = Int.MAX_VALUE
}
}
countOfTildes = 0
countOfBackticks = 0
}
}
if (!isInCode && startsWith(delimiter, currentOffset)) {
result.add(substring(rangeStart, rangeEnd))
currentOffset += delimiter.length
rangeStart = currentOffset
rangeEnd = currentOffset
continue
}

++rangeEnd
++currentOffset
}
result.add(substring(rangeStart, rangeEnd))
return result
}

}
2 changes: 2 additions & 0 deletions plugins/base/src/test/kotlin/markdown/ParserTest.kt
Expand Up @@ -951,6 +951,7 @@ class ParserTest : KDocTest() {
fun `Multilined Code Block`() {
val kdoc = """
| ```kotlin
| @Suppress("UNUSED_VARIABLE")
| val x: Int = 0
| val y: String = "Text"
|
Expand All @@ -968,6 +969,7 @@ class ParserTest : KDocTest() {
listOf(
CodeBlock(
listOf(
Text("@Suppress(\"UNUSED_VARIABLE\")"), Br,
Text("val x: Int = 0"), Br,
Text("val y: String = \"Text\""), Br, Br,
Text(" val z: Boolean = true"), Br,
Expand Down
@@ -1,12 +1,11 @@
package parsers

import org.jetbrains.dokka.base.parsers.moduleAndPackage.IllegalModuleAndPackageDocumentation
import org.intellij.markdown.MarkdownElementTypes
import org.jetbrains.dokka.base.parsers.moduleAndPackage.*
import org.jetbrains.dokka.base.parsers.moduleAndPackage.ModuleAndPackageDocumentation.Classifier.Module
import org.jetbrains.dokka.base.parsers.moduleAndPackage.ModuleAndPackageDocumentation.Classifier.Package
import org.jetbrains.dokka.base.parsers.moduleAndPackage.ModuleAndPackageDocumentationFile
import org.jetbrains.dokka.base.parsers.moduleAndPackage.ModuleAndPackageDocumentationFragment
import org.jetbrains.dokka.base.parsers.moduleAndPackage.ModuleAndPackageDocumentationSource
import org.jetbrains.dokka.base.parsers.moduleAndPackage.parseModuleAndPackageDocumentationFragments
import org.jetbrains.dokka.model.doc.*
import org.jetbrains.dokka.utilities.DokkaLogger
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -216,6 +215,64 @@ class ParseModuleAndPackageDocumentationFragmentsTest {
)
}

@Test
fun `at in code block is supported`() {
val fragment = parseModuleAndPackageDocumentationFragments(
source(
"""
# Module My Module
```
@Smth
```
@author Smb
""".trimIndent()
)
)

assertEquals(
"```\n" +
"@Smth\n" +
"```\n" +
"@author Smb", fragment.single().documentation,
"Expected documentation being available"
)

val parsingContext = ModuleAndPackageDocumentationParsingContext(object : DokkaLogger {
override var warningsCount: Int = 0
override var errorsCount: Int = 0
override fun debug(message: String) {}
override fun info(message: String) {}
override fun progress(message: String) {}
override fun warn(message: String) {}
override fun error(message: String) {}
})
val parsedFragment = parseModuleAndPackageDocumentation(parsingContext, fragment.single())
val expectedDocumentationNode = DocumentationNode(
listOf(
Description(
CustomDocTag(
listOf(
CodeBlock(
listOf(
Text("@Smth")
)
)
), name = MarkdownElementTypes.MARKDOWN_FILE.name
)
),
Author(
CustomDocTag(
listOf(
P(listOf(Text("Smb")))
), name = MarkdownElementTypes.MARKDOWN_FILE.name
)
)
)
)
assertEquals(
expectedDocumentationNode, parsedFragment.documentation
)
}

private fun source(documentation: String): ModuleAndPackageDocumentationSource =
object : ModuleAndPackageDocumentationSource() {
Expand Down

0 comments on commit c59d6ef

Please sign in to comment.