All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.javiersc.gradle.plugins.docs.DocsPlugin.kt Maven / Gradle / Ivy

There is a newer version: 0.1.0-rc.45
Show newest version
package com.javiersc.gradle.plugins.docs

import com.javiersc.gradle.plugins.core.endWithNewLine
import com.javiersc.gradle.plugins.core.removeDuplicateEmptyLines
import com.javiersc.gradle.plugins.docs.internal.hasKotlinGradlePlugin
import java.io.File
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.dokka.gradle.DokkaPlugin
import org.jetbrains.dokka.gradle.DokkaTaskPartial
import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated
import ru.vyarus.gradle.plugin.mkdocs.MkdocsExtension

abstract class DocsPlugin : Plugin {

    override fun apply(target: Project) {
        check(target == target.rootProject) { "`docs` plugin must be applied in the root project" }

        target.pluginManager.apply("ru.vyarus.mkdocs")
        target.pluginManager.apply("org.jetbrains.dokka")

        DocsExtension.createExtension(target)

        target.buildDotDocsFolder()

        target.extensions.findByType(MkdocsExtension::class.java)?.apply {
            strict = false
            sourcesDir = "${target.rootProject.rootDir}/build/.docs"
            buildDir = "${target.rootProject.rootDir}/build/docs"
            publish.docPath = "_site"
        }

        target.allprojects { project ->
            project.afterEvaluate {
                if (it.hasKotlinGradlePlugin) {
                    it.pluginManager.apply(DokkaPlugin::class.java)
                    it.tasks.withType(DokkaTaskPartial::class.java) { dokkaTaskPartial ->
                        dokkaTaskPartial.dokkaSourceSets.configureEach { sourceSetBuilder ->
                            sourceSetBuilder.includes.from(listOf("MODULE.md"))
                        }
                    }
                }
            }
        }

        target.tasks.register("buildDocs") { buildDocs ->
            buildDocs.group = "documentation"

            val mkdocsBuildTask = target.tasks.named("mkdocsBuild")
            val dokkaHtmlMultiModuleTask = target.tasks.named("dokkaHtmlMultiModule")

            buildDocs.dependsOn(mkdocsBuildTask)
            buildDocs.dependsOn(dokkaHtmlMultiModuleTask)
            dokkaHtmlMultiModuleTask.get().dependsOn(mkdocsBuildTask)

            target.gradle.taskGraph.beforeTask { task ->
                if (task.name == mkdocsBuildTask.name) {
                    buildDocs.project.buildDotDocsFolder()
                    target.buildBuildDotDocs()
                    target.buildChangelogInDocs()
                    target.buildApiInDocs()
                    target.buildProjectsInDocs()
                    target.buildReportsInDocs()
                    target.sanitizeMkdocsFile()
                }
            }

            buildDocs.doLast { target.moveApiDocsInToDocs() }
        }
    }
}

private val Project.apiIndexHtmlContent: String
    get() =
        """
            
            
                
            
            
            
            
        """.trimIndent()

private fun Project.buildDotDocsFolder() {
    val dotDocsFile = file("$rootDir/.docs")
    if (dotDocsFile.exists().not()) {
        file("$dotDocsFile/mkdocs.yml").apply {
            ensureParentDirsCreated()
            createNewFile()
            writeText(
                """|# TODO: Change all necessary properties
                   |site_name: NAME # TODO
                   |site_description: DESCRIPTION # TODO
                   |site_author: AUTHOR # TODO
                   |remote_branch: gh-pages
                   |
                   |repo_name: REPO NAME # TODO
                   |repo_url: https://github.com/USER/REPO-NAME # TODO
                   |
                   |copyright: 'Copyright © 2021 AUTHOR' # TODO
                   |
                   |theme:
                   |  name: 'material'
                   |  language: 'en'
                   |  # TODO favicon: 'assets/favicon.png'
                   |  # TODO logo: 'assets/logo.svg'
                   |  font:
                   |    text: 'Fira Sans'
                   |    code: 'JetBrains Mono'
                   |  palette:
                   |    - media: "(prefers-color-scheme: light)"
                   |      scheme: default
                   |      primary: 'white'
                   |      accent: 'white'
                   |      toggle:
                   |        icon: material/weather-sunny
                   |        name: Switch to dark mode
                   |    - media: "(prefers-color-scheme: dark)"
                   |      primary: 'indigo'
                   |      accent: 'light blue'
                   |      scheme: slate
                   |      toggle:
                   |        icon: material/weather-night
                   |        name: Switch to light mode
                   |
                   |nav:
                   |  - Overview: index.md
                   |
                   |plugins:
                   |  - search
                   |
                   |markdown_extensions:
                   |  - admonition
                   |  - smarty
                   |  - codehilite:
                   |      guess_lang: false
                   |      linenums: True
                   |  - footnotes
                   |  - meta
                   |  - toc:
                   |      permalink: true
                   |  - pymdownx.betterem:
                   |      smart_enable: all
                   |  - pymdownx.caret
                   |  - pymdownx.details
                   |  - pymdownx.inlinehilite
                   |  - pymdownx.magiclink
                   |  - pymdownx.smartsymbols
                   |  - pymdownx.superfences
                   |  - tables
                   |
                   |extra:
                   |  social:
                   |    - icon: fontawesome/brands/github
                   |      link: https://github.com/USER # TODO
                   |    - icon: fontawesome/brands/twitter
                   |      link: https://twitter.com/USER # TODO
                   |
                   |extra_css:
                   |  - css/all.css
                   |
                """.trimMargin()
            )
        }

        file("$dotDocsFile/docs/css/all.css").apply {
            ensureParentDirsCreated()
            createNewFile()
            writeText(
                """
                    code {
                        font-weight: 600;
                    }

                    @media screen and (min-width: 76.1875em) {
                        .md-nav--primary .md-nav__title {
                            display: none;
                        }
                    }

                    .md-nav__link--active {
                        font-weight: bold;
                    }

                """.trimIndent()
            )
        }

        file("$dotDocsFile/docs/assets/empty.file").apply {
            ensureParentDirsCreated()
            createNewFile()
        }
    }
}

private fun Project.buildBuildDotDocs() {
    copy {
        it.from("$rootDir/.docs")
        it.into("$rootDir/build/.docs")
    }

    if (file("$rootDir/.docs/docs/index.md").exists().not()) {
        copy {
            it.from("$rootDir/README.md")
            it.into("$rootDir/build/.docs/docs")
            it.rename { fileName -> fileName.replace(fileName, "index.md") }
        }

        file("$rootDir/build/.docs/docs/index.md").apply {
            writeText(
                readLines().joinToString("\n") { line ->
                    line.replace(".docs/docs/assets", "assets")
                }
            )
        }
    }
}

private fun Project.buildApiInDocs() {
    val docsNavigation = getDocsNavigation()
    val navsPlusApiDocs =
        docsNavigation.navs +
            """
                |  - API docs:
                |    - Latest: api/
                |    - Snapshot: api/snapshot/
            """.trimMargin()

    writeNavigation(navsPlusApiDocs)
}

private fun Project.moveApiDocsInToDocs() {
    val dokkaOutputDir = File("${rootProject.rootDir}/build/dokka/htmlMultiModule/")
    val apiDir = File("${rootProject.rootDir}/build/docs/_site/api/").apply(File::mkdirs)
    if (version.toString().endsWith("-SNAPSHOT")) {
        copy {
            it.from(dokkaOutputDir.path)
            it.into(File("$apiDir/snapshot").path)
        }
    } else {
        file("$apiDir/index.html").apply {
            createNewFile()
            writeText(apiIndexHtmlContent)
        }
        copy {
            it.from(dokkaOutputDir.path)
            it.into(File("$apiDir/versions/${version}").path)
        }
    }
}

private fun Project.buildChangelogInDocs() {
    if (file("$rootDir/CHANGELOG.md").exists()) {
        copy {
            it.from("$rootDir/CHANGELOG.md")
            it.into("$rootDir/build/.docs/docs")
        }

        file("$rootDir/build/.docs/docs/CHANGELOG.md").apply {
            val content = readLines()
            val firstVersionLine =
                content.indexOfFirst { line ->
                    line.contains("## [") && !line.contains("[Unreleased]")
                } - 1

            runCatching {
                val contentUpdated =
                    (content.subList(0, 1) + content.subList(firstVersionLine, content.count()))
                        .joinToString("\n")
                writeText(contentUpdated)
            }
        }

        val docsNavigation = getDocsNavigation()
        val navsPlusChangelog = docsNavigation.navs + "  - Changelog: CHANGELOG.md"

        writeNavigation(navsPlusChangelog)
    }
}

private data class ProjectInfo(val name: String, val projectPath: String, val mdFile: File) {

    val filePath: String = projectPath.split(':').filter(String::isNotEmpty).joinToString("/")
}

@OptIn(ExperimentalStdlibApi::class)
private fun Project.buildProjectsInDocs() {
    val projectsPath = "$rootDir/build/.docs/docs/projects"

    val projectsInfo: List =
        subprojects.sortedBy(Project::getPath).mapNotNull { project ->
            val mdFiles =
                project.projectDir.walkTopDown().maxDepth(1).filter { file ->
                    file.name.endsWith(".md", true)
                }
            val mdFile =
                mdFiles.find { file -> file.name.contains("MODULE", true) }
                    ?: mdFiles.find { file -> file.name.contains("README", true) }
                        ?: mdFiles.firstOrNull()

            if (mdFile != null) {
                ProjectInfo(project.name, project.path, mdFile)
            } else {
                logger.lifecycle(
                    "${project.name} hasn't a markdown file, so it won't be added to docs"
                )
                null
            }
        }

    projectsInfo.forEach { projectInfo ->
        val projectsNavPath = "$projectsPath/${projectInfo.filePath}"

        copy {
            it.from(projectInfo.mdFile)
            it.into(projectsNavPath)
            it.rename { fileName -> fileName.replace(fileName, "${projectInfo.name}.md") }
        }

        file("$projectsNavPath/${projectInfo.name}.md").apply {
            writeText(
                readLines().joinToString("\n") { line ->
                    line.replace(".docs/docs/assets", "assets")
                }
            )
        }
    }

    val docsNavigation = getDocsNavigation()
    val navProjects: List =
        buildMap> {
                projectsInfo.forEach { projectInfo ->
                    val fullPath = "projects/${projectInfo.projectPath.drop(1).replace(":", "/")}"
                    val parentPath = fullPath.replaceAfterLast(projectInfo.name, "")
                    val projects = this[parentPath].orEmpty()
                    val paths = parentPath.split("/")
                    paths.reduce { previousPath, path ->
                        val accumulatedPath = "$previousPath/$path"
                        put(accumulatedPath, this[accumulatedPath].orEmpty())
                        accumulatedPath
                    }
                    put(parentPath, projects + projectInfo.name)
                }
            }
            .flatMap { (path, projects) ->
                val count = path.count { it == '/' } + 1
                val indent = buildString { repeat(count) { append("  ") } }
                buildList {
                    add("- ${path.split('/').last()}:".prependIndent(indent))
                    addAll(
                        projects.map { project ->
                            "- $project: $path/$project.md".prependIndent("$indent  ")
                        }
                    )
                }
            }
            .cleanNavProjects()

    val navsPlusProjects = docsNavigation.navs + "  - Projects:" + navProjects

    writeNavigation(navsPlusProjects)
}

@OptIn(ExperimentalStdlibApi::class)
private fun Project.buildReportsInDocs() {
    val allTests = docsExtension.navigation.get().reports.get().allTests.get()
    val codeAnalysis = docsExtension.navigation.get().reports.get().codeAnalysis.get()
    val codeCoverage = docsExtension.navigation.get().reports.get().codeCoverage.get()
    val codeQuality = docsExtension.navigation.get().reports.get().codeQuality.get()

    fun MutableList.createReportSection(title: String, pathAndFileName: String) {
        val content =
            """
                |# $title
                |
                |
                |
            """.trimMargin()

        file("$rootDir/build/.docs/docs/reports/$pathAndFileName.md").apply {
            ensureParentDirsCreated()
            createNewFile()
            writeText(content)
        }
        add("    - $title: reports/$pathAndFileName.md")
    }

    val navReports: String =
        buildList {
                if (allTests && codeAnalysis || codeCoverage || codeQuality) add("  - Reports:")
                if (allTests) createReportSection("All tests", "all-tests")
                if (codeAnalysis) createReportSection("Code analysis", "code-analysis")
                if (codeCoverage) createReportSection("Code coverage", "code-coverage")
                if (codeQuality) createReportSection("Code quality", "code-quality")
            }
            .joinToString("\n")

    val docsNavigation = getDocsNavigation()

    val navsPlusReports = docsNavigation.navs + navReports.lines()

    writeNavigation(navsPlusReports)
}

private val Project.mkdocsBuildFile: File
    get() = file("$rootDir/build/.docs/mkdocs.yml")

private data class DocsNavigation(val index: Int, val navs: List)

@OptIn(ExperimentalStdlibApi::class)
private fun Project.writeNavigation(newNavigations: List) {
    mkdocsBuildFile.writeText(
        buildList {
                addAll(mkdocsBuildFile.readLines())
                removeAt(getDocsNavigation().index)
                removeAll(getDocsNavigation().navs)
                add("")
                add("nav:")
                addAll(newNavigations)
                add("")
            }
            .joinToString("\n")
    )
}

private fun Project.getDocsNavigation(): DocsNavigation {
    val content = mkdocsBuildFile.readLines()
    val navIndex = content.indexOfFirst { it.replace(" ", "").startsWith("nav:", true) }
    return DocsNavigation(
        index = navIndex,
        navs =
            content.subList(navIndex + 1, content.count()).takeWhile {
                it.replace(" ", "").startsWith("-")
            }
    )
}

@OptIn(ExperimentalStdlibApi::class)
private fun List.cleanNavProjects(): List =
    buildList {
            val lines = this@cleanNavProjects

            fun String.isReference() =
                filterNot(Char::isWhitespace).replaceBefore(":", "").drop(1).isEmpty()

            fun String.isReferenceOf(reference: String) =
                trimIndent().takeWhile { it != ':' } ==
                    reference.trimIndent().takeWhile { it != ':' }

            lines.reduce { previous, line ->
                when {
                    previous == line -> {
                        removeLast()
                        add(line)
                        line
                    }
                    previous.isReference() && line.isReferenceOf(previous) -> {
                        add(previous.takeWhile(Char::isWhitespace) + line.trimIndent())
                        line
                    }
                    line.isReferenceOf(previous) && line.isReference() -> {
                        add(previous)
                        previous
                    }
                    else -> {
                        add(previous)
                        line
                    }
                }
            }
        }
        .distinctBy(String::trimIndent)

private fun Project.sanitizeMkdocsFile() {
    val content = mkdocsBuildFile.readLines().removeDuplicateEmptyLines().endWithNewLine()
    mkdocsBuildFile.writeText(content)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy