aboutsummaryrefslogtreecommitdiff
path: root/dokka-subprojects/plugin-gfm/src
diff options
context:
space:
mode:
authorIgnat Beresnev <ignat.beresnev@jetbrains.com>2023-11-10 11:46:54 +0100
committerGitHub <noreply@github.com>2023-11-10 11:46:54 +0100
commit8e5c63d035ef44a269b8c43430f43f5c8eebfb63 (patch)
tree1b915207b2b9f61951ddbf0ff2e687efd053d555 /dokka-subprojects/plugin-gfm/src
parenta44efd4ba0c2e4ab921ff75e0f53fc9335aa79db (diff)
downloaddokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.gz
dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.bz2
dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.zip
Restructure the project to utilize included builds (#3174)
* Refactor and simplify artifact publishing * Update Gradle to 8.4 * Refactor and simplify convention plugins and build scripts Fixes #3132 --------- Co-authored-by: Adam <897017+aSemy@users.noreply.github.com> Co-authored-by: Oleg Yukhnevich <whyoleg@gmail.com>
Diffstat (limited to 'dokka-subprojects/plugin-gfm/src')
-rw-r--r--dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt63
-rw-r--r--dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/gfmTemplating.kt39
-rw-r--r--dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/location/MarkdownLocationProvider.kt23
-rw-r--r--dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/BriefCommentPreprocessor.kt22
-rw-r--r--dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/CommonmarkRenderer.kt414
-rw-r--r--dokka-subprojects/plugin-gfm/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin5
-rw-r--r--dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/CodeWrappingTest.kt86
-rw-r--r--dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/DivergentTest.kt505
-rw-r--r--dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/GfmRenderingOnlyTestBase.kt34
-rw-r--r--dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/GroupWrappingTest.kt95
-rw-r--r--dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/SimpleElementsTest.kt393
-rw-r--r--dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/SourceSetDependentHintTest.kt184
12 files changed, 1863 insertions, 0 deletions
diff --git a/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt
new file mode 100644
index 00000000..3fd7b514
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+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.local.LocationProviderFactory
+import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat
+import org.jetbrains.dokka.gfm.location.MarkdownLocationProvider
+import org.jetbrains.dokka.gfm.renderer.BriefCommentPreprocessor
+import org.jetbrains.dokka.gfm.renderer.CommonmarkRenderer
+import org.jetbrains.dokka.plugability.*
+import org.jetbrains.dokka.renderers.PostAction
+import org.jetbrains.dokka.renderers.Renderer
+import org.jetbrains.dokka.transformers.pages.PageTransformer
+
+public class GfmPlugin : DokkaPlugin() {
+
+ public val gfmPreprocessors: ExtensionPoint<PageTransformer> by extensionPoint<PageTransformer>()
+
+ private val dokkaBase by lazy { plugin<DokkaBase>() }
+
+ public val renderer: Extension<Renderer, *, *> by extending {
+ CoreExtensions.renderer providing ::CommonmarkRenderer override dokkaBase.htmlRenderer
+ }
+
+ public val locationProvider: Extension<LocationProviderFactory, *, *> by extending {
+ dokkaBase.locationProviderFactory providing MarkdownLocationProvider::Factory override dokkaBase.locationProvider
+ }
+
+ public val rootCreator: Extension<PageTransformer, *, *> by extending {
+ gfmPreprocessors with RootCreator
+ }
+
+ public val briefCommentPreprocessor: Extension<PageTransformer, *, *> by extending {
+ gfmPreprocessors with BriefCommentPreprocessor()
+ }
+
+ public val packageListCreator: Extension<PageTransformer, *, *> by extending {
+ (gfmPreprocessors
+ providing { PackageListCreator(it, RecognizedLinkFormat.DokkaGFM) }
+ order { after(rootCreator) })
+ }
+
+ internal val alphaVersionNotifier by extending {
+ CoreExtensions.postActions providing { ctx ->
+ PostAction {
+ ctx.logger.info(
+ "The GFM output format is still in Alpha so you may find bugs and experience migration " +
+ "issues when using it. You use it at your own risk."
+ )
+ }
+ }
+ }
+
+ @OptIn(DokkaPluginApiPreview::class)
+ override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement =
+ PluginApiPreviewAcknowledgement
+}
diff --git a/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/gfmTemplating.kt b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/gfmTemplating.kt
new file mode 100644
index 00000000..194127df
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/gfmTemplating.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.gfm
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.CLASS
+import org.jetbrains.dokka.base.templating.toJsonString
+import org.jetbrains.dokka.links.DRI
+
+@JsonTypeInfo(use = CLASS)
+public sealed class GfmCommand {
+
+ public companion object {
+ private const val delimiter = "\u1680"
+
+ public val templateCommandRegex: Regex =
+ Regex("<!---$delimiter GfmCommand ([^$delimiter ]*)$delimiter--->(.+?)(?=<!---$delimiter)<!---$delimiter--->")
+
+ public val MatchResult.command: String
+ get() = groupValues[1]
+
+ public val MatchResult.label: String
+ get() = groupValues[2]
+
+ public fun Appendable.templateCommand(command: GfmCommand, content: Appendable.() -> Unit) {
+ append("<!---$delimiter GfmCommand ${toJsonString(command)}$delimiter--->")
+ content()
+ append("<!---$delimiter--->")
+ }
+ }
+}
+
+public class ResolveLinkGfmCommand(
+ public val dri: DRI
+) : GfmCommand()
+
+
diff --git a/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/location/MarkdownLocationProvider.kt b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/location/MarkdownLocationProvider.kt
new file mode 100644
index 00000000..f331a6d9
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/location/MarkdownLocationProvider.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.gfm.location
+
+import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider
+import org.jetbrains.dokka.base.resolvers.local.LocationProvider
+import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory
+import org.jetbrains.dokka.pages.RootPageNode
+import org.jetbrains.dokka.plugability.DokkaContext
+
+public class MarkdownLocationProvider(
+ pageGraphRoot: RootPageNode,
+ dokkaContext: DokkaContext
+) : DokkaLocationProvider(pageGraphRoot, dokkaContext, ".md") {
+
+ public class Factory(private val context: DokkaContext) : LocationProviderFactory {
+ override fun getLocationProvider(pageNode: RootPageNode): LocationProvider =
+ MarkdownLocationProvider(pageNode, context)
+ }
+}
+
diff --git a/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/BriefCommentPreprocessor.kt b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/BriefCommentPreprocessor.kt
new file mode 100644
index 00000000..6023cca1
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/BriefCommentPreprocessor.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.gfm.renderer
+
+import org.jetbrains.dokka.pages.*
+import org.jetbrains.dokka.transformers.pages.PageTransformer
+
+public class BriefCommentPreprocessor : PageTransformer {
+ override fun invoke(input: RootPageNode): RootPageNode {
+ return input.transformContentPagesTree { contentPage ->
+ contentPage.modified(content = contentPage.content.recursiveMapTransform<ContentGroup, ContentNode> {
+ if (it.dci.kind == ContentKind.BriefComment) {
+ it.copy(style = it.style + setOf(TextStyle.Block))
+ } else {
+ it
+ }
+ })
+ }
+ }
+}
diff --git a/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/CommonmarkRenderer.kt b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/CommonmarkRenderer.kt
new file mode 100644
index 00000000..3bc420ac
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/CommonmarkRenderer.kt
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+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.GfmCommand.Companion.templateCommand
+import org.jetbrains.dokka.gfm.GfmPlugin
+import org.jetbrains.dokka.gfm.ResolveLinkGfmCommand
+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
+import org.jetbrains.dokka.transformers.pages.PageTransformer
+import org.jetbrains.dokka.utilities.htmlEscape
+
+public open class CommonmarkRenderer(
+ context: DokkaContext
+) : DefaultRenderer<StringBuilder>(context) {
+
+ override val preprocessors: List<PageTransformer> = context.plugin<GfmPlugin>().query { gfmPreprocessors }
+
+ private val isPartial = context.configuration.delayTemplateSubstitution
+
+ override fun StringBuilder.wrapGroup(
+ node: ContentGroup,
+ pageContext: ContentPage,
+ childrenCallback: StringBuilder.() -> Unit
+ ) {
+ return when {
+ node.hasStyle(TextStyle.Block) || node.hasStyle(TextStyle.Paragraph) -> {
+ buildParagraph()
+ childrenCallback()
+ buildParagraph()
+ }
+ node.dci.kind == ContentKind.Deprecation -> {
+ append("---")
+ childrenCallback()
+ append("---")
+ buildNewLine()
+ }
+ node.hasStyle(ContentStyle.Footnote) -> {
+ childrenCallback()
+ buildParagraph()
+ }
+ else -> childrenCallback()
+ }
+ }
+
+ override fun StringBuilder.buildHeader(level: Int, node: ContentHeader, content: StringBuilder.() -> Unit) {
+ buildParagraph()
+ append("#".repeat(level) + " ")
+ content()
+ buildParagraph()
+ }
+
+ override fun StringBuilder.buildLink(address: String, content: StringBuilder.() -> Unit) {
+ append("[")
+ content()
+ append("]($address)")
+ }
+
+ override fun StringBuilder.buildList(
+ node: ContentList,
+ pageContext: ContentPage,
+ sourceSetRestriction: Set<DisplaySourceSet>?
+ ) {
+ buildParagraph()
+ buildList(node, pageContext)
+ buildParagraph()
+ }
+
+ private fun StringBuilder.buildList(
+ node: ContentList,
+ pageContext: ContentPage
+ ) {
+ node.children.forEachIndexed { i, it ->
+ if (node.ordered) {
+ // number is irrelevant, but a nice touch
+ // period is more widely compatible
+ append("${i + 1}. ")
+ } else {
+ append("- ")
+ }
+
+ /*
+ Handle case when list item transitions to another complex node with no preceding text.
+ For example, the equivalent of:
+ <li>
+ <ul><li><ul>Item</ul></li></ul>
+ </li>
+
+ Would be:
+ -
+ - Item
+ */
+ if (it is ContentGroup && it.children.firstOrNull()?.let { it !is ContentText } == true) {
+ append("\n ")
+ }
+
+ buildString { it.build(this, pageContext, it.sourceSets) }
+ .replace("\n", "\n ") // apply indent
+ .trim().let { append(it) }
+ buildNewLine()
+ }
+ }
+
+ override fun StringBuilder.buildDRILink(
+ node: ContentDRILink,
+ pageContext: ContentPage,
+ sourceSetRestriction: Set<DisplaySourceSet>?
+ ) {
+ val location = locationProvider.resolve(node.address, node.sourceSets, pageContext)
+ if (location == null) {
+ if (isPartial) {
+ templateCommand(ResolveLinkGfmCommand(node.address)) {
+ buildText(node.children, pageContext, sourceSetRestriction)
+ }
+ } else {
+ buildText(node.children, pageContext, sourceSetRestriction)
+ }
+ } else {
+ buildLink(location) {
+ buildText(node.children, pageContext, sourceSetRestriction)
+ }
+ }
+ }
+
+ override fun StringBuilder.buildLineBreak() {
+ append("\\")
+ buildNewLine()
+ }
+
+ private fun StringBuilder.buildNewLine() {
+ append("\n")
+ }
+
+ private fun StringBuilder.buildParagraph() {
+ buildNewLine()
+ buildNewLine()
+ }
+
+ 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) ->
+ buildParagraph()
+ buildSourceSetTags(platforms.toSet())
+ buildLineBreak()
+ append(text.trim())
+ buildParagraph()
+ }
+ }
+ }
+
+ 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.firstOrNull()?.children?.size ?: node.children.firstOrNull()?.children?.size ?: 0
+ if (size <= 0) return
+
+ if (node.header.isNotEmpty()) {
+ node.header.forEach {
+ it.children.forEach {
+ append("| ")
+ it.build(this, pageContext, it.sourceSets)
+ append(" ")
+ }
+ }
+ } else {
+ append("| ".repeat(size))
+ }
+ append("|")
+ buildNewLine()
+
+ append("|---".repeat(size))
+ append("|")
+ buildNewLine()
+
+ node.children.forEach { row ->
+ row.children.forEach { cell ->
+ append("| ")
+ append(buildString { cell.build(this, pageContext) }
+ .trim()
+ .replace("#+ ".toRegex(), "") // Workaround for headers inside tables
+ .replace("\\\n", "\n\n")
+ .replace("\n[\n]+".toRegex(), "<br>")
+ .replace("\n", " ")
+ )
+ append(" ")
+ }
+ append("|")
+ buildNewLine()
+ }
+ }
+ }
+
+ override fun StringBuilder.buildText(textNode: ContentText) {
+ if (textNode.extra[HtmlContent] != null) {
+ append(textNode.text)
+ } else if (textNode.text.isNotBlank()) {
+ val decorators = decorators(textNode.style)
+ append(textNode.text.takeWhile { it == ' ' })
+ append(decorators)
+ append(textNode.text.trim().htmlEscape())
+ 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)
+ }.trim().replace("\n[\n]+".toRegex(), "\n\n")
+
+ 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, _, sourceSet ->
+ instance.before?.let { before ->
+ buildString { buildContentNode(before, pageContext, sourceSet) }
+ } ?: ""
+ }, { instance, _, sourceSet ->
+ instance.after?.let { after ->
+ buildString { buildContentNode(after, pageContext, sourceSet) }
+ } ?: ""
+ })
+
+ distinct.values.forEach { entry ->
+ val (instance, sourceSets) = entry.getInstanceAndSourceSets()
+
+ buildParagraph()
+ buildSourceSetTags(sourceSets)
+ buildLineBreak()
+
+ instance.before?.let {
+ buildContentNode(
+ it,
+ pageContext,
+ sourceSets.first()
+ ) // It's workaround to render content only once
+ buildParagraph()
+ }
+
+ 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)
+ buildLineBreak()
+ }
+ innerInstance.divergent.build(
+ this@buildDivergent,
+ pageContext,
+ setOf(innerSourceSets.first())
+ ) // It's workaround to render content only once
+ buildParagraph()
+ }
+
+ instance.after?.let {
+ buildContentNode(
+ it,
+ pageContext,
+ sourceSets.first()
+ ) // It's workaround to render content only once
+ }
+
+ buildParagraph()
+ }
+ }
+
+ override fun StringBuilder.buildCodeBlock(code: ContentCodeBlock, pageContext: ContentPage) {
+ append("```")
+ append(code.language.ifEmpty { "kotlin" })
+ buildNewLine()
+ code.children.forEach {
+ if (it is ContentText) {
+ // since this is a code block where text will be rendered as is,
+ // no need to escape text, apply styles, etc. Just need the plain value
+ append(it.text)
+ } else if (it is ContentBreakLine) {
+ // since this is a code block where text will be rendered as is,
+ // there's no need to add tailing slash for line breaks
+ buildNewLine()
+ }
+ }
+ buildNewLine()
+ append("```")
+ buildNewLine()
+ }
+
+ override fun StringBuilder.buildCodeInline(code: ContentCodeInline, pageContext: ContentPage) {
+ append("`")
+ code.children.filterIsInstance<ContentText>().forEach { append(it.text) }
+ append("`")
+ }
+
+ 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}")
+ }
+
+ return 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")
+ is RenderingStrategy.DriLocationResolvableWrite -> outputWriter.write(
+ path,
+ strategy.contentToResolve { dri, sourcesets ->
+ locationProvider.resolve(dri, sourcesets)
+ },
+ ""
+ )
+ is RenderingStrategy.PageLocationResolvableWrite -> outputWriter.write(
+ path,
+ strategy.contentToResolve { pageToLocate, context ->
+ locationProvider.resolve(pageToLocate, context)
+ },
+ ""
+ )
+ 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 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 })
+}
diff --git a/dokka-subprojects/plugin-gfm/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/dokka-subprojects/plugin-gfm/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin
new file mode 100644
index 00000000..83575bb1
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin
@@ -0,0 +1,5 @@
+#
+# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+#
+
+org.jetbrains.dokka.gfm.GfmPlugin
diff --git a/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/CodeWrappingTest.kt b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/CodeWrappingTest.kt
new file mode 100644
index 00000000..0d6a18cc
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/CodeWrappingTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package renderers.gfm
+
+import org.jetbrains.dokka.gfm.renderer.CommonmarkRenderer
+import renderers.testPage
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class CodeWrappingTest : GfmRenderingOnlyTestBase() {
+ @Test
+ fun wrappedCodeBlock() {
+ val page = testPage {
+ codeBlock {
+ text("fun myCode(): String")
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |```kotlin
+ |fun myCode(): String
+ |```""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `should preserve original text without escaping`() {
+ val page = testPage {
+ codeBlock {
+ text("<----> **text** & ~~this~~ and \"that\"")
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |```kotlin
+ |<----> **text** & ~~this~~ and "that"
+ |```""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+
+ @Test
+ fun wrappedInlineCode() {
+ val page = testPage {
+ text("This function adds the values of ")
+ codeInline {
+ text("left")
+ }
+ text(" and ")
+ codeInline {
+ text("right")
+ }
+ text(".\nBoth numbers must be finite, or an exception occurs.\n")
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |This function adds the values of `left` and `right`.
+ |Both numbers must be finite, or an exception occurs.""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `should not add trailing backslash to newline elements for code inline code`() {
+ val page = testPage {
+ text("This adds some symbols (")
+ codeInline {
+ text("<----> **text** & ~~this~~ and \"that\"")
+ }
+ text(") to the test")
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |This adds some symbols (`<----> **text** & ~~this~~ and "that"`) to the test""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+}
diff --git a/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/DivergentTest.kt b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/DivergentTest.kt
new file mode 100644
index 00000000..8d90fac2
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/DivergentTest.kt
@@ -0,0 +1,505 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package renderers.gfm
+
+import org.jetbrains.dokka.DokkaSourceSetID
+import org.jetbrains.dokka.Platform
+import org.jetbrains.dokka.gfm.renderer.CommonmarkRenderer
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.pages.ContentDivergentGroup
+import renderers.testPage
+import testApi.testRunner.defaultSourceSet
+import java.io.File
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class DivergentTest : GfmRenderingOnlyTestBase() {
+ private val js = defaultSourceSet.copy(
+ "js",
+ DokkaSourceSetID("root", "js"),
+ analysisPlatform = Platform.js,
+ sourceRoots = setOf(File("pl1"))
+ )
+ private val jvm = defaultSourceSet.copy(
+ "jvm",
+ DokkaSourceSetID("root", "jvm"),
+ analysisPlatform = Platform.jvm,
+ sourceRoots = setOf(File("pl1"))
+ )
+ private val native = defaultSourceSet.copy(
+ "native",
+ DokkaSourceSetID("root", "native"),
+ analysisPlatform = Platform.native,
+ sourceRoots = setOf(File("pl1"))
+ )
+
+ @Test
+ fun simpleWrappingCase() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(js)) {
+ divergent {
+ text("a")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[js]\
+ |a""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun noPlatformHintCase() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test"), implicitlySourceSetHinted = false) {
+ instance(setOf(DRI("test", "Test")), setOf(js)) {
+ divergent {
+ text("a")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[js]\
+ |a""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun divergentBetweenSourceSets() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(js)) {
+ divergent {
+ text("a")
+ }
+ }
+ instance(setOf(DRI("test", "Test")), setOf(jvm)) {
+ divergent {
+ text("b")
+ }
+ }
+ instance(setOf(DRI("test", "Test")), setOf(native)) {
+ divergent {
+ text("c")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[js, jvm, native]\
+ |[js]\
+ |a
+ |
+ |[jvm]\
+ |b
+ |
+ |[native]\
+ |c""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun divergentInOneSourceSet() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(js)) {
+ divergent {
+ text("a")
+ }
+ }
+ instance(setOf(DRI("test", "Test2")), setOf(js)) {
+ divergent {
+ text("b")
+ }
+ }
+ instance(setOf(DRI("test", "Test3")), setOf(js)) {
+ divergent {
+ text("c")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[js]\
+ |a
+ |
+ |b
+ |
+ |c""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun divergentInAndBetweenSourceSets() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(native)) {
+ divergent {
+ text("a")
+ }
+ }
+ instance(setOf(DRI("test", "Test")), setOf(js)) {
+ divergent {
+ text("b")
+ }
+ }
+ instance(setOf(DRI("test", "Test")), setOf(jvm)) {
+ divergent {
+ text("c")
+ }
+ }
+ instance(setOf(DRI("test", "Test2")), setOf(js)) {
+ divergent {
+ text("d")
+ }
+ }
+ instance(setOf(DRI("test", "Test3")), setOf(native)) {
+ divergent {
+ text("e")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[native, js, jvm]\
+ |[native]\
+ |a
+ |
+ |[js]\
+ |b
+ |
+ |[jvm]\
+ |c
+ |
+ |[js]\
+ |d
+ |
+ |[native]\
+ |e""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun divergentInAndBetweenSourceSetsWithGrouping() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(native)) {
+ divergent {
+ text("a")
+ }
+ after {
+ text("a+")
+ }
+ }
+ instance(setOf(DRI("test", "Test")), setOf(js)) {
+ divergent {
+ text("b")
+ }
+ after {
+ text("bd+")
+ }
+ }
+ instance(setOf(DRI("test", "Test")), setOf(jvm)) {
+ divergent {
+ text("c")
+ }
+ }
+ instance(setOf(DRI("test", "Test2")), setOf(js)) {
+ divergent {
+ text("d")
+ }
+ after {
+ text("bd+")
+ }
+ }
+ instance(setOf(DRI("test", "Test3")), setOf(native)) {
+ divergent {
+ text("e")
+ }
+ after {
+ text("e+")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[native]\
+ |a
+ |
+ |a+
+ |
+ |[js]\
+ |b
+ |
+ |d
+ |
+ |bd+
+ |
+ |[jvm]\
+ |c
+ |
+ |[native]\
+ |e
+ |
+ |e+""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun divergentSameBefore() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(native)) {
+ before {
+ text("ab-")
+ }
+ divergent {
+ text("a")
+ }
+ }
+ instance(setOf(DRI("test", "Test2")), setOf(native)) {
+ before {
+ text("ab-")
+ }
+ divergent {
+ text("b")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[native]\
+ |ab-
+ |
+ |a
+ |
+ |b""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun divergentSameAfter() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(native)) {
+ divergent {
+ text("a")
+ }
+ after {
+ text("ab+")
+ }
+ }
+ instance(setOf(DRI("test", "Test2")), setOf(native)) {
+ divergent {
+ text("b")
+ }
+ after {
+ text("ab+")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[native]\
+ |a
+ |
+ |b
+ |
+ |ab+""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun divergentGroupedByBeforeAndAfter() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(native)) {
+ before {
+ text("ab-")
+ }
+ divergent {
+ text("a")
+ }
+ after {
+ text("ab+")
+ }
+ }
+ instance(setOf(DRI("test", "Test2")), setOf(native)) {
+ before {
+ text("ab-")
+ }
+ divergent {
+ text("b")
+ }
+ after {
+ text("ab+")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[native]\
+ |ab-
+ |
+ |a
+ |
+ |b
+ |
+ |ab+""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun divergentDifferentBeforeAndAfter() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(native)) {
+ before {
+ text("a-")
+ }
+ divergent {
+ text("a")
+ }
+ after {
+ text("ab+")
+ }
+ }
+ instance(setOf(DRI("test", "Test2")), setOf(native)) {
+ before {
+ text("b-")
+ }
+ divergent {
+ text("b")
+ }
+ after {
+ text("ab+")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[native]\
+ |a-
+ |
+ |a
+ |
+ |ab+
+ |
+ |[native]\
+ |b-
+ |
+ |b
+ |
+ |ab+""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun divergentInAndBetweenSourceSetsWithGroupingAncCommonParts() {
+ val page = testPage {
+ divergentGroup(ContentDivergentGroup.GroupID("test")) {
+ instance(setOf(DRI("test", "Test")), setOf(native)) {
+ divergent {
+ text("a")
+ }
+ after {
+ text("a+")
+ }
+ }
+ instance(setOf(DRI("test", "Test")), setOf(js)) {
+ divergent {
+ text("b")
+ }
+ after {
+ text("bd+")
+ }
+ }
+ instance(setOf(DRI("test", "Test")), setOf(jvm)) {
+ divergent {
+ text("c")
+ }
+ after {
+ text("bd+")
+ }
+ }
+ instance(setOf(DRI("test", "Test2")), setOf(js)) {
+ divergent {
+ text("d")
+ }
+ after {
+ text("bd+")
+ }
+ }
+ instance(setOf(DRI("test", "Test3")), setOf(native)) {
+ divergent {
+ text("e")
+ }
+ after {
+ text("e+")
+ }
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[native]\
+ |a
+ |
+ |a+
+ |
+ |[js, jvm]\
+ |[js]\
+ |b
+ |
+ |[jvm]\
+ |c
+ |
+ |[js]\
+ |d
+ |
+ |bd+
+ |
+ |[native]\
+ |e
+ |
+ |e+""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+}
diff --git a/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/GfmRenderingOnlyTestBase.kt b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/GfmRenderingOnlyTestBase.kt
new file mode 100644
index 00000000..9080a4fd
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/GfmRenderingOnlyTestBase.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package renderers.gfm
+
+import org.jetbrains.dokka.DokkaConfigurationImpl
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.renderers.RootCreator
+import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProviderFactory
+import org.jetbrains.dokka.base.resolvers.external.javadoc.JavadocExternalLocationProviderFactory
+import org.jetbrains.dokka.gfm.GfmPlugin
+import org.jetbrains.dokka.gfm.location.MarkdownLocationProvider
+import org.jetbrains.dokka.testApi.context.MockContext
+import renderers.RenderingOnlyTestBase
+import utils.TestOutputWriter
+
+abstract class GfmRenderingOnlyTestBase : RenderingOnlyTestBase<String>() {
+
+ val files = TestOutputWriter()
+ override val context = MockContext(
+ DokkaBase().outputWriter to { files },
+ DokkaBase().locationProviderFactory to MarkdownLocationProvider::Factory,
+ DokkaBase().externalLocationProviderFactory to ::JavadocExternalLocationProviderFactory,
+ DokkaBase().externalLocationProviderFactory to ::DefaultExternalLocationProviderFactory,
+ GfmPlugin().gfmPreprocessors to { RootCreator },
+
+ testConfiguration = DokkaConfigurationImpl(moduleName = "root", finalizeCoroutines = false)
+ )
+
+ override val renderedContent: String by lazy {
+ files.contents.getValue("test-page.md")
+ }
+}
diff --git a/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/GroupWrappingTest.kt b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/GroupWrappingTest.kt
new file mode 100644
index 00000000..13cb16c4
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/GroupWrappingTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package renderers.gfm
+
+import org.jetbrains.dokka.gfm.renderer.CommonmarkRenderer
+import org.jetbrains.dokka.pages.TextStyle
+import renderers.testPage
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class GroupWrappingTest : GfmRenderingOnlyTestBase() {
+
+ @Test
+ fun notWrapped() {
+ val page = testPage {
+ group {
+ text("a")
+ text("b")
+ }
+ text("c")
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |abc""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun paragraphWrapped() {
+ val page = testPage {
+ group(styles = setOf(TextStyle.Paragraph)) {
+ text("a")
+ text("b")
+ }
+ text("c")
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |ab
+ |
+ |c""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun blockWrapped() {
+ val page = testPage {
+ group(styles = setOf(TextStyle.Block)) {
+ text("a")
+ text("b")
+ }
+ text("c")
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |ab
+ |
+ |c""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun nested() {
+ val page = testPage {
+ group(styles = setOf(TextStyle.Block)) {
+ text("a")
+ group(styles = setOf(TextStyle.Block)) {
+ group(styles = setOf(TextStyle.Block)) {
+ text("b")
+ text("c")
+ }
+ }
+ text("d")
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |a
+ |
+ |bc
+ |
+ |d""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+}
diff --git a/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/SimpleElementsTest.kt b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/SimpleElementsTest.kt
new file mode 100644
index 00000000..720a0af5
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/SimpleElementsTest.kt
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package renderers.gfm
+
+import org.jetbrains.dokka.gfm.renderer.CommonmarkRenderer
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.pages.ContentEmbeddedResource
+import org.jetbrains.dokka.pages.ContentKind
+import org.jetbrains.dokka.pages.DCI
+import org.jetbrains.dokka.pages.TextStyle
+import renderers.RawTestPage
+import renderers.testPage
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class SimpleElementsTest : GfmRenderingOnlyTestBase() {
+
+ @Test
+ fun header() {
+ val page = testPage {
+ header(1, "The Hobbit or There and Back Again")
+ }
+ val expect = "//[testPage](test-page.md)\n\n# The Hobbit or There and Back Again"
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun link() {
+ val page = testPage {
+ link("They are not all accounted for, the lost Seeing Stones.", "http://www.google.com")
+ }
+ val expect =
+ "//[testPage](test-page.md)\n\n[They are not all accounted for, the lost Seeing Stones.](http://www.google.com)"
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun bold() {
+ val page = testPage {
+ text(
+ "That there’s some good in this world, Mr. Frodo… and it’s worth fighting for.",
+ styles = setOf(TextStyle.Bold)
+ )
+ }
+ val expect =
+ "//[testPage](test-page.md)\n\n**That there’s some good in this world, Mr. Frodo… and it’s worth fighting for.**"
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun italic() {
+ val page = testPage {
+ text("Even the smallest person can change the course of the future.", styles = setOf(TextStyle.Italic))
+ }
+ val expect = "//[testPage](test-page.md)\n\n*Even the smallest person can change the course of the future.*"
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun italicAndBold() {
+ val page = testPage {
+ text(
+ "There is no curse in Elvish, Entish, or the tongues of Men for this treachery.",
+ styles = setOf(TextStyle.Bold, TextStyle.Italic)
+ )
+ }
+ val expect =
+ "//[testPage](test-page.md)\n\n***There is no curse in Elvish, Entish, or the tongues of Men for this treachery.***"
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun strikethrough() {
+ val page = testPage {
+ text(
+ "A day may come when the courage of men fails… but it is not THIS day",
+ styles = setOf(TextStyle.Strikethrough)
+ )
+ }
+ val expect =
+ "//[testPage](test-page.md)\n\n~~A day may come when the courage of men fails… but it is not THIS day~~"
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun images() {
+ val image = ContentEmbeddedResource(
+ children = emptyList(),
+ address = "https://www.google.pl/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png",
+ altText = "This is a google logo",
+ dci = DCI(setOf(DRI.topLevel), ContentKind.Main),
+ sourceSets = emptySet()
+ )
+ val page = RawTestPage(content = image)
+ val expect =
+ "//[testPage](test-page.md)\n\n![This is a google logo](https://www.google.pl/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png)"
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun simpleTableWithHeader() {
+ val page = testPage {
+ table {
+ header {
+ text("Col1")
+ text("Col2")
+ text("Col3")
+ }
+ row {
+ text("Text1")
+ text("Text2")
+ text("Text3")
+ }
+ row {
+ text("Text4")
+ text("Text5")
+ text("Text6")
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ || Col1 | Col2 | Col3 |
+ ||---|---|---|
+ || Text1 | Text2 | Text3 |
+ || Text4 | Text5 | Text6 |""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun simpleTableWithoutHeader() {
+ val page = testPage {
+ table {
+ row {
+ text("Text1")
+ text("Text2")
+ text("Text3")
+ }
+ row {
+ text("Text4")
+ text("Text5")
+ text("Text6")
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ || | | |
+ ||---|---|---|
+ || Text1 | Text2 | Text3 |
+ || Text4 | Text5 | Text6 |""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `table with extra cell in row`() {
+ val page = testPage {
+ table {
+ header {
+ text("Col1")
+ }
+ row {
+ text("Text1")
+ text("Text2")
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ || Col1 |
+ ||---|
+ || Text1 | Text2 |""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `table with extra cell in header`() {
+ val page = testPage {
+ table {
+ header {
+ text("Col1")
+ text("Col2")
+ }
+ row {
+ text("Text1")
+ }
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ || Col1 | Col2 |
+ ||---|---|
+ || Text1 |""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun escapeText() {
+ val page = testPage {
+ text(
+ "<b>a</b>",
+ )
+ }
+ val expect =
+ "//[testPage](test-page.md)\n\n&lt;b&gt;a&lt;/b&gt;"
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `unordered list with two items`() {
+ val page = testPage {
+ unorderedList {
+ item { text("Item 1") }
+ item { text("Item 2") }
+ }
+ }
+
+ val expect = """|//[testPage](test-page.md)
+ |
+ |- Item 1
+ |- Item 2""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `unordered list with styled text`() {
+ val page = testPage {
+ unorderedList {
+ item {
+ text("Nobody", styles = setOf(TextStyle.Italic))
+ text(" tosses a Dwarf!")
+ }
+ }
+ }
+
+ val expect = "//[testPage](test-page.md)\n\n- *Nobody* tosses a Dwarf!"
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `ordered list with two items`() {
+ val page = testPage {
+ orderedList {
+ item { text("Item 1") }
+ item { text("Item 2") }
+ }
+ }
+
+ val expect = """|//[testPage](test-page.md)
+ |
+ |1. Item 1
+ |2. Item 2""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `ordered list with nested unordered list`() {
+ val page = testPage {
+ orderedList {
+ item {
+ text("And another list:")
+ unorderedList {
+ item { text("Item 1") }
+ item { text("Item 2") }
+ }
+ }
+ item { text("Following item") }
+ }
+ }
+
+ val expect = """|//[testPage](test-page.md)
+ |
+ |1. And another list:
+ |
+ | - Item 1
+ | - Item 2
+ |2. Following item""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `ordered list with nested table`() {
+ val page = testPage {
+ orderedList {
+ item {
+ text("The following table is nested in a list:")
+ table {
+ header {
+ text("Col1")
+ text("Col2")
+ }
+ row {
+ text("Text1")
+ text("Text2")
+ }
+ }
+ }
+ }
+ }
+
+ val expect = """|//[testPage](test-page.md)
+ |
+ |1. The following table is nested in a list:
+ | | Col1 | Col2 |
+ | |---|---|
+ | | Text1 | Text2 |""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `three levels of list`() {
+ val page = testPage {
+ unorderedList {
+ item {
+ text("Level 1")
+ unorderedList {
+ item {
+ text("Level 2")
+ unorderedList {
+ item {
+ text("Level 3")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Extra newlines are not pretty but do not impact formatting
+ val expect = """|//[testPage](test-page.md)
+ |
+ |- Level 1
+ |
+ | - Level 2
+ |
+ | - Level 3""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun `nested list with no text preceding it`() {
+ val page = testPage {
+ unorderedList {
+ item {
+ unorderedList {
+ item {
+ text("Nested")
+ }
+ }
+ }
+ }
+ }
+
+ val expect = """|//[testPage](test-page.md)
+ |
+ |-
+ | - Nested""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+}
diff --git a/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/SourceSetDependentHintTest.kt b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/SourceSetDependentHintTest.kt
new file mode 100644
index 00000000..3f0129af
--- /dev/null
+++ b/dokka-subprojects/plugin-gfm/src/test/kotlin/renderers/gfm/SourceSetDependentHintTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package renderers.gfm
+
+import org.jetbrains.dokka.DokkaSourceSetID
+import org.jetbrains.dokka.Platform
+import org.jetbrains.dokka.gfm.renderer.CommonmarkRenderer
+import org.jetbrains.dokka.pages.TextStyle
+import renderers.testPage
+import testApi.testRunner.defaultSourceSet
+import java.io.File
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class SourceSetDependentHintTest : GfmRenderingOnlyTestBase() {
+
+ private val pl1 = defaultSourceSet.copy(
+ "pl1",
+ DokkaSourceSetID("root", "pl1"),
+ analysisPlatform = Platform.js,
+ sourceRoots = setOf(File("pl1"))
+ )
+ private val pl2 = defaultSourceSet.copy(
+ "pl2",
+ DokkaSourceSetID("root", "pl2"),
+ analysisPlatform = Platform.jvm,
+ sourceRoots = setOf(File("pl1"))
+ )
+ private val pl3 = defaultSourceSet.copy(
+ "pl3",
+ DokkaSourceSetID("root", "pl3"),
+ analysisPlatform = Platform.native,
+ sourceRoots = setOf(File("pl1"))
+ )
+
+ @Test
+ fun platformIndependentCase() {
+ val page = testPage {
+ sourceSetDependentHint(sourceSets = setOf(pl1, pl2, pl3), styles = setOf(TextStyle.Block)) {
+ text("a")
+ text("b")
+ text("c")
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[pl1, pl2, pl3]\
+ |abc""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun completelyDivergentCase() {
+ val page = testPage {
+ sourceSetDependentHint(sourceSets = setOf(pl1, pl2, pl3), styles = setOf(TextStyle.Block)) {
+ text("a", sourceSets = setOf(pl1))
+ text("b", sourceSets = setOf(pl2))
+ text("c", sourceSets = setOf(pl3))
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[pl1]\
+ |a
+ |
+ |[pl2]\
+ |b
+ |
+ |[pl3]\
+ |c""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun overlappingCase() {
+ val page = testPage {
+ sourceSetDependentHint(sourceSets = setOf(pl1, pl2), styles = setOf(TextStyle.Block)) {
+ text("a", sourceSets = setOf(pl1))
+ text("b", sourceSets = setOf(pl1, pl2))
+ text("c", sourceSets = setOf(pl2))
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[pl1]\
+ |ab
+ |
+ |[pl2]\
+ |bc""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun caseThatCanBeSimplified() {
+ val page = testPage {
+ sourceSetDependentHint(sourceSets = setOf(pl1, pl2), styles = setOf(TextStyle.Block)) {
+ text("a", sourceSets = setOf(pl1, pl2))
+ text("b", sourceSets = setOf(pl1))
+ text("b", sourceSets = setOf(pl2))
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[pl1, pl2]\
+ |ab""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun caseWithGroupBreakingSimplification() {
+ val page = testPage {
+ sourceSetDependentHint(sourceSets = setOf(pl1, pl2), styles = setOf(TextStyle.Block)) {
+ group(styles = setOf(TextStyle.Block)) {
+ text("a", sourceSets = setOf(pl1, pl2))
+ text("b", sourceSets = setOf(pl1))
+ }
+ text("b", sourceSets = setOf(pl2))
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[pl1]\
+ |ab
+ |
+ |[pl2]\
+ |a
+ |
+ |b""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun caseWithGroupNotBreakingSimplification() {
+ val page = testPage {
+ sourceSetDependentHint(sourceSets = setOf(pl1, pl2)) {
+ group {
+ text("a", sourceSets = setOf(pl1, pl2))
+ text("b", sourceSets = setOf(pl1))
+ }
+ text("b", sourceSets = setOf(pl2))
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[pl1, pl2]\
+ |ab""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+
+ @Test
+ fun partiallyUnifiedCase() {
+ val page = testPage {
+ sourceSetDependentHint(sourceSets = setOf(pl1, pl2, pl3), styles = setOf(TextStyle.Block)) {
+ text("a", sourceSets = setOf(pl1))
+ text("a", sourceSets = setOf(pl2))
+ text("b", sourceSets = setOf(pl3))
+ }
+ }
+ val expect = """|//[testPage](test-page.md)
+ |
+ |[pl1, pl2]\
+ |a
+ |
+ |[pl3]\
+ |b""".trimMargin()
+
+ CommonmarkRenderer(context).render(page)
+ assertEquals(expect, renderedContent)
+ }
+}