aboutsummaryrefslogtreecommitdiff
path: root/dokka-subprojects/plugin-base/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'dokka-subprojects/plugin-base/src/main')
-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
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin5
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/docs/javadoc/EnumValueOf.java.template12
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/docs/javadoc/EnumValues.java.template8
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumEntries.kt.template3
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumValueOf.kt.template4
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumValues.kt.template3
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/format/gfm.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/format/html-as-java.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/format/html.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/format/java-layout-html.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/format/jekyll.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/format/kotlin-website-html.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/format/markdown.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/anchor-copy-button.svg8
-rwxr-xr-xdokka-subprojects/plugin-base/src/main/resources/dokka/images/arrow_down.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/burger.svg9
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/copy-icon.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/copy-successful-icon.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/footer-go-to-link.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/go-to-top-icon.svg8
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/homepage.svg5
-rwxr-xr-xdokka-subprojects/plugin-base/src/main/resources/dokka/images/logo-icon.svg14
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/abstract-class-kotlin.svg26
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/abstract-class.svg20
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/annotation-kotlin.svg13
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/annotation.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/class-kotlin.svg13
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/class.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/enum-kotlin.svg13
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/enum.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/exception-class.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/field-value.svg10
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/field-variable.svg10
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/function.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/interface-kotlin.svg13
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/interface.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/object.svg13
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/typealias-kotlin.svg13
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/images/theme-toggle.svg7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/javadoc.properties6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/clipboard.js56
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/navigation-loader.js95
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/platform-content-handler.js400
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/prism.js22
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/symbol-parameters-wrapper_deferred.js64
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/styles/font-jb-sans-auto.css36
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/styles/logo-styles.css9
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/styles/prism.css217
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/styles/style.css1513
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/templates/base.ftl44
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/footer.ftl7
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/header.ftl31
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/page_metadata.ftl6
-rw-r--r--dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/source_set_selector.ftl9
167 files changed, 13181 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 }
diff --git a/dokka-subprojects/plugin-base/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/dokka-subprojects/plugin-base/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin
new file mode 100644
index 00000000..a014a209
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin
@@ -0,0 +1,5 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+org.jetbrains.dokka.base.DokkaBase
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/javadoc/EnumValueOf.java.template b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/javadoc/EnumValueOf.java.template
new file mode 100644
index 00000000..233f8819
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/javadoc/EnumValueOf.java.template
@@ -0,0 +1,12 @@
+/**
+ * Returns the enum constant of this type with the specified
+ * name.
+ * The string must match exactly an identifier used to declare
+ * an enum constant in this type. (Extraneous whitespace
+ * characters are not permitted.)
+ *
+ * @return the enum constant with the specified name
+ * @throws IllegalArgumentException if this enum type has no
+ * constant with the specified name
+ */
+ \ No newline at end of file
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/javadoc/EnumValues.java.template b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/javadoc/EnumValues.java.template
new file mode 100644
index 00000000..4aed38a6
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/javadoc/EnumValues.java.template
@@ -0,0 +1,8 @@
+/**
+ * Returns an array containing the constants of this enum
+ * type, in the order they're declared. This method may be
+ * used to iterate over the constants.
+ *
+ * @return an array containing the constants of this enum
+ * type, in the order they're declared
+ */
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumEntries.kt.template b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumEntries.kt.template
new file mode 100644
index 00000000..20d16421
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumEntries.kt.template
@@ -0,0 +1,3 @@
+Returns a representation of an immutable list of all enum entries, in the order they're declared.
+
+This method may be used to iterate over the enum entries.
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumValueOf.kt.template b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumValueOf.kt.template
new file mode 100644
index 00000000..fbf8fa8d
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumValueOf.kt.template
@@ -0,0 +1,4 @@
+Returns the enum constant of this type with the specified name. The string must match exactly an identifier used
+to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)
+
+@throws kotlin.IllegalArgumentException if this enum type has no constant with the specified name
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumValues.kt.template b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumValues.kt.template
new file mode 100644
index 00000000..c0e3559c
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/docs/kdoc/EnumValues.kt.template
@@ -0,0 +1,3 @@
+Returns an array containing the constants of this enum type, in the order they're declared.
+
+This method may be used to iterate over the constants.
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/format/gfm.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/gfm.properties
new file mode 100644
index 00000000..66b1ea8f
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/gfm.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.Formats.GFMFormatDescriptor
+description=Produces documentation in GitHub-flavored markdown format
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/format/html-as-java.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/html-as-java.properties
new file mode 100644
index 00000000..cbb5a399
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/html-as-java.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.Formats.HtmlAsJavaFormatDescriptor
+description=Produces output in HTML format using Java syntax
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/format/html.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/html.properties
new file mode 100644
index 00000000..42438d16
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/html.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.Formats.HtmlFormatDescriptor
+description=Produces output in HTML format
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/format/java-layout-html.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/java-layout-html.properties
new file mode 100644
index 00000000..79925edd
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/java-layout-html.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatDescriptor
+description=Produces Kotlin Style Docs with Javadoc like layout
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/format/jekyll.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/jekyll.properties
new file mode 100644
index 00000000..28f55afc
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/jekyll.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.Formats.JekyllFormatDescriptor
+description=Produces documentation in Jekyll format
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/format/kotlin-website-html.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/kotlin-website-html.properties
new file mode 100644
index 00000000..4e8dea39
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/kotlin-website-html.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.Formats.KotlinWebsiteHtmlFormatDescriptor
+description=Generates Kotlin website documentation
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/format/markdown.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/markdown.properties
new file mode 100644
index 00000000..62a0f2b2
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/format/markdown.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.Formats.MarkdownFormatDescriptor
+description=Produces documentation in markdown format
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/anchor-copy-button.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/anchor-copy-button.svg
new file mode 100644
index 00000000..19c1fa3f
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/anchor-copy-button.svg
@@ -0,0 +1,8 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M21.2496 5.3C20.3496 4.5 19.2496 4 18.0496 4C16.8496 4 15.6496 4.5 14.8496 5.3L10.3496 9.8L11.7496 11.2L16.2496 6.7C17.2496 5.7 18.8496 5.7 19.8496 6.7C20.8496 7.7 20.8496 9.3 19.8496 10.3L15.3496 14.8L16.7496 16.2L21.2496 11.7C22.1496 10.8 22.5496 9.7 22.5496 8.5C22.5496 7.3 22.1496 6.2 21.2496 5.3Z" fill="#637282"/>
+ <path d="M8.35 16.7998C7.35 17.7998 5.75 17.7998 4.75 16.7998C3.75 15.7998 3.75 14.1998 4.75 13.1998L9.25 8.6998L7.85 7.2998L3.35 11.7998C1.55 13.5998 1.55 16.3998 3.35 18.1998C4.25 19.0998 5.35 19.4998 6.55 19.4998C7.75 19.4998 8.85 19.0998 9.75 18.1998L14.25 13.6998L12.85 12.2998L8.35 16.7998Z" fill="#637282"/>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/arrow_down.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/arrow_down.svg
new file mode 100755
index 00000000..639aaf12
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/arrow_down.svg
@@ -0,0 +1,7 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="24" height="24" viewBox="-5 -3 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M11 9l-6 5.25V3.75z" fill="currentColor"/>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/burger.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/burger.svg
new file mode 100644
index 00000000..fcca732b
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/burger.svg
@@ -0,0 +1,9 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M23.3379 5.83331H4.67126V8.16665H23.3379V5.83331Z" fill="white"/>
+ <path d="M23.3379 12.8333H4.67126V15.1666H23.3379V12.8333Z" fill="white"/>
+ <path d="M4.67126 19.8333H23.3379V22.1666H4.67126V19.8333Z" fill="white"/>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/copy-icon.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/copy-icon.svg
new file mode 100644
index 00000000..2cb02ec6
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/copy-icon.svg
@@ -0,0 +1,7 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M5 4H15V16H5V4ZM17 7H19V18V20H17H8V18H17V7Z" fill="black"/>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/copy-successful-icon.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/copy-successful-icon.svg
new file mode 100644
index 00000000..c4b95383
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/copy-successful-icon.svg
@@ -0,0 +1,7 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M18 9C18 14 14 18 9 18C4 18 0 14 0 9C0 4 4 0 9 0C14 0 18 4 18 9ZM14.2 6.2L12.8 4.8L7.5 10.1L5.3 7.8L3.8 9.2L7.5 13L14.2 6.2Z" fill="#4DBB5F"/>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/footer-go-to-link.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/footer-go-to-link.svg
new file mode 100644
index 00000000..a87add7a
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/footer-go-to-link.svg
@@ -0,0 +1,7 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M8 0H2.3949L4.84076 2.44586L0 7.28662L0.713376 8L5.55414 3.15924L8 5.6051V0Z" fill="#637282"/>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/go-to-top-icon.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/go-to-top-icon.svg
new file mode 100644
index 00000000..abc3d1ce
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/go-to-top-icon.svg
@@ -0,0 +1,8 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M11.3337 9.66683H0.666992L6.00033 3.66683L11.3337 9.66683Z" fill="#637282"/>
+ <path d="M0.666992 0.333496H11.3337V1.66683H0.666992V0.333496Z" fill="#637282"/>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/homepage.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/homepage.svg
new file mode 100644
index 00000000..a3d7602b
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/homepage.svg
@@ -0,0 +1,5 @@
+<!-- SOURCE: https://www.svgrepo.com/svg/416627/home-house-ui -->
+<svg fill="#ffffff" width="64px" height="64px" viewBox="0 0 512.00 512.00" xmlns="http://www.w3.org/2000/svg">
+ <path d="M256,0C114.615,0,0,114.615,0,256s114.615,256,256,256s256-114.615,256-256S397.385,0,256,0z M404.861,263.236 L404.861,263.236c-7.297,7.297-18.066,8.993-26.986,5.104v97.098c0,20.193-16.37,36.562-36.562,36.562H170.688 c-20.193,0-36.562-16.37-36.562-36.562v-97.098c-8.919,3.89-19.689,2.193-26.986-5.104c-9.519-9.519-9.519-24.952,0-34.471 L238.764,97.139h0c9.519-9.519,24.952-9.519,34.471,0l131.625,131.625C414.38,238.283,414.38,253.717,404.861,263.236z"/>
+ <path d="M286.469,267.938h-60.938c-6.731,0-12.188,5.457-12.188,12.188v73.125c0,6.731,5.457,12.188,12.188,12.188h60.938 c6.731,0,12.188-5.457,12.188-12.188v-73.125C298.656,273.394,293.2,267.938,286.469,267.938z"/>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/logo-icon.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/logo-icon.svg
new file mode 100755
index 00000000..e42f9570
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/logo-icon.svg
@@ -0,0 +1,14 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M64 64H0V0H64L31.3373 31.5369L64 64Z" fill="url(#paint0_radial)"/>
+ <defs>
+ <radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(61.8732 2.63097) scale(73.3111)">
+ <stop offset="0.00343514" stop-color="#EF4857"/>
+ <stop offset="0.4689" stop-color="#D211EC"/>
+ <stop offset="1" stop-color="#7F52FF"/>
+ </radialGradient>
+ </defs>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/abstract-class-kotlin.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/abstract-class-kotlin.svg
new file mode 100644
index 00000000..19d6148c
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/abstract-class-kotlin.svg
@@ -0,0 +1,26 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="abstractClassKotlin">
+<path id="Fill 1" fill-rule="evenodd" clip-rule="evenodd" d="M3 3.1055C1.764 4.3685 1 6.0935 1 8.0005C1 9.9065 1.764 11.6315 3 12.8945V3.1055Z" fill="#9AA7B0" fill-opacity="0.8"/>
+<path id="Combined Shape" fill-rule="evenodd" clip-rule="evenodd" d="M13 8V3.1055C14.2359 4.36739 14.9999 6.0932 15 8H13Z" fill="#9AA7B0" fill-opacity="0.8"/>
+<g id="idea/community/platform/icons/src/nodes/class">
+<mask id="mask0" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="4" y="1" width="8" height="14">
+<path id="Mask" d="M4 1H12V8H8V15H4V1Z" fill="white"/>
+</mask>
+<g mask="url(#mask0)">
+<g id="class">
+<path id="Fill 1_2" fill-rule="evenodd" clip-rule="evenodd" d="M15 8C15 11.866 11.866 15 8 15C4.134 15 1 11.866 1 8C1 4.134 4.134 1 8 1C11.866 1 15 4.134 15 8Z" fill="#40B6E0" fill-opacity="0.6"/>
+<g id="&#226;&#140;&#152;/alphabet/nodes/c">
+<path id="&#226;&#140;&#152;/alphabet/nodes/c_2" fill-rule="evenodd" clip-rule="evenodd" d="M10 9.28253C9.53001 9.74153 9.02801 9.978 8.10001 10C7.06101 10.022 6.00001 9.2794 6.00001 8.0004C6.00001 6.7124 6.97101 6 8.10001 6C9.37251 6 9.90001 6.55426 9.90001 6.55426L10.5162 5.83673C9.82941 5.27017 9.28828 5.0004 8.09821 5.0004C6.34021 5.0004 5.00021 6.3584 5.00021 8.0004C5.00021 9.6824 6.36421 11.0004 8.00221 11.0004C9.29286 11.0004 10.0232 10.5934 10.6162 9.9814L10 9.28253Z" fill="#231F20" fill-opacity="0.7"/>
+</g>
+</g>
+</g>
+</g>
+<g id="&#226;&#140;&#152;/modifier/kotlin">
+<path id="&#226;&#140;&#152;/modifier/kotlin_2" d="M16 16H9V9H16L12.4 12.4L16 16Z" fill="#B99BF8"/>
+</g>
+</g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/abstract-class.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/abstract-class.svg
new file mode 100644
index 00000000..60182030
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/abstract-class.svg
@@ -0,0 +1,20 @@
+<!-- Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+ <defs>
+ <rect id="abstractclass-a" width="8" height="14"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#9AA7B0" fill-opacity=".8" d="M3 3.1055C1.764 4.3685 1 6.0935 1 8.0005 1 9.9065 1.764 11.6315 3 12.8945L3 3.1055zM13 3.1055L13 12.8945C14.236 11.6315 15 9.9065 15 8.0005 15 6.0935 14.236 4.3675 13 3.1055"/>
+ <g transform="translate(4 1)">
+ <mask id="abstractclass-b" fill="#fff">
+ <use xlink:href="#abstractclass-a"/>
+ </mask>
+ <g mask="url(#abstractclass-b)">
+ <g transform="translate(-4 -1)">
+ <path fill="#40B6E0" fill-opacity=".6" d="M15,8 C15,11.866 11.866,15 8,15 C4.134,15 1,11.866 1,8 C1,4.134 4.134,1 8,1 C11.866,1 15,4.134 15,8"/>
+ <path fill="#231F20" fill-opacity=".7" d="M5,4.28253174 C4.53,4.74153174 4.028,4.978 3.1,5 C2.061,5.022 1,4.2794 1,3.0004 C1,1.7124 1.971,1 3.1,1 C3.94833171,1 4.54833171,1.18475342 4.9,1.55426025 L5.5162,0.836730957 C4.8293999,0.270175195 4.28826904,0.0004 3.0982,0.0004 C1.3402,0.0004 0.0002,1.3584 0.0002,3.0004 C0.0002,4.6824 1.3642,6.0004 3.0022,6.0004 C4.29284668,6.0004 5.0232,5.5934 5.6162,4.9814 C5.2054,4.51548783 5,4.28253174 5,4.28253174 Z" transform="translate(5 5)"/>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/annotation-kotlin.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/annotation-kotlin.svg
new file mode 100644
index 00000000..b90f508c
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/annotation-kotlin.svg
@@ -0,0 +1,13 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="annotationKotlin">
+<g id="&#226;&#140;&#152;/modifier/kotlin">
+<path id="&#226;&#140;&#152;/modifier/kotlin_2" d="M16 16H9V9H16L12.4 12.4L16 16Z" fill="#B99BF8"/>
+</g>
+<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8 15C4.134 15 1 11.866 1 8C1 4.134 4.134 1 8 1C11.866 1 15 4.134 15 8H8V15Z" fill="#62B543" fill-opacity="0.6"/>
+<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 9.32546V9.99479C7.96296 9.99826 7.92599 10 7.88911 10C7.07966 10 6.00011 9.9211 6.00011 8.0001C6.00011 6.32043 7.45594 6.0001 8.00011 6.0001C8.15311 6.0001 9.74511 6.0551 9.82411 6.0791L9.75124 8H8.76699C8.7695 7.96484 8.77154 7.9292 8.77311 7.8931L8.84211 6.6991L8.80011 6.6891C8.68511 6.6621 8.59811 6.6481 8.50011 6.6371C8.40211 6.6271 8.30411 6.6221 8.20211 6.6221C7.97811 6.6221 7.78611 6.6681 7.62811 6.7611C7.47311 6.8511 7.34511 6.9741 7.24611 7.1241C7.15111 7.2721 7.08111 7.4411 7.03911 7.6261C6.99711 7.8091 6.97611 7.9961 6.97611 8.1841C6.97611 8.5861 7.04911 8.8721 7.19711 9.0581C7.34911 9.2481 7.55411 9.3451 7.80511 9.3451C7.87359 9.3451 7.93863 9.33855 8.00001 9.32546ZM11.9819 8H11.0207C11.0512 7.78917 11.0601 7.61595 11.0601 7.5471C11.0601 4.90741 8.70811 4.7451 8.31611 4.7451C7.77111 4.7451 4.94355 4.85089 4.94355 8.0006C4.94355 8.58402 4.94355 11.2461 7.88911 11.2461C7.91058 11.2461 7.94864 11.2438 8.00001 11.2394V11.9994C7.9664 11.9999 7.93243 12.0001 7.89811 12.0001C7.15577 12.0001 4.00211 12.0001 4.00211 8.0006C4.00211 4.0011 7.66743 4.0011 8.31611 4.0011C8.65106 4.0011 12.0001 4.08643 12.0001 7.5571C12.0001 7.71468 11.9938 7.86209 11.9819 8Z" fill="#231F20" fill-opacity="0.7"/>
+</g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/annotation.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/annotation.svg
new file mode 100644
index 00000000..b80c54b4
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/annotation.svg
@@ -0,0 +1,7 @@
+<!-- Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#62B543" fill-opacity=".6" d="M15,8 C15,11.866 11.866,15 8,15 C4.134,15 1,11.866 1,8 C1,4.134 4.134,1 8,1 C11.866,1 15,4.134 15,8"/>
+ <path fill="#231F20" fill-opacity=".7" d="M3.6281,2.7611 C3.4731,2.8511 3.3451,2.9741 3.2461,3.1241 C3.1511,3.2721 3.0811,3.4411 3.0391,3.6261 C2.9971,3.8091 2.9761,3.9961 2.9761,4.1841 C2.9761,4.5861 3.0491,4.8721 3.1971,5.0581 C3.3491,5.2481 3.5541,5.3451 3.8051,5.3451 C3.9701,5.3451 4.1151,5.3071 4.2371,5.2311 C4.3571,5.1581 4.4571,5.0531 4.5331,4.9201 C4.6061,4.7931 4.6631,4.6401 4.7011,4.4641 C4.7391,4.2941 4.7641,4.1011 4.7731,3.8931 L4.8421,2.6991 L4.8001,2.6891 C4.6851,2.6621 4.5981,2.6481 4.5001,2.6371 C4.4021,2.6271 4.3041,2.6221 4.2021,2.6221 C3.9781,2.6221 3.7861,2.6681 3.6281,2.7611 Z M0.0021,4.0006 C0.0021,0.0011 3.66741943,0.0011 4.3161,0.0011 C4.65105644,0.0011 8.0001,0.0864290039 8.0001,3.5571 C8.0001,6.0091 6.4751,6 6.1701,6 C5.67331784,5.97 5.31431784,5.7737 5.0931,5.4111 C4.68260397,5.8037 4.28127064,6 3.8891,6 C3.0796519,6 2.0001,5.9211 2.0001,4.0001 C2.0001,2.32043457 3.45593262,2.0001 4.0001,2.0001 C4.1531,2.0001 5.7451,2.0551 5.8241,2.0791 L5.7441,4.1881 C5.6361,4.89276667 5.7991,5.2451 6.2331,5.2451 C6.95605469,5.2451 7.0601,3.7831 7.0601,3.5471 C7.0601,0.907409668 4.7081,0.7451 4.3161,0.7451 C3.7711,0.7451 0.94354248,0.850891113 0.94354248,4.0006 C0.94354248,4.58402311 0.94354248,7.2461 3.8891,7.2461 C4.0901,7.2461 5.7441,7.04302979 6.1621,6.8281 L6.1621,7.5781 C5.8551,7.7031 5.0931,8.0001 3.8981,8.0001 C3.15576172,8.0001 0.0021,8.0001 0.0021,4.0006 Z" transform="translate(4 4)"/>
+ </g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/class-kotlin.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/class-kotlin.svg
new file mode 100644
index 00000000..797a2423
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/class-kotlin.svg
@@ -0,0 +1,13 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="classKotlin">
+<g id="&#226;&#140;&#152;/modifier/kotlin">
+<path id="&#226;&#140;&#152;/modifier/kotlin_2" d="M16 16H9V9H16L12.4 12.4L16 16Z" fill="#B99BF8"/>
+</g>
+<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8 15C4.134 15 1 11.866 1 8C1 4.134 4.134 1 8 1C11.866 1 15 4.134 15 8H8V15Z" fill="#40B6E0" fill-opacity="0.6"/>
+<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 11.0004C6.36299 10.9992 5.00021 9.68165 5.00021 8.0004C5.00021 6.3584 6.34021 5.0004 8.09821 5.0004C9.28828 5.0004 9.82941 5.27018 10.5162 5.83673L9.90001 6.55426C9.54835 6.18475 8.94835 6 8.10001 6C6.97101 6 6.00001 6.7124 6.00001 8.0004C6.00001 9.23838 6.99405 9.97382 8.00001 9.99976V11.0004V11.0004Z" fill="#231F20" fill-opacity="0.7"/>
+</g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/class.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/class.svg
new file mode 100644
index 00000000..3f1ad167
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/class.svg
@@ -0,0 +1,7 @@
+<!-- Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#40B6E0" fill-opacity=".6" d="M15,8 C15,11.866 11.866,15 8,15 C4.134,15 1,11.866 1,8 C1,4.134 4.134,1 8,1 C11.866,1 15,4.134 15,8"/>
+ <path fill="#231F20" fill-opacity=".7" d="M5,4.28253174 C4.53,4.74153174 4.028,4.978 3.1,5 C2.061,5.022 1,4.2794 1,3.0004 C1,1.7124 1.971,1 3.1,1 C3.94833171,1 4.54833171,1.18475342 4.9,1.55426025 L5.5162,0.836730957 C4.8293999,0.270175195 4.28826904,0.0004 3.0982,0.0004 C1.3402,0.0004 0.0002,1.3584 0.0002,3.0004 C0.0002,4.6824 1.3642,6.0004 3.0022,6.0004 C4.29284668,6.0004 5.0232,5.5934 5.6162,4.9814 C5.2054,4.51548783 5,4.28253174 5,4.28253174 Z" transform="translate(5 5)"/>
+ </g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/enum-kotlin.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/enum-kotlin.svg
new file mode 100644
index 00000000..775a7cc9
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/enum-kotlin.svg
@@ -0,0 +1,13 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="enumKotlin">
+<g id="&#226;&#140;&#152;/modifier/kotlin">
+<path id="&#226;&#140;&#152;/modifier/kotlin_2" d="M16 16H9V9H16L12.4 12.4L16 16Z" fill="#B99BF8"/>
+</g>
+<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8 15C4.134 15 1 11.866 1 8C1 4.134 4.134 1 8 1C11.866 1 15 4.134 15 8H8V15Z" fill="#40B6E0" fill-opacity="0.6"/>
+<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M8 11H6V5H10V6H7V7H9V8H7V10H8V11Z" fill="#231F20" fill-opacity="0.7"/>
+</g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/enum.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/enum.svg
new file mode 100644
index 00000000..fa7f2476
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/enum.svg
@@ -0,0 +1,7 @@
+<!-- Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#40B6E0" fill-opacity=".6" d="M15,8 C15,11.866 11.866,15 8,15 C4.134,15 1,11.866 1,8 C1,4.134 4.134,1 8,1 C11.866,1 15,4.134 15,8"/>
+ <polygon fill="#231F20" fill-opacity=".7" points="4 6 0 6 0 0 4 0 4 1 1 1 1 2 3.5 2 3.5 3 1 3 1 5 4 5" transform="translate(6 5)"/>
+ </g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/exception-class.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/exception-class.svg
new file mode 100644
index 00000000..c0b2bdeb
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/exception-class.svg
@@ -0,0 +1,7 @@
+<!-- Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#40B6E0" fill-opacity=".6" d="M15,8 C15,11.866 11.866,15 8,15 C4.134,15 1,11.866 1,8 C1,4.134 4.134,1 8,1 C11.866,1 15,4.134 15,8"/>
+ <polygon fill="#231F20" fill-opacity=".7" points="7 13 9 9 4 9 9 3 8 7 12 7"/>
+ </g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/field-value.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/field-value.svg
new file mode 100644
index 00000000..2771ee56
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/field-value.svg
@@ -0,0 +1,10 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <g fill="none" fill-rule="evenodd">
+ <rect width="14" height="14" x="1" y="1" fill="#B99BF8" fill-opacity=".6" rx="3"/>
+ <path fill="#231F20" fill-opacity=".7" d="M2.2939,6 L-0.0001,0 L1.2,0 C2.3886,3.13933333 2.98856667,4.73933333 2.9999,4.8 L4.8,0 L5.9999,0 L3.7059,6 L2.2939,6 Z" transform="translate(5 5)"/>
+ </g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/field-variable.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/field-variable.svg
new file mode 100644
index 00000000..e2d2bbd0
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/field-variable.svg
@@ -0,0 +1,10 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#B99BF8" fill-opacity=".6" d="M15,8 C15,11.866 11.866,15 8,15 C4.134,15 1,11.866 1,8 C1,4.134 4.134,1 8,1 C11.866,1 15,4.134 15,8"/>
+ <path fill="#231F20" fill-opacity=".7" d="M2.2939,6 L-0.0001,0 L1.2,0 C2.3886,3.13933333 2.98856667,4.73933333 2.9999,4.8 L4.8,0 L5.9999,0 L3.7059,6 L2.2939,6 Z" transform="translate(5 5)"/>
+ </g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/function.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/function.svg
new file mode 100644
index 00000000..f0da64a0
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/function.svg
@@ -0,0 +1,7 @@
+<!-- Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#F98B9E" fill-opacity=".6" d="M15,8 C15,11.866 11.866,15 8,15 C4.134,15 1,11.866 1,8 C1,4.134 4.134,1 8,1 C11.866,1 15,4.134 15,8"/>
+ <path fill="#231F20" fill-opacity=".7" d="M1,8 L2,8 L2,4 L3.5,4 L3.5,3 L2,3 C1.99687783,2.36169171 1.99509925,2.02835838 1.99466424,2 C1.98704681,1.50341351 2.13289549,1.0728225 2.43221029,0.972167969 C2.91964141,0.808253079 3.56884985,1.02114795 3.68984985,1.06414795 L3.98519897,0.226043701 C3.90948298,0.198825534 3.4559021,0 2.81140137,0 C2.16690063,1.40512602e-16 1.81677246,0.0614013672 1.4818929,0.388793945 C1.16513106,0.698473875 1.01614114,1.22015248 1.00124609,2 C1.00039414,2.04460465 0.999980878,2.95274463 1,3 C1.00000736,3.01819872 0.666674031,3.01819872 0,3 L0,3.972 L1,3.972 L1,8 Z" transform="translate(6 4)"/>
+ </g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/interface-kotlin.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/interface-kotlin.svg
new file mode 100644
index 00000000..5e163260
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/interface-kotlin.svg
@@ -0,0 +1,13 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="interfaceKotlin">
+<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8 15C4.134 15 1 11.866 1 8C1 4.134 4.134 1 8 1C11.866 1 15 4.134 15 8H8V15Z" fill="#62B543" fill-opacity="0.6"/>
+<path id="Vector_2" opacity="0.7" d="M8 11H6V10.0065L7.4 10V6H6V5H10V6H8.6V8H8V11Z" fill="#231F20"/>
+<g id="&#226;&#140;&#152;/modifier/kotlin">
+<path id="&#226;&#140;&#152;/modifier/kotlin_2" d="M16 16H9V9H16L12.4 12.4L16 16Z" fill="#B99BF8"/>
+</g>
+</g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/interface.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/interface.svg
new file mode 100644
index 00000000..32063ba2
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/interface.svg
@@ -0,0 +1,7 @@
+<!-- Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#62B543" fill-opacity=".6" d="M15,8 C15,11.866 11.866,15 8,15 C4.134,15 1,11.866 1,8 C1,4.134 4.134,1 8,1 C11.866,1 15,4.134 15,8"/>
+ <polygon fill="#231F20" fill-rule="nonzero" points="8.6 10 8.6 6 10 6 10 5 6 5 6 6 7.4 6 7.4 10 6 10.007 6 11 10 11 10 10" opacity=".7"/>
+ </g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/object.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/object.svg
new file mode 100644
index 00000000..31f0ee3e
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/object.svg
@@ -0,0 +1,13 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="objectKotlin">
+<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8 15C4.134 15 1 11.866 1 8C1 4.134 4.134 1 8 1C11.866 1 15 4.134 15 8H8V15Z" fill="#F4AF3D" fill-opacity="0.6"/>
+<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M11 8H9.94262C9.94262 6.87293 9.13115 5.94475 7.9918 5.94475C6.85246 5.94475 6.05738 6.85635 6.05738 7.98343V8C6.05738 9.12437 6.86496 10.0508 8 10.0552V11C7.99727 11 7.99454 11 7.9918 11C6.22951 11 5 9.64917 5 8.01657V8C5 6.3674 6.2459 5 8.0082 5C9.77049 5 11 6.35083 11 7.98343V8Z" fill="#231F20" fill-opacity="0.7"/>
+<g id="&#226;&#140;&#152;/modifier/kotlin">
+<path id="&#226;&#140;&#152;/modifier/kotlin_2" d="M16 16H9V9H16L12.4 12.4L16 16Z" fill="#B99BF8"/>
+</g>
+</g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/typealias-kotlin.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/typealias-kotlin.svg
new file mode 100644
index 00000000..f4bb238b
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/nav-icons/typealias-kotlin.svg
@@ -0,0 +1,13 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="typeAlias">
+<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8 15C4.134 15 1 11.866 1 8C1 4.134 4.134 1 8 1C11.866 1 15 4.134 15 8H8V15Z" fill="#B99BF8" fill-opacity="0.6"/>
+<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M8.5 8H8V11H7.5V6H5.5V5H10.5V6H8.5V8Z" fill="#231F20" fill-opacity="0.7"/>
+<g id="&#226;&#140;&#152;/modifier/kotlin">
+<path id="&#226;&#140;&#152;/modifier/kotlin_2" d="M16 16H9V9H16L12.4 12.4L16 16Z" fill="#B99BF8"/>
+</g>
+</g>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/images/theme-toggle.svg b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/theme-toggle.svg
new file mode 100644
index 00000000..df86202b
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/images/theme-toggle.svg
@@ -0,0 +1,7 @@
+<!--
+ - Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M19.9824 29.0078C21.7625 29.0078 23.5025 28.48 24.9826 27.491C26.4626 26.5021 27.6161 25.0965 28.2973 23.452C28.9785 21.8074 29.1568 19.9978 28.8095 18.252C28.4622 16.5062 27.6051 14.9025 26.3464 13.6439C25.0877 12.3852 23.4841 11.528 21.7382 11.1807C19.9924 10.8335 18.1828 11.0117 16.5383 11.6929C14.8937 12.3741 13.4881 13.5276 12.4992 15.0077C11.5103 16.4877 10.9824 18.2278 10.9824 20.0078C10.9851 22.3939 11.9342 24.6816 13.6214 26.3688C15.3087 28.0561 17.5963 29.0051 19.9824 29.0078ZM19.9824 13.0078C21.8389 13.0078 23.6194 13.7453 24.9322 15.0581C26.2449 16.3708 26.9824 18.1513 26.9824 20.0078C26.9824 21.8643 26.2449 23.6448 24.9322 24.9576C23.6194 26.2703 21.8389 27.0078 19.9824 27.0078V13.0078Z" fill="white"/>
+</svg>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties
new file mode 100644
index 00000000..214fe8d7
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.InboundExternalLinkResolutionService$Dokka
+description=Uses Dokka Default resolver
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties
new file mode 100644
index 00000000..285fc11a
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.Formats.JavaLayoutHtmlInboundLinkResolutionService
+description=Resolver for JavaLayoutHtml
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/javadoc.properties b/dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/javadoc.properties
new file mode 100644
index 00000000..66fcc7c9
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/inbound-link-resolver/javadoc.properties
@@ -0,0 +1,6 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+class=org.jetbrains.dokka.InboundExternalLinkResolutionService$Javadoc
+description=Uses Javadoc Default resolver
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/clipboard.js b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/clipboard.js
new file mode 100644
index 00000000..7a4f33c5
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/clipboard.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+window.addEventListener('load', () => {
+ document.querySelectorAll('span.copy-icon').forEach(element => {
+ element.addEventListener('click', (el) => copyElementsContentToClipboard(element));
+ })
+
+ document.querySelectorAll('span.anchor-icon').forEach(element => {
+ element.addEventListener('click', (el) => {
+ if(element.hasAttribute('pointing-to')){
+ const location = hrefWithoutCurrentlyUsedAnchor() + '#' + element.getAttribute('pointing-to')
+ copyTextToClipboard(element, location)
+ }
+ });
+ })
+})
+
+const copyElementsContentToClipboard = (element) => {
+ const selection = window.getSelection();
+ const range = document.createRange();
+ range.selectNodeContents(element.parentNode.parentNode);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ copyAndShowPopup(element, () => selection.removeAllRanges())
+}
+
+const copyTextToClipboard = (element, text) => {
+ var textarea = document.createElement("textarea");
+ textarea.textContent = text;
+ textarea.style.position = "fixed";
+ document.body.appendChild(textarea);
+ textarea.select();
+
+ copyAndShowPopup(element, () => document.body.removeChild(textarea))
+}
+
+const copyAndShowPopup = (element, after) => {
+ try {
+ document.execCommand('copy');
+ element.nextElementSibling.classList.add('active-popup');
+ setTimeout(() => {
+ element.nextElementSibling.classList.remove('active-popup');
+ }, 1200);
+ } catch (e) {
+ console.error('Failed to write to clipboard:', e)
+ }
+ finally {
+ if(after) after()
+ }
+}
+
+const hrefWithoutCurrentlyUsedAnchor = () => window.location.href.split('#')[0]
+
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/navigation-loader.js b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/navigation-loader.js
new file mode 100644
index 00000000..3df7ac8c
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/navigation-loader.js
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+navigationPageText = fetch(pathToRoot + "navigation.html").then(response => response.text())
+
+displayNavigationFromPage = () => {
+ navigationPageText.then(data => {
+ document.getElementById("sideMenu").innerHTML = data;
+ }).then(() => {
+ document.querySelectorAll(".overview > a").forEach(link => {
+ link.setAttribute("href", pathToRoot + link.getAttribute("href"));
+ })
+ }).then(() => {
+ document.querySelectorAll(".sideMenuPart").forEach(nav => {
+ if (!nav.classList.contains("hidden"))
+ nav.classList.add("hidden")
+ })
+ }).then(() => {
+ revealNavigationForCurrentPage()
+ }).then(() => {
+ scrollNavigationToSelectedElement()
+ })
+ document.querySelectorAll('.footer a[href^="#"]').forEach(anchor => {
+ anchor.addEventListener('click', function (e) {
+ e.preventDefault();
+ document.querySelector(this.getAttribute('href')).scrollIntoView({
+ behavior: 'smooth'
+ });
+ });
+ });
+}
+
+revealNavigationForCurrentPage = () => {
+ let pageId = document.getElementById("content").attributes["pageIds"].value.toString();
+ let parts = document.querySelectorAll(".sideMenuPart");
+ let found = 0;
+ do {
+ parts.forEach(part => {
+ if (part.attributes['pageId'].value.indexOf(pageId) !== -1 && found === 0) {
+ found = 1;
+ if (part.classList.contains("hidden")) {
+ part.classList.remove("hidden");
+ part.setAttribute('data-active', "");
+ }
+ revealParents(part)
+ }
+ });
+ pageId = pageId.substring(0, pageId.lastIndexOf("/"))
+ } while (pageId.indexOf("/") !== -1 && found === 0)
+};
+revealParents = (part) => {
+ if (part.classList.contains("sideMenuPart")) {
+ if (part.classList.contains("hidden"))
+ part.classList.remove("hidden");
+ revealParents(part.parentNode)
+ }
+};
+
+scrollNavigationToSelectedElement = () => {
+ let selectedElement = document.querySelector('div.sideMenuPart[data-active]')
+ if (selectedElement == null) { // nothing selected, probably just the main page opened
+ return
+ }
+
+ let hasIcon = selectedElement.querySelectorAll(":scope > div.overview span.nav-icon").length > 0
+
+ // for instance enums also have children and are expandable, but are not package/module elements
+ let isPackageElement = selectedElement.children.length > 1 && !hasIcon
+ if (isPackageElement) {
+ // if package is selected or linked, it makes sense to align it to top
+ // so that you can see all the members it contains
+ selectedElement.scrollIntoView(true)
+ } else {
+ // if a member within a package is linked, it makes sense to center it since it,
+ // this should make it easier to look at surrounding members
+ selectedElement.scrollIntoView({
+ behavior: 'auto',
+ block: 'center',
+ inline: 'center'
+ })
+ }
+}
+
+/*
+ This is a work-around for safari being IE of our times.
+ It doesn't fire a DOMContentLoaded, presumabely because eventListener is added after it wants to do it
+*/
+if (document.readyState == 'loading') {
+ window.addEventListener('DOMContentLoaded', () => {
+ displayNavigationFromPage()
+ })
+} else {
+ displayNavigationFromPage()
+}
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/platform-content-handler.js b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/platform-content-handler.js
new file mode 100644
index 00000000..811c4788
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/platform-content-handler.js
@@ -0,0 +1,400 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+filteringContext = {
+ dependencies: {},
+ restrictedDependencies: [],
+ activeFilters: []
+}
+let highlightedAnchor;
+let topNavbarOffset;
+let instances = [];
+let sourcesetNotification;
+
+const samplesDarkThemeName = 'darcula'
+const samplesLightThemeName = 'idea'
+
+window.addEventListener('load', () => {
+ document.querySelectorAll("div[data-platform-hinted]")
+ .forEach(elem => elem.addEventListener('click', (event) => togglePlatformDependent(event, elem)))
+ const filterSection = document.getElementById('filter-section')
+ if (filterSection) {
+ filterSection.addEventListener('click', (event) => filterButtonHandler(event))
+ initializeFiltering()
+ }
+ initTabs()
+ handleAnchor()
+ initHidingLeftNavigation()
+ topNavbarOffset = document.getElementById('navigation-wrapper')
+ darkModeSwitch()
+})
+
+const darkModeSwitch = () => {
+ const localStorageKey = "dokka-dark-mode"
+ const storage = localStorage.getItem(localStorageKey)
+ const osDarkSchemePreferred = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
+ const darkModeEnabled = storage ? JSON.parse(storage) : osDarkSchemePreferred
+ const element = document.getElementById("theme-toggle-button")
+ initPlayground(darkModeEnabled ? samplesDarkThemeName : samplesLightThemeName)
+
+ element.addEventListener('click', () => {
+ const enabledClasses = document.getElementsByTagName("html")[0].classList
+ enabledClasses.toggle("theme-dark")
+
+ //if previously we had saved dark theme then we set it to light as this is what we save in local storage
+ const darkModeEnabled = enabledClasses.contains("theme-dark")
+ if (darkModeEnabled) {
+ initPlayground(samplesDarkThemeName)
+ } else {
+ initPlayground(samplesLightThemeName)
+ }
+ localStorage.setItem(localStorageKey, JSON.stringify(darkModeEnabled))
+ })
+}
+
+const initPlayground = (theme) => {
+ if (!samplesAreEnabled()) return
+ instances.forEach(instance => instance.destroy())
+ instances = []
+
+ // Manually tag code fragments as not processed by playground since we also manually destroy all of its instances
+ document.querySelectorAll('code.runnablesample').forEach(node => {
+ node.removeAttribute("data-kotlin-playground-initialized");
+ })
+
+ KotlinPlayground('code.runnablesample', {
+ getInstance: playgroundInstance => {
+ instances.push(playgroundInstance)
+ },
+ theme: theme
+ });
+}
+
+// We check if type is accessible from the current scope to determine if samples script is present
+// As an alternative we could extract this samples-specific script to new js file but then we would handle dark mode in 2 separate files which is not ideal
+const samplesAreEnabled = () => {
+ try {
+ KotlinPlayground
+ return true
+ } catch (e) {
+ return false
+ }
+}
+
+
+const initHidingLeftNavigation = () => {
+ document.getElementById("menu-toggle").onclick = function (event) {
+ //Events need to be prevented from bubbling since they will trigger next handler
+ event.preventDefault();
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+ document.getElementById("leftColumn").classList.toggle("open");
+ }
+
+ document.getElementById("main").onclick = () => {
+ document.getElementById("leftColumn").classList.remove("open");
+ }
+}
+
+// Hash change is needed in order to allow for linking inside the same page with anchors
+// If this is not present user is forced to refresh the site in order to use an anchor
+window.onhashchange = handleAnchor
+
+function scrollToElementInContent(element) {
+ const scrollToElement = () => document.getElementById('main').scrollTo({
+ top: element.offsetTop - topNavbarOffset.offsetHeight,
+ behavior: "smooth"
+ })
+
+ const waitAndScroll = () => {
+ setTimeout(() => {
+ if (topNavbarOffset) {
+ scrollToElement()
+ } else {
+ waitForScroll()
+ }
+ }, 50)
+ }
+
+ if (topNavbarOffset) {
+ scrollToElement()
+ } else {
+ waitAndScroll()
+ }
+}
+
+
+function handleAnchor() {
+ if (highlightedAnchor) {
+ highlightedAnchor.classList.remove('anchor-highlight')
+ highlightedAnchor = null;
+ }
+
+ let searchForContentTarget = function (element) {
+ if (element && element.hasAttribute) {
+ if (element.hasAttribute("data-togglable")) return element.getAttribute("data-togglable");
+ else return searchForContentTarget(element.parentNode)
+ } else return null
+ }
+
+ let findAnyTab = function (target) {
+ let result = null
+ document.querySelectorAll('div[tabs-section] > button[data-togglable]')
+ .forEach(node => {
+ if(node.getAttribute("data-togglable").split(",").includes(target)) {
+ result = node
+ }
+ })
+ return result
+ }
+
+ let anchor = window.location.hash
+ if (anchor != "") {
+ anchor = anchor.substring(1)
+ let element = document.querySelector('a[data-name="' + anchor + '"]')
+
+ if (element) {
+ const content = element.nextElementSibling
+ const contentStyle = window.getComputedStyle(content)
+ if(contentStyle.display == 'none') {
+ let tab = findAnyTab(searchForContentTarget(content))
+ if (tab) {
+ toggleSections(tab)
+ }
+ }
+
+ if (content) {
+ content.classList.add('anchor-highlight')
+ highlightedAnchor = content
+ }
+
+ scrollToElementInContent(element)
+ }
+ }
+}
+
+function initTabs() {
+ // we could have only a single type of data - classlike or package
+ const mainContent = document.querySelector('.main-content');
+ const type = mainContent ? mainContent.getAttribute("data-page-type") : null;
+ const localStorageKey = "active-tab-" + type;
+ document.querySelectorAll('div[tabs-section]').forEach(element => {
+ showCorrespondingTabBody(element);
+ element.addEventListener('click', ({target}) => {
+ const togglable = target ? target.getAttribute("data-togglable") : null;
+ if (!togglable) return;
+
+ localStorage.setItem(localStorageKey, JSON.stringify(togglable));
+ toggleSections(target);
+ });
+ });
+
+ const cached = localStorage.getItem(localStorageKey);
+ if (!cached) return;
+
+ const tab = document.querySelector(
+ 'div[tabs-section] > button[data-togglable="' + JSON.parse(cached) + '"]'
+ );
+ if (!tab) return;
+
+ toggleSections(tab);
+}
+
+function showCorrespondingTabBody(element) {
+ const buttonWithKey = element.querySelector("button[data-active]")
+ if (buttonWithKey) {
+ toggleSections(buttonWithKey)
+ }
+}
+
+function filterButtonHandler(event) {
+ if (event.target.tagName == "BUTTON" && event.target.hasAttribute("data-filter")) {
+ let sourceset = event.target.getAttribute("data-filter")
+ if (filteringContext.activeFilters.indexOf(sourceset) != -1) {
+ filterSourceset(sourceset)
+ } else {
+ unfilterSourceset(sourceset)
+ }
+ }
+}
+
+function initializeFiltering() {
+ filteringContext.dependencies = JSON.parse(sourceset_dependencies)
+ document.querySelectorAll("#filter-section > button")
+ .forEach(p => filteringContext.restrictedDependencies.push(p.getAttribute("data-filter")))
+ Object.keys(filteringContext.dependencies).forEach(p => {
+ filteringContext.dependencies[p] = filteringContext.dependencies[p]
+ .filter(q => -1 !== filteringContext.restrictedDependencies.indexOf(q))
+ })
+ let cached = window.localStorage.getItem('inactive-filters')
+ if (cached) {
+ let parsed = JSON.parse(cached)
+ filteringContext.activeFilters = filteringContext.restrictedDependencies
+ .filter(q => parsed.indexOf(q) == -1)
+ } else {
+ filteringContext.activeFilters = filteringContext.restrictedDependencies
+ }
+ refreshFiltering()
+}
+
+function filterSourceset(sourceset) {
+ filteringContext.activeFilters = filteringContext.activeFilters.filter(p => p != sourceset)
+ refreshFiltering()
+ addSourcesetFilterToCache(sourceset)
+}
+
+function unfilterSourceset(sourceset) {
+ if (filteringContext.activeFilters.length == 0) {
+ filteringContext.activeFilters = filteringContext.dependencies[sourceset].concat([sourceset])
+ refreshFiltering()
+ filteringContext.dependencies[sourceset].concat([sourceset]).forEach(p => removeSourcesetFilterFromCache(p))
+ } else {
+ filteringContext.activeFilters.push(sourceset)
+ refreshFiltering()
+ removeSourcesetFilterFromCache(sourceset)
+ }
+
+}
+
+function addSourcesetFilterToCache(sourceset) {
+ let cached = localStorage.getItem('inactive-filters')
+ if (cached) {
+ let parsed = JSON.parse(cached)
+ localStorage.setItem('inactive-filters', JSON.stringify(parsed.concat([sourceset])))
+ } else {
+ localStorage.setItem('inactive-filters', JSON.stringify([sourceset]))
+ }
+}
+
+function removeSourcesetFilterFromCache(sourceset) {
+ let cached = localStorage.getItem('inactive-filters')
+ if (cached) {
+ let parsed = JSON.parse(cached)
+ localStorage.setItem('inactive-filters', JSON.stringify(parsed.filter(p => p != sourceset)))
+ }
+}
+
+function toggleSections(target) {
+ const activateTabs = (containerClass) => {
+ for (const element of document.getElementsByClassName(containerClass)) {
+ for (const child of element.children) {
+ if (child.getAttribute("data-togglable") === target.getAttribute("data-togglable")) {
+ child.setAttribute("data-active", "")
+ } else {
+ child.removeAttribute("data-active")
+ }
+ }
+ }
+ }
+ const toggleTargets = target.getAttribute("data-togglable").split(",")
+ const activateTabsBody = (containerClass) => {
+ document.querySelectorAll("." + containerClass + " *[data-togglable]")
+ .forEach(child => {
+ if (toggleTargets.includes(child.getAttribute("data-togglable"))) {
+ child.setAttribute("data-active", "")
+ } else if(!child.classList.contains("sourceset-dependent-content")) { // data-togglable is used to switch source set as well, ignore it
+ child.removeAttribute("data-active")
+ }
+ })
+ }
+ activateTabs("tabs-section")
+ activateTabsBody("tabs-section-body")
+}
+
+function togglePlatformDependent(e, container) {
+ let target = e.target
+ if (target.tagName != 'BUTTON') return;
+ let index = target.getAttribute('data-toggle')
+
+ for (let child of container.children) {
+ if (child.hasAttribute('data-toggle-list')) {
+ for (let bm of child.children) {
+ if (bm == target) {
+ bm.setAttribute('data-active', "")
+ } else if (bm != target) {
+ bm.removeAttribute('data-active')
+ }
+ }
+ } else if (child.getAttribute('data-togglable') == index) {
+ child.setAttribute('data-active', "")
+ } else {
+ child.removeAttribute('data-active')
+ }
+ }
+}
+
+function refreshFiltering() {
+ let sourcesetList = filteringContext.activeFilters
+ document.querySelectorAll("[data-filterable-set]")
+ .forEach(
+ elem => {
+ let platformList = elem.getAttribute("data-filterable-set").split(',').filter(v => -1 !== sourcesetList.indexOf(v))
+ elem.setAttribute("data-filterable-current", platformList.join(','))
+ }
+ )
+ refreshFilterButtons()
+ refreshPlatformTabs()
+ refreshNoContentNotification()
+ refreshPlaygroundSamples()
+}
+
+function refreshPlaygroundSamples() {
+ document.querySelectorAll('code.runnablesample').forEach(node => {
+ const playground = node.KotlinPlayground;
+ /* Some samples may be hidden by filter, they have 0px height for visible code area
+ * after rendering. Call this method for re-calculate code area height */
+ playground && playground.view.codemirror.refresh();
+ });
+}
+
+function refreshNoContentNotification() {
+ const element = document.getElementsByClassName("main-content")[0]
+ if(filteringContext.activeFilters.length === 0){
+ element.style.display = "none";
+
+ const appended = document.createElement("div")
+ appended.className = "filtered-message"
+ appended.innerText = "All documentation is filtered, please adjust your source set filters in top-right corner of the screen"
+ sourcesetNotification = appended
+ element.parentNode.prepend(appended)
+ } else {
+ if(sourcesetNotification) sourcesetNotification.remove()
+ element.style.display = "block"
+ }
+}
+
+function refreshPlatformTabs() {
+ document.querySelectorAll(".platform-hinted > .platform-bookmarks-row").forEach(
+ p => {
+ let active = false;
+ let firstAvailable = null
+ p.childNodes.forEach(
+ element => {
+ if (element.getAttribute("data-filterable-current") != '') {
+ if (firstAvailable == null) {
+ firstAvailable = element
+ }
+ if (element.hasAttribute("data-active")) {
+ active = true;
+ }
+ }
+ }
+ )
+ if (active == false && firstAvailable) {
+ firstAvailable.click()
+ }
+ }
+ )
+}
+
+function refreshFilterButtons() {
+ document.querySelectorAll("#filter-section > button")
+ .forEach(f => {
+ if (filteringContext.activeFilters.indexOf(f.getAttribute("data-filter")) != -1) {
+ f.setAttribute("data-active", "")
+ } else {
+ f.removeAttribute("data-active")
+ }
+ })
+}
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/prism.js b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/prism.js
new file mode 100644
index 00000000..07423626
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/prism.js
@@ -0,0 +1,22 @@
+/* PrismJS 1.29.0
+https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+java+javadoc+javadoclike+kotlin&plugins=keep-markup */
+var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++t}),e.__id},clone:function e(n,t){var r,i;switch(t=t||{},a.util.type(n)){case"Object":if(i=a.util.objId(n),t[i])return t[i];for(var l in r={},t[i]=r,n)n.hasOwnProperty(l)&&(r[l]=e(n[l],t));return r;case"Array":return i=a.util.objId(n),t[i]?t[i]:(r=[],t[i]=r,n.forEach((function(n,a){r[a]=e(n,t)})),r);default:return n}},getLanguage:function(e){for(;e;){var t=n.exec(e.className);if(t)return t[1].toLowerCase();e=e.parentElement}return"none"},setLanguage:function(e,t){e.className=e.className.replace(RegExp(n,"gi"),""),e.classList.add("language-"+t)},currentScript:function(){if("undefined"==typeof document)return null;if("currentScript"in document)return document.currentScript;try{throw new Error}catch(r){var e=(/at [^(\r\n]*\((.*):[^:]+:[^:]+\)$/i.exec(r.stack)||[])[1];if(e){var n=document.getElementsByTagName("script");for(var t in n)if(n[t].src==e)return n[t]}return null}},isActive:function(e,n,t){for(var r="no-"+n;e;){var a=e.classList;if(a.contains(n))return!0;if(a.contains(r))return!1;e=e.parentElement}return!!t}},languages:{plain:r,plaintext:r,text:r,txt:r,extend:function(e,n){var t=a.util.clone(a.languages[e]);for(var r in n)t[r]=n[r];return t},insertBefore:function(e,n,t,r){var i=(r=r||a.languages)[e],l={};for(var o in i)if(i.hasOwnProperty(o)){if(o==n)for(var s in t)t.hasOwnProperty(s)&&(l[s]=t[s]);t.hasOwnProperty(o)||(l[o]=i[o])}var u=r[e];return r[e]=l,a.languages.DFS(a.languages,(function(n,t){t===u&&n!=e&&(this[n]=l)})),l},DFS:function e(n,t,r,i){i=i||{};var l=a.util.objId;for(var o in n)if(n.hasOwnProperty(o)){t.call(n,o,n[o],r||o);var s=n[o],u=a.util.type(s);"Object"!==u||i[l(s)]?"Array"!==u||i[l(s)]||(i[l(s)]=!0,e(s,t,o,i)):(i[l(s)]=!0,e(s,t,null,i))}}},plugins:{},highlightAll:function(e,n){a.highlightAllUnder(document,e,n)},highlightAllUnder:function(e,n,t){var r={callback:t,container:e,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};a.hooks.run("before-highlightall",r),r.elements=Array.prototype.slice.apply(r.container.querySelectorAll(r.selector)),a.hooks.run("before-all-elements-highlight",r);for(var i,l=0;i=r.elements[l++];)a.highlightElement(i,!0===n,r.callback)},highlightElement:function(n,t,r){var i=a.util.getLanguage(n),l=a.languages[i];a.util.setLanguage(n,i);var o=n.parentElement;o&&"pre"===o.nodeName.toLowerCase()&&a.util.setLanguage(o,i);var s={element:n,language:i,grammar:l,code:n.textContent};function u(e){s.highlightedCode=e,a.hooks.run("before-insert",s),s.element.innerHTML=s.highlightedCode,a.hooks.run("after-highlight",s),a.hooks.run("complete",s),r&&r.call(s.element)}if(a.hooks.run("before-sanity-check",s),(o=s.element.parentElement)&&"pre"===o.nodeName.toLowerCase()&&!o.hasAttribute("tabindex")&&o.setAttribute("tabindex","0"),!s.code)return a.hooks.run("complete",s),void(r&&r.call(s.element));if(a.hooks.run("before-highlight",s),s.grammar)if(t&&e.Worker){var c=new Worker(a.filename);c.onmessage=function(e){u(e.data)},c.postMessage(JSON.stringify({language:s.language,code:s.code,immediateClose:!0}))}else u(a.highlight(s.code,s.grammar,s.language));else u(a.util.encode(s.code))},highlight:function(e,n,t){var r={code:e,grammar:n,language:t};if(a.hooks.run("before-tokenize",r),!r.grammar)throw new Error('The language "'+r.language+'" has no grammar.');return r.tokens=a.tokenize(r.code,r.grammar),a.hooks.run("after-tokenize",r),i.stringify(a.util.encode(r.tokens),r.language)},tokenize:function(e,n){var t=n.rest;if(t){for(var r in t)n[r]=t[r];delete n.rest}var a=new s;return u(a,a.head,e),o(e,a,n,a.head,0),function(e){for(var n=[],t=e.head.next;t!==e.tail;)n.push(t.value),t=t.next;return n}(a)},hooks:{all:{},add:function(e,n){var t=a.hooks.all;t[e]=t[e]||[],t[e].push(n)},run:function(e,n){var t=a.hooks.all[e];if(t&&t.length)for(var r,i=0;r=t[i++];)r(n)}},Token:i};function i(e,n,t,r){this.type=e,this.content=n,this.alias=t,this.length=0|(r||"").length}function l(e,n,t,r){e.lastIndex=n;var a=e.exec(t);if(a&&r&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function o(e,n,t,r,s,g){for(var f in t)if(t.hasOwnProperty(f)&&t[f]){var h=t[f];h=Array.isArray(h)?h:[h];for(var d=0;d<h.length;++d){if(g&&g.cause==f+","+d)return;var v=h[d],p=v.inside,m=!!v.lookbehind,y=!!v.greedy,k=v.alias;if(y&&!v.pattern.global){var x=v.pattern.toString().match(/[imsuy]*$/)[0];v.pattern=RegExp(v.pattern.source,x+"g")}for(var b=v.pattern||v,w=r.next,A=s;w!==n.tail&&!(g&&A>=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(j<O||"string"==typeof C.value);C=C.next)L++,j+=C.value.length;L--,E=e.slice(A,j),P.index-=A}else if(!(P=l(b,0,E,m)))continue;S=P.index;var N=P[0],_=E.slice(0,S),M=E.slice(S+N.length),W=A+E.length;g&&W>g.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a<t&&r!==e.tail;a++)r=r.next;n.next=r,r.prev=n,e.length-=a}if(e.Prism=a,i.stringify=function e(n,t){if("string"==typeof n)return n;if(Array.isArray(n)){var r="";return n.forEach((function(n){r+=e(n,t)})),r}var i={type:n.type,content:e(n.content,t),tag:"span",classes:["token",n.type],attributes:{},language:t},l=n.alias;l&&(Array.isArray(l)?Array.prototype.push.apply(i.classes,l):i.classes.push(l)),a.hooks.run("wrap",i);var o="";for(var s in i.attributes)o+=" "+s+'="'+(i.attributes[s]||"").replace(/"/g,"&quot;")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+o+">"+i.content+"</"+i.tag+">"},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
+Prism.languages.markup={comment:{pattern:/<!--(?:(?!<!--)[\s\S])*?-->/,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/<!DOCTYPE(?:[^>"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|<!--(?:[^-]|-(?!->))*-->)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^<!|>$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&amp;/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^<!\[CDATA\[|\]\]>$/i;var t={"included-cdata":{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:<!\\[CDATA\\[(?:[^\\]]|\\](?!\\]>))*\\]\\]>|(?!<!\\[CDATA\\[)[^])*?(?=</__>)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml;
+!function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism);
+Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/};
+Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript;
+!function(e){var n=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,t="(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*",s={pattern:RegExp("(^|[^\\w.])"+t+"[A-Z](?:[\\d_A-Z]*[a-z]\\w*)?\\b"),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}},punctuation:/\./}};e.languages.java=e.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"/,lookbehind:!0,greedy:!0},"class-name":[s,{pattern:RegExp("(^|[^\\w.])"+t+"[A-Z]\\w*(?=\\s+\\w+\\s*[;,=()]|\\s*(?:\\[[\\s,]*\\]\\s*)?::\\s*new\\b)"),lookbehind:!0,inside:s.inside},{pattern:RegExp("(\\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\\s+)"+t+"[A-Z]\\w*\\b"),lookbehind:!0,inside:s.inside}],keyword:n,function:[e.languages.clike.function,{pattern:/(::\s*)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0},constant:/\b[A-Z][A-Z_\d]+\b/}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"},char:{pattern:/'(?:\\.|[^'\\\r\n]){1,6}'/,greedy:!0}}),e.languages.insertBefore("java","class-name",{annotation:{pattern:/(^|[^.])@\w+(?:\s*\.\s*\w+)*/,lookbehind:!0,alias:"punctuation"},generics:{pattern:/<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{"class-name":s,keyword:n,punctuation:/[<>(),.:]/,operator:/[?&|]/}},import:[{pattern:RegExp("(\\bimport\\s+)"+t+"(?:[A-Z]\\w*|\\*)(?=\\s*;)"),lookbehind:!0,inside:{namespace:s.inside.namespace,punctuation:/\./,operator:/\*/,"class-name":/\w+/}},{pattern:RegExp("(\\bimport\\s+static\\s+)"+t+"(?:\\w+|\\*)(?=\\s*;)"),lookbehind:!0,alias:"static",inside:{namespace:s.inside.namespace,static:/\b\w+$/,punctuation:/\./,operator:/\*/,"class-name":/\w+/}}],namespace:{pattern:RegExp("(\\b(?:exports|import(?:\\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\\s+)(?!<keyword>)[a-z]\\w*(?:\\.[a-z]\\w*)*\\.?".replace(/<keyword>/g,(function(){return n.source}))),lookbehind:!0,inside:{punctuation:/\./}}})}(Prism);
+!function(a){var e=a.languages.javadoclike={parameter:{pattern:/(^[\t ]*(?:\/{3}|\*|\/\*\*)\s*@(?:arg|arguments|param)\s+)\w+/m,lookbehind:!0},keyword:{pattern:/(^[\t ]*(?:\/{3}|\*|\/\*\*)\s*|\{)@[a-z][a-zA-Z-]+\b/m,lookbehind:!0},punctuation:/[{}]/};Object.defineProperty(e,"addSupport",{value:function(e,n){"string"==typeof e&&(e=[e]),e.forEach((function(e){!function(e,n){var t="doc-comment",r=a.languages[e];if(r){var o=r[t];if(o||(o=(r=a.languages.insertBefore(e,"comment",{"doc-comment":{pattern:/(^|[^\\])\/\*\*[^/][\s\S]*?(?:\*\/|$)/,lookbehind:!0,alias:"comment"}}))[t]),o instanceof RegExp&&(o=r[t]={pattern:o}),Array.isArray(o))for(var i=0,s=o.length;i<s;i++)o[i]instanceof RegExp&&(o[i]={pattern:o[i]}),n(o[i]);else n(o)}}(e,(function(a){a.inside||(a.inside={}),a.inside.rest=n}))}))}}),e.addSupport(["java","javascript","php"],e)}(Prism);
+!function(a){var e=/(^(?:[\t ]*(?:\*\s*)*))[^*\s].*$/m,n="(?:\\b[a-zA-Z]\\w+\\s*\\.\\s*)*\\b[A-Z]\\w*(?:\\s*<mem>)?|<mem>".replace(/<mem>/g,(function(){return"#\\s*\\w+(?:\\s*\\([^()]*\\))?"}));a.languages.javadoc=a.languages.extend("javadoclike",{}),a.languages.insertBefore("javadoc","keyword",{reference:{pattern:RegExp("(@(?:exception|link|linkplain|see|throws|value)\\s+(?:\\*\\s*)?)(?:"+n+")"),lookbehind:!0,inside:{function:{pattern:/(#\s*)\w+(?=\s*\()/,lookbehind:!0},field:{pattern:/(#\s*)\w+/,lookbehind:!0},namespace:{pattern:/\b(?:[a-z]\w*\s*\.\s*)+/,inside:{punctuation:/\./}},"class-name":/\b[A-Z]\w*/,keyword:a.languages.java.keyword,punctuation:/[#()[\],.]/}},"class-name":{pattern:/(@param\s+)<[A-Z]\w*>/,lookbehind:!0,inside:{punctuation:/[.<>]/}},"code-section":[{pattern:/(\{@code\s+(?!\s))(?:[^\s{}]|\s+(?![\s}])|\{(?:[^{}]|\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})*\})+(?=\s*\})/,lookbehind:!0,inside:{code:{pattern:e,lookbehind:!0,inside:a.languages.java,alias:"language-java"}}},{pattern:/(<(code|pre|tt)>(?!<code>)\s*)\S(?:\S|\s+\S)*?(?=\s*<\/\2>)/,lookbehind:!0,inside:{line:{pattern:e,lookbehind:!0,inside:{tag:a.languages.markup.tag,entity:a.languages.markup.entity,code:{pattern:/.+/,inside:a.languages.java,alias:"language-java"}}}}}],tag:a.languages.markup.tag,entity:a.languages.markup.entity}),a.languages.javadoclike.addSupport("java",a.languages.javadoc)}(Prism);
+!function(n){n.languages.kotlin=n.languages.extend("clike",{keyword:{pattern:/(^|[^.])\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|var|vararg|when|where|while)\b/,lookbehind:!0},function:[{pattern:/(?:`[^\r\n`]+`|\b\w+)(?=\s*\()/,greedy:!0},{pattern:/(\.)(?:`[^\r\n`]+`|\w+)(?=\s*\{)/,lookbehind:!0,greedy:!0}],number:/\b(?:0[xX][\da-fA-F]+(?:_[\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?(?:[eE][+-]?\d+(?:_\d+)*)?[fFL]?)\b/,operator:/\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\/*%<>]=?|[?:]:?|\.\.|&&|\|\||\b(?:and|inv|or|shl|shr|ushr|xor)\b/}),delete n.languages.kotlin["class-name"];var e={"interpolation-punctuation":{pattern:/^\$\{?|\}$/,alias:"punctuation"},expression:{pattern:/[\s\S]+/,inside:n.languages.kotlin}};n.languages.insertBefore("kotlin","string",{"string-literal":[{pattern:/"""(?:[^$]|\$(?:(?!\{)|\{[^{}]*\}))*?"""/,alias:"multiline",inside:{interpolation:{pattern:/\$(?:[a-z_]\w*|\{[^{}]*\})/i,inside:e},string:/[\s\S]+/}},{pattern:/"(?:[^"\\\r\n$]|\\.|\$(?:(?!\{)|\{[^{}]*\}))*"/,alias:"singleline",inside:{interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$(?:[a-z_]\w*|\{[^{}]*\})/i,lookbehind:!0,inside:e},string:/[\s\S]+/}}],char:{pattern:/'(?:[^'\\\r\n]|\\(?:.|u[a-fA-F0-9]{0,4}))'/,greedy:!0}}),delete n.languages.kotlin.string,n.languages.insertBefore("kotlin","keyword",{annotation:{pattern:/\B@(?:\w+:)?(?:[A-Z]\w*|\[[^\]]+\])/,alias:"builtin"}}),n.languages.insertBefore("kotlin","function",{label:{pattern:/\b\w+@|@\w+\b/,alias:"symbol"}}),n.languages.kt=n.languages.kotlin,n.languages.kts=n.languages.kotlin}(Prism);
+"undefined"!=typeof Prism&&"undefined"!=typeof document&&document.createRange&&(Prism.plugins.KeepMarkup=!0,Prism.hooks.add("before-highlight",(function(e){if(e.element.children.length&&Prism.util.isActive(e.element,"keep-markup",!0)){var n=Prism.util.isActive(e.element,"drop-tokens",!1),t=0,o=[];r(e.element),o.length&&(e.keepMarkup=o)}function d(e){if(function(e){return!n||"span"!==e.nodeName.toLowerCase()||!e.classList.contains("token")}(e)){var d={element:e,posOpen:t};o.push(d),r(e),d.posClose=t}else r(e)}function r(e){for(var n=0,o=e.childNodes.length;n<o;n++){var r=e.childNodes[n];1===r.nodeType?d(r):3===r.nodeType&&(t+=r.data.length)}}})),Prism.hooks.add("after-highlight",(function(e){if(e.keepMarkup&&e.keepMarkup.length){var n=function(e,t){for(var o=0,d=e.childNodes.length;o<d;o++){var r=e.childNodes[o];if(1===r.nodeType){if(!n(r,t))return!1}else 3===r.nodeType&&(!t.nodeStart&&t.pos+r.data.length>t.node.posOpen&&(t.nodeStart=r,t.nodeStartPos=t.node.posOpen-t.pos),t.nodeStart&&t.pos+r.data.length>=t.node.posClose&&(t.nodeEnd=r,t.nodeEndPos=t.node.posClose-t.pos),t.pos+=r.data.length);if(t.nodeStart&&t.nodeEnd){var s=document.createRange();return s.setStart(t.nodeStart,t.nodeStartPos),s.setEnd(t.nodeEnd,t.nodeEndPos),t.node.element.innerHTML="",t.node.element.appendChild(s.extractContents()),s.insertNode(t.node.element),s.detach(),!1}}return!0};e.keepMarkup.forEach((function(t){n(e.element,{node:t,pos:0})})),e.highlightedCode=e.element.innerHTML}})));
+
+/*
+ * This is NOT part of the prism.js main script, it's specific to Dokka.
+ * Dokka generates <br> tags for new lines inside <pre> blocks and it works visually,
+ * but it causes prism.js to incorrectly parse some tags (such as inline comments)
+ *
+ * This can be removed if there are no `<br>` tags inside `<pre>` anymore, but
+ * if there still are - DO NOT remove this hook when upading prism.js to a newer version
+ */
+Prism.hooks.add('before-sanity-check', function (env){env.element.innerHTML = env.element.innerHTML.replace(/<br>/g, '\n');env.code = env.element.textContent;});
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/symbol-parameters-wrapper_deferred.js b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/symbol-parameters-wrapper_deferred.js
new file mode 100644
index 00000000..7ecae7a6
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/scripts/symbol-parameters-wrapper_deferred.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+// helps with some corner cases where <wbr> starts working already,
+// but the signature is not yet long enough to be wrapped
+(function() {
+ const leftPaddingPx = 60;
+
+ function createNbspIndent() {
+ let indent = document.createElement("span");
+ indent.append(document.createTextNode("\u00A0\u00A0\u00A0\u00A0"));
+ indent.classList.add("nbsp-indent");
+ return indent;
+ }
+
+ function wrapSymbolParameters(entry) {
+ const symbol = entry.target;
+ const symbolBlockWidth = entry.borderBoxSize && entry.borderBoxSize[0] && entry.borderBoxSize[0].inlineSize;
+
+ // Even though the script is marked as `defer` and we wait for `DOMContentLoaded` event,
+ // or if this block is a part of hidden tab, it can happen that `symbolBlockWidth` is 0,
+ // indicating that something hasn't been loaded.
+ // In this case, observer will be triggered onсe again when it will be ready.
+ if (symbolBlockWidth > 0) {
+ const node = symbol.querySelector(".parameters");
+
+ if (node) {
+ // if window resize happened and observer was triggered, reset previously wrapped
+ // parameters as they might not need wrapping anymore, and check again
+ node.classList.remove("wrapped");
+ node.querySelectorAll(".parameter .nbsp-indent")
+ .forEach(indent => indent.remove());
+
+ const innerTextWidth = Array.from(symbol.children)
+ .filter(it => !it.classList.contains("block")) // blocks are usually on their own (like annotations), so ignore it
+ .map(it => it.getBoundingClientRect().width)
+ .reduce((a, b) => a + b, 0);
+
+ // if signature text takes up more than a single line, wrap params for readability
+ if (innerTextWidth > (symbolBlockWidth - leftPaddingPx)) {
+ node.classList.add("wrapped");
+ node.querySelectorAll(".parameter").forEach(param => {
+ // has to be a physical indent so that it can be copied. styles like
+ // paddings and `::before { content: " " }` do not work for that
+ param.prepend(createNbspIndent());
+ });
+ }
+ }
+ }
+ }
+
+ const symbolsObserver = new ResizeObserver(entries => entries.forEach(wrapSymbolParameters));
+
+ function initHandlers() {
+ document.querySelectorAll("div.symbol").forEach(symbol => symbolsObserver.observe(symbol));
+ }
+
+ if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', initHandlers);
+ else initHandlers();
+
+ // ToDo: Add `unobserve` if dokka will be SPA-like:
+ // https://github.com/w3c/csswg-drafts/issues/5155
+})();
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/font-jb-sans-auto.css b/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/font-jb-sans-auto.css
new file mode 100644
index 00000000..bdc68723
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/font-jb-sans-auto.css
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+/* Light weight */
+@font-face {
+ font-family: 'JetBrains Sans';
+ src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Light.woff2') format('woff2'), url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Light.woff') format('woff');
+ font-weight: 300;
+ font-style: normal;
+}
+/* Regular weight */
+@font-face {
+ font-family: 'JetBrains Sans';
+ src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Regular.woff2') format('woff2'), url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Regular.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+}
+/* SemiBold weight */
+@font-face {
+ font-family: 'JetBrains Sans';
+ src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-SemiBold.woff2') format('woff2'), url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-SemiBold.woff') format('woff');
+ font-weight: 600;
+ font-style: normal;
+}
+
+@supports (font-variation-settings: normal) {
+ @font-face {
+ font-family: 'JetBrains Sans';
+ src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans.woff2') format('woff2 supports variations'),
+ url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans.woff2') format('woff2-variations'),
+ url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans.woff') format('woff-variations');
+ font-weight: 100 900;
+ font-style: normal;
+ }
+}
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/logo-styles.css b/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/logo-styles.css
new file mode 100644
index 00000000..69804e46
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/logo-styles.css
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+:root {
+ --dokka-logo-image-url: url('../images/logo-icon.svg');
+ --dokka-logo-height: 50px;
+ --dokka-logo-width: 50px;
+}
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/prism.css b/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/prism.css
new file mode 100644
index 00000000..2d3a091e
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/prism.css
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+/*
+ * Custom Dokka styles
+ */
+code .token {
+ white-space: pre;
+}
+
+/**
+ * Styles based on webhelp's prism.js styles
+ * Changes:
+ * - Since webhelp's styles are in .pcss, they use nesting which is not achievable in native CSS
+ * so nested css blocks have been unrolled (like dark theme).
+ * - Webhelp uses "Custom Class" prism.js plugin, so all of their prism classes are prefixed with "--prism".
+ * Dokka doesn't seem to need this plugin at the moment, so all "--prism" prefixes have been removed.
+ * - Removed all styles related to `pre` and `code` tags. Kotlinlang's resulting styles are so spread out and complicated
+ * that it's difficult to gather in one place. Instead use code styles defined in the main Dokka styles,
+ * which at the moment looks fairly similar.
+ *
+ * Based on prism.js default theme
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+ color: #8c8c8c;
+}
+
+.token.punctuation {
+ color: #999;
+}
+
+.token.namespace {
+ opacity: 0.7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+ color: #871094;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+ color: #067d17;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+ color: #9a6e3a;
+ /* This background color was intended by the author of this theme. */
+ background: hsla(0, 0%, 100%, 0.5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+ font-size: inherit; /* to override .keyword */
+ color: #0033b3;
+}
+
+.token.function {
+ color: #00627a;
+}
+
+.token.class-name {
+ color: #000000;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+ color: #871094;
+}
+
+.token.important,
+.token.bold {
+ font-weight: bold;
+}
+.token.italic {
+ font-style: italic;
+}
+
+.token.entity {
+ cursor: help;
+}
+
+.token.operator {
+ background: none;
+}
+
+/*
+ * DARK THEME
+ */
+:root.theme-dark .token.comment,
+:root.theme-dark .token.prolog,
+:root.theme-dark .token.cdata {
+ color: #808080;
+}
+
+:root.theme-dark .token.delimiter,
+:root.theme-dark .token.boolean,
+:root.theme-dark .token.keyword,
+:root.theme-dark .token.selector,
+:root.theme-dark .token.important,
+:root.theme-dark .token.atrule {
+ color: #cc7832;
+}
+
+:root.theme-dark .token.operator,
+:root.theme-dark .token.punctuation,
+:root.theme-dark .token.attr-name {
+ color: #a9b7c6;
+}
+
+:root.theme-dark .token.tag,
+:root.theme-dark .token.tag .punctuation,
+:root.theme-dark .token.doctype,
+:root.theme-dark .token.builtin {
+ color: #e8bf6a;
+}
+
+:root.theme-dark .token.entity,
+:root.theme-dark .token.number,
+:root.theme-dark .token.symbol {
+ color: #6897bb;
+}
+
+:root.theme-dark .token.property,
+:root.theme-dark .token.constant,
+:root.theme-dark .token.variable {
+ color: #9876aa;
+}
+
+:root.theme-dark .token.string,
+:root.theme-dark .token.char {
+ color: #6a8759;
+}
+
+:root.theme-dark .token.attr-value,
+:root.theme-dark .token.attr-value .punctuation {
+ color: #a5c261;
+}
+
+:root.theme-dark .token.attr-value .punctuation:first-child {
+ color: #a9b7c6;
+}
+
+:root.theme-dark .token.url {
+ text-decoration: underline;
+
+ color: #287bde;
+ background: transparent;
+}
+
+:root.theme-dark .token.function {
+ color: #ffc66d;
+}
+
+:root.theme-dark .token.regex {
+ background: #364135;
+}
+
+:root.theme-dark .token.deleted {
+ background: #484a4a;
+}
+
+:root.theme-dark .token.inserted {
+ background: #294436;
+}
+
+:root.theme-dark .token.class-name {
+ color: #a9b7c6;
+}
+
+:root.theme-dark .token.function {
+ color: #ffc66d;
+}
+
+:root.theme-darkcode .language-css .token.property,
+:root.theme-darkcode .language-css,
+:root.theme-dark .token.property + .token.punctuation {
+ color: #a9b7c6;
+}
+
+code.language-css .token.id {
+ color: #ffc66d;
+}
+
+:root.theme-dark code.language-css .token.selector > .token.class,
+:root.theme-dark code.language-css .token.selector > .token.attribute,
+:root.theme-dark code.language-css .token.selector > .token.pseudo-class,
+:root.theme-dark code.language-css .token.selector > .token.pseudo-element {
+ color: #ffc66d;
+}
+
+:root.theme-dark .language-plaintext .token {
+ /* plaintext code should be colored as article text */
+ color: inherit !important;
+}
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/style.css b/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/style.css
new file mode 100644
index 00000000..62b0ddbd
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/styles/style.css
@@ -0,0 +1,1513 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+@import url('./font-jb-sans-auto.css');
+@import url('https://fonts.googleapis.com/css?family=JetBrains+Mono');
+
+/* --- root styles --- */
+:root {
+ --default-gray: #f4f4f4;
+ --default-font-color: black;
+ --header-font-color: var(--default-font-color);
+
+ --breadcrumb-font-color: #637282;
+ --breadcrumb-margin: 24px;
+ --hover-link-color: #5B5DEF;
+
+ --footer-height: 64px;
+ --footer-padding-top: 48px;
+ --footer-background: var(--default-gray);
+ --footer-font-color: var(--average-color);
+ --footer-go-to-top-color: white;
+
+ --horizontal-spacing-for-content: 16px;
+ --bottom-spacing: 16px;
+ --color-scrollbar: rgba(39, 40, 44, 0.40);
+ --color-scrollbar-track: var(--default-gray);
+ --default-white: #fff;
+ --background-color: var(--default-white);
+ --dark-mode-and-search-icon-color: var(--default-white);
+ --color-dark: #27282c;
+ --default-font-family: JetBrains Sans, Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI,Roboto, Oxygen, Ubuntu,Cantarell, Droid Sans, Helvetica Neue, Arial, sans-serif;
+ --default-monospace-font-family: JetBrains Mono, SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace;
+ --default-font-size: 15px;
+ --average-color: var(--color-dark);
+ --brief-color: var(--average-color);
+ --copy-icon-color: rgba(39, 40, 44, .7);
+ --copy-icon-hover-color: var(--color-dark);
+ --code-background: rgba(39, 40, 44, .05);
+ --border-color: rgba(39, 40, 44, .2);
+ --navigation-highlight-color: rgba(39, 40, 44, 0.05);
+ --top-navigation-height: 73px;
+ --max-width: 1160px;
+ --white-10: hsla(0, 0%, 100%, .1);
+
+ --active-tab-border-color: #7F52FF;
+ --inactive-tab-border-color: rgba(164, 164, 170, 0.7);
+
+ --active-section-color: #7F52FF;
+ --inactive-section-color: rgba(25, 25, 28, .7);
+
+ --sidebar-width: 280px;
+ --sidemenu-section-active-color: #7F52FF;
+}
+
+html {
+ height: 100%;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+ scrollbar-color: rgba(39, 40, 44, 0.40) #F4F4F4;
+ scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-track);
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ color: var(--default-font-color);
+}
+
+html ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+html ::-webkit-scrollbar-track {
+ background-color: var(--color-scrollbar-track);
+}
+
+html ::-webkit-scrollbar-thumb {
+ width: 8px;
+ border-radius: 6px;
+ background: rgba(39, 40, 44, 0.40);
+ background: var(--color-scrollbar);
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+}
+/* /--- root styles --- */
+
+/* --- global tags styles --- */
+body, table {
+ background: var(--background-color);
+ font-family: var(--default-font-family);
+ font-style: normal;
+ font-weight: normal;
+ font-size: var(--default-font-size);
+ line-height: 1.6;
+ margin: 0;
+}
+
+h1 {
+ font-size: 40px;
+ line-height: 48px;
+ letter-spacing: -1px;
+}
+
+h2 {
+ font-size: 31px;
+ line-height: 40px;
+ letter-spacing: -0.5px;
+}
+
+h3 {
+ font-size: 20px;
+ line-height: 28px;
+ letter-spacing: -0.2px;
+}
+
+p, ul, ol, table, pre, dl {
+ margin: 0;
+}
+
+a {
+ text-decoration: none;
+}
+
+u {
+ text-decoration: none;
+ padding-bottom: 2px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+blockquote {
+ border-left: 1ch solid var(--default-gray);
+ margin: 0;
+ padding-left: 1ch;
+ font-style: italic;
+ color: var(--average-color);
+}
+
+.theme-dark blockquote {
+ color: var(--default-font-color);
+ border-left-color: var(--code-background);
+}
+
+pre {
+ display: block;
+}
+
+dt {
+ color: #444;
+ font-weight: 530;
+}
+
+img {
+ max-width: 100%;
+}
+
+small {
+ font-size: 11px;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ padding: 5px;
+}
+
+th, td {
+ padding: 12px 10px 11px;
+ text-align: left;
+ vertical-align: top;
+}
+
+tbody > tr {
+ min-height: 56px;
+}
+
+td:first-child {
+ width: 20vw;
+}
+/* /--- global tags styles --- */
+
+/* --- utils classes --- */
+.w-100 {
+ width: 100%;
+}
+
+.no-gutters {
+ margin: 0;
+ padding: 0;
+}
+
+.d-flex {
+ display: flex;
+}
+
+.floating-right {
+ float: right;
+}
+
+.pull-right {
+ float: right;
+ margin-left: auto
+}
+
+.clearfix::after {
+ display: block;
+ content: '';
+ clear: both;
+ height: 0;
+}
+/* /--- utils classes --- */
+
+/* ---dark theme --- */
+.theme-dark {
+ --background-color: #27282c;
+ --color-dark: #3d3d41;
+ --default-font-color: hsla(0, 0%, 100%, 0.8);
+ --border-color: hsla(0, 0%, 100%, 0.2);
+ --code-background: hsla(0, 0%, 100%, 0.05);
+ --breadcrumb-font-color: #8c8c8e;
+ --brief-color: hsla(0, 0%, 100%, 0.4);
+ --copy-icon-color: hsla(0, 0%, 100%, 0.6);
+ --copy-icon-hover-color: #fff;
+
+ --active-tab-border-color: var(--default-font-color);
+ --inactive-tab-border-color: hsla(0, 0%, 100%, 0.4);
+
+ --active-section-color: var(--default-font-color);
+ --inactive-section-color: hsla(0, 0%, 100%, 0.4);
+
+ --navigation-highlight-color: rgba(255, 255, 255, 0.05);
+ --footer-background: hsla(0, 0%, 100%, 0.05);
+ --footer-font-color: hsla(0, 0%, 100%, 0.6);
+ --footer-go-to-top-color: var(--footer-font-color);
+
+ --sidemenu-section-active-color: var(--color-dark);
+}
+/* /---dark theme --- */
+
+.root {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+/* --- Navigation styles --- */
+.navigation {
+ display: flex;
+ justify-content: space-between;
+
+ color: #fff;
+ background-color: var(--color-dark);
+ font-family: var(--default-font-family);
+ letter-spacing: -0.1px;
+
+ /* Reset margin and use padding for border */
+ margin-left: 0;
+ margin-right: 0;
+ padding: 10px var(--horizontal-spacing-for-content);
+
+ z-index: 4;
+}
+
+.navigation--inner {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ flex: 1 1 auto;
+}
+
+.navigation--inner, .navigation-title {
+ min-height: 40px;
+}
+
+.navigation-title, .filter-section {
+ align-items: center;
+}
+
+.navigation-title {
+ display: flex;
+ align-items: center;
+}
+
+/* --- Navigation MENU --- */
+.menu-toggle {
+ color: var(--background-color);
+ line-height: 0;
+ font-size: 0;
+ text-indent: -9999px;
+
+ background: transparent;
+ border: none;
+ padding: 0;
+ margin-right: 16px;
+ outline: none;
+
+ transition: margin .2s ease-out;
+ z-index: 5;
+}
+
+@media (min-width: 760px) {
+ .menu-toggle {
+ display: none;
+ }
+}
+
+.menu-toggle::before {
+ display: block;
+ content: '';
+ background: url('../images/burger.svg') no-repeat center;
+ height: 28px;
+ width: 28px;
+}
+/* /--- Navigation MENU --- */
+
+.library-version {
+ position: relative;
+ top: -4px;
+ margin-left: 3px;
+
+ color: rgba(255,255,255,.7);
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 16px;
+}
+
+.filter-section {
+ z-index: 0;
+}
+
+.no-js .filter-section {
+ display: none;
+}
+
+@media (min-width: 760px) {
+ .filter-section {
+ padding: 5px 0 5px;
+ }
+}
+/* --- Navigation controls --- */
+.navigation-controls {
+ display: flex;
+ margin-left: 4px;
+}
+
+@media (min-width: 760px) {
+ .navigation-controls {
+ align-items: center;
+ }
+}
+
+.no-js .navigation-controls {
+ display: none;
+}
+
+/* --- Navigation THEME --- */
+.navigation-controls--search {
+ display: inline-flex;
+ font-size: 0;
+ line-height: 0;
+}
+
+.navigation-controls--theme {
+ display: block;
+ border-radius: 50%;
+ background-color: inherit;
+ padding: 0;
+ border: none;
+ cursor: pointer;
+ font-size: 0;
+ line-height: 0;
+}
+
+.navigation-controls--theme::before {
+ height: 40px;
+ width: 40px;
+}
+
+.navigation-controls--theme:hover {
+ background: var(--white-10);
+}
+
+.navigation-controls--theme::before {
+ display: block;
+ content: url("../images/theme-toggle.svg");
+}
+
+@media (max-width: 759px) {
+ .navigation-controls--theme {
+ display: none;
+ }
+}
+/* /--- Navigation THEME --- */
+
+/* --- Navigation HOMEPAGE --- */
+.navigation-controls--homepage {
+ height: 40px;
+ width: 40px;
+ display: block;
+ border-radius: 50%;
+ cursor: pointer;
+}
+
+.navigation-controls--homepage a::before {
+ height: 100%;
+ width: 20px;
+ margin-left: 10px;
+ display: block;
+ content: "";
+ background: url("../images/homepage.svg");
+ background-size: 100% 100%;
+}
+
+.navigation-controls--homepage:hover {
+ background: var(--white-10);
+}
+
+@media (max-width: 759px) {
+ .navigation-controls--homepage {
+ display: none;
+ }
+}
+/* /--- Navigation HOMEPAGE --- */
+
+.navigation .platform-selector:not([data-active]) {
+ color: #fff;
+}
+/* /--- Navigation controls --- */
+/* /--- Navigation styles --- */
+
+/* --- Layout styles --- */
+
+#container {
+ display: flex;
+ flex: 1 1 auto;
+ min-height: 0; /* full height exclude header */
+}
+
+#container > .sidebar, #container > #main {
+ overflow: auto;
+}
+
+#main {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0; /* full width, but no affects for sidebar */
+}
+
+.sidebar {
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ border-right: 1px solid var(--border-color);
+ width: var(--sidebar-width);
+}
+
+.no-js .sidebar {
+ display: none;
+}
+
+@media (max-width: 759px) {
+ #container {
+ position: relative;
+ }
+
+ .sidebar {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ box-sizing: border-box;
+ background: var(--background-color);
+ margin-left: calc(-1 * var(--sidebar-width));
+ transition: margin .2s ease-out;
+ z-index: 4;
+ }
+
+ .sidebar.open {
+ margin-left: 0;
+ }
+
+ .sidebar.open ~ #main .navigation-controls--search {
+ display: none;
+ }
+
+ .sidebar.open ~ #main .menu-toggle {
+ margin-left: var(--sidebar-width);
+ }
+}
+
+.sidebar--inner {
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ padding-top: 22px;
+ padding-bottom: 16px;
+}
+/* /--- Layout styles --- */
+
+/* --- Main Content styles --- */
+.main-content {
+ padding-bottom: var(--bottom-spacing);
+ margin-left: auto;
+ margin-right: auto;
+ max-width: var(--max-width);
+ width: 100%;
+ z-index: 0;
+}
+
+.main-content > * {
+ margin-left: var(--horizontal-spacing-for-content);
+ margin-right: var(--horizontal-spacing-for-content);
+}
+
+.main-content .content > hr {
+ margin: 30px 0;
+ border-top: 3px double #8c8b8b;
+}
+
+.main-content :is(h1, h2) {
+ font-weight: 530;
+}
+/* /--- Main Content styles --- */
+
+/* /--- Breadcrumbs styles --- */
+.breadcrumbs, .breadcrumbs a, .breadcrumbs a:hover {
+ margin-top: var(--breadcrumb-margin);
+ color: var(--breadcrumb-font-color);
+ overflow-wrap: break-word;
+}
+
+.breadcrumbs .delimiter {
+ margin: auto 2px;
+}
+
+.breadcrumbs .current {
+ color: var(--default-font-color);
+}
+/* /--- Breadcrumbs styles --- */
+
+.tabs-section,
+.platform-hinted > .platform-bookmarks-row {
+ margin-left: -8px;
+ margin-right: -8px;
+}
+
+.section-tab,
+.platform-hinted > .platform-bookmarks-row > .platform-bookmark {
+ border: 0;
+ padding: 11px 3px;
+ margin: 0 8px;
+ cursor: pointer;
+ outline: none;
+ font-size: var(--default-font-size);
+ background-color: transparent;
+ color: var(--inactive-section-color);
+ border-bottom: 1px solid var(--inactive-tab-border-color);
+}
+
+.platform-hinted > .platform-bookmarks-row {
+ margin-bottom: 16px;
+}
+
+.no-js .platform-bookmarks-row + .sourceset-dependent-content {
+ margin-top: 8px;
+}
+
+.no-js .platform-bookmarks-row + .sourceset-dependent-content:last-of-type {
+ margin-top: 0;
+}
+
+.section-tab:hover {
+ color: var(--default-font-color);
+ border-bottom: 2px solid var(--default-font-color);
+}
+
+.section-tab[data-active=''] {
+ color: var(--active-section-color);
+ border-bottom: 2px solid var(--active-tab-border-color);
+}
+
+.tabs-section-body > div {
+ margin-top: 12px;
+}
+
+.tabs-section-body .with-platform-tabs {
+ padding-top: 12px;
+ padding-bottom: 12px;
+}
+
+.cover > .platform-hinted {
+ padding-bottom: 12px;
+}
+
+.cover {
+ display: flex;
+ flex-direction: column;
+}
+
+.cover .platform-hinted.with-platform-tabs .sourceset-dependent-content > .block ~ .symbol {
+ padding-top: 16px;
+ padding-left: 0;
+}
+
+.cover .sourceset-dependent-content > .block {
+ padding: 16px 0;
+ font-size: 18px;
+ line-height: 28px;
+}
+
+.cover .platform-hinted.with-platform-tabs .sourceset-dependent-content > .block {
+ padding: 0;
+ font-size: var(--default-font-size);
+}
+
+.cover ~ .divergent-group {
+ margin-top: 24px;
+ padding: 24px 8px 8px 8px;
+}
+
+.cover ~ .divergent-group .main-subrow .symbol {
+ width: 100%;
+}
+
+.main-content p.paragraph,
+.sample-container, blockquote,
+.content > .symbol {
+ margin-top: 8px;
+}
+
+blockquote,
+.content > .symbol:first-of-type,
+p.paragraph:first-child,
+.brief p.paragraph {
+ margin-top: 0;
+}
+
+.content .kdoc-tag > p.paragraph {
+ margin-top: 0;
+}
+
+.content h4 {
+ margin-bottom: 0;
+}
+
+.divergent-group {
+ background-color: var(--background-color);
+ padding: 16px 0 8px 0;
+ margin-bottom: 2px;
+}
+
+.divergent-group .table-row, tbody > tr {
+ border-bottom: 1px solid var(--border-color);
+}
+
+.divergent-group .table-row:last-of-type, tbody > tr:last-of-type {
+ border-bottom: none;
+}
+
+.title > .divergent-group:first-of-type {
+ padding-top: 0;
+}
+
+.sample-container, div.CodeMirror {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+code.paragraph {
+ display: block;
+}
+
+.overview > .navButton {
+ position: absolute;
+ align-items: center;
+ display: flex;
+ justify-content: flex-end;
+ padding: 2px 2px 2px 0;
+ margin-right: 5px;
+ cursor: pointer;
+}
+
+.strikethrough {
+ text-decoration: line-through;
+}
+
+.symbol:empty {
+ padding: 0;
+}
+
+.symbol:not(.token), code {
+ background-color: var(--code-background);
+ align-items: center;
+ box-sizing: border-box;
+ white-space: pre-wrap;
+ font-family: var(--default-monospace-font-family);
+ font-size: var(--default-font-size);
+}
+
+.symbol:not(.token), code.block {
+ display: block;
+ padding: 12px 32px 12px 12px;
+ border-radius: 8px;
+ line-height: 24px;
+ position: relative;
+}
+
+code {
+ overflow-x: auto;
+ max-width: 100%;
+}
+
+code:not(.block) {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.symbol > a {
+ color: var(--hover-link-color);
+}
+
+.copy-icon {
+ cursor: pointer;
+}
+
+.sample-container span.copy-icon {
+ display: none;
+}
+
+.js .sample-container:hover span.copy-icon {
+ display: inline-block;
+}
+
+.sample-container span.copy-icon::before {
+ width: 24px;
+ height: 24px;
+ display: inline-block;
+ content: '';
+ /* masks are required if you want to change color of the icon dynamically instead of using those provided with the SVG */
+ -webkit-mask: url("../images/copy-icon.svg") no-repeat 50% 50%;
+ mask: url("../images/copy-icon.svg") no-repeat 50% 50%;
+ -webkit-mask-size: cover;
+ mask-size: cover;
+ background-color: var(--copy-icon-color);
+}
+
+.sample-container span.copy-icon:hover::before {
+ background-color: var(--copy-icon-hover-color);
+}
+
+.copy-popup-wrapper {
+ display: none;
+ align-items: center;
+ position: absolute;
+ z-index: 1000;
+ background: var(--background-color);
+ font-weight: normal;
+ font-family: var(--default-font-family);
+ width: max-content;
+ font-size: var(--default-font-size);
+ cursor: default;
+ border: 1px solid #D8DCE1;
+ box-sizing: border-box;
+ box-shadow: 0 5px 10px var(--ring-popup-shadow-color);
+ border-radius: 3px;
+ color: var(--default-font-color);
+}
+
+.copy-popup-wrapper > .copy-popup-icon::before {
+ content: url("../images/copy-successful-icon.svg");
+ padding: 8px;
+}
+
+.copy-popup-wrapper > .copy-popup-icon {
+ position: relative;
+ top: 3px;
+}
+
+.copy-popup-wrapper.popup-to-left {
+ /* since it is in position absolute we can just move it to the left to make it always appear on the left side of the icon */
+ left: -15em;
+}
+
+.table-row:hover .copy-popup-wrapper.active-popup,
+.sample-container:hover .copy-popup-wrapper.active-popup {
+ display: flex !important;
+}
+
+.copy-popup-wrapper:hover {
+ font-weight: normal;
+}
+
+.copy-popup-wrapper > span:last-child {
+ padding-right: 14px;
+}
+
+.symbol .top-right-position, .sample-container .top-right-position {
+ /* it is important for a parent to have a position: relative */
+ position: absolute;
+ top: 8px;
+ right: 8px;
+}
+
+.sideMenuPart > .overview {
+ display: flex;
+ align-items: center;
+ position: relative;
+ user-select: none; /* there's a weird bug with text selection */
+ padding: 8px 0;
+}
+
+.sideMenuPart a {
+ display: block;
+ align-items: center;
+ color: var(--default-font-color);
+ overflow: hidden;
+ padding-left: 23px;
+}
+
+.sideMenuPart a:hover {
+ text-decoration: none;
+ color: var(--default-font-color);
+}
+
+.sideMenuPart > .overview:before {
+ box-sizing: border-box;
+ content: '';
+ top: 0;
+ width: var(--sidebar-width);
+ right: 0;
+ bottom: 0;
+ position: absolute;
+ z-index: -1;
+}
+
+.overview:hover:before {
+ background-color: var(--navigation-highlight-color);
+}
+
+#nav-submenu {
+ padding-left: 24px;
+}
+
+.sideMenuPart {
+ padding-left: 12px;
+ box-sizing: border-box;
+}
+
+.sideMenuPart.hidden > .overview .navButtonContent::before {
+ transform: rotate(0deg);
+}
+
+.sideMenuPart > .overview .navButtonContent::before {
+ content: '';
+
+ -webkit-mask: url("../images/arrow_down.svg") no-repeat 50% 50%;
+ mask: url("../images/arrow_down.svg") no-repeat 50% 50%;
+ -webkit-mask-size: cover;
+ mask-size: cover;
+ background-color: var(--default-font-color);
+
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ transform: rotate(90deg);
+ width: 16px;
+ height: 16px;
+}
+
+.sideMenuPart[data-active] > .overview .navButtonContent::before {
+ background-color: var(--default-white);
+}
+
+.sideMenuPart.hidden > .navButton .navButtonContent::after {
+ content: '\02192';
+}
+
+.sideMenuPart.hidden > .sideMenuPart {
+ display: none;
+}
+
+.overview .nav-link-grid {
+ display: grid;
+ grid-template-columns: 16px auto; /* first is the icon, then name */
+ grid-gap: 6px;
+ align-items: center;
+}
+
+.nav-icon {
+ width: 16px;
+ height: 16px;
+}
+
+.nav-icon.class::before {
+ content: url("../images/nav-icons/class.svg");
+}
+
+.nav-icon.class-kt::before {
+ content: url("../images/nav-icons/class-kotlin.svg");
+}
+
+.nav-icon.function::before {
+ content: url("../images/nav-icons/function.svg");
+}
+
+.nav-icon.enum-class::before {
+ content: url("../images/nav-icons/enum.svg");
+}
+
+.nav-icon.enum-class-kt::before {
+ content: url("../images/nav-icons/enum-kotlin.svg");
+}
+
+.nav-icon.annotation-class::before {
+ content: url("../images/nav-icons/annotation.svg");
+}
+
+.nav-icon.annotation-class-kt::before {
+ content: url("../images/nav-icons/annotation-kotlin.svg");
+}
+
+.nav-icon.abstract-class::before {
+ content: url("../images/nav-icons/abstract-class.svg");
+}
+
+.nav-icon.abstract-class-kt::before {
+ content: url("../images/nav-icons/abstract-class-kotlin.svg");
+}
+
+.nav-icon.exception-class::before {
+ content: url("../images/nav-icons/exception-class.svg");
+}
+
+.nav-icon.interface::before {
+ content: url("../images/nav-icons/interface.svg");
+}
+
+.nav-icon.interface-kt::before {
+ content: url("../images/nav-icons/interface-kotlin.svg");
+}
+
+.nav-icon.object::before {
+ content: url("../images/nav-icons/object.svg");
+}
+
+.nav-icon.typealias-kt::before {
+ content: url("../images/nav-icons/typealias-kotlin.svg");
+}
+
+.nav-icon.val::before {
+ content: url("../images/nav-icons/field-value.svg");
+}
+
+.nav-icon.var::before {
+ content: url("../images/nav-icons/field-variable.svg");
+}
+
+.filtered > a, .filtered > .navButton {
+ display: none;
+}
+
+
+.brief {
+ white-space: pre-wrap;
+ overflow: hidden;
+}
+
+h1.cover {
+ font-size: 52px;
+ line-height: 56px;
+ letter-spacing: -1.5px;
+ margin-bottom: 0;
+ padding-bottom: 32px;
+ display: block;
+}
+
+@media (max-width: 1119px) {
+ h1.cover {
+ font-size: 48px;
+ line-height: 48px;
+ padding-bottom: 8px;
+ }
+}
+
+@media (max-width: 759px) {
+ h1.cover {
+ font-size: 32px;
+ line-height: 32px;
+ }
+}
+
+.UnderCoverText {
+ font-size: 16px;
+ line-height: 28px;
+}
+
+.UnderCoverText code {
+ font-size: inherit;
+}
+
+.UnderCoverText table {
+ margin: 8px 0 8px 0;
+ word-break: break-word;
+}
+
+@media (max-width: 960px) {
+ .UnderCoverText table {
+ display: block;
+ word-break: normal;
+ overflow: auto;
+ }
+}
+
+.main-content a:not([data-name]) {
+ padding-bottom: 2px;
+ border-bottom: 1px solid var(--border-color);
+ cursor: pointer;
+ text-decoration: none;
+ color: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ transition: color .1s, border-color .1s;
+}
+
+.main-content a:hover {
+ border-bottom-color: unset;
+ color: inherit
+}
+
+a small {
+ font-size: 11px;
+ margin-top: -0.6em;
+ display: block;
+}
+
+p.paragraph img {
+ display: block;
+}
+
+.deprecation-content {
+ margin: 20px 10px;
+ border:1px solid var(--border-color);
+ padding: 13px 15px 16px 15px;
+}
+
+.deprecation-content > h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.deprecation-content > h4 {
+ font-size: 16px;
+ margin-top: 15px;
+ margin-bottom: 0;
+}
+
+.deprecation-content code.block {
+ padding: 5px 10px;
+ display: inline-block;
+}
+
+.deprecation-content .footnote {
+ margin-left: 25px;
+ font-size: 13px;
+ font-weight: bold;
+ display: block;
+}
+
+.deprecation-content .footnote > p {
+ margin: 0;
+}
+
+[data-filterable-current=''] {
+ display: none !important;
+}
+
+.platform-tags, .filter-section {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: -8px;
+ margin-left: -4px;
+}
+
+.platform-tag {
+ --platform-tag-color: #bababb;
+ border: 0 none;
+ margin-right: 4px;
+ margin-bottom: 8px;
+
+ font-family: var(--default-font-family);
+ font-size: 13px;
+ line-height: 1.5;
+ text-transform: capitalize;
+}
+
+.platform-tag.js-like, .platform-tag.jvm-like, .platform-tag.wasm-like {
+ text-transform: uppercase;
+}
+
+.filter-section .platform-tag {
+ cursor: pointer;
+ border-radius: 4px;
+ padding: 2px 16px;
+}
+
+.filter-section .platform-tag.jvm-like[data-active], .platform-tags .platform-tag.jvm-like {
+ --platform-tag-color: #4dbb5f;
+}
+
+.filter-section .platform-tag.js-like[data-active], .platform-tags .platform-tag.js-like {
+ --platform-tag-color: #ffc700;
+}
+
+.filter-section .platform-tag.native-like[data-active], .platform-tags .platform-tag.native-like {
+ --platform-tag-color: #E082F3;
+}
+
+.filter-section .platform-tag.wasm-like[data-active], .platform-tags .platform-tag.wasm-like {
+ --platform-tag-color: #9585F9;
+}
+
+.filter-section .platform-tag[data-active]:hover {
+ color: #fff;
+ background-color: rgba(186, 186, 187, .7);
+}
+
+.filter-section .platform-tag:not([data-active]) {
+ color: #fff;
+ /* Safari doesn't work correctly for `outline` with `border-radius` */
+ /* outline: 1px solid rgba(255,255,255,.6); */
+ /* ...use `box-shadow` instead: */
+ box-shadow: 0 0 0 1px rgb(255 255 255 / 60%);
+ background-color: rgba(255,255,255,.05);
+}
+
+.filter-section .platform-tag[data-active] {
+ color: #19191c;
+ background-color: var(--platform-tag-color);
+}
+
+.platform-tags .platform-tag {
+ display: flex;
+ align-items: center;
+}
+
+.platform-tags .platform-tag::before {
+ display: inline-block;
+ content: '';
+ border-radius: 50%;
+ background: var(--platform-tag-color);
+ margin: 0 4px 0 8px;
+ height: 8px;
+ width: 8px;
+
+ font-size: 13px;
+ line-height: 1.6;
+}
+
+td.content {
+ padding-left: 24px;
+ padding-top: 16px;
+ display: flex;
+ flex-direction: column;
+}
+
+.main-subrow {
+ display: flex;
+ flex-direction: row;
+ padding: 0;
+ flex-wrap: wrap;
+}
+
+.main-subrow > div {
+ margin-bottom: 8px;
+}
+
+.main-subrow > div > span {
+ display: flex;
+ position: relative;
+}
+
+.js .main-subrow:hover .anchor-icon {
+ opacity: 1;
+ transition: 0.2s;
+}
+
+.main-subrow .anchor-icon {
+ opacity: 0;
+ transition: 0.2s 0.5s;
+}
+
+.main-subrow .anchor-icon::before {
+ content: url("../images/anchor-copy-button.svg");
+}
+
+.main-subrow .anchor-icon:hover {
+ cursor: pointer;
+}
+
+.main-subrow .anchor-icon:hover > svg path {
+ fill: var(--hover-link-color);
+}
+
+@media (hover: none) {
+ .main-subrow .anchor-icon {
+ display: none;
+ }
+}
+
+.main-subrow .anchor-wrapper {
+ position: relative;
+ width: 24px;
+ height: 16px;
+ margin-left: 3px;
+}
+
+.inline-flex {
+ display: inline-flex;
+}
+
+.platform-hinted {
+ flex: auto;
+ display: block;
+}
+
+.platform-hinted > .platform-bookmarks-row > .platform-bookmark {
+ min-width: 64px;
+ background: inherit;
+ flex: none;
+ order: 5;
+ align-self: flex-start;
+}
+
+.platform-hinted > .platform-bookmarks-row > .platform-bookmark:hover {
+ color: var(--default-font-color);
+ border-bottom: 2px solid var(--default-font-color);
+}
+
+.platform-hinted > .platform-bookmarks-row > .platform-bookmark[data-active=''] {
+ border-bottom: 2px solid var(--active-tab-border-color);
+ color: var(--active-section-color);
+}
+
+.no-js .platform-bookmarks-row, .no-js .tabs-section {
+ display: none;
+}
+
+.js .platform-hinted > .content:not([data-active]),
+.js .tabs-section-body *[data-togglable]:not([data-active]) {
+ display: none;
+}
+
+/* Work around an issue: https://github.com/JetBrains/kotlin-playground/issues/91
+Applies for main description blocks with platform tabs.
+Just in case of possible performance degradation it excluding tabs with briefs on classlike page */
+#content > div:not(.tabbedcontent) .sourceset-dependent-content:not([data-active]) {
+ display: block !important;
+ visibility: hidden;
+ height: 0;
+ position: fixed;
+ top: 0;
+}
+
+.with-platform-tags {
+ display: flex;
+}
+
+.with-platform-tags ~ .main-subrow {
+ padding-top: 8px;
+}
+
+.cover .with-platform-tabs {
+ font-size: var(--default-font-size);
+}
+
+.cover > .with-platform-tabs > .content {
+ padding: 8px 16px;
+ border: 1px solid var(--border-color);
+}
+
+.cover > .block {
+ padding-top: 48px;
+ padding-bottom: 24px;
+ font-size: 18px;
+ line-height: 28px;
+}
+
+.cover > .block:empty {
+ padding-bottom: 0;
+}
+
+.parameters.wrapped > .parameter {
+ display: block;
+}
+
+.table-row .inline-comment {
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+.table-row .platform-hinted .sourceset-dependent-content .brief,
+.table-row .platform-hinted .sourceset-dependent-content .inline-comment {
+ padding: 8px;
+}
+
+.sideMenuPart[data-active] > .overview:before {
+ background: var(--sidemenu-section-active-color);
+}
+
+.sideMenuPart[data-active] > .overview > a {
+ color: var(--default-white);
+}
+
+.table {
+ display: flex;
+ flex-direction: column;
+}
+
+.table-row {
+ display: flex;
+ flex-direction: column;
+ border-bottom: 1px solid var(--border-color);
+ padding: 11px 0 12px 0;
+ background-color: var(--background-color);
+}
+
+.table-row:last-of-type {
+ border-bottom: none;
+}
+
+.table-row .brief-comment {
+ color: var(--brief-color);
+}
+
+.platform-dependent-row {
+ display: grid;
+ padding-top: 8px;
+}
+
+.title-row {
+ display: grid;
+ grid-template-columns: auto auto 7em;
+ width: 100%;
+}
+
+@media print, (min-width: 960px) {
+ .title-row {
+ grid-template-columns: 20% auto 7em;
+ }
+}
+
+.keyValue {
+ display: grid;
+ grid-gap: 8px;
+}
+
+@media print, (min-width: 960px) {
+ .keyValue {
+ grid-template-columns: 20% 80%;
+ }
+ .keyValue > div:first-child {
+ word-break: break-word;
+ }
+}
+
+@media print, (max-width: 960px) {
+ div.wrapper {
+ width: auto;
+ margin: 0;
+ }
+
+ header, section, footer {
+ float: none;
+ position: static;
+ width: auto;
+ }
+
+ header {
+ padding-right: 320px;
+ }
+
+ section {
+ border: 1px solid #e5e5e5;
+ border-width: 1px 0;
+ padding: 20px 0;
+ margin: 0 0 20px;
+ }
+
+ header a small {
+ display: inline;
+ }
+
+ header ul {
+ position: absolute;
+ right: 50px;
+ top: 52px;
+ }
+}
+
+.anchor-highlight {
+ border: 1px solid var(--hover-link-color) !important;
+ box-shadow: 0 0 0 0.2em #c8e1ff;
+ margin-top: 0.2em;
+ margin-bottom: 0.2em;
+}
+
+.filtered-message {
+ margin: 25px;
+ font-size: 20px;
+ font-weight: bolder;
+}
+
+div.runnablesample {
+ height: fit-content;
+}
+
+/* --- footer --- */
+.footer {
+ clear: both;
+ display: flex;
+ align-items: center;
+ position: relative;
+ min-height: var(--footer-height);
+ font-size: 12px;
+ line-height: 16px;
+ letter-spacing: 0.2px;
+ color: var(--footer-font-color);
+ margin-top: auto;
+ background-color: var(--footer-background);
+}
+
+.footer span.go-to-top-icon {
+ border-radius: 2em;
+ padding: 11px 10px !important;
+ background-color: var(--footer-go-to-top-color);
+}
+
+.footer span.go-to-top-icon > a::before {
+ content: url("../images/go-to-top-icon.svg");
+}
+
+.footer > span:first-child {
+ margin-left: var(--horizontal-spacing-for-content);
+ padding-left: 0;
+}
+
+.footer > span:last-child {
+ margin-right: var(--horizontal-spacing-for-content);
+ padding-right: 0;
+}
+
+.footer > span {
+ padding: 0 16px;
+}
+
+.footer a {
+ color: var(--breadcrumb-font-color);
+}
+
+.footer span.go-to-top-icon > #go-to-top-link {
+ padding: 0;
+ border: none;
+}
+
+.footer .padded-icon {
+ padding-left: 0.5em;
+}
+
+.footer .padded-icon::before {
+ content: url("../images/footer-go-to-link.svg");
+}
+/* /--- footer --- */
+
+/* Logo styles */
+:root {
+ --dokka-logo-image-url: url('../images/logo-icon.svg');
+ --dokka-logo-height: 50px;
+ --dokka-logo-width: 50px;
+}
+
+.library-name--link {
+ display: flex;
+ align-items: center;
+ color: #fff;
+ font-weight: 530;
+}
+
+.library-name--link::before {
+ content: '';
+ background: var(--dokka-logo-image-url) center no-repeat;
+ background-size: var(--dokka-logo-height) var(--dokka-logo-width);
+ margin-right: 5px;
+ width: var(--dokka-logo-height);
+ height: var(--dokka-logo-width);
+}
+
+@media (max-width: 759px) {
+ .library-name--link::before {
+ display: none;
+ }
+}
+/* / Logo styles */
+
+/*
+the hack to hide the headers inside tabs for a package page because each tab
+has only one header, and the header text is the same as the tab name, so no point in showing it
+*/
+.main-content[data-page-type="package"] .tabs-section-body h2 {
+ display: none;
+}
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/base.ftl b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/base.ftl
new file mode 100644
index 00000000..0311f9f8
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/base.ftl
@@ -0,0 +1,44 @@
+<#import "includes/page_metadata.ftl" as page_metadata>
+<#import "includes/header.ftl" as header>
+<#import "includes/footer.ftl" as footer>
+<!DOCTYPE html>
+<html class="no-js">
+<head>
+ <meta name="viewport" content="width=device-width, initial-scale=1" charset="UTF-8">
+ <@page_metadata.display/>
+ <@template_cmd name="pathToRoot"><script>var pathToRoot = "${pathToRoot}";</script></@template_cmd>
+ <script>document.documentElement.classList.replace("no-js","js");</script>
+ <#-- This script doesn't need to be there but it is nice to have
+ since app in dark mode doesn't 'blink' (class is added before it is rendered) -->
+ <script>const storage = localStorage.getItem("dokka-dark-mode")
+ if (storage == null) {
+ const osDarkSchemePreferred = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
+ if (osDarkSchemePreferred === true) {
+ document.getElementsByTagName("html")[0].classList.add("theme-dark")
+ }
+ } else {
+ const savedDarkMode = JSON.parse(storage)
+ if(savedDarkMode === true) {
+ document.getElementsByTagName("html")[0].classList.add("theme-dark")
+ }
+ }
+ </script>
+ <#-- Resources (scripts, stylesheets) are handled by Dokka.
+ Use customStyleSheets and customAssets to change them. -->
+ <@resources/>
+</head>
+<body>
+ <div class="root">
+ <@header.display/>
+ <div id="container">
+ <div class="sidebar" id="leftColumn">
+ <div class="sidebar--inner" id="sideMenu"></div>
+ </div>
+ <div id="main">
+ <@content/>
+ <@footer.display/>
+ </div>
+ </div>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/footer.ftl b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/footer.ftl
new file mode 100644
index 00000000..461a8162
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/footer.ftl
@@ -0,0 +1,7 @@
+<#macro display>
+ <div class="footer">
+ <span class="go-to-top-icon"><a href="#content" id="go-to-top-link"></a></span><span>${footerMessage}</span><span
+ class="pull-right"><span>Generated by </span><a
+ href="https://github.com/Kotlin/dokka"><span>dokka</span><span class="padded-icon"></span></a></span>
+ </div>
+</#macro> \ No newline at end of file
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/header.ftl b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/header.ftl
new file mode 100644
index 00000000..d399e633
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/header.ftl
@@ -0,0 +1,31 @@
+<#import "source_set_selector.ftl" as source_set_selector>
+<#macro display>
+<nav class="navigation" id="navigation-wrapper">
+ <div class="navigation--inner">
+ <div class="navigation-title">
+ <button class="menu-toggle" id="menu-toggle" type="button">toggle menu</button>
+ <div class="library-name">
+ <@template_cmd name="pathToRoot">
+ <a class="library-name--link" href="${pathToRoot}index.html">
+ <@template_cmd name="projectName">
+ ${projectName}
+ </@template_cmd>
+ </a>
+ </@template_cmd>
+ </div>
+ <div class="library-version">
+ <#-- This can be handled by the versioning plugin -->
+ <@version/>
+ </div>
+ </div>
+ <@source_set_selector.display/>
+ </div>
+ <div class="navigation-controls">
+ <#if homepageLink?has_content>
+ <div class="navigation-controls--btn navigation-controls--homepage" id="homepage-link" role="button"><a href="${homepageLink}"></a></div>
+ </#if>
+ <button class="navigation-controls--btn navigation-controls--theme" id="theme-toggle-button" type="button">switch theme</button>
+ <div class="navigation-controls--btn navigation-controls--search" id="searchBar" role="button">search in API</div>
+ </div>
+</nav>
+</#macro>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/page_metadata.ftl b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/page_metadata.ftl
new file mode 100644
index 00000000..7cab4582
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/page_metadata.ftl
@@ -0,0 +1,6 @@
+<#macro display>
+ <title>${pageName}</title>
+ <@template_cmd name="pathToRoot">
+ <link href="${pathToRoot}images/logo-icon.svg" rel="icon" type="image/svg">
+ </@template_cmd>
+</#macro>
diff --git a/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/source_set_selector.ftl b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/source_set_selector.ftl
new file mode 100644
index 00000000..2d848071
--- /dev/null
+++ b/dokka-subprojects/plugin-base/src/main/resources/dokka/templates/includes/source_set_selector.ftl
@@ -0,0 +1,9 @@
+<#macro display>
+ <#if sourceSets?has_content>
+ <div class="filter-section" id="filter-section">
+ <#list sourceSets as ss>
+ <button class="platform-tag platform-selector ${ss.platform}-like" data-active="" data-filter="${ss.filter}">${ss.name}</button>
+ </#list>
+ </div>
+ </#if>
+</#macro>