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