From aeb2014eee704be377c06205d16f60562d2a8cf1 Mon Sep 17 00:00:00 2001 From: Błażej Kardyś Date: Fri, 17 Jul 2020 03:48:03 +0200 Subject: Fixing javadoc comment parser for psi files --- core/src/main/kotlin/model/WithChildren.kt | 3 + .../pages/comments/DocTagToContentConverter.kt | 9 +- .../psi/DefaultPsiToDocumentableTranslator.kt | 3 +- .../main/kotlin/translators/psi/JavadocParser.kt | 180 +++++++++++++-------- .../kotlin/content/params/ContentForParamsTest.kt | 54 +++---- .../content/seealso/ContentForSeeAlsoTest.kt | 34 ++-- .../SkippingParenthesisForConstructorsTest.kt | 2 +- plugins/base/src/test/kotlin/enums/EnumsTest.kt | 4 +- .../transformers/CommentsToContentConverterTest.kt | 164 +++++++++++-------- .../test/kotlin/translators/JavadocParserTest.kt | 156 ++++++++++++++++++ plugins/base/src/test/kotlin/translators/utils.kt | 42 +++-- .../jetbrains/dokka/javadoc/JavadocPageCreator.kt | 21 ++- .../javadoc/location/JavadocLocationProvider.kt | 5 +- .../dokka/javadoc/pages/JavadocPageNodes.kt | 1 + .../renderer/JavadocContentToHtmlTranslator.kt | 65 +++++--- .../JavadocContentToTemplateMapTranslator.kt | 13 +- .../dokka/javadoc/renderer/KorteJavadocRenderer.kt | 2 +- .../javadoc/src/main/resources/views/class.korte | 2 +- .../javadoc/JavadocClasslikeTemplateMapTest.kt | 8 +- 19 files changed, 534 insertions(+), 234 deletions(-) create mode 100644 plugins/base/src/test/kotlin/translators/JavadocParserTest.kt diff --git a/core/src/main/kotlin/model/WithChildren.kt b/core/src/main/kotlin/model/WithChildren.kt index 589bcd2a..06ff55b0 100644 --- a/core/src/main/kotlin/model/WithChildren.kt +++ b/core/src/main/kotlin/model/WithChildren.kt @@ -13,6 +13,9 @@ inline fun WithChildren<*>.firstChildOfTypeOrNull(predicate: (T) -> inline fun WithChildren<*>.firstChildOfType(): T = children.filterIsInstance().first() +inline fun WithChildren<*>.childrenOfType(): List = + children.filterIsInstance() + inline fun WithChildren<*>.firstChildOfType(predicate: (T) -> Boolean): T = children.filterIsInstance().first(predicate) 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 0f953e0f..9d667623 100644 --- a/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt +++ b/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt @@ -53,6 +53,9 @@ object DocTagToContentConverter : CommentsToContentConverter { ) ) + fun P.collapseParagraphs(): P = + if (children.size == 1 && children.first() is P) (children.first() as P).collapseParagraphs() else this + return when (docTag) { is H1 -> buildHeader(1) is H2 -> buildHeader(2) @@ -63,12 +66,14 @@ object DocTagToContentConverter : CommentsToContentConverter { is Ul -> buildList(false) is Ol -> buildList(true, docTag.params["start"]?.toInt() ?: 1) is Li -> listOf( - ContentGroup(children = buildChildren(docTag), dci, sourceSets, styles, extra) + ContentGroup(buildChildren(docTag), dci, sourceSets, styles, extra) ) is Br -> buildNewLine() is B -> buildChildren(docTag, setOf(TextStyle.Strong)) is I -> buildChildren(docTag, setOf(TextStyle.Italic)) - is P -> buildChildren(docTag, newStyles = setOf(TextStyle.Paragraph)) + is P -> listOf( + ContentGroup(buildChildren(docTag.collapseParagraphs()), dci, sourceSets, styles + setOf(TextStyle.Paragraph), extra) + ) is A -> listOf( ContentResolvedLink( buildChildren(docTag), diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt index df5d4ee1..9ed37c30 100644 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -285,8 +285,7 @@ class DefaultPsiToDocumentableTranslator( psiParameter.name, DocumentationNode( listOfNotNull(docs.firstChildOfTypeOrNull { - it.firstChildOfTypeOrNull() - ?.firstChildOfTypeOrNull()?.body == psiParameter.name + it.name == psiParameter.name })).toSourceSetDependent(), null, getBound(psiParameter.type), diff --git a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt index 81955fde..8262b3c6 100644 --- a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt +++ b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt @@ -34,18 +34,30 @@ class JavadocParser( docComment.getDescription()?.let { nodes.add(it) } nodes.addAll(docComment.tags.mapNotNull { tag -> when (tag.name) { - "param" -> Param(P(convertJavadocElements(tag.dataElements.toList())), tag.text) - "throws" -> Throws(P(convertJavadocElements(tag.dataElements.toList())), tag.text) - "return" -> Return(P(convertJavadocElements(tag.dataElements.toList()))) - "author" -> Author(P(convertJavadocElements(tag.dataElements.toList()))) - "see" -> See(P(getSeeTagElementContent(tag)), tag.referenceElement()?.text.orEmpty(), null) - "deprecated" -> Deprecated(P(convertJavadocElements(tag.dataElements.toList()))) + "param" -> Param( + wrapTagIfNecessary(convertJavadocElements(tag.contentElements())), + tag.children.firstIsInstanceOrNull()?.text.orEmpty() + ) + "throws" -> Throws(wrapTagIfNecessary(convertJavadocElements(tag.contentElements())), tag.text) + "return" -> Return(wrapTagIfNecessary(convertJavadocElements(tag.contentElements()))) + "author" -> Author(wrapTagIfNecessary(convertJavadocElements(tag.contentElements()))) + "see" -> getSeeTagElementContent(tag).let { + See( + wrapTagIfNecessary(it.first), + tag.referenceElement()?.text.orEmpty(), + it.second + ) + } + "deprecated" -> Deprecated(wrapTagIfNecessary(convertJavadocElements(tag.dataElements.toList()))) else -> null } }) return DocumentationNode(nodes) } + private fun wrapTagIfNecessary(list: List): DocTag = + if (list.size == 1) list.first() else P(list) + private fun findClosestDocComment(element: PsiNamedElement): PsiDocComment? { (element as? PsiDocCommentOwner)?.docComment?.run { return this } if (element is PsiMethod) { @@ -119,88 +131,118 @@ class JavadocParser( } } - private fun getSeeTagElementContent(tag: PsiDocTag): List = - listOfNotNull(tag.referenceElement()?.toDocumentationLink()) + private fun getSeeTagElementContent(tag: PsiDocTag): Pair, DRI?> { + val content = tag.referenceElement()?.toDocumentationLink() + return Pair(listOfNotNull(content), content?.dri) + } private fun PsiDocComment.getDescription(): Description? { - val nonEmptyDescriptionElements = descriptionElements.filter { it.text.trim().isNotEmpty() } - val convertedDescriptionElements = convertJavadocElements(nonEmptyDescriptionElements) - if (convertedDescriptionElements.isNotEmpty()) { - return Description(P(convertedDescriptionElements)) + val nonEmptyDescriptionElements = descriptionElements.filter { it.text.isNotBlank() } + return convertJavadocElements(nonEmptyDescriptionElements).takeIf { it.isNotEmpty() }?.let { + Description(wrapTagIfNecessary(it)) } - - return null } - private fun convertJavadocElements(elements: Iterable): List = - elements.mapNotNull { - when (it) { - is PsiReference -> convertJavadocElements(it.children.toList()) - is PsiInlineDocTag -> listOfNotNull(convertInlineDocTag(it)) - is PsiDocParamRef -> listOfNotNull(it.toDocumentationLink()) - is PsiDocTagValue, - is LeafPsiElement -> Jsoup.parse(it.text.trim()).body().childNodes().mapNotNull(::convertHtmlNode) - else -> null + private inner class Parse : (Iterable, Boolean) -> List { + val driMap = mutableMapOf() + + 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 -> (if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true) text?.trim() else text)?.takeUnless { it.isBlank() } + else -> null + } + + private fun PsiElement.toDocumentationLinkString( + labelElement: PsiElement? = null + ): String? = + reference?.resolve()?.let { + if (it !is PsiParameter) { + val dri = DRI.from(it) + driMap[dri.toString()] = dri + val label = labelElement ?: defaultLabel() + """${label.text}""" + } else null } - }.flatten() - private fun convertHtmlNode(node: Node, insidePre: Boolean = false): DocTag? = when (node) { - is TextNode -> Text(body = if (insidePre) node.wholeText else node.text()) - is Element -> createBlock(node) - else -> null - } + private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) { + "link", "linkplain" -> { + tag.referenceElement()?.toDocumentationLinkString(tag.dataElements.firstIsInstanceOrNull()) + } + "code", "literal" -> { + "${tag.text}" + } + "index" -> "${tag.children.filterIsInstance().joinToString { it.text }}" + else -> tag.text + } - private fun createBlock(element: Element): DocTag { - val children = element.childNodes().mapNotNull { convertHtmlNode(it) } - return when (element.tagName()) { - "p" -> P(listOf(Br, Br) + children) - "b" -> B(children) - "strong" -> Strong(children) - "i" -> I(children) - "em" -> Em(children) - "code" -> CodeBlock(children) - "pre" -> Pre(children) - "ul" -> Ul(children) - "ol" -> Ol(children) - "li" -> Li(children) - "a" -> createLink(element, children) - else -> Text(body = element.ownText()) + private fun createLink(element: Element, children: List): DocTag { + return when { + element.hasAttr("docref") -> + A(children, params = mapOf("docref" to element.attr("docref"))) + element.hasAttr("href") -> + A(children, params = mapOf("href" to element.attr("href"))) + element.hasAttr("data-dri") && driMap.containsKey(element.attr("data-dri")) -> + DocumentationLink(driMap[element.attr("data-dri")]!!, children) + else -> Text(children = children) + } } - } - private fun createLink(element: Element, children: List): DocTag { - return when { - element.hasAttr("docref") -> { - A(children, params = mapOf("docref" to element.attr("docref"))) + private fun createBlock(element: Element): DocTag? { + val children = element.childNodes().mapNotNull { convertHtmlNode(it) } + fun ifChildrenPresent(operation: () -> DocTag): DocTag? { + return if (children.isNotEmpty()) operation() else null } - element.hasAttr("href") -> { - A(children, params = mapOf("href" to element.attr("href"))) + return when (element.tagName()) { + "blockquote" -> ifChildrenPresent { BlockQuote(children) } + "p" -> ifChildrenPresent { P(children) } + "b" -> ifChildrenPresent { B(children) } + "strong" -> ifChildrenPresent { Strong(children) } + "index" -> Index(children) + "i" -> ifChildrenPresent { I(children) } + "em" -> Em(children) + "code" -> ifChildrenPresent { + if (element.hasAttr("data-inline")) CodeInline(children) else CodeBlock( + children + ) + } + "pre" -> Pre(children) + "ul" -> ifChildrenPresent { Ul(children) } + "ol" -> ifChildrenPresent { Ol(children) } + "li" -> Li(children) + "a" -> createLink(element, children) + else -> Text(body = element.ownText()) } - else -> Text(children = children) } + + private fun convertHtmlNode(node: Node, insidePre: Boolean = false): DocTag? = when (node) { + is TextNode -> Text(body = if (insidePre) node.wholeText else node.text()) + is Element -> createBlock(node) + else -> null + } + + override fun invoke(elements: Iterable, asParagraph: Boolean): List = + Jsoup.parseBodyFragment(elements.mapNotNull { it.stringify() }.joinToString(" ", prefix = if (asParagraph) "

" else "")) + .body().childNodes().mapNotNull { convertHtmlNode(it) } } + private fun PsiDocTag.contentElements(): List = + dataElements.mapNotNull { it.takeIf { it.text.isNotBlank() } } + + private fun convertJavadocElements(elements: Iterable, asParagraph: Boolean = true): List = Parse()(elements, asParagraph) + private fun PsiDocToken.isSharpToken() = tokenType.toString() == "DOC_TAG_VALUE_SHARP_TOKEN" + private fun PsiDocToken.isLeadingAsterisk() = tokenType.toString() == "DOC_COMMENT_LEADING_ASTERISKS" + private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null) = reference?.resolve()?.let { val dri = DRI.from(it) - val label = labelElement ?: children.firstOrNull { - it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken() - } ?: this - DocumentationLink(dri, convertJavadocElements(listOfNotNull(label))) - } - - private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) { - "link", "linkplain" -> { - tag.referenceElement()?.toDocumentationLink(tag.dataElements.firstIsInstanceOrNull()) + val label = labelElement ?: defaultLabel() + DocumentationLink(dri, convertJavadocElements(listOfNotNull(label), asParagraph = false)) } - "code", "literal" -> { - CodeInline(listOf(Text(tag.text))) - } - "index" -> Index(tag.children.filterIsInstance().map { Text(it.text) }) - else -> Text(tag.text) - } private fun PsiDocTag.referenceElement(): PsiElement? = linkElement()?.let { @@ -211,6 +253,10 @@ class JavadocParser( } } + private fun PsiElement.defaultLabel() = children.firstOrNull { + it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken() + } ?: this + private fun PsiDocTag.linkElement(): PsiElement? = valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace } } diff --git a/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt index a9689bc5..4ac5717d 100644 --- a/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt +++ b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt @@ -86,7 +86,7 @@ class ContentForParamsTest : AbstractCoreTest() { divergentGroup { divergentInstance { before { - pWrapped("comment to function") + group { pWrapped("comment to function") } } divergent { bareSignature( @@ -131,8 +131,8 @@ class ContentForParamsTest : AbstractCoreTest() { divergentGroup { divergentInstance { before { - unnamedTag("Author") { +"Kordyjan" } - unnamedTag("Since") { +"0.11" } + unnamedTag("Author") { group { +"Kordyjan" } } + unnamedTag("Since") { group { +"0.11" } } } divergent { bareSignature( @@ -178,9 +178,9 @@ class ContentForParamsTest : AbstractCoreTest() { divergentGroup { divergentInstance { before { - pWrapped("comment to function") - unnamedTag("Author") { +"Kordyjan" } - unnamedTag("Since") { +"0.11" } + group { pWrapped("comment to function") } + unnamedTag("Author") { group { +"Kordyjan" } } + unnamedTag("Since") { group { +"0.11" } } } divergent { bareSignature( @@ -225,14 +225,14 @@ class ContentForParamsTest : AbstractCoreTest() { divergentGroup { divergentInstance { before { - pWrapped("comment to function") + group { pWrapped("comment to function") } header(2) { +"Parameters" } group { platformHinted { table { group { +"abc" - group { +"comment to param" } + group { group { +"comment to param" } } } } } @@ -283,22 +283,22 @@ class ContentForParamsTest : AbstractCoreTest() { divergentGroup { divergentInstance { before { - pWrapped("comment to function") + group { group { group { +"comment to function" } } } header(2) { +"Parameters" } group { platformHinted { table { group { +"first" - group { +"comment to first param" } + group { group { +"comment to first param" } } } group { +"second" - group { +"comment to second param" } + group { group { +"comment to second param" } } } group { +"third" - group { +"comment to third param" } + group { group { +"comment to third param" } } } } } @@ -351,15 +351,15 @@ class ContentForParamsTest : AbstractCoreTest() { table { group { +"first" - group { +"comment to first param" } + group { group { +"comment to first param" } } } group { +"second" - group { +"comment to second param" } + group { group { +"comment to second param" } } } group { +"third" - group { +"comment to third param" } + group { group { +"comment to third param" } } } } } @@ -406,18 +406,18 @@ class ContentForParamsTest : AbstractCoreTest() { divergentGroup { divergentInstance { before { - pWrapped("comment to function") + group { pWrapped("comment to function") } header(2) { +"Parameters" } group { platformHinted { table { group { +"" - group { +"comment to receiver" } + group { group { +"comment to receiver" } } } group { +"abc" - group { +"comment to param" } + group { group { +"comment to param" } } } } } @@ -468,18 +468,18 @@ class ContentForParamsTest : AbstractCoreTest() { divergentGroup { divergentInstance { before { - pWrapped("comment to function") + group { group { group { +"comment to function" } } } header(2) { +"Parameters" } group { platformHinted { table { group { +"first" - group { +"comment to first param" } + group { group { +"comment to first param" } } } group { +"third" - group { +"comment to third param" } + group { group { +"comment to third param" } } } } } @@ -529,9 +529,9 @@ class ContentForParamsTest : AbstractCoreTest() { divergentGroup { divergentInstance { before { - pWrapped("comment to function") - unnamedTag("Author") { +"Kordyjan" } - unnamedTag("Since") { +"0.11" } + group { pWrapped("comment to function") } + unnamedTag("Author") { group { +"Kordyjan" } } + unnamedTag("Since") { group { +"0.11" } } header(2) { +"Parameters" } group { @@ -539,15 +539,15 @@ class ContentForParamsTest : AbstractCoreTest() { table { group { +"first" - group { +"comment to first param" } + group { group { +"comment to first param" } } } group { +"second" - group { +"comment to second param" } + group { group { +"comment to second param" } } } group { +"third" - group { +"comment to third param" } + group { group { +"comment to third param" } } } } } diff --git a/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt b/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt index 24970660..fd51c895 100644 --- a/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt +++ b/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt @@ -89,7 +89,7 @@ class ContentForSeeAlsoTest : AbstractCoreTest() { group { //DRI should be "test//abc/#/-1/" link { +"abc" } - group { } + group { group { } } } } } @@ -144,7 +144,9 @@ class ContentForSeeAlsoTest : AbstractCoreTest() { group { //DRI should be "test//abc/#/-1/" link { +"abc" } - group { +"Comment to abc" } + group { + group { +"Comment to abc" } + } } } } @@ -199,7 +201,9 @@ class ContentForSeeAlsoTest : AbstractCoreTest() { group { //DRI should be "kotlin.collections/Collection////" link { +"Collection" } - group { } + group { + group { } + } } } } @@ -254,7 +258,9 @@ class ContentForSeeAlsoTest : AbstractCoreTest() { group { //DRI should be "test//abc/#/-1/" link { +"Collection" } - group { +"Comment to stdliblink" } + group { + group { +"Comment to stdliblink" } + } } } } @@ -305,9 +311,9 @@ class ContentForSeeAlsoTest : AbstractCoreTest() { divergentGroup { divergentInstance { before { - pWrapped("random comment") - unnamedTag("Author") { +"pikinier20" } - unnamedTag("Since") { +"0.11" } + group { group { group { +"random comment"} } } + unnamedTag("Author") { group { +"pikinier20" } } + unnamedTag("Since") { group { +"0.11" } } header(2) { +"See also" } group { @@ -316,7 +322,9 @@ class ContentForSeeAlsoTest : AbstractCoreTest() { group { //DRI should be "test//abc/#/-1/" link { +"Collection" } - group { +"Comment to stdliblink" } + group { + group { +"Comment to stdliblink" } + } } } } @@ -372,7 +380,9 @@ class ContentForSeeAlsoTest : AbstractCoreTest() { group { //DRI should be "test//abc/#/-1/" link { +"abc" } - group { +"Comment to abc2" } + group { + group { +"Comment to abc2" } + } } } } @@ -428,12 +438,14 @@ class ContentForSeeAlsoTest : AbstractCoreTest() { group { //DRI should be "test//abc/#/-1/" link { +"abc" } - group { +"Comment to abc1" } + group { + group { +"Comment to abc1" } + } } group { //DRI should be "test//abc/#/-1/" link { +"Collection" } - group { +"Comment to collection" } + group { group { +"Comment to collection" } } } } } diff --git a/plugins/base/src/test/kotlin/content/signatures/SkippingParenthesisForConstructorsTest.kt b/plugins/base/src/test/kotlin/content/signatures/SkippingParenthesisForConstructorsTest.kt index 90a38055..b3da3f71 100644 --- a/plugins/base/src/test/kotlin/content/signatures/SkippingParenthesisForConstructorsTest.kt +++ b/plugins/base/src/test/kotlin/content/signatures/SkippingParenthesisForConstructorsTest.kt @@ -230,7 +230,7 @@ class ConstructorsSignaturesTest : AbstractCoreTest() { platformHinted { group { group { - +"ctor comment" + group { +"ctor comment" } } } group { diff --git a/plugins/base/src/test/kotlin/enums/EnumsTest.kt b/plugins/base/src/test/kotlin/enums/EnumsTest.kt index 6a973f8e..9cd41dcd 100644 --- a/plugins/base/src/test/kotlin/enums/EnumsTest.kt +++ b/plugins/base/src/test/kotlin/enums/EnumsTest.kt @@ -210,7 +210,9 @@ class EnumsTest : AbstractCoreTest() { platformHinted { group { group { - + "Sample docs for E1" + group { + +"Sample docs for E1" + } } } group { diff --git a/plugins/base/src/test/kotlin/transformers/CommentsToContentConverterTest.kt b/plugins/base/src/test/kotlin/transformers/CommentsToContentConverterTest.kt index 5197afc6..ad023d84 100644 --- a/plugins/base/src/test/kotlin/transformers/CommentsToContentConverterTest.kt +++ b/plugins/base/src/test/kotlin/transformers/CommentsToContentConverterTest.kt @@ -37,7 +37,7 @@ class CommentsToContentConverterTest { fun `simple text`() { val docTag = P(listOf(Text("This is simple test of string Next line"))) executeTest(docTag) { - +"This is simple test of string Next line" + group { +"This is simple test of string Next line" } } } @@ -51,9 +51,11 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - +"This is simple test of string" - node() - +"Next line" + group { + +"This is simple test of string" + node() + +"Next line" + } } } @@ -66,10 +68,14 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - +"Paragraph number one" - +"Paragraph" - node() - +"number two" + group { + group { +"Paragraph number one" } + group { + +"Paragraph" + node() + +"number two" + } + } } } @@ -122,20 +128,22 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - node { - group { +"Outer first Outer next line" } - group { +"Outer second" } + group { node { - group { +"Middle first Middle next line" } - group { +"Middle second" } + group { +"Outer first Outer next line" } + group { +"Outer second" } node { - group { +"Inner first Inner next line" } + group { +"Middle first Middle next line" } + group { +"Middle second" } + node { + group { +"Inner first Inner next line" } + } + group { +"Middle third" } } - group { +"Middle third" } + group { +"Outer third" } } - group { +"Outer third" } + group { +"New paragraph" } } - +"New paragraph" } } @@ -149,9 +157,11 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - header(1) { +"Header 1" } - +"Following text" - +"New paragraph" + group { + header(1) { +"Header 1" } + group { +"Following text" } + group { +"New paragraph" } + } } } @@ -174,18 +184,20 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - header(1) {+"Header 1"} - +"Text 1" - header(2) {+"Header 2"} - +"Text 2" - header(3) {+"Header 3"} - +"Text 3" - header(4) {+"Header 4"} - +"Text 4" - header(5) {+"Header 5"} - +"Text 5" - header(6) {+"Header 6"} - +"Text 6" + group { + header(1) { +"Header 1" } + group { +"Text 1" } + header(2) { +"Header 2" } + group { +"Text 2" } + header(3) { +"Header 3" } + group { +"Text 3" } + header(4) { +"Header 4" } + group { +"Text 4" } + header(5) { +"Header 5" } + group { +"Text 5" } + header(6) { +"Header 6" } + group { +"Text 6" } + } } } @@ -211,12 +223,14 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - node { - +"Blockquotes are very handy in email to emulate reply text. This line is part of the same quote." - } - +"Quote break." - node { - +"Quote" + group { + node { + +"Blockquotes are very handy in email to emulate reply text. This line is part of the same quote." + } + group { +"Quote break." } + node { + +"Quote" + } } } } @@ -245,16 +259,18 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - node { - +"text 1 text 2" + group { node { - +"text 3 text 4" + +"text 1 text 2" + node { + +"text 3 text 4" + } + +"text 5" + } + group { +"Quote break." } + node { + +"Quote" } - +"text 5" - } - +"Quote break." - node { - +"Quote" } } } @@ -278,21 +294,23 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - node { - +"val x: Int = 0" - node() - +"val y: String = \"Text\"" - node() - node() - +" val z: Boolean = true" - node() - +"for(i in 0..10) {" - node() - +" println(i)" - node() - +"}" + group { + node { + +"val x: Int = 0" + node() + +"val y: String = \"Text\"" + node() + node() + +" val z: Boolean = true" + node() + +"for(i in 0..10) {" + node() + +" println(i)" + node() + +"}" + } + group { +"Sample text" } } - +"Sample text" } } @@ -307,7 +325,7 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - link { + group { link { +"I'm an inline-style link" check { assertEquals( @@ -315,7 +333,7 @@ class CommentsToContentConverterTest { "https://www.google.com" ) } - } + } } } } @@ -384,20 +402,24 @@ class CommentsToContentConverterTest { ) ) executeTest(docTag) { - node { - group { +"Outer first Outer next line" } - group { +"Outer second" } + group { node { - group { +"Middle first Middle next line" } - group { +"Middle second" } + group { +"Outer first Outer next line" } + group { +"Outer second" } node { - +"Inner first Inner next line" + group { +"Middle first Middle next line" } + group { +"Middle second" } + node { + +"Inner first Inner next line" + } + group { +"Middle third" } } - group { +"Middle third" } + group { +"Outer third" } + } + group { + +"New paragraph" } - group { +"Outer third" } } - +"New paragraph" } } } \ No newline at end of file diff --git a/plugins/base/src/test/kotlin/translators/JavadocParserTest.kt b/plugins/base/src/test/kotlin/translators/JavadocParserTest.kt new file mode 100644 index 00000000..a1fbb2a0 --- /dev/null +++ b/plugins/base/src/test/kotlin/translators/JavadocParserTest.kt @@ -0,0 +1,156 @@ +package translators + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.childrenOfType +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.firstChildOfType +import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.* +import utils.text + +class JavadocParserTest : AbstractCoreTest() { + + private fun performJavadocTest(testOperation: (DModule) -> Unit) { + val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/main/java") + } + } + } + + testInline( + """ + |/src/main/java/sample/Date2.java + |/** + | * The class Date represents a specific instant + | * in time, with millisecond precision. + | *

+ | * Prior to JDK 1.1, the class Date had two additional + | * functions. It allowed the interpretation of dates as year, month, day, hour, + | * minute, and second values. It also allowed the formatting and parsing + | * of date strings. Unfortunately, the API for these functions was not + | * amenable to internationalization. As of JDK 1.1, the + | * Calendar class should be used to convert between dates and time + | * fields and the DateFormat class should be used to format and + | * parse date strings. + | * The corresponding methods in Date are deprecated. + | *

+ | * Although the Date class is intended to reflect + | * coordinated universal time (UTC), it may not do so exactly, + | * depending on the host environment of the Java Virtual Machine. + | * Nearly all modern operating systems assume that 1 day = + | * 24 × 60 × 60 = 86400 seconds + | * in all cases. In UTC, however, about once every year or two there + | * is an extra second, called a "leap second." The leap + | * second is always added as the last second of the day, and always + | * on December 31 or June 30. For example, the last minute of the + | * year 1995 was 61 seconds long, thanks to an added leap second. + | * Most computer clocks are not accurate enough to be able to reflect + | * the leap-second distinction. + | *

+ | * Some computer standards are defined in terms of Greenwich mean + | * time (GMT), which is equivalent to universal time (UT). GMT is + | * the "civil" name for the standard; UT is the + | * "scientific" name for the same standard. The + | * distinction between UTC and UT is that UTC is based on an atomic + | * clock and UT is based on astronomical observations, which for all + | * practical purposes is an invisibly fine hair to split. Because the + | * earth's rotation is not uniform (it slows down and speeds up + | * in complicated ways), UT does not always flow uniformly. Leap + | * seconds are introduced as needed into UTC so as to keep UTC within + | * 0.9 seconds of UT1, which is a version of UT with certain + | * corrections applied. There are other time and date systems as + | * well; for example, the time scale used by the satellite-based + | * global positioning system (GPS) is synchronized to UTC but is + | * not adjusted for leap seconds. An interesting source of + | * further information is the U.S. Naval Observatory, particularly + | * the Directorate of Time at: + | *

+            | *     http://tycho.usno.navy.mil
+            | * 
+ | *

+ | * and their definitions of "Systems of Time" at: + | *

+            | *     http://tycho.usno.navy.mil/systime.html
+            | * 
+ | *

+ | * In all methods of class Date that accept or return + | * year, month, date, hours, minutes, and seconds values, the + | * following representations are used: + | *

    + | *
  • A year y is represented by the integer + | * y - 1900. + | *
  • A month is represented by an integer from 0 to 11; 0 is January, + | * 1 is February, and so forth; thus 11 is December. + | *
  • A date (day of month) is represented by an integer from 1 to 31 + | * in the usual manner. + | *
  • An hour is represented by an integer from 0 to 23. Thus, the hour + | * from midnight to 1 a.m. is hour 0, and the hour from noon to 1 + | * p.m. is hour 12. + | *
  • A minute is represented by an integer from 0 to 59 in the usual manner. + | *
  • A second is represented by an integer from 0 to 61; the values 60 and + | * 61 occur only for leap seconds and even then only in Java + | * implementations that actually track leap seconds correctly. Because + | * of the manner in which leap seconds are currently introduced, it is + | * extremely unlikely that two leap seconds will occur in the same + | * minute, but this specification follows the date and time conventions + | * for ISO C. + | *
+ | *

+ | * In all cases, arguments given to methods for these purposes need + | * not fall within the indicated ranges; for example, a date may be + | * specified as January 32 and is interpreted as meaning February 1. + | * + | * @author James Gosling + | * @author Arthur van Hoff + | * @author Alan Liu + | * @see java.text.DateFormat + | * @see java.util.Calendar + | * @since JDK1.0 + | * @apiSince 1 + | */ + |public class Date2 implements java.io.Serializable, java.lang.Cloneable, java.lang.Comparable { + | void x() { } + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = testOperation + } + } + + @Test + fun `correctly parsed list`() { + performJavadocTest { module -> + val dateDescription = module.descriptionOf("Date2")!! + assertEquals(6, dateDescription.firstChildOfType

    ().children.filterIsInstance
  • ().size) + } + } + + @Test + fun `correctly parsed author tags`() { + performJavadocTest { module -> + val authors = module.findClasslike().documentation.values.single().childrenOfType() + assertEquals(3, authors.size) + assertEquals("James Gosling", authors[0].firstChildOfType().text()) + assertEquals("Arthur van Hoff", authors[1].firstChildOfType().text()) + assertEquals("Alan Liu", authors[2].firstChildOfType().text()) + } + } + + @Test + fun `correctly parsed see tags`() { + performJavadocTest { module -> + val sees = module.findClasslike().documentation.values.single().childrenOfType() + assertEquals(2, sees.size) + assertEquals(DRI("java.text", "DateFormat"), sees[0].address) + assertEquals("java.text.DateFormat", sees[0].name) + assertEquals(DRI("java.util", "Calendar"), sees[1].address) + assertEquals("java.util.Calendar", sees[1].name) + } + } +} diff --git a/plugins/base/src/test/kotlin/translators/utils.kt b/plugins/base/src/test/kotlin/translators/utils.kt index 96d3035a..71b4a28b 100644 --- a/plugins/base/src/test/kotlin/translators/utils.kt +++ b/plugins/base/src/test/kotlin/translators/utils.kt @@ -1,16 +1,40 @@ package translators -import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.* import org.jetbrains.dokka.model.doc.Description import org.jetbrains.dokka.model.doc.Text +import java.util.NoSuchElementException -fun DModule.documentationOf(className: String, functionName: String): String { - return (packages.single() - .classlikes.single { it.name == className } - .functions.single { it.name == functionName } - .documentation.values.singleOrNull() - ?.children?.singleOrNull() - .run { this as? Description } - ?.root?.children?.single() as? Text) +fun DModule.documentationOf(className: String, functionName: String? = null): String = + descriptionOf(className, functionName) + ?.firstChildOfType() ?.body.orEmpty() + +fun DModule.descriptionOf(className: String, functionName: String? = null): Description? { + val classlike = packages.single() + .classlikes.single { it.name == className } + val target: Documentable = + if (functionName != null) classlike.functions.single { it.name == functionName } else classlike + return target.documentation.values.singleOrNull() + ?.firstChildOfTypeOrNull() +} + +fun DModule.findPackage(packageName: String? = null) = + packageName?.let { packages.firstOrNull { pkg -> pkg.name == packageName } + ?: throw NoSuchElementException("No packageName with name $packageName") } ?: packages.single() + +fun DModule.findClasslike(packageName: String? = null, className: String? = null): DClasslike { + val pkg = findPackage(packageName) + return className?.let { + pkg.classlikes.firstOrNull { cls -> cls.name == className } + ?: throw NoSuchElementException("No classlike with name $className") + } ?: pkg.classlikes.single() +} + +fun DModule.findFunction(packageName: String? = null, className: String, functionName: String? = null): DFunction { + val classlike = findClasslike(packageName, className) + return functionName?.let { + classlike.functions.firstOrNull { fn -> fn.name == functionName } + ?: throw NoSuchElementException("No classlike with name $functionName") + } ?: classlike.functions.single() } \ No newline at end of file diff --git a/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/JavadocPageCreator.kt b/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/JavadocPageCreator.kt index 8b86fab0..b3bb49d3 100644 --- a/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/JavadocPageCreator.kt +++ b/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/JavadocPageCreator.kt @@ -66,7 +66,8 @@ open class JavadocPageCreator( ) }, documentable = c, - extra = ((c as? WithExtraProperties)?.extra ?: PropertyContainer.empty()) + c.indexesInDocumentation() + extra = ((c as? WithExtraProperties)?.extra + ?: PropertyContainer.empty()) + c.indexesInDocumentation() ) } @@ -142,6 +143,7 @@ open class JavadocPageCreator( dri = dri, signature = signatureForNode(this, jvm), brief = brief(jvm), + description = descriptionToContentNodes(jvm), parameters = parameters.mapNotNull { val signature = signatureForNode(it, jvm) signature.modifiers?.let { type -> @@ -194,16 +196,21 @@ open class JavadocPageCreator( briefFromContentNodes(descriptionToContentNodes(sourceSet)) private fun briefFromContentNodes(description: List): List { - val contents = mutableListOf() - for (node in description) { + var sentenceFound = false + fun lookthrough(node: ContentNode): ContentNode = if (node is ContentText && firstSentenceRegex.containsMatchIn(node.text)) { - contents.add(node.copy(text = firstSentenceRegex.find(node.text)?.value.orEmpty())) - break + sentenceFound = true + node.copy(text = firstSentenceRegex.find(node.text)?.value.orEmpty()) + } else if (node is ContentGroup) { + node.copy(children = node.children.mapNotNull { + if (!sentenceFound) lookthrough(it) else null + }, style = node.style - TextStyle.Paragraph) } else { - contents.add(node) + node } + return description.mapNotNull { + if (!sentenceFound) lookthrough(it) else null } - return contents } private fun DParameter.brief(sourceSet: DokkaSourceSet? = highestJvmSourceSet): List = diff --git a/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/location/JavadocLocationProvider.kt b/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/location/JavadocLocationProvider.kt index 7f27ff18..e0a7768c 100644 --- a/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/location/JavadocLocationProvider.kt +++ b/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/location/JavadocLocationProvider.kt @@ -66,7 +66,7 @@ class JavadocLocationProvider(pageRoot: RootPageNode, dokkaContext: DokkaContext override fun resolve(dri: DRI, sourceSets: Set, context: PageNode?): String { return nodeIndex[dri]?.let { resolve(it, context) } - ?: nodeIndex[dri.parent]?.let { + ?: nodeIndex[dri.parent]?.takeIf { it is JavadocClasslikePageNode }?.let { val anchor = when (val anchorElement = (it as? JavadocClasslikePageNode)?.findAnchorableByDRI(dri)) { is JavadocFunctionNode -> anchorElement.getAnchor() is JavadocEntryNode -> anchorElement.name @@ -80,7 +80,8 @@ class JavadocLocationProvider(pageRoot: RootPageNode, dokkaContext: DokkaContext private fun JavadocFunctionNode.getAnchor(): String = "$name(${parameters.joinToString(",") { - when (val bound = if (it.typeBound is org.jetbrains.dokka.model.Nullable) it.typeBound.inner else it.typeBound) { + when (val bound = + if (it.typeBound is org.jetbrains.dokka.model.Nullable) it.typeBound.inner else it.typeBound) { is TypeConstructor -> bound.dri.classNames.orEmpty() is OtherParameter -> bound.name is PrimitiveJavaType -> bound.name diff --git a/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/pages/JavadocPageNodes.kt b/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/pages/JavadocPageNodes.kt index 790e15c5..ec4591c3 100644 --- a/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/pages/JavadocPageNodes.kt +++ b/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/pages/JavadocPageNodes.kt @@ -115,6 +115,7 @@ data class JavadocPropertyNode( data class JavadocFunctionNode( val signature: JavadocSignatureContentNode, val brief: List, + val description: List, val parameters: List, val name: String, override val dri: DRI, diff --git a/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/renderer/JavadocContentToHtmlTranslator.kt b/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/renderer/JavadocContentToHtmlTranslator.kt index 906c9d5a..1e9a3d65 100644 --- a/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/renderer/JavadocContentToHtmlTranslator.kt +++ b/plugins/javadoc/src/main/kotlin/org/jetbrains/dokka/javadoc/renderer/JavadocContentToHtmlTranslator.kt @@ -14,37 +14,51 @@ internal class JavadocContentToHtmlTranslator( fun htmlForContentNode(node: ContentNode, relative: PageNode?): String = when (node) { - is ContentGroup -> htmlForContentNodes(node.children, node.style, relative) - is ContentText -> buildText(node) - is ContentDRILink -> buildLink( - locationProvider.resolve(node.address, node.sourceSets, relative), - htmlForContentNodes(node.children, node.style, relative) - ) - is ContentResolvedLink -> buildLink(node.address, htmlForContentNodes(node.children, node.style, relative)) - is ContentCode -> htmlForCode(node.children) + is ContentGroup -> + if (node.style.contains(TextStyle.Paragraph)) htmlForParagraph(node.children, relative) + else htmlForContentNodes(node.children, relative) + is ContentText -> htmlForText(node) + is ContentDRILink -> buildLinkFromNode(node, relative) + is ContentResolvedLink -> buildLinkFromNode(node, relative) + is ContentCode -> htmlForCode(node.children, relative) + is ContentList -> htmlForList(node.children, relative) is JavadocSignatureContentNode -> htmlForSignature(node, relative) + is ContentBreakLine -> "
    " else -> "" } - fun htmlForContentNodes(list: List, styles: Set