From 7a02e0d1419042ac9a3c4db290072aae88c391f1 Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Mon, 6 Nov 2023 22:25:30 +0200 Subject: [PATCH 1/4] Implement custom code block renderers support --- .../plugin-base/api/plugin-base.api | 6 + .../org/jetbrains/dokka/base/DokkaBase.kt | 1 + .../renderers/html/HtmlCodeBlockRenderer.kt | 27 +++ .../dokka/base/renderers/html/HtmlRenderer.kt | 23 +- .../kotlin/renderers/html/CodeBlocksTest.kt | 202 ++++++++++++++++++ 5 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt create mode 100644 dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt diff --git a/dokka-subprojects/plugin-base/api/plugin-base.api b/dokka-subprojects/plugin-base/api/plugin-base.api index 13f877e353..e4a2e74de7 100644 --- a/dokka-subprojects/plugin-base/api/plugin-base.api +++ b/dokka-subprojects/plugin-base/api/plugin-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; @@ -297,6 +298,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/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt index ca86d4d52d..59072e5a14 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/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/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt new file mode 100644 index 0000000000..589afc7360 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/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/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt index 083876d5cc..70b0d666d7 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/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/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt new file mode 100644 index 0000000000..71e843afc9 --- /dev/null +++ b/dokka-subprojects/plugin-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 + } +} From e5ea3246ceb2d170c08abd8ea8a79e05b3b4457f Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Fri, 10 Nov 2023 17:42:50 +0200 Subject: [PATCH 2/4] Fixes after PR review: * remove `code` from HtmlCodeBlockRenderer.isApplicable * add documentation for extension * add test for multiline code with linebreaks * revert default rendering to original implementation --- .../plugin-base/api/plugin-base.api | 2 +- .../org/jetbrains/dokka/base/DokkaBase.kt | 7 ++ .../renderers/html/HtmlCodeBlockRenderer.kt | 6 +- .../dokka/base/renderers/html/HtmlRenderer.kt | 25 +++--- .../kotlin/renderers/html/CodeBlocksTest.kt | 80 ++++++++++++++++--- 5 files changed, 91 insertions(+), 29 deletions(-) diff --git a/dokka-subprojects/plugin-base/api/plugin-base.api b/dokka-subprojects/plugin-base/api/plugin-base.api index e4a2e74de7..4041713948 100644 --- a/dokka-subprojects/plugin-base/api/plugin-base.api +++ b/dokka-subprojects/plugin-base/api/plugin-base.api @@ -300,7 +300,7 @@ public final class org/jetbrains/dokka/base/renderers/html/CustomResourceInstall 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 abstract fun isApplicable (Ljava/lang/String;)Z } public final class org/jetbrains/dokka/base/renderers/html/HtmlFormatingUtilsKt { diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt index 59072e5a14..6fa4270b9a 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt @@ -48,6 +48,13 @@ public class DokkaBase : DokkaPlugin() { public val externalLocationProviderFactory: ExtensionPoint by extensionPoint() public val outputWriter: ExtensionPoint by extensionPoint() public val htmlPreprocessors: ExtensionPoint by extensionPoint() + + /** + * Extension point for providing custom HTML code block renderers. + * + * This extension point allows overriding the rendering of code blocks in different programming languages. + * Multiple renderers can be installed to support different languages independently. + */ public val htmlCodeBlockRenderers: ExtensionPoint by extensionPoint() @Deprecated("It is not used anymore") diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt index 589afc7360..33bfc51adb 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt @@ -14,11 +14,9 @@ import kotlinx.html.FlowContent public interface HtmlCodeBlockRenderer { /** - * Whether this renderer supports given [language]. - * - * [code] can be useful to determine applicability if [language] is not provided (empty string) + * Whether this renderer supports given [language] */ - public fun isApplicable(language: String, code: String): Boolean + public fun isApplicable(language: String): Boolean /** * Defines how to render [code] for specified [language] via HTML tags diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt index 70b0d666d7..55a90c1d9e 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt @@ -817,25 +817,24 @@ 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)) { + if (renderer.isApplicable(code.language)) { // we use first applicable renderer to override rendering + val codeText = buildString { + code.children.forEach { + when (it) { + is ContentText -> append(it.text) + is ContentBreakLine -> appendLine() + } + } + } return with(renderer) { - buildCodeBlock(code.language,codeText) + buildCodeBlock(code.language, codeText) } } } - // if there are no custom renderers - fall back to default + // if there are no applicable custom renderers - fall back to default div("sample-container") { val codeLang = "lang-" + code.language.ifEmpty { "kotlin" } @@ -843,7 +842,7 @@ public open class HtmlRenderer( pre { code(stylesWithBlock.joinToString(" ") { it.toString().toLowerCase() }) { attributes["theme"] = "idea" - text(codeText) + code.children.forEach { buildContentNode(it, pageContext) } } } /* diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt index 71e843afc9..57e4486303 100644 --- a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt @@ -59,15 +59,15 @@ class CodeBlocksTest : BaseAbstractTest() { // where XXX=language of code block assertEquals( """test("hello kotlin")""", - content.getElementsByClass("lang-kotlin").singleOrNull()?.text() + content.getElementsByClass("lang-kotlin").singleOrNull()?.wholeText() ) assertEquals( """test("hello custom")""", - content.getElementsByClass("lang-custom").singleOrNull()?.text() + content.getElementsByClass("lang-custom").singleOrNull()?.wholeText() ) assertEquals( """test("hello other")""", - content.getElementsByClass("lang-other").singleOrNull()?.text() + content.getElementsByClass("lang-other").singleOrNull()?.wholeText() ) } @@ -99,15 +99,15 @@ class CodeBlocksTest : BaseAbstractTest() { val content = renderedContent("root/test/test.html") assertEquals( """test("hello kotlin")""", - content.getElementsByClass("lang-kotlin").singleOrNull()?.text() + content.getElementsByClass("lang-kotlin").singleOrNull()?.wholeText() ) assertEquals( """test("hello custom")""", - content.getElementsByClass("custom-language-block").singleOrNull()?.text() + content.getElementsByClass("custom-language-block").singleOrNull()?.wholeText() ) assertEquals( """test("hello other")""", - content.getElementsByClass("lang-other").singleOrNull()?.text() + content.getElementsByClass("lang-other").singleOrNull()?.wholeText() ) } @@ -139,15 +139,73 @@ class CodeBlocksTest : BaseAbstractTest() { val content = renderedContent("root/test/test.html") assertEquals( """test("hello kotlin")""", - content.getElementsByClass("lang-kotlin").singleOrNull()?.text() + content.getElementsByClass("lang-kotlin").singleOrNull()?.wholeText() ) assertEquals( """test("hello custom")""", - content.getElementsByClass("custom-language-block").singleOrNull()?.text() + content.getElementsByClass("custom-language-block").singleOrNull()?.wholeText() ) assertEquals( """test("hello other")""", - content.getElementsByClass("other-language-block").singleOrNull()?.text() + content.getElementsByClass("other-language-block").singleOrNull()?.wholeText() + ) + } + + @Test + fun `multiline code block rendering with linebreaks`() = testCode( + """ + /src/test.kt + package test + + /** + * Hello, world! + * + * ```kotlin + * // something before linebreak + * + * test("hello kotlin") + * ``` + * + * ```custom + * // something before linebreak + * + * test("hello custom") + * ``` + * + * ```other + * // something before linebreak + * + * 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( + """ + // something before linebreak + + test("hello kotlin") + """.trimIndent(), + content.getElementsByClass("lang-kotlin").singleOrNull()?.wholeText() + ) + assertEquals( + """ + // something before linebreak + + test("hello custom") + """.trimIndent(), + content.getElementsByClass("custom-language-block").singleOrNull()?.wholeText() + ) + assertEquals( + """ + // something before linebreak + + test("hello other") + """.trimIndent(), + content.getElementsByClass("lang-other").singleOrNull()?.wholeText() ) } @@ -165,7 +223,7 @@ class CodeBlocksTest : BaseAbstractTest() { } private object CustomHtmlBlockRenderer : HtmlCodeBlockRenderer { - override fun isApplicable(language: String, code: String): Boolean = language == "custom" + override fun isApplicable(language: String): Boolean = language == "custom" override fun FlowContent.buildCodeBlock(language: String, code: String) { div("custom-language-block") { @@ -175,7 +233,7 @@ class CodeBlocksTest : BaseAbstractTest() { } private object CustomOtherHtmlBlockRenderer : HtmlCodeBlockRenderer { - override fun isApplicable(language: String, code: String): Boolean = language == "other" + override fun isApplicable(language: String): Boolean = language == "other" override fun FlowContent.buildCodeBlock(language: String, code: String) { div("other-language-block") { From e7d6f3de680c925e5c1429a315986339b9bc53aa Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Tue, 14 Nov 2023 19:23:06 +0200 Subject: [PATCH 3/4] HtmlCodeBlockRenderer improvements: * split applicability for defined and undefined languages * make language nullable * improve documentation for applicability and building of content * add a lot of tests for multiple renderers combinations --- .../renderers/html/HtmlCodeBlockRenderer.kt | 32 +- .../dokka/base/renderers/html/HtmlRenderer.kt | 28 +- .../kotlin/renderers/html/CodeBlocksTest.kt | 300 +++++++++++------- 3 files changed, 233 insertions(+), 127 deletions(-) diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt index 33bfc51adb..29af6f980b 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlCodeBlockRenderer.kt @@ -14,12 +14,36 @@ import kotlinx.html.FlowContent public interface HtmlCodeBlockRenderer { /** - * Whether this renderer supports given [language] + * Whether this renderer supports rendering Markdown code blocks + * for the given [language] explicitly specified in the fenced code block definition, */ - public fun isApplicable(language: String): Boolean + public fun isApplicableForDefinedLanguage(language: String): Boolean /** - * Defines how to render [code] for specified [language] via HTML tags + * Whether this renderer supports rendering Markdown code blocks + * for the given [code] when language is not specified in fenced code blocks + * or indented code blocks are used. */ - public fun FlowContent.buildCodeBlock(language: String, code: String) + public fun isApplicableForUndefinedLanguage(code: String): Boolean + + /** + * Defines how to render [code] for specified [language] via HTML tags. + * + * The value of the [language] will be the same as in the input Markdown fenced code block definition. + * In the following example [language] = `kotlin` and [code] = `val a`: + * ~~~markdown + * ```kotlin + * val a + * ``` + * ~~~ + * The value of the [language] will be `null` if language is not specified in the fenced code block definition + * or indented code blocks are used. + * In the following example [language] = `null` and [code] = `val a`: + * ~~~markdown + * ``` + * val a + * ``` + * ~~~ + */ + public fun FlowContent.buildCodeBlock(language: String?, code: String) } diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt index 55a90c1d9e..e7b77383d9 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/renderers/html/HtmlRenderer.kt @@ -817,19 +817,25 @@ public open class HtmlRenderer( code: ContentCodeBlock, pageContext: ContentPage ) { - customCodeBlockRenderers.forEach { renderer -> - if (renderer.isApplicable(code.language)) { - // we use first applicable renderer to override rendering - val codeText = buildString { - code.children.forEach { - when (it) { - is ContentText -> append(it.text) - is ContentBreakLine -> appendLine() - } + if (customCodeBlockRenderers.isNotEmpty()) { + val language = code.language.takeIf(String::isNotBlank) + val codeText = buildString { + code.children.forEach { + when (it) { + is ContentText -> append(it.text) + is ContentBreakLine -> appendLine() } } - return with(renderer) { - buildCodeBlock(code.language, codeText) + } + + // we use first applicable renderer to override rendering + val applicableRenderer = when (language) { + null -> customCodeBlockRenderers.firstOrNull { it.isApplicableForUndefinedLanguage(codeText) } + else -> customCodeBlockRenderers.firstOrNull { it.isApplicableForDefinedLanguage(language) } + } + if (applicableRenderer != null) { + return with(applicableRenderer) { + buildCodeBlock(language, codeText) } } } diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt index 57e4486303..c30463f97e 100644 --- a/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt +++ b/dokka-subprojects/plugin-base/src/test/kotlin/renderers/html/CodeBlocksTest.kt @@ -12,11 +12,14 @@ 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 org.jsoup.nodes.Element import signatures.renderedContent import utils.TestOutputWriter import utils.TestOutputWriterPlugin +import utils.assertContains import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNull class CodeBlocksTest : BaseAbstractTest() { @@ -28,91 +31,137 @@ class CodeBlocksTest : BaseAbstractTest() { } } + private val contentWithExplicitLanguages = + """ + /src/test.kt + package test + + /** + * Hello, world! + * + * ```kotlin + * test("hello kotlin") + * ``` + * + * ```custom + * test("hello custom") + * ``` + * + * ```other + * test("hello other") + * ``` + */ + fun test(string: String) {} + """.trimIndent() + @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(), + contentWithExplicitLanguages, 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()?.wholeText() - ) - assertEquals( - """test("hello custom")""", - content.getElementsByClass("lang-custom").singleOrNull()?.wholeText() - ) - assertEquals( - """test("hello other")""", - content.getElementsByClass("lang-other").singleOrNull()?.wholeText() - ) + assertEquals("""test("hello kotlin")""", content.textOfSingleElementByClass("lang-kotlin")) + assertEquals("""test("hello custom")""", content.textOfSingleElementByClass("lang-custom")) + assertEquals("""test("hello other")""", content.textOfSingleElementByClass("lang-other")) } @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 + contentWithExplicitLanguages, + listOf(SingleRendererPlugin(CustomDefinedHtmlBlockRenderer)) ) { val content = renderedContent("root/test/test.html") - assertEquals( - """test("hello kotlin")""", - content.getElementsByClass("lang-kotlin").singleOrNull()?.wholeText() - ) - assertEquals( - """test("hello custom")""", - content.getElementsByClass("custom-language-block").singleOrNull()?.wholeText() - ) - assertEquals( - """test("hello other")""", - content.getElementsByClass("lang-other").singleOrNull()?.wholeText() - ) + + assertEquals("""test("hello kotlin")""", content.textOfSingleElementByClass("lang-kotlin")) + assertEquals("""test("hello custom")""", content.textOfSingleElementByClass("custom-defined-language-block")) + assertEquals("""test("hello other")""", content.textOfSingleElementByClass("lang-other")) } @Test fun `code block rendering with multiple custom renderers`() = testCode( + contentWithExplicitLanguages, + listOf(MultiRendererPlugin(CustomDefinedHtmlBlockRenderer, OtherDefinedHtmlBlockRenderer)) + ) { + val content = renderedContent("root/test/test.html") + + assertEquals("""test("hello kotlin")""", content.textOfSingleElementByClass("lang-kotlin")) + assertEquals("""test("hello custom")""", content.textOfSingleElementByClass("custom-defined-language-block")) + assertEquals("""test("hello other")""", content.textOfSingleElementByClass("other-defined-language-block")) + } + + private val contentWithImplicitLanguages = + """ + /src/test.kt + package test + + /** + * Hello, world! + * + * ``` + * test("hello kotlin") + * ``` + * + * ``` + * test("hello custom") + * ``` + * + * ``` + * test("hello other") + * ``` + */ + fun test(string: String) {} + """.trimIndent() + + @Test + fun `default code block rendering with undefined language`() = testCode( + contentWithImplicitLanguages, + emptyList() + ) { + val content = renderedContent("root/test/test.html") + + val contentsDefault = content.getElementsByClass("lang-kotlin").map(Element::wholeText) + + assertContains(contentsDefault, """test("hello kotlin")""") + assertContains(contentsDefault, """test("hello custom")""") + assertContains(contentsDefault, """test("hello other")""") + + assertEquals(3, contentsDefault.size) + } + + @Test + fun `code block rendering with custom renderer and undefined language`() = testCode( + contentWithImplicitLanguages, + listOf(SingleRendererPlugin(CustomUndefinedHtmlBlockRenderer)) + ) { + val content = renderedContent("root/test/test.html") + + val contentsDefault = content.getElementsByClass("lang-kotlin").map(Element::wholeText) + + assertContains(contentsDefault, """test("hello kotlin")""") + assertContains(contentsDefault, """test("hello other")""") + + assertEquals(2, contentsDefault.size) + + assertEquals("""test("hello custom")""", content.textOfSingleElementByClass("custom-undefined-language-block")) + } + + @Test + fun `code block rendering with multiple custom renderers and undefined language`() = testCode( + contentWithImplicitLanguages, + listOf(MultiRendererPlugin(CustomUndefinedHtmlBlockRenderer, OtherUndefinedHtmlBlockRenderer)) + ) { + val content = renderedContent("root/test/test.html") + + assertEquals("""test("hello kotlin")""", content.textOfSingleElementByClass("lang-kotlin")) + assertEquals("""test("hello custom")""", content.textOfSingleElementByClass("custom-undefined-language-block")) + assertEquals("""test("hello other")""", content.textOfSingleElementByClass("other-undefined-language-block")) + } + + @Test + fun `code block rendering with multiple mixed custom renderers`() = testCode( """ /src/test.kt package test @@ -124,7 +173,7 @@ class CodeBlocksTest : BaseAbstractTest() { * test("hello kotlin") * ``` * - * ```custom + * ``` * test("hello custom") * ``` * @@ -134,21 +183,18 @@ class CodeBlocksTest : BaseAbstractTest() { */ fun test(string: String) {} """.trimIndent(), - listOf(CustomPlugin(applyOtherRenderer = true)) + listOf( + MultiRendererPlugin( + CustomUndefinedHtmlBlockRenderer, + OtherDefinedHtmlBlockRenderer, + ) + ) ) { val content = renderedContent("root/test/test.html") - assertEquals( - """test("hello kotlin")""", - content.getElementsByClass("lang-kotlin").singleOrNull()?.wholeText() - ) - assertEquals( - """test("hello custom")""", - content.getElementsByClass("custom-language-block").singleOrNull()?.wholeText() - ) - assertEquals( - """test("hello other")""", - content.getElementsByClass("other-language-block").singleOrNull()?.wholeText() - ) + + assertEquals("""test("hello kotlin")""", content.textOfSingleElementByClass("lang-kotlin")) + assertEquals("""test("hello custom")""", content.textOfSingleElementByClass("custom-undefined-language-block")) + assertEquals("""test("hello other")""", content.textOfSingleElementByClass("other-defined-language-block")) } @Test @@ -171,16 +217,10 @@ class CodeBlocksTest : BaseAbstractTest() { * * test("hello custom") * ``` - * - * ```other - * // something before linebreak - * - * test("hello other") - * ``` */ fun test(string: String) {} """.trimIndent(), - listOf(CustomPlugin(applyOtherRenderer = false)) // we add only one custom renderer + listOf(SingleRendererPlugin(CustomDefinedHtmlBlockRenderer)) ) { val content = renderedContent("root/test/test.html") assertEquals( @@ -189,7 +229,7 @@ class CodeBlocksTest : BaseAbstractTest() { test("hello kotlin") """.trimIndent(), - content.getElementsByClass("lang-kotlin").singleOrNull()?.wholeText() + content.textOfSingleElementByClass("lang-kotlin") ) assertEquals( """ @@ -197,15 +237,7 @@ class CodeBlocksTest : BaseAbstractTest() { test("hello custom") """.trimIndent(), - content.getElementsByClass("custom-language-block").singleOrNull()?.wholeText() - ) - assertEquals( - """ - // something before linebreak - - test("hello other") - """.trimIndent(), - content.getElementsByClass("lang-other").singleOrNull()?.wholeText() + content.textOfSingleElementByClass("custom-defined-language-block") ) } @@ -222,36 +254,80 @@ class CodeBlocksTest : BaseAbstractTest() { } } - private object CustomHtmlBlockRenderer : HtmlCodeBlockRenderer { - override fun isApplicable(language: String): Boolean = language == "custom" + private fun Element.textOfSingleElementByClass(className: String): String { + val elements = getElementsByClass(className) + assertEquals(1, elements.size) + return elements.single().wholeText() + } + + private object CustomDefinedHtmlBlockRenderer : HtmlCodeBlockRenderer { + override fun isApplicableForDefinedLanguage(language: String): Boolean = language == "custom" + override fun isApplicableForUndefinedLanguage(code: String): Boolean = false - override fun FlowContent.buildCodeBlock(language: String, code: String) { - div("custom-language-block") { + override fun FlowContent.buildCodeBlock(language: String?, code: String) { + assertEquals("custom", language) + div("custom-defined-language-block") { text(code) } } } - private object CustomOtherHtmlBlockRenderer : HtmlCodeBlockRenderer { - override fun isApplicable(language: String): Boolean = language == "other" + private object OtherDefinedHtmlBlockRenderer : HtmlCodeBlockRenderer { + override fun isApplicableForDefinedLanguage(language: String): Boolean = language == "other" + override fun isApplicableForUndefinedLanguage(code: String): Boolean = false - override fun FlowContent.buildCodeBlock(language: String, code: String) { - div("other-language-block") { + override fun FlowContent.buildCodeBlock(language: String?, code: String) { + assertEquals("other", language) + div("other-defined-language-block") { text(code) } } } - class CustomPlugin(applyOtherRenderer: Boolean) : DokkaPlugin() { - val customHtmlBlockRenderer by extending { - plugin().htmlCodeBlockRenderers with CustomHtmlBlockRenderer + private object CustomUndefinedHtmlBlockRenderer : HtmlCodeBlockRenderer { + override fun isApplicableForDefinedLanguage(language: String): Boolean = false + override fun isApplicableForUndefinedLanguage(code: String): Boolean = code.contains("custom") + + override fun FlowContent.buildCodeBlock(language: String?, code: String) { + assertNull(language) + div("custom-undefined-language-block") { + text(code) + } } + } + + private object OtherUndefinedHtmlBlockRenderer : HtmlCodeBlockRenderer { + override fun isApplicableForDefinedLanguage(language: String): Boolean = false + override fun isApplicableForUndefinedLanguage(code: String): Boolean = code.contains("other") - val otherHtmlBlockRenderer by extending { - plugin().htmlCodeBlockRenderers with CustomOtherHtmlBlockRenderer applyIf { - applyOtherRenderer + override fun FlowContent.buildCodeBlock(language: String?, code: String) { + assertNull(language) + div("other-undefined-language-block") { + text(code) } } + } + + class SingleRendererPlugin(renderer: HtmlCodeBlockRenderer) : DokkaPlugin() { + val codeBlockRenderer by extending { + plugin().htmlCodeBlockRenderers with renderer + } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = + PluginApiPreviewAcknowledgement + } + + class MultiRendererPlugin( + renderer1: HtmlCodeBlockRenderer, + renderer2: HtmlCodeBlockRenderer + ) : DokkaPlugin() { + val codeBlockRenderer1 by extending { + plugin().htmlCodeBlockRenderers with renderer1 + } + val codeBlockRenderer2 by extending { + plugin().htmlCodeBlockRenderers with renderer2 + } @OptIn(DokkaPluginApiPreview::class) override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = From e05cef5d92e2efa9e6307e49521b1a26f652f758 Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Tue, 14 Nov 2023 19:30:28 +0200 Subject: [PATCH 4/4] fix api dump --- dokka-subprojects/plugin-base/api/plugin-base.api | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dokka-subprojects/plugin-base/api/plugin-base.api b/dokka-subprojects/plugin-base/api/plugin-base.api index 4041713948..8d768b4222 100644 --- a/dokka-subprojects/plugin-base/api/plugin-base.api +++ b/dokka-subprojects/plugin-base/api/plugin-base.api @@ -300,7 +300,8 @@ public final class org/jetbrains/dokka/base/renderers/html/CustomResourceInstall 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;)Z + public abstract fun isApplicableForDefinedLanguage (Ljava/lang/String;)Z + public abstract fun isApplicableForUndefinedLanguage (Ljava/lang/String;)Z } public final class org/jetbrains/dokka/base/renderers/html/HtmlFormatingUtilsKt {