diff options
Diffstat (limited to 'dokka-runners/dokkatoo/buildSrc/src')
20 files changed, 1387 insertions, 0 deletions
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/android-setup.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/android-setup.gradle.kts new file mode 100644 index 00000000..ed22d799 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/android-setup.gradle.kts @@ -0,0 +1,78 @@ +package buildsrc.conventions + +import org.jetbrains.kotlin.util.suffixIfNot + + +/** + * Utilities for preparing Android projects + */ + +plugins { + base + id("buildsrc.conventions.base") +} + + +val androidSdkDirPath: Provider<String> = providers + // first try getting the SDK installed on via GitHub step setup-android + .environmentVariable("ANDROID_SDK_ROOT").map(::File) + // else get the project-local SDK + .orElse(layout.projectDirectory.file("projects/ANDROID_SDK").asFile) + .map { it.invariantSeparatorsPath } + + +val createAndroidLocalPropertiesFile by tasks.registering { + + val localPropertiesFile = temporaryDir.resolve("local.properties") + outputs.file(localPropertiesFile).withPropertyName("localPropertiesFile") + + val androidSdkDirPath = androidSdkDirPath + inputs.property("androidSdkDirPath", androidSdkDirPath) + + doLast { + localPropertiesFile.apply { + parentFile.mkdirs() + createNewFile() + writeText( + """ + |# DO NOT EDIT - Generated by $path + | + |sdk.dir=${androidSdkDirPath.get()} + | + """.trimMargin() + ) + } + } +} + + +val updateAndroidLocalProperties by tasks.registering { + + // find all local.properties files + val localPropertiesFiles = layout.projectDirectory.dir("projects") + .asFileTree + .matching { include("**/local.properties") } + .files + + outputs.files(localPropertiesFiles).withPropertyName("localPropertiesFiles") + + val androidSdkDirPath = androidSdkDirPath + inputs.property("androidSdkDirPath", androidSdkDirPath) + + doLast { + localPropertiesFiles + .filter { it.exists() } + .forEach { file -> + file.writeText( + file.useLines { lines -> + lines.joinToString("\n") { line -> + when { + line.startsWith("sdk.dir=") -> "sdk.dir=${androidSdkDirPath.get()}" + else -> line + } + }.suffixIfNot("\n") + } + ) + } + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts new file mode 100644 index 00000000..60bfa2fe --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts @@ -0,0 +1,155 @@ +package buildsrc.conventions + +import java.time.Duration +import org.gradle.api.tasks.testing.logging.TestLogEvent + +/** + * A convention plugin that sets up common config and sensible defaults for all subprojects. + */ + +plugins { + base +} + +if (project != rootProject) { + project.version = rootProject.version + project.group = rootProject.group +} + +tasks.withType<AbstractArchiveTask>().configureEach { + // https://docs.gradle.org/current/userguide/working_with_files.html#sec:reproducible_archives + isPreserveFileTimestamps = false + isReproducibleFileOrder = true +} + +tasks.withType<AbstractTestTask>().configureEach { + timeout.set(Duration.ofMinutes(60)) + + testLogging { + showCauses = true + showExceptions = true + showStackTraces = true + showStandardStreams = true + events( + TestLogEvent.PASSED, + TestLogEvent.FAILED, + TestLogEvent.SKIPPED, + TestLogEvent.STARTED, + TestLogEvent.STANDARD_ERROR, + TestLogEvent.STANDARD_OUT, + ) + } +} + +tasks.withType<AbstractCopyTask>().configureEach { + includeEmptyDirs = false +} + +val updateTestReportCss by tasks.registering { + description = "Hack so the Gradle test reports have dark mode" + // the CSS is based on https://github.com/gradle/gradle/pull/12177 + + mustRunAfter(tasks.withType<Test>()) + mustRunAfter(tasks.withType<TestReport>()) + + val cssFiles = layout.buildDirectory.asFileTree.matching { + include("reports/**/css/base-style.css") + include("reports/**/css/style.css") + } + + outputs.files(cssFiles.files) + + doLast { + cssFiles.forEach { cssFile -> + val fileContent = cssFile.readText() + + if ("/* Dark mode */" in fileContent) { + return@forEach + } else { + when (cssFile.name) { + "base-style.css" -> cssFile.writeText( + fileContent + """ + + /* Dark mode */ + @media (prefers-color-scheme: dark) { + html { + background: black; + } + body, a, a:visited { + color: #E7E7E7FF; + } + #footer, #footer a { + color: #cacaca; + } + ul.tabLinks li { + border: solid 1px #cacaca; + background-color: #151515; + } + ul.tabLinks li:hover { + background-color: #383838; + } + ul.tabLinks li.selected { + background-color: #002d32; + border-color: #007987; + } + div.tab th, div.tab table { + border-bottom: solid #d0d0d0 1px; + } + span.code pre { + background-color: #0a0a0a; + border: solid 1px #5f5f5f; + } + } + """.trimIndent() + ) + + "style.css" -> cssFile.writeText( + fileContent + """ + + /* Dark mode */ + @media (prefers-color-scheme: dark) { + .breadcrumbs, .breadcrumbs a { + color: #9b9b9b; + } + #successRate, .summaryGroup { + border: solid 2px #d0d0d0; + } + .success, .success a { + color: #7fff7f; + } + div.success, #successRate.success { + background-color: #001c00; + border-color: #7fff7f; + } + .failures, .failures a { + color: #a30000; + } + .skipped, .skipped a { + color: #a26d13; + } + div.failures, #successRate.failures { + background-color: #170000; + border-color: #a30000; + } + } + """.trimIndent() + ) + } + } + } + } +} + +tasks.withType<Test>().configureEach { + finalizedBy(updateTestReportCss) +} + +tasks.withType<TestReport>().configureEach { + finalizedBy(updateTestReportCss) +} + +tasks.matching { it.name == "validatePlugins" }.configureEach { + // prevent warning + // Task ':validatePlugins' uses this output of task ':updateTestReportCss' without declaring an explicit or implicit dependency. + mustRunAfter(updateTestReportCss) +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokka-source-downloader.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokka-source-downloader.gradle.kts new file mode 100644 index 00000000..69e384e1 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokka-source-downloader.gradle.kts @@ -0,0 +1,68 @@ +package buildsrc.conventions + +import buildsrc.settings.DokkaSourceDownloaderSettings +import buildsrc.utils.asConsumer +import buildsrc.utils.asProvider +import buildsrc.utils.dropDirectories +import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE +import org.gradle.kotlin.dsl.support.serviceOf + +plugins { + id("buildsrc.conventions.base") +} + +val dsdExt: DokkaSourceDownloaderSettings = extensions.create<DokkaSourceDownloaderSettings>( + DokkaSourceDownloaderSettings.EXTENSION_NAME +) + +val kotlinDokkaSource by configurations.creating<Configuration> { + asConsumer() + attributes { + attribute(USAGE_ATTRIBUTE, objects.named("externals-dokka-src")) + } +} + +val kotlinDokkaSourceElements by configurations.registering { + asProvider() + attributes { + attribute(USAGE_ATTRIBUTE, objects.named("externals-dokka-src")) + } +} + +dependencies { + kotlinDokkaSource(dsdExt.dokkaVersion.map { "kotlin:dokka:$it@zip" }) +} + +val prepareDokkaSource by tasks.registering(Sync::class) { + group = "dokka setup" + description = "Download & unpack Kotlin Dokka source code" + + inputs.property("dokkaVersion", dsdExt.dokkaVersion).optional(false) + + val archives = serviceOf<ArchiveOperations>() + + from( + kotlinDokkaSource.incoming + .artifacts + .resolvedArtifacts + .map { artifacts -> + artifacts.map { archives.zipTree(it.file) } + } + ) { + // drop the first dir (dokka-$version) + eachFile { + relativePath = relativePath.dropDirectories(1) + } + } + + into(temporaryDir) + + exclude( + "*.github", + "*.gradle", + "**/gradlew", + "**/gradlew.bat", + "**/gradle/wrapper/gradle-wrapper.jar", + "**/gradle/wrapper/gradle-wrapper.properties", + ) +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects-base.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects-base.gradle.kts new file mode 100644 index 00000000..5c2c45fa --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects-base.gradle.kts @@ -0,0 +1,27 @@ +package buildsrc.conventions + +import buildsrc.utils.asConsumer +import buildsrc.utils.asProvider + +plugins { + id("buildsrc.conventions.base") +} + + +val exampleProjectsAttribute: Attribute<String> = + Attribute.of("example-projects", String::class.java) + +dependencies.attributesSchema { + attribute(exampleProjectsAttribute) +} + + +val exampleProjects by configurations.registering { + asConsumer() + attributes { attribute(exampleProjectsAttribute, "dokka") } +} + +val exampleProjectsElements by configurations.registering { + asProvider() + attributes { attribute(exampleProjectsAttribute, "dokka") } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects.gradle.kts new file mode 100644 index 00000000..c6994a83 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects.gradle.kts @@ -0,0 +1,160 @@ +package buildsrc.conventions + +import buildsrc.settings.* +import buildsrc.tasks.* +import buildsrc.utils.* + +plugins { + id("buildsrc.conventions.base") + id("buildsrc.conventions.dokka-source-downloader") + id("buildsrc.conventions.maven-publish-test") + id("buildsrc.conventions.dokkatoo-example-projects-base") +} + +val mavenPublishTestExtension = extensions.getByType<MavenPublishTestSettings>() +val dokkaTemplateProjectSettings = + extensions.create<DokkaTemplateProjectSettings>( + DokkaTemplateProjectSettings.EXTENSION_NAME, + { project.copySpec() } + ).apply { + this.destinationBaseDir.convention(layout.projectDirectory) + } + +val prepareDokkaSource by tasks.existing(Sync::class) + +dokkaTemplateProjectSettings.dokkaSourceDir.convention( + prepareDokkaSource.flatMap { + layout.dir(providers.provider { + it.destinationDir + }) + } +) + +tasks.withType<SetupDokkaProjects>().configureEach { + dependsOn(prepareDokkaSource) + + dokkaSourceDir.convention(dokkaTemplateProjectSettings.dokkaSourceDir) + destinationBaseDir.convention(dokkaTemplateProjectSettings.destinationBaseDir) + + templateProjects.addAllLater(provider { + dokkaTemplateProjectSettings.templateProjects + }) +} + +val setupDokkaTemplateProjects by tasks.registering(SetupDokkaProjects::class) + +fun createDokkatooExampleProjectsSettings( + projectDir: Directory = project.layout.projectDirectory +): DokkatooExampleProjectsSettings { + return extensions.create<DokkatooExampleProjectsSettings>( + DokkatooExampleProjectsSettings.EXTENSION_NAME + ).apply { + + // find all Gradle settings files + val settingsFiles = projectDir.asFileTree + .matching { + include( + "**/*dokkatoo*/**/settings.gradle.kts", + "**/*dokkatoo*/**/settings.gradle", + ) + }.files + + // for each settings file, create a DokkatooExampleProjectSpec + settingsFiles.forEach { + val destinationDir = it.parentFile + val name = destinationDir.toRelativeString(projectDir.asFile).toAlphaNumericCamelCase() + exampleProjects.register(name) { + this.exampleProjectDir.set(destinationDir) + } + } + + exampleProjects.configureEach { + gradlePropertiesContent.add( + mavenPublishTestExtension.testMavenRepoPath.map { testMavenRepoPath -> + "testMavenRepo=$testMavenRepoPath" + } + ) + } + } +} + +val dokkatooExampleProjectsSettings = createDokkatooExampleProjectsSettings() + +val updateDokkatooExamplesGradleProperties by tasks.registering( + UpdateDokkatooExampleProjects::class +) { + group = DokkatooExampleProjectsSettings.TASK_GROUP + + mustRunAfter(tasks.withType<SetupDokkaProjects>()) + + exampleProjects.addAllLater(providers.provider { + dokkatooExampleProjectsSettings.exampleProjects + }) +} + +val dokkatooVersion = provider { project.version.toString() } + +val updateDokkatooExamplesBuildFiles by tasks.registering { + group = DokkatooExampleProjectsSettings.TASK_GROUP + description = "Update the Gradle build files in the Dokkatoo examples" + + outputs.upToDateWhen { false } + + mustRunAfter(tasks.withType<SetupDokkaProjects>()) + shouldRunAfter(updateDokkatooExamplesGradleProperties) + + val dokkatooVersion = dokkatooVersion + + val dokkatooDependencyVersionMatcher = """ + \"dev\.adamko\.dokkatoo\:dokkatoo\-plugin\:([^"]+?)\" + """.trimIndent().toRegex() + + val dokkatooPluginVersionMatcher = """ + id[^"]+?"dev\.adamko\.dokkatoo".+?version "([^"]+?)" + """.trimIndent().toRegex() + + val gradleBuildFiles = + layout.projectDirectory.asFileTree + .matching { + include( + "**/*dokkatoo*/**/build.gradle.kts", + "**/*dokkatoo*/**/build.gradle", + ) + }.elements + outputs.files(gradleBuildFiles) + + doLast { + gradleBuildFiles.get().forEach { fileLocation -> + val file = fileLocation.asFile + if (file.exists()) { + file.writeText( + file.readText() + .replace(dokkatooPluginVersionMatcher) { + val oldVersion = it.groupValues[1] + it.value.replace(oldVersion, dokkatooVersion.get()) + } + .replace(dokkatooDependencyVersionMatcher) { + val oldVersion = it.groupValues[1] + it.value.replace(oldVersion, dokkatooVersion.get()) + } + ) + } + } + } +} + + +val updateDokkatooExamples by tasks.registering { + group = DokkatooExampleProjectsSettings.TASK_GROUP + description = "lifecycle task for all '${DokkatooExampleProjectsSettings.TASK_GROUP}' tasks" + dependsOn( + setupDokkaTemplateProjects, + updateDokkatooExamplesGradleProperties, + updateDokkatooExamplesBuildFiles, + ) +} + +tasks.assemble { + dependsOn(updateDokkatooExamples) + dependsOn(setupDokkaTemplateProjects) +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/gradle-plugin-variants.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/gradle-plugin-variants.gradle.kts new file mode 100644 index 00000000..1d9fc43b --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/gradle-plugin-variants.gradle.kts @@ -0,0 +1,44 @@ +package buildsrc.conventions + +import org.gradle.api.attributes.plugin.GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE + +plugins { + id("buildsrc.conventions.base") + `java-gradle-plugin` +} + +fun registerGradleVariant(name: String, gradleVersion: String) { + val variantSources = sourceSets.create(name) + + java { + registerFeature(variantSources.name) { + usingSourceSet(variantSources) + capability("${project.group}", "${project.name}", "${project.version}") + + withJavadocJar() + withSourcesJar() + } + } + + configurations + .matching { it.isCanBeConsumed && it.name.startsWith(variantSources.name) } + .configureEach { + attributes { + attribute(GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, objects.named(gradleVersion)) + } + } + + tasks.named<Copy>(variantSources.processResourcesTaskName) { + val copyPluginDescriptors = rootSpec.addChild() + copyPluginDescriptors.into("META-INF/gradle-plugins") +// copyPluginDescriptors.into(tasks.pluginDescriptors.flatMap { it.outputDirectory }) + copyPluginDescriptors.from(tasks.pluginDescriptors) + } + + dependencies { + add(variantSources.compileOnlyConfigurationName, gradleApi()) + } +} + +registerGradleVariant("gradle7", "7.6") +registerGradleVariant("gradle8", "8.0") diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/java-base.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/java-base.gradle.kts new file mode 100644 index 00000000..203b80f2 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/java-base.gradle.kts @@ -0,0 +1,19 @@ +package buildsrc.conventions + +import org.gradle.api.JavaVersion +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.`java-base` + +plugins { + id("buildsrc.conventions.base") + `java` +} + +extensions.getByType<JavaPluginExtension>().apply { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } + withSourcesJar() +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/kotlin-gradle-plugin.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/kotlin-gradle-plugin.gradle.kts new file mode 100644 index 00000000..4174088a --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/kotlin-gradle-plugin.gradle.kts @@ -0,0 +1,37 @@ +package buildsrc.conventions + +plugins { + id("buildsrc.conventions.base") + id("buildsrc.conventions.java-base") + id("org.gradle.kotlin.kotlin-dsl") + id("com.gradle.plugin-publish") +} + +tasks.validatePlugins { + enableStricterValidation.set(true) +} + +val createJavadocJarReadme by tasks.registering(Sync::class) { + description = "generate a readme.txt for the Javadoc JAR" + from( + resources.text.fromString( + """ + This Javadoc JAR is intentionally empty. + + For documentation, see the sources JAR or https://github.com/adamko-dev/dokkatoo/ + + """.trimIndent() + ) + ) { + rename { "readme.txt" } + } + into(temporaryDir) +} + + +// The Gradle Publish Plugin enables the Javadoc JAR in afterEvaluate, so find it lazily +tasks.withType<Jar>() + .matching { it.name == "javadocJar" } + .configureEach { + from(createJavadocJarReadme) + } diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publish-test.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publish-test.gradle.kts new file mode 100644 index 00000000..38678b5b --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publish-test.gradle.kts @@ -0,0 +1,93 @@ +package buildsrc.conventions + +import buildsrc.settings.MavenPublishTestSettings +import buildsrc.utils.* + + +/** Utility for publishing a project to a local Maven directory for use in integration tests. */ + +plugins { + base +} + +val Gradle.rootGradle: Gradle get() = generateSequence(gradle) { it.parent }.last() + +val mavenPublishTestExtension = extensions.create<MavenPublishTestSettings>( + "mavenPublishTest", + gradle.rootGradle.rootProject.layout.buildDirectory.dir("test-maven-repo"), +) + + +val publishToTestMavenRepo by tasks.registering { + group = PublishingPlugin.PUBLISH_TASK_GROUP + description = "Publishes all Maven publications to the test Maven repository." +} + + +plugins.withType<MavenPublishPlugin>().all { + extensions + .getByType<PublishingExtension>() + .publications + .withType<MavenPublication>().all publication@{ + val publicationName = this@publication.name + val installTaskName = "publish${publicationName.uppercaseFirstChar()}PublicationToTestMavenRepo" + + // Register a publication task for each publication. + // Use PublishToMavenLocal, because the PublishToMavenRepository task will *always* create + // a new jar, even if nothing has changed, and append a timestamp, which results in a large + // directory and tasks are never up-to-date. + // PublishToMavenLocal does not append a timestamp, so the target directory is smaller, and + // up-to-date checks work. + val installTask = tasks.register<PublishToMavenLocal>(installTaskName) { + description = "Publishes Maven publication '$publicationName' to the test Maven repository." + group = PublishingPlugin.PUBLISH_TASK_GROUP + outputs.cacheIf { true } + publication = this@publication + val destinationDir = mavenPublishTestExtension.testMavenRepo.get().asFile + inputs.property("testMavenRepoTempDir", destinationDir.invariantSeparatorsPath) + doFirst { + /** + * `maven.repo.local` will set the destination directory for this [PublishToMavenLocal] task. + * + * @see org.gradle.api.internal.artifacts.mvnsettings.DefaultLocalMavenRepositoryLocator.getLocalMavenRepository + */ + System.setProperty("maven.repo.local", destinationDir.absolutePath) + } + } + + publishToTestMavenRepo.configure { + dependsOn(installTask) + } + + tasks.check { + mustRunAfter(installTask) + } + } +} + + +val testMavenPublication by configurations.registering { + asConsumer() + attributes { + attribute(MavenPublishTestSettings.attribute, "testMavenRepo") + } +} + +val testMavenPublicationElements by configurations.registering { + asProvider() + extendsFrom(testMavenPublication.get()) + attributes { + attribute(MavenPublishTestSettings.attribute, "testMavenRepo") + } + outgoing { + artifact(mavenPublishTestExtension.testMavenRepo) { + builtBy(publishToTestMavenRepo) + } + } +} + +dependencies { + attributesSchema { + attribute(MavenPublishTestSettings.attribute) + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publishing.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publishing.gradle.kts new file mode 100644 index 00000000..7af7b69f --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publishing.gradle.kts @@ -0,0 +1,137 @@ +package buildsrc.conventions + +import buildsrc.settings.MavenPublishingSettings + +plugins { + `maven-publish` + signing +} + +val mavenPublishing = + extensions.create<MavenPublishingSettings>(MavenPublishingSettings.EXTENSION_NAME, project) + + +//region POM convention +publishing { + publications.withType<MavenPublication>().configureEach { + pom { + name.convention("Dokkatoo") + description.convention("Dokkatoo is a Gradle plugin that generates documentation for your Kotlin projects") + url.convention("https://github.com/adamko-dev/dokkatoo") + + scm { + connection.convention("scm:git:https://github.com/adamko-dev/dokkatoo") + developerConnection.convention("scm:git:https://github.com/adamko-dev/dokkatoo") + url.convention("https://github.com/adamko-dev/dokkatoo") + } + + licenses { + license { + name.convention("Apache-2.0") + url.convention("https://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + + developers { + developer { + email.set("adam@adamko.dev") + } + } + } + } +} +//endregion + + +//region GitHub branch publishing +publishing { + repositories { + maven(mavenPublishing.githubPublishDir) { + name = "GitHubPublish" + } + } +} +//endregion + + +//region Maven Central publishing/signing +publishing { + repositories { + val mavenCentralUsername = mavenPublishing.mavenCentralUsername.orNull + val mavenCentralPassword = mavenPublishing.mavenCentralPassword.orNull + if (!mavenCentralUsername.isNullOrBlank() && !mavenCentralPassword.isNullOrBlank()) { + maven(mavenPublishing.sonatypeReleaseUrl) { + name = "SonatypeRelease" + credentials { + username = mavenCentralUsername + password = mavenCentralPassword + } + } + } + } + + // com.gradle.plugin-publish automatically adds a Javadoc jar +} + +signing { + logger.info("maven-publishing.gradle.kts enabled signing for ${project.path}") + + val keyId = mavenPublishing.signingKeyId.orNull + val key = mavenPublishing.signingKey.orNull + val password = mavenPublishing.signingPassword.orNull + + if (!keyId.isNullOrBlank() && !key.isNullOrBlank() && !password.isNullOrBlank()) { + useInMemoryPgpKeys(keyId, key, password) + } + + setRequired({ + gradle.taskGraph.allTasks.filterIsInstance<PublishToMavenRepository>().any { + it.repository.name == "SonatypeRelease" + } + }) +} + +//afterEvaluate { +// com.gradle.plugin-publish automatically signs tasks in a weird way, that stops this from working: +// signing { +// sign(publishing.publications) +// } +//} +//endregion + + +//region Fix Gradle warning about signing tasks using publishing task outputs without explicit dependencies +// https://youtrack.jetbrains.com/issue/KT-46466 https://github.com/gradle/gradle/issues/26091 +tasks.withType<AbstractPublishToMaven>().configureEach { + val signingTasks = tasks.withType<Sign>() + mustRunAfter(signingTasks) +} +//endregion + + +//region publishing logging +tasks.withType<AbstractPublishToMaven>().configureEach { + val publicationGAV = provider { publication?.run { "$group:$artifactId:$version" } } + doLast("log publication GAV") { + if (publicationGAV.isPresent) { + logger.lifecycle("[task: ${path}] ${publicationGAV.get()}") + } + } +} +//endregion + + +//region IJ workarounds +// manually define the Kotlin DSL accessors because IntelliJ _still_ doesn't load them properly +fun Project.publishing(configure: PublishingExtension.() -> Unit): Unit = + extensions.configure(configure) + +val Project.publishing: PublishingExtension + get() = extensions.getByType() + +fun Project.signing(configure: SigningExtension.() -> Unit): Unit = + extensions.configure(configure) + +val Project.signing: SigningExtension + get() = extensions.getByType() +//endregion diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaSourceDownloaderSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaSourceDownloaderSettings.kt new file mode 100644 index 00000000..c3f9906c --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaSourceDownloaderSettings.kt @@ -0,0 +1,13 @@ +package buildsrc.settings + +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.Property + +abstract class DokkaSourceDownloaderSettings : ExtensionAware { + + abstract val dokkaVersion: Property<String> + + companion object { + const val EXTENSION_NAME = "dokkaSourceDownload" + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaTemplateProjectSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaTemplateProjectSettings.kt new file mode 100644 index 00000000..7bacafb9 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaTemplateProjectSettings.kt @@ -0,0 +1,96 @@ +package buildsrc.settings + +import buildsrc.utils.adding +import buildsrc.utils.domainObjectContainer +import buildsrc.utils.toAlphaNumericCamelCase +import javax.inject.Inject +import org.gradle.api.Named +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.CopySpec +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.kotlin.dsl.* + +private typealias TemplateProjectsContainer = NamedDomainObjectContainer<DokkaTemplateProjectSettings.DokkaTemplateProjectSpec> + +abstract class DokkaTemplateProjectSettings @Inject constructor( + private val objects: ObjectFactory, + private val copySpecs: () -> CopySpec +) : ExtensionAware { + + /** Directory that will contain the projects downloaded from the Dokka source code. */ + abstract val dokkaSourceDir: DirectoryProperty + + abstract val destinationBaseDir: DirectoryProperty + + internal val templateProjects: TemplateProjectsContainer = + // create an extension so Gradle will generate DSL accessors + extensions.adding("templateProjects", objects.domainObjectContainer { name -> + objects.newInstance<DokkaTemplateProjectSpec>(name, copySpecs()) + }) + + /** + * Copy a directory from the Dokka source project into a local directory. + * + * @param[source] Source dir, relative to [templateProjectsDir] + * @param[destination] Destination dir, relative to [destinationBaseDir] + */ + fun register( + source: String, + destination: String, + configure: DokkaTemplateProjectSpec.() -> Unit = {}, + ) { + val name = source.toAlphaNumericCamelCase() + templateProjects.register(name) { + this.sourcePath.set(source) + this.destinationPath.set(destination) + configure() + } + } + + fun configureEach(configure: DokkaTemplateProjectSpec.() -> Unit) { + templateProjects.configureEach(configure) + } + + /** + * Details for how to copy a Dokka template project from the Dokka project to a local directory. + */ + abstract class DokkaTemplateProjectSpec @Inject constructor( + private val named: String, + @get:Internal + internal val copySpec: CopySpec, + ) : Named { + + @get:Input + abstract val sourcePath: Property<String> + + @get:Input + @get:Optional + abstract val destinationPath: Property<String> + + @get:Input + abstract val additionalPaths: SetProperty<String> + + @get:InputFiles + abstract val additionalFiles: ConfigurableFileCollection + + fun configureCopy(configure: CopySpec.() -> Unit) { + copySpec.configure() + } + + @Input + override fun getName(): String = named + } + + companion object { + const val EXTENSION_NAME = "dokkaTemplateProjects" + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkatooExampleProjectsSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkatooExampleProjectsSettings.kt new file mode 100644 index 00000000..a3124904 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkatooExampleProjectsSettings.kt @@ -0,0 +1,62 @@ +package buildsrc.settings + +import buildsrc.utils.adding +import buildsrc.utils.domainObjectContainer +import javax.inject.Inject +import org.gradle.api.Named +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile + +/** + * Settings for the [buildsrc.conventions.Dokkatoo_example_projects_gradle] convention plugin + */ +abstract class DokkatooExampleProjectsSettings @Inject constructor( + objects: ObjectFactory, +) : ExtensionAware { + + val exampleProjects: NamedDomainObjectContainer<DokkatooExampleProjectSpec> = + // create an extension so Gradle will generate DSL accessors + extensions.adding("exampleProjects", objects.domainObjectContainer()) + + abstract class DokkatooExampleProjectSpec( + private val name: String + ): Named { + + /** The `gradle.properties` file of the example project */ + @get:OutputFile + val gradlePropertiesFile: Provider<RegularFile> + get() = exampleProjectDir.file("gradle.properties") + + /** The directory that contains the example project */ + @get:Internal + abstract val exampleProjectDir: DirectoryProperty + + /** + * Content to add to the `gradle.properties` file. + * + * Elements may span multiple lines. + * + * Elements will be sorted before appending to the file (to improve caching & reproducibility). + */ + @get:Input + @get:Optional + abstract val gradlePropertiesContent: ListProperty<String> + + @Input + override fun getName(): String = name + } + + companion object { + const val TASK_GROUP = "dokkatoo examples" + const val EXTENSION_NAME = "dokkatooExampleProjects" + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishTestSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishTestSettings.kt new file mode 100644 index 00000000..0a701986 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishTestSettings.kt @@ -0,0 +1,19 @@ +package buildsrc.settings + +import org.gradle.api.attributes.Attribute +import org.gradle.api.file.Directory +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.Provider + +/** + * Settings for the [buildsrc.conventions.Maven_publish_test_gradle] convention plugin. + */ +abstract class MavenPublishTestSettings( + val testMavenRepo: Provider<Directory> +) : ExtensionAware { + val testMavenRepoPath: Provider<String> = testMavenRepo.map { it.asFile.invariantSeparatorsPath } + + companion object { + val attribute = Attribute.of("maven-publish-test", String::class.java) + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishingSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishingSettings.kt new file mode 100644 index 00000000..9ec28faa --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishingSettings.kt @@ -0,0 +1,68 @@ +package buildsrc.settings + +import java.io.File +import javax.inject.Inject +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.kotlin.dsl.* + + +/** + * Settings for the [buildsrc.conventions.Maven_publish_test_gradle] convention plugin. + */ +abstract class MavenPublishingSettings @Inject constructor( + private val project: Project, + private val providers: ProviderFactory, +) { + + private val isReleaseVersion: Provider<Boolean> = + providers.provider { !project.version.toString().endsWith("-SNAPSHOT") } + + val sonatypeReleaseUrl: Provider<String> = + isReleaseVersion.map { isRelease -> + if (isRelease) { + "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + } else { + "https://s01.oss.sonatype.org/content/repositories/snapshots/" + } + } + + val mavenCentralUsername: Provider<String> = + d2Prop("mavenCentralUsername") + .orElse(providers.environmentVariable("MAVEN_SONATYPE_USERNAME")) + val mavenCentralPassword: Provider<String> = + d2Prop("mavenCentralPassword") + .orElse(providers.environmentVariable("MAVEN_SONATYPE_PASSWORD")) + + val signingKeyId: Provider<String> = + d2Prop("signing.keyId") + .orElse(providers.environmentVariable("MAVEN_SONATYPE_SIGNING_KEY_ID")) + val signingKey: Provider<String> = + d2Prop("signing.key") + .orElse(providers.environmentVariable("MAVEN_SONATYPE_SIGNING_KEY")) + val signingPassword: Provider<String> = + d2Prop("signing.password") + .orElse(providers.environmentVariable("MAVEN_SONATYPE_SIGNING_PASSWORD")) + + val githubPublishDir: Provider<File> = + providers.environmentVariable("GITHUB_PUBLISH_DIR").map { File(it) } + + private fun d2Prop(name: String): Provider<String> = + providers.gradleProperty("org.jetbrains.dokka.dokkatoo.$name") + + private fun <T : Any> d2Prop(name: String, convert: (String) -> T): Provider<T> = + d2Prop(name).map(convert) + + companion object { + const val EXTENSION_NAME = "mavenPublishing" + + /** Retrieve the [KayrayBuildProperties] extension. */ + internal val Project.mavenPublishing: MavenPublishingSettings + get() = extensions.getByType() + + /** Configure the [KayrayBuildProperties] extension. */ + internal fun Project.mavenPublishing(configure: MavenPublishingSettings.() -> Unit) = + extensions.configure(configure) + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/SetupDokkaProjects.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/SetupDokkaProjects.kt new file mode 100644 index 00000000..d473d287 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/SetupDokkaProjects.kt @@ -0,0 +1,73 @@ +package buildsrc.tasks + +import buildsrc.settings.DokkaTemplateProjectSettings.DokkaTemplateProjectSpec +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.file.ProjectLayout +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.* + +abstract class SetupDokkaProjects @Inject constructor( + private val fs: FileSystemOperations, + private val layout: ProjectLayout, + private val providers: ProviderFactory, +) : DefaultTask() { + + @get:OutputDirectories + val destinationDirs: FileCollection + get() = layout.files( + destinationBaseDir.map { base -> + templateProjects.map { spec -> base.dir(spec.destinationPath) } + } + ) + + @get:Internal // tracked by destinationDirs + abstract val destinationBaseDir: DirectoryProperty + + @get:Nested + abstract val templateProjects: NamedDomainObjectContainer<DokkaTemplateProjectSpec> + + @get:InputDirectory + abstract val dokkaSourceDir: DirectoryProperty + + @get:InputFiles + val additionalFiles: FileCollection + get() = layout.files( + providers.provider { + templateProjects.map { it.additionalFiles } + } + ) + + init { + group = "dokka examples" + } + + @TaskAction + internal fun action() { + val dokkaSourceDir = dokkaSourceDir.get() + val destinationBaseDir = destinationBaseDir.get() + val templateProjects = templateProjects.filter { it.destinationPath.isPresent } + + templateProjects.forEach { spec -> + fs.sync { + with(spec.copySpec) + + from(dokkaSourceDir.dir(spec.sourcePath)) + + from( + spec.additionalPaths.get().map { additionalPath -> + dokkaSourceDir.asFile.resolve(additionalPath) + } + ) + + from(spec.additionalFiles) + + into(destinationBaseDir.dir(spec.destinationPath)) + } + } + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/UpdateDokkatooExampleProjects.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/UpdateDokkatooExampleProjects.kt new file mode 100644 index 00000000..7737e098 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/UpdateDokkatooExampleProjects.kt @@ -0,0 +1,49 @@ +package buildsrc.tasks + +import buildsrc.settings.DokkatooExampleProjectsSettings.DokkatooExampleProjectSpec +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.TaskAction + +/** + * Utility for updating the `gradle.properties` of projects used in automated tests. + */ +@CacheableTask +abstract class UpdateDokkatooExampleProjects @Inject constructor( + @get:Internal + val objects: ObjectFactory +) : DefaultTask() { + + @get:Nested + abstract val exampleProjects: NamedDomainObjectContainer<DokkatooExampleProjectSpec> + + private val taskPath: String = path // renamed for clarity + + @TaskAction + fun update() { + exampleProjects.forEach { exampleProject -> + updateGradleProperties(exampleProject) + } + } + + private fun updateGradleProperties(exampleProject: DokkatooExampleProjectSpec) { + + val gradlePropertiesContent = exampleProject.gradlePropertiesContent.orNull?.sorted() ?: return + + val content = buildString { + appendLine("# DO NOT EDIT - Generated by $taskPath") + appendLine() + + gradlePropertiesContent.forEach { + appendLine(it) + } + } + + exampleProject.gradlePropertiesFile.get().asFile.writeText(content) + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/gradle.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/gradle.kt new file mode 100644 index 00000000..0af662d4 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/gradle.kt @@ -0,0 +1,118 @@ +package buildsrc.utils + +import java.io.File +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.NamedDomainObjectFactory +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.component.AdhocComponentWithVariants +import org.gradle.api.file.RelativePath +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.ExtensionContainer +import org.gradle.kotlin.dsl.* + +/** + * Mark this [Configuration] as one that will be consumed by other subprojects. + * + * ``` + * isCanBeResolved = false + * isCanBeConsumed = true + * ``` + */ +fun Configuration.asProvider( + visible: Boolean = true +) { + isVisible = visible + isCanBeResolved = false + isCanBeConsumed = true +} + +/** + * Mark this [Configuration] as one that will consume artifacts from other subprojects (also known as 'resolving') + * + * ``` + * isCanBeResolved = true + * isCanBeConsumed = false + * ``` + * */ +fun Configuration.asConsumer( + visible: Boolean = false +) { + isVisible = visible + isCanBeResolved = true + isCanBeConsumed = false +} + + +/** Drop the first [count] directories from the path */ +fun RelativePath.dropDirectories(count: Int): RelativePath = + RelativePath(true, *segments.drop(count).toTypedArray()) + + +/** Drop the first directory from the path */ +fun RelativePath.dropDirectory(): RelativePath = + dropDirectories(1) + + +/** Drop the first directory from the path */ +fun RelativePath.dropDirectoriesWhile( + segmentPrediate: (segment: String) -> Boolean +): RelativePath = + RelativePath( + true, + *segments.dropWhile(segmentPrediate).toTypedArray(), + ) + + +/** + * Don't publish test fixtures (which causes warnings when publishing) + * + * https://docs.gradle.org/current/userguide/java_testing.html#publishing_test_fixtures + */ +fun Project.skipTestFixturesPublications() { + val javaComponent = components["java"] as AdhocComponentWithVariants + javaComponent.withVariantsFromConfiguration(configurations["testFixturesApiElements"]) { skip() } + javaComponent.withVariantsFromConfiguration(configurations["testFixturesRuntimeElements"]) { skip() } +} + + +/** + * Add an extension to the [ExtensionContainer], and return the value. + * + * Adding an extension is especially useful for improving the DSL in build scripts when [T] is a + * [NamedDomainObjectContainer]. + * Using an extension will allow Gradle to generate + * [type-safe model accessors](https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:accessor_applicability) + * for added types. + * + * ([name] should match the property name. This has to be done manually. I tried using a + * delegated-property provider but then Gradle can't introspect the types properly, so it fails to + * create accessors). + */ +internal inline fun <reified T : Any> ExtensionContainer.adding( + name: String, + value: T, +): T { + add<T>(name, value) + return value +} + +/** + * Create a new [NamedDomainObjectContainer], using + * [org.gradle.kotlin.dsl.domainObjectContainer] + * (but [T] is `reified`). + * + * @param[factory] an optional factory for creating elements + * @see org.gradle.kotlin.dsl.domainObjectContainer + */ +internal inline fun <reified T : Any> ObjectFactory.domainObjectContainer( + factory: NamedDomainObjectFactory<T>? = null +): NamedDomainObjectContainer<T> = + if (factory == null) { + domainObjectContainer(T::class) + } else { + domainObjectContainer(T::class, factory) + } + +/** workaround for the overly verbose replacement for the deprecated [Project.getBuildDir] property */ +val Project.buildDir_: File get() = layout.buildDirectory.get().asFile diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/intellij.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/intellij.kt new file mode 100644 index 00000000..f93e7683 --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/intellij.kt @@ -0,0 +1,45 @@ +package buildsrc.utils + +import org.gradle.api.Project +import org.gradle.api.file.ProjectLayout +import org.gradle.plugins.ide.idea.model.IdeaModule + + +/** exclude generated Gradle code, so it doesn't clog up search results */ +fun IdeaModule.excludeGeneratedGradleDsl(layout: ProjectLayout) { + + val generatedSrcDirs = listOf( + "kotlin-dsl-accessors", + "kotlin-dsl-external-plugin-spec-builders", + "kotlin-dsl-plugins", + ) + + excludeDirs.addAll( + layout.projectDirectory.asFile.walk() + .filter { it.isDirectory && it.parentFile.name in generatedSrcDirs } + .flatMap { file -> + file.walk().maxDepth(1).filter { it.isDirectory }.toList() + } + ) +} + + +/** Sets a logo for project IDEs */ +fun Project.initIdeProjectLogo( + svgLogoPath: String +) { + val logoSvg = rootProject.layout.projectDirectory.file(svgLogoPath) + val ideaDir = rootProject.layout.projectDirectory.dir(".idea") + + if ( + logoSvg.asFile.exists() + && ideaDir.asFile.exists() + && !ideaDir.file("icon.png").asFile.exists() + && !ideaDir.file("icon.svg").asFile.exists() + ) { + copy { + from(logoSvg) { rename { "icon.svg" } } + into(ideaDir) + } + } +} diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/strings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/strings.kt new file mode 100644 index 00000000..6a0749ce --- /dev/null +++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/strings.kt @@ -0,0 +1,26 @@ +package buildsrc.utils + + +/** Title case the first char of a string */ +internal fun String.uppercaseFirstChar(): String = mapFirstChar(Character::toTitleCase) + + +/** Lowercase the first char of a string */ +internal fun String.lowercaseFirstChar(): String = mapFirstChar(Character::toLowerCase) + + +private inline fun String.mapFirstChar( + transform: (Char) -> Char +): String = if (isNotEmpty()) transform(this[0]) + substring(1) else this + + +/** + * Exclude all non-alphanumeric characters and converts the result into a camelCase string. + */ +internal fun String.toAlphaNumericCamelCase(): String = + map { if (it.isLetterOrDigit()) it else ' ' } + .joinToString("") + .split(" ") + .filter { it.isNotBlank() } + .joinToString("") { it.uppercaseFirstChar() } + .lowercaseFirstChar() |