From 1618e552c136e25d86bf0708e0d760841c77c139 Mon Sep 17 00:00:00 2001 From: Błażej Kardyś Date: Tue, 5 Jan 2021 17:59:38 +0100 Subject: Versioning (#1654) * Adding versioning mechanism for multimodule * Versioning improvement * Refactor configuration, add ordering * Fix integration tests * Change packages, unignore test Co-authored-by: Marcin Aman --- .../projects/it-multimodule-1/second/build.gradle | 2 +- .../it/gradle/Android0GradleIntegrationTest.kt | 1 - .../dokka/it/gradle/BasicGradleIntegrationTest.kt | 2 +- .../dokka/it/gradle/BasicGroovyIntegrationTest.kt | 2 +- plugins/all-modules-page/build.gradle.kts | 9 +- .../src/main/kotlin/AllModulesPageGeneration.kt | 12 +- .../src/main/kotlin/AllModulesPagePlugin.kt | 42 +----- .../src/main/kotlin/ExternalModuleLinkResolver.kt | 85 +++++++++++++ .../src/main/kotlin/MultimoduleLocationProvider.kt | 1 - .../src/main/kotlin/MultimodulePageCreator.kt | 10 ++ .../src/main/kotlin/ResolveLinkCommandHandler.kt | 45 +++++++ .../templates/DirectiveBasedTemplateProcessing.kt | 141 --------------------- .../kotlin/templates/ExternalModuleLinkResolver.kt | 77 ----------- .../FallbackTemplateProcessingStrategy.kt | 18 --- .../JsonElementBasedTemplateProcessingStrategy.kt | 74 ----------- .../main/kotlin/templates/PathToRootSubstitutor.kt | 14 -- .../src/main/kotlin/templates/Substitutor.kt | 7 - .../src/main/kotlin/templates/TemplateProcessor.kt | 60 --------- .../AddToNavigationCommandResolutionTest.kt | 137 -------------------- .../templates/AddToSearchCommandResolutionTest.kt | 90 ------------- .../templates/ResolveLinkCommandResolutionTest.kt | 107 ---------------- .../ResolveLinkGfmCommandResolutionTest.kt | 74 ----------- .../templates/SubstitutionCommandResolutionTest.kt | 69 ---------- .../assertHtmlEqualsIgnoringWhitespace.kt | 18 --- .../templates/mockedPackageListFactory.kt | 12 -- .../templates/ResolveLinkCommandResolutionTest.kt | 108 ++++++++++++++++ .../ResolveLinkGfmCommandResolutionTest.kt | 74 +++++++++++ .../kotlin/templates/mockedPackageListFactory.kt | 12 ++ plugins/base/base-test-utils/build.gradle.kts | 1 + .../utils/assertHtmlEqualsIgnoringWhitespace.kt | 18 +++ .../src/main/kotlin/renderers/html/HtmlRenderer.kt | 8 +- .../resolvers/local/DokkaLocationProvider.kt | 2 +- .../main/kotlin/templating/InsertTemplateExtra.kt | 12 ++ .../test/kotlin/resourceLinks/ResourceLinksTest.kt | 2 +- .../gfm/gfm-template-processing/build.gradle.kts | 1 + .../GfmTemplateProcessingPlugin.kt | 6 +- .../GfmTemplateProcessingStrategy.kt | 40 +++--- plugins/templating/build.gradle.kts | 18 +++ .../templates/AddToNavigationCommandHandler.kt | 56 ++++++++ .../src/main/kotlin/templates/CommandHandler.kt | 11 ++ .../templates/DirectiveBasedTemplateProcessing.kt | 41 ++++++ .../FallbackTemplateProcessingStrategy.kt | 13 ++ .../JsonElementBasedTemplateProcessingStrategy.kt | 68 ++++++++++ .../main/kotlin/templates/PathToRootSubstitutor.kt | 14 ++ .../kotlin/templates/SubstitutionCommandHandler.kt | 60 +++++++++ .../src/main/kotlin/templates/Substitutor.kt | 7 + .../src/main/kotlin/templates/TemplateProcessor.kt | 56 ++++++++ .../src/main/kotlin/templates/TemplatingPlugin.kt | 50 ++++++++ .../org.jetbrains.dokka.plugability.DokkaPlugin | 1 + .../AddToNavigationCommandResolutionTest.kt | 137 ++++++++++++++++++++ .../templates/AddToSearchCommandResolutionTest.kt | 89 +++++++++++++ .../templates/SubstitutionCommandResolutionTest.kt | 69 ++++++++++ .../templates/TemplatingDokkaTestGenerator.kt | 63 +++++++++ .../kotlin/templates/TestTemplatingGeneration.kt | 22 ++++ .../test/kotlin/templates/TestTemplatingPlugin.kt | 16 +++ plugins/versioning/build.gradle.kts | 19 +++ .../versioning/ReplaceVersionCommandConsumer.kt | 45 +++++++ .../kotlin/versioning/ReplaceVersionsCommand.kt | 26 ++++ .../kotlin/versioning/VersioningConfiguration.kt | 20 +++ .../main/kotlin/versioning/VersioningHandler.kt | 101 +++++++++++++++ .../src/main/kotlin/versioning/VersioningPlugin.kt | 39 ++++++ .../kotlin/versioning/VersionsNavigationCreator.kt | 53 ++++++++ .../src/main/kotlin/versioning/VersionsOrdering.kt | 23 ++++ .../main/kotlin/versioning/htmlPreprocessors.kt | 24 ++++ .../org.jetbrains.dokka.plugability.DokkaPlugin | 1 + .../main/resources/dokka/styles/multimodule.css | 37 ++++++ .../dokka/gradle/AndroidAutoConfigurationTest.kt | 4 +- settings.gradle.kts | 2 + 68 files changed, 1602 insertions(+), 976 deletions(-) create mode 100644 plugins/all-modules-page/src/main/kotlin/ExternalModuleLinkResolver.kt create mode 100644 plugins/all-modules-page/src/main/kotlin/ResolveLinkCommandHandler.kt delete mode 100644 plugins/all-modules-page/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt delete mode 100644 plugins/all-modules-page/src/main/kotlin/templates/ExternalModuleLinkResolver.kt delete mode 100644 plugins/all-modules-page/src/main/kotlin/templates/FallbackTemplateProcessingStrategy.kt delete mode 100644 plugins/all-modules-page/src/main/kotlin/templates/JsonElementBasedTemplateProcessingStrategy.kt delete mode 100644 plugins/all-modules-page/src/main/kotlin/templates/PathToRootSubstitutor.kt delete mode 100644 plugins/all-modules-page/src/main/kotlin/templates/Substitutor.kt delete mode 100644 plugins/all-modules-page/src/main/kotlin/templates/TemplateProcessor.kt delete mode 100644 plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/AddToNavigationCommandResolutionTest.kt delete mode 100644 plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/AddToSearchCommandResolutionTest.kt delete mode 100644 plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkCommandResolutionTest.kt delete mode 100644 plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkGfmCommandResolutionTest.kt delete mode 100644 plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/SubstitutionCommandResolutionTest.kt delete mode 100644 plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/assertHtmlEqualsIgnoringWhitespace.kt delete mode 100644 plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/mockedPackageListFactory.kt create mode 100644 plugins/all-modules-page/src/test/kotlin/templates/ResolveLinkCommandResolutionTest.kt create mode 100644 plugins/all-modules-page/src/test/kotlin/templates/ResolveLinkGfmCommandResolutionTest.kt create mode 100644 plugins/all-modules-page/src/test/kotlin/templates/mockedPackageListFactory.kt create mode 100644 plugins/base/base-test-utils/src/main/kotlin/utils/assertHtmlEqualsIgnoringWhitespace.kt create mode 100644 plugins/base/src/main/kotlin/templating/InsertTemplateExtra.kt create mode 100644 plugins/templating/build.gradle.kts create mode 100644 plugins/templating/src/main/kotlin/templates/AddToNavigationCommandHandler.kt create mode 100644 plugins/templating/src/main/kotlin/templates/CommandHandler.kt create mode 100644 plugins/templating/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt create mode 100644 plugins/templating/src/main/kotlin/templates/FallbackTemplateProcessingStrategy.kt create mode 100644 plugins/templating/src/main/kotlin/templates/JsonElementBasedTemplateProcessingStrategy.kt create mode 100644 plugins/templating/src/main/kotlin/templates/PathToRootSubstitutor.kt create mode 100644 plugins/templating/src/main/kotlin/templates/SubstitutionCommandHandler.kt create mode 100644 plugins/templating/src/main/kotlin/templates/Substitutor.kt create mode 100644 plugins/templating/src/main/kotlin/templates/TemplateProcessor.kt create mode 100644 plugins/templating/src/main/kotlin/templates/TemplatingPlugin.kt create mode 100644 plugins/templating/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin create mode 100644 plugins/templating/src/test/kotlin/templates/AddToNavigationCommandResolutionTest.kt create mode 100644 plugins/templating/src/test/kotlin/templates/AddToSearchCommandResolutionTest.kt create mode 100644 plugins/templating/src/test/kotlin/templates/SubstitutionCommandResolutionTest.kt create mode 100644 plugins/templating/src/test/kotlin/templates/TemplatingDokkaTestGenerator.kt create mode 100644 plugins/templating/src/test/kotlin/templates/TestTemplatingGeneration.kt create mode 100644 plugins/templating/src/test/kotlin/templates/TestTemplatingPlugin.kt create mode 100644 plugins/versioning/build.gradle.kts create mode 100644 plugins/versioning/src/main/kotlin/versioning/ReplaceVersionCommandConsumer.kt create mode 100644 plugins/versioning/src/main/kotlin/versioning/ReplaceVersionsCommand.kt create mode 100644 plugins/versioning/src/main/kotlin/versioning/VersioningConfiguration.kt create mode 100644 plugins/versioning/src/main/kotlin/versioning/VersioningHandler.kt create mode 100644 plugins/versioning/src/main/kotlin/versioning/VersioningPlugin.kt create mode 100644 plugins/versioning/src/main/kotlin/versioning/VersionsNavigationCreator.kt create mode 100644 plugins/versioning/src/main/kotlin/versioning/VersionsOrdering.kt create mode 100644 plugins/versioning/src/main/kotlin/versioning/htmlPreprocessors.kt create mode 100644 plugins/versioning/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin create mode 100644 plugins/versioning/src/main/resources/dokka/styles/multimodule.css diff --git a/integration-tests/gradle/projects/it-multimodule-1/second/build.gradle b/integration-tests/gradle/projects/it-multimodule-1/second/build.gradle index 188917bc..2b62f963 100644 --- a/integration-tests/gradle/projects/it-multimodule-1/second/build.gradle +++ b/integration-tests/gradle/projects/it-multimodule-1/second/build.gradle @@ -6,7 +6,7 @@ dokkaHtml { dokkaSourceSets { "main" { externalDocumentationLink { - url.set(new URL("file://" + rootProject.rootDir.toPath().toAbsolutePath().resolve("first/build/dokka/html/first/"))) + url.set(new URL("file://" + rootProject.rootDir.toPath().toAbsolutePath().resolve("first/build/dokka/html/"))) packageListUrl.set(new URL("file://" + rootProject.rootDir.toPath().toAbsolutePath().resolve("first/build/dokka/html/first/package-list"))) } } diff --git a/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/Android0GradleIntegrationTest.kt b/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/Android0GradleIntegrationTest.kt index bfeed55e..216ff395 100644 --- a/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/Android0GradleIntegrationTest.kt +++ b/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/Android0GradleIntegrationTest.kt @@ -26,7 +26,6 @@ class Android0GradleIntegrationTest(override val versions: BuildVersions) : Abst ) } - @BeforeTest fun prepareProjectFiles() { assumeAndroidSdkInstalled() diff --git a/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/BasicGradleIntegrationTest.kt b/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/BasicGradleIntegrationTest.kt index 33b4885e..d51f27e6 100644 --- a/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/BasicGradleIntegrationTest.kt +++ b/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/BasicGradleIntegrationTest.kt @@ -67,7 +67,7 @@ class BasicGradleIntegrationTest(override val versions: BuildVersions) : Abstrac val moduleOutputDir = File(this, "-basic -project") assertTrue(moduleOutputDir.isDirectory, "Missing module directory") - val moduleIndexHtml = File(moduleOutputDir, "index.html") + val moduleIndexHtml = File(this, "index.html") assertTrue(moduleIndexHtml.isFile, "Missing module index.html") val modulePackageDir = File(moduleOutputDir, "it.basic") diff --git a/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/BasicGroovyIntegrationTest.kt b/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/BasicGroovyIntegrationTest.kt index 0e125258..9746c8b8 100644 --- a/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/BasicGroovyIntegrationTest.kt +++ b/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/BasicGroovyIntegrationTest.kt @@ -65,7 +65,7 @@ class BasicGroovyIntegrationTest(override val versions: BuildVersions) : Abstrac val moduleOutputDir = File(this, "it-basic-groovy") assertTrue(moduleOutputDir.isDirectory, "Missing module directory") - val moduleIndexHtml = File(moduleOutputDir, "index.html") + val moduleIndexHtml = File(this, "index.html") assertTrue(moduleIndexHtml.isFile, "Missing module index.html") val modulePackageDir = File(moduleOutputDir, "it.basic") diff --git a/plugins/all-modules-page/build.gradle.kts b/plugins/all-modules-page/build.gradle.kts index c6e88574..9c5fe1c3 100644 --- a/plugins/all-modules-page/build.gradle.kts +++ b/plugins/all-modules-page/build.gradle.kts @@ -6,6 +6,8 @@ registerDokkaArtifactPublication("dokkaAllModulesPage") { dependencies { implementation(project(":plugins:base")) + implementation(project(":plugins:templating")) + implementation(project(":plugins:versioning")) testImplementation(project(":plugins:base")) testImplementation(project(":plugins:base:base-test-utils")) testImplementation(project(":plugins:gfm")) @@ -13,10 +15,9 @@ dependencies { val coroutines_version: String by project implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") - - implementation("org.jsoup:jsoup:1.12.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.1") - val kotlinx_html_version: String by project - testImplementation("org.jetbrains.kotlinx:kotlinx-html-jvm:$kotlinx_html_version") + implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:$kotlinx_html_version") + + implementation("org.jsoup:jsoup:1.12.1") } \ No newline at end of file diff --git a/plugins/all-modules-page/src/main/kotlin/AllModulesPageGeneration.kt b/plugins/all-modules-page/src/main/kotlin/AllModulesPageGeneration.kt index 5ac854b4..0013feed 100644 --- a/plugins/all-modules-page/src/main/kotlin/AllModulesPageGeneration.kt +++ b/plugins/all-modules-page/src/main/kotlin/AllModulesPageGeneration.kt @@ -8,15 +8,23 @@ import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.plugin import org.jetbrains.dokka.plugability.query import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.templates.TemplatingPlugin +import org.jetbrains.dokka.versioning.VersioningPlugin class AllModulesPageGeneration(private val context: DokkaContext) : Generation { private val allModulesPagePlugin by lazy { context.plugin() } + private val templatingPlugin by lazy { context.plugin() } + private val versioningPlugin by lazy { context.plugin() } override fun Timer.generate() { + report("Creating all modules page") val pages = createAllModulesPage() + report("Copy previous documentation") + handlePreviousDocs() + report("Transforming pages") val transformedPages = transformAllModulesPage(pages) @@ -29,6 +37,8 @@ class AllModulesPageGeneration(private val context: DokkaContext) : Generation { override val generationName = "index page for project" + fun handlePreviousDocs() = versioningPlugin.querySingle { versioningHandler }.invoke() + fun createAllModulesPage() = allModulesPagePlugin.querySingle { allModulesPageCreator }.invoke() fun transformAllModulesPage(pages: RootPageNode) = @@ -39,5 +49,5 @@ class AllModulesPageGeneration(private val context: DokkaContext) : Generation { } fun processSubmodules() = - allModulesPagePlugin.querySingle { templateProcessor }.process() + templatingPlugin.querySingle { templateProcessor }.process() } \ No newline at end of file diff --git a/plugins/all-modules-page/src/main/kotlin/AllModulesPagePlugin.kt b/plugins/all-modules-page/src/main/kotlin/AllModulesPagePlugin.kt index c99293ef..9f4b62ee 100644 --- a/plugins/all-modules-page/src/main/kotlin/AllModulesPagePlugin.kt +++ b/plugins/all-modules-page/src/main/kotlin/AllModulesPagePlugin.kt @@ -1,31 +1,29 @@ package org.jetbrains.dokka.allModulesPage import org.jetbrains.dokka.CoreExtensions -import org.jetbrains.dokka.allModulesPage.templates.* import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProviderFactory import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.templates.TemplatingPlugin import org.jetbrains.dokka.transformers.pages.PageCreator import org.jetbrains.dokka.transformers.pages.PageTransformer class AllModulesPagePlugin : DokkaPlugin() { - val templateProcessor by extensionPoint() - val templateProcessingStrategy by extensionPoint() val partialLocationProviderFactory by extensionPoint() val allModulesPageCreator by extensionPoint() val allModulesPageTransformer by extensionPoint() val externalModuleLinkResolver by extensionPoint() - val substitutor by extensionPoint() - val allModulesPageCreators by extending { allModulesPageCreator providing ::MultimodulePageCreator } + val dokkaBase by lazy { plugin() } + val multimoduleLocationProvider by extending { - (plugin().locationProviderFactory + (dokkaBase.locationProviderFactory providing MultimoduleLocationProvider::Factory override plugin().locationProvider) } @@ -37,37 +35,11 @@ class AllModulesPagePlugin : DokkaPlugin() { val allModulesPageGeneration by extending { (CoreExtensions.generation providing ::AllModulesPageGeneration - override plugin().singleGeneration) - } - - val defaultTemplateProcessor by extending { - templateProcessor providing ::DefaultTemplateProcessor - } - - val directiveBasedHtmlTemplateProcessingStrategy by extending { - templateProcessingStrategy providing ::DirectiveBasedHtmlTemplateProcessingStrategy order { - before(fallbackProcessingStrategy) - } - } - - val fallbackProcessingStrategy by extending { - templateProcessingStrategy providing ::FallbackTemplateProcessingStrategy - } - - val navigationSearchTemplateStrategy by extending { - templateProcessingStrategy providing ::NavigationSearchTemplateStrategy order { - before(fallbackProcessingStrategy) - } - } - - val pagesSearchTemplateStrategy by extending { - templateProcessingStrategy providing ::PagesSearchTemplateStrategy order { - before(fallbackProcessingStrategy) - } + override dokkaBase.singleGeneration) } - val pathToRootSubstitutor by extending { - substitutor providing ::PathToRootSubstitutor + val resolveLinkCommandHandler by extending { + plugin().directiveBasedCommandHandlers providing ::ResolveLinkCommandHandler } val multiModuleLinkResolver by extending { diff --git a/plugins/all-modules-page/src/main/kotlin/ExternalModuleLinkResolver.kt b/plugins/all-modules-page/src/main/kotlin/ExternalModuleLinkResolver.kt new file mode 100644 index 00000000..74513957 --- /dev/null +++ b/plugins/all-modules-page/src/main/kotlin/ExternalModuleLinkResolver.kt @@ -0,0 +1,85 @@ +package org.jetbrains.dokka.allModulesPage + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider.Companion.identifierToFilename +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import java.io.File +import java.net.URL + +interface ExternalModuleLinkResolver { + fun resolve(dri: DRI, fileContext: File): String? + fun resolveLinkToModuleIndex(moduleName: String): String? +} + +class DefaultExternalModuleLinkResolver(val context: DokkaContext) : ExternalModuleLinkResolver { + private val elpFactory = context.plugin().query { externalLocationProviderFactory } + private val externalDocumentations by lazy(::setupExternalDocumentations) + private val elps by lazy { + elpFactory.flatMap { externalDocumentations.map { ed -> it.getExternalLocationProvider(ed) } }.filterNotNull() + } + + private fun setupExternalDocumentations(): List { + val packageLists = + context.configuration.modules.map(::loadPackageListForModule).toMap() + return packageLists.mapNotNull { (module, packageList) -> + packageList?.let { + context.configuration.modules.find { it.name == module.name }?.let { m -> + ExternalDocumentation( + URL("file:/${m.relativePathToOutputDirectory.toRelativeOutputDir()}"), + packageList + ) + } + } + } + } + + private fun File.toRelativeOutputDir(): File = if(isAbsolute) { + relativeToOrSelf(context.configuration.outputDir) + } else { + this + } + + private fun loadPackageListForModule(module: DokkaConfiguration.DokkaModuleDescription) = + module.sourceOutputDirectory.resolve(File(identifierToFilename(module.name))).let { + it to PackageList.load( + URL("file:" + it.resolve("package-list").path), + 8, + true + ) + } + + override fun resolve(dri: DRI, fileContext: File): String? { + val absoluteLink = elps.mapNotNull { it.resolve(dri) }.firstOrNull() ?: return null + val modulePath = context.configuration.outputDir.absolutePath.split(File.separator) + val contextPath = fileContext.absolutePath.split(File.separator) + val commonPathElements = modulePath.zip(contextPath) + .takeWhile { (a, b) -> a == b }.count() + + return (List(contextPath.size - commonPathElements - 1) { ".." } + modulePath.drop(commonPathElements)).joinToString( + "/" + ) + absoluteLink.removePrefix("file:") + } + + override fun resolveLinkToModuleIndex(moduleName: String): String? = + context.configuration.modules.firstOrNull { it.name == moduleName } + ?.let { module -> + val (_, packageList) = loadPackageListForModule(module) + val extension = when (packageList?.linkFormat) { + RecognizedLinkFormat.KotlinWebsiteHtml, + RecognizedLinkFormat.DokkaOldHtml, + RecognizedLinkFormat.DokkaHtml -> ".html" + RecognizedLinkFormat.DokkaGFM, + RecognizedLinkFormat.DokkaJekyll -> ".md" + else -> "" + } + "${module.relativePathToOutputDirectory}/index$extension" + } + +} diff --git a/plugins/all-modules-page/src/main/kotlin/MultimoduleLocationProvider.kt b/plugins/all-modules-page/src/main/kotlin/MultimoduleLocationProvider.kt index 1dbbd386..c7c32d55 100644 --- a/plugins/all-modules-page/src/main/kotlin/MultimoduleLocationProvider.kt +++ b/plugins/all-modules-page/src/main/kotlin/MultimoduleLocationProvider.kt @@ -1,7 +1,6 @@ package org.jetbrains.dokka.allModulesPage import org.jetbrains.dokka.allModulesPage.MultimodulePageCreator.Companion.MULTIMODULE_PACKAGE_PLACEHOLDER -import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.resolvers.local.DokkaBaseLocationProvider import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory import org.jetbrains.dokka.links.DRI diff --git a/plugins/all-modules-page/src/main/kotlin/MultimodulePageCreator.kt b/plugins/all-modules-page/src/main/kotlin/MultimodulePageCreator.kt index a333d7c4..458cf4c1 100644 --- a/plugins/all-modules-page/src/main/kotlin/MultimodulePageCreator.kt +++ b/plugins/all-modules-page/src/main/kotlin/MultimodulePageCreator.kt @@ -8,6 +8,7 @@ import org.jetbrains.dokka.base.parsers.moduleAndPackage.ModuleAndPackageDocumen import org.jetbrains.dokka.base.parsers.moduleAndPackage.parseModuleAndPackageDocumentation import org.jetbrains.dokka.base.parsers.moduleAndPackage.parseModuleAndPackageDocumentationFragments import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint +import org.jetbrains.dokka.base.templating.InsertTemplateExtra import org.jetbrains.dokka.base.transformers.pages.comments.DocTagToContentConverter import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder import org.jetbrains.dokka.links.DRI @@ -17,10 +18,14 @@ import org.jetbrains.dokka.model.doc.P import org.jetbrains.dokka.model.properties.PropertyContainer import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.configuration import org.jetbrains.dokka.plugability.plugin import org.jetbrains.dokka.plugability.querySingle import org.jetbrains.dokka.transformers.pages.PageCreator import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.versioning.ReplaceVersionsCommand +import org.jetbrains.dokka.versioning.VersioningConfiguration +import org.jetbrains.dokka.versioning.VersioningPlugin class MultimodulePageCreator( private val context: DokkaContext, @@ -39,6 +44,11 @@ class MultimodulePageCreator( kind = ContentKind.Cover, sourceSets = sourceSetData ) { + /* The line below checks if there is a provided configuration for versioning. + If not, we are skipping the template for inserting versions navigation */ + configuration(context)?.let { + group(extra = PropertyContainer.withAll(InsertTemplateExtra(ReplaceVersionsCommand))) { } + } header(2, "All modules:") table(styles = setOf(MultimoduleTable)) { modules.forEach { module -> diff --git a/plugins/all-modules-page/src/main/kotlin/ResolveLinkCommandHandler.kt b/plugins/all-modules-page/src/main/kotlin/ResolveLinkCommandHandler.kt new file mode 100644 index 00000000..e881a5ab --- /dev/null +++ b/plugins/all-modules-page/src/main/kotlin/ResolveLinkCommandHandler.kt @@ -0,0 +1,45 @@ +package org.jetbrains.dokka.allModulesPage + +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.base.templating.ResolveLinkCommand +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.templates.CommandHandler +import org.jsoup.nodes.Attributes +import org.jsoup.nodes.Element +import org.jsoup.parser.Tag +import java.io.File + +class ResolveLinkCommandHandler(context: DokkaContext) : CommandHandler { + + private val externalModuleLinkResolver = + context.plugin().querySingle { externalModuleLinkResolver } + + override fun handleCommand(element: Element, command: Command, input: File, output: File) { + command as ResolveLinkCommand + val link = externalModuleLinkResolver.resolve(command.dri, output) + if (link == null) { + val children = element.childNodes().toList() + val attributes = Attributes().apply { + put("data-unresolved-link", command.dri.toString()) + } + val el = Element(Tag.valueOf("span"), "", attributes).apply { + children.forEach { ch -> appendChild(ch) } + } + element.replaceWith(el) + return + } + + val attributes = Attributes().apply { + put("href", link) + } + val children = element.childNodes().toList() + val el = Element(Tag.valueOf("a"), "", attributes).apply { + children.forEach { ch -> appendChild(ch) } + } + element.replaceWith(el) + } + + override fun canHandle(command: Command): Boolean = command is ResolveLinkCommand +} \ No newline at end of file diff --git a/plugins/all-modules-page/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt b/plugins/all-modules-page/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt deleted file mode 100644 index 2b065731..00000000 --- a/plugins/all-modules-page/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt +++ /dev/null @@ -1,141 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.jetbrains.dokka.allModulesPage.AllModulesPagePlugin -import org.jetbrains.dokka.base.templating.* -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.plugin -import org.jetbrains.dokka.plugability.query -import org.jetbrains.dokka.plugability.querySingle -import org.jsoup.Jsoup -import org.jsoup.nodes.* -import org.jsoup.parser.Tag -import java.io.File -import java.nio.file.Files -import java.util.concurrent.ConcurrentHashMap - -class DirectiveBasedHtmlTemplateProcessingStrategy(private val context: DokkaContext) : TemplateProcessingStrategy { - private val navigationFragments = ConcurrentHashMap() - - private val substitutors = context.plugin().query { substitutor } - private val externalModuleLinkResolver = context.plugin().querySingle { externalModuleLinkResolver } - - override suspend fun process(input: File, output: File): Boolean = coroutineScope { - if (input.extension == "html") { - launch { - val document = withContext(IO) { Jsoup.parse(input, "UTF-8") } - document.outputSettings().indentAmount(0).prettyPrint(false) - document.select("dokka-template-command").forEach { - when (val command = parseJson(it.attr("data"))) { - is ResolveLinkCommand -> resolveLink(it, command, output) - is AddToNavigationCommand -> navigationFragments[command.moduleName] = it - is SubstitutionCommand -> substitute(it, TemplatingContext(input, output, it, command)) - else -> context.logger.warn("Unknown templating command $command") - } - } - withContext(IO) { Files.write(output.toPath(), listOf(document.outerHtml())) } - } - true - } else false - } - - private fun substitute(element: Element, commandContext: TemplatingContext) { - val regex = commandContext.command.pattern.toRegex() - element.children().forEach { it.traverseToSubstitute(regex, commandContext) } - - val childrenCopy = element.children().toList() - val position = element.elementSiblingIndex() - val parent = element.parent() - element.remove() - - parent.insertChildren(position, childrenCopy) - } - - private fun Node.traverseToSubstitute(regex: Regex, commandContext: TemplatingContext) { - when (this) { - is TextNode -> replaceWith(TextNode(wholeText.substitute(regex, commandContext))) - is DataNode -> replaceWith(DataNode(wholeData.substitute(regex, commandContext))) - is Element -> { - attributes().forEach { attr(it.key, it.value.substitute(regex, commandContext)) } - childNodes().forEach { it.traverseToSubstitute(regex, commandContext) } - } - } - } - - private fun String.substitute(regex: Regex, commandContext: TemplatingContext) = buildString { - var lastOffset = 0 - regex.findAll(this@substitute).forEach { match -> - append(this@substitute, lastOffset, match.range.first) - append(findSubstitution(commandContext, match)) - lastOffset = match.range.last + 1 - } - append(this@substitute, lastOffset, this@substitute.length) - } - - private fun findSubstitution(commandContext: TemplatingContext, match: MatchResult): String = - substitutors.asSequence().mapNotNull { it.trySubstitute(commandContext, match) }.firstOrNull() ?: match.value - - override suspend fun finish(output: File) { - if (navigationFragments.isNotEmpty()) { - val attributes = Attributes().apply { - put("class", "sideMenu") - } - val node = Element(Tag.valueOf("div"), "", attributes) - navigationFragments.entries.sortedBy { it.key }.forEach { (moduleName, command) -> - command.select("a").forEach { a -> - a.attr("href")?.also { a.attr("href", "${moduleName}/${it}") } - } - command.childNodes().toList().forEachIndexed { index, child -> - if (index == 0) { - child.attr("id", "$moduleName-nav-submenu") - } - node.appendChild(child) - } - } - - withContext(IO) { - Files.write(output.resolve("navigation.html").toPath(), listOf(node.outerHtml())) - } - - node.select("a").forEach { a -> - a.attr("href")?.also { a.attr("href", "../${it}") } - } - navigationFragments.keys.forEach { - withContext(IO) { - Files.write( - output.resolve(it).resolve("navigation.html").toPath(), - listOf(node.outerHtml()) - ) - } - } - } - } - - private fun resolveLink(it: Element, command: ResolveLinkCommand, fileContext: File) { - - val link = externalModuleLinkResolver.resolve(command.dri, fileContext) - if (link == null) { - val children = it.childNodes().toList() - val attributes = Attributes().apply { - put("data-unresolved-link", command.dri.toString()) - } - val element = Element(Tag.valueOf("span"), "", attributes).apply { - children.forEach { ch -> appendChild(ch) } - } - it.replaceWith(element) - return - } - - val attributes = Attributes().apply { - put("href", link) - } - val children = it.childNodes().toList() - val element = Element(Tag.valueOf("a"), "", attributes).apply { - children.forEach { ch -> appendChild(ch) } - } - it.replaceWith(element) - } -} diff --git a/plugins/all-modules-page/src/main/kotlin/templates/ExternalModuleLinkResolver.kt b/plugins/all-modules-page/src/main/kotlin/templates/ExternalModuleLinkResolver.kt deleted file mode 100644 index d0e787b6..00000000 --- a/plugins/all-modules-page/src/main/kotlin/templates/ExternalModuleLinkResolver.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider.Companion.identifierToFilename -import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation -import org.jetbrains.dokka.base.resolvers.shared.PackageList -import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.plugin -import org.jetbrains.dokka.plugability.query -import java.io.File -import java.net.URL - -interface ExternalModuleLinkResolver { - fun resolve(dri: DRI, fileContext: File): String? - fun resolveLinkToModuleIndex(moduleName: String): String? -} - -class DefaultExternalModuleLinkResolver(val context: DokkaContext) : ExternalModuleLinkResolver { - private val elpFactory = context.plugin().query { externalLocationProviderFactory } - private val externalDocumentations by lazy(::setupExternalDocumentations) - private val elps by lazy { - elpFactory.flatMap { externalDocumentations.map { ed -> it.getExternalLocationProvider(ed) } }.filterNotNull() - } - - private fun setupExternalDocumentations(): List { - val packageLists = - context.configuration.modules.map(::loadPackageListForModule).toMap() - return packageLists.mapNotNull { (module, packageList) -> - packageList?.let { - ExternalDocumentation( - URL("file:/${module.name}/${module.name}"), - packageList - ) - } - } - } - - private fun loadPackageListForModule(module: DokkaConfiguration.DokkaModuleDescription) = - module.sourceOutputDirectory.resolve(File(identifierToFilename(module.name))).let { - it to PackageList.load( - URL("file:" + it.resolve("package-list").path), - 8, - true - ) - } - - override fun resolve(dri: DRI, fileContext: File): String? { - val absoluteLink = elps.mapNotNull { it.resolve(dri) }.firstOrNull() ?: return null - val modulePath = context.configuration.outputDir.absolutePath.split(File.separator) - val contextPath = fileContext.absolutePath.split(File.separator) - val commonPathElements = modulePath.zip(contextPath) - .takeWhile { (a, b) -> a == b }.count() - - return (List(contextPath.size - commonPathElements - 1) { ".." } + modulePath.drop(commonPathElements)).joinToString( - "/" - ) + absoluteLink.removePrefix("file:") - } - - override fun resolveLinkToModuleIndex(moduleName: String): String? = - context.configuration.modules.firstOrNull { it.name == moduleName } - ?.let { module -> - val (_, packageList) = loadPackageListForModule(module) - val extension = when (packageList?.linkFormat) { - RecognizedLinkFormat.KotlinWebsiteHtml, - RecognizedLinkFormat.DokkaOldHtml, - RecognizedLinkFormat.DokkaHtml -> ".html" - RecognizedLinkFormat.DokkaGFM, - RecognizedLinkFormat.DokkaJekyll -> ".md" - else -> "" - } - "${module.relativePathToOutputDirectory}/${identifierToFilename(moduleName)}/index$extension" - } - -} diff --git a/plugins/all-modules-page/src/main/kotlin/templates/FallbackTemplateProcessingStrategy.kt b/plugins/all-modules-page/src/main/kotlin/templates/FallbackTemplateProcessingStrategy.kt deleted file mode 100644 index 9b5251ac..00000000 --- a/plugins/all-modules-page/src/main/kotlin/templates/FallbackTemplateProcessingStrategy.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import org.jetbrains.dokka.plugability.DokkaContext -import java.io.File -import java.nio.file.Files - -class FallbackTemplateProcessingStrategy(dokkaContext: DokkaContext) : TemplateProcessingStrategy { - - override suspend fun process(input: File, output: File): Boolean = coroutineScope { - launch(IO) { - Files.copy(input.toPath(), output.toPath()) - } - true - } -} \ No newline at end of file diff --git a/plugins/all-modules-page/src/main/kotlin/templates/JsonElementBasedTemplateProcessingStrategy.kt b/plugins/all-modules-page/src/main/kotlin/templates/JsonElementBasedTemplateProcessingStrategy.kt deleted file mode 100644 index c6c67752..00000000 --- a/plugins/all-modules-page/src/main/kotlin/templates/JsonElementBasedTemplateProcessingStrategy.kt +++ /dev/null @@ -1,74 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import java.io.File -import java.nio.file.Files -import com.fasterxml.jackson.module.kotlin.treeToValue -import org.jetbrains.dokka.base.renderers.html.SearchRecord -import org.jetbrains.dokka.base.templating.* -import org.jetbrains.dokka.plugability.DokkaContext -import java.util.concurrent.ConcurrentHashMap - -abstract class BaseJsonNavigationTemplateProcessingStrategy(val context: DokkaContext) : TemplateProcessingStrategy { - abstract val navigationFileNameWithoutExtension: String - abstract val path: String - - private val fragments = ConcurrentHashMap>() - - open fun canProcess(file: File): Boolean = - file.extension == "json" && file.nameWithoutExtension == navigationFileNameWithoutExtension - - override suspend fun process(input: File, output: File): Boolean = coroutineScope { - val canProcess = canProcess(input) - if (canProcess) { - launch { - withContext(Dispatchers.IO) { - runCatching { parseJson(input.readText()) }.getOrNull() - }?.let { command -> - fragments[command.moduleName] = command.elements - } ?: fallbackToCopy(input, output) - } - } - canProcess - } - - override suspend fun finish(output: File) { - if (fragments.isNotEmpty()) { - val content = toJsonString(fragments.entries.flatMap { (moduleName, navigation) -> - navigation.map { it.withResolvedLocation(moduleName) } - }) - withContext(Dispatchers.IO) { - output.resolve("$path/$navigationFileNameWithoutExtension.json").writeText(content) - - fragments.keys.forEach { - output.resolve(it).resolve("$path/$navigationFileNameWithoutExtension.json").writeText(content) - } - } - } - } - - private suspend fun fallbackToCopy(input: File, output: File) { - context.logger.warn("Falling back to just copying file for ${input.name} even thought it should process it") - withContext(Dispatchers.IO) { input.copyTo(output) } - } - - private fun SearchRecord.withResolvedLocation(moduleName: String): SearchRecord = - copy(location = "$moduleName/$location") - -} - -class NavigationSearchTemplateStrategy(val dokkaContext: DokkaContext) : - BaseJsonNavigationTemplateProcessingStrategy(dokkaContext) { - override val navigationFileNameWithoutExtension: String = "navigation-pane" - override val path: String = "scripts" -} - -class PagesSearchTemplateStrategy(val dokkaContext: DokkaContext) : - BaseJsonNavigationTemplateProcessingStrategy(dokkaContext) { - override val navigationFileNameWithoutExtension: String = "pages" - override val path: String = "scripts" -} \ No newline at end of file diff --git a/plugins/all-modules-page/src/main/kotlin/templates/PathToRootSubstitutor.kt b/plugins/all-modules-page/src/main/kotlin/templates/PathToRootSubstitutor.kt deleted file mode 100644 index 5056b724..00000000 --- a/plugins/all-modules-page/src/main/kotlin/templates/PathToRootSubstitutor.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand -import org.jetbrains.dokka.base.templating.SubstitutionCommand -import org.jetbrains.dokka.plugability.DokkaContext -import java.io.File - -class PathToRootSubstitutor(private val dokkaContext: DokkaContext) : Substitutor { - override fun trySubstitute(context: TemplatingContext, match: MatchResult): String? = - if (context.command is PathToRootSubstitutionCommand) { - context.output.toPath().parent.relativize(dokkaContext.configuration.outputDir.toPath()).toString().split(File.separator).joinToString(separator = "/", postfix = "/") { it } - } else null - -} \ No newline at end of file diff --git a/plugins/all-modules-page/src/main/kotlin/templates/Substitutor.kt b/plugins/all-modules-page/src/main/kotlin/templates/Substitutor.kt deleted file mode 100644 index 98f1d88e..00000000 --- a/plugins/all-modules-page/src/main/kotlin/templates/Substitutor.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import org.jetbrains.dokka.base.templating.SubstitutionCommand - -fun interface Substitutor { - fun trySubstitute(context: TemplatingContext, match: MatchResult): String? -} \ No newline at end of file diff --git a/plugins/all-modules-page/src/main/kotlin/templates/TemplateProcessor.kt b/plugins/all-modules-page/src/main/kotlin/templates/TemplateProcessor.kt deleted file mode 100644 index 18d63df0..00000000 --- a/plugins/all-modules-page/src/main/kotlin/templates/TemplateProcessor.kt +++ /dev/null @@ -1,60 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import kotlinx.coroutines.* -import org.jetbrains.dokka.allModulesPage.AllModulesPagePlugin -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 -import org.jsoup.nodes.Element -import java.io.File -import java.nio.file.Files -import java.nio.file.Path -import kotlin.coroutines.coroutineContext - -interface TemplateProcessor { - fun process() -} - -interface TemplateProcessingStrategy { - suspend fun process(input: File, output: File): Boolean - suspend fun finish(output: File) {} -} - -class DefaultTemplateProcessor( - private val context: DokkaContext, -): TemplateProcessor { - - private val strategies: List = context.plugin().query { templateProcessingStrategy } - - override fun process() = runBlocking(Dispatchers.Default) { - coroutineScope { - context.configuration.modules.forEach { - launch { - it.sourceOutputDirectory.visit(context.configuration.outputDir.resolve(it.relativePathToOutputDirectory)) - } - } - } - strategies.map { it.finish(context.configuration.outputDir) } - Unit - } - - private suspend fun File.visit(target: File): Unit = coroutineScope { - val source = this@visit - if (source.isDirectory) { - target.mkdir() - source.list()?.forEach { - launch { source.resolve(it).visit(target.resolve(it)) } - } - } else { - strategies.asSequence().first { it.process(source, target) } - } - } -} - -data class TemplatingContext( - val input: File, - val output: File, - val element: Element, - val command: T, -) \ No newline at end of file diff --git a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/AddToNavigationCommandResolutionTest.kt b/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/AddToNavigationCommandResolutionTest.kt deleted file mode 100644 index f917916a..00000000 --- a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/AddToNavigationCommandResolutionTest.kt +++ /dev/null @@ -1,137 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import kotlinx.html.a -import kotlinx.html.div -import kotlinx.html.id -import kotlinx.html.span -import kotlinx.html.stream.createHTML -import org.jetbrains.dokka.DokkaModuleDescriptionImpl -import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest -import org.jetbrains.dokka.base.renderers.html.templateCommand -import org.jetbrains.dokka.base.templating.AddToNavigationCommand -import org.jetbrains.dokka.plugability.DokkaContext -import org.junit.Rule -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import org.junit.rules.TemporaryFolder - -class AddToNavigationCommandResolutionTest : MultiModuleAbstractTest() { - @get:Rule - val folder: TemporaryFolder = TemporaryFolder() - - @Test - fun `should substitute AddToNavigationCommand in root directory`() = - addToNavigationTest { - val output = folder.root.resolve("navigation.html").readText() - val expected = expectedOutput( - ModuleWithPrefix("module1"), - ModuleWithPrefix("module2") - ) - assertHtmlEqualsIgnoringWhitespace(expected, output) - } - - @ParameterizedTest - @ValueSource(strings = ["module1", "module2"]) - fun `should substitute AddToNavigationCommand in modules directory`(moduleName: String) = - addToNavigationTest { - val output = folder.root.resolve(moduleName).resolve("navigation.html").readText() - val expected = expectedOutput( - ModuleWithPrefix("module1", ".."), - ModuleWithPrefix("module2", "..") - ) - assertHtmlEqualsIgnoringWhitespace(expected, output) - } - - private fun expectedOutput(vararg modulesWithPrefix: ModuleWithPrefix) = createHTML(prettyPrint = true) - .div("sideMenu") { - modulesWithPrefix.forEach { (moduleName, prefix) -> - val relativePrefix = prefix?.let { "$it/" } ?: "" - div("sideMenuPart") { - id = "$moduleName-nav-submenu" - div("overview") { - a { - href = "$relativePrefix$moduleName/module-page.html" - span { - +"module-$moduleName" - } - } - } - div("sideMenuPart") { - id = "$moduleName-nav-submenu-0" - div("overview") { - a { - href = "$relativePrefix$moduleName/$moduleName/package-page.html" - span { - +"package-$moduleName" - } - } - } - } - } - } - } - - private fun inputForModule(moduleName: String) = createHTML() - .templateCommand(AddToNavigationCommand(moduleName)) { - div("sideMenuPart") { - id = "$moduleName-nav-submenu" - div("overview") { - a { - href = "module-page.html" - span { - +"module-$moduleName" - } - } - } - div("sideMenuPart") { - id = "$moduleName-nav-submenu-0" - div("overview") { - a { - href = "$moduleName/package-page.html" - span { - +"package-$moduleName" - } - } - } - } - } - } - - private fun addToNavigationTest(test: (DokkaContext) -> Unit) { - folder.create() - val module1 = folder.newFolder("module1") - val module2 = folder.newFolder("module2") - - val configuration = dokkaConfiguration { - modules = listOf( - DokkaModuleDescriptionImpl( - name = "module1", - relativePathToOutputDirectory = module1, - includes = emptySet(), - sourceOutputDirectory = module1, - ), - DokkaModuleDescriptionImpl( - name = "module2", - relativePathToOutputDirectory = module2, - includes = emptySet(), - sourceOutputDirectory = module2, - ), - ) - this.outputDir = folder.root - } - - val module1Navigation = module1.resolve("navigation.html") - module1Navigation.writeText(inputForModule("module1")) - val module2Navigation = module2.resolve("navigation.html") - module2Navigation.writeText(inputForModule("module2")) - - testFromData(configuration, preserveOutputLocation = true) { - submoduleProcessingStage = { ctx -> - test(ctx) - } - } - } - - private data class ModuleWithPrefix(val moduleName: String, val prefix: String? = null) -} \ No newline at end of file diff --git a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/AddToSearchCommandResolutionTest.kt b/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/AddToSearchCommandResolutionTest.kt deleted file mode 100644 index 238134c7..00000000 --- a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/AddToSearchCommandResolutionTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import org.jetbrains.dokka.DokkaModuleDescriptionImpl -import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest -import org.jetbrains.dokka.base.renderers.html.SearchRecord -import org.jetbrains.dokka.base.templating.AddToSearch -import org.jetbrains.dokka.base.templating.parseJson -import org.jetbrains.dokka.base.templating.toJsonString -import org.junit.Rule -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import org.junit.rules.TemporaryFolder -import java.io.File -import kotlin.test.assertEquals - -class AddToSearchCommandResolutionTest : MultiModuleAbstractTest() { - companion object { - val elements = listOf( - SearchRecord(name = "name1", location = "location1"), - SearchRecord(name = "name2", location = "location2") - ) - val fromModule1 = AddToSearch( - moduleName = "module1", - elements = elements - ) - val fromModule2 = AddToSearch( - moduleName = "module2", - elements = elements - ) - } - - @get:Rule - val folder: TemporaryFolder = TemporaryFolder() - - @ParameterizedTest - @ValueSource(strings = ["navigation-pane.json", "pages.json"]) - fun `should merge navigation templates`(fileName: String) { - val (module1Navigation, module2Navigation) = setupTestDirectoriesWithContent(fileName) - - val outputDir = folder.root - val configuration = dokkaConfiguration { - modules = listOf( - DokkaModuleDescriptionImpl( - name = "module1", - relativePathToOutputDirectory = folder.root.resolve("module1"), - includes = emptySet(), - sourceOutputDirectory = folder.root.resolve("module1"), - ), - DokkaModuleDescriptionImpl( - name = "module2", - relativePathToOutputDirectory = folder.root.resolve("module2"), - includes = emptySet(), - sourceOutputDirectory = folder.root.resolve("module2"), - ), - ) - this.outputDir = outputDir - } - - testFromData(configuration, preserveOutputLocation = true) { - submoduleProcessingStage = { _ -> - val expected = elements.map { it.copy(location = "module1/${it.location}") } + - elements.map { it.copy(location = "module2/${it.location}") } - - val output = - parseJson>(outputDir.resolve("scripts/${fileName}").readText()) - assertEquals(expected, output.sortedBy { it.location }) - - val outputFromModule1 = parseJson>(module1Navigation.readText()) - assertEquals(expected, outputFromModule1.sortedBy { it.location }) - - val outputFromModule2 = parseJson>(module2Navigation.readText()) - assertEquals(expected, outputFromModule2.sortedBy { it.location }) - } - } - } - - private fun setupTestDirectoriesWithContent(fileName: String): List { - folder.create() - val scriptsForModule1 = folder.newFolder("module1", "scripts") - val scriptsForModule2 = folder.newFolder("module2", "scripts") - folder.newFolder("scripts") - - val module1Navigation = scriptsForModule1.resolve(fileName) - module1Navigation.writeText(toJsonString(fromModule1)) - val module2Navigation = scriptsForModule2.resolve(fileName) - module2Navigation.writeText(toJsonString(fromModule2)) - - return listOf(module1Navigation, module2Navigation) - } -} \ No newline at end of file diff --git a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkCommandResolutionTest.kt b/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkCommandResolutionTest.kt deleted file mode 100644 index 1b4e8638..00000000 --- a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkCommandResolutionTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import kotlinx.html.a -import kotlinx.html.span -import kotlinx.html.stream.createHTML -import org.jetbrains.dokka.DokkaModuleDescriptionImpl -import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest -import org.jetbrains.dokka.base.renderers.html.templateCommand -import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat -import org.jetbrains.dokka.base.templating.ResolveLinkCommand -import org.jetbrains.dokka.links.DRI -import org.junit.Rule -import org.junit.jupiter.api.Test -import org.junit.rules.TemporaryFolder -import java.io.File - -class ResolveLinkCommandResolutionTest : MultiModuleAbstractTest() { - @get:Rule - val folder: TemporaryFolder = TemporaryFolder() - - fun configuration() = dokkaConfiguration { - modules = listOf( - DokkaModuleDescriptionImpl( - name = "module1", - relativePathToOutputDirectory = folder.root.resolve("module1"), - includes = emptySet(), - sourceOutputDirectory = folder.root.resolve("module1"), - ), - DokkaModuleDescriptionImpl( - name = "module2", - relativePathToOutputDirectory = folder.root.resolve("module2"), - includes = emptySet(), - sourceOutputDirectory = folder.root.resolve("module2"), - ) - ) - this.outputDir = folder.root - } - - @Test - fun `should resolve link to another module`() { - val testedDri = DRI( - packageName = "package2", - classNames = "Sample", - ) - val link = createHTML().templateCommand(ResolveLinkCommand(testedDri)) { - span { - +"Sample" - } - } - - val expected = createHTML().a { - href = "../../module2/module2/package2/-sample/index.html" - span { - +"Sample" - } - } - - val contentFile = setup(link) - val configuration = configuration() - - testFromData(configuration, preserveOutputLocation = true) { - submoduleProcessingStage = { - assertHtmlEqualsIgnoringWhitespace(expected, contentFile.readText()) - } - } - } - - @Test - fun `should produce content when link is not resolvable`() { - val testedDri = DRI( - packageName = "not-resolvable-package", - classNames = "Sample", - ) - val link = createHTML().templateCommand(ResolveLinkCommand(testedDri)) { - span { - +"Sample" - } - } - - val expected = createHTML().span { - attributes["data-unresolved-link"] = testedDri.toString() - span { - +"Sample" - } - } - - val contentFile = setup(link) - val configuration = configuration() - - testFromData(configuration, preserveOutputLocation = true) { - submoduleProcessingStage = { - assertHtmlEqualsIgnoringWhitespace(expected, contentFile.readText()) - } - } - } - - fun setup(content: String): File { - folder.create() - val innerModule1 = folder.newFolder("module1", "module1") - val innerModule2 = folder.newFolder("module2", "module2") - val packageList = innerModule2.resolve("package-list") - packageList.writeText(mockedPackageListForPackages(RecognizedLinkFormat.DokkaHtml, "package2")) - val contentFile = innerModule1.resolve("index.html") - contentFile.writeText(content) - return contentFile - } -} \ No newline at end of file diff --git a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkGfmCommandResolutionTest.kt b/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkGfmCommandResolutionTest.kt deleted file mode 100644 index 62aa9338..00000000 --- a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkGfmCommandResolutionTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import org.jetbrains.dokka.DokkaModuleDescriptionImpl -import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest -import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat -import org.jetbrains.dokka.gfm.GfmCommand.Companion.templateCommand -import org.jetbrains.dokka.gfm.GfmPlugin -import org.jetbrains.dokka.gfm.ResolveLinkGfmCommand -import org.jetbrains.dokka.gfm.templateProcessing.GfmTemplateProcessingPlugin -import org.jetbrains.dokka.links.DRI -import org.junit.Rule -import org.junit.jupiter.api.Test -import org.junit.rules.TemporaryFolder -import java.io.File -import kotlin.test.assertEquals - -class ResolveLinkGfmCommandResolutionTest : MultiModuleAbstractTest() { - @get:Rule - val folder: TemporaryFolder = TemporaryFolder() - - fun configuration() = dokkaConfiguration { - modules = listOf( - DokkaModuleDescriptionImpl( - name = "module1", - relativePathToOutputDirectory = folder.root.resolve("module1"), - includes = emptySet(), - sourceOutputDirectory = folder.root.resolve("module1"), - ), - DokkaModuleDescriptionImpl( - name = "module2", - relativePathToOutputDirectory = folder.root.resolve("module2"), - includes = emptySet(), - sourceOutputDirectory = folder.root.resolve("module2"), - ) - ) - this.outputDir = folder.root - } - - @Test - fun `should resolve link to another module`(){ - val testedDri = DRI( - packageName = "package2", - classNames = "Sample", - ) - - val link = StringBuilder().apply { - templateCommand(ResolveLinkGfmCommand(testedDri)){ - append("Sample text inside") - } - }.toString() - - val expected = "[Sample text inside](../../module2/module2/package2/-sample/index.md)" - - val content = setup(link) - val configuration = configuration() - - testFromData(configuration, pluginOverrides = listOf(GfmTemplateProcessingPlugin(), GfmPlugin()), preserveOutputLocation = true) { - submoduleProcessingStage = { - assertEquals(expected, content.readText().trim()) - } - } - } - - fun setup(content: String): File { - folder.create() - val innerModule1 = folder.newFolder("module1", "module1") - val innerModule2 = folder.newFolder("module2", "module2") - val packageList = innerModule2.resolve("package-list") - packageList.writeText(mockedPackageListForPackages(RecognizedLinkFormat.DokkaGFM, "package2")) - val contentFile = innerModule1.resolve("index.md") - contentFile.writeText(content) - return contentFile - } -} \ No newline at end of file diff --git a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/SubstitutionCommandResolutionTest.kt b/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/SubstitutionCommandResolutionTest.kt deleted file mode 100644 index 89984b46..00000000 --- a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/SubstitutionCommandResolutionTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import kotlinx.html.a -import kotlinx.html.div -import kotlinx.html.id -import kotlinx.html.stream.createHTML -import org.jetbrains.dokka.DokkaModuleDescriptionImpl -import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest -import org.jetbrains.dokka.base.renderers.html.templateCommand -import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand -import org.junit.Rule -import org.junit.rules.TemporaryFolder -import org.junit.jupiter.api.Test -import java.io.File - -class SubstitutionCommandResolutionTest : MultiModuleAbstractTest() { - - @get:Rule - val folder: TemporaryFolder = TemporaryFolder() - - @Test - fun `should handle PathToRootCommand`() { - val template = createHTML() - .templateCommand(PathToRootSubstitutionCommand(pattern = "###", default = "default")) { - a { - href = "###index.html" - div { - id = "logo" - } - } - } - - val expected = createHTML().a { - href = "../index.html" - div { - id = "logo" - } - } - - val testedFile = createDirectoriesAndWriteContent(template) - - val configuration = dokkaConfiguration { - modules = listOf( - DokkaModuleDescriptionImpl( - name = "module1", - relativePathToOutputDirectory = folder.root.resolve("module1"), - includes = emptySet(), - sourceOutputDirectory = folder.root.resolve("module1"), - ) - ) - this.outputDir = folder.root - } - - testFromData(configuration, preserveOutputLocation = true){ - submoduleProcessingStage = { - assertHtmlEqualsIgnoringWhitespace(expected, testedFile.readText()) - } - } - } - - private fun createDirectoriesAndWriteContent(content: String): File { - folder.create() - val module1 = folder.newFolder("module1") - val module1Content = module1.resolve("index.html") - module1Content.writeText(content) - return module1Content - } - -} \ No newline at end of file diff --git a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/assertHtmlEqualsIgnoringWhitespace.kt b/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/assertHtmlEqualsIgnoringWhitespace.kt deleted file mode 100644 index 5a9ff531..00000000 --- a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/assertHtmlEqualsIgnoringWhitespace.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import junit.framework.Assert.assertEquals -import org.jsoup.Jsoup - -/** - * Parses it using JSOUP, trims whitespace at the end of the line and asserts if they are equal - * parsing is required to unify the formatting - */ -fun assertHtmlEqualsIgnoringWhitespace(expected: String, actual: String) { - assertEquals( - Jsoup.parse(expected).outerHtml().trimSpacesAtTheEndOfLine(), - Jsoup.parse(actual).outerHtml().trimSpacesAtTheEndOfLine() - ) -} - -private fun String.trimSpacesAtTheEndOfLine(): String = - replace(" \n", "\n") \ No newline at end of file diff --git a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/mockedPackageListFactory.kt b/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/mockedPackageListFactory.kt deleted file mode 100644 index 7a10041b..00000000 --- a/plugins/all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/mockedPackageListFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.jetbrains.dokka.allModulesPage.templates - -import org.jetbrains.dokka.base.renderers.PackageListService -import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat - -internal fun mockedPackageListForPackages(format: RecognizedLinkFormat, vararg packages: String): String = - """ - ${PackageListService.DOKKA_PARAM_PREFIX}.format:${format.formatName} - ${PackageListService.DOKKA_PARAM_PREFIX}.linkExtension:${format.linkExtension} - - ${packages.sorted().joinToString(separator = "\n", postfix = "\n") { it }} - """.trimIndent() \ No newline at end of file diff --git a/plugins/all-modules-page/src/test/kotlin/templates/ResolveLinkCommandResolutionTest.kt b/plugins/all-modules-page/src/test/kotlin/templates/ResolveLinkCommandResolutionTest.kt new file mode 100644 index 00000000..cbf254a0 --- /dev/null +++ b/plugins/all-modules-page/src/test/kotlin/templates/ResolveLinkCommandResolutionTest.kt @@ -0,0 +1,108 @@ +package org.jetbrains.dokka.allModulesPage.templates + +import kotlinx.html.a +import kotlinx.html.span +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest +import org.jetbrains.dokka.base.renderers.html.templateCommand +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.base.templating.ResolveLinkCommand +import org.jetbrains.dokka.links.DRI +import org.junit.Rule +import org.junit.jupiter.api.Test +import org.junit.rules.TemporaryFolder +import utils.assertHtmlEqualsIgnoringWhitespace +import java.io.File + +class ResolveLinkCommandResolutionTest : MultiModuleAbstractTest() { + @get:Rule + val folder: TemporaryFolder = TemporaryFolder() + + private fun configuration() = dokkaConfiguration { + modules = listOf( + DokkaModuleDescriptionImpl( + name = "module1", + relativePathToOutputDirectory = folder.root.resolve("module1"), + includes = emptySet(), + sourceOutputDirectory = folder.root.resolve("module1"), + ), + DokkaModuleDescriptionImpl( + name = "module2", + relativePathToOutputDirectory = folder.root.resolve("module2"), + includes = emptySet(), + sourceOutputDirectory = folder.root.resolve("module2"), + ) + ) + this.outputDir = folder.root + } + + @Test + fun `should resolve link to another module`() { + val testedDri = DRI( + packageName = "package2", + classNames = "Sample", + ) + val link = createHTML().templateCommand(ResolveLinkCommand(testedDri)) { + span { + +"Sample" + } + } + + val expected = createHTML().a { + href = "../../module2/package2/-sample/index.html" + span { + +"Sample" + } + } + + val contentFile = setup(link) + val configuration = configuration() + + testFromData(configuration, preserveOutputLocation = true) { + submoduleProcessingStage = { + assertHtmlEqualsIgnoringWhitespace(expected, contentFile.readText()) + } + } + } + + @Test + fun `should produce content when link is not resolvable`() { + val testedDri = DRI( + packageName = "not-resolvable-package", + classNames = "Sample", + ) + val link = createHTML().templateCommand(ResolveLinkCommand(testedDri)) { + span { + +"Sample" + } + } + + val expected = createHTML().span { + attributes["data-unresolved-link"] = testedDri.toString() + span { + +"Sample" + } + } + + val contentFile = setup(link) + val configuration = configuration() + + testFromData(configuration, preserveOutputLocation = true) { + submoduleProcessingStage = { + assertHtmlEqualsIgnoringWhitespace(expected, contentFile.readText()) + } + } + } + + fun setup(content: String): File { + folder.create() + val innerModule1 = folder.newFolder("module1", "module1") + val innerModule2 = folder.newFolder("module2", "module2") + val packageList = innerModule2.resolve("package-list") + packageList.writeText(mockedPackageListForPackages(RecognizedLinkFormat.DokkaHtml, "package2")) + val contentFile = innerModule1.resolve("index.html") + contentFile.writeText(content) + return contentFile + } +} \ No newline at end of file diff --git a/plugins/all-modules-page/src/test/kotlin/templates/ResolveLinkGfmCommandResolutionTest.kt b/plugins/all-modules-page/src/test/kotlin/templates/ResolveLinkGfmCommandResolutionTest.kt new file mode 100644 index 00000000..75576727 --- /dev/null +++ b/plugins/all-modules-page/src/test/kotlin/templates/ResolveLinkGfmCommandResolutionTest.kt @@ -0,0 +1,74 @@ +package org.jetbrains.dokka.allModulesPage.templates + +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.gfm.GfmCommand.Companion.templateCommand +import org.jetbrains.dokka.gfm.GfmPlugin +import org.jetbrains.dokka.gfm.ResolveLinkGfmCommand +import org.jetbrains.dokka.gfm.templateProcessing.GfmTemplateProcessingPlugin +import org.jetbrains.dokka.links.DRI +import org.junit.Rule +import org.junit.jupiter.api.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.assertEquals + +class ResolveLinkGfmCommandResolutionTest : MultiModuleAbstractTest() { + @get:Rule + val folder: TemporaryFolder = TemporaryFolder() + + private fun configuration() = dokkaConfiguration { + modules = listOf( + DokkaModuleDescriptionImpl( + name = "module1", + relativePathToOutputDirectory = folder.root.resolve("module1"), + includes = emptySet(), + sourceOutputDirectory = folder.root.resolve("module1"), + ), + DokkaModuleDescriptionImpl( + name = "module2", + relativePathToOutputDirectory = folder.root.resolve("module2"), + includes = emptySet(), + sourceOutputDirectory = folder.root.resolve("module2"), + ) + ) + this.outputDir = folder.root + } + + @Test + fun `should resolve link to another module`(){ + val testedDri = DRI( + packageName = "package2", + classNames = "Sample", + ) + + val link = StringBuilder().apply { + templateCommand(ResolveLinkGfmCommand(testedDri)){ + append("Sample text inside") + } + }.toString() + + val expected = "[Sample text inside](../../module2/package2/-sample/index.md)" + + val content = setup(link) + val configuration = configuration() + + testFromData(configuration, pluginOverrides = listOf(GfmTemplateProcessingPlugin(), GfmPlugin()), preserveOutputLocation = true) { + submoduleProcessingStage = { + assertEquals(expected, content.readText().trim()) + } + } + } + + private fun setup(content: String): File { + folder.create() + val innerModule1 = folder.newFolder("module1", "module1") + val innerModule2 = folder.newFolder("module2", "module2") + val packageList = innerModule2.resolve("package-list") + packageList.writeText(mockedPackageListForPackages(RecognizedLinkFormat.DokkaGFM, "package2")) + val contentFile = innerModule1.resolve("index.md") + contentFile.writeText(content) + return contentFile + } +} diff --git a/plugins/all-modules-page/src/test/kotlin/templates/mockedPackageListFactory.kt b/plugins/all-modules-page/src/test/kotlin/templates/mockedPackageListFactory.kt new file mode 100644 index 00000000..7a10041b --- /dev/null +++ b/plugins/all-modules-page/src/test/kotlin/templates/mockedPackageListFactory.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.allModulesPage.templates + +import org.jetbrains.dokka.base.renderers.PackageListService +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat + +internal fun mockedPackageListForPackages(format: RecognizedLinkFormat, vararg packages: String): String = + """ + ${PackageListService.DOKKA_PARAM_PREFIX}.format:${format.formatName} + ${PackageListService.DOKKA_PARAM_PREFIX}.linkExtension:${format.linkExtension} + + ${packages.sorted().joinToString(separator = "\n", postfix = "\n") { it }} + """.trimIndent() \ No newline at end of file diff --git a/plugins/base/base-test-utils/build.gradle.kts b/plugins/base/base-test-utils/build.gradle.kts index 1de0ac0f..5515c965 100644 --- a/plugins/base/base-test-utils/build.gradle.kts +++ b/plugins/base/base-test-utils/build.gradle.kts @@ -2,4 +2,5 @@ dependencies { compileOnly(project(":plugins:base")) implementation(project(":core:test-api")) implementation("org.jsoup:jsoup:1.12.1") + implementation(kotlin("test-junit")) } diff --git a/plugins/base/base-test-utils/src/main/kotlin/utils/assertHtmlEqualsIgnoringWhitespace.kt b/plugins/base/base-test-utils/src/main/kotlin/utils/assertHtmlEqualsIgnoringWhitespace.kt new file mode 100644 index 00000000..f8ef8a41 --- /dev/null +++ b/plugins/base/base-test-utils/src/main/kotlin/utils/assertHtmlEqualsIgnoringWhitespace.kt @@ -0,0 +1,18 @@ +package utils + +import junit.framework.Assert.assertEquals +import org.jsoup.Jsoup + +/** + * Parses it using JSOUP, trims whitespace at the end of the line and asserts if they are equal + * parsing is required to unify the formatting + */ +fun assertHtmlEqualsIgnoringWhitespace(expected: String, actual: String) { + assertEquals( + Jsoup.parse(expected).outerHtml().trimSpacesAtTheEndOfLine(), + Jsoup.parse(actual).outerHtml().trimSpacesAtTheEndOfLine() + ) +} + +private fun String.trimSpacesAtTheEndOfLine(): String = + replace(" \n", "\n") \ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt index 1584df02..5c877f03 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -11,6 +11,7 @@ import org.jetbrains.dokka.base.renderers.isImage import org.jetbrains.dokka.base.renderers.pageId import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint import org.jetbrains.dokka.base.resolvers.local.DokkaBaseLocationProvider +import org.jetbrains.dokka.base.templating.InsertTemplateExtra import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand import org.jetbrains.dokka.base.templating.ResolveLinkCommand import org.jetbrains.dokka.links.DRI @@ -107,6 +108,7 @@ open class HtmlRenderer( node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() } node.hasStyle(TextStyle.Block) -> div(additionalClasses) { childrenCallback() } node.isAnchorable -> buildAnchor(node.anchor!!, node.anchorLabel!!, node.sourceSetsFilters) { childrenCallback() } + node.extra[InsertTemplateExtra] != null -> node.extra[InsertTemplateExtra]?.let { templateCommand(it.command) } ?: Unit else -> childrenCallback() } } @@ -800,11 +802,7 @@ open class HtmlRenderer( } } } else a { - href = pathToRoot.split("/") - .filter { it.isNotBlank() } - .drop(1).takeIf { it.isNotEmpty() } - ?.joinToString(separator = "/", postfix = "/index.html") - ?: "index.html" + href = pathToRoot + "index.html" div { id = "logo" } diff --git a/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProvider.kt index c10029bc..5488f754 100644 --- a/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProvider.kt +++ b/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProvider.kt @@ -25,7 +25,7 @@ open class DokkaLocationProvider( page.children.forEach { registerPath(it, prefix) } } else { val newPrefix = prefix + page.pathName - put(page, newPrefix) + put(page, if (page is ModulePageNode) prefix else newPrefix) page.children.forEach { registerPath(it, newPrefix) } } diff --git a/plugins/base/src/main/kotlin/templating/InsertTemplateExtra.kt b/plugins/base/src/main/kotlin/templating/InsertTemplateExtra.kt new file mode 100644 index 00000000..b2e883f0 --- /dev/null +++ b/plugins/base/src/main/kotlin/templating/InsertTemplateExtra.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.base.templating + +import org.jetbrains.dokka.model.properties.ExtraProperty +import org.jetbrains.dokka.pages.ContentNode + +data class InsertTemplateExtra(val command: Command) : ExtraProperty { + + companion object : ExtraProperty.Key + + override val key: ExtraProperty.Key + get() = Companion +} \ No newline at end of file diff --git a/plugins/base/src/test/kotlin/resourceLinks/ResourceLinksTest.kt b/plugins/base/src/test/kotlin/resourceLinks/ResourceLinksTest.kt index 57345f5c..709274f2 100644 --- a/plugins/base/src/test/kotlin/resourceLinks/ResourceLinksTest.kt +++ b/plugins/base/src/test/kotlin/resourceLinks/ResourceLinksTest.kt @@ -62,7 +62,7 @@ class ResourceLinksTest : BaseAbstractTest() { r -> assert(it.`is`("[href=$r], [src=$r]")) } relativeResources.forEach { - r -> assert(it.`is`("[href=../$r] , [src=../$r]")) + r -> assert(it.`is`("[href=$r] , [src=$r]")) } } } diff --git a/plugins/gfm/gfm-template-processing/build.gradle.kts b/plugins/gfm/gfm-template-processing/build.gradle.kts index f95ef0e4..b401c0c7 100644 --- a/plugins/gfm/gfm-template-processing/build.gradle.kts +++ b/plugins/gfm/gfm-template-processing/build.gradle.kts @@ -4,6 +4,7 @@ dependencies { implementation(project(":plugins:base")) implementation(project(":plugins:gfm")) implementation(project(":plugins:all-modules-page")) + implementation(project(":plugins:templating")) val coroutines_version: String by project implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") diff --git a/plugins/gfm/gfm-template-processing/src/main/kotlin/org/jetbrains/dokka/gfm/templateProcessing/GfmTemplateProcessingPlugin.kt b/plugins/gfm/gfm-template-processing/src/main/kotlin/org/jetbrains/dokka/gfm/templateProcessing/GfmTemplateProcessingPlugin.kt index 7df740a5..ba08bcd2 100644 --- a/plugins/gfm/gfm-template-processing/src/main/kotlin/org/jetbrains/dokka/gfm/templateProcessing/GfmTemplateProcessingPlugin.kt +++ b/plugins/gfm/gfm-template-processing/src/main/kotlin/org/jetbrains/dokka/gfm/templateProcessing/GfmTemplateProcessingPlugin.kt @@ -6,19 +6,21 @@ import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.gfm.GfmPlugin import org.jetbrains.dokka.gfm.location.MarkdownLocationProvider import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.templates.TemplatingPlugin class GfmTemplateProcessingPlugin : DokkaPlugin() { private val allModulesPagePlugin by lazy { plugin() } + private val templateProcessingPlugin by lazy { plugin() } private val gfmPlugin by lazy { plugin() } private val dokkaBase by lazy { plugin()} val gfmTemplateProcessingStrategy by extending { - (allModulesPagePlugin.templateProcessingStrategy + (templateProcessingPlugin.templateProcessingStrategy providing ::GfmTemplateProcessingStrategy - order { before(allModulesPagePlugin.fallbackProcessingStrategy) }) + order { before(templateProcessingPlugin.fallbackProcessingStrategy) }) } val gfmLocationProvider by extending { diff --git a/plugins/gfm/gfm-template-processing/src/main/kotlin/org/jetbrains/dokka/gfm/templateProcessing/GfmTemplateProcessingStrategy.kt b/plugins/gfm/gfm-template-processing/src/main/kotlin/org/jetbrains/dokka/gfm/templateProcessing/GfmTemplateProcessingStrategy.kt index 3f2bbd3e..93ce9659 100644 --- a/plugins/gfm/gfm-template-processing/src/main/kotlin/org/jetbrains/dokka/gfm/templateProcessing/GfmTemplateProcessingStrategy.kt +++ b/plugins/gfm/gfm-template-processing/src/main/kotlin/org/jetbrains/dokka/gfm/templateProcessing/GfmTemplateProcessingStrategy.kt @@ -1,10 +1,6 @@ package org.jetbrains.dokka.gfm.templateProcessing -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import org.jetbrains.dokka.allModulesPage.AllModulesPagePlugin -import org.jetbrains.dokka.allModulesPage.templates.TemplateProcessingStrategy import org.jetbrains.dokka.base.templating.parseJson import org.jetbrains.dokka.gfm.GfmCommand import org.jetbrains.dokka.gfm.GfmCommand.Companion.command @@ -15,6 +11,7 @@ import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.plugin import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.templates.TemplateProcessingStrategy import java.io.BufferedWriter import java.io.File @@ -23,33 +20,30 @@ class GfmTemplateProcessingStrategy(val context: DokkaContext) : TemplateProcess private val externalModuleLinkResolver = context.plugin().querySingle { externalModuleLinkResolver } - override suspend fun process(input: File, output: File): Boolean = coroutineScope { + override fun process(input: File, output: File): Boolean = if (input.extension == "md") { - launch(IO) { - input.bufferedReader().use { reader -> - //This should also work whenever we have a misconfigured dokka and output is pointing to the input - //the same way that html processing does - if (input.absolutePath == output.absolutePath) { - context.logger.info("Attempting to process GFM templates in place for directory $input, this suggests miss configuration.") - val lines = reader.readLines() - output.bufferedWriter().use { writer -> - lines.forEach { line -> - writer.processAndWrite(line, output) - } - + input.bufferedReader().use { reader -> + //This should also work whenever we have a misconfigured dokka and output is pointing to the input + //the same way that html processing does + if (input.absolutePath == output.absolutePath) { + context.logger.info("Attempting to process GFM templates in place for directory $input, this suggests miss configuration.") + val lines = reader.readLines() + output.bufferedWriter().use { writer -> + lines.forEach { line -> + writer.processAndWrite(line, output) } - } else { - output.bufferedWriter().use { writer -> - reader.lineSequence().forEach { line -> - writer.processAndWrite(line, output) - } + + } + } else { + output.bufferedWriter().use { writer -> + reader.lineSequence().forEach { line -> + writer.processAndWrite(line, output) } } } } true } else false - } private fun BufferedWriter.processAndWrite(line: String, output: File) = processLine(line, output).run { diff --git a/plugins/templating/build.gradle.kts b/plugins/templating/build.gradle.kts new file mode 100644 index 00000000..6c160a9f --- /dev/null +++ b/plugins/templating/build.gradle.kts @@ -0,0 +1,18 @@ +import org.jetbrains.registerDokkaArtifactPublication + +registerDokkaArtifactPublication("templating-plugin") { + artifactId = "templating-plugin" +} + +dependencies { + implementation(project(":plugins:base")) + + val coroutines_version: String by project + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.1") + val kotlinx_html_version: String by project + implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:$kotlinx_html_version") + + implementation("org.jsoup:jsoup:1.12.1") + testImplementation(project(":plugins:base:base-test-utils")) +} \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/AddToNavigationCommandHandler.kt b/plugins/templating/src/main/kotlin/templates/AddToNavigationCommandHandler.kt new file mode 100644 index 00000000..3e7e1290 --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/AddToNavigationCommandHandler.kt @@ -0,0 +1,56 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.base.templating.AddToNavigationCommand +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.plugability.DokkaContext +import org.jsoup.nodes.Attributes +import org.jsoup.nodes.Element +import org.jsoup.parser.Tag +import java.io.File +import java.nio.file.Files +import java.util.concurrent.ConcurrentHashMap + +class AddToNavigationCommandHandler(val context: DokkaContext) : CommandHandler { + private val navigationFragments = ConcurrentHashMap() + + override fun handleCommand(element: Element, command: Command, input: File, output: File) { + command as AddToNavigationCommand + context.configuration.modules.find { it.name == command.moduleName } + ?.relativePathToOutputDirectory + ?.relativeToOrSelf(context.configuration.outputDir) + ?.let { key -> navigationFragments[key.toString()] = element } + } + + override fun canHandle(command: Command) = command is AddToNavigationCommand + + override fun finish(output: File) { + if (navigationFragments.isNotEmpty()) { + val attributes = Attributes().apply { + put("class", "sideMenu") + } + val node = Element(Tag.valueOf("div"), "", attributes) + navigationFragments.entries.sortedBy { it.key }.forEach { (moduleName, command) -> + command.select("a").forEach { a -> + a.attr("href")?.also { a.attr("href", "${moduleName}/${it}") } + } + command.childNodes().toList().forEachIndexed { index, child -> + if (index == 0) { + child.attr("id", "$moduleName-nav-submenu") + } + node.appendChild(child) + } + } + + Files.write(output.resolve("navigation.html").toPath(), listOf(node.outerHtml())) + node.select("a").forEach { a -> + a.attr("href")?.also { a.attr("href", "../${it}") } + } + navigationFragments.keys.forEach { + Files.write( + output.resolve(it).resolve("navigation.html").toPath(), + listOf(node.outerHtml()) + ) + } + } + } +} \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/CommandHandler.kt b/plugins/templating/src/main/kotlin/templates/CommandHandler.kt new file mode 100644 index 00000000..d72092a1 --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/CommandHandler.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.base.templating.Command +import org.jsoup.nodes.Element +import java.io.File + +interface CommandHandler { + fun handleCommand(element: Element, command: Command, input: File, output: File) + fun canHandle(command: Command): Boolean + fun finish(output: File) {} +} \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt b/plugins/templating/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt new file mode 100644 index 00000000..c3b9aa53 --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt @@ -0,0 +1,41 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.base.templating.parseJson +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import java.io.File +import java.nio.file.Files + +class DirectiveBasedHtmlTemplateProcessingStrategy(private val context: DokkaContext) : TemplateProcessingStrategy { + + private val directiveBasedCommandHandlers = + context.plugin().query { directiveBasedCommandHandlers } + + override fun process(input: File, output: File): Boolean = + if (input.isFile && input.extension == "html") { + val document = Jsoup.parse(input, "UTF-8") + document.outputSettings().indentAmount(0).prettyPrint(false) + document.select("dokka-template-command").forEach { + handleCommand(it, parseJson(it.attr("data")), input, output) + } + Files.write(output.toPath(), listOf(document.outerHtml())) + true + } else false + + fun handleCommand(element: Element, command: Command, input: File, output: File) { + val handlers = directiveBasedCommandHandlers.filter { it.canHandle(command) } + if (handlers.isEmpty()) + context.logger.warn("Unknown templating command $command") + else + handlers.forEach { it.handleCommand(element, command, input, output) } + + } + + override fun finish(output: File) { + directiveBasedCommandHandlers.forEach { it.finish(output) } + } +} diff --git a/plugins/templating/src/main/kotlin/templates/FallbackTemplateProcessingStrategy.kt b/plugins/templating/src/main/kotlin/templates/FallbackTemplateProcessingStrategy.kt new file mode 100644 index 00000000..4e88c318 --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/FallbackTemplateProcessingStrategy.kt @@ -0,0 +1,13 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.plugability.DokkaContext +import java.io.File +import java.nio.file.Files + +class FallbackTemplateProcessingStrategy(dokkaContext: DokkaContext) : TemplateProcessingStrategy { + + override fun process(input: File, output: File): Boolean { + if(input != output) input.copyTo(output, overwrite = true) + return true + } +} \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/JsonElementBasedTemplateProcessingStrategy.kt b/plugins/templating/src/main/kotlin/templates/JsonElementBasedTemplateProcessingStrategy.kt new file mode 100644 index 00000000..a2d55209 --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/JsonElementBasedTemplateProcessingStrategy.kt @@ -0,0 +1,68 @@ +package org.jetbrains.dokka.allModulesPage.templates + +import org.jetbrains.dokka.base.renderers.html.SearchRecord +import org.jetbrains.dokka.base.templating.AddToSearch +import org.jetbrains.dokka.base.templating.parseJson +import org.jetbrains.dokka.base.templating.toJsonString +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.templates.TemplateProcessingStrategy +import java.io.File +import java.util.concurrent.ConcurrentHashMap + +abstract class BaseJsonNavigationTemplateProcessingStrategy(val context: DokkaContext) : TemplateProcessingStrategy { + abstract val navigationFileNameWithoutExtension: String + abstract val path: String + + private val fragments = ConcurrentHashMap>() + + open fun canProcess(file: File): Boolean = + file.extension == "json" && file.nameWithoutExtension == navigationFileNameWithoutExtension + + override fun process(input: File, output: File): Boolean { + val canProcess = canProcess(input) + if (canProcess) { + runCatching { parseJson(input.readText()) }.getOrNull()?.let { command -> + context.configuration.modules.find { it.name == command.moduleName }?.relativePathToOutputDirectory + ?.relativeToOrSelf(context.configuration.outputDir) + ?.let { key -> + fragments[key.toString()] = command.elements + } + } ?: fallbackToCopy(input, output) + } + return canProcess + } + + override fun finish(output: File) { + if (fragments.isNotEmpty()) { + val content = toJsonString(fragments.entries.flatMap { (moduleName, navigation) -> + navigation.map { it.withResolvedLocation(moduleName) } + }) + output.resolve("$path/$navigationFileNameWithoutExtension.json").writeText(content) + + fragments.keys.forEach { + output.resolve(it).resolve("$path/$navigationFileNameWithoutExtension.json").writeText(content) + } + } + } + + private fun fallbackToCopy(input: File, output: File) { + context.logger.warn("Falling back to just copying file for ${input.name} even thought it should process it") + input.copyTo(output) + } + + private fun SearchRecord.withResolvedLocation(moduleName: String): SearchRecord = + copy(location = "$moduleName/$location") + +} + +class NavigationSearchTemplateStrategy(val dokkaContext: DokkaContext) : + BaseJsonNavigationTemplateProcessingStrategy(dokkaContext) { + override val navigationFileNameWithoutExtension: String = "navigation-pane" + override val path: String = "scripts" +} + +class PagesSearchTemplateStrategy(val dokkaContext: DokkaContext) : + BaseJsonNavigationTemplateProcessingStrategy(dokkaContext) { + override val navigationFileNameWithoutExtension: String = "pages" + override val path: String = "scripts" +} \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/PathToRootSubstitutor.kt b/plugins/templating/src/main/kotlin/templates/PathToRootSubstitutor.kt new file mode 100644 index 00000000..da81432e --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/PathToRootSubstitutor.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand +import org.jetbrains.dokka.base.templating.SubstitutionCommand +import org.jetbrains.dokka.plugability.DokkaContext +import java.io.File + +class PathToRootSubstitutor(private val dokkaContext: DokkaContext) : Substitutor { + + override fun trySubstitute(context: TemplatingContext, match: MatchResult): String? = + if (context.command is PathToRootSubstitutionCommand) { + context.output.toPath().parent.relativize(dokkaContext.configuration.outputDir.toPath()).toString().split(File.separator).joinToString(separator = "/", postfix = "/") { it } + } else null +} \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/SubstitutionCommandHandler.kt b/plugins/templating/src/main/kotlin/templates/SubstitutionCommandHandler.kt new file mode 100644 index 00000000..c7b15137 --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/SubstitutionCommandHandler.kt @@ -0,0 +1,60 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.base.templating.SubstitutionCommand +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import org.jsoup.nodes.DataNode +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode +import java.io.File + +class SubstitutionCommandHandler(context: DokkaContext) : CommandHandler { + + override fun handleCommand(element: Element, command: Command, input: File, output: File) { + command as SubstitutionCommand + substitute(element, TemplatingContext(input, output, element, command)) + } + + override fun canHandle(command: Command): Boolean = command is SubstitutionCommand + + private val substitutors = context.plugin().query { substitutor } + + private fun findSubstitution(commandContext: TemplatingContext, match: MatchResult): String = + substitutors.asSequence().mapNotNull { it.trySubstitute(commandContext, match) }.firstOrNull() ?: match.value + + private fun substitute(element: Element, commandContext: TemplatingContext) { + val regex = commandContext.command.pattern.toRegex() + element.children().forEach { it.traverseToSubstitute(regex, commandContext) } + + val childrenCopy = element.children().toList() + val position = element.elementSiblingIndex() + val parent = element.parent() + element.remove() + + parent.insertChildren(position, childrenCopy) + } + + private fun Node.traverseToSubstitute(regex: Regex, commandContext: TemplatingContext) { + when (this) { + is TextNode -> replaceWith(TextNode(wholeText.substitute(regex, commandContext))) + is DataNode -> replaceWith(DataNode(wholeData.substitute(regex, commandContext))) + is Element -> { + attributes().forEach { attr(it.key, it.value.substitute(regex, commandContext)) } + childNodes().forEach { it.traverseToSubstitute(regex, commandContext) } + } + } + } + + private fun String.substitute(regex: Regex, commandContext: TemplatingContext) = buildString { + var lastOffset = 0 + regex.findAll(this@substitute).forEach { match -> + append(this@substitute, lastOffset, match.range.first) + append(findSubstitution(commandContext, match)) + lastOffset = match.range.last + 1 + } + append(this@substitute, lastOffset, this@substitute.length) + } +} \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/Substitutor.kt b/plugins/templating/src/main/kotlin/templates/Substitutor.kt new file mode 100644 index 00000000..55463974 --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/Substitutor.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.base.templating.SubstitutionCommand + +fun interface Substitutor { + fun trySubstitute(context: TemplatingContext, match: MatchResult): String? +} \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/TemplateProcessor.kt b/plugins/templating/src/main/kotlin/templates/TemplateProcessor.kt new file mode 100644 index 00000000..8fbd76b6 --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/TemplateProcessor.kt @@ -0,0 +1,56 @@ +package org.jetbrains.dokka.templates + +import kotlinx.coroutines.* +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 +import org.jsoup.nodes.Element +import java.io.File + +interface TemplateProcessor { + fun process() +} + +interface TemplateProcessingStrategy { + fun process(input: File, output: File): Boolean + fun finish(output: File) {} +} + +class DefaultTemplateProcessor( + private val context: DokkaContext, +): TemplateProcessor { + + private val strategies: List = context.plugin().query { templateProcessingStrategy } + + override fun process() = runBlocking(Dispatchers.Default) { + coroutineScope { + context.configuration.modules.forEach { + launch { + it.sourceOutputDirectory.visit(context.configuration.outputDir.resolve(it.relativePathToOutputDirectory)) + } + } + } + strategies.map { it.finish(context.configuration.outputDir) } + Unit + } + + private suspend fun File.visit(target: File): Unit = coroutineScope { + val source = this@visit + if (source.isDirectory) { + target.mkdir() + source.list()?.forEach { + launch { source.resolve(it).visit(target.resolve(it)) } + } + } else { + strategies.first { it.process(source, target) } + } + } +} + +data class TemplatingContext( + val input: File, + val output: File, + val element: Element, + val command: T, +) \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/TemplatingPlugin.kt b/plugins/templating/src/main/kotlin/templates/TemplatingPlugin.kt new file mode 100644 index 00000000..29ca4904 --- /dev/null +++ b/plugins/templating/src/main/kotlin/templates/TemplatingPlugin.kt @@ -0,0 +1,50 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.allModulesPage.templates.NavigationSearchTemplateStrategy +import org.jetbrains.dokka.allModulesPage.templates.PagesSearchTemplateStrategy +import org.jetbrains.dokka.plugability.DokkaPlugin + +class TemplatingPlugin : DokkaPlugin() { + + val templateProcessor by extensionPoint() + val templateProcessingStrategy by extensionPoint() + val directiveBasedCommandHandlers by extensionPoint() + + val substitutor by extensionPoint() + + val defaultTemplateProcessor by extending { + templateProcessor providing ::DefaultTemplateProcessor + } + + val directiveBasedHtmlTemplateProcessingStrategy by extending { + templateProcessingStrategy providing ::DirectiveBasedHtmlTemplateProcessingStrategy order { + before(fallbackProcessingStrategy) + } + } + val navigationSearchTemplateStrategy by extending { + templateProcessingStrategy providing ::NavigationSearchTemplateStrategy order { + before(fallbackProcessingStrategy) + } + } + + val pagesSearchTemplateStrategy by extending { + templateProcessingStrategy providing ::PagesSearchTemplateStrategy order { + before(fallbackProcessingStrategy) + } + } + + val fallbackProcessingStrategy by extending { + templateProcessingStrategy providing ::FallbackTemplateProcessingStrategy + } + + val pathToRootSubstitutor by extending { + substitutor providing ::PathToRootSubstitutor + } + + val addToNavigationCommandHandler by extending { + directiveBasedCommandHandlers providing ::AddToNavigationCommandHandler + } + val substitutionCommandHandler by extending { + directiveBasedCommandHandlers providing ::SubstitutionCommandHandler + } +} \ No newline at end of file diff --git a/plugins/templating/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/plugins/templating/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..38e2d1bf --- /dev/null +++ b/plugins/templating/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.templates.TemplatingPlugin \ No newline at end of file diff --git a/plugins/templating/src/test/kotlin/templates/AddToNavigationCommandResolutionTest.kt b/plugins/templating/src/test/kotlin/templates/AddToNavigationCommandResolutionTest.kt new file mode 100644 index 00000000..19c940c2 --- /dev/null +++ b/plugins/templating/src/test/kotlin/templates/AddToNavigationCommandResolutionTest.kt @@ -0,0 +1,137 @@ +package org.jetbrains.dokka.templates + +import kotlinx.html.a +import kotlinx.html.div +import kotlinx.html.id +import kotlinx.html.span +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import org.jetbrains.dokka.base.renderers.html.templateCommand +import org.jetbrains.dokka.base.templating.AddToNavigationCommand +import org.jetbrains.dokka.plugability.DokkaContext +import org.junit.Rule +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.junit.rules.TemporaryFolder +import utils.assertHtmlEqualsIgnoringWhitespace + +class AddToNavigationCommandResolutionTest : TemplatingAbstractTest() { + @get:Rule + val folder: TemporaryFolder = TemporaryFolder() + + @Test + fun `should substitute AddToNavigationCommand in root directory`() = + addToNavigationTest { + val output = folder.root.resolve("navigation.html").readText() + val expected = expectedOutput( + ModuleWithPrefix("module1"), + ModuleWithPrefix("module2") + ) + assertHtmlEqualsIgnoringWhitespace(expected, output) + } + + @ParameterizedTest + @ValueSource(strings = ["module1", "module2"]) + fun `should substitute AddToNavigationCommand in modules directory`(moduleName: String) = + addToNavigationTest { + val output = folder.root.resolve(moduleName).resolve("navigation.html").readText() + val expected = expectedOutput( + ModuleWithPrefix("module1", ".."), + ModuleWithPrefix("module2", "..") + ) + assertHtmlEqualsIgnoringWhitespace(expected, output) + } + + private fun expectedOutput(vararg modulesWithPrefix: ModuleWithPrefix) = createHTML(prettyPrint = true) + .div("sideMenu") { + modulesWithPrefix.forEach { (moduleName, prefix) -> + val relativePrefix = prefix?.let { "$it/" } ?: "" + div("sideMenuPart") { + id = "$moduleName-nav-submenu" + div("overview") { + a { + href = "$relativePrefix$moduleName/module-page.html" + span { + +"module-$moduleName" + } + } + } + div("sideMenuPart") { + id = "$moduleName-nav-submenu-0" + div("overview") { + a { + href = "$relativePrefix$moduleName/$moduleName/package-page.html" + span { + +"package-$moduleName" + } + } + } + } + } + } + } + + private fun inputForModule(moduleName: String) = createHTML() + .templateCommand(AddToNavigationCommand(moduleName)) { + div("sideMenuPart") { + id = "$moduleName-nav-submenu" + div("overview") { + a { + href = "module-page.html" + span { + +"module-$moduleName" + } + } + } + div("sideMenuPart") { + id = "$moduleName-nav-submenu-0" + div("overview") { + a { + href = "$moduleName/package-page.html" + span { + +"package-$moduleName" + } + } + } + } + } + } + + private fun addToNavigationTest(test: (DokkaContext) -> Unit) { + folder.create() + val module1 = folder.newFolder("module1") + val module2 = folder.newFolder("module2") + + val configuration = dokkaConfiguration { + modules = listOf( + DokkaModuleDescriptionImpl( + name = "module1", + relativePathToOutputDirectory = module1, + includes = emptySet(), + sourceOutputDirectory = module1, + ), + DokkaModuleDescriptionImpl( + name = "module2", + relativePathToOutputDirectory = module2, + includes = emptySet(), + sourceOutputDirectory = module2, + ), + ) + this.outputDir = folder.root + } + + val module1Navigation = module1.resolve("navigation.html") + module1Navigation.writeText(inputForModule("module1")) + val module2Navigation = module2.resolve("navigation.html") + module2Navigation.writeText(inputForModule("module2")) + + testFromData(configuration, preserveOutputLocation = true) { + submoduleProcessingStage = { ctx -> + test(ctx) + } + } + } + + private data class ModuleWithPrefix(val moduleName: String, val prefix: String? = null) +} \ No newline at end of file diff --git a/plugins/templating/src/test/kotlin/templates/AddToSearchCommandResolutionTest.kt b/plugins/templating/src/test/kotlin/templates/AddToSearchCommandResolutionTest.kt new file mode 100644 index 00000000..a962d524 --- /dev/null +++ b/plugins/templating/src/test/kotlin/templates/AddToSearchCommandResolutionTest.kt @@ -0,0 +1,89 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import org.jetbrains.dokka.base.renderers.html.SearchRecord +import org.jetbrains.dokka.base.templating.AddToSearch +import org.jetbrains.dokka.base.templating.parseJson +import org.jetbrains.dokka.base.templating.toJsonString +import org.junit.Rule +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.assertEquals + +class AddToSearchCommandResolutionTest : TemplatingAbstractTest() { + companion object { + val elements = listOf( + SearchRecord(name = "name1", location = "location1"), + SearchRecord(name = "name2", location = "location2") + ) + val fromModule1 = AddToSearch( + moduleName = "module1", + elements = elements + ) + val fromModule2 = AddToSearch( + moduleName = "module2", + elements = elements + ) + } + + @get:Rule + val folder: TemporaryFolder = TemporaryFolder() + + @ParameterizedTest + @ValueSource(strings = ["navigation-pane.json", "pages.json"]) + fun `should merge navigation templates`(fileName: String) { + val (module1Navigation, module2Navigation) = setupTestDirectoriesWithContent(fileName) + + val outputDir = folder.root + val configuration = dokkaConfiguration { + modules = listOf( + DokkaModuleDescriptionImpl( + name = "module1", + relativePathToOutputDirectory = folder.root.resolve("module1"), + includes = emptySet(), + sourceOutputDirectory = folder.root.resolve("module1"), + ), + DokkaModuleDescriptionImpl( + name = "module2", + relativePathToOutputDirectory = folder.root.resolve("module2"), + includes = emptySet(), + sourceOutputDirectory = folder.root.resolve("module2"), + ), + ) + this.outputDir = outputDir + } + + testFromData(configuration, preserveOutputLocation = true) { + submoduleProcessingStage = { _ -> + val expected = elements.map { it.copy(location = "module1/${it.location}") } + + elements.map { it.copy(location = "module2/${it.location}") } + + val output = + parseJson>(outputDir.resolve("scripts/${fileName}").readText()) + assertEquals(expected, output.sortedBy { it.location }) + + val outputFromModule1 = parseJson>(module1Navigation.readText()) + assertEquals(expected, outputFromModule1.sortedBy { it.location }) + + val outputFromModule2 = parseJson>(module2Navigation.readText()) + assertEquals(expected, outputFromModule2.sortedBy { it.location }) + } + } + } + + private fun setupTestDirectoriesWithContent(fileName: String): List { + folder.create() + val scriptsForModule1 = folder.newFolder("module1", "scripts") + val scriptsForModule2 = folder.newFolder("module2", "scripts") + folder.newFolder("scripts") + + val module1Navigation = scriptsForModule1.resolve(fileName) + module1Navigation.writeText(toJsonString(fromModule1)) + val module2Navigation = scriptsForModule2.resolve(fileName) + module2Navigation.writeText(toJsonString(fromModule2)) + + return listOf(module1Navigation, module2Navigation) + } +} \ No newline at end of file diff --git a/plugins/templating/src/test/kotlin/templates/SubstitutionCommandResolutionTest.kt b/plugins/templating/src/test/kotlin/templates/SubstitutionCommandResolutionTest.kt new file mode 100644 index 00000000..d7143f11 --- /dev/null +++ b/plugins/templating/src/test/kotlin/templates/SubstitutionCommandResolutionTest.kt @@ -0,0 +1,69 @@ +package org.jetbrains.dokka.templates + +import kotlinx.html.a +import kotlinx.html.div +import kotlinx.html.id +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import org.jetbrains.dokka.base.renderers.html.templateCommand +import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.junit.jupiter.api.Test +import utils.assertHtmlEqualsIgnoringWhitespace +import java.io.File + +class SubstitutionCommandResolutionTest : TemplatingAbstractTest() { + + @get:Rule + val folder: TemporaryFolder = TemporaryFolder() + + @Test + fun `should handle PathToRootCommand`() { + val template = createHTML() + .templateCommand(PathToRootSubstitutionCommand(pattern = "###", default = "default")) { + a { + href = "###index.html" + div { + id = "logo" + } + } + } + + val expected = createHTML().a { + href = "../index.html" + div { + id = "logo" + } + } + + val testedFile = createDirectoriesAndWriteContent(template) + + val configuration = dokkaConfiguration { + modules = listOf( + DokkaModuleDescriptionImpl( + name = "module1", + relativePathToOutputDirectory = folder.root.resolve("module1"), + includes = emptySet(), + sourceOutputDirectory = folder.root.resolve("module1"), + ) + ) + this.outputDir = folder.root + } + + testFromData(configuration, preserveOutputLocation = true){ + submoduleProcessingStage = { + assertHtmlEqualsIgnoringWhitespace(expected, testedFile.readText()) + } + } + } + + private fun createDirectoriesAndWriteContent(content: String): File { + folder.create() + val module1 = folder.newFolder("module1") + val module1Content = module1.resolve("index.html") + module1Content.writeText(content) + return module1Content + } + +} \ No newline at end of file diff --git a/plugins/templating/src/test/kotlin/templates/TemplatingDokkaTestGenerator.kt b/plugins/templating/src/test/kotlin/templates/TemplatingDokkaTestGenerator.kt new file mode 100644 index 00000000..906fb1ea --- /dev/null +++ b/plugins/templating/src/test/kotlin/templates/TemplatingDokkaTestGenerator.kt @@ -0,0 +1,63 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaGenerator +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.testApi.logger.TestLogger +import org.jetbrains.dokka.testApi.testRunner.AbstractTest +import org.jetbrains.dokka.testApi.testRunner.DokkaTestGenerator +import org.jetbrains.dokka.testApi.testRunner.TestBuilder +import org.jetbrains.dokka.testApi.testRunner.TestMethods +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.DokkaLogger + +class TemplatingDokkaTestGenerator( + configuration: DokkaConfiguration, + logger: DokkaLogger, + testMethods: TemplatingTestMethods, + additionalPlugins: List = emptyList() +) : DokkaTestGenerator( + configuration, + logger, + testMethods, + additionalPlugins + TemplatingPlugin() + TestTemplatingPlugin() +) { + override fun generate() = with(testMethods) { + val dokkaGenerator = DokkaGenerator(configuration, logger) + + val context = + dokkaGenerator.initializePlugins(configuration, logger, additionalPlugins) + + pluginsSetupStage(context) + + val generation = context.single(CoreExtensions.generation) as TestTemplatingGeneration + + generation.processSubmodules() + submoduleProcessingStage(context) + } + +} + +open class TemplatingTestMethods( + open val pluginsSetupStage: (DokkaContext) -> Unit, + open val submoduleProcessingStage: (DokkaContext) -> Unit, +) : TestMethods + +class TemplatingTestBuilder : TestBuilder() { + var pluginsSetupStage: (DokkaContext) -> Unit = {} + var submoduleProcessingStage: (DokkaContext) -> Unit = {} + + override fun build() = TemplatingTestMethods( + pluginsSetupStage, + submoduleProcessingStage, + ) +} + +abstract class TemplatingAbstractTest(logger: TestLogger = TestLogger(DokkaConsoleLogger)) : + AbstractTest( + ::TemplatingTestBuilder, + ::TemplatingDokkaTestGenerator, + logger, + ) diff --git a/plugins/templating/src/test/kotlin/templates/TestTemplatingGeneration.kt b/plugins/templating/src/test/kotlin/templates/TestTemplatingGeneration.kt new file mode 100644 index 00000000..0a5bae4b --- /dev/null +++ b/plugins/templating/src/test/kotlin/templates/TestTemplatingGeneration.kt @@ -0,0 +1,22 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.Timer +import org.jetbrains.dokka.generation.Generation +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle + +class TestTemplatingGeneration(context: DokkaContext): Generation { + + val templatingPlugin by lazy { context.plugin() } + + override fun Timer.generate() { + report("Processing submodules") + processSubmodules() + } + + fun processSubmodules() = + templatingPlugin.querySingle { templateProcessor }.process() + + override val generationName = "test template generation" +} \ No newline at end of file diff --git a/plugins/templating/src/test/kotlin/templates/TestTemplatingPlugin.kt b/plugins/templating/src/test/kotlin/templates/TestTemplatingPlugin.kt new file mode 100644 index 00000000..1ed961b8 --- /dev/null +++ b/plugins/templating/src/test/kotlin/templates/TestTemplatingPlugin.kt @@ -0,0 +1,16 @@ +package org.jetbrains.dokka.templates + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.plugability.DokkaPlugin + +class TestTemplatingPlugin: DokkaPlugin() { + + val dokkaBase by lazy { plugin() } + + val allModulesPageGeneration by extending { + (CoreExtensions.generation + providing ::TestTemplatingGeneration + override dokkaBase.singleGeneration) + } +} \ No newline at end of file diff --git a/plugins/versioning/build.gradle.kts b/plugins/versioning/build.gradle.kts new file mode 100644 index 00000000..39cc0bdf --- /dev/null +++ b/plugins/versioning/build.gradle.kts @@ -0,0 +1,19 @@ +import org.jetbrains.registerDokkaArtifactPublication + +registerDokkaArtifactPublication("versioning-plugin") { + artifactId = "versioning-plugin" +} + +dependencies { + implementation(project(":plugins:base")) + implementation(project(":plugins:templating")) + + val coroutines_version: String by project + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.1") + val kotlinx_html_version: String by project + implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:$kotlinx_html_version") + + implementation("org.jsoup:jsoup:1.12.1") + implementation("org.apache.maven:maven-artifact:3.6.3") +} \ No newline at end of file diff --git a/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionCommandConsumer.kt b/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionCommandConsumer.kt new file mode 100644 index 00000000..2577b2da --- /dev/null +++ b/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionCommandConsumer.kt @@ -0,0 +1,45 @@ +package org.jetbrains.dokka.versioning + +import kotlinx.html.unsafe +import kotlinx.html.visit +import kotlinx.html.visitAndFinalize +import org.jetbrains.dokka.base.renderers.html.TemplateBlock +import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer +import org.jetbrains.dokka.base.renderers.html.templateCommandFor +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle + +class ReplaceVersionCommandConsumer(context: DokkaContext) : ImmediateHtmlCommandConsumer { + + private val versionsNavigationCreator = + context.plugin().querySingle { versionsNavigationCreator } + + override fun canProcess(command: Command) = command is ReplaceVersionsCommand + + override fun processCommand( + command: Command, + block: TemplateBlock, + tagConsumer: ImmediateResolutionTagConsumer + ) { + command as ReplaceVersionsCommand + templateCommandFor(command, tagConsumer).visit { + unsafe { + +versionsNavigationCreator() + } + } + } + + override fun processCommandAndFinalize( + command: Command, + block: TemplateBlock, + tagConsumer: ImmediateResolutionTagConsumer + ): R = + templateCommandFor(command, tagConsumer).visitAndFinalize(tagConsumer) { + unsafe { + +versionsNavigationCreator() + } + } +} \ No newline at end of file diff --git a/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionsCommand.kt b/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionsCommand.kt new file mode 100644 index 00000000..2d2c8e36 --- /dev/null +++ b/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionsCommand.kt @@ -0,0 +1,26 @@ +package org.jetbrains.dokka.versioning + + +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.templates.CommandHandler +import org.jsoup.nodes.Element +import java.io.File + +object ReplaceVersionsCommand : Command + +class ReplaceVersionCommandHandler(context: DokkaContext) : CommandHandler { + + val versionsNavigationCreator by lazy { + context.plugin().querySingle { versionsNavigationCreator } + } + + override fun canHandle(command: Command): Boolean = command is ReplaceVersionsCommand + + override fun handleCommand(element: Element, command: Command, input: File, output: File) { + element.empty() + element.append(versionsNavigationCreator(output)) + } +} \ No newline at end of file diff --git a/plugins/versioning/src/main/kotlin/versioning/VersioningConfiguration.kt b/plugins/versioning/src/main/kotlin/versioning/VersioningConfiguration.kt new file mode 100644 index 00000000..98eef33d --- /dev/null +++ b/plugins/versioning/src/main/kotlin/versioning/VersioningConfiguration.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.versioning + +import org.jetbrains.dokka.plugability.ConfigurableBlock +import org.jetbrains.dokka.plugability.DokkaContext +import java.io.File + +data class VersioningConfiguration( + var olderVersionsDir: File? = defaultOlderVersionsDir, + var versionsOrdering: List? = defaultVersionsOrdering, + var version: String? = defaultVersion, +) : ConfigurableBlock { + fun versionFromConfigurationOrModule(dokkaContext: DokkaContext): String = + version ?: dokkaContext.configuration.moduleVersion ?: "1.0" + + companion object { + val defaultOlderVersionsDir: File? = null + val defaultVersionsOrdering: List? = null + val defaultVersion = null + } +} \ No newline at end of file diff --git a/plugins/versioning/src/main/kotlin/versioning/VersioningHandler.kt b/plugins/versioning/src/main/kotlin/versioning/VersioningHandler.kt new file mode 100644 index 00000000..699c87e6 --- /dev/null +++ b/plugins/versioning/src/main/kotlin/versioning/VersioningHandler.kt @@ -0,0 +1,101 @@ +package org.jetbrains.dokka.versioning + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import kotlinx.coroutines.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.configuration +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import org.jetbrains.dokka.templates.TemplateProcessingStrategy +import org.jetbrains.dokka.templates.TemplatingPlugin +import java.io.File + +interface VersioningHandler : () -> Unit { + fun getVersions(): Map + fun currentVersion(): File? +} + +typealias VersionId = String + +class DefaultVersioningHandler(val context: DokkaContext) : VersioningHandler { + + private val mapper = ObjectMapper() + + private lateinit var versions: Map + + private val processingStrategies: List = + context.plugin().query { templateProcessingStrategy } + + private val configuration = configuration(context) + + override fun getVersions() = versions + + override fun currentVersion() = configuration?.let { versionsConfiguration -> + versions[versionsConfiguration.versionFromConfigurationOrModule(context)] + } + + override fun invoke() { + configuration?.let { versionsConfiguration -> + versions = + mapOf(versionsConfiguration.versionFromConfigurationOrModule(context) to context.configuration.outputDir) + versionsConfiguration.olderVersionsDir?.let { + handlePreviousVersions(it, context.configuration.outputDir) + } + mapper.writeValue( + context.configuration.outputDir.resolve(VERSIONS_FILE), + Version(versionsConfiguration.versionFromConfigurationOrModule(context)) + ) + } + } + + private fun handlePreviousVersions(olderVersionDir: File, output: File): Map { + assert(olderVersionDir.isDirectory) { "Supplied previous version $olderVersionDir is not a directory!" } + return versionsWithOriginDir(olderVersionDir) + .also { fetched -> + versions = versions + fetched.map { (key, _) -> + key to output.resolve(OLDER_VERSIONS_DIR).resolve(key) + }.toMap() + } + .onEach { (version, path) -> copyVersion(version, path, output) }.toMap() + } + + private fun versionsWithOriginDir(olderVersionRootDir: File) = + olderVersionRootDir.listFiles().orEmpty().mapNotNull { versionDir -> + versionDir.listFiles { _, name -> name == VERSIONS_FILE }?.firstOrNull()?.let { file -> + val versionsContent = mapper.readValue(file) + Pair(versionsContent.version, versionDir) + }.also { + if (it == null) context.logger.warn("Failed to find versions file named $VERSIONS_FILE in $versionDir") + } + } + + private fun copyVersion(version: VersionId, versionRoot: File, output: File) { + val targetParent = output.resolve(OLDER_VERSIONS_DIR).resolve(version).apply { mkdirs() } + runBlocking(Dispatchers.Default) { + coroutineScope { + versionRoot.listFiles().orEmpty().forEach { versionRootContent -> + launch { + if (versionRootContent.isDirectory) versionRootContent.copyRecursively( + targetParent.resolve(versionRootContent.name), + overwrite = true + ) + else processingStrategies.first { + it.process(versionRootContent, targetParent.resolve(versionRootContent.name)) + } + } + } + } + } + } + + private data class Version( + @JsonProperty("version") val version: String, + ) + + companion object { + private const val OLDER_VERSIONS_DIR = "older" + private const val VERSIONS_FILE = "version.json" + } +} \ No newline at end of file diff --git a/plugins/versioning/src/main/kotlin/versioning/VersioningPlugin.kt b/plugins/versioning/src/main/kotlin/versioning/VersioningPlugin.kt new file mode 100644 index 00000000..9c20a128 --- /dev/null +++ b/plugins/versioning/src/main/kotlin/versioning/VersioningPlugin.kt @@ -0,0 +1,39 @@ +package org.jetbrains.dokka.versioning + +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.configuration +import org.jetbrains.dokka.templates.TemplatingPlugin + +class VersioningPlugin : DokkaPlugin() { + + val versioningHandler by extensionPoint() + val versionsNavigationCreator by extensionPoint() + val versionsOrdering by extensionPoint() + + private val dokkaBase by lazy { plugin() } + private val templatingPlugin by lazy { plugin() } + + val defaultVersioningHandler by extending { + versioningHandler providing ::DefaultVersioningHandler + } + val defaultVersioningNavigationCreator by extending { + versionsNavigationCreator providing ::HtmlVersionsNavigationCreator + } + val replaceVersionCommandHandler by extending { + templatingPlugin.directiveBasedCommandHandlers providing ::ReplaceVersionCommandHandler + } + val resolveLinkConsumer by extending { + dokkaBase.immediateHtmlCommandConsumer providing ::ReplaceVersionCommandConsumer + } + val cssStyleInstaller by extending { + dokkaBase.htmlPreprocessors with MultiModuleStylesInstaller order { after(dokkaBase.assetsInstaller) } + } + val versionsDefaultOrdering by extending { + versionsOrdering providing { ctx -> + configuration(ctx)?.versionsOrdering?.let { + ByConfigurationVersionOrdering(ctx) + } ?: SemVerVersionOrdering() + } + } +} \ No newline at end of file diff --git a/plugins/versioning/src/main/kotlin/versioning/VersionsNavigationCreator.kt b/plugins/versioning/src/main/kotlin/versioning/VersionsNavigationCreator.kt new file mode 100644 index 00000000..76653d47 --- /dev/null +++ b/plugins/versioning/src/main/kotlin/versioning/VersionsNavigationCreator.kt @@ -0,0 +1,53 @@ +package org.jetbrains.dokka.versioning + +import kotlinx.html.a +import kotlinx.html.button +import kotlinx.html.div +import kotlinx.html.i +import kotlinx.html.stream.appendHTML +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import java.io.File +import java.nio.file.Files.isDirectory +import java.nio.file.Path + +interface VersionsNavigationCreator { + operator fun invoke(): String + operator fun invoke(output: File): String +} + +class HtmlVersionsNavigationCreator(val context: DokkaContext) : VersionsNavigationCreator { + + private val versioningHandler by lazy { context.plugin().querySingle { versioningHandler } } + + private val versionsOrdering by lazy { context.plugin().querySingle { versionsOrdering } } + + override fun invoke(): String = + versioningHandler.currentVersion()?.let { invoke(it) }.orEmpty() + + override fun invoke(output: File): String { + val position = output.takeIf { it.isDirectory } ?: output.parentFile + return versioningHandler.getVersions() + .let { versions -> versionsOrdering.order(versions.keys.toList()).map { it to versions[it] } } + .takeIf { it.isNotEmpty() } + ?.let { versions -> + StringBuilder().appendHTML().div(classes = "versions-dropdown") { + button(classes = "versions-dropdown-button") { + versions.first { (_, versionLocation) -> versionLocation?.absolutePath == position.absolutePath } + .let { (version, _) -> + text(version) + } + i(classes = "fa fa-caret-down") + } + div(classes = "versions-dropdown-data") { + versions.forEach { (version, path) -> + a(href = path?.resolve("index.html")?.toRelativeString(position)) { + text(version) + } + } + } + }.toString() + }.orEmpty() + } +} \ No newline at end of file diff --git a/plugins/versioning/src/main/kotlin/versioning/VersionsOrdering.kt b/plugins/versioning/src/main/kotlin/versioning/VersionsOrdering.kt new file mode 100644 index 00000000..f72e2df6 --- /dev/null +++ b/plugins/versioning/src/main/kotlin/versioning/VersionsOrdering.kt @@ -0,0 +1,23 @@ +package org.jetbrains.dokka.versioning + +import org.apache.maven.artifact.versioning.ComparableVersion +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.configuration +import org.jetbrains.dokka.versioning.VersionId +import org.jetbrains.dokka.versioning.VersioningConfiguration +import org.jetbrains.dokka.versioning.VersioningPlugin + +fun interface VersionsOrdering { + fun order(records: List): List +} + +class ByConfigurationVersionOrdering(val dokkaContext: DokkaContext) : VersionsOrdering { + override fun order(records: List): List = + configuration(dokkaContext)?.versionsOrdering + ?: throw IllegalStateException("Attempted to use a configuration ordering without providing configuration") +} + +class SemVerVersionOrdering : VersionsOrdering { + override fun order(records: List): List = + records.map { it to ComparableVersion(it) }.sortedByDescending { it.second }.map { it.first } +} \ No newline at end of file diff --git a/plugins/versioning/src/main/kotlin/versioning/htmlPreprocessors.kt b/plugins/versioning/src/main/kotlin/versioning/htmlPreprocessors.kt new file mode 100644 index 00000000..5852ba9e --- /dev/null +++ b/plugins/versioning/src/main/kotlin/versioning/htmlPreprocessors.kt @@ -0,0 +1,24 @@ +package org.jetbrains.dokka.versioning + +import org.jetbrains.dokka.pages.RendererSpecificResourcePage +import org.jetbrains.dokka.pages.RenderingStrategy +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.transformers.pages.PageTransformer + +object MultiModuleStylesInstaller : PageTransformer { + private val stylesPages = listOf( + "styles/multimodule.css", + ) + + override fun invoke(input: RootPageNode): RootPageNode = + input.modified( + children = input.children + stylesPages.toRenderSpecificResourcePage() + ).transformContentPagesTree { + it.modified( + embeddedResources = it.embeddedResources + stylesPages + ) + } +} + +private fun List.toRenderSpecificResourcePage(): List = + map { RendererSpecificResourcePage(it, emptyList(), RenderingStrategy.Copy("/dokka/$it")) } diff --git a/plugins/versioning/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/plugins/versioning/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..1b0f1e3e --- /dev/null +++ b/plugins/versioning/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.versioning.VersioningPlugin \ No newline at end of file diff --git a/plugins/versioning/src/main/resources/dokka/styles/multimodule.css b/plugins/versioning/src/main/resources/dokka/styles/multimodule.css new file mode 100644 index 00000000..4334d759 --- /dev/null +++ b/plugins/versioning/src/main/resources/dokka/styles/multimodule.css @@ -0,0 +1,37 @@ +.versions-dropdown { + float: right; +} + +.versions-dropdown-button { + border: none; + cursor: pointer; + padding: 5px; +} + +.versions-dropdown-button:hover, +.versions-dropdown-button:focus { + background-color: #f1f1f1; +} + +.versions-dropdown-data { + display: none; + position: absolute; + background-color: #f1f1f1; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +.versions-dropdown-data a { + padding: 5px; + text-decoration: none; + display: block; + color: black; +} + +.versions-dropdown-data a:hover { + background-color: #ddd +} + +.versions-dropdown:hover .versions-dropdown-data { + display: block; +} \ No newline at end of file diff --git a/runners/gradle-plugin/src/test/kotlin/org/jetbrains/dokka/gradle/AndroidAutoConfigurationTest.kt b/runners/gradle-plugin/src/test/kotlin/org/jetbrains/dokka/gradle/AndroidAutoConfigurationTest.kt index 76215762..455a906b 100644 --- a/runners/gradle-plugin/src/test/kotlin/org/jetbrains/dokka/gradle/AndroidAutoConfigurationTest.kt +++ b/runners/gradle-plugin/src/test/kotlin/org/jetbrains/dokka/gradle/AndroidAutoConfigurationTest.kt @@ -69,11 +69,13 @@ class AndroidAutoConfigurationTest { dokkaTasks.flatMap { it.dokkaSourceSets }.forEach { sourceSet -> /* + There is no better way of checking for empty classpath at the moment (without resolving dependencies). We assume, that an empty classpath can be resolved We assume, that a non-empty classpath will not be able to resolve (no repositories defined) */ + assertFailsWith { sourceSet.classpath.files } } } -} +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f9b348d0..b9433391 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,8 @@ include("plugins:base:frontend") include("plugins:base:search-component") include("plugins:base:base-test-utils") include("plugins:all-modules-page") +include("plugins:templating") +include("plugins:versioning") include("plugins:mathjax") include("plugins:gfm") -- cgit