aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/src
diff options
context:
space:
mode:
authorMarcin Aman <marcin.aman@gmail.com>2020-10-30 19:01:09 +0100
committerGitHub <noreply@github.com>2020-10-30 19:01:09 +0100
commit1aba0ec4973d7915caa93f1e9b3146ad82111903 (patch)
treeb9424de4bc22f8453ecb32aaa8f7f020bcd49e9f /plugins/base/src
parentda498f50eabfad8969eb7795a535e97f7e25ca58 (diff)
downloaddokka-1aba0ec4973d7915caa93f1e9b3146ad82111903.tar.gz
dokka-1aba0ec4973d7915caa93f1e9b3146ad82111903.tar.bz2
dokka-1aba0ec4973d7915caa93f1e9b3146ad82111903.zip
Fix parsing first word in deprecated (#1595)
Fix parsing first word in `Deprecated` annotations, fix `Throws` and `See` tags
Diffstat (limited to 'plugins/base/src')
-rw-r--r--plugins/base/src/main/kotlin/parsers/HtmlParser.kt89
-rw-r--r--plugins/base/src/main/kotlin/parsers/MarkdownParser.kt61
-rw-r--r--plugins/base/src/main/kotlin/parsers/Parser.kt77
-rw-r--r--plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt55
-rw-r--r--plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt8
-rw-r--r--plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt33
-rw-r--r--plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt11
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt193
-rw-r--r--plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt690
-rw-r--r--plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt10
-rw-r--r--plugins/base/src/test/kotlin/content/signatures/SkippingParenthesisForConstructorsTest.kt24
-rw-r--r--plugins/base/src/test/kotlin/utils/contentUtils.kt16
12 files changed, 1039 insertions, 228 deletions
diff --git a/plugins/base/src/main/kotlin/parsers/HtmlParser.kt b/plugins/base/src/main/kotlin/parsers/HtmlParser.kt
deleted file mode 100644
index ece3cf24..00000000
--- a/plugins/base/src/main/kotlin/parsers/HtmlParser.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package org.jetbrains.dokka.base.parsers
-
-import org.jetbrains.dokka.model.doc.*
-import org.jetbrains.dokka.base.parsers.factories.DocTagsFromStringFactory
-import org.jsoup.Jsoup
-import org.jsoup.nodes.Node
-import org.jsoup.select.NodeFilter
-import org.jsoup.select.NodeTraversor
-
-class HtmlParser : Parser() {
-
- inner class NodeFilterImpl : NodeFilter {
-
- private val nodesCache: MutableMap<Int, MutableList<DocTag>> = mutableMapOf()
- private var currentDepth = 0
-
- fun collect(): DocTag = nodesCache[currentDepth]!![0]
-
- override fun tail(node: Node?, depth: Int): NodeFilter.FilterResult {
- val nodeName = node!!.nodeName()
- val nodeAttributes = node.attributes()
-
- if(nodeName in listOf("#document", "html", "head"))
- return NodeFilter.FilterResult.CONTINUE
-
- val body: String
- val params: Map<String, String>
-
-
- if(nodeName != "#text") {
- body = ""
- params = nodeAttributes.map { it.key to it.value }.toMap()
- } else {
- body = nodeAttributes["#text"]
- params = emptyMap()
- }
-
- val docNode = if(depth < currentDepth) {
- DocTagsFromStringFactory.getInstance(nodeName, nodesCache.getOrDefault(currentDepth, mutableListOf()).toList(), params, body).also {
- nodesCache[currentDepth] = mutableListOf()
- currentDepth = depth
- }
- } else {
- DocTagsFromStringFactory.getInstance(nodeName, emptyList(), params, body)
- }
-
- nodesCache.getOrDefault(depth, mutableListOf()) += docNode
- return NodeFilter.FilterResult.CONTINUE
- }
-
- override fun head(node: Node?, depth: Int): NodeFilter.FilterResult {
-
- val nodeName = node!!.nodeName()
-
- if(currentDepth < depth) {
- currentDepth = depth
- nodesCache[currentDepth] = mutableListOf()
- }
-
- if(nodeName in listOf("#document", "html", "head"))
- return NodeFilter.FilterResult.CONTINUE
-
- return NodeFilter.FilterResult.CONTINUE
- }
- }
-
-
- private fun htmlToDocNode(string: String): DocTag {
- val document = Jsoup.parse(string)
- val nodeFilterImpl = NodeFilterImpl()
- NodeTraversor.filter(nodeFilterImpl, document.root())
- return nodeFilterImpl.collect()
- }
-
- private fun replaceLinksWithHrefs(javadoc: String): String = Regex("\\{@link .*?}").replace(javadoc) {
- val split = it.value.dropLast(1).split(" ")
- if(split.size !in listOf(2, 3))
- return@replace it.value
- if(split.size == 3)
- return@replace "<documentationlink href=\"${split[1]}\">${split[2]}</documentationlink>"
- else
- return@replace "<documentationlink href=\"${split[1]}\">${split[1]}</documentationlink>"
- }
-
- override fun parseStringToDocNode(extractedString: String) = htmlToDocNode(extractedString)
- override fun preparse(text: String) = replaceLinksWithHrefs(text)
-}
-
-
diff --git a/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt
index e7a36d3f..f496d704 100644
--- a/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt
+++ b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt
@@ -38,6 +38,27 @@ open class MarkdownParser(
override fun preparse(text: String) = text
+ override fun parseTagWithBody(tagName: String, content: String): TagWrapper =
+ when (tagName) {
+ "see" -> {
+ val referencedName = content.substringBefore(' ')
+ See(
+ parseStringToDocNode(content.substringAfter(' ')),
+ referencedName,
+ externalDri(referencedName)
+ )
+ }
+ "throws", "exception" -> {
+ val dri = externalDri(content.substringBefore(' '))
+ Throws(
+ parseStringToDocNode(content.substringAfter(' ')),
+ dri?.fqName() ?: content.substringBefore(' '),
+ dri
+ )
+ }
+ else -> super.parseTagWithBody(tagName, content)
+ }
+
private fun headersHandler(node: ASTNode) =
DocTagsFromIElementFactory.getInstance(
node.type,
@@ -424,6 +445,12 @@ open class MarkdownParser(
DocumentationNode(emptyList())
} else {
fun parseStringToDocNode(text: String) = MarkdownParser(externalDri).parseStringToDocNode(text)
+
+ fun pointedLink(tag: KDocTag): DRI? = (parseStringToDocNode("[${tag.getSubjectName()}]")).let {
+ val link = it.children[0].children[0]
+ if (link is DocumentationLink) link.dri else null
+ }
+
DocumentationNode(
(listOf(kDocTag) + getAllKDocTags(findParent(kDocTag))).map {
when (it.knownTag) {
@@ -432,14 +459,22 @@ open class MarkdownParser(
it.name!!
)
KDocKnownTag.AUTHOR -> Author(parseStringToDocNode(it.getContent()))
- KDocKnownTag.THROWS -> Throws(
- parseStringToDocNode(it.getContent()),
- it.getSubjectName().orEmpty()
- )
- KDocKnownTag.EXCEPTION -> Throws(
- parseStringToDocNode(it.getContent()),
- it.getSubjectName().orEmpty()
- )
+ KDocKnownTag.THROWS -> {
+ val dri = pointedLink(it)
+ Throws(
+ parseStringToDocNode(it.getContent()),
+ dri?.fqName() ?: it.getSubjectName().orEmpty(),
+ dri,
+ )
+ }
+ KDocKnownTag.EXCEPTION -> {
+ val dri = pointedLink(it)
+ Throws(
+ parseStringToDocNode(it.getContent()),
+ dri?.fqName() ?: it.getSubjectName().orEmpty(),
+ dri
+ )
+ }
KDocKnownTag.PARAM -> Param(
parseStringToDocNode(it.getContent()),
it.getSubjectName().orEmpty()
@@ -449,12 +484,7 @@ open class MarkdownParser(
KDocKnownTag.SEE -> See(
parseStringToDocNode(it.getContent()),
it.getSubjectName().orEmpty(),
- (parseStringToDocNode("[${it.getSubjectName()}]"))
- .let {
- val link = it.children[0].children[0]
- if (link is DocumentationLink) link.dri
- else null
- }
+ pointedLink(it),
)
KDocKnownTag.SINCE -> Since(parseStringToDocNode(it.getContent()))
KDocKnownTag.CONSTRUCTOR -> Constructor(parseStringToDocNode(it.getContent()))
@@ -473,6 +503,9 @@ open class MarkdownParser(
}
}
+ //Horrible hack but since link resolution is passed as a function i am not able to resolve them otherwise
+ fun DRI.fqName(): String = "$packageName.$classNames"
+
private fun findParent(kDoc: PsiElement): PsiElement =
if (kDoc is KDocSection) findParent(kDoc.parent) else kDoc
diff --git a/plugins/base/src/main/kotlin/parsers/Parser.kt b/plugins/base/src/main/kotlin/parsers/Parser.kt
index 228cc88b..960d7a64 100644
--- a/plugins/base/src/main/kotlin/parsers/Parser.kt
+++ b/plugins/base/src/main/kotlin/parsers/Parser.kt
@@ -8,47 +8,44 @@ abstract class Parser {
abstract fun preparse(text: String): String
- fun parse(text: String): DocumentationNode {
-
- val list = jkdocToListOfPairs(preparse(text))
-
- val mappedList: List<TagWrapper> = list.map {
- when (it.first) {
- "description" -> Description(parseStringToDocNode(it.second))
- "author" -> Author(parseStringToDocNode(it.second))
- "version" -> Version(parseStringToDocNode(it.second))
- "since" -> Since(parseStringToDocNode(it.second))
- "see" -> See(
- parseStringToDocNode(it.second.substringAfter(' ')),
- it.second.substringBefore(' '),
- null
- )
- "param" -> Param(
- parseStringToDocNode(it.second.substringAfter(' ')),
- it.second.substringBefore(' ')
- )
- "property" -> Property(
- parseStringToDocNode(it.second.substringAfter(' ')),
- it.second.substringBefore(' ')
- )
- "return" -> Return(parseStringToDocNode(it.second))
- "constructor" -> Constructor(parseStringToDocNode(it.second))
- "receiver" -> Receiver(parseStringToDocNode(it.second))
- "throws", "exception" -> Throws(
- parseStringToDocNode(it.second.substringAfter(' ')),
- it.second.substringBefore(' ')
- )
- "deprecated" -> Deprecated(parseStringToDocNode(it.second))
- "sample" -> Sample(
- parseStringToDocNode(it.second.substringAfter(' ')),
- it.second.substringBefore(' ')
- )
- "suppress" -> Suppress(parseStringToDocNode(it.second))
- else -> CustomTagWrapper(parseStringToDocNode(it.second), it.first)
- }
+ open fun parse(text: String): DocumentationNode =
+ DocumentationNode(jkdocToListOfPairs(preparse(text)).map { (tag, content) -> parseTagWithBody(tag, content) })
+
+ open fun parseTagWithBody(tagName: String, content: String): TagWrapper =
+ when (tagName) {
+ "description" -> Description(parseStringToDocNode(content))
+ "author" -> Author(parseStringToDocNode(content))
+ "version" -> Version(parseStringToDocNode(content))
+ "since" -> Since(parseStringToDocNode(content))
+ "see" -> See(
+ parseStringToDocNode(content.substringAfter(' ')),
+ content.substringBefore(' '),
+ null
+ )
+ "param" -> Param(
+ parseStringToDocNode(content.substringAfter(' ')),
+ content.substringBefore(' ')
+ )
+ "property" -> Property(
+ parseStringToDocNode(content.substringAfter(' ')),
+ content.substringBefore(' ')
+ )
+ "return" -> Return(parseStringToDocNode(content))
+ "constructor" -> Constructor(parseStringToDocNode(content))
+ "receiver" -> Receiver(parseStringToDocNode(content))
+ "throws", "exception" -> Throws(
+ parseStringToDocNode(content.substringAfter(' ')),
+ content.substringBefore(' '),
+ null
+ )
+ "deprecated" -> Deprecated(parseStringToDocNode(content))
+ "sample" -> Sample(
+ parseStringToDocNode(content.substringAfter(' ')),
+ content.substringBefore(' ')
+ )
+ "suppress" -> Suppress(parseStringToDocNode(content))
+ else -> CustomTagWrapper(parseStringToDocNode(content), tagName)
}
- return DocumentationNode(mappedList)
- }
private fun jkdocToListOfPairs(javadoc: String): List<Pair<String, String>> =
"description $javadoc"
diff --git a/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt b/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt
index d05979e3..a3a9ad6a 100644
--- a/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt
+++ b/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt
@@ -6,6 +6,7 @@ import org.jetbrains.dokka.model.doc.*
import org.jetbrains.dokka.model.properties.PropertyContainer
import org.jetbrains.dokka.model.toDisplaySourceSets
import org.jetbrains.dokka.pages.*
+import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
open class DocTagToContentConverter : CommentsToContentConverter {
override fun buildContent(
@@ -148,15 +149,42 @@ open class DocTagToContentConverter : CommentsToContentConverter {
)
)
is Strikethrough -> buildChildren(docTag, setOf(TextStyle.Strikethrough))
- is Table -> listOf(
- ContentTable(
- buildTableRows(docTag.children.filterIsInstance<Th>(), CommentTable),
- buildTableRows(docTag.children.filterIsInstance<Tr>(), CommentTable),
- dci,
- sourceSets.toDisplaySourceSets(),
- styles + CommentTable
- )
- )
+ is Table -> {
+ //https://html.spec.whatwg.org/multipage/tables.html#the-caption-element
+ if (docTag.children.any { it is TBody }) {
+ val head = docTag.children.filterIsInstance<THead>().flatMap { it.children }
+ val body = docTag.children.filterIsInstance<TBody>().flatMap { it.children }
+ listOf(
+ ContentTable(
+ header = buildTableRows(head.filterIsInstance<Th>(), CommentTable),
+ caption = docTag.children.firstIsInstanceOrNull<Caption>()?.let {
+ ContentGroup(
+ buildContent(it, dci, sourceSets),
+ dci,
+ sourceSets.toDisplaySourceSets(),
+ styles,
+ extra
+ )
+ },
+ buildTableRows(body.filterIsInstance<Tr>(), CommentTable),
+ dci,
+ sourceSets.toDisplaySourceSets(),
+ styles + CommentTable
+ )
+ )
+ } else {
+ listOf(
+ ContentTable(
+ header = buildTableRows(docTag.children.filterIsInstance<Th>(), CommentTable),
+ caption = null,
+ buildTableRows(docTag.children.filterIsInstance<Tr>(), CommentTable),
+ dci,
+ sourceSets.toDisplaySourceSets(),
+ styles + CommentTable
+ )
+ )
+ }
+ }
is Th,
is Tr -> listOf(
ContentGroup(
@@ -190,6 +218,15 @@ open class DocTagToContentConverter : CommentsToContentConverter {
} else {
buildChildren(docTag)
}
+ is Caption -> listOf(
+ ContentGroup(
+ buildChildren(docTag),
+ dci,
+ sourceSets.toDisplaySourceSets(),
+ styles + ContentStyle.Caption,
+ extra = extra
+ )
+ )
else -> buildChildren(docTag)
}
diff --git a/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt
index 069d1125..2c6d301c 100644
--- a/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt
+++ b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt
@@ -62,14 +62,14 @@ class SourceLinksTransformer(val context: DokkaContext, val builder: PageContent
) {
header(2, "Sources", kind = ContentKind.Source)
+ContentTable(
- emptyList(),
- sources.map {
+ header = emptyList(),
+ children = sources.map {
buildGroup(node.dri, setOf(it.first), kind = ContentKind.Source, extra = mainExtra + SymbolAnchorHint(it.second, ContentKind.Source)) {
link("(source)", it.second)
}
},
- DCI(node.dri, ContentKind.Source),
- node.documentable!!.sourceSets.toDisplaySourceSets(),
+ dci = DCI(node.dri, ContentKind.Source),
+ sourceSets = node.documentable!!.sourceSets.toDisplaySourceSets(),
style = emptySet(),
extra = mainExtra + SimpleAttr.header("Sources")
)
diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt
index b2a9d5d2..e0ced0aa 100644
--- a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt
+++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt
@@ -179,10 +179,10 @@ open class DefaultPageCreator(
if (map.values.any()) {
header(2, "Inheritors") { }
+ContentTable(
- listOf(contentBuilder.contentFor(mainDRI, mainSourcesetData) {
+ header = listOf(contentBuilder.contentFor(mainDRI, mainSourcesetData) {
text("Name")
}),
- map.entries.flatMap { entry -> entry.value.map { Pair(entry.key, it) } }
+ children = map.entries.flatMap { entry -> entry.value.map { Pair(entry.key, it) } }
.groupBy({ it.second }, { it.first }).map { (classlike, platforms) ->
val label = classlike.classNames?.substringBeforeLast(".") ?: classlike.toString()
.also { logger.warn("No class name found for DRI $classlike") }
@@ -190,8 +190,8 @@ open class DefaultPageCreator(
link(label, classlike)
}
},
- DCI(setOf(dri), ContentKind.Inheritors),
- sourceSets.toDisplaySourceSets(),
+ dci = DCI(setOf(dri), ContentKind.Inheritors),
+ sourceSets = sourceSets.toDisplaySourceSets(),
style = emptySet(),
extra = mainExtra + SimpleAttr.header("Inheritors")
)
@@ -429,6 +429,30 @@ open class DefaultPageCreator(
}
}
}
+ fun DocumentableContentBuilder.contentForThrows() {
+ val throws = tags.withTypeNamed<Throws>()
+ if (throws.isNotEmpty()) {
+ header(4, "Throws")
+ sourceSetDependentHint(sourceSets = platforms.toSet(), kind = ContentKind.SourceSetDependentHint) {
+ platforms.forEach { sourceset ->
+ table(kind = ContentKind.Main, sourceSets = setOf(sourceset)) {
+ throws.entries.mapNotNull { entry ->
+ entry.value[sourceset]?.let { throws ->
+ buildGroup(sourceSets = setOf(sourceset)) {
+ group(styles = mainStyles + ContentStyle.RowTitle) {
+ throws.exceptionAddress?.let {
+ link(text = entry.key, address = it)
+ } ?: text(entry.key)
+ }
+ comment(throws.root)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
fun DocumentableContentBuilder.contentForSamples() {
val samples = tags.withTypeNamed<Sample>()
@@ -461,6 +485,7 @@ open class DefaultPageCreator(
contentForSamples()
contentForSeeAlso()
contentForParams()
+ contentForThrows()
}
}.children
}
diff --git a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt
index 1865f276..9fee60cb 100644
--- a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt
+++ b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt
@@ -154,6 +154,7 @@ open class PageContentBuilder(
) {
contents += ContentTable(
defaultHeaders,
+ null,
operation(),
DCI(mainDRI, kind),
sourceSets.toDisplaySourceSets(), styles, extra
@@ -177,8 +178,8 @@ open class PageContentBuilder(
if (renderWhenEmpty || elements.any()) {
header(level, name, kind = kind) { }
contents += ContentTable(
- headers ?: defaultHeaders,
- elements
+ header = headers ?: defaultHeaders,
+ children = elements
.let {
if (needsSorting)
it.sortedWith(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)) { it.name })
@@ -190,8 +191,10 @@ open class PageContentBuilder(
operation(it)
}
},
- DCI(mainDRI, kind),
- sourceSets.toDisplaySourceSets(), styles, extra
+ dci = DCI(mainDRI, kind),
+ sourceSets = sourceSets.toDisplaySourceSets(),
+ style = styles,
+ extra = extra
)
}
}
diff --git a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt
index fad5ff36..782f792a 100644
--- a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt
+++ b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt
@@ -8,12 +8,16 @@ import com.intellij.psi.javadoc.*
import com.intellij.psi.util.PsiTreeUtil
import org.intellij.markdown.MarkdownElementTypes
import org.jetbrains.dokka.analysis.from
+import org.jetbrains.dokka.base.parsers.factories.DocTagsFromStringFactory
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.doc.*
import org.jetbrains.dokka.model.doc.Deprecated
import org.jetbrains.dokka.utilities.DokkaLogger
import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName
import org.jetbrains.kotlin.name.FqName
+import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace
+import org.jetbrains.kotlin.psi.psiUtil.siblings
+import org.jetbrains.kotlin.tools.projectWizard.core.ParsingState
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import org.jsoup.Jsoup
@@ -36,12 +40,27 @@ class JavadocParser(
nodes.addAll(docComment.tags.mapNotNull { tag ->
when (tag.name) {
"param" -> Param(
- wrapTagIfNecessary(convertJavadocElements(tag.contentElements())),
- tag.children.firstIsInstanceOrNull<PsiDocParamRef>()?.text.orEmpty()
+ wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded().drop(1))),
+ tag.dataElements.firstOrNull()?.text.orEmpty()
)
- "throws" -> Throws(wrapTagIfNecessary(convertJavadocElements(tag.contentElements())), tag.text)
- "return" -> Return(wrapTagIfNecessary(convertJavadocElements(tag.contentElements())))
- "author" -> Author(wrapTagIfNecessary(convertJavadocElements(tag.authorContentElements()))) // Workaround: PSI returns first word after @author tag as a `DOC_TAG_VALUE_ELEMENT`, then the rest as a `DOC_COMMENT_DATA`, so for `Name Surname` we get them parted
+ "throws" -> {
+ val resolved = tag.resolveException()
+ val dri = resolved?.let { DRI.from(it) }
+ Throws(
+ root = wrapTagIfNecessary(convertJavadocElements(tag.dataElements.drop(1))),
+ /* we always would like to have a fully qualified name as name,
+ * because it will be used as a display name later and we would like to have those unified
+ * even if documentation states shortened version
+ *
+ * Only if dri search fails we should use the provided phrase (since then we are not able to get a fq name)
+ * */
+ name = resolved?.getKotlinFqName()?.asString()
+ ?: tag.dataElements.firstOrNull()?.text.orEmpty(),
+ exceptionAddress = dri
+ )
+ }
+ "return" -> Return(wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded())))
+ "author" -> Author(wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded()))) // Workaround: PSI returns first word after @author tag as a `DOC_TAG_VALUE_ELEMENT`, then the rest as a `DOC_COMMENT_DATA`, so for `Name Surname` we get them parted
"see" -> getSeeTagElementContent(tag).let {
See(
wrapTagIfNecessary(it.first),
@@ -49,13 +68,16 @@ class JavadocParser(
it.second
)
}
- "deprecated" -> Deprecated(wrapTagIfNecessary(convertJavadocElements(tag.dataElements.toList())))
+ "deprecated" -> Deprecated(wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded())))
else -> null
}
})
return DocumentationNode(nodes)
}
+ private fun PsiDocTag.resolveException(): PsiElement? =
+ dataElements.firstOrNull()?.firstChild?.referenceElementOrSelf()?.resolveToGetDri()
+
private fun wrapTagIfNecessary(list: List<DocTag>): CustomDocTag =
if (list.size == 1 && (list.first() as? CustomDocTag)?.name == MarkdownElementTypes.MARKDOWN_FILE.name)
list.first() as CustomDocTag
@@ -145,20 +167,99 @@ class JavadocParser(
}
}
+ private data class ParserState(
+ val previousElement: PsiElement? = null,
+ val openPreTags: Int = 0,
+ val closedPreTags: Int = 0
+ )
+
+ private data class ParsingResult(val newState: ParserState = ParserState(), val parsedLine: String? = null) {
+ operator fun plus(other: ParsingResult): ParsingResult =
+ ParsingResult(
+ other.newState,
+ listOfNotNull(parsedLine, other.parsedLine).joinToString(separator = "")
+ )
+ }
+
private inner class Parse : (Iterable<PsiElement>, Boolean) -> List<DocTag> {
val driMap = mutableMapOf<String, DRI>()
- private fun PsiElement.stringify(): String? = when (this) {
- is PsiReference -> children.joinToString("") { it.stringify().orEmpty() }
- is PsiInlineDocTag -> convertInlineDocTag(this)
- is PsiDocParamRef -> toDocumentationLinkString()
- is PsiDocTagValue,
- is LeafPsiElement -> text.let {
- if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it?.drop(1) else it
- }.let {
- if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it?.dropLastWhile { it == ' ' } else it
+ private fun PsiElement.stringify(state: ParserState): ParsingResult =
+ when (this) {
+ is PsiReference -> children.fold(ParsingResult(state)) { acc, e -> acc + e.stringify(acc.newState) }
+ else -> stringifySimpleElement(state)
}
- else -> null
+
+ private fun PsiElement.stringifySimpleElement(state: ParserState): ParsingResult {
+ val openPre = state.openPreTags + "<pre(\\s+.*)?>".toRegex().findAll(text).toList().size
+ val closedPre = state.closedPreTags + "</pre>".toRegex().findAll(text).toList().size
+ val isInsidePre = openPre > closedPre
+ val parsed = when (this) {
+ is PsiInlineDocTag -> convertInlineDocTag(this)
+ is PsiDocParamRef -> toDocumentationLinkString()
+ is PsiDocTagValue,
+ is LeafPsiElement -> {
+ if (isInsidePre) {
+ /*
+ For values in the <pre> tag we try to keep formatting, so only the leading space is trimmed,
+ since it is there because it separates this line from the leading asterisk
+ */
+ text.let {
+ if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true && it.firstOrNull() == ' ') it.drop(1) else it
+ }.let {
+ if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it.dropLastWhile { it == ' ' } else it
+ }
+ } else {
+ /*
+ Outside of the <pre> we would like to trim everything from the start and end of a line since
+ javadoc doesn't care about it.
+ */
+ text.let {
+ if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text != " " && state.previousElement !is PsiInlineDocTag) it?.trimStart() else it
+ }?.let {
+ if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text != " ") it.trimEnd() else it
+ }?.let {
+ if (shouldHaveSpaceAtTheEnd()) "$it " else it
+ }
+ }
+ }
+ else -> null
+ }
+ val previousElement = if (text.trim() == "") state.previousElement else this
+ return ParsingResult(
+ state.copy(
+ previousElement = previousElement,
+ closedPreTags = closedPre,
+ openPreTags = openPre
+ ), parsed
+ )
+ }
+
+ /**
+ * We would like to know if we need to have a space after a this tag
+ *
+ * The space is required when:
+ * - tag spans multiple lines, between every line we would need a space
+ *
+ * We wouldn't like to render a space if:
+ * - tag is followed by an end of comment
+ * - after a tag there is another tag (eg. multiple @author tags)
+ * - they end with an html tag like: <a href="...">Something</a> since then the space will be displayed in the following text
+ * - next line starts with a <p> or <pre> token
+ */
+ private fun PsiElement.shouldHaveSpaceAtTheEnd(): Boolean {
+ val siblings = siblings(withItself = false).toList().filterNot { it.text.trim() == "" }
+ val nextNotEmptySibling = (siblings.firstOrNull() as? PsiDocToken)
+ val furtherNotEmptySibling =
+ (siblings.drop(1).firstOrNull { it is PsiDocToken && !it.isLeadingAsterisk() } as? PsiDocToken)
+ val lastHtmlTag = text.trim().substringAfterLast("<")
+ val endsWithAnUnclosedTag = lastHtmlTag.endsWith(">") && !lastHtmlTag.startsWith("</")
+
+ return (nextSibling as? PsiWhiteSpace)?.text == "\n " &&
+ (getNextSiblingIgnoringWhitespace() as? PsiDocToken)?.tokenType?.toString() != END_COMMENT_TYPE &&
+ nextNotEmptySibling?.isLeadingAsterisk() == true &&
+ furtherNotEmptySibling?.tokenType?.toString() == COMMENT_TYPE &&
+ !endsWithAnUnclosedTag
}
private fun PsiElement.toDocumentationLinkString(
@@ -172,7 +273,7 @@ class JavadocParser(
dri.toString()
} ?: UNRESOLVED_PSI_ELEMENT
- return """<a data-dri=$dri>${label.joinToString(" ") { it.text }}</a>"""
+ return """<a data-dri="$dri">${label.joinToString(" ") { it.text }}</a>"""
}
private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) {
@@ -216,6 +317,13 @@ class JavadocParser(
"ol" -> ifChildrenPresent { Ol(children) }
"li" -> Li(children)
"a" -> createLink(element, children)
+ "table" -> ifChildrenPresent { Table(children) }
+ "tr" -> ifChildrenPresent { Tr(children) }
+ "td" -> Td(children)
+ "thead" -> THead(children)
+ "tbody" -> TBody(children)
+ "tfoot" -> TFoot(children)
+ "caption" -> ifChildrenPresent { Caption(children) }
else -> Text(body = element.ownText())
}
}
@@ -228,23 +336,24 @@ class JavadocParser(
}
override fun invoke(elements: Iterable<PsiElement>, asParagraph: Boolean): List<DocTag> =
- Jsoup.parseBodyFragment(elements.mapNotNull { it.stringify() }.dropWhile { it.isBlank() }
- .dropLastWhile { it.isBlank() }.joinToString(
- "",
- prefix = if (asParagraph) "<p>" else "",
- postfix = if (asParagraph) "</p>" else ""
- )
- ).body().childNodes().mapNotNull { convertHtmlNode(it) }
+ elements.fold(ParsingResult()) { acc, e ->
+ acc + e.stringify(acc.newState)
+ }.parsedLine?.let {
+ val trimmed = it.trim()
+ val toParse = if (asParagraph) "<p>$trimmed</p>" else trimmed
+ Jsoup.parseBodyFragment(toParse).body().childNodes().mapNotNull { convertHtmlNode(it) }
+ }.orEmpty()
}
- private fun PsiDocTag.contentElements(): List<PsiElement> =
- dataElements.mapNotNull { it.takeIf { it is PsiDocToken && it.text.isNotBlank() } }
-
- private fun PsiDocTag.authorContentElements(): List<PsiElement> = listOfNotNull(
- dataElements[0],
- dataElements[0].nextSibling?.takeIf { it.text != dataElements.drop(1).firstOrNull()?.text },
- *dataElements.drop(1).toTypedArray()
- )
+ private fun PsiDocTag.contentElementsWithSiblingIfNeeded(): List<PsiElement> = if (dataElements.isNotEmpty()) {
+ listOfNotNull(
+ dataElements[0],
+ dataElements[0].nextSibling?.takeIf { it.text != dataElements.drop(1).firstOrNull()?.text },
+ *dataElements.drop(1).toTypedArray()
+ )
+ } else {
+ emptyList()
+ }
private fun convertJavadocElements(elements: Iterable<PsiElement>, asParagraph: Boolean = true): List<DocTag> =
Parse()(elements, asParagraph)
@@ -254,20 +363,22 @@ class JavadocParser(
private fun PsiDocToken.isLeadingAsterisk() = tokenType.toString() == "DOC_COMMENT_LEADING_ASTERISKS"
private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null) =
- reference?.resolve()?.let {
+ resolveToGetDri()?.let {
val dri = DRI.from(it)
val label = labelElement ?: defaultLabel()
DocumentationLink(dri, convertJavadocElements(listOfNotNull(label), asParagraph = false))
}
+ private fun PsiElement.resolveToGetDri(): PsiElement? =
+ reference?.resolve()
+
private fun PsiDocTag.referenceElement(): PsiElement? =
- linkElement()?.let {
- if (it.node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) {
- PsiTreeUtil.findChildOfType(it, PsiJavaCodeReferenceElement::class.java)
- } else {
- it
- }
- }
+ linkElement()?.referenceElementOrSelf()
+
+ private fun PsiElement.referenceElementOrSelf(): PsiElement? =
+ if (node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) {
+ PsiTreeUtil.findChildOfType(this, PsiJavaCodeReferenceElement::class.java)
+ } else this
private fun PsiElement.defaultLabel() = children.firstOrNull {
it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken()
@@ -278,5 +389,7 @@ class JavadocParser(
companion object {
private const val UNRESOLVED_PSI_ELEMENT = "UNRESOLVED_PSI_ELEMENT"
+ private const val END_COMMENT_TYPE = "DOC_COMMENT_END"
+ private const val COMMENT_TYPE = "DOC_COMMENT_DATA"
}
}
diff --git a/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt
index efe8949c..4108af82 100644
--- a/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt
+++ b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt
@@ -7,12 +7,14 @@ import org.jetbrains.dokka.model.dfs
import org.jetbrains.dokka.model.doc.DocumentationNode
import org.jetbrains.dokka.model.doc.Param
import org.jetbrains.dokka.model.doc.Text
+import org.jetbrains.dokka.pages.ContentDRILink
import org.jetbrains.dokka.pages.ContentPage
import org.jetbrains.dokka.pages.MemberPageNode
import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.junit.jupiter.api.Test
import utils.*
+import kotlin.test.assertEquals
class ContentForParamsTest : AbstractCoreTest() {
private val testConfiguration = dokkaConfiguration {
@@ -150,7 +152,7 @@ class ContentForParamsTest : AbstractCoreTest() {
+"Woolfy"
}
}
- unnamedTag("Since") { comment { +"0.11" } }
+ unnamedTag("Since") { comment { +"0.11" } }
}
}
}
@@ -175,7 +177,8 @@ class ContentForParamsTest : AbstractCoreTest() {
""".trimIndent(), testConfiguration
) {
pagesTransformationStage = { module ->
- val classPage = module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
+ val classPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
classPage.content.assertNode {
group {
header { +"DocGenProcessor" }
@@ -218,7 +221,8 @@ class ContentForParamsTest : AbstractCoreTest() {
""".trimIndent(), testConfiguration
) {
pagesTransformationStage = { module ->
- val classPage = module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
+ val classPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
classPage.content.assertNode {
group {
header { +"DocGenProcessor" }
@@ -246,6 +250,682 @@ class ContentForParamsTest : AbstractCoreTest() {
}
@Test
+ fun `deprecated with multiple links inside`() {
+ testInline(
+ """
+ |/src/main/java/sample/DocGenProcessor.java
+ |package sample;
+ |/**
+ | * Return the target fragment set by {@link #setTargetFragment}.
+ | *
+ | * @deprecated Instead of using a target fragment to pass results, the fragment requesting a
+ | * result should use
+ | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentManager#setFragmentResult(String, Bundle)} to deliver results to
+ | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentResultListener} instances registered by other fragments via
+ | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentManager#setFragmentResultListener(String, LifecycleOwner,
+ | * FragmentResultListener)}.
+ | */
+ | public class DocGenProcessor {
+ | public String setTargetFragment(){
+ | return "";
+ | }
+ |}
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val classPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
+ classPage.content.assertNode {
+ group {
+ header { +"DocGenProcessor" }
+ platformHinted {
+ group {
+ skipAllNotMatching() //Signature
+ }
+ group {
+ comment {
+ +"Return the target fragment set by "
+ link { +"setTargetFragment" }
+ +"."
+ }
+ }
+ group {
+ header(4) { +"Deprecated" }
+ comment {
+ +"Instead of using a target fragment to pass results, the fragment requesting a result should use "
+ link { +"FragmentManager#setFragmentResult(String, Bundle)" }
+ +" to deliver results to "
+ link { +"FragmentResultListener" }
+ +" instances registered by other fragments via "
+ link { +"FragmentManager#setFragmentResultListener(String, LifecycleOwner, FragmentResultListener)" }
+ +"."
+ }
+ }
+ }
+ }
+ skipAllNotMatching()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `deprecated with an html link in multiple lines`() {
+ testInline(
+ """
+ |/src/main/java/sample/DocGenProcessor.java
+ |package sample;
+ |/**
+ | * @deprecated Use
+ | * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ | * TabLayout and ViewPager</a> instead.
+ | */
+ | public class DocGenProcessor { }
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val classPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
+ classPage.content.assertNode {
+ group {
+ header { +"DocGenProcessor" }
+ platformHinted {
+ group {
+ skipAllNotMatching() //Signature
+ }
+ group {
+ header(4) { +"Deprecated" }
+ comment {
+ +"Use "
+ link { +"TabLayout and ViewPager" }
+ +" instead."
+ }
+ }
+ }
+ }
+ skipAllNotMatching()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `deprecated with an multiple inline links`() {
+ testInline(
+ """
+ |/src/main/java/sample/DocGenProcessor.java
+ |package sample;
+ |/**
+ | * FragmentManagerNonConfig stores the retained instance fragments across
+ | * activity recreation events.
+ | *
+ | * <p>Apps should treat objects of this type as opaque, returned by
+ | * and passed to the state save and restore process for fragments in
+ | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentController#retainNestedNonConfig()} and
+ | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)}.</p>
+ | *
+ | * @deprecated Have your {@link java.util.HashMap FragmentHostCallback} implement
+ | * {@link java.util.HashMap } to automatically retain the Fragment's
+ | * non configuration state.
+ | */
+ | public class DocGenProcessor { }
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val classPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
+ classPage.content.assertNode {
+ group {
+ header { +"DocGenProcessor" }
+ platformHinted {
+ group {
+ skipAllNotMatching() //Signature
+ }
+ group {
+ comment {
+ group {
+ +"FragmentManagerNonConfig stores the retained instance fragments across activity recreation events. "
+ }
+ group {
+ +"Apps should treat objects of this type as opaque, returned by and passed to the state save and restore process for fragments in "
+ link { +"FragmentController#retainNestedNonConfig()" }
+ +" and "
+ link { +"FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)" }
+ +"."
+ }
+ }
+ }
+ group {
+ header(4) { +"Deprecated" }
+ comment {
+ +"Have your "
+ link { +"FragmentHostCallback" }
+ +" implement "
+ link { +"java.util.HashMap" }
+ +" to automatically retain the Fragment's non configuration state."
+ }
+ }
+ }
+ }
+ skipAllNotMatching()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `multiline throws with comment`() {
+ testInline(
+ """
+ |/src/main/java/sample/DocGenProcessor.java
+ |package sample;
+ | public class DocGenProcessor {
+ | /**
+ | * a normal comment
+ | *
+ | * @throws java.lang.IllegalStateException if the Dialog has not yet been created (before
+ | * onCreateDialog) or has been destroyed (after onDestroyView).
+ | * @throws java.lang.RuntimeException when {@link java.util.HashMap#containsKey(java.lang.Object) Hash
+ | * Map} doesn't contain value.
+ | */
+ | public static void sample(){ }
+ |}
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val functionPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" }.children.single { it.name == "sample" } as ContentPage
+ functionPage.content.assertNode {
+ group {
+ header(1) { +"sample" }
+ }
+ divergentGroup {
+ divergentInstance {
+ divergent {
+ skipAllNotMatching() //Signature
+ }
+ after {
+ group { pWrapped("a normal comment") }
+ header(4) { +"Throws" }
+ platformHinted {
+ table {
+ group {
+ group {
+ link { +"java.lang.IllegalStateException" }
+ }
+ comment { +"if the Dialog has not yet been created (before onCreateDialog) or has been destroyed (after onDestroyView)." }
+ }
+ group {
+ group {
+ link { +"java.lang.RuntimeException" }
+ }
+ comment {
+ +"when "
+ link { +"Hash Map" }
+ +" doesn't contain value."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `multiline kotlin throws with comment`() {
+ testInline(
+ """
+ |/src/main/kotlin/sample/sample.kt
+ |package sample;
+ | /**
+ | * a normal comment
+ | *
+ | * @throws java.lang.IllegalStateException if the Dialog has not yet been created (before
+ | * onCreateDialog) or has been destroyed (after onDestroyView).
+ | * @exception RuntimeException when [Hash Map][java.util.HashMap.containsKey] doesn't contain value.
+ | */
+ | fun sample(){ }
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val functionPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "sample" } as ContentPage
+ functionPage.content.assertNode {
+ group {
+ header(1) { +"sample" }
+ }
+ divergentGroup {
+ divergentInstance {
+ divergent {
+ skipAllNotMatching() //Signature
+ }
+ after {
+ group { pWrapped("a normal comment") }
+ header(4) { +"Throws" }
+ platformHinted {
+ table {
+ group {
+ group {
+ link {
+ check {
+ assertEquals(
+ "java.lang/IllegalStateException///PointingToDeclaration/",
+ (this as ContentDRILink).address.toString()
+ )
+ }
+ +"java.lang.IllegalStateException"
+ }
+ }
+ comment { +"if the Dialog has not yet been created (before onCreateDialog) or has been destroyed (after onDestroyView)." }
+ }
+ group {
+ group {
+ link {
+ check {
+ assertEquals(
+ "java.lang/RuntimeException///PointingToDeclaration/",
+ (this as ContentDRILink).address.toString()
+ )
+ }
+ +"java.lang.RuntimeException"
+ }
+ }
+ comment {
+ +"when "
+ link { +"Hash Map" }
+ +" doesn't contain value."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `multiline throws where exception is not in the same line as description`() {
+ testInline(
+ """
+ |/src/main/java/sample/DocGenProcessor.java
+ |package sample;
+ | public class DocGenProcessor {
+ | /**
+ | * a normal comment
+ | *
+ | * @throws java.lang.IllegalStateException if the Dialog has not yet been created (before
+ | * onCreateDialog) or has been destroyed (after onDestroyView).
+ | * @throws java.lang.RuntimeException when
+ | * {@link java.util.HashMap#containsKey(java.lang.Object) Hash
+ | * Map}
+ | * doesn't contain value.
+ | */
+ | public static void sample(){ }
+ |}
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val functionPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" }.children.single { it.name == "sample" } as ContentPage
+ functionPage.content.assertNode {
+ group {
+ header(1) { +"sample" }
+ }
+ divergentGroup {
+ divergentInstance {
+ divergent {
+ skipAllNotMatching() //Signature
+ }
+ after {
+ group { pWrapped("a normal comment") }
+ header(4) { +"Throws" }
+ platformHinted {
+ table {
+ group {
+ group {
+ link {
+ check {
+ assertEquals(
+ "java.lang/IllegalStateException///PointingToDeclaration/",
+ (this as ContentDRILink).address.toString()
+ )
+ }
+ +"java.lang.IllegalStateException"
+ }
+ }
+ comment { +"if the Dialog has not yet been created (before onCreateDialog) or has been destroyed (after onDestroyView)." }
+ }
+ group {
+ group {
+ link {
+ check {
+ assertEquals(
+ "java.lang/RuntimeException///PointingToDeclaration/",
+ (this as ContentDRILink).address.toString()
+ )
+ }
+ +"java.lang.RuntimeException"
+ }
+ }
+ comment {
+ +"when "
+ link { +"Hash Map" }
+ +" doesn't contain value."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ @Test
+ fun `documentation splitted in 2 using enters`() {
+ testInline(
+ """
+ |/src/main/java/sample/DocGenProcessor.java
+ |package sample;
+ |/**
+ | * Listener for handling fragment results.
+ | *
+ | * This object should be passed to
+ | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentManager#setFragmentResultListener(String, LifecycleOwner, FragmentResultListener)}
+ | * and it will listen for results with the same key that are passed into
+ | * {@link java.util.HashMap#containsKey(java.lang.Object) FragmentManager#setFragmentResult(String, Bundle)}.
+ | *
+ | */
+ | public class DocGenProcessor { }
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val classPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
+ classPage.content.assertNode {
+ group {
+ header { +"DocGenProcessor" }
+ platformHinted {
+ group {
+ skipAllNotMatching() //Signature
+ }
+ group {
+ comment {
+ +"Listener for handling fragment results. This object should be passed to "
+ link { +"FragmentManager#setFragmentResultListener(String, LifecycleOwner, FragmentResultListener)" }
+ +" and it will listen for results with the same key that are passed into "
+ link { +"FragmentManager#setFragmentResult(String, Bundle)" }
+ +"."
+ }
+ }
+ }
+ }
+ skipAllNotMatching()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `multiline return tag with param`() {
+ testInline(
+ """
+ |/src/main/java/sample/DocGenProcessor.java
+ |package sample;
+ | public class DocGenProcessor {
+ | /**
+ | * a normal comment
+ | *
+ | * @param testParam Sample description for test param that has a type of {@link java.lang.String String}
+ | * @return empty string when
+ | * {@link java.util.HashMap#containsKey(java.lang.Object) Hash
+ | * Map}
+ | * doesn't contain value.
+ | */
+ | public static String sample(String testParam){
+ | return "";
+ | }
+ |}
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val functionPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" }.children.single { it.name == "sample" } as ContentPage
+ functionPage.content.assertNode {
+ group {
+ header(1) { +"sample" }
+ }
+ divergentGroup {
+ divergentInstance {
+ divergent {
+ skipAllNotMatching() //Signature
+ }
+ after {
+ group { pWrapped("a normal comment") }
+ group {
+ header(4) { +"Return" }
+ comment {
+ +"empty string when "
+ link { +"Hash Map" }
+ +" doesn't contain value."
+ }
+ }
+ header(2) { +"Parameters" }
+ group {
+ platformHinted {
+ table {
+ group {
+ +"testParam"
+ comment {
+ +"Sample description for test param that has a type of "
+ link { +"String" }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `return tag in kotlin`() {
+ testInline(
+ """
+ |/src/main/kotlin/sample/sample.kt
+ |package sample;
+ | /**
+ | * a normal comment
+ | *
+ | * @return empty string when [Hash Map](java.util.HashMap.containsKey) doesn't contain value.
+ | *
+ | */
+ |fun sample(): String {
+ | return ""
+ | }
+ |}
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val functionPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "sample" } as ContentPage
+ functionPage.content.assertNode {
+ group {
+ header(1) { +"sample" }
+ }
+ divergentGroup {
+ divergentInstance {
+ divergent {
+ skipAllNotMatching() //Signature
+ }
+ after {
+ group { pWrapped("a normal comment") }
+ group {
+ header(4) { +"Return" }
+ comment {
+ +"empty string when "
+ link { +"Hash Map" }
+ +" doesn't contain value."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `list with links and description`() {
+ testInline(
+ """
+ |/src/main/java/sample/DocGenProcessor.java
+ |package sample;
+ |/**
+ | * Static library support version of the framework's {@link java.lang.String}.
+ | * Used to write apps that run on platforms prior to Android 3.0. When running
+ | * on Android 3.0 or above, this implementation is still used; it does not try
+ | * to switch to the framework's implementation. See the framework {@link java.lang.String}
+ | * documentation for a class overview.
+ | *
+ | * <p>The main differences when using this support version instead of the framework version are:
+ | * <ul>
+ | * <li>Your activity must extend {@link java.lang.String FragmentActivity}
+ | * <li>You must call {@link java.util.HashMap#containsKey(java.lang.Object) FragmentActivity#getSupportFragmentManager} to get the
+ | * {@link java.util.HashMap FragmentManager}
+ | * </ul>
+ | *
+ | */
+ |public class DocGenProcessor { }
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val classPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
+ classPage.content.assertNode {
+ group {
+ header { +"DocGenProcessor" }
+ platformHinted {
+ group {
+ skipAllNotMatching() //Signature
+ }
+ group {
+ comment {
+ group {
+ +"Static library support version of the framework's "
+ link { +"java.lang.String" }
+ +". Used to write apps that run on platforms prior to Android 3.0."
+ +" When running on Android 3.0 or above, this implementation is still used; it does not try to switch to the framework's implementation. See the framework "
+ link { +"java.lang.String" }
+ +" documentation for a class overview. " //TODO this probably shouldnt have a space but it is minor
+ }
+ group {
+ +"The main differences when using this support version instead of the framework version are: "
+ }
+ list {
+ group {
+ +"Your activity must extend "
+ link { +"FragmentActivity" }
+ }
+ group {
+ +"You must call "
+ link { +"FragmentActivity#getSupportFragmentManager" }
+ +" to get the "
+ link { +"FragmentManager" }
+ }
+ }
+ }
+ }
+ }
+ }
+ skipAllNotMatching()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `documentation with table`() {
+ testInline(
+ """
+ |/src/main/java/sample/DocGenProcessor.java
+ |package sample;
+ |/**
+ | * <table>
+ | * <caption>List of supported types</caption>
+ | * <tr>
+ | * <td>cell 11</td> <td>cell 21</td>
+ | * </tr>
+ | * <tr>
+ | * <td>cell 12</td> <td>cell 22</td>
+ | * </tr>
+ | * </table>
+ | */
+ | public class DocGenProcessor { }
+ """.trimIndent(), testConfiguration
+ ) {
+ pagesTransformationStage = { module ->
+ val classPage =
+ module.children.single { it.name == "sample" }.children.single { it.name == "DocGenProcessor" } as ContentPage
+ classPage.content.assertNode {
+ group {
+ header { +"DocGenProcessor" }
+ platformHinted {
+ group {
+ skipAllNotMatching() //Signature
+ }
+ comment {
+ table {
+ check {
+ caption!!.assertNode {
+ caption {
+ +"List of supported types"
+ }
+ }
+ }
+ group {
+ group {
+ +"cell 11"
+ }
+ group {
+ +"cell 21"
+ }
+ }
+ group {
+ group {
+ +"cell 12"
+ }
+ group {
+ +"cell 22"
+ }
+ }
+ }
+ }
+ }
+ }
+ skipAllNotMatching()
+ }
+ }
+ }
+ }
+
+
+ @Test
fun `undocumented parameter and other tags`() {
testInline(
"""
@@ -631,8 +1311,8 @@ class ContentForParamsTest : AbstractCoreTest() {
}
after {
group { pWrapped("comment to function") }
- unnamedTag("Author") { comment { +"Kordyjan" } }
- unnamedTag("Since") { comment { +"0.11" } }
+ unnamedTag("Author") { comment { +"Kordyjan" } }
+ unnamedTag("Since") { comment { +"0.11" } }
header(2) { +"Parameters" }
group {
diff --git a/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt b/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt
index a2c45f63..4bc33c03 100644
--- a/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt
+++ b/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt
@@ -1,10 +1,12 @@
package content.seealso
import matchers.content.*
+import org.jetbrains.dokka.pages.ContentDRILink
import org.jetbrains.dokka.pages.ContentPage
import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest
import org.junit.jupiter.api.Test
import utils.*
+import kotlin.test.assertEquals
class ContentForSeeAlsoTest : AbstractCoreTest() {
private val testConfiguration = dokkaConfiguration {
@@ -207,8 +209,12 @@ class ContentForSeeAlsoTest : AbstractCoreTest() {
platformHinted {
table {
group {
- //DRI should be "kotlin.collections/Collection////"
- link { +"Collection" }
+ link {
+ check {
+ assertEquals("kotlin.collections/Collection///PointingToDeclaration/", (this as ContentDRILink).address.toString())
+ }
+ +"Collection"
+ }
group { }
}
}
diff --git a/plugins/base/src/test/kotlin/content/signatures/SkippingParenthesisForConstructorsTest.kt b/plugins/base/src/test/kotlin/content/signatures/SkippingParenthesisForConstructorsTest.kt
index df1bfbc6..b09e5252 100644
--- a/plugins/base/src/test/kotlin/content/signatures/SkippingParenthesisForConstructorsTest.kt
+++ b/plugins/base/src/test/kotlin/content/signatures/SkippingParenthesisForConstructorsTest.kt
@@ -35,7 +35,7 @@ class ConstructorsSignaturesTest : AbstractCoreTest() {
header(1) { +"SomeClass" }
platformHinted {
group {
- +"class"
+ +"class "
link { +"SomeClass" }
}
}
@@ -65,7 +65,7 @@ class ConstructorsSignaturesTest : AbstractCoreTest() {
header(1) { +"SomeClass" }
platformHinted {
group {
- +"class"
+ +"class "
link { +"SomeClass" }
}
}
@@ -95,9 +95,9 @@ class ConstructorsSignaturesTest : AbstractCoreTest() {
header(1) { +"SomeClass" }
platformHinted {
group {
- +"class"
+ +"class "
link { +"SomeClass" }
- +"(a:"
+ +"(a: "
group { link { +"String" } }
+")"
}
@@ -128,9 +128,9 @@ class ConstructorsSignaturesTest : AbstractCoreTest() {
header(1) { +"SomeClass" }
platformHinted {
group {
- +"class"
+ +"class "
link { +"SomeClass" }
- +"(a:" // TODO: Make sure if we still do not want to have "val" here
+ +"(a: " // TODO: Make sure if we still do not want to have "val" here
group { link { +"String" } }
+")"
}
@@ -162,9 +162,9 @@ class ConstructorsSignaturesTest : AbstractCoreTest() {
header(1) { +"SomeClass" }
platformHinted {
group {
- +"class"
+ +"class "
link { +"SomeClass" }
- +"(a:"
+ +"(a: "
group { link { +"String" } }
+")"
}
@@ -213,9 +213,9 @@ class ConstructorsSignaturesTest : AbstractCoreTest() {
header(1) { +"SomeClass" }
platformHinted {
group {
- +"class"
+ +"class "
link { +"SomeClass" }
- +"(a:"
+ +"(a: "
group { link { +"String" } }
+")"
}
@@ -229,9 +229,9 @@ class ConstructorsSignaturesTest : AbstractCoreTest() {
link { +"SomeClass" }
platformHinted {
group {
- +"fun"
+ +"fun "
link { +"SomeClass" }
- +"(a:"
+ +"(a: "
group {
link { +"String" }
}
diff --git a/plugins/base/src/test/kotlin/utils/contentUtils.kt b/plugins/base/src/test/kotlin/utils/contentUtils.kt
index 38d6450c..5d8673d0 100644
--- a/plugins/base/src/test/kotlin/utils/contentUtils.kt
+++ b/plugins/base/src/test/kotlin/utils/contentUtils.kt
@@ -33,7 +33,9 @@ fun ContentMatcherBuilder<*>.bareSignature(
unwrapAnnotation(it)
}
}
- +("$visibility $modifier ${keywords.joinToString("") { "$it " }} fun")
+ if (visibility.isNotBlank()) +"$visibility "
+ if (modifier.isNotBlank()) +"$modifier "
+ +("${keywords.joinToString("") { "$it " }}fun ")
link { +name }
+"("
params.forEachIndexed { id, (n, t) ->
@@ -45,7 +47,7 @@ fun ContentMatcherBuilder<*>.bareSignature(
+it
}
- +"$n:"
+ +"$n: "
group { link { +(t.type) } }
if (id != params.lastIndex)
+", "
@@ -90,7 +92,9 @@ fun ContentMatcherBuilder<*>.bareSignatureWithReceiver(
unwrapAnnotation(it)
}
}
- +("$visibility $modifier ${keywords.joinToString("") { "$it " }} fun")
+ if (visibility != null && visibility.isNotBlank()) +"$visibility "
+ if (modifier != null && modifier.isNotBlank()) +"$modifier "
+ +("${keywords.joinToString("") { "$it " }}fun ")
group {
link { +receiver }
}
@@ -106,7 +110,7 @@ fun ContentMatcherBuilder<*>.bareSignatureWithReceiver(
+it
}
- +"$n:"
+ +"$n: "
group { link { +(t.type) } }
if (id != params.lastIndex)
+", "
@@ -150,7 +154,9 @@ fun ContentMatcherBuilder<*>.propertySignature(
unwrapAnnotation(it)
}
}
- +("$visibility $modifier ${keywords.joinToString("") { "$it " }} $preposition")
+ if (visibility.isNotBlank()) +"$visibility "
+ if (modifier.isNotBlank()) +"$modifier "
+ +("${keywords.joinToString("") { "$it " }}$preposition ")
link { +name }
if (type != null) {
+(": ")