Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix using @ symbol inside code block #2418

Merged
merged 3 commits into from Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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