diff options
Diffstat (limited to 'plugins/base/src/main')
79 files changed, 9649 insertions, 0 deletions
diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt new file mode 100644 index 00000000..6586f2dc --- /dev/null +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -0,0 +1,225 @@ +@file:Suppress("unused") + +package org.jetbrains.dokka.base + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.analysis.KotlinAnalysis +import org.jetbrains.dokka.base.allModulePage.MultimodulePageCreator +import org.jetbrains.dokka.base.renderers.* +import org.jetbrains.dokka.base.renderers.html.* +import org.jetbrains.dokka.base.signatures.KotlinSignatureProvider +import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.resolvers.external.* +import org.jetbrains.dokka.base.resolvers.local.DefaultLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory +import org.jetbrains.dokka.base.transformers.documentables.* +import org.jetbrains.dokka.base.transformers.documentables.DefaultDocumentableMerger +import org.jetbrains.dokka.base.transformers.documentables.ModuleAndPackageDocumentationTransformer +import org.jetbrains.dokka.base.transformers.documentables.ReportUndocumentedTransformer +import org.jetbrains.dokka.base.transformers.pages.annotations.SinceKotlinTransformer +import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.base.transformers.pages.comments.DocTagToContentConverter +import org.jetbrains.dokka.base.transformers.pages.merger.FallbackPageMergerStrategy +import org.jetbrains.dokka.base.transformers.pages.merger.PageMerger +import org.jetbrains.dokka.base.transformers.pages.merger.PageMergerStrategy +import org.jetbrains.dokka.base.transformers.pages.merger.SameMethodNamePageMergerStrategy +import org.jetbrains.dokka.base.transformers.pages.samples.DefaultSamplesTransformer +import org.jetbrains.dokka.base.transformers.pages.sourcelinks.SourceLinksTransformer +import org.jetbrains.dokka.base.translators.descriptors.DefaultDescriptorToDocumentableTranslator +import org.jetbrains.dokka.base.translators.documentables.DefaultDocumentableToPageTranslator +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.base.translators.psi.DefaultPsiToDocumentableTranslator +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.transformers.pages.PageTransformer + +class DokkaBase : DokkaPlugin() { + val pageMergerStrategy by extensionPoint<PageMergerStrategy>() + val commentsToContentConverter by extensionPoint<CommentsToContentConverter>() + val signatureProvider by extensionPoint<SignatureProvider>() + val locationProviderFactory by extensionPoint<LocationProviderFactory>() + val externalLocationProviderFactory by extensionPoint<ExternalLocationProviderFactory>() + val outputWriter by extensionPoint<OutputWriter>() + val htmlPreprocessors by extensionPoint<PageTransformer>() + val kotlinAnalysis by extensionPoint<KotlinAnalysis>() + val tabSortingStrategy by extensionPoint<TabSortingStrategy>() + + + val descriptorToDocumentableTranslator by extending { + CoreExtensions.sourceToDocumentableTranslator providing { ctx -> + DefaultDescriptorToDocumentableTranslator(ctx.single(kotlinAnalysis)) + } + } + + val psiToDocumentableTranslator by extending { + CoreExtensions.sourceToDocumentableTranslator providing { ctx -> + DefaultPsiToDocumentableTranslator(ctx.single(kotlinAnalysis)) + } + } + + val documentableMerger by extending { + CoreExtensions.documentableMerger with DefaultDocumentableMerger + } + + val deprecatedDocumentableFilter by extending { + CoreExtensions.preMergeDocumentableTransformer providing ::DeprecatedDocumentableFilterTransformer + } + + val documentableVisbilityFilter by extending { + CoreExtensions.preMergeDocumentableTransformer providing ::DocumentableVisibilityFilterTransformer + } + + val emptyPackagesFilter by extending { + CoreExtensions.preMergeDocumentableTransformer providing ::EmptyPackagesFilterTransformer order { + after(deprecatedDocumentableFilter, documentableVisbilityFilter) + } + } + + val actualTypealiasAdder by extending { + CoreExtensions.documentableTransformer with ActualTypealiasAdder() + } + + val modulesAndPackagesDocumentation by extending { + CoreExtensions.preMergeDocumentableTransformer providing { ctx -> + ModuleAndPackageDocumentationTransformer(ctx, ctx.single(kotlinAnalysis)) + } + } + + val kotlinSignatureProvider by extending { + signatureProvider providing { ctx -> + KotlinSignatureProvider(ctx.single(commentsToContentConverter), ctx.logger) + } + } + + val sinceKotlinTransformer by extending { + CoreExtensions.documentableTransformer providing ::SinceKotlinTransformer + } + + val inheritorsExtractor by extending { + CoreExtensions.documentableTransformer with InheritorsExtractorTransformer() + } + + + val undocumentedCodeReporter by extending { + CoreExtensions.documentableTransformer with ReportUndocumentedTransformer() + } + + val extensionsExtractor by extending { + CoreExtensions.documentableTransformer with ExtensionExtractorTransformer() + } + + val documentableToPageTranslator by extending { + CoreExtensions.documentableToPageTranslator providing { ctx -> + DefaultDocumentableToPageTranslator( + ctx.single(commentsToContentConverter), + ctx.single(signatureProvider), + ctx.logger + ) + } + } + + val docTagToContentConverter by extending { + commentsToContentConverter with DocTagToContentConverter + } + + val pageMerger by extending { + CoreExtensions.pageTransformer providing { ctx -> PageMerger(ctx[pageMergerStrategy]) } + } + + val fallbackMerger by extending { + pageMergerStrategy providing { ctx -> FallbackPageMergerStrategy(ctx.logger) } + } + + val sameMethodNameMerger by extending { + pageMergerStrategy providing { ctx -> SameMethodNamePageMergerStrategy(ctx.logger) } order { + before(fallbackMerger) + } + } + + val defaultTabSortingStrategy by extending { + tabSortingStrategy with DefaultTabSortingStrategy() + } + + val htmlRenderer by extending { + CoreExtensions.renderer providing ::HtmlRenderer + } + + + val defaultKotlinAnalysis by extending { + kotlinAnalysis providing { ctx -> KotlinAnalysis(ctx) } + } + + val locationProvider by extending { + locationProviderFactory providing ::DefaultLocationProviderFactory + } + + val javadocLocationProvider by extending { + externalLocationProviderFactory with JavadocExternalLocationProviderFactory() + } + + val dokkaLocationProvider by extending { + externalLocationProviderFactory with DokkaExternalLocationProviderFactory() + } + + val fileWriter by extending { + outputWriter providing ::FileWriter + } + + val rootCreator by extending { + htmlPreprocessors with RootCreator + } + + val defaultSamplesTransformer by extending { + CoreExtensions.pageTransformer providing ::DefaultSamplesTransformer order { + before(pageMerger) + } + } + + val sourceLinksTransformer by extending { + htmlPreprocessors providing { + SourceLinksTransformer( + it, + PageContentBuilder( + it.single(commentsToContentConverter), + it.single(signatureProvider), + it.logger + ) + ) + } order { after(rootCreator) } + } + + val navigationPageInstaller by extending { + htmlPreprocessors with NavigationPageInstaller order { after(rootCreator) } + } + + val searchPageInstaller by extending { + htmlPreprocessors with SearchPageInstaller order { after(rootCreator) } + } + + val resourceInstaller by extending { + htmlPreprocessors with ResourceInstaller order { after(rootCreator) } + } + + val styleAndScriptsAppender by extending { + htmlPreprocessors with StyleAndScriptsAppender order { after(rootCreator) } + } + + val packageListCreator by extending { + htmlPreprocessors providing { + PackageListCreator( + it, + "html", + "html" + ) + } order { after(rootCreator) } + } + + val sourcesetDependencyAppender by extending { + htmlPreprocessors providing ::SourcesetDependencyAppender order { after(rootCreator) } + } + + val allModulePageCreators by extending { + CoreExtensions.allModulePageCreator providing { + MultimodulePageCreator(it) + } + } +} diff --git a/plugins/base/src/main/kotlin/allModulePage/MultimodulePageCreator.kt b/plugins/base/src/main/kotlin/allModulePage/MultimodulePageCreator.kt new file mode 100644 index 00000000..de2242f2 --- /dev/null +++ b/plugins/base/src/main/kotlin/allModulePage/MultimodulePageCreator.kt @@ -0,0 +1,81 @@ +package org.jetbrains.dokka.base.allModulePage + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.DokkaException +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.local.MultimoduleLocationProvider.Companion.MULTIMODULE_PACKAGE_PLACEHOLDER +import org.jetbrains.dokka.base.transformers.pages.comments.DocTagToContentConverter +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.P +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.base.parsers.MarkdownParser +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.pages.PageCreator +import org.jetbrains.dokka.utilities.DokkaLogger +import java.io.File + +class MultimodulePageCreator( + val context: DokkaContext +) : PageCreator { + private val logger: DokkaLogger = context.logger + + override fun invoke(): RootPageNode { + val parser = MarkdownParser(logger = logger) + val modules = context.configuration.modules + modules.forEach(::throwOnMissingModuleDocFile) + + val commentsConverter = context.plugin(DokkaBase::class)?.querySingle { commentsToContentConverter } + val signatureProvider = context.plugin(DokkaBase::class)?.querySingle { signatureProvider } + if (commentsConverter == null || signatureProvider == null) + throw IllegalStateException("Both comments converter and signature provider must not be null") + + val sourceSetData = emptySet<DokkaSourceSet>() + val builder = PageContentBuilder(commentsConverter, signatureProvider, context.logger) + val contentNode = builder.contentFor( + dri = DRI(MULTIMODULE_PACKAGE_PLACEHOLDER), + kind = ContentKind.Cover, + sourceSets = sourceSetData + ) { + header(2, "All modules:") + table(styles = setOf(MultimoduleTable)) { + modules.mapNotNull { module -> + val paragraph = module.docFile.let(::File).readText().let { parser.parse(it).firstParagraph() } + paragraph?.let { + val dri = DRI(packageName = MULTIMODULE_PACKAGE_PLACEHOLDER, classNames = module.name) + val dci = DCI(setOf(dri), ContentKind.Main) + val header = + ContentHeader(listOf(linkNode(module.name, dri)), 2, dci, emptySet(), emptySet()) + val content = ContentGroup( + DocTagToContentConverter.buildContent(it, dci, emptySet()), + dci, + emptySet(), + emptySet() + ) + ContentGroup(listOf(header, content), dci, emptySet(), emptySet()) + } + } + } + } + return MultimoduleRootPageNode( + "Modules", + setOf(DRI(packageName = MULTIMODULE_PACKAGE_PLACEHOLDER, classNames = "allModules")), + contentNode + ) + } + + private fun throwOnMissingModuleDocFile(module: DokkaConfiguration.DokkaModuleDescription) { + val docFile = File(module.docFile) + if (!docFile.exists() || !docFile.isFile) { + throw DokkaException( + "Missing documentation file for module ${module.name}: ${docFile.absolutePath}" + ) + } + } + + private fun DocumentationNode.firstParagraph() = + this.children.flatMap { it.root.children }.filterIsInstance<P>().firstOrNull() +} diff --git a/plugins/base/src/main/kotlin/parsers/HtmlParser.kt b/plugins/base/src/main/kotlin/parsers/HtmlParser.kt new file mode 100644 index 00000000..ece3cf24 --- /dev/null +++ b/plugins/base/src/main/kotlin/parsers/HtmlParser.kt @@ -0,0 +1,89 @@ +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 new file mode 100644 index 00000000..05eb71ab --- /dev/null +++ b/plugins/base/src/main/kotlin/parsers/MarkdownParser.kt @@ -0,0 +1,420 @@ +package org.jetbrains.dokka.base.parsers + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.model.doc.* +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.CompositeASTNode +import org.intellij.markdown.ast.LeafASTNode +import org.intellij.markdown.ast.impl.ListItemCompositeNode +import org.intellij.markdown.flavours.gfm.GFMElementTypes +import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor +import org.intellij.markdown.flavours.gfm.GFMTokenTypes +import org.jetbrains.dokka.analysis.DokkaResolutionFacade +import org.jetbrains.dokka.analysis.from +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.base.parsers.factories.DocTagsFromIElementFactory +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink +import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import java.net.MalformedURLException +import org.intellij.markdown.parser.MarkdownParser as IntellijMarkdownParser + +class MarkdownParser( + private val resolutionFacade: DokkaResolutionFacade? = null, + private val declarationDescriptor: DeclarationDescriptor? = null, + private val logger: DokkaLogger +) : Parser() { + + inner class MarkdownVisitor(val text: String, val destinationLinksMap: Map<String, String>) { + + private fun headersHandler(node: ASTNode): DocTag = + DocTagsFromIElementFactory.getInstance( + node.type, + visitNode(node.children.find { it.type == MarkdownTokenTypes.ATX_CONTENT } + ?: throw IllegalStateException("Wrong AST Tree. ATX Header does not contain expected content")).children + ) + + private fun horizontalRulesHandler(node: ASTNode): DocTag = + DocTagsFromIElementFactory.getInstance(MarkdownTokenTypes.HORIZONTAL_RULE) + + private fun emphasisHandler(node: ASTNode): DocTag = + DocTagsFromIElementFactory.getInstance( + node.type, + children = listOf(visitNode(node.children[node.children.size / 2])) + ) + + private fun blockquotesHandler(node: ASTNode): DocTag = + DocTagsFromIElementFactory.getInstance( + node.type, children = node.children + .filterIsInstance<CompositeASTNode>() + .evaluateChildren() + ) + + private fun listsHandler(node: ASTNode): DocTag { + + val children = node.children.filterIsInstance<ListItemCompositeNode>().flatMap { + if (it.children.last().type in listOf( + MarkdownElementTypes.ORDERED_LIST, + MarkdownElementTypes.UNORDERED_LIST + ) + ) { + val nestedList = it.children.last() + (it.children as MutableList).removeAt(it.children.lastIndex) + listOf(it, nestedList) + } else + listOf(it) + } + + return DocTagsFromIElementFactory.getInstance( + node.type, + children = + children + .map { + if (it.type == MarkdownElementTypes.LIST_ITEM) + DocTagsFromIElementFactory.getInstance( + it.type, + children = it + .children + .filterIsInstance<CompositeASTNode>() + .evaluateChildren() + ) + else + visitNode(it) + }, + params = + if (node.type == MarkdownElementTypes.ORDERED_LIST) { + val listNumberNode = node.children.first().children.first() + mapOf( + "start" to text.substring( + listNumberNode.startOffset, + listNumberNode.endOffset + ).trim().dropLast(1) + ) + } else + emptyMap() + ) + } + + private fun resolveDRI(mdLink: String): DRI? = + mdLink + .removePrefix("[") + .removeSuffix("]") + .let { link -> + try { + java.net.URL(link) + null + } catch (e: MalformedURLException) { + try { + if (resolutionFacade != null && declarationDescriptor != null) { + resolveKDocLink( + resolutionFacade.resolveSession.bindingContext, + resolutionFacade, + declarationDescriptor, + null, + link.split('.') + ).minByOrNull { it is ClassDescriptor }?.let { DRI.from(it) } + } else null + } catch (e1: IllegalArgumentException) { + logger.warn("Couldn't resolve link for $mdLink") + null + } + } + } + + private fun referenceLinksHandler(node: ASTNode): DocTag { + val linkLabel = node.children.find { it.type == MarkdownElementTypes.LINK_LABEL } + ?: throw IllegalStateException("Wrong AST Tree. Reference link does not contain expected content") + val linkText = node.children.findLast { it.type == MarkdownElementTypes.LINK_TEXT } ?: linkLabel + + val linkKey = text.substring(linkLabel.startOffset, linkLabel.endOffset) + + val link = destinationLinksMap[linkKey.toLowerCase()] ?: linkKey + + return linksHandler(linkText, link) + } + + private fun inlineLinksHandler(node: ASTNode): DocTag { + val linkText = node.children.find { it.type == MarkdownElementTypes.LINK_TEXT } + ?: throw IllegalStateException("Wrong AST Tree. Inline link does not contain expected content") + val linkDestination = node.children.find { it.type == MarkdownElementTypes.LINK_DESTINATION } + ?: throw IllegalStateException("Wrong AST Tree. Inline link does not contain expected content") + val linkTitle = node.children.find { it.type == MarkdownElementTypes.LINK_TITLE } + + val link = text.substring(linkDestination.startOffset, linkDestination.endOffset) + + return linksHandler(linkText, link, linkTitle) + } + + private fun autoLinksHandler(node: ASTNode): DocTag { + val link = text.substring(node.startOffset + 1, node.endOffset - 1) + + return linksHandler(node, link) + } + + private fun linksHandler(linkText: ASTNode, link: String, linkTitle: ASTNode? = null): DocTag { + val dri: DRI? = resolveDRI(link) + val params = if (linkTitle == null) + mapOf("href" to link) + else + mapOf("href" to link, "title" to text.substring(linkTitle.startOffset + 1, linkTitle.endOffset - 1)) + + return DocTagsFromIElementFactory.getInstance( + MarkdownElementTypes.INLINE_LINK, + params = params, + children = linkText.children.drop(1).dropLast(1).evaluateChildren(), + dri = dri + ) + } + + private fun imagesHandler(node: ASTNode): DocTag { + val linkNode = + node.children.last().children.find { it.type == MarkdownElementTypes.LINK_LABEL }!!.children[1] + val link = text.substring(linkNode.startOffset, linkNode.endOffset) + val src = mapOf("src" to link) + return DocTagsFromIElementFactory.getInstance( + node.type, + params = src, + children = listOf(visitNode(node.children.last().children.find { it.type == MarkdownElementTypes.LINK_TEXT }!!)) + ) + } + + private fun codeSpansHandler(node: ASTNode): DocTag = + DocTagsFromIElementFactory.getInstance( + node.type, + children = listOf( + DocTagsFromIElementFactory.getInstance( + MarkdownTokenTypes.TEXT, + body = text.substring(node.startOffset + 1, node.endOffset - 1).replace('\n', ' ').trimIndent() + ) + + ) + ) + + private fun codeFencesHandler(node: ASTNode): DocTag = + DocTagsFromIElementFactory.getInstance( + node.type, + children = node + .children + .dropWhile { it.type != MarkdownTokenTypes.CODE_FENCE_CONTENT } + .dropLastWhile { it.type != MarkdownTokenTypes.CODE_FENCE_CONTENT } + .map { + if (it.type == MarkdownTokenTypes.EOL) + LeafASTNode(MarkdownTokenTypes.HARD_LINE_BREAK, 0, 0) + else + it + }.evaluateChildren(), + params = node + .children + .find { it.type == MarkdownTokenTypes.FENCE_LANG } + ?.let { mapOf("lang" to text.substring(it.startOffset, it.endOffset)) } + ?: emptyMap() + ) + + private fun codeBlocksHandler(node: ASTNode): DocTag = + DocTagsFromIElementFactory.getInstance(node.type, children = node.children.evaluateChildren()) + + private fun defaultHandler(node: ASTNode): DocTag = + DocTagsFromIElementFactory.getInstance( + MarkdownElementTypes.PARAGRAPH, + children = node.children.evaluateChildren() + ) + + fun visitNode(node: ASTNode): DocTag = + when (node.type) { + MarkdownElementTypes.ATX_1, + MarkdownElementTypes.ATX_2, + MarkdownElementTypes.ATX_3, + MarkdownElementTypes.ATX_4, + MarkdownElementTypes.ATX_5, + MarkdownElementTypes.ATX_6 -> headersHandler(node) + MarkdownTokenTypes.HORIZONTAL_RULE -> horizontalRulesHandler(node) + MarkdownElementTypes.STRONG, + MarkdownElementTypes.EMPH -> emphasisHandler(node) + MarkdownElementTypes.FULL_REFERENCE_LINK, + MarkdownElementTypes.SHORT_REFERENCE_LINK -> referenceLinksHandler(node) + MarkdownElementTypes.INLINE_LINK -> inlineLinksHandler(node) + MarkdownElementTypes.AUTOLINK -> autoLinksHandler(node) + MarkdownElementTypes.BLOCK_QUOTE -> blockquotesHandler(node) + MarkdownElementTypes.UNORDERED_LIST, + MarkdownElementTypes.ORDERED_LIST -> listsHandler(node) + MarkdownElementTypes.CODE_BLOCK -> codeBlocksHandler(node) + MarkdownElementTypes.CODE_FENCE -> codeFencesHandler(node) + MarkdownElementTypes.CODE_SPAN -> codeSpansHandler(node) + MarkdownElementTypes.IMAGE -> imagesHandler(node) + MarkdownTokenTypes.HARD_LINE_BREAK -> DocTagsFromIElementFactory.getInstance(node.type) + MarkdownTokenTypes.CODE_FENCE_CONTENT, + MarkdownTokenTypes.CODE_LINE, + MarkdownTokenTypes.TEXT -> DocTagsFromIElementFactory.getInstance( + MarkdownTokenTypes.TEXT, + body = text + .substring(node.startOffset, node.endOffset).transform() + ) + MarkdownElementTypes.MARKDOWN_FILE -> if (node.children.size == 1) visitNode(node.children.first()) else defaultHandler( + node + ) + GFMElementTypes.STRIKETHROUGH -> DocTagsFromIElementFactory.getInstance( + GFMElementTypes.STRIKETHROUGH, + body = text + .substring(node.startOffset, node.endOffset).transform() + ) + GFMElementTypes.TABLE -> DocTagsFromIElementFactory.getInstance( + GFMElementTypes.TABLE, + children = node.children.filterTabSeparators().evaluateChildren() + ) + GFMElementTypes.HEADER -> DocTagsFromIElementFactory.getInstance( + GFMElementTypes.HEADER, + children = node.children.filterTabSeparators().evaluateChildren() + ) + GFMElementTypes.ROW -> DocTagsFromIElementFactory.getInstance( + GFMElementTypes.ROW, + children = node.children.filterTabSeparators().evaluateChildren() + ) + else -> defaultHandler(node) + } + + private fun List<ASTNode>.filterTabSeparators() = + this.filterNot { it.type == GFMTokenTypes.TABLE_SEPARATOR } + + private fun List<ASTNode>.evaluateChildren(): List<DocTag> = + this.removeUselessTokens().mergeLeafASTNodes().map { visitNode(it) } + + private fun List<ASTNode>.removeUselessTokens(): List<ASTNode> = + this.filterIndexed { index, node -> + !(node.type == MarkdownElementTypes.LINK_DEFINITION || ( + node.type == MarkdownTokenTypes.EOL && + this.getOrNull(index - 1)?.type == MarkdownTokenTypes.HARD_LINE_BREAK + )) + } + + private val notLeafNodes = listOf(MarkdownTokenTypes.HORIZONTAL_RULE, MarkdownTokenTypes.HARD_LINE_BREAK) + + private fun List<ASTNode>.isNotLeaf(index: Int): Boolean = + if (index in 0..this.lastIndex) + (this[index] is CompositeASTNode) || this[index].type in notLeafNodes + else + false + + private fun List<ASTNode>.mergeLeafASTNodes(): List<ASTNode> { + val children: MutableList<ASTNode> = mutableListOf() + var index = 0 + while (index <= this.lastIndex) { + if (this.isNotLeaf(index)) { + children += this[index] + } else { + val startOffset = this[index].startOffset + while (index < this.lastIndex) { + if (this.isNotLeaf(index + 1) || this[index + 1].startOffset != this[index].endOffset) { + val endOffset = this[index].endOffset + if (text.substring(startOffset, endOffset).transform().trim().isNotEmpty()) + children += LeafASTNode(MarkdownTokenTypes.TEXT, startOffset, endOffset) + break + } + index++ + } + if (index == this.lastIndex) { + val endOffset = this[index].endOffset + if (text.substring(startOffset, endOffset).transform().trim().isNotEmpty()) + children += LeafASTNode(MarkdownTokenTypes.TEXT, startOffset, endOffset) + } + } + index++ + } + return children + } + + private fun String.transform() = this + .replace(Regex("\n\n+"), "") // Squashing new lines between paragraphs + .replace(Regex("\n"), " ") + .replace(Regex(" >+ +"), " ") // Replacement used in blockquotes, get rid of garbage + } + + + private fun getAllDestinationLinks(text: String, node: ASTNode): List<Pair<String, String>> = + node.children + .filter { it.type == MarkdownElementTypes.LINK_DEFINITION } + .map { + text.substring(it.children[0].startOffset, it.children[0].endOffset).toLowerCase() to + text.substring(it.children[2].startOffset, it.children[2].endOffset) + } + + node.children.filterIsInstance<CompositeASTNode>().flatMap { getAllDestinationLinks(text, it) } + + + private fun markdownToDocNode(text: String): DocTag { + + val flavourDescriptor = GFMFlavourDescriptor() + val markdownAstRoot: ASTNode = IntellijMarkdownParser(flavourDescriptor).buildMarkdownTreeFromString(text) + + return MarkdownVisitor(text, getAllDestinationLinks(text, markdownAstRoot).toMap()).visitNode(markdownAstRoot) + } + + override fun parseStringToDocNode(extractedString: String) = markdownToDocNode(extractedString) + override fun preparse(text: String) = text + + private fun findParent(kDoc: PsiElement): PsiElement = + if (kDoc is KDocSection) findParent(kDoc.parent) else kDoc + + private fun getAllKDocTags(kDocImpl: PsiElement): List<KDocTag> = + kDocImpl.children.filterIsInstance<KDocTag>().filterNot { it is KDocSection } + kDocImpl.children.flatMap { + getAllKDocTags( + it + ) + } + + fun parseFromKDocTag(kDocTag: KDocTag?): DocumentationNode { + return if (kDocTag == null) + DocumentationNode(emptyList()) + else + DocumentationNode( + (listOf(kDocTag) + getAllKDocTags(findParent(kDocTag))).map { + when (it.knownTag) { + null -> if (it.name == null) Description(parseStringToDocNode(it.getContent())) else CustomTagWrapper( + parseStringToDocNode(it.getContent()), + 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.PARAM -> Param( + parseStringToDocNode(it.getContent()), + it.getSubjectName().orEmpty() + ) + KDocKnownTag.RECEIVER -> Receiver(parseStringToDocNode(it.getContent())) + KDocKnownTag.RETURN -> Return(parseStringToDocNode(it.getContent())) + KDocKnownTag.SEE -> See( + parseStringToDocNode(it.getContent()), + it.getSubjectName().orEmpty(), + parseStringToDocNode("[${it.getSubjectName()}]") + .let { + val link = it.children[0] + if (link is DocumentationLink) link.dri + else null + } + ) + KDocKnownTag.SINCE -> Since(parseStringToDocNode(it.getContent())) + KDocKnownTag.CONSTRUCTOR -> Constructor(parseStringToDocNode(it.getContent())) + KDocKnownTag.PROPERTY -> Property( + parseStringToDocNode(it.getContent()), + it.getSubjectName().orEmpty() + ) + KDocKnownTag.SAMPLE -> Sample( + parseStringToDocNode(it.getContent()), + it.getSubjectName().orEmpty() + ) + KDocKnownTag.SUPPRESS -> Suppress(parseStringToDocNode(it.getContent())) + } + } + ) + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/parsers/Parser.kt b/plugins/base/src/main/kotlin/parsers/Parser.kt new file mode 100644 index 00000000..1dd0c34a --- /dev/null +++ b/plugins/base/src/main/kotlin/parsers/Parser.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.base.parsers + +import org.jetbrains.dokka.model.doc.* + +abstract class Parser { + + abstract fun parseStringToDocNode(extractedString: String): DocTag + 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) + } + } + return DocumentationNode(mappedList) + } + + private fun jkdocToListOfPairs(javadoc: String): List<Pair<String, String>> = + "description $javadoc" + .split("\n@") + .map { + it.substringBefore(' ') to it.substringAfter(' ') + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/parsers/factories/DocTagsFromIElementFactory.kt b/plugins/base/src/main/kotlin/parsers/factories/DocTagsFromIElementFactory.kt new file mode 100644 index 00000000..277fd35e --- /dev/null +++ b/plugins/base/src/main/kotlin/parsers/factories/DocTagsFromIElementFactory.kt @@ -0,0 +1,43 @@ +package org.jetbrains.dokka.base.parsers.factories + +import org.jetbrains.dokka.model.doc.* +import org.intellij.markdown.IElementType +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.flavours.gfm.GFMElementTypes +import org.jetbrains.dokka.links.DRI +import java.lang.NullPointerException + +object DocTagsFromIElementFactory { + fun getInstance(type: IElementType, children: List<DocTag> = emptyList(), params: Map<String, String> = emptyMap(), body: String? = null, dri: DRI? = null) = + when(type) { + MarkdownElementTypes.SHORT_REFERENCE_LINK, + MarkdownElementTypes.FULL_REFERENCE_LINK, + MarkdownElementTypes.INLINE_LINK -> if(dri == null) A(children, params) else DocumentationLink(dri, children, params) + MarkdownElementTypes.STRONG -> B(children, params) + MarkdownElementTypes.BLOCK_QUOTE -> BlockQuote(children, params) + MarkdownElementTypes.CODE_SPAN -> CodeInline(children, params) + MarkdownElementTypes.CODE_BLOCK, + MarkdownElementTypes.CODE_FENCE -> CodeBlock(children, params) + MarkdownElementTypes.ATX_1 -> H1(children, params) + MarkdownElementTypes.ATX_2 -> H2(children, params) + MarkdownElementTypes.ATX_3 -> H3(children, params) + MarkdownElementTypes.ATX_4 -> H4(children, params) + MarkdownElementTypes.ATX_5 -> H5(children, params) + MarkdownElementTypes.ATX_6 -> H6(children, params) + MarkdownElementTypes.EMPH -> I(children, params) + MarkdownElementTypes.IMAGE -> Img(children, params) + MarkdownElementTypes.LIST_ITEM -> Li(children, params) + MarkdownElementTypes.ORDERED_LIST -> Ol(children, params) + MarkdownElementTypes.UNORDERED_LIST -> Ul(children, params) + MarkdownElementTypes.PARAGRAPH -> P(children, params) + MarkdownTokenTypes.TEXT -> Text(body ?: throw NullPointerException("Text body should be at least empty string passed to DocNodes factory!"), children, params ) + MarkdownTokenTypes.HORIZONTAL_RULE -> HorizontalRule + MarkdownTokenTypes.HARD_LINE_BREAK -> Br + GFMElementTypes.STRIKETHROUGH -> Strikethrough(children, params) + GFMElementTypes.TABLE -> Table(children, params) + GFMElementTypes.HEADER -> Th(children, params) + GFMElementTypes.ROW -> Tr(children, params) + else -> CustomDocTag(children, params) + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/parsers/factories/DocTagsFromStringFactory.kt b/plugins/base/src/main/kotlin/parsers/factories/DocTagsFromStringFactory.kt new file mode 100644 index 00000000..124dc3b4 --- /dev/null +++ b/plugins/base/src/main/kotlin/parsers/factories/DocTagsFromStringFactory.kt @@ -0,0 +1,77 @@ +package org.jetbrains.dokka.base.parsers.factories + +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.links.DRI +import java.lang.NullPointerException + +object DocTagsFromStringFactory { + fun getInstance(name: String, children: List<DocTag> = emptyList(), params: Map<String, String> = emptyMap(), body: String? = null, dri: DRI? = null) = + when(name) { + "a" -> A(children, params) + "big" -> Big(children, params) + "b" -> B(children, params) + "blockquote" -> BlockQuote(children, params) + "br" -> Br + "cite" -> Cite(children, params) + "code" -> if(params.isEmpty()) CodeInline(children, params) else CodeBlock(children, params) + "dd" -> Dd(children, params) + "dfn" -> Dfn(children, params) + "dir" -> Dir(children, params) + "div" -> Div(children, params) + "dl" -> Dl(children, params) + "dt" -> Dt(children, params) + "Em" -> Em(children, params) + "font" -> Font(children, params) + "footer" -> Footer(children, params) + "frame" -> Frame(children, params) + "frameset" -> FrameSet(children, params) + "h1" -> H1(children, params) + "h2" -> H2(children, params) + "h3" -> H3(children, params) + "h4" -> H4(children, params) + "h5" -> H5(children, params) + "h6" -> H6(children, params) + "head" -> Head(children, params) + "header" -> Header(children, params) + "html" -> Html(children, params) + "i" -> I(children, params) + "iframe" -> IFrame(children, params) + "img" -> Img(children, params) + "input" -> Input(children, params) + "li" -> Li(children, params) + "link" -> Link(children, params) + "listing" -> Listing(children, params) + "main" -> Main(children, params) + "menu" -> Menu(children, params) + "meta" -> Meta(children, params) + "nav" -> Nav(children, params) + "noframes" -> NoFrames(children, params) + "noscript" -> NoScript(children, params) + "ol" -> Ol(children, params) + "p" -> P(children, params) + "pre" -> Pre(children, params) + "script" -> Script(children, params) + "section" -> Section(children, params) + "small" -> Small(children, params) + "span" -> Span(children, params) + "strong" -> Strong(children, params) + "sub" -> Sub(children, params) + "sup" -> Sup(children, params) + "table" -> Table(children, params) + "#text" -> Text(body ?: throw NullPointerException("Text body should be at least empty string passed to DocNodes factory!"), children, params) + "tBody" -> TBody(children, params) + "td" -> Td(children, params) + "tFoot" -> TFoot(children, params) + "th" -> Th(children, params) + "tHead" -> THead(children, params) + "title" -> Title(children, params) + "tr" -> Tr(children, params) + "tt" -> Tt(children, params) + "u" -> U(children, params) + "ul" -> Ul(children, params) + "var" -> Var(children, params) + "documentationlink" -> DocumentationLink(dri ?: throw NullPointerException("DRI cannot be passed null while constructing documentation link!"), children, params) + "hr" -> HorizontalRule + else -> CustomDocTag(children, params) + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt new file mode 100644 index 00000000..afee1b33 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt @@ -0,0 +1,200 @@ +package org.jetbrains.dokka.base.renderers + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.local.LocationProvider +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.renderers.Renderer +import org.jetbrains.dokka.transformers.pages.PageTransformer + +abstract class DefaultRenderer<T>( + protected val context: DokkaContext +) : Renderer { + + protected val outputWriter = context.plugin<DokkaBase>().querySingle { outputWriter } + + protected lateinit var locationProvider: LocationProvider + private set + + protected open val preprocessors: Iterable<PageTransformer> = emptyList() + + abstract fun T.buildHeader(level: Int, node: ContentHeader, content: T.() -> Unit) + abstract fun T.buildLink(address: String, content: T.() -> Unit) + abstract fun T.buildList( + node: ContentList, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) + + abstract fun T.buildNewLine() + abstract fun T.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) + abstract fun T.buildTable( + node: ContentTable, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) + + abstract fun T.buildText(textNode: ContentText) + abstract fun T.buildNavigation(page: PageNode) + + abstract fun buildPage(page: ContentPage, content: (T, ContentPage) -> Unit): String + abstract fun buildError(node: ContentNode) + + open fun T.buildPlatformDependent( + content: PlatformHintedContent, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) = buildContentNode(content.inner, pageContext) + + open fun T.buildGroup( + node: ContentGroup, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) = + wrapGroup(node, pageContext) { node.children.forEach { it.build(this, pageContext, sourceSetRestriction) } } + + open fun T.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) = + node.children.forEach { it.build(this, pageContext) } + + open fun T.wrapGroup(node: ContentGroup, pageContext: ContentPage, childrenCallback: T.() -> Unit) = + childrenCallback() + + open fun T.buildLinkText( + nodes: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + nodes.forEach { it.build(this, pageContext, sourceSetRestriction) } + } + + open fun T.buildCodeBlock(code: ContentCodeBlock, pageContext: ContentPage) { + code.children.forEach { it.build(this, pageContext) } + } + + open fun T.buildCodeInline(code: ContentCodeInline, pageContext: ContentPage) { + code.children.forEach { it.build(this, pageContext) } + } + + open fun T.buildHeader( + node: ContentHeader, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + buildHeader(node.level, node) { node.children.forEach { it.build(this, pageContext, sourceSetRestriction) } } + } + + open fun ContentNode.build( + builder: T, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) = + builder.buildContentNode(this, pageContext, sourceSetRestriction) + + open fun T.buildContentNode( + node: ContentNode, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + if (sourceSetRestriction == null || node.sourceSets.any { it in sourceSetRestriction }) { + when (node) { + is ContentText -> buildText(node) + is ContentHeader -> buildHeader(node, pageContext, sourceSetRestriction) + is ContentCodeBlock -> buildCodeBlock(node, pageContext) + is ContentCodeInline -> buildCodeInline(node, pageContext) + is ContentDRILink -> + buildLink(locationProvider.resolve(node.address, node.sourceSets, pageContext)) { + buildLinkText(node.children, pageContext, sourceSetRestriction) + } + is ContentResolvedLink -> buildLink(node.address) { + buildLinkText(node.children, pageContext, sourceSetRestriction) + } + is ContentEmbeddedResource -> buildResource(node, pageContext) + is ContentList -> buildList(node, pageContext, sourceSetRestriction) + is ContentTable -> buildTable(node, pageContext, sourceSetRestriction) + is ContentGroup -> buildGroup(node, pageContext, sourceSetRestriction) + is ContentBreakLine -> buildNewLine() + is PlatformHintedContent -> buildPlatformDependent(node, pageContext, sourceSetRestriction) + is ContentDivergentGroup -> buildDivergent(node, pageContext) + is ContentDivergentInstance -> buildDivergentInstance(node, pageContext) + else -> buildError(node) + } + } + } + + open fun T.buildDivergentInstance(node: ContentDivergentInstance, pageContext: ContentPage) { + node.before?.build(this, pageContext) + node.divergent.build(this, pageContext) + node.after?.build(this, pageContext) + } + + open fun buildPageContent(context: T, page: ContentPage) { + context.buildNavigation(page) + page.content.build(context, page) + } + + open suspend fun renderPage(page: PageNode) { + val path by lazy { locationProvider.resolve(page, skipExtension = true) } + when (page) { + is ContentPage -> outputWriter.write(path, buildPage(page) { c, p -> buildPageContent(c, p) }, ".html") + is RendererSpecificPage -> when (val strategy = page.strategy) { + is RenderingStrategy.Copy -> outputWriter.writeResources(strategy.from, path) + is RenderingStrategy.Write -> outputWriter.write(path, strategy.text, "") + is RenderingStrategy.Callback -> outputWriter.write(path, strategy.instructions(this, page), ".html") + RenderingStrategy.DoNothing -> Unit + } + else -> throw AssertionError( + "Page ${page.name} cannot be rendered by renderer as it is not renderer specific nor contains content" + ) + } + } + + private suspend fun renderPages(root: PageNode) { + coroutineScope { + renderPage(root) + + root.children.forEach { + launch { renderPages(it) } + } + } + } + + override fun render(root: RootPageNode) { + val newRoot = preprocessors.fold(root) { acc, t -> t(acc) } + + locationProvider = + context.plugin<DokkaBase>().querySingle { locationProviderFactory }.getLocationProvider(newRoot) + + runBlocking(Dispatchers.Default) { + renderPages(newRoot) + } + } + + protected fun ContentDivergentGroup.groupDivergentInstances( + pageContext: ContentPage, + beforeTransformer: (ContentDivergentInstance, ContentPage, DokkaSourceSet) -> String, + afterTransformer: (ContentDivergentInstance, ContentPage, DokkaSourceSet) -> String + ): Map<SerializedBeforeAndAfter, List<InstanceWithSource>> = + children.flatMap { instance -> + instance.sourceSets.map { sourceSet -> + Pair(instance, sourceSet) to Pair( + beforeTransformer(instance, pageContext, sourceSet), + afterTransformer(instance, pageContext, sourceSet) + ) + } + }.groupBy( + Pair<InstanceWithSource, SerializedBeforeAndAfter>::second, + Pair<InstanceWithSource, SerializedBeforeAndAfter>::first + ) +} + +internal typealias SerializedBeforeAndAfter = Pair<String, String> +internal typealias InstanceWithSource = Pair<ContentDivergentInstance, DokkaSourceSet> + +fun ContentPage.sourceSets() = this.content.sourceSets
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/DefaultTabSortingStrategy.kt b/plugins/base/src/main/kotlin/renderers/DefaultTabSortingStrategy.kt new file mode 100644 index 00000000..3b849fec --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/DefaultTabSortingStrategy.kt @@ -0,0 +1,31 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.pages.ContentKind +import org.jetbrains.dokka.pages.ContentNode +import org.jetbrains.dokka.pages.Kind +import org.jetbrains.dokka.utilities.DokkaLogger + +private val kindsOrder = listOf( + ContentKind.Classlikes, + ContentKind.Constructors, + ContentKind.Functions, + ContentKind.Properties, + ContentKind.Extensions, + ContentKind.Parameters, + ContentKind.Inheritors, + ContentKind.Source, + ContentKind.Sample, + ContentKind.Comment +) + +class DefaultTabSortingStrategy : TabSortingStrategy { + override fun <T: ContentNode> sort(tabs: Collection<T>): List<T> { + val tabMap: Map<Kind, MutableList<T>> = kindsOrder.asSequence().map { it to mutableListOf<T>() }.toMap() + val unrecognized: MutableList<T> = mutableListOf() + tabs.forEach { + tabMap[it.dci.kind]?.add(it) ?: unrecognized.add(it) + } + return tabMap.values.flatten() + unrecognized + } + +} diff --git a/plugins/base/src/main/kotlin/renderers/FileWriter.kt b/plugins/base/src/main/kotlin/renderers/FileWriter.kt new file mode 100644 index 00000000..181295c0 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/FileWriter.kt @@ -0,0 +1,88 @@ +package org.jetbrains.dokka.base.renderers + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.dokka.plugability.DokkaContext +import java.io.File +import java.io.IOException +import java.net.URI +import java.nio.file.* + +class FileWriter(val context: DokkaContext): OutputWriter { + private val createdFiles: MutableSet<String> = mutableSetOf() + private val jarUriPrefix = "jar:file:" + private val root = context.configuration.outputDir + + override suspend fun write(path: String, text: String, ext: String) { + if (createdFiles.contains(path)) { + context.logger.error("An attempt to write ${root}/$path several times!") + return + } + createdFiles.add(path) + + try { + val dir = Paths.get(root, path.dropLastWhile { it != '/' }).toFile() + withContext(Dispatchers.IO) { + dir.mkdirsOrFail() + Files.write(Paths.get(root, "$path$ext"), text.lines()) + } + } catch (e: Throwable) { + context.logger.error("Failed to write $this. ${e.message}") + e.printStackTrace() + } + } + + override suspend fun writeResources(pathFrom: String, pathTo: String) = + if (javaClass.getResource(pathFrom).toURI().toString().startsWith(jarUriPrefix)) { + copyFromJar(pathFrom, pathTo) + } else { + copyFromDirectory(pathFrom, pathTo) + } + + + private suspend fun copyFromDirectory(pathFrom: String, pathTo: String) { + val dest = Paths.get(root, pathTo).toFile() + val uri = javaClass.getResource(pathFrom).toURI() + withContext(Dispatchers.IO) { + File(uri).copyRecursively(dest, true) + } + } + + private suspend fun copyFromJar(pathFrom: String, pathTo: String) { + val rebase = fun(path: String) = + "$pathTo/${path.removePrefix(pathFrom)}" + val dest = Paths.get(root, pathTo).toFile() + dest.mkdirsOrFail() + val uri = javaClass.getResource(pathFrom).toURI() + val fs = getFileSystemForURI(uri) + val path = fs.getPath(pathFrom) + for (file in Files.walk(path).iterator()) { + if (Files.isDirectory(file)) { + val dirPath = file.toAbsolutePath().toString() + withContext(Dispatchers.IO) { + Paths.get(root, rebase(dirPath)).toFile().mkdirsOrFail() + } + } else { + val filePath = file.toAbsolutePath().toString() + withContext(Dispatchers.IO) { + Paths.get(root, rebase(filePath)).toFile().writeBytes( + this@FileWriter.javaClass.getResourceAsStream(filePath).readBytes() + ) + } + } + } + } + + private fun File.mkdirsOrFail() { + if (!mkdirs() && !exists()) { + throw IOException("Failed to create directory $this") + } + } + + private fun getFileSystemForURI(uri: URI): FileSystem = + try { + FileSystems.newFileSystem(uri, emptyMap<String, Any>()) + } catch (e: FileSystemAlreadyExistsException) { + FileSystems.getFileSystem(uri) + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/OutputWriter.kt b/plugins/base/src/main/kotlin/renderers/OutputWriter.kt new file mode 100644 index 00000000..1827c7f0 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/OutputWriter.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.base.renderers + +interface OutputWriter { + + suspend fun write(path: String, text: String, ext: String) + suspend fun writeResources(pathFrom: String, pathTo: String) +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/PackageListService.kt b/plugins/base/src/main/kotlin/renderers/PackageListService.kt new file mode 100644 index 00000000..d4333200 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/PackageListService.kt @@ -0,0 +1,51 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.parent +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +class PackageListService(val context: DokkaContext) { + + fun formatPackageList(module: RootPageNode, format: String, linkExtension: String): String { + + val packages = mutableSetOf<String>() + val nonStandardLocations = mutableMapOf<String, String>() + + val locationProvider = + context.plugin<DokkaBase>().querySingle { locationProviderFactory }.getLocationProvider(module) + + fun visit(node: PageNode, parentDris: Set<DRI>) { + + if (node is PackagePageNode) { + packages.add(node.name) + } + + val contentPage = node.safeAs<ContentPage>() + contentPage?.dri?.forEach { + if (parentDris.isNotEmpty() && it.parent !in parentDris) { + nonStandardLocations[it.toString()] = locationProvider.resolve(node) + } + } + + node.children.forEach { visit(it, contentPage?.dri ?: setOf()) } + } + + visit(module, setOf()) + + return buildString { + appendln("\$dokka.format:${format}") + appendln("\$dokka.linkExtension:${linkExtension}") + nonStandardLocations.map { (signature, location) -> "\$dokka.location:$signature\u001f$location" } + .sorted().joinTo(this, separator = "\n", postfix = "\n") + + packages.sorted().joinTo(this, separator = "\n", postfix = "\n") + } + + } + +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/TabSortingStrategy.kt b/plugins/base/src/main/kotlin/renderers/TabSortingStrategy.kt new file mode 100644 index 00000000..dcf49ca9 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/TabSortingStrategy.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.pages.ContentNode + +interface TabSortingStrategy { + fun <T: ContentNode> sort(tabs: Collection<T>) : List<T> +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt new file mode 100644 index 00000000..614d8f6e --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -0,0 +1,760 @@ +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.renderers.DefaultRenderer +import org.jetbrains.dokka.base.renderers.TabSortingStrategy +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import org.jetbrains.dokka.plugability.querySingle +import java.io.File +import java.net.URI + +open class HtmlRenderer( + context: DokkaContext +) : DefaultRenderer<FlowContent>(context) { + + private val sourceSetDependencyMap = context.configuration.sourceSets.map { sourceSet -> + sourceSet to context.configuration.sourceSets.filter { sourceSet.dependentSourceSets.contains(it.sourceSetID) } + }.toMap() + + private val isMultiplatform by lazy { + sourceSetDependencyMap.size > 1 + } + + private val pageList = mutableMapOf<String, Pair<String, String>>() + + override val preprocessors = context.plugin<DokkaBase>().query { htmlPreprocessors } + + private val tabSortingStrategy = context.plugin<DokkaBase>().querySingle { tabSortingStrategy } + + private fun <T : ContentNode> sortTabs(strategy: TabSortingStrategy, tabs: Collection<T>): List<T> { + val sorted = strategy.sort(tabs) + if (sorted.size != tabs.size) + context.logger.warn("Tab sorting strategy has changed number of tabs from ${tabs.size} to ${sorted.size}") + return sorted; + } + + override fun FlowContent.wrapGroup( + node: ContentGroup, + pageContext: ContentPage, + childrenCallback: FlowContent.() -> Unit + ) { + val additionalClasses = node.style.joinToString(" ") { it.toString().toLowerCase() } + return when { + node.hasStyle(ContentStyle.TabbedContent) -> div(additionalClasses) { + val secondLevel = node.children.filterIsInstance<ContentComposite>().flatMap { it.children } + .filterIsInstance<ContentHeader>().flatMap { it.children }.filterIsInstance<ContentText>() + val firstLevel = node.children.filterIsInstance<ContentHeader>().flatMap { it.children } + .filterIsInstance<ContentText>() + + val renderable = firstLevel.union(secondLevel).let { sortTabs(tabSortingStrategy, it) } + + div(classes = "tabs-section") { + attributes["tabs-section"] = "tabs-section" + renderable.forEachIndexed { index, node -> + button(classes = "section-tab") { + if (index == 0) attributes["data-active"] = "" + attributes["data-togglable"] = node.text + text(node.text) + } + } + } + div(classes = "tabs-section-body") { + childrenCallback() + } + } + node.hasStyle(ContentStyle.WithExtraAttributes) -> div() { + node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } + childrenCallback() + } + node.dci.kind in setOf(ContentKind.Symbol) -> div("symbol $additionalClasses") { + childrenCallback() + if (node.hasStyle(TextStyle.Monospace)) copyButton() + } + node.hasStyle(TextStyle.BreakableAfter) -> { + span() { childrenCallback() } + wbr { } + } + node.hasStyle(TextStyle.Breakable) -> { + span("breakable-word") { childrenCallback() } + } + node.hasStyle(TextStyle.Span) -> span() { childrenCallback() } + node.dci.kind == ContentKind.Symbol -> div("symbol $additionalClasses") { childrenCallback() } + node.dci.kind == ContentKind.BriefComment -> div("brief $additionalClasses") { childrenCallback() } + node.dci.kind == ContentKind.Cover -> div("cover $additionalClasses") { + filterButtons(pageContext) + childrenCallback() + } + node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() } + node.hasStyle(TextStyle.Block) -> div(additionalClasses) { childrenCallback() } + else -> childrenCallback() + } + } + + private fun FlowContent.filterButtons(page: ContentPage) { + if (isMultiplatform) { + div(classes = "filter-section") { + id = "filter-section" + page.content.withDescendants().flatMap { it.sourceSets }.distinct().forEach { + button(classes = "platform-tag platform-selector") { + attributes["data-active"] = "" + attributes["data-filter"] = it.sourceSetID.toString() + when (it.analysisPlatform.key) { + "common" -> classes = classes + "common-like" + "native" -> classes = classes + "native-like" + "jvm" -> classes = classes + "jvm-like" + "js" -> classes = classes + "js-like" + } + text(it.displayName) + } + } + } + } + } + + private fun FlowContent.copyButton() = span(classes = "top-right-position") { + span("copy-icon") { + unsafe { + raw( + """<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M5 4H15V16H5V4ZM17 7H19V18V20H17H8V18H17V7Z" fill="black"/> + </svg>""".trimIndent() + ) + } + } + copiedPopup("Content copied to clipboard", "popup-to-left") + } + + private fun FlowContent.copiedPopup(notificationContent: String, additionalClasses: String = "") = + div("copy-popup-wrapper $additionalClasses") { + unsafe { + raw( + """ + <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M18 9C18 14 14 18 9 18C4 18 0 14 0 9C0 4 4 0 9 0C14 0 18 4 18 9ZM14.2 6.2L12.8 4.8L7.5 10.1L5.3 7.8L3.8 9.2L7.5 13L14.2 6.2Z" fill="#4DBB5F"/> + </svg> + """.trimIndent() + ) + } + span { + text(notificationContent) + } + } + + override fun FlowContent.buildPlatformDependent( + content: PlatformHintedContent, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) = + buildPlatformDependent( + content.sourceSets.filter { + sourceSetRestriction == null || it in sourceSetRestriction + }.map { it to setOf(content.inner) }.toMap(), + pageContext, + content.extra, + content.style + ) + + private fun FlowContent.buildPlatformDependent( + nodes: Map<DokkaSourceSet, Collection<ContentNode>>, + pageContext: ContentPage, + extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), + styles: Set<Style> = emptySet() + ) { + val contents = contentsForSourceSetDependent(nodes, pageContext) + val shouldHaveTabs = contents.size != 1 + + val styles = "platform-hinted ${styles.joinToString()}" + if (shouldHaveTabs) " with-platform-tabs" else "" + div(styles) { + attributes["data-platform-hinted"] = "data-platform-hinted" + extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } + if (shouldHaveTabs) { + div("platform-bookmarks-row") { + attributes["data-toggle-list"] = "data-toggle-list" + contents.forEachIndexed { index, pair -> + button(classes = "platform-bookmark") { + attributes["data-filterable-current"] = pair.first.sourceSetID.toString() + attributes["data-filterable-set"] = pair.first.sourceSetID.toString() + if (index == 0) attributes["data-active"] = "" + attributes["data-toggle"] = pair.first.sourceSetID.toString() + when (pair.first.analysisPlatform.key) { + "common" -> classes = classes + "common-like" + "native" -> classes = classes + "native-like" + "jvm" -> classes = classes + "jvm-like" + "js" -> classes = classes + "js-like" + } + attributes["data-toggle"] = pair.first.sourceSetID.toString() + text(pair.first.displayName) + } + } + } + } + contents.forEach { + consumer.onTagContentUnsafe { +it.second } + } + } + } + + private fun contentsForSourceSetDependent( + nodes: Map<DokkaSourceSet, Collection<ContentNode>>, + pageContext: ContentPage, + ): List<Pair<DokkaSourceSet, String>> { + var counter = 0 + return nodes.toList().map { (sourceSet, elements) -> + sourceSet to createHTML(prettyPrint = false).div { + elements.forEach { + buildContentNode(it, pageContext, setOf(sourceSet)) + } + }.stripDiv() + }.groupBy( + Pair<DokkaSourceSet, String>::second, + Pair<DokkaSourceSet, String>::first + ).entries.flatMap { (html, sourceSets) -> + sourceSets.filterNot { + sourceSetDependencyMap[it].orEmpty().any { dependency -> sourceSets.contains(dependency) } + }.map { + it to createHTML(prettyPrint = false).div(classes = "content sourceset-depenent-content") { + if (counter++ == 0) attributes["data-active"] = "" + attributes["data-togglable"] = it.sourceSetID.toString() + unsafe { + +html + } + } + } + } + } + + override fun FlowContent.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) { + + val distinct = + node.groupDivergentInstances(pageContext, { instance, contentPage, sourceSet -> + createHTML(prettyPrint = false).div { + instance.before?.let { before -> + buildContentNode(before, pageContext, setOf(sourceSet)) + } + }.stripDiv() + }, { instance, contentPage, sourceSet -> + createHTML(prettyPrint = false).div { + instance.after?.let { after -> + buildContentNode(after, pageContext, setOf(sourceSet)) + } + }.stripDiv() + }) + + distinct.forEach { + val groupedDivergent = it.value.groupBy { it.second } + + consumer.onTagContentUnsafe { + +createHTML().div("divergent-group") { + attributes["data-filterable-current"] = groupedDivergent.keys.joinToString(" ") { + it.sourceSetID.toString() + } + attributes["data-filterable-set"] = groupedDivergent.keys.joinToString(" ") { + it.sourceSetID.toString() + } + + val divergentForPlatformDependent = groupedDivergent.map { (sourceSet, elements) -> + sourceSet to elements.map { e -> e.first.divergent } + }.toMap() + + val content = contentsForSourceSetDependent(divergentForPlatformDependent, pageContext) + + consumer.onTagContentUnsafe { + +createHTML().div("brief-with-platform-tags") { + consumer.onTagContentUnsafe { + +createHTML().div("inner-brief-with-platform-tags") { + consumer.onTagContentUnsafe { +it.key.first } + } + } + + consumer.onTagContentUnsafe { + +createHTML().span("pull-right") { + if ((distinct.size > 1 && groupedDivergent.size == 1) || groupedDivergent.size == 1 || content.size == 1) { + if (node.sourceSets.size != 1) { + createPlatformTags(node, setOf(content.first().first)) + } + } + } + } + } + } + div("main-subrow") { + if (node.implicitlySourceSetHinted) { + buildPlatformDependent(divergentForPlatformDependent, pageContext) + } else { + it.value.forEach { + buildContentNode(it.first.divergent, pageContext, setOf(it.second)) + } + } + } + consumer.onTagContentUnsafe { +it.key.second } + } + } + } + } + + override fun FlowContent.buildList( + node: ContentList, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) = if (node.ordered) ol { buildListItems(node.children, pageContext, sourceSetRestriction) } + else ul { buildListItems(node.children, pageContext, sourceSetRestriction) } + + open fun OL.buildListItems( + items: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + items.forEach { + if (it is ContentList) + buildList(it, pageContext) + else + li { it.build(this, pageContext, sourceSetRestriction) } + } + } + + open fun UL.buildListItems( + items: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + items.forEach { + if (it is ContentList) + buildList(it, pageContext) + else + li { it.build(this, pageContext) } + } + } + + override fun FlowContent.buildResource( + node: ContentEmbeddedResource, + pageContext: ContentPage + ) { // TODO: extension point there + val imageExtensions = setOf("png", "jpg", "jpeg", "gif", "bmp", "tif", "webp", "svg") + return if (File(node.address).extension.toLowerCase() in imageExtensions) { + //TODO: add imgAttrs parsing + val imgAttrs = node.extra.allOfType<SimpleAttr>().joinAttr() + img(src = node.address, alt = node.altText) + } else { + println("Unrecognized resource type: $node") + } + } + + private fun FlowContent.buildRow( + node: ContentGroup, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>?, + style: Set<Style> + ) { + node.children + .filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } + .takeIf { it.isNotEmpty() } + ?.let { + val anchorName = node.dci.dri.first().toString() + withAnchor(anchorName) { + div(classes = "table-row") { + if (!style.contains(MultimoduleTable)) { + attributes["data-filterable-current"] = node.sourceSets.joinToString(" ") { + it.sourceSetID.toString() + } + attributes["data-filterable-set"] = node.sourceSets.joinToString(" ") { + it.sourceSetID.toString() + } + } + + it.filterIsInstance<ContentLink>().takeIf { it.isNotEmpty() }?.let { + div("main-subrow " + node.style.joinToString(" ")) { + it.filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } + .forEach { + span { + it.build(this, pageContext, sourceSetRestriction) + buildAnchor(anchorName) + } + if (ContentKind.shouldBePlatformTagged(node.dci.kind) && (node.sourceSets.size == 1)) + createPlatformTags(node) + } + } + } + + it.filter { it !is ContentLink }.takeIf { it.isNotEmpty() }?.let { + div("platform-dependent-row keyValue") { + val title = it.filter { it.style.contains(ContentStyle.RowTitle) } + div { + title.forEach { + it.build(this, pageContext, sourceSetRestriction) + } + } + div("title") { + (it - title).forEach { + it.build(this, pageContext, sourceSetRestriction) + } + } + } + } + } + } + } + } + + private fun FlowContent.createPlatformTagBubbles(sourceSets: List<DokkaSourceSet>) { + if (isMultiplatform) { + div("platform-tags") { + sourceSets.forEach { + div("platform-tag") { + when (it.analysisPlatform.key) { + "common" -> classes = classes + "common-like" + "native" -> classes = classes + "native-like" + "jvm" -> classes = classes + "jvm-like" + "js" -> classes = classes + "js-like" + } + text(it.displayName) + } + } + } + } + } + + private fun FlowContent.createPlatformTags(node: ContentNode, sourceSetRestriction: Set<DokkaSourceSet>? = null) { + node.takeIf { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } }?.let { + createPlatformTagBubbles(node.sourceSets.filter { + sourceSetRestriction == null || it in sourceSetRestriction + }) + } + } + + override fun FlowContent.buildTable( + node: ContentTable, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) { + when (node.dci.kind) { + ContentKind.Comment -> buildDefaultTable(node, pageContext, sourceSetRestriction) + else -> div(classes = "table") { + node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } + node.children.forEach { + buildRow(it, pageContext, sourceSetRestriction, node.style) + } + } + } + + } + + fun FlowContent.buildDefaultTable( + node: ContentTable, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) { + table { + thead { + node.header.forEach { + tr { + it.children.forEach { + th { + it.build(this@table, pageContext, sourceSetRestriction) + } + } + } + } + } + tbody { + node.children.forEach { + tr { + it.children.forEach { + td { + it.build(this, pageContext, sourceSetRestriction) + } + } + } + } + } + } + } + + + override fun FlowContent.buildHeader(level: Int, node: ContentHeader, content: FlowContent.() -> Unit) { + val anchor = node.extra[SimpleAttr.SimpleAttrKey("anchor")]?.extraValue + val classes = node.style.joinToString { it.toString() }.toLowerCase() + when (level) { + 1 -> h1(classes = classes) { withAnchor(anchor, content) } + 2 -> h2(classes = classes) { withAnchor(anchor, content) } + 3 -> h3(classes = classes) { withAnchor(anchor, content) } + 4 -> h4(classes = classes) { withAnchor(anchor, content) } + 5 -> h5(classes = classes) { withAnchor(anchor, content) } + else -> h6(classes = classes) { withAnchor(anchor, content) } + } + } + + private fun FlowContent.withAnchor(anchorName: String?, content: FlowContent.() -> Unit) { + a { + anchorName?.let { attributes["data-name"] = it } + } + content() + } + + + override fun FlowContent.buildNavigation(page: PageNode) = + div(classes = "breadcrumbs") { + locationProvider.ancestors(page).asReversed().forEach { node -> + text("/") + if (node.isNavigable) buildLink(node, page) + else text(node.name) + } + } + + private fun FlowContent.buildLink(to: PageNode, from: PageNode) = + buildLink(locationProvider.resolve(to, from)) { + text(to.name) + } + + private fun FlowContent.buildAnchor(pointingTo: String) { + span(classes = "anchor-wrapper") { + span(classes = "anchor-icon") { + attributes["pointing-to"] = pointingTo + unsafe { + raw( + """ + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M21.2496 5.3C20.3496 4.5 19.2496 4 18.0496 4C16.8496 4 15.6496 4.5 14.8496 5.3L10.3496 9.8L11.7496 11.2L16.2496 6.7C17.2496 5.7 18.8496 5.7 19.8496 6.7C20.8496 7.7 20.8496 9.3 19.8496 10.3L15.3496 14.8L16.7496 16.2L21.2496 11.7C22.1496 10.8 22.5496 9.7 22.5496 8.5C22.5496 7.3 22.1496 6.2 21.2496 5.3Z"/> + <path d="M8.35 16.7998C7.35 17.7998 5.75 17.7998 4.75 16.7998C3.75 15.7998 3.75 14.1998 4.75 13.1998L9.25 8.6998L7.85 7.2998L3.35 11.7998C1.55 13.5998 1.55 16.3998 3.35 18.1998C4.25 19.0998 5.35 19.4998 6.55 19.4998C7.75 19.4998 8.85 19.0998 9.75 18.1998L14.25 13.6998L12.85 12.2998L8.35 16.7998Z"/> + </svg> + """.trimIndent() + ) + } + } + copiedPopup("Link copied to clipboard") + } + } + + fun FlowContent.buildLink( + to: DRI, + platforms: List<DokkaSourceSet>, + from: PageNode? = null, + block: FlowContent.() -> Unit + ) = buildLink(locationProvider.resolve(to, platforms.toSet(), from), block) + + override fun buildError(node: ContentNode) { + context.logger.error("Unknown ContentNode type: $node") + } + + override fun FlowContent.buildNewLine() { + br() + } + + override fun FlowContent.buildLink(address: String, content: FlowContent.() -> Unit) = + a(href = address, block = content) + + override fun FlowContent.buildCodeBlock( + code: ContentCodeBlock, + pageContext: ContentPage + ) { + div("sample-container") { + code(code.style.joinToString(" ") { it.toString().toLowerCase() }) { + attributes["theme"] = "idea" + code.children.forEach { buildContentNode(it, pageContext) } + } + } + } + + override fun FlowContent.buildCodeInline( + code: ContentCodeInline, + pageContext: ContentPage + ) { + code { + code.children.forEach { buildContentNode(it, pageContext) } + } + } + + private fun getSymbolSignature(page: ContentPage) = page.content.dfs { it.dci.kind == ContentKind.Symbol } + + private fun flattenToText(node: ContentNode): String { + fun getContentTextNodes(node: ContentNode, sourceSetRestriction: DokkaSourceSet): List<ContentText> = + when (node) { + is ContentText -> listOf(node) + is ContentComposite -> node.children + .filter { sourceSetRestriction in it.sourceSets } + .flatMap { getContentTextNodes(it, sourceSetRestriction) } + .takeIf { node.dci.kind != ContentKind.Annotations } + .orEmpty() + else -> emptyList() + } + + val sourceSetRestriction = + node.sourceSets.find { it.analysisPlatform == Platform.common } ?: node.sourceSets.first() + return getContentTextNodes(node, sourceSetRestriction).joinToString("") { it.text } + } + + override suspend fun renderPage(page: PageNode) { + super.renderPage(page) + if (page is ContentPage && page !is ModulePageNode && page !is PackagePageNode) { + val signature = getSymbolSignature(page) + val textNodes = signature?.let { flattenToText(it) } + val documentable = page.documentable + if (documentable != null) { + listOf( + documentable.dri.packageName, + documentable.dri.classNames, + documentable.dri.callable?.name + ) + .filter { !it.isNullOrEmpty() } + .takeIf { it.isNotEmpty() } + ?.joinToString(".") + ?.let { + pageList.put(it, Pair(textNodes ?: page.name, locationProvider.resolve(page))) + } + + } + } + } + + override fun FlowContent.buildText(textNode: ContentText) { + when { + textNode.hasStyle(TextStyle.Indented) -> consumer.onTagContentEntity(Entities.nbsp) + } + text(textNode.text) + } + + private fun generatePagesList() = + pageList.entries + .filter { !it.key.isNullOrEmpty() } + .groupBy { it.key.substringAfterLast(".") } + .entries + .mapIndexed { topLevelIndex, entry -> + if (entry.value.size > 1) { + listOf( + "{\'name\': \'${entry.key}\', \'index\': \'$topLevelIndex\', \'disabled\': true, \'searchKey\':\'${entry.key}\' }" + ) + entry.value.mapIndexed { index, subentry -> + "{\'name\': \'${subentry.value.first}\', \'level\': 1, \'index\': \'$topLevelIndex.$index\', \'description\':\'${subentry.key}\', \'location\':\'${subentry.value.second}\', 'searchKey':'${entry.key}'}" + } + } else { + val subentry = entry.value.single() + listOf( + "{\'name\': \'${subentry.value.first}\', \'index\': \'$topLevelIndex\', \'description\':\'${subentry.key}\', \'location\':\'${subentry.value.second}\', 'searchKey':'${entry.key}'}" + ) + } + } + .flatten() + .joinToString(prefix = "[", separator = ",\n", postfix = "]") + + override fun render(root: RootPageNode) { + super.render(root) + runBlocking(Dispatchers.Default) { + launch { + outputWriter.write("scripts/pages", "var pages = ${generatePagesList()}", ".js") + } + } + } + + private fun PageNode.root(path: String) = locationProvider.resolveRoot(this) + path + + override fun buildPage(page: ContentPage, content: (FlowContent, ContentPage) -> Unit): String = + buildHtml(page, page.embeddedResources) { + div { + id = "content" + attributes["pageIds"] = page.dri.first().toString() + content(this, page) + } + } + + private fun resolveLink(link: String, page: PageNode): String = if (URI(link).isAbsolute) link else page.root(link) + + open fun buildHtml(page: PageNode, resources: List<String>, content: FlowContent.() -> Unit) = + createHTML().html { + head { + meta(name = "viewport", content = "width=device-width, initial-scale=1", charset = "UTF-8") + title(page.name) + resources.forEach { + when { + it.substringBefore('?').substringAfterLast('.') == "css" -> link( + rel = LinkRel.stylesheet, + href = resolveLink(it, page) + ) + it.substringBefore('?').substringAfterLast('.') == "js" -> script( + type = ScriptType.textJavaScript, + src = resolveLink(it, page) + ) { + async = true + } + else -> unsafe { +it } + } + } + script { unsafe { +"""var pathToRoot = "${locationProvider.resolveRoot(page)}";""" } } + } + body { + div { + id = "container" + div { + id = "leftColumn" + div { + id = "logo" + } + div { + id = "sideMenu" + } + } + div { + id = "main" + div { + id = "searchBar" + } + script(type = ScriptType.textJavaScript, src = page.root("scripts/pages.js")) {} + script(type = ScriptType.textJavaScript, src = page.root("scripts/main.js")) {} + content() + div(classes = "footer") { + span("go-to-top-icon") { + a(href = "#container") { + unsafe { + raw( + """ + <svg width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M11.3337 9.66683H0.666992L6.00033 3.66683L11.3337 9.66683Z" fill="black"/> + <path d="M0.666992 0.333496H11.3337V1.66683H0.666992V0.333496Z" fill="black"/> + </svg> + """.trimIndent() + ) + } + } + } + span { text("© 2020 Copyright") } + span("pull-right") { + span { text("Sponsored and developed by dokka") } + a(href= "https://github.com/Kotlin/dokka") { + span(classes = "padded-icon") { + unsafe { + raw( + """ + <svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M8 0H2.3949L4.84076 2.44586L0 7.28662L0.713376 8L5.55414 3.15924L8 5.6051V0Z" fill="black"/> + </svg> + """.trimIndent() + ) + } + } + } + } + } + } + } + } + } +} + +fun List<SimpleAttr>.joinAttr() = joinToString(" ") { it.extraKey + "=" + it.extraValue } + +private fun String.stripDiv() = drop(5).dropLast(6) // TODO: Find a way to do it without arbitrary trims + +private val PageNode.isNavigable: Boolean + get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing + +fun PropertyContainer<ContentNode>.extraHtmlAttributes() = allOfType<SimpleAttr>() diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt new file mode 100644 index 00000000..8fe2a6c8 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt @@ -0,0 +1,51 @@ +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.RendererSpecificPage +import org.jetbrains.dokka.pages.RenderingStrategy + +class NavigationPage(val root: NavigationNode) : RendererSpecificPage { + override val name = "navigation" + + override val children = emptyList<PageNode>() + + override fun modified(name: String, children: List<PageNode>) = this + + override val strategy = RenderingStrategy<HtmlRenderer> { + createHTML().visit(root, "nav-submenu", this) + } + + private fun <R> TagConsumer<R>.visit(node: NavigationNode, navId: String, renderer: HtmlRenderer): R = + with(renderer) { + div("sideMenuPart") { + id = navId + attributes["pageId"] = node.dri.toString() + div("overview") { + buildLink(node.dri, node.sourceSets.toList()) { +node.name } + if (node.children.isNotEmpty()) { + span("navButton") { + onClick = """document.getElementById("$navId").classList.toggle("hidden");""" + span("navButtonContent") + } + } + } + node.children.withIndex().forEach { (n, p) -> visit(p, "$navId-$n", renderer) } + } + } +} + +class NavigationNode( + val name: String, + val dri: DRI, + val sourceSets: Set<DokkaSourceSet>, + val children: List<NavigationNode> +) + +fun NavigationPage.transform(block: (NavigationNode) -> NavigationNode) = NavigationPage(root.transform(block)) + +fun NavigationNode.transform(block: (NavigationNode) -> NavigationNode) = + run(block).let { NavigationNode(it.name, it.dri, it.sourceSets, it.children.map(block)) } diff --git a/plugins/base/src/main/kotlin/renderers/html/Tags.kt b/plugins/base/src/main/kotlin/renderers/html/Tags.kt new file mode 100644 index 00000000..a3951eff --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/Tags.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.* + +@HtmlTagMarker +fun FlowOrPhrasingContent.wbr(classes : String? = null, block : WBR.() -> Unit = {}) : Unit = WBR(attributesMapOf("class", classes), consumer).visit(block) + +@Suppress("unused") +open class WBR(initialAttributes : Map<String, String>, override val consumer : TagConsumer<*>) : HTMLTag("wbr", consumer, initialAttributes, null, true, false), + HtmlBlockInlineTag { + +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt new file mode 100644 index 00000000..2f7c8ee1 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt @@ -0,0 +1,112 @@ +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.h1 +import kotlinx.html.id +import kotlinx.html.table +import kotlinx.html.tbody +import org.jetbrains.dokka.base.renderers.sourceSets +import org.jetbrains.dokka.model.DEnum +import org.jetbrains.dokka.model.DEnumEntry +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer + + +object SearchPageInstaller : PageTransformer { + override fun invoke(input: RootPageNode) = input.modified(children = input.children + searchPage) + + private val searchPage = RendererSpecificResourcePage( + name = "Search", + children = emptyList(), + strategy = RenderingStrategy<HtmlRenderer> { + buildHtml(it, listOf("styles/style.css", "scripts/pages.js", "scripts/search.js")) { + h1 { + id = "searchTitle" + text("Search results for ") + } + table { + tbody { + id = "searchTable" + } + } + } + }) +} + +object NavigationPageInstaller : PageTransformer { + override fun invoke(input: RootPageNode) = input.modified( + children = input.children + NavigationPage( + input.children.filterIsInstance<ContentPage>().single() + .let(NavigationPageInstaller::visit) + ) + ) + + private fun visit(page: ContentPage): NavigationNode = + NavigationNode( + page.name, + page.dri.first(), + page.sourceSets(), + page.navigableChildren() + ) + + private fun ContentPage.navigableChildren(): List<NavigationNode> { + if (this !is ClasslikePageNode) { + return children.filterIsInstance<ContentPage>() + .map { visit(it) } + } else if (documentable is DEnum) { + return children.filter { it is ContentPage && it.documentable is DEnumEntry } + .map { visit(it as ContentPage) } + } + + return emptyList() + } +} + +object ResourceInstaller : PageTransformer { + override fun invoke(input: RootPageNode) = input.modified(children = input.children + resourcePages) + + private val resourcePages = listOf("styles", "scripts", "images").map { + RendererSpecificResourcePage(it, emptyList(), RenderingStrategy.Copy("/dokka/$it")) + } +} + +object StyleAndScriptsAppender : PageTransformer { + override fun invoke(input: RootPageNode) = input.transformContentPagesTree { + it.modified( + embeddedResources = it.embeddedResources + listOf( + "styles/style.css", + "scripts/navigationLoader.js", + "scripts/platformContentHandler.js", + "scripts/sourceset_dependencies.js", + "scripts/clipboard.js", + "styles/jetbrains-mono.css" + ) + ) + } +} + +class SourcesetDependencyAppender(val context: DokkaContext) : PageTransformer { + override fun invoke(input: RootPageNode): RootPageNode { + val dependenciesMap = context.configuration.sourceSets.map { + it.sourceSetID to it.dependentSourceSets + }.toMap() + + fun createDependenciesJson(): String = "sourceset_dependencies = '{${ + dependenciesMap.entries.joinToString(", ") { + "\"${it.key}\": [${it.value.joinToString(",") { + "\"$it\"" + }}]" + } + }}'" + + val deps = RendererSpecificResourcePage( + name = "scripts/sourceset_dependencies.js", + children = emptyList(), + strategy = RenderingStrategy.Write(createDependenciesJson()) + ) + + return input.modified( + children = input.children + deps + ) + } +} diff --git a/plugins/base/src/main/kotlin/renderers/preprocessors.kt b/plugins/base/src/main/kotlin/renderers/preprocessors.kt new file mode 100644 index 00000000..bf2a9eb4 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/preprocessors.kt @@ -0,0 +1,27 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer + +object RootCreator : PageTransformer { + override fun invoke(input: RootPageNode) = + RendererSpecificRootPage("", listOf(input), RenderingStrategy.DoNothing) +} + + +class PackageListCreator(val context: DokkaContext, val format: String, val linkExtension: String) : PageTransformer { + override fun invoke(input: RootPageNode) = + input.modified(children = input.children.map { + it.takeUnless { it is ModulePageNode } + ?: it.modified(children = it.children + packageList(input)) // TODO packageList should take module as an input + }) + + + private fun packageList(pageNode: RootPageNode) = + RendererSpecificResourcePage( + "${pageNode.name}/package-list", + emptyList(), + RenderingStrategy.Write(PackageListService(context).formatPackageList(pageNode, format, linkExtension)) + ) +} diff --git a/plugins/base/src/main/kotlin/resolvers/anchors/AnchorsHint.kt b/plugins/base/src/main/kotlin/resolvers/anchors/AnchorsHint.kt new file mode 100644 index 00000000..1b741484 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/anchors/AnchorsHint.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.base.resolvers.anchors + +import org.jetbrains.dokka.model.properties.ExtraProperty +import org.jetbrains.dokka.pages.ContentNode + +// TODO IMPORTANT: https://github.com/Kotlin/dokka/issues/1054 +object SymbolAnchorHint: ExtraProperty<ContentNode>, ExtraProperty.Key<ContentNode, SymbolAnchorHint> { + override val key = this +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt new file mode 100644 index 00000000..ff9186f7 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt @@ -0,0 +1,35 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.local.identifierToFilename +import org.jetbrains.dokka.links.DRI + + +class DokkaExternalLocationProviderFactory : ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( + object : ExternalLocationProviderFactory { + override fun getExternalLocationProvider(param: String): ExternalLocationProvider? = + when (param) { + "kotlin-website-html", "html" -> DokkaExternalLocationProvider(param, ".html") + "markdown" -> DokkaExternalLocationProvider(param, ".md") + else -> null + } + } +) + +class DokkaExternalLocationProvider(override val param: String, val extension: String) : ExternalLocationProvider { + + override fun DRI.toLocation(): String { // TODO: classes without packages? + + val classNamesChecked = classNames ?: return "${packageName ?: ""}/index$extension" + + val classLink = (listOfNotNull(packageName) + classNamesChecked.split('.')).joinToString( + "/", + transform = ::identifierToFilename + ) + + val callableChecked = callable ?: return "$classLink/index$extension" + + return "$classLink/${identifierToFilename( + callableChecked.name + )}$extension" + } +} diff --git a/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt new file mode 100644 index 00000000..83de9911 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt @@ -0,0 +1,26 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.links.DRI +import java.util.concurrent.ConcurrentHashMap + + +interface ExternalLocationProvider { + + val param: String + fun DRI.toLocation(): String +} + +interface ExternalLocationProviderFactory { + + fun getExternalLocationProvider(param: String): ExternalLocationProvider? +} + +class ExternalLocationProviderFactoryWithCache(val ext: ExternalLocationProviderFactory) : ExternalLocationProviderFactory { + + private val locationProviders = ConcurrentHashMap<String, CacheWrapper>() + + override fun getExternalLocationProvider(param: String): ExternalLocationProvider? = + locationProviders.getOrPut(param) { CacheWrapper(ext.getExternalLocationProvider(param)) }.provider +} + +private class CacheWrapper(val provider: ExternalLocationProvider?)
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt new file mode 100644 index 00000000..c52c9bbb --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt @@ -0,0 +1,37 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.utilities.htmlEscape + +class JavadocExternalLocationProviderFactory : ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( + object : ExternalLocationProviderFactory { + override fun getExternalLocationProvider(param: String): ExternalLocationProvider? = + when(param) { + "javadoc1" -> JavadocExternalLocationProvider(param, "()", ", ") // Covers JDK 1 - 7 + "javadoc8" -> JavadocExternalLocationProvider(param, "--", "-") // Covers JDK 8 - 9 + "javadoc10" -> JavadocExternalLocationProvider(param, "()", ",") // Covers JDK 10 + else -> null + } + } +) + +class JavadocExternalLocationProvider(override val param: String, val brackets: String, val separator: String) : ExternalLocationProvider { + + override fun DRI.toLocation(): String { + + val packageLink = packageName?.replace(".", "/") + if (classNames == null) { + return "$packageLink/package-summary.html".htmlEscape() + } + val classLink = if (packageLink == null) "$classNames.html" else "$packageLink/$classNames.html" + val callableChecked = callable ?: return classLink.htmlEscape() + + val callableLink = "$classLink#" + + callableChecked.name + + "${brackets.first()}" + + callableChecked.params.joinToString(separator) + + "${brackets.last()}" + + return callableLink.htmlEscape() + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/local/BaseLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/BaseLocationProvider.kt new file mode 100644 index 00000000..4204006e --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/BaseLocationProvider.kt @@ -0,0 +1,142 @@ +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +abstract class BaseLocationProvider(protected val dokkaContext: DokkaContext) : LocationProvider { + + protected val externalLocationProviderFactories = + dokkaContext.plugin<DokkaBase>().query { externalLocationProviderFactory } + private val cache: MutableMap<URL, DefaultLocationProvider.LocationInfo> = mutableMapOf() + private val lock = ReentrantReadWriteLock() + + protected fun getExternalLocation( + dri: DRI, + sourceSets: Set<DokkaSourceSet> + ): String { + val jdkToExternalDocumentationLinks = dokkaContext.configuration.sourceSets + .filter { sourceSet -> + sourceSets.contains(sourceSet) + } + .groupBy({ it.jdkVersion }, { it.externalDocumentationLinks }) + .map { it.key to it.value.flatten().distinct() }.toMap() + + val toResolve: MutableMap<Int, MutableList<DokkaConfiguration.ExternalDocumentationLink>> = mutableMapOf() + for ((jdk, links) in jdkToExternalDocumentationLinks) { + for (link in links) { + val info = lock.read { cache[link.packageListUrl] } + if (info == null) { + toResolve.getOrPut(jdk) { mutableListOf() }.add(link) + } else if (info.packages.contains(dri.packageName)) { + return link.url.toExternalForm() + getLink(dri, info) + } + } + } + // Not in cache, resolve packageLists + for ((jdk, links) in toResolve) { + for (link in links) { + if (dokkaContext.configuration.offlineMode && link.packageListUrl.protocol.toLowerCase() != "file") + continue + val locationInfo = + loadPackageList(jdk, link.packageListUrl) + if (locationInfo.packages.contains(dri.packageName)) { + return link.url.toExternalForm() + getLink(dri, locationInfo) + } + } + toResolve.remove(jdk) + } + return "" + } + + private fun getLink(dri: DRI, locationInfo: DefaultLocationProvider.LocationInfo): String = + locationInfo.locations[dri.packageName + "." + dri.classNames] + ?: // Not sure if it can be here, previously it shadowed only kotlin/dokka related sources, here it shadows both dokka/javadoc, cause I cannot distinguish what LocationProvider has been hypothetically chosen + if (locationInfo.externalLocationProvider != null) + with(locationInfo.externalLocationProvider) { + dri.toLocation() + } + else + throw IllegalStateException("Have not found any convenient ExternalLocationProvider for $dri DRI!") + + private fun loadPackageList(jdk: Int, url: URL): DefaultLocationProvider.LocationInfo = lock.write { + val packageListStream = url.doOpenConnectionToReadContent().getInputStream() + val (params, packages) = + packageListStream + .bufferedReader() + .useLines { lines -> lines.partition { it.startsWith(DOKKA_PARAM_PREFIX) } } + + val paramsMap = params.asSequence() + .map { it.removePrefix(DOKKA_PARAM_PREFIX).split(":", limit = 2) } + .groupBy({ (key, _) -> key }, { (_, value) -> value }) + + val format = paramsMap["format"]?.singleOrNull() ?: when { + jdk < 8 -> "javadoc1" // Covers JDK 1 - 7 + jdk < 10 -> "javadoc8" // Covers JDK 8 - 9 + else -> "javadoc10" // Covers JDK 10+ + } + + val locations = paramsMap["location"].orEmpty() + .map { it.split("\u001f", limit = 2) } + .map { (key, value) -> key to value } + .toMap() + + val externalLocationProvider = + externalLocationProviderFactories.asSequence().map { it.getExternalLocationProvider(format) } + .filterNotNull().take(1).firstOrNull() + + val info = DefaultLocationProvider.LocationInfo( + externalLocationProvider, + packages.toSet(), + locations + ) + cache[url] = info + return info + } + + private fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection { + val connection = this.openConnection().apply { + connectTimeout = timeout + readTimeout = timeout + } + + when (connection) { + is HttpURLConnection -> { + return when (connection.responseCode) { + in 200..299 -> { + connection + } + HttpURLConnection.HTTP_MOVED_PERM, + HttpURLConnection.HTTP_MOVED_TEMP, + HttpURLConnection.HTTP_SEE_OTHER -> { + if (redirectsAllowed > 0) { + val newUrl = connection.getHeaderField("Location") + URL(newUrl).doOpenConnectionToReadContent(timeout, redirectsAllowed - 1) + } else { + throw RuntimeException("Too many redirects") + } + } + else -> { + throw RuntimeException("Unhandled http code: ${connection.responseCode}") + } + } + } + else -> return connection + } + } + + companion object { + const val DOKKA_PARAM_PREFIX = "\$dokka." + } + +} diff --git a/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt new file mode 100644 index 00000000..1df0a700 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt @@ -0,0 +1,217 @@ +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProvider +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection +import java.util.* + +private const val PAGE_WITH_CHILDREN_SUFFIX = "index" + +open class DefaultLocationProvider( + protected val pageGraphRoot: RootPageNode, + dokkaContext: DokkaContext +) : BaseLocationProvider(dokkaContext) { + protected open val extension = ".html" + + protected val pagesIndex: Map<DRI, ContentPage> = pageGraphRoot.withDescendants().filterIsInstance<ContentPage>() + .map { it.dri.map { dri -> dri to it } }.flatten() + .groupingBy { it.first } + .aggregate { dri, _, (_, page), first -> + if (first) page else throw AssertionError("Multiple pages associated with dri: $dri") + } + + protected val anchorsIndex = pageGraphRoot.withDescendants().filterIsInstance<ContentPage>() + .flatMap { page -> + page.content.withDescendants() + .filter { it.extra[SymbolAnchorHint] != null } + .mapNotNull { it.dci.dri.singleOrNull() } + .distinct() + .map { it to page } + }.toMap() + + + protected val pathsIndex: Map<PageNode, List<String>> = IdentityHashMap<PageNode, List<String>>().apply { + fun registerPath(page: PageNode, prefix: List<String>) { + val newPrefix = prefix + page.pathName + put(page, newPrefix) + page.children.forEach { registerPath(it, newPrefix) } + } + put(pageGraphRoot, emptyList()) + pageGraphRoot.children.forEach { registerPath(it, emptyList()) } + } + + override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean): String = + pathTo(node, context) + if (!skipExtension) extension else "" + + override fun resolve(dri: DRI, sourceSets: Set<DokkaSourceSet>, context: PageNode?): String = + pagesIndex[dri]?.let { resolve(it, context) } + ?: anchorsIndex[dri]?.let { resolve(it, context) + "#$dri" } + // Not found in PageGraph, that means it's an external link + ?: getExternalLocation(dri, sourceSets) + + override fun resolveRoot(node: PageNode): String = + pathTo(pageGraphRoot, node).removeSuffix(PAGE_WITH_CHILDREN_SUFFIX) + + override fun ancestors(node: PageNode): List<PageNode> = + generateSequence(node) { it.parent() }.toList() + + protected open fun pathTo(node: PageNode, context: PageNode?): String { + fun pathFor(page: PageNode) = pathsIndex[page] ?: throw AssertionError( + "${page::class.simpleName}(${page.name}) does not belong to current page graph so it is impossible to compute its path" + ) + + val contextNode = + if (context !is ClasslikePageNode && context?.children?.isEmpty() == true && context.parent() != null) context.parent() else context + val nodePath = pathFor(node) + val contextPath = contextNode?.let { pathFor(it) }.orEmpty() + + val commonPathElements = nodePath.asSequence().zip(contextPath.asSequence()) + .takeWhile { (a, b) -> a == b }.count() + + return (List(contextPath.size - commonPathElements) { ".." } + nodePath.drop(commonPathElements) + + if (node is ClasslikePageNode || node.children.isNotEmpty()) + listOf(PAGE_WITH_CHILDREN_SUFFIX) + else + emptyList() + ).joinToString("/") + } + + private fun PageNode.parent() = pageGraphRoot.parentMap[this] + + private val cache: MutableMap<URL, LocationInfo> = mutableMapOf() + + private fun getLocation( + dri: DRI, + jdkToExternalDocumentationLinks: Map<Int, List<DokkaConfiguration.ExternalDocumentationLink>> + ): String { + val toResolve: MutableMap<Int, MutableList<DokkaConfiguration.ExternalDocumentationLink>> = mutableMapOf() + for ((jdk, links) in jdkToExternalDocumentationLinks) { + for (link in links) { + val info = cache[link.packageListUrl] + if (info == null) { + toResolve.getOrPut(jdk) { mutableListOf() }.add(link) + } else if (info.packages.contains(dri.packageName)) { + return link.url.toExternalForm() + getLink(dri, info) + } + } + } + // Not in cache, resolve packageLists + for ((jdk, links) in toResolve) { + for (link in links) { + if(dokkaContext.configuration.offlineMode && link.packageListUrl.protocol.toLowerCase() != "file") + continue + val locationInfo = + loadPackageList(jdk, link.packageListUrl) + if (locationInfo.packages.contains(dri.packageName)) { + return link.url.toExternalForm() + getLink(dri, locationInfo) + } + } + toResolve.remove(jdk) + } + return "" + } + + private fun getLink(dri: DRI, locationInfo: LocationInfo): String = + locationInfo.locations[dri.packageName + "." + dri.classNames] + ?: // Not sure if it can be here, previously it shadowed only kotlin/dokka related sources, here it shadows both dokka/javadoc, cause I cannot distinguish what LocationProvider has been hypothetically chosen + if (locationInfo.externalLocationProvider != null) + with(locationInfo.externalLocationProvider) { + dri.toLocation() + } + else + throw IllegalStateException("Have not found any convenient ExternalLocationProvider for $dri DRI!") + + private fun loadPackageList(jdk: Int, url: URL): LocationInfo { + val packageListStream = url.doOpenConnectionToReadContent().getInputStream() + val (params, packages) = + packageListStream + .bufferedReader() + .useLines { lines -> lines.partition { it.startsWith(DOKKA_PARAM_PREFIX) } } + + val paramsMap = params.asSequence() + .map { it.removePrefix(DOKKA_PARAM_PREFIX).split(":", limit = 2) } + .groupBy({ (key, _) -> key }, { (_, value) -> value }) + + val format = paramsMap["format"]?.singleOrNull() ?: when { + jdk < 8 -> "javadoc1" // Covers JDK 1 - 7 + jdk < 10 -> "javadoc8" // Covers JDK 8 - 9 + else -> "javadoc10" // Covers JDK 10+ + } + + val locations = paramsMap["location"].orEmpty() + .map { it.split("\u001f", limit = 2) } + .map { (key, value) -> key to value } + .toMap() + + val externalLocationProvider = + externalLocationProviderFactories.asSequence().map { it.getExternalLocationProvider(format) } + .filterNotNull().take(1).firstOrNull() + + val info = LocationInfo( + externalLocationProvider, + packages.toSet(), + locations + ) + cache[url] = info + return info + } + + private fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection { + val connection = this.openConnection().apply { + connectTimeout = timeout + readTimeout = timeout + } + + when (connection) { + is HttpURLConnection -> { + return when (connection.responseCode) { + in 200..299 -> { + connection + } + HttpURLConnection.HTTP_MOVED_PERM, + HttpURLConnection.HTTP_MOVED_TEMP, + HttpURLConnection.HTTP_SEE_OTHER -> { + if (redirectsAllowed > 0) { + val newUrl = connection.getHeaderField("Location") + URL(newUrl).doOpenConnectionToReadContent(timeout, redirectsAllowed - 1) + } else { + throw RuntimeException("Too many redirects") + } + } + else -> { + throw RuntimeException("Unhandled http code: ${connection.responseCode}") + } + } + } + else -> return connection + } + } + + data class LocationInfo( + val externalLocationProvider: ExternalLocationProvider?, + val packages: Set<String>, + val locations: Map<String, String> + ) +} + +private val reservedFilenames = setOf("index", "con", "aux", "lst", "prn", "nul", "eof", "inp", "out") + +internal fun identifierToFilename(name: String): String { + if (name.isEmpty()) return "--root--" + val escaped = name.replace("<|>".toRegex(), "-") + val lowercase = escaped.replace("[A-Z]".toRegex()) { matchResult -> "-" + matchResult.value.toLowerCase() } + return if (lowercase in reservedFilenames) "--$lowercase--" else lowercase +} + +private val PageNode.pathName: String + get() = if (this is PackagePageNode) name else identifierToFilename( + name + ) diff --git a/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt new file mode 100644 index 00000000..442d2e6d --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt @@ -0,0 +1,23 @@ +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.pages.MultimoduleRootPageNode +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +class DefaultLocationProviderFactory(private val context: DokkaContext) : LocationProviderFactory { + + private val cache = ConcurrentHashMap<CacheWrapper, LocationProvider>() + + override fun getLocationProvider(pageNode: RootPageNode) = cache.computeIfAbsent(CacheWrapper(pageNode)) { + if (pageNode.children.first() is MultimoduleRootPageNode) MultimoduleLocationProvider(pageNode, context) + else DefaultLocationProvider(pageNode, context) + } +} + +private class CacheWrapper(val pageNode: RootPageNode) { + override fun equals(other: Any?) = other is CacheWrapper && other.pageNode == this.pageNode + + override fun hashCode() = System.identityHashCode(pageNode) +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt new file mode 100644 index 00000000..745636d0 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.RootPageNode + +interface LocationProvider { + fun resolve(dri: DRI, sourceSets: Set<DokkaSourceSet>, context: PageNode? = null): String + fun resolve(node: PageNode, context: PageNode? = null, skipExtension: Boolean = false): String + fun resolveRoot(node: PageNode): String + fun ancestors(node: PageNode): List<PageNode> +} + +interface LocationProviderFactory { + fun getLocationProvider(pageNode: RootPageNode): LocationProvider +} + diff --git a/plugins/base/src/main/kotlin/resolvers/local/MultimoduleLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/MultimoduleLocationProvider.kt new file mode 100644 index 00000000..54aded35 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/MultimoduleLocationProvider.kt @@ -0,0 +1,32 @@ +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext + +class MultimoduleLocationProvider(private val root: RootPageNode, context: DokkaContext) : LocationProvider { + + private val defaultLocationProvider = DefaultLocationProvider(root, context) + + val paths = context.configuration.modules.map { + it.name to it.path + }.toMap() + + override fun resolve(dri: DRI, sourceSets: Set<DokkaSourceSet>, context: PageNode?): String = + dri.takeIf { it.packageName == MULTIMODULE_PACKAGE_PLACEHOLDER }?.classNames?.let { paths[it] }?.let { + "$it/${identifierToFilename(dri.classNames.orEmpty())}/index.html" + } ?: defaultLocationProvider.resolve(dri, sourceSets, context) + + override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean): String = + defaultLocationProvider.resolve(node, context, skipExtension) + + override fun resolveRoot(node: PageNode): String = defaultLocationProvider.resolveRoot(node) + + override fun ancestors(node: PageNode): List<PageNode> = listOf(root) + + companion object { + const val MULTIMODULE_PACKAGE_PLACEHOLDER = ".ext" + } +} diff --git a/plugins/base/src/main/kotlin/signatures/JvmSignatureUtils.kt b/plugins/base/src/main/kotlin/signatures/JvmSignatureUtils.kt new file mode 100644 index 00000000..689f6db5 --- /dev/null +++ b/plugins/base/src/main/kotlin/signatures/JvmSignatureUtils.kt @@ -0,0 +1,141 @@ +package org.jetbrains.dokka.base.signatures + +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet + +interface JvmSignatureUtils { + + fun PageContentBuilder.DocumentableContentBuilder.annotationsBlock(d: Documentable) + + fun PageContentBuilder.DocumentableContentBuilder.annotationsInline(d: Documentable) + + fun <T : Documentable> WithExtraProperties<T>.modifiers(): SourceSetDependent<Set<ExtraModifiers>> + + fun Collection<ExtraModifiers>.toSignatureString(): String = + joinToString("") { it.name.toLowerCase() + " " } + + fun <T : Documentable> WithExtraProperties<T>.annotations(): SourceSetDependent<List<Annotations.Annotation>> = + extra[Annotations]?.content ?: emptyMap() + + private fun PageContentBuilder.DocumentableContentBuilder.annotations( + d: Documentable, + ignored: Set<Annotations.Annotation>, + styles: Set<Style>, + operation: PageContentBuilder.DocumentableContentBuilder.(Annotations.Annotation) -> Unit + ): Unit = when (d) { + is DFunction -> d.annotations() + is DProperty -> d.annotations() + is DClass -> d.annotations() + is DInterface -> d.annotations() + is DObject -> d.annotations() + is DEnum -> d.annotations() + is DAnnotation -> d.annotations() + is DTypeParameter -> d.annotations() + is DEnumEntry -> d.annotations() + is DTypeAlias -> d.annotations() + is DParameter -> d.annotations() + else -> null + }?.let { + it.entries.forEach { + it.value.filter { it !in ignored && it.mustBeDocumented }.takeIf { it.isNotEmpty() }?.let { annotations -> + group(sourceSets = setOf(it.key), styles = styles, kind = ContentKind.Annotations) { + annotations.forEach { + operation(it) + } + } + } + } + } ?: Unit + + fun PageContentBuilder.DocumentableContentBuilder.toSignatureString( + a: Annotations.Annotation, + renderAtStrategy: AtStrategy, + listBrackets: Pair<Char, Char>, + classExtension: String + ) { + + when (renderAtStrategy) { + is All, is OnlyOnce -> text("@") + is Never -> Unit + } + link(a.dri.classNames!!, a.dri) + text("(") + a.params.entries.forEachIndexed { i, it -> + group(styles = setOf(TextStyle.BreakableAfter)) { + text(it.key + " = ") + when (renderAtStrategy) { + is All -> All + is Never, is OnlyOnce -> Never + }.let { strategy -> + valueToSignature(it.value, strategy, listBrackets, classExtension) + } + if (i != a.params.entries.size - 1) text(", ") + } + } + text(")") + } + + private fun PageContentBuilder.DocumentableContentBuilder.valueToSignature( + a: AnnotationParameterValue, + renderAtStrategy: AtStrategy, + listBrackets: Pair<Char, Char>, + classExtension: String + ): Unit = when (a) { + is AnnotationValue -> toSignatureString(a.annotation, renderAtStrategy, listBrackets, classExtension) + is ArrayValue -> { + text(listBrackets.first.toString()) + a.value.forEachIndexed { i, it -> + group(styles = setOf(TextStyle.BreakableAfter)) { + valueToSignature(it, renderAtStrategy, listBrackets, classExtension) + if (i != a.value.size - 1) text(", ") + } + } + text(listBrackets.second.toString()) + } + is EnumValue -> link(a.enumName, a.enumDri) + is ClassValue -> link(a.className + classExtension, a.classDRI) + is StringValue -> group(styles = setOf(TextStyle.Breakable)) { text(a.value) } + } + + fun PageContentBuilder.DocumentableContentBuilder.annotationsBlockWithIgnored( + d: Documentable, + ignored: Set<Annotations.Annotation>, + renderAtStrategy: AtStrategy, + listBrackets: Pair<Char, Char>, + classExtension: String + ) { + annotations(d, ignored, setOf(TextStyle.Block)) { + group { + toSignatureString(it, renderAtStrategy, listBrackets, classExtension) + } + } + } + + fun PageContentBuilder.DocumentableContentBuilder.annotationsInlineWithIgnored( + d: Documentable, + ignored: Set<Annotations.Annotation>, + renderAtStrategy: AtStrategy, + listBrackets: Pair<Char, Char>, + classExtension: String + ) { + annotations(d, ignored, setOf(TextStyle.Span)) { + toSignatureString(it, renderAtStrategy, listBrackets, classExtension) + text(Typography.nbsp.toString()) + } + } + + fun <T : Documentable> WithExtraProperties<T>.stylesIfDeprecated(sourceSetData: DokkaSourceSet): Set<TextStyle> = + if (extra[Annotations]?.content?.get(sourceSetData)?.any { + it.dri == DRI("kotlin", "Deprecated") + || it.dri == DRI("java.lang", "Deprecated") + } == true) setOf(TextStyle.Strikethrough) else emptySet() +} + +sealed class AtStrategy +object All : AtStrategy() +object OnlyOnce : AtStrategy() +object Never : AtStrategy() diff --git a/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt b/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt new file mode 100644 index 00000000..37e0ea83 --- /dev/null +++ b/plugins/base/src/main/kotlin/signatures/KotlinSignatureProvider.kt @@ -0,0 +1,382 @@ +package org.jetbrains.dokka.base.signatures + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.dri +import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.driOrNull +import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.Nullable +import org.jetbrains.dokka.model.TypeConstructor +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.ContentKind +import org.jetbrains.dokka.pages.ContentNode +import org.jetbrains.dokka.pages.TextStyle +import org.jetbrains.dokka.utilities.DokkaLogger +import kotlin.text.Typography.nbsp + +class KotlinSignatureProvider(ctcc: CommentsToContentConverter, logger: DokkaLogger) : SignatureProvider, + JvmSignatureUtils by KotlinSignatureUtils { + private val contentBuilder = PageContentBuilder(ctcc, this, logger) + + private val ignoredVisibilities = setOf(JavaVisibility.Public, KotlinVisibility.Public) + private val ignoredModifiers = setOf(JavaModifier.Final, KotlinModifier.Final) + private val ignoredExtraModifiers = setOf( + ExtraModifiers.KotlinOnlyModifiers.TailRec, + ExtraModifiers.KotlinOnlyModifiers.External + ) + private val platformSpecificModifiers: Map<ExtraModifiers, Set<Platform>> = mapOf( + ExtraModifiers.KotlinOnlyModifiers.External to setOf(Platform.js) + ) + + override fun signature(documentable: Documentable): List<ContentNode> = when (documentable) { + is DFunction -> functionSignature(documentable) + is DProperty -> propertySignature(documentable) + is DClasslike -> classlikeSignature(documentable) + is DTypeParameter -> signature(documentable) + is DEnumEntry -> signature(documentable) + is DTypeAlias -> signature(documentable) + else -> throw NotImplementedError( + "Cannot generate signature for ${documentable::class.qualifiedName} ${documentable.name}" + ) + } + + private fun <T> PageContentBuilder.DocumentableContentBuilder.processExtraModifiers(t: T) + where T : Documentable, T : WithExtraProperties<T> { + sourceSetDependentText( + t.modifiers() + .mapValues { entry -> + entry.value.filter { + it !in ignoredExtraModifiers || entry.key.analysisPlatform in (platformSpecificModifiers[it] + ?: emptySet()) + } + } + ) { + it.toSignatureString() + } + } + + private fun signature(e: DEnumEntry): List<ContentNode> = + e.sourceSets.map { + contentBuilder.contentFor( + e, + ContentKind.Symbol, + setOf(TextStyle.Monospace, TextStyle.Block) + e.stylesIfDeprecated(it), + sourceSets = setOf(it) + ) { + group(styles = setOf(TextStyle.Block)) { + annotationsBlock(e) + link(e.name, e.dri, styles = emptySet()) + e.extra[ConstructorValues]?.let { constructorValues -> + constructorValues.values[it] + text(constructorValues.values[it]?.joinToString(prefix = "(", postfix = ")") ?: "") + } + } + } + } + + private fun actualTypealiasedSignature(c: DClasslike, sourceSet: DokkaSourceSet, aliasedType: Bound) = + contentBuilder.contentFor( + c, + ContentKind.Symbol, + setOf(TextStyle.Monospace) + ((c as? WithExtraProperties<out Documentable>)?.stylesIfDeprecated(sourceSet) + ?: emptySet()), + sourceSets = setOf(sourceSet) + ) { + text("actual typealias ") + link(c.name.orEmpty(), c.dri) + text(" = ") + signatureForProjection(aliasedType) + } + + @Suppress("UNCHECKED_CAST") + private fun <T : DClasslike> classlikeSignature(c: T): List<ContentNode> = + c.sourceSets.map { sourceSetData -> + (c as? WithExtraProperties<out DClasslike>)?.extra?.get(ActualTypealias)?.underlyingType?.get(sourceSetData) + ?.let { + actualTypealiasedSignature(c, sourceSetData, it) + } ?: regularSignature(c, sourceSetData) + } + + + private fun regularSignature(c: DClasslike, sourceSet: DokkaSourceSet) = + contentBuilder.contentFor( + c, + ContentKind.Symbol, + setOf(TextStyle.Monospace) + ((c as? WithExtraProperties<out Documentable>)?.stylesIfDeprecated(sourceSet) + ?: emptySet()), + sourceSets = setOf(sourceSet) + ) { + annotationsBlock(c) + text(c.visibility[sourceSet]?.takeIf { it !in ignoredVisibilities }?.name?.let { "$it " } ?: "") + if (c is DClass) { + text( + if (c.modifier[sourceSet] !in ignoredModifiers) + when { + c.extra[AdditionalModifiers]?.content?.contains(ExtraModifiers.KotlinOnlyModifiers.Data) == true -> "" + c.modifier[sourceSet] is JavaModifier.Empty -> "${KotlinModifier.Open.name} " + else -> c.modifier[sourceSet]?.name?.let { "$it " } ?: "" + } + else + "" + ) + } + if (c is DInterface) { + c.extra[AdditionalModifiers]?.content?.let { additionalModifiers -> + sourceSetDependentText(additionalModifiers, setOf(sourceSet)) { extraModifiers -> + if (ExtraModifiers.KotlinOnlyModifiers.Fun in extraModifiers) "fun " + else "" + } + } + } + when (c) { + is DClass -> { + processExtraModifiers(c) + text("class ") + } + is DInterface -> { + processExtraModifiers(c) + text("interface ") + } + is DEnum -> { + processExtraModifiers(c) + text("enum ") + } + is DObject -> { + processExtraModifiers(c) + text("object ") + } + is DAnnotation -> { + processExtraModifiers(c) + text("annotation class ") + } + } + link(c.name!!, c.dri) + if (c is WithGenerics) { + list(c.generics, prefix = "<", suffix = "> ") { + +buildSignature(it) + } + } + if (c is WithConstructors) { + val pConstructor = c.constructors.singleOrNull { it.extra[PrimaryConstructorExtra] != null } + if (pConstructor?.sourceSets?.contains(sourceSet) == true) { + if (pConstructor.annotations().values.any { it.isNotEmpty() }) { + text(nbsp.toString()) + annotationsInline(pConstructor) + text("constructor") + } + list( + pConstructor.parameters, + "(", + ")", + ",", + pConstructor.sourceSets.toSet() + ) { + annotationsInline(it) + text(it.name ?: "", styles = mainStyles.plus(TextStyle.Bold)) + text(": ") + signatureForProjection(it.type) + } + } + } + if (c is WithSupertypes) { + c.supertypes.filter { it.key == sourceSet }.map { (s, dris) -> + list(dris, prefix = " : ", sourceSets = setOf(s)) { + link(it.dri.sureClassNames, it.dri, sourceSets = setOf(s)) + } + } + } + } + + private fun propertySignature(p: DProperty) = + p.sourceSets.map { + contentBuilder.contentFor( + p, + ContentKind.Symbol, + setOf(TextStyle.Monospace) + p.stylesIfDeprecated(it), + sourceSets = setOf(it) + ) { + annotationsBlock(p) + text(p.visibility[it].takeIf { it !in ignoredVisibilities }?.name?.let { "$it " } ?: "") + text( + p.modifier[it].takeIf { it !in ignoredModifiers }?.let { + if (it is JavaModifier.Empty) KotlinModifier.Open else it + }?.name?.let { "$it " } ?: "" + ) + text(p.modifiers()[it]?.toSignatureString() ?: "") + p.setter?.let { text("var ") } ?: text("val ") + list(p.generics, prefix = "<", suffix = "> ") { + +buildSignature(it) + } + p.receiver?.also { + signatureForProjection(it.type) + text(".") + } + link(p.name, p.dri) + text(": ") + signatureForProjection(p.type) + } + } + + private fun functionSignature(f: DFunction) = + f.sourceSets.map { + contentBuilder.contentFor( + f, + ContentKind.Symbol, + setOf(TextStyle.Monospace) + f.stylesIfDeprecated(it), + sourceSets = setOf(it) + ) { + annotationsBlock(f) + text(f.visibility[it]?.takeIf { it !in ignoredVisibilities }?.name?.let { "$it " } ?: "") + text(f.modifier[it]?.takeIf { it !in ignoredModifiers }?.let { + if (it is JavaModifier.Empty) KotlinModifier.Open else it + }?.name?.let { "$it " } ?: "" + ) + text(f.modifiers()[it]?.toSignatureString() ?: "") + text("fun ") + list(f.generics, prefix = "<", suffix = "> ") { + +buildSignature(it) + } + f.receiver?.also { + signatureForProjection(it.type) + text(".") + } + link(f.name, f.dri) + text("(") + list(f.parameters) { + annotationsInline(it) + processExtraModifiers(it) + text(it.name!!) + text(": ") + signatureForProjection(it.type) + } + text(")") + if (f.documentReturnType()) { + text(": ") + signatureForProjection(f.type) + } + } + } + + private fun DFunction.documentReturnType() = when { + this.isConstructor -> false + this.type is TypeConstructor && (this.type as TypeConstructor).dri == DriOfUnit -> false + this.type is Void -> false + else -> true + } + + private fun signature(t: DTypeAlias) = + t.sourceSets.map { + contentBuilder.contentFor(t, styles = t.stylesIfDeprecated(it), sourceSets = setOf(it)) { + t.underlyingType.entries.groupBy({ it.value }, { it.key }).map { (type, platforms) -> + +contentBuilder.contentFor( + t, + ContentKind.Symbol, + setOf(TextStyle.Monospace), + sourceSets = platforms.toSet() + ) { + text(t.visibility[it]?.takeIf { it !in ignoredVisibilities }?.name?.let { "$it " } ?: "") + processExtraModifiers(t) + text("typealias ") + signatureForProjection(t.type) + text(" = ") + signatureForTypealiasTarget(t, type) + } + } + } + } + + private fun signature(t: DTypeParameter) = + t.sourceSets.map { + contentBuilder.contentFor(t, styles = t.stylesIfDeprecated(it), sourceSets = setOf(it)) { + link(t.name, t.dri.withTargetToDeclaration()) + list(t.bounds, prefix = " : ") { + signatureForProjection(it) + } + } + } + + private fun PageContentBuilder.DocumentableContentBuilder.signatureForTypealiasTarget( + typeAlias: DTypeAlias, bound: Bound + ) { + signatureForProjection( + p = bound, + showFullyQualifiedName = + bound.driOrNull?.packageName != typeAlias.dri.packageName && + bound.driOrNull?.packageName != "kotlin" + ) + } + + private fun PageContentBuilder.DocumentableContentBuilder.signatureForProjection( + p: Projection, showFullyQualifiedName: Boolean = false + ): Unit = + when (p) { + is OtherParameter -> link(p.name, p.declarationDRI) + + is TypeConstructor -> if (p.function) + +funType(mainDRI.single(), mainSourcesetData, p) + else + group(styles = emptySet()) { + val linkText = if (showFullyQualifiedName && p.dri.packageName != null) { + "${p.dri.packageName}.${p.dri.classNames.orEmpty()}" + } else p.dri.classNames.orEmpty() + + link(linkText, p.dri) + list(p.projections, prefix = "<", suffix = ">") { + signatureForProjection(it, showFullyQualifiedName) + } + } + + is Variance -> group(styles = emptySet()) { + text(p.kind.toString() + " ") + signatureForProjection(p.inner, showFullyQualifiedName) + } + + is Star -> text("*") + + is Nullable -> group(styles = emptySet()) { + signatureForProjection(p.inner, showFullyQualifiedName) + text("?") + } + + is JavaObject -> link("Any", DriOfAny) + is Void -> link("Unit", DriOfUnit) + is PrimitiveJavaType -> signatureForProjection(p.translateToKotlin(), showFullyQualifiedName) + is Dynamic -> text("dynamic") + is UnresolvedBound -> text(p.name) + } + + private fun funType(dri: DRI, sourceSets: Set<DokkaSourceSet>, type: TypeConstructor) = + contentBuilder.contentFor(dri, sourceSets, ContentKind.Main) { + if (type.extension) { + signatureForProjection(type.projections.first()) + text(".") + } + + val args = if (type.extension) + type.projections.drop(1) + else + type.projections + + text("(") + args.subList(0, args.size - 1).forEachIndexed { i, arg -> + signatureForProjection(arg) + if (i < args.size - 2) text(", ") + } + text(") -> ") + signatureForProjection(args.last()) + } +} + +private fun PrimitiveJavaType.translateToKotlin() = TypeConstructor( + dri = dri, + projections = emptyList() +) + +val TypeConstructor.function + get() = modifier == FunctionModifiers.FUNCTION || modifier == FunctionModifiers.EXTENSION + +val TypeConstructor.extension + get() = modifier == FunctionModifiers.EXTENSION diff --git a/plugins/base/src/main/kotlin/signatures/KotlinSignatureUtils.kt b/plugins/base/src/main/kotlin/signatures/KotlinSignatureUtils.kt new file mode 100644 index 00000000..0a10875a --- /dev/null +++ b/plugins/base/src/main/kotlin/signatures/KotlinSignatureUtils.kt @@ -0,0 +1,48 @@ +package org.jetbrains.dokka.base.signatures + +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.DriOfAny +import org.jetbrains.dokka.links.DriOfUnit +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.WithExtraProperties + +object KotlinSignatureUtils : JvmSignatureUtils { + + private val strategy = OnlyOnce + private val listBrackets = Pair('[', ']') + private val classExtension = "::class" + private val ignoredAnnotations = setOf( + Annotations.Annotation(DRI("kotlin", "SinceKotlin"), emptyMap()), + Annotations.Annotation(DRI("kotlin", "Deprecated"), emptyMap()) + ) + + + override fun PageContentBuilder.DocumentableContentBuilder.annotationsBlock(d: Documentable) = + annotationsBlockWithIgnored(d, ignoredAnnotations, strategy, listBrackets, classExtension) + + override fun PageContentBuilder.DocumentableContentBuilder.annotationsInline(d: Documentable) = + annotationsInlineWithIgnored(d, ignoredAnnotations, strategy, listBrackets, classExtension) + + override fun <T : Documentable> WithExtraProperties<T>.modifiers() = + extra[AdditionalModifiers]?.content?.entries?.map { + it.key to it.value.filterIsInstance<ExtraModifiers.KotlinOnlyModifiers>().toSet() + }?.toMap() ?: emptyMap() + + + val PrimitiveJavaType.dri: DRI get() = DRI("kotlin", name.capitalize()) + + val Bound.driOrNull: DRI? + get() { + return when (this) { + is OtherParameter -> this.declarationDRI + is TypeConstructor -> this.dri + is Nullable -> this.inner.driOrNull + is PrimitiveJavaType -> this.dri + is Void -> DriOfUnit + is JavaObject -> DriOfAny + is Dynamic -> null + is UnresolvedBound -> null + } + } +} diff --git a/plugins/base/src/main/kotlin/signatures/SignatureProvider.kt b/plugins/base/src/main/kotlin/signatures/SignatureProvider.kt new file mode 100644 index 00000000..e1933fb8 --- /dev/null +++ b/plugins/base/src/main/kotlin/signatures/SignatureProvider.kt @@ -0,0 +1,8 @@ +package org.jetbrains.dokka.base.signatures + +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.pages.ContentNode + +interface SignatureProvider { + fun signature(documentable: Documentable): List<ContentNode> +} diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ActualTypealiasAdder.kt b/plugins/base/src/main/kotlin/transformers/documentables/ActualTypealiasAdder.kt new file mode 100644 index 00000000..1b65fc22 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/ActualTypealiasAdder.kt @@ -0,0 +1,84 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer + +class ActualTypealiasAdder : DocumentableTransformer { + + override fun invoke(modules: DModule, context: DokkaContext) = modules.generateTypealiasesMap().let { aliases -> + modules.copy(packages = modules.packages.map { it.copy(classlikes = addActualTypeAliasToClasslikes(it.classlikes, aliases)) }) + } + + private fun DModule.generateTypealiasesMap(): Map<DRI, DTypeAlias> = + packages.flatMap { pkg -> + pkg.typealiases.map { typeAlias -> + typeAlias.dri to typeAlias + } + }.toMap() + + + private fun addActualTypeAliasToClasslikes( + elements: Iterable<DClasslike>, + typealiases: Map<DRI, DTypeAlias> + ): List<DClasslike> = elements.flatMap { + when (it) { + is DClass -> addActualTypeAlias( + it.copy( + classlikes = addActualTypeAliasToClasslikes(it.classlikes, typealiases) + ).let(::listOf), + typealiases + ) + is DEnum -> addActualTypeAlias( + it.copy( + classlikes = addActualTypeAliasToClasslikes(it.classlikes, typealiases) + ).let(::listOf), + typealiases + ) + is DInterface -> addActualTypeAlias( + it.copy( + classlikes = addActualTypeAliasToClasslikes(it.classlikes, typealiases) + ).let(::listOf), + typealiases + ) + is DObject -> addActualTypeAlias( + it.copy( + classlikes = addActualTypeAliasToClasslikes(it.classlikes, typealiases) + ).let(::listOf), + typealiases + ) + is DAnnotation -> addActualTypeAlias( + it.copy( + classlikes = addActualTypeAliasToClasslikes(it.classlikes, typealiases) + ).let(::listOf), + typealiases + ) + else -> throw IllegalStateException("${it::class.qualifiedName} ${it.name} cannot have extra added") + } + } + + private fun <T> addActualTypeAlias( + elements: Iterable<T>, + typealiases: Map<DRI, DTypeAlias> + ): List<T> where T : DClasslike, T : WithExtraProperties<T>, T : WithExpectActual = + elements.map { element -> + if (element.expectPresentInSet != null) { + typealiases[element.dri]?.let { ta -> + element.withNewExtras(element.extra + ActualTypealias(ta.underlyingType)).let { + when(it) { + is DClass -> it.copy(sourceSets = element.sourceSets + ta.sourceSets) + is DEnum -> it.copy(sourceSets = element.sourceSets + ta.sourceSets) + is DInterface -> it.copy(sourceSets = element.sourceSets + ta.sourceSets) + is DObject -> it.copy(sourceSets = element.sourceSets + ta.sourceSets) + is DAnnotation -> it.copy(sourceSets = element.sourceSets + ta.sourceSets) + else -> throw IllegalStateException("${it::class.qualifiedName} ${it.name} cannot have copy its sourceSets") + } + } as T + } ?: element + } else { + element + } + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt b/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt new file mode 100644 index 00000000..c8e4f565 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt @@ -0,0 +1,200 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.mergeExtras +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.DocumentableMerger +import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult + +internal object DefaultDocumentableMerger : DocumentableMerger { + + override fun invoke(modules: Collection<DModule>, context: DokkaContext): DModule { + + val projectName = + modules.fold(modules.first().name) { acc, module -> acc.commonPrefixWith(module.name) } + .takeIf { it.isNotEmpty() } + ?: "project" + + return modules.reduce { left, right -> + val list = listOf(left, right) + DModule( + name = projectName, + packages = merge( + list.flatMap { it.packages }, + DPackage::mergeWith + ), + documentation = list.map { it.documentation }.flatMap { it.entries }.associate { (k,v) -> k to v }, + expectPresentInSet = list.firstNotNullResult { it.expectPresentInSet }, + sourceSets = list.flatMap { it.sourceSets }.toSet() + ).mergeExtras(left, right) + } + } +} + +private fun <T : Documentable> merge(elements: List<T>, reducer: (T, T) -> T): List<T> = + elements.groupingBy { it.dri } + .reduce { _, left, right -> reducer(left, right) } + .values.toList() + +private fun <T> mergeExpectActual( + elements: List<T>, + reducer: (T, T) -> T +): List<T> where T : Documentable, T : WithExpectActual { + + fun analyzeExpectActual(sameDriElements: List<T>) = sameDriElements.reduce(reducer) + + return elements.groupBy { it.dri }.values.map(::analyzeExpectActual) +} + +fun DPackage.mergeWith(other: DPackage): DPackage = copy( + functions = mergeExpectActual(functions + other.functions, DFunction::mergeWith), + properties = mergeExpectActual(properties + other.properties, DProperty::mergeWith), + classlikes = mergeExpectActual(classlikes + other.classlikes, DClasslike::mergeWith), + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + typealiases = merge(typealiases + other.typealiases, DTypeAlias::mergeWith), + sourceSets = sourceSets + other.sourceSets +).mergeExtras(this, other) + +fun DFunction.mergeWith(other: DFunction): DFunction = copy( + parameters = merge(this.parameters + other.parameters, DParameter::mergeWith), + receiver = receiver?.let { r -> other.receiver?.let { r.mergeWith(it) } ?: r } ?: other.receiver, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources+ other.sources, + visibility = visibility + other.visibility, + modifier = modifier + other.modifier, + sourceSets = sourceSets + other.sourceSets, + generics = merge(generics + other.generics, DTypeParameter::mergeWith) +).mergeExtras(this, other) + +fun DProperty.mergeWith(other: DProperty): DProperty = copy( + receiver = receiver?.let { r -> other.receiver?.let { r.mergeWith(it) } ?: r } ?: other.receiver, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources+ other.sources, + visibility = visibility + other.visibility, + modifier = modifier + other.modifier, + sourceSets = sourceSets + other.sourceSets, + getter = getter?.let { g -> other.getter?.let { g.mergeWith(it) } ?: g } ?: other.getter, + setter = setter?.let { s -> other.setter?.let { s.mergeWith(it) } ?: s } ?: other.setter, + generics = merge(generics + other.generics, DTypeParameter::mergeWith) +).mergeExtras(this, other) + +fun DClasslike.mergeWith(other: DClasslike): DClasslike = when { + this is DClass && other is DClass -> mergeWith(other) + this is DEnum && other is DEnum -> mergeWith(other) + this is DInterface && other is DInterface -> mergeWith(other) + this is DObject && other is DObject -> mergeWith(other) + this is DAnnotation && other is DAnnotation -> mergeWith(other) + else -> throw IllegalStateException("${this::class.qualifiedName} ${this.name} cannot be mergesd with ${other::class.qualifiedName} ${other.name}") +} + +fun DClass.mergeWith(other: DClass): DClass = copy( + constructors = mergeExpectActual( + constructors + other.constructors, + DFunction::mergeWith + ), + functions = mergeExpectActual(functions + other.functions, DFunction::mergeWith), + properties = mergeExpectActual(properties + other.properties, DProperty::mergeWith), + classlikes = mergeExpectActual(classlikes + other.classlikes, DClasslike::mergeWith), + companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, + generics = merge(generics + other.generics, DTypeParameter::mergeWith), + modifier = modifier + other.modifier, + supertypes = supertypes + other.supertypes, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources+ other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets +).mergeExtras(this, other) + +fun DEnum.mergeWith(other: DEnum): DEnum = copy( + entries = merge(entries + other.entries, DEnumEntry::mergeWith), + constructors = mergeExpectActual( + constructors + other.constructors, + DFunction::mergeWith + ), + functions = mergeExpectActual(functions + other.functions, DFunction::mergeWith), + properties = mergeExpectActual(properties + other.properties, DProperty::mergeWith), + classlikes = mergeExpectActual(classlikes + other.classlikes, DClasslike::mergeWith), + companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, + supertypes = supertypes + other.supertypes, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources+ other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets +).mergeExtras(this, other) + +fun DEnumEntry.mergeWith(other: DEnumEntry): DEnumEntry = copy( + functions = mergeExpectActual(functions + other.functions, DFunction::mergeWith), + properties = mergeExpectActual(properties + other.properties, DProperty::mergeWith), + classlikes = mergeExpectActual(classlikes + other.classlikes, DClasslike::mergeWith), + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sourceSets = sourceSets + other.sourceSets +).mergeExtras(this, other) + +fun DObject.mergeWith(other: DObject): DObject = copy( + functions = mergeExpectActual(functions + other.functions, DFunction::mergeWith), + properties = mergeExpectActual(properties + other.properties, DProperty::mergeWith), + classlikes = mergeExpectActual(classlikes + other.classlikes, DClasslike::mergeWith), + supertypes = supertypes + other.supertypes, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources+ other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets +).mergeExtras(this, other) + +fun DInterface.mergeWith(other: DInterface): DInterface = copy( + functions = mergeExpectActual(functions + other.functions, DFunction::mergeWith), + properties = mergeExpectActual(properties + other.properties, DProperty::mergeWith), + classlikes = mergeExpectActual(classlikes + other.classlikes, DClasslike::mergeWith), + companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, + generics = merge(generics + other.generics, DTypeParameter::mergeWith), + supertypes = supertypes + other.supertypes, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources+ other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets +).mergeExtras(this, other) + +fun DAnnotation.mergeWith(other: DAnnotation): DAnnotation = copy( + constructors = mergeExpectActual( + constructors + other.constructors, + DFunction::mergeWith + ), + functions = mergeExpectActual(functions + other.functions, DFunction::mergeWith), + properties = mergeExpectActual(properties + other.properties, DProperty::mergeWith), + classlikes = mergeExpectActual(classlikes + other.classlikes, DClasslike::mergeWith), + companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources+ other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets, + generics = merge(generics + other.generics, DTypeParameter::mergeWith) +).mergeExtras(this, other) + +fun DParameter.mergeWith(other: DParameter): DParameter = copy( + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sourceSets = sourceSets + other.sourceSets +).mergeExtras(this, other) + +fun DTypeParameter.mergeWith(other: DTypeParameter): DTypeParameter = copy( + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sourceSets = sourceSets + other.sourceSets +).mergeExtras(this, other) + +fun DTypeAlias.mergeWith(other: DTypeAlias): DTypeAlias = copy( + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + underlyingType = underlyingType + other.underlyingType, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets +).mergeExtras(this, other)
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/documentables/DeprecatedDocumentableFilterTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/DeprecatedDocumentableFilterTransformer.kt new file mode 100644 index 00000000..109aa640 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/DeprecatedDocumentableFilterTransformer.kt @@ -0,0 +1,258 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer + +class DeprecatedDocumentableFilterTransformer(val context: DokkaContext) : PreMergeDocumentableTransformer { + override fun invoke(modules: List<DModule>) = modules.map { original -> + val sourceSet = original.sourceSets.single() + val packageOptions = + sourceSet.perPackageOptions + original.let { + DeprecatedDocumentableFilter(sourceSet, packageOptions).processModule(it) + } + } + + private class DeprecatedDocumentableFilter( + val globalOptions: DokkaConfiguration.DokkaSourceSet, + val packageOptions: List<DokkaConfiguration.PackageOptions> + ) { + + fun <T> T.isAllowedInPackage(): Boolean where T : WithExtraProperties<T>, T : Documentable { + val packageName = this.dri.packageName + val condition = packageName != null && packageOptions.firstOrNull { + packageName.startsWith(it.prefix) + }?.skipDeprecated + ?: globalOptions.skipDeprecated + + fun T.isDeprecated() = extra[Annotations]?.let { annotations -> + annotations.content.values.flatten().any { + it.dri.toString() == "kotlin/Deprecated///PointingToDeclaration/" + } + } ?: false + + return !(condition && this.isDeprecated()) + } + + fun processModule(original: DModule) = + filterPackages(original.packages).let { (modified, packages) -> + if (!modified) original + else + DModule( + original.name, + packages = packages, + documentation = original.documentation, + sourceSets = original.sourceSets, + extra = original.extra + ) + } + + + private fun filterPackages(packages: List<DPackage>): Pair<Boolean, List<DPackage>> { + var packagesListChanged = false + val filteredPackages = packages.mapNotNull { pckg -> + var modified = false + val functions = filterFunctions(pckg.functions).let { (listModified, list) -> + modified = modified || listModified + list + } + val properties = filterProperties(pckg.properties).let { (listModified, list) -> + modified = modified || listModified + list + } + val classlikes = filterClasslikes(pckg.classlikes).let { (listModified, list) -> + modified = modified || listModified + list + } + when { + !modified -> pckg + else -> { + packagesListChanged = true + DPackage( + pckg.dri, + functions, + properties, + classlikes, + pckg.typealiases, + pckg.documentation, + pckg.expectPresentInSet, + pckg.sourceSets, + pckg.extra + ) + } + } + } + return Pair(packagesListChanged, filteredPackages) + } + + private fun filterFunctions( + functions: List<DFunction> + ) = functions.filter { it.isAllowedInPackage() }.let { + Pair(it.size != functions.size, it) + } + + private fun filterProperties( + properties: List<DProperty> + ): Pair<Boolean, List<DProperty>> = properties.filter { + it.isAllowedInPackage() + }.let { + Pair(properties.size != it.size, it) + } + + private fun filterEnumEntries(entries: List<DEnumEntry>) = + entries.filter { it.isAllowedInPackage() }.map { entry -> + DEnumEntry( + entry.dri, + entry.name, + entry.documentation, + entry.expectPresentInSet, + filterFunctions(entry.functions).second, + filterProperties(entry.properties).second, + filterClasslikes(entry.classlikes).second, + entry.sourceSets, + entry.extra + ) + } + + private fun filterClasslikes( + classlikeList: List<DClasslike> + ): Pair<Boolean, List<DClasslike>> { + var modified = false + return classlikeList.filter { classlike -> + when (classlike) { + is DClass -> classlike.isAllowedInPackage() + is DInterface -> classlike.isAllowedInPackage() + is DEnum -> classlike.isAllowedInPackage() + is DObject -> classlike.isAllowedInPackage() + is DAnnotation -> classlike.isAllowedInPackage() + } + }.map { classlike -> + fun helper(): DClasslike = when (classlike) { + is DClass -> DClass( + classlike.dri, + classlike.name, + filterFunctions(classlike.constructors).let { + modified = modified || it.first; it.second + }, + filterFunctions(classlike.functions).let { + modified = modified || it.first; it.second + }, + filterProperties(classlike.properties).let { + modified = modified || it.first; it.second + }, + filterClasslikes(classlike.classlikes).let { + modified = modified || it.first; it.second + }, + classlike.sources, + classlike.visibility, + classlike.companion, + classlike.generics, + classlike.supertypes, + classlike.documentation, + classlike.expectPresentInSet, + classlike.modifier, + classlike.sourceSets, + classlike.extra + ) + is DAnnotation -> DAnnotation( + classlike.name, + classlike.dri, + classlike.documentation, + classlike.expectPresentInSet, + classlike.sources, + filterFunctions(classlike.functions).let { + modified = modified || it.first; it.second + }, + filterProperties(classlike.properties).let { + modified = modified || it.first; it.second + }, + filterClasslikes(classlike.classlikes).let { + modified = modified || it.first; it.second + }, + classlike.visibility, + classlike.companion, + filterFunctions(classlike.constructors).let { + modified = modified || it.first; it.second + }, + classlike.generics, + classlike.sourceSets, + classlike.extra + ) + is DEnum -> DEnum( + classlike.dri, + classlike.name, + filterEnumEntries(classlike.entries), + classlike.documentation, + classlike.expectPresentInSet, + classlike.sources, + filterFunctions(classlike.functions).let { + modified = modified || it.first; it.second + }, + filterProperties(classlike.properties).let { + modified = modified || it.first; it.second + }, + filterClasslikes(classlike.classlikes).let { + modified = modified || it.first; it.second + }, + classlike.visibility, + classlike.companion, + filterFunctions(classlike.constructors).let { + modified = modified || it.first; it.second + }, + classlike.supertypes, + classlike.sourceSets, + classlike.extra + ) + is DInterface -> DInterface( + classlike.dri, + classlike.name, + classlike.documentation, + classlike.expectPresentInSet, + classlike.sources, + filterFunctions(classlike.functions).let { + modified = modified || it.first; it.second + }, + filterProperties(classlike.properties).let { + modified = modified || it.first; it.second + }, + filterClasslikes(classlike.classlikes).let { + modified = modified || it.first; it.second + }, + classlike.visibility, + classlike.companion, + classlike.generics, + classlike.supertypes, + classlike.sourceSets, + classlike.extra + ) + is DObject -> DObject( + classlike.name, + classlike.dri, + classlike.documentation, + classlike.expectPresentInSet, + classlike.sources, + filterFunctions(classlike.functions).let { + modified = modified || it.first; it.second + }, + filterProperties(classlike.properties).let { + modified = modified || it.first; it.second + }, + filterClasslikes(classlike.classlikes).let { + modified = modified || it.first; it.second + }, + classlike.visibility, + classlike.supertypes, + classlike.sourceSets, + classlike.extra + ) + } + helper() + }.let { + Pair(it.size != classlikeList.size || modified, it) + } + } + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/documentables/DocumentableVisibilityFilterTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/DocumentableVisibilityFilterTransformer.kt new file mode 100644 index 00000000..ff05beed --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/DocumentableVisibilityFilterTransformer.kt @@ -0,0 +1,327 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet + +class DocumentableVisibilityFilterTransformer(val context: DokkaContext) : PreMergeDocumentableTransformer { + + override fun invoke(modules: List<DModule>) = modules.map { original -> + val sourceSet = original.sourceSets.single() + val packageOptions = sourceSet.perPackageOptions + DocumentableVisibilityFilter(packageOptions, sourceSet).processModule(original) + } + + private class DocumentableVisibilityFilter( + val packageOptions: List<DokkaConfiguration.PackageOptions>, + val globalOptions: DokkaSourceSet + ) { + fun Visibility.isAllowedInPackage(packageName: String?) = when (this) { + is JavaVisibility.Public, + is JavaVisibility.Default, + is KotlinVisibility.Public -> true + else -> packageName != null + && packageOptions.firstOrNull { packageName.startsWith(it.prefix) }?.includeNonPublic + ?: globalOptions.includeNonPublic + } + + fun processModule(original: DModule) = + filterPackages(original.packages).let { (modified, packages) -> + if (!modified) original + else + DModule( + original.name, + packages = packages, + documentation = original.documentation, + sourceSets = original.sourceSets, + extra = original.extra + ) + } + + + private fun filterPackages(packages: List<DPackage>): Pair<Boolean, List<DPackage>> { + var packagesListChanged = false + val filteredPackages = packages.map { + var modified = false + val functions = filterFunctions(it.functions).let { (listModified, list) -> + modified = modified || listModified + list + } + val properties = filterProperties(it.properties).let { (listModified, list) -> + modified = modified || listModified + list + } + val classlikes = filterClasslikes(it.classlikes).let { (listModified, list) -> + modified = modified || listModified + list + } + when { + !modified -> it + else -> { + packagesListChanged = true + DPackage( + it.dri, + functions, + properties, + classlikes, + it.typealiases, + it.documentation, + it.expectPresentInSet, + it.sourceSets, + it.extra + ) + } + } + } + return Pair(packagesListChanged, filteredPackages) + } + + private fun <T : WithVisibility> alwaysTrue(a: T, p: DokkaSourceSet) = true + private fun <T : WithVisibility> alwaysFalse(a: T, p: DokkaSourceSet) = false + + private fun WithVisibility.visibilityForPlatform(data: DokkaSourceSet): Visibility? = visibility[data] + + private fun <T> T.filterPlatforms( + additionalCondition: (T, DokkaSourceSet) -> Boolean = ::alwaysTrue, + alternativeCondition: (T, DokkaSourceSet) -> Boolean = ::alwaysFalse + ) where T : Documentable, T : WithVisibility = + sourceSets.filter { d -> + visibilityForPlatform(d)?.isAllowedInPackage(dri.packageName) == true && + additionalCondition(this, d) || + alternativeCondition(this, d) + }.toSet() + + private fun <T> List<T>.transform( + additionalCondition: (T, DokkaSourceSet) -> Boolean = ::alwaysTrue, + alternativeCondition: (T, DokkaSourceSet) -> Boolean = ::alwaysFalse, + recreate: (T, Set<DokkaSourceSet>) -> T + ): Pair<Boolean, List<T>> where T : Documentable, T : WithVisibility { + var changed = false + val values = mapNotNull { t -> + val filteredPlatforms = t.filterPlatforms(additionalCondition, alternativeCondition) + when (filteredPlatforms.size) { + t.visibility.size -> t + 0 -> { + changed = true + null + } + else -> { + changed = true + recreate(t, filteredPlatforms) + } + } + } + return Pair(changed, values) + } + + private fun filterFunctions( + functions: List<DFunction>, + additionalCondition: (DFunction, DokkaSourceSet) -> Boolean = ::alwaysTrue + ) = + functions.transform(additionalCondition) { original, filteredPlatforms -> + with(original) { + DFunction( + dri, + name, + isConstructor, + parameters, + documentation.filtered(filteredPlatforms), + expectPresentInSet.filtered(filteredPlatforms), + sources.filtered(filteredPlatforms), + visibility.filtered(filteredPlatforms), + type, + generics.mapNotNull { it.filter(filteredPlatforms) }, + receiver, + modifier, + filteredPlatforms, + extra + ) + } + } + + private fun hasVisibleAccessorsForPlatform(property: DProperty, data: DokkaSourceSet) = + property.getter?.visibilityForPlatform(data)?.isAllowedInPackage(property.dri.packageName) == true || + property.setter?.visibilityForPlatform(data)?.isAllowedInPackage(property.dri.packageName) == true + + private fun filterProperties( + properties: List<DProperty>, + additionalCondition: (DProperty, DokkaSourceSet) -> Boolean = ::alwaysTrue + ): Pair<Boolean, List<DProperty>> = + properties.transform(additionalCondition, ::hasVisibleAccessorsForPlatform) { original, filteredPlatforms -> + with(original) { + DProperty( + dri, + name, + documentation.filtered(filteredPlatforms), + expectPresentInSet.filtered(filteredPlatforms), + sources.filtered(filteredPlatforms), + visibility.filtered(filteredPlatforms), + type, + receiver, + setter, + getter, + modifier, + filteredPlatforms, + generics.mapNotNull { it.filter(filteredPlatforms) }, + extra + ) + } + } + + private fun filterEnumEntries(entries: List<DEnumEntry>, filteredPlatforms: Set<DokkaSourceSet>) = + entries.mapNotNull { entry -> + if (filteredPlatforms.containsAll(entry.sourceSets)) entry + else { + val intersection = filteredPlatforms.intersect(entry.sourceSets) + if (intersection.isEmpty()) null + else DEnumEntry( + entry.dri, + entry.name, + entry.documentation.filtered(intersection), + entry.expectPresentInSet.filtered(filteredPlatforms), + filterFunctions(entry.functions) { _, data -> data in intersection }.second, + filterProperties(entry.properties) { _, data -> data in intersection }.second, + filterClasslikes(entry.classlikes) { _, data -> data in intersection }.second, + intersection, + entry.extra + ) + } + } + + private fun filterClasslikes( + classlikeList: List<DClasslike>, + additionalCondition: (DClasslike, DokkaSourceSet) -> Boolean = ::alwaysTrue + ): Pair<Boolean, List<DClasslike>> { + var classlikesListChanged = false + val filteredClasslikes: List<DClasslike> = classlikeList.mapNotNull { + with(it) { + val filteredPlatforms = filterPlatforms(additionalCondition) + if (filteredPlatforms.isEmpty()) { + classlikesListChanged = true + null + } else { + var modified = sourceSets.size != filteredPlatforms.size + val functions = + filterFunctions(functions) { _, data -> data in filteredPlatforms }.let { (listModified, list) -> + modified = modified || listModified + list + } + val properties = + filterProperties(properties) { _, data -> data in filteredPlatforms }.let { (listModified, list) -> + modified = modified || listModified + list + } + val classlikes = + filterClasslikes(classlikes) { _, data -> data in filteredPlatforms }.let { (listModified, list) -> + modified = modified || listModified + list + } + val companion = + if (this is WithCompanion) filterClasslikes(listOfNotNull(companion)) { _, data -> data in filteredPlatforms }.let { (listModified, list) -> + modified = modified || listModified + list.firstOrNull() as DObject? + } else null + val constructors = if (this is WithConstructors) + filterFunctions(constructors) { _, data -> data in filteredPlatforms }.let { (listModified, list) -> + modified = modified || listModified + list + } else emptyList() + val generics = + if (this is WithGenerics) generics.mapNotNull { param -> param.filter(filteredPlatforms) } else emptyList() + val enumEntries = + if (this is DEnum) filterEnumEntries(entries, filteredPlatforms) else emptyList() + classlikesListChanged = classlikesListChanged || modified + when { + !modified -> this + this is DClass -> DClass( + dri, + name, + constructors, + functions, + properties, + classlikes, + sources.filtered(filteredPlatforms), + visibility.filtered(filteredPlatforms), + companion, + generics, + supertypes.filtered(filteredPlatforms), + documentation.filtered(filteredPlatforms), + expectPresentInSet.filtered(filteredPlatforms), + modifier, + filteredPlatforms, + extra + ) + this is DAnnotation -> DAnnotation( + name, + dri, + documentation.filtered(filteredPlatforms), + expectPresentInSet.filtered(filteredPlatforms), + sources.filtered(filteredPlatforms), + functions, + properties, + classlikes, + visibility.filtered(filteredPlatforms), + companion, + constructors, + generics, + filteredPlatforms, + extra + ) + this is DEnum -> DEnum( + dri, + name, + enumEntries, + documentation.filtered(filteredPlatforms), + expectPresentInSet.filtered(filteredPlatforms), + sources.filtered(filteredPlatforms), + functions, + properties, + classlikes, + visibility.filtered(filteredPlatforms), + companion, + constructors, + supertypes.filtered(filteredPlatforms), + filteredPlatforms, + extra + ) + this is DInterface -> DInterface( + dri, + name, + documentation.filtered(filteredPlatforms), + expectPresentInSet.filtered(filteredPlatforms), + sources.filtered(filteredPlatforms), + functions, + properties, + classlikes, + visibility.filtered(filteredPlatforms), + companion, + generics, + supertypes.filtered(filteredPlatforms), + filteredPlatforms, + extra + ) + this is DObject -> DObject( + name, + dri, + documentation.filtered(filteredPlatforms), + expectPresentInSet.filtered(filteredPlatforms), + sources.filtered(filteredPlatforms), + functions, + properties, + classlikes, + visibility, + supertypes.filtered(filteredPlatforms), + filteredPlatforms, + extra + ) + else -> null + } + } + } + } + return Pair(classlikesListChanged, filteredClasslikes) + } + } +} diff --git a/plugins/base/src/main/kotlin/transformers/documentables/EmptyPackagesFilterTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/EmptyPackagesFilterTransformer.kt new file mode 100644 index 00000000..61abfbd7 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/EmptyPackagesFilterTransformer.kt @@ -0,0 +1,28 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.DPackage +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer + +class EmptyPackagesFilterTransformer(val context: DokkaContext) : PreMergeDocumentableTransformer { + override fun invoke(modules: List<DModule>): List<DModule> = modules.map { original -> + original.let { + EmptyPackagesFilter(original.sourceSets.single()).processModule(it) + } + } + + private class EmptyPackagesFilter( + val sourceSet: DokkaConfiguration.DokkaSourceSet + ) { + fun DPackage.shouldBeSkipped() = sourceSet.skipEmptyPackages && + functions.isEmpty() && + properties.isEmpty() && + classlikes.isEmpty() + + fun processModule(module: DModule) = module.copy( + packages = module.packages.filter { !it.shouldBeSkipped() } + ) + } +} diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt new file mode 100644 index 00000000..2da25d4b --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/ExtensionExtractorTransformer.kt @@ -0,0 +1,127 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.toList +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.DriOfAny +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.ExtraProperty +import org.jetbrains.dokka.model.properties.MergeStrategy +import org.jetbrains.dokka.model.properties.plus +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer + + +class ExtensionExtractorTransformer : DocumentableTransformer { + override fun invoke(original: DModule, context: DokkaContext): DModule = runBlocking(Dispatchers.Default) { + val channel = Channel<Pair<DRI, Callable>>(10) + launch { + coroutineScope { + original.packages.forEach { launch { collectExtensions(it, channel) } } + } + channel.close() + } + val extensionMap = channel.consumeAsFlow().toList().toMultiMap() + + val newPackages = original.packages.map { async { it.addExtensionInformation(extensionMap) } } + original.copy(packages = newPackages.awaitAll()) + } +} + +private suspend fun <T : Documentable> T.addExtensionInformation( + extensionMap: Map<DRI, List<Callable>> +): T = coroutineScope { + val newClasslikes = (this@addExtensionInformation as? WithScope) + ?.classlikes + ?.map { async { it.addExtensionInformation(extensionMap) } } + .orEmpty() + + @Suppress("UNCHECKED_CAST") + when (this@addExtensionInformation) { + is DPackage -> { + val newTypealiases = typealiases.map { async { it.addExtensionInformation(extensionMap) } } + copy(classlikes = newClasslikes.awaitAll(), typealiases = newTypealiases.awaitAll()) + } + is DClass -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) + is DEnum -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) + is DInterface -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) + is DObject -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) + is DAnnotation -> copy(classlikes = newClasslikes.awaitAll(), extra = extra + extensionMap.find(dri)) + is DTypeAlias -> copy(extra = extra + extensionMap.find(dri)) + else -> throw IllegalStateException( + "${this@addExtensionInformation::class.simpleName} is not expected to have extensions" + ) + } as T +} + +private fun Map<DRI, List<Callable>>.find(dri: DRI) = get(dri)?.toSet()?.let(::CallableExtensions) + +private suspend fun collectExtensions( + documentable: Documentable, + channel: SendChannel<Pair<DRI, Callable>> +): Unit = coroutineScope { + if (documentable is WithScope) { + documentable.classlikes.forEach { + launch { collectExtensions(it, channel) } + } + + if (documentable is DObject || documentable is DPackage) { + (documentable.properties.asSequence() + documentable.functions.asSequence()) + .flatMap(Callable::asPairsWithReceiverDRIs) + .forEach { channel.send(it) } + } + } +} + + +private fun Callable.asPairsWithReceiverDRIs(): Sequence<Pair<DRI, Callable>> = + receiver?.type?.let(::findReceiverDRIs).orEmpty().map { it to this } + +// In normal cases we return at max one DRI, but sometimes receiver type can be bound by more than one type constructor +// for example `fun <T> T.example() where T: A, T: B` is extension of both types A and B +// Note: in some cases returning empty sequence doesn't mean that we cannot determine the DRI but only that we don't +// care about it since there is nowhere to put documentation of given extension. +private fun Callable.findReceiverDRIs(bound: Bound): Sequence<DRI> = when (bound) { + is Nullable -> findReceiverDRIs(bound.inner) + is OtherParameter -> + if (this is DFunction && bound.declarationDRI == this.dri) + generics.find { it.name == bound.name }?.bounds?.asSequence()?.flatMap(::findReceiverDRIs).orEmpty() + else + emptySequence() + is TypeConstructor -> sequenceOf(bound.dri) + is PrimitiveJavaType -> emptySequence() + is Void -> emptySequence() + is JavaObject -> sequenceOf(DriOfAny) + is Dynamic -> sequenceOf(DriOfAny) + is UnresolvedBound -> emptySequence() +} + +private fun <T, U> Iterable<Pair<T, U>>.toMultiMap(): Map<T, List<U>> = + groupBy(Pair<T, *>::first, Pair<*, U>::second) + +data class CallableExtensions(val extensions: Set<Callable>) : ExtraProperty<Documentable> { + companion object Key : ExtraProperty.Key<Documentable, CallableExtensions> { + override fun mergeStrategyFor(left: CallableExtensions, right: CallableExtensions) = + MergeStrategy.Replace(CallableExtensions(left.extensions + right.extensions)) + } + + override val key = Key +} + +//TODO IMPORTANT remove this terrible hack after updating to 1.4-M3 +fun <T : Any> ReceiveChannel<T>.consumeAsFlow(): Flow<T> = flow { + try { + while (true) { + emit(receive()) + } + } catch (_: ClosedReceiveChannelException) { + // cool and good + } +}.flowOn(Dispatchers.Default)
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/documentables/InheritorsExtractorTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/InheritorsExtractorTransformer.kt new file mode 100644 index 00000000..85256d51 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/InheritorsExtractorTransformer.kt @@ -0,0 +1,85 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.ExtraProperty +import org.jetbrains.dokka.model.properties.MergeStrategy +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer + +class InheritorsExtractorTransformer : DocumentableTransformer { + override fun invoke(original: DModule, context: DokkaContext): DModule = + original.generateInheritanceMap().let { inheritanceMap -> original.appendInheritors(inheritanceMap) as DModule } + + private fun <T : Documentable> T.appendInheritors(inheritanceMap: Map<DokkaSourceSet, Map<DRI, List<DRI>>>): Documentable = + InheritorsInfo(inheritanceMap.getForDRI(dri)).let { info -> + when (this) { + is DModule -> copy(packages = packages.map { it.appendInheritors(inheritanceMap) as DPackage }) + is DPackage -> copy(classlikes = classlikes.map { it.appendInheritors(inheritanceMap) as DClasslike }) + is DClass -> if (info.isNotEmpty()) { + copy( + extra = extra + info, + classlikes = classlikes.map { it.appendInheritors(inheritanceMap) as DClasslike }) + } else { + copy(classlikes = classlikes.map { it.appendInheritors(inheritanceMap) as DClasslike }) + } + is DEnum -> if (info.isNotEmpty()) { + copy( + extra = extra + info, + classlikes = classlikes.map { it.appendInheritors(inheritanceMap) as DClasslike }) + } else { + copy(classlikes = classlikes.map { it.appendInheritors(inheritanceMap) as DClasslike }) + } + is DInterface -> if (info.isNotEmpty()) { + copy( + extra = extra + info, + classlikes = classlikes.map { it.appendInheritors(inheritanceMap) as DClasslike }) + } else { + copy(classlikes = classlikes.map { it.appendInheritors(inheritanceMap) as DClasslike }) + } + is DObject -> copy(classlikes = classlikes.map { it.appendInheritors(inheritanceMap) as DClasslike }) + is DAnnotation -> copy(classlikes = classlikes.map { it.appendInheritors(inheritanceMap) as DClasslike }) + else -> this + } + } + + private fun InheritorsInfo.isNotEmpty() = this.value.values.fold(0) { acc, list -> acc + list.size } > 0 + + private fun Map<DokkaSourceSet, Map<DRI, List<DRI>>>.getForDRI(dri: DRI) = + map { (v, k) -> + v to k[dri] + }.map { (k, v) -> k to v.orEmpty() }.toMap() + + private fun DModule.generateInheritanceMap() = + getInheritanceEntriesRec().filterNot { it.second.isEmpty() }.groupBy({ it.first }) { it.second } + .map { (k, v) -> + k to v.flatMap { p -> p.groupBy({ it.first }) { it.second }.toList() } + .groupBy({ it.first }) { it.second }.map { (k2, v2) -> k2 to v2.flatten() }.toMap() + }.filter { it.second.values.isNotEmpty() }.toMap() + + private fun <T : Documentable> T.getInheritanceEntriesRec(): List<Pair<DokkaSourceSet, List<Pair<DRI, DRI>>>> = + this.toInheritanceEntries() + children.flatMap { it.getInheritanceEntriesRec() } + + private fun <T : Documentable> T.toInheritanceEntries() = + (this as? WithSupertypes)?.let { + it.supertypes.map { (k, v) -> k to v.map { it.dri to dri } } + }.orEmpty() + +} + +class InheritorsInfo(val value: SourceSetDependent<List<DRI>>) : ExtraProperty<Documentable> { + companion object : ExtraProperty.Key<Documentable, InheritorsInfo> { + override fun mergeStrategyFor(left: InheritorsInfo, right: InheritorsInfo): MergeStrategy<Documentable> = + MergeStrategy.Replace( + InheritorsInfo( + (left.value.entries.toList() + right.value.entries.toList()) + .groupBy({ it.key }) { it.value } + .map { (k, v) -> k to v.flatten() }.toMap() + ) + ) + } + + override val key: ExtraProperty.Key<Documentable, *> = InheritorsInfo +} + diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ModuleAndPackageDocumentationTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/ModuleAndPackageDocumentationTransformer.kt new file mode 100644 index 00000000..4a98a5e0 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/ModuleAndPackageDocumentationTransformer.kt @@ -0,0 +1,108 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.analysis.KotlinAnalysis +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.parsers.MarkdownParser +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import java.nio.file.Files +import java.nio.file.Paths + + +internal class ModuleAndPackageDocumentationTransformer( + private val context: DokkaContext, + private val kotlinAnalysis: KotlinAnalysis +) : PreMergeDocumentableTransformer { + + override fun invoke(modules: List<DModule>): List<DModule> { + + val modulesAndPackagesDocumentation = + context.configuration.sourceSets + .map { + Pair(it.moduleDisplayName, it) to + it.includes.map { Paths.get(it) } + .also { + it.forEach { + if (Files.notExists(it)) + context.logger.warn("Not found file under this path ${it.toAbsolutePath()}") + } + } + .filter { Files.exists(it) } + .flatMap { + it.toFile() + .readText() + .split(Regex("(\n|^)# (?=(Module|Package))")) // Matches heading with Module/Package to split by + .filter { it.isNotEmpty() } + .map { + it.split( + Regex(" "), + 2 + ) + } // Matches space between Module/Package and fully qualified name + }.groupBy({ it[0] }, { + it[1].split(Regex("\n"), 2) // Matches new line after fully qualified name + .let { it[0].trim() to it[1].trim() } + }).mapValues { + it.value.toMap() + } + }.toMap() + + return modules.map { module -> + + val moduleDocumentation = + module.sourceSets.mapNotNull { pd -> + val doc = modulesAndPackagesDocumentation[Pair(module.name, pd)] + val facade = kotlinAnalysis[pd].facade + try { + doc?.get("Module")?.get(module.name)?.run { + pd to MarkdownParser( + facade, + facade.moduleDescriptor.getPackage(FqName.topLevel(Name.identifier(""))), + context.logger + ).parse(this) + } + } catch (e: IllegalArgumentException) { + context.logger.error(e.message.orEmpty()) + null + } + }.toMap() + + val packagesDocumentation = module.packages.map { + it.name to it.sourceSets.mapNotNull { pd -> + val doc = modulesAndPackagesDocumentation[Pair(module.name, pd)] + val facade = kotlinAnalysis[pd].facade + val descriptor = facade.moduleDescriptor.getPackage(FqName(it.name.let { if(it == "[JS root]") "" else it })) + doc?.get("Package")?.get(it.name)?.run { + pd to MarkdownParser( + facade, + descriptor, + context.logger + ).parse(this) + } + }.toMap() + }.toMap() + + module.copy( + documentation = mergeDocumentation(module.documentation, moduleDocumentation), + packages = module.packages.map { + val packageDocumentation = packagesDocumentation[it.name] + if (packageDocumentation != null && packageDocumentation.isNotEmpty()) + it.copy(documentation = mergeDocumentation(it.documentation, packageDocumentation)) + else + it + } + ) + } + } + + private fun mergeDocumentation(origin: Map<DokkaSourceSet, DocumentationNode>, new: Map<DokkaSourceSet, DocumentationNode>) = + (origin.asSequence() + new.asSequence()) + .distinct() + .groupBy({ it.key }, { it.value }) + .mapValues { (_, values) -> DocumentationNode(values.flatMap { it.children }) } + +} diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt new file mode 100644 index 00000000..2ebd4c62 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt @@ -0,0 +1,165 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.DescriptorDocumentableSource +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor.Kind.FAKE_OVERRIDE +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor.Kind.SYNTHESIZED +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +internal class ReportUndocumentedTransformer : DocumentableTransformer { + + override fun invoke(original: DModule, context: DokkaContext): DModule = original.apply { + withDescendants().forEach { documentable -> invoke(documentable, context) } + } + + private fun invoke(documentable: Documentable, context: DokkaContext) { + documentable.sourceSets.forEach { sourceSet -> + if (shouldBeReportedIfNotDocumented(documentable, sourceSet, context)) { + reportIfUndocumented(context, documentable, sourceSet) + } + } + } + + private fun shouldBeReportedIfNotDocumented( + documentable: Documentable, sourceSet: DokkaSourceSet, context: DokkaContext + ): Boolean { + val packageOptionsOrNull = packageOptionsOrNull(sourceSet, documentable) + + if (!(packageOptionsOrNull?.reportUndocumented ?: sourceSet.reportUndocumented)) { + return false + } + + if (documentable is DParameter || documentable is DPackage || documentable is DModule) { + return false + } + + if (isConstructor(documentable)) { + return false + } + + if (isFakeOverride(documentable, sourceSet)) { + return false + } + + if (isSynthesized(documentable, sourceSet)) { + return false + } + + if (isPrivateOrInternalApi(documentable, sourceSet)) { + return false + } + + return true + } + + private fun reportIfUndocumented( + context: DokkaContext, + documentable: Documentable, + sourceSet: DokkaSourceSet + ) { + if (isUndocumented(documentable, sourceSet)) { + val documentableDescription = with(documentable) { + buildString { + dri.packageName?.run { + append(this) + append("/") + } + + dri.classNames?.run { + append(this) + append("/") + } + + dri.callable?.run { + append(name) + append("/") + append(signature()) + append("/") + } + + val sourceSetName = sourceSet.displayName + if (sourceSetName != null.toString()) { + append(" ($sourceSetName)") + } + } + } + + context.logger.warn("Undocumented: $documentableDescription") + } + } + + private fun isUndocumented(documentable: Documentable, sourceSet: DokkaSourceSet): Boolean { + fun resolveDependentSourceSets(sourceSet: DokkaSourceSet): List<DokkaSourceSet> { + return sourceSet.dependentSourceSets.mapNotNull { sourceSetID -> + documentable.sourceSets.singleOrNull { it.sourceSetID == sourceSetID } + } + } + + fun withAllDependentSourceSets(sourceSet: DokkaSourceSet): Sequence<DokkaSourceSet> { + return sequence { + yield(sourceSet) + for (dependentSourceSet in resolveDependentSourceSets(sourceSet)) { + yieldAll(withAllDependentSourceSets(dependentSourceSet)) + } + } + } + + return withAllDependentSourceSets(sourceSet).all { sourceSetOrDependentSourceSet -> + documentable.documentation[sourceSetOrDependentSourceSet]?.children?.isEmpty() ?: true + } + } + + private fun isConstructor(documentable: Documentable): Boolean { + if (documentable !is DFunction) return false + return documentable.isConstructor + } + + private fun isFakeOverride(documentable: Documentable, sourceSet: DokkaSourceSet): Boolean { + return callableMemberDescriptorOrNull(documentable, sourceSet)?.kind == FAKE_OVERRIDE + } + + private fun isSynthesized(documentable: Documentable, sourceSet: DokkaSourceSet): Boolean { + return callableMemberDescriptorOrNull(documentable, sourceSet)?.kind == SYNTHESIZED + } + + private fun callableMemberDescriptorOrNull( + documentable: Documentable, sourceSet: DokkaSourceSet + ): CallableMemberDescriptor? { + if (documentable is WithExpectActual) { + return documentable.sources[sourceSet] + .safeAs<DescriptorDocumentableSource>()?.descriptor + .safeAs() + } + + return null + } + + private fun isPrivateOrInternalApi(documentable: Documentable, sourceSet: DokkaSourceSet): Boolean { + return when (documentable.safeAs<WithVisibility>()?.visibility?.get(sourceSet)) { + KotlinVisibility.Public -> false + KotlinVisibility.Private -> true + KotlinVisibility.Protected -> true + KotlinVisibility.Internal -> true + JavaVisibility.Public -> false + JavaVisibility.Private -> true + JavaVisibility.Protected -> true + JavaVisibility.Default -> true + null -> false + } + } + + private fun packageOptionsOrNull( + dokkaSourceSet: DokkaSourceSet, + documentable: Documentable + ): DokkaConfiguration.PackageOptions? { + val packageName = documentable.dri.packageName ?: return null + return dokkaSourceSet.perPackageOptions + .filter { packageOptions -> packageName.startsWith(packageOptions.prefix) } + .maxBy { packageOptions -> packageOptions.prefix.length } + } +} diff --git a/plugins/base/src/main/kotlin/transformers/pages/annotations/SinceKotlinTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/annotations/SinceKotlinTransformer.kt new file mode 100644 index 00000000..7914e88f --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/annotations/SinceKotlinTransformer.kt @@ -0,0 +1,82 @@ +package org.jetbrains.dokka.base.transformers.pages.annotations + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.CustomTagWrapper +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +class SinceKotlinTransformer(val context: DokkaContext) : DocumentableTransformer { + + override fun invoke(original: DModule, context: DokkaContext) = original.transform() as DModule + + private fun <T : Documentable> T.transform(): Documentable = + when (this) { + is DModule -> copy( + packages = packages.map { it.transform() as DPackage } + ) + is DPackage -> copy( + classlikes = classlikes.map { it.transform() as DClasslike }, + functions = functions.map { it.transform() as DFunction }, + properties = properties.map { it.transform() as DProperty } + ) + is DClass -> copy( + documentation = appendSinceKotlin(), + classlikes = classlikes.map { it.transform() as DClasslike }, + functions = functions.map { it.transform() as DFunction }, + properties = properties.map { it.transform() as DProperty } + ) + is DEnum -> copy( + documentation = appendSinceKotlin(), + classlikes = classlikes.map { it.transform() as DClasslike }, + functions = functions.map { it.transform() as DFunction }, + properties = properties.map { it.transform() as DProperty } + ) + is DInterface -> copy( + documentation = appendSinceKotlin(), + classlikes = classlikes.map { it.transform() as DClasslike }, + functions = functions.map { it.transform() as DFunction }, + properties = properties.map { it.transform() as DProperty } + ) + is DObject -> copy( + documentation = appendSinceKotlin(), + classlikes = classlikes.map { it.transform() as DClasslike }, + functions = functions.map { it.transform() as DFunction }, + properties = properties.map { it.transform() as DProperty } + ) + is DAnnotation -> copy( + documentation = appendSinceKotlin(), + classlikes = classlikes.map { it.transform() as DClasslike }, + functions = functions.map { it.transform() as DFunction }, + properties = properties.map { it.transform() as DProperty } + ) + is DFunction -> copy( + documentation = appendSinceKotlin() + ) + is DProperty -> copy( + documentation = appendSinceKotlin() + ) + is DParameter -> copy( + documentation = appendSinceKotlin() + ) + else -> this.also { context.logger.warn("Unrecognized documentable $this while SinceKotlin transformation") } + } + + private fun Documentable.appendSinceKotlin() = + sourceSets.fold(documentation) { acc, sourceSet -> + safeAs<WithExtraProperties<Documentable>>()?.extra?.get(Annotations)?.content?.get(sourceSet)?.find { + it.dri == DRI("kotlin", "SinceKotlin") + }?.params?.get("version").safeAs<StringValue>()?.value?.let { version -> + acc.mapValues { + if (it.key == sourceSet) it.value.copy( + it.value.children + listOf( + CustomTagWrapper(Text(version.dropWhile { it == '"' }.dropLastWhile { it == '"' }), "Since Kotlin") + ) + ) else it.value + } + } ?: acc + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/comments/CommentsToContentConverter.kt b/plugins/base/src/main/kotlin/transformers/pages/comments/CommentsToContentConverter.kt new file mode 100644 index 00000000..fa9ce37e --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/comments/CommentsToContentConverter.kt @@ -0,0 +1,16 @@ +package org.jetbrains.dokka.base.transformers.pages.comments + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.model.doc.DocTag +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.pages.* + +interface CommentsToContentConverter { + fun buildContent( + docTag: DocTag, + dci: DCI, + sourceSets: Set<DokkaSourceSet>, + styles: Set<Style> = emptySet(), + extras: PropertyContainer<ContentNode> = PropertyContainer.empty() + ): List<ContentNode> +} diff --git a/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt b/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt new file mode 100644 index 00000000..0f953e0f --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt @@ -0,0 +1,178 @@ +package org.jetbrains.dokka.base.transformers.pages.comments + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.pages.* + +object DocTagToContentConverter : CommentsToContentConverter { + override fun buildContent( + docTag: DocTag, + dci: DCI, + sourceSets: Set<DokkaSourceSet>, + styles: Set<Style>, + extra: PropertyContainer<ContentNode> + ): List<ContentNode> { + + fun buildChildren(docTag: DocTag, newStyles: Set<Style> = emptySet(), newExtras: SimpleAttr? = null) = + docTag.children.flatMap { + buildContent(it, dci, sourceSets, styles + newStyles, newExtras?.let { extra + it } ?: extra) + } + + fun buildTableRows(rows: List<DocTag>, newStyle: Style): List<ContentGroup> = + rows.flatMap { + buildContent(it, dci, sourceSets, styles + newStyle, extra) as List<ContentGroup> + } + + fun buildHeader(level: Int) = + listOf( + ContentHeader( + buildChildren(docTag), + level, + dci, + sourceSets, + styles + ) + ) + + fun buildList(ordered: Boolean, start: Int = 1) = + listOf( + ContentList( + buildChildren(docTag), + ordered, + dci, + sourceSets, + styles, + ((PropertyContainer.empty<ContentNode>()) + SimpleAttr("start", start.toString())) + ) + ) + + fun buildNewLine() = listOf( + ContentBreakLine( + sourceSets + ) + ) + + return when (docTag) { + is H1 -> buildHeader(1) + is H2 -> buildHeader(2) + is H3 -> buildHeader(3) + is H4 -> buildHeader(4) + is H5 -> buildHeader(5) + is H6 -> buildHeader(6) + is Ul -> buildList(false) + is Ol -> buildList(true, docTag.params["start"]?.toInt() ?: 1) + is Li -> listOf( + ContentGroup(children = 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 A -> listOf( + ContentResolvedLink( + buildChildren(docTag), + docTag.params.get("href")!!, + dci, + sourceSets, + styles + ) + ) + is DocumentationLink -> listOf( + ContentDRILink( + buildChildren(docTag), + docTag.dri, + DCI( + setOf(docTag.dri), + ContentKind.Main + ), + sourceSets, + styles + ) + ) + is BlockQuote -> listOf( + ContentCodeBlock( + buildChildren(docTag), + "", + dci, + sourceSets, + styles + ) + ) + is CodeInline -> listOf( + ContentCodeInline( + buildChildren(docTag), + "", + dci, + sourceSets, + styles + ) + ) + is CodeBlock -> listOf( + ContentCodeBlock( + buildChildren(docTag), + "", + dci, + sourceSets, + styles + ) + ) + is Img -> listOf( + ContentEmbeddedResource( + address = docTag.params["href"]!!, + altText = docTag.params["alt"], + dci = dci, + sourceSets = sourceSets, + style = styles, + extra = extra + ) + ) + is HorizontalRule -> listOf( + ContentText( + "", + dci, + sourceSets, + setOf() + ) + ) + is Text -> listOf( + ContentText( + docTag.body, + dci, + sourceSets, + styles + ) + ) + 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, + styles + CommentTable + ) + ) + is Th, + is Tr -> listOf( + ContentGroup( + docTag.children.map { + ContentGroup(buildChildren(it), dci, sourceSets, styles, extra) + }, + dci, + sourceSets, + styles + ) + ) + is Index -> listOf( + ContentGroup( + buildChildren(docTag, newStyles = styles + ContentStyle.InDocumentationAnchor), + dci, + sourceSets, + styles + ) + ) + else -> buildChildren(docTag) + } + } +} diff --git a/plugins/base/src/main/kotlin/transformers/pages/merger/FallbackPageMergerStrategy.kt b/plugins/base/src/main/kotlin/transformers/pages/merger/FallbackPageMergerStrategy.kt new file mode 100644 index 00000000..df0c27ee --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/merger/FallbackPageMergerStrategy.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.base.transformers.pages.merger + +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.utilities.DokkaLogger + +class FallbackPageMergerStrategy(private val logger: DokkaLogger) : PageMergerStrategy { + override fun tryMerge(pages: List<PageNode>, path: List<String>): List<PageNode> { + val renderedPath = path.joinToString(separator = "/") + if (pages.size != 1) logger.warn("For $renderedPath: expected 1 page, but got ${pages.size}") + return listOf(pages.first()) + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/merger/PageMerger.kt b/plugins/base/src/main/kotlin/transformers/pages/merger/PageMerger.kt new file mode 100644 index 00000000..4faf3ad4 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/merger/PageMerger.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka.base.transformers.pages.merger + +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.transformers.pages.PageTransformer + +class PageMerger(private val strategies: Iterable<PageMergerStrategy>) : PageTransformer { + override fun invoke(input: RootPageNode): RootPageNode = + input.modified(children = input.children.map { it.mergeChildren(emptyList()) }) + + private fun PageNode.mergeChildren(path: List<String>): PageNode = children.groupBy { it::class }.map { + it.value.groupBy { it.name }.map { (n, v) -> mergePageNodes(v, path + n) }.map { it.assertSingle(path) } + }.let { pages -> + modified(children = pages.flatten().map { it.mergeChildren(path + it.name) }) + } + + private fun mergePageNodes(pages: List<PageNode>, path: List<String>): List<PageNode> = + strategies.fold(pages) { acc, strategy -> tryMerge(strategy, acc, path) } + + private fun tryMerge(strategy: PageMergerStrategy, pages: List<PageNode>, path: List<String>) = + if (pages.size > 1) strategy.tryMerge(pages, path) else pages +} + +private fun <T> Iterable<T>.assertSingle(path: List<String>): T = try { + single() + } catch (e: Exception) { + val renderedPath = path.joinToString(separator = "/") + throw IllegalStateException("Page merger is misconfigured. Error for $renderedPath: ${e.message}") + }
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/merger/PageMergerStrategy.kt b/plugins/base/src/main/kotlin/transformers/pages/merger/PageMergerStrategy.kt new file mode 100644 index 00000000..b73b17e0 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/merger/PageMergerStrategy.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.base.transformers.pages.merger + +import org.jetbrains.dokka.pages.PageNode + +interface PageMergerStrategy { + + fun tryMerge(pages: List<PageNode>, path: List<String>): List<PageNode> + +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt b/plugins/base/src/main/kotlin/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt new file mode 100644 index 00000000..d81f131b --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt @@ -0,0 +1,40 @@ +package org.jetbrains.dokka.base.transformers.pages.merger + +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.utilities.DokkaLogger + +class SameMethodNamePageMergerStrategy(val logger: DokkaLogger) : PageMergerStrategy { + override fun tryMerge(pages: List<PageNode>, path: List<String>): List<PageNode> { + val members = pages.filterIsInstance<MemberPageNode>().takeIf { it.isNotEmpty() } ?: return pages + val name = pages.first().name.also { + if (pages.any { page -> page.name != it }) { // Is this even possible? + logger.error("Page names for $it do not match!") + } + } + val dri = members.flatMap { it.dri }.toSet() + + + val merged = MemberPageNode( + dri = dri, + name = name, + children = members.flatMap { it.children }.distinct(), + content = squashDivergentInstances(members), + embeddedResources = members.flatMap { it.embeddedResources }.distinct(), + documentable = null + ) + + return (pages - members) + listOf(merged) + } + + private fun squashDivergentInstances(nodes: List<MemberPageNode>): ContentNode = + nodes.map { it.content } + .reduce { acc, node -> + acc.mapTransform<ContentDivergentGroup, ContentNode> { g -> + g.copy(children = (g.children + + (node.dfs { it is ContentDivergentGroup && it.groupID == g.groupID } as? ContentDivergentGroup) + ?.children?.single() + ).filterNotNull() + ) + } + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt new file mode 100644 index 00000000..a391b534 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt @@ -0,0 +1,38 @@ +package org.jetbrains.dokka.base.transformers.pages.samples + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.kotlin.idea.kdoc.resolveKDocSampleLink +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +class DefaultSamplesTransformer(context: DokkaContext) : SamplesTransformer(context) { + + override fun processBody(psiElement: PsiElement): String { + val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() + val lines = text.split("\n") + val indent = lines.filter(String::isNotBlank).map { it.takeWhile(Char::isWhitespace).count() }.min() ?: 0 + return lines.joinToString("\n") { it.drop(indent) } + } + + private fun processSampleBody(psiElement: PsiElement): String = when (psiElement) { + is KtDeclarationWithBody -> { + val bodyExpression = psiElement.bodyExpression + when (bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> psiElement.text + } + + override fun processImports(psiElement: PsiElement): String { + val psiFile = psiElement.containingFile + return when(val text = psiFile.safeAs<KtFile>()?.importList?.text) { + is String -> text + else -> "" + } + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/samples/KotlinWebsiteSamplesTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/samples/KotlinWebsiteSamplesTransformer.kt new file mode 100644 index 00000000..c099644f --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/samples/KotlinWebsiteSamplesTransformer.kt @@ -0,0 +1,196 @@ +package org.jetbrains.dokka.base.transformers.pages.samples + +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.impl.source.tree.LeafPsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.allChildren +import org.jetbrains.kotlin.psi.psiUtil.prevLeaf +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.resolve.ImportPath +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.io.PrintWriter +import java.io.StringWriter + +// TODO Inspect below class for any bugs. Big chunk of was ripped from 0.10.1 +class KotlinWebsiteSamplesTransformer(context: DokkaContext): SamplesTransformer(context) { + + private class SampleBuilder : KtTreeVisitorVoid() { + val builder = StringBuilder() + val text: String + get() = builder.toString() + + val errors = mutableListOf<ConvertError>() + + data class ConvertError(val e: Exception, val text: String, val loc: String) + + fun KtValueArgument.extractStringArgumentValue() = + (getArgumentExpression() as KtStringTemplateExpression) + .entries.joinToString("") { it.text } + + + fun convertAssertPrints(expression: KtCallExpression) { + val (argument, commentArgument) = expression.valueArguments + builder.apply { + append("println(") + append(argument.text) + append(") // ") + append(commentArgument.extractStringArgumentValue()) + } + } + + fun convertAssertTrueFalse(expression: KtCallExpression, expectedResult: Boolean) { + val (argument) = expression.valueArguments + builder.apply { + expression.valueArguments.getOrNull(1)?.let { + append("// ${it.extractStringArgumentValue()}") + val ws = expression.prevLeaf { it is PsiWhiteSpace } + append(ws?.text ?: "\n") + } + append("println(\"") + append(argument.text) + append(" is \${") + append(argument.text) + append("}\") // $expectedResult") + } + } + + fun convertAssertFails(expression: KtCallExpression) { + val valueArguments = expression.valueArguments + + val funcArgument: KtValueArgument + val message: KtValueArgument? + + if (valueArguments.size == 1) { + message = null + funcArgument = valueArguments.first() + } else { + message = valueArguments.first() + funcArgument = valueArguments.last() + } + + builder.apply { + val argument = funcArgument.extractFunctionalArgumentText() + append(argument.lines().joinToString(separator = "\n") { "// $it" }) + append(" // ") + if (message != null) { + append(message.extractStringArgumentValue()) + } + append(" will fail") + } + } + + private fun KtValueArgument.extractFunctionalArgumentText(): String { + return if (getArgumentExpression() is KtLambdaExpression) + PsiTreeUtil.findChildOfType(this, KtBlockExpression::class.java)?.text ?: "" + else + text + } + + fun convertAssertFailsWith(expression: KtCallExpression) { + val (funcArgument) = expression.valueArguments + val (exceptionType) = expression.typeArguments + builder.apply { + val argument = funcArgument.extractFunctionalArgumentText() + append(argument.lines().joinToString(separator = "\n") { "// $it" }) + append(" // will fail with ") + append(exceptionType.text) + } + } + + override fun visitCallExpression(expression: KtCallExpression) { + when (expression.calleeExpression?.text) { + "assertPrints" -> convertAssertPrints(expression) + "assertTrue" -> convertAssertTrueFalse(expression, expectedResult = true) + "assertFalse" -> convertAssertTrueFalse(expression, expectedResult = false) + "assertFails" -> convertAssertFails(expression) + "assertFailsWith" -> convertAssertFailsWith(expression) + else -> super.visitCallExpression(expression) + } + } + + private fun reportProblemConvertingElement(element: PsiElement, e: Exception) { + val text = element.text + val document = PsiDocumentManager.getInstance(element.project).getDocument(element.containingFile) + + val lineInfo = if (document != null) { + val lineNumber = document.getLineNumber(element.startOffset) + "$lineNumber, ${element.startOffset - document.getLineStartOffset(lineNumber)}" + } else { + "offset: ${element.startOffset}" + } + errors += ConvertError(e, text, lineInfo) + } + + override fun visitElement(element: PsiElement) { + if (element is LeafPsiElement) + builder.append(element.text) + + element.acceptChildren(object : PsiElementVisitor() { + override fun visitElement(element: PsiElement) { + try { + element.accept(this@SampleBuilder) + } catch (e: Exception) { + try { + reportProblemConvertingElement(element, e) + } finally { + builder.append(element.text) //recover + } + } + } + }) + } + + } + + private fun PsiElement.buildSampleText(): String { + val sampleBuilder = SampleBuilder() + this.accept(sampleBuilder) + + sampleBuilder.errors.forEach { + val sw = StringWriter() + val pw = PrintWriter(sw) + it.e.printStackTrace(pw) + + this@KotlinWebsiteSamplesTransformer.context.logger.error("${containingFile.name}: (${it.loc}): Exception thrown while converting \n```\n${it.text}\n```\n$sw") + } + return sampleBuilder.text + } + + val importsToIgnore = arrayOf("samples.*", "samples.Sample").map { ImportPath.fromString(it) } + + override fun processImports(psiElement: PsiElement): String { + val psiFile = psiElement.containingFile + return when(val text = psiFile.safeAs<KtFile>()?.importList) { + is KtImportList -> text.let { + it.allChildren.filter { + it !is KtImportDirective || it.importPath !in importsToIgnore + }.joinToString(separator = "\n") { it.text } + } + else -> "" + } + } + + override fun processBody(psiElement: PsiElement): String { + val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() + val lines = text.split("\n") + val indent = lines.filter(String::isNotBlank).map { it.takeWhile(Char::isWhitespace).count() }.min() ?: 0 + return lines.joinToString("\n") { it.drop(indent) } + } + + private fun processSampleBody(psiElement: PsiElement) = when (psiElement) { + is KtDeclarationWithBody -> { + val bodyExpression = psiElement.bodyExpression + val bodyExpressionText = bodyExpression!!.buildSampleText() + when (bodyExpression) { + is KtBlockExpression -> bodyExpressionText.removeSurrounding("{", "}") + else -> bodyExpressionText + } + } + else -> psiElement.buildSampleText() + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt new file mode 100644 index 00000000..695ef050 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt @@ -0,0 +1,151 @@ +package org.jetbrains.dokka.base.transformers.pages.samples + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.AnalysisEnvironment +import org.jetbrains.dokka.analysis.DokkaMessageCollector +import org.jetbrains.dokka.analysis.DokkaResolutionFacade +import org.jetbrains.dokka.analysis.EnvironmentAndFacade +import org.jetbrains.dokka.base.renderers.sourceSets +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.Sample +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer +import org.jetbrains.kotlin.idea.kdoc.resolveKDocSampleLink +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.utils.PathUtil +import java.io.File + +abstract class SamplesTransformer(val context: DokkaContext) : PageTransformer { + + abstract fun processBody(psiElement: PsiElement): String + abstract fun processImports(psiElement: PsiElement): String + + final override fun invoke(input: RootPageNode): RootPageNode { + val analysis = setUpAnalysis(context) + val kotlinPlaygroundScript = + "<script src=\"https://unpkg.com/kotlin-playground@1\" data-selector=\"code.runnablesample\"></script>" + + return input.transformContentPagesTree { page -> + page.documentable?.documentation?.entries?.fold(page) { acc, entry -> + entry.value.children.filterIsInstance<Sample>().fold(acc) { acc, sample -> + acc.modified( + content = acc.content.addSample(page, entry.key, sample.name, analysis), + embeddedResources = acc.embeddedResources + kotlinPlaygroundScript + ) + } + } ?: page + } + } + + private fun setUpAnalysis(context: DokkaContext) = context.configuration.sourceSets.map { + it to AnalysisEnvironment(DokkaMessageCollector(context.logger), it.analysisPlatform).run { + if (analysisPlatform == Platform.jvm) { + addClasspath(PathUtil.getJdkClassesRootsFromCurrentJre()) + } + it.classpath.forEach { addClasspath(File(it)) } + + addSources(it.samples.map { it }) + + loadLanguageVersionSettings(it.languageVersion, it.apiVersion) + + val environment = createCoreEnvironment() + val (facade, _) = createResolutionFacade(environment) + EnvironmentAndFacade(environment, facade) + } + }.toMap() + + private fun ContentNode.addSample( + contentPage: ContentPage, + platform: DokkaSourceSet, + fqName: String, + analysis: Map<DokkaSourceSet, EnvironmentAndFacade> + ): ContentNode { + val facade = analysis[platform]?.facade + ?: return this.also { context.logger.warn("Cannot resolve facade for platform ${platform.moduleDisplayName}") } + val psiElement = fqNameToPsiElement(facade, fqName) + ?: return this.also { context.logger.warn("Cannot find PsiElement corresponding to $fqName") } + val imports = + processImports(psiElement) + val body = processBody(psiElement) + val node = contentCode(contentPage.sourceSets(), contentPage.dri, createSampleBody(imports, body), "kotlin") + + return dfs(fqName, node) + } + + protected open fun createSampleBody(imports: String, body: String) = + """ |$imports + |fun main() { + | //sampleStart + | $body + | //sampleEnd + |}""".trimMargin() + + private fun ContentNode.dfs(fqName: String, node: ContentCodeBlock): ContentNode { + return when (this) { + is ContentHeader -> copy(children.map { it.dfs(fqName, node) }) + is ContentDivergentGroup -> @Suppress("UNCHECKED_CAST") copy(children.map { + it.dfs(fqName, node) + } as List<ContentDivergentInstance>) + is ContentDivergentInstance -> copy( + before.let { it?.dfs(fqName, node) }, + divergent.dfs(fqName, node), + after.let { it?.dfs(fqName, node) }) + is ContentCodeBlock -> copy(children.map { it.dfs(fqName, node) }) + is ContentCodeInline -> copy(children.map { it.dfs(fqName, node) }) + is ContentDRILink -> copy(children.map { it.dfs(fqName, node) }) + is ContentResolvedLink -> copy(children.map { it.dfs(fqName, node) }) + is ContentEmbeddedResource -> copy(children.map { it.dfs(fqName, node) }) + is ContentTable -> copy(children = children.map { it.dfs(fqName, node) as ContentGroup }) + is ContentList -> copy(children.map { it.dfs(fqName, node) }) + is ContentGroup -> copy(children.map { it.dfs(fqName, node) }) + is PlatformHintedContent -> copy(inner.dfs(fqName, node)) + is ContentText -> if (text == fqName) node else this + is ContentBreakLine -> this + else -> this.also { context.logger.error("Could not recognize $this ContentNode in SamplesTransformer") } + } + } + + private fun fqNameToPsiElement(resolutionFacade: DokkaResolutionFacade, functionName: String): PsiElement? { + val packageName = functionName.takeWhile { it != '.' } + val descriptor = resolutionFacade.resolveSession.getPackageFragment(FqName(packageName)) + ?: return null.also { context.logger.warn("Cannot find descriptor for package $packageName") } + val symbol = resolveKDocSampleLink( + BindingContext.EMPTY, + resolutionFacade, + descriptor, + functionName.split(".") + ).firstOrNull() ?: return null.also { context.logger.warn("Unresolved function $functionName in @sample") } + return DescriptorToSourceUtils.descriptorToDeclaration(symbol) + } + + private fun contentCode( + sourceSets: Set<DokkaSourceSet>, + dri: Set<DRI>, + content: String, + language: String, + styles: Set<Style> = emptySet(), + extra: PropertyContainer<ContentNode> = PropertyContainer.empty() + ) = + ContentCodeBlock( + children = listOf( + ContentText( + text = content, + dci = DCI(dri, ContentKind.Sample), + sourceSets = sourceSets, + style = emptySet(), + extra = PropertyContainer.empty() + ) + ), + language = language, + dci = DCI(dri, ContentKind.Sample), + sourceSets = sourceSets, + style = styles + ContentStyle.RunnableSample + TextStyle.Monospace, + extra = extra + ) +} diff --git a/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt new file mode 100644 index 00000000..f0e62f11 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt @@ -0,0 +1,130 @@ +package org.jetbrains.dokka.base.transformers.pages.sourcelinks + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiDocumentManager +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.model.DocumentableSource +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.DescriptorDocumentableSource +import org.jetbrains.dokka.analysis.PsiDocumentableSource +import org.jetbrains.dokka.model.WithExpectActual +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer +import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource +import org.jetbrains.kotlin.resolve.source.getPsi +import org.jetbrains.kotlin.utils.addToStdlib.cast + +class SourceLinksTransformer(val context: DokkaContext, val builder: PageContentBuilder) : PageTransformer { + + override fun invoke(input: RootPageNode) = + input.transformContentPagesTree { node -> + when (val documentable = node.documentable) { + is WithExpectActual -> resolveSources(documentable) + .takeIf { it.isNotEmpty() } + ?.let { node.addSourcesContent(it) } + ?: node + else -> node + } + } + + private fun getSourceLinks() = context.configuration.sourceSets + .flatMap { it.sourceLinks.map { sl -> SourceLink(sl, it) } } + + private fun resolveSources(documentable: WithExpectActual) = documentable.sources + .mapNotNull { entry -> + getSourceLinks().find { entry.value.path.contains(it.path) && it.sourceSetData == entry.key }?.let { + Pair( + entry.key, + entry.value.toLink(it) + ) + } + } + + private fun ContentPage.addSourcesContent(sources: List<Pair<DokkaSourceSet, String>>) = builder + .buildSourcesContent(this, sources) + .let { + this.modified( + content = this.content.addTable(it) + ) + } + + private fun PageContentBuilder.buildSourcesContent( + node: ContentPage, + sources: List<Pair<DokkaSourceSet, String>> + ) = contentFor( + node.dri.first(), + node.documentable!!.sourceSets.toSet() + ) { + header(2, "Sources", kind = ContentKind.Source) + +ContentTable( + emptyList(), + sources.map { + buildGroup(node.dri, setOf(it.first), kind = ContentKind.Source) { + link("(source)", it.second) + } + }, + DCI(node.dri, ContentKind.Source), + node.documentable!!.sourceSets.toSet(), + style = emptySet(), + extra = mainExtra + SimpleAttr.header("Sources") + ) + } + + private fun DocumentableSource.toLink(sourceLink: SourceLink): String { + val lineNumber = when (this) { + is DescriptorDocumentableSource -> this.descriptor + .cast<DeclarationDescriptorWithSource>() + .source.getPsi() + ?.lineNumber() + is PsiDocumentableSource -> this.psi.lineNumber() + else -> null + } + return sourceLink.url + + this.path.split(sourceLink.path)[1] + + sourceLink.lineSuffix + + "${lineNumber ?: 1}" + } + + private fun ContentNode.addTable(table: ContentGroup): ContentNode = + when (this) { + is ContentGroup -> { + if(hasTabbedContent()){ + copy( + children = children.map { + if(it.hasStyle(ContentStyle.TabbedContent) && it is ContentGroup){ + it.copy(children = it.children + table) + } else { + it + } + } + ) + } else { + copy(children = children + table) + } + + } + else -> ContentGroup( + children = listOf(this, table), + extra = this.extra, + sourceSets = this.sourceSets, + dci = this.dci, + style = this.style + ) + } + + private fun PsiElement.lineNumber(): Int? { + val doc = PsiDocumentManager.getInstance(project).getDocument(containingFile) + // IJ uses 0-based line-numbers; external source browsers use 1-based + return doc?.getLineNumber(textRange.startOffset)?.plus(1) + } +} + +data class SourceLink(val path: String, val url: String, val lineSuffix: String?, val sourceSetData: DokkaSourceSet) { + constructor(sourceLinkDefinition: DokkaConfiguration.SourceLinkDefinition, sourceSetData: DokkaSourceSet) : this( + sourceLinkDefinition.path, sourceLinkDefinition.url, sourceLinkDefinition.lineSuffix, sourceSetData + ) +} + +fun ContentGroup.hasTabbedContent(): Boolean = children.any { it.hasStyle(ContentStyle.TabbedContent) }
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt new file mode 100644 index 00000000..ffceaaa7 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt @@ -0,0 +1,782 @@ +package org.jetbrains.dokka.base.translators.descriptors + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.DescriptorDocumentableSource +import org.jetbrains.dokka.analysis.DokkaResolutionFacade +import org.jetbrains.dokka.analysis.KotlinAnalysis +import org.jetbrains.dokka.analysis.from +import org.jetbrains.dokka.base.parsers.MarkdownParser +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.Nullable +import org.jetbrains.dokka.model.TypeConstructor +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.builtins.isExtensionFunctionType +import org.jetbrains.kotlin.builtins.isFunctionType +import org.jetbrains.kotlin.codegen.isJvmStaticInObjectOrClassOrInterface +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.Visibility +import org.jetbrains.kotlin.descriptors.annotations.Annotated +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.descriptors.impl.DeclarationDescriptorVisitorEmptyBodies +import org.jetbrains.kotlin.idea.kdoc.findKDoc +import org.jetbrains.kotlin.load.kotlin.toSourceElement +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.calls.callUtil.getValueArgumentsInParentheses +import org.jetbrains.kotlin.resolve.calls.components.isVararg +import org.jetbrains.kotlin.resolve.constants.ConstantValue +import org.jetbrains.kotlin.resolve.constants.KClassValue.Value.LocalClass +import org.jetbrains.kotlin.resolve.constants.KClassValue.Value.NormalClass +import org.jetbrains.kotlin.resolve.descriptorUtil.annotationClass +import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny +import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.MemberScope +import org.jetbrains.kotlin.resolve.source.KotlinSourceElement +import org.jetbrains.kotlin.resolve.source.PsiSourceElement +import org.jetbrains.kotlin.types.DynamicType +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeProjection +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.nio.file.Paths +import org.jetbrains.kotlin.resolve.constants.AnnotationValue as ConstantsAnnotationValue +import org.jetbrains.kotlin.resolve.constants.ArrayValue as ConstantsArrayValue +import org.jetbrains.kotlin.resolve.constants.EnumValue as ConstantsEnumValue +import org.jetbrains.kotlin.resolve.constants.KClassValue as ConstantsKtClassValue + +class DefaultDescriptorToDocumentableTranslator( + private val kotlinAnalysis: KotlinAnalysis +) : SourceToDocumentableTranslator { + + override fun invoke(sourceSet: DokkaSourceSet, context: DokkaContext): DModule { + val (environment, facade) = kotlinAnalysis[sourceSet] + val packageFragments = environment.getSourceFiles().asSequence() + .map { it.packageFqName } + .distinct() + .mapNotNull { facade.resolveSession.getPackageFragment(it) } + .toList() + + return DokkaDescriptorVisitor(sourceSet, kotlinAnalysis[sourceSet].facade, context.logger).run { + packageFragments.mapNotNull { it.safeAs<PackageFragmentDescriptor>() }.map { + visitPackageFragmentDescriptor( + it, + DRIWithPlatformInfo(DRI.topLevel, emptyMap()) + ) + } + }.let { DModule(sourceSet.moduleDisplayName, it, emptyMap(), null, setOf(sourceSet)) } + } +} + +data class DRIWithPlatformInfo( + val dri: DRI, + val actual: SourceSetDependent<DocumentableSource> +) + +fun DRI.withEmptyInfo() = DRIWithPlatformInfo(this, emptyMap()) + +private class DokkaDescriptorVisitor( + private val sourceSet: DokkaSourceSet, + private val resolutionFacade: DokkaResolutionFacade, + private val logger: DokkaLogger +) : DeclarationDescriptorVisitorEmptyBodies<Documentable, DRIWithPlatformInfo>() { + override fun visitDeclarationDescriptor(descriptor: DeclarationDescriptor, parent: DRIWithPlatformInfo): Nothing { + throw IllegalStateException("${javaClass.simpleName} should never enter ${descriptor.javaClass.simpleName}") + } + + private fun Collection<DeclarationDescriptor>.filterDescriptorsInSourceSet() = filter { + it.toSourceElement.containingFile.toString().let { path -> + path.isNotBlank() && sourceSet.sourceRoots.any { root -> + Paths.get(path).startsWith(Paths.get(root.path)) + } + } + } + + private fun <T> T.toSourceSetDependent() = mapOf(sourceSet to this) + + override fun visitPackageFragmentDescriptor( + descriptor: PackageFragmentDescriptor, + parent: DRIWithPlatformInfo + ): DPackage { + val name = descriptor.fqName.asString().takeUnless { it.isBlank() } ?: fallbackPackageName() + val driWithPlatform = DRI(packageName = name).withEmptyInfo() + val scope = descriptor.getMemberScope() + + return DPackage( + dri = driWithPlatform.dri, + functions = scope.functions(driWithPlatform, true), + properties = scope.properties(driWithPlatform, true), + classlikes = scope.classlikes(driWithPlatform, true), + typealiases = scope.typealiases(driWithPlatform, true), + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet) + ) + } + + override fun visitClassDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DClasslike = + when (descriptor.kind) { + ClassKind.ENUM_CLASS -> enumDescriptor(descriptor, parent) + ClassKind.OBJECT -> objectDescriptor(descriptor, parent) + ClassKind.INTERFACE -> interfaceDescriptor(descriptor, parent) + ClassKind.ANNOTATION_CLASS -> annotationDescriptor(descriptor, parent) + else -> classDescriptor(descriptor, parent) + } + + private fun interfaceDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DInterface { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val info = descriptor.resolveClassDescriptionData() + + return DInterface( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + classlikes = scope.classlikes(driWithPlatform), + sources = descriptor.createSources(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + documentation = info.docs, + generics = descriptor.declaredTypeParameters.map { it.toTypeParameter() }, + companion = descriptor.companion(driWithPlatform), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.allImplementedInterfaces.toSourceSetDependent()) + ) + ) + } + + private fun objectDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DObject { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val info = descriptor.resolveClassDescriptionData() + + + return DObject( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + classlikes = scope.classlikes(driWithPlatform), + sources = descriptor.createSources(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + documentation = info.docs, + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.allImplementedInterfaces.toSourceSetDependent()) + ) + ) + } + + private fun enumDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DEnum { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val info = descriptor.resolveClassDescriptionData() + + return DEnum( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + entries = scope.enumEntries(driWithPlatform), + constructors = descriptor.constructors.map { visitConstructorDescriptor(it, driWithPlatform) }, + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + classlikes = scope.classlikes(driWithPlatform), + sources = descriptor.createSources(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + documentation = info.docs, + companion = descriptor.companion(driWithPlatform), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.allImplementedInterfaces.toSourceSetDependent()) + ) + ) + } + + private fun enumEntryDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DEnumEntry { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + + return DEnumEntry( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + documentation = descriptor.resolveDescriptorData(), + classlikes = scope.classlikes(driWithPlatform), + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + sourceSets = setOf(sourceSet), + expectPresentInSet = sourceSet.takeIf { isExpect }, + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ConstructorValues(descriptor.getAppliedConstructorParameters().toSourceSetDependent()) + ) + ) + } + + fun annotationDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DAnnotation { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + + return DAnnotation( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + documentation = descriptor.resolveDescriptorData(), + classlikes = scope.classlikes(driWithPlatform), + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + expectPresentInSet = null, + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ), + companion = descriptor.companionObjectDescriptor?.let { objectDescriptor(it, driWithPlatform) }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = descriptor.declaredTypeParameters.map { it.toTypeParameter() }, + constructors = descriptor.constructors.map { visitConstructorDescriptor(it, driWithPlatform) }, + sources = descriptor.createSources() + ) + } + + private fun classDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DClass { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val info = descriptor.resolveClassDescriptionData() + val actual = descriptor.createSources() + + return DClass( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + constructors = descriptor.constructors.map { + visitConstructorDescriptor( + it, + if (it.isPrimary) DRIWithPlatformInfo(driWithPlatform.dri, actual) + else DRIWithPlatformInfo(driWithPlatform.dri, emptyMap()) + ) + }, + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + classlikes = scope.classlikes(driWithPlatform), + sources = actual, + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + generics = descriptor.declaredTypeParameters.map { it.toTypeParameter() }, + documentation = info.docs, + modifier = descriptor.modifier().toSourceSetDependent(), + companion = descriptor.companion(driWithPlatform), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.allImplementedInterfaces.toSourceSetDependent()) + ) + ) + } + + override fun visitPropertyDescriptor(descriptor: PropertyDescriptor, parent: DRIWithPlatformInfo): DProperty { + val dri = parent.dri.copy(callable = Callable.from(descriptor)) + val isExpect = descriptor.isExpect + + val actual = descriptor.createSources() + return DProperty( + dri = dri, + name = descriptor.name.asString(), + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor(it, DRIWithPlatformInfo(dri, actual)) + }, + sources = actual, + getter = descriptor.accessors.filterIsInstance<PropertyGetterDescriptor>().singleOrNull()?.let { + visitPropertyAccessorDescriptor(it, descriptor, dri) + }, + setter = descriptor.accessors.filterIsInstance<PropertySetterDescriptor>().singleOrNull()?.let { + visitPropertyAccessorDescriptor(it, descriptor, dri) + }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = descriptor.resolveDescriptorData(), + modifier = descriptor.modifier().toSourceSetDependent(), + type = descriptor.returnType!!.toBound(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + generics = descriptor.typeParameters.map { it.toTypeParameter() }, + extra = PropertyContainer.withAll( + (descriptor.additionalExtras() + descriptor.getAnnotationsWithBackingField() + .toAdditionalExtras()).toSet().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotationsWithBackingField().toSourceSetDependent().toAnnotations() + ) + ) + } + + fun CallableMemberDescriptor.createDRI(wasOverridenBy: DRI? = null): Pair<DRI, DRI?> = + if (kind == CallableMemberDescriptor.Kind.DECLARATION || overriddenDescriptors.isEmpty()) + Pair(DRI.from(this), wasOverridenBy) + else + overriddenDescriptors.first().createDRI(DRI.from(this)) + + override fun visitFunctionDescriptor(descriptor: FunctionDescriptor, parent: DRIWithPlatformInfo): DFunction { + val (dri, inheritedFrom) = descriptor.createDRI() + val isExpect = descriptor.isExpect + + val actual = descriptor.createSources() + return DFunction( + dri = dri, + name = descriptor.name.asString(), + isConstructor = false, + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor(it, DRIWithPlatformInfo(dri, actual)) + }, + parameters = descriptor.valueParameters.mapIndexed { index, desc -> + parameter(index, desc, DRIWithPlatformInfo(dri, actual)) + }, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sources = actual, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = descriptor.typeParameters.map { it.toTypeParameter() }, + documentation = descriptor.takeIf { it.kind != CallableMemberDescriptor.Kind.SYNTHESIZED }?.resolveDescriptorData() ?: emptyMap(), + modifier = descriptor.modifier().toSourceSetDependent(), + type = descriptor.returnType!!.toBound(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + InheritedFunction(inheritedFrom.toSourceSetDependent()), + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ) + ) + } + + override fun visitConstructorDescriptor(descriptor: ConstructorDescriptor, parent: DRIWithPlatformInfo): DFunction { + val dri = parent.dri.copy(callable = Callable.from(descriptor)) + val actual = descriptor.createSources() + val isExpect = descriptor.isExpect + + return DFunction( + dri = dri, + name = "<init>", + isConstructor = true, + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor(it, DRIWithPlatformInfo(dri, actual)) + }, + parameters = descriptor.valueParameters.mapIndexed { index, desc -> + parameter(index, desc, DRIWithPlatformInfo(dri, actual)) + }, + sources = actual, + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = descriptor.resolveDescriptorData().let { sourceSetDependent -> + if (descriptor.isPrimary) { + sourceSetDependent.map { entry -> + Pair( + entry.key, + entry.value.copy(children = (entry.value.children.find { it is Constructor }?.root?.let { constructor -> + listOf(Description(constructor)) + } ?: emptyList<TagWrapper>()) + entry.value.children.filterIsInstance<Param>())) + }.toMap() + } else { + sourceSetDependent + } + }, + type = descriptor.returnType.toBound(), + modifier = descriptor.modifier().toSourceSetDependent(), + generics = descriptor.typeParameters.map { it.toTypeParameter() }, + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll<DFunction>( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ).let { + if (descriptor.isPrimary) { + it + PrimaryConstructorExtra + } else it + } + ) + } + + override fun visitReceiverParameterDescriptor( + descriptor: ReceiverParameterDescriptor, + parent: DRIWithPlatformInfo + ) = DParameter( + dri = parent.dri.copy(target = PointingToDeclaration), + name = null, + type = descriptor.type.toBound(), + expectPresentInSet = null, + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll(descriptor.getAnnotations().toSourceSetDependent().toAnnotations()) + ) + + private fun visitPropertyAccessorDescriptor( + descriptor: PropertyAccessorDescriptor, + propertyDescriptor: PropertyDescriptor, + parent: DRI + ): DFunction { + val dri = parent.copy(callable = Callable.from(descriptor)) + val isGetter = descriptor is PropertyGetterDescriptor + val isExpect = descriptor.isExpect + + fun PropertyDescriptor.asParameter(parent: DRI) = + DParameter( + parent.copy(target = PointingToCallableParameters(parameterIndex = 1)), + this.name.asString(), + type = this.type.toBound(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + getAnnotationsWithBackingField().toSourceSetDependent().toAnnotations() + ) + ) + + val name = run { + val modifier = if (isGetter) "get" else "set" + val rawName = propertyDescriptor.name.asString() + "$modifier${rawName[0].toUpperCase()}${rawName.drop(1)}" + } + + val parameters = + if (isGetter) { + emptyList() + } else { + listOf(propertyDescriptor.asParameter(dri)) + } + + return DFunction( + dri, + name, + isConstructor = false, + parameters = parameters, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = descriptor.resolveDescriptorData(), + type = descriptor.returnType!!.toBound(), + generics = descriptor.typeParameters.map { it.toTypeParameter() }, + modifier = descriptor.modifier().toSourceSetDependent(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor( + it, + DRIWithPlatformInfo(dri, descriptor.createSources()) + ) + }, + sources = descriptor.createSources(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ) + ) + } + + override fun visitTypeAliasDescriptor(descriptor: TypeAliasDescriptor, parent: DRIWithPlatformInfo?) = + with(descriptor) { + DTypeAlias( + dri = DRI.from(this), + name = name.asString(), + type = defaultType.toBound(), + expectPresentInSet = null, + underlyingType = underlyingType.toBound().toSourceSetDependent(), + visibility = visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = resolveDescriptorData(), + sourceSets = setOf(sourceSet) + ) + } + + private fun parameter(index: Int, descriptor: ValueParameterDescriptor, parent: DRIWithPlatformInfo) = + DParameter( + dri = parent.dri.copy(target = PointingToCallableParameters(index)), + name = descriptor.name.asString(), + type = descriptor.type.toBound(), + expectPresentInSet = null, + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll(listOfNotNull( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + descriptor.getDefaultValue()?.let { DefaultValue(it) } + )) + ) + + private fun MemberScope.getContributedDescriptors(kindFilter: DescriptorKindFilter, shouldFilter: Boolean) = + getContributedDescriptors(kindFilter) { true }.let { + if (shouldFilter) it.filterDescriptorsInSourceSet() else it + } + + private fun MemberScope.functions(parent: DRIWithPlatformInfo, packageLevel: Boolean = false): List<DFunction> = + getContributedDescriptors(DescriptorKindFilter.FUNCTIONS, packageLevel) + .filterIsInstance<FunctionDescriptor>() + .map { visitFunctionDescriptor(it, parent) } + + private fun MemberScope.properties(parent: DRIWithPlatformInfo, packageLevel: Boolean = false): List<DProperty> = + getContributedDescriptors(DescriptorKindFilter.VALUES, packageLevel) + .filterIsInstance<PropertyDescriptor>() + .map { visitPropertyDescriptor(it, parent) } + + private fun MemberScope.classlikes(parent: DRIWithPlatformInfo, packageLevel: Boolean = false): List<DClasslike> = + getContributedDescriptors(DescriptorKindFilter.CLASSIFIERS, packageLevel) + .filter { it is ClassDescriptor && it.kind != ClassKind.ENUM_ENTRY } + .map { visitClassDescriptor(it as ClassDescriptor, parent) } + + private fun MemberScope.typealiases(parent: DRIWithPlatformInfo, packageLevel: Boolean = false): List<DTypeAlias> = + getContributedDescriptors(DescriptorKindFilter.TYPE_ALIASES, packageLevel) + .filterIsInstance<TypeAliasDescriptor>() + .map { visitTypeAliasDescriptor(it, parent) } + + private fun MemberScope.enumEntries(parent: DRIWithPlatformInfo): List<DEnumEntry> = + this.getContributedDescriptors(DescriptorKindFilter.CLASSIFIERS) { true } + .filterIsInstance<ClassDescriptor>() + .filter { it.kind == ClassKind.ENUM_ENTRY } + .map { enumEntryDescriptor(it, parent) } + + + private fun DeclarationDescriptor.resolveDescriptorData(): SourceSetDependent<DocumentationNode> = + getDocumentation()?.toSourceSetDependent() ?: emptyMap() + + private fun ClassDescriptor.resolveClassDescriptionData(): ClassInfo { + tailrec fun buildInheritanceInformation( + inheritorClass: ClassDescriptor?, + interfaces: List<ClassDescriptor>, + level: Int = 0, + inheritanceInformation: Set<InheritanceLevel> = emptySet() + ): Set<InheritanceLevel> { + if (inheritorClass == null && interfaces.isEmpty()) return inheritanceInformation + + val updated = inheritanceInformation + InheritanceLevel( + level, + inheritorClass?.let { DRI.from(it) }, + interfaces.map { DRI.from(it) }) + val superInterfacesFromClass = inheritorClass?.getSuperInterfaces().orEmpty() + return buildInheritanceInformation( + inheritorClass = inheritorClass?.getSuperClassNotAny(), + interfaces = interfaces.flatMap { it.getSuperInterfaces() } + superInterfacesFromClass, + level = level + 1, + inheritanceInformation = updated + ) + } + return ClassInfo( + buildInheritanceInformation(getSuperClassNotAny(), getSuperInterfaces()).sortedBy { it.level }, + resolveDescriptorData() + ) + } + + private fun TypeParameterDescriptor.toTypeParameter() = + DTypeParameter( + DRI.from(this), + name.identifier, + resolveDescriptorData(), + null, + upperBounds.map { it.toBound() }, + setOf(sourceSet), + extra = PropertyContainer.withAll(additionalExtras().toSourceSetDependent().toAdditionalModifiers()) + ) + + private fun KotlinType.toBound(): Bound = when (this) { + is DynamicType -> Dynamic + else -> when (val ctor = constructor.declarationDescriptor) { + is TypeParameterDescriptor -> OtherParameter( + declarationDRI = DRI.from(ctor.containingDeclaration).withPackageFallbackTo(fallbackPackageName()), + name = ctor.name.asString() + ) + else -> TypeConstructor( + DRI.from(constructor.declarationDescriptor!!), // TODO: remove '!!' + arguments.map { it.toProjection() }, + if (isExtensionFunctionType) FunctionModifiers.EXTENSION + else if (isFunctionType) FunctionModifiers.FUNCTION + else FunctionModifiers.NONE + ) + }.let { + if (isMarkedNullable) Nullable(it) else it + } + } + + private fun TypeProjection.toProjection(): Projection = + if (isStarProjection) Star else formPossiblyVariant() + + private fun TypeProjection.formPossiblyVariant(): Projection = type.fromPossiblyNullable().let { + when (projectionKind) { + org.jetbrains.kotlin.types.Variance.INVARIANT -> it + org.jetbrains.kotlin.types.Variance.IN_VARIANCE -> Variance(Variance.Kind.In, it) + org.jetbrains.kotlin.types.Variance.OUT_VARIANCE -> Variance(Variance.Kind.Out, it) + } + } + + private fun KotlinType.fromPossiblyNullable(): Bound = + toBound().let { if (isMarkedNullable) Nullable(it) else it } + + private fun DeclarationDescriptor.getDocumentation() = findKDoc().let { + MarkdownParser(resolutionFacade, this, logger).parseFromKDocTag(it) + }.takeIf { it.children.isNotEmpty() } + + private fun ClassDescriptor.companion(dri: DRIWithPlatformInfo): DObject? = companionObjectDescriptor?.let { + objectDescriptor(it, dri) + } + + private fun MemberDescriptor.modifier() = when (modality) { + Modality.FINAL -> KotlinModifier.Final + Modality.SEALED -> KotlinModifier.Sealed + Modality.OPEN -> KotlinModifier.Open + Modality.ABSTRACT -> KotlinModifier.Abstract + else -> KotlinModifier.Empty + } + + private fun MemberDescriptor.createSources(): SourceSetDependent<DocumentableSource> = + DescriptorDocumentableSource(this).toSourceSetDependent() + + private fun FunctionDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Infix.takeIf { isInfix }, + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { isInline }, + ExtraModifiers.KotlinOnlyModifiers.Suspend.takeIf { isSuspend }, + ExtraModifiers.KotlinOnlyModifiers.Operator.takeIf { isOperator }, + ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + ExtraModifiers.KotlinOnlyModifiers.TailRec.takeIf { isTailrec }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { DescriptorUtils.isOverride(this) } + ).toSet() + + private fun ClassDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { isInline }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Inner.takeIf { isInner }, + ExtraModifiers.KotlinOnlyModifiers.Data.takeIf { isData }, + ExtraModifiers.KotlinOnlyModifiers.Fun.takeIf { isFun } + ).toSet() + + private fun ValueParameterDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.NoInline.takeIf { isNoinline }, + ExtraModifiers.KotlinOnlyModifiers.CrossInline.takeIf { isCrossinline }, + ExtraModifiers.KotlinOnlyModifiers.Const.takeIf { isConst }, + ExtraModifiers.KotlinOnlyModifiers.LateInit.takeIf { isLateInit }, + ExtraModifiers.KotlinOnlyModifiers.VarArg.takeIf { isVararg } + ).toSet() + + private fun TypeParameterDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Reified.takeIf { isReified } + ).toSet() + + private fun PropertyDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Const.takeIf { isConst }, + ExtraModifiers.KotlinOnlyModifiers.LateInit.takeIf { isLateInit }, + ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { DescriptorUtils.isOverride(this) } + ) + + private fun Annotated.getAnnotations() = annotations.mapNotNull { it.toAnnotation() } + + private fun ConstantValue<*>.toValue(): AnnotationParameterValue? = when (this) { + is ConstantsAnnotationValue -> value.toAnnotation()?.let { AnnotationValue(it) } + is ConstantsArrayValue -> ArrayValue(value.mapNotNull { it.toValue() }) + is ConstantsEnumValue -> EnumValue( + fullEnumEntryName(), + DRI(enumClassId.packageFqName.asString(), fullEnumEntryName()) + ) + is ConstantsKtClassValue -> when (value) { + is NormalClass -> (value as NormalClass).value.classId.let { + ClassValue( + it.relativeClassName.asString(), + DRI(it.packageFqName.asString(), it.relativeClassName.asString()) + ) + } + is LocalClass -> (value as LocalClass).type.let { + ClassValue( + it.toString(), + DRI.from(it.constructor.declarationDescriptor as DeclarationDescriptor) + ) + } + } + else -> StringValue(toString()) + } + + private fun AnnotationDescriptor.toAnnotation(): Annotations.Annotation { + return Annotations.Annotation( + DRI.from(annotationClass as DeclarationDescriptor), + allValueArguments.map { it.key.asString() to it.value.toValue() }.filter { + it.second != null + }.toMap() as Map<String, AnnotationParameterValue>, + annotationClass!!.annotations.hasAnnotation(FqName("kotlin.annotation.MustBeDocumented")) + ) + } + + private fun PropertyDescriptor.getAnnotationsWithBackingField(): List<Annotations.Annotation> = + getAnnotations() + (backingField?.getAnnotations() ?: emptyList()) + + private fun List<Annotations.Annotation>.toAdditionalExtras() = mapNotNull { + try { + ExtraModifiers.valueOf(it.dri.classNames?.toLowerCase() ?: "") + } catch (e: IllegalArgumentException) { + null + } + } + + + private fun ValueParameterDescriptor.getDefaultValue(): String? = + (source as? KotlinSourceElement)?.psi?.children?.find { it is KtExpression }?.text + + private fun ClassDescriptor.getAppliedConstructorParameters() = + (source as PsiSourceElement).psi?.children?.flatMap { + it.safeAs<KtInitializerList>()?.initializersAsText().orEmpty() + }.orEmpty() + + private fun KtInitializerList.initializersAsText() = + initializers.firstIsInstanceOrNull<KtCallElement>() + ?.getValueArgumentsInParentheses() + ?.flatMap { it.childrenAsText() } + .orEmpty() + + private fun ValueArgument.childrenAsText() = this.safeAs<KtValueArgument>()?.children?.map { it.text }.orEmpty() + + private data class InheritanceLevel(val level: Int, val superclass: DRI?, val interfaces: List<DRI>) + + private data class ClassInfo(val inheritance: List<InheritanceLevel>, val docs: SourceSetDependent<DocumentationNode>){ + val supertypes: List<DriWithKind> + get() = inheritance.firstOrNull { it.level == 0 }?.let { + listOfNotNull(it.superclass?.let { DriWithKind(it, KotlinClassKindTypes.CLASS) }) + it.interfaces.map { DriWithKind(it, KotlinClassKindTypes.INTERFACE) } + }.orEmpty() + + val allImplementedInterfaces: List<DRI> + get() = inheritance.flatMap { it.interfaces }.distinct() + } + + private fun Visibility.toDokkaVisibility(): org.jetbrains.dokka.model.Visibility = when (this) { + Visibilities.PUBLIC -> KotlinVisibility.Public + Visibilities.PROTECTED -> KotlinVisibility.Protected + Visibilities.INTERNAL -> KotlinVisibility.Internal + Visibilities.PRIVATE -> KotlinVisibility.Private + else -> KotlinVisibility.Public + } + + private fun ConstantsEnumValue.fullEnumEntryName() = + "${this.enumClassId.relativeClassName.asString()}.${this.enumEntryName.identifier}" + + private fun fallbackPackageName(): String = + "[${sourceSet.displayName} root]"// TODO: error-prone, find a better way to do it +} + +private fun DRI.withPackageFallbackTo(fallbackPackage: String): DRI { + return if (packageName.isNullOrBlank()) { + copy(packageName = fallbackPackage) + } else { + this + } +} diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultDocumentableToPageTranslator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultDocumentableToPageTranslator.kt new file mode 100644 index 00000000..04251947 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultDocumentableToPageTranslator.kt @@ -0,0 +1,17 @@ +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.pages.ModulePageNode +import org.jetbrains.dokka.transformers.documentation.DocumentableToPageTranslator +import org.jetbrains.dokka.utilities.DokkaLogger + +class DefaultDocumentableToPageTranslator( + private val commentsToContentConverter: CommentsToContentConverter, + private val signatureProvider: SignatureProvider, + private val logger: DokkaLogger +) : DocumentableToPageTranslator { + override fun invoke(module: DModule): ModulePageNode = + DefaultPageCreator(commentsToContentConverter, signatureProvider, logger).pageForModule(module) +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt new file mode 100644 index 00000000..02f4b54e --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -0,0 +1,525 @@ +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions +import org.jetbrains.dokka.base.transformers.documentables.InheritorsInfo +import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet + +private typealias GroupedTags = Map<KClass<out TagWrapper>, List<Pair<DokkaSourceSet?, TagWrapper>>> + +private val specialTags: Set<KClass<out TagWrapper>> = + setOf(Property::class, Description::class, Constructor::class, Receiver::class, Param::class, See::class) + +open class DefaultPageCreator( + commentsToContentConverter: CommentsToContentConverter, + signatureProvider: SignatureProvider, + val logger: DokkaLogger +) { + protected open val contentBuilder = PageContentBuilder(commentsToContentConverter, signatureProvider, logger) + + open fun pageForModule(m: DModule) = + ModulePageNode(m.name.ifEmpty { "<root>" }, contentForModule(m), m, m.packages.map(::pageForPackage)) + + open fun pageForPackage(p: DPackage): PackagePageNode = PackagePageNode( + p.name, contentForPackage(p), setOf(p.dri), p, + p.classlikes.map(::pageForClasslike) + + p.functions.map(::pageForFunction) + ) + + open fun pageForEnumEntry(e: DEnumEntry): ClasslikePageNode = + ClasslikePageNode( + e.name, contentForEnumEntry(e), setOf(e.dri), e, + e.classlikes.map(::pageForClasslike) + + e.filteredFunctions.map(::pageForFunction) + ) + + open fun pageForClasslike(c: DClasslike): ClasslikePageNode { + val constructors = if (c is WithConstructors) c.constructors else emptyList() + + return ClasslikePageNode( + c.name.orEmpty(), contentForClasslike(c), setOf(c.dri), c, + constructors.map(::pageForFunction) + + c.classlikes.map(::pageForClasslike) + + c.filteredFunctions.map(::pageForFunction) + + if (c is DEnum) c.entries.map(::pageForEnumEntry) else emptyList() + ) + } + + open fun pageForFunction(f: DFunction) = MemberPageNode(f.name, contentForFunction(f), setOf(f.dri), f) + + open fun pageForTypeAlias(t: DTypeAlias) = MemberPageNode(t.name, contentForTypeAlias(t), setOf(t.dri), t) + + private val WithScope.filteredFunctions: List<DFunction> + get() = functions.mapNotNull { function -> + function.takeIf { + it.sourceSets.any { sourceSet -> it.extra[InheritedFunction]?.isInherited(sourceSet) != true } + } + } + + protected open fun contentForModule(m: DModule) = contentBuilder.contentFor(m) { + group(kind = ContentKind.Cover) { + cover(m.name) + if (contentForDescription(m).isNotEmpty()) { + sourceSetDependentHint( + m.dri, + m.sourceSets.toSet(), + kind = ContentKind.SourceSetDependentHint, + styles = setOf(TextStyle.UnderCoverText) + ) { + +contentForDescription(m) + } + } + } + +contentForComments(m) + block("Packages", 2, ContentKind.Packages, m.packages, m.sourceSets.toSet()) { + link(it.name, it.dri) + } +// text("Index\n") TODO +// text("Link to allpage here") + } + + protected open fun contentForPackage(p: DPackage) = contentBuilder.contentFor(p) { + group(kind = ContentKind.Cover) { + cover("Package ${p.name}") + if (contentForDescription(p).isNotEmpty()) { + sourceSetDependentHint( + p.dri, + p.sourceSets.toSet(), + kind = ContentKind.SourceSetDependentHint, + styles = setOf(TextStyle.UnderCoverText) + ) { + +contentForDescription(p) + } + } + } + group(styles = setOf(ContentStyle.TabbedContent)) { + +contentForComments(p) + +contentForScope(p, p.dri, p.sourceSets) + } + } + + protected open fun contentForScope( + s: WithScope, + dri: DRI, + sourceSets: Set<DokkaSourceSet> + ) = contentBuilder.contentFor(s as Documentable) { + val types = listOf( + s.classlikes, + (s as? DPackage)?.typealiases ?: emptyList() + ).flatten() + divergentBlock("Types", types, ContentKind.Classlikes, extra = mainExtra + SimpleAttr.header("Types")) + divergentBlock( + "Functions", + s.functions, + ContentKind.Functions, + extra = mainExtra + SimpleAttr.header("Functions") + ) + block( + "Properties", + 2, + ContentKind.Properties, + s.properties, + sourceSets.toSet(), + needsAnchors = true, + extra = mainExtra + SimpleAttr.header("Properties") + ) { + link(it.name, it.dri, kind = ContentKind.Main) + sourceSetDependentHint(it.dri, it.sourceSets.toSet(), kind = ContentKind.SourceSetDependentHint) { + contentForBrief(it) + +buildSignature(it) + } + } + s.safeAs<WithExtraProperties<Documentable>>()?.let { it.extra[InheritorsInfo] }?.let { inheritors -> + val map = inheritors.value.filter { it.value.isNotEmpty() } + if (map.values.any()) { + header(2, "Inheritors") { } + +ContentTable( + listOf(contentBuilder.contentFor(mainDRI, mainSourcesetData){ + text("Name") + }), + map.entries.flatMap { entry -> entry.value.map { Pair(entry.key, it) } } + .groupBy({ it.second }, { it.first }).map { (classlike, platforms) -> + buildGroup(setOf(dri), platforms.toSet(), ContentKind.Inheritors) { + link( + classlike.classNames?.substringBeforeLast(".") ?: classlike.toString() + .also { logger.warn("No class name found for DRI $classlike") }, classlike + ) + } + }, + DCI(setOf(dri), ContentKind.Inheritors), + sourceSets.toSet(), + style = emptySet(), + extra = mainExtra + SimpleAttr.header("Inheritors") + ) + } + } + } + + protected open fun contentForEnumEntry(e: DEnumEntry) = contentBuilder.contentFor(e) { + group(kind = ContentKind.Cover) { + cover(e.name) + sourceSetDependentHint(e.dri, e.sourceSets.toSet()) { + +contentForDescription(e) + +buildSignature(e) + } + } + group(styles = setOf(ContentStyle.TabbedContent)) { + +contentForComments(e) + +contentForScope(e, e.dri, e.sourceSets) + } + } + + protected open fun contentForClasslike(c: DClasslike) = contentBuilder.contentFor(c) { + @Suppress("UNCHECKED_CAST") + val extensions = (c as WithExtraProperties<DClasslike>) + .extra[CallableExtensions]?.extensions + ?.filterIsInstance<Documentable>().orEmpty() + // Extensions are added to sourceSets since they can be placed outside the sourceSets from classlike + // Example would be an Interface in common and extension function in jvm + group(kind = ContentKind.Cover, sourceSets = mainSourcesetData + extensions.sourceSets) { + cover(c.name.orEmpty()) + sourceSetDependentHint(c.dri, c.sourceSets) { + +contentForDescription(c) + +buildSignature(c) + } + } + + group(styles = setOf(ContentStyle.TabbedContent), sourceSets = mainSourcesetData + extensions.sourceSets) { + +contentForComments(c) + if (c is WithConstructors) { + block( + "Constructors", + 2, + ContentKind.Constructors, + c.constructors.filter { it.extra[PrimaryConstructorExtra] == null || it.documentation.isNotEmpty() }, + c.sourceSets, + extra = PropertyContainer.empty<ContentNode>() + SimpleAttr.header("Constructors") + ) { + link(it.name, it.dri, kind = ContentKind.Main) + sourceSetDependentHint( + it.dri, + it.sourceSets.toSet(), + kind = ContentKind.SourceSetDependentHint, + styles = emptySet() + ) { + contentForBrief(it) + +buildSignature(it) + } + } + } + if (c is DEnum) { + block( + "Entries", + 2, + ContentKind.Classlikes, + c.entries, + c.sourceSets.toSet(), + needsSorting = false, + extra = mainExtra + SimpleAttr.header("Entries"), + styles = emptySet() + ) { + link(it.name, it.dri) + sourceSetDependentHint(it.dri, it.sourceSets.toSet(), kind = ContentKind.SourceSetDependentHint) { + contentForBrief(it) + +buildSignature(it) + } + } + } + +contentForScope(c, c.dri, c.sourceSets) + + divergentBlock("Extensions", extensions, ContentKind.Extensions, extra = mainExtra + SimpleAttr.header("Extensions")) + } + } + + @Suppress("UNCHECKED_CAST") + private inline fun <reified T : TagWrapper> GroupedTags.withTypeUnnamed(): SourceSetDependent<T> = + (this[T::class] as List<Pair<DokkaSourceSet, T>>?)?.toMap().orEmpty() + + @Suppress("UNCHECKED_CAST") + private inline fun <reified T : NamedTagWrapper> GroupedTags.withTypeNamed(): Map<String, SourceSetDependent<T>> = + (this[T::class] as List<Pair<DokkaSourceSet, T>>?) + ?.groupBy { it.second.name } + ?.mapValues { (_, v) -> v.toMap() } + ?.toSortedMap(String.CASE_INSENSITIVE_ORDER) + .orEmpty() + + private inline fun <reified T : TagWrapper> GroupedTags.isNotEmptyForTag(): Boolean = + this[T::class]?.isNotEmpty() ?: false + + protected open fun contentForDescription( + d: Documentable + ): List<ContentNode> { + val tags: GroupedTags = d.documentation.flatMap { (pd, doc) -> + doc.children.asSequence().map { pd to it }.toList() + }.groupBy { it.second::class } + + val platforms = d.sourceSets.toSet() + + return contentBuilder.contentFor(d, styles = setOf(TextStyle.Block)) { + val description = tags.withTypeUnnamed<Description>() + if (description.any { it.value.root.children.isNotEmpty() }) { + platforms.forEach { platform -> + description[platform]?.also { + group(sourceSets = setOf(platform)) { + comment(it.root) + } + } + } + } + + val unnamedTags: List<SourceSetDependent<TagWrapper>> = + tags.filterNot { (k, _) -> k.isSubclassOf(NamedTagWrapper::class) || k in specialTags } + .map { (_, v) -> v.mapNotNull { (k, v) -> k?.let { it to v } }.toMap() } + if (unnamedTags.isNotEmpty()) { + platforms.forEach { platform -> + unnamedTags.forEach { pdTag -> + pdTag[platform]?.also { tag -> + group(sourceSets = setOf(platform)) { + header(4, tag.toHeaderString()) + comment(tag.root) + } + } + } + } + } + + contentForSinceKotlin(d) + }.children + } + + private fun Documentable.getPossibleFallbackSourcesets(sourceSet: DokkaSourceSet) = + this.sourceSets.filter { it.sourceSetID in sourceSet.dependentSourceSets } + + private fun <V> Map<DokkaSourceSet, V>.fallback(sourceSets: List<DokkaSourceSet>): V? = + sourceSets.firstOrNull { it in this.keys }.let { this[it] } + + protected open fun contentForComments( + d: Documentable + ): List<ContentNode> { + val tags: GroupedTags = d.documentation.flatMap { (pd, doc) -> + doc.children.asSequence().map { pd to it }.toList() + }.groupBy { it.second::class } + + val platforms = d.sourceSets + + fun DocumentableContentBuilder.contentForParams() { + if (tags.isNotEmptyForTag<Param>()) { + header(2, "Parameters", kind = ContentKind.Parameters) + group( + extra = mainExtra + SimpleAttr.header("Parameters"), + styles = setOf(ContentStyle.WithExtraAttributes) + ) { + sourceSetDependentHint(sourceSets = platforms.toSet(), kind = ContentKind.SourceSetDependentHint) { + val receiver = tags.withTypeUnnamed<Receiver>() + val params = tags.withTypeNamed<Param>() + table(kind = ContentKind.Parameters) { + platforms.flatMap { platform -> + val possibleFallbacks = d.getPossibleFallbackSourcesets(platform) + val receiverRow = (receiver[platform] ?: receiver.fallback(possibleFallbacks))?.let { + buildGroup(sourceSets = setOf(platform), kind = ContentKind.Parameters) { + text("<receiver>", styles = mainStyles + ContentStyle.RowTitle) + comment(it.root) + } + } + + val paramRows = params.mapNotNull { (_, param) -> + (param[platform] ?: param.fallback(possibleFallbacks))?.let { + buildGroup(sourceSets = setOf(platform), kind = ContentKind.Parameters) { + text( + it.name, + kind = ContentKind.Parameters, + styles = mainStyles + ContentStyle.RowTitle + ) + comment(it.root) + } + } + } + + listOfNotNull(receiverRow) + paramRows + } + } + } + } + } + } + + fun DocumentableContentBuilder.contentForSeeAlso() { + if (tags.isNotEmptyForTag<See>()) { + header(2, "See also", kind = ContentKind.Comment) + group( + extra = mainExtra + SimpleAttr.header("See also"), + styles = setOf(ContentStyle.WithExtraAttributes) + ) { + sourceSetDependentHint(sourceSets = platforms.toSet(), kind = ContentKind.SourceSetDependentHint) { + val seeAlsoTags = tags.withTypeNamed<See>() + table(kind = ContentKind.Sample) { + platforms.flatMap { platform -> + val possibleFallbacks = d.getPossibleFallbackSourcesets(platform) + seeAlsoTags.mapNotNull { (_, see) -> + (see[platform] ?: see.fallback(possibleFallbacks))?.let { + buildGroup( + sourceSets = setOf(platform), + kind = ContentKind.Comment, + styles = mainStyles + ContentStyle.RowTitle + ) { + if (it.address != null) link( + it.name, + it.address!!, + kind = ContentKind.Comment + ) + else text(it.name, kind = ContentKind.Comment) + comment(it.root) + } + } + } + } + } + } + } + } + } + + fun DocumentableContentBuilder.contentForSamples() { + val samples = tags.withTypeNamed<Sample>() + if (samples.isNotEmpty()) { + header(2, "Samples", kind = ContentKind.Sample) + group( + extra = mainExtra + SimpleAttr.header("Samples"), + styles = emptySet() + ) { + sourceSetDependentHint(sourceSets = platforms.toSet(), kind = ContentKind.SourceSetDependentHint) { + platforms.map { platformData -> + val content = samples.filter { it.value.isEmpty() || platformData in it.value } + group( + sourceSets = setOf(platformData), + kind = ContentKind.Sample, + styles = setOf(TextStyle.Monospace, ContentStyle.RunnableSample) + ) { + content.forEach { + text(it.key) + } + } + } + } + } + } + } + + return contentBuilder.contentFor(d) { + if (tags.isNotEmpty()) { + contentForSamples() + contentForSeeAlso() + contentForParams() + } + }.children + } + + protected open fun DocumentableContentBuilder.contentForBrief(documentable: Documentable) { + documentable.sourceSets.forEach { sourceSet -> + documentable.documentation[sourceSet]?.children?.firstOrNull()?.root?.let { + group(sourceSets = setOf(sourceSet), kind = ContentKind.BriefComment) { + comment(it) + } + } + } + } + + protected open fun DocumentableContentBuilder.contentForSinceKotlin(documentable: Documentable) { + documentable.documentation.mapValues { + it.value.children.find { it is CustomTagWrapper && it.name == "Since Kotlin" } as CustomTagWrapper? + }.run { + documentable.sourceSets.forEach { sourceSet -> + this[sourceSet]?.also { tag -> + group(sourceSets = setOf(sourceSet), kind = ContentKind.Comment, styles = setOf(TextStyle.Block)) { + header(4, tag.name) + comment(tag.root) + } + } + } + } + } + + protected open fun contentForFunction(f: DFunction) = contentForMember(f) + protected open fun contentForTypeAlias(t: DTypeAlias) = contentForMember(t) + protected open fun contentForMember(d: Documentable) = contentBuilder.contentFor(d) { + group(kind = ContentKind.Cover) { + cover(d.name.orEmpty()) + } + divergentGroup(ContentDivergentGroup.GroupID("member")) { + instance(setOf(d.dri), d.sourceSets.toSet()) { + before { + +contentForDescription(d) + +contentForComments(d) + } + divergent(kind = ContentKind.Symbol) { + +buildSignature(d) + } + } + } + } + + protected open fun DocumentableContentBuilder.divergentBlock( + name: String, + collection: Collection<Documentable>, + kind: ContentKind, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + if (collection.any()) { + header(2, name, kind = kind) + table(kind, extra = extra, styles = emptySet()) { + collection + .groupBy { it.name } + // This hacks displaying actual typealias signatures along classlike ones + .mapValues { if (it.value.any { it is DClasslike }) it.value.filter { it !is DTypeAlias } else it.value } + .toSortedMap(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)){it}) + .map { (elementName, elements) -> // This groupBy should probably use LocationProvider + buildGroup( + dri = elements.map { it.dri }.toSet(), + sourceSets = elements.flatMap { it.sourceSets }.toSet(), + kind = kind, + styles = emptySet() + ) { + link(elementName.orEmpty(), elements.first().dri, kind = kind) + divergentGroup( + ContentDivergentGroup.GroupID(name), + elements.map { it.dri }.toSet(), + kind = kind + ) { + elements.map { + instance(setOf(it.dri), it.sourceSets.toSet()) { + before { + contentForBrief(it) + contentForSinceKotlin(it) + } + divergent { + group { + +buildSignature(it) + } + } + } + } + } + } + } + } + } + } + + + protected open fun TagWrapper.toHeaderString() = this.javaClass.toGenericString().split('.').last() + + private val List<Documentable>.sourceSets: Set<DokkaSourceSet> + get() = flatMap { it.sourceSets }.toSet() +} diff --git a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt new file mode 100644 index 00000000..b7927076 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt @@ -0,0 +1,474 @@ +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint +import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.doc.DocTag +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.utilities.DokkaLogger + +@DslMarker +annotation class ContentBuilderMarker + +open class PageContentBuilder( + val commentsConverter: CommentsToContentConverter, + val signatureProvider: SignatureProvider, + val logger: DokkaLogger +) { + fun contentFor( + dri: DRI, + sourceSets: Set<DokkaSourceSet>, + kind: Kind = ContentKind.Main, + styles: Set<Style> = emptySet(), + extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), + block: DocumentableContentBuilder.() -> Unit + ): ContentGroup = + DocumentableContentBuilder(setOf(dri), sourceSets, styles, extra) + .apply(block) + .build(sourceSets, kind, styles, extra) + + fun contentFor( + dri: Set<DRI>, + sourceSets: Set<DokkaSourceSet>, + kind: Kind = ContentKind.Main, + styles: Set<Style> = emptySet(), + extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), + block: DocumentableContentBuilder.() -> Unit + ): ContentGroup = + DocumentableContentBuilder(dri, sourceSets, styles, extra) + .apply(block) + .build(sourceSets, kind, styles, extra) + + fun contentFor( + d: Documentable, + kind: Kind = ContentKind.Main, + styles: Set<Style> = emptySet(), + extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), + sourceSets: Set<DokkaSourceSet> = d.sourceSets.toSet(), + block: DocumentableContentBuilder.() -> Unit = {} + ): ContentGroup = + DocumentableContentBuilder(setOf(d.dri), sourceSets, styles, extra) + .apply(block) + .build(sourceSets, kind, styles, extra) + + @ContentBuilderMarker + open inner class DocumentableContentBuilder( + val mainDRI: Set<DRI>, + val mainSourcesetData: Set<DokkaSourceSet>, + val mainStyles: Set<Style>, + val mainExtra: PropertyContainer<ContentNode> + ) { + protected val contents = mutableListOf<ContentNode>() + + fun build( + sourceSets: Set<DokkaSourceSet>, + kind: Kind, + styles: Set<Style>, + extra: PropertyContainer<ContentNode> + ) = ContentGroup( + contents.toList(), + DCI(mainDRI, kind), + sourceSets, + styles, + extra + ) + + operator fun ContentNode.unaryPlus() { + contents += this + } + + operator fun Collection<ContentNode>.unaryPlus() { + contents += this + } + + private val defaultHeaders + get() = listOf( + contentFor(mainDRI, mainSourcesetData){ + text("Name") + }, + contentFor(mainDRI, mainSourcesetData){ + text("Summary") + } + ) + + fun header( + level: Int, + text: String, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit = {} + ) { + contents += ContentHeader( + level, + contentFor( + mainDRI, + sourceSets, + kind, + styles, + extra + SimpleAttr("anchor", text.replace("\\s".toRegex(), "").toLowerCase()) + ) { + text(text, kind = kind) + block() + } + ) + } + + fun cover( + text: String, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles + TextStyle.Cover, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit = {} + ) { + header(1, text, sourceSets = sourceSets, styles = styles, extra = extra, block = block) + } + + fun text( + text: String, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + contents += createText(text, kind, sourceSets, styles, extra) + } + + fun buildSignature(d: Documentable) = signatureProvider.signature(d) + + fun table( + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + operation: DocumentableContentBuilder.() -> List<ContentGroup> + ) { + contents += ContentTable( + defaultHeaders, + operation(), + DCI(mainDRI, kind), + sourceSets, styles, extra + ) + } + + fun <T : Documentable> block( + name: String, + level: Int, + kind: Kind = ContentKind.Main, + elements: Iterable<T>, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + renderWhenEmpty: Boolean = false, + needsSorting: Boolean = true, + headers: List<ContentGroup>? = null, + needsAnchors: Boolean = false, + operation: DocumentableContentBuilder.(T) -> Unit + ) { + if (renderWhenEmpty || elements.any()) { + header(level, name, kind = kind) { } + contents += ContentTable( + headers ?: defaultHeaders, + elements + .let { + if (needsSorting) + it.sortedWith(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)) { it.name }) + else it + } + .map { + val newExtra = if (needsAnchors) extra + SymbolAnchorHint else extra + buildGroup(setOf(it.dri), it.sourceSets.toSet(), kind, styles, newExtra) { + operation(it) + } + }, + DCI(mainDRI, kind), + sourceSets, styles, extra + ) + } + } + + fun <T> list( + elements: List<T>, + prefix: String = "", + suffix: String = "", + separator: String = ", ", + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, // TODO: children should be aware of this platform data + operation: DocumentableContentBuilder.(T) -> Unit + ) { + if (elements.isNotEmpty()) { + if (prefix.isNotEmpty()) text(prefix, sourceSets = sourceSets) + elements.dropLast(1).forEach { + operation(it) + text(separator, sourceSets = sourceSets) + } + operation(elements.last()) + if (suffix.isNotEmpty()) text(suffix, sourceSets = sourceSets) + } + } + + fun link( + text: String, + address: DRI, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + contents += linkNode(text, address, kind, sourceSets, styles, extra) + } + + fun linkNode( + text: String, + address: DRI, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) = ContentDRILink( + listOf(createText(text, kind, sourceSets, styles, extra)), + address, + DCI(mainDRI, kind), + sourceSets + ) + + fun link( + text: String, + address: String, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + contents += ContentResolvedLink( + children = listOf(createText(text, kind, sourceSets, styles, extra)), + address = address, + extra = PropertyContainer.empty(), + dci = DCI(mainDRI, kind), + sourceSets = sourceSets, + style = emptySet() + ) + } + + fun link( + address: DRI, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += ContentDRILink( + contentFor(mainDRI, sourceSets, kind, styles, extra, block).children, + address, + DCI(mainDRI, kind), + sourceSets + ) + } + + fun comment( + docTag: DocTag, + kind: Kind = ContentKind.Comment, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + val content = commentsConverter.buildContent( + docTag, + DCI(mainDRI, kind), + sourceSets + ) + contents += ContentGroup(content, DCI(mainDRI, kind), sourceSets, styles, extra) + } + + fun group( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += buildGroup(dri, sourceSets, kind, styles, extra, block) + } + + fun divergentGroup( + groupID: ContentDivergentGroup.GroupID, + dri: Set<DRI> = mainDRI, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + implicitlySourceSetHinted: Boolean = true, + block: DivergentBuilder.() -> Unit + ) { + contents += + DivergentBuilder(dri, kind, styles, extra) + .apply(block) + .build(groupID = groupID, implicitlySourceSetHinted = implicitlySourceSetHinted) + } + + fun buildGroup( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ): ContentGroup = contentFor(dri, sourceSets, kind, styles, extra, block) + + fun sourceSetDependentHint( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += PlatformHintedContent( + buildGroup(dri, sourceSets, kind, styles, extra, block), + sourceSets + ) + } + + fun sourceSetDependentHint( + dri: DRI, + sourcesetData: Set<DokkaSourceSet> = mainSourcesetData, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += PlatformHintedContent( + buildGroup(setOf(dri), sourcesetData, kind, styles, extra, block), + sourcesetData + ) + } + + protected fun createText( + text: String, + kind: Kind, + sourceSets: Set<DokkaSourceSet>, + styles: Set<Style>, + extra: PropertyContainer<ContentNode> + ) = + ContentText(text, DCI(mainDRI, kind), sourceSets, styles, extra) + + fun <T> sourceSetDependentText( + value: SourceSetDependent<T>, + sourceSets: Set<DokkaSourceSet> = value.keys, + transform: (T) -> String + ) = value.entries.filter { it.key in sourceSets }.mapNotNull { (p, v) -> + transform(v).takeIf { it.isNotBlank() }?.let { it to p } + }.groupBy({ it.first }) { it.second }.forEach { + text(it.key, sourceSets = it.value.toSet()) + } + } + + @ContentBuilderMarker + open inner class DivergentBuilder( + private val mainDRI: Set<DRI>, + private val mainKind: Kind, + private val mainStyles: Set<Style>, + private val mainExtra: PropertyContainer<ContentNode> + ) { + private val instances: MutableList<ContentDivergentInstance> = mutableListOf() + fun instance( + dri: Set<DRI>, + sourceSets: Set<DokkaSourceSet>, // Having correct sourcesetData is crucial here, that's why there's no default + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DivergentInstanceBuilder.() -> Unit + ) { + instances += DivergentInstanceBuilder(dri, sourceSets, styles, extra) + .apply(block) + .build(kind) + } + + fun build( + groupID: ContentDivergentGroup.GroupID, + implicitlySourceSetHinted: Boolean, + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) = ContentDivergentGroup( + instances.toList(), + DCI(mainDRI, kind), + styles, + extra, + groupID, + implicitlySourceSetHinted + ) + } + + @ContentBuilderMarker + open inner class DivergentInstanceBuilder( + private val mainDRI: Set<DRI>, + private val mainSourceSets: Set<DokkaSourceSet>, + private val mainStyles: Set<Style>, + private val mainExtra: PropertyContainer<ContentNode> + ) { + private var before: ContentNode? = null + private var divergent: ContentNode? = null + private var after: ContentNode? = null + + fun before( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contentFor(dri, sourceSets, kind, styles, extra, block) + .takeIf { it.hasAnyContent() } + .also { before = it } + } + + fun divergent( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + divergent = contentFor(dri, sourceSets, kind, styles, extra, block) + } + + fun after( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contentFor(dri, sourceSets, kind, styles, extra, block) + .takeIf { it.hasAnyContent() } + .also { after = it } + } + + + fun build( + kind: Kind, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) = + ContentDivergentInstance( + before, + divergent ?: throw IllegalStateException("Divergent block needs divergent part"), + after, + DCI(mainDRI, kind), + sourceSets, + styles, + extra + ) + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt new file mode 100644 index 00000000..6f980383 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -0,0 +1,480 @@ +package org.jetbrains.dokka.base.translators.psi + +import com.intellij.lang.jvm.JvmModifier +import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute +import com.intellij.lang.jvm.types.JvmReferenceType +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.* +import com.intellij.psi.impl.source.PsiClassReferenceType +import com.intellij.psi.impl.source.PsiImmediateClassType +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.KotlinAnalysis +import org.jetbrains.dokka.analysis.PsiDocumentableSource +import org.jetbrains.dokka.analysis.from +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.DriWithKind +import org.jetbrains.dokka.links.nextTarget +import org.jetbrains.dokka.links.withClass +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.DocumentationLink +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.model.properties.PropertyContainer +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.descriptors.Visibilities +import org.jetbrains.kotlin.load.java.JvmAbi +import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName +import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.io.File + +class DefaultPsiToDocumentableTranslator( + private val kotlinAnalysis: KotlinAnalysis +) : SourceToDocumentableTranslator { + + override fun invoke(sourceSet: DokkaSourceSet, context: DokkaContext): DModule { + + fun isFileInSourceRoots(file: File) : Boolean { + return sourceSet.sourceRoots.any { root -> file.path.startsWith(File(root.path).absolutePath) } + } + + val (environment, _) = kotlinAnalysis[sourceSet] + + val sourceRoots = environment.configuration.get(CLIConfigurationKeys.CONTENT_ROOTS) + ?.filterIsInstance<JavaSourceRoot>() + ?.mapNotNull { it.file.takeIf(::isFileInSourceRoots) } + ?: listOf() + val localFileSystem = VirtualFileManager.getInstance().getFileSystem("file") + + val psiFiles = sourceRoots.map { sourceRoot -> + sourceRoot.absoluteFile.walkTopDown().mapNotNull { + localFileSystem.findFileByPath(it.path)?.let { vFile -> + PsiManager.getInstance(environment.project).findFile(vFile) as? PsiJavaFile + } + }.toList() + }.flatten() + + val docParser = + DokkaPsiParser( + sourceSet, + context.logger + ) + return DModule( + sourceSet.moduleDisplayName, + psiFiles.mapNotNull { it.safeAs<PsiJavaFile>() }.groupBy { it.packageName }.map { (packageName, psiFiles) -> + val dri = DRI(packageName = packageName) + DPackage( + dri, + emptyList(), + emptyList(), + psiFiles.flatMap { psiFile -> + psiFile.classes.map { docParser.parseClasslike(it, dri) } + }, + emptyList(), + emptyMap(), + null, + setOf(sourceSet) + ) + }, + emptyMap(), + null, + setOf(sourceSet) + ) + } + + class DokkaPsiParser( + private val sourceSetData: DokkaSourceSet, + private val logger: DokkaLogger + ) { + + private val javadocParser: JavaDocumentationParser = JavadocParser(logger) + + private val cachedBounds = hashMapOf<String, Bound>() + + private fun PsiModifierListOwner.getVisibility() = modifierList?.children?.toList()?.let { ml -> + when { + ml.any { it.text == PsiKeyword.PUBLIC } -> JavaVisibility.Public + ml.any { it.text == PsiKeyword.PROTECTED } -> JavaVisibility.Protected + ml.any { it.text == PsiKeyword.PRIVATE } -> JavaVisibility.Private + else -> JavaVisibility.Default + } + } ?: JavaVisibility.Default + + private val PsiMethod.hash: Int + get() = "$returnType $name$parameterList".hashCode() + + private val PsiClassType.shouldBeIgnored: Boolean + get() = isClass("java.lang.Enum") || isClass("java.lang.Object") + + private fun PsiClassType.isClass(qName: String): Boolean { + val shortName = qName.substringAfterLast('.') + if (className == shortName) { + val psiClass = resolve() + return psiClass?.qualifiedName == qName + } + return false + } + + private fun <T> T.toSourceSetDependent() = mapOf(sourceSetData to this) + + fun parseClasslike(psi: PsiClass, parent: DRI): DClasslike = with(psi) { + val dri = parent.withClass(name.toString()) + val inheritanceTree = mutableListOf<AncestorLevel>() + val superMethodsKeys = hashSetOf<Int>() + val superMethods = mutableListOf<Pair<PsiMethod, DRI>>() + methods.forEach { superMethodsKeys.add(it.hash) } + fun parseSupertypes(superTypes: Array<PsiClassType>, level: Int = 0) { + if(superTypes.isEmpty()) return + val parsedClasses = superTypes.filter { !it.shouldBeIgnored }.mapNotNull { + it.resolve()?.let { + when { + it.isInterface -> DRI.from(it) to JavaClassKindTypes.INTERFACE + else -> DRI.from(it) to JavaClassKindTypes.CLASS + } + } + } + val (classes, interfaces) = parsedClasses.partition { it.second == JavaClassKindTypes.CLASS } + inheritanceTree.add(AncestorLevel(level, classes.firstOrNull()?.first, interfaces.map { it.first })) + + superTypes.forEach { type -> + (type as? PsiClassType)?.takeUnless { type.shouldBeIgnored }?.resolve()?.let { + val definedAt = DRI.from(it) + it.methods.forEach { method -> + val hash = method.hash + if (!method.isConstructor && !superMethodsKeys.contains(hash) && + method.getVisibility() != Visibilities.PRIVATE + ) { + superMethodsKeys.add(hash) + superMethods.add(Pair(method, definedAt)) + } + } + parseSupertypes(it.superTypes, level + 1) + } + } + } + parseSupertypes(superTypes) + val (regularFunctions, accessors) = splitFunctionsAndAccessors() + val documentation = javadocParser.parseDocumentation(this).toSourceSetDependent() + val allFunctions = regularFunctions.mapNotNull { if (!it.isConstructor) parseFunction(it) else null } + + superMethods.map { parseFunction(it.first, inheritedFrom = it.second) } + val source = PsiDocumentableSource(this).toSourceSetDependent() + val classlikes = innerClasses.map { parseClasslike(it, dri) } + val visibility = getVisibility().toSourceSetDependent() + val ancestors = inheritanceTree.filter { it.level == 0 }.flatMap { + listOfNotNull(it.superclass?.let { + DriWithKind( + dri = it, + kind = JavaClassKindTypes.CLASS + ) + }) + it.interfaces.map { DriWithKind(dri = it, kind = JavaClassKindTypes.INTERFACE) } + }.toSourceSetDependent() + val modifiers = getModifier().toSourceSetDependent() + val implementedInterfacesExtra = ImplementedInterfaces(inheritanceTree.flatMap { it.interfaces }.distinct().toSourceSetDependent()) + return when { + isAnnotationType -> + DAnnotation( + name.orEmpty(), + dri, + documentation, + null, + source, + allFunctions, + fields.mapNotNull { parseField(it, accessors[it].orEmpty()) }, + classlikes, + visibility, + null, + constructors.map { parseFunction(it, true) }, + mapTypeParameters(dri), + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + isEnum -> DEnum( + dri, + name.orEmpty(), + fields.filterIsInstance<PsiEnumConstant>().map { entry -> + DEnumEntry( + dri.withClass("${entry.name}"), + entry.name, + javadocParser.parseDocumentation(entry).toSourceSetDependent(), + null, + emptyList(), + emptyList(), + emptyList(), + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + }, + documentation, + null, + source, + allFunctions, + fields.filter { it !is PsiEnumConstant }.map { parseField(it, accessors[it].orEmpty()) }, + classlikes, + visibility, + null, + constructors.map { parseFunction(it, true) }, + ancestors, + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + isInterface -> DInterface( + dri, + name.orEmpty(), + documentation, + null, + source, + allFunctions, + fields.mapNotNull { parseField(it, accessors[it].orEmpty()) }, + classlikes, + visibility, + null, + mapTypeParameters(dri), + ancestors, + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + else -> DClass( + dri, + name.orEmpty(), + constructors.map { parseFunction(it, true) }, + allFunctions, + fields.mapNotNull { parseField(it, accessors[it].orEmpty()) }, + classlikes, + source, + visibility, + null, + mapTypeParameters(dri), + ancestors, + documentation, + null, + modifiers, + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + } + } + + private fun parseFunction( + psi: PsiMethod, + isConstructor: Boolean = false, + inheritedFrom: DRI? = null + ): DFunction { + val dri = DRI.from(psi) + val docs = javadocParser.parseDocumentation(psi) + return DFunction( + dri, + if (isConstructor) "<init>" else psi.name, + isConstructor, + psi.parameterList.parameters.map { psiParameter -> + DParameter( + dri.copy(target = dri.target.nextTarget()), + psiParameter.name, + DocumentationNode( + listOfNotNull(docs.firstChildOfTypeOrNull<Param> { + it.firstChildOfTypeOrNull<DocumentationLink>() + ?.firstChildOfTypeOrNull<Text>()?.body == psiParameter.name + })).toSourceSetDependent(), + null, + getBound(psiParameter.type), + setOf(sourceSetData) + ) + }, + docs.toSourceSetDependent(), + null, + PsiDocumentableSource(psi).toSourceSetDependent(), + psi.getVisibility().toSourceSetDependent(), + psi.returnType?.let { getBound(type = it) } ?: Void, + psi.mapTypeParameters(dri), + null, + psi.getModifier().toSourceSetDependent(), + setOf(sourceSetData), + psi.additionalExtras().let { + PropertyContainer.withAll( + InheritedFunction(inheritedFrom.toSourceSetDependent()), + it.toSourceSetDependent().toAdditionalModifiers(), + (psi.annotations.toList().toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent() + .toAnnotations() + ) + } + ) + } + + private fun PsiModifierListOwner.additionalExtras() = listOfNotNull( + ExtraModifiers.JavaOnlyModifiers.Static.takeIf { hasModifier(JvmModifier.STATIC) }, + ExtraModifiers.JavaOnlyModifiers.Native.takeIf { hasModifier(JvmModifier.NATIVE) }, + ExtraModifiers.JavaOnlyModifiers.Synchronized.takeIf { hasModifier(JvmModifier.SYNCHRONIZED) }, + ExtraModifiers.JavaOnlyModifiers.StrictFP.takeIf { hasModifier(JvmModifier.STRICTFP) }, + ExtraModifiers.JavaOnlyModifiers.Transient.takeIf { hasModifier(JvmModifier.TRANSIENT) }, + ExtraModifiers.JavaOnlyModifiers.Volatile.takeIf { hasModifier(JvmModifier.VOLATILE) }, + ExtraModifiers.JavaOnlyModifiers.Transitive.takeIf { hasModifier(JvmModifier.TRANSITIVE) } + ).toSet() + + private fun Set<ExtraModifiers>.toListOfAnnotations() = map { + if (it !is ExtraModifiers.JavaOnlyModifiers.Static) + Annotations.Annotation(DRI("kotlin.jvm", it.name.toLowerCase().capitalize()), emptyMap()) + else + Annotations.Annotation(DRI("kotlin.jvm", "JvmStatic"), emptyMap()) + } + + private fun getBound(type: PsiType): Bound = + cachedBounds.getOrPut(type.canonicalText) { + when (type) { + is PsiClassReferenceType -> { + val resolved: PsiClass = type.resolve() + ?: return UnresolvedBound(type.presentableText) + when { + resolved.qualifiedName == "java.lang.Object" -> JavaObject + resolved is PsiTypeParameter && resolved.owner != null -> + OtherParameter( + declarationDRI = DRI.from(resolved.owner!!), + name = resolved.name.orEmpty() + ) + else -> + TypeConstructor(DRI.from(resolved), type.parameters.map { getProjection(it) }) + } + } + is PsiArrayType -> TypeConstructor( + DRI("kotlin", "Array"), + listOf(getProjection(type.componentType)) + ) + is PsiPrimitiveType -> if (type.name == "void") Void else PrimitiveJavaType(type.name) + is PsiImmediateClassType -> JavaObject + else -> throw IllegalStateException("${type.presentableText} is not supported by PSI parser") + } + } + + private fun getVariance(type: PsiWildcardType): Projection = when { + type.extendsBound != PsiType.NULL -> Variance(Variance.Kind.Out, getBound(type.extendsBound)) + type.superBound != PsiType.NULL -> Variance(Variance.Kind.In, getBound(type.superBound)) + else -> throw IllegalStateException("${type.presentableText} has incorrect bounds") + } + + private fun getProjection(type: PsiType): Projection = when (type) { + is PsiEllipsisType -> Star + is PsiWildcardType -> getVariance(type) + else -> getBound(type) + } + + private fun PsiModifierListOwner.getModifier() = when { + hasModifier(JvmModifier.ABSTRACT) -> JavaModifier.Abstract + hasModifier(JvmModifier.FINAL) -> JavaModifier.Final + else -> JavaModifier.Empty + } + + private fun PsiTypeParameterListOwner.mapTypeParameters(dri: DRI): List<DTypeParameter> { + fun mapBounds(bounds: Array<JvmReferenceType>): List<Bound> = + if (bounds.isEmpty()) emptyList() else bounds.mapNotNull { + (it as? PsiClassType)?.let { classType -> Nullable(getBound(classType)) } + } + return typeParameters.map { type -> + DTypeParameter( + dri.copy(target = dri.target.nextTarget()), + type.name.orEmpty(), + javadocParser.parseDocumentation(type).toSourceSetDependent(), + null, + mapBounds(type.bounds), + setOf(sourceSetData) + ) + } + } + + private fun PsiMethod.getPropertyNameForFunction() = + getAnnotation(DescriptorUtils.JVM_NAME.asString())?.findAttributeValue("name")?.text + ?: when { + JvmAbi.isGetterName(name) -> propertyNameByGetMethodName(Name.identifier(name))?.asString() + JvmAbi.isSetterName(name) -> propertyNamesBySetMethodName(Name.identifier(name)).firstOrNull() + ?.asString() + else -> null + } + + private fun PsiClass.splitFunctionsAndAccessors(): Pair<MutableList<PsiMethod>, MutableMap<PsiField, MutableList<PsiMethod>>> { + val fieldNames = fields.map { it.name to it }.toMap() + val accessors = mutableMapOf<PsiField, MutableList<PsiMethod>>() + val regularMethods = mutableListOf<PsiMethod>() + methods.forEach { method -> + val field = method.getPropertyNameForFunction()?.let { name -> fieldNames[name] } + if (field != null) { + accessors.getOrPut(field, ::mutableListOf).add(method) + } else { + regularMethods.add(method) + } + } + return regularMethods to accessors + } + + private fun parseField(psi: PsiField, accessors: List<PsiMethod>): DProperty { + val dri = DRI.from(psi) + return DProperty( + dri, + psi.name, + javadocParser.parseDocumentation(psi).toSourceSetDependent(), + null, + PsiDocumentableSource(psi).toSourceSetDependent(), + psi.getVisibility().toSourceSetDependent(), + getBound(psi.type), + null, + accessors.firstOrNull { it.hasParameters() }?.let { parseFunction(it) }, + accessors.firstOrNull { it.returnType == psi.type }?.let { parseFunction(it) }, + psi.getModifier().toSourceSetDependent(), + setOf(sourceSetData), + emptyList(), + psi.additionalExtras().let { + PropertyContainer.withAll<DProperty>( + it.toSourceSetDependent().toAdditionalModifiers(), + (psi.annotations.toList().toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent() + .toAnnotations() + ) + } + ) + } + + private fun Collection<PsiAnnotation>.toListOfAnnotations() = + filter { it !is KtLightAbstractAnnotation }.mapNotNull { it.toAnnotation() } + + private fun JvmAnnotationAttribute.toValue(): AnnotationParameterValue = when (this) { + is PsiNameValuePair -> value?.toValue() ?: StringValue("") + else -> StringValue(this.attributeName) + } + + private fun PsiAnnotationMemberValue.toValue(): AnnotationParameterValue? = when (this) { + is PsiAnnotation -> toAnnotation()?.let { AnnotationValue(it) } + is PsiArrayInitializerMemberValue -> ArrayValue(initializers.mapNotNull { it.toValue() }) + is PsiReferenceExpression -> psiReference?.let { EnumValue(text ?: "", DRI.from(it)) } + is PsiClassObjectAccessExpression -> { + val psiClass = ((type as PsiImmediateClassType).parameters.single() as PsiClassReferenceType).resolve() + psiClass?.let { ClassValue(text ?: "", DRI.from(psiClass)) } + } + else -> StringValue(text ?: "") + } + + private fun PsiAnnotation.toAnnotation() = psiReference?.let { psiElement -> + Annotations.Annotation( + DRI.from(psiElement), + attributes.filter { it !is KtLightAbstractAnnotation }.mapNotNull { it.attributeName to it.toValue() } + .toMap(), + (psiElement as PsiClass).annotations.any { + it.hasQualifiedName("java.lang.annotation.Documented") + } + ) + } + + private val PsiElement.psiReference + get() = getChildOfType<PsiJavaCodeReferenceElement>()?.resolve() + } + + private data class AncestorLevel(val level: Int, val superclass: DRI?, val interfaces: List<DRI>) +} diff --git a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt new file mode 100644 index 00000000..81955fde --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt @@ -0,0 +1,216 @@ +package org.jetbrains.dokka.base.translators.psi + +import com.intellij.psi.* +import com.intellij.psi.impl.source.javadoc.PsiDocParamRef +import com.intellij.psi.impl.source.tree.JavaDocElementType +import com.intellij.psi.impl.source.tree.LeafPsiElement +import com.intellij.psi.javadoc.* +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.analysis.from +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.utils.addToStdlib.firstIsInstanceOrNull +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode + +interface JavaDocumentationParser { + fun parseDocumentation(element: PsiNamedElement): DocumentationNode +} + +class JavadocParser( + private val logger: DokkaLogger // TODO: Add logging +) : JavaDocumentationParser { + + override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { + val docComment = findClosestDocComment(element) ?: return DocumentationNode(emptyList()) + val nodes = mutableListOf<TagWrapper>() + 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()))) + else -> null + } + }) + return DocumentationNode(nodes) + } + + private fun findClosestDocComment(element: PsiNamedElement): PsiDocComment? { + (element as? PsiDocCommentOwner)?.docComment?.run { return this } + if (element is PsiMethod) { + val superMethods = element.findSuperMethodsOrEmptyArray() + if (superMethods.isEmpty()) return null + + if (superMethods.size == 1) { + return findClosestDocComment(superMethods.single()) + } + + val superMethodDocumentation = superMethods.map(::findClosestDocComment) + if (superMethodDocumentation.size == 1) { + return superMethodDocumentation.single() + } + + logger.warn( + "Conflicting documentation for ${DRI.from(element)}" + + "${superMethods.map { DRI.from(it) }}" + ) + + /* Prioritize super class over interface */ + val indexOfSuperClass = superMethods.indexOfFirst { method -> + val parent = method.parent + if (parent is PsiClass) !parent.isInterface + else false + } + + return if (indexOfSuperClass >= 0) superMethodDocumentation[indexOfSuperClass] + else superMethodDocumentation.first() + } + + return null + } + + /** + * Workaround for failing [PsiMethod.findSuperMethods]. + * This might be resolved once ultra light classes are enabled for dokka + * See [KT-39518](https://youtrack.jetbrains.com/issue/KT-39518) + */ + private fun PsiMethod.findSuperMethodsOrEmptyArray(): Array<PsiMethod> { + return try { + /* + We are not even attempting to call "findSuperMethods" on all methods called "getGetter" or "getSetter" + on any object implementing "kotlin.reflect.KProperty", since we know that those methods will fail + (KT-39518). Just catching the exception is not good enough, since "findSuperMethods" will + print the whole exception to stderr internally and then spoil the console. + */ + val kPropertyFqName = FqName("kotlin.reflect.KProperty") + if ( + this.parent?.safeAs<PsiClass>()?.implementsInterface(kPropertyFqName) == true && + (this.name == "getSetter" || this.name == "getGetter") + ) { + logger.warn("Skipped lookup of super methods for ${getKotlinFqName()} (KT-39518)") + return emptyArray() + } + findSuperMethods() + } catch (exception: Throwable) { + logger.warn("Failed to lookup of super methods for ${getKotlinFqName()} (KT-39518)") + emptyArray() + } + } + + private fun PsiClass.implementsInterface(fqName: FqName): Boolean { + return allInterfaces().any { it.getKotlinFqName() == fqName } + } + + private fun PsiClass.allInterfaces(): Sequence<PsiClass> { + return sequence { + this.yieldAll(interfaces.toList()) + interfaces.forEach { yieldAll(it.allInterfaces()) } + } + } + + private fun getSeeTagElementContent(tag: PsiDocTag): List<DocTag> = + listOfNotNull(tag.referenceElement()?.toDocumentationLink()) + + private fun PsiDocComment.getDescription(): Description? { + val nonEmptyDescriptionElements = descriptionElements.filter { it.text.trim().isNotEmpty() } + val convertedDescriptionElements = convertJavadocElements(nonEmptyDescriptionElements) + if (convertedDescriptionElements.isNotEmpty()) { + return Description(P(convertedDescriptionElements)) + } + + return null + } + + private fun convertJavadocElements(elements: Iterable<PsiElement>): List<DocTag> = + 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 + } + }.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 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>): 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"))) + } + else -> Text(children = children) + } + } + + private fun PsiDocToken.isSharpToken() = tokenType.toString() == "DOC_TAG_VALUE_SHARP_TOKEN" + + 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<PsiDocToken>()) + } + "code", "literal" -> { + CodeInline(listOf(Text(tag.text))) + } + "index" -> Index(tag.children.filterIsInstance<PsiDocTagValue>().map { Text(it.text) }) + else -> Text(tag.text) + } + + private fun PsiDocTag.referenceElement(): PsiElement? = + linkElement()?.let { + if (it.node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) { + PsiTreeUtil.findChildOfType(it, PsiJavaCodeReferenceElement::class.java) + } else { + it + } + } + + private fun PsiDocTag.linkElement(): PsiElement? = + valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace } +} diff --git a/plugins/base/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/plugins/base/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..bc8de448 --- /dev/null +++ b/plugins/base/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.base.DokkaBase diff --git a/plugins/base/src/main/resources/dokka/format/gfm.properties b/plugins/base/src/main/resources/dokka/format/gfm.properties new file mode 100644 index 00000000..5e8f7aa8 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/format/gfm.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.GFMFormatDescriptor +description=Produces documentation in GitHub-flavored markdown format diff --git a/plugins/base/src/main/resources/dokka/format/html-as-java.properties b/plugins/base/src/main/resources/dokka/format/html-as-java.properties new file mode 100644 index 00000000..f598f377 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/format/html-as-java.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.HtmlAsJavaFormatDescriptor +description=Produces output in HTML format using Java syntax
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/format/html.properties b/plugins/base/src/main/resources/dokka/format/html.properties new file mode 100644 index 00000000..7881dfae --- /dev/null +++ b/plugins/base/src/main/resources/dokka/format/html.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.HtmlFormatDescriptor +description=Produces output in HTML format
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/format/java-layout-html.properties b/plugins/base/src/main/resources/dokka/format/java-layout-html.properties new file mode 100644 index 00000000..fbb2bbed --- /dev/null +++ b/plugins/base/src/main/resources/dokka/format/java-layout-html.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatDescriptor +description=Produces Kotlin Style Docs with Javadoc like layout
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/format/jekyll.properties b/plugins/base/src/main/resources/dokka/format/jekyll.properties new file mode 100644 index 00000000..b11401a4 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/format/jekyll.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.JekyllFormatDescriptor +description=Produces documentation in Jekyll format
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/format/kotlin-website-html.properties b/plugins/base/src/main/resources/dokka/format/kotlin-website-html.properties new file mode 100644 index 00000000..f4c320b9 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/format/kotlin-website-html.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.KotlinWebsiteHtmlFormatDescriptor +description=Generates Kotlin website documentation
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/format/markdown.properties b/plugins/base/src/main/resources/dokka/format/markdown.properties new file mode 100644 index 00000000..6217a6df --- /dev/null +++ b/plugins/base/src/main/resources/dokka/format/markdown.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.MarkdownFormatDescriptor +description=Produces documentation in markdown format
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/images/arrow_down.svg b/plugins/base/src/main/resources/dokka/images/arrow_down.svg new file mode 100755 index 00000000..89e7df47 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/images/arrow_down.svg @@ -0,0 +1,3 @@ +<svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.71824 1.66658L9.01113 0.959473L5.00497 4.96447L1.00008 0.959473L0.292969 1.66658L5.01113 6.38474L9.71824 1.66658Z" fill="#A1AAB4"/> +</svg> diff --git a/plugins/base/src/main/resources/dokka/images/docs_logo.svg b/plugins/base/src/main/resources/dokka/images/docs_logo.svg new file mode 100644 index 00000000..7c1e3ae8 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/images/docs_logo.svg @@ -0,0 +1,7 @@ +<svg width="125" height="27" viewBox="0 0 125 27" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M89.1611 7.6297V25.6345V25.6867H103.843V21.8039H93.3589V10.3852H103.843V6.50244H89.1611V7.6297Z" fill="#27282C"/> +<path d="M124.989 21.8039L114.778 10.3852H124.905V6.50244H109.059V10.3852L119.459 21.8039H109.059V25.6867H125V21.8039H124.989Z" fill="#27282C"/> +<path d="M58.2978 7.76556C56.5872 6.46086 54.4463 5.67804 52.1271 5.67804C46.5336 5.67804 42 10.1871 42 15.7503C42 21.3135 46.5336 25.8226 52.1271 25.8226C54.4463 25.8226 56.5872 25.0502 58.2978 23.735V25.7182H62.4955V0H58.2978V7.76556ZM52.1271 21.8041C48.7584 21.8041 46.0298 19.0903 46.0298 15.7399C46.0298 12.3894 48.7584 9.67563 52.1271 9.67563C55.4958 9.67563 58.2243 12.3894 58.2243 15.7399C58.2138 19.0903 55.4853 21.8041 52.1271 21.8041Z" fill="#27282C"/> +<path d="M75.9698 5.8656C70.3763 5.8656 65.8428 10.3746 65.8428 15.9379C65.8428 21.5011 70.3763 26.0101 75.9698 26.0101C81.5633 26.0101 86.0969 21.5011 86.0969 15.9379C86.0969 10.3746 81.5633 5.8656 75.9698 5.8656ZM75.9698 21.9916C72.6012 21.9916 69.8726 19.2779 69.8726 15.9274C69.8726 12.577 72.6012 9.86319 75.9698 9.86319C79.3385 9.86319 82.0671 12.577 82.0671 15.9274C82.0671 19.2779 79.3385 21.9916 75.9698 21.9916Z" fill="#27282C"/> +<path d="M26 26H0V0H26L12.9243 12.9747L26 26Z" fill="#F8873C"/> +</svg> diff --git a/plugins/base/src/main/resources/dokka/images/logo-icon.svg b/plugins/base/src/main/resources/dokka/images/logo-icon.svg new file mode 100755 index 00000000..1b3b3670 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/images/logo-icon.svg @@ -0,0 +1,3 @@ +<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M26 26H0V0H26L12.9243 12.9747L26 26Z" fill="#F8873C"/> +</svg> diff --git a/plugins/base/src/main/resources/dokka/images/logo-text.svg b/plugins/base/src/main/resources/dokka/images/logo-text.svg new file mode 100755 index 00000000..7bf3e6c5 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/images/logo-text.svg @@ -0,0 +1,6 @@ +<svg width="83" height="27" viewBox="0 0 83 27" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M47.1611 7.6297V25.6345V25.6867H61.8428V21.8039H51.3589V10.3852H61.8428V6.50244H47.1611V7.6297Z" fill="#27282C"/> +<path d="M82.9891 21.8039L72.778 10.3852H82.9051V6.50244H67.0586V10.3852L77.4585 21.8039H67.0586V25.6867H82.9996V21.8039H82.9891Z" fill="#27282C"/> +<path d="M16.2978 7.76556C14.5872 6.46086 12.4463 5.67804 10.1271 5.67804C4.53357 5.67804 0 10.1871 0 15.7503C0 21.3135 4.53357 25.8226 10.1271 25.8226C12.4463 25.8226 14.5872 25.0502 16.2978 23.735V25.7182H20.4955V0H16.2978V7.76556ZM10.1271 21.8041C6.75838 21.8041 4.02984 19.0903 4.02984 15.7399C4.02984 12.3894 6.75838 9.67563 10.1271 9.67563C13.4958 9.67563 16.2243 12.3894 16.2243 15.7399C16.2138 19.0903 13.4853 21.8041 10.1271 21.8041Z" fill="#27282C"/> +<path d="M33.9703 5.86566C28.3768 5.86566 23.8433 10.3747 23.8433 15.9379C23.8433 21.5011 28.3768 26.0102 33.9703 26.0102C39.5638 26.0102 44.0974 21.5011 44.0974 15.9379C44.0974 10.3747 39.5638 5.86566 33.9703 5.86566ZM33.9703 21.9917C30.6016 21.9917 27.8731 19.2779 27.8731 15.9275C27.8731 12.577 30.6016 9.86325 33.9703 9.86325C37.339 9.86325 40.0676 12.577 40.0676 15.9275C40.0676 19.2779 37.339 21.9917 33.9703 21.9917Z" fill="#27282C"/> +</svg> diff --git a/plugins/base/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties b/plugins/base/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties new file mode 100644 index 00000000..c484a920 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.InboundExternalLinkResolutionService$Dokka +description=Uses Dokka Default resolver
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties b/plugins/base/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties new file mode 100644 index 00000000..3b61eabe --- /dev/null +++ b/plugins/base/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.JavaLayoutHtmlInboundLinkResolutionService +description=Resolver for JavaLayoutHtml
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/inbound-link-resolver/javadoc.properties b/plugins/base/src/main/resources/dokka/inbound-link-resolver/javadoc.properties new file mode 100644 index 00000000..0d5d7d17 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/inbound-link-resolver/javadoc.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.InboundExternalLinkResolutionService$Javadoc +description=Uses Javadoc Default resolver
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/scripts/clipboard.js b/plugins/base/src/main/resources/dokka/scripts/clipboard.js new file mode 100644 index 00000000..b00ce246 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/scripts/clipboard.js @@ -0,0 +1,52 @@ +window.addEventListener('load', () => { + document.querySelectorAll('span.copy-icon').forEach(element => { + element.addEventListener('click', (el) => copyElementsContentToClipboard(element)); + }) + + document.querySelectorAll('span.anchor-icon').forEach(element => { + element.addEventListener('click', (el) => { + if(element.hasAttribute('pointing-to')){ + const location = hrefWithoutCurrentlyUsedAnchor() + '#' + element.getAttribute('pointing-to') + copyTextToClipboard(element, location) + } + }); + }) +}) + +const copyElementsContentToClipboard = (element) => { + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(element.parentNode.parentNode); + selection.removeAllRanges(); + selection.addRange(range); + + copyAndShowPopup(element, () => selection.removeAllRanges()) +} + +const copyTextToClipboard = (element, text) => { + var textarea = document.createElement("textarea"); + textarea.textContent = text; + textarea.style.position = "fixed"; + document.body.appendChild(textarea); + textarea.select(); + + copyAndShowPopup(element, () => document.body.removeChild(textarea)) +} + +const copyAndShowPopup = (element, after) => { + try { + document.execCommand('copy'); + element.nextElementSibling.classList.add('active-popup'); + setTimeout(() => { + element.nextElementSibling.classList.remove('active-popup'); + }, 1200); + } catch (e) { + console.error('Failed to write to clipboard:', e) + } + finally { + if(after) after() + } +} + +const hrefWithoutCurrentlyUsedAnchor = () => window.location.href.split('#')[0] + diff --git a/plugins/base/src/main/resources/dokka/scripts/navigationLoader.js b/plugins/base/src/main/resources/dokka/scripts/navigationLoader.js new file mode 100644 index 00000000..c2f60ec5 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/scripts/navigationLoader.js @@ -0,0 +1,54 @@ +window.addEventListener('load', () => { + fetch(pathToRoot + "navigation.html") + .then(response => response.text()) + .then(data => { + document.getElementById("sideMenu").innerHTML = data; + }).then(() => { + document.querySelectorAll(".overview > a").forEach(link => { + link.setAttribute("href", pathToRoot + link.getAttribute("href")); + }) + }).then(() => { + document.querySelectorAll(".sideMenuPart").forEach(nav => { + if (!nav.classList.contains("hidden")) nav.classList.add("hidden") + }) + }).then(() => { + revealNavigationForCurrentPage() + }) + + /* Smooth scrolling support for going to the top of the page */ + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + + document.querySelector(this.getAttribute('href')).scrollIntoView({ + behavior: 'smooth' + }); + }); + }); +}) + +revealNavigationForCurrentPage = () => { + let pageId = document.getElementById("content").attributes["pageIds"].value.toString(); + let parts = document.querySelectorAll(".sideMenuPart"); + let found = 0; + do { + parts.forEach(part => { + if (part.attributes['pageId'].value.indexOf(pageId) !== -1 && found === 0) { + found = 1; + if (part.classList.contains("hidden")){ + part.classList.remove("hidden"); + part.setAttribute('data-active',""); + } + revealParents(part) + } + }); + pageId = pageId.substring(0, pageId.lastIndexOf("/")) + } while (pageId.indexOf("/") !== -1 && found === 0) +}; + +revealParents = (part) => { + if (part.classList.contains("sideMenuPart")) { + if (part.classList.contains("hidden")) part.classList.remove("hidden"); + revealParents(part.parentNode) + } +};
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/scripts/platformContentHandler.js b/plugins/base/src/main/resources/dokka/scripts/platformContentHandler.js new file mode 100644 index 00000000..6f10b08a --- /dev/null +++ b/plugins/base/src/main/resources/dokka/scripts/platformContentHandler.js @@ -0,0 +1,226 @@ +filteringContext = { + dependencies: {}, + restrictedDependencies: [], + activeFilters: [] +} +window.addEventListener('load', () => { + document.querySelectorAll("div[data-platform-hinted]") + .forEach(elem => elem.addEventListener('click', (event) => togglePlatformDependent(event,elem))) + document.querySelectorAll("div[tabs-section]") + .forEach(elem => elem.addEventListener('click', (event) => toggleSectionsEventHandler(event))) + const filterSection = document.getElementById('filter-section') + if (filterSection) { + filterSection.addEventListener('click', (event) => filterButtonHandler(event)) + initializeFiltering() + } + initTabs() + handleAnchor() +}) + +function handleAnchor() { + let searchForTab = function(element) { + if(element && element.hasAttribute) { + if(element.hasAttribute("data-togglable")) return element; + else return searchForTab(element.parentNode) + } else return null + } + let anchor = window.location.hash + if (anchor != "") { + anchor = anchor.substring(1) + let element = document.querySelector('a[data-name="' + anchor+'"]') + if (element) { + let tab = searchForTab(element) + if (tab) { + let found = document.querySelector('.tabs-section > .section-tab[data-togglable="' + tab.getAttribute("data-togglable") + '"]') + toggleSections(tab) + element.scrollIntoView({behavior: "smooth"}) + } + } + } +} + +function initTabs(){ + document.querySelectorAll("div[tabs-section]") + .forEach(element => { + showCorrespondingTabBody(element) + element.addEventListener('click', (event) => toggleSectionsEventHandler(event)) + }) + let cached = localStorage.getItem("active-tab") + if (cached) { + let parsed = JSON.parse(cached) + let tab = document.querySelector('div[tabs-section] > button[data-togglable="' + parsed + '"]') + if(tab) { + toggleSections(tab) + } + } +} + +function showCorrespondingTabBody(element){ + const key = element.querySelector("button[data-active]").getAttribute("data-togglable") + document.querySelector(".tabs-section-body") + .querySelector("div[data-togglable='" + key + "']") + .setAttribute("data-active", "") +} + +function filterButtonHandler(event) { + if(event.target.tagName == "BUTTON" && event.target.hasAttribute("data-filter")) { + let sourceset = event.target.getAttribute("data-filter") + if(filteringContext.activeFilters.indexOf(sourceset) != -1) { + filterSourceset(sourceset) + } else { + unfilterSourceset(sourceset) + } + } +} + +function initializeFiltering() { + filteringContext.dependencies = JSON.parse(sourceset_dependencies) + document.querySelectorAll("#filter-section > button") + .forEach(p => filteringContext.restrictedDependencies.push(p.getAttribute("data-filter"))) + Object.keys(filteringContext.dependencies).forEach(p => { + filteringContext.dependencies[p] = filteringContext.dependencies[p] + .filter(q => -1 !== filteringContext.restrictedDependencies.indexOf(q)) + }) + let cached = window.localStorage.getItem('inactive-filters') + if (cached) { + let parsed = JSON.parse(cached) + filteringContext.activeFilters = filteringContext.restrictedDependencies + .filter(q => parsed.indexOf(q) == -1 ) + } else { + filteringContext.activeFilters = filteringContext.restrictedDependencies + } + refreshFiltering() +} + +function filterSourceset(sourceset) { + filteringContext.activeFilters = filteringContext.activeFilters.filter(p => p != sourceset) + refreshFiltering() + addSourcesetFilterToCache(sourceset) +} + +function unfilterSourceset(sourceset) { + if(filteringContext.activeFilters.length == 0) { + filteringContext.activeFilters = filteringContext.dependencies[sourceset].concat([sourceset]) + refreshFiltering() + filteringContext.dependencies[sourceset].concat([sourceset]).forEach(p => removeSourcesetFilterFromCache(p)) + } else { + filteringContext.activeFilters.push(sourceset) + refreshFiltering() + removeSourcesetFilterFromCache(sourceset) + } + +} + +function addSourcesetFilterToCache(sourceset) { + let cached = localStorage.getItem('inactive-filters') + if (cached) { + let parsed = JSON.parse(cached) + localStorage.setItem('inactive-filters', JSON.stringify(parsed.concat([sourceset]))) + } else { + localStorage.setItem('inactive-filters', JSON.stringify([sourceset])) + } +} + +function removeSourcesetFilterFromCache(sourceset) { + let cached = localStorage.getItem('inactive-filters') + if (cached) { + let parsed = JSON.parse(cached) + localStorage.setItem('inactive-filters', JSON.stringify(parsed.filter(p => p != sourceset))) + } +} + +function toggleSections(target) { + localStorage.setItem('active-tab', JSON.stringify(target.getAttribute("data-togglable"))) + const activateTabs = (containerClass) => { + for(const element of document.getElementsByClassName(containerClass)){ + for(const child of element.children){ + if(child.getAttribute("data-togglable") === target.getAttribute("data-togglable")){ + child.setAttribute("data-active", "") + } else { + child.removeAttribute("data-active") + } + } + } + } + + activateTabs("tabs-section") + activateTabs("tabs-section-body") +} + +function toggleSectionsEventHandler(evt){ + if(!evt.target.getAttribute("data-togglable")) return + toggleSections(evt.target) +} + +function togglePlatformDependent(e, container) { + let target = e.target + if (target.tagName != 'BUTTON') return; + let index = target.getAttribute('data-toggle') + + for(let child of container.children){ + if(child.hasAttribute('data-toggle-list')){ + for(let bm of child.children){ + if(bm == target){ + bm.setAttribute('data-active',"") + } else if(bm != target) { + bm.removeAttribute('data-active') + } + } + } + else if(child.getAttribute('data-togglable') == index) { + child.setAttribute('data-active',"") + } + else { + child.removeAttribute('data-active') + } + } +} + +function refreshFiltering() { + let sourcesetList = filteringContext.activeFilters + document.querySelectorAll("[data-filterable-set]") + .forEach( + elem => { + let platformList = elem.getAttribute("data-filterable-set").split(' ').filter(v => -1 !== sourcesetList.indexOf(v)) + elem.setAttribute("data-filterable-current", platformList.join(' ')) + } + ) + refreshFilterButtons() + refreshPlatformTabs() +} + +function refreshPlatformTabs() { + document.querySelectorAll(".platform-hinted > .platform-bookmarks-row").forEach( + p => { + let active = false; + let firstAvailable = null + p.childNodes.forEach( + element => { + if(element.getAttribute("data-filterable-current") != ''){ + if( firstAvailable == null) { + firstAvailable = element + } + if(element.hasAttribute("data-active")) { + active = true; + } + } + } + ) + if( active == false && firstAvailable) { + firstAvailable.click() + } + } + ) +} + +function refreshFilterButtons() { + document.querySelectorAll("#filter-section > button") + .forEach(f => { + if(filteringContext.activeFilters.indexOf(f.getAttribute("data-filter")) != -1){ + f.setAttribute("data-active","") + } else { + f.removeAttribute("data-active") + } + }) +} + diff --git a/plugins/base/src/main/resources/dokka/scripts/scripts.js b/plugins/base/src/main/resources/dokka/scripts/scripts.js new file mode 100644 index 00000000..c2e29b9f --- /dev/null +++ b/plugins/base/src/main/resources/dokka/scripts/scripts.js @@ -0,0 +1,11 @@ +document.getElementById("navigationFilter").oninput = function (e) { + var input = e.target.value; + var menuParts = document.getElementsByClassName("sideMenuPart") + for (let part of menuParts) { + if(part.querySelector("a").textContent.startsWith(input)) { + part.classList.remove("filtered"); + } else { + part.classList.add("filtered"); + } + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/scripts/search.js b/plugins/base/src/main/resources/dokka/scripts/search.js new file mode 100644 index 00000000..04d88ab5 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/scripts/search.js @@ -0,0 +1,7 @@ +let query = new URLSearchParams(window.location.search).get("query"); +document.getElementById("searchTitle").innerHTML += '"' + query + '":'; +document.getElementById("searchTable").innerHTML = pages + .filter(el => el.name.toLowerCase().startsWith(query.toLowerCase())) + .reduce((acc, element) => { + return acc + '<tr><td><a href="' + element.location + '">' + element.name + '</a></td></tr>' + }, "");
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/styles/jetbrains-mono.css b/plugins/base/src/main/resources/dokka/styles/jetbrains-mono.css new file mode 100644 index 00000000..2af32a92 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/styles/jetbrains-mono.css @@ -0,0 +1,13 @@ +@font-face{ + font-family: 'JetBrains Mono'; + src: url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/web/woff2/JetBrainsMono-Regular.woff2') format('woff2'); + font-weight: normal; + font-style: normal; +} + +@font-face{ + font-family: 'JetBrains Mono'; + src: url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/web/woff2/JetBrainsMono-Bold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; +}
\ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/styles/style.css b/plugins/base/src/main/resources/dokka/styles/style.css new file mode 100644 index 00000000..57ffb8a5 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/styles/style.css @@ -0,0 +1,1048 @@ +@import url(https://fonts.googleapis.com/css?family=Open+Sans:300i,400,700); +@import url('https://rsms.me/inter/inter.css'); +@import url('jetbrains-mono.css'); + +:root { + --breadcrumb-font-color: #A6AFBA; + --hover-link-color: #5B5DEF; + --footer-height: 64px; + --footer-padding-top: 48px; + --horizontal-spacing-for-content: 42px; +} + +#content { + padding: 0 var(--horizontal-spacing-for-content); + height: calc(100% - var(--footer-height) - var(--footer-padding-top)); +} + +.breadcrumbs { + padding: 24px 0; + color: var(--breadcrumb-font-color); +} + +.breadcrumbs a { + color: var(--breadcrumb-font-color) +} + +.breadcrumbs a:hover { + color: var(--hover-link-color) +} + +.tabs-section > .section-tab:first-child { + margin-left: 0; +} + +.section-tab { + border: 0; + cursor: pointer; + background-color: transparent; + border-bottom: 1px solid #DADFE6; + padding: 11px 3px; + font-size: 14px; + color: #637282; + outline: none; + margin: 0 8px; +} + +.section-tab:hover { + color: #282E34; + border-bottom: 2px solid var(--hover-link-color); +} + +.section-tab[data-active=''] { + color: #282E34; + border-bottom: 2px solid var(--hover-link-color); +} + +.tabs-section-body { + margin: 12px 0; + background-color: white; +} + +.tabs-section-body > .table { + margin: 12px 0; +} + +.tabs-section-body .with-platform-tabs > div { + margin: 0 12px; +} + +.tabs-section-body .table .with-platform-tabs > div { + margin: 0; +} + +.tabs-section-body .with-platform-tabs { + padding-top: 12px; + padding-bottom: 12px; +} + +.tabs-section-body .with-platform-tabs .sourceset-depenent-content .table-row { + background-color: #f4f4f4; + border-bottom: 2px solid white; +} + +.cover > .platform-hinted { + padding-top: 24px; + margin-top: 24px; + padding-bottom: 16px; +} + +.cover { + display: flex; + flex-direction: column; + width: 100%; + padding-bottom: 48px; +} + +.tabbedcontent { + padding: 14px 0; +} + +.cover .platform-hinted .sourceset-depenent-content > .symbol, +.cover > .symbol { + background-color: white; +} + +.cover .platform-hinted.with-platform-tabs .sourceset-depenent-content > .symbol { + background-color: #f4f4f4; +} + +.cover .platform-hinted.with-platform-tabs .sourceset-depenent-content > .block ~ .symbol { + padding-top: 16px; + padding-left: 0; +} + +.cover .sourceset-depenent-content > .block { + padding: 16px 0; + font-size: 18px; + line-height: 28px; +} + +.cover .platform-hinted.with-platform-tabs .sourceset-depenent-content > .block { + padding: 0; + font-size: 14px; +} + +.cover ~ .divergent-group { + margin-top: 24px; + padding: 24px 8px 8px 8px; +} + +.cover ~ .divergent-group .main-subrow .symbol { + width: 100%; +} + +.divergent-group { + background-color: white; + padding: 16px 8px; + margin-bottom: 2px; +} + +.divergent-group .table-row { + background-color: #F4F4F4; + border-bottom: 2px solid white; +} + +.title > .divergent-group:first-of-type { + padding-top: 0; +} + +#container { + display: flex; + flex-direction: row; + min-height: 100%; +} + +#main { + width: 100%; + max-width: calc(100% - 280px); +} + +#leftColumn { + width: 280px; + min-height: 100%; + border-right: 1px solid #DADFE6; + flex: 0 0 auto; +} + +@media screen and (max-width: 600px) { + #container { + flex-direction: column; + } + + #leftColumn { + border-right: none; + } +} + +#sideMenu { + max-height: calc(100% - 90px); + padding-top: 16px; + position: relative; +} + +#sideMenu img { + margin: 1em 0.25em; +} + +#sideMenu hr { + background: #DADFE6; +} + +#searchBar { + float: right; +} + +#logo { + background-size: 125px 26px; + border-bottom: 1px solid #DADFE6; + background-repeat: no-repeat; + background-image: url(../images/docs_logo.svg); + background-origin: content-box; + padding-left: 24px; + padding-top: 24px; + height: 48px; +} + +.monospace, +.code { + font-family: monospace; +} + +.sample-container, .code-area { + display: flex; + flex-direction: column; +} + +code.paragraph { + display: block; +} + +.overview > .navButton { + height: 100%; + align-items: center; + display: flex; + justify-content: flex-end; + padding-right: 24px; +} + +.strikethrough { + text-decoration: line-through; +} + +.symbol:empty { + padding: 0; +} + +.symbol { + background-color: #F4F4F4; + align-items: center; + display: block; + padding: 8px 8px 8px 16px; + box-sizing: border-box; + white-space: pre-wrap; + font-weight: bold; + position: relative; + line-height: 24px; +} + +.symbol span.copy-icon path { + fill: #637282; +} + +.symbol span.copy-icon:hover path { + fill: black; +} + +.copy-popup-wrapper { + display: none; + align-items: center; + position: absolute; + z-index: 1000; + background: white; + font-weight: normal; + font-family: 'Inter', "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + width: max-content; + font-size: 14px; + cursor: default; + border: 1px solid #D8DCE1; + box-sizing: border-box; + box-shadow: 0px 5px 10px var(--ring-popup-shadow-color); + border-radius: 3px; +} + +.copy-popup-wrapper.popup-to-left { + /* since it is in position absolute we can just move it to the left to make it always appear on the left side of the icon */ + left: -15em; +} + +.copy-popup-wrapper.active-popup { + display: flex !important; +} + +.copy-popup-wrapper:hover { + font-weight: normal; +} + +.copy-popup-wrapper svg { + padding: 8px; +} + +.copy-popup-wrapper > span:last-child { + padding-right: 14px; +} + +.symbol .top-right-position { + /* it is important for a parent to have a position: relative */ + position: absolute; + top: 8px; + right: 8px; +} + +.sideMenuPart > .overview { + height: 40px; + display: flex; + align-items: center; + position: relative; + user-select: none; /* there's a weird bug with text selection */ +} + +.sideMenuPart a { + display: flex; + align-items: center; + flex: 1; + height: 100%; + color: #637282; + overflow: hidden; +} + +.sideMenuPart > .overview:before { + box-sizing: border-box; + content: ''; + top: 0; + width: 280px; + right: 0; + bottom: 0; + position: absolute; + z-index: -1; +} + +.overview:hover:before { + background-color: #DADFE5; +} + +#nav-submenu { + padding-left: 24px; +} + +.sideMenuPart { + padding-left: 12px; + box-sizing: border-box; +} + +.sideMenuPart .hidden > .overview .navButtonContent::before { + transform: rotate(0deg); +} + +.sideMenuPart > .overview .navButtonContent::before { + content: url("../images/arrow_down.svg"); + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + transform: rotate(180deg); +} + +.sideMenuPart.hidden > .navButton .navButtonContent::after { + content: '\02192'; +} + +.sideMenuPart.hidden > .sideMenuPart { + height: 0; + visibility: hidden; +} + +.filtered > a, .filtered > .navButton { + display: none; +} + +body, table { + font-family: 'Inter', "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + background: #F4F4F4; + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 24px; + margin: 0; + height: 100%; + /*max-width: 1440px; TODO: This results in worse experience on ultrawide, but on 16:9/16:10 looks better.*/ +} + +table { + width: 100%; + border-collapse: collapse; + background-color: #ffffff; + padding: 5px; +} + +tbody > tr { + border-bottom: 2px solid #F4F4F4; + min-height: 56px; +} + +td:first-child { + width: 20vw; +} + +.keyword { + color: black; + font-family: JetBrains Mono, Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + font-size: 12px; +} + +.symbol { + font-family: JetBrains Mono, Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + font-size: 12px; + min-height: 43px; +} + +.symbol > a { + color: var(--hover-link-color); +} + +.identifier { + color: darkblue; + font-size: 12px; + font-family: JetBrains Mono, Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; +} + +.brief { + white-space: pre-wrap; + overflow: hidden; + padding-top: 8px; +} + +h1, h2, h3, h4, h5, h6 { + color: #222; + font-weight: bold; +} + +p, ul, ol, table, pre, dl { + margin: 0 0 20px; +} + +h1 { + font-weight: bold; + font-size: 40px; + line-height: 48px; + letter-spacing: -1px; +} + + +h1.cover { + font-size: 60px; + line-height: 64px; + letter-spacing: -1.5px; + + margin-left: calc(-1 * var(--horizontal-spacing-for-content)); + margin-right: calc(-1 * var(--horizontal-spacing-for-content)); + padding-left: var(--horizontal-spacing-for-content); + padding-right: var(--horizontal-spacing-for-content); + border-bottom: 1px solid #DADFE6; + + margin-bottom: 0; + padding-bottom: 32px; +} + +h2 { + color: #393939; + font-size: 31px; + line-height: 40px; + letter-spacing: -0.5px; +} + +h3 { + font-size: 20px; + line-height: 28px; + letter-spacing: -0.2px; +} + +h4 { + margin: 0; +} + +h3, h4, h5, h6 { + color: #494949; +} + +.UnderCoverText { + font-size: 18px; + line-height: 28px; +} + +a { + color: #5B5DEF; + font-weight: 400; + text-decoration: none; +} + +a:hover { + color: #5B5DEF; + text-decoration: underline; +} + +a small { + font-size: 11px; + color: #555; + margin-top: -0.6em; + display: block; +} + +.wrapper { + width: 860px; + margin: 0 auto; +} + +blockquote { + border-left: 1px solid #e5e5e5; + margin: 0; + padding: 0 0 0 20px; + font-style: italic; +} + +code, pre { + font-family: Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + color: #333; + font-size: 12px; +} + +pre { + display: block; + overflow-x: auto; +} + +th, td { + text-align: left; + vertical-align: top; + padding: 5px 10px; +} + +dt { + color: #444; + font-weight: 700; +} + +th { + color: #444; +} + +img { + max-width: 100%; +} + +header { + width: 270px; + float: left; + position: fixed; +} + +header ul { + list-style: none; + height: 40px; + + padding: 0; + + background: #eee; + background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f8f8f8), color-stop(100%, #dddddd)); + background: -webkit-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); + background: -o-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); + background: -ms-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); + background: linear-gradient(top, #f8f8f8 0%, #dddddd 100%); + + border-radius: 5px; + border: 1px solid #d2d2d2; + box-shadow: inset #fff 0 1px 0, inset rgba(0, 0, 0, 0.03) 0 -1px 0; + width: 270px; +} + +header li { + width: 89px; + float: left; + border-right: 1px solid #d2d2d2; + height: 40px; +} + +header ul a { + line-height: 1; + font-size: 11px; + color: #999; + display: block; + text-align: center; + padding-top: 6px; + height: 40px; +} + +strong { + color: #222; + font-weight: 700; +} + +header ul li + li { + width: 88px; + border-left: 1px solid #fff; +} + +header ul li + li + li { + border-right: none; + width: 89px; +} + +header ul a strong { + font-size: 14px; + display: block; + color: #222; +} + +section { + width: 500px; + float: right; + padding-bottom: 50px; +} + +small { + font-size: 11px; +} + +hr { + border: 0; + background: #e5e5e5; + height: 1px; + margin: 0 0 20px; +} + +footer { + width: 270px; + float: left; + position: fixed; + bottom: 50px; +} + +.platform-tag { + display: flex; + flex-direction: row; + padding: 4px 8px; + height: 24px; + border-radius: 100px; + box-sizing: border-box; + border: 1px solid transparent; + margin: 2px; + font-family: Inter, Arial, sans-serif; + font-size: 12px; + font-weight: 400; + font-style: normal; + font-stretch: normal; + line-height: normal; + letter-spacing: normal; + text-align: center; + outline: none; + + color: #fff + +} + +.platform-tags { + flex: 0 0 auto; + display: flex; +} + +.platform-tags > .platform-tag { + align-self: center; +} + +.platform-tag.jvm-like { + background-color: #4DBB5F; + color: white; +} + +.platform-tag.js-like { + background-color: #FED236; + color: white; +} + +.platform-tag.native-like { + background-color: #CD74F6; + color: white; +} + +.platform-tag.common-like { + background-color: #A6AFBA; + color: white; +} + +.filter-section { + display: flex; + flex-direction: row; + align-self: flex-end; + min-height: 30px; + position: absolute; + top: 20px; + right: 88px; + z-index: 0; +} + +.platform-selector:hover { + border: 1px solid #A6AFBA !important; +} + +[data-filterable-current=''] { + display: none !important; +} + +.platform-selector:not([data-active]) { + border: 1px solid #DADFE6; + background-color: transparent; + color: #637282; +} + +td.content { + padding-left: 24px; + padding-top: 16px; + display: flex; + flex-direction: column; +} + +.main-subrow { + display: flex; + flex-direction: row; + padding: 0; + justify-content: space-between; +} + +.main-subrow > span { + display: flex; + position: relative; +} + +.main-subrow > span > a { + text-decoration: none; + font-style: normal; + font-weight: 600; + font-size: 14px; + color: #282E34; +} + +.main-subrow > span > a:hover { + color: var(--hover-link-color); +} + +.main-subrow:hover .anchor-icon { + opacity: 1; + transition: 0.2s; +} + +.main-subrow .anchor-icon { + padding: 0 8px; + opacity: 0; + transition: 0.2s 0.5s; +} + +.main-subrow .anchor-icon > svg path { + fill: #637282; +} + +.main-subrow .anchor-icon:hover { + cursor: pointer; +} + +.main-subrow .anchor-icon:hover > svg path { + fill: var(--hover-link-color); +} + +.main-subrow .anchor-wrapper { + position: relative; +} + +.platform-hinted { + flex: auto; + display: block; + margin-bottom: 5px; +} + +.platform-hinted > .platform-bookmarks-row > .platform-bookmark { + min-width: 64px; + height: 36px; + border: 2px solid white; + background: white; + outline: none; + flex: none; + order: 5; + align-self: flex-start; + margin: 0; +} + +.platform-hinted > .platform-bookmarks-row > .platform-bookmark.jvm-like:hover { + border-top: 2px solid rgba(77, 187, 95, 0.3); +} + +.platform-hinted > .platform-bookmarks-row > .platform-bookmark.js-like:hover { + border-top: 2px solid rgba(254, 175, 54, 0.3); +} + +.platform-hinted > .platform-bookmarks-row > .platform-bookmark.native-like:hover { + border-top: 2px solid rgba(105, 118, 249, 0.3); +} + +.platform-hinted > .platform-bookmarks-row > .platform-bookmark.common-like:hover { + border-top: 2px solid rgba(161, 170, 180, 0.3); +} + +.platform-hinted > .platform-bookmarks-row > .platform-bookmark.jvm-like[data-active=''] { + border: 2px solid #F4F4F4; + border-top: 2px solid #4DBB5F; + + background: #F4F4F4; +} + +.platform-hinted > .platform-bookmarks-row > .platform-bookmark.js-like[data-active=''] { + border: 2px solid #F4F4F4; + border-top: 2px solid #FED236; + + background: #F4F4F4; +} + +.platform-hinted > .platform-bookmarks-row > .platform-bookmark.native-like[data-active=''] { + border: 2px solid #F4F4F4; + border-top: 2px solid #CD74F6; + + background: #F4F4F4; +} + +.platform-hinted > .platform-bookmarks-row > .platform-bookmark.common-like[data-active=''] { + border: 2px solid #F4F4F4; + border-top: 2px solid #A6AFBA; + + background: #F4F4F4; +} + +.platform-hinted > .content:not([data-active]), +.tabs-section-body > *:not([data-active]) { + visibility: hidden; + height: 0; + position: fixed; + top: 0; +} + +.inner-brief-with-platform-tags { + display: block; + width: 100% +} + +.brief-with-platform-tags { + display: flex; +} + +.brief-with-platform-tags ~ .main-subrow { + padding-top: 16px; +} + +.cover .with-platform-tabs { + background-color: white; +} + +.cover > .with-platform-tabs .platform-bookmarks-row { + margin: 0 16px; +} + +.cover > .with-platform-tabs > .content { + margin: 0 16px; + background-color: #f4f4f4; + padding: 8px 16px; +} + +.cover > .block { + padding-top: 48px; + padding-bottom: 24px; + font-size: 18px; + line-height: 28px; +} + +.cover > .block:empty { + padding-bottom: 0; +} + +.table-row .with-platform-tabs .sourceset-depenent-content .brief { + padding: 16px; + background-color: #f4f4f4; +} + +.sideMenuPart[data-active] > .overview:before { + border-left: 4px solid var(--hover-link-color); + background: rgba(91, 93, 239, 0.15); +} + +.table { + display: flex; + flex-direction: column; +} + +.table-row { + display: flex; + flex-direction: column; + background: white; + border-bottom: 2px solid #f4f4f4; + padding: 16px 24px 16px 24px; +} + +.platform-dependent-row { + display: grid; + padding-top: 8px; +} + +.title-row { + display: grid; + grid-template-columns: auto auto 7em; + width: 100%; +} + +.keyValue { + display: grid; +} + +@media print, screen and (min-width: 960px) { + .keyValue { + grid-template-columns: 20% 80%; + } + + .title-row { + grid-template-columns: 20% auto 7em; + } +} + +@media print, screen and (max-width: 960px) { + + div.wrapper { + width: auto; + margin: 0; + } + + header, section, footer { + float: none; + position: static; + width: auto; + } + + header { + padding-right: 320px; + } + + section { + border: 1px solid #e5e5e5; + border-width: 1px 0; + padding: 20px 0; + margin: 0 0 20px; + } + + header a small { + display: inline; + } + + header ul { + position: absolute; + right: 50px; + top: 52px; + } +} + +@media print, screen and (max-width: 720px) { + body { + word-wrap: break-word; + } + + header { + padding: 0; + } + + header ul, header p.view { + position: static; + } + + pre, code { + word-wrap: normal; + } +} + +@media print, screen and (max-width: 480px) { + body { + padding-right: 15px; + } + + header ul { + display: none; + } +} + +@media print { + body { + padding: 0.4in; + font-size: 12pt; + color: #444; + } +} + +.footer { + clear: both; + display: flex; + align-items: center; + position: relative; + height: var(--footer-height); + border-top: 1px solid #DADFE6; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.2px; + color: var(--breadcrumb-font-color); + margin-top: var(--footer-padding-top); +} + +.footer span.go-to-top-icon { + border-radius: 2em; + padding: 11px 10px !important; + background-color: white; +} + +.footer span.go-to-top-icon path { + fill: #637282; +} + +.footer > span:first-child { + margin-left: var(--horizontal-spacing-for-content); + padding-left: 0; +} + +.footer > span:last-child { + margin-right: var(--horizontal-spacing-for-content); + padding-right: 0; +} + +.footer > span { + padding: 0 16px; +} + +.footer .padded-icon { + padding-left: 0.5em; +} + +/*For svg*/ +.footer path { + fill: var(--breadcrumb-font-color); +} + +.pull-right { + float: right; + margin-left: auto +} + +div.runnablesample { + height: fit-content; +}
\ No newline at end of file |