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.base.transformers.documentables.CallableExtensions
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.model.properties.PropertyContainer
import org.jetbrains.dokka.model.properties.WithExtraProperties
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 = "###"
internal const val TOGGLEABLE_CONTENT_TYPE_ATTR = "data-togglable"
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 shouldRenderSourceSetTabs: Boolean = false
override val preprocessors = context.plugin().query { htmlPreprocessors }
/**
* Tabs themselves are created in HTML plugin since, currently, only HTML format supports them.
* [TabbedContentType] is used to mark content that should be inside tab content.
* A tab can display multiple [TabbedContentType].
* The content style [ContentStyle.TabbedContent] is used to determine where tabs will be generated.
*
* @see TabbedContentType
* @see ContentStyle.TabbedContent
*/
private fun createTabs(pageContext: ContentPage): List {
return when(pageContext) {
is ClasslikePage -> createTabsForClasslikes(pageContext)
is PackagePage -> createTabsForPackage(pageContext)
else -> throw IllegalArgumentException("Page ${pageContext.name} cannot have tabs")
}
}
private fun createTabsForClasslikes(page: ClasslikePage): List {
val documentables = page.documentables
fun List.shouldDocumentConstructors() = !this.any { it is DAnnotation }
val csEnum = documentables.filterIsInstance()
val csWithConstructor = documentables.filterIsInstance()
val scopes = documentables.filterIsInstance()
val constructorsToDocumented = csWithConstructor.flatMap { it.constructors }
val containsRenderableConstructors = constructorsToDocumented.isNotEmpty() && documentables.shouldDocumentConstructors()
val containsRenderableMembers =
containsRenderableConstructors || scopes.any { it.classlikes.isNotEmpty() || it.functions.isNotEmpty() || it.properties.isNotEmpty() }
@Suppress("UNCHECKED_CAST")
val extensions = (documentables as List>).flatMap {
it.extra[CallableExtensions]?.extensions
?.filterIsInstance().orEmpty()
}
.distinctBy { it.sourceSets to it.dri } // [Documentable] has expensive equals/hashCode at the moment, see #2620
return listOfNotNull(
if(!containsRenderableMembers) null else
ContentTab(
"Members",
listOf(
BasicTabbedContentType.CONSTRUCTOR,
BasicTabbedContentType.TYPE,
BasicTabbedContentType.PROPERTY,
BasicTabbedContentType.FUNCTION
)
),
if (extensions.isEmpty()) null else ContentTab(
"Members & Extensions",
listOf(
BasicTabbedContentType.CONSTRUCTOR,
BasicTabbedContentType.TYPE,
BasicTabbedContentType.PROPERTY,
BasicTabbedContentType.FUNCTION,
BasicTabbedContentType.EXTENSION_PROPERTY,
BasicTabbedContentType.EXTENSION_FUNCTION
)
),
if(csEnum.isEmpty()) null else ContentTab(
"Entries",
listOf(
BasicTabbedContentType.ENTRY
)
)
)
}
private fun createTabsForPackage(page: PackagePage): List {
val p = page.documentables.single() as DPackage
return listOfNotNull(
if (p.typealiases.isEmpty() && p.classlikes.isEmpty()) null else ContentTab(
"Types",
listOf(
BasicTabbedContentType.TYPE,
)
),
if (p.functions.isEmpty()) null else ContentTab(
"Functions",
listOf(
BasicTabbedContentType.FUNCTION,
BasicTabbedContentType.EXTENSION_FUNCTION,
)
),
if (p.properties.isEmpty()) null else ContentTab(
"Properties",
listOf(
BasicTabbedContentType.PROPERTY,
BasicTabbedContentType.EXTENSION_PROPERTY,
)
)
)
}
private fun TagConsumer.prepareForTemplates() =
if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this
else ImmediateResolutionTagConsumer(this, context)
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 contentTabs = createTabs(pageContext)
div(classes = "tabs-section") {
attributes["tabs-section"] = "tabs-section"
contentTabs.forEachIndexed { index, contentTab ->
button(classes = "section-tab") {
if (index == 0) attributes["data-active"] = ""
attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] =
contentTab.tabbedContentTypes.joinToString(",") { it.toHtmlAttribute() }
text(contentTab.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()
}
node.extra.extraTabbedContentType() != null -> div() {
node.extra.extraTabbedContentType()?.let { attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] = it.value.toHtmlAttribute() }
this@wrapGroup.childrenCallback()
}
else -> childrenCallback()
}
}
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