diff options
author | Ignat Beresnev <ignat.beresnev@jetbrains.com> | 2023-11-10 11:46:54 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-10 11:46:54 +0100 |
commit | 8e5c63d035ef44a269b8c43430f43f5c8eebfb63 (patch) | |
tree | 1b915207b2b9f61951ddbf0ff2e687efd053d555 /dokka-subprojects/plugin-base/src/main/kotlin | |
parent | a44efd4ba0c2e4ab921ff75e0f53fc9335aa79db (diff) | |
download | dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.gz dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.bz2 dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.zip |
Restructure the project to utilize included builds (#3174)
* Refactor and simplify artifact publishing
* Update Gradle to 8.4
* Refactor and simplify convention plugins and build scripts
Fixes #3132
---------
Co-authored-by: Adam <897017+aSemy@users.noreply.github.com>
Co-authored-by: Oleg Yukhnevich <whyoleg@gmail.com>
Diffstat (limited to 'dokka-subprojects/plugin-base/src/main/kotlin')
111 files changed, 10312 insertions, 0 deletions
diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt new file mode 100644 index 00000000..ca86d4d5 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt @@ -0,0 +1,299 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.base.generation.SingleModuleGeneration +import org.jetbrains.dokka.base.renderers.* +import org.jetbrains.dokka.base.renderers.html.* +import org.jetbrains.dokka.base.renderers.html.command.consumers.PathToRootConsumer +import org.jetbrains.dokka.base.renderers.html.command.consumers.ReplaceVersionsConsumer +import org.jetbrains.dokka.base.renderers.html.command.consumers.ResolveLinkConsumer +import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.external.javadoc.JavadocExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.base.signatures.KotlinSignatureProvider +import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer +import org.jetbrains.dokka.base.transformers.documentables.* +import org.jetbrains.dokka.base.transformers.pages.DefaultSamplesTransformer +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.* +import org.jetbrains.dokka.base.transformers.pages.sourcelinks.SourceLinksTransformer +import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider +import org.jetbrains.dokka.base.transformers.pages.tags.SinceKotlinTagContentProvider +import org.jetbrains.dokka.base.translators.documentables.DefaultDocumentableToPageTranslator +import org.jetbrains.dokka.generation.Generation +import org.jetbrains.dokka.plugability.* +import org.jetbrains.dokka.renderers.Renderer +import org.jetbrains.dokka.transformers.documentation.* +import org.jetbrains.dokka.transformers.pages.PageTransformer + +@Suppress("unused") +public class DokkaBase : DokkaPlugin() { + + public val preMergeDocumentableTransformer: ExtensionPoint<PreMergeDocumentableTransformer> by extensionPoint() + public val pageMergerStrategy: ExtensionPoint<PageMergerStrategy> by extensionPoint() + public val commentsToContentConverter: ExtensionPoint<CommentsToContentConverter> by extensionPoint() + public val customTagContentProvider: ExtensionPoint<CustomTagContentProvider> by extensionPoint() + public val signatureProvider: ExtensionPoint<SignatureProvider> by extensionPoint() + public val locationProviderFactory: ExtensionPoint<LocationProviderFactory> by extensionPoint() + public val externalLocationProviderFactory: ExtensionPoint<ExternalLocationProviderFactory> by extensionPoint() + public val outputWriter: ExtensionPoint<OutputWriter> by extensionPoint() + public val htmlPreprocessors: ExtensionPoint<PageTransformer> by extensionPoint() + + @Deprecated("It is not used anymore") + public val tabSortingStrategy: ExtensionPoint<TabSortingStrategy> by extensionPoint() + public val immediateHtmlCommandConsumer: ExtensionPoint<ImmediateHtmlCommandConsumer> by extensionPoint() + + + public val singleGeneration: Extension<Generation, *, *> by extending { + CoreExtensions.generation providing ::SingleModuleGeneration + } + + public val documentableMerger: Extension<DocumentableMerger, *, *> by extending { + CoreExtensions.documentableMerger providing ::DefaultDocumentableMerger + } + + public val deprecatedDocumentableFilter: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer providing ::DeprecatedDocumentableFilterTransformer + } + + public val suppressedDocumentableFilter: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer providing ::SuppressedByConfigurationDocumentableFilterTransformer + } + + public val suppressedBySuppressTagDocumentableFilter: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer providing ::SuppressTagDocumentableFilter + } + + public val documentableVisibilityFilter: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer providing ::DocumentableVisibilityFilterTransformer + } + + public val obviousFunctionsVisbilityFilter: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer providing ::ObviousFunctionsDocumentableFilterTransformer + } + + public val inheritedEntriesVisbilityFilter: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer providing ::InheritedEntriesDocumentableFilterTransformer + } + + public val kotlinArrayDocumentableReplacer: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer providing ::KotlinArrayDocumentableReplacerTransformer + } + + public val emptyPackagesFilter: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer providing ::EmptyPackagesFilterTransformer order { + after( + deprecatedDocumentableFilter, + suppressedDocumentableFilter, + documentableVisibilityFilter, + suppressedBySuppressTagDocumentableFilter, + obviousFunctionsVisbilityFilter, + inheritedEntriesVisbilityFilter, + ) + } + } + + public val emptyModulesFilter: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer with EmptyModulesFilterTransformer() order { + after(emptyPackagesFilter) + } + } + + public val modulesAndPackagesDocumentation: Extension<PreMergeDocumentableTransformer, *, *> by extending { + preMergeDocumentableTransformer providing ::ModuleAndPackageDocumentationTransformer + } + + public val actualTypealiasAdder: Extension<DocumentableTransformer, *, *> by extending { + CoreExtensions.documentableTransformer with ActualTypealiasAdder() + } + + public val kotlinSignatureProvider: Extension<SignatureProvider, *, *> by extending { + signatureProvider providing ::KotlinSignatureProvider + } + + public val sinceKotlinTransformer: Extension<DocumentableTransformer, *, *> by extending { + CoreExtensions.documentableTransformer providing ::SinceKotlinTransformer applyIf { + SinceKotlinTransformer.shouldDisplaySinceKotlin() + } order { + before(extensionsExtractor) + } + } + + public val inheritorsExtractor: Extension<DocumentableTransformer, *, *> by extending { + CoreExtensions.documentableTransformer with InheritorsExtractorTransformer() + } + + public val undocumentedCodeReporter: Extension<DocumentableTransformer, *, *> by extending { + CoreExtensions.documentableTransformer with ReportUndocumentedTransformer() + } + + public val extensionsExtractor: Extension<DocumentableTransformer, *, *> by extending { + CoreExtensions.documentableTransformer with ExtensionExtractorTransformer() + } + + public val documentableToPageTranslator: Extension<DocumentableToPageTranslator, *, *> by extending { + CoreExtensions.documentableToPageTranslator providing ::DefaultDocumentableToPageTranslator + } + + public val docTagToContentConverter: Extension<CommentsToContentConverter, *, *> by extending { + commentsToContentConverter with DocTagToContentConverter() + } + + public val sinceKotlinTagContentProvider: Extension<CustomTagContentProvider, *, *> by extending { + customTagContentProvider with SinceKotlinTagContentProvider applyIf { + SinceKotlinTransformer.shouldDisplaySinceKotlin() + } + } + + public val pageMerger: Extension<PageTransformer, *, *> by extending { + CoreExtensions.pageTransformer providing ::PageMerger + } + + public val sourceSetMerger: Extension<PageTransformer, *, *> by extending { + CoreExtensions.pageTransformer providing ::SourceSetMergingPageTransformer + } + + public val fallbackMerger: Extension<PageMergerStrategy, *, *> by extending { + pageMergerStrategy providing { ctx -> FallbackPageMergerStrategy(ctx.logger) } + } + + public val sameMethodNameMerger: Extension<PageMergerStrategy, *, *> by extending { + pageMergerStrategy providing { ctx -> SameMethodNamePageMergerStrategy(ctx.logger) } order { + before(fallbackMerger) + } + } + + public val htmlRenderer: Extension<Renderer, *, *> by extending { + CoreExtensions.renderer providing ::HtmlRenderer + } + + public val locationProvider: Extension<LocationProviderFactory, *, *> by extending { + locationProviderFactory providing ::DokkaLocationProviderFactory + } + + public val javadocLocationProvider: Extension<ExternalLocationProviderFactory, *, *> by extending { + externalLocationProviderFactory providing ::JavadocExternalLocationProviderFactory + } + + public val dokkaLocationProvider: Extension<ExternalLocationProviderFactory, *, *> by extending { + externalLocationProviderFactory providing ::DefaultExternalLocationProviderFactory + } + + public val fileWriter: Extension<OutputWriter, *, *> by extending { + outputWriter providing ::FileWriter + } + + public val rootCreator: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors with RootCreator applyIf { !delayTemplateSubstitution } + } + + public val defaultSamplesTransformer: Extension<PageTransformer, *, *> by extending { + CoreExtensions.pageTransformer providing ::DefaultSamplesTransformer order { + before(pageMerger) + } + } + + public val sourceLinksTransformer: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors providing ::SourceLinksTransformer order { after(rootCreator) } + } + + public val navigationPageInstaller: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors providing ::NavigationPageInstaller order { after(rootCreator) } + } + + public val scriptsInstaller: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors providing ::ScriptsInstaller order { after(rootCreator) } + } + + public val stylesInstaller: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors providing ::StylesInstaller order { after(rootCreator) } + } + + public val assetsInstaller: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors with AssetsInstaller order { after(rootCreator) } applyIf { !delayTemplateSubstitution } + } + + public val customResourceInstaller: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors providing { ctx -> CustomResourceInstaller(ctx) } order { + after(stylesInstaller) + after(scriptsInstaller) + after(assetsInstaller) + } + } + + public val packageListCreator: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors providing { + PackageListCreator(it, RecognizedLinkFormat.DokkaHtml) + } order { after(rootCreator) } + } + + public val sourcesetDependencyAppender: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors providing ::SourcesetDependencyAppender order { after(rootCreator) } + } + + public val resolveLinkConsumer: Extension<ImmediateHtmlCommandConsumer, *, *> by extending { + immediateHtmlCommandConsumer with ResolveLinkConsumer + } + public val replaceVersionConsumer: Extension<ImmediateHtmlCommandConsumer, *, *> by extending { + immediateHtmlCommandConsumer providing ::ReplaceVersionsConsumer + } + public val pathToRootConsumer: Extension<ImmediateHtmlCommandConsumer, *, *> by extending { + immediateHtmlCommandConsumer with PathToRootConsumer + } + public val baseSearchbarDataInstaller: Extension<PageTransformer, *, *> by extending { + htmlPreprocessors providing ::SearchbarDataInstaller order { after(sourceLinksTransformer) } + } + + //<editor-fold desc="Deprecated API left for compatibility"> + @Suppress("DEPRECATION_ERROR") + @Deprecated(message = org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val kotlinAnalysis: ExtensionPoint<org.jetbrains.dokka.analysis.KotlinAnalysis> by extensionPoint() + + @Suppress("DEPRECATION_ERROR") + @Deprecated(message = org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val externalDocumentablesProvider: ExtensionPoint<org.jetbrains.dokka.base.translators.descriptors.ExternalDocumentablesProvider> by extensionPoint() + + @Suppress("DEPRECATION_ERROR") + @Deprecated(message = org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val externalClasslikesTranslator: ExtensionPoint<org.jetbrains.dokka.base.translators.descriptors.ExternalClasslikesTranslator> by extensionPoint() + + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated(message = org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val descriptorToDocumentableTranslator: org.jetbrains.dokka.plugability.Extension<org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator, *, *> + get() = throw org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError() + + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated(message = org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val psiToDocumentableTranslator: org.jetbrains.dokka.plugability.Extension<org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator, *, *> + get() = throw org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError() + + @Suppress("DEPRECATION_ERROR", "DeprecatedCallableAddReplaceWith") + @Deprecated(message = org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val defaultKotlinAnalysis: org.jetbrains.dokka.plugability.Extension<org.jetbrains.dokka.analysis.KotlinAnalysis, *, *> + get() = throw org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError() + + @Suppress("DEPRECATION_ERROR", "DeprecatedCallableAddReplaceWith") + @Deprecated(message = org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val defaultExternalDocumentablesProvider: org.jetbrains.dokka.plugability.Extension<org.jetbrains.dokka.base.translators.descriptors.ExternalDocumentablesProvider, *, *> + get() = throw org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError() + + @Suppress("DEPRECATION_ERROR", "DeprecatedCallableAddReplaceWith") + @Deprecated(message = org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val defaultExternalClasslikesTranslator: org.jetbrains.dokka.plugability.Extension<org.jetbrains.dokka.base.translators.descriptors.ExternalClasslikesTranslator, *, *> + get() = throw org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError() + //</editor-fold> + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = + PluginApiPreviewAcknowledgement +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBaseConfiguration.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBaseConfiguration.kt new file mode 100644 index 00000000..34195f65 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBaseConfiguration.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base + +import org.jetbrains.dokka.plugability.ConfigurableBlock +import java.io.File +import java.time.Year + +public data class DokkaBaseConfiguration( + var customStyleSheets: List<File> = defaultCustomStyleSheets, + var customAssets: List<File> = defaultCustomAssets, + var separateInheritedMembers: Boolean = separateInheritedMembersDefault, + var footerMessage: String = defaultFooterMessage, + var mergeImplicitExpectActualDeclarations: Boolean = mergeImplicitExpectActualDeclarationsDefault, + var templatesDir: File? = defaultTemplatesDir, + var homepageLink: String? = null, +) : ConfigurableBlock { + public companion object { + public val defaultFooterMessage: String = "© ${Year.now().value} Copyright" + public val defaultCustomStyleSheets: List<File> = emptyList() + public val defaultCustomAssets: List<File> = emptyList() + public const val separateInheritedMembersDefault: Boolean = false + public const val mergeImplicitExpectActualDeclarationsDefault: Boolean = false + public val defaultTemplatesDir: File? = null + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/AnalysisApiDeprecatedError.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/AnalysisApiDeprecatedError.kt new file mode 100644 index 00000000..52280b3e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/AnalysisApiDeprecatedError.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.deprecated + +import org.jetbrains.dokka.InternalDokkaApi + +// TODO all API that mentions this message or error can be removed in Dokka >= 2.1 + +internal const val ANALYSIS_API_DEPRECATION_MESSAGE = + "Dokka's Analysis API has been reworked. Please, see the following issue for details and migration options: " + + "https://github.com/Kotlin/dokka/issues/3099" + +@InternalDokkaApi +public class AnalysisApiDeprecatedError : Error(ANALYSIS_API_DEPRECATION_MESSAGE) diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/KotlinAnalysisDeprecatedApi.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/KotlinAnalysisDeprecatedApi.kt new file mode 100644 index 00000000..1d9e7e9f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/KotlinAnalysisDeprecatedApi.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch", "FunctionName", "UNUSED_PARAMETER", "unused", "DEPRECATION_ERROR", + "DeprecatedCallableAddReplaceWith", "unused" +) + +package org.jetbrains.dokka.analysis + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE +import org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.utilities.DokkaLogger +import java.io.Closeable + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public abstract class KotlinAnalysis( + private val parent: KotlinAnalysis? = null +) : Closeable { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public operator fun get(key: DokkaConfiguration.DokkaSourceSet): AnalysisContext = throw AnalysisApiDeprecatedError() + + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public operator fun get(key: DokkaSourceSetID): AnalysisContext = throw AnalysisApiDeprecatedError() + + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + protected abstract fun find(sourceSetID: DokkaSourceSetID): AnalysisContext? +} + +public class AnalysisContext(environment: Any, facade: Any, private val analysisEnvironment: Any) : Closeable { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val environment: Any get() = throw AnalysisApiDeprecatedError() + + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public val facade: Any get() = throw AnalysisApiDeprecatedError() + + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public operator fun component1(): Any = throw AnalysisApiDeprecatedError() + + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public operator fun component2(): Any = throw AnalysisApiDeprecatedError() + + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + override fun close() { throw AnalysisApiDeprecatedError() } +} + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public class DokkaAnalysisConfiguration(public val ignoreCommonBuiltIns: Boolean = false) + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public fun KotlinAnalysis(context: DokkaContext): KotlinAnalysis = throw AnalysisApiDeprecatedError() + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public fun KotlinAnalysis( + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + logger: DokkaLogger, + analysisConfiguration: DokkaAnalysisConfiguration = DokkaAnalysisConfiguration() +): KotlinAnalysis = throw AnalysisApiDeprecatedError() + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public fun ProjectKotlinAnalysis( + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + logger: DokkaLogger, + analysisConfiguration: DokkaAnalysisConfiguration = DokkaAnalysisConfiguration() +): KotlinAnalysis = throw AnalysisApiDeprecatedError() + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public fun SamplesKotlinAnalysis( + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + logger: DokkaLogger, + projectKotlinAnalysis: KotlinAnalysis, + analysisConfiguration: DokkaAnalysisConfiguration = DokkaAnalysisConfiguration() +): KotlinAnalysis = throw AnalysisApiDeprecatedError() + diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/ParsersDeprecatedAPI.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/ParsersDeprecatedAPI.kt new file mode 100644 index 00000000..55b1daab --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/ParsersDeprecatedAPI.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch", "DEPRECATION_ERROR", "DeprecatedCallableAddReplaceWith", "unused") + +package org.jetbrains.dokka.base.parsers + +import org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE +import org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.DocTag +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.TagWrapper + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public abstract class Parser { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public open fun parseStringToDocNode(extractedString: String): DocTag = throw AnalysisApiDeprecatedError() + + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public open fun preparse(text: String): String = throw AnalysisApiDeprecatedError() + + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public open fun parseTagWithBody(tagName: String, content: String): TagWrapper = throw AnalysisApiDeprecatedError() +} + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public open class MarkdownParser( + private val externalDri: (String) -> DRI?, + private val kdocLocation: String?, +) : Parser() { + public companion object { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public fun parseFromKDocTag( + @Suppress("UNUSED_PARAMETER") kDocTag: Any?, + @Suppress("UNUSED_PARAMETER") externalDri: (String) -> DRI?, + @Suppress("UNUSED_PARAMETER") kdocLocation: String?, + @Suppress("UNUSED_PARAMETER") parseWithChildren: Boolean = true + ): DocumentationNode = throw AnalysisApiDeprecatedError() + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/ParsersFactoriesDeprecatedAPI.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/ParsersFactoriesDeprecatedAPI.kt new file mode 100644 index 00000000..7b84803c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/ParsersFactoriesDeprecatedAPI.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("DeprecatedCallableAddReplaceWith", "PackageDirectoryMismatch", "unused") + +package org.jetbrains.dokka.base.parsers.factories + +import org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE +import org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.DocTag + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public object DocTagsFromStringFactory { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public fun getInstance( + @Suppress("UNUSED_PARAMETER") name: String, + @Suppress("UNUSED_PARAMETER") children: List<DocTag> = emptyList(), + @Suppress("UNUSED_PARAMETER") params: Map<String, String> = emptyMap(), + @Suppress("UNUSED_PARAMETER") body: String? = null, + @Suppress("UNUSED_PARAMETER") dri: DRI? = null, + ): DocTag = throw AnalysisApiDeprecatedError() +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/TranslatorDescriptorsDeprecatedAPI.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/TranslatorDescriptorsDeprecatedAPI.kt new file mode 100644 index 00000000..87d82ccf --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/TranslatorDescriptorsDeprecatedAPI.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch", "DEPRECATION_ERROR", "DeprecatedCallableAddReplaceWith", "unused") + +package org.jetbrains.dokka.base.translators.descriptors + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE +import org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DClasslike +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.sources.AsyncSourceToDocumentableTranslator + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public fun interface ExternalDocumentablesProvider { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public fun findClasslike(dri: DRI, sourceSet: DokkaConfiguration.DokkaSourceSet): DClasslike? +} + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public class DefaultExternalDocumentablesProvider( + @Suppress("UNUSED_PARAMETER") context: DokkaContext +) : ExternalDocumentablesProvider { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + override fun findClasslike(dri: DRI, sourceSet: DokkaConfiguration.DokkaSourceSet): DClasslike = + throw AnalysisApiDeprecatedError() +} + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public fun interface ExternalClasslikesTranslator { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + public fun translateClassDescriptor(descriptor: Any, sourceSet: DokkaConfiguration.DokkaSourceSet): DClasslike +} + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public class DefaultDescriptorToDocumentableTranslator( + private val context: DokkaContext +) : AsyncSourceToDocumentableTranslator, ExternalClasslikesTranslator { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + override suspend fun invokeSuspending(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext, ): DModule = + throw AnalysisApiDeprecatedError() + + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + override fun translateClassDescriptor(descriptor: Any, sourceSet: DokkaConfiguration.DokkaSourceSet): DClasslike = + throw AnalysisApiDeprecatedError() +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/TranslatorPsiDeprecatedAPI.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/TranslatorPsiDeprecatedAPI.kt new file mode 100644 index 00000000..1906a7b1 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/TranslatorPsiDeprecatedAPI.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch", "DeprecatedCallableAddReplaceWith", "unused") + +package org.jetbrains.dokka.base.translators.psi + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.deprecated.ANALYSIS_API_DEPRECATION_MESSAGE +import org.jetbrains.dokka.base.deprecated.AnalysisApiDeprecatedError +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.sources.AsyncSourceToDocumentableTranslator + +@Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) +public class DefaultPsiToDocumentableTranslator( + @Suppress("UNUSED_PARAMETER") context: DokkaContext, +) : AsyncSourceToDocumentableTranslator { + @Deprecated(message = ANALYSIS_API_DEPRECATION_MESSAGE, level = DeprecationLevel.ERROR) + override suspend fun invokeSuspending( + sourceSet: DokkaConfiguration.DokkaSourceSet, + context: DokkaContext, + ): DModule = throw AnalysisApiDeprecatedError() +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/generation/SingleModuleGeneration.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/generation/SingleModuleGeneration.kt new file mode 100644 index 00000000..8ea109b9 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/generation/SingleModuleGeneration.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.generation + +import kotlinx.coroutines.* +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaException +import org.jetbrains.dokka.Timer +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.generation.Generation +import org.jetbrains.dokka.generation.exitGenerationGracefully +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import org.jetbrains.dokka.transformers.sources.AsyncSourceToDocumentableTranslator +import org.jetbrains.dokka.utilities.parallelMap +import org.jetbrains.dokka.utilities.report + +public class SingleModuleGeneration(private val context: DokkaContext) : Generation { + + override fun Timer.generate() { + report("Validity check") + validityCheck(context) + + // Step 1: translate sources into documentables & transform documentables (change internally) + report("Creating documentation models") + val modulesFromPlatforms = createDocumentationModels() + + report("Transforming documentation model before merging") + val transformedDocumentationBeforeMerge = transformDocumentationModelBeforeMerge(modulesFromPlatforms) + + report("Merging documentation models") + val transformedDocumentationAfterMerge = mergeDocumentationModels(transformedDocumentationBeforeMerge) + ?: exitGenerationGracefully("Nothing to document") + + report("Transforming documentation model after merging") + val transformedDocumentation = transformDocumentationModelAfterMerge(transformedDocumentationAfterMerge) + + // Step 2: Generate pages & transform them (change internally) + report("Creating pages") + val pages = createPages(transformedDocumentation) + + report("Transforming pages") + val transformedPages = transformPages(pages) + + // Step 3: Rendering + report("Rendering") + render(transformedPages) + + report("Running post-actions") + runPostActions() + + reportAfterRendering() + } + + override val generationName: String = "documentation for ${context.configuration.moduleName}" + + /** + * Implementation note: it runs in a separated single thread due to existing support of coroutines (see #2936) + */ + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) + public fun createDocumentationModels(): List<DModule> = newSingleThreadContext("Generating documentable model").use { coroutineContext -> // see https://github.com/Kotlin/dokka/issues/3151 + runBlocking(coroutineContext) { + context.configuration.sourceSets.parallelMap { sourceSet -> translateSources(sourceSet, context) }.flatten() + .also { modules -> if (modules.isEmpty()) exitGenerationGracefully("Nothing to document") } + } + } + + + public fun transformDocumentationModelBeforeMerge(modulesFromPlatforms: List<DModule>): List<DModule> { + return context.plugin<DokkaBase>() + .query { preMergeDocumentableTransformer } + .fold(modulesFromPlatforms) { acc, t -> t(acc) } + } + + public fun mergeDocumentationModels(modulesFromPlatforms: List<DModule>): DModule? = + context.single(CoreExtensions.documentableMerger).invoke(modulesFromPlatforms) + + public fun transformDocumentationModelAfterMerge(documentationModel: DModule): DModule = + context[CoreExtensions.documentableTransformer].fold(documentationModel) { acc, t -> t(acc, context) } + + public fun createPages(transformedDocumentation: DModule): RootPageNode = + context.single(CoreExtensions.documentableToPageTranslator).invoke(transformedDocumentation) + + public fun transformPages(pages: RootPageNode): RootPageNode = + context[CoreExtensions.pageTransformer].fold(pages) { acc, t -> t(acc) } + + public fun render(transformedPages: RootPageNode) { + context.single(CoreExtensions.renderer).render(transformedPages) + } + + public fun runPostActions() { + context[CoreExtensions.postActions].forEach { it() } + } + + public fun validityCheck(context: DokkaContext) { + val (preGenerationCheckResult, checkMessages) = context[CoreExtensions.preGenerationCheck].fold( + Pair(true, emptyList<String>()) + ) { acc, checker -> checker() + acc } + if (!preGenerationCheckResult) throw DokkaException( + "Pre-generation validity check failed: ${checkMessages.joinToString(",")}" + ) + } + + public fun reportAfterRendering() { + context.unusedPoints.takeIf { it.isNotEmpty() }?.also { + context.logger.info("Unused extension points found: ${it.joinToString(", ")}") + } + + context.logger.report() + + if (context.configuration.failOnWarning && (context.logger.warningsCount > 0 || context.logger.errorsCount > 0)) { + throw DokkaException( + "Failed with warningCount=${context.logger.warningsCount} and errorCount=${context.logger.errorsCount}" + ) + } + } + + private suspend fun translateSources(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext) = + context[CoreExtensions.sourceToDocumentableTranslator].parallelMap { translator -> + when (translator) { + is AsyncSourceToDocumentableTranslator -> translator.invokeSuspending(sourceSet, context) + else -> translator.invoke(sourceSet, context) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/DefaultRenderer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/DefaultRenderer.kt new file mode 100644 index 00000000..eed7794e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/DefaultRenderer.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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.DokkaException +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.local.LocationProvider +import org.jetbrains.dokka.model.DisplaySourceSet +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 + +public abstract class DefaultRenderer<T>( + protected val context: DokkaContext +) : Renderer { + + protected val outputWriter: OutputWriter = context.plugin<DokkaBase>().querySingle { outputWriter } + + protected lateinit var locationProvider: LocationProvider + private set + + protected open val preprocessors: Iterable<PageTransformer> = emptyList() + + public abstract fun T.buildHeader(level: Int, node: ContentHeader, content: T.() -> Unit) + public abstract fun T.buildLink(address: String, content: T.() -> Unit) + public abstract fun T.buildList( + node: ContentList, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? = null + ) + + public abstract fun T.buildLineBreak() + public open fun T.buildLineBreak(node: ContentBreakLine, pageContext: ContentPage) { + buildLineBreak() + } + + public abstract fun T.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) + public abstract fun T.buildTable( + node: ContentTable, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? = null + ) + + public abstract fun T.buildText(textNode: ContentText) + public abstract fun T.buildNavigation(page: PageNode) + + public abstract fun buildPage(page: ContentPage, content: (T, ContentPage) -> Unit): String + public abstract fun buildError(node: ContentNode) + + public open fun T.buildPlatformDependent( + content: PlatformHintedContent, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + buildContentNode(content.inner, pageContext) + } + + public open fun T.buildGroup( + node: ContentGroup, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? = null + ) { + wrapGroup(node, pageContext) { node.children.forEach { it.build(this, pageContext, sourceSetRestriction) } } + } + + public open fun T.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) { + node.children.forEach { it.build(this, pageContext) } + } + + public open fun T.wrapGroup(node: ContentGroup, pageContext: ContentPage, childrenCallback: T.() -> Unit) { + childrenCallback() + } + + public open fun T.buildText( + nodes: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? = null + ) { + nodes.forEach { it.build(this, pageContext, sourceSetRestriction) } + } + + public open fun T.buildCodeBlock(code: ContentCodeBlock, pageContext: ContentPage) { + code.children.forEach { it.build(this, pageContext) } + } + + public open fun T.buildCodeInline(code: ContentCodeInline, pageContext: ContentPage) { + code.children.forEach { it.build(this, pageContext) } + } + + public open fun T.buildHeader( + node: ContentHeader, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? = null + ) { + buildHeader(node.level, node) { node.children.forEach { it.build(this, pageContext, sourceSetRestriction) } } + } + + public open fun ContentNode.build( + builder: T, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? = null + ) { + builder.buildContentNode(this, pageContext, sourceSetRestriction) + } + + public fun T.buildContentNode( + node: ContentNode, + pageContext: ContentPage, + sourceSetRestriction: DisplaySourceSet + ) { + buildContentNode(node, pageContext, setOf(sourceSetRestriction)) + } + + public open fun T.buildContentNode( + node: ContentNode, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? = null + ) { + if (sourceSetRestriction.isNullOrEmpty() || 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 -> buildDRILink(node, pageContext, sourceSetRestriction) + is ContentResolvedLink -> buildResolvedLink(node, 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 -> buildLineBreak(node, pageContext) + is PlatformHintedContent -> buildPlatformDependent(node, pageContext, sourceSetRestriction) + is ContentDivergentGroup -> buildDivergent(node, pageContext) + is ContentDivergentInstance -> buildDivergentInstance(node, pageContext) + else -> buildError(node) + } + } + } + + public open fun T.buildDRILink( + node: ContentDRILink, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + locationProvider.resolve(node.address, node.sourceSets, pageContext)?.let { address -> + buildLink(address) { + buildText(node.children, pageContext, sourceSetRestriction) + } + } ?: buildText(node.children, pageContext, sourceSetRestriction) + } + + public open fun T.buildResolvedLink( + node: ContentResolvedLink, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + buildLink(node.address) { + buildText(node.children, pageContext, sourceSetRestriction) + } + } + + public open fun T.buildDivergentInstance(node: ContentDivergentInstance, pageContext: ContentPage) { + node.before?.build(this, pageContext) + node.divergent.build(this, pageContext) + node.after?.build(this, pageContext) + } + + public open fun buildPageContent(context: T, page: ContentPage) { + context.buildNavigation(page) + page.content.build(context, page) + } + + public open suspend fun renderPage(page: PageNode) { + val path by lazy { + locationProvider.resolve(page, skipExtension = true) + ?: throw DokkaException("Cannot resolve path for ${page.name}") + } + 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") + is RenderingStrategy.DriLocationResolvableWrite -> outputWriter.write( + path, + strategy.contentToResolve { dri, sourcesets -> + locationProvider.resolve(dri, sourcesets) + }, + "" + ) + is RenderingStrategy.PageLocationResolvableWrite -> outputWriter.write( + path, + strategy.contentToResolve { pageToLocate, context -> + locationProvider.resolve(pageToLocate, context) + }, + "" + ) + 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, DisplaySourceSet) -> String, + afterTransformer: (ContentDivergentInstance, ContentPage, DisplaySourceSet) -> 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, DisplaySourceSet> + +public fun ContentPage.sourceSets(): Set<DisplaySourceSet> = this.content.sourceSets diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/FileWriter.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/FileWriter.kt new file mode 100644 index 00000000..1a1c3b42 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/FileWriter.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +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.* + +public class FileWriter( + public val context: DokkaContext +): OutputWriter { + private val createdFiles: MutableSet<String> = mutableSetOf() + private val createdFilesMutex = Mutex() + private val jarUriPrefix = "jar:file:" + private val root = context.configuration.outputDir + + override suspend fun write(path: String, text: String, ext: String) { + if (checkFileCreated(path)) return + + try { + val dir = Paths.get(root.absolutePath, path.dropLastWhile { it != '/' }).toFile() + withContext(Dispatchers.IO) { + dir.mkdirsOrFail() + Files.write(Paths.get(root.absolutePath, "$path$ext"), text.lines()) + } + } catch (e: Throwable) { + context.logger.error("Failed to write $this. ${e.message}") + e.printStackTrace() + } + } + + private suspend fun checkFileCreated(path: String): Boolean = createdFilesMutex.withLock { + if (createdFiles.contains(path)) { + context.logger.error("An attempt to write ${root}/$path several times!") + return true + } + createdFiles.add(path) + return false + } + + override suspend fun writeResources(pathFrom: String, pathTo: String) { + if (javaClass.getResource(pathFrom)?.toURI()?.toString()?.startsWith(jarUriPrefix) == true) { + copyFromJar(pathFrom, pathTo) + } else { + copyFromDirectory(pathFrom, pathTo) + } + } + + + private suspend fun copyFromDirectory(pathFrom: String, pathTo: String) { + val dest = Paths.get(root.path, pathTo).toFile() + val uri = javaClass.getResource(pathFrom)?.toURI() + val file = uri?.let { File(it) } ?: File(pathFrom) + withContext(Dispatchers.IO) { + file.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.path, pathTo).toFile() + if(dest.isDirectory){ + dest.mkdirsOrFail() + } else { + dest.parentFile.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.path, rebase(dirPath)).toFile().mkdirsOrFail() + } + } else { + val filePath = file.toAbsolutePath().toString() + withContext(Dispatchers.IO) { + Paths.get(root.path, rebase(filePath)).toFile().writeBytes( + this@FileWriter.javaClass.getResourceAsStream(filePath).use { it?.readBytes() } + ?: throw IllegalStateException("Can not get a resource from $filePath") + ) + } + } + } + } + + 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) + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/OutputWriter.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/OutputWriter.kt new file mode 100644 index 00000000..3fdd1802 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/OutputWriter.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers + +public interface OutputWriter { + + public suspend fun write(path: String, text: String, ext: String) + public suspend fun writeResources(pathFrom: String, pathTo: String) +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/PackageListService.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/PackageListService.kt new file mode 100644 index 00000000..3ed6cd21 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/PackageListService.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.shared.LinkFormat +import org.jetbrains.dokka.base.resolvers.shared.PackageList.Companion.DOKKA_PARAM_PREFIX +import org.jetbrains.dokka.base.resolvers.shared.PackageList.Companion.MODULE_DELIMITER +import org.jetbrains.dokka.base.resolvers.shared.PackageList.Companion.SINGLE_MODULE_NAME +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle + +public class PackageListService( + public val context: DokkaContext, + public val rootPage: RootPageNode +) { + + public fun createPackageList(module: ModulePage, format: LinkFormat): String { + + val packages = mutableSetOf<String>() + val nonStandardLocations = mutableMapOf<String, String>() + + val locationProvider = + context.plugin<DokkaBase>().querySingle { locationProviderFactory }.getLocationProvider(rootPage) + + fun visit(node: PageNode) { + if (node is PackagePage) { + node.name + .takeUnless { name -> name.startsWith("[") && name.endsWith("]") } // Do not include the package name for declarations without one + ?.let { packages.add(it) } + } + + val contentPage = node as? ContentPage + contentPage?.dri?.forEach { dri -> + val nodeLocation = locationProvider.resolve(node, context = module, skipExtension = true) + ?: run { context.logger.error("Cannot resolve path for ${node.name}!"); null } + + if (dri != DRI.topLevel && locationProvider.expectedLocationForDri(dri) != nodeLocation) { + nonStandardLocations[dri.toString()] = "$nodeLocation.${format.linkExtension}" + } + } + + node.children.forEach { visit(it) } + } + + visit(module) + return renderPackageList( + nonStandardLocations = nonStandardLocations, + modules = mapOf(SINGLE_MODULE_NAME to packages), + format = format.formatName, + linkExtension = format.linkExtension + ) + } + + public companion object { + public fun renderPackageList( + nonStandardLocations: Map<String, String>, + modules: Map<String, Set<String>>, + format: String, + linkExtension: String + ): String = buildString { + appendLine("$DOKKA_PARAM_PREFIX.format:${format}") + appendLine("$DOKKA_PARAM_PREFIX.linkExtension:${linkExtension}") + nonStandardLocations.map { (signature, location) -> + "$DOKKA_PARAM_PREFIX.location:$signature\u001f$location" + }.sorted().joinTo(this, separator = "\n", postfix = "\n") + + modules.mapNotNull { (module, packages) -> + ("$MODULE_DELIMITER$module\n".takeIf { module != SINGLE_MODULE_NAME }.orEmpty() + + packages.filter(String::isNotBlank).sorted().joinToString(separator = "\n")) + .takeIf { packages.isNotEmpty() } + }.joinTo(this, separator = "\n", postfix = "\n") + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/TabSortingStrategy.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/TabSortingStrategy.kt new file mode 100644 index 00000000..665b6717 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/TabSortingStrategy.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.pages.ContentNode + +public interface TabSortingStrategy { + public fun <T: ContentNode> sort(tabs: Collection<T>) : List<T> +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/contentTypeChecking.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/contentTypeChecking.kt new file mode 100644 index 00000000..0fcb0efb --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/contentTypeChecking.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.base.renderers.HtmlFileExtensions.imageExtensions +import org.jetbrains.dokka.pages.ContentEmbeddedResource +import java.io.File + +public fun ContentEmbeddedResource.isImage(): Boolean { + return File(address).extension.toLowerCase() in imageExtensions +} + +public val String.URIExtension: String + get() = substringBefore('?').substringAfterLast('.') + +public fun String.isImage(): Boolean = + URIExtension in imageExtensions + +public object HtmlFileExtensions { + public val imageExtensions: Set<String> = setOf("png", "jpg", "jpeg", "gif", "bmp", "tif", "webp", "svg") +} + diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlContent.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlContent.kt new file mode 100644 index 00000000..1ef6e04c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlContent.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html + +import org.jetbrains.dokka.pages.ContentBreakLine +import org.jetbrains.dokka.pages.Style + + +/** + * Html-specific style that represents <hr> tag if used in conjunction with [ContentBreakLine] + */ +internal object HorizontalBreakLineStyle : Style { + // this exists as a simple internal solution to avoid introducing unnecessary public API on content level. + // If you have the need to implement proper horizontal divider (i.e to support `---` markdown element), + // consider removing this and providing proper API for all formats and levels +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt new file mode 100644 index 00000000..083876d5 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt @@ -0,0 +1,1013 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.renderers.* +import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer +import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelFactory +import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelMerger +import org.jetbrains.dokka.base.renderers.html.innerTemplating.DokkaTemplateTypes +import org.jetbrains.dokka.base.renderers.html.innerTemplating.HtmlTemplater +import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint +import org.jetbrains.dokka.base.resolvers.local.DokkaBaseLocationProvider +import org.jetbrains.dokka.base.templating.* +import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions +import org.jetbrains.dokka.base.translators.documentables.shouldDocumentConstructors +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.pages.HtmlContent +import org.jetbrains.dokka.plugability.* +import org.jetbrains.dokka.transformers.pages.PageTransformer +import org.jetbrains.dokka.utilities.htmlEscape + +internal const val TEMPLATE_REPLACEMENT: String = "###" +internal const val TOGGLEABLE_CONTENT_TYPE_ATTR = "data-togglable" + +public open class HtmlRenderer( + context: DokkaContext +) : DefaultRenderer<FlowContent>(context) { + private val sourceSetDependencyMap: Map<DokkaSourceSetID, List<DokkaSourceSetID>> = + context.configuration.sourceSets.associate { sourceSet -> + sourceSet.sourceSetID to context.configuration.sourceSets + .map { it.sourceSetID } + .filter { it in sourceSet.dependentSourceSets } + } + + private val templateModelFactories = listOf(DefaultTemplateModelFactory(context)) // TODO: Make extension point + private val templateModelMerger = DefaultTemplateModelMerger() + private val templater = HtmlTemplater(context).apply { + setupSharedModel(templateModelMerger.invoke(templateModelFactories) { buildSharedModel() }) + } + + private var shouldRenderSourceSetTabs: Boolean = false + + override val preprocessors: List<PageTransformer> = context.plugin<DokkaBase>().query { htmlPreprocessors } + + /** + * Tabs themselves are created in HTML plugin since, currently, only HTML format supports them. + * [TabbedContentType] is used to mark content that should be inside tab content. + * A tab can display multiple [TabbedContentType]. + * The content style [ContentStyle.TabbedContent] is used to determine where tabs will be generated. + * + * @see TabbedContentType + * @see ContentStyle.TabbedContent + */ + private fun createTabs(pageContext: ContentPage): List<ContentTab> { + return when(pageContext) { + is ClasslikePage -> createTabsForClasslikes(pageContext) + is PackagePage -> createTabsForPackage(pageContext) + else -> throw IllegalArgumentException("Page ${pageContext.name} cannot have tabs") + } + } + + private fun createTabsForClasslikes(page: ClasslikePage): List<ContentTab> { + val documentables = page.documentables + val csEnum = documentables.filterIsInstance<DEnum>() + val csWithConstructor = documentables.filterIsInstance<WithConstructors>() + val scopes = documentables.filterIsInstance<WithScope>() + val constructorsToDocumented = csWithConstructor.flatMap { it.constructors } + + val containsRenderableConstructors = constructorsToDocumented.isNotEmpty() && documentables.shouldDocumentConstructors() + val containsRenderableMembers = + containsRenderableConstructors || scopes.any { it.classlikes.isNotEmpty() || it.functions.isNotEmpty() || it.properties.isNotEmpty() } + + @Suppress("UNCHECKED_CAST") + val extensions = (documentables as List<WithExtraProperties<DClasslike>>).flatMap { + it.extra[CallableExtensions]?.extensions + ?.filterIsInstance<Documentable>().orEmpty() + } + .distinctBy { it.sourceSets to it.dri } // [Documentable] has expensive equals/hashCode at the moment, see #2620 + return listOfNotNull( + if(!containsRenderableMembers) null else + ContentTab( + "Members", + listOf( + BasicTabbedContentType.CONSTRUCTOR, + BasicTabbedContentType.TYPE, + BasicTabbedContentType.PROPERTY, + BasicTabbedContentType.FUNCTION + ) + ), + if (extensions.isEmpty()) null else ContentTab( + "Members & Extensions", + listOf( + BasicTabbedContentType.CONSTRUCTOR, + BasicTabbedContentType.TYPE, + BasicTabbedContentType.PROPERTY, + BasicTabbedContentType.FUNCTION, + BasicTabbedContentType.EXTENSION_PROPERTY, + BasicTabbedContentType.EXTENSION_FUNCTION + ) + ), + if(csEnum.isEmpty()) null else ContentTab( + "Entries", + listOf( + BasicTabbedContentType.ENTRY + ) + ) + ) + } + + private fun createTabsForPackage(page: PackagePage): List<ContentTab> { + val p = page.documentables.single() as DPackage + return listOfNotNull( + if (p.typealiases.isEmpty() && p.classlikes.isEmpty()) null else ContentTab( + "Types", + listOf( + BasicTabbedContentType.TYPE, + ) + ), + if (p.functions.isEmpty()) null else ContentTab( + "Functions", + listOf( + BasicTabbedContentType.FUNCTION, + BasicTabbedContentType.EXTENSION_FUNCTION, + ) + ), + if (p.properties.isEmpty()) null else ContentTab( + "Properties", + listOf( + BasicTabbedContentType.PROPERTY, + BasicTabbedContentType.EXTENSION_PROPERTY, + ) + ) + ) + } + + private fun <R> TagConsumer<R>.prepareForTemplates() = + if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this + else ImmediateResolutionTagConsumer(this, context) + + 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 contentTabs = createTabs(pageContext) + + div(classes = "tabs-section") { + attributes["tabs-section"] = "tabs-section" + contentTabs.forEachIndexed { index, contentTab -> + button(classes = "section-tab") { + if (index == 0) attributes["data-active"] = "" + attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] = + contentTab.tabbedContentTypes.joinToString(",") { it.toHtmlAttribute() } + text(contentTab.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() + } + node.hasStyle(ContentStyle.KDocTag) -> span("kdoc-tag") { childrenCallback() } + node.hasStyle(ContentStyle.Footnote) -> div("footnote") { childrenCallback() } + 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 == SymbolContentKind.Parameters -> { + span("parameters $additionalClasses") { + childrenCallback() + } + } + node.dci.kind == SymbolContentKind.Parameter -> { + span("parameter $additionalClasses") { + childrenCallback() + } + } + node.hasStyle(TextStyle.InlineComment) -> div("inline-comment") { childrenCallback() } + node.dci.kind == ContentKind.BriefComment -> div("brief $additionalClasses") { childrenCallback() } + node.dci.kind == ContentKind.Cover -> div("cover $additionalClasses") { //TODO this can be removed + childrenCallback() + } + node.dci.kind == ContentKind.Deprecation -> div("deprecation-content") { childrenCallback() } + node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() } + node.hasStyle(TextStyle.Block) -> div(additionalClasses) { + childrenCallback() + } + node.hasStyle(TextStyle.Quotation) -> blockQuote(additionalClasses) { childrenCallback() } + node.hasStyle(TextStyle.FloatingRight) -> span("clearfix") { span("floating-right") { childrenCallback() } } + node.hasStyle(TextStyle.Strikethrough) -> strike { childrenCallback() } + node.isAnchorable -> buildAnchor( + node.anchor!!, + node.anchorLabel!!, + node.buildSourceSetFilterValues() + ) { childrenCallback() } + node.extra[InsertTemplateExtra] != null -> node.extra[InsertTemplateExtra]?.let { templateCommand(it.command) } + ?: Unit + node.hasStyle(ListStyle.DescriptionTerm) -> DT(emptyMap(), consumer).visit { + this@wrapGroup.childrenCallback() + } + node.hasStyle(ListStyle.DescriptionDetails) -> DD(emptyMap(), consumer).visit { + this@wrapGroup.childrenCallback() + } + node.extra.extraTabbedContentType() != null -> div() { + node.extra.extraTabbedContentType()?.let { attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] = it.value.toHtmlAttribute() } + this@wrapGroup.childrenCallback() + } + else -> childrenCallback() + } + } + + private fun FlowContent.copyButton() = span(classes = "top-right-position") { + span("copy-icon") + copiedPopup("Content copied to clipboard", "popup-to-left") + } + + private fun FlowContent.copiedPopup(notificationContent: String, additionalClasses: String = "") = + div("copy-popup-wrapper $additionalClasses") { + span("copy-popup-icon") + span { + text(notificationContent) + } + } + + override fun FlowContent.buildPlatformDependent( + content: PlatformHintedContent, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + buildPlatformDependent( + content.sourceSets.filter { + sourceSetRestriction == null || it in sourceSetRestriction + }.associateWith { setOf(content.inner) }, + pageContext, + content.extra, + content.style + ) + } + + private fun FlowContent.buildPlatformDependent( + nodes: Map<DisplaySourceSet, Collection<ContentNode>>, + pageContext: ContentPage, + extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), + styles: Set<Style> = emptySet(), + shouldHaveTabs: Boolean = shouldRenderSourceSetTabs + ) { + val contents = contentsForSourceSetDependent(nodes, pageContext) + val isOnlyCommonContent = contents.singleOrNull()?.let { (sourceSet, _) -> + sourceSet.platform == Platform.common + && sourceSet.name.equals("common", ignoreCase = true) + && sourceSet.sourceSetIDs.all.all { sourceSetDependencyMap[it]?.isEmpty() == true } + } ?: false + + // little point in rendering a single "common" tab - it can be + // assumed that code without any tabs is common by default + val renderTabs = shouldHaveTabs && !isOnlyCommonContent + + val divStyles = "platform-hinted ${styles.joinToString()}" + if (renderTabs) " with-platform-tabs" else "" + div(divStyles) { + attributes["data-platform-hinted"] = "data-platform-hinted" + extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } + if (renderTabs) { + 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.sourceSetIDs.merged.toString() + attributes["data-filterable-set"] = pair.first.sourceSetIDs.merged.toString() + if (index == 0) attributes["data-active"] = "" + attributes["data-toggle"] = pair.first.sourceSetIDs.merged.toString() + text(pair.first.name) + } + } + } + } + contents.forEach { + consumer.onTagContentUnsafe { +it.second } + } + } + } + + private fun contentsForSourceSetDependent( + nodes: Map<DisplaySourceSet, Collection<ContentNode>>, + pageContext: ContentPage, + ): List<Pair<DisplaySourceSet, String>> { + var counter = 0 + return nodes.toList().map { (sourceSet, elements) -> + val htmlContent = createHTML(prettyPrint = false).prepareForTemplates().div { + elements.forEach { + buildContentNode(it, pageContext, sourceSet) + } + }.stripDiv() + sourceSet to createHTML(prettyPrint = false).prepareForTemplates() + .div(classes = "content sourceset-dependent-content") { + if (counter++ == 0) attributes["data-active"] = "" + attributes["data-togglable"] = sourceSet.sourceSetIDs.merged.toString() + unsafe { + +htmlContent + } + } + }.sortedBy { it.first.comparableKey } + } + + override fun FlowContent.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) { + if (node.implicitlySourceSetHinted) { + val groupedInstancesBySourceSet = node.children.flatMap { instance -> + instance.sourceSets.map { sourceSet -> instance to sourceSet } + }.groupBy( + Pair<ContentDivergentInstance, DisplaySourceSet>::second, + Pair<ContentDivergentInstance, DisplaySourceSet>::first + ) + + val nodes = groupedInstancesBySourceSet.mapValues { + val distinct = + groupDivergentInstancesWithSourceSet(it.value, it.key, pageContext, + beforeTransformer = { instance, _, sourceSet -> + createHTML(prettyPrint = false).prepareForTemplates().div { + instance.before?.let { before -> + buildContentNode(before, pageContext, sourceSet) + } + }.stripDiv() + }, + afterTransformer = { instance, _, sourceSet -> + createHTML(prettyPrint = false).prepareForTemplates().div { + instance.after?.let { after -> + buildContentNode(after, pageContext, sourceSet) + } + }.stripDiv() + }) + + val isPageWithOverloadedMembers = pageContext is MemberPage && pageContext.documentables().size > 1 + + val contentOfSourceSet = mutableListOf<ContentNode>() + distinct.onEachIndexed{ index, (_, distinctInstances) -> + distinctInstances.firstOrNull()?.before?.let { contentOfSourceSet.add(it) } + contentOfSourceSet.addAll(distinctInstances.map { it.divergent }) + (distinctInstances.firstOrNull()?.after ?: if (index != distinct.size - 1) ContentBreakLine(setOf(it.key)) else null) + ?.let { contentOfSourceSet.add(it) } + + // content kind main is important for declarations list to avoid double line breaks + if (node.dci.kind == ContentKind.Main && index != distinct.size - 1) { + if (isPageWithOverloadedMembers) { + // add some spacing and distinction between function/property overloads. + // not ideal, but there's no other place to modify overloads page atm + contentOfSourceSet.add(ContentBreakLine(setOf(it.key), style = setOf(HorizontalBreakLineStyle))) + } else { + contentOfSourceSet.add(ContentBreakLine(setOf(it.key))) + } + } + } + contentOfSourceSet + } + buildPlatformDependent(nodes, pageContext) + } else { + node.children.forEach { + buildContentNode(it.divergent, pageContext, it.sourceSets) + } + } + } + + private fun groupDivergentInstancesWithSourceSet( + instances: List<ContentDivergentInstance>, + sourceSet: DisplaySourceSet, + pageContext: ContentPage, + beforeTransformer: (ContentDivergentInstance, ContentPage, DisplaySourceSet) -> String, + afterTransformer: (ContentDivergentInstance, ContentPage, DisplaySourceSet) -> String + ): Map<SerializedBeforeAndAfter, List<ContentDivergentInstance>> = + instances.map { instance -> + instance to Pair( + beforeTransformer(instance, pageContext, sourceSet), + afterTransformer(instance, pageContext, sourceSet) + ) + }.groupBy( + Pair<ContentDivergentInstance, SerializedBeforeAndAfter>::second, + Pair<ContentDivergentInstance, SerializedBeforeAndAfter>::first + ) + + private fun ContentPage.documentables(): List<Documentable> { + return (this as? WithDocumentables)?.documentables ?: emptyList() + } + + override fun FlowContent.buildList( + node: ContentList, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + return when { + node.ordered -> { + ol { buildListItems(node.children, pageContext, sourceSetRestriction) } + } + node.hasStyle(ListStyle.DescriptionList) -> { + dl { node.children.forEach { it.build(this, pageContext, sourceSetRestriction) } } + } + else -> { + ul { buildListItems(node.children, pageContext, sourceSetRestriction) } + } + } + } + + public open fun OL.buildListItems( + items: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? = null + ) { + items.forEach { + if (it is ContentList) + buildList(it, pageContext) + else + li { it.build(this, pageContext, sourceSetRestriction) } + } + } + + public open fun UL.buildListItems( + items: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? = 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 + if (node.isImage()) { + img(src = node.address, alt = node.altText) + } else { + println("Unrecognized resource type: $node") + } + } + + private fun FlowContent.buildRow( + node: ContentGroup, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + node.children + .filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } + .takeIf { it.isNotEmpty() } + ?.let { + when (pageContext) { + is MultimoduleRootPage -> buildRowForMultiModule(node, it, pageContext, sourceSetRestriction) + is ModulePage -> buildRowForModule(node, it, pageContext, sourceSetRestriction) + else -> buildRowForContent(node, it, pageContext, sourceSetRestriction) + } + } + } + + private fun FlowContent.buildRowForMultiModule( + contextNode: ContentGroup, + toRender: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + buildAnchor(contextNode) + div(classes = "table-row") { + div("main-subrow " + contextNode.style.joinToString(separator = " ")) { + buildRowHeaderLink(toRender, pageContext, sourceSetRestriction, contextNode.anchor, "w-100") + div { + buildRowBriefSectionForDocs(toRender, pageContext, sourceSetRestriction) + } + } + } + } + + private fun FlowContent.buildRowForModule( + contextNode: ContentGroup, + toRender: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + buildAnchor(contextNode) + div(classes = "table-row") { + addSourceSetFilteringAttributes(contextNode) + div { + div("main-subrow " + contextNode.style.joinToString(separator = " ")) { + buildRowHeaderLink(toRender, pageContext, sourceSetRestriction, contextNode.anchor) + div("pull-right") { + if (ContentKind.shouldBePlatformTagged(contextNode.dci.kind)) { + createPlatformTags(contextNode, cssClasses = "no-gutters") + } + } + } + div { + buildRowBriefSectionForDocs(toRender, pageContext, sourceSetRestriction) + } + } + } + } + + private fun FlowContent.buildRowForContent( + contextNode: ContentGroup, + toRender: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + buildAnchor(contextNode) + div(classes = "table-row") { + contextNode.extra.extraTabbedContentType()?.let { attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] = it.value.toHtmlAttribute() } + addSourceSetFilteringAttributes(contextNode) + div("main-subrow keyValue " + contextNode.style.joinToString(separator = " ")) { + buildRowHeaderLink(toRender, pageContext, sourceSetRestriction, contextNode.anchor) + div { + toRender.filter { it !is ContentLink && !it.hasStyle(ContentStyle.RowTitle) } + .takeIf { it.isNotEmpty() }?.let { + div("title") { + it.forEach { + it.build(this, pageContext, sourceSetRestriction) + } + } + } + } + } + } + } + + private fun FlowContent.buildRowHeaderLink( + toRender: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>?, + anchorDestination: String?, + classes: String = "" + ) { + toRender.filter { it is ContentLink || it.hasStyle(ContentStyle.RowTitle) }.takeIf { it.isNotEmpty() }?.let { + div(classes) { + it.filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } + .forEach { + span("inline-flex") { + div { + it.build(this, pageContext, sourceSetRestriction) + } + if (it is ContentLink && !anchorDestination.isNullOrBlank()) { + buildAnchorCopyButton(anchorDestination) + } + } + } + } + } + } + + private fun FlowContent.addSourceSetFilteringAttributes( + contextNode: ContentGroup, + ) { + attributes["data-filterable-current"] = contextNode.buildSourceSetFilterValues() + attributes["data-filterable-set"] = contextNode.buildSourceSetFilterValues() + } + + private fun ContentNode.buildSourceSetFilterValues(): String { + // This value is used in HTML and JS for filtering out source set declarations, + // it is expected that the separator is the same here and there. + // See https://github.com/Kotlin/dokka/issues/3011#issuecomment-1568620493 + return this.sourceSets.joinToString(",") { + it.sourceSetIDs.merged.toString() + } + } + + private fun FlowContent.buildRowBriefSectionForDocs( + toRender: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>?, + ) { + toRender.filter { it !is ContentLink }.takeIf { it.isNotEmpty() }?.let { + it.forEach { + span(classes = if (it.dci.kind == ContentKind.Comment) "brief-comment" else "") { + it.build(this, pageContext, sourceSetRestriction) + } + } + } + } + + private fun FlowContent.createPlatformTagBubbles(sourceSets: List<DisplaySourceSet>, cssClasses: String = "") { + div("platform-tags $cssClasses") { + sourceSets.sortedBy { it.name }.forEach { + div("platform-tag") { + when (it.platform.key) { + "common" -> classes = classes + "common-like" + "native" -> classes = classes + "native-like" + "jvm" -> classes = classes + "jvm-like" + "js" -> classes = classes + "js-like" + "wasm" -> classes = classes + "wasm-like" + } + text(it.name) + } + } + } + } + + private fun FlowContent.createPlatformTags( + node: ContentNode, + sourceSetRestriction: Set<DisplaySourceSet>? = null, + cssClasses: String = "" + ) { + node.takeIf { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } }?.let { + createPlatformTagBubbles(node.sourceSets.filter { + sourceSetRestriction == null || it in sourceSetRestriction + }.sortedBy { it.name }, cssClasses) + } + } + + override fun FlowContent.buildTable( + node: ContentTable, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + when { + node.style.contains(CommentTable) -> buildDefaultTable(node, pageContext, sourceSetRestriction) + else -> div(classes = "table") { + node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } + node.children.forEach { + buildRow(it, pageContext, sourceSetRestriction) + } + } + } + + } + + public fun FlowContent.buildDefaultTable( + node: ContentTable, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + 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 classes = node.style.joinToString { it.toString() }.toLowerCase() + when (level) { + 1 -> h1(classes = classes, content) + 2 -> h2(classes = classes, content) + 3 -> h3(classes = classes, content) + 4 -> h4(classes = classes, content) + 5 -> h5(classes = classes, content) + else -> h6(classes = classes, content) + } + } + + private fun FlowContent.buildAnchor( + anchor: String, + anchorLabel: String, + sourceSets: String, + content: FlowContent.() -> Unit + ) { + a { + attributes["data-name"] = anchor + attributes["anchor-label"] = anchorLabel + attributes["id"] = anchor + attributes["data-filterable-set"] = sourceSets + } + content() + } + + private fun FlowContent.buildAnchor(anchor: String, anchorLabel: String, sourceSets: String) = + buildAnchor(anchor, anchorLabel, sourceSets) {} + + private fun FlowContent.buildAnchor(node: ContentNode) { + node.anchorLabel?.let { label -> buildAnchor(node.anchor!!, label, node.buildSourceSetFilterValues()) } + } + + + override fun FlowContent.buildNavigation(page: PageNode) { + div(classes = "breadcrumbs") { + val path = locationProvider.ancestors(page).filterNot { it is RendererSpecificPage }.asReversed() + if (path.size > 1) { + buildNavigationElement(path.first(), page) + path.drop(1).forEach { node -> + span(classes = "delimiter") { + text("/") + } + buildNavigationElement(node, page) + } + } + } + } + + private fun FlowContent.buildNavigationElement(node: PageNode, page: PageNode) = + if (node.isNavigable) { + val isCurrentPage = (node == page) + if (isCurrentPage) { + span(classes = "current") { + text(node.name) + } + } else { + buildLink(node, page) + } + } else { + text(node.name) + } + + private fun FlowContent.buildLink(to: PageNode, from: PageNode) = + locationProvider.resolve(to, from)?.let { path -> + buildLink(path) { + text(to.name) + } + } ?: span { + attributes["data-unresolved-link"] = to.name.htmlEscape() + text(to.name) + } + + public fun FlowContent.buildAnchorCopyButton(pointingTo: String) { + span(classes = "anchor-wrapper") { + span(classes = "anchor-icon") { + attributes["pointing-to"] = pointingTo + } + copiedPopup("Link copied to clipboard") + } + } + + public fun FlowContent.buildLink( + to: DRI, + platforms: List<DisplaySourceSet>, + from: PageNode? = null, + block: FlowContent.() -> Unit + ) { + locationProvider.resolve(to, platforms.toSet(), from)?.let { buildLink(it, block) } + ?: run { context.logger.error("Cannot resolve path for `$to` from `$from`"); block() } + } + + override fun buildError(node: ContentNode) { + context.logger.error("Unknown ContentNode type: $node") + } + + override fun FlowContent.buildLineBreak() { + br() + } + override fun FlowContent.buildLineBreak(node: ContentBreakLine, pageContext: ContentPage) { + if (node.style.contains(HorizontalBreakLineStyle)) { + hr() + } else { + buildLineBreak() + } + } + + override fun FlowContent.buildLink(address: String, content: FlowContent.() -> Unit) { + a(href = address, block = content) + } + + override fun FlowContent.buildDRILink( + node: ContentDRILink, + pageContext: ContentPage, + sourceSetRestriction: Set<DisplaySourceSet>? + ) { + locationProvider.resolve(node.address, node.sourceSets, pageContext)?.let { address -> + buildLink(address) { + buildText(node.children, pageContext, sourceSetRestriction) + } + } ?: if (isPartial) { + templateCommand(ResolveLinkCommand(node.address)) { + buildText(node.children, pageContext, sourceSetRestriction) + } + } else { + span { + attributes["data-unresolved-link"] = node.address.toString().htmlEscape() + buildText(node.children, pageContext, sourceSetRestriction) + } + } + } + + override fun FlowContent.buildCodeBlock( + code: ContentCodeBlock, + pageContext: ContentPage + ) { + div("sample-container") { + val codeLang = "lang-" + code.language.ifEmpty { "kotlin" } + val stylesWithBlock = code.style + TextStyle.Block + codeLang + pre { + code(stylesWithBlock.joinToString(" ") { it.toString().toLowerCase() }) { + attributes["theme"] = "idea" + code.children.forEach { buildContentNode(it, pageContext) } + } + } + /* + Disable copy button on samples as: + - it is useless + - it overflows with playground's run button + */ + if (!code.style.contains(ContentStyle.RunnableSample)) copyButton() + } + } + + override fun FlowContent.buildCodeInline( + code: ContentCodeInline, + pageContext: ContentPage + ) { + val codeLang = "lang-" + code.language.ifEmpty { "kotlin" } + val stylesWithBlock = code.style + codeLang + code(stylesWithBlock.joinToString(" ") { it.toString().toLowerCase() }) { + code.children.forEach { buildContentNode(it, pageContext) } + } + } + + override fun FlowContent.buildText(textNode: ContentText) { + buildText(textNode, textNode.style) + } + + private fun FlowContent.buildText(textNode: ContentText, unappliedStyles: Set<Style>) { + when { + textNode.extra[HtmlContent] != null -> { + consumer.onTagContentUnsafe { raw(textNode.text) } + } + unappliedStyles.contains(TextStyle.Indented) -> { + consumer.onTagContentEntity(Entities.nbsp) + buildText(textNode, unappliedStyles - TextStyle.Indented) + } + unappliedStyles.isNotEmpty() -> { + val styleToApply = unappliedStyles.first() + applyStyle(styleToApply) { + buildText(textNode, unappliedStyles - styleToApply) + } + } + textNode.hasStyle(ContentStyle.RowTitle) || textNode.hasStyle(TextStyle.Cover) -> + buildBreakableText(textNode.text) + else -> text(textNode.text) + } + } + + private inline fun FlowContent.applyStyle(styleToApply: Style, crossinline body: FlowContent.() -> Unit) { + when (styleToApply) { + TextStyle.Bold -> b { body() } + TextStyle.Italic -> i { body() } + TextStyle.Strikethrough -> strike { body() } + TextStyle.Strong -> strong { body() } + TextStyle.Var -> htmlVar { body() } + TextStyle.Underlined -> underline { body() } + is TokenStyle -> span("token ${styleToApply.prismJsClass()}") { body() } + else -> body() + } + } + + private fun TokenStyle.prismJsClass(): String = when(this) { + // Prism.js parser adds Builtin token instead of Annotation + // for some reason, so we also add it for consistency and correct coloring + TokenStyle.Annotation -> "annotation builtin" + else -> this.toString().toLowerCase() + } + + override fun render(root: RootPageNode) { + shouldRenderSourceSetTabs = shouldRenderSourceSetTabs(root) + super.render(root) + } + + override fun buildPage(page: ContentPage, content: (FlowContent, ContentPage) -> Unit): String = + buildHtml(page, page.embeddedResources) { + content(this, page) + } + + private fun PageNode.getDocumentableType(): String? = + when(this) { + is PackagePage -> "package" + is ClasslikePage -> "classlike" + is MemberPage -> "member" + else -> null + } + + public open fun buildHtml( + page: PageNode, + resources: List<String>, content: FlowContent.() -> Unit + ): String { + return templater.renderFromTemplate(DokkaTemplateTypes.BASE) { + val generatedContent = + createHTML().div("main-content") { + page.getDocumentableType()?.let { attributes["data-page-type"] = it } + id = "content" + (page as? ContentPage)?.let { + attributes["pageIds"] = "${context.configuration.moduleName}::${page.pageId}" + } + content() + } + + templateModelMerger.invoke(templateModelFactories) { + buildModel( + page, + resources, + locationProvider, + generatedContent + ) + } + } + } + + /** + * This is deliberately left open for plugins that have some other pages above ours and would like to link to them + * instead of ours when clicking the logo + */ + public open fun FlowContent.clickableLogo(page: PageNode, pathToRoot: String) { + if (context.configuration.delayTemplateSubstitution && page is ContentPage) { + templateCommand(PathToRootSubstitutionCommand(pattern = "###", default = pathToRoot)) { + a { + href = "###index.html" + templateCommand( + ProjectNameSubstitutionCommand( + pattern = "@@@", + default = context.configuration.moduleName + ) + ) { + span { + text("@@@") + } + } + } + } + } else { + a { + href = pathToRoot + "index.html" + text(context.configuration.moduleName) + } + } + } + + private val ContentNode.isAnchorable: Boolean + get() = anchorLabel != null + + private val ContentNode.anchorLabel: String? + get() = extra[SymbolAnchorHint]?.anchorName + + private val ContentNode.anchor: String? + get() = extra[SymbolAnchorHint]?.contentKind?.let { contentKind -> + (locationProvider as DokkaBaseLocationProvider).anchorForDCI(DCI(dci.dri, contentKind), sourceSets) + } + + private val isPartial = context.configuration.delayTemplateSubstitution +} + +private fun TabbedContentType.toHtmlAttribute(): String = + when(this) { + is BasicTabbedContentType -> + when(this) { + BasicTabbedContentType.ENTRY -> "ENTRY" + BasicTabbedContentType.TYPE -> "TYPE" + BasicTabbedContentType.CONSTRUCTOR -> "CONSTRUCTOR" + BasicTabbedContentType.FUNCTION -> "FUNCTION" + BasicTabbedContentType.PROPERTY -> "PROPERTY" + BasicTabbedContentType.EXTENSION_PROPERTY -> "EXTENSION_PROPERTY" + BasicTabbedContentType.EXTENSION_FUNCTION -> "EXTENSION_FUNCTION" + } + else -> throw IllegalStateException("Unknown TabbedContentType $this") + } + +/** + * Tabs for a content with [ContentStyle.TabbedContent]. + * + * @see ContentStyle.TabbedContent] + */ +private data class ContentTab(val text: String, val tabbedContentTypes: List<TabbedContentType>) + +public fun List<SimpleAttr>.joinAttr(): String = 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 + +private fun PropertyContainer<ContentNode>.extraHtmlAttributes() = allOfType<SimpleAttr>() +private fun PropertyContainer<ContentNode>.extraTabbedContentType() = this[TabbedContentTypeExtra] + +private val DisplaySourceSet.comparableKey + get() = sourceSetIDs.merged.let { it.scopeId + it.sourceSetName } diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/NavigationDataProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/NavigationDataProvider.kt new file mode 100644 index 00000000..fccfd145 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/NavigationDataProvider.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html + +import org.jetbrains.dokka.base.renderers.sourceSets +import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.annotations +import org.jetbrains.dokka.base.transformers.documentables.isDeprecated +import org.jetbrains.dokka.base.transformers.documentables.isException +import org.jetbrains.dokka.base.utils.canonicalAlphabeticalOrder +import org.jetbrains.dokka.model.* +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.analysis.kotlin.internal.DocumentableLanguage +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin + +public abstract class NavigationDataProvider( + dokkaContext: DokkaContext +) { + private val documentableSourceLanguageParser = dokkaContext.plugin<InternalKotlinAnalysisPlugin>().querySingle { documentableSourceLanguageParser } + + public open fun navigableChildren(input: RootPageNode): NavigationNode = input.withDescendants() + .first { it is ModulePage || it is MultimoduleRootPage }.let { visit(it as ContentPage) } + + public open fun visit(page: ContentPage): NavigationNode = + NavigationNode( + name = page.displayableName(), + dri = page.dri.first(), + sourceSets = page.sourceSets(), + icon = chooseNavigationIcon(page), + styles = chooseStyles(page), + children = page.navigableChildren() + ) + + /** + * Parenthesis is applied in 1 case: + * - page only contains functions (therefore documentable from this page is [DFunction]) + */ + private fun ContentPage.displayableName(): String = + if (this is WithDocumentables && documentables.all { it is DFunction }) { + "$name()" + } else { + name + } + + private fun chooseNavigationIcon(contentPage: ContentPage): NavigationNodeIcon? = + if (contentPage is WithDocumentables) { + val documentable = contentPage.documentables.firstOrNull() + val isJava = documentable?.hasAnyJavaSources() ?: false + + when (documentable) { + is DTypeAlias -> NavigationNodeIcon.TYPEALIAS_KT + is DClass -> when { + documentable.isException -> NavigationNodeIcon.EXCEPTION + documentable.isAbstract() -> { + if (isJava) NavigationNodeIcon.ABSTRACT_CLASS else NavigationNodeIcon.ABSTRACT_CLASS_KT + } + else -> if (isJava) NavigationNodeIcon.CLASS else NavigationNodeIcon.CLASS_KT + } + is DFunction -> NavigationNodeIcon.FUNCTION + is DProperty -> { + val isVar = documentable.extra[IsVar] != null + if (isVar) NavigationNodeIcon.VAR else NavigationNodeIcon.VAL + } + is DInterface -> if (isJava) NavigationNodeIcon.INTERFACE else NavigationNodeIcon.INTERFACE_KT + is DEnum, + is DEnumEntry -> if (isJava) NavigationNodeIcon.ENUM_CLASS else NavigationNodeIcon.ENUM_CLASS_KT + is DAnnotation -> { + if (isJava) NavigationNodeIcon.ANNOTATION_CLASS else NavigationNodeIcon.ANNOTATION_CLASS_KT + } + is DObject -> NavigationNodeIcon.OBJECT + else -> null + } + } else { + null + } + + private fun Documentable.hasAnyJavaSources(): Boolean { + return this.sourceSets.any { sourceSet -> + documentableSourceLanguageParser.getLanguage(this, sourceSet) == DocumentableLanguage.JAVA + } + } + + private fun DClass.isAbstract() = + modifier.values.all { it is KotlinModifier.Abstract || it is JavaModifier.Abstract } + + private fun chooseStyles(page: ContentPage): Set<Style> = + if (page.containsOnlyDeprecatedDocumentables()) setOf(TextStyle.Strikethrough) else emptySet() + + private fun ContentPage.containsOnlyDeprecatedDocumentables(): Boolean { + if (this !is WithDocumentables) { + return false + } + return this.documentables.isNotEmpty() && this.documentables.all { it.isDeprecatedForAllSourceSets() } + } + + private fun Documentable.isDeprecatedForAllSourceSets(): Boolean { + val sourceSetAnnotations = this.annotations() + return sourceSetAnnotations.isNotEmpty() && sourceSetAnnotations.all { (_, annotations) -> + annotations.any { it.isDeprecated() } + } + } + + private val navigationNodeOrder: Comparator<NavigationNode> = + compareBy(canonicalAlphabeticalOrder) { it.name } + + private fun ContentPage.navigableChildren() = + if (this is ClasslikePage) { + this.navigableChildren() + } else { + children + .filterIsInstance<ContentPage>() + .map { visit(it) } + .sortedWith(navigationNodeOrder) + } + + private fun ClasslikePage.navigableChildren(): List<NavigationNode> { + // Classlikes should only have other classlikes as navigable children + val navigableChildren = children + .filterIsInstance<ClasslikePage>() + .map { visit(it) } + + val isEnumPage = documentables.any { it is DEnum } + return if (isEnumPage) { + // no sorting for enum entries, should be the same order as in source code + navigableChildren + } else { + navigableChildren.sortedWith(navigationNodeOrder) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/NavigationPage.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/NavigationPage.kt new file mode 100644 index 00000000..eae43daf --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/NavigationPage.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.base.renderers.html.NavigationNodeIcon.CLASS +import org.jetbrains.dokka.base.renderers.html.NavigationNodeIcon.CLASS_KT +import org.jetbrains.dokka.base.renderers.pageId +import org.jetbrains.dokka.base.templating.AddToNavigationCommand +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.model.WithChildren +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext + +public class NavigationPage( + public val root: NavigationNode, + public val moduleName: String, + public val context: DokkaContext +) : RendererSpecificPage { + + override val name: String = "navigation" + + override val children: List<PageNode> = emptyList() + + override fun modified(name: String, children: List<PageNode>): NavigationPage = this + + override val strategy: RenderingStrategy = RenderingStrategy<HtmlRenderer> { + createHTML().visit(root, this) + } + + private fun <R> TagConsumer<R>.visit(node: NavigationNode, renderer: HtmlRenderer): R = with(renderer) { + if (context.configuration.delayTemplateSubstitution) { + templateCommand(AddToNavigationCommand(moduleName)) { + visit(node, "${moduleName}-nav-submenu", renderer) + } + } else { + visit(node, "${moduleName}-nav-submenu", renderer) + } + } + + private fun <R> TagConsumer<R>.visit(node: NavigationNode, navId: String, renderer: HtmlRenderer): R = + with(renderer) { + div("sideMenuPart") { + id = navId + attributes["pageId"] = "${moduleName}::${node.pageId}" + div("overview") { + if (node.children.isNotEmpty()) { + span("navButton") { + onClick = """document.getElementById("$navId").classList.toggle("hidden");""" + span("navButtonContent") + } + } + buildLink(node.dri, node.sourceSets.toList()) { + val withIcon = node.icon != null + if (withIcon) { + // in case link text is so long that it needs to have word breaks, + // and it stretches to two or more lines, make sure the icon + // is always on the left in the grid and is not wrapped with text + span("nav-link-grid") { + span("nav-link-child ${node.icon?.style()}") + span("nav-link-child") { + nodeText(node) + } + } + } else { + nodeText(node) + } + } + } + node.children.withIndex().forEach { (n, p) -> visit(p, "$navId-$n", renderer) } + } + } + + private fun FlowContent.nodeText(node: NavigationNode) { + if (node.styles.contains(TextStyle.Strikethrough)) { + strike { + buildBreakableText(node.name) + } + } else { + buildBreakableText(node.name) + } + } +} + +public data class NavigationNode( + val name: String, + val dri: DRI, + val sourceSets: Set<DisplaySourceSet>, + val icon: NavigationNodeIcon?, + val styles: Set<Style> = emptySet(), + override val children: List<NavigationNode> +) : WithChildren<NavigationNode> + +/** + * [CLASS] represents a neutral (a.k.a Java-style) icon, + * whereas [CLASS_KT] should be Kotlin-styled + */ +public enum class NavigationNodeIcon( + private val cssClass: String +) { + CLASS("class"), + CLASS_KT("class-kt"), + ABSTRACT_CLASS("abstract-class"), + ABSTRACT_CLASS_KT("abstract-class-kt"), + ENUM_CLASS("enum-class"), + ENUM_CLASS_KT("enum-class-kt"), + ANNOTATION_CLASS("annotation-class"), + ANNOTATION_CLASS_KT("annotation-class-kt"), + INTERFACE("interface"), + INTERFACE_KT("interface-kt"), + FUNCTION("function"), + EXCEPTION("exception-class"), + OBJECT("object"), + TYPEALIAS_KT("typealias-kt"), + VAL("val"), + VAR("var"); + + internal fun style(): String = "nav-icon $cssClass" +} + +public fun NavigationPage.transform(block: (NavigationNode) -> NavigationNode): NavigationPage = + NavigationPage(root.transform(block), moduleName, context) + +public fun NavigationNode.transform(block: (NavigationNode) -> NavigationNode): NavigationNode = + run(block).let { NavigationNode(it.name, it.dri, it.sourceSets, it.icon, it.styles, it.children.map(block)) } diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/SearchbarDataInstaller.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/SearchbarDataInstaller.kt new file mode 100644 index 00000000..83d4b24f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/SearchbarDataInstaller.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.renderers.sourceSets +import org.jetbrains.dokka.base.templating.AddToSearch +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer + +public data class SearchRecord( + val name: String, + val description: String? = null, + val location: String, + val searchKeys: List<String> = listOf(name) +) { + public companion object +} + +public open class SearchbarDataInstaller( + public val context: DokkaContext +) : PageTransformer { + + public data class DRIWithSourceSets(val dri: DRI, val sourceSet: Set<DisplaySourceSet>) + + public data class SignatureWithId(val driWithSourceSets: DRIWithSourceSets, val displayableSignature: String) { + public constructor(dri: DRI, page: ContentPage) : this( DRIWithSourceSets(dri, page.sourceSets()), + getSymbolSignature(page, dri)?.let { flattenToText(it) } ?: page.name) + + val id: String + get() = with(driWithSourceSets.dri) { + listOfNotNull( + packageName?.takeIf { it.isNotBlank() }, + classNames, + callable?.name + ).joinToString(".") + } + } + + private val mapper = jacksonObjectMapper() + + public open fun generatePagesList( + pages: List<SignatureWithId>, + locationResolver: DriResolver + ): List<SearchRecord> = + pages.map { pageWithId -> + createSearchRecord( + name = pageWithId.displayableSignature, + description = pageWithId.id, + location = resolveLocation(locationResolver, pageWithId.driWithSourceSets).orEmpty(), + searchKeys = listOf( + pageWithId.id.substringAfterLast("."), + pageWithId.displayableSignature, + pageWithId.id, + ) + ) + }.sortedWith(compareBy({ it.name }, { it.description })) + + public open fun createSearchRecord( + name: String, + description: String?, + location: String, + searchKeys: List<String> + ): SearchRecord = + SearchRecord(name, description, location, searchKeys) + + public open fun processPage(page: PageNode): List<SignatureWithId> = + when (page) { + is ContentPage -> page.takeIf { page !is ModulePageNode && page !is PackagePageNode }?.dri + ?.map { dri -> SignatureWithId(dri, page) }.orEmpty() + else -> emptyList() + } + + private fun resolveLocation(locationResolver: DriResolver, driWithSourceSets: DRIWithSourceSets): String? = + locationResolver(driWithSourceSets.dri, driWithSourceSets.sourceSet).also { location -> + if (location.isNullOrBlank()) context.logger.warn("Cannot resolve path for ${driWithSourceSets.dri}") + } + + override fun invoke(input: RootPageNode): RootPageNode { + val signatureWithIds = input.withDescendants().fold(emptyList<SignatureWithId>()) { pageList, page -> + pageList + processPage(page) + } + val page = RendererSpecificResourcePage( + name = "scripts/pages.json", + children = emptyList(), + strategy = RenderingStrategy.DriLocationResolvableWrite { resolver -> + val content = signatureWithIds.run { + generatePagesList(this, resolver) + } + + if (context.configuration.delayTemplateSubstitution) { + mapper.writeValueAsString(AddToSearch(context.configuration.moduleName, content)) + } else { + mapper.writeValueAsString(content) + } + }) + + return input.modified(children = input.children + page) + } +} + +private fun getSymbolSignature(page: ContentPage, dri: DRI) = + page.content.dfs { it.dci.kind == ContentKind.Symbol && it.dci.dri.contains(dri) } + +private fun flattenToText(node: ContentNode): String { + fun getContentTextNodes(node: ContentNode, sourceSetRestriction: DisplaySourceSet): 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 && node.dci.kind != ContentKind.Source } + .orEmpty() + else -> emptyList() + } + + val sourceSetRestriction = + node.sourceSets.find { it.platform == Platform.common } ?: node.sourceSets.first() + return getContentTextNodes(node, sourceSetRestriction).joinToString("") { it.text } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/Tags.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/Tags.kt new file mode 100644 index 00000000..7d6fc390 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/Tags.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.base.templating.toJsonString + +public typealias TemplateBlock = TemplateCommand.() -> Unit + +@HtmlTagMarker +public fun FlowOrPhrasingContent.wbr(classes: String? = null, block: WBR.() -> Unit = {}): Unit = + WBR(attributesMapOf("class", classes), consumer).visit(block) + +@Suppress("unused") +public open class WBR(initialAttributes: Map<String, String>, consumer: TagConsumer<*>) : + HTMLTag("wbr", consumer, initialAttributes, namespace = null, inlineTag = true, emptyTag = false), + HtmlBlockInlineTag + +/** + * Work-around until next version of kotlinx.html doesn't come out + */ +@HtmlTagMarker +public inline fun FlowOrPhrasingContent.strike(classes : String? = null, crossinline block : STRIKE.() -> Unit = {}) : Unit = STRIKE(attributesMapOf("class", classes), consumer).visit(block) + +public open class STRIKE(initialAttributes: Map<String, String>, override val consumer: TagConsumer<*>) : + HTMLTag("strike", consumer, initialAttributes, null, false, false), HtmlBlockInlineTag + +@HtmlTagMarker +public inline fun FlowOrPhrasingContent.underline(classes : String? = null, crossinline block : UNDERLINE.() -> Unit = {}) : Unit = UNDERLINE(attributesMapOf("class", classes), consumer).visit(block) + +public open class UNDERLINE(initialAttributes: Map<String, String>, override val consumer: TagConsumer<*>) : + HTMLTag("u", consumer, initialAttributes, null, false, false), HtmlBlockInlineTag + +public const val TEMPLATE_COMMAND_SEPARATOR: String = ":" +public const val TEMPLATE_COMMAND_BEGIN_BORDER: String = "[+]cmd" +public const val TEMPLATE_COMMAND_END_BORDER: String = "[-]cmd" + +public fun FlowOrMetaDataContent.templateCommandAsHtmlComment(data: Command, block: FlowOrMetaDataContent.() -> Unit = {}): Unit = + (consumer as? ImmediateResolutionTagConsumer)?.processCommand(data, block) + ?: let{ + comment( "$TEMPLATE_COMMAND_BEGIN_BORDER$TEMPLATE_COMMAND_SEPARATOR${toJsonString(data)}") + block() + comment(TEMPLATE_COMMAND_END_BORDER) + } + +public fun <T: Appendable> T.templateCommandAsHtmlComment(command: Command, action: T.() -> Unit ) { + append("<!--$TEMPLATE_COMMAND_BEGIN_BORDER$TEMPLATE_COMMAND_SEPARATOR${toJsonString(command)}-->") + action() + append("<!--$TEMPLATE_COMMAND_END_BORDER-->") +} + +public fun FlowOrMetaDataContent.templateCommand(data: Command, block: TemplateBlock = {}): Unit = + (consumer as? ImmediateResolutionTagConsumer)?.processCommand(data, block) + ?: TemplateCommand(attributesMapOf("data", toJsonString(data)), consumer).visit(block) + +public fun <T> TagConsumer<T>.templateCommand(data: Command, block: TemplateBlock = {}): T = + (this as? ImmediateResolutionTagConsumer)?.processCommandAndFinalize(data, block) + ?: TemplateCommand(attributesMapOf("data", toJsonString(data)), this).visitAndFinalize(this, block) + +public fun templateCommandFor(data: Command, consumer: TagConsumer<*>): TemplateCommand = + TemplateCommand(attributesMapOf("data", toJsonString(data)), consumer) + +public class TemplateCommand(initialAttributes: Map<String, String>, consumer: TagConsumer<*>) : + HTMLTag( + "dokka-template-command", + consumer, + initialAttributes, + namespace = null, + inlineTag = true, + emptyTag = false + ), + CommonAttributeGroupFacadeFlowInteractivePhrasingContent + +// This hack is outrageous. I hate it but I cannot find any other way around `kotlinx.html` type system. +public fun TemplateBlock.buildAsInnerHtml(): String = createHTML(prettyPrint = false).run { + TemplateCommand(emptyMap, this).visitAndFinalize(this, this@buildAsInnerHtml).substringAfter(">").substringBeforeLast("<") +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ImmediateResolutionTagConsumer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ImmediateResolutionTagConsumer.kt new file mode 100644 index 00000000..9cde1fca --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ImmediateResolutionTagConsumer.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html.command.consumers + +import kotlinx.html.TagConsumer +import kotlinx.html.visit +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.renderers.html.TemplateBlock +import org.jetbrains.dokka.base.renderers.html.templateCommand +import org.jetbrains.dokka.base.renderers.html.templateCommandFor +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query + +public class ImmediateResolutionTagConsumer<out R>( + private val downstream: TagConsumer<R>, + private val context: DokkaContext +): TagConsumer<R> by downstream { + + public fun processCommand(command: Command, block: TemplateBlock) { + context.plugin<DokkaBase>().query { immediateHtmlCommandConsumer } + .find { it.canProcess(command) } + ?.processCommand(command, block, this) + ?: run { templateCommandFor(command, downstream).visit(block) } + } + + public fun processCommandAndFinalize(command: Command, block: TemplateBlock): R { + return context.plugin<DokkaBase>().query { immediateHtmlCommandConsumer } + .find { it.canProcess(command) } + ?.processCommandAndFinalize(command, block, this) + ?: downstream.templateCommand(command, block) + } +} + diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/PathToRootConsumer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/PathToRootConsumer.kt new file mode 100644 index 00000000..9ac6eb91 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/PathToRootConsumer.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html.command.consumers + +import org.jetbrains.dokka.base.renderers.html.TemplateBlock +import org.jetbrains.dokka.base.renderers.html.buildAsInnerHtml +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer +import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand + +public object PathToRootConsumer: ImmediateHtmlCommandConsumer { + override fun canProcess(command: Command): Boolean = command is PathToRootSubstitutionCommand + + override fun <R> processCommand(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>) { + command as PathToRootSubstitutionCommand + tagConsumer.onTagContentUnsafe { +block.buildAsInnerHtml().replace(command.pattern, command.default) } + } + + override fun <R> processCommandAndFinalize(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>): R { + processCommand(command, block, tagConsumer) + return tagConsumer.finalize() + } + +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ReplaceVersionsConsumer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ReplaceVersionsConsumer.kt new file mode 100644 index 00000000..dd95c202 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ReplaceVersionsConsumer.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html.command.consumers + +import org.jetbrains.dokka.base.renderers.html.TemplateBlock +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer +import org.jetbrains.dokka.base.templating.ReplaceVersionsCommand +import org.jetbrains.dokka.plugability.DokkaContext + +public class ReplaceVersionsConsumer(private val context: DokkaContext) : ImmediateHtmlCommandConsumer { + override fun canProcess(command: Command): Boolean = command is ReplaceVersionsCommand + + override fun <R> processCommand( + command: Command, + block: TemplateBlock, + tagConsumer: ImmediateResolutionTagConsumer<R> + ) { + command as ReplaceVersionsCommand + tagConsumer.onTagContentUnsafe { +context.configuration.moduleVersion.orEmpty() } + } + + override fun <R> processCommandAndFinalize(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>): R { + processCommand(command, block, tagConsumer) + return tagConsumer.finalize() + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ResolveLinkConsumer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ResolveLinkConsumer.kt new file mode 100644 index 00000000..292e88b0 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ResolveLinkConsumer.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html.command.consumers + +import kotlinx.html.SPAN +import kotlinx.html.span +import kotlinx.html.unsafe +import kotlinx.html.visit +import org.jetbrains.dokka.base.renderers.html.TemplateBlock +import org.jetbrains.dokka.base.renderers.html.buildAsInnerHtml +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer +import org.jetbrains.dokka.base.templating.ResolveLinkCommand +import org.jetbrains.dokka.utilities.htmlEscape + +public object ResolveLinkConsumer: ImmediateHtmlCommandConsumer { + override fun canProcess(command: Command): Boolean = command is ResolveLinkCommand + + override fun <R> processCommand(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>) { + command as ResolveLinkCommand + SPAN(mapOf("data-unresolved-link" to command.dri.toString().htmlEscape()), tagConsumer).visit { + unsafe { block.buildAsInnerHtml() } + } + } + + override fun <R> processCommandAndFinalize(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>): R { + command as ResolveLinkCommand + return tagConsumer.span { + attributes["data-unresolved-link"] = command.dri.toString().htmlEscape() + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/htmlFormatingUtils.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/htmlFormatingUtils.kt new file mode 100644 index 00000000..b6ce4147 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/htmlFormatingUtils.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.FlowContent +import kotlinx.html.span + +public fun FlowContent.buildTextBreakableAfterCapitalLetters(name: String, hasLastElement: Boolean = false) { + if (name.contains(" ")) { + val withOutSpaces = name.split(" ") + withOutSpaces.dropLast(1).forEach { + buildBreakableText(it) + +" " + } + buildBreakableText(withOutSpaces.last()) + } else { + val content = name.replace(Regex("(?<=[a-z])([A-Z])"), " $1").split(" ") + joinToHtml(content, hasLastElement) { + it + } + } +} + +public fun FlowContent.buildBreakableDotSeparatedHtml(name: String) { + val phrases = name.split(".") + phrases.forEachIndexed { i, e -> + val elementWithOptionalDot = e.takeIf { i == phrases.lastIndex } ?: "$e." + if (e.length > 10) { + buildTextBreakableAfterCapitalLetters(elementWithOptionalDot, hasLastElement = i == phrases.lastIndex) + } else { + buildBreakableHtmlElement(elementWithOptionalDot, i == phrases.lastIndex) + } + } +} + +private fun FlowContent.joinToHtml(elements: List<String>, hasLastElement: Boolean = true, onEach: (String) -> String) { + elements.dropLast(1).forEach { + buildBreakableHtmlElement(onEach(it)) + } + elements.takeIf { it.isNotEmpty() && it.last().isNotEmpty() }?.let { + if (hasLastElement) { + span { + buildBreakableHtmlElement(it.last(), last = true) + } + } else { + buildBreakableHtmlElement(it.last(), last = false) + } + } +} + +private fun FlowContent.buildBreakableHtmlElement(element: String, last: Boolean = false) { + element.takeIf { it.isNotBlank() }?.let { + span { + +it + } + } + if (!last) { + wbr { } + } +} + +public fun FlowContent.buildBreakableText(name: String) { + if (name.contains(".")) buildBreakableDotSeparatedHtml(name) + else buildTextBreakableAfterCapitalLetters(name, hasLastElement = true) +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/htmlPreprocessors.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/htmlPreprocessors.kt new file mode 100644 index 00000000..dad013e2 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/htmlPreprocessors.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html + +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.DokkaBaseConfiguration +import org.jetbrains.dokka.base.templating.AddToSourcesetDependencies +import org.jetbrains.dokka.base.templating.toJsonString +import org.jetbrains.dokka.pages.RendererSpecificResourcePage +import org.jetbrains.dokka.pages.RenderingStrategy +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.configuration +import org.jetbrains.dokka.transformers.pages.PageTransformer + +public open class NavigationPageInstaller( + public val context: DokkaContext +) : NavigationDataProvider(context), PageTransformer { + override fun invoke(input: RootPageNode): RootPageNode = + input.modified( + children = input.children + NavigationPage( + root = navigableChildren(input), + moduleName = context.configuration.moduleName, + context = context + ) + ) +} + +public class CustomResourceInstaller( + public val dokkaContext: DokkaContext +) : PageTransformer { + private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(dokkaContext) + + private val customAssets = configuration?.customAssets?.map { + RendererSpecificResourcePage("images/${it.name}", emptyList(), RenderingStrategy.Copy(it.absolutePath)) + }.orEmpty() + + private val customStylesheets = configuration?.customStyleSheets?.map { + RendererSpecificResourcePage("styles/${it.name}", emptyList(), RenderingStrategy.Copy(it.absolutePath)) + }.orEmpty() + + override fun invoke(input: RootPageNode): RootPageNode { + val customResourcesPaths = (customAssets + customStylesheets).map { it.name }.toSet() + val withEmbeddedResources = + input.transformContentPagesTree { it.modified(embeddedResources = it.embeddedResources + customResourcesPaths) } + if(dokkaContext.configuration.delayTemplateSubstitution) + return withEmbeddedResources + val (currentResources, otherPages) = withEmbeddedResources.children.partition { it is RendererSpecificResourcePage } + return input.modified(children = otherPages + currentResources.filterNot { it.name in customResourcesPaths } + customAssets + customStylesheets) + } +} + +public class ScriptsInstaller(private val dokkaContext: DokkaContext) : PageTransformer { + + // scripts ending with `_deferred.js` are loaded with `defer`, otherwise `async` + private val scriptsPages = listOf( + "scripts/clipboard.js", + "scripts/navigation-loader.js", + "scripts/platform-content-handler.js", + "scripts/main.js", + "scripts/prism.js", + + // It's important for this script to be deferred because it has logic that makes decisions based on + // rendered elements (for instance taking their clientWidth), and if not all styles are loaded/applied + // at the time of inspecting them, it will give incorrect results and might lead to visual bugs. + // should be easy to test if you open any page in incognito or by reloading it (Ctrl+Shift+R) + "scripts/symbol-parameters-wrapper_deferred.js", + ) + + override fun invoke(input: RootPageNode): RootPageNode = + input.let { root -> + if (dokkaContext.configuration.delayTemplateSubstitution) root + else root.modified(children = input.children + scriptsPages.toRenderSpecificResourcePage()) + }.transformContentPagesTree { + it.modified( + embeddedResources = it.embeddedResources + scriptsPages + ) + } +} + +public class StylesInstaller(private val dokkaContext: DokkaContext) : PageTransformer { + private val stylesPages = listOf( + "styles/style.css", + "styles/main.css", + "styles/prism.css", + "styles/logo-styles.css", + "styles/font-jb-sans-auto.css" + ) + + override fun invoke(input: RootPageNode): RootPageNode = + input.let { root -> + if (dokkaContext.configuration.delayTemplateSubstitution) root + else root.modified(children = input.children + stylesPages.toRenderSpecificResourcePage()) + }.transformContentPagesTree { + it.modified( + embeddedResources = it.embeddedResources + stylesPages + ) + } +} + +public object AssetsInstaller : PageTransformer { + private val imagesPages = listOf( + "images/arrow_down.svg", + "images/logo-icon.svg", + "images/go-to-top-icon.svg", + "images/footer-go-to-link.svg", + "images/anchor-copy-button.svg", + "images/copy-icon.svg", + "images/copy-successful-icon.svg", + "images/theme-toggle.svg", + "images/burger.svg", + "images/homepage.svg", + + // navigation icons + "images/nav-icons/abstract-class.svg", + "images/nav-icons/abstract-class-kotlin.svg", + "images/nav-icons/annotation.svg", + "images/nav-icons/annotation-kotlin.svg", + "images/nav-icons/class.svg", + "images/nav-icons/class-kotlin.svg", + "images/nav-icons/enum.svg", + "images/nav-icons/enum-kotlin.svg", + "images/nav-icons/exception-class.svg", + "images/nav-icons/field-value.svg", + "images/nav-icons/field-variable.svg", + "images/nav-icons/function.svg", + "images/nav-icons/interface.svg", + "images/nav-icons/interface-kotlin.svg", + "images/nav-icons/object.svg", + "images/nav-icons/typealias-kotlin.svg", + ) + + override fun invoke(input: RootPageNode): RootPageNode = input.modified( + children = input.children + imagesPages.toRenderSpecificResourcePage() + ) +} + +private fun List<String>.toRenderSpecificResourcePage(): List<RendererSpecificResourcePage> = + map { RendererSpecificResourcePage(it, emptyList(), RenderingStrategy.Copy("/dokka/$it")) } + +public class SourcesetDependencyAppender( + public val context: DokkaContext +) : PageTransformer { + private val name = "scripts/sourceset_dependencies.js" + override fun invoke(input: RootPageNode): RootPageNode { + val dependenciesMap = context.configuration.sourceSets.associate { + it.sourceSetID to it.dependentSourceSets + } + + fun createDependenciesJson(): String = + dependenciesMap.map { (key, values) -> key.toString() to values.map { it.toString() } }.toMap() + .let { content -> + if (context.configuration.delayTemplateSubstitution) { + toJsonString(AddToSourcesetDependencies(context.configuration.moduleName, content)) + } else { + "sourceset_dependencies='${toJsonString(content)}'" + } + } + + val deps = RendererSpecificResourcePage( + name = name, + children = emptyList(), + strategy = RenderingStrategy.Write(createDependenciesJson()) + ) + + return input.modified( + children = input.children + deps + ).transformContentPagesTree { it.modified(embeddedResources = it.embeddedResources + name) } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt new file mode 100644 index 00000000..fe6f0089 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt @@ -0,0 +1,234 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html.innerTemplating + +import freemarker.core.Environment +import freemarker.template.* +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.DokkaBaseConfiguration +import org.jetbrains.dokka.base.renderers.URIExtension +import org.jetbrains.dokka.base.renderers.html.TEMPLATE_REPLACEMENT +import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer +import org.jetbrains.dokka.base.renderers.html.templateCommand +import org.jetbrains.dokka.base.renderers.html.templateCommandAsHtmlComment +import org.jetbrains.dokka.base.renderers.isImage +import org.jetbrains.dokka.base.resolvers.local.LocationProvider +import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand +import org.jetbrains.dokka.base.templating.ProjectNameSubstitutionCommand +import org.jetbrains.dokka.base.templating.ReplaceVersionsCommand +import org.jetbrains.dokka.base.templating.SubstitutionCommand +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.configuration +import java.net.URI + +public class DefaultTemplateModelFactory( + public val context: DokkaContext +) : TemplateModelFactory { + private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context) + private val isPartial = context.configuration.delayTemplateSubstitution + + private fun <R> TagConsumer<R>.prepareForTemplates() = + if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this + else ImmediateResolutionTagConsumer(this, context) + + public data class SourceSetModel(val name: String, val platform: String, val filter: String) + + override fun buildModel( + page: PageNode, + resources: List<String>, + locationProvider: LocationProvider, + content: String + ): TemplateMap { + val path = locationProvider.resolve(page) + val pathToRoot = locationProvider.pathToRoot(page) + val mapper = mutableMapOf<String, Any>() + mapper["pageName"] = page.name + mapper["resources"] = PrintDirective { + val sb = StringBuilder() + if (isPartial) + sb.templateCommandAsHtmlComment( + PathToRootSubstitutionCommand( + TEMPLATE_REPLACEMENT, + default = pathToRoot + ) + ) { resourcesForPage(TEMPLATE_REPLACEMENT, resources) } + else + sb.resourcesForPage(pathToRoot, resources) + sb.toString() + } + mapper["content"] = PrintDirective { content } + mapper["version"] = PrintDirective { + createHTML().prepareForTemplates().templateCommand(ReplaceVersionsCommand(path.orEmpty())) + } + mapper["template_cmd"] = TemplateDirective(context.configuration, pathToRoot) + + if (page is ContentPage) { + val sourceSets = page.content.withDescendants() + .flatMap { it.sourceSets } + .distinct() + .sortedBy { it.comparableKey } + .map { SourceSetModel(it.name, it.platform.key, it.sourceSetIDs.merged.toString()) } + .toList() + + if (sourceSets.isNotEmpty()) { + mapper["sourceSets"] = sourceSets + } + } + return mapper + } + + override fun buildSharedModel(): TemplateMap { + val mapper = mutableMapOf<String, Any>() + + mapper["footerMessage"] = + (configuration?.footerMessage?.takeIf(String::isNotBlank) ?: DokkaBaseConfiguration.defaultFooterMessage) + + configuration?.homepageLink?.takeIf(String::isNotBlank)?.let { mapper["homepageLink"] = it } + + return mapper + } + + private val DisplaySourceSet.comparableKey + get() = sourceSetIDs.merged.let { it.scopeId + it.sourceSetName } + private val String.isAbsolute: Boolean + get() = URI(this).isAbsolute + + private fun Appendable.resourcesForPage(pathToRoot: String, resources: List<String>): Unit = + resources.forEach { resource -> + + val resourceHtml = with(createHTML()) { + when { + + resource.URIExtension == "css" -> + link( + rel = LinkRel.stylesheet, + href = if (resource.isAbsolute) resource else "$pathToRoot$resource" + ) + + resource.URIExtension == "js" -> + script( + type = ScriptType.textJavaScript, + src = if (resource.isAbsolute) resource else "$pathToRoot$resource" + ) { + if (resource == "scripts/main.js" || resource.endsWith("_deferred.js")) + defer = true + else + async = true + } + + resource.isImage() -> link(href = if (resource.isAbsolute) resource else "$pathToRoot$resource") + else -> null + } + } + if (resourceHtml != null) { + append(resourceHtml) + } + } + +} + +private class PrintDirective(val generateData: () -> String) : TemplateDirectiveModel { + override fun execute( + env: Environment, + params: MutableMap<Any?, Any?>?, + loopVars: Array<TemplateModel>?, + body: TemplateDirectiveBody? + ) { + if (params?.isNotEmpty() == true) throw TemplateModelException( + "Parameters are not allowed" + ) + if (loopVars?.isNotEmpty() == true) throw TemplateModelException( + "Loop variables are not allowed" + ) + env.out.write(generateData()) + } +} + +private class TemplateDirective( + val configuration: DokkaConfiguration, + val pathToRoot: String +) : TemplateDirectiveModel { + override fun execute( + env: Environment, + params: MutableMap<Any?, Any?>?, + loopVars: Array<TemplateModel>?, + body: TemplateDirectiveBody? + ) { + val commandName = params?.get(PARAM_NAME) ?: throw TemplateModelException( + "The required $PARAM_NAME parameter is missing." + ) + val replacement = (params[PARAM_REPLACEMENT] as? SimpleScalar)?.asString ?: TEMPLATE_REPLACEMENT + + when ((commandName as? SimpleScalar)?.asString) { + "pathToRoot" -> { + body ?: throw TemplateModelException( + "No directive body for $commandName command." + ) + executeSubstituteCommand( + PathToRootSubstitutionCommand( + replacement, pathToRoot + ), + "pathToRoot", + pathToRoot, + Context(env, body) + ) + } + + "projectName" -> { + body ?: throw TemplateModelException( + "No directive body $commandName command." + ) + executeSubstituteCommand( + ProjectNameSubstitutionCommand( + replacement, configuration.moduleName + ), + "projectName", + configuration.moduleName, + Context(env, body) + ) + } + + else -> throw TemplateModelException( + "The parameter $PARAM_NAME $commandName is unknown" + ) + } + } + + private data class Context(val env: Environment, val body: TemplateDirectiveBody) + + private fun executeSubstituteCommand( + command: SubstitutionCommand, + name: String, + value: String, + ctx: Context + ) { + if (configuration.delayTemplateSubstitution) + ctx.env.out.templateCommandAsHtmlComment(command) { + renderWithLocalVar(name, command.pattern, ctx) + } + else { + renderWithLocalVar(name, value, ctx) + } + } + + private fun renderWithLocalVar(name: String, value: String, ctx: Context) = + with(ctx) { + env.setVariable(name, SimpleScalar(value)) + body.render(env.out) + env.setVariable(name, null) + } + + companion object { + const val PARAM_NAME = "name" + const val PARAM_REPLACEMENT = "replacement" + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt new file mode 100644 index 00000000..2f17183d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html.innerTemplating + +public class DefaultTemplateModelMerger : TemplateModelMerger { + override fun invoke( + factories: List<TemplateModelFactory>, + buildModel: TemplateModelFactory.() -> TemplateMap + ): TemplateMap { + val mapper = mutableMapOf<String, Any?>() + factories.map(buildModel).forEach { partialModel -> + partialModel.forEach { (k, v) -> + mapper[k] = v + } + } + return mapper + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/HtmlTemplater.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/HtmlTemplater.kt new file mode 100644 index 00000000..1638c9c0 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/HtmlTemplater.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html.innerTemplating + +import freemarker.cache.ClassTemplateLoader +import freemarker.cache.FileTemplateLoader +import freemarker.cache.MultiTemplateLoader +import freemarker.log.Logger +import freemarker.template.Configuration +import freemarker.template.TemplateExceptionHandler +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.DokkaBaseConfiguration +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.configuration +import java.io.StringWriter + + +public enum class DokkaTemplateTypes( + public val path: String +) { + BASE("base.ftl") +} + +public typealias TemplateMap = Map<String, Any?> + +public class HtmlTemplater( + context: DokkaContext +) { + + init { + // to disable logging, but it isn't reliable see [Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY] + // (use SLF4j further) + System.setProperty( + Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY, + System.getProperty(Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY) ?: Logger.LIBRARY_NAME_NONE + ) + } + + private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context) + private val templaterConfiguration = + Configuration(Configuration.VERSION_2_3_31).apply { configureTemplateEngine() } + + private fun Configuration.configureTemplateEngine() { + val loaderFromResources = ClassTemplateLoader(javaClass, "/dokka/templates") + templateLoader = configuration?.templatesDir?.let { + MultiTemplateLoader( + arrayOf( + FileTemplateLoader(it), + loaderFromResources + ) + ) + } ?: loaderFromResources + + unsetLocale() + defaultEncoding = "UTF-8" + templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLER + logTemplateExceptions = false + wrapUncheckedExceptions = true + fallbackOnNullLoopVariable = false + templateUpdateDelayMilliseconds = Long.MAX_VALUE + } + + public fun setupSharedModel(model: TemplateMap) { + templaterConfiguration.setSharedVariables(model) + } + + public fun renderFromTemplate( + templateType: DokkaTemplateTypes, + generateModel: () -> TemplateMap + ): String { + val out = StringWriter() + // Freemarker has own thread-safe cache to keep templates + val template = templaterConfiguration.getTemplate(templateType.path) + val model = generateModel() + template.process(model, out) + + return out.toString() + } +} + diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelFactory.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelFactory.kt new file mode 100644 index 00000000..3af11bf9 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelFactory.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html.innerTemplating + +import org.jetbrains.dokka.base.resolvers.local.LocationProvider +import org.jetbrains.dokka.pages.PageNode + +public interface TemplateModelFactory { + public fun buildModel( + page: PageNode, + resources: List<String>, + locationProvider: LocationProvider, + content: String + ): TemplateMap + + public fun buildSharedModel(): TemplateMap +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelMerger.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelMerger.kt new file mode 100644 index 00000000..ada0c6cd --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelMerger.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html.innerTemplating + +public fun interface TemplateModelMerger { + public fun invoke(factories: List<TemplateModelFactory>, buildModel: TemplateModelFactory.() -> TemplateMap): TemplateMap +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/shouldRenderSourceSetBubbles.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/shouldRenderSourceSetBubbles.kt new file mode 100644 index 00000000..a7bafadb --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/shouldRenderSourceSetBubbles.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers.html + +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.RootPageNode + +internal fun shouldRenderSourceSetTabs(page: RootPageNode): Boolean { + return page.withDescendants() + .flatMap { pageNode -> + if (pageNode is ContentPage) pageNode.content.withDescendants() + else emptySequence() + } + .flatMap { contentNode -> contentNode.sourceSets } + .distinct() + .count() > 1 +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/pageId.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/pageId.kt new file mode 100644 index 00000000..f5d75cfc --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/pageId.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.base.renderers.html.NavigationNode +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.pages.ContentPage + +internal val ContentPage.pageId: String + get() = pageId(dri.first(), sourceSets()) + +internal val NavigationNode.pageId: String + get() = pageId(dri, sourceSets) + +@JvmName("shortenSourceSetsToUrl") +internal fun Set<DisplaySourceSet>.shortenToUrl() = + sortedBy { it.sourceSetIDs.merged.let { it.scopeId + it.sourceSetName } }.joinToString().hashCode() + +internal fun DRI.shortenToUrl() = toString() + +@JvmName("shortenDrisToUrl") +internal fun Set<DRI>.shortenToUrl() = sortedBy { it.toString() }.joinToString().hashCode() + +/** + * Page Id is required to have a sourceSet in order to distinguish between different pages that has same DRI but different sourceSet + * like main functions that are not expect/actual + */ +private fun pageId(dri: DRI, sourceSets: Set<DisplaySourceSet>): String = "${dri.shortenToUrl()}/${sourceSets.shortenToUrl()}" diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/preprocessors.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/preprocessors.kt new file mode 100644 index 00000000..a3a32651 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/preprocessors.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.base.resolvers.shared.LinkFormat +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer + +public object RootCreator : PageTransformer { + override fun invoke(input: RootPageNode): RootPageNode = + RendererSpecificRootPage("", listOf(input), RenderingStrategy.DoNothing) +} + +public class PackageListCreator( + public val context: DokkaContext, + public val format: LinkFormat, + public val outputFilesNames: List<String> = listOf("package-list") +) : PageTransformer { + override fun invoke(input: RootPageNode): RootPageNode { + return input.transformPageNodeTree { pageNode -> + pageNode.takeIf { it is ModulePage }?.let { it.modified(children = it.children + packageList(input, it as ModulePage)) } ?: pageNode + } + } + + private fun packageList(rootPageNode: RootPageNode, module: ModulePage): List<RendererSpecificPage> { + val content = PackageListService(context, rootPageNode).createPackageList( + module, + format + ) + return outputFilesNames.map { fileName -> + RendererSpecificResourcePage( + fileName, + emptyList(), + RenderingStrategy.Write(content) + ) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/anchors/AnchorsHint.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/anchors/AnchorsHint.kt new file mode 100644 index 00000000..c9218947 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/anchors/AnchorsHint.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.anchors + +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.properties.ExtraProperty +import org.jetbrains.dokka.pages.ContentNode +import org.jetbrains.dokka.pages.Kind + +public data class SymbolAnchorHint(val anchorName: String, val contentKind: Kind) : ExtraProperty<ContentNode> { + override val key: ExtraProperty.Key<ContentNode, SymbolAnchorHint> = SymbolAnchorHint + + public companion object : ExtraProperty.Key<ContentNode, SymbolAnchorHint> { + public fun from(d: Documentable, contentKind: Kind): SymbolAnchorHint? = + d.name?.let { SymbolAnchorHint(it, contentKind) } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/DefaultExternalLocationProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/DefaultExternalLocationProvider.kt new file mode 100644 index 00000000..32825303 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/DefaultExternalLocationProvider.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider.Companion.identifierToFilename +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.plugability.DokkaContext + +public open class DefaultExternalLocationProvider( + public val externalDocumentation: ExternalDocumentation, + public val extension: String, + public val dokkaContext: DokkaContext +) : ExternalLocationProvider { + public val docURL: String = externalDocumentation.documentationURL.toString().removeSuffix("/") + "/" + + override fun resolve(dri: DRI): String? { + externalDocumentation.packageList.locations[dri.toString()]?.let { path -> return "$docURL$path" } + + if (dri.packageName !in externalDocumentation.packageList.packages) + return null + + return dri.constructPath() + } + + protected open fun DRI.constructPath(): String { + val modulePart = packageName?.let { packageName -> + externalDocumentation.packageList.moduleFor(packageName)?.let { + if (it.isNotBlank()) + "$it/" + else + "" + } + }.orEmpty() + + val docWithModule = docURL + modulePart + val classNamesChecked = classNames ?: return "$docWithModule${packageName ?: ""}/index$extension" + val classLink = (listOfNotNull(packageName) + classNamesChecked.split('.')) + .joinToString("/", transform = ::identifierToFilename) + + val fileName = callable?.let { identifierToFilename(it.name) } ?: "index" + return "$docWithModule$classLink/$fileName$extension" + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/DefaultExternalLocationProviderFactory.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/DefaultExternalLocationProviderFactory.kt new file mode 100644 index 00000000..09ddca01 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/DefaultExternalLocationProviderFactory.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.plugability.DokkaContext + +public class DefaultExternalLocationProviderFactory( + public val context: DokkaContext, +) : ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( + { doc -> + when (doc.packageList.linkFormat) { + RecognizedLinkFormat.KotlinWebsite, + RecognizedLinkFormat.KotlinWebsiteHtml, + RecognizedLinkFormat.DokkaOldHtml, + -> Dokka010ExternalLocationProvider(doc, ".html", context) + + RecognizedLinkFormat.DokkaHtml -> DefaultExternalLocationProvider(doc, ".html", context) + RecognizedLinkFormat.DokkaGFM, + RecognizedLinkFormat.DokkaJekyll, + -> DefaultExternalLocationProvider(doc, ".md", context) + + else -> null + } + } +) diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/Dokka010ExternalLocationProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/Dokka010ExternalLocationProvider.kt new file mode 100644 index 00000000..f887c9bc --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/Dokka010ExternalLocationProvider.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider.Companion.identifierToFilename +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.plugability.DokkaContext + +public open class Dokka010ExternalLocationProvider( + public val externalDocumentation: ExternalDocumentation, + public val extension: String, + public val dokkaContext: DokkaContext +) : ExternalLocationProvider { + public val docURL: String = externalDocumentation.documentationURL.toString().removeSuffix("/") + "/" + + override fun resolve(dri: DRI): String? { + + val fqName = listOfNotNull( + dri.packageName.takeIf { it?.isNotBlank() == true }, + dri.classNames.takeIf { it?.isNotBlank() == true }?.removeCompanion() + ).joinToString(".") + val relocationId = + fqName.let { if (dri.callable != null) it + "$" + dri.callable!!.toOldString() else it } + externalDocumentation.packageList.locations[relocationId]?.let { path -> return "$docURL$path" } + + if (dri.packageName !in externalDocumentation.packageList.packages) + return null + + val classNamesChecked = dri.classNames?.removeCompanion() + ?: return "$docURL${dri.packageName ?: ""}/index$extension" + + val classLink = (listOfNotNull(dri.packageName) + classNamesChecked.split('.')) + .joinToString("/", transform = ::identifierToFilename) + + val callableChecked = dri.callable ?: return "$docURL$classLink/index$extension" + return "$docURL$classLink/" + identifierToFilename(callableChecked.name) + extension + } + + private fun String.removeCompanion() = removeSuffix(".Companion") + + private fun Callable.toOldString() = name + params.joinToString(", ", "(", ")") + (receiver?.let { "#$it" } ?: "") +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProvider.kt new file mode 100644 index 00000000..238b6342 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProvider.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.links.DRI + +/** + * Provides the path to the page documenting a [DRI] in an external documentation source + */ +public fun interface ExternalLocationProvider { + /** + * @return Path to the page containing the [dri] or null if the path cannot be created + * (eg. when the package-list does not contain [dri]'s package) + */ + public fun resolve(dri: DRI): String? +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProviderFactory.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProviderFactory.kt new file mode 100644 index 00000000..952f4d51 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProviderFactory.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation + +public fun interface ExternalLocationProviderFactory { + public fun getExternalLocationProvider(doc: ExternalDocumentation): ExternalLocationProvider? +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProviderFactoryWithCache.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProviderFactoryWithCache.kt new file mode 100644 index 00000000..0b56e174 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProviderFactoryWithCache.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import java.util.concurrent.ConcurrentHashMap + +public class ExternalLocationProviderFactoryWithCache( + public val ext: ExternalLocationProviderFactory +) : ExternalLocationProviderFactory { + + private val locationProviders = ConcurrentHashMap<ExternalDocumentation, CacheWrapper>() + + override fun getExternalLocationProvider(doc: ExternalDocumentation): ExternalLocationProvider? = + locationProviders.getOrPut(doc) { CacheWrapper(ext.getExternalLocationProvider(doc)) }.provider + + private class CacheWrapper(val provider: ExternalLocationProvider?) +} + diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/AndroidExternalLocationProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/AndroidExternalLocationProvider.kt new file mode 100644 index 00000000..8c18be0c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/AndroidExternalLocationProvider.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.external.javadoc + +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.plugability.DokkaContext + +public open class AndroidExternalLocationProvider( + externalDocumentation: ExternalDocumentation, + dokkaContext: DokkaContext +) : JavadocExternalLocationProvider(externalDocumentation, "", "", dokkaContext) { + + override fun anchorPart(callable: Callable): String = callable.name.toLowerCase() + +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/JavadocExternalLocationProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/JavadocExternalLocationProvider.kt new file mode 100644 index 00000000..65ee0e02 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/JavadocExternalLocationProvider.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.external.javadoc + +import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.DRIExtraContainer +import org.jetbrains.dokka.links.EnumEntryDRIExtra +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.utilities.htmlEscape + +public open class JavadocExternalLocationProvider( + externalDocumentation: ExternalDocumentation, + public val brackets: String, + public val separator: String, + dokkaContext: DokkaContext +) : DefaultExternalLocationProvider(externalDocumentation, ".html", dokkaContext) { + + override fun DRI.constructPath(): String { + val packageLink = packageName?.replace(".", "/") + val modulePart = packageName?.let { packageName -> + externalDocumentation.packageList.moduleFor(packageName)?.let { + if (it.isNotBlank()) + "$it/" + else + "" + } + }.orEmpty() + + val docWithModule = docURL + modulePart + + if (classNames == null) { + return "$docWithModule$packageLink/package-summary$extension".htmlEscape() + } + + if (DRIExtraContainer(extra)[EnumEntryDRIExtra] != null) { + val lastIndex = classNames?.lastIndexOf(".") ?: 0 + val (classSplit, enumEntityAnchor) = + classNames?.substring(0, lastIndex) to classNames?.substring(lastIndex + 1) + + val classLink = + if (packageLink == null) "${classSplit}$extension" else "$packageLink/${classSplit}$extension" + return "$docWithModule$classLink#$enumEntityAnchor".htmlEscape() + } + + val classLink = if (packageLink == null) "${classNames}$extension" else "$packageLink/${classNames}$extension" + val callableChecked = callable ?: return "$docWithModule$classLink".htmlEscape() + + return ("$docWithModule$classLink#" + anchorPart(callableChecked)).htmlEscape() + } + + protected open fun anchorPart(callable: Callable): String { + return callable.name + + "${brackets.first()}" + + callable.params.joinToString(separator) + + "${brackets.last()}" + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/JavadocExternalLocationProviderFactory.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/JavadocExternalLocationProviderFactory.kt new file mode 100644 index 00000000..dc184e49 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/JavadocExternalLocationProviderFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.external.javadoc + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.androidSdk +import org.jetbrains.dokka.androidX +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProviderFactoryWithCache +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.plugability.DokkaContext + +public class JavadocExternalLocationProviderFactory( + public val context: DokkaContext, +) : ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( + { doc -> + when (doc.packageList.url) { + DokkaConfiguration.ExternalDocumentationLink.androidX().packageListUrl, + DokkaConfiguration.ExternalDocumentationLink.androidSdk().packageListUrl, + -> + AndroidExternalLocationProvider(doc, context) + + else -> + when (doc.packageList.linkFormat) { + RecognizedLinkFormat.Javadoc1 -> + JavadocExternalLocationProvider(doc, "()", ", ", context) // Covers JDK 1 - 7 + RecognizedLinkFormat.Javadoc8 -> + JavadocExternalLocationProvider(doc, "--", "-", context) // Covers JDK 8 - 9 + RecognizedLinkFormat.Javadoc10, + RecognizedLinkFormat.DokkaJavadoc, + -> + JavadocExternalLocationProvider(doc, "()", ",", context) // Covers JDK 10 + else -> null + } + } + } +) diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DefaultLocationProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DefaultLocationProvider.kt new file mode 100644 index 00000000..24d0f13e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DefaultLocationProvider.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.external.Dokka010ExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.external.javadoc.AndroidExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.external.javadoc.JavadocExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query + +public abstract class DefaultLocationProvider( + protected val pageGraphRoot: RootPageNode, + protected val dokkaContext: DokkaContext +) : LocationProvider { + protected val externalLocationProviderFactories: List<ExternalLocationProviderFactory> = + dokkaContext.plugin<DokkaBase>().query { externalLocationProviderFactory } + + protected val externalLocationProviders: Map<ExternalDocumentation, ExternalLocationProvider?> = dokkaContext + .configuration + .sourceSets + .flatMap { sourceSet -> + sourceSet.externalDocumentationLinks.map { + PackageList.load(it.packageListUrl, sourceSet.jdkVersion, dokkaContext.configuration.offlineMode) + ?.let { packageList -> ExternalDocumentation(it.url, packageList) } + } + } + .filterNotNull().associateWith { extDocInfo -> + externalLocationProviderFactories + .mapNotNull { it.getExternalLocationProvider(extDocInfo) } + .firstOrNull() + ?: run { dokkaContext.logger.error("No ExternalLocationProvider for '${extDocInfo.packageList.url}' found"); null } + } + + protected val packagesIndex: Map<String, ExternalLocationProvider?> = + externalLocationProviders + .flatMap { (extDocInfo, externalLocationProvider) -> + extDocInfo.packageList.packages.map { packageName -> packageName to externalLocationProvider } + }.groupBy { it.first }.mapValues { (_, lst) -> + lst.map { it.second } + .sortedWith(compareBy(nullsLast(ExternalLocationProviderOrdering)) { it }) + .firstOrNull() + } + .filterKeys(String::isNotBlank) + + + protected val locationsIndex: Map<String, ExternalLocationProvider?> = externalLocationProviders + .flatMap { (extDocInfo, externalLocationProvider) -> + extDocInfo.packageList.locations.keys.map { relocatedDri -> relocatedDri to externalLocationProvider } + } + .toMap() + .filterKeys(String::isNotBlank) + + protected open fun getExternalLocation(dri: DRI, sourceSets: Set<DisplaySourceSet>): String? = + packagesIndex[dri.packageName]?.resolve(dri) + ?: locationsIndex[dri.toString()]?.resolve(dri) + ?: externalLocationProviders.values.mapNotNull { it?.resolve(dri) }.firstOrNull() + + private object ExternalLocationProviderOrdering : Comparator<ExternalLocationProvider> { + private val desiredOrdering = listOf( + DefaultExternalLocationProvider::class, + Dokka010ExternalLocationProvider::class, + AndroidExternalLocationProvider::class, + JavadocExternalLocationProvider::class + ) + + override fun compare(o1: ExternalLocationProvider, o2: ExternalLocationProvider): Int = + desiredOrdering.indexOf(o1::class).compareTo(desiredOrdering.indexOf(o2::class)) + } + +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaBaseLocationProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaBaseLocationProvider.kt new file mode 100644 index 00000000..ca3786ad --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaBaseLocationProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.base.renderers.shortenToUrl +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.pages.DCI +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.utilities.urlEncoded + +public abstract class DokkaBaseLocationProvider( + pageGraphRoot: RootPageNode, + dokkaContext: DokkaContext +) : DefaultLocationProvider(pageGraphRoot, dokkaContext) { + + /** + * Anchors should be unique and should contain sourcesets, dri and contentKind. + * The idea is to make them as short as possible and just use a hashCode from sourcesets in order to match the + * 2040 characters limit + */ + public open fun anchorForDCI(dci: DCI, sourceSets: Set<DisplaySourceSet>): String = + (dci.dri.shortenToUrl().toString() + "/" + dci.kind + "/" + sourceSets.shortenToUrl()).urlEncoded() + +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaLocationProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaLocationProvider.kt new file mode 100644 index 00000000..aedbfb88 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaLocationProvider.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.base.renderers.sourceSets +import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import java.util.* + +public open class DokkaLocationProvider( + pageGraphRoot: RootPageNode, + dokkaContext: DokkaContext, + public val extension: String = ".html" +) : DokkaBaseLocationProvider(pageGraphRoot, dokkaContext) { + protected open val PAGE_WITH_CHILDREN_SUFFIX: String = "index" + + protected open val pathsIndex: Map<PageNode, List<String>> = IdentityHashMap<PageNode, List<String>>().apply { + fun registerPath(page: PageNode, prefix: List<String>) { + if (page is RootPageNode && page.forceTopLevelName) { + put(page, prefix + PAGE_WITH_CHILDREN_SUFFIX) + page.children.forEach { registerPath(it, prefix) } + } else { + val newPrefix = prefix + page.pathName + put(page, if (page is ModulePageNode) prefix else newPrefix) + page.children.forEach { registerPath(it, newPrefix) } + } + + } + put(pageGraphRoot, emptyList()) + pageGraphRoot.children.forEach { registerPath(it, emptyList()) } + } + + protected val pagesIndex: Map<DRIWithSourceSets, ContentPage> = + pageGraphRoot.withDescendants().filterIsInstance<ContentPage>() + .flatMap { page -> + page.dri.flatMap { dri -> + page.sourceSets().ifEmpty { setOf(null) } + .map { sourceSet -> DRIWithSourceSets(dri, setOfNotNull(sourceSet)) to page } + .let { + if (it.size > 1) { + it + (DRIWithSourceSets(dri, page.sourceSets()) to page) + } else { + it + } + } + } + } + .groupingBy { it.first } + .aggregate { key, _, (_, page), first -> + if (first) page else throw AssertionError("Multiple pages associated with key: ${key.dri}/${key.sourceSet}") + } + + protected val anchorsIndex: Map<DRIWithSourceSets, PageWithKind> = + pageGraphRoot.withDescendants().filterIsInstance<ContentPage>() + .flatMap { page -> + page.content.withDescendants() + .filter { it.extra[SymbolAnchorHint] != null && it.dci.dri.any() } + .flatMap { content -> + content.dci.dri.map { dri -> + (dri to content.sourceSets) to content.extra[SymbolAnchorHint]?.contentKind!! + } + } + .distinct() + .flatMap { (pair, kind) -> + val (dri, sourceSets) = pair + sourceSets.ifEmpty { setOf(null) }.map { sourceSet -> + DRIWithSourceSets(dri, setOfNotNull(sourceSet)) to PageWithKind(page, kind) + } + } + }.toMap() + + override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean): String = + pathTo(node, context) + if (!skipExtension) extension else "" + + override fun resolve(dri: DRI, sourceSets: Set<DisplaySourceSet>, context: PageNode?): String? = + sourceSets.ifEmpty { setOf(null) }.mapNotNull { sourceSet -> + val driWithSourceSets = DRIWithSourceSets(dri, setOfNotNull(sourceSet)) + getLocalLocation(driWithSourceSets, context) + ?: getLocalLocation(driWithSourceSets.copy(dri = dri.copy(target = PointingToDeclaration)), context) + // Not found in PageGraph, that means it's an external link + ?: getExternalLocation(dri, sourceSets) + ?: getExternalLocation(dri.copy(target = PointingToDeclaration), sourceSets) + }.distinct().singleOrNull() + + private fun getLocalLocation(driWithSourceSets: DRIWithSourceSets, context: PageNode?): String? { + val (dri, originalSourceSet) = driWithSourceSets + val allSourceSets: List<Set<DisplaySourceSet>> = + listOf(originalSourceSet) + originalSourceSet.let { oss -> + val ossIds = oss.computeSourceSetIds() + dokkaContext.configuration.sourceSets.filter { it.sourceSetID in ossIds } + .flatMap { it.dependentSourceSets } + .mapNotNull { ssid -> + dokkaContext.configuration.sourceSets.find { it.sourceSetID == ssid }?.toDisplaySourceSet() + }.map { + setOf(it) + } + } + + return getLocalPageLink(dri, allSourceSets, context) + ?: getLocalAnchor(dri, allSourceSets, context) + } + + private fun getLocalPageLink(dri: DRI, allSourceSets: Iterable<Set<DisplaySourceSet>>, context: PageNode?) = + allSourceSets.mapNotNull { displaySourceSet -> + pagesIndex[DRIWithSourceSets(dri, displaySourceSet)] + }.firstOrNull()?.let { page -> resolve(page, context) } + + private fun getLocalAnchor(dri: DRI, allSourceSets: Iterable<Set<DisplaySourceSet>>, context: PageNode?) = + allSourceSets.mapNotNull { displaySourceSet -> + anchorsIndex[DRIWithSourceSets(dri, displaySourceSet)]?.let { (page, kind) -> + val dci = DCI(setOf(dri), kind) + resolve(page, context) + "#" + anchorForDCI(dci, displaySourceSet) + } + }.firstOrNull() + + override fun pathToRoot(from: PageNode): String = + pathTo(pageGraphRoot, from).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 the current page graph so it is impossible to compute its path" + ) + + val nodePath = pathFor(node) + val contextPath = context?.let { pathFor(it) }.orEmpty() + val endedContextPath = if (context?.isIndexPage() == false) + contextPath.toMutableList().also { it.removeLastOrNull() } + else contextPath + + val commonPathElements = nodePath.asSequence().zip(endedContextPath.asSequence()) + .takeWhile { (a, b) -> a == b }.count() + + return (List(endedContextPath.size - commonPathElements) { ".." } + nodePath.drop(commonPathElements) + + if (node.isIndexPage()) + listOf(PAGE_WITH_CHILDREN_SUFFIX) + else + emptyList() + ).joinToString("/") + } + + private fun PageNode.isIndexPage() = this is ClasslikePageNode || children.isNotEmpty() + + private fun PageNode.parent() = pageGraphRoot.parentMap[this] + + private val PageNode.pathName: String + get() = if (this is PackagePageNode || this is RendererSpecificResourcePage) name else identifierToFilename(name) + + protected data class DRIWithSourceSets(val dri: DRI, val sourceSet: Set<DisplaySourceSet>) + + protected data class PageWithKind(val page: ContentPage, val kind: Kind) + + public companion object { + public val reservedFilenames: Set<String> = setOf("index", "con", "aux", "lst", "prn", "nul", "eof", "inp", "out") + + //Taken from: https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names + internal val reservedCharacters = setOf('|', '>', '<', '*', ':', '"', '?', '%') + + public fun identifierToFilename(name: String): String { + if (name.isEmpty()) return "--root--" + return sanitizeFileName(name, reservedFilenames, reservedCharacters) + } + } +} + +internal fun sanitizeFileName(name: String, reservedFileNames: Set<String>, reservedCharacters: Set<Char>): String { + val lowercase = name.replace("[A-Z]".toRegex()) { matchResult -> "-" + matchResult.value.toLowerCase() } + val withoutReservedFileNames = if (lowercase in reservedFileNames) "--$lowercase--" else lowercase + return reservedCharacters.fold(withoutReservedFileNames) { acc, character -> + if (character in acc) acc.replace(character.toString(), "[${character.toInt()}]") + else acc + } +} + diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaLocationProviderFactory.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaLocationProviderFactory.kt new file mode 100644 index 00000000..bd9fa1bb --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaLocationProviderFactory.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import java.util.concurrent.ConcurrentHashMap + +public class DokkaLocationProviderFactory( + private val context: DokkaContext +) : LocationProviderFactory { + private val cache = ConcurrentHashMap<CacheWrapper, LocationProvider>() + + override fun getLocationProvider(pageNode: RootPageNode): LocationProvider { + return cache.computeIfAbsent(CacheWrapper(pageNode)) { + DokkaLocationProvider(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) + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/LocationProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/LocationProvider.kt new file mode 100644 index 00000000..dbcd5c76 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/LocationProvider.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.DokkaException +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider.Companion.identifierToFilename +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.pages.PageNode + +public interface LocationProvider { + public fun resolve(dri: DRI, sourceSets: Set<DisplaySourceSet>, context: PageNode? = null): String? + public fun resolve(node: PageNode, context: PageNode? = null, skipExtension: Boolean = false): String? + public fun pathToRoot(from: PageNode): String + public fun ancestors(node: PageNode): List<PageNode> + + /** + * This method should return guessed filesystem location for a given [DRI] + * It is used to decide if a [DRI] should be present in the relocation list of the + * generated package-list so it is ok if the path differs from the one returned by [resolve] + * @return Path to a giver [DRI] or null if path should not be considered for relocations + */ + public fun expectedLocationForDri(dri: DRI): String = + (listOf(dri.packageName) + + dri.classNames?.split(".")?.map { identifierToFilename(it) }.orEmpty() + + listOf(dri.callable?.let { identifierToFilename(it.name) } ?: "index") + ).filterNotNull().joinToString("/") +} + +public fun LocationProvider.resolveOrThrow( + dri: DRI, sourceSets: Set<DisplaySourceSet>, + context: PageNode? = null +): String { + return resolve(dri = dri, sourceSets = sourceSets, context = context) + ?: throw DokkaException("Cannot resolve path for $dri") +} + +public fun LocationProvider.resolveOrThrow( + node: PageNode, + context: PageNode? = null, + skipExtension: Boolean = false +): String { + return resolve(node = node, context = context, skipExtension = skipExtension) + ?: throw DokkaException("Cannot resolve path for ${node.name}") +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/LocationProviderFactory.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/LocationProviderFactory.kt new file mode 100644 index 00000000..31cac868 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/LocationProviderFactory.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.pages.RootPageNode + +public fun interface LocationProviderFactory { + public fun getLocationProvider(pageNode: RootPageNode): LocationProvider +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/ExternalDocumentation.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/ExternalDocumentation.kt new file mode 100644 index 00000000..db0c5492 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/ExternalDocumentation.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.shared + +import java.net.URL + +public data class ExternalDocumentation(val documentationURL: URL, val packageList: PackageList) diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/LinkFormat.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/LinkFormat.kt new file mode 100644 index 00000000..4f0d4932 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/LinkFormat.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.shared + +public interface LinkFormat { + public val formatName: String + public val linkExtension: String +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/PackageList.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/PackageList.kt new file mode 100644 index 00000000..8297f875 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/PackageList.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.shared + +import java.net.URL + +public typealias Module = String + +public data class PackageList( + val linkFormat: RecognizedLinkFormat, + val modules: Map<Module, Set<String>>, + val locations: Map<String, String>, + val url: URL +) { + val packages: Set<String> + get() = modules.values.flatten().toSet() + + public fun moduleFor(packageName: String): Module? { + return modules.asSequence() + .filter { it.value.contains(packageName) } + .firstOrNull()?.key + } + + public companion object { + public const val PACKAGE_LIST_NAME: String = "package-list" + public const val MODULE_DELIMITER: String = "module:" + public const val DOKKA_PARAM_PREFIX: String = "\$dokka" + public const val SINGLE_MODULE_NAME: String = "" + + public fun load(url: URL, jdkVersion: Int, offlineMode: Boolean = false): PackageList? { + if (offlineMode && url.protocol.toLowerCase() != "file") + return null + + val packageListStream = runCatching { url.readContent() }.onFailure { + println("Failed to download package-list from $url, this might suggest that remote resource is not available," + + " module is empty or dokka output got corrupted") + return null + }.getOrThrow() + + val (params, packages) = packageListStream + .bufferedReader() + .useLines { lines -> lines.partition { it.startsWith(DOKKA_PARAM_PREFIX) } } + + val paramsMap = splitParams(params) + val format = linkFormat(paramsMap["format"]?.singleOrNull(), jdkVersion) + val locations = splitLocations(paramsMap["location"].orEmpty()).filterKeys(String::isNotEmpty) + + val modulesMap = splitPackages(packages) + return PackageList(format, modulesMap, locations, url) + } + + private fun splitParams(params: List<String>) = params.asSequence() + .map { it.removePrefix("$DOKKA_PARAM_PREFIX.").split(":", limit = 2) } + .groupBy({ (key, _) -> key }, { (_, value) -> value }) + + private fun splitLocations(locations: List<String>) = locations.map { it.split("\u001f", limit = 2) } + .associate { (key, value) -> key to value } + + private fun splitPackages(packages: List<String>): Map<Module, Set<String>> = + packages.fold(("" to mutableMapOf<Module, Set<String>>())) { (lastModule, acc), el -> + val currentModule : String + when { + el.startsWith(MODULE_DELIMITER) -> currentModule = el.substringAfter(MODULE_DELIMITER) + el.isNotBlank() -> { + currentModule = lastModule + acc[currentModule] = acc.getOrDefault(lastModule, emptySet()) + el + } + else -> currentModule = lastModule + } + currentModule to acc + }.second + + private fun linkFormat(formatName: String?, jdkVersion: Int) = + formatName?.let { RecognizedLinkFormat.fromString(it) } + ?: when { + jdkVersion < 8 -> RecognizedLinkFormat.Javadoc1 // Covers JDK 1 - 7 + jdkVersion < 10 -> RecognizedLinkFormat.Javadoc8 // Covers JDK 8 - 9 + else -> RecognizedLinkFormat.Javadoc10 // Covers JDK 10+ + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/RecognizedLinkFormat.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/RecognizedLinkFormat.kt new file mode 100644 index 00000000..4810c9e5 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/RecognizedLinkFormat.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.shared + +public enum class RecognizedLinkFormat( + override val formatName: String, + override val linkExtension: String +) : LinkFormat { + DokkaHtml("html-v1", "html"), + DokkaJavadoc("javadoc-v1", "html"), + DokkaGFM("gfm-v1", "md"), + DokkaJekyll("jekyll-v1", "html"), + Javadoc1("javadoc1", "html"), + Javadoc8("javadoc8", "html"), + Javadoc10("javadoc10", "html"), + DokkaOldHtml("html", "html"), + KotlinWebsite("kotlin-website", "html"), + KotlinWebsiteHtml("kotlin-website-html", "html"); + + public companion object { + private val values = values() + + public fun fromString(formatName: String): RecognizedLinkFormat? { + return values.firstOrNull { it.formatName == formatName } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/utils.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/utils.kt new file mode 100644 index 00000000..a6d9afc6 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/utils.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.resolvers.shared + +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection + +internal fun URL.readContent(timeout: Int = 10000, redirectsAllowed: Int = 16): InputStream { + fun URL.doOpenConnection(timeout: Int, redirectsAllowed: Int): 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).doOpenConnection(timeout, redirectsAllowed - 1) + } else { + throw RuntimeException("Too many redirects") + } + } + + else -> throw RuntimeException("Unhandled HTTP code: ${connection.responseCode}") + } + + else -> return connection + } + } + return doOpenConnection(timeout, redirectsAllowed).getInputStream() +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/JvmSignatureUtils.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/JvmSignatureUtils.kt new file mode 100644 index 00000000..e5f85803 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/JvmSignatureUtils.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.signatures + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.drisOfAllNestedBounds +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.AnnotationTarget +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.* + +public interface JvmSignatureUtils { + + public fun PageContentBuilder.DocumentableContentBuilder.annotationsBlock(d: AnnotationTarget) + + public fun PageContentBuilder.DocumentableContentBuilder.annotationsInline(d: AnnotationTarget) + + public fun <T : Documentable> WithExtraProperties<T>.modifiers(): SourceSetDependent<Set<ExtraModifiers>> + + public fun Collection<ExtraModifiers>.toSignatureString(): String = + joinToString("") { it.name.toLowerCase() + " " } + + @Suppress("UNCHECKED_CAST") + public fun Documentable.annotations(): Map<DokkaSourceSet, List<Annotations.Annotation>> { + return (this as? WithExtraProperties<Documentable>)?.annotations() ?: emptyMap() + } + + public fun <T : AnnotationTarget> WithExtraProperties<T>.annotations(): SourceSetDependent<List<Annotations.Annotation>> = + extra[Annotations]?.directAnnotations ?: emptyMap() + + @Suppress("UNCHECKED_CAST") + public operator fun <T : Iterable<*>> SourceSetDependent<T>.plus(other: SourceSetDependent<T>): SourceSetDependent<T> { + return LinkedHashMap(this).apply { + for ((k, v) in other) { + put(k, get(k).let { if (it != null) (it + v) as T else v }) + } + } + } + + public fun DProperty.annotations(): SourceSetDependent<List<Annotations.Annotation>> { + return (extra[Annotations]?.directAnnotations ?: emptyMap()) + + (getter?.annotations() ?: emptyMap()).mapValues { it.value.map { it.copy( scope = Annotations.AnnotationScope.GETTER) } } + + (setter?.annotations() ?: emptyMap()).mapValues { it.value.map { it.copy( scope = Annotations.AnnotationScope.SETTER) } } + } + + private fun PageContentBuilder.DocumentableContentBuilder.annotations( + d: AnnotationTarget, + 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() + is TypeParameter -> d.annotations() + is GenericTypeConstructor -> d.annotations() + is FunctionalTypeConstructor -> d.annotations() + is JavaObject -> 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 + + public fun PageContentBuilder.DocumentableContentBuilder.toSignatureString( + a: Annotations.Annotation, + renderAtStrategy: AtStrategy, + listBrackets: Pair<Char, Char>, + classExtension: String + ) { + + when (renderAtStrategy) { + is All, is OnlyOnce -> { + when(a.scope) { + Annotations.AnnotationScope.GETTER -> text("@get:", styles = mainStyles + TokenStyle.Annotation) + Annotations.AnnotationScope.SETTER -> text("@set:", styles = mainStyles + TokenStyle.Annotation) + else -> text("@", styles = mainStyles + TokenStyle.Annotation) + } + link(a.dri.classNames!!, a.dri, styles = mainStyles + TokenStyle.Annotation) + } + is Never -> link(a.dri.classNames!!, a.dri) + } + val isNoWrappedBrackets = a.params.entries.isEmpty() && renderAtStrategy is OnlyOnce + listParams( + a.params.entries, + if (isNoWrappedBrackets) null else Pair('(', ')') + ) { + text(it.key) + text(" = ", styles = mainStyles + TokenStyle.Operator) + when (renderAtStrategy) { + is All -> All + is Never, is OnlyOnce -> Never + }.let { strategy -> + valueToSignature(it.value, strategy, listBrackets, classExtension) + } + } + } + + 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 -> { + listParams(a.value, listBrackets) { valueToSignature(it, renderAtStrategy, listBrackets, classExtension) } + } + is EnumValue -> link(a.enumName, a.enumDri) + is ClassValue -> link(a.className + classExtension, a.classDRI) + is StringValue -> group(styles = setOf(TextStyle.Breakable)) { stringLiteral( "\"${a.text()}\"") } + is BooleanValue -> group(styles = setOf(TextStyle.Breakable)) { booleanLiteral(a.value) } + is LiteralValue -> group(styles = setOf(TextStyle.Breakable)) { constant(a.text()) } + } + + private fun<T> PageContentBuilder.DocumentableContentBuilder.listParams( + params: Collection<T>, + listBrackets: Pair<Char, Char>?, + outFn: PageContentBuilder.DocumentableContentBuilder.(T) -> Unit + ) { + listBrackets?.let{ punctuation(it.first.toString()) } + params.forEachIndexed { i, it -> + group(styles = setOf(TextStyle.BreakableAfter)) { + this.outFn(it) + if (i != params.size - 1) punctuation(", ") + } + } + listBrackets?.let{ punctuation(it.second.toString()) } + } + + public fun PageContentBuilder.DocumentableContentBuilder.annotationsBlockWithIgnored( + d: AnnotationTarget, + ignored: Set<Annotations.Annotation>, + renderAtStrategy: AtStrategy, + listBrackets: Pair<Char, Char>, + classExtension: String + ) { + annotations(d, ignored, setOf(TextStyle.Block)) { + group { + toSignatureString(it, renderAtStrategy, listBrackets, classExtension) + } + } + } + + public fun PageContentBuilder.DocumentableContentBuilder.annotationsInlineWithIgnored( + d: AnnotationTarget, + 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()) + } + } + + public fun <T : Documentable> WithExtraProperties<T>.stylesIfDeprecated(sourceSetData: DokkaSourceSet): Set<TextStyle> { + val directAnnotations = extra[Annotations]?.directAnnotations?.get(sourceSetData) ?: emptyList() + val hasAnyDeprecatedAnnotation = + directAnnotations.any { it.dri == DRI("kotlin", "Deprecated") || it.dri == DRI("java.lang", "Deprecated") } + + return if (hasAnyDeprecatedAnnotation) setOf(TextStyle.Strikethrough) else emptySet() + } + + public infix fun DFunction.uses(typeParameter: DTypeParameter): Boolean { + val parameterDris = parameters.flatMap { listOf(it.dri) + it.type.drisOfAllNestedBounds } + val receiverDris = + listOfNotNull( + receiver?.dri, + *receiver?.type?.drisOfAllNestedBounds?.toTypedArray() ?: emptyArray() + ) + val allDris = parameterDris + receiverDris + return typeParameter.dri in allDris + } + + /** + * Builds a distinguishable [function] parameters block, so that it + * can be processed or custom rendered down the road. + * + * Resulting structure: + * ``` + * SymbolContentKind.Parameters(style = wrapped) { + * SymbolContentKind.Parameter(style = indented) { param, } + * SymbolContentKind.Parameter(style = indented) { param, } + * SymbolContentKind.Parameter(style = indented) { param } + * } + * ``` + * Wrapping and indentation of parameters is applied conditionally, see [shouldWrapParams] + */ + public fun PageContentBuilder.DocumentableContentBuilder.parametersBlock( + function: DFunction, + paramBuilder: PageContentBuilder.DocumentableContentBuilder.(DParameter) -> Unit + ) { + group(kind = SymbolContentKind.Parameters, styles = emptySet()) { + function.parameters.dropLast(1).forEach { + group(kind = SymbolContentKind.Parameter) { + paramBuilder(it) + punctuation(", ") + } + } + group(kind = SymbolContentKind.Parameter) { + paramBuilder(function.parameters.last()) + } + } + } +} + +public sealed class AtStrategy +public object All : AtStrategy() +public object OnlyOnce : AtStrategy() +public object Never : AtStrategy() diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureProvider.kt new file mode 100644 index 00000000..2180e776 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureProvider.kt @@ -0,0 +1,503 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.signatures + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.DokkaBase +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.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.utilities.DokkaLogger +import kotlin.text.Typography.nbsp + +public class KotlinSignatureProvider( + ctcc: CommentsToContentConverter, + logger: DokkaLogger +) : SignatureProvider, JvmSignatureUtils by KotlinSignatureUtils { + + public constructor(context: DokkaContext) : this( + context.plugin<DokkaBase>().querySingle { commentsToContentConverter }, + context.logger, + ) + + 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, Platform.wasm) + ) + + 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()) + } + }, styles = mainStyles + TokenStyle.Keyword + ) { + it.toSignatureString() + } + } + + private fun signature(e: DEnumEntry): List<ContentNode> = + e.sourceSets.map { + contentBuilder.contentFor( + e, + ContentKind.Symbol, + setOf(TextStyle.Monospace), + sourceSets = setOf(it) + ) { + group(styles = setOf(TextStyle.Block)) { + annotationsBlock(e) + link(e.name, e.dri, styles = mainStyles + e.stylesIfDeprecated(it)) + } + } + } + + private fun classlikeSignature(c: DClasslike): List<ContentNode> { + @Suppress("UNCHECKED_CAST") + val typeAlias = (c as? WithExtraProperties<DClasslike>) + ?.extra + ?.get(ActualTypealias) + ?.typeAlias + + return c.sourceSets.map { sourceSetData -> + if (typeAlias != null && sourceSetData in typeAlias.sourceSets) { + regularSignature(typeAlias, sourceSetData) + } else { + regularSignature(c, sourceSetData) + } + } + } + + private fun <T : Documentable> PageContentBuilder.DocumentableContentBuilder.defaultValueAssign( + d: WithExtraProperties<T>, + sourceSet: DokkaSourceSet + ) { + // a default value of parameter can be got from expect source set + // but expect properties cannot have a default value + d.extra[DefaultValue]?.expression?.let { + it[sourceSet] ?: if (d is DParameter) it[d.expectPresentInSet] else null + }?.let { expr -> + operator(" = ") + highlightValue(expr) + } + } + + private fun regularSignature(c: DClasslike, sourceSet: DokkaSourceSet): ContentGroup { + @Suppress("UNCHECKED_CAST") + val deprecationStyles = (c as? WithExtraProperties<out Documentable>) + ?.stylesIfDeprecated(sourceSet) + ?: emptySet() + + return contentBuilder.contentFor( + c, + ContentKind.Symbol, + setOf(TextStyle.Monospace), + sourceSets = setOf(sourceSet) + ) { + annotationsBlock(c) + c.visibility[sourceSet]?.takeIf { it !in ignoredVisibilities }?.name?.let { keyword("$it ") } + if (c.isExpectActual) keyword(if (sourceSet == c.expectPresentInSet) "expect " else "actual ") + if (c is DClass) { + val modifier = + if (c.modifier[sourceSet] !in ignoredModifiers) { + when { + c.extra[AdditionalModifiers]?.content?.get(sourceSet)?.contains(ExtraModifiers.KotlinOnlyModifiers.Data) == true -> "" + c.modifier[sourceSet] is JavaModifier.Empty -> "${KotlinModifier.Open.name} " + else -> c.modifier[sourceSet]?.name?.let { "$it " } + } + } else { + null + } + modifier?.takeIf { it.isNotEmpty() }?.let { keyword(it) } + } + when (c) { + is DClass -> { + processExtraModifiers(c) + keyword("class ") + } + is DInterface -> { + processExtraModifiers(c) + keyword("interface ") + } + is DEnum -> { + processExtraModifiers(c) + keyword("enum ") + } + is DObject -> { + processExtraModifiers(c) + keyword("object ") + } + is DAnnotation -> { + processExtraModifiers(c) + keyword("annotation class ") + } + } + link(c.name!!, c.dri, styles = mainStyles + deprecationStyles) + if (c is WithGenerics) { + list(c.generics, prefix = "<", suffix = ">", + separatorStyles = mainStyles + TokenStyle.Punctuation, + surroundingCharactersStyle = mainStyles + TokenStyle.Operator) { + annotationsInline(it) + +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) + keyword("constructor") + } + + // for primary constructor, opening and closing parentheses + // should be present only if it has parameters. If there are + // no parameters, it should result in `class Example` + if (pConstructor.parameters.isNotEmpty()) { + val parameterPropertiesByName = c.properties + .filter { it.isAlsoParameter(sourceSet) } + .associateBy { it.name } + + punctuation("(") + parametersBlock(pConstructor) { param -> + annotationsInline(param) + parameterPropertiesByName[param.name]?.let { property -> + property.setter?.let { keyword("var ") } ?: keyword("val ") + } + text(param.name.orEmpty()) + operator(": ") + signatureForProjection(param.type) + defaultValueAssign(param, sourceSet) + } + punctuation(")") + } + } + } + if (c is WithSupertypes) { + c.supertypes.filter { it.key == sourceSet }.map { (s, typeConstructors) -> + list(typeConstructors, prefix = " : ", sourceSets = setOf(s)) { + link(it.typeConstructor.dri.sureClassNames, it.typeConstructor.dri, sourceSets = setOf(s)) + list(it.typeConstructor.projections, prefix = "<", suffix = "> ", + separatorStyles = mainStyles + TokenStyle.Punctuation, + surroundingCharactersStyle = mainStyles + TokenStyle.Operator) { + signatureForProjection(it) + } + } + } + } + } + } + + /** + * An example would be a primary constructor `class A(val s: String)`, + * where `s` is both a function parameter and a property + */ + private fun DProperty.isAlsoParameter(sourceSet: DokkaSourceSet): Boolean { + return this.extra[IsAlsoParameter] + ?.inSourceSets + ?.any { it.sourceSetID == sourceSet.sourceSetID } + ?: false + } + + private fun propertySignature(p: DProperty) = + p.sourceSets.map { sourceSet -> + contentBuilder.contentFor( + p, + ContentKind.Symbol, + setOf(TextStyle.Monospace), + sourceSets = setOf(sourceSet) + ) { + annotationsBlock(p) + p.visibility[sourceSet].takeIf { it !in ignoredVisibilities }?.name?.let { keyword("$it ") } + if (p.isExpectActual) keyword(if (sourceSet == p.expectPresentInSet) "expect " else "actual ") + p.modifier[sourceSet].takeIf { it !in ignoredModifiers }?.let { + if (it is JavaModifier.Empty) KotlinModifier.Open else it + }?.name?.let { keyword("$it ") } + p.modifiers()[sourceSet]?.toSignatureString()?.let { keyword(it) } + if (p.isMutable()) keyword("var ") else keyword("val ") + list(p.generics, prefix = "<", suffix = "> ", + separatorStyles = mainStyles + TokenStyle.Punctuation, + surroundingCharactersStyle = mainStyles + TokenStyle.Operator) { + annotationsInline(it) + +buildSignature(it) + } + p.receiver?.also { + signatureForProjection(it.type) + punctuation(".") + } + link(p.name, p.dri, styles = mainStyles + p.stylesIfDeprecated(sourceSet)) + operator(": ") + signatureForProjection(p.type) + + if (p.isNotMutable()) { + defaultValueAssign(p, sourceSet) + } + } + } + + private fun DProperty.isNotMutable(): Boolean = !isMutable() + + private fun DProperty.isMutable(): Boolean { + return this.extra[IsVar] != null || this.setter != null + } + + private fun PageContentBuilder.DocumentableContentBuilder.highlightValue(expr: Expression) = when (expr) { + is IntegerConstant -> constant(expr.value.toString()) + is FloatConstant -> constant(expr.value.toString() + "f") + is DoubleConstant -> constant(expr.value.toString()) + is BooleanConstant -> booleanLiteral(expr.value) + is StringConstant -> stringLiteral("\"${expr.value}\"") + is ComplexExpression -> text(expr.value) + else -> Unit + } + + private fun functionSignature(f: DFunction) = + f.sourceSets.map { sourceSet -> + contentBuilder.contentFor( + f, + ContentKind.Symbol, + setOf(TextStyle.Monospace), + sourceSets = setOf(sourceSet) + ) { + annotationsBlock(f) + f.visibility[sourceSet]?.takeIf { it !in ignoredVisibilities }?.name?.let { keyword("$it ") } + if (f.isExpectActual) keyword(if (sourceSet == f.expectPresentInSet) "expect " else "actual ") + if (f.isConstructor) { + keyword("constructor") + } else { + f.modifier[sourceSet]?.takeIf { it !in ignoredModifiers }?.let { + if (it is JavaModifier.Empty) KotlinModifier.Open else it + }?.name?.let { keyword("$it ") } + f.modifiers()[sourceSet]?.toSignatureString()?.let { keyword(it) } + keyword("fun ") + list( + f.generics, prefix = "<", suffix = "> ", + separatorStyles = mainStyles + TokenStyle.Punctuation, + surroundingCharactersStyle = mainStyles + TokenStyle.Operator + ) { + annotationsInline(it) + +buildSignature(it) + } + f.receiver?.also { + signatureForProjection(it.type) + punctuation(".") + } + link(f.name, f.dri, styles = mainStyles + TokenStyle.Function + f.stylesIfDeprecated(sourceSet)) + } + // for a function, opening and closing parentheses must be present + // anyway, even if it has no parameters, resulting in `fun test(): R` + punctuation("(") + if (f.parameters.isNotEmpty()) { + parametersBlock(f) { param -> + annotationsInline(param) + processExtraModifiers(param) + text(param.name!!) + operator(": ") + signatureForProjection(param.type) + defaultValueAssign(param, sourceSet) + } + } + punctuation(")") + if (f.documentReturnType()) { + operator(": ") + 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 { + regularSignature(t, it) + } + + private fun regularSignature( + t: DTypeAlias, + sourceSet: DokkaSourceSet + ) = contentBuilder.contentFor(t, sourceSets = setOf(sourceSet)) { + t.underlyingType.entries.groupBy({ it.value }, { it.key }).map { (type, platforms) -> + +contentBuilder.contentFor( + t, + ContentKind.Symbol, + setOf(TextStyle.Monospace), + sourceSets = platforms.toSet() + ) { + annotationsBlock(t) + t.visibility[sourceSet]?.takeIf { it !in ignoredVisibilities }?.name?.let { keyword("$it ") } + if (t.expectPresentInSet != null) keyword("actual ") + processExtraModifiers(t) + keyword("typealias ") + group(styles = mainStyles + t.stylesIfDeprecated(sourceSet)) { + signatureForProjection(t.type) + } + operator(" = ") + signatureForTypealiasTarget(t, type) + } + } + } + + private fun signature(t: DTypeParameter) = + t.sourceSets.map { + contentBuilder.contentFor(t, sourceSets = setOf(it)) { + group(styles = mainStyles + t.stylesIfDeprecated(it)) { + signatureForProjection(t.variantTypeParameter.withDri(t.dri.withTargetToDeclaration())) + } + list( + elements = t.nontrivialBounds, + prefix = " : ", + surroundingCharactersStyle = mainStyles + TokenStyle.Operator + ) { bound -> + signatureForProjection(bound) + } + } + } + + private fun PageContentBuilder.DocumentableContentBuilder.signatureForTypealiasTarget( + typeAlias: DTypeAlias, bound: Bound + ) { + signatureForProjection( + p = bound, + showFullyQualifiedName = bound.driOrNull?.classNames == typeAlias.dri.classNames + ) + } + + private fun PageContentBuilder.DocumentableContentBuilder.signatureForProjection( + p: Projection, showFullyQualifiedName: Boolean = false + ) { + return when (p) { + is TypeParameter -> { + if (p.presentableName != null) { + text(p.presentableName!!) + operator(": ") + } + annotationsInline(p) + link(p.name, p.dri) + } + is FunctionalTypeConstructor -> +funType(mainDRI.single(), mainSourcesetData, p) + is GenericTypeConstructor -> + group(styles = emptySet()) { + val linkText = if (showFullyQualifiedName && p.dri.packageName != null) { + "${p.dri.packageName}.${p.dri.classNames.orEmpty()}" + } else p.dri.classNames.orEmpty() + if (p.presentableName != null) { + text(p.presentableName!!) + operator(": ") + } + annotationsInline(p) + link(linkText, p.dri) + list(p.projections, prefix = "<", suffix = ">", + separatorStyles = mainStyles + TokenStyle.Punctuation, + surroundingCharactersStyle = mainStyles + TokenStyle.Operator) { + signatureForProjection(it, showFullyQualifiedName) + } + } + + is Variance<*> -> group(styles = emptySet()) { + keyword("$p ".takeIf { it.isNotBlank() } ?: "") + signatureForProjection(p.inner, showFullyQualifiedName) + } + + is Star -> operator("*") + + is Nullable -> group(styles = emptySet()) { + signatureForProjection(p.inner, showFullyQualifiedName) + operator("?") + } + is DefinitelyNonNullable -> group(styles = emptySet()) { + signatureForProjection(p.inner, showFullyQualifiedName) + operator(" & ") + link("Any", DriOfAny) + } + + is TypeAliased -> signatureForProjection(p.typeAlias) + is JavaObject -> { + annotationsInline(p) + 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: FunctionalTypeConstructor) = + contentBuilder.contentFor(dri, sourceSets, ContentKind.Main) { + + if (type.presentableName != null) { + text(type.presentableName!!) + operator(": ") + } + annotationsInline(type) + if (type.isSuspendable) keyword("suspend ") + + if (type.isExtensionFunction) { + signatureForProjection(type.projections.first()) + punctuation(".") + } + + val args = if (type.isExtensionFunction) + type.projections.drop(1) + else + type.projections + + punctuation("(") + args.subList(0, args.size - 1).forEachIndexed { i, arg -> + signatureForProjection(arg) + if (i < args.size - 2) punctuation(", ") + } + punctuation(")") + operator(" -> ") + signatureForProjection(args.last()) + } +} + +private fun PrimitiveJavaType.translateToKotlin() = GenericTypeConstructor( + dri = dri, + projections = emptyList(), + presentableName = null +) + +private val DTypeParameter.nontrivialBounds: List<Bound> + get() = bounds.filterNot { it is Nullable && it.inner.driOrNull == DriOfAny } diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureUtils.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureUtils.kt new file mode 100644 index 00000000..f16fbeb0 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureUtils.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.signatures + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.transformers.pages.annotations.SinceKotlinTransformer +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.AnnotationTarget +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.ContentKind + +public object KotlinSignatureUtils : JvmSignatureUtils { + + private const val classExtension = "::class" + private val strategy = OnlyOnce + private val listBrackets = Pair('[', ']') + private val ignoredAnnotations = setOf( + /** + * Rendered separately, see [SinceKotlinTransformer] + */ + Annotations.Annotation(DRI("kotlin", "SinceKotlin"), emptyMap()), + + /** + * Rendered separately as its own block, see usage of [ContentKind.Deprecation] + */ + Annotations.Annotation(DRI("kotlin", "Deprecated"), emptyMap()), + Annotations.Annotation(DRI("kotlin", "DeprecatedSinceKotlin"), emptyMap()), + Annotations.Annotation(DRI("java.lang", "Deprecated"), emptyMap()), // could be used as well for interop + ) + + + override fun PageContentBuilder.DocumentableContentBuilder.annotationsBlock(d: AnnotationTarget) { + annotationsBlockWithIgnored(d, ignoredAnnotations, strategy, listBrackets, classExtension) + } + + override fun PageContentBuilder.DocumentableContentBuilder.annotationsInline(d: AnnotationTarget) { + annotationsInlineWithIgnored(d, ignoredAnnotations, strategy, listBrackets, classExtension) + } + + override fun <T : Documentable> WithExtraProperties<T>.modifiers(): SourceSetDependent<Set<ExtraModifiers>> { + return extra[AdditionalModifiers]?.content?.entries?.associate { + it.key to it.value.filterIsInstance<ExtraModifiers.KotlinOnlyModifiers>().toSet() + } ?: emptyMap() + } + + + public val PrimitiveJavaType.dri: DRI get() = DRI("kotlin", name.capitalize()) + + public val Bound.driOrNull: DRI? + get() { + return when (this) { + is TypeParameter -> dri + is TypeConstructor -> dri + is Nullable -> inner.driOrNull + is DefinitelyNonNullable -> inner.driOrNull + is PrimitiveJavaType -> dri + is Void -> DriOfUnit + is JavaObject -> DriOfAny + is Dynamic -> null + is UnresolvedBound -> null + is TypeAliased -> typeAlias.driOrNull + } + } + + public val Projection.drisOfAllNestedBounds: List<DRI> get() = when (this) { + is TypeParameter -> listOf(dri) + is TypeConstructor -> listOf(dri) + projections.flatMap { it.drisOfAllNestedBounds } + is Nullable -> inner.drisOfAllNestedBounds + is DefinitelyNonNullable -> inner.drisOfAllNestedBounds + is PrimitiveJavaType -> listOf(dri) + is Void -> listOf(DriOfUnit) + is JavaObject -> listOf(DriOfAny) + is Dynamic -> emptyList() + is UnresolvedBound -> emptyList() + is Variance<*> -> inner.drisOfAllNestedBounds + is Star -> emptyList() + is TypeAliased -> listOfNotNull(typeAlias.driOrNull, inner.driOrNull) + } + +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/SignatureProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/SignatureProvider.kt new file mode 100644 index 00000000..76245a40 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/SignatureProvider.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.signatures + +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.pages.ContentNode + +public fun interface SignatureProvider { + public fun signature(documentable: Documentable): List<ContentNode> +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToNavigationCommand.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToNavigationCommand.kt new file mode 100644 index 00000000..03bf8e6a --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToNavigationCommand.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +public class AddToNavigationCommand( + public val moduleName: String +) : Command diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToSearch.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToSearch.kt new file mode 100644 index 00000000..8c2ccc79 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToSearch.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +import org.jetbrains.dokka.base.renderers.html.SearchRecord + +public data class AddToSearch( + val moduleName: String, + val elements: List<SearchRecord> +): Command diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToSourcesetDependencies.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToSourcesetDependencies.kt new file mode 100644 index 00000000..c9774e30 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToSourcesetDependencies.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +public data class AddToSourcesetDependencies( + val moduleName: String, + val content: Map<String, List<String>> +) : Command diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/Command.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/Command.kt new file mode 100644 index 00000000..94ed00d4 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/Command.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.CLASS + +@JsonTypeInfo(use = CLASS) +public interface Command + +public abstract class SubstitutionCommand : Command { + public abstract val pattern: String +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ImmediateHtmlCommandConsumer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ImmediateHtmlCommandConsumer.kt new file mode 100644 index 00000000..f1735490 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ImmediateHtmlCommandConsumer.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +import org.jetbrains.dokka.base.renderers.html.TemplateBlock +import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer + +public interface ImmediateHtmlCommandConsumer { + public fun canProcess(command: Command): Boolean + + public fun <R> processCommand(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>) + + public fun <R> processCommandAndFinalize(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>): R +} + diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/InsertTemplateExtra.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/InsertTemplateExtra.kt new file mode 100644 index 00000000..b4316e0f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/InsertTemplateExtra.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +import org.jetbrains.dokka.model.properties.ExtraProperty +import org.jetbrains.dokka.pages.ContentNode + +public data class InsertTemplateExtra(val command: Command) : ExtraProperty<ContentNode> { + + public companion object : ExtraProperty.Key<ContentNode, InsertTemplateExtra> + + override val key: ExtraProperty.Key<ContentNode, *> + get() = Companion +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/PathToRootSubstitutionCommand.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/PathToRootSubstitutionCommand.kt new file mode 100644 index 00000000..070a38ee --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/PathToRootSubstitutionCommand.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +public data class PathToRootSubstitutionCommand( + override val pattern: String, + val default: String +): SubstitutionCommand() diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ProjectNameSubstitutionCommand.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ProjectNameSubstitutionCommand.kt new file mode 100644 index 00000000..6218530e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ProjectNameSubstitutionCommand.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +public data class ProjectNameSubstitutionCommand( + override val pattern: String, + val default: String +): SubstitutionCommand() diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ReplaceVersionsCommand.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ReplaceVersionsCommand.kt new file mode 100644 index 00000000..62a51047 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ReplaceVersionsCommand.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +public data class ReplaceVersionsCommand(val location: String = ""): Command diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ResolveLinkCommand.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ResolveLinkCommand.kt new file mode 100644 index 00000000..1669b435 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ResolveLinkCommand.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +import org.jetbrains.dokka.links.DRI + +public class ResolveLinkCommand( + public val dri: DRI +): Command diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/jsonMapperForPlugins.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/jsonMapperForPlugins.kt new file mode 100644 index 00000000..a679a23d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/jsonMapperForPlugins.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.templating + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer +import com.fasterxml.jackson.databind.type.TypeFactory +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.jetbrains.dokka.base.DokkaBase +import java.io.File + +// TODO [beresnev] try to get rid of this copy-paste in #2933 +// THIS IS COPIED FROM BASE SINCE IT NEEDS TO BE INSTANTIATED ON THE SAME CLASS LOADER AS PLUGINS + +private val objectMapper = run { + val module = SimpleModule().apply { + addSerializer(FileSerializer) + } + jacksonObjectMapper() + .apply { + typeFactory = PluginTypeFactory() + } + .registerModule(module) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) +} + +@PublishedApi +internal class TypeReference<T> @PublishedApi internal constructor( + internal val jackson: com.fasterxml.jackson.core.type.TypeReference<T> +) { + companion object { + @PublishedApi + internal inline operator fun <reified T> invoke(): TypeReference<T> = TypeReference(jacksonTypeRef()) + } +} + +public fun toJsonString(value: Any): String = objectMapper.writeValueAsString(value) + +public inline fun <reified T : Any> parseJson(json: String): T = parseJson(json, TypeReference()) + +@PublishedApi +internal fun <T : Any> parseJson(json: String, typeReference: TypeReference<T>): T = + objectMapper.readValue(json, typeReference.jackson) + + +private object FileSerializer : StdScalarSerializer<File>(File::class.java) { + override fun serialize(value: File, g: JsonGenerator, provider: SerializerProvider) { + g.writeString(value.path) + } +} + +@Suppress("DEPRECATION") // for TypeFactory constructor, no way to use non-deprecated one, it's essentially identical +private class PluginTypeFactory: TypeFactory(null) { + override fun findClass(className: String): Class<out Any>? = + Class.forName(className, true, DokkaBase::class.java.classLoader) ?: super.findClass(className) +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ActualTypealiasAdder.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ActualTypealiasAdder.kt new file mode 100644 index 00000000..dde1a2af --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ActualTypealiasAdder.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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 + +/** + * Since we can not merge [DClasslike] with [DTypeAlias.underlyingType] and [DTypeAlias.extra], + * we have this transformer to add [ActualTypealias] extra in expect [DClasslike] + * + * The transformer should be applied after merging all documentables + */ +// TODO assign actual [DTypeAlias.expectPresentInSet] an expect source set, currently, [DTypeAlias.expectPresentInSet] always = null +public class ActualTypealiasAdder : DocumentableTransformer { + + override fun invoke(original: DModule, context: DokkaContext): DModule { + return original.generateTypealiasesMap().let { aliases -> + original.copy(packages = original.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 : WithSources = + elements.map { element -> + if (element.expectPresentInSet != null) { + typealiases[element.dri]?.let { ta -> + val actualTypealiasExtra = ActualTypealias(ta.copy(expectPresentInSet = element.expectPresentInSet)) + val merged = element.withNewExtras(element.extra + actualTypealiasExtra).let { + when (it) { + is DClass -> it.copy( + documentation = element.documentation + ta.documentation, + sources = element.sources + ta.sources, + sourceSets = element.sourceSets + ta.sourceSets + ) + + is DEnum -> it.copy( + documentation = element.documentation + ta.documentation, + sources = element.sources + ta.sources, + sourceSets = element.sourceSets + ta.sourceSets + ) + + is DInterface -> it.copy( + documentation = element.documentation + ta.documentation, + sources = element.sources + ta.sources, + sourceSets = element.sourceSets + ta.sourceSets + ) + + is DObject -> it.copy( + documentation = element.documentation + ta.documentation, + sources = element.sources + ta.sources, + sourceSets = element.sourceSets + ta.sourceSets + ) + + is DAnnotation -> it.copy( + documentation = element.documentation + ta.documentation, + sources = element.sources + ta.sources, + sourceSets = element.sourceSets + ta.sourceSets + ) + + else -> throw IllegalStateException("${it::class.qualifiedName} ${it.name} cannot have copy its sourceSets") + } + } + @Suppress("UNCHECKED_CAST") + merged as T + } ?: element + } else { + element + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier.kt new file mode 100644 index 00000000..e9c7342e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +@Deprecated( + message = "Declaration was moved to dokka-core", + replaceWith = ReplaceWith("org.jetbrains.dokka.transformers.documentation.ClashingDriIdentifier"), + level = DeprecationLevel.WARNING // TODO change to error after Kotlin 1.9.20 +) +public typealias ClashingDriIdentifier = org.jetbrains.dokka.transformers.documentation.ClashingDriIdentifier diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DefaultDocumentableMerger.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DefaultDocumentableMerger.kt new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DefaultDocumentableMerger.kt diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DeprecatedDocumentableFilterTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DeprecatedDocumentableFilterTransformer.kt new file mode 100644 index 00000000..4905e876 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DeprecatedDocumentableFilterTransformer.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.PackageOptions +import org.jetbrains.dokka.model.Annotations +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.EnumValue +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.perPackageOptions +import org.jetbrains.dokka.transformers.documentation.sourceSet + +/** + * If [PackageOptions.skipDeprecated] or [DokkaConfiguration.DokkaSourceSet.skipDeprecated] is set + * to `true`, suppresses documentables marked with [kotlin.Deprecated] or [java.lang.Deprecated]. + * Package options are given preference over global options. + * + * Documentables with [kotlin.Deprecated.level] set to [DeprecationLevel.HIDDEN] + * are suppressed regardless of global and package options. + */ +public class DeprecatedDocumentableFilterTransformer( + context: DokkaContext +) : SuppressedByConditionDocumentableFilterTransformer(context) { + + override fun shouldBeSuppressed(d: Documentable): Boolean { + val annotations = (d as? WithExtraProperties<*>)?.annotations() ?: return false + if (annotations.isEmpty()) + return false + + val deprecatedAnnotations = filterDeprecatedAnnotations(annotations) + if (deprecatedAnnotations.isEmpty()) + return false + + val kotlinDeprecated = deprecatedAnnotations.find { it.dri.packageName == "kotlin" } + if (kotlinDeprecated?.isHidden() == true) + return true + + return perPackageOptions(d)?.skipDeprecated ?: sourceSet(d).skipDeprecated + } + + private fun WithExtraProperties<*>.annotations(): List<Annotations.Annotation> { + return this.extra.allOfType<Annotations>().flatMap { annotations -> + annotations.directAnnotations.values.singleOrNull() ?: emptyList() + } + } + + private fun filterDeprecatedAnnotations(annotations: List<Annotations.Annotation>): List<Annotations.Annotation> { + return annotations.filter { + (it.dri.packageName == "kotlin" && it.dri.classNames == "Deprecated") || + (it.dri.packageName == "java.lang" && it.dri.classNames == "Deprecated") + } + } + + private fun Annotations.Annotation.isHidden(): Boolean { + val level = (this.params["level"] as? EnumValue) ?: return false + return level.enumName == "DeprecationLevel.HIDDEN" + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DocumentableReplacerTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DocumentableReplacerTransformer.kt new file mode 100644 index 00000000..10b25a20 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DocumentableReplacerTransformer.kt @@ -0,0 +1,239 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer + +public abstract class DocumentableReplacerTransformer( + public val context: DokkaContext +) : PreMergeDocumentableTransformer { + override fun invoke(modules: List<DModule>): List<DModule> = + modules.map { module -> + val (documentable, wasChanged) = processModule(module) + documentable.takeIf { wasChanged } ?: module + } + + protected open fun processModule(module: DModule): AnyWithChanges<DModule> { + val afterProcessing = module.packages.map { processPackage(it) } + val processedModule = module.takeIf { afterProcessing.none { it.changed } } + ?: module.copy(packages = afterProcessing.mapNotNull { it.target }) + return AnyWithChanges(processedModule, afterProcessing.any { it.changed }) + } + + protected open fun processPackage(dPackage: DPackage): AnyWithChanges<DPackage> { + val classlikes = dPackage.classlikes.map { processClassLike(it) } + val typeAliases = dPackage.typealiases.map { processTypeAlias(it) } + val functions = dPackage.functions.map { processFunction(it) } + val properies = dPackage.properties.map { processProperty(it) } + + val wasChanged = (classlikes + typeAliases + functions + properies).any { it.changed } + return (dPackage.takeIf { !wasChanged } ?: dPackage.copy( + classlikes = classlikes.mapNotNull { it.target }, + typealiases = typeAliases.mapNotNull { it.target }, + functions = functions.mapNotNull { it.target }, + properties = properies.mapNotNull { it.target } + )).let { processedPackage -> AnyWithChanges(processedPackage, wasChanged) } + } + + protected open fun processClassLike(classlike: DClasslike): AnyWithChanges<DClasslike> { + val functions = classlike.functions.map { processFunction(it) } + val classlikes = classlike.classlikes.map { processClassLike(it) } + val properties = classlike.properties.map { processProperty(it) } + val companion = (classlike as? WithCompanion)?.companion?.let { processClassLike(it) } + + val wasClasslikeChanged = (functions + classlikes + properties).any { it.changed } || companion?.changed == true + return when (classlike) { + is DClass -> { + val constructors = classlike.constructors.map { processFunction(it) } + val generics = classlike.generics.map { processTypeParameter(it) } + val wasClassChange = + wasClasslikeChanged || constructors.any { it.changed } || generics.any { it.changed } + (classlike.takeIf { !wasClassChange } ?: classlike.copy( + functions = functions.mapNotNull { it.target }, + classlikes = classlikes.mapNotNull { it.target }, + properties = properties.mapNotNull { it.target }, + constructors = constructors.mapNotNull { it.target }, + generics = generics.mapNotNull { it.target }, + companion = companion?.target as? DObject + )).let { AnyWithChanges(it, wasClassChange) } + } + is DInterface -> { + val generics = classlike.generics.map { processTypeParameter(it) } + val wasInterfaceChange = wasClasslikeChanged || generics.any { it.changed } + (classlike.takeIf { !wasInterfaceChange } ?: classlike.copy( + functions = functions.mapNotNull { it.target }, + classlikes = classlikes.mapNotNull { it.target }, + properties = properties.mapNotNull { it.target }, + generics = generics.mapNotNull { it.target }, + companion = companion?.target as? DObject + )).let { AnyWithChanges(it, wasClasslikeChanged) } + } + is DObject -> (classlike.takeIf { !wasClasslikeChanged } ?: classlike.copy( + functions = functions.mapNotNull { it.target }, + classlikes = classlikes.mapNotNull { it.target }, + properties = properties.mapNotNull { it.target }, + )).let { AnyWithChanges(it, wasClasslikeChanged) } + is DAnnotation -> { + val constructors = classlike.constructors.map { processFunction(it) } + val generics = classlike.generics.map { processTypeParameter(it) } + val wasClassChange = + wasClasslikeChanged || constructors.any { it.changed } || generics.any { it.changed } + (classlike.takeIf { !wasClassChange } ?: classlike.copy( + functions = functions.mapNotNull { it.target }, + classlikes = classlikes.mapNotNull { it.target }, + properties = properties.mapNotNull { it.target }, + constructors = constructors.mapNotNull { it.target }, + generics = generics.mapNotNull { it.target }, + companion = companion?.target as? DObject + )).let { AnyWithChanges(it, wasClassChange) } + } + is DEnum -> { + val constructors = classlike.constructors.map { processFunction(it) } + val entries = classlike.entries.map { processEnumEntry(it) } + val wasClassChange = + wasClasslikeChanged || (constructors + entries).any { it.changed } + (classlike.takeIf { !wasClassChange } ?: classlike.copy( + functions = functions.mapNotNull { it.target }, + classlikes = classlikes.mapNotNull { it.target }, + properties = properties.mapNotNull { it.target }, + constructors = constructors.mapNotNull { it.target }, + companion = companion?.target as? DObject, + entries = entries.mapNotNull { it.target } + )).let { AnyWithChanges(it, wasClassChange) } + } + } + } + + protected open fun processEnumEntry(dEnumEntry: DEnumEntry): AnyWithChanges<DEnumEntry> { + val functions = dEnumEntry.functions.map { processFunction(it) } + val properties = dEnumEntry.properties.map { processProperty(it) } + val classlikes = dEnumEntry.classlikes.map { processClassLike(it) } + + val wasChanged = (functions + properties + classlikes).any { it.changed } + return (dEnumEntry.takeIf { !wasChanged } ?: dEnumEntry.copy( + functions = functions.mapNotNull { it.target }, + classlikes = classlikes.mapNotNull { it.target }, + properties = properties.mapNotNull { it.target }, + )).let { AnyWithChanges(it, wasChanged) } + } + + protected open fun processFunction(dFunction: DFunction): AnyWithChanges<DFunction> { + val type = processBound(dFunction.type) + val parameters = dFunction.parameters.map { processParameter(it) } + val receiver = dFunction.receiver?.let { processParameter(it) } + val generics = dFunction.generics.map { processTypeParameter(it) } + + val wasChanged = parameters.any { it.changed } || receiver?.changed == true + || type.changed || generics.any { it.changed } + return (dFunction.takeIf { !wasChanged } ?: dFunction.copy( + type = type.target ?: dFunction.type, + parameters = parameters.mapNotNull { it.target }, + receiver = receiver?.target, + generics = generics.mapNotNull { it.target }, + )).let { AnyWithChanges(it, wasChanged) } + } + + protected open fun processProperty(dProperty: DProperty): AnyWithChanges<DProperty> { + val getter = dProperty.getter?.let { processFunction(it) } + val setter = dProperty.setter?.let { processFunction(it) } + val type = processBound(dProperty.type) + val generics = dProperty.generics.map { processTypeParameter(it) } + + val wasChanged = getter?.changed == true || setter?.changed == true + || type.changed || generics.any { it.changed } + return (dProperty.takeIf { !wasChanged } ?: dProperty.copy( + type = type.target ?: dProperty.type, + setter = setter?.target, + getter = getter?.target, + generics = generics.mapNotNull { it.target } + )).let { AnyWithChanges(it, wasChanged) } + } + + protected open fun processParameter(dParameter: DParameter): AnyWithChanges<DParameter> { + val type = processBound(dParameter.type) + + val wasChanged = type.changed + return (dParameter.takeIf { !wasChanged } ?: dParameter.copy( + type = type.target ?: dParameter.type, + )).let { AnyWithChanges(it, wasChanged) } + } + + protected open fun processTypeParameter(dTypeParameter: DTypeParameter): AnyWithChanges<DTypeParameter> { + val bounds = dTypeParameter.bounds.map { processBound(it) } + + val wasChanged = bounds.any { it.changed } + return (dTypeParameter.takeIf { !wasChanged } ?: dTypeParameter.copy( + bounds = bounds.mapIndexed { i, v -> v.target ?: dTypeParameter.bounds[i] } + )).let { AnyWithChanges(it, wasChanged) } + } + + protected open fun processBound(bound: Bound): AnyWithChanges<Bound> { + return when(bound) { + is GenericTypeConstructor -> processGenericTypeConstructor(bound) + is FunctionalTypeConstructor -> processFunctionalTypeConstructor(bound) + else -> AnyWithChanges(bound, false) + } + } + + protected open fun processVariance(variance: Variance<*>): AnyWithChanges<Variance<*>> { + val bound = processBound(variance.inner) + if (!bound.changed) + return AnyWithChanges(variance, false) + return when (variance) { + is Covariance<*> -> AnyWithChanges( + Covariance(bound.target ?: variance.inner), true) + is Contravariance<*> -> AnyWithChanges( + Contravariance(bound.target ?: variance.inner), true) + is Invariance<*> -> AnyWithChanges( + Invariance(bound.target ?: variance.inner), true) + else -> AnyWithChanges(variance, false) + } + } + + protected open fun processProjection(projection: Projection): AnyWithChanges<Projection> = + when (projection) { + is Bound -> processBound(projection) + is Variance<Bound> -> processVariance(projection) + else -> AnyWithChanges(projection, false) + } + + protected open fun processGenericTypeConstructor( + genericTypeConstructor: GenericTypeConstructor + ): AnyWithChanges<GenericTypeConstructor> { + val projections = genericTypeConstructor.projections.map { processProjection(it) } + + val wasChanged = projections.any { it.changed } + return (genericTypeConstructor.takeIf { !wasChanged } ?: genericTypeConstructor.copy( + projections = projections.mapNotNull { it.target } + )).let { AnyWithChanges(it, wasChanged) } + } + + protected open fun processFunctionalTypeConstructor( + functionalTypeConstructor: FunctionalTypeConstructor + ): AnyWithChanges<FunctionalTypeConstructor> { + val projections = functionalTypeConstructor.projections.map { processProjection(it) } + + val wasChanged = projections.any { it.changed } + return (functionalTypeConstructor.takeIf { !wasChanged } ?: functionalTypeConstructor.copy( + projections = projections.mapNotNull { it.target } + )).let { AnyWithChanges(it, wasChanged) } + } + + protected open fun processTypeAlias(dTypeAlias: DTypeAlias): AnyWithChanges<DTypeAlias> { + val underlyingType = dTypeAlias.underlyingType.mapValues { processBound(it.value) } + val generics = dTypeAlias.generics.map { processTypeParameter(it) } + + val wasChanged = underlyingType.any { it.value.changed } || generics.any { it.changed } + return (dTypeAlias.takeIf { !wasChanged } ?: dTypeAlias.copy( + underlyingType = underlyingType.mapValues { it.value.target ?: dTypeAlias.underlyingType.getValue(it.key) }, + generics = generics.mapNotNull { it.target } + )).let { AnyWithChanges(it, wasChanged) } + } + + + protected data class AnyWithChanges<out T>(val target: T?, val changed: Boolean = false) +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DocumentableVisibilityFilterTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DocumentableVisibilityFilterTransformer.kt new file mode 100644 index 00000000..6155a71f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DocumentableVisibilityFilterTransformer.kt @@ -0,0 +1,388 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.DokkaDefaults +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer + +public class DocumentableVisibilityFilterTransformer( + public val context: DokkaContext +) : PreMergeDocumentableTransformer { + + override fun invoke(modules: List<DModule>): List<DModule> { + return 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 KotlinVisibility.Public -> isAllowedInPackage(packageName, DokkaConfiguration.Visibility.PUBLIC) + is JavaVisibility.Private, + is KotlinVisibility.Private -> isAllowedInPackage(packageName, DokkaConfiguration.Visibility.PRIVATE) + is JavaVisibility.Protected, + is KotlinVisibility.Protected -> isAllowedInPackage(packageName, DokkaConfiguration.Visibility.PROTECTED) + is KotlinVisibility.Internal -> isAllowedInPackage(packageName, DokkaConfiguration.Visibility.INTERNAL) + is JavaVisibility.Default -> isAllowedInPackage(packageName, DokkaConfiguration.Visibility.PACKAGE) + } + + private fun isAllowedInPackage(packageName: String?, visibility: DokkaConfiguration.Visibility): Boolean { + val packageOpts = packageName.takeIf { it != null }?.let { name -> + packageOptions.firstOrNull { Regex(it.matchingRegex).matches(name) } + } + + val (documentedVisibilities, includeNonPublic) = + @Suppress("DEPRECATION") // for includeNonPublic, preserve backwards compatibility + when { + packageOpts != null -> packageOpts.documentedVisibilities to packageOpts.includeNonPublic + else -> globalOptions.documentedVisibilities to globalOptions.includeNonPublic + } + + // if `documentedVisibilities` is explicitly overridden by the user (i.e. not default value by reference), + // deprecated `includeNonPublic` should not be taken into account, so that only one setting prevails + val isDocumentedVisibilitiesOverridden = documentedVisibilities !== DokkaDefaults.documentedVisibilities + return documentedVisibilities.contains(visibility) || (!isDocumentedVisibilitiesOverridden && 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 + } + val typeAliases = filterTypeAliases(it.typealiases).let { (listModified, list) -> + modified = modified || listModified + list + } + when { + !modified -> it + else -> { + packagesListChanged = true + DPackage( + it.dri, + functions, + properties, + classlikes, + typeAliases, + it.documentation, + it.expectPresentInSet, + it.sourceSets, + it.extra + ) + } + } + } + return Pair(packagesListChanged, filteredPackages) + } + + @Suppress("UNUSED_PARAMETER") + private fun <T : WithVisibility> alwaysTrue(a: T, p: DokkaSourceSet) = true + @Suppress("UNUSED_PARAMETER") + private fun <T : WithVisibility> alwaysFalse(a: T, p: DokkaSourceSet) = false + @Suppress("UNUSED_PARAMETER") + private fun <T> alwaysNoModify(a: T, sourceSets: Set<DokkaSourceSet>) = false to a + + 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, + modify: (T, Set<DokkaSourceSet>) -> Pair<Boolean, T> = ::alwaysNoModify, + 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 -> { + val (wasChanged, element) = modify(t, filteredPlatforms) + changed = changed || wasChanged + element + } + 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) { + copy( + documentation = documentation.filtered(filteredPlatforms), + expectPresentInSet = expectPresentInSet.filtered(filteredPlatforms), + sources = sources.filtered(filteredPlatforms), + visibility = visibility.filtered(filteredPlatforms), + generics = generics.mapNotNull { it.filter(filteredPlatforms) }, + sourceSets = filteredPlatforms, + ) + } + } + + 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, + additionalConditionAccessors: (DFunction, DokkaSourceSet) -> Boolean = ::alwaysTrue + ): Pair<Boolean, List<DProperty>> { + + val modifier: (DProperty, Set<DokkaSourceSet>) -> Pair<Boolean, DProperty> = + { original, _ -> + val setter = original.setter?.let { filterFunctions(listOf(it), additionalConditionAccessors) } + val getter = original.getter?.let { filterFunctions(listOf(it), additionalConditionAccessors) } + + val modified = setter?.first == true || getter?.first == true + + val property = + if (modified) + original.copy( + setter = setter?.second?.firstOrNull(), + getter = getter?.second?.firstOrNull() + ) + else original + modified to property + } + + return properties.transform( + additionalCondition, + ::hasVisibleAccessorsForPlatform, + modifier + ) { original, filteredPlatforms -> + val setter = original.setter?.let { filterFunctions(listOf(it), additionalConditionAccessors) } + val getter = original.getter?.let { filterFunctions(listOf(it), additionalConditionAccessors) } + + with(original) { + copy( + documentation = documentation.filtered(filteredPlatforms), + expectPresentInSet = expectPresentInSet.filtered(filteredPlatforms), + sources = sources.filtered(filteredPlatforms), + visibility = visibility.filtered(filteredPlatforms), + sourceSets = filteredPlatforms, + generics = generics.mapNotNull { it.filter(filteredPlatforms) }, + setter = setter?.second?.firstOrNull(), + getter = getter?.second?.firstOrNull() + ) + } + } + } + + private fun filterEnumEntries(entries: List<DEnumEntry>, filteredPlatforms: Set<DokkaSourceSet>): Pair<Boolean, List<DEnumEntry>> = + entries.fold(Pair(false, emptyList())) { acc, entry -> + val intersection = filteredPlatforms.intersect(entry.sourceSets) + if (intersection.isEmpty()) Pair(true, acc.second) + else { + val functions = filterFunctions(entry.functions) { _, data -> data in intersection } + val properties = filterProperties(entry.properties) { _, data -> data in intersection } + val classlikes = filterClasslikes(entry.classlikes) { _, data -> data in intersection } + + DEnumEntry( + entry.dri, + entry.name, + entry.documentation.filtered(intersection), + entry.expectPresentInSet.filtered(filteredPlatforms), + functions.second, + properties.second, + classlikes.second, + intersection, + entry.extra + ).let { Pair(functions.first || properties.first || classlikes.first, acc.second + it) } + } + } + + 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).let { (listModified, list) -> + modified = modified || listModified + list + } else emptyList() + classlikesListChanged = classlikesListChanged || modified + when { + !modified -> this + this is DClass -> copy( + constructors = constructors, + functions = functions, + properties = properties, + classlikes = classlikes, + sources = sources.filtered(filteredPlatforms), + visibility = visibility.filtered(filteredPlatforms), + companion = companion, + generics = generics, + supertypes = supertypes.filtered(filteredPlatforms), + documentation = documentation.filtered(filteredPlatforms), + expectPresentInSet = expectPresentInSet.filtered(filteredPlatforms), + sourceSets = filteredPlatforms + ) + this is DAnnotation -> copy( + documentation = documentation.filtered(filteredPlatforms), + expectPresentInSet = expectPresentInSet.filtered(filteredPlatforms), + sources = sources.filtered(filteredPlatforms), + functions = functions, + properties = properties, + classlikes = classlikes, + visibility = visibility.filtered(filteredPlatforms), + companion = companion, + constructors = constructors, + generics = generics, + sourceSets = filteredPlatforms + ) + this is DEnum -> copy( + entries = enumEntries, + documentation = documentation.filtered(filteredPlatforms), + expectPresentInSet = expectPresentInSet.filtered(filteredPlatforms), + sources = sources.filtered(filteredPlatforms), + functions = functions, + properties = properties, + classlikes = classlikes, + visibility = visibility.filtered(filteredPlatforms), + companion = companion, + constructors = constructors, + supertypes = supertypes.filtered(filteredPlatforms), + sourceSets = filteredPlatforms + ) + this is DInterface -> copy( + documentation = documentation.filtered(filteredPlatforms), + expectPresentInSet = expectPresentInSet.filtered(filteredPlatforms), + sources = sources.filtered(filteredPlatforms), + functions = functions, + properties = properties, + classlikes = classlikes, + visibility = visibility.filtered(filteredPlatforms), + companion = companion, + generics = generics, + supertypes = supertypes.filtered(filteredPlatforms), + sourceSets = filteredPlatforms + ) + this is DObject -> copy( + documentation = documentation.filtered(filteredPlatforms), + expectPresentInSet = expectPresentInSet.filtered(filteredPlatforms), + sources = sources.filtered(filteredPlatforms), + functions = functions, + properties = properties, + classlikes = classlikes, + supertypes = supertypes.filtered(filteredPlatforms), + sourceSets = filteredPlatforms + ) + else -> null + } + } + } + } + return Pair(classlikesListChanged, filteredClasslikes) + } + + private fun filterTypeAliases( + typeAliases: List<DTypeAlias>, + additionalCondition: (DTypeAlias, DokkaSourceSet) -> Boolean = ::alwaysTrue + ) = + typeAliases.transform(additionalCondition) { original, filteredPlatforms -> + with(original) { + copy( + documentation = documentation.filtered(filteredPlatforms), + expectPresentInSet = expectPresentInSet.filtered(filteredPlatforms), + underlyingType = underlyingType.filtered(filteredPlatforms), + visibility = visibility.filtered(filteredPlatforms), + generics = generics.mapNotNull { it.filter(filteredPlatforms) }, + sourceSets = filteredPlatforms, + ) + } + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/EmptyModulesFilterTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/EmptyModulesFilterTransformer.kt new file mode 100644 index 00000000..7a2387dc --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/EmptyModulesFilterTransformer.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer + +public class EmptyModulesFilterTransformer : PreMergeDocumentableTransformer { + override fun invoke(modules: List<DModule>): List<DModule> { + return modules.filter { it.children.isNotEmpty() } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/EmptyPackagesFilterTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/EmptyPackagesFilterTransformer.kt new file mode 100644 index 00000000..30ac8f70 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/EmptyPackagesFilterTransformer.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer +import org.jetbrains.dokka.transformers.documentation.sourceSet + +public class EmptyPackagesFilterTransformer( + public val context: DokkaContext +) : PreMergeDocumentableTransformer { + override fun invoke(modules: List<DModule>): List<DModule> { + return modules.mapNotNull(::filterModule) + } + + private fun filterModule(module: DModule): DModule? { + val nonEmptyPackages = module.packages.filterNot { pkg -> + sourceSet(pkg).skipEmptyPackages && pkg.children.isEmpty() + } + + return when { + nonEmptyPackages == module.packages -> module + nonEmptyPackages.isEmpty() -> null + else -> module.copy(packages = nonEmptyPackages) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ExtensionExtractorTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ExtensionExtractorTransformer.kt new file mode 100644 index 00000000..e6102622 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ExtensionExtractorTransformer.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +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.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer +import org.jetbrains.dokka.utilities.parallelForEach +import org.jetbrains.dokka.utilities.parallelMap +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin + +public class ExtensionExtractorTransformer : DocumentableTransformer { + override fun invoke(original: DModule, context: DokkaContext): DModule = runBlocking(Dispatchers.Default) { + val classGraph = async { + if (!context.configuration.suppressInheritedMembers) + context.plugin<InternalKotlinAnalysisPlugin>().querySingle { fullClassHierarchyBuilder }.build(original) + else + emptyMap() + } + + val channel = Channel<Pair<DRI, Callable>>(10) + launch { + original.packages.parallelForEach { collectExtensions(it, channel) } + channel.close() + } + val extensionMap = channel.toList().toMultiMap() + + val newPackages = original.packages.parallelMap { it.addExtensionInformation(classGraph.await(), extensionMap) } + original.copy(packages = newPackages) + } + + private suspend fun <T : Documentable> T.addExtensionInformation( + classGraph: SourceSetDependent<Map<DRI, List<DRI>>>, + extensionMap: Map<DRI, List<Callable>> + ): T = coroutineScope { + val newClasslikes = (this@addExtensionInformation as? WithScope) + ?.classlikes + ?.map { async { it.addExtensionInformation(classGraph, extensionMap) } } + .orEmpty() + + @Suppress("UNCHECKED_CAST") + when (this@addExtensionInformation) { + is DPackage -> { + val newTypealiases = typealiases.map { async { it.addExtensionInformation(classGraph, extensionMap) } } + copy(classlikes = newClasslikes.awaitAll(), typealiases = newTypealiases.awaitAll()) + } + + is DClass -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DEnum -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DInterface -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DObject -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DAnnotation -> copy( + classlikes = newClasslikes.awaitAll(), + extra = extra + findExtensions(classGraph, extensionMap) + ) + + is DTypeAlias -> copy(extra = extra + findExtensions(classGraph, extensionMap)) + else -> throw IllegalStateException( + "${this@addExtensionInformation::class.simpleName} is not expected to have extensions" + ) + } as T + } + + 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 { it.asPairsWithReceiverDRIs() } + .forEach { channel.send(it) } + } + } + } + + private fun <T : Documentable> T.findExtensions( + classGraph: SourceSetDependent<Map<DRI, List<DRI>>>, + extensionMap: Map<DRI, List<Callable>> + ): CallableExtensions? { + val resultSet = mutableSetOf<Callable>() + + fun collectFrom(element: DRI) { + extensionMap[element]?.let { resultSet.addAll(it) } + sourceSets.forEach { sourceSet -> classGraph[sourceSet]?.get(element)?.forEach { collectFrom(it) } } + } + collectFrom(dri) + + return if (resultSet.isEmpty()) null else CallableExtensions(resultSet) + } + + private fun Callable.asPairsWithReceiverDRIs(): Sequence<Pair<DRI, Callable>> = + receiver?.type?.let { findReceiverDRIs(it) }.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 + // another one `typealias A = 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 DefinitelyNonNullable -> findReceiverDRIs(bound.inner) + is TypeParameter -> + if (this is DFunction && bound.dri == this.dri) + generics.find { it.name == bound.name }?.bounds?.asSequence()?.flatMap { findReceiverDRIs(it) }.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() + is TypeAliased -> findReceiverDRIs(bound.typeAlias) + findReceiverDRIs(bound.inner) + } + + private fun <T, U> Iterable<Pair<T, U>>.toMultiMap(): Map<T, List<U>> = + groupBy(Pair<T, *>::first, Pair<*, U>::second) +} + +public data class CallableExtensions(val extensions: Set<Callable>) : ExtraProperty<Documentable> { + public companion object Key : ExtraProperty.Key<Documentable, CallableExtensions> { + override fun mergeStrategyFor(left: CallableExtensions, right: CallableExtensions): MergeStrategy<Documentable> = + MergeStrategy.Replace(CallableExtensions(left.extensions + right.extensions)) + } + + override val key: Key = Key +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/InheritedEntriesDocumentableFilterTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/InheritedEntriesDocumentableFilterTransformer.kt new file mode 100644 index 00000000..d9b7053a --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/InheritedEntriesDocumentableFilterTransformer.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.InheritedMember +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.plugability.DokkaContext + +public class InheritedEntriesDocumentableFilterTransformer( + context: DokkaContext +) : SuppressedByConditionDocumentableFilterTransformer(context) { + + override fun shouldBeSuppressed(d: Documentable): Boolean { + @Suppress("UNCHECKED_CAST") + val inheritedMember = (d as? WithExtraProperties<Documentable>)?.extra?.get(InheritedMember) + val containsInheritedFrom = inheritedMember?.inheritedFrom?.any { entry -> entry.value != null } ?: false + + return context.configuration.suppressInheritedMembers && containsInheritedFrom + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/InheritorsExtractorTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/InheritorsExtractorTransformer.kt new file mode 100644 index 00000000..2c7d6b89 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/InheritorsExtractorTransformer.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +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.transformers.documentation.DocumentableTransformer + +public 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] + }.associate { (k, v) -> k to v.orEmpty() } + + 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.typeConstructor.dri to dri } } + }.orEmpty() + +} + +public class InheritorsInfo( + public val value: SourceSetDependent<List<DRI>> +) : ExtraProperty<Documentable> { + public 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/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/KotlinArrayDocumentableReplacerTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/KotlinArrayDocumentableReplacerTransformer.kt new file mode 100644 index 00000000..7a360cb8 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/KotlinArrayDocumentableReplacerTransformer.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.plugability.DokkaContext + +public class KotlinArrayDocumentableReplacerTransformer( + context: DokkaContext +): DocumentableReplacerTransformer(context) { + + private fun Documentable.isJVM() = + sourceSets.any{ it.analysisPlatform == Platform.jvm } + + override fun processGenericTypeConstructor(genericTypeConstructor: GenericTypeConstructor): AnyWithChanges<GenericTypeConstructor> = + genericTypeConstructor.takeIf { genericTypeConstructor.dri == DRI("kotlin", "Array") } + ?.let { + with(it.projections.firstOrNull() as? Variance<Bound>) { + with(this?.inner as? GenericTypeConstructor) { + when (this?.dri) { + DRI("kotlin", "Int") -> + AnyWithChanges( + GenericTypeConstructor(DRI("kotlin", "IntArray"), emptyList()), + true) + DRI("kotlin", "Boolean") -> + AnyWithChanges( + GenericTypeConstructor(DRI("kotlin", "BooleanArray"), emptyList()), + true) + DRI("kotlin", "Float") -> + AnyWithChanges( + GenericTypeConstructor(DRI("kotlin", "FloatArray"), emptyList()), + true) + DRI("kotlin", "Double") -> + AnyWithChanges( + GenericTypeConstructor(DRI("kotlin", "DoubleArray"), emptyList()), + true) + DRI("kotlin", "Long") -> + AnyWithChanges( + GenericTypeConstructor(DRI("kotlin", "LongArray"), emptyList()), + true) + DRI("kotlin", "Short") -> + AnyWithChanges( + GenericTypeConstructor(DRI("kotlin", "ShortArray"), emptyList()), + true) + DRI("kotlin", "Char") -> + AnyWithChanges( + GenericTypeConstructor(DRI("kotlin", "CharArray"), emptyList()), + true) + DRI("kotlin", "Byte") -> + AnyWithChanges( + GenericTypeConstructor(DRI("kotlin", "ByteArray"), emptyList()), + true) + else -> null + } + } + } + } + ?: super.processGenericTypeConstructor(genericTypeConstructor) + + override fun processModule(module: DModule): AnyWithChanges<DModule> = + if(module.isJVM()) + super.processModule(module) + else AnyWithChanges(module) +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ModuleAndPackageDocumentationTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ModuleAndPackageDocumentationTransformer.kt new file mode 100644 index 00000000..c19bc15e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ModuleAndPackageDocumentationTransformer.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.internal.ModuleAndPackageDocumentationReader + +internal class ModuleAndPackageDocumentationTransformer( + private val moduleAndPackageDocumentationReader: ModuleAndPackageDocumentationReader +) : PreMergeDocumentableTransformer { + + constructor(context: DokkaContext) : this( + context.plugin<InternalKotlinAnalysisPlugin>().querySingle { moduleAndPackageDocumentationReader } + ) + + override fun invoke(modules: List<DModule>): List<DModule> { + return modules.map { module -> + module.copy( + documentation = module.documentation + moduleAndPackageDocumentationReader.read(module), + packages = module.packages.map { pkg -> + pkg.copy( + documentation = pkg.documentation + moduleAndPackageDocumentationReader.read(pkg) + ) + } + ) + } + } + + private operator fun SourceSetDependent<DocumentationNode>.plus( + other: SourceSetDependent<DocumentationNode> + ): Map<DokkaSourceSet, DocumentationNode> = + (asSequence() + other.asSequence()) + .distinct() + .groupBy({ it.key }, { it.value }) + .mapValues { (_, values) -> DocumentationNode(values.flatMap { it.children }) } + +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ObviousFunctionsDocumentableFilterTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ObviousFunctionsDocumentableFilterTransformer.kt new file mode 100644 index 00000000..09c6ac87 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ObviousFunctionsDocumentableFilterTransformer.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.DFunction +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.ObviousMember +import org.jetbrains.dokka.plugability.DokkaContext + +public class ObviousFunctionsDocumentableFilterTransformer( + context: DokkaContext +) : SuppressedByConditionDocumentableFilterTransformer(context) { + override fun shouldBeSuppressed(d: Documentable): Boolean = + context.configuration.suppressObviousFunctions && d is DFunction && d.extra[ObviousMember] != null +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ReportUndocumentedTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ReportUndocumentedTransformer.kt new file mode 100644 index 00000000..2b270f18 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ReportUndocumentedTransformer.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin + +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 + } + + val syntheticDetector = context.plugin<InternalKotlinAnalysisPlugin>().querySingle { syntheticDocumentableDetector } + if (syntheticDetector.isSynthetic(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> = 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 isPrivateOrInternalApi(documentable: Documentable, sourceSet: DokkaSourceSet): Boolean { + return when ((documentable as? 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 -> Regex(packageOptions.matchingRegex).matches(packageName) } + .maxByOrNull { packageOptions -> packageOptions.matchingRegex.length } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressTagDocumentableFilter.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressTagDocumentableFilter.kt new file mode 100644 index 00000000..1dbf1262 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressTagDocumentableFilter.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.doc.Suppress +import org.jetbrains.dokka.plugability.DokkaContext + +public class SuppressTagDocumentableFilter( + public val dokkaContext: DokkaContext +) : SuppressedByConditionDocumentableFilterTransformer(dokkaContext) { + override fun shouldBeSuppressed(d: Documentable): Boolean = + d.documentation.any { (_, docs) -> docs.dfs { it is Suppress } != null } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressedByConditionDocumentableFilterTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressedByConditionDocumentableFilterTransformer.kt new file mode 100644 index 00000000..4631cece --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressedByConditionDocumentableFilterTransformer.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer + +public abstract class SuppressedByConditionDocumentableFilterTransformer( + public val context: DokkaContext +) : PreMergeDocumentableTransformer { + override fun invoke(modules: List<DModule>): List<DModule> = + modules.map { module -> + val (documentable, wasChanged) = processModule(module) + documentable.takeIf { wasChanged } ?: module + } + + public abstract fun shouldBeSuppressed(d: Documentable): Boolean + + private fun processModule(module: DModule): DocumentableWithChanges<DModule> { + val afterProcessing = module.packages.map { processPackage(it) } + val processedModule = module.takeIf { afterProcessing.none { it.changed } } + ?: module.copy(packages = afterProcessing.mapNotNull { it.documentable }) + return DocumentableWithChanges(processedModule, afterProcessing.any { it.changed }) + } + + private fun processPackage(dPackage: DPackage): DocumentableWithChanges<DPackage> { + if (shouldBeSuppressed(dPackage)) return DocumentableWithChanges.filteredDocumentable() + + val classlikes = dPackage.classlikes.map { processClassLike(it) } + val typeAliases = dPackage.typealiases.map { processMember(it) } + val functions = dPackage.functions.map { processMember(it) } + val properies = dPackage.properties.map { processProperty(it) } + + val wasChanged = (classlikes + typeAliases + functions + properies).any { it.changed } + return (dPackage.takeIf { !wasChanged } ?: dPackage.copy( + classlikes = classlikes.mapNotNull { it.documentable }, + typealiases = typeAliases.mapNotNull { it.documentable }, + functions = functions.mapNotNull { it.documentable }, + properties = properies.mapNotNull { it.documentable } + )).let { processedPackage -> DocumentableWithChanges(processedPackage, wasChanged) } + } + + private fun processClassLike(classlike: DClasslike): DocumentableWithChanges<DClasslike> { + if (shouldBeSuppressed(classlike)) return DocumentableWithChanges.filteredDocumentable() + + val functions = classlike.functions.map { processMember(it) } + val classlikes = classlike.classlikes.map { processClassLike(it) } + val properties = classlike.properties.map { processProperty(it) } + val companion = (classlike as? WithCompanion)?.companion?.let { processClassLike(it) } + + val wasClasslikeChanged = (functions + classlikes + properties).any { it.changed } || companion?.changed == true + return when (classlike) { + is DClass -> { + val constructors = classlike.constructors.map { processMember(it) } + val wasClassChange = + wasClasslikeChanged || constructors.any { it.changed } + (classlike.takeIf { !wasClassChange } ?: classlike.copy( + functions = functions.mapNotNull { it.documentable }, + classlikes = classlikes.mapNotNull { it.documentable }, + properties = properties.mapNotNull { it.documentable }, + constructors = constructors.mapNotNull { it.documentable }, + companion = companion?.documentable as? DObject + )).let { DocumentableWithChanges(it, wasClassChange) } + } + is DInterface -> (classlike.takeIf { !wasClasslikeChanged } ?: classlike.copy( + functions = functions.mapNotNull { it.documentable }, + classlikes = classlikes.mapNotNull { it.documentable }, + properties = properties.mapNotNull { it.documentable }, + companion = companion?.documentable as? DObject + )).let { DocumentableWithChanges(it, wasClasslikeChanged) } + is DObject -> (classlike.takeIf { !wasClasslikeChanged } ?: classlike.copy( + functions = functions.mapNotNull { it.documentable }, + classlikes = classlikes.mapNotNull { it.documentable }, + properties = properties.mapNotNull { it.documentable }, + )).let { DocumentableWithChanges(it, wasClasslikeChanged) } + is DAnnotation -> { + val constructors = classlike.constructors.map { processMember(it) } + val wasClassChange = + wasClasslikeChanged || constructors.any { it.changed } + (classlike.takeIf { !wasClassChange } ?: classlike.copy( + functions = functions.mapNotNull { it.documentable }, + classlikes = classlikes.mapNotNull { it.documentable }, + properties = properties.mapNotNull { it.documentable }, + constructors = constructors.mapNotNull { it.documentable }, + companion = companion?.documentable as? DObject + )).let { DocumentableWithChanges(it, wasClassChange) } + } + is DEnum -> { + val constructors = classlike.constructors.map { processMember(it) } + val entries = classlike.entries.map { processEnumEntry(it) } + val wasClassChange = + wasClasslikeChanged || (constructors + entries).any { it.changed } + (classlike.takeIf { !wasClassChange } ?: classlike.copy( + functions = functions.mapNotNull { it.documentable }, + classlikes = classlikes.mapNotNull { it.documentable }, + properties = properties.mapNotNull { it.documentable }, + constructors = constructors.mapNotNull { it.documentable }, + companion = companion?.documentable as? DObject, + entries = entries.mapNotNull { it.documentable } + )).let { DocumentableWithChanges(it, wasClassChange) } + } + } + } + + private fun processEnumEntry(dEnumEntry: DEnumEntry): DocumentableWithChanges<DEnumEntry> { + if (shouldBeSuppressed(dEnumEntry)) return DocumentableWithChanges.filteredDocumentable() + + val functions = dEnumEntry.functions.map { processMember(it) } + val properties = dEnumEntry.properties.map { processProperty(it) } + val classlikes = dEnumEntry.classlikes.map { processClassLike(it) } + + val wasChanged = (functions + properties + classlikes).any { it.changed } + return (dEnumEntry.takeIf { !wasChanged } ?: dEnumEntry.copy( + functions = functions.mapNotNull { it.documentable }, + classlikes = classlikes.mapNotNull { it.documentable }, + properties = properties.mapNotNull { it.documentable }, + )).let { DocumentableWithChanges(it, wasChanged) } + } + + private fun processProperty(dProperty: DProperty): DocumentableWithChanges<DProperty> { + if (shouldBeSuppressed(dProperty)) return DocumentableWithChanges.filteredDocumentable() + + val getter = dProperty.getter?.let { processMember(it) } ?: DocumentableWithChanges(null, false) + val setter = dProperty.setter?.let { processMember(it) } ?: DocumentableWithChanges(null, false) + + val wasChanged = getter.changed || setter.changed + return (dProperty.takeIf { !wasChanged } ?: dProperty.copy( + getter = getter.documentable, + setter = setter.documentable + )).let { DocumentableWithChanges(it, wasChanged) } + } + + private fun <T : Documentable> processMember(member: T): DocumentableWithChanges<T> = + if (shouldBeSuppressed(member)) DocumentableWithChanges.filteredDocumentable() + else DocumentableWithChanges(member, false) + + private data class DocumentableWithChanges<T : Documentable>(val documentable: T?, val changed: Boolean = false) { + companion object { + fun <T : Documentable> filteredDocumentable(): DocumentableWithChanges<T> = + DocumentableWithChanges(null, true) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressedByConfigurationDocumentableFilterTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressedByConfigurationDocumentableFilterTransformer.kt new file mode 100644 index 00000000..3195f88d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressedByConfigurationDocumentableFilterTransformer.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer +import org.jetbrains.dokka.transformers.documentation.perPackageOptions +import org.jetbrains.dokka.transformers.documentation.source +import org.jetbrains.dokka.transformers.documentation.sourceSet +import java.io.File + +public class SuppressedByConfigurationDocumentableFilterTransformer( + public val context: DokkaContext +) : PreMergeDocumentableTransformer { + override fun invoke(modules: List<DModule>): List<DModule> { + return modules.mapNotNull(::filterModule) + } + + private fun filterModule(module: DModule): DModule? { + val packages = module.packages.mapNotNull { pkg -> filterPackage(pkg) } + return when { + packages == module.packages -> module + packages.isEmpty() -> null + else -> module.copy(packages = packages) + } + } + + private fun filterPackage(pkg: DPackage): DPackage? { + val options = perPackageOptions(pkg) + if (options?.suppress == true) { + return null + } + + val filteredChildren = pkg.children.filterNot(::isSuppressed) + return when { + filteredChildren == pkg.children -> pkg + filteredChildren.isEmpty() -> null + else -> pkg.copy( + functions = filteredChildren.filterIsInstance<DFunction>(), + classlikes = filteredChildren.filterIsInstance<DClasslike>(), + typealiases = filteredChildren.filterIsInstance<DTypeAlias>(), + properties = filteredChildren.filterIsInstance<DProperty>() + ) + } + } + + private fun isSuppressed(documentable: Documentable): Boolean { + if (documentable !is WithSources) return false + val sourceFile = File(source(documentable).path).absoluteFile + return sourceSet(documentable).suppressedFiles.any { suppressedFile -> + sourceFile.startsWith(suppressedFile.absoluteFile) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/utils.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/utils.kt new file mode 100644 index 00000000..60a6396a --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/utils.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.model.Annotations +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.ExceptionInSupertypes +import org.jetbrains.dokka.model.properties.WithExtraProperties + +public val <T : Documentable> WithExtraProperties<T>.isException: Boolean + get() = extra[ExceptionInSupertypes] != null + + +public val <T : Documentable> WithExtraProperties<T>.deprecatedAnnotation: Annotations.Annotation? + get() = extra[Annotations]?.let { annotations -> + annotations.directAnnotations.values.flatten().firstOrNull { + it.isDeprecated() + } + } + +/** + * @return true if [T] has [kotlin.Deprecated] or [java.lang.Deprecated] + * annotation for **any** source set + */ +public fun <T : Documentable> WithExtraProperties<T>.isDeprecated(): Boolean = deprecatedAnnotation != null + +/** + * @return true for [kotlin.Deprecated] and [java.lang.Deprecated] + */ +public fun Annotations.Annotation.isDeprecated(): Boolean { + return (this.dri.packageName == "kotlin" && this.dri.classNames == "Deprecated") || + (this.dri.packageName == "java.lang" && this.dri.classNames == "Deprecated") +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/DefaultSamplesTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/DefaultSamplesTransformer.kt new file mode 100644 index 00000000..1ba049c8 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/DefaultSamplesTransformer.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DisplaySourceSet +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.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.pages.PageTransformer +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.internal.SampleProvider +import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory + +internal const val KOTLIN_PLAYGROUND_SCRIPT = "https://unpkg.com/kotlin-playground@1/dist/playground.min.js" + +internal class DefaultSamplesTransformer(val context: DokkaContext) : PageTransformer { + + private val sampleProviderFactory: SampleProviderFactory = context.plugin<InternalKotlinAnalysisPlugin>().querySingle { sampleProviderFactory } + + override fun invoke(input: RootPageNode): RootPageNode { + return sampleProviderFactory.build().use { sampleProvider -> + input.transformContentPagesTree { page -> + val samples = (page as? WithDocumentables)?.documentables?.flatMap { + it.documentation.entries.flatMap { entry -> + entry.value.children.filterIsInstance<Sample>().map { entry.key to it } + } + } ?: return@transformContentPagesTree page + + val newContent = samples.fold(page.content) { acc, (sampleSourceSet, sample) -> + sampleProvider.getSample(sampleSourceSet, sample.name) + ?.let { + acc.addSample(page, sample.name, it) + } ?: acc + } + + page.modified( + content = newContent, + embeddedResources = page.embeddedResources + KOTLIN_PLAYGROUND_SCRIPT + ) + } + } + } + + + private fun ContentNode.addSample( + contentPage: ContentPage, + fqLink: String, + sample: SampleProvider.SampleSnippet, + ): ContentNode { + val node = contentCode(contentPage.content.sourceSets, contentPage.dri, createSampleBody(sample.imports, sample.body), "kotlin") + return dfs(fqLink, node) + } + + 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 contentCode( + sourceSets: Set<DisplaySourceSet>, + 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/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/annotations/SinceKotlinTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/annotations/SinceKotlinTransformer.kt new file mode 100644 index 00000000..9ff5960d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/annotations/SinceKotlinTransformer.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.annotations + + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.markdown.jb.MARKDOWN_ELEMENT_FILE_NAME +import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.annotations +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.CustomDocTag +import org.jetbrains.dokka.model.doc.CustomTagWrapper +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer +import org.jetbrains.dokka.utilities.associateWithNotNull + +public class SinceKotlinVersion(str: String) : Comparable<SinceKotlinVersion> { + private val parts: List<Int> = str.split(".").map { it.toInt() } + + /** + * Corner case: 1.0 == 1.0.0 + */ + override fun compareTo(other: SinceKotlinVersion): Int { + val i1 = parts.listIterator() + val i2 = other.parts.listIterator() + + while (i1.hasNext() || i2.hasNext()) { + val diff = (if (i1.hasNext()) i1.next() else 0) - (if (i2.hasNext()) i2.next() else 0) + if (diff != 0) return diff + } + + return 0 + } + + override fun toString(): String = parts.joinToString(".") +} + +public class SinceKotlinTransformer( + public val context: DokkaContext +) : DocumentableTransformer { + + private val minSinceKotlinVersionOfPlatform = mapOf( + Platform.common to SinceKotlinVersion("1.0"), + Platform.jvm to SinceKotlinVersion("1.0"), + Platform.js to SinceKotlinVersion("1.1"), + Platform.native to SinceKotlinVersion("1.3"), + Platform.wasm to SinceKotlinVersion("1.8"), + ) + + override fun invoke(original: DModule, context: DokkaContext): DModule = original.transform() as DModule + + private fun <T : Documentable> T.transform(parent: SourceSetDependent<SinceKotlinVersion>? = null): Documentable { + val versions = calculateVersions(parent) + return 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 }, + typealiases = typealiases.map { it.transform() as DTypeAlias } + ) + + is DClass -> copy( + documentation = appendSinceKotlin(versions), + classlikes = classlikes.map { it.transform(versions) as DClasslike }, + functions = functions.map { it.transform(versions) as DFunction }, + properties = properties.map { it.transform(versions) as DProperty } + ) + + is DEnum -> copy( + documentation = appendSinceKotlin(versions), + classlikes = classlikes.map { it.transform(versions) as DClasslike }, + functions = functions.map { it.transform(versions) as DFunction }, + properties = properties.map { it.transform(versions) as DProperty } + ) + + is DInterface -> copy( + documentation = appendSinceKotlin(versions), + classlikes = classlikes.map { it.transform(versions) as DClasslike }, + functions = functions.map { it.transform(versions) as DFunction }, + properties = properties.map { it.transform(versions) as DProperty } + ) + + is DObject -> copy( + documentation = appendSinceKotlin(versions), + classlikes = classlikes.map { it.transform(versions) as DClasslike }, + functions = functions.map { it.transform(versions) as DFunction }, + properties = properties.map { it.transform(versions) as DProperty } + ) + + is DTypeAlias -> copy( + documentation = appendSinceKotlin(versions) + ) + + is DAnnotation -> copy( + documentation = appendSinceKotlin(versions), + classlikes = classlikes.map { it.transform(versions) as DClasslike }, + functions = functions.map { it.transform(versions) as DFunction }, + properties = properties.map { it.transform(versions) as DProperty } + ) + + is DFunction -> copy( + documentation = appendSinceKotlin(versions) + ) + + is DProperty -> copy( + documentation = appendSinceKotlin(versions) + ) + + is DParameter -> copy( + documentation = appendSinceKotlin(versions) + ) + + else -> this.also { context.logger.warn("Unrecognized documentable $this while SinceKotlin transformation") } + } + } + + private fun List<Annotations.Annotation>.findSinceKotlinAnnotation(): Annotations.Annotation? = + this.find { it.dri.packageName == "kotlin" && it.dri.classNames == "SinceKotlin" } + + private fun Documentable.getVersion(sourceSet: DokkaConfiguration.DokkaSourceSet): SinceKotlinVersion { + val annotatedVersion = + annotations()[sourceSet] + ?.findSinceKotlinAnnotation() + ?.params?.let { it["version"] as? StringValue }?.value + ?.let { SinceKotlinVersion(it) } + + val minSinceKotlin = minSinceKotlinVersionOfPlatform[sourceSet.analysisPlatform] + ?: throw IllegalStateException("No value for platform: ${sourceSet.analysisPlatform}") + + return annotatedVersion?.takeIf { version -> version >= minSinceKotlin } ?: minSinceKotlin + } + + + private fun Documentable.calculateVersions(parent: SourceSetDependent<SinceKotlinVersion>?): SourceSetDependent<SinceKotlinVersion> { + return sourceSets.associateWithNotNull { sourceSet -> + val version = getVersion(sourceSet) + val parentVersion = parent?.get(sourceSet) + if (parentVersion != null) + maxOf(version, parentVersion) + else + version + } + } + + private fun Documentable.appendSinceKotlin(versions: SourceSetDependent<SinceKotlinVersion>) = + sourceSets.fold(documentation) { acc, sourceSet -> + + val version = versions[sourceSet] + + val sinceKotlinCustomTag = CustomTagWrapper( + CustomDocTag( + listOf( + Text( + version.toString() + ) + ), + name = MARKDOWN_ELEMENT_FILE_NAME + ), + "Since Kotlin" + ) + if (acc[sourceSet] == null) + acc + (sourceSet to DocumentationNode(listOf(sinceKotlinCustomTag))) + else + acc.mapValues { + if (it.key == sourceSet) it.value.copy( + it.value.children + listOf( + sinceKotlinCustomTag + ) + ) else it.value + } + } + + internal companion object { + internal const val SHOULD_DISPLAY_SINCE_KOTLIN_SYS_PROP = "dokka.shouldDisplaySinceKotlin" + internal fun shouldDisplaySinceKotlin() = + System.getProperty(SHOULD_DISPLAY_SINCE_KOTLIN_SYS_PROP) in listOf("true", "1") + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/comments/CommentsToContentConverter.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/comments/CommentsToContentConverter.kt new file mode 100644 index 00000000..6ca3f8d0 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/comments/CommentsToContentConverter.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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.ContentNode +import org.jetbrains.dokka.pages.DCI +import org.jetbrains.dokka.pages.Style + +public interface CommentsToContentConverter { + public fun buildContent( + docTag: DocTag, + dci: DCI, + sourceSets: Set<DokkaSourceSet>, + styles: Set<Style> = emptySet(), + extras: PropertyContainer<ContentNode> = PropertyContainer.empty() + ): List<ContentNode> +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/comments/DocTagToContentConverter.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/comments/DocTagToContentConverter.kt new file mode 100644 index 00000000..e4e0f53f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/comments/DocTagToContentConverter.kt @@ -0,0 +1,270 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.comments + + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.markdown.jb.MARKDOWN_ELEMENT_FILE_NAME +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.model.properties.plus +import org.jetbrains.dokka.model.toDisplaySourceSets +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.utilities.firstIsInstanceOrNull + +public open class DocTagToContentConverter : CommentsToContentConverter { + override fun buildContent( + docTag: DocTag, + dci: DCI, + sourceSets: Set<DokkaSourceSet>, + styles: Set<Style>, + extras: 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 { extras + it } ?: extras) + } + + fun buildTableRows(rows: List<DocTag>, newStyle: Style): List<ContentGroup> = + rows.flatMap { + @Suppress("UNCHECKED_CAST") + buildContent(it, dci, sourceSets, styles + newStyle, extras) as List<ContentGroup> + } + + fun buildHeader(level: Int) = + listOf( + ContentHeader( + buildChildren(docTag), + level, + dci, + sourceSets.toDisplaySourceSets(), + styles + ) + ) + + fun buildList(ordered: Boolean, newStyles: Set<Style> = emptySet(), start: Int = 1) = + listOf( + ContentList( + buildChildren(docTag), + ordered, + dci, + sourceSets.toDisplaySourceSets(), + styles + newStyles, + ((PropertyContainer.empty<ContentNode>()) + SimpleAttr("start", start.toString())) + ) + ) + + fun buildNewLine() = listOf( + ContentBreakLine( + sourceSets.toDisplaySourceSets() + ) + ) + + fun P.collapseParagraphs(): P = + if (children.size == 1 && children.first() is P) (children.first() as P).collapseParagraphs() else this + + return when (docTag) { + is H1 -> buildHeader(1) + is H2 -> buildHeader(2) + is H3 -> buildHeader(3) + is H4 -> buildHeader(4) + is H5 -> buildHeader(5) + is H6 -> buildHeader(6) + is Ul -> buildList(false) + is Ol -> buildList(true, start = docTag.params["start"]?.toInt() ?: 1) + is Li -> listOf( + ContentGroup(buildChildren(docTag), dci, sourceSets.toDisplaySourceSets(), styles, extras) + ) + is Dl -> buildList(false, newStyles = setOf(ListStyle.DescriptionList)) + is Dt -> listOf( + ContentGroup( + buildChildren(docTag), + dci, + sourceSets.toDisplaySourceSets(), + styles + ListStyle.DescriptionTerm + ) + ) + is Dd -> listOf( + ContentGroup( + buildChildren(docTag), + dci, + sourceSets.toDisplaySourceSets(), + styles + ListStyle.DescriptionDetails + ) + ) + is Br -> buildNewLine() + is B -> buildChildren(docTag, setOf(TextStyle.Strong)) + is I -> buildChildren(docTag, setOf(TextStyle.Italic)) + is P -> listOf( + ContentGroup( + buildChildren(docTag.collapseParagraphs()), + dci, + sourceSets.toDisplaySourceSets(), + styles + setOf(TextStyle.Paragraph), + extras + ) + ) + is A -> listOf( + ContentResolvedLink( + buildChildren(docTag), + docTag.params.getValue("href"), + dci, + sourceSets.toDisplaySourceSets(), + styles + ) + ) + is DocumentationLink -> listOf( + ContentDRILink( + buildChildren(docTag), + docTag.dri, + DCI( + setOf(docTag.dri), + ContentKind.Main + ), + sourceSets.toDisplaySourceSets(), + styles + ) + ) + is BlockQuote -> listOf( + ContentGroup( + buildChildren(docTag), + dci, + sourceSets.toDisplaySourceSets(), + styles + TextStyle.Quotation, + ) + ) + is Pre, is CodeBlock -> listOf( + ContentCodeBlock( + buildChildren(docTag), + docTag.params.getOrDefault("lang", ""), + dci, + sourceSets.toDisplaySourceSets(), + styles + ) + ) + is CodeInline -> listOf( + ContentCodeInline( + buildChildren(docTag), + "", + dci, + sourceSets.toDisplaySourceSets(), + styles + ) + ) + is Img -> listOf( + ContentEmbeddedResource( + address = docTag.params["href"]!!, + altText = docTag.params["alt"], + dci = dci, + sourceSets = sourceSets.toDisplaySourceSets(), + style = styles, + extra = extras + ) + ) + is HorizontalRule -> listOf( + ContentText( + "", + dci, + sourceSets.toDisplaySourceSets(), + setOf() + ) + ) + is Text -> listOf( + ContentText( + docTag.body, + dci, + sourceSets.toDisplaySourceSets(), + styles, + extras + HtmlContent.takeIf { docTag.params["content-type"] == "html" } + ) + ) + is Strikethrough -> buildChildren(docTag, setOf(TextStyle.Strikethrough)) + is Table -> { + //https://html.spec.whatwg.org/multipage/tables.html#the-caption-element + if (docTag.children.any { it is TBody }) { + val head = docTag.children.filterIsInstance<THead>().flatMap { it.children } + val body = docTag.children.filterIsInstance<TBody>().flatMap { it.children } + listOf( + ContentTable( + header = buildTableRows(head.filterIsInstance<Th>(), CommentTable), + caption = docTag.children.firstIsInstanceOrNull<Caption>()?.let { + ContentGroup( + buildContent(it, dci, sourceSets), + dci, + sourceSets.toDisplaySourceSets(), + styles, + extras + ) + }, + buildTableRows(body.filterIsInstance<Tr>(), CommentTable), + dci, + sourceSets.toDisplaySourceSets(), + styles + CommentTable + ) + ) + } else { + listOf( + ContentTable( + header = buildTableRows(docTag.children.filterIsInstance<Th>(), CommentTable), + caption = null, + buildTableRows(docTag.children.filterIsInstance<Tr>(), CommentTable), + dci, + sourceSets.toDisplaySourceSets(), + styles + CommentTable + ) + ) + } + } + is Th, + is Tr -> listOf( + ContentGroup( + docTag.children.map { + ContentGroup(buildChildren(it), dci, sourceSets.toDisplaySourceSets(), styles, extras) + }, + dci, + sourceSets.toDisplaySourceSets(), + styles + ) + ) + is Index -> listOf( + ContentGroup( + buildChildren(docTag, newStyles = styles + ContentStyle.InDocumentationAnchor), + dci, + sourceSets.toDisplaySourceSets(), + styles + ) + ) + is CustomDocTag -> if (docTag.isNonemptyFile()) { + listOf( + ContentGroup( + buildChildren(docTag), + dci, + sourceSets.toDisplaySourceSets(), + styles, + extra = extras + ) + ) + } else { + buildChildren(docTag) + } + is Caption -> listOf( + ContentGroup( + buildChildren(docTag), + dci, + sourceSets.toDisplaySourceSets(), + styles + ContentStyle.Caption, + extra = extras + ) + ) + is Var -> buildChildren(docTag, setOf(TextStyle.Var)) + is U -> buildChildren(docTag, setOf(TextStyle.Underlined)) + + else -> buildChildren(docTag) + } + } + + private fun CustomDocTag.isNonemptyFile() = name == MARKDOWN_ELEMENT_FILE_NAME && children.size > 1 +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/FallbackPageMergerStrategy.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/FallbackPageMergerStrategy.kt new file mode 100644 index 00000000..80886cc5 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/FallbackPageMergerStrategy.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.merger + +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.utilities.DokkaLogger + +public class FallbackPageMergerStrategy( + private val logger: DokkaLogger +) : PageMergerStrategy { + override fun tryMerge(pages: List<PageNode>, path: List<String>): List<PageNode> { + pages.map { + (it as? ContentPage) + } + val renderedPath = path.joinToString(separator = "/") + if (pages.size != 1) logger.warn("For $renderedPath: expected 1 page, but got ${pages.size}") + return listOf(pages.first()) + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/PageMerger.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/PageMerger.kt new file mode 100644 index 00000000..e52c233c --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/PageMerger.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.merger + +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import org.jetbrains.dokka.transformers.pages.PageTransformer + +public class PageMerger(context: DokkaContext) : PageTransformer { + + private val strategies: Iterable<PageMergerStrategy> = context.plugin<DokkaBase>().query { pageMergerStrategy } + + 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}") + } diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/PageMergerStrategy.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/PageMergerStrategy.kt new file mode 100644 index 00000000..ea1b1f03 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/PageMergerStrategy.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.merger + +import org.jetbrains.dokka.pages.PageNode + +public fun interface PageMergerStrategy { + + public fun tryMerge(pages: List<PageNode>, path: List<String>): List<PageNode> + +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt new file mode 100644 index 00000000..864545e6 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.merger + +import org.jetbrains.dokka.base.renderers.sourceSets +import org.jetbrains.dokka.base.transformers.documentables.isDeprecated +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.utilities.DokkaLogger + +/** + * Merges [MemberPage] elements that have the same name. + * That includes **both** properties and functions. + */ +public class SameMethodNamePageMergerStrategy( + public val logger: DokkaLogger +) : PageMergerStrategy { + override fun tryMerge(pages: List<PageNode>, path: List<String>): List<PageNode> { + val members = pages + .filterIsInstance<MemberPageNode>() + .takeIf { it.isNotEmpty() } + ?.sortedBy { it.containsDeprecatedDocumentables() } // non-deprecated first + ?: 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).withSourceSets(members.allSourceSets()), + embeddedResources = members.flatMap { it.embeddedResources }.distinct(), + documentables = members.flatMap { it.documentables } + ) + + return (pages - members) + listOf(merged) + } + + @Suppress("UNCHECKED_CAST") + private fun MemberPageNode.containsDeprecatedDocumentables() = + this.documentables.any { (it as? WithExtraProperties<Documentable>)?.isDeprecated() == true } + + private fun List<MemberPageNode>.allSourceSets(): Set<DisplaySourceSet> = + fold(emptySet()) { acc, e -> acc + e.sourceSets() } + + 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 ?: emptyList()) + ) + ) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/SourceSetMergingPageTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/SourceSetMergingPageTransformer.kt new file mode 100644 index 00000000..8d52a39d --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/SourceSetMergingPageTransformer.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.merger + +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.model.toDisplaySourceSets +import org.jetbrains.dokka.pages.ContentComposite +import org.jetbrains.dokka.pages.ContentNode +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer + +public class SourceSetMergingPageTransformer(context: DokkaContext) : PageTransformer { + + private val mergedSourceSets = context.configuration.sourceSets.toDisplaySourceSets() + .associateBy { sourceSet -> sourceSet.key } + + override fun invoke(input: RootPageNode): RootPageNode { + return input.transformContentPagesTree { contentPage -> + val content: ContentNode = contentPage.content + contentPage.modified(content = transformWithMergedSourceSets(content)) + } + } + + private fun transformWithMergedSourceSets( + contentNode: ContentNode + ): ContentNode { + val mergedSourceSets = contentNode.sourceSets.map { mergedSourceSets.getValue(it.key) }.toSet() + return when (contentNode) { + is ContentComposite -> contentNode + .transformChildren(::transformWithMergedSourceSets) + .withSourceSets(mergedSourceSets) + else -> contentNode.withSourceSets(mergedSourceSets.toSet()) + } + } +} + +private val DisplaySourceSet.key get() = SourceSetMergingKey(name, platform) + +private data class SourceSetMergingKey(private val displayName: String, private val platform: Platform) diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/sourcelinks/SourceLinksTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/sourcelinks/SourceLinksTransformer.kt new file mode 100644 index 00000000..80eeca7e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/sourcelinks/SourceLinksTransformer.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.sourcelinks + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +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.transformers.pages.PageTransformer +import java.io.File + +public class SourceLinksTransformer( + public val context: DokkaContext +) : PageTransformer { + + private val builder : PageContentBuilder = PageContentBuilder( + context.plugin<DokkaBase>().querySingle { commentsToContentConverter }, + context.plugin<DokkaBase>().querySingle { signatureProvider }, + context.logger + ) + + override fun invoke(input: RootPageNode): RootPageNode { + val sourceLinks = getSourceLinksFromConfiguration() + if (sourceLinks.isEmpty()) { + return input + } + return input.transformContentPagesTree { node -> + when (node) { + is WithDocumentables -> { + val sources = node.documentables + .filterIsInstance<WithSources>() + .fold(mutableMapOf<DRI, List<Pair<DokkaSourceSet, String>>>()) { acc, documentable -> + val dri = (documentable as Documentable).dri + acc.compute(dri) { _, v -> + val sources = resolveSources(sourceLinks, documentable) + v?.plus(sources) ?: sources + } + acc + } + if (sources.isNotEmpty()) + node.modified(content = transformContent(node.content, sources)) + else + node + } + else -> node + } + } + } + + private fun getSourceLinksFromConfiguration(): List<SourceLink> { + return context.configuration.sourceSets + .flatMap { it.sourceLinks.map { sl -> SourceLink(sl, it) } } + } + + private fun resolveSources( + sourceLinks: List<SourceLink>, documentable: WithSources + ): List<Pair<DokkaSourceSet, String>> { + return documentable.sources.mapNotNull { (sourceSet, documentableSource) -> + val sourceLink = sourceLinks.find { sourceLink -> + File(documentableSource.path).startsWith(sourceLink.path) && sourceLink.sourceSetData == sourceSet + } ?: return@mapNotNull null + + sourceSet to documentableSource.toLink(sourceLink) + } + } + + private fun DocumentableSource.toLink(sourceLink: SourceLink): String { + val sourcePath = File(this.path).invariantSeparatorsPath + val sourceLinkPath = File(sourceLink.path).invariantSeparatorsPath + + val lineNumber = this.computeLineNumber() + return sourceLink.url + + sourcePath.split(sourceLinkPath)[1] + + sourceLink.lineSuffix + + "${lineNumber ?: 1}" + } + + private fun ContentNode.signatureGroupOrNull() = + (this as? ContentGroup)?.takeIf { it.dci.kind == ContentKind.Symbol } + + private fun transformContent( + contentNode: ContentNode, sources: Map<DRI, List<Pair<DokkaSourceSet, String>>> + ): ContentNode = + contentNode.signatureGroupOrNull()?.let { sg -> + val sgIds = sg.sourceSets.computeSourceSetIds() + sources[sg.dci.dri.singleOrNull()]?.let { sourceLinks -> + sourceLinks + .filter { it.first.sourceSetID in sgIds } + .takeIf { it.isNotEmpty() } + ?.let { filteredSourcesLinks -> + sg.copy(children = sg.children + filteredSourcesLinks.map { + buildContentLink( + sg.dci.dri.first(), + it.first, + it.second + ) + }) + } + } + } ?: when (contentNode) { + is ContentComposite -> contentNode.transformChildren { transformContent(it, sources) } + else -> contentNode + } + + private fun buildContentLink(dri: DRI, sourceSet: DokkaSourceSet, link: String) = builder.contentFor( + dri, + setOf(sourceSet), + ContentKind.Source, + setOf(TextStyle.FloatingRight) + ) { + text("(") + link("source", link) + text(")") + } +} + +public data class SourceLink( + val path: String, + val url: String, + val lineSuffix: String?, + val sourceSetData: DokkaSourceSet +) { + public constructor( + sourceLinkDefinition: DokkaConfiguration.SourceLinkDefinition, + sourceSetData: DokkaSourceSet + ) : this( + sourceLinkDefinition.localDirectory, + sourceLinkDefinition.remoteUrl.toExternalForm(), + sourceLinkDefinition.remoteLineSuffix, + sourceSetData + ) +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/tags/CustomTagContentProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/tags/CustomTagContentProvider.kt new file mode 100644 index 00000000..fcec234f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/tags/CustomTagContentProvider.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.tags + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder +import org.jetbrains.dokka.model.doc.CustomTagWrapper +import org.jetbrains.dokka.model.doc.DocTag + +/** + * Provides an ability to render custom doc tags + * + * Custom tags can be generated during build, for instance via transformers from converting an annotation + * (such as in [org.jetbrains.dokka.base.transformers.pages.annotations.SinceKotlinTransformer]) + * + * Also, custom tags can come from the kdoc itself, where "custom" is defined as unknown to the compiler/spec. + * `@property` and `@throws` are not custom tags - they are defined by the spec and have special meaning + * and separate blocks on the documentation page, it's clear how to render it. Whereas `@usesMathJax` is + * a custom tag - it's application/plugin specific and is not handled by dokka by default. + * + * Using this provider, we can map custom tags (such as `@usesMathJax`) and generate content for it that + * will be displayed on the pages. + */ +public interface CustomTagContentProvider { + + /** + * Whether this content provider supports given [CustomTagWrapper]. + * + * Tags can be filtered out either by name or by nested [DocTag] type + */ + public fun isApplicable(customTag: CustomTagWrapper): Boolean + + /** + * Full blown content description, most likely to be on a separate page + * dedicated to just one element (i.e one class/function), so any + * amount of detail should be fine. + */ + public fun DocumentableContentBuilder.contentForDescription( + sourceSet: DokkaSourceSet, + customTag: CustomTagWrapper + ) {} + + /** + * Brief comment section, usually displayed as a summary/preview. + * + * For instance, when listing all functions of a class on one page, + * it'll be too much to display complete documentation for each function. + * Instead, a small brief is shown for each one (i.e the first paragraph + * or some other important information) - the user can go to the dedicated + * page for more details if they find the brief interesting. + * + * Tag-wise, it would make sense to include `Since Kotlin`, since it's + * important information for the users of stdlib. It would make little + * sense to include `@usesMathjax` here, as this information seems + * to be more specific and detailed than is needed for a brief. + */ + public fun DocumentableContentBuilder.contentForBrief( + sourceSet: DokkaSourceSet, + customTag: CustomTagWrapper + ) {} +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/tags/SinceKotlinTagContentProvider.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/tags/SinceKotlinTagContentProvider.kt new file mode 100644 index 00000000..7c35f719 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/tags/SinceKotlinTagContentProvider.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.transformers.pages.tags + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.translators.documentables.KDOC_TAG_HEADER_LEVEL +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder +import org.jetbrains.dokka.model.doc.CustomTagWrapper +import org.jetbrains.dokka.pages.TextStyle + +public object SinceKotlinTagContentProvider : CustomTagContentProvider { + + private const val SINCE_KOTLIN_TAG_NAME = "Since Kotlin" + + override fun isApplicable(customTag: CustomTagWrapper): Boolean = customTag.name == SINCE_KOTLIN_TAG_NAME + + override fun DocumentableContentBuilder.contentForDescription( + sourceSet: DokkaConfiguration.DokkaSourceSet, + customTag: CustomTagWrapper + ) { + group(sourceSets = setOf(sourceSet), styles = emptySet()) { + header(KDOC_TAG_HEADER_LEVEL, customTag.name) + comment(customTag.root) + } + } + + override fun DocumentableContentBuilder.contentForBrief( + sourceSet: DokkaConfiguration.DokkaSourceSet, + customTag: CustomTagWrapper + ) { + group(sourceSets = setOf(sourceSet), styles = setOf(TextStyle.InlineComment)) { + text(customTag.name + " ", styles = setOf(TextStyle.Bold)) + comment(customTag.root, styles = emptySet()) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultDocumentableToPageTranslator.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultDocumentableToPageTranslator.kt new file mode 100644 index 00000000..0b2597d5 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultDocumentableToPageTranslator.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.DokkaBaseConfiguration +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.pages.ModulePageNode +import org.jetbrains.dokka.plugability.* +import org.jetbrains.dokka.transformers.documentation.DocumentableToPageTranslator +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin + +public class DefaultDocumentableToPageTranslator( + context: DokkaContext +) : DocumentableToPageTranslator { + private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context) + private val commentsToContentConverter = context.plugin<DokkaBase>().querySingle { commentsToContentConverter } + private val signatureProvider = context.plugin<DokkaBase>().querySingle { signatureProvider } + private val customTagContentProviders = context.plugin<DokkaBase>().query { customTagContentProvider } + private val documentableSourceLanguageParser = context.plugin<InternalKotlinAnalysisPlugin>().querySingle { documentableSourceLanguageParser } + private val logger = context.logger + + override fun invoke(module: DModule): ModulePageNode = + DefaultPageCreator( + configuration, + commentsToContentConverter, + signatureProvider, + logger, + customTagContentProviders, + documentableSourceLanguageParser + ).pageForModule(module) +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultPageCreator.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultPageCreator.kt new file mode 100644 index 00000000..5c8ac512 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultPageCreator.kt @@ -0,0 +1,779 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.DokkaBaseConfiguration +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.transformers.documentation.ClashingDriIdentifier +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.base.utils.canonicalAlphabeticalOrder +import org.jetbrains.dokka.links.Callable +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.dokka.analysis.kotlin.internal.DocumentableSourceLanguageParser +import org.jetbrains.dokka.analysis.kotlin.internal.DocumentableLanguage +import kotlin.reflect.KClass + +internal typealias GroupedTags = Map<KClass<out TagWrapper>, List<Pair<DokkaSourceSet?, TagWrapper>>> + +public open class DefaultPageCreator( + configuration: DokkaBaseConfiguration?, + commentsToContentConverter: CommentsToContentConverter, + signatureProvider: SignatureProvider, + public val logger: DokkaLogger, + public val customTagContentProviders: List<CustomTagContentProvider> = emptyList(), + public val documentableAnalyzer: DocumentableSourceLanguageParser +) { + protected open val contentBuilder: PageContentBuilder = PageContentBuilder( + commentsToContentConverter, signatureProvider, logger + ) + + protected val mergeImplicitExpectActualDeclarations: Boolean = + configuration?.mergeImplicitExpectActualDeclarations + ?: DokkaBaseConfiguration.mergeImplicitExpectActualDeclarationsDefault + + protected val separateInheritedMembers: Boolean = + configuration?.separateInheritedMembers ?: DokkaBaseConfiguration.separateInheritedMembersDefault + + public open fun pageForModule(m: DModule): ModulePageNode = + ModulePageNode(m.name.ifEmpty { "<root>" }, contentForModule(m), listOf(m), m.packages.map(::pageForPackage)) + + /** + * We want to generate separated pages for no-actual typealias. + * Actual typealias are displayed on pages for their expect class (trough [ActualTypealias] extra). + * + * @see ActualTypealias + */ + private fun List<Documentable>.filterOutActualTypeAlias(): List<Documentable> { + fun List<Documentable>.hasExpectClass(dri: DRI) = find { it is DClasslike && it.dri == dri && it.expectPresentInSet != null } != null + return this.filterNot { it is DTypeAlias && this.hasExpectClass(it.dri) } + } + + public open fun pageForPackage(p: DPackage): PackagePageNode { + val children = if (mergeImplicitExpectActualDeclarations) { + (p.classlikes + p.typealiases).filterOutActualTypeAlias() + .mergeClashingDocumentable().map(::pageForClasslikes) + + p.functions.mergeClashingDocumentable().map(::pageForFunctions) + + p.properties.mergeClashingDocumentable().map(::pageForProperties) + } else { + (p.classlikes + p.typealiases).filterOutActualTypeAlias() + .renameClashingDocumentable().map(::pageForClasslike) + + p.functions.renameClashingDocumentable().map(::pageForFunction) + + p.properties.mapNotNull(::pageForProperty) + } + return PackagePageNode( + name = p.name, + content = contentForPackage(p), + dri = setOf(p.dri), + documentables = listOf(p), + children = children + ) + } + + public open fun pageForEnumEntry(e: DEnumEntry): ClasslikePageNode = pageForEnumEntries(listOf(e)) + + public open fun pageForClasslike(c: Documentable): ClasslikePageNode = pageForClasslikes(listOf(c)) + + public open fun pageForEnumEntries(documentables: List<DEnumEntry>): ClasslikePageNode { + val dri = documentables.dri.also { + if (it.size != 1) { + logger.error("Documentable dri should have the same one ${it.first()} inside the one page!") + } + } + + val classlikes = documentables.flatMap { it.classlikes } + val functions = documentables.flatMap { it.filteredFunctions } + val props = documentables.flatMap { it.filteredProperties } + + val childrenPages = if (mergeImplicitExpectActualDeclarations) + functions.mergeClashingDocumentable().map(::pageForFunctions) + + props.mergeClashingDocumentable().map(::pageForProperties) + else + classlikes.renameClashingDocumentable().map(::pageForClasslike) + + functions.renameClashingDocumentable().map(::pageForFunction) + + props.renameClashingDocumentable().mapNotNull(::pageForProperty) + + return ClasslikePageNode( + documentables.first().nameAfterClash(), contentForClasslikesAndEntries(documentables), dri, documentables, + childrenPages + ) + } + + /** + * @param documentables a list of [DClasslike] and [DTypeAlias] with the same dri in different sourceSets + */ + public open fun pageForClasslikes(documentables: List<Documentable>): ClasslikePageNode { + val dri = documentables.dri.also { + if (it.size != 1) { + logger.error("Documentable dri should have the same one ${it.first()} inside the one page!") + } + } + + val classlikes = documentables.filterIsInstance<DClasslike>() + + val constructors = + if (classlikes.shouldDocumentConstructors()) { + classlikes.flatMap { (it as? WithConstructors)?.constructors ?: emptyList() } + } else { + emptyList() + } + + val nestedClasslikes = classlikes.flatMap { it.classlikes } + val functions = classlikes.flatMap { it.filteredFunctions } + val props = classlikes.flatMap { it.filteredProperties } + val entries = classlikes.flatMap { if (it is DEnum) it.entries else emptyList() } + + val childrenPages = constructors.map(::pageForFunction) + + if (mergeImplicitExpectActualDeclarations) + nestedClasslikes.mergeClashingDocumentable().map(::pageForClasslikes) + + functions.mergeClashingDocumentable().map(::pageForFunctions) + + props.mergeClashingDocumentable().map(::pageForProperties) + + entries.mergeClashingDocumentable().map(::pageForEnumEntries) + else + nestedClasslikes.renameClashingDocumentable().map(::pageForClasslike) + + functions.renameClashingDocumentable().map(::pageForFunction) + + props.renameClashingDocumentable().mapNotNull(::pageForProperty) + + entries.renameClashingDocumentable().map(::pageForEnumEntry) + + + return ClasslikePageNode( + documentables.first().nameAfterClash(), contentForClasslikesAndEntries(documentables), dri, documentables, + childrenPages + ) + } + + private fun <T> T.toClashedName() where T : Documentable, T : WithExtraProperties<T> = + (extra[ClashingDriIdentifier]?.value?.joinToString(", ", "[", "]") { it.displayName } ?: "") + name.orEmpty() + + private fun <T : Documentable> List<T>.renameClashingDocumentable(): List<T> = + groupBy { it.dri }.values.flatMap { elements -> + if (elements.size == 1) elements else elements.mapNotNull { element -> + element.renameClashingDocumentable() + } + } + + @Suppress("UNCHECKED_CAST") + private fun <T : Documentable> T.renameClashingDocumentable(): T? = when (this) { + is DClass -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DObject -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DAnnotation -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DInterface -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DEnum -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DFunction -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DProperty -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + is DTypeAlias -> copy(extra = this.extra + DriClashAwareName(this.toClashedName())) + else -> null + } as? T? + + private fun <T : Documentable> List<T>.mergeClashingDocumentable(): List<List<T>> = + groupBy { it.dri }.values.toList() + + public open fun pageForFunction(f: DFunction): MemberPageNode = + MemberPageNode(f.nameAfterClash(), contentForFunction(f), setOf(f.dri), listOf(f)) + + public open fun pageForFunctions(fs: List<DFunction>): MemberPageNode { + val dri = fs.dri.also { + if (it.size != 1) { + logger.error("Function dri should have the same one ${it.first()} inside the one page!") + } + } + return MemberPageNode(fs.first().nameAfterClash(), contentForMembers(fs), dri, fs) + } + + public open fun pageForProperty(p: DProperty): MemberPageNode? = + MemberPageNode(p.nameAfterClash(), contentForProperty(p), setOf(p.dri), listOf(p)) + + public open fun pageForProperties(ps: List<DProperty>): MemberPageNode { + val dri = ps.dri.also { + if (it.size != 1) { + logger.error("Property dri should have the same one ${it.first()} inside the one page!") + } + } + return MemberPageNode(ps.first().nameAfterClash(), contentForMembers(ps), dri, ps) + } + + private fun <T> T.isInherited(): Boolean where T : Documentable, T : WithExtraProperties<T> = + sourceSets.all { sourceSet -> extra[InheritedMember]?.isInherited(sourceSet) == true } + + private val WithScope.filteredFunctions: List<DFunction> + get() = functions.filterNot { it.isInherited() } + + private val WithScope.filteredProperties: List<DProperty> + get() = properties.filterNot { it.isInherited() } + + private fun Collection<Documentable>.splitPropsAndFuns(): Pair<List<DProperty>, List<DFunction>> { + val first = ArrayList<DProperty>() + val second = ArrayList<DFunction>() + for (element in this) { + when (element) { + is DProperty -> first.add(element) + is DFunction -> second.add(element) + else -> throw IllegalStateException("Expected only properties or functions") + } + } + return Pair(first, second) + } + + private fun <T> Collection<T>.splitInheritedExtension(dri: Set<DRI>): Pair<List<T>, List<T>> where T : org.jetbrains.dokka.model.Callable = + partition { it.receiver?.dri !in dri } + + private fun <T> Collection<T>.splitInherited(): Pair<List<T>, List<T>> where T : Documentable, T : WithExtraProperties<T> = + partition { it.isInherited() } + + protected open fun contentForModule(m: DModule): ContentGroup { + return 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) + } + } + } + + block( + name = "Packages", + level = 2, + kind = ContentKind.Packages, + elements = m.packages, + sourceSets = m.sourceSets.toSet(), + needsAnchors = true, + headers = listOf( + headers("Name") + ) + ) { + val documentations = it.sourceSets.map { platform -> + it.descriptions[platform]?.also { it.root } + } + val haveSameContent = + documentations.all { it?.root == documentations.firstOrNull()?.root && it?.root != null } + + link(it.name, it.dri) + if (it.sourceSets.size == 1 || (documentations.isNotEmpty() && haveSameContent)) { + documentations.first()?.let { firstParagraphComment(kind = ContentKind.Comment, content = it.root) } + } + } + } + } + + protected open fun contentForPackage(p: DPackage): ContentGroup { + return contentBuilder.contentFor(p) { + group(kind = ContentKind.Cover) { + cover("Package-level declarations") + if (contentForDescription(p).isNotEmpty()) { + sourceSetDependentHint( + dri = p.dri, + sourcesetData = p.sourceSets.toSet(), + kind = ContentKind.SourceSetDependentHint, + styles = setOf(TextStyle.UnderCoverText) + ) { + +contentForDescription(p) + } + } + } + group(styles = setOf(ContentStyle.TabbedContent), extra = mainExtra) { + +contentForScope(p, p.dri, p.sourceSets) + } + } + } + + protected open fun contentForScopes( + scopes: List<WithScope>, + sourceSets: Set<DokkaSourceSet>, + extensions: List<Documentable> = emptyList() + ): ContentGroup { + val types = scopes.flatMap { it.classlikes } + scopes.filterIsInstance<DPackage>().flatMap { it.typealiases } + return contentForScope( + @Suppress("UNCHECKED_CAST") + (scopes as List<Documentable>).dri, + sourceSets, + types, + scopes.flatMap { it.functions }, + scopes.flatMap { it.properties }, + extensions + ) + } + + protected open fun contentForScope( + s: WithScope, + dri: DRI, + sourceSets: Set<DokkaSourceSet>, + extensions: List<Documentable> = emptyList() + ): ContentGroup { + val types = listOf( + s.classlikes, + (s as? DPackage)?.typealiases ?: emptyList() + ).flatten() + return contentForScope(setOf(dri), sourceSets, types, s.functions, s.properties, extensions) + } + + private fun contentForScope( + dri: Set<DRI>, + sourceSets: Set<DokkaSourceSet>, + types: List<Documentable>, + functions: List<DFunction>, + properties: List<DProperty>, + extensions: List<Documentable> + ) = contentBuilder.contentFor(dri, sourceSets) { + divergentBlock( + "Types", + types, + ContentKind.Classlikes + ) + val (extensionProps, extensionFuns) = extensions.splitPropsAndFuns() + if (separateInheritedMembers) { + val (inheritedFunctions, memberFunctions) = functions.splitInherited() + val (inheritedProperties, memberProperties) = properties.splitInherited() + + val (inheritedExtensionFunctions, extensionFunctions) = extensionFuns.splitInheritedExtension(dri) + val (inheritedExtensionProperties, extensionProperties) = extensionProps.splitInheritedExtension(dri) + propertiesBlock( + "Properties", memberProperties + extensionProperties + ) + propertiesBlock( + "Inherited properties", inheritedProperties + inheritedExtensionProperties + ) + functionsBlock("Functions", memberFunctions + extensionFunctions) + functionsBlock( + "Inherited functions", inheritedFunctions + inheritedExtensionFunctions + ) + } else { + propertiesBlock( + "Properties", properties + extensionProps + ) + functionsBlock("Functions", functions + extensionFuns) + } + } + + private fun Iterable<DFunction>.sorted() = + sortedWith(compareBy({ it.name }, { it.parameters.size }, { it.dri.toString() })) + + /** + * @param documentables a list of [DClasslike] and [DEnumEntry] and [DTypeAlias] with the same dri in different sourceSets + */ + protected open fun contentForClasslikesAndEntries(documentables: List<Documentable>): ContentGroup = + contentBuilder.contentFor(documentables.dri, documentables.sourceSets) { + val classlikes = documentables.filterIsInstance<DClasslike>() + + @Suppress("UNCHECKED_CAST") + val extensions = (classlikes as List<WithExtraProperties<DClasslike>>).flatMap { + it.extra[CallableExtensions]?.extensions + ?.filterIsInstance<Documentable>().orEmpty() + } + .distinctBy { it.sourceSets to it.dri } // [Documentable] has expensive equals/hashCode at the moment, see #2620 + + // 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(documentables.first().name.orEmpty()) + sourceSetDependentHint(documentables.dri, documentables.sourceSets) { + documentables.forEach { + +buildSignature(it) + +contentForDescription(it) + } + } + } + val csEnum = classlikes.filterIsInstance<DEnum>() + val csWithConstructor = classlikes.filterIsInstance<WithConstructors>() + val scopes = documentables.filterIsInstance<WithScope>() + val constructorsToDocumented = csWithConstructor.flatMap { it.constructors } + + group( + styles = setOf(ContentStyle.TabbedContent), + sourceSets = mainSourcesetData + extensions.sourceSets, + extra = mainExtra + ) { + if (constructorsToDocumented.isNotEmpty() && documentables.shouldDocumentConstructors()) { + +contentForConstructors(constructorsToDocumented, classlikes.dri, classlikes.sourceSets) + } + if (csEnum.isNotEmpty()) { + +contentForEntries(csEnum.flatMap { it.entries }, csEnum.dri, csEnum.sourceSets) + } + +contentForScopes(scopes, documentables.sourceSets, extensions) + } + } + protected open fun contentForConstructors( + constructorsToDocumented: List<DFunction>, + dri: Set<DRI>, + sourceSets: Set<DokkaSourceSet> + ): ContentGroup { + return contentBuilder.contentFor(dri, sourceSets) { + multiBlock( + name = "Constructors", + level = 2, + kind = ContentKind.Constructors, + groupedElements = constructorsToDocumented.groupBy { it.name } + .map { (_, v) -> v.first().name to v }, + sourceSets = (constructorsToDocumented as List<Documentable>).sourceSets, + needsAnchors = true, + extra = PropertyContainer.empty<ContentNode>() + TabbedContentTypeExtra( + BasicTabbedContentType.CONSTRUCTOR + ), + ) { key, ds -> + link(key, ds.first().dri, kind = ContentKind.Main, styles = setOf(ContentStyle.RowTitle)) + sourceSetDependentHint( + dri = ds.dri, + sourceSets = ds.sourceSets, + kind = ContentKind.SourceSetDependentHint, + styles = emptySet(), + extra = PropertyContainer.empty() + ) { + ds.forEach { + +buildSignature(it) + contentForBrief(it) + } + } + } + } + } + + protected open fun contentForEntries( + entries: List<DEnumEntry>, + dri: Set<DRI>, + sourceSets: Set<DokkaSourceSet> + ): ContentGroup { + return contentBuilder.contentFor(dri, sourceSets) { + multiBlock( + name = "Entries", + level = 2, + kind = ContentKind.Classlikes, + groupedElements = entries.groupBy { it.name }.toList(), + sourceSets = entries.sourceSets, + needsSorting = false, + needsAnchors = true, + extra = mainExtra + TabbedContentTypeExtra(BasicTabbedContentType.ENTRY), + styles = emptySet() + ) { key, ds -> + link(key, ds.first().dri) + sourceSetDependentHint( + dri = ds.dri, + sourceSets = ds.sourceSets, + kind = ContentKind.SourceSetDependentHint, + extra = PropertyContainer.empty<ContentNode>() + ) { + ds.forEach { + +buildSignature(it) + contentForBrief(it) + } + } + } + } + } + + + + protected open fun contentForDescription( + d: Documentable + ): List<ContentNode> { + val sourceSets = d.sourceSets.toSet() + val tags = d.groupedTags + + return contentBuilder.contentFor(d) { + deprecatedSectionContent(d, sourceSets) + + descriptionSectionContent(d, sourceSets) + customTagSectionContent(d, sourceSets, customTagContentProviders) + unnamedTagSectionContent(d, sourceSets) { toHeaderString() } + + paramsSectionContent(tags) + seeAlsoSectionContent(tags) + throwsSectionContent(tags) + samplesSectionContent(tags) + + inheritorsSectionContent(d, logger) + }.children + } + + protected open fun DocumentableContentBuilder.contentForBrief( + documentable: Documentable + ) { + documentable.sourceSets.forEach { sourceSet -> + documentable.documentation[sourceSet]?.let { + /* + Get description or a tag that holds documentation. + This tag can be either property or constructor but constructor tags are handled already in analysis so we + only need to keep an eye on property + + We purposefully ignore all other tags as they should not be visible in brief + */ + it.firstMemberOfTypeOrNull<Description>() ?: it.firstMemberOfTypeOrNull<Property>() + .takeIf { documentable is DProperty } + }?.let { + group(sourceSets = setOf(sourceSet), kind = ContentKind.BriefComment) { + createBriefComment(documentable, sourceSet, it) + } + } + } + } + + private fun DocumentableContentBuilder.createBriefComment( + documentable: Documentable, + sourceSet: DokkaSourceSet, + tag: TagWrapper + ) { + val language = documentableAnalyzer.getLanguage(documentable, sourceSet) + when(language) { + DocumentableLanguage.JAVA -> firstSentenceComment(tag.root) + DocumentableLanguage.KOTLIN -> firstParagraphComment(tag.root) + else -> firstParagraphComment(tag.root) + } + } + + protected open fun contentForFunction(f: DFunction): ContentGroup = contentForMember(f) + + protected open fun contentForProperty(p: DProperty): ContentGroup = contentForMember(p) + + protected open fun contentForMember(d: Documentable): ContentGroup = contentForMembers(listOf(d)) + + protected open fun contentForMembers(doumentables: List<Documentable>): ContentGroup = + contentBuilder.contentFor(doumentables.dri, doumentables.sourceSets) { + group(kind = ContentKind.Cover) { + cover(doumentables.first().name.orEmpty()) + } + divergentGroup(ContentDivergentGroup.GroupID("member")) { + doumentables.forEach { d -> + instance(setOf(d.dri), d.sourceSets) { + divergent { + +buildSignature(d) + } + after { + +contentForDescription(d) + } + } + } + } + } + + private fun DocumentableContentBuilder.functionsBlock( + name: String, + list: Collection<DFunction> + ) { + divergentBlock( + name, + list.sorted(), + ContentKind.Functions, + extra = mainExtra + ) + } + + private fun DocumentableContentBuilder.propertiesBlock( + name: String, + list: Collection<DProperty> + ) { + divergentBlock( + name, + list, + ContentKind.Properties, + extra = mainExtra + ) + + } + private data class NameAndIsExtension(val name:String?, val isExtension: Boolean) + + private fun groupAndSortDivergentCollection(collection: Collection<Documentable>): List<Map.Entry<NameAndIsExtension, List<Documentable>>> { + val groupKeyComparator: Comparator<Map.Entry<NameAndIsExtension, List<Documentable>>> = + compareBy<Map.Entry<NameAndIsExtension, List<Documentable>>, String?>( + nullsFirst(canonicalAlphabeticalOrder) + ) { it.key.name } + .thenBy { it.key.isExtension } + + return collection + .groupBy { + NameAndIsExtension( + it.name, + it.isExtension() + ) + } // This groupBy should probably use LocationProvider + // 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 } + .entries.sortedWith(groupKeyComparator) + } + + protected open fun DocumentableContentBuilder.divergentBlock( + name: String, + collection: Collection<Documentable>, + kind: ContentKind, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + if (collection.any()) { + val onlyExtensions = collection.all { it.isExtension() } + val groupExtra = when(kind) { + ContentKind.Functions -> extra + TabbedContentTypeExtra(if (onlyExtensions) BasicTabbedContentType.EXTENSION_FUNCTION else BasicTabbedContentType.FUNCTION) + ContentKind.Properties -> extra + TabbedContentTypeExtra(if (onlyExtensions) BasicTabbedContentType.EXTENSION_PROPERTY else BasicTabbedContentType.PROPERTY) + ContentKind.Classlikes -> extra + TabbedContentTypeExtra(BasicTabbedContentType.TYPE) + else -> extra + } + + group(extra = groupExtra) { + // be careful: groupExtra will be applied for children by default + header(2, name, kind = kind, extra = extra) { } + val isFunctions = collection.any { it is DFunction } + table(kind, extra = extra, styles = emptySet()) { + header { + group { text("Name") } + group { text("Summary") } + } + groupAndSortDivergentCollection(collection) + .forEach { (elementNameAndIsExtension, elements) -> // This groupBy should probably use LocationProvider + val elementName = elementNameAndIsExtension.name + val isExtension = elementNameAndIsExtension.isExtension + val rowExtra = + if (isExtension) extra + TabbedContentTypeExtra(if(isFunctions) BasicTabbedContentType.EXTENSION_FUNCTION else BasicTabbedContentType.EXTENSION_PROPERTY) else extra + val rowKind = if (isExtension) ContentKind.Extensions else kind + val sortedElements = sortDivergentElementsDeterministically(elements) + row( + dri = sortedElements.map { it.dri }.toSet(), + sourceSets = sortedElements.flatMap { it.sourceSets }.toSet(), + kind = rowKind, + styles = emptySet(), + extra = elementName?.let { name -> rowExtra + SymbolAnchorHint(name, kind) } ?: rowExtra + ) { + link( + text = elementName.orEmpty(), + address = sortedElements.first().dri, + kind = rowKind, + styles = setOf(ContentStyle.RowTitle), + sourceSets = sortedElements.sourceSets.toSet(), + extra = extra + ) + divergentGroup( + ContentDivergentGroup.GroupID(name), + sortedElements.map { it.dri }.toSet(), + kind = rowKind, + extra = extra + ) { + sortedElements.map { element -> + instance( + setOf(element.dri), + element.sourceSets.toSet(), + extra = PropertyContainer.withAll( + SymbolAnchorHint(element.name ?: "", rowKind) + ) + ) { + divergent(extra = PropertyContainer.empty()) { + group { + +buildSignature(element) + } + } + after( + extra = PropertyContainer.empty() + ) { + contentForBrief(element) + contentForCustomTagsBrief(element) + } + } + } + } + } + } + } + } + } + } + + /** + * Divergent elements, such as extensions for the same receiver, can have identical signatures + * if they are declared in different places. If such elements are shown on the same page together, + * they need to be rendered deterministically to have reproducible builds. + * + * For example, you can have three identical extensions, if they are declared as: + * 1) top-level in package A + * 2) top-level in package B + * 3) inside a companion object in package A/B + * + * @see divergentBlock + * + * @param elements can contain types (annotation/class/interface/object/typealias), functions and properties + * @return the original list if it has one or zero elements + */ + private fun sortDivergentElementsDeterministically(elements: List<Documentable>): List<Documentable> = + elements.takeIf { it.size > 1 } // the majority are single-element lists, but no real benchmarks done + ?.sortedWith(divergentDocumentableComparator) + ?: elements + + private fun DocumentableContentBuilder.contentForCustomTagsBrief(documentable: Documentable) { + val customTags = documentable.customTags + if (customTags.isEmpty()) return + + documentable.sourceSets.forEach { sourceSet -> + customTags.forEach { (_, sourceSetTag) -> + sourceSetTag[sourceSet]?.let { tag -> + customTagContentProviders.filter { it.isApplicable(tag) }.forEach { provider -> + with(provider) { + contentForBrief(sourceSet, tag) + } + } + } + } + } + } + + protected open fun TagWrapper.toHeaderString(): String = this.javaClass.toGenericString().split('.').last() +} + +internal val List<Documentable>.sourceSets: Set<DokkaSourceSet> + get() = flatMap { it.sourceSets }.toSet() + +internal val List<Documentable>.dri: Set<DRI> + get() = map { it.dri }.toSet() + +internal val Documentable.groupedTags: GroupedTags + get() = documentation.flatMap { (pd, doc) -> + doc.children.map { pd to it }.toList() + }.groupBy { it.second::class } + +internal val Documentable.descriptions: SourceSetDependent<Description> + get() = groupedTags.withTypeUnnamed() + +internal val Documentable.customTags: Map<String, SourceSetDependent<CustomTagWrapper>> + get() = groupedTags.withTypeNamed() + +/** + * @see DefaultPageCreator.sortDivergentElementsDeterministically for usage + */ +private val divergentDocumentableComparator = + compareBy<Documentable, String?>(nullsLast()) { it.dri.packageName } + .thenBy(nullsFirst()) { it.dri.classNames } // nullsFirst for top level to be first + .thenBy( + nullsLast( + compareBy<Callable> { it.params.size } + .thenBy { it.signature() } + ) + ) { it.dri.callable } + +@Suppress("UNCHECKED_CAST") +private fun <T : Documentable> T.nameAfterClash(): String = + ((this as? WithExtraProperties<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() + +// Annotations might have constructors to substitute reflection invocations +// and for internal/compiler purposes, but they are not expected to be documented +// and instantiated directly under normal circumstances, so constructors should not be rendered. +internal fun List<Documentable>.shouldDocumentConstructors() = !this.any { it is DAnnotation } diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DeprecationSectionCreator.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DeprecationSectionCreator.kt new file mode 100644 index 00000000..0f51578f --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DeprecationSectionCreator.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.annotations +import org.jetbrains.dokka.base.transformers.documentables.isDeprecated +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.pages.ContentKind +import org.jetbrains.dokka.pages.ContentStyle +import org.jetbrains.dokka.pages.TextStyle + +/** + * Main header for [Deprecated] section + */ +private const val DEPRECATED_HEADER_LEVEL = 3 + +/** + * Header for a direct parameter of [Deprecated] annotation, + * such as [Deprecated.message] and [Deprecated.replaceWith] + */ +private const val DIRECT_PARAM_HEADER_LEVEL = 4 + +internal fun PageContentBuilder.DocumentableContentBuilder.deprecatedSectionContent( + documentable: Documentable, + platforms: Set<DokkaConfiguration.DokkaSourceSet> +) { + val allAnnotations = documentable.annotations() + if (allAnnotations.isEmpty()) { + return + } + + platforms.forEach { platform -> + val platformAnnotations = allAnnotations[platform] ?: emptyList() + val deprecatedPlatformAnnotations = platformAnnotations.filter { it.isDeprecated() } + + if (deprecatedPlatformAnnotations.isNotEmpty()) { + group(kind = ContentKind.Deprecation, sourceSets = setOf(platform), styles = emptySet()) { + val kotlinAnnotation = deprecatedPlatformAnnotations.find { it.dri.packageName == "kotlin" } + val javaAnnotation = deprecatedPlatformAnnotations.find { it.dri.packageName == "java.lang" } + + // If both annotations are present, priority is given to Kotlin's annotation since it + // contains more useful information, and Java's annotation is probably there + // for interop with Java callers, so it should be OK to ignore it + if (kotlinAnnotation != null) { + createKotlinDeprecatedSectionContent(kotlinAnnotation, platformAnnotations) + } else if (javaAnnotation != null) { + createJavaDeprecatedSectionContent(javaAnnotation) + } + } + } + } +} + +/** + * @see [DeprecatedSinceKotlin] + */ +private fun findDeprecatedSinceKotlinAnnotation(annotations: List<Annotations.Annotation>): Annotations.Annotation? { + return annotations.firstOrNull { + it.dri.packageName == "kotlin" && it.dri.classNames == "DeprecatedSinceKotlin" + } +} + +/** + * Section with details for Kotlin's [kotlin.Deprecated] annotation + */ +private fun DocumentableContentBuilder.createKotlinDeprecatedSectionContent( + deprecatedAnnotation: Annotations.Annotation, + allAnnotations: List<Annotations.Annotation> +) { + val deprecatedSinceKotlinAnnotation = findDeprecatedSinceKotlinAnnotation(allAnnotations) + header( + level = DEPRECATED_HEADER_LEVEL, + text = createKotlinDeprecatedHeaderText(deprecatedAnnotation, deprecatedSinceKotlinAnnotation) + ) + + deprecatedSinceKotlinAnnotation?.let { + createDeprecatedSinceKotlinFootnoteContent(it) + } + + deprecatedAnnotation.takeStringParam("message")?.let { + group(styles = setOf(TextStyle.Paragraph)) { + text(it) + } + } + + createReplaceWithSectionContent(deprecatedAnnotation) +} + +private fun createKotlinDeprecatedHeaderText( + kotlinDeprecatedAnnotation: Annotations.Annotation, + deprecatedSinceKotlinAnnotation: Annotations.Annotation? +): String { + if (deprecatedSinceKotlinAnnotation != null) { + // In this case there's no single level, it's dynamic based on api version, + // so there should be a footnote with levels and their respective versions + return "Deprecated" + } + + val deprecationLevel = kotlinDeprecatedAnnotation.params["level"]?.let { (it as? EnumValue)?.enumName } + return when (deprecationLevel) { + "DeprecationLevel.ERROR" -> "Deprecated (with error)" + "DeprecationLevel.HIDDEN" -> "Deprecated (hidden)" + else -> "Deprecated" + } +} + +/** + * Footnote for [DeprecatedSinceKotlin] annotation used in stdlib + * + * Notice that values are empty by default, so it's not guaranteed that all three will be set + */ +private fun DocumentableContentBuilder.createDeprecatedSinceKotlinFootnoteContent( + deprecatedSinceKotlinAnnotation: Annotations.Annotation +) { + group(styles = setOf(ContentStyle.Footnote)) { + deprecatedSinceKotlinAnnotation.takeStringParam("warningSince")?.let { + group(styles = setOf(TextStyle.Paragraph)) { + text("Warning since $it") + } + } + deprecatedSinceKotlinAnnotation.takeStringParam("errorSince")?.let { + group(styles = setOf(TextStyle.Paragraph)) { + text("Error since $it") + } + } + deprecatedSinceKotlinAnnotation.takeStringParam("hiddenSince")?.let { + group(styles = setOf(TextStyle.Paragraph)) { + text("Hidden since $it") + } + } + } +} + +/** + * Section for [ReplaceWith] parameter of [kotlin.Deprecated] annotation + */ +private fun DocumentableContentBuilder.createReplaceWithSectionContent(kotlinDeprecatedAnnotation: Annotations.Annotation) { + val replaceWithAnnotation = (kotlinDeprecatedAnnotation.params["replaceWith"] as? AnnotationValue)?.annotation + ?: return + + header( + level = DIRECT_PARAM_HEADER_LEVEL, + text = "Replace with" + ) + + // Signature: vararg val imports: String + val imports = (replaceWithAnnotation.params["imports"] as? ArrayValue) + ?.value + ?.mapNotNull { (it as? StringValue)?.value } + ?: emptyList() + + if (imports.isNotEmpty()) { + codeBlock(language = "kotlin", styles = setOf(TextStyle.Monospace)) { + imports.forEach { + text("import $it") + breakLine() + } + } + } + + replaceWithAnnotation.takeStringParam("expression")?.removeSurrounding("`")?.let { + codeBlock(language = "kotlin", styles = setOf(TextStyle.Monospace)) { + text(it) + } + } +} + +/** + * Section with details for Java's [java.lang.Deprecated] annotation + */ +private fun DocumentableContentBuilder.createJavaDeprecatedSectionContent( + deprecatedAnnotation: Annotations.Annotation, +) { + val isForRemoval = deprecatedAnnotation.takeBooleanParam("forRemoval", default = false) + header( + level = DEPRECATED_HEADER_LEVEL, + text = if (isForRemoval) "Deprecated (for removal)" else "Deprecated" + ) + deprecatedAnnotation.takeStringParam("since")?.let { + group(styles = setOf(ContentStyle.Footnote)) { + text("Since version $it") + } + } +} + +private fun Annotations.Annotation.takeBooleanParam(name: String, default: Boolean): Boolean = + (this.params[name] as? BooleanValue)?.value ?: default + +private fun Annotations.Annotation.takeStringParam(name: String): String? = + (this.params[name] as? StringValue)?.takeIf { it.value.isNotEmpty() }?.value diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DescriptionSections.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DescriptionSections.kt new file mode 100644 index 00000000..e2489260 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DescriptionSections.kt @@ -0,0 +1,349 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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.TextStyle +import org.jetbrains.dokka.utilities.DokkaLogger +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), + ) { + 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(): SourceSetDependent<List<DRI>> { + @Suppress("UNCHECKED_CAST") + val withExtra = this as? WithExtraProperties<Documentable> + + return withExtra + ?.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, + ) { + body() + } +} + +private fun DRI.friendlyClassName() = classNames?.substringAfterLast(".") + +private fun <T> Map<String, SourceSetDependent<T>>.availableSourceSets() = values.flatMap { it.keys }.toSet() diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DriClashAwareName.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DriClashAwareName.kt new file mode 100644 index 00000000..362bb9b9 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DriClashAwareName.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.properties.ExtraProperty + +public data class DriClashAwareName(val value: String?): ExtraProperty<Documentable> { + public companion object : ExtraProperty.Key<Documentable, DriClashAwareName> + override val key: ExtraProperty.Key<Documentable, *> = Companion +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/PageContentBuilder.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/PageContentBuilder.kt new file mode 100644 index 00000000..4ddda674 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/PageContentBuilder.kt @@ -0,0 +1,781 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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.model.properties.plus +import org.jetbrains.dokka.model.toDisplaySourceSets +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.utilities.DokkaLogger + +@DslMarker +public annotation class ContentBuilderMarker + +public open class PageContentBuilder( + public val commentsConverter: CommentsToContentConverter, + public val signatureProvider: SignatureProvider, + public val logger: DokkaLogger +) { + public 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) + + public 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) + + public 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 + public open inner class DocumentableContentBuilder( + public val mainDRI: Set<DRI>, + public val mainSourcesetData: Set<DokkaSourceSet>, + public val mainStyles: Set<Style>, + public val mainExtra: PropertyContainer<ContentNode> + ) { + protected val contents: MutableList<ContentNode> = mutableListOf<ContentNode>() + + public fun build( + sourceSets: Set<DokkaSourceSet>, + kind: Kind, + styles: Set<Style>, + extra: PropertyContainer<ContentNode> + ): ContentGroup { + return ContentGroup( + children = contents.toList(), + dci = DCI(mainDRI, kind), + sourceSets = sourceSets.toDisplaySourceSets(), + style = styles, + extra = extra + ) + } + + public operator fun ContentNode.unaryPlus() { + contents += this + } + + public operator fun Collection<ContentNode>.unaryPlus() { + contents += this + } + + public 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 + SymbolAnchorHint(text.replace("\\s".toRegex(), "").toLowerCase(), kind) + ) { + text(text, kind = kind) + block() + } + ) + } + + public 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) + } + + public fun constant(text: String) { + text(text, styles = mainStyles + TokenStyle.Constant) + } + + public fun keyword(text: String) { + text(text, styles = mainStyles + TokenStyle.Keyword) + } + + public fun stringLiteral(text: String) { + text(text, styles = mainStyles + TokenStyle.String) + } + + public fun booleanLiteral(value: Boolean) { + text(value.toString(), styles = mainStyles + TokenStyle.Boolean) + } + + public fun punctuation(text: String) { + text(text, styles = mainStyles + TokenStyle.Punctuation) + } + + public fun operator(text: String) { + text(text, styles = mainStyles + TokenStyle.Operator) + } + + public 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) + } + + public fun breakLine(sourceSets: Set<DokkaSourceSet> = mainSourcesetData) { + contents += ContentBreakLine(sourceSets.toDisplaySourceSets()) + } + + public fun buildSignature(d: Documentable): List<ContentNode> = signatureProvider.signature(d) + + public fun table( + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + operation: TableBuilder.() -> Unit = {} + ) { + contents += TableBuilder(mainDRI, sourceSets, kind, styles, extra).apply { + operation() + }.build() + } + + public fun unorderedList( + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + operation: ListBuilder.() -> Unit = {} + ) { + contents += ListBuilder(false, mainDRI, sourceSets, kind, styles, extra).apply(operation).build() + } + + public fun orderedList( + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + operation: ListBuilder.() -> Unit = {} + ) { + contents += ListBuilder(true, mainDRI, sourceSets, kind, styles, extra).apply(operation).build() + } + + public fun descriptionList( + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + operation: ListBuilder.() -> Unit = {} + ) { + contents += ListBuilder(false, mainDRI, sourceSets, kind, styles + ListStyle.DescriptionList, extra) + .apply(operation) + .build() + } + + internal fun headers(vararg label: String) = contentFor(mainDRI, mainSourcesetData) { + label.forEach { text(it) } + } + + public 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> = emptyList(), + needsAnchors: Boolean = false, + operation: DocumentableContentBuilder.(T) -> Unit + ) { + if (renderWhenEmpty || elements.any()) { + header(level, name, kind = kind) { } + contents += ContentTable( + header = headers, + children = elements + .let { + if (needsSorting) + it.sortedWith(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)) { it.name }) + else it + } + .map { + val newExtra = if (needsAnchors) extra + SymbolAnchorHint.from(it, kind) else extra + buildGroup(setOf(it.dri), it.sourceSets.toSet(), kind, styles, newExtra) { + operation(it) + } + }, + dci = DCI(mainDRI, kind), + sourceSets = sourceSets.toDisplaySourceSets(), + style = styles, + extra = extra + ) + } + } + + public fun <T : Pair<String, List<Documentable>>> multiBlock( + name: String, + level: Int, + kind: Kind = ContentKind.Main, + groupedElements: Iterable<T>, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + renderWhenEmpty: Boolean = false, + needsSorting: Boolean = true, + headers: List<ContentGroup> = emptyList(), + needsAnchors: Boolean = false, + operation: DocumentableContentBuilder.(String, List<Documentable>) -> Unit + ) { + + if (renderWhenEmpty || groupedElements.any()) { + group(extra = extra) { + header(level, name, kind = kind) { } + contents += ContentTable( + header = headers, + children = groupedElements + .let { + if (needsSorting) + it.sortedWith(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)) { it.first }) + else it + } + .map { + val documentables = it.second + val newExtra = if (needsAnchors) extra + SymbolAnchorHint( + it.first, + kind + ) else extra + buildGroup( + documentables.map { it.dri }.toSet(), + documentables.flatMap { it.sourceSets }.toSet(), + kind, + styles, + newExtra + ) { + operation(it.first, documentables) + } + }, + dci = DCI(mainDRI, kind), + sourceSets = sourceSets.toDisplaySourceSets(), + style = styles, + extra = extra + ) + } + } + } + + public 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 + surroundingCharactersStyle: Set<Style> = mainStyles, + separatorStyles: Set<Style> = mainStyles, + operation: DocumentableContentBuilder.(T) -> Unit + ) { + if (elements.isNotEmpty()) { + if (prefix.isNotEmpty()) text(prefix, sourceSets = sourceSets, styles = surroundingCharactersStyle) + elements.dropLast(1).forEach { + operation(it) + text(separator, sourceSets = sourceSets, styles = separatorStyles) + } + operation(elements.last()) + if (suffix.isNotEmpty()) text(suffix, sourceSets = sourceSets, styles = surroundingCharactersStyle) + } + } + + public 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, DCI(mainDRI, kind), sourceSets, styles, extra) + } + + public fun linkNode( + text: String, + address: DRI, + dci: DCI = DCI(mainDRI, ContentKind.Main), + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ): ContentLink { + return ContentDRILink( + listOf(createText(text, dci.kind, sourceSets, styles, extra)), + address, + dci, + sourceSets.toDisplaySourceSets(), + extra = extra + ) + } + + public 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.toDisplaySourceSets(), + style = emptySet() + ) + } + + public 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.toDisplaySourceSets(), + extra = extra + ) + } + + public 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.toDisplaySourceSets(), styles, extra) + } + + public fun codeBlock( + language: String = "", + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += ContentCodeBlock( + contentFor(mainDRI, sourceSets, kind, styles, extra, block).children, + language, + DCI(mainDRI, kind), + sourceSets.toDisplaySourceSets(), + styles, + extra + ) + } + + public fun codeInline( + language: String = "", + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += ContentCodeInline( + contentFor(mainDRI, sourceSets, kind, styles, extra, block).children, + language, + DCI(mainDRI, kind), + sourceSets.toDisplaySourceSets(), + styles, + extra + ) + } + + public fun firstParagraphComment( + content: DocTag, + kind: Kind = ContentKind.Comment, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + firstParagraphBrief(content)?.let { brief -> + val builtDescription = commentsConverter.buildContent( + brief, + DCI(mainDRI, kind), + sourceSets + ) + + contents += ContentGroup( + builtDescription, + DCI(mainDRI, kind), + sourceSets.toDisplaySourceSets(), + styles, + extra + ) + } + } + + public fun firstSentenceComment( + content: DocTag, + kind: Kind = ContentKind.Comment, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ){ + val builtDescription = commentsConverter.buildContent( + content, + DCI(mainDRI, kind), + sourceSets + ) + + contents += ContentGroup( + firstSentenceBriefFromContentNodes(builtDescription), + DCI(mainDRI, kind), + sourceSets.toDisplaySourceSets(), + styles, + extra + ) + } + + public 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) + } + + public 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) + } + + public 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) + + public 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.toDisplaySourceSets() + ) + } + + public 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.toDisplaySourceSets() + ) + } + + protected fun createText( + text: String, + kind: Kind, + sourceSets: Set<DokkaSourceSet>, + styles: Set<Style>, + extra: PropertyContainer<ContentNode> + ): ContentText { + return ContentText(text, DCI(mainDRI, kind), sourceSets.toDisplaySourceSets(), styles, extra) + } + + public fun <T> sourceSetDependentText( + value: SourceSetDependent<T>, + sourceSets: Set<DokkaSourceSet> = value.keys, + styles: Set<Style> = mainStyles, + 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(), styles = styles) } + } + } + + @ContentBuilderMarker + public open inner class TableBuilder( + private val mainDRI: Set<DRI>, + private val mainSourceSets: Set<DokkaSourceSet>, + private val mainKind: Kind, + private val mainStyles: Set<Style>, + private val mainExtra: PropertyContainer<ContentNode> + ) { + private val headerRows: MutableList<ContentGroup> = mutableListOf() + private val rows: MutableList<ContentGroup> = mutableListOf() + private var caption: ContentGroup? = null + + public fun header( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + headerRows += contentFor(dri, sourceSets, kind, styles, extra, block) + } + + public fun row( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + rows += contentFor(dri, sourceSets, kind, styles, extra, block) + } + + public fun caption( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + caption = contentFor(dri, sourceSets, kind, styles, extra, block) + } + + public fun build( + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ): ContentTable { + return ContentTable( + headerRows, + caption, + rows, + DCI(mainDRI, kind), + sourceSets.toDisplaySourceSets(), + styles, extra + ) + } + } + + @ContentBuilderMarker + public 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() + + public 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) + } + + public fun build( + groupID: ContentDivergentGroup.GroupID, + implicitlySourceSetHinted: Boolean, + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ): ContentDivergentGroup { + return ContentDivergentGroup( + children = instances.toList(), + dci = DCI(mainDRI, kind), + style = styles, + extra = extra, + groupID = groupID, + implicitlySourceSetHinted = implicitlySourceSetHinted + ) + } + } + + @ContentBuilderMarker + public 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 + + public 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 } + } + + public 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) + } + + public 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 } + } + + public fun build( + kind: Kind, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ): ContentDivergentInstance { + return ContentDivergentInstance( + before, + divergent ?: throw IllegalStateException("Divergent block needs divergent part"), + after, + DCI(mainDRI, kind), + sourceSets.toDisplaySourceSets(), + styles, + extra + ) + } + } + + @ContentBuilderMarker + public open inner class ListBuilder( + public val ordered: Boolean, + private val mainDRI: Set<DRI>, + private val mainSourceSets: Set<DokkaSourceSet>, + private val mainKind: Kind, + private val mainStyles: Set<Style>, + private val mainExtra: PropertyContainer<ContentNode> + ) { + private val contentNodes: MutableList<ContentNode> = mutableListOf() + + public fun item( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contentNodes += contentFor(dri, sourceSets, kind, styles, extra, block) + } + + public fun build( + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ): ContentList { + return ContentList( + contentNodes, + ordered, + DCI(mainDRI, kind), + sourceSets.toDisplaySourceSets(), + styles, extra + ) + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/briefFromContentNodes.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/briefFromContentNodes.kt new file mode 100644 index 00000000..a073f73a --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/briefFromContentNodes.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.base.utils.firstNotNullOfOrNull +import org.jetbrains.dokka.model.doc.CustomDocTag +import org.jetbrains.dokka.model.doc.DocTag +import org.jetbrains.dokka.model.doc.P +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.* + +public fun firstParagraphBrief(docTag: DocTag): DocTag? = + when(docTag){ + is P -> docTag + is CustomDocTag -> docTag.children.firstNotNullOfOrNull { firstParagraphBrief(it) } + is Text -> docTag + else -> null + } + +public fun firstSentenceBriefFromContentNodes(description: List<ContentNode>): List<ContentNode> { + val firstSentenceRegex = """^((?:[^.?!]|[.!?](?!\s))*[.!?])""".toRegex() + + //Description that is entirely based on html content. In html it is hard to define a brief so we render all of it + if(description.all { it.withDescendants().all { it is ContentGroup || (it as? ContentText)?.isHtml == true } }){ + return description + } + + var sentenceFound = false + fun lookthrough(node: ContentNode, neighbours: List<ContentNode>, currentIndex: Int): ContentNode = + if (node.finishesWithSentenceNotFollowedByHtml(firstSentenceRegex, neighbours, currentIndex) || node.containsSentenceFinish(firstSentenceRegex)) { + node as ContentText + sentenceFound = true + node.copy(text = firstSentenceRegex.find(node.text)?.value.orEmpty()) + } else if (node is ContentGroup) { + node.copy(children = node.children.mapIndexedNotNull { i, element -> + if (!sentenceFound) lookthrough(element, node.children, i) else null + }, style = node.style - TextStyle.Paragraph) + } else { + node + } + return description.mapIndexedNotNull { i, element -> + if (!sentenceFound) lookthrough(element, description, i) else null + } +} + +private fun ContentNode.finishesWithSentenceNotFollowedByHtml(firstSentenceRegex: Regex, neighbours: List<ContentNode>, currentIndex: Int): Boolean = + this is ContentText && !isHtml && matchContainsEnd(this, firstSentenceRegex) && !neighbours.nextElementIsHtml(currentIndex) + +private fun ContentNode.containsSentenceFinish(firstSentenceRegex: Regex): Boolean = + this is ContentText && !isHtml && firstSentenceRegex.containsMatchIn(text) && !matchContainsEnd(this, firstSentenceRegex) + +private fun matchContainsEnd(node: ContentText, regex: Regex): Boolean = + regex.find(node.text)?.let { node.text.endsWith(it.value) } ?: false + +private fun List<ContentNode>.nextElementIsHtml(currentElementIndex: Int): Boolean = + currentElementIndex != lastIndex && get(currentElementIndex + 1).isHtml + +private val ContentNode.isHtml + get() = extra[HtmlContent] != null diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/utils/CollectionExtensions.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/utils/CollectionExtensions.kt new file mode 100644 index 00000000..96a0a039 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/utils/CollectionExtensions.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.utils + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +internal inline fun <T, R : Any> Iterable<T>.firstNotNullOfOrNull(transform: (T) -> R?): R? { + for (element in this) { + val result = transform(element) + if (result != null) { + return result + } + } + return null +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/utils/alphabeticalOrder.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/utils/alphabeticalOrder.kt new file mode 100644 index 00000000..ed620b34 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/utils/alphabeticalOrder.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.base.utils + + +/** + * Canonical alphabetical order to sort named elements + */ +internal val canonicalAlphabeticalOrder: Comparator<in String> = String.CASE_INSENSITIVE_ORDER.thenBy { it } |