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.SourceSetData import org.jetbrains.dokka.model.properties.PropertyContainer 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 import java.net.URI open class HtmlRenderer( context: DokkaContext ) : DefaultRenderer(context) { private val sourceSetDependencyMap = with(context.sourceSetCache) { allSourceSets.map { sourceSet -> sourceSet to allSourceSets.filter { sourceSet.dependentSourceSets.contains(it.sourceSetName ) } }.toMap() } private val pageList = mutableListOf() override val preprocessors = context.plugin().query { htmlPreprocessors } override fun FlowContent.wrapGroup( node: ContentGroup, pageContext: ContentPage, childrenCallback: FlowContent.() -> Unit ) { val additionalClasses = node.style.joinToString(" ") { it.toString().toLowerCase() } return when { node.hasStyle(ContentStyle.TabbedContent) -> div(additionalClasses) { val secondLevel = node.children.filterIsInstance().flatMap { it.children }.filterIsInstance().flatMap { it.children }.filterIsInstance() val firstLevel = node.children.filterIsInstance().flatMap { it.children }.filterIsInstance() val renderable = firstLevel.union(secondLevel) div(classes = "tabs-section"){ attributes["tabs-section"] = "tabs-section" renderable.forEachIndexed { index, node -> button(classes = "section-tab"){ if(index == 0 ) attributes["data-active"] = "" attributes["data-togglable"] = node.text text(node.text) } } } div(classes = "tabs-section-body"){ childrenCallback() } } node.hasStyle(ContentStyle.WithExtraAttributes) -> div() { node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } childrenCallback() } 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") { filterButtons(node) childrenCallback() } node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() } node.hasStyle(TextStyle.Block) -> div(additionalClasses) { childrenCallback() } else -> childrenCallback() } } private fun FlowContent.filterButtons(group: ContentGroup) { div(classes = "filter-section") { id = "filter-section" group.sourceSets.forEach { button(classes = "platform-tag platform-selector") { attributes["data-active"] = "" attributes["data-filter"] = it.sourceSetName when(it.platform.key){ "common" -> classes = classes + "common-like" "native" -> classes = classes + "native-like" "jvm" -> classes = classes + "jvm-like" "js" -> classes = classes + "js-like" } text(it.sourceSetName) } } } } override fun FlowContent.buildPlatformDependent(content: PlatformHintedContent, pageContext: ContentPage) = buildPlatformDependent(content.sourceSets.map { it to setOf(content.inner) }.toMap(), pageContext, content.extra) private fun FlowContent.buildPlatformDependent( nodes: Map>, pageContext: ContentPage, extra: PropertyContainer = PropertyContainer.empty() ) { var mergedToOneSourceSet : SourceSetData? = null div("platform-hinted") { attributes["data-platform-hinted"] = "data-platform-hinted" extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } val additionalClasses = if(nodes.toList().size == 1) "single-content" else "" var counter = 0 val contents = nodes.toList().map { (sourceSet, elements) -> sourceSet to createHTML(prettyPrint = false).div { elements.forEach { buildContentNode(it, pageContext, setOf(sourceSet)) } }.stripDiv() }.groupBy(Pair::second, Pair::first).entries.flatMap { (html, sourceSets) -> sourceSets.filterNot { sourceSetDependencyMap[it].orEmpty().any { dependency -> sourceSets.contains(dependency) } }.map { it to createHTML(prettyPrint = false).div(classes = "content $additionalClasses") { if (counter++ == 0) attributes["data-active"] = "" attributes["data-togglable"] = it.sourceSetName unsafe { +html } } } } if (contents.size != 1) { div("platform-bookmarks-row") { attributes["data-toggle-list"] = "data-toggle-list" contents.forEachIndexed { index, pair -> button(classes = "platform-bookmark") { attributes["data-filterable-current"] = pair.first.sourceSetName attributes["data-filterable-set"] = pair.first.sourceSetName if (index == 0) attributes["data-active"] = "" attributes["data-toggle"] = pair.first.sourceSetName when( pair.first.platform.key ){ "common" -> classes = classes + "common-like" "native" -> classes = classes + "native-like" "jvm" -> classes = classes + "jvm-like" "js" -> classes = classes + "js-like" } attributes["data-toggle"] = pair.first.sourceSetName text(pair.first.sourceSetName) } } } } else if (nodes.size > 1) { mergedToOneSourceSet = contents.first().first } contents.forEach { consumer.onTagContentUnsafe { +it.second } } } mergedToOneSourceSet?.let { createPlatformTagBubbles(listOf(it)) } } 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 { +createHTML().div("divergent-group"){ attributes["data-filterable-current"] = groupedDivergent.keys.joinToString(" ") { it.sourceSetName } attributes["data-filterable-set"] = groupedDivergent.keys.joinToString(" ") { it.sourceSetName } consumer.onTagContentUnsafe { +it.key.first } 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?, style: Set