package org.jetbrains.dokka.base.renderers.html
import kotlinx.html.*
import kotlinx.html.stream.createHTML
import org.jetbrains.dokka.base.renderers.DefaultRenderer
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.Function
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.plugability.DokkaContext
import java.io.File
open class HtmlRenderer(
context: DokkaContext
) : DefaultRenderer(context) {
private val pageList = mutableListOf()
override val preprocessors = listOf(
RootCreator,
SearchPageInstaller,
ResourceInstaller,
NavigationPageInstaller,
StyleAndScriptsAppender
)
override fun FlowContent.buildList(node: ContentList, pageContext: ContentPage) =
if (node.ordered) ol {
buildListItems(node.children, pageContext)
}
else ul {
buildListItems(node.children, pageContext)
}
open fun OL.buildListItems(items: List, pageContext: ContentPage) {
items.forEach {
if (it is ContentList)
buildList(it, pageContext)
else
li { it.build(this, pageContext) }
}
}
open fun UL.buildListItems(items: List, pageContext: ContentPage) {
items.forEach {
if (it is ContentList)
buildList(it, pageContext)
else
li { it.build(this, pageContext) }
}
}
override fun FlowContent.buildResource(
node: ContentEmbeddedResource,
pageContext: ContentPage
) { // TODO: extension point there
val imageExtensions = setOf("png", "jpg", "jpeg", "gif", "bmp", "tif", "webp", "svg")
return if (File(node.address).extension.toLowerCase() in imageExtensions) {
//TODO: add imgAttrs parsing
val imgAttrs = node.extras.filterIsInstance().joinAttr()
img(src = node.address, alt = node.altText)
} else {
println("Unrecognized resource type: $node")
}
}
override fun FlowContent.buildTable(node: ContentTable, pageContext: ContentPage) {
table {
thead {
node.header.forEach {
tr {
it.children.forEach {
th {
it.build(this@table, pageContext)
}
}
}
}
}
tbody {
node.children.forEach {
tr {
it.children.forEach {
td {
it.build(this, pageContext)
}
}
}
}
}
}
}
override fun FlowContent.buildHeader(level: Int, content: FlowContent.() -> Unit) {
when (level) {
1 -> h1(block = content)
2 -> h2(block = content)
3 -> h3(block = content)
4 -> h4(block = content)
5 -> h5(block = content)
else -> h6(block = content)
}
}
override fun FlowContent.buildNavigation(page: PageNode) =
locationProvider.ancestors(page).asReversed().forEach { node ->
text("/")
if (node.isNavigable) buildLink(node, page)
else text(node.name)
}
private fun FlowContent.buildLink(to: PageNode, from: PageNode) =
buildLink(locationProvider.resolve(to, from)) {
text(to.name)
}
fun FlowContent.buildLink(
to: DRI,
platforms: List,
from: PageNode? = null,
block: FlowContent.() -> Unit
) = buildLink(locationProvider.resolve(to, platforms, from), block)
override fun buildError(node: ContentNode) {
context.logger.error("Unknown ContentNode type: $node")
}
override fun FlowContent.buildNewLine() {
br()
}
override fun FlowContent.buildLink(address: String, content: FlowContent.() -> Unit) =
a(href = address, block = content)
override fun FlowContent.buildCode(code: List, language: String, pageContext: ContentPage) {
buildNewLine()
code.forEach {
+((it as? ContentText)?.text ?: run { context.logger.error("Cannot cast $it as ContentText!"); "" })
buildNewLine()
}
}
override fun renderPage(page: PageNode) {
super.renderPage(page)
if (page is ContentPage) {
pageList.add(
"""{ "name": "${page.name}", ${if (page is ClasslikePageNode) "\"class\": \"${page.name}\"," else ""} "location": "${locationProvider.resolve(
page
)}" }"""
)
}
}
override fun FlowContent.buildText(textNode: ContentText) {
text(textNode.text)
}
override fun render(root: RootPageNode) {
super.render(root)
outputWriter.write("scripts/pages", "var pages = [\n${pageList.joinToString(",\n")}\n]", ".js")
}
private fun PageNode.root(path: String) = locationProvider.resolveRoot(this) + path
override fun buildPage(page: ContentPage, content: (FlowContent, ContentPage) -> Unit): String =
buildHtml(page, page.embeddedResources) { content(this, page) }
open fun buildHtml(page: PageNode, resources: List, content: FlowContent.() -> Unit) =
createHTML().html {
head {
title(page.name)
with(resources) {
filter { it.substringBefore('?').substringAfterLast('.') == "css" }
.forEach { link(rel = LinkRel.stylesheet, href = page.root(it)) }
filter { it.substringBefore('?').substringAfterLast('.') == "js" }
.forEach { script(type = ScriptType.textJavaScript, src = page.root(it)) { async = true } }
}
script { unsafe { +"""var pathToRoot = "${locationProvider.resolveRoot(page)}";""" } }
}
body {
div {
id = "navigation"
div {
id = "searchBar"
form(action = page.root("-search.html"), method = FormMethod.get) {
id = "searchForm"
input(type = InputType.search, name = "query")
input(type = InputType.submit) { value = "Search" }
}
}
div {
id = "sideMenu"
}
}
div {
id = "content"
content()
}
}
}
}
fun List.joinAttr() = joinToString(" ") { it.key + "=" + it.value }
private fun PageNode.pageKind() = when (this) {
is PackagePageNode -> "package"
is ClasslikePageNode -> "class"
is MemberPageNode -> when (this.documentable) {
is Function -> "function"
else -> "other"
}
else -> "other"
}
private val PageNode.isNavigable: Boolean
get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing