Skip to content

Commit

Permalink
Implement custom code block renderers support
Browse files Browse the repository at this point in the history
  • Loading branch information
whyoleg committed Nov 6, 2023
1 parent 84e48b5 commit e38326d
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 1 deletion.
6 changes: 6 additions & 0 deletions plugins/base/api/base.api
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public final class org/jetbrains/dokka/base/DokkaBase : org/jetbrains/dokka/plug
public final fun getExternalLocationProviderFactory ()Lorg/jetbrains/dokka/plugability/ExtensionPoint;
public final fun getFallbackMerger ()Lorg/jetbrains/dokka/plugability/Extension;
public final fun getFileWriter ()Lorg/jetbrains/dokka/plugability/Extension;
public final fun getHtmlCodeBlockRenderers ()Lorg/jetbrains/dokka/plugability/ExtensionPoint;
public final fun getHtmlPreprocessors ()Lorg/jetbrains/dokka/plugability/ExtensionPoint;
public final fun getHtmlRenderer ()Lorg/jetbrains/dokka/plugability/Extension;
public final fun getImmediateHtmlCommandConsumer ()Lorg/jetbrains/dokka/plugability/ExtensionPoint;
Expand Down Expand Up @@ -294,6 +295,11 @@ public final class org/jetbrains/dokka/base/renderers/html/CustomResourceInstall
public fun invoke (Lorg/jetbrains/dokka/pages/RootPageNode;)Lorg/jetbrains/dokka/pages/RootPageNode;
}

public abstract interface class org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer {
public abstract fun buildCodeBlock (Lkotlinx/html/FlowContent;Ljava/lang/String;Ljava/lang/String;)V
public abstract fun isApplicable (Ljava/lang/String;Ljava/lang/String;)Z
}

public final class org/jetbrains/dokka/base/renderers/html/HtmlFormatingUtilsKt {
public static final fun buildBreakableDotSeparatedHtml (Lkotlinx/html/FlowContent;Ljava/lang/String;)V
public static final fun buildBreakableText (Lkotlinx/html/FlowContent;Ljava/lang/String;)V
Expand Down
1 change: 1 addition & 0 deletions plugins/base/src/main/kotlin/DokkaBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class DokkaBase : DokkaPlugin() {
public val externalLocationProviderFactory: ExtensionPoint<ExternalLocationProviderFactory> by extensionPoint()
public val outputWriter: ExtensionPoint<OutputWriter> by extensionPoint()
public val htmlPreprocessors: ExtensionPoint<PageTransformer> by extensionPoint()
public val htmlCodeBlockRenderers: ExtensionPoint<HtmlCodeBlockRenderer> by extensionPoint()

@Deprecated("It is not used anymore")
public val tabSortingStrategy: ExtensionPoint<TabSortingStrategy> by extensionPoint()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package org.jetbrains.dokka.base.renderers.html

import kotlinx.html.FlowContent

/**
* Provides an ability to override code blocks rendering differently dependent on the code language.
*
* Multiple renderers can be installed to support different languages in an independent way.
*/
public interface HtmlCodeBlockRenderer {

/**
* Whether this renderer supports given [language].
*
* [code] can be useful to determine applicability if [language] is not provided (empty string)
*/
public fun isApplicable(language: String, code: String): Boolean

/**
* Defines how to render [code] for specified [language] via HTML tags
*/
public fun FlowContent.buildCodeBlock(language: String, code: String)
}
23 changes: 22 additions & 1 deletion plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public open class HtmlRenderer(
private var shouldRenderSourceSetTabs: Boolean = false

override val preprocessors: List<PageTransformer> = context.plugin<DokkaBase>().query { htmlPreprocessors }
private val customCodeBlockRenderers = context.plugin<DokkaBase>().query { htmlCodeBlockRenderers }

/**
* Tabs themselves are created in HTML plugin since, currently, only HTML format supports them.
Expand Down Expand Up @@ -816,13 +817,33 @@ public open class HtmlRenderer(
code: ContentCodeBlock,
pageContext: ContentPage
) {
val codeText = buildString {
code.children.forEach {
when (it) {
is ContentText -> append(it.text)
is ContentBreakLine -> appendLine()
}
}
}

customCodeBlockRenderers.forEach { renderer ->
if (renderer.isApplicable(code.language, codeText)) {
// we use first applicable renderer to override rendering
return with(renderer) {
buildCodeBlock(code.language,codeText)
}
}
}

// if there are no custom renderers - fall back to default

div("sample-container") {
val codeLang = "lang-" + code.language.ifEmpty { "kotlin" }
val stylesWithBlock = code.style + TextStyle.Block + codeLang
pre {
code(stylesWithBlock.joinToString(" ") { it.toString().toLowerCase() }) {
attributes["theme"] = "idea"
code.children.forEach { buildContentNode(it, pageContext) }
text(codeText)
}
}
/*
Expand Down
202 changes: 202 additions & 0 deletions plugins/base/src/test/kotlin/renderers/html/CodeBlocksTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package renderers.html

import kotlinx.html.FlowContent
import kotlinx.html.div
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.renderers.html.HtmlCodeBlockRenderer
import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest
import org.jetbrains.dokka.plugability.DokkaPlugin
import org.jetbrains.dokka.plugability.DokkaPluginApiPreview
import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement
import signatures.renderedContent
import utils.TestOutputWriter
import utils.TestOutputWriterPlugin
import kotlin.test.Test
import kotlin.test.assertEquals

class CodeBlocksTest : BaseAbstractTest() {

private val configuration = dokkaConfiguration {
sourceSets {
sourceSet {
sourceRoots = listOf("src/")
}
}
}

@Test
fun `default code block rendering`() = testCode(
"""
/src/test.kt
package test
/**
* Hello, world!
*
* ```kotlin
* test("hello kotlin")
* ```
*
* ```custom
* test("hello custom")
* ```
*
* ```other
* test("hello other")
* ```
*/
fun test(string: String) {}
""".trimIndent(),
emptyList()
) {
val content = renderedContent("root/test/test.html")

// by default, every code block is rendered as an element with `lang-XXX` class,
// where XXX=language of code block
assertEquals(
"""test("hello kotlin")""",
content.getElementsByClass("lang-kotlin").singleOrNull()?.text()
)
assertEquals(
"""test("hello custom")""",
content.getElementsByClass("lang-custom").singleOrNull()?.text()
)
assertEquals(
"""test("hello other")""",
content.getElementsByClass("lang-other").singleOrNull()?.text()
)
}

@Test
fun `code block rendering with custom renderer`() = testCode(
"""
/src/test.kt
package test
/**
* Hello, world!
*
* ```kotlin
* test("hello kotlin")
* ```
*
* ```custom
* test("hello custom")
* ```
*
* ```other
* test("hello other")
* ```
*/
fun test(string: String) {}
""".trimIndent(),
listOf(CustomPlugin(applyOtherRenderer = false)) // we add only one custom renderer
) {
val content = renderedContent("root/test/test.html")
assertEquals(
"""test("hello kotlin")""",
content.getElementsByClass("lang-kotlin").singleOrNull()?.text()
)
assertEquals(
"""test("hello custom")""",
content.getElementsByClass("custom-language-block").singleOrNull()?.text()
)
assertEquals(
"""test("hello other")""",
content.getElementsByClass("lang-other").singleOrNull()?.text()
)
}

@Test
fun `code block rendering with multiple custom renderers`() = testCode(
"""
/src/test.kt
package test
/**
* Hello, world!
*
* ```kotlin
* test("hello kotlin")
* ```
*
* ```custom
* test("hello custom")
* ```
*
* ```other
* test("hello other")
* ```
*/
fun test(string: String) {}
""".trimIndent(),
listOf(CustomPlugin(applyOtherRenderer = true))
) {
val content = renderedContent("root/test/test.html")
assertEquals(
"""test("hello kotlin")""",
content.getElementsByClass("lang-kotlin").singleOrNull()?.text()
)
assertEquals(
"""test("hello custom")""",
content.getElementsByClass("custom-language-block").singleOrNull()?.text()
)
assertEquals(
"""test("hello other")""",
content.getElementsByClass("other-language-block").singleOrNull()?.text()
)
}

private fun testCode(
source: String,
pluginOverrides: List<DokkaPlugin>,
block: TestOutputWriter.() -> Unit
) {
val writerPlugin = TestOutputWriterPlugin()
testInline(source, configuration, pluginOverrides = pluginOverrides + listOf(writerPlugin)) {
renderingStage = { _, _ ->
writerPlugin.writer.block()
}
}
}

private object CustomHtmlBlockRenderer : HtmlCodeBlockRenderer {
override fun isApplicable(language: String, code: String): Boolean = language == "custom"

override fun FlowContent.buildCodeBlock(language: String, code: String) {
div("custom-language-block") {
text(code)
}
}
}

private object CustomOtherHtmlBlockRenderer : HtmlCodeBlockRenderer {
override fun isApplicable(language: String, code: String): Boolean = language == "other"

override fun FlowContent.buildCodeBlock(language: String, code: String) {
div("other-language-block") {
text(code)
}
}
}

class CustomPlugin(applyOtherRenderer: Boolean) : DokkaPlugin() {
val customHtmlBlockRenderer by extending {
plugin<DokkaBase>().htmlCodeBlockRenderers with CustomHtmlBlockRenderer
}

val otherHtmlBlockRenderer by extending {
plugin<DokkaBase>().htmlCodeBlockRenderers with CustomOtherHtmlBlockRenderer applyIf {
applyOtherRenderer
}
}

@OptIn(DokkaPluginApiPreview::class)
override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement =
PluginApiPreviewAcknowledgement
}
}

0 comments on commit e38326d

Please sign in to comment.