aboutsummaryrefslogtreecommitdiff
path: root/dokka-subprojects/plugin-base/src/main/kotlin
diff options
context:
space:
mode:
authorIgnat Beresnev <ignat.beresnev@jetbrains.com>2023-11-10 11:46:54 +0100
committerGitHub <noreply@github.com>2023-11-10 11:46:54 +0100
commit8e5c63d035ef44a269b8c43430f43f5c8eebfb63 (patch)
tree1b915207b2b9f61951ddbf0ff2e687efd053d555 /dokka-subprojects/plugin-base/src/main/kotlin
parenta44efd4ba0c2e4ab921ff75e0f53fc9335aa79db (diff)
downloaddokka-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')
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt299
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBaseConfiguration.kt28
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/AnalysisApiDeprecatedError.kt16
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/KotlinAnalysisDeprecatedApi.kt77
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/ParsersDeprecatedAPI.kt42
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/ParsersFactoriesDeprecatedAPI.kt24
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/TranslatorDescriptorsDeprecatedAPI.kt50
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/deprecated/TranslatorPsiDeprecatedAPI.kt25
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/generation/SingleModuleGeneration.kt131
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/DefaultRenderer.kt257
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/FileWriter.kt109
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/OutputWriter.kt11
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/PackageListService.kt80
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/TabSortingStrategy.kt11
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/contentTypeChecking.kt24
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlContent.kt18
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt1013
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/NavigationDataProvider.kt134
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/NavigationPage.kt129
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/SearchbarDataInstaller.kt128
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/Tags.kt82
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ImmediateResolutionTagConsumer.kt37
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/PathToRootConsumer.kt26
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ReplaceVersionsConsumer.kt29
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/command/consumers/ResolveLinkConsumer.kt34
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/htmlFormatingUtils.kt67
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/htmlPreprocessors.kt172
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt234
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt20
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/HtmlTemplater.kt82
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelFactory.kt19
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelMerger.kt9
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/shouldRenderSourceSetBubbles.kt20
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/pageId.kt31
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/preprocessors.kt41
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/anchors/AnchorsHint.kt19
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/DefaultExternalLocationProvider.kt46
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/DefaultExternalLocationProviderFactory.kt28
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/Dokka010ExternalLocationProvider.kt46
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProvider.kt18
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProviderFactory.kt11
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/ExternalLocationProviderFactoryWithCache.kt21
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/AndroidExternalLocationProvider.kt18
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/JavadocExternalLocationProvider.kt62
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/external/javadoc/JavadocExternalLocationProviderFactory.kt39
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DefaultLocationProvider.kt82
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaBaseLocationProvider.kt27
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaLocationProvider.kt182
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/DokkaLocationProviderFactory.kt26
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/LocationProvider.kt47
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/local/LocationProviderFactory.kt11
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/ExternalDocumentation.kt9
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/LinkFormat.kt10
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/PackageList.kt83
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/RecognizedLinkFormat.kt29
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/resolvers/shared/utils.kt41
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/JvmSignatureUtils.kt231
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureProvider.kt503
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/KotlinSignatureUtils.kt86
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/signatures/SignatureProvider.kt12
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToNavigationCommand.kt9
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToSearch.kt12
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/AddToSourcesetDependencies.kt10
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/Command.kt15
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ImmediateHtmlCommandConsumer.kt17
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/InsertTemplateExtra.kt16
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/PathToRootSubstitutionCommand.kt10
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ProjectNameSubstitutionCommand.kt10
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ReplaceVersionsCommand.kt7
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/ResolveLinkCommand.kt11
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/templating/jsonMapperForPlugins.kt62
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ActualTypealiasAdder.kt127
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier.kt12
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DefaultDocumentableMerger.kt0
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DeprecatedDocumentableFilterTransformer.kt62
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DocumentableReplacerTransformer.kt239
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/DocumentableVisibilityFilterTransformer.kt388
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/EmptyModulesFilterTransformer.kt14
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/EmptyPackagesFilterTransformer.kt30
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ExtensionExtractorTransformer.kt160
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/InheritedEntriesDocumentableFilterTransformer.kt23
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/InheritorsExtractorTransformer.kt91
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/KotlinArrayDocumentableReplacerTransformer.kt68
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ModuleAndPackageDocumentationTransformer.kt47
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ObviousFunctionsDocumentableFilterTransformer.kt17
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/ReportUndocumentedTransformer.kt143
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressTagDocumentableFilter.kt17
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressedByConditionDocumentableFilterTransformer.kt146
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/SuppressedByConfigurationDocumentableFilterTransformer.kt57
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/documentables/utils.kt35
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/DefaultSamplesTransformer.kt117
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/annotations/SinceKotlinTransformer.kt186
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/comments/CommentsToContentConverter.kt22
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/comments/DocTagToContentConverter.kt270
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/FallbackPageMergerStrategy.kt22
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/PageMerger.kt40
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/PageMergerStrategy.kt13
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/SameMethodNamePageMergerStrategy.kt68
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/merger/SourceSetMergingPageTransformer.kt43
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/sourcelinks/SourceLinksTransformer.kt140
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/tags/CustomTagContentProvider.kt63
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/tags/SinceKotlinTagContentProvider.kt38
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultDocumentableToPageTranslator.kt34
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DefaultPageCreator.kt779
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DeprecationSectionCreator.kt194
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DescriptionSections.kt349
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/DriClashAwareName.kt13
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/PageContentBuilder.kt781
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/translators/documentables/briefFromContentNodes.kt62
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/utils/CollectionExtensions.kt16
-rw-r--r--dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/utils/alphabeticalOrder.kt11
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 }