diff options
author | Vadim Mishenev <vad-mishenev@yandex.ru> | 2022-03-10 12:33:28 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-10 12:33:28 +0300 |
commit | 597b365ad4512b63387e54d16ba00eb94dccb97d (patch) | |
tree | 463f77cb2b27b0664b03be444bba1cbcb10bcbce /plugins/base/src/main/kotlin/renderers/html/innerTemplating | |
parent | 8537f0f6fc0d84c0f9918db4f7db9be053d632a8 (diff) | |
download | dokka-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')
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 |