From 8e5c63d035ef44a269b8c43430f43f5c8eebfb63 Mon Sep 17 00:00:00 2001 From: Ignat Beresnev Date: Fri, 10 Nov 2023 11:46:54 +0100 Subject: 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 --- .../kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt | 63 ++++ .../org/jetbrains/dokka/gfm/gfmTemplating.kt | 39 ++ .../dokka/gfm/location/MarkdownLocationProvider.kt | 23 ++ .../dokka/gfm/renderer/BriefCommentPreprocessor.kt | 22 ++ .../dokka/gfm/renderer/CommonmarkRenderer.kt | 414 +++++++++++++++++++++ 5 files changed, 561 insertions(+) create mode 100644 dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/GfmPlugin.kt create mode 100644 dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/gfmTemplating.kt create mode 100644 dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/location/MarkdownLocationProvider.kt create mode 100644 dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/BriefCommentPreprocessor.kt create mode 100644 dokka-subprojects/plugin-gfm/src/main/kotlin/org/jetbrains/dokka/gfm/renderer/CommonmarkRenderer.kt (limited to 'dokka-subprojects/plugin-gfm/src/main/kotlin/org') 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 by extensionPoint() + + private val dokkaBase by lazy { plugin() } + + public val renderer: Extension by extending { + CoreExtensions.renderer providing ::CommonmarkRenderer override dokkaBase.htmlRenderer + } + + public val locationProvider: Extension by extending { + dokkaBase.locationProviderFactory providing MarkdownLocationProvider::Factory override dokkaBase.locationProvider + } + + public val rootCreator: Extension by extending { + gfmPreprocessors with RootCreator + } + + public val briefCommentPreprocessor: Extension by extending { + gfmPreprocessors with BriefCommentPreprocessor() + } + + public val packageListCreator: Extension 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("(.+?)(?=") + + 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("") + content() + append("") + } + } +} + +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 { + 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(context) { + + override val preprocessors: List = context.plugin().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? + ) { + 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: +
  • +
      • Item
    +
  • + + 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? + ) { + 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? + ) { + 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) -> + 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? + ) { + 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(), "
    ") + .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().forEach { append(it.text) } + append("`") + } + + private fun decorators(styles: Set