aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/base/src/main/kotlin')
-rw-r--r--plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt4
-rw-r--r--plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt5
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt156
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/Tags.kt6
-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
9 files changed, 362 insertions, 129 deletions
diff --git a/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt b/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt
index 8ea8818d..a9ccf600 100644
--- a/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt
+++ b/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt
@@ -9,7 +9,8 @@ data class DokkaBaseConfiguration(
var customAssets: List<File> = defaultCustomAssets,
var separateInheritedMembers: Boolean = separateInheritedMembersDefault,
var footerMessage: String = defaultFooterMessage,
- var mergeImplicitExpectActualDeclarations: Boolean = mergeImplicitExpectActualDeclarationsDefault
+ var mergeImplicitExpectActualDeclarations: Boolean = mergeImplicitExpectActualDeclarationsDefault,
+ var templatesDir: File? = defaultTemplatesDir
) : ConfigurableBlock {
companion object {
val defaultFooterMessage = "© ${Year.now().value} Copyright"
@@ -17,5 +18,6 @@ data class DokkaBaseConfiguration(
val defaultCustomAssets: List<File> = emptyList()
const val separateInheritedMembersDefault: Boolean = false
const val mergeImplicitExpectActualDeclarationsDefault: Boolean = false
+ val defaultTemplatesDir: File? = null
}
} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt b/plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt
index 4619bc53..1cec4769 100644
--- a/plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt
+++ b/plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt
@@ -8,8 +8,11 @@ fun ContentEmbeddedResource.isImage(): Boolean {
return File(address).extension.toLowerCase() in imageExtensions
}
+val String.URIExtension: String
+ get() = substringBefore('?').substringAfterLast('.')
+
fun String.isImage(): Boolean =
- substringBefore('?').substringAfterLast('.') in imageExtensions
+ URIExtension in imageExtensions
object HtmlFileExtensions {
val imageExtensions = setOf("png", "jpg", "jpeg", "gif", "bmp", "tif", "webp", "svg")
diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
index 0ba085cd..05559469 100644
--- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
+++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
@@ -4,10 +4,12 @@ import kotlinx.html.*
import kotlinx.html.stream.createHTML
import org.jetbrains.dokka.DokkaSourceSetID
import org.jetbrains.dokka.base.DokkaBase
-import org.jetbrains.dokka.base.DokkaBaseConfiguration
-import org.jetbrains.dokka.base.DokkaBaseConfiguration.Companion.defaultFooterMessage
import org.jetbrains.dokka.base.renderers.*
import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer
+import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelFactory
+import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelMerger
+import org.jetbrains.dokka.base.renderers.html.innerTemplating.DokkaTemplateTypes
+import org.jetbrains.dokka.base.renderers.html.innerTemplating.HtmlTemplater
import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint
import org.jetbrains.dokka.base.resolvers.local.DokkaBaseLocationProvider
import org.jetbrains.dokka.base.templating.*
@@ -28,8 +30,6 @@ internal const val TEMPLATE_REPLACEMENT: String = "###"
open class HtmlRenderer(
context: DokkaContext
) : DefaultRenderer<FlowContent>(context) {
- private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context)
-
private val sourceSetDependencyMap: Map<DokkaSourceSetID, List<DokkaSourceSetID>> =
context.configuration.sourceSets.associate { sourceSet ->
sourceSet.sourceSetID to context.configuration.sourceSets
@@ -37,6 +37,12 @@ open class HtmlRenderer(
.filter { it in sourceSet.dependentSourceSets }
}
+ private val templateModelFactories = listOf(DefaultTemplateModelFactory(context)) // TODO: Make extension point
+ private val templateModelMerger = DefaultTemplateModelMerger()
+ private val templater = HtmlTemplater(context).apply {
+ setupSharedModel(templateModelMerger.invoke(templateModelFactories) { buildSharedModel() })
+ }
+
private var shouldRenderSourceSetBubbles: Boolean = false
override val preprocessors = context.plugin<DokkaBase>().query { htmlPreprocessors }
@@ -764,138 +770,34 @@ open class HtmlRenderer(
override fun buildPage(page: ContentPage, content: (FlowContent, ContentPage) -> Unit): String =
buildHtml(page, page.embeddedResources) {
- div("main-content") {
- id = "content"
- attributes["pageIds"] = "${context.configuration.moduleName}::${page.pageId}"
- content(this, page)
- }
+ content(this, page)
}
private val String.isAbsolute: Boolean
get() = URI(this).isAbsolute
- open fun buildHtml(page: PageNode, resources: List<String>, content: FlowContent.() -> Unit): String {
- val path = locationProvider.resolve(page)
- val pathToRoot = locationProvider.pathToRoot(page)
- return createHTML().prepareForTemplates().html {
- head {
- meta(name = "viewport", content = "width=device-width, initial-scale=1", charset = "UTF-8")
- title(page.name)
- templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) {
- link(href = "${TEMPLATE_REPLACEMENT}images/logo-icon.svg", rel = "icon", type = "image/svg")
- }
- 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 {
- unsafe {
- +"""
- const storage = localStorage.getItem("dokka-dark-mode")
- const savedDarkMode = storage ? JSON.parse(storage) : false
- if(savedDarkMode === true){
- document.getElementsByTagName("html")[0].classList.add("theme-dark")
- }
- """.trimIndent()
- }
- }
- resources.forEach {
- when {
- it.substringBefore('?').substringAfterLast('.') == "css" ->
- if (it.isAbsolute) link(
- rel = LinkRel.stylesheet,
- href = it
- )
- else templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) {
- link(
- rel = LinkRel.stylesheet,
- href = TEMPLATE_REPLACEMENT + it
- )
- }
- it.substringBefore('?').substringAfterLast('.') == "js" ->
- if (it.isAbsolute) script(
- type = ScriptType.textJavaScript,
- src = it
- ) {
- async = true
- } else templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) {
- script(
- type = ScriptType.textJavaScript,
- src = TEMPLATE_REPLACEMENT + it
- ) {
- if (it == "scripts/main.js")
- defer = true
- else
- async = true
- }
- }
- it.isImage() -> if (it.isAbsolute) link(href = it)
- else templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) {
- link(href = TEMPLATE_REPLACEMENT + it)
- }
- else -> unsafe { +it }
- }
- }
- }
- body {
- div("navigation-wrapper") {
- id = "navigation-wrapper"
- div {
- id = "leftToggler"
- span("icon-toggler")
- }
- div("library-name") {
- clickableLogo(page, pathToRoot)
- }
- div { templateCommand(ReplaceVersionsCommand(path.orEmpty())) }
- div("pull-right d-flex") {
- filterButtons(page)
- button {
- id = "theme-toggle-button"
- span {
- id = "theme-toggle"
- }
- }
- div {
- id = "searchBar"
- }
- }
- }
- div {
- id = "container"
- div {
- id = "leftColumn"
- div {
- id = "sideMenu"
- }
- }
- div {
- id = "main"
- content()
- div(classes = "footer") {
- span("go-to-top-icon") {
- a(href = "#content") {
- id = "go-to-top-link"
- }
- }
- span {
- configuration?.footerMessage?.takeIf { it.isNotEmpty() }
- ?.let { unsafe { raw(it) } }
- ?: text(defaultFooterMessage)
- }
- span("pull-right") {
- span { text("Generated by ") }
- a(href = "https://github.com/Kotlin/dokka") {
- span { text("dokka") }
- span(classes = "padded-icon")
- }
- }
- }
+
+ open fun buildHtml(page: PageNode, resources: List<String>, content: FlowContent.() -> Unit): String =
+ templater.renderFromTemplate(DokkaTemplateTypes.BASE) {
+ val generatedContent =
+ createHTML().div("main-content") {
+ id = "content"
+ (page as? ContentPage)?.let {
+ attributes["pageIds"] = "${context.configuration.moduleName}::${page.pageId}"
}
+ content()
}
+
+ templateModelMerger.invoke(templateModelFactories) {
+ buildModel(
+ page,
+ resources,
+ locationProvider,
+ shouldRenderSourceSetBubbles,
+ generatedContent
+ )
}
}
- }
/**
* This is deliberately left open for plugins that have some other pages above ours and would like to link to them
diff --git a/plugins/base/src/main/kotlin/renderers/html/Tags.kt b/plugins/base/src/main/kotlin/renderers/html/Tags.kt
index 94a53c27..ef27b934 100644
--- a/plugins/base/src/main/kotlin/renderers/html/Tags.kt
+++ b/plugins/base/src/main/kotlin/renderers/html/Tags.kt
@@ -38,6 +38,12 @@ fun FlowOrMetaDataContent.templateCommandAsHtmlComment(data: Command, block: Flo
comment(TEMPLATE_COMMAND_END_BORDER)
}
+fun <T: Appendable> T.templateCommandAsHtmlComment(command: Command, action: T.() -> Unit ) {
+ append("<!--$TEMPLATE_COMMAND_BEGIN_BORDER$TEMPLATE_COMMAND_SEPARATOR${toJsonString(command)}-->")
+ action()
+ append("<!--$TEMPLATE_COMMAND_END_BORDER-->")
+}
+
fun FlowOrMetaDataContent.templateCommand(data: Command, block: TemplateBlock = {}): Unit =
(consumer as? ImmediateResolutionTagConsumer)?.processCommand(data, block)
?: TemplateCommand(attributesMapOf("data", toJsonString(data)), consumer).visit(block)
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