package org.jetbrains.dokka.gfm import org.jetbrains.dokka.CoreExtensions import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.renderers.DefaultRenderer import org.jetbrains.dokka.base.renderers.PackageListCreator import org.jetbrains.dokka.base.renderers.RootCreator import org.jetbrains.dokka.base.resolvers.local.DefaultLocationProvider import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.DokkaPlugin import org.jetbrains.dokka.plugability.plugin import org.jetbrains.dokka.plugability.query import org.jetbrains.dokka.transformers.pages.PageTransformer class GfmPlugin : DokkaPlugin() { val gfmPreprocessors by extensionPoint() private val dokkaBase by lazy { plugin() } val renderer by extending { (CoreExtensions.renderer providing { CommonmarkRenderer(it) } override dokkaBase.htmlRenderer) } val locationProvider by extending { (dokkaBase.locationProviderFactory providing { MarkdownLocationProviderFactory(it) } override dokkaBase.locationProvider) } val rootCreator by extending { gfmPreprocessors with RootCreator } val packageListCreator by extending { (gfmPreprocessors providing { PackageListCreator(it, "gfm", "md") } order { after(rootCreator) }) } } open class CommonmarkRenderer( context: DokkaContext ) : DefaultRenderer(context) { override val preprocessors = context.plugin().query { gfmPreprocessors } override fun StringBuilder.wrapGroup( node: ContentGroup, pageContext: ContentPage, childrenCallback: StringBuilder.() -> Unit ) { return when { node.hasStyle(TextStyle.Block) -> { childrenCallback() buildNewLine() } node.hasStyle(TextStyle.Paragraph) -> { buildParagraph() childrenCallback() buildParagraph() } else -> childrenCallback() } } override fun StringBuilder.buildHeader(level: Int, node: ContentHeader, content: StringBuilder.() -> Unit) { buildParagraph() append("#".repeat(level) + " ") content() buildNewLine() } override fun StringBuilder.buildLink(address: String, content: StringBuilder.() -> Unit) { append("[") content() append("]($address)") } override fun StringBuilder.buildList( node: ContentList, pageContext: ContentPage, sourceSetRestriction: Set? ) { buildListLevel(node, pageContext) } private fun StringBuilder.buildListItem(items: List, pageContext: ContentPage) { items.forEach { if (it is ContentList) { buildList(it, pageContext) } else { append("
  • ") append(buildString { it.build(this, pageContext, it.sourceSets) }.trim()) append("
  • ") } } } private fun StringBuilder.buildListLevel(node: ContentList, pageContext: ContentPage) { if (node.ordered) { append("
      ") buildListItem(node.children, pageContext) append("
    ") } else { append("
      ") buildListItem(node.children, pageContext) append("
    ") } } override fun StringBuilder.buildNewLine() { append(" \n") } private fun StringBuilder.buildParagraph() { append("\n\n") } override fun StringBuilder.buildPlatformDependent( content: PlatformHintedContent, pageContext: ContentPage, sourceSetRestriction: Set? ) { buildPlatformDependentItem(content.inner, content.sourceSets, pageContext) } private fun StringBuilder.buildPlatformDependentItem( content: ContentNode, sourceSets: Set, pageContext: ContentPage, ) { if (content is ContentGroup && content.children.firstOrNull { it is ContentTable } != null) { buildContentNode(content, pageContext, sourceSets) } else { val distinct = sourceSets.map { it to buildString { buildContentNode(content, pageContext, setOf(it)) } }.groupBy(Pair::second, Pair::first) distinct.filter { it.key.isNotBlank() }.forEach { (text, platforms) -> append(" ") buildSourceSetTags(platforms.toSet()) append(" $text ") buildNewLine() } } } override fun StringBuilder.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) { append("Resource") } override fun StringBuilder.buildTable( node: ContentTable, pageContext: ContentPage, sourceSetRestriction: Set? ) { buildNewLine() if (node.dci.kind == ContentKind.Sample || node.dci.kind == ContentKind.Parameters) { node.sourceSets.forEach { sourcesetData -> append(sourcesetData.displayName) buildNewLine() buildTable( node.copy( children = node.children.filter { it.sourceSets.contains(sourcesetData) }, dci = node.dci.copy(kind = ContentKind.Main) ), pageContext, sourceSetRestriction ) buildNewLine() } } else { val size = node.header.size if (node.header.isNotEmpty()) { append("| ") node.header.forEach { it.children.forEach { append(" ") it.build(this, pageContext, it.sourceSets) } append("| ") } append("\n") } else { append("| ".repeat(size)) if (size > 0) append("|\n") } append("|---".repeat(size)) if (size > 0) append("|\n") node.children.forEach { val builder = StringBuilder() it.children.forEach { builder.append("| ") builder.append( buildString { it.build(this, pageContext) }.replace( Regex("#+ "), "" ) ) // Workaround for headers inside tables } append(builder.toString().withEntersAsHtml()) append(" | ".repeat(size - it.children.size)) append("\n") } } } override fun StringBuilder.buildText(textNode: ContentText) { if (textNode.text.isNotBlank()) { val decorators = decorators(textNode.style) append(textNode.text.takeWhile { it == ' ' }) append(decorators) append(textNode.text.trim()) append(decorators.reversed()) append(textNode.text.takeLastWhile { it == ' ' }) } } override fun StringBuilder.buildNavigation(page: PageNode) { locationProvider.ancestors(page).asReversed().forEach { node -> append("/") if (node.isNavigable) buildLink(node, page) else append(node.name) } buildParagraph() } override fun buildPage(page: ContentPage, content: (StringBuilder, ContentPage) -> Unit): String = buildString { content(this, page) } override fun buildError(node: ContentNode) { context.logger.warn("Markdown renderer has encountered problem. The unmatched node is $node") } override fun StringBuilder.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) { val distinct = node.groupDivergentInstances(pageContext, { instance, contentPage, sourceSet -> instance.before?.let { before -> buildString { buildContentNode(before, pageContext, setOf(sourceSet)) } } ?: "" }, { instance, contentPage, sourceSet -> instance.after?.let { after -> buildString { buildContentNode(after, pageContext, setOf(sourceSet)) } } ?: "" }) distinct.values.forEach { entry -> val (instance, sourceSets) = entry.getInstanceAndSourceSets() buildSourceSetTags(sourceSets) buildNewLine() instance.before?.let { append("Brief description") buildNewLine() buildContentNode( it, pageContext, setOf(sourceSets.first()) ) // It's workaround to render content only once buildNewLine() } append("Content") buildNewLine() entry.groupBy { buildString { buildContentNode(it.first.divergent, pageContext, setOf(it.second)) } } .values.forEach { innerEntry -> val (innerInstance, innerSourceSets) = innerEntry.getInstanceAndSourceSets() if (sourceSets.size > 1) { buildSourceSetTags(innerSourceSets) buildNewLine() } innerInstance.divergent.build( this@buildDivergent, pageContext, setOf(innerSourceSets.first()) ) // It's workaround to render content only once buildNewLine() } instance.after?.let { append("More info") buildNewLine() buildContentNode( it, pageContext, setOf(sourceSets.first()) ) // It's workaround to render content only once buildNewLine() } buildParagraph() } } private fun decorators(styles: Set