diff options
18 files changed, 1060 insertions, 244 deletions
diff --git a/core/content-matcher-test-utils/src/main/kotlin/matchers/content/ContentMatchersDsl.kt b/core/content-matcher-test-utils/src/main/kotlin/matchers/content/ContentMatchersDsl.kt index 67c0e692..264be933 100644 --- a/core/content-matcher-test-utils/src/main/kotlin/matchers/content/ContentMatchersDsl.kt +++ b/core/content-matcher-test-utils/src/main/kotlin/matchers/content/ContentMatchersDsl.kt @@ -3,7 +3,6 @@ package matchers.content import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo -import assertk.assertions.matches import org.jetbrains.dokka.model.withDescendants import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.test.tools.matchers.content.* @@ -53,12 +52,6 @@ fun <T : ContentComposite> ContentMatcherBuilder<T>.hasExactText(expected: Strin } } -fun <T : ContentComposite> ContentMatcherBuilder<T>.textMatches(pattern: Regex) { - assertions += { - assertThat(this::extractedText).matches(pattern) - } -} - inline fun <reified S : ContentComposite> ContentMatcherBuilder<*>.composite( block: ContentMatcherBuilder<S>.() -> Unit ) { @@ -96,6 +89,13 @@ fun ContentMatcherBuilder<*>.table(block: ContentMatcherBuilder<ContentTable>.() fun ContentMatcherBuilder<*>.platformHinted(block: ContentMatcherBuilder<ContentGroup>.() -> Unit) = composite<PlatformHintedContent> { group(block) } +fun ContentMatcherBuilder<*>.list(block: ContentMatcherBuilder<ContentList>.() -> Unit) = composite(block) + +fun ContentMatcherBuilder<*>.caption(block: ContentMatcherBuilder<ContentGroup>.() -> Unit) = composite<ContentGroup> { + block() + check { assertThat(this::style).contains(ContentStyle.Caption) } +} + fun ContentMatcherBuilder<*>.br() = node<ContentBreakLine>() fun ContentMatcherBuilder<*>.somewhere(block: ContentMatcherBuilder<*>.() -> Unit) { diff --git a/core/content-matcher-test-utils/src/main/kotlin/matchers/content/contentMatchers.kt b/core/content-matcher-test-utils/src/main/kotlin/matchers/content/contentMatchers.kt index f42e28f3..6a0e1c97 100644 --- a/core/content-matcher-test-utils/src/main/kotlin/matchers/content/contentMatchers.kt +++ b/core/content-matcher-test-utils/src/main/kotlin/matchers/content/contentMatchers.kt @@ -69,10 +69,9 @@ private class TextMatcherState( ) : MatchWalkerState() { override fun next(node: ContentNode): MatchWalkerState { node as? ContentText ?: throw MatcherError("Expected text: \"$text\" but got\n${node.debugRepresentation()}", anchor) - val trimmed = node.text.trim() return when { - text == trimmed -> rest.pop() - text.startsWith(trimmed) -> TextMatcherState(text.removePrefix(node.text).trim(), rest, anchor) + text == node.text -> rest.pop() + text.startsWith(node.text) -> TextMatcherState(text.removePrefix(node.text), rest, anchor) else -> throw MatcherError("Expected text: \"$text\", but got: \"${node.text}\"", anchor) } } @@ -111,7 +110,7 @@ private class SkippingMatcherState( private class FurtherSiblings(val list: List<MatcherElement>, val parent: CompositeMatcher<*>) { fun pop(): MatchWalkerState = when (val head = list.firstOrNull()) { - is TextMatcher -> TextMatcherState(head.text.trim(), drop(), head) + is TextMatcher -> TextMatcherState(head.text, drop(), head) is NodeMatcher<*> -> NodeMatcherState(head, drop()) is Anything -> SkippingMatcherState(drop().pop()) null -> EmptyMatcherState(parent) diff --git a/core/src/main/kotlin/model/doc/DocTag.kt b/core/src/main/kotlin/model/doc/DocTag.kt index 04b5c913..7698ebb3 100644 --- a/core/src/main/kotlin/model/doc/DocTag.kt +++ b/core/src/main/kotlin/model/doc/DocTag.kt @@ -354,3 +354,8 @@ data class Var( override val children: List<DocTag> = emptyList(), override val params: Map<String, String> = emptyMap() ) : DocTag() + +data class Caption( + override val children: List<DocTag> = emptyList(), + override val params: Map<String, String> = emptyMap() +) : DocTag()
\ No newline at end of file diff --git a/core/src/main/kotlin/model/doc/TagWrapper.kt b/core/src/main/kotlin/model/doc/TagWrapper.kt index cfe99b1e..cea132a8 100644 --- a/core/src/main/kotlin/model/doc/TagWrapper.kt +++ b/core/src/main/kotlin/model/doc/TagWrapper.kt @@ -22,7 +22,7 @@ data class Param(override val root: DocTag, override val name: String) : NamedTa data class Return(override val root: DocTag) : TagWrapper() data class Receiver(override val root: DocTag) : TagWrapper() data class Constructor(override val root: DocTag) : TagWrapper() -data class Throws(override val root: DocTag, override val name: String) : NamedTagWrapper() +data class Throws(override val root: DocTag, override val name: String, val exceptionAddress: DRI?) : NamedTagWrapper() data class Sample(override val root: DocTag, override val name: String) : NamedTagWrapper() data class Deprecated(override val root: DocTag) : TagWrapper() data class Property(override val root: DocTag, override val name: String) : NamedTagWrapper() diff --git a/core/src/main/kotlin/pages/ContentNodes.kt b/core/src/main/kotlin/pages/ContentNodes.kt index 77e6ebb2..303fa803 100644 --- a/core/src/main/kotlin/pages/ContentNodes.kt +++ b/core/src/main/kotlin/pages/ContentNodes.kt @@ -182,6 +182,7 @@ interface ContentComposite : ContentNode { /** Tables */ data class ContentTable( val header: List<ContentGroup>, + val caption: ContentGroup? = null, override val children: List<ContentGroup>, override val dci: DCI, override val sourceSets: Set<DisplaySourceSet>, @@ -343,7 +344,7 @@ enum class TextStyle : Style { } enum class ContentStyle : Style { - RowTitle, TabbedContent, WithExtraAttributes, RunnableSample, InDocumentationAnchor + RowTitle, TabbedContent, WithExtraAttributes, RunnableSample, InDocumentationAnchor, Caption } object CommentTable : Style 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) { +(": ") diff --git a/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt b/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt index 05d17308..6a687fe3 100644 --- a/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt +++ b/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt @@ -174,10 +174,10 @@ class KotlinAsJavaPluginTest : AbstractCoreTest() { divergentInstance { divergent { group { - +"final" + +"final " group { link { - +" String" + +"String" } } link { @@ -274,7 +274,7 @@ class KotlinAsJavaPluginTest : AbstractCoreTest() { } platformHinted { group { - +"public final class" + +"public final class " link { +"C" } |