diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index 488d2d8d65..0ad9c045b1 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -510,6 +510,8 @@ public final class org/jetbrains/dokka/base/renderers/html/StylesInstaller : org } public final class org/jetbrains/dokka/base/renderers/html/TagsKt { + public static final field TEMPLATE_COMMAND_BEGIN_BORDER Ljava/lang/String; + public static final field TEMPLATE_COMMAND_END_BORDER Ljava/lang/String; public static final fun buildAsInnerHtml (Lkotlin/jvm/functions/Function1;)Ljava/lang/String; public static final fun strike (Lkotlinx/html/FlowOrPhrasingContent;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public static synthetic fun strike$default (Lkotlinx/html/FlowOrPhrasingContent;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V @@ -517,6 +519,8 @@ public final class org/jetbrains/dokka/base/renderers/html/TagsKt { public static final fun templateCommand (Lkotlinx/html/TagConsumer;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public static synthetic fun templateCommand$default (Lkotlinx/html/FlowOrMetaDataContent;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static synthetic fun templateCommand$default (Lkotlinx/html/TagConsumer;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun templateCommandAsHtmlComment (Lkotlinx/html/FlowOrMetaDataContent;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun templateCommandAsHtmlComment$default (Lkotlinx/html/FlowOrMetaDataContent;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static final fun templateCommandFor (Lorg/jetbrains/dokka/base/templating/Command;Lkotlinx/html/TagConsumer;)Lorg/jetbrains/dokka/base/renderers/html/TemplateCommand; public static final fun wbr (Lkotlinx/html/FlowOrPhrasingContent;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public static synthetic fun wbr$default (Lkotlinx/html/FlowOrPhrasingContent;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt index 2906e8f277..09232519ba 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -23,6 +23,8 @@ import org.jetbrains.dokka.utilities.htmlEscape import org.jetbrains.kotlin.utils.addIfNotNull import java.net.URI +internal const val TEMPLATE_REPLACEMENT: String = "###" + open class HtmlRenderer( context: DokkaContext ) : DefaultRenderer(context) { @@ -778,11 +780,11 @@ open class HtmlRenderer( head { meta(name = "viewport", content = "width=device-width, initial-scale=1", charset = "UTF-8") title(page.name) - templateCommand(PathToRootSubstitutionCommand("###", default = pathToRoot)) { - link(href = "###images/logo-icon.svg", rel = "icon", type = "image/svg") + templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) { + link(href = "${TEMPLATE_REPLACEMENT}images/logo-icon.svg", rel = "icon", type = "image/svg") } - templateCommand(PathToRootSubstitutionCommand("###", default = pathToRoot)) { - script { unsafe { +"""var pathToRoot = "###";""" } } + templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) { + script { unsafe { +"""var pathToRoot = "$TEMPLATE_REPLACEMENT";""" } } } // This script doesn't need to be there but it is nice to have since app in dark mode doesn't 'blink' (class is added before it is rendered) script { @@ -803,10 +805,10 @@ open class HtmlRenderer( rel = LinkRel.stylesheet, href = it ) - else templateCommand(PathToRootSubstitutionCommand("###", default = pathToRoot)) { + else templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) { link( rel = LinkRel.stylesheet, - href = "###$it" + href = TEMPLATE_REPLACEMENT + it ) } it.substringBefore('?').substringAfterLast('.') == "js" -> @@ -815,10 +817,10 @@ open class HtmlRenderer( src = it ) { async = true - } else templateCommand(PathToRootSubstitutionCommand("###", default = pathToRoot)) { + } else templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) { script( type = ScriptType.textJavaScript, - src = "###$it" + src = TEMPLATE_REPLACEMENT + it ) { if (it == "scripts/main.js") defer = true @@ -827,8 +829,8 @@ open class HtmlRenderer( } } it.isImage() -> if (it.isAbsolute) link(href = it) - else templateCommand(PathToRootSubstitutionCommand("###", default = pathToRoot)) { - link(href = "###$it") + else templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) { + link(href = TEMPLATE_REPLACEMENT + it) } else -> unsafe { +it } } diff --git a/plugins/base/src/main/kotlin/renderers/html/Tags.kt b/plugins/base/src/main/kotlin/renderers/html/Tags.kt index f79d16338a..6ebf5c7f31 100644 --- a/plugins/base/src/main/kotlin/renderers/html/Tags.kt +++ b/plugins/base/src/main/kotlin/renderers/html/Tags.kt @@ -26,6 +26,16 @@ inline fun FlowOrPhrasingContent.strike(classes : String? = null, crossinline bl open class STRIKE(initialAttributes : Map, override val consumer : TagConsumer<*>) : HTMLTag("strike", consumer, initialAttributes, null, false, false), HtmlBlockInlineTag { } +const val TEMPLATE_COMMAND_BEGIN_BORDER = "[+]cmd:" +const val TEMPLATE_COMMAND_END_BORDER = "[-]cmd" + +fun FlowOrMetaDataContent.templateCommandAsHtmlComment(data: Command, block: FlowOrMetaDataContent.() -> Unit = {}): Unit = + (consumer as? ImmediateResolutionTagConsumer)?.processCommand(data, block) + ?: let{ + comment( "$TEMPLATE_COMMAND_BEGIN_BORDER${toJsonString(data)}") + block() + comment(TEMPLATE_COMMAND_END_BORDER) + } fun FlowOrMetaDataContent.templateCommand(data: Command, block: TemplateBlock = {}): Unit = (consumer as? ImmediateResolutionTagConsumer)?.processCommand(data, block) diff --git a/plugins/templating/api/templating.api b/plugins/templating/api/templating.api index a0fb512288..6ad3983123 100644 --- a/plugins/templating/api/templating.api +++ b/plugins/templating/api/templating.api @@ -40,6 +40,16 @@ public final class org/jetbrains/dokka/templates/CommandHandler$DefaultImpls { public static fun finish (Lorg/jetbrains/dokka/templates/CommandHandler;Ljava/io/File;)V } +public abstract interface class org/jetbrains/dokka/templates/CommentCommandHandler { + public abstract fun canHandle (Lorg/jetbrains/dokka/base/templating/Command;)Z + public abstract fun finish (Ljava/io/File;)V + public abstract fun handleCommand (Ljava/util/List;Lorg/jetbrains/dokka/base/templating/Command;Ljava/io/File;Ljava/io/File;)V +} + +public final class org/jetbrains/dokka/templates/CommentCommandHandler$DefaultImpls { + public static fun finish (Lorg/jetbrains/dokka/templates/CommentCommandHandler;Ljava/io/File;)V +} + public final class org/jetbrains/dokka/templates/DefaultMultiModuleTemplateProcessor : org/jetbrains/dokka/templates/MultiModuleTemplateProcessor { public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; @@ -54,6 +64,7 @@ public final class org/jetbrains/dokka/templates/DefaultSubmoduleTemplateProcess public final class org/jetbrains/dokka/templates/DirectiveBasedHtmlTemplateProcessingStrategy : org/jetbrains/dokka/templates/TemplateProcessingStrategy { public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V public fun finish (Ljava/io/File;)V + public final fun handleCommand (Ljava/util/List;Lorg/jetbrains/dokka/base/templating/Command;Ljava/io/File;Ljava/io/File;)V public final fun handleCommand (Lorg/jsoup/nodes/Element;Lorg/jetbrains/dokka/base/templating/Command;Ljava/io/File;Ljava/io/File;)V public fun process (Ljava/io/File;Ljava/io/File;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaModuleDescription;)Z } @@ -77,10 +88,11 @@ public abstract interface class org/jetbrains/dokka/templates/SubmoduleTemplateP public abstract fun process (Ljava/util/List;)Lorg/jetbrains/dokka/templates/TemplatingResult; } -public final class org/jetbrains/dokka/templates/SubstitutionCommandHandler : org/jetbrains/dokka/templates/CommandHandler { +public final class org/jetbrains/dokka/templates/SubstitutionCommandHandler : org/jetbrains/dokka/templates/CommandHandler, org/jetbrains/dokka/templates/CommentCommandHandler { public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V public fun canHandle (Lorg/jetbrains/dokka/base/templating/Command;)Z public fun finish (Ljava/io/File;)V + public fun handleCommand (Ljava/util/List;Lorg/jetbrains/dokka/base/templating/Command;Ljava/io/File;Ljava/io/File;)V public fun handleCommand (Lorg/jsoup/nodes/Element;Lorg/jetbrains/dokka/base/templating/Command;Ljava/io/File;Ljava/io/File;)V } @@ -101,17 +113,17 @@ public abstract interface class org/jetbrains/dokka/templates/TemplateProcessor } public final class org/jetbrains/dokka/templates/TemplatingContext { - public fun (Ljava/io/File;Ljava/io/File;Lorg/jsoup/nodes/Element;Lorg/jetbrains/dokka/base/templating/Command;)V + public fun (Ljava/io/File;Ljava/io/File;Ljava/util/List;Lorg/jetbrains/dokka/base/templating/Command;)V public final fun component1 ()Ljava/io/File; public final fun component2 ()Ljava/io/File; - public final fun component3 ()Lorg/jsoup/nodes/Element; + public final fun component3 ()Ljava/util/List; public final fun component4 ()Lorg/jetbrains/dokka/base/templating/Command; - public final fun copy (Ljava/io/File;Ljava/io/File;Lorg/jsoup/nodes/Element;Lorg/jetbrains/dokka/base/templating/Command;)Lorg/jetbrains/dokka/templates/TemplatingContext; - public static synthetic fun copy$default (Lorg/jetbrains/dokka/templates/TemplatingContext;Ljava/io/File;Ljava/io/File;Lorg/jsoup/nodes/Element;Lorg/jetbrains/dokka/base/templating/Command;ILjava/lang/Object;)Lorg/jetbrains/dokka/templates/TemplatingContext; + public final fun copy (Ljava/io/File;Ljava/io/File;Ljava/util/List;Lorg/jetbrains/dokka/base/templating/Command;)Lorg/jetbrains/dokka/templates/TemplatingContext; + public static synthetic fun copy$default (Lorg/jetbrains/dokka/templates/TemplatingContext;Ljava/io/File;Ljava/io/File;Ljava/util/List;Lorg/jetbrains/dokka/base/templating/Command;ILjava/lang/Object;)Lorg/jetbrains/dokka/templates/TemplatingContext; public fun equals (Ljava/lang/Object;)Z public final fun getCommand ()Lorg/jetbrains/dokka/base/templating/Command; - public final fun getElement ()Lorg/jsoup/nodes/Element; public final fun getInput ()Ljava/io/File; + public final fun getNodes ()Ljava/util/List; public final fun getOutput ()Ljava/io/File; public fun hashCode ()I public fun toString ()Ljava/lang/String; diff --git a/plugins/templating/src/main/kotlin/templates/CommandHandler.kt b/plugins/templating/src/main/kotlin/templates/CommandHandler.kt index d72092a19c..30277648b5 100644 --- a/plugins/templating/src/main/kotlin/templates/CommandHandler.kt +++ b/plugins/templating/src/main/kotlin/templates/CommandHandler.kt @@ -2,10 +2,17 @@ package org.jetbrains.dokka.templates import org.jetbrains.dokka.base.templating.Command import org.jsoup.nodes.Element +import org.jsoup.nodes.Node import java.io.File interface CommandHandler { fun handleCommand(element: Element, command: Command, input: File, output: File) fun canHandle(command: Command): Boolean fun finish(output: File) {} +} + +interface CommentCommandHandler { + fun handleCommand(nodes: List, command: Command, input: File, output: File) + fun canHandle(command: Command): Boolean + fun finish(output: File) {} } \ No newline at end of file diff --git a/plugins/templating/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt b/plugins/templating/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt index 2b4951a171..e242cd53e1 100644 --- a/plugins/templating/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt +++ b/plugins/templating/src/main/kotlin/templates/DirectiveBasedTemplateProcessing.kt @@ -1,13 +1,18 @@ package org.jetbrains.dokka.templates import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.renderers.html.TEMPLATE_COMMAND_BEGIN_BORDER +import org.jetbrains.dokka.base.renderers.html.TEMPLATE_COMMAND_END_BORDER import org.jetbrains.dokka.base.templating.Command import org.jetbrains.dokka.base.templating.parseJson import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.plugin import org.jetbrains.dokka.plugability.query import org.jsoup.Jsoup +import org.jsoup.nodes.Comment import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode import java.io.File import java.nio.file.Files @@ -20,9 +25,17 @@ class DirectiveBasedHtmlTemplateProcessingStrategy(private val context: DokkaCon if (input.isFile && input.extension == "html") { val document = Jsoup.parse(input, "UTF-8") document.outputSettings().indentAmount(0).prettyPrint(false) + document.select("dokka-template-command").forEach { handleCommand(it, parseJson(it.attr("data")), input, output) } + extractCommandsFromComments(document) { nodes, command -> + val nodesTrimed = + nodes.dropWhile { (it is TextNode && it.isBlank).also { res -> if (res) it.remove() } } + .dropLastWhile { (it is TextNode && it.isBlank).also { res -> if (res) it.remove() } } + handleCommand(nodesTrimed, command, input, output) + } + Files.write(output.toPath(), listOf(document.outerHtml())) true } else false @@ -33,7 +46,43 @@ class DirectiveBasedHtmlTemplateProcessingStrategy(private val context: DokkaCon context.logger.warn("Unknown templating command $command") else handlers.forEach { it.handleCommand(element, command, input, output) } + } + + fun handleCommand(nodes: List, command: Command, input: File, output: File) { + val handlers = directiveBasedCommandHandlers.filterIsInstance().filter { it.canHandle(command) } + if (handlers.isEmpty()) + context.logger.warn("Unknown templating command $command") + else + handlers.forEach { it.handleCommand(nodes, command, input, output) } + } + private fun extractCommandsFromComments(node: Node, startFrom: Int = 0, handler: (List, Command) -> Unit) { + val nodes: MutableList = mutableListOf() + var lastStartBorder: Comment? = null + var firstStartBorder: Comment? = null + for (ind in startFrom until node.childNodeSize()) { + when (val currentChild = node.childNode(ind)) { + is Comment -> if (currentChild.data?.startsWith(TEMPLATE_COMMAND_BEGIN_BORDER) == true) { + lastStartBorder = currentChild + firstStartBorder = firstStartBorder ?: currentChild + nodes.clear() + } else if (lastStartBorder != null && currentChild.data?.startsWith(TEMPLATE_COMMAND_END_BORDER) == true) { + lastStartBorder.remove() + val cmd: Command? = + lastStartBorder.data?.removePrefix(TEMPLATE_COMMAND_BEGIN_BORDER)?.let { parseJson(it) } + cmd?.let { handler(nodes, it) } + currentChild.remove() + extractCommandsFromComments(node, firstStartBorder?.siblingIndex() ?: 0, handler) + return + } else { + lastStartBorder?.let { nodes.add(currentChild) } + } + else -> { + extractCommandsFromComments(currentChild, handler = handler) + lastStartBorder?.let { nodes.add(currentChild) } + } + } + } } override fun finish(output: File) { diff --git a/plugins/templating/src/main/kotlin/templates/SubstitutionCommandHandler.kt b/plugins/templating/src/main/kotlin/templates/SubstitutionCommandHandler.kt index c7b151378e..faceef4f51 100644 --- a/plugins/templating/src/main/kotlin/templates/SubstitutionCommandHandler.kt +++ b/plugins/templating/src/main/kotlin/templates/SubstitutionCommandHandler.kt @@ -11,30 +11,37 @@ import org.jsoup.nodes.Node import org.jsoup.nodes.TextNode import java.io.File -class SubstitutionCommandHandler(context: DokkaContext) : CommandHandler { +class SubstitutionCommandHandler(context: DokkaContext) : CommandHandler, CommentCommandHandler { override fun handleCommand(element: Element, command: Command, input: File, output: File) { command as SubstitutionCommand - substitute(element, TemplatingContext(input, output, element, command)) + val childrenCopy = element.children().toList() + substitute(childrenCopy, TemplatingContext(input, output, childrenCopy, command)) + + val position = element.elementSiblingIndex() + val parent = element.parent() + element.remove() + + parent?.insertChildren(position, childrenCopy) + } + + override fun handleCommand(nodes: List, command: Command, input: File, output: File) { + command as SubstitutionCommand + substitute(nodes, TemplatingContext(input, output, nodes, command)) } override fun canHandle(command: Command): Boolean = command is SubstitutionCommand + override fun finish(output: File) { } + private val substitutors = context.plugin().query { substitutor } private fun findSubstitution(commandContext: TemplatingContext, match: MatchResult): String = substitutors.asSequence().mapNotNull { it.trySubstitute(commandContext, match) }.firstOrNull() ?: match.value - private fun substitute(element: Element, commandContext: TemplatingContext) { + private fun substitute(elements: List, commandContext: TemplatingContext) { val regex = commandContext.command.pattern.toRegex() - element.children().forEach { it.traverseToSubstitute(regex, commandContext) } - - val childrenCopy = element.children().toList() - val position = element.elementSiblingIndex() - val parent = element.parent() - element.remove() - - parent.insertChildren(position, childrenCopy) + elements.forEach { it.traverseToSubstitute(regex, commandContext) } } private fun Node.traverseToSubstitute(regex: Regex, commandContext: TemplatingContext) { diff --git a/plugins/templating/src/main/kotlin/templates/TemplateProcessor.kt b/plugins/templating/src/main/kotlin/templates/TemplateProcessor.kt index 5f36530bbf..9ea9139955 100644 --- a/plugins/templating/src/main/kotlin/templates/TemplateProcessor.kt +++ b/plugins/templating/src/main/kotlin/templates/TemplateProcessor.kt @@ -12,7 +12,7 @@ import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.plugin import org.jetbrains.dokka.plugability.query import org.jetbrains.dokka.plugability.querySingle -import org.jsoup.nodes.Element +import org.jsoup.nodes.Node import java.io.File interface TemplateProcessor @@ -88,7 +88,7 @@ class DefaultMultiModuleTemplateProcessor( data class TemplatingContext( val input: File, val output: File, - val element: Element, + val nodes: List, val command: T, ) diff --git a/plugins/templating/src/test/kotlin/templates/SubstitutionCommandResolutionTest.kt b/plugins/templating/src/test/kotlin/templates/SubstitutionCommandResolutionTest.kt index ce2a8afdc7..44acf34061 100644 --- a/plugins/templating/src/test/kotlin/templates/SubstitutionCommandResolutionTest.kt +++ b/plugins/templating/src/test/kotlin/templates/SubstitutionCommandResolutionTest.kt @@ -3,13 +3,15 @@ package org.jetbrains.dokka.templates import kotlinx.html.a import kotlinx.html.div import kotlinx.html.id +import kotlinx.html.span import kotlinx.html.stream.createHTML import org.jetbrains.dokka.DokkaModuleDescriptionImpl import org.jetbrains.dokka.base.renderers.html.templateCommand +import org.jetbrains.dokka.base.renderers.html.templateCommandAsHtmlComment import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand import org.junit.Rule -import org.junit.rules.TemporaryFolder import org.junit.jupiter.api.Test +import org.junit.rules.TemporaryFolder import utils.assertHtmlEqualsIgnoringWhitespace import java.io.File @@ -36,7 +38,56 @@ class SubstitutionCommandResolutionTest : TemplatingAbstractTest() { id = "logo" } } + checkSubstitutedResult(template, expected) + } + + @Test + fun `should handle PathToRootCommand as HTML comment`() { + val template = createHTML().span { + templateCommandAsHtmlComment(PathToRootSubstitutionCommand(pattern = "###", default = "default")) { + this@span.a { + href = "###index.html" + div { + id = "logo" + } + } + templateCommandAsHtmlComment(PathToRootSubstitutionCommand(pattern = "####", default = "default")) { + this@span.a { + href = "####index.html" + div { + id = "logo" + } + } + } + } + } + + val expected = createHTML().span { + a { + href = "../index.html" + div { + id = "logo" + } + } + a { + href = "../index.html" + div { + id = "logo" + } + } + } + checkSubstitutedResult(template, expected) + } + + private fun createDirectoriesAndWriteContent(content: String): File { + folder.create() + val module1 = folder.newFolder("module1") + val module1Content = module1.resolve("index.html") + module1Content.writeText(content) + return module1Content + } + private fun checkSubstitutedResult(template: String, expected:String) { val testedFile = createDirectoriesAndWriteContent(template) val configuration = dokkaConfiguration { @@ -57,13 +108,4 @@ class SubstitutionCommandResolutionTest : TemplatingAbstractTest() { } } } - - private fun createDirectoriesAndWriteContent(content: String): File { - folder.create() - val module1 = folder.newFolder("module1") - val module1Content = module1.resolve("index.html") - module1Content.writeText(content) - return module1Content - } - }