package org.jetbrains.dokka.base.renderers.html
import kotlinx.html.*
import kotlinx.html.stream.createHTML
import org.jetbrains.dokka.DokkaSourceSetID
import org.jetbrains.dokka.Platform
import org.jetbrains.dokka.base.DokkaBase
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.*
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.model.properties.PropertyContainer
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.pages.HtmlContent
import org.jetbrains.dokka.plugability.*
import org.jetbrains.dokka.utilities.htmlEscape
import org.jetbrains.kotlin.utils.addIfNotNull
internal const val TEMPLATE_REPLACEMENT: String = "###"
open class HtmlRenderer(
context: DokkaContext
) : DefaultRenderer(context) {
private val sourceSetDependencyMap: Map> =
context.configuration.sourceSets.associate { sourceSet ->
sourceSet.sourceSetID to context.configuration.sourceSets
.map { it.sourceSetID }
.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().query { htmlPreprocessors }
private val tabSortingStrategy = context.plugin().querySingle { tabSortingStrategy }
private fun TagConsumer.prepareForTemplates() =
if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this
else ImmediateResolutionTagConsumer(this, context)
private fun sortTabs(strategy: TabSortingStrategy, tabs: Collection): List {
val sorted = strategy.sort(tabs)
if (sorted.size != tabs.size)
context.logger.warn("Tab sorting strategy has changed number of tabs from ${tabs.size} to ${sorted.size}")
return sorted
}
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 = sortTabs(tabSortingStrategy, 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 in setOf(ContentKind.Symbol) -> div("symbol $additionalClasses") {
childrenCallback()
}
node.hasStyle(ContentStyle.KDocTag) -> span("kdoc-tag") { childrenCallback() }
node.hasStyle(ContentStyle.Footnote) -> div("footnote") { childrenCallback() }
node.hasStyle(TextStyle.BreakableAfter) -> {
span { childrenCallback() }
wbr { }
}
node.hasStyle(TextStyle.Breakable) -> {
span("breakable-word") { childrenCallback() }
}
node.hasStyle(TextStyle.Span) -> span { childrenCallback() }
node.dci.kind == ContentKind.Symbol -> div("symbol $additionalClasses") { childrenCallback() }
node.dci.kind == SymbolContentKind.Parameters -> {
span("parameters $additionalClasses") {
childrenCallback()
}
}
node.dci.kind == SymbolContentKind.Parameter -> {
span("parameter $additionalClasses") {
childrenCallback()
}
}
node.hasStyle(TextStyle.InlineComment) -> div("inline-comment") { childrenCallback() }
node.dci.kind == ContentKind.BriefComment -> div("brief $additionalClasses") { childrenCallback() }
node.dci.kind == ContentKind.Cover -> div("cover $additionalClasses") { //TODO this can be removed
childrenCallback()
}
node.dci.kind == ContentKind.Deprecation -> div("deprecation-content") { childrenCallback() }
node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() }
node.hasStyle(TextStyle.Block) -> div(additionalClasses) { childrenCallback() }
node.hasStyle(TextStyle.Quotation) -> blockQuote(additionalClasses) { childrenCallback() }
node.hasStyle(TextStyle.FloatingRight) -> span("clearfix") { span("floating-right") { childrenCallback() } }
node.hasStyle(TextStyle.Strikethrough) -> strike { childrenCallback() }
node.isAnchorable -> buildAnchor(
node.anchor!!,
node.anchorLabel!!,
node.sourceSetsFilters
) { childrenCallback() }
node.extra[InsertTemplateExtra] != null -> node.extra[InsertTemplateExtra]?.let { templateCommand(it.command) }
?: Unit
node.hasStyle(ListStyle.DescriptionTerm) -> DT(emptyMap(), consumer).visit {
this@wrapGroup.childrenCallback()
}
node.hasStyle(ListStyle.DescriptionDetails) -> DD(emptyMap(), consumer).visit {
this@wrapGroup.childrenCallback()
}
else -> childrenCallback()
}
}
private fun FlowContent.filterButtons(page: PageNode) {
if (shouldRenderSourceSetBubbles && page is ContentPage) {
div(classes = "filter-section") {
id = "filter-section"
page.content.withDescendants().flatMap { it.sourceSets }.distinct()
.sortedBy { it.comparableKey }.forEach {
button(classes = "platform-tag platform-selector") {
attributes["data-active"] = ""
attributes["data-filter"] = it.sourceSetIDs.merged.toString()
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.name)
}
}
}
}
}
private fun FlowContent.copyButton() = span(classes = "top-right-position") {
span("copy-icon")
copiedPopup("Content copied to clipboard", "popup-to-left")
}
private fun FlowContent.copiedPopup(notificationContent: String, additionalClasses: String = "") =
div("copy-popup-wrapper $additionalClasses") {
span("copy-popup-icon")
span {
text(notificationContent)
}
}
override fun FlowContent.buildPlatformDependent(
content: PlatformHintedContent,
pageContext: ContentPage,
sourceSetRestriction: Set?
) =
buildPlatformDependent(
content.sourceSets.filter {
sourceSetRestriction == null || it in sourceSetRestriction
}.associateWith { setOf(content.inner) },
pageContext,
content.extra,
content.style
)
private fun FlowContent.buildPlatformDependent(
nodes: Map>,
pageContext: ContentPage,
extra: PropertyContainer = PropertyContainer.empty(),
styles: Set