diff options
Diffstat (limited to 'plugins/base/src/main')
3 files changed, 394 insertions, 344 deletions
diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt index 9d45f7a3..266d6e94 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -6,28 +6,19 @@ import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint import org.jetbrains.dokka.base.signatures.SignatureProvider import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions import org.jetbrains.dokka.base.transformers.documentables.ClashingDriIdentifier -import org.jetbrains.dokka.base.transformers.documentables.InheritorsInfo import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.links.PointingToDeclaration 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 -internal const val KDOC_TAG_HEADER_LEVEL = 4 - -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, Param::class, See::class) +internal typealias GroupedTags = Map<KClass<out TagWrapper>, List<Pair<DokkaSourceSet?, TagWrapper>>> open class DefaultPageCreator( configuration: DokkaBaseConfiguration?, @@ -203,7 +194,6 @@ open class DefaultPageCreator( } } } - +contentForComments(m) block( "Packages", @@ -244,7 +234,6 @@ open class DefaultPageCreator( } } group(styles = setOf(ContentStyle.TabbedContent)) { - +contentForComments(p) +contentForScope(p, p.dri, p.sourceSets) } } @@ -254,25 +243,13 @@ open class DefaultPageCreator( sourceSets: Set<DokkaSourceSet> ): ContentGroup { val types = scopes.flatMap { it.classlikes } + scopes.filterIsInstance<DPackage>().flatMap { it.typealiases } - val inheritors = scopes.fold(mutableMapOf<DokkaSourceSet, List<DRI>>()) { acc, scope -> - val inheritorsForScope = - scope.safeAs<WithExtraProperties<Documentable>>()?.let { it.extra[InheritorsInfo] }?.let { inheritors -> - inheritors.value.filter { it.value.isNotEmpty() } - }.orEmpty() - inheritorsForScope.forEach { (k, v) -> - acc.compute(k) { _, value -> value?.plus(v) ?: v } - } - acc - } - return contentForScope( @Suppress("UNCHECKED_CAST") (scopes as List<Documentable>).dri, sourceSets, types, scopes.flatMap { it.functions }, - scopes.flatMap { it.properties }, - inheritors + scopes.flatMap { it.properties } ) } @@ -285,12 +262,7 @@ open class DefaultPageCreator( s.classlikes, (s as? DPackage)?.typealiases ?: emptyList() ).flatten() - val inheritors = - s.safeAs<WithExtraProperties<Documentable>>()?.let { it.extra[InheritorsInfo] }?.let { inheritors -> - inheritors.value.filter { it.value.isNotEmpty() } - }.orEmpty() - - return contentForScope(setOf(dri), sourceSets, types, s.functions, s.properties, inheritors) + return contentForScope(setOf(dri), sourceSets, types, s.functions, s.properties) } protected open fun contentForScope( @@ -298,8 +270,7 @@ open class DefaultPageCreator( sourceSets: Set<DokkaSourceSet>, types: List<Documentable>, functions: List<DFunction>, - properties: List<DProperty>, - inheritors: SourceSetDependent<List<DRI>> + properties: List<DProperty> ) = contentBuilder.contentFor(dri, sourceSets) { divergentBlock("Types", types, ContentKind.Classlikes, extra = mainExtra + SimpleAttr.header("Types")) if (separateInheritedMembers) { @@ -313,31 +284,6 @@ open class DefaultPageCreator( functionsBlock("Functions", functions) propertiesBlock("Properties", properties, sourceSets) } - if (inheritors.values.any()) { - header(2, "Inheritors") { } - +ContentTable( - header = listOf(contentBuilder.contentFor(mainDRI, mainSourcesetData) { - text("Name") - }), - children = inheritors.entries.flatMap { entry -> entry.value.map { Pair(entry.key, it) } } - .groupBy({ it.second }, { it.first }).map { (classlike, platforms) -> - val label = classlike.classNames?.substringAfterLast(".") ?: classlike.toString() - .also { logger.warn("No class name found for DRI $classlike") } - buildGroup( - setOf(classlike), - platforms.toSet(), - ContentKind.Inheritors, - extra = mainExtra + SymbolAnchorHint(label, ContentKind.Inheritors) - ) { - link(label, classlike) - } - }, - dci = DCI(dri, ContentKind.Inheritors), - sourceSets = sourceSets.toDisplaySourceSets(), - style = emptySet(), - extra = mainExtra + SimpleAttr.header("Inheritors") - ) - } } private fun Iterable<DFunction>.sorted() = @@ -367,9 +313,7 @@ open class DefaultPageCreator( } } } - group(styles = setOf(ContentStyle.TabbedContent), sourceSets = mainSourcesetData + extensions.sourceSets) { - +contentForComments(documentables) val csWithConstructor = classlikes.filterIsInstance<WithConstructors>() if (csWithConstructor.isNotEmpty() && documentables.shouldRenderConstructors()) { val constructorsToDocumented = csWithConstructor.flatMap { it.constructors } @@ -442,272 +386,28 @@ open class DefaultPageCreator( // and instantiated directly under normal circumstances, so constructors should not be rendered. private fun List<Documentable>.shouldRenderConstructors() = !this.any { it is DAnnotation } - @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>>?) - ?.groupByTo(linkedMapOf()) { it.second.name } - ?.mapValues { (_, v) -> v.toMap() } - .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.groupedTags - val platforms = d.sourceSets.toSet() - - return contentBuilder.contentFor(d, styles = setOf(TextStyle.Block)) { - deprecatedSectionContent(d, platforms) - - val descriptions = d.descriptions - if (descriptions.any { it.value.root.children.isNotEmpty() }) { - platforms.forEach { platform -> - descriptions[platform]?.also { - group(sourceSets = setOf(platform), styles = emptySet()) { - comment(it.root) - } - } - } - } - - val customTags = d.customTags - if (customTags.isNotEmpty()) { - platforms.forEach { platform -> - customTags.forEach { (_, sourceSetTag) -> - sourceSetTag[platform]?.let { tag -> - customTagContentProviders.filter { it.isApplicable(tag) }.forEach { provider -> - group(sourceSets = setOf(platform), styles = setOf(ContentStyle.KDocTag)) { - with(provider) { - contentForDescription(platform, tag) - } - } - } - } - } - } - } - - val unnamedTags = tags.filterNot { (k, _) -> k.isSubclassOf(NamedTagWrapper::class) || k in specialTags } - .values.flatten().groupBy { it.first }.mapValues { it.value.map { it.second } } - if (unnamedTags.isNotEmpty()) { - platforms.forEach { platform -> - unnamedTags[platform]?.let { tags -> - if (tags.isNotEmpty()) { - tags.groupBy { it::class }.forEach { (_, sameCategoryTags) -> - group(sourceSets = setOf(platform), styles = setOf(ContentStyle.KDocTag)) { - header( - level = KDOC_TAG_HEADER_LEVEL, - text = sameCategoryTags.first().toHeaderString(), - styles = setOf() - ) - sameCategoryTags.forEach { comment(it.root, styles = setOf()) } - } - } - } - } - } - } - }.children - } - - private fun Set<DokkaSourceSet>.getPossibleFallbackSourcesets(sourceSet: DokkaSourceSet) = - this.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, - isPlatformHintedContent: Boolean = true - ) = contentForComments(d.dri, d.sourceSets, d.groupedTags, isPlatformHintedContent) - - protected open fun contentForComments( - d: List<Documentable>, - isPlatformHintedContent: Boolean = true - ) = contentForComments(d.first().dri, d.sourceSets, d.groupedTags, isPlatformHintedContent) - - protected open fun contentForComments( - dri: DRI, - sourceSets: Set<DokkaSourceSet>, - tags: GroupedTags, - isPlatformHintedContent: Boolean = true - ): List<ContentNode> { - - fun DocumentableContentBuilder.buildContent( - platforms: Set<DokkaSourceSet>, - contentBuilder: DocumentableContentBuilder.() -> Unit - ) = if (isPlatformHintedContent) - sourceSetDependentHint( - sourceSets = platforms, - kind = ContentKind.SourceSetDependentHint, - block = contentBuilder - ) - else - contentBuilder() - - fun DocumentableContentBuilder.contentForParams() { - if (tags.isNotEmptyForTag<Param>()) { - val params = tags.withTypeNamed<Param>() - val availablePlatforms = params.values.flatMap { it.keys }.toSet() - - header(KDOC_TAG_HEADER_LEVEL, "Parameters", kind = ContentKind.Parameters, sourceSets = availablePlatforms) - group( - extra = mainExtra + SimpleAttr.header("Parameters"), - styles = setOf(ContentStyle.WithExtraAttributes), - sourceSets = availablePlatforms - ) { - buildContent(availablePlatforms) { - table(kind = ContentKind.Parameters, sourceSets = availablePlatforms) { - availablePlatforms.forEach { platform -> - val possibleFallbacks = sourceSets.getPossibleFallbackSourcesets(platform) - params.mapNotNull { (_, param) -> - (param[platform] ?: param.fallback(possibleFallbacks))?.let { - row(sourceSets = setOf(platform), kind = ContentKind.Parameters) { - text( - it.name, - kind = ContentKind.Parameters, - styles = mainStyles + setOf(ContentStyle.RowTitle, TextStyle.Underlined) - ) - if (it.isNotEmpty()) { - comment(it.root) - } - } - } - } - } - } - } - } - } - } + val sourceSets = d.sourceSets.toSet() + val tags = d.groupedTags - fun DocumentableContentBuilder.contentForSeeAlso() { - if (tags.isNotEmptyForTag<See>()) { - val seeAlsoTags = tags.withTypeNamed<See>() - val availablePlatforms = seeAlsoTags.values.flatMap { it.keys }.toSet() + return contentBuilder.contentFor(d) { + deprecatedSectionContent(d, sourceSets) - header(KDOC_TAG_HEADER_LEVEL, "See also", kind = ContentKind.Comment, sourceSets = availablePlatforms) - group( - extra = mainExtra + SimpleAttr.header("See also"), - styles = setOf(ContentStyle.WithExtraAttributes), - sourceSets = availablePlatforms - ) { - buildContent(availablePlatforms) { - table(kind = ContentKind.Sample) { - availablePlatforms.forEach { platform -> - val possibleFallbacks = sourceSets.getPossibleFallbackSourcesets(platform) - seeAlsoTags.forEach { (_, see) -> - (see[platform] ?: see.fallback(possibleFallbacks))?.let { seeTag -> - row( - sourceSets = setOf(platform), - kind = ContentKind.Comment, - styles = this@group.mainStyles, - ) { - seeTag.address?.let { dri -> - link( - text = seeTag.name.removePrefix("${dri.packageName}."), - address = dri, - kind = ContentKind.Comment, - styles = mainStyles + ContentStyle.RowTitle - ) - } ?: text( - text = seeTag.name, - kind = ContentKind.Comment, - styles = mainStyles + ContentStyle.RowTitle - ) - if (seeTag.isNotEmpty()) { - comment(seeTag.root) - } - } - } - } - } - } - } - } - } - } + descriptionSectionContent(d, sourceSets) + customTagSectionContent(d, sourceSets, customTagContentProviders) + unnamedTagSectionContent(d, sourceSets) { toHeaderString() } - fun DocumentableContentBuilder.contentForThrows() { - val throws = tags.withTypeNamed<Throws>() - if (throws.isNotEmpty()) { - val availablePlatforms = throws.values.flatMap { it.keys }.toSet() - - header(KDOC_TAG_HEADER_LEVEL, "Throws", sourceSets = availablePlatforms) - buildContent(availablePlatforms) { - availablePlatforms.forEach { sourceset -> - table( - kind = ContentKind.Main, - sourceSets = setOf(sourceset), - extra = mainExtra + SimpleAttr.header("Throws") - ) { - throws.entries.forEach { entry -> - entry.value[sourceset]?.let { throws -> - row(sourceSets = setOf(sourceset)) { - group(styles = mainStyles + ContentStyle.RowTitle) { - throws.exceptionAddress?.let { - val className = it.takeIf { it.target is PointingToDeclaration }?.classNames - link(text = className ?: entry.key, address = it) - } ?: text(entry.key) - } - if (throws.isNotEmpty()) { - comment(throws.root) - } - } - } - } - } - } - } - } - } - - fun DocumentableContentBuilder.contentForSamples() { - val samples = tags.withTypeNamed<Sample>() - if (samples.isNotEmpty()) { - val availablePlatforms = samples.values.flatMap { it.keys }.toSet() - header(KDOC_TAG_HEADER_LEVEL, "Samples", kind = ContentKind.Sample, sourceSets = availablePlatforms) - group( - extra = mainExtra + SimpleAttr.header("Samples"), - styles = emptySet(), - sourceSets = availablePlatforms - ) { - buildContent(availablePlatforms) { - availablePlatforms.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) - } - } - } - } - } - } - } + paramsSectionContent(tags) + seeAlsoSectionContent(tags) + throwsSectionContent(tags) + samplesSectionContent(tags) - return contentBuilder.contentFor(dri, sourceSets) { - if (tags.isNotEmpty()) { - contentForSamples() - contentForSeeAlso() - contentForParams() - contentForThrows() - } + inheritorsSectionContent(d, logger) }.children } - private fun TagWrapper.isNotEmpty() = this.children.isNotEmpty() - protected open fun DocumentableContentBuilder.contentForBrief(documentable: Documentable) { documentable.sourceSets.forEach { sourceSet -> documentable.documentation[sourceSet]?.let { @@ -761,7 +461,6 @@ open class DefaultPageCreator( } after { +contentForDescription(d) - +contentForComments(d, isPlatformHintedContent = false) } } } @@ -890,35 +589,39 @@ open class DefaultPageCreator( } protected open fun TagWrapper.toHeaderString() = this.javaClass.toGenericString().split('.').last() +} - private val List<Documentable>.sourceSets: Set<DokkaSourceSet> - get() = flatMap { it.sourceSets }.toSet() +internal val List<Documentable>.sourceSets: Set<DokkaSourceSet> + get() = flatMap { it.sourceSets }.toSet() - private val List<Documentable>.dri: Set<DRI> - get() = map { it.dri }.toSet() +internal val List<Documentable>.dri: Set<DRI> + get() = map { it.dri }.toSet() - private val Documentable.groupedTags: GroupedTags - get() = documentation.flatMap { (pd, doc) -> - doc.children.map { pd to it }.toList() - }.groupBy { it.second::class } +internal val Documentable.groupedTags: GroupedTags + get() = documentation.flatMap { (pd, doc) -> + doc.children.map { pd to it }.toList() + }.groupBy { it.second::class } - private val List<Documentable>.groupedTags: GroupedTags - get() = this.flatMap { - it.documentation.flatMap { (pd, doc) -> - doc.children.map { pd to it }.toList() - } - }.groupBy { it.second::class } +internal val Documentable.descriptions: SourceSetDependent<Description> + get() = groupedTags.withTypeUnnamed() - private val Documentable.descriptions: SourceSetDependent<Description> - get() = groupedTags.withTypeUnnamed() +internal val Documentable.customTags: Map<String, SourceSetDependent<CustomTagWrapper>> + get() = groupedTags.withTypeNamed() - private val Documentable.customTags: Map<String, SourceSetDependent<CustomTagWrapper>> - get() = groupedTags.withTypeNamed() +private val Documentable.hasSeparatePage: Boolean + get() = this !is DTypeAlias - private val Documentable.hasSeparatePage: Boolean - get() = this !is DTypeAlias +@Suppress("UNCHECKED_CAST") +private fun <T : Documentable> T.nameAfterClash(): String = + ((this as? WithExtraProperties<out Documentable>)?.extra?.get(DriClashAwareName)?.value ?: name).orEmpty() - @Suppress("UNCHECKED_CAST") - private fun <T : Documentable> T.nameAfterClash(): String = - ((this as? WithExtraProperties<out Documentable>)?.extra?.get(DriClashAwareName)?.value ?: name).orEmpty() -} +@Suppress("UNCHECKED_CAST") +internal inline fun <reified T : TagWrapper> GroupedTags.withTypeUnnamed(): SourceSetDependent<T> = + (this[T::class] as List<Pair<DokkaSourceSet, T>>?)?.toMap().orEmpty() + +@Suppress("UNCHECKED_CAST") +internal inline fun <reified T : NamedTagWrapper> GroupedTags.withTypeNamed(): Map<String, SourceSetDependent<T>> = + (this[T::class] as List<Pair<DokkaSourceSet, T>>?) + ?.groupByTo(linkedMapOf()) { it.second.name } + ?.mapValues { (_, v) -> v.toMap() } + .orEmpty()
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/documentables/DescriptionSections.kt b/plugins/base/src/main/kotlin/translators/documentables/DescriptionSections.kt new file mode 100644 index 00000000..1720b1d0 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/documentables/DescriptionSections.kt @@ -0,0 +1,346 @@ +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.transformers.documentables.InheritorsInfo +import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.WithScope +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.orEmpty +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.ContentKind +import org.jetbrains.dokka.pages.ContentStyle +import org.jetbrains.dokka.pages.SimpleAttr +import org.jetbrains.dokka.pages.TextStyle +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf + +internal const val KDOC_TAG_HEADER_LEVEL = 4 + +private val unnamedTagsExceptions: Set<KClass<out TagWrapper>> = + setOf(Property::class, Description::class, Constructor::class, Param::class, See::class) + +internal fun PageContentBuilder.DocumentableContentBuilder.descriptionSectionContent( + documentable: Documentable, + sourceSets: Set<DokkaConfiguration.DokkaSourceSet>, +) { + val descriptions = documentable.descriptions + if (descriptions.any { it.value.root.children.isNotEmpty() }) { + sourceSets.forEach { sourceSet -> + descriptions[sourceSet]?.also { + group(sourceSets = setOf(sourceSet), styles = emptySet()) { + comment(it.root) + } + } + } + } +} + +/** + * Custom tags are tags which are not part of the [KDoc specification](https://kotlinlang.org/docs/kotlin-doc.html). For instance, a user-defined tag + * which is specific to the user's code base would be considered a custom tag. + * + * For details, see [CustomTagContentProvider] + */ +internal fun PageContentBuilder.DocumentableContentBuilder.customTagSectionContent( + documentable: Documentable, + sourceSets: Set<DokkaConfiguration.DokkaSourceSet>, + customTagContentProviders: List<CustomTagContentProvider>, +) { + val customTags = documentable.customTags + if (customTags.isEmpty()) return + + sourceSets.forEach { sourceSet -> + customTags.forEach { (_, sourceSetTag) -> + sourceSetTag[sourceSet]?.let { tag -> + customTagContentProviders.filter { it.isApplicable(tag) }.forEach { provider -> + group(sourceSets = setOf(sourceSet), styles = setOf(ContentStyle.KDocTag)) { + with(provider) { + contentForDescription(sourceSet, tag) + } + } + } + } + } + } +} + +/** + * Tags in KDoc are used in form of "@tag name value". + * This function handles tags that have only value parameter without name. + * List of such tags: `@return`, `@author`, `@since`, `@receiver` + */ +internal fun PageContentBuilder.DocumentableContentBuilder.unnamedTagSectionContent( + documentable: Documentable, + sourceSets: Set<DokkaConfiguration.DokkaSourceSet>, + toHeaderString: TagWrapper.() -> String, +) { + val unnamedTags = documentable.groupedTags + .filterNot { (k, _) -> k.isSubclassOf(NamedTagWrapper::class) || k in unnamedTagsExceptions } + .values.flatten().groupBy { it.first } + .mapValues { it.value.map { it.second } } + .takeIf { it.isNotEmpty() } ?: return + + sourceSets.forEach { sourceSet -> + unnamedTags[sourceSet]?.let { tags -> + if (tags.isNotEmpty()) { + tags.groupBy { it::class }.forEach { (_, sameCategoryTags) -> + group(sourceSets = setOf(sourceSet), styles = setOf(ContentStyle.KDocTag)) { + header( + level = KDOC_TAG_HEADER_LEVEL, + text = sameCategoryTags.first().toHeaderString(), + styles = setOf() + ) + sameCategoryTags.forEach { comment(it.root, styles = setOf()) } + } + } + } + } + } +} + + +internal fun PageContentBuilder.DocumentableContentBuilder.paramsSectionContent(tags: GroupedTags) { + val params = tags.withTypeNamed<Param>() + if (params.isEmpty()) return + + val availableSourceSets = params.availableSourceSets() + tableSectionContentBlock( + blockName = "Parameters", + kind = ContentKind.Parameters, + sourceSets = availableSourceSets + ) { + availableSourceSets.forEach { sourceSet -> + val possibleFallbacks = availableSourceSets.getPossibleFallback(sourceSet) + params.mapNotNull { (_, param) -> + (param[sourceSet] ?: param.fallback(possibleFallbacks))?.let { + row(sourceSets = setOf(sourceSet), kind = ContentKind.Parameters) { + text( + it.name, + kind = ContentKind.Parameters, + styles = mainStyles + setOf(ContentStyle.RowTitle, TextStyle.Underlined) + ) + if (it.isNotEmpty()) { + comment(it.root) + } + } + } + } + } + } +} + +internal fun PageContentBuilder.DocumentableContentBuilder.seeAlsoSectionContent(tags: GroupedTags) { + val seeAlsoTags = tags.withTypeNamed<See>() + if (seeAlsoTags.isEmpty()) return + + val availableSourceSets = seeAlsoTags.availableSourceSets() + tableSectionContentBlock( + blockName = "See also", + kind = ContentKind.Comment, + sourceSets = availableSourceSets + ) { + availableSourceSets.forEach { sourceSet -> + val possibleFallbacks = availableSourceSets.getPossibleFallback(sourceSet) + seeAlsoTags.forEach { (_, see) -> + (see[sourceSet] ?: see.fallback(possibleFallbacks))?.let { seeTag -> + row( + sourceSets = setOf(sourceSet), + kind = ContentKind.Comment + ) { + seeTag.address?.let { dri -> + link( + text = seeTag.name.removePrefix("${dri.packageName}."), + address = dri, + kind = ContentKind.Comment, + styles = mainStyles + ContentStyle.RowTitle + ) + } ?: text( + text = seeTag.name, + kind = ContentKind.Comment, + styles = mainStyles + ContentStyle.RowTitle + ) + if (seeTag.isNotEmpty()) { + comment(seeTag.root) + } + } + } + } + } + } +} + +/** + * Used for multi-value tags (e.g. params) when values are missed on some platforms. + * It this case description is inherited from parent platform. + * E.g. if param hasn't description in JVM, the description is taken from common. + */ +private fun Set<DokkaConfiguration.DokkaSourceSet>.getPossibleFallback(sourceSet: DokkaConfiguration.DokkaSourceSet) = + this.filter { it.sourceSetID in sourceSet.dependentSourceSets } + +private fun <V> Map<DokkaConfiguration.DokkaSourceSet, V>.fallback(sourceSets: List<DokkaConfiguration.DokkaSourceSet>): V? = + sourceSets.firstOrNull { it in this.keys }.let { this[it] } + +internal fun PageContentBuilder.DocumentableContentBuilder.throwsSectionContent(tags: GroupedTags) { + val throwsTags = tags.withTypeNamed<Throws>() + if (throwsTags.isEmpty()) return + + val availableSourceSets = throwsTags.availableSourceSets() + tableSectionContentBlock( + blockName = "Throws", + kind = ContentKind.Main, + sourceSets = availableSourceSets + ) { + throwsTags.forEach { (throwsName, throwsPerSourceSet) -> + throwsPerSourceSet.forEach { (sourceSet, throws) -> + row(sourceSets = setOf(sourceSet)) { + group(styles = mainStyles + ContentStyle.RowTitle) { + throws.exceptionAddress?.let { + val className = it.takeIf { it.target is PointingToDeclaration }?.classNames + link(text = className ?: throwsName, address = it) + } ?: text(throwsName) + } + if (throws.isNotEmpty()) { + comment(throws.root) + } + } + } + } + } +} + +private fun TagWrapper.isNotEmpty() = this.children.isNotEmpty() + +internal fun PageContentBuilder.DocumentableContentBuilder.samplesSectionContent(tags: GroupedTags) { + val samples = tags.withTypeNamed<Sample>() + if (samples.isEmpty()) return + + val availableSourceSets = samples.availableSourceSets() + + header(KDOC_TAG_HEADER_LEVEL, "Samples", kind = ContentKind.Sample, sourceSets = availableSourceSets) + availableSourceSets.forEach { sourceSet -> + group( + sourceSets = setOf(sourceSet), + kind = ContentKind.Sample, + styles = setOf(TextStyle.Monospace, ContentStyle.RunnableSample), + extra = mainExtra + SimpleAttr.header("Samples") + ) { + samples.filter { it.value.isEmpty() || sourceSet in it.value } + .forEach { text(text = it.key, sourceSets = setOf(sourceSet)) } + } + } +} + +internal fun PageContentBuilder.DocumentableContentBuilder.inheritorsSectionContent( + documentable: Documentable, + logger: DokkaLogger, +) { + val inheritors = if (documentable is WithScope) documentable.inheritors() else return + if (inheritors.values.none()) return + + // split content section for the case: + // parent is in the shared source set (without expect-actual) and inheritor is in the platform code + if (documentable.isDefinedInSharedSourceSetOnly(inheritors.keys.toSet())) + sharedSourceSetOnlyInheritorsSectionContent(inheritors, logger) + else + multiplatformInheritorsSectionContent(documentable, inheritors, logger) +} + +private fun WithScope.inheritors() = safeAs<WithExtraProperties<Documentable>>() + ?.let { it.extra[InheritorsInfo] } + ?.let { inheritors -> inheritors.value.filter { it.value.isNotEmpty() } } + .orEmpty() + +/** + * Detect that documentable is located only in the shared code without expect-actuals + * Value of `analysisPlatform` will be [Platform.common] in cases if a source set shared between 2 different platforms. + * But if it shared between 2 same platforms (e.g. jvm("awt") and jvm("android")) + * then the source set will be still marked as jvm platform. + * + * So, we also try to check if any of inheritors source sets depends on current documentable source set. + * that will mean that the source set is shared. + */ +private fun Documentable.isDefinedInSharedSourceSetOnly(inheritorsSourceSets: Set<DokkaConfiguration.DokkaSourceSet>) = + sourceSets.size == 1 && + (sourceSets.first().analysisPlatform == Platform.common + || sourceSets.first().hasDependentSourceSet(inheritorsSourceSets)) + +private fun DokkaConfiguration.DokkaSourceSet.hasDependentSourceSet( + sourceSets: Set<DokkaConfiguration.DokkaSourceSet>, +) = + sourceSets.any { sourceSet -> sourceSet.dependentSourceSets.any { it == this.sourceSetID } } + +private fun PageContentBuilder.DocumentableContentBuilder.multiplatformInheritorsSectionContent( + documentable: Documentable, + inheritors: Map<DokkaConfiguration.DokkaSourceSet, List<DRI>>, + logger: DokkaLogger, +) { + // intersect is used for removing duplication in case of merged classlikes from different platforms + val availableSourceSets = inheritors.keys.toSet().intersect(documentable.sourceSets) + + tableSectionContentBlock( + blockName = "Inheritors", + kind = ContentKind.Inheritors, + sourceSets = availableSourceSets + ) { + availableSourceSets.forEach { sourceSet -> + inheritors[sourceSet]?.forEach { classlike: DRI -> + inheritorRow(classlike, logger, sourceSet) + } + } + } +} + +private fun PageContentBuilder.DocumentableContentBuilder.sharedSourceSetOnlyInheritorsSectionContent( + inheritors: Map<DokkaConfiguration.DokkaSourceSet, List<DRI>>, + logger: DokkaLogger, +) { + val uniqueInheritors = inheritors.values.flatten().toSet() + tableSectionContentBlock( + blockName = "Inheritors", + kind = ContentKind.Inheritors, + ) { + uniqueInheritors.forEach { classlike -> + inheritorRow(classlike, logger) + } + } +} + +private fun PageContentBuilder.TableBuilder.inheritorRow( + classlike: DRI, logger: DokkaLogger, sourceSet: DokkaConfiguration.DokkaSourceSet? = null, +) = row { + link( + text = classlike.friendlyClassName() + ?: classlike.toString().also { logger.warn("No class name found for DRI $classlike") }, + address = classlike, + sourceSets = sourceSet?.let { setOf(it) } ?: mainSourcesetData + ) +} + +private fun PageContentBuilder.DocumentableContentBuilder.tableSectionContentBlock( + blockName: String, + kind: ContentKind, + sourceSets: Set<DokkaConfiguration.DokkaSourceSet> = mainSourcesetData, + body: PageContentBuilder.TableBuilder.() -> Unit, +) { + header(KDOC_TAG_HEADER_LEVEL, text = blockName, kind = kind, sourceSets = sourceSets) + table( + kind = kind, + sourceSets = sourceSets, + extra = mainExtra + SimpleAttr.header(blockName) + ) { + body() + } +} + +private fun DRI.friendlyClassName() = classNames?.substringAfterLast(".") + +private fun <T> Map<String, SourceSetDependent<T>>.availableSourceSets() = values.flatMap { it.keys }.toSet() + + diff --git a/plugins/base/src/main/resources/dokka/styles/style.css b/plugins/base/src/main/resources/dokka/styles/style.css index 431db3b8..5242a76d 100644 --- a/plugins/base/src/main/resources/dokka/styles/style.css +++ b/plugins/base/src/main/resources/dokka/styles/style.css @@ -946,9 +946,10 @@ td.content { display: none; } -/*Work around an issue: https://github.com/JetBrains/kotlin-playground/issues/91*/ -.platform-hinted[data-togglable="Samples"] > .content:not([data-active]), -.tabs-section-body > *[data-togglable="Samples"]:not([data-active]) { +/* Work around an issue: https://github.com/JetBrains/kotlin-playground/issues/91 +Applies for main description blocks with platform tabs. +Just in case of possible performance degradation it excluding tabs with briefs on classlike page */ +#content > div:not(.tabbedcontent) .sourceset-dependent-content:not([data-active]) { display: block !important; visibility: hidden; height: 0; |