package org.jetbrains.dokka.base.renderers.html import kotlinx.coroutines.* import kotlinx.html.* import kotlinx.html.stream.createHTML import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.renderers.DefaultRenderer import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.DFunction import org.jetbrains.dokka.model.SourceSetData import org.jetbrains.dokka.pages.* 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 java.io.File open class HtmlRenderer( context: DokkaContext ) : DefaultRenderer(context) { private val pageList = mutableListOf() override val preprocessors = context.plugin().query { htmlPreprocessors } + context.plugin().querySingle { samplesTransformer } override fun FlowContent.wrapGroup( node: ContentGroup, pageContext: ContentPage, childrenCallback: FlowContent.() -> Unit ) { val additionalClasses = node.style.joinToString(" ") { it.toString().toLowerCase() } return when { node.dci.kind == ContentKind.Symbol -> div("symbol $additionalClasses") { childrenCallback() } node.dci.kind == ContentKind.BriefComment -> div("brief $additionalClasses") { childrenCallback() } node.dci.kind == ContentKind.Cover -> div("cover $additionalClasses") { childrenCallback() } node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() } node.hasStyle(TextStyle.Block) -> div(additionalClasses) { childrenCallback() } else -> childrenCallback() } } override fun FlowContent.buildPlatformDependent(content: PlatformHintedContent, pageContext: ContentPage) = buildPlatformDependent(content.sourceSets.map { it to setOf(content.inner) }.toMap(), pageContext) private fun FlowContent.buildPlatformDependent( nodes: Map>, pageContext: ContentPage ) { div("platform-hinted") { attributes["data-platform-hinted"] = "data-platform-hinted" val contents = nodes.toList().mapIndexed { index, (sourceSet, elements) -> sourceSet to createHTML(prettyPrint = false).div(classes = "content") { if (index == 0) attributes["data-active"] = "" attributes["data-togglable"] = sourceSet.sourceSetName elements.forEach { buildContentNode(it, pageContext, setOf(sourceSet)) } } } if (contents.size != 1) { div("platform-bookmarks-row") { attributes["data-toggle-list"] = "data-toggle-list" contents.forEachIndexed { index, pair -> button(classes = "platform-bookmark") { if (index == 0) attributes["data-active"] = "" attributes["data-toggle"] = pair.first.sourceSetName text(pair.first.sourceSetName) } } } } contents.forEach { consumer.onTagContentUnsafe { +it.second } } } } override fun FlowContent.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) { val distinct = node.children.flatMap { instance -> instance.sourceSets.map { sourceSet -> Pair(instance, sourceSet) to Pair( createHTML(prettyPrint = false).div { instance.before?.let { before -> buildContentNode(before, pageContext, setOf(sourceSet)) } }.stripDiv(), createHTML(prettyPrint = false).div { instance.after?.let { after -> buildContentNode(after, pageContext, setOf(sourceSet)) } }.stripDiv() ) } }.groupBy( Pair, Pair>::second, Pair, Pair>::first ) distinct.forEach { val groupedDivergent = it.value.groupBy { it.second } consumer.onTagContentUnsafe { +it.key.first } consumer.onTagContentUnsafe { +createHTML(prettyPrint = false).div("main-subrow") { if (node.implicitlySourceSetHinted) { buildPlatformDependent( groupedDivergent.map { (sourceSet, elements) -> sourceSet to elements.map { e -> e.first.divergent } }.toMap(), pageContext ) if (distinct.size > 1 && groupedDivergent.size == 1) { createPlatformTags(node, groupedDivergent.keys) } } else { it.value.forEach { buildContentNode(it.first.divergent, pageContext, setOf(it.second)) } } } } consumer.onTagContentUnsafe { +it.key.second } } } override fun FlowContent.buildList( node: ContentList, pageContext: ContentPage, sourceSetRestriction: Set? ) = if (node.ordered) ol { buildListItems(node.children, pageContext, sourceSetRestriction) } else ul { buildListItems(node.children, pageContext, sourceSetRestriction) } open fun OL.buildListItems( items: List, pageContext: ContentPage, sourceSetRestriction: Set? = null ) { items.forEach { if (it is ContentList) buildList(it, pageContext) else li { it.build(this, pageContext, sourceSetRestriction) } } } open fun UL.buildListItems( items: List, pageContext: ContentPage, sourceSetRestriction: Set? = null ) { 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.extra.allOfType().joinAttr() img(src = node.address, alt = node.altText) } else { println("Unrecognized resource type: $node") } } private fun FlowContent.buildRow( node: ContentGroup, pageContext: ContentPage, sourceSetRestriction: Set? ) { node.children .filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } .takeIf { it.isNotEmpty() } ?.let { div(classes = "table-row") { it.filterIsInstance().takeIf { it.isNotEmpty() }?.let { div("main-subrow " + node.style.joinToString(" ")) { it.filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } .forEach { it.build(this, pageContext, sourceSetRestriction) if (ContentKind.shouldBePlatformTagged(node.dci.kind) && (node.sourceSets.size == 1)) createPlatformTags(node) } } } it.filter { it !is ContentLink }.takeIf { it.isNotEmpty() }?.let { div("platform-dependent-row keyValue") { val title = it.filter { it.style.contains(ContentStyle.RowTitle) } div { title.forEach { it.build(this, pageContext, sourceSetRestriction) } } div("title") { (it - title).forEach { it.build(this, pageContext, sourceSetRestriction) } } } } } } } private fun FlowContent.createPlatformTags(node: ContentNode, sourceSetRestriction: Set? = null) { node.takeIf { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } }?.let { div("platform-tags") { node.sourceSets.filter { sourceSetRestriction == null || it in sourceSetRestriction }.forEach { div("platform-tag") { if (it.sourceSetName.equals("common", ignoreCase = true)) classes = classes + "common" text(it.sourceSetName) } } } } } override fun FlowContent.buildTable( node: ContentTable, pageContext: ContentPage, sourceSetRestriction: Set? ) { div(classes = "table") { node.children.forEach { buildRow(it, pageContext, sourceSetRestriction) } } } 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) = div(classes = "breadcrumbs") { 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 ) { span(classes = "code") { val iterator = code.iterator() while (iterator.hasNext()) { val element = iterator.next() +(when (element) { is ContentText -> element.text is ContentBreakLine -> "\n" else -> run { context.logger.error("Cannot cast $element as ContentText!"); "" } }) if (iterator.hasNext()) { buildNewLine() } } } } override suspend 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) { when { textNode.hasStyle(TextStyle.Indented) -> consumer.onTagContentEntity(Entities.nbsp) } text(textNode.text) } override fun render(root: RootPageNode) { super.render(root) runBlocking(Dispatchers.Default) { launch { 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) { div { id = "content" attributes["pageIds"] = page.dri.first().toString() content(this, page) } } open fun buildHtml(page: PageNode, resources: List, content: FlowContent.() -> Unit) = createHTML().html { head { meta(name = "viewport", content = "width=device-width, initial-scale=1", charset = "UTF-8") 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 = "container" div { id = "leftColumn" div { id = "logo" } div { id = "sideMenu" } } div { id = "main" div { id = "searchBar" } script(type = ScriptType.textJavaScript, src = page.root("scripts/pages.js")) {} script(type = ScriptType.textJavaScript, src = page.root("scripts/main.js")) {} content() } } } } } fun List.joinAttr() = joinToString(" ") { it.extraKey + "=" + it.extraValue } private fun String.stripDiv() = drop(5).dropLast(6) // TODO: Find a way to do it without arbitrary trims private val PageNode.isNavigable: Boolean get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing