diff options
author | Marcin Aman <marcin.aman@gmail.com> | 2020-10-30 19:01:09 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-30 19:01:09 +0100 |
commit | 1aba0ec4973d7915caa93f1e9b3146ad82111903 (patch) | |
tree | b9424de4bc22f8453ecb32aaa8f7f020bcd49e9f /plugins/base/src/main | |
parent | da498f50eabfad8969eb7795a535e97f7e25ca58 (diff) | |
download | dokka-1aba0ec4973d7915caa93f1e9b3146ad82111903.tar.gz dokka-1aba0ec4973d7915caa93f1e9b3146ad82111903.tar.bz2 dokka-1aba0ec4973d7915caa93f1e9b3146ad82111903.zip |
Fix parsing first word in deprecated (#1595)
Fix parsing first word in `Deprecated` annotations, fix `Throws` and `See` tags
Diffstat (limited to 'plugins/base/src/main')
8 files changed, 323 insertions, 204 deletions
diff --git a/plugins/base/src/main/kotlin/parsers/HtmlParser.kt b/plugins/base/src/main/kotlin/parsers/HtmlParser.kt deleted file mode 100644 index ece3cf24..00000000 --- a/plugins/base/src/main/kotlin/parsers/HtmlParser.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.jetbrains.dokka.base.parsers - -import org.jetbrains.dokka.model.doc.* -import org.jetbrains.dokka.base.parsers.factories.DocTagsFromStringFactory -import org.jsoup.Jsoup -import org.jsoup.nodes.Node -import org.jsoup.select.NodeFilter -import org.jsoup.select.NodeTraversor - -class HtmlParser : Parser() { - - inner class NodeFilterImpl : NodeFilter { - - private val nodesCache: MutableMap<Int, MutableList<DocTag>> = mutableMapOf() - private var currentDepth = 0 - - fun collect(): DocTag = nodesCache[currentDepth]!![0] - - override fun tail(node: Node?, depth: Int): NodeFilter.FilterResult { - val nodeName = node!!.nodeName() - val nodeAttributes = node.attributes() - - if(nodeName in listOf("#document", "html", "head")) - return NodeFilter.FilterResult.CONTINUE - - val body: String - val params: Map<String, String> - - - if(nodeName != "#text") { - body = "" - params = nodeAttributes.map { it.key to it.value }.toMap() - } else { - body = nodeAttributes["#text"] - params = emptyMap() - } - - val docNode = if(depth < currentDepth) { - DocTagsFromStringFactory.getInstance(nodeName, nodesCache.getOrDefault(currentDepth, mutableListOf()).toList(), params, body).also { - nodesCache[currentDepth] = mutableListOf() - currentDepth = depth - } - } else { - DocTagsFromStringFactory.getInstance(nodeName, emptyList(), params, body) - } - - nodesCache.getOrDefault(depth, mutableListOf()) += docNode - return NodeFilter.FilterResult.CONTINUE - } - - override fun head(node: Node?, depth: Int): NodeFilter.FilterResult { - - val nodeName = node!!.nodeName() - - if(currentDepth < depth) { - currentDepth = depth - nodesCache[currentDepth] = mutableListOf() - } - - if(nodeName in listOf("#document", "html", "head")) - return NodeFilter.FilterResult.CONTINUE - - return NodeFilter.FilterResult.CONTINUE - } - } - - - private fun htmlToDocNode(string: String): DocTag { - val document = Jsoup.parse(string) - val nodeFilterImpl = NodeFilterImpl() - NodeTraversor.filter(nodeFilterImpl, document.root()) - return nodeFilterImpl.collect() - } - - private fun replaceLinksWithHrefs(javadoc: String): String = Regex("\\{@link .*?}").replace(javadoc) { - val split = it.value.dropLast(1).split(" ") - if(split.size !in listOf(2, 3)) - return@replace it.value - if(split.size == 3) - return@replace "<documentationlink href=\"${split[1]}\">${split[2]}</documentationlink>" - else - return@replace "<documentationlink href=\"${split[1]}\">${split[1]}</documentationlink>" - } - - override fun parseStringToDocNode(extractedString: String) = htmlToDocNode(extractedString) - override fun preparse(text: String) = replaceLinksWithHrefs(text) -} - - diff --git a/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt index e7a36d3f..f496d704 100644 --- a/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt +++ b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt @@ -38,6 +38,27 @@ open class MarkdownParser( override fun preparse(text: String) = text + override fun parseTagWithBody(tagName: String, content: String): TagWrapper = + when (tagName) { + "see" -> { + val referencedName = content.substringBefore(' ') + See( + parseStringToDocNode(content.substringAfter(' ')), + referencedName, + externalDri(referencedName) + ) + } + "throws", "exception" -> { + val dri = externalDri(content.substringBefore(' ')) + Throws( + parseStringToDocNode(content.substringAfter(' ')), + dri?.fqName() ?: content.substringBefore(' '), + dri + ) + } + else -> super.parseTagWithBody(tagName, content) + } + private fun headersHandler(node: ASTNode) = DocTagsFromIElementFactory.getInstance( node.type, @@ -424,6 +445,12 @@ open class MarkdownParser( DocumentationNode(emptyList()) } else { fun parseStringToDocNode(text: String) = MarkdownParser(externalDri).parseStringToDocNode(text) + + fun pointedLink(tag: KDocTag): DRI? = (parseStringToDocNode("[${tag.getSubjectName()}]")).let { + val link = it.children[0].children[0] + if (link is DocumentationLink) link.dri else null + } + DocumentationNode( (listOf(kDocTag) + getAllKDocTags(findParent(kDocTag))).map { when (it.knownTag) { @@ -432,14 +459,22 @@ open class MarkdownParser( it.name!! ) KDocKnownTag.AUTHOR -> Author(parseStringToDocNode(it.getContent())) - KDocKnownTag.THROWS -> Throws( - parseStringToDocNode(it.getContent()), - it.getSubjectName().orEmpty() - ) - KDocKnownTag.EXCEPTION -> Throws( - parseStringToDocNode(it.getContent()), - it.getSubjectName().orEmpty() - ) + KDocKnownTag.THROWS -> { + val dri = pointedLink(it) + Throws( + parseStringToDocNode(it.getContent()), + dri?.fqName() ?: it.getSubjectName().orEmpty(), + dri, + ) + } + KDocKnownTag.EXCEPTION -> { + val dri = pointedLink(it) + Throws( + parseStringToDocNode(it.getContent()), + dri?.fqName() ?: it.getSubjectName().orEmpty(), + dri + ) + } KDocKnownTag.PARAM -> Param( parseStringToDocNode(it.getContent()), it.getSubjectName().orEmpty() @@ -449,12 +484,7 @@ open class MarkdownParser( KDocKnownTag.SEE -> See( parseStringToDocNode(it.getContent()), it.getSubjectName().orEmpty(), - (parseStringToDocNode("[${it.getSubjectName()}]")) - .let { - val link = it.children[0].children[0] - if (link is DocumentationLink) link.dri - else null - } + pointedLink(it), ) KDocKnownTag.SINCE -> Since(parseStringToDocNode(it.getContent())) KDocKnownTag.CONSTRUCTOR -> Constructor(parseStringToDocNode(it.getContent())) @@ -473,6 +503,9 @@ open class MarkdownParser( } } + //Horrible hack but since link resolution is passed as a function i am not able to resolve them otherwise + fun DRI.fqName(): String = "$packageName.$classNames" + private fun findParent(kDoc: PsiElement): PsiElement = if (kDoc is KDocSection) findParent(kDoc.parent) else kDoc diff --git a/plugins/base/src/main/kotlin/parsers/Parser.kt b/plugins/base/src/main/kotlin/parsers/Parser.kt index 228cc88b..960d7a64 100644 --- a/plugins/base/src/main/kotlin/parsers/Parser.kt +++ b/plugins/base/src/main/kotlin/parsers/Parser.kt @@ -8,47 +8,44 @@ abstract class Parser { abstract fun preparse(text: String): String - fun parse(text: String): DocumentationNode { - - val list = jkdocToListOfPairs(preparse(text)) - - val mappedList: List<TagWrapper> = list.map { - when (it.first) { - "description" -> Description(parseStringToDocNode(it.second)) - "author" -> Author(parseStringToDocNode(it.second)) - "version" -> Version(parseStringToDocNode(it.second)) - "since" -> Since(parseStringToDocNode(it.second)) - "see" -> See( - parseStringToDocNode(it.second.substringAfter(' ')), - it.second.substringBefore(' '), - null - ) - "param" -> Param( - parseStringToDocNode(it.second.substringAfter(' ')), - it.second.substringBefore(' ') - ) - "property" -> Property( - parseStringToDocNode(it.second.substringAfter(' ')), - it.second.substringBefore(' ') - ) - "return" -> Return(parseStringToDocNode(it.second)) - "constructor" -> Constructor(parseStringToDocNode(it.second)) - "receiver" -> Receiver(parseStringToDocNode(it.second)) - "throws", "exception" -> Throws( - parseStringToDocNode(it.second.substringAfter(' ')), - it.second.substringBefore(' ') - ) - "deprecated" -> Deprecated(parseStringToDocNode(it.second)) - "sample" -> Sample( - parseStringToDocNode(it.second.substringAfter(' ')), - it.second.substringBefore(' ') - ) - "suppress" -> Suppress(parseStringToDocNode(it.second)) - else -> CustomTagWrapper(parseStringToDocNode(it.second), it.first) - } + open fun parse(text: String): DocumentationNode = + DocumentationNode(jkdocToListOfPairs(preparse(text)).map { (tag, content) -> parseTagWithBody(tag, content) }) + + open fun parseTagWithBody(tagName: String, content: String): TagWrapper = + when (tagName) { + "description" -> Description(parseStringToDocNode(content)) + "author" -> Author(parseStringToDocNode(content)) + "version" -> Version(parseStringToDocNode(content)) + "since" -> Since(parseStringToDocNode(content)) + "see" -> See( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' '), + null + ) + "param" -> Param( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' ') + ) + "property" -> Property( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' ') + ) + "return" -> Return(parseStringToDocNode(content)) + "constructor" -> Constructor(parseStringToDocNode(content)) + "receiver" -> Receiver(parseStringToDocNode(content)) + "throws", "exception" -> Throws( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' '), + null + ) + "deprecated" -> Deprecated(parseStringToDocNode(content)) + "sample" -> Sample( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' ') + ) + "suppress" -> Suppress(parseStringToDocNode(content)) + else -> CustomTagWrapper(parseStringToDocNode(content), tagName) } - return DocumentationNode(mappedList) - } private fun jkdocToListOfPairs(javadoc: String): List<Pair<String, String>> = "description $javadoc" diff --git a/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt b/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt index d05979e3..a3a9ad6a 100644 --- a/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt +++ b/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt @@ -6,6 +6,7 @@ import org.jetbrains.dokka.model.doc.* import org.jetbrains.dokka.model.properties.PropertyContainer import org.jetbrains.dokka.model.toDisplaySourceSets import org.jetbrains.dokka.pages.* +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull open class DocTagToContentConverter : CommentsToContentConverter { override fun buildContent( @@ -148,15 +149,42 @@ open class DocTagToContentConverter : CommentsToContentConverter { ) ) is Strikethrough -> buildChildren(docTag, setOf(TextStyle.Strikethrough)) - is Table -> listOf( - ContentTable( - buildTableRows(docTag.children.filterIsInstance<Th>(), CommentTable), - buildTableRows(docTag.children.filterIsInstance<Tr>(), CommentTable), - dci, - sourceSets.toDisplaySourceSets(), - styles + CommentTable - ) - ) + is Table -> { + //https://html.spec.whatwg.org/multipage/tables.html#the-caption-element + if (docTag.children.any { it is TBody }) { + val head = docTag.children.filterIsInstance<THead>().flatMap { it.children } + val body = docTag.children.filterIsInstance<TBody>().flatMap { it.children } + listOf( + ContentTable( + header = buildTableRows(head.filterIsInstance<Th>(), CommentTable), + caption = docTag.children.firstIsInstanceOrNull<Caption>()?.let { + ContentGroup( + buildContent(it, dci, sourceSets), + dci, + sourceSets.toDisplaySourceSets(), + styles, + extra + ) + }, + buildTableRows(body.filterIsInstance<Tr>(), CommentTable), + dci, + sourceSets.toDisplaySourceSets(), + styles + CommentTable + ) + ) + } else { + listOf( + ContentTable( + header = buildTableRows(docTag.children.filterIsInstance<Th>(), CommentTable), + caption = null, + buildTableRows(docTag.children.filterIsInstance<Tr>(), CommentTable), + dci, + sourceSets.toDisplaySourceSets(), + styles + CommentTable + ) + ) + } + } is Th, is Tr -> listOf( ContentGroup( @@ -190,6 +218,15 @@ open class DocTagToContentConverter : CommentsToContentConverter { } else { buildChildren(docTag) } + is Caption -> listOf( + ContentGroup( + buildChildren(docTag), + dci, + sourceSets.toDisplaySourceSets(), + styles + ContentStyle.Caption, + extra = extra + ) + ) else -> buildChildren(docTag) } diff --git a/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt index 069d1125..2c6d301c 100644 --- a/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt @@ -62,14 +62,14 @@ class SourceLinksTransformer(val context: DokkaContext, val builder: PageContent ) { header(2, "Sources", kind = ContentKind.Source) +ContentTable( - emptyList(), - sources.map { + header = emptyList(), + children = sources.map { buildGroup(node.dri, setOf(it.first), kind = ContentKind.Source, extra = mainExtra + SymbolAnchorHint(it.second, ContentKind.Source)) { link("(source)", it.second) } }, - DCI(node.dri, ContentKind.Source), - node.documentable!!.sourceSets.toDisplaySourceSets(), + dci = DCI(node.dri, ContentKind.Source), + sourceSets = node.documentable!!.sourceSets.toDisplaySourceSets(), style = emptySet(), extra = mainExtra + SimpleAttr.header("Sources") ) diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt index b2a9d5d2..e0ced0aa 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -179,10 +179,10 @@ open class DefaultPageCreator( if (map.values.any()) { header(2, "Inheritors") { } +ContentTable( - listOf(contentBuilder.contentFor(mainDRI, mainSourcesetData) { + header = listOf(contentBuilder.contentFor(mainDRI, mainSourcesetData) { text("Name") }), - map.entries.flatMap { entry -> entry.value.map { Pair(entry.key, it) } } + children = map.entries.flatMap { entry -> entry.value.map { Pair(entry.key, it) } } .groupBy({ it.second }, { it.first }).map { (classlike, platforms) -> val label = classlike.classNames?.substringBeforeLast(".") ?: classlike.toString() .also { logger.warn("No class name found for DRI $classlike") } @@ -190,8 +190,8 @@ open class DefaultPageCreator( link(label, classlike) } }, - DCI(setOf(dri), ContentKind.Inheritors), - sourceSets.toDisplaySourceSets(), + dci = DCI(setOf(dri), ContentKind.Inheritors), + sourceSets = sourceSets.toDisplaySourceSets(), style = emptySet(), extra = mainExtra + SimpleAttr.header("Inheritors") ) @@ -429,6 +429,30 @@ open class DefaultPageCreator( } } } + fun DocumentableContentBuilder.contentForThrows() { + val throws = tags.withTypeNamed<Throws>() + if (throws.isNotEmpty()) { + header(4, "Throws") + sourceSetDependentHint(sourceSets = platforms.toSet(), kind = ContentKind.SourceSetDependentHint) { + platforms.forEach { sourceset -> + table(kind = ContentKind.Main, sourceSets = setOf(sourceset)) { + throws.entries.mapNotNull { entry -> + entry.value[sourceset]?.let { throws -> + buildGroup(sourceSets = setOf(sourceset)) { + group(styles = mainStyles + ContentStyle.RowTitle) { + throws.exceptionAddress?.let { + link(text = entry.key, address = it) + } ?: text(entry.key) + } + comment(throws.root) + } + } + } + } + } + } + } + } fun DocumentableContentBuilder.contentForSamples() { val samples = tags.withTypeNamed<Sample>() @@ -461,6 +485,7 @@ open class DefaultPageCreator( contentForSamples() contentForSeeAlso() contentForParams() + contentForThrows() } }.children } diff --git a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt index 1865f276..9fee60cb 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt @@ -154,6 +154,7 @@ open class PageContentBuilder( ) { contents += ContentTable( defaultHeaders, + null, operation(), DCI(mainDRI, kind), sourceSets.toDisplaySourceSets(), styles, extra @@ -177,8 +178,8 @@ open class PageContentBuilder( if (renderWhenEmpty || elements.any()) { header(level, name, kind = kind) { } contents += ContentTable( - headers ?: defaultHeaders, - elements + header = headers ?: defaultHeaders, + children = elements .let { if (needsSorting) it.sortedWith(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)) { it.name }) @@ -190,8 +191,10 @@ open class PageContentBuilder( operation(it) } }, - DCI(mainDRI, kind), - sourceSets.toDisplaySourceSets(), styles, extra + dci = DCI(mainDRI, kind), + sourceSets = sourceSets.toDisplaySourceSets(), + style = styles, + extra = extra ) } } diff --git a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt index fad5ff36..782f792a 100644 --- a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt +++ b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt @@ -8,12 +8,16 @@ import com.intellij.psi.javadoc.* import com.intellij.psi.util.PsiTreeUtil import org.intellij.markdown.MarkdownElementTypes import org.jetbrains.dokka.analysis.from +import org.jetbrains.dokka.base.parsers.factories.DocTagsFromStringFactory import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.doc.* import org.jetbrains.dokka.model.doc.Deprecated import org.jetbrains.dokka.utilities.DokkaLogger import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace +import org.jetbrains.kotlin.psi.psiUtil.siblings +import org.jetbrains.kotlin.tools.projectWizard.core.ParsingState import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull import org.jetbrains.kotlin.utils.addToStdlib.safeAs import org.jsoup.Jsoup @@ -36,12 +40,27 @@ class JavadocParser( nodes.addAll(docComment.tags.mapNotNull { tag -> when (tag.name) { "param" -> Param( - wrapTagIfNecessary(convertJavadocElements(tag.contentElements())), - tag.children.firstIsInstanceOrNull<PsiDocParamRef>()?.text.orEmpty() + wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded().drop(1))), + tag.dataElements.firstOrNull()?.text.orEmpty() ) - "throws" -> Throws(wrapTagIfNecessary(convertJavadocElements(tag.contentElements())), tag.text) - "return" -> Return(wrapTagIfNecessary(convertJavadocElements(tag.contentElements()))) - "author" -> Author(wrapTagIfNecessary(convertJavadocElements(tag.authorContentElements()))) // Workaround: PSI returns first word after @author tag as a `DOC_TAG_VALUE_ELEMENT`, then the rest as a `DOC_COMMENT_DATA`, so for `Name Surname` we get them parted + "throws" -> { + val resolved = tag.resolveException() + val dri = resolved?.let { DRI.from(it) } + Throws( + root = wrapTagIfNecessary(convertJavadocElements(tag.dataElements.drop(1))), + /* we always would like to have a fully qualified name as name, + * because it will be used as a display name later and we would like to have those unified + * even if documentation states shortened version + * + * Only if dri search fails we should use the provided phrase (since then we are not able to get a fq name) + * */ + name = resolved?.getKotlinFqName()?.asString() + ?: tag.dataElements.firstOrNull()?.text.orEmpty(), + exceptionAddress = dri + ) + } + "return" -> Return(wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded()))) + "author" -> Author(wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded()))) // Workaround: PSI returns first word after @author tag as a `DOC_TAG_VALUE_ELEMENT`, then the rest as a `DOC_COMMENT_DATA`, so for `Name Surname` we get them parted "see" -> getSeeTagElementContent(tag).let { See( wrapTagIfNecessary(it.first), @@ -49,13 +68,16 @@ class JavadocParser( it.second ) } - "deprecated" -> Deprecated(wrapTagIfNecessary(convertJavadocElements(tag.dataElements.toList()))) + "deprecated" -> Deprecated(wrapTagIfNecessary(convertJavadocElements(tag.contentElementsWithSiblingIfNeeded()))) else -> null } }) return DocumentationNode(nodes) } + private fun PsiDocTag.resolveException(): PsiElement? = + dataElements.firstOrNull()?.firstChild?.referenceElementOrSelf()?.resolveToGetDri() + private fun wrapTagIfNecessary(list: List<DocTag>): CustomDocTag = if (list.size == 1 && (list.first() as? CustomDocTag)?.name == MarkdownElementTypes.MARKDOWN_FILE.name) list.first() as CustomDocTag @@ -145,20 +167,99 @@ class JavadocParser( } } + private data class ParserState( + val previousElement: PsiElement? = null, + val openPreTags: Int = 0, + val closedPreTags: Int = 0 + ) + + private data class ParsingResult(val newState: ParserState = ParserState(), val parsedLine: String? = null) { + operator fun plus(other: ParsingResult): ParsingResult = + ParsingResult( + other.newState, + listOfNotNull(parsedLine, other.parsedLine).joinToString(separator = "") + ) + } + private inner class Parse : (Iterable<PsiElement>, Boolean) -> List<DocTag> { val driMap = mutableMapOf<String, DRI>() - private fun PsiElement.stringify(): String? = when (this) { - is PsiReference -> children.joinToString("") { it.stringify().orEmpty() } - is PsiInlineDocTag -> convertInlineDocTag(this) - is PsiDocParamRef -> toDocumentationLinkString() - is PsiDocTagValue, - is LeafPsiElement -> text.let { - if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it?.drop(1) else it - }.let { - if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it?.dropLastWhile { it == ' ' } else it + private fun PsiElement.stringify(state: ParserState): ParsingResult = + when (this) { + is PsiReference -> children.fold(ParsingResult(state)) { acc, e -> acc + e.stringify(acc.newState) } + else -> stringifySimpleElement(state) } - else -> null + + private fun PsiElement.stringifySimpleElement(state: ParserState): ParsingResult { + val openPre = state.openPreTags + "<pre(\\s+.*)?>".toRegex().findAll(text).toList().size + val closedPre = state.closedPreTags + "</pre>".toRegex().findAll(text).toList().size + val isInsidePre = openPre > closedPre + val parsed = when (this) { + is PsiInlineDocTag -> convertInlineDocTag(this) + is PsiDocParamRef -> toDocumentationLinkString() + is PsiDocTagValue, + is LeafPsiElement -> { + if (isInsidePre) { + /* + For values in the <pre> tag we try to keep formatting, so only the leading space is trimmed, + since it is there because it separates this line from the leading asterisk + */ + text.let { + if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true && it.firstOrNull() == ' ') it.drop(1) else it + }.let { + if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it.dropLastWhile { it == ' ' } else it + } + } else { + /* + Outside of the <pre> we would like to trim everything from the start and end of a line since + javadoc doesn't care about it. + */ + text.let { + if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text != " " && state.previousElement !is PsiInlineDocTag) it?.trimStart() else it + }?.let { + if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text != " ") it.trimEnd() else it + }?.let { + if (shouldHaveSpaceAtTheEnd()) "$it " else it + } + } + } + else -> null + } + val previousElement = if (text.trim() == "") state.previousElement else this + return ParsingResult( + state.copy( + previousElement = previousElement, + closedPreTags = closedPre, + openPreTags = openPre + ), parsed + ) + } + + /** + * We would like to know if we need to have a space after a this tag + * + * The space is required when: + * - tag spans multiple lines, between every line we would need a space + * + * We wouldn't like to render a space if: + * - tag is followed by an end of comment + * - after a tag there is another tag (eg. multiple @author tags) + * - they end with an html tag like: <a href="...">Something</a> since then the space will be displayed in the following text + * - next line starts with a <p> or <pre> token + */ + private fun PsiElement.shouldHaveSpaceAtTheEnd(): Boolean { + val siblings = siblings(withItself = false).toList().filterNot { it.text.trim() == "" } + val nextNotEmptySibling = (siblings.firstOrNull() as? PsiDocToken) + val furtherNotEmptySibling = + (siblings.drop(1).firstOrNull { it is PsiDocToken && !it.isLeadingAsterisk() } as? PsiDocToken) + val lastHtmlTag = text.trim().substringAfterLast("<") + val endsWithAnUnclosedTag = lastHtmlTag.endsWith(">") && !lastHtmlTag.startsWith("</") + + return (nextSibling as? PsiWhiteSpace)?.text == "\n " && + (getNextSiblingIgnoringWhitespace() as? PsiDocToken)?.tokenType?.toString() != END_COMMENT_TYPE && + nextNotEmptySibling?.isLeadingAsterisk() == true && + furtherNotEmptySibling?.tokenType?.toString() == COMMENT_TYPE && + !endsWithAnUnclosedTag } private fun PsiElement.toDocumentationLinkString( @@ -172,7 +273,7 @@ class JavadocParser( dri.toString() } ?: UNRESOLVED_PSI_ELEMENT - return """<a data-dri=$dri>${label.joinToString(" ") { it.text }}</a>""" + return """<a data-dri="$dri">${label.joinToString(" ") { it.text }}</a>""" } private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) { @@ -216,6 +317,13 @@ class JavadocParser( "ol" -> ifChildrenPresent { Ol(children) } "li" -> Li(children) "a" -> createLink(element, children) + "table" -> ifChildrenPresent { Table(children) } + "tr" -> ifChildrenPresent { Tr(children) } + "td" -> Td(children) + "thead" -> THead(children) + "tbody" -> TBody(children) + "tfoot" -> TFoot(children) + "caption" -> ifChildrenPresent { Caption(children) } else -> Text(body = element.ownText()) } } @@ -228,23 +336,24 @@ class JavadocParser( } override fun invoke(elements: Iterable<PsiElement>, asParagraph: Boolean): List<DocTag> = - Jsoup.parseBodyFragment(elements.mapNotNull { it.stringify() }.dropWhile { it.isBlank() } - .dropLastWhile { it.isBlank() }.joinToString( - "", - prefix = if (asParagraph) "<p>" else "", - postfix = if (asParagraph) "</p>" else "" - ) - ).body().childNodes().mapNotNull { convertHtmlNode(it) } + elements.fold(ParsingResult()) { acc, e -> + acc + e.stringify(acc.newState) + }.parsedLine?.let { + val trimmed = it.trim() + val toParse = if (asParagraph) "<p>$trimmed</p>" else trimmed + Jsoup.parseBodyFragment(toParse).body().childNodes().mapNotNull { convertHtmlNode(it) } + }.orEmpty() } - private fun PsiDocTag.contentElements(): List<PsiElement> = - dataElements.mapNotNull { it.takeIf { it is PsiDocToken && it.text.isNotBlank() } } - - private fun PsiDocTag.authorContentElements(): List<PsiElement> = listOfNotNull( - dataElements[0], - dataElements[0].nextSibling?.takeIf { it.text != dataElements.drop(1).firstOrNull()?.text }, - *dataElements.drop(1).toTypedArray() - ) + private fun PsiDocTag.contentElementsWithSiblingIfNeeded(): List<PsiElement> = if (dataElements.isNotEmpty()) { + listOfNotNull( + dataElements[0], + dataElements[0].nextSibling?.takeIf { it.text != dataElements.drop(1).firstOrNull()?.text }, + *dataElements.drop(1).toTypedArray() + ) + } else { + emptyList() + } private fun convertJavadocElements(elements: Iterable<PsiElement>, asParagraph: Boolean = true): List<DocTag> = Parse()(elements, asParagraph) @@ -254,20 +363,22 @@ class JavadocParser( private fun PsiDocToken.isLeadingAsterisk() = tokenType.toString() == "DOC_COMMENT_LEADING_ASTERISKS" private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null) = - reference?.resolve()?.let { + resolveToGetDri()?.let { val dri = DRI.from(it) val label = labelElement ?: defaultLabel() DocumentationLink(dri, convertJavadocElements(listOfNotNull(label), asParagraph = false)) } + private fun PsiElement.resolveToGetDri(): PsiElement? = + reference?.resolve() + private fun PsiDocTag.referenceElement(): PsiElement? = - linkElement()?.let { - if (it.node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) { - PsiTreeUtil.findChildOfType(it, PsiJavaCodeReferenceElement::class.java) - } else { - it - } - } + linkElement()?.referenceElementOrSelf() + + private fun PsiElement.referenceElementOrSelf(): PsiElement? = + if (node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) { + PsiTreeUtil.findChildOfType(this, PsiJavaCodeReferenceElement::class.java) + } else this private fun PsiElement.defaultLabel() = children.firstOrNull { it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken() @@ -278,5 +389,7 @@ class JavadocParser( companion object { private const val UNRESOLVED_PSI_ELEMENT = "UNRESOLVED_PSI_ELEMENT" + private const val END_COMMENT_TYPE = "DOC_COMMENT_END" + private const val COMMENT_TYPE = "DOC_COMMENT_DATA" } } |