diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index ae87255803..65591af161 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -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; @@ -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 diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt index ca86d4d52d..59072e5a14 100644 --- a/plugins/base/src/main/kotlin/DokkaBase.kt +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -48,6 +48,7 @@ public class DokkaBase : DokkaPlugin() { public val externalLocationProviderFactory: ExtensionPoint by extensionPoint() public val outputWriter: ExtensionPoint by extensionPoint() public val htmlPreprocessors: ExtensionPoint by extensionPoint() + public val htmlCodeBlockRenderers: ExtensionPoint by extensionPoint() @Deprecated("It is not used anymore") public val tabSortingStrategy: ExtensionPoint by extensionPoint() diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlCodeBlockRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlCodeBlockRenderer.kt new file mode 100644 index 0000000000..589afc7360 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlCodeBlockRenderer.kt @@ -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) +} diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt index 083876d5cc..70b0d666d7 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -52,6 +52,7 @@ public open class HtmlRenderer( private var shouldRenderSourceSetTabs: Boolean = false override val preprocessors: List = context.plugin().query { htmlPreprocessors } + private val customCodeBlockRenderers = context.plugin().query { htmlCodeBlockRenderers } /** * Tabs themselves are created in HTML plugin since, currently, only HTML format supports them. @@ -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) } } /* diff --git a/plugins/base/src/test/kotlin/renderers/html/CodeBlocksTest.kt b/plugins/base/src/test/kotlin/renderers/html/CodeBlocksTest.kt new file mode 100644 index 0000000000..71e843afc9 --- /dev/null +++ b/plugins/base/src/test/kotlin/renderers/html/CodeBlocksTest.kt @@ -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, + 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().htmlCodeBlockRenderers with CustomHtmlBlockRenderer + } + + val otherHtmlBlockRenderer by extending { + plugin().htmlCodeBlockRenderers with CustomOtherHtmlBlockRenderer applyIf { + applyOtherRenderer + } + } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = + PluginApiPreviewAcknowledgement + } +}