Skip to content

Commit

Permalink
Propagate Ank changes to Dokka, and parallelise processor
Browse files Browse the repository at this point in the history
  • Loading branch information
nomisRev committed Aug 11, 2021
1 parent d339335 commit dd3b4d8
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 77 deletions.
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {
}

group = "com.nomisrev"
version = "1.0-SNAPSHOT"
version = "1.1-SNAPSHOT"

repositories {
mavenCentral()
Expand All @@ -22,6 +22,10 @@ dependencies {
compileOnly("org.jetbrains.dokka:dokka-core:$dokkaVersion")
implementation("org.jetbrains.dokka:dokka-base:$dokkaVersion")

implementation("io.arrow-kt:arrow-core:0.13.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
implementation("io.arrow-kt:arrow-fx-coroutines:0.13.2")

runtimeOnly(kotlin("reflect"))
runtimeOnly(kotlin("script-runtime"))
runtimeOnly("org.jetbrains.kotlin:kotlin-script-runtime:1.5.0") { isTransitive = false }
Expand Down
3 changes: 2 additions & 1 deletion sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ dependencies {
implementation("io.arrow-kt:arrow-fx-coroutines:0.13.2")
implementation("io.kotest:kotest-property:4.6.1")
implementation("io.kotest:kotest-assertions-core:4.6.1")
dokkaHtmlPlugin("org.example:awesome-dokka-plugin:1.0-SNAPSHOT")

dokkaHtmlPlugin("com.nomisrev:ank-dokka-plugin:1.1-SNAPSHOT")
}

tasks.withType<DokkaTask>().configureEach {
Expand Down
16 changes: 2 additions & 14 deletions sample/src/main/kotlin/example.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,11 @@ fun exampleException(): Unit = Unit
*/
fun exampleReplace(): Unit = Unit

///**
// * This function is documented, and Ank can correctly handle the exception
// *
// * ```java:ank
// * public String hello(String name) {
// * return "Hello " + name + "!"
// * }
// *
// * hello("Λrrow")
// * ```
// */
//fun exampleJava(): Unit = Unit

/**
* This function is documented, and I have access to my classpath from docs
* Since it's silent, it doesn't add `// Either.Right(1)` as output.
*
* ```kotlin:ank
* ```kotlin:ank:silent
* import arrow.core.Either
*
* Either.Right(1)
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
rootProject.name = "awesome-dokka-plugin"
rootProject.name = "ank-dokka-plugin"
include("sample")
68 changes: 17 additions & 51 deletions src/main/kotlin/com/github/nomisrev/ank/AnkDokkaPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.github.nomisrev.ank

import arrow.fx.coroutines.parTraverse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.model.DModule
import org.jetbrains.dokka.model.doc.Br
import org.jetbrains.dokka.model.doc.CodeBlock
import org.jetbrains.dokka.model.doc.Text
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.plugability.DokkaPlugin
import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer
Expand All @@ -17,63 +17,29 @@ class AnkDokkaPlugin : DokkaPlugin() {
}

/**
* => Every module can have its own classpath (ONLY WORKS FOR JVM due to ScriptEngine)
* => Every KDoc belongs to a File which has a path, can we figure this out?
* => This doesn't replace KotlinX Knit, so don't support MARKDOWN for now.
* => Properly log errors through DokkaLogger
*/
private class AnkCompiler(private val ctx: DokkaContext) : PreMergeDocumentableTransformer {

// Could we optimise with `suspend` and running in parallel?
override fun invoke(modules: List<DModule>): List<DModule> =
modules.also {
ctx.logger.error(colored(ANSI_PURPLE, "Λnk Dokka Plugin is running"))
override fun invoke(modules: List<DModule>): List<DModule> = runBlocking(Dispatchers.Default) {
ctx.logger.warn(colored(ANSI_PURPLE, "Λnk Dokka Plugin is running"))

val classpath = modules.flatMap { it.sourceSets.firstOrNull()?.classpath.orEmpty() }
// Shall we process all packages in parallel??
modules.parTraverse { module ->
val classpath = module.sourceSets.firstOrNull()?.classpath.orEmpty()
.map { it.toURI().toURL().toString() }

Engine.compileCode(it.allSnippets(), classpath)
}
}


Engine.createEngine(classpath).use { engine ->
val packages =
module.packages.parTraverseCodeBlock(module) { module, `package`, documentable, node, wrapper, codeBlock ->
Snippet(module, `package`, documentable, node, wrapper, codeBlock)?.let {
Engine.compileCode(engine, it)
}?.toCodeBlock()
}

private fun CodeBlock.asStringOrNull(): String? =
buildString {
children.forEach { tag ->
when (tag) {
is Text -> append(tag.body)
Br -> append("\n")
else -> return null
module.copy(packages = packages)
}
}
}

private fun List<DModule>.allSnippets(): List<Snippet> =
flatMap { module ->
module.packages.flatMap { `package` ->
`package`.children.flatMap { documentable ->
documentable.documentation.values.flatMap { node ->
node.children.flatMap { tagWrapper ->
tagWrapper.children.mapNotNull { docTag ->
(docTag as? CodeBlock)?.let { code ->
code.params["lang"]?.let { fence ->
fenceRegexStart.matchEntire(fence)?.let { match ->
docTag.asStringOrNull()?.let { rawCode ->
val lang = match.groupValues[1].trim()
val path = """
Module: ${module.name}
package: ${`package`.packageName}
KDoc of: $documentable
""".trimIndent()
Snippet(path, fence, lang, rawCode)
}
}
}
}
}
}
}
}
}
}
}
45 changes: 45 additions & 0 deletions src/main/kotlin/com/github/nomisrev/ank/debug.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.github.nomisrev.ank

import org.jetbrains.dokka.model.DModule
import org.jetbrains.dokka.model.doc.CodeBlock

// Get all codeBlocks => write some dump version
private fun List<DModule>.allCodeBlocks(): List<CodeBlock> =
flatMap { module ->
module.packages.flatMap { `package` ->
`package`.children.flatMap { documentable ->
documentable.documentation.values.flatMap { node ->
node.children.flatMap { tagWrapper ->
tagWrapper.children.mapNotNull { docTag ->
(docTag as? CodeBlock)
}
}
}
}
}
}

private fun List<DModule>.allSnippets(): List<Snippet> =
flatMap { module ->
module.packages.flatMap { `package` ->
`package`.children.flatMap { documentable ->
documentable.documentation.values.flatMap { node ->
node.children.flatMap { wrapper ->
wrapper.children.mapNotNull { docTag ->
(docTag as? CodeBlock)?.let { code ->
code.params["lang"]?.let { fence ->
fenceRegexStart.matchEntire(fence)?.let { match ->
docTag.asStringOrNull()?.let { rawCode ->
val lang = match.groupValues[1].trim()
val path = SnippetPath(module, `package`, documentable, node, wrapper, code)
Snippet(path, fence, lang, rawCode)
}
}
}
}
}
}
}
}
}
}
130 changes: 130 additions & 0 deletions src/main/kotlin/com/github/nomisrev/ank/dokka.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.github.nomisrev.ank

import arrow.fx.coroutines.parTraverse
import org.jetbrains.dokka.model.DAnnotation
import org.jetbrains.dokka.model.DClass
import org.jetbrains.dokka.model.DClasslike
import org.jetbrains.dokka.model.DEnum
import org.jetbrains.dokka.model.DInterface
import org.jetbrains.dokka.model.DModule
import org.jetbrains.dokka.model.DObject
import org.jetbrains.dokka.model.DPackage
import org.jetbrains.dokka.model.Documentable
import org.jetbrains.dokka.model.SourceSetDependent
import org.jetbrains.dokka.model.doc.Author
import org.jetbrains.dokka.model.doc.CodeBlock
import org.jetbrains.dokka.model.doc.CodeInline
import org.jetbrains.dokka.model.doc.Constructor
import org.jetbrains.dokka.model.doc.CustomDocTag
import org.jetbrains.dokka.model.doc.CustomTagWrapper
import org.jetbrains.dokka.model.doc.Deprecated
import org.jetbrains.dokka.model.doc.Description
import org.jetbrains.dokka.model.doc.DocTag
import org.jetbrains.dokka.model.doc.DocumentationNode
import org.jetbrains.dokka.model.doc.Param
import org.jetbrains.dokka.model.doc.Property
import org.jetbrains.dokka.model.doc.Receiver
import org.jetbrains.dokka.model.doc.Return
import org.jetbrains.dokka.model.doc.Sample
import org.jetbrains.dokka.model.doc.See
import org.jetbrains.dokka.model.doc.Since
import org.jetbrains.dokka.model.doc.Suppress
import org.jetbrains.dokka.model.doc.TagWrapper
import org.jetbrains.dokka.model.doc.Throws
import org.jetbrains.dokka.model.doc.Version


/**
* This methods gives you all info available for a detected `CodeBlock`.
* It **does not** filter the list, but if you return a non-null value it will
* update the existing value in the [DModule]
*/
suspend fun List<DPackage>.parTraverseCodeBlock(
dModule: DModule,
transform: (module: DModule, `package`: DPackage, documentable: Documentable, node: DocumentationNode, wrapper: TagWrapper, CodeBlock) -> CodeBlock?
): List<DPackage> = parTraverse { `package` ->
`package`.copy(
properties = `package`.properties.map { property ->
property.copy(documentation = property.documentation.process(dModule, `package`, property, transform))
},
functions = `package`.functions.map { function ->
function.copy(documentation = function.documentation.process(dModule, `package`, function, transform))
},
classlikes = `package`.classlikes.map { it.process(dModule, `package`, transform) },
typealiases = `package`.typealiases.map { typeAlias ->
typeAlias.copy(documentation = typeAlias.documentation.process(dModule, `package`, typeAlias, transform))
}
)
}

private fun DClasslike.process(
module: DModule,
`package`: DPackage,
transform: (module: DModule, `package`: DPackage, documentable: Documentable, node: DocumentationNode, wrapper: TagWrapper, CodeBlock) -> CodeBlock?
): DClasslike =
when (this) {
is DClass -> copy(documentation = documentation.process(module, `package`, this, transform))
is DEnum -> copy(documentation = documentation.process(module, `package`, this, transform))
is DInterface -> copy(documentation = documentation.process(module, `package`, this, transform))
is DObject -> copy(documentation = documentation.process(module, `package`, this, transform))
is DAnnotation -> copy(documentation = documentation.process(module, `package`, this, transform))
}

private fun SourceSetDependent<DocumentationNode>.process(
module: DModule,
`package`: DPackage,
documentable: Documentable,
transform: (module: DModule, `package`: DPackage, documentable: Documentable, node: DocumentationNode, wrapper: TagWrapper, code: CodeBlock) -> CodeBlock?
): SourceSetDependent<DocumentationNode> =
mapValues { (_, node) -> node.process(module, `package`, documentable, node, transform) }

private fun DocumentationNode.process(
module: DModule,
`package`: DPackage,
documentable: Documentable,
node: DocumentationNode,
transform: (module: DModule, `package`: DPackage, documentable: Documentable, node: DocumentationNode, wrapper: TagWrapper, code: CodeBlock) -> CodeBlock?
): DocumentationNode =
copy(children = children.map {
when (it) {
is See -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Param -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Throws -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Sample -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Property -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is CustomTagWrapper -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Description -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Author -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Version -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Since -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Return -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Receiver -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Constructor -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Deprecated -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
is Suppress -> it.copy(root = it.root.process(module, `package`, documentable, node, it, transform))
}
})

private fun DocTag.process(
module: DModule,
`package`: DPackage,
documentable: Documentable,
node: DocumentationNode,
wrapper: TagWrapper,
transform: (module: DModule, `package`: DPackage, documentable: Documentable, node: DocumentationNode, wrapper: TagWrapper, code: CodeBlock) -> CodeBlock?
): DocTag =
when (this) {
is CodeBlock -> transform(module, `package`, documentable, node, wrapper, this) ?: this
is CodeInline -> this
is CustomDocTag -> copy(children = children.map {
it.process(
module,
`package`,
documentable,
node,
wrapper,
transform
)
})
else -> this
}
47 changes: 45 additions & 2 deletions src/main/kotlin/com/github/nomisrev/ank/domain.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package com.github.nomisrev.ank

import org.jetbrains.dokka.model.DModule
import org.jetbrains.dokka.model.DPackage
import org.jetbrains.dokka.model.Documentable
import org.jetbrains.dokka.model.doc.Br
import org.jetbrains.dokka.model.doc.CodeBlock
import org.jetbrains.dokka.model.doc.DocumentationNode
import org.jetbrains.dokka.model.doc.TagWrapper
import org.jetbrains.dokka.model.doc.Text

public const val AnkBlock: String = ":ank"
public const val AnkSilentBlock: String = ":ank:silent"
public const val AnkReplaceBlock: String = ":ank:replace"
Expand All @@ -10,8 +19,17 @@ public const val AnkPlaygroundExtension: String = ":ank:playground:extension"

public val fenceRegexStart = "(.*)$AnkBlock.*".toRegex()

data class SnippetPath(
val module: DModule,
val `package`: DPackage,
val documentable: Documentable,
val node: DocumentationNode,
val tagWrapper: TagWrapper,
val codeBlock: CodeBlock
)

public data class Snippet(
val path: String,
val path: SnippetPath,
val fence: String,
val lang: String,
val code: String,
Expand All @@ -21,4 +39,29 @@ public data class Snippet(
val isReplace: Boolean = fence.contains(AnkReplaceBlock)
val isFail: Boolean = fence.contains(AnkFailBlock)
val isPlayground: Boolean = fence.contains(AnkPlayground)
}

fun toCodeBlock(): CodeBlock =
CodeBlock(
code.lines().flatMap { listOf(Text(it), Br) },
mapOf("lang" to fence)
)
}

fun Snippet(
module: DModule,
`package`: DPackage,
documentable: Documentable,
node: DocumentationNode,
wrapper: TagWrapper,
code: CodeBlock
): Snippet? =
code.params["lang"]?.let { fence ->
fenceRegexStart.matchEntire(fence)?.let { match ->
code.asStringOrNull()?.let { rawCode ->
val lang = match.groupValues[1].trim()
val path =
SnippetPath(module, `package`, documentable, node, wrapper, code)
Snippet(path, fence, lang, rawCode)
}
}
}
Loading

0 comments on commit dd3b4d8

Please sign in to comment.