aboutsummaryrefslogtreecommitdiff
path: root/plugins/gfm/src/main/kotlin/org
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/gfm/src/main/kotlin/org')
-rw-r--r--plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt36
-rw-r--r--plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/location/MarkdownLocationProvider.kt16
-rw-r--r--plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/CommonmarkRenderer.kt322
3 files changed, 374 insertions, 0 deletions
diff --git a/plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt b/plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt
new file mode 100644
index 00000000..3f2eae4d
--- /dev/null
+++ b/plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt
@@ -0,0 +1,36 @@
+package org.jetbrains.dokka.gfm
+
+import org.jetbrains.dokka.CoreExtensions
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.renderers.PackageListCreator
+import org.jetbrains.dokka.base.renderers.RootCreator
+import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat
+import org.jetbrains.dokka.gfm.location.MarkdownLocationProviderFactory
+import org.jetbrains.dokka.gfm.renderer.CommonmarkRenderer
+import org.jetbrains.dokka.plugability.DokkaPlugin
+import org.jetbrains.dokka.transformers.pages.PageTransformer
+
+class GfmPlugin : DokkaPlugin() {
+
+ val gfmPreprocessors by extensionPoint<PageTransformer>()
+
+ private val dokkaBase by lazy { plugin<DokkaBase>() }
+
+ val renderer by extending {
+ CoreExtensions.renderer providing ::CommonmarkRenderer override dokkaBase.htmlRenderer
+ }
+
+ val locationProvider by extending {
+ dokkaBase.locationProviderFactory providing ::MarkdownLocationProviderFactory override dokkaBase.locationProvider
+ }
+
+ val rootCreator by extending {
+ gfmPreprocessors with RootCreator
+ }
+
+ val packageListCreator by extending {
+ (gfmPreprocessors
+ providing { PackageListCreator(it, RecognizedLinkFormat.DokkaGFM) }
+ order { after(rootCreator) })
+ }
+}
diff --git a/plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/location/MarkdownLocationProvider.kt b/plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/location/MarkdownLocationProvider.kt
new file mode 100644
index 00000000..6f96dbd5
--- /dev/null
+++ b/plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/location/MarkdownLocationProvider.kt
@@ -0,0 +1,16 @@
+package org.jetbrains.dokka.gfm.location
+
+import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider
+import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory
+import org.jetbrains.dokka.pages.RootPageNode
+import org.jetbrains.dokka.plugability.DokkaContext
+
+class MarkdownLocationProviderFactory(val context: DokkaContext) : LocationProviderFactory {
+ override fun getLocationProvider(pageNode: RootPageNode) = MarkdownLocationProvider(pageNode, context)
+}
+
+class MarkdownLocationProvider(
+ pageGraphRoot: RootPageNode,
+ dokkaContext: DokkaContext
+) : DokkaLocationProvider(pageGraphRoot, dokkaContext, ".md")
+
diff --git a/plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/CommonmarkRenderer.kt b/plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/CommonmarkRenderer.kt
new file mode 100644
index 00000000..0655ce20
--- /dev/null
+++ b/plugins/gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/CommonmarkRenderer.kt
@@ -0,0 +1,322 @@
+package org.jetbrains.dokka.gfm.renderer
+
+import org.jetbrains.dokka.DokkaException
+import org.jetbrains.dokka.base.renderers.DefaultRenderer
+import org.jetbrains.dokka.base.renderers.isImage
+import org.jetbrains.dokka.gfm.GfmPlugin
+import org.jetbrains.dokka.model.DisplaySourceSet
+import org.jetbrains.dokka.pages.*
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.plugin
+import org.jetbrains.dokka.plugability.query
+
+open class CommonmarkRenderer(
+ context: DokkaContext
+) : DefaultRenderer<StringBuilder>(context) {
+
+ override val preprocessors = context.plugin<GfmPlugin>().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<DisplaySourceSet>?
+ ) {
+ buildListLevel(node, pageContext)
+ }
+
+ private fun StringBuilder.buildListItem(items: List<ContentNode>, pageContext: ContentPage) {
+ items.forEach {
+ if (it is ContentList) {
+ buildList(it, pageContext)
+ } else {
+ append("<li>")
+ append(buildString { it.build(this, pageContext, it.sourceSets) }.trim())
+ append("</li>")
+ }
+ }
+ }
+
+ private fun StringBuilder.buildListLevel(node: ContentList, pageContext: ContentPage) {
+ if (node.ordered) {
+ append("<ol>")
+ buildListItem(node.children, pageContext)
+ append("</ol>")
+ } else {
+ append("<ul>")
+ buildListItem(node.children, pageContext)
+ append("</ul>")
+ }
+ }
+
+ override fun StringBuilder.buildNewLine() {
+ append(" \n")
+ }
+
+ private fun StringBuilder.buildParagraph() {
+ append("\n\n")
+ }
+
+ override fun StringBuilder.buildPlatformDependent(
+ content: PlatformHintedContent,
+ pageContext: ContentPage,
+ sourceSetRestriction: Set<DisplaySourceSet>?
+ ) {
+ buildPlatformDependentItem(content.inner, content.sourceSets, pageContext)
+ }
+
+ private fun StringBuilder.buildPlatformDependentItem(
+ content: ContentNode,
+ sourceSets: Set<DisplaySourceSet>,
+ 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<DisplaySourceSet, String>::second, Pair<DisplaySourceSet, String>::first)
+
+ distinct.filter { it.key.isNotBlank() }.forEach { (text, platforms) ->
+ append(" ")
+ buildSourceSetTags(platforms.toSet())
+ append(" $text ")
+ buildNewLine()
+ }
+ }
+ }
+
+ override fun StringBuilder.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) {
+ if(node.isImage()){
+ append("!")
+ }
+ append("[${node.altText}](${node.address})")
+ }
+
+ override fun StringBuilder.buildTable(
+ node: ContentTable,
+ pageContext: ContentPage,
+ sourceSetRestriction: Set<DisplaySourceSet>?
+ ) {
+ buildNewLine()
+ if (node.dci.kind == ContentKind.Sample || node.dci.kind == ContentKind.Parameters) {
+ node.sourceSets.forEach { sourcesetData ->
+ append(sourcesetData.name)
+ 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("<a name=\"${it.dci.dri.first()}\"></a>")
+ 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, sourceSet) }
+ } ?: ""
+ }, { instance, contentPage, sourceSet ->
+ instance.after?.let { after ->
+ buildString { buildContentNode(after, pageContext, sourceSet) }
+ } ?: ""
+ })
+
+ distinct.values.forEach { entry ->
+ val (instance, sourceSets) = entry.getInstanceAndSourceSets()
+
+ buildSourceSetTags(sourceSets)
+ buildNewLine()
+ instance.before?.let {
+ append("Brief description")
+ buildNewLine()
+ buildContentNode(
+ it,
+ pageContext,
+ 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,
+ sourceSets.first()
+ ) // It's workaround to render content only once
+ buildNewLine()
+ }
+
+ buildParagraph()
+ }
+ }
+
+ private fun decorators(styles: Set<Style>) = buildString {
+ styles.forEach {
+ when (it) {
+ TextStyle.Bold -> append("**")
+ TextStyle.Italic -> append("*")
+ TextStyle.Strong -> append("**")
+ TextStyle.Strikethrough -> append("~~")
+ else -> Unit
+ }
+ }
+ }
+
+ private val PageNode.isNavigable: Boolean
+ get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing
+
+ private fun StringBuilder.buildLink(to: PageNode, from: PageNode) =
+ buildLink(locationProvider.resolve(to, from)!!) {
+ append(to.name)
+ }
+
+ override suspend fun renderPage(page: PageNode) {
+ val path by lazy {
+ locationProvider.resolve(page, skipExtension = true)
+ ?: throw DokkaException("Cannot resolve path for ${page.name}")
+ }
+
+ when (page) {
+ is ContentPage -> outputWriter.write(path, buildPage(page) { c, p -> buildPageContent(c, p) }, ".md")
+ is RendererSpecificPage -> when (val strategy = page.strategy) {
+ is RenderingStrategy.Copy -> outputWriter.writeResources(strategy.from, path)
+ is RenderingStrategy.Write -> outputWriter.write(path, strategy.text, "")
+ is RenderingStrategy.Callback -> outputWriter.write(path, strategy.instructions(this, page), ".md")
+ RenderingStrategy.DoNothing -> Unit
+ }
+ else -> throw AssertionError(
+ "Page ${page.name} cannot be rendered by renderer as it is not renderer specific nor contains content"
+ )
+ }
+ }
+
+ private fun String.withEntersAsHtml(): String = replace("\n", "<br>")
+
+ private fun List<Pair<ContentDivergentInstance, DisplaySourceSet>>.getInstanceAndSourceSets() =
+ this.let { Pair(it.first().first, it.map { it.second }.toSet()) }
+
+ private fun StringBuilder.buildSourceSetTags(sourceSets: Set<DisplaySourceSet>) =
+ append(sourceSets.joinToString(prefix = "[", postfix = "]") { it.name })
+} \ No newline at end of file