Skip to content

Commit

Permalink
Fix and refactor Sample Transformer
Browse files Browse the repository at this point in the history
  • Loading branch information
vmishenev committed Aug 2, 2023
1 parent 2c01749 commit a4fdd90
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 13 deletions.
10 changes: 8 additions & 2 deletions plugins/base/src/main/kotlin/DokkaBase.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@file:Suppress("unused")

package org.jetbrains.dokka.base

import org.jetbrains.dokka.CoreExtensions
Expand All @@ -19,6 +17,7 @@ import org.jetbrains.dokka.base.signatures.KotlinSignatureProvider
import org.jetbrains.dokka.base.signatures.SignatureProvider
import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer
import org.jetbrains.dokka.base.transformers.documentables.*
import org.jetbrains.dokka.base.transformers.pages.DefaultSamplesTransformer
import org.jetbrains.dokka.base.transformers.pages.annotations.SinceKotlinTransformer
import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter
import org.jetbrains.dokka.base.transformers.pages.comments.DocTagToContentConverter
Expand All @@ -33,6 +32,7 @@ import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement
import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer
import org.jetbrains.dokka.transformers.pages.PageTransformer

@Suppress("unused")
class DokkaBase : DokkaPlugin() {

val preMergeDocumentableTransformer by extensionPoint<PreMergeDocumentableTransformer>()
Expand Down Expand Up @@ -191,6 +191,12 @@ class DokkaBase : DokkaPlugin() {
htmlPreprocessors with RootCreator applyIf { !delayTemplateSubstitution }
}

val defaultSamplesTransformer by extending {
CoreExtensions.pageTransformer providing ::DefaultSamplesTransformer order {
before(pageMerger)
}
}

val sourceLinksTransformer by extending {
htmlPreprocessors providing ::SourceLinksTransformer order { after(rootCreator) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.jetbrains.dokka.pages.ContentPage
import org.jetbrains.dokka.pages.PageNode
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.plugability.configuration
import org.jsoup.Jsoup
import java.net.URI

class DefaultTemplateModelFactory(val context: DokkaContext) : TemplateModelFactory {
Expand Down Expand Up @@ -91,29 +92,41 @@ class DefaultTemplateModelFactory(val context: DokkaContext) : TemplateModelFact
get() = URI(this).isAbsolute

private fun Appendable.resourcesForPage(pathToRoot: String, resources: List<String>): Unit =
resources.forEach {
resources.forEach { resource ->
/**
* Workaround for resources that have no extension
* e.g. `<script src="`https://unpkg.com/kotlin-playground@1`"></script>`
*/
if (resource.startsWith("<")) {
val tag = Jsoup.parse(resource).head().child(0)
if (tag.tagName() == "script" || tag.tagName() == "style") {
append(resource)
}
return@forEach
}

val resourceHtml = with(createHTML()) {
when {
it.URIExtension == "css" ->

resource.URIExtension == "css" ->
link(
rel = LinkRel.stylesheet,
href = if (it.isAbsolute) it else "$pathToRoot$it"
href = if (resource.isAbsolute) resource else "$pathToRoot$resource"
)
it.URIExtension == "js" ->
resource.URIExtension == "js" ->
script(
type = ScriptType.textJavaScript,
src = if (it.isAbsolute) it else "$pathToRoot$it"
src = if (resource.isAbsolute) resource else "$pathToRoot$resource"
) {
if (it == "scripts/main.js" || it.endsWith("_deferred.js"))
if (resource == "scripts/main.js" || resource.endsWith("_deferred.js"))
defer = true
else
async = true
}
it.isImage() -> link(href = if (it.isAbsolute) it else "$pathToRoot$it")
resource.isImage() -> link(href = if (resource.isAbsolute) resource else "$pathToRoot$resource")
else -> null
}
}

if (resourceHtml != null) {
append(resourceHtml)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.jetbrains.dokka.base.transformers.pages

import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.DisplaySourceSet
import org.jetbrains.dokka.model.doc.Sample
import org.jetbrains.dokka.model.properties.PropertyContainer
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.plugability.plugin
import org.jetbrains.dokka.plugability.querySingle
import org.jetbrains.dokka.transformers.pages.PageTransformer
import org.jetbrains.kotlin.analysis.kotlin.internal.InternalKotlinAnalysisPlugin
import org.jetbrains.kotlin.analysis.kotlin.internal.SampleProvider
import org.jetbrains.kotlin.analysis.kotlin.internal.SampleProviderFactory

internal const val KOTLIN_PLAYGROUND_SCRIPT = "<script src=\"`https://unpkg.com/kotlin-playground@1`\"></script>"

internal class DefaultSamplesTransformer(val context: DokkaContext) : PageTransformer {

private val sampleProviderFactory: SampleProviderFactory = context.plugin<InternalKotlinAnalysisPlugin>().querySingle { sampleProviderFactory }

override fun invoke(input: RootPageNode): RootPageNode {
return sampleProviderFactory.build().use { sampleProvider ->
input.transformContentPagesTree { page ->
val samples = (page as? WithDocumentables)?.documentables?.flatMap {
it.documentation.entries.flatMap { entry ->
entry.value.children.filterIsInstance<Sample>().map { entry.key to it }
}
} ?: return@transformContentPagesTree page

val newContent = samples.fold(page.content) { acc, (sampleSourceSet, sample) ->
sampleProvider.getSample(sampleSourceSet, sample.name)
?.let {
acc.addSample(page, sample.name, it)
} ?: acc
}

page.modified(
content = newContent,
embeddedResources = page.embeddedResources + KOTLIN_PLAYGROUND_SCRIPT
)
}
}
}


private fun ContentNode.addSample(
contentPage: ContentPage,
fqLink: String,
sample: SampleProvider.SampleSnippet,
): ContentNode {
val node = contentCode(contentPage.content.sourceSets, contentPage.dri, createSampleBody(sample.imports, sample.body), "kotlin")
return dfs(fqLink, node)
}

fun createSampleBody(imports: String, body: String) =
""" |$imports
|fun main() {
| //sampleStart
| $body
| //sampleEnd
|}""".trimMargin()

private fun ContentNode.dfs(fqName: String, node: ContentCodeBlock): ContentNode {
return when (this) {
is ContentHeader -> copy(children.map { it.dfs(fqName, node) })
is ContentDivergentGroup -> @Suppress("UNCHECKED_CAST") copy(children.map {
it.dfs(fqName, node)
} as List<ContentDivergentInstance>)
is ContentDivergentInstance -> copy(
before.let { it?.dfs(fqName, node) },
divergent.dfs(fqName, node),
after.let { it?.dfs(fqName, node) })
is ContentCodeBlock -> copy(children.map { it.dfs(fqName, node) })
is ContentCodeInline -> copy(children.map { it.dfs(fqName, node) })
is ContentDRILink -> copy(children.map { it.dfs(fqName, node) })
is ContentResolvedLink -> copy(children.map { it.dfs(fqName, node) })
is ContentEmbeddedResource -> copy(children.map { it.dfs(fqName, node) })
is ContentTable -> copy(children = children.map { it.dfs(fqName, node) as ContentGroup })
is ContentList -> copy(children.map { it.dfs(fqName, node) })
is ContentGroup -> copy(children.map { it.dfs(fqName, node) })
is PlatformHintedContent -> copy(inner.dfs(fqName, node))
is ContentText -> if (text == fqName) node else this
is ContentBreakLine -> this
else -> this.also { context.logger.error("Could not recognize $this ContentNode in SamplesTransformer") }
}
}

private fun contentCode(
sourceSets: Set<DisplaySourceSet>,
dri: Set<DRI>,
content: String,
language: String,
styles: Set<Style> = emptySet(),
extra: PropertyContainer<ContentNode> = PropertyContainer.empty()
) =
ContentCodeBlock(
children = listOf(
ContentText(
text = content,
dci = DCI(dri, ContentKind.Sample),
sourceSets = sourceSets,
style = emptySet(),
extra = PropertyContainer.empty()
)
),
language = language,
dci = DCI(dri, ContentKind.Sample),
sourceSets = sourceSets,
style = styles + ContentStyle.RunnableSample + TextStyle.Monospace,
extra = extra
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package content.samples

import matchers.content.*
import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest
import org.jetbrains.dokka.base.transformers.pages.KOTLIN_PLAYGROUND_SCRIPT
import org.jetbrains.dokka.model.DisplaySourceSet
import org.junit.jupiter.api.Test
import utils.TestOutputWriterPlugin
import utils.classSignature
import utils.findTestType
import java.nio.file.Paths
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

class ContentForSamplesTest : BaseAbstractTest() {
private val testDataDir = getTestDataDir("content/samples").toAbsolutePath()
Expand Down Expand Up @@ -61,6 +64,7 @@ class ContentForSamplesTest : BaseAbstractTest() {

@Test
fun `samples block is rendered in the description`() {
val writerPlugin = TestOutputWriterPlugin()
testInline(
"""
|/src/main/kotlin/test/source.kt
Expand All @@ -70,10 +74,12 @@ class ContentForSamplesTest : BaseAbstractTest() {
| * @sample [test.sampleForClassDescription]
| */
|class Foo
""".trimIndent(), testConfiguration
""".trimIndent(), testConfiguration,
pluginOverrides = listOf(writerPlugin)
) {
pagesTransformationStage = { module ->
val page = module.findTestType("test", "Foo")
assert(KOTLIN_PLAYGROUND_SCRIPT in page.embeddedResources)
page.content.assertNode {
group {
header(1) { +"Foo" }
Expand Down Expand Up @@ -101,6 +107,9 @@ class ContentForSamplesTest : BaseAbstractTest() {
skipAllNotMatching()
}
}
renderingStage = { _, _ ->
assertNotEquals(-1, writerPlugin.writer.contents["root/test/-foo/index.html"]?.indexOf(KOTLIN_PLAYGROUND_SCRIPT))
}
}
}

Expand Down Expand Up @@ -134,6 +143,7 @@ class ContentForSamplesTest : BaseAbstractTest() {
) {
pagesTransformationStage = { module ->
val page = module.findTestType("pageMerger", "Parent")
assert(KOTLIN_PLAYGROUND_SCRIPT in page.embeddedResources)
page.content.assertNode {
group {
header(1) { +"Parent" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class InternalKotlinAnalysisPlugin : DokkaPlugin() {

val documentableSourceLanguageParser by extensionPoint<DocumentableSourceLanguageParser>()

val sampleProviderFactory by extensionPoint<SampleProviderFactory>()

@OptIn(DokkaPluginApiPreview::class)
override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.jetbrains.kotlin.analysis.kotlin.internal

import org.jetbrains.dokka.DokkaConfiguration
import org.jetbrains.dokka.InternalDokkaApi

@InternalDokkaApi
interface SampleProviderFactory {
fun build(): SampleProvider
}

/**
* It's closeable. Otherwise, there is a chance of memory leak.
*/
@InternalDokkaApi
interface SampleProvider: AutoCloseable {
class SampleSnippet(val imports: String, val body:String)


/**
* @return [SampleSnippet] or null if it has not found by [fqLink]
*/
fun getSample(sourceSet: DokkaConfiguration.DokkaSourceSet, fqLink: String): SampleSnippet?
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.jetbrains.dokka.renderers.PostAction
import org.jetbrains.kotlin.analysis.kotlin.internal.InternalKotlinAnalysisPlugin
import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation

@Suppress("unused")
@InternalDokkaApi
class CompilerDescriptorAnalysisPlugin : DokkaPlugin() {

Expand All @@ -42,7 +43,7 @@ class CompilerDescriptorAnalysisPlugin : DokkaPlugin() {
plugin<InternalKotlinAnalysisPlugin>().documentableSourceLanguageParser providing { CompilerDocumentableSourceLanguageParser() }
}

internal val defaultKotlinAnalysis by extending {
internal val defaultKotlinAnalysis by extending {
kotlinAnalysis providing { ctx ->
ProjectKotlinAnalysis(
sourceSets = ctx.configuration.sourceSets,
Expand All @@ -51,7 +52,7 @@ class CompilerDescriptorAnalysisPlugin : DokkaPlugin() {
}
}

internal val descriptorToDocumentableTranslator by extending {
internal val descriptorToDocumentableTranslator by extending {
CoreExtensions.sourceToDocumentableTranslator providing ::DefaultDescriptorToDocumentableTranslator
}

Expand All @@ -63,6 +64,10 @@ class CompilerDescriptorAnalysisPlugin : DokkaPlugin() {
plugin<InternalKotlinAnalysisPlugin>().fullClassHierarchyBuilder providing { DescriptorFullClassHierarchyBuilder() }
}

internal val kotlinSampleProviderFactory by extending {
plugin<InternalKotlinAnalysisPlugin>().sampleProviderFactory providing ::KotlinSampleProviderFactory
}

internal val descriptorSyntheticDocumentableDetector by extending {
plugin<InternalKotlinAnalysisPlugin>().syntheticDocumentableDetector providing { DescriptorSyntheticDocumentableDetector() }
}
Expand Down
Loading

0 comments on commit a4fdd90

Please sign in to comment.