aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/src/main/kotlin/renderers/html/innerTemplating
diff options
context:
space:
mode:
authorVadim Mishenev <vad-mishenev@yandex.ru>2022-03-10 12:33:28 +0300
committerGitHub <noreply@github.com>2022-03-10 12:33:28 +0300
commit597b365ad4512b63387e54d16ba00eb94dccb97d (patch)
tree463f77cb2b27b0664b03be444bba1cbcb10bcbce /plugins/base/src/main/kotlin/renderers/html/innerTemplating
parent8537f0f6fc0d84c0f9918db4f7db9be053d632a8 (diff)
downloaddokka-597b365ad4512b63387e54d16ba00eb94dccb97d.tar.gz
dokka-597b365ad4512b63387e54d16ba00eb94dccb97d.tar.bz2
dokka-597b365ad4512b63387e54d16ba00eb94dccb97d.zip
KT-50452 Make flexible html for customization (#2374)
Diffstat (limited to 'plugins/base/src/main/kotlin/renderers/html/innerTemplating')
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt207
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt16
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt76
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt16
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt5
5 files changed, 320 insertions, 0 deletions
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt
new file mode 100644
index 00000000..9f1ca57e
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt
@@ -0,0 +1,207 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+import freemarker.core.Environment
+import freemarker.template.*
+import kotlinx.html.*
+import kotlinx.html.stream.createHTML
+import org.jetbrains.dokka.DokkaConfiguration
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.DokkaBaseConfiguration
+import org.jetbrains.dokka.base.renderers.URIExtension
+import org.jetbrains.dokka.base.renderers.html.TEMPLATE_REPLACEMENT
+import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer
+import org.jetbrains.dokka.base.renderers.html.templateCommand
+import org.jetbrains.dokka.base.renderers.html.templateCommandAsHtmlComment
+import org.jetbrains.dokka.base.renderers.isImage
+import org.jetbrains.dokka.base.resolvers.local.LocationProvider
+import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand
+import org.jetbrains.dokka.base.templating.ProjectNameSubstitutionCommand
+import org.jetbrains.dokka.base.templating.ReplaceVersionsCommand
+import org.jetbrains.dokka.base.templating.SubstitutionCommand
+import org.jetbrains.dokka.model.DisplaySourceSet
+import org.jetbrains.dokka.model.withDescendants
+import org.jetbrains.dokka.pages.ContentPage
+import org.jetbrains.dokka.pages.PageNode
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.configuration
+import java.net.URI
+
+class DefaultTemplateModelFactory(val context: DokkaContext) : TemplateModelFactory {
+ private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context)
+ private val isPartial = context.configuration.delayTemplateSubstitution
+
+ private fun <R> TagConsumer<R>.prepareForTemplates() =
+ if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this
+ else ImmediateResolutionTagConsumer(this, context)
+
+ data class SourceSetModel(val name: String, val platform: String, val filter: String)
+
+ override fun buildModel(
+ page: PageNode,
+ resources: List<String>,
+ locationProvider: LocationProvider,
+ shouldRenderSourceSetBubbles: Boolean,
+ content: String
+ ): TemplateMap {
+ val path = locationProvider.resolve(page)
+ val pathToRoot = locationProvider.pathToRoot(page)
+ val mapper = mutableMapOf<String, Any>()
+ mapper["pageName"] = page.name
+ mapper["resources"] = PrintDirective {
+ val sb = StringBuilder()
+ if (isPartial)
+ sb.templateCommandAsHtmlComment(
+ PathToRootSubstitutionCommand(
+ TEMPLATE_REPLACEMENT,
+ default = pathToRoot
+ )
+ ) { resourcesForPage(TEMPLATE_REPLACEMENT, resources) }
+ else
+ sb.resourcesForPage(pathToRoot, resources)
+ sb.toString()
+ }
+ mapper["content"] = PrintDirective { content }
+ mapper["version"] = PrintDirective {
+ createHTML().prepareForTemplates().templateCommand(ReplaceVersionsCommand(path.orEmpty()))
+ }
+ mapper["template_cmd"] = TemplateDirective(context.configuration, pathToRoot)
+
+ if (shouldRenderSourceSetBubbles && page is ContentPage) {
+ val sourceSets = page.content.withDescendants()
+ .flatMap { it.sourceSets }
+ .distinct()
+ .sortedBy { it.comparableKey }
+ .map { SourceSetModel(it.name, it.platform.key, it.sourceSetIDs.merged.toString()) }
+ .toList()
+ mapper["sourceSets"] = sourceSets
+ }
+ return mapper
+ }
+
+ override fun buildSharedModel(): TemplateMap = mapOf<String, Any>(
+ "footerMessage" to (configuration?.footerMessage?.takeIf { it.isNotEmpty() }
+ ?: DokkaBaseConfiguration.defaultFooterMessage)
+ )
+
+ private val DisplaySourceSet.comparableKey
+ get() = sourceSetIDs.merged.let { it.scopeId + it.sourceSetName }
+ private val String.isAbsolute: Boolean
+ get() = URI(this).isAbsolute
+
+ private fun Appendable.resourcesForPage(pathToRoot: String, resources: List<String>): Unit =
+ resources.forEach {
+ append(with(createHTML()) {
+ when {
+ it.URIExtension == "css" ->
+ link(
+ rel = LinkRel.stylesheet,
+ href = if (it.isAbsolute) it else "$pathToRoot$it"
+ )
+ it.URIExtension == "js" ->
+ script(
+ type = ScriptType.textJavaScript,
+ src = if (it.isAbsolute) it else "$pathToRoot$it"
+ ) {
+ if (it == "scripts/main.js")
+ defer = true
+ else
+ async = true
+ }
+ it.isImage() -> link(href = if (it.isAbsolute) it else "$pathToRoot$it")
+ else -> null
+ }
+ } ?: it)
+ }
+}
+
+private class PrintDirective(val generateData: () -> String) : TemplateDirectiveModel {
+ override fun execute(
+ env: Environment,
+ params: MutableMap<Any?, Any?>?,
+ loopVars: Array<TemplateModel>?,
+ body: TemplateDirectiveBody?
+ ) {
+ if (params?.isNotEmpty() == true) throw TemplateModelException(
+ "Parameters are not allowed"
+ )
+ if (loopVars?.isNotEmpty() == true) throw TemplateModelException(
+ "Loop variables are not allowed"
+ )
+ env.out.write(generateData())
+ }
+}
+
+private class TemplateDirective(val configuration: DokkaConfiguration, val pathToRoot: String) : TemplateDirectiveModel {
+ override fun execute(
+ env: Environment,
+ params: MutableMap<Any?, Any?>?,
+ loopVars: Array<TemplateModel>?,
+ body: TemplateDirectiveBody?
+ ) {
+ val commandName = params?.get(PARAM_NAME) ?: throw TemplateModelException(
+ "The required $PARAM_NAME parameter is missing."
+ )
+ val replacement = (params[PARAM_REPLACEMENT] as? SimpleScalar)?.asString ?: TEMPLATE_REPLACEMENT
+
+ when ((commandName as? SimpleScalar)?.asString) {
+ "pathToRoot" -> {
+ body ?: throw TemplateModelException(
+ "No directive body for $commandName command."
+ )
+ executeSubstituteCommand(
+ PathToRootSubstitutionCommand(
+ replacement, pathToRoot
+ ),
+ "pathToRoot",
+ pathToRoot,
+ Context(env, body)
+ )
+ }
+ "projectName" -> {
+ body ?: throw TemplateModelException(
+ "No directive body $commandName command."
+ )
+ executeSubstituteCommand(
+ ProjectNameSubstitutionCommand(
+ replacement, configuration.moduleName
+ ),
+ "projectName",
+ configuration.moduleName,
+ Context(env, body)
+ )
+ }
+ else -> throw TemplateModelException(
+ "The parameter $PARAM_NAME $commandName is unknown"
+ )
+ }
+ }
+
+ private data class Context(val env: Environment, val body: TemplateDirectiveBody)
+
+ private fun executeSubstituteCommand(
+ command: SubstitutionCommand,
+ name: String,
+ value: String,
+ ctx: Context
+ ) {
+ if (configuration.delayTemplateSubstitution)
+ ctx.env.out.templateCommandAsHtmlComment(command) {
+ renderWithLocalVar(name, command.pattern, ctx)
+ }
+ else {
+ renderWithLocalVar(name, value, ctx)
+ }
+ }
+
+ private fun renderWithLocalVar(name: String, value: String, ctx: Context) =
+ with(ctx) {
+ env.setVariable(name, SimpleScalar(value))
+ body.render(env.out)
+ env.setVariable(name, null)
+ }
+
+ companion object {
+ const val PARAM_NAME = "name"
+ const val PARAM_REPLACEMENT = "replacement"
+ }
+} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt
new file mode 100644
index 00000000..7d548721
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt
@@ -0,0 +1,16 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+class DefaultTemplateModelMerger : TemplateModelMerger {
+ override fun invoke(
+ factories: List<TemplateModelFactory>,
+ buildModel: TemplateModelFactory.() -> TemplateMap
+ ): TemplateMap {
+ val mapper = mutableMapOf<String, Any?>()
+ factories.map(buildModel).forEach { partialModel ->
+ partialModel.forEach { (k, v) ->
+ mapper[k] = v
+ }
+ }
+ return mapper
+ }
+} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt
new file mode 100644
index 00000000..e3d16d98
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt
@@ -0,0 +1,76 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+import freemarker.cache.ClassTemplateLoader
+import freemarker.cache.FileTemplateLoader
+import freemarker.cache.MultiTemplateLoader
+import freemarker.log.Logger
+import freemarker.template.Configuration
+import freemarker.template.TemplateExceptionHandler
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.DokkaBaseConfiguration
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.configuration
+import java.io.StringWriter
+
+
+enum class DokkaTemplateTypes(val path: String) {
+ BASE("base.ftl")
+}
+
+typealias TemplateMap = Map<String, Any?>
+
+class HtmlTemplater(
+ context: DokkaContext
+) {
+
+ init {
+ // to disable logging, but it isn't reliable see [Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY]
+ // (use SLF4j further)
+ System.setProperty(
+ Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY,
+ System.getProperty(Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY) ?: Logger.LIBRARY_NAME_NONE
+ )
+ }
+
+ private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context)
+ private val templaterConfiguration =
+ Configuration(Configuration.VERSION_2_3_31).apply { configureTemplateEngine() }
+
+ private fun Configuration.configureTemplateEngine() {
+ val loaderFromResources = ClassTemplateLoader(javaClass, "/dokka/templates")
+ templateLoader = configuration?.templatesDir?.let {
+ MultiTemplateLoader(
+ arrayOf(
+ FileTemplateLoader(it),
+ loaderFromResources
+ )
+ )
+ } ?: loaderFromResources
+
+ unsetLocale()
+ defaultEncoding = "UTF-8"
+ templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLER
+ logTemplateExceptions = false
+ wrapUncheckedExceptions = true
+ fallbackOnNullLoopVariable = false
+ templateUpdateDelayMilliseconds = Long.MAX_VALUE
+ }
+
+ fun setupSharedModel(model: TemplateMap) {
+ templaterConfiguration.setSharedVariables(model)
+ }
+
+ fun renderFromTemplate(
+ templateType: DokkaTemplateTypes,
+ generateModel: () -> TemplateMap
+ ): String {
+ val out = StringWriter()
+ // Freemarker has own thread-safe cache to keep templates
+ val template = templaterConfiguration.getTemplate(templateType.path)
+ val model = generateModel()
+ template.process(model, out)
+
+ return out.toString()
+ }
+}
+
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt
new file mode 100644
index 00000000..ceecf201
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt
@@ -0,0 +1,16 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+import org.jetbrains.dokka.base.resolvers.local.LocationProvider
+import org.jetbrains.dokka.pages.PageNode
+
+interface TemplateModelFactory {
+ fun buildModel(
+ page: PageNode,
+ resources: List<String>,
+ locationProvider: LocationProvider,
+ shouldRenderSourceSetBubbles: Boolean,
+ content: String
+ ): TemplateMap
+
+ fun buildSharedModel(): TemplateMap
+} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt
new file mode 100644
index 00000000..7ad96d8f
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt
@@ -0,0 +1,5 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+fun interface TemplateModelMerger {
+ fun invoke(factories: List<TemplateModelFactory>, buildModel: TemplateModelFactory.() -> TemplateMap): TemplateMap
+} \ No newline at end of file