diff options
Diffstat (limited to 'plugins/base/src/main/kotlin/renderers')
11 files changed, 1346 insertions, 0 deletions
diff --git a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt new file mode 100644 index 00000000..afee1b33 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt @@ -0,0 +1,200 @@ +package org.jetbrains.dokka.base.renderers + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.local.LocationProvider +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.renderers.Renderer +import org.jetbrains.dokka.transformers.pages.PageTransformer + +abstract class DefaultRenderer<T>( + protected val context: DokkaContext +) : Renderer { + + protected val outputWriter = context.plugin<DokkaBase>().querySingle { outputWriter } + + protected lateinit var locationProvider: LocationProvider + private set + + protected open val preprocessors: Iterable<PageTransformer> = emptyList() + + abstract fun T.buildHeader(level: Int, node: ContentHeader, content: T.() -> Unit) + abstract fun T.buildLink(address: String, content: T.() -> Unit) + abstract fun T.buildList( + node: ContentList, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) + + abstract fun T.buildNewLine() + abstract fun T.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) + abstract fun T.buildTable( + node: ContentTable, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) + + abstract fun T.buildText(textNode: ContentText) + abstract fun T.buildNavigation(page: PageNode) + + abstract fun buildPage(page: ContentPage, content: (T, ContentPage) -> Unit): String + abstract fun buildError(node: ContentNode) + + open fun T.buildPlatformDependent( + content: PlatformHintedContent, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) = buildContentNode(content.inner, pageContext) + + open fun T.buildGroup( + node: ContentGroup, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) = + wrapGroup(node, pageContext) { node.children.forEach { it.build(this, pageContext, sourceSetRestriction) } } + + open fun T.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) = + node.children.forEach { it.build(this, pageContext) } + + open fun T.wrapGroup(node: ContentGroup, pageContext: ContentPage, childrenCallback: T.() -> Unit) = + childrenCallback() + + open fun T.buildLinkText( + nodes: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + nodes.forEach { it.build(this, pageContext, sourceSetRestriction) } + } + + open fun T.buildCodeBlock(code: ContentCodeBlock, pageContext: ContentPage) { + code.children.forEach { it.build(this, pageContext) } + } + + open fun T.buildCodeInline(code: ContentCodeInline, pageContext: ContentPage) { + code.children.forEach { it.build(this, pageContext) } + } + + open fun T.buildHeader( + node: ContentHeader, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + buildHeader(node.level, node) { node.children.forEach { it.build(this, pageContext, sourceSetRestriction) } } + } + + open fun ContentNode.build( + builder: T, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) = + builder.buildContentNode(this, pageContext, sourceSetRestriction) + + open fun T.buildContentNode( + node: ContentNode, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + if (sourceSetRestriction == null || node.sourceSets.any { it in sourceSetRestriction }) { + when (node) { + is ContentText -> buildText(node) + is ContentHeader -> buildHeader(node, pageContext, sourceSetRestriction) + is ContentCodeBlock -> buildCodeBlock(node, pageContext) + is ContentCodeInline -> buildCodeInline(node, pageContext) + is ContentDRILink -> + buildLink(locationProvider.resolve(node.address, node.sourceSets, pageContext)) { + buildLinkText(node.children, pageContext, sourceSetRestriction) + } + is ContentResolvedLink -> buildLink(node.address) { + buildLinkText(node.children, pageContext, sourceSetRestriction) + } + is ContentEmbeddedResource -> buildResource(node, pageContext) + is ContentList -> buildList(node, pageContext, sourceSetRestriction) + is ContentTable -> buildTable(node, pageContext, sourceSetRestriction) + is ContentGroup -> buildGroup(node, pageContext, sourceSetRestriction) + is ContentBreakLine -> buildNewLine() + is PlatformHintedContent -> buildPlatformDependent(node, pageContext, sourceSetRestriction) + is ContentDivergentGroup -> buildDivergent(node, pageContext) + is ContentDivergentInstance -> buildDivergentInstance(node, pageContext) + else -> buildError(node) + } + } + } + + open fun T.buildDivergentInstance(node: ContentDivergentInstance, pageContext: ContentPage) { + node.before?.build(this, pageContext) + node.divergent.build(this, pageContext) + node.after?.build(this, pageContext) + } + + open fun buildPageContent(context: T, page: ContentPage) { + context.buildNavigation(page) + page.content.build(context, page) + } + + open suspend fun renderPage(page: PageNode) { + val path by lazy { locationProvider.resolve(page, skipExtension = true) } + when (page) { + is ContentPage -> outputWriter.write(path, buildPage(page) { c, p -> buildPageContent(c, p) }, ".html") + 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), ".html") + RenderingStrategy.DoNothing -> Unit + } + else -> throw AssertionError( + "Page ${page.name} cannot be rendered by renderer as it is not renderer specific nor contains content" + ) + } + } + + private suspend fun renderPages(root: PageNode) { + coroutineScope { + renderPage(root) + + root.children.forEach { + launch { renderPages(it) } + } + } + } + + override fun render(root: RootPageNode) { + val newRoot = preprocessors.fold(root) { acc, t -> t(acc) } + + locationProvider = + context.plugin<DokkaBase>().querySingle { locationProviderFactory }.getLocationProvider(newRoot) + + runBlocking(Dispatchers.Default) { + renderPages(newRoot) + } + } + + protected fun ContentDivergentGroup.groupDivergentInstances( + pageContext: ContentPage, + beforeTransformer: (ContentDivergentInstance, ContentPage, DokkaSourceSet) -> String, + afterTransformer: (ContentDivergentInstance, ContentPage, DokkaSourceSet) -> String + ): Map<SerializedBeforeAndAfter, List<InstanceWithSource>> = + children.flatMap { instance -> + instance.sourceSets.map { sourceSet -> + Pair(instance, sourceSet) to Pair( + beforeTransformer(instance, pageContext, sourceSet), + afterTransformer(instance, pageContext, sourceSet) + ) + } + }.groupBy( + Pair<InstanceWithSource, SerializedBeforeAndAfter>::second, + Pair<InstanceWithSource, SerializedBeforeAndAfter>::first + ) +} + +internal typealias SerializedBeforeAndAfter = Pair<String, String> +internal typealias InstanceWithSource = Pair<ContentDivergentInstance, DokkaSourceSet> + +fun ContentPage.sourceSets() = this.content.sourceSets
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/DefaultTabSortingStrategy.kt b/plugins/base/src/main/kotlin/renderers/DefaultTabSortingStrategy.kt new file mode 100644 index 00000000..3b849fec --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/DefaultTabSortingStrategy.kt @@ -0,0 +1,31 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.pages.ContentKind +import org.jetbrains.dokka.pages.ContentNode +import org.jetbrains.dokka.pages.Kind +import org.jetbrains.dokka.utilities.DokkaLogger + +private val kindsOrder = listOf( + ContentKind.Classlikes, + ContentKind.Constructors, + ContentKind.Functions, + ContentKind.Properties, + ContentKind.Extensions, + ContentKind.Parameters, + ContentKind.Inheritors, + ContentKind.Source, + ContentKind.Sample, + ContentKind.Comment +) + +class DefaultTabSortingStrategy : TabSortingStrategy { + override fun <T: ContentNode> sort(tabs: Collection<T>): List<T> { + val tabMap: Map<Kind, MutableList<T>> = kindsOrder.asSequence().map { it to mutableListOf<T>() }.toMap() + val unrecognized: MutableList<T> = mutableListOf() + tabs.forEach { + tabMap[it.dci.kind]?.add(it) ?: unrecognized.add(it) + } + return tabMap.values.flatten() + unrecognized + } + +} diff --git a/plugins/base/src/main/kotlin/renderers/FileWriter.kt b/plugins/base/src/main/kotlin/renderers/FileWriter.kt new file mode 100644 index 00000000..181295c0 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/FileWriter.kt @@ -0,0 +1,88 @@ +package org.jetbrains.dokka.base.renderers + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.dokka.plugability.DokkaContext +import java.io.File +import java.io.IOException +import java.net.URI +import java.nio.file.* + +class FileWriter(val context: DokkaContext): OutputWriter { + private val createdFiles: MutableSet<String> = mutableSetOf() + private val jarUriPrefix = "jar:file:" + private val root = context.configuration.outputDir + + override suspend fun write(path: String, text: String, ext: String) { + if (createdFiles.contains(path)) { + context.logger.error("An attempt to write ${root}/$path several times!") + return + } + createdFiles.add(path) + + try { + val dir = Paths.get(root, path.dropLastWhile { it != '/' }).toFile() + withContext(Dispatchers.IO) { + dir.mkdirsOrFail() + Files.write(Paths.get(root, "$path$ext"), text.lines()) + } + } catch (e: Throwable) { + context.logger.error("Failed to write $this. ${e.message}") + e.printStackTrace() + } + } + + override suspend fun writeResources(pathFrom: String, pathTo: String) = + if (javaClass.getResource(pathFrom).toURI().toString().startsWith(jarUriPrefix)) { + copyFromJar(pathFrom, pathTo) + } else { + copyFromDirectory(pathFrom, pathTo) + } + + + private suspend fun copyFromDirectory(pathFrom: String, pathTo: String) { + val dest = Paths.get(root, pathTo).toFile() + val uri = javaClass.getResource(pathFrom).toURI() + withContext(Dispatchers.IO) { + File(uri).copyRecursively(dest, true) + } + } + + private suspend fun copyFromJar(pathFrom: String, pathTo: String) { + val rebase = fun(path: String) = + "$pathTo/${path.removePrefix(pathFrom)}" + val dest = Paths.get(root, pathTo).toFile() + dest.mkdirsOrFail() + val uri = javaClass.getResource(pathFrom).toURI() + val fs = getFileSystemForURI(uri) + val path = fs.getPath(pathFrom) + for (file in Files.walk(path).iterator()) { + if (Files.isDirectory(file)) { + val dirPath = file.toAbsolutePath().toString() + withContext(Dispatchers.IO) { + Paths.get(root, rebase(dirPath)).toFile().mkdirsOrFail() + } + } else { + val filePath = file.toAbsolutePath().toString() + withContext(Dispatchers.IO) { + Paths.get(root, rebase(filePath)).toFile().writeBytes( + this@FileWriter.javaClass.getResourceAsStream(filePath).readBytes() + ) + } + } + } + } + + private fun File.mkdirsOrFail() { + if (!mkdirs() && !exists()) { + throw IOException("Failed to create directory $this") + } + } + + private fun getFileSystemForURI(uri: URI): FileSystem = + try { + FileSystems.newFileSystem(uri, emptyMap<String, Any>()) + } catch (e: FileSystemAlreadyExistsException) { + FileSystems.getFileSystem(uri) + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/OutputWriter.kt b/plugins/base/src/main/kotlin/renderers/OutputWriter.kt new file mode 100644 index 00000000..1827c7f0 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/OutputWriter.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.base.renderers + +interface OutputWriter { + + suspend fun write(path: String, text: String, ext: String) + suspend fun writeResources(pathFrom: String, pathTo: String) +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/PackageListService.kt b/plugins/base/src/main/kotlin/renderers/PackageListService.kt new file mode 100644 index 00000000..d4333200 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/PackageListService.kt @@ -0,0 +1,51 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.parent +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.kotlin.utils.addToStdlib.safeAs + +class PackageListService(val context: DokkaContext) { + + fun formatPackageList(module: RootPageNode, format: String, linkExtension: String): String { + + val packages = mutableSetOf<String>() + val nonStandardLocations = mutableMapOf<String, String>() + + val locationProvider = + context.plugin<DokkaBase>().querySingle { locationProviderFactory }.getLocationProvider(module) + + fun visit(node: PageNode, parentDris: Set<DRI>) { + + if (node is PackagePageNode) { + packages.add(node.name) + } + + val contentPage = node.safeAs<ContentPage>() + contentPage?.dri?.forEach { + if (parentDris.isNotEmpty() && it.parent !in parentDris) { + nonStandardLocations[it.toString()] = locationProvider.resolve(node) + } + } + + node.children.forEach { visit(it, contentPage?.dri ?: setOf()) } + } + + visit(module, setOf()) + + return buildString { + appendln("\$dokka.format:${format}") + appendln("\$dokka.linkExtension:${linkExtension}") + nonStandardLocations.map { (signature, location) -> "\$dokka.location:$signature\u001f$location" } + .sorted().joinTo(this, separator = "\n", postfix = "\n") + + packages.sorted().joinTo(this, separator = "\n", postfix = "\n") + } + + } + +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/TabSortingStrategy.kt b/plugins/base/src/main/kotlin/renderers/TabSortingStrategy.kt new file mode 100644 index 00000000..dcf49ca9 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/TabSortingStrategy.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.pages.ContentNode + +interface TabSortingStrategy { + fun <T: ContentNode> sort(tabs: Collection<T>) : List<T> +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt new file mode 100644 index 00000000..614d8f6e --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -0,0 +1,760 @@ +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.renderers.DefaultRenderer +import org.jetbrains.dokka.base.renderers.TabSortingStrategy +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.model.withDescendants +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.plugability.querySingle +import java.io.File +import java.net.URI + +open class HtmlRenderer( + context: DokkaContext +) : DefaultRenderer<FlowContent>(context) { + + private val sourceSetDependencyMap = context.configuration.sourceSets.map { sourceSet -> + sourceSet to context.configuration.sourceSets.filter { sourceSet.dependentSourceSets.contains(it.sourceSetID) } + }.toMap() + + private val isMultiplatform by lazy { + sourceSetDependencyMap.size > 1 + } + + private val pageList = mutableMapOf<String, Pair<String, String>>() + + override val preprocessors = context.plugin<DokkaBase>().query { htmlPreprocessors } + + private val tabSortingStrategy = context.plugin<DokkaBase>().querySingle { tabSortingStrategy } + + private fun <T : ContentNode> sortTabs(strategy: TabSortingStrategy, tabs: Collection<T>): List<T> { + val sorted = strategy.sort(tabs) + if (sorted.size != tabs.size) + context.logger.warn("Tab sorting strategy has changed number of tabs from ${tabs.size} to ${sorted.size}") + return sorted; + } + + override fun FlowContent.wrapGroup( + node: ContentGroup, + pageContext: ContentPage, + childrenCallback: FlowContent.() -> Unit + ) { + val additionalClasses = node.style.joinToString(" ") { it.toString().toLowerCase() } + return when { + node.hasStyle(ContentStyle.TabbedContent) -> div(additionalClasses) { + val secondLevel = node.children.filterIsInstance<ContentComposite>().flatMap { it.children } + .filterIsInstance<ContentHeader>().flatMap { it.children }.filterIsInstance<ContentText>() + val firstLevel = node.children.filterIsInstance<ContentHeader>().flatMap { it.children } + .filterIsInstance<ContentText>() + + val renderable = firstLevel.union(secondLevel).let { sortTabs(tabSortingStrategy, it) } + + div(classes = "tabs-section") { + attributes["tabs-section"] = "tabs-section" + renderable.forEachIndexed { index, node -> + button(classes = "section-tab") { + if (index == 0) attributes["data-active"] = "" + attributes["data-togglable"] = node.text + text(node.text) + } + } + } + div(classes = "tabs-section-body") { + childrenCallback() + } + } + node.hasStyle(ContentStyle.WithExtraAttributes) -> div() { + node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } + childrenCallback() + } + node.dci.kind in setOf(ContentKind.Symbol) -> div("symbol $additionalClasses") { + childrenCallback() + if (node.hasStyle(TextStyle.Monospace)) copyButton() + } + node.hasStyle(TextStyle.BreakableAfter) -> { + span() { childrenCallback() } + wbr { } + } + node.hasStyle(TextStyle.Breakable) -> { + span("breakable-word") { childrenCallback() } + } + node.hasStyle(TextStyle.Span) -> span() { childrenCallback() } + node.dci.kind == ContentKind.Symbol -> div("symbol $additionalClasses") { childrenCallback() } + node.dci.kind == ContentKind.BriefComment -> div("brief $additionalClasses") { childrenCallback() } + node.dci.kind == ContentKind.Cover -> div("cover $additionalClasses") { + filterButtons(pageContext) + childrenCallback() + } + node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() } + node.hasStyle(TextStyle.Block) -> div(additionalClasses) { childrenCallback() } + else -> childrenCallback() + } + } + + private fun FlowContent.filterButtons(page: ContentPage) { + if (isMultiplatform) { + div(classes = "filter-section") { + id = "filter-section" + page.content.withDescendants().flatMap { it.sourceSets }.distinct().forEach { + button(classes = "platform-tag platform-selector") { + attributes["data-active"] = "" + attributes["data-filter"] = it.sourceSetID.toString() + when (it.analysisPlatform.key) { + "common" -> classes = classes + "common-like" + "native" -> classes = classes + "native-like" + "jvm" -> classes = classes + "jvm-like" + "js" -> classes = classes + "js-like" + } + text(it.displayName) + } + } + } + } + } + + private fun FlowContent.copyButton() = span(classes = "top-right-position") { + span("copy-icon") { + unsafe { + raw( + """<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M5 4H15V16H5V4ZM17 7H19V18V20H17H8V18H17V7Z" fill="black"/> + </svg>""".trimIndent() + ) + } + } + copiedPopup("Content copied to clipboard", "popup-to-left") + } + + private fun FlowContent.copiedPopup(notificationContent: String, additionalClasses: String = "") = + div("copy-popup-wrapper $additionalClasses") { + unsafe { + raw( + """ + <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M18 9C18 14 14 18 9 18C4 18 0 14 0 9C0 4 4 0 9 0C14 0 18 4 18 9ZM14.2 6.2L12.8 4.8L7.5 10.1L5.3 7.8L3.8 9.2L7.5 13L14.2 6.2Z" fill="#4DBB5F"/> + </svg> + """.trimIndent() + ) + } + span { + text(notificationContent) + } + } + + override fun FlowContent.buildPlatformDependent( + content: PlatformHintedContent, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) = + buildPlatformDependent( + content.sourceSets.filter { + sourceSetRestriction == null || it in sourceSetRestriction + }.map { it to setOf(content.inner) }.toMap(), + pageContext, + content.extra, + content.style + ) + + private fun FlowContent.buildPlatformDependent( + nodes: Map<DokkaSourceSet, Collection<ContentNode>>, + pageContext: ContentPage, + extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), + styles: Set<Style> = emptySet() + ) { + val contents = contentsForSourceSetDependent(nodes, pageContext) + val shouldHaveTabs = contents.size != 1 + + val styles = "platform-hinted ${styles.joinToString()}" + if (shouldHaveTabs) " with-platform-tabs" else "" + div(styles) { + attributes["data-platform-hinted"] = "data-platform-hinted" + extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } + if (shouldHaveTabs) { + div("platform-bookmarks-row") { + attributes["data-toggle-list"] = "data-toggle-list" + contents.forEachIndexed { index, pair -> + button(classes = "platform-bookmark") { + attributes["data-filterable-current"] = pair.first.sourceSetID.toString() + attributes["data-filterable-set"] = pair.first.sourceSetID.toString() + if (index == 0) attributes["data-active"] = "" + attributes["data-toggle"] = pair.first.sourceSetID.toString() + when (pair.first.analysisPlatform.key) { + "common" -> classes = classes + "common-like" + "native" -> classes = classes + "native-like" + "jvm" -> classes = classes + "jvm-like" + "js" -> classes = classes + "js-like" + } + attributes["data-toggle"] = pair.first.sourceSetID.toString() + text(pair.first.displayName) + } + } + } + } + contents.forEach { + consumer.onTagContentUnsafe { +it.second } + } + } + } + + private fun contentsForSourceSetDependent( + nodes: Map<DokkaSourceSet, Collection<ContentNode>>, + pageContext: ContentPage, + ): List<Pair<DokkaSourceSet, String>> { + var counter = 0 + return nodes.toList().map { (sourceSet, elements) -> + sourceSet to createHTML(prettyPrint = false).div { + elements.forEach { + buildContentNode(it, pageContext, setOf(sourceSet)) + } + }.stripDiv() + }.groupBy( + Pair<DokkaSourceSet, String>::second, + Pair<DokkaSourceSet, String>::first + ).entries.flatMap { (html, sourceSets) -> + sourceSets.filterNot { + sourceSetDependencyMap[it].orEmpty().any { dependency -> sourceSets.contains(dependency) } + }.map { + it to createHTML(prettyPrint = false).div(classes = "content sourceset-depenent-content") { + if (counter++ == 0) attributes["data-active"] = "" + attributes["data-togglable"] = it.sourceSetID.toString() + unsafe { + +html + } + } + } + } + } + + override fun FlowContent.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) { + + val distinct = + node.groupDivergentInstances(pageContext, { instance, contentPage, sourceSet -> + createHTML(prettyPrint = false).div { + instance.before?.let { before -> + buildContentNode(before, pageContext, setOf(sourceSet)) + } + }.stripDiv() + }, { instance, contentPage, sourceSet -> + createHTML(prettyPrint = false).div { + instance.after?.let { after -> + buildContentNode(after, pageContext, setOf(sourceSet)) + } + }.stripDiv() + }) + + distinct.forEach { + val groupedDivergent = it.value.groupBy { it.second } + + consumer.onTagContentUnsafe { + +createHTML().div("divergent-group") { + attributes["data-filterable-current"] = groupedDivergent.keys.joinToString(" ") { + it.sourceSetID.toString() + } + attributes["data-filterable-set"] = groupedDivergent.keys.joinToString(" ") { + it.sourceSetID.toString() + } + + val divergentForPlatformDependent = groupedDivergent.map { (sourceSet, elements) -> + sourceSet to elements.map { e -> e.first.divergent } + }.toMap() + + val content = contentsForSourceSetDependent(divergentForPlatformDependent, pageContext) + + consumer.onTagContentUnsafe { + +createHTML().div("brief-with-platform-tags") { + consumer.onTagContentUnsafe { + +createHTML().div("inner-brief-with-platform-tags") { + consumer.onTagContentUnsafe { +it.key.first } + } + } + + consumer.onTagContentUnsafe { + +createHTML().span("pull-right") { + if ((distinct.size > 1 && groupedDivergent.size == 1) || groupedDivergent.size == 1 || content.size == 1) { + if (node.sourceSets.size != 1) { + createPlatformTags(node, setOf(content.first().first)) + } + } + } + } + } + } + div("main-subrow") { + if (node.implicitlySourceSetHinted) { + buildPlatformDependent(divergentForPlatformDependent, pageContext) + } else { + it.value.forEach { + buildContentNode(it.first.divergent, pageContext, setOf(it.second)) + } + } + } + consumer.onTagContentUnsafe { +it.key.second } + } + } + } + } + + override fun FlowContent.buildList( + node: ContentList, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) = if (node.ordered) ol { buildListItems(node.children, pageContext, sourceSetRestriction) } + else ul { buildListItems(node.children, pageContext, sourceSetRestriction) } + + open fun OL.buildListItems( + items: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + items.forEach { + if (it is ContentList) + buildList(it, pageContext) + else + li { it.build(this, pageContext, sourceSetRestriction) } + } + } + + open fun UL.buildListItems( + items: List<ContentNode>, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? = null + ) { + items.forEach { + if (it is ContentList) + buildList(it, pageContext) + else + li { it.build(this, pageContext) } + } + } + + override fun FlowContent.buildResource( + node: ContentEmbeddedResource, + pageContext: ContentPage + ) { // TODO: extension point there + val imageExtensions = setOf("png", "jpg", "jpeg", "gif", "bmp", "tif", "webp", "svg") + return if (File(node.address).extension.toLowerCase() in imageExtensions) { + //TODO: add imgAttrs parsing + val imgAttrs = node.extra.allOfType<SimpleAttr>().joinAttr() + img(src = node.address, alt = node.altText) + } else { + println("Unrecognized resource type: $node") + } + } + + private fun FlowContent.buildRow( + node: ContentGroup, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>?, + style: Set<Style> + ) { + node.children + .filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } + .takeIf { it.isNotEmpty() } + ?.let { + val anchorName = node.dci.dri.first().toString() + withAnchor(anchorName) { + div(classes = "table-row") { + if (!style.contains(MultimoduleTable)) { + attributes["data-filterable-current"] = node.sourceSets.joinToString(" ") { + it.sourceSetID.toString() + } + attributes["data-filterable-set"] = node.sourceSets.joinToString(" ") { + it.sourceSetID.toString() + } + } + + it.filterIsInstance<ContentLink>().takeIf { it.isNotEmpty() }?.let { + div("main-subrow " + node.style.joinToString(" ")) { + it.filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } + .forEach { + span { + it.build(this, pageContext, sourceSetRestriction) + buildAnchor(anchorName) + } + if (ContentKind.shouldBePlatformTagged(node.dci.kind) && (node.sourceSets.size == 1)) + createPlatformTags(node) + } + } + } + + it.filter { it !is ContentLink }.takeIf { it.isNotEmpty() }?.let { + div("platform-dependent-row keyValue") { + val title = it.filter { it.style.contains(ContentStyle.RowTitle) } + div { + title.forEach { + it.build(this, pageContext, sourceSetRestriction) + } + } + div("title") { + (it - title).forEach { + it.build(this, pageContext, sourceSetRestriction) + } + } + } + } + } + } + } + } + + private fun FlowContent.createPlatformTagBubbles(sourceSets: List<DokkaSourceSet>) { + if (isMultiplatform) { + div("platform-tags") { + sourceSets.forEach { + div("platform-tag") { + when (it.analysisPlatform.key) { + "common" -> classes = classes + "common-like" + "native" -> classes = classes + "native-like" + "jvm" -> classes = classes + "jvm-like" + "js" -> classes = classes + "js-like" + } + text(it.displayName) + } + } + } + } + } + + private fun FlowContent.createPlatformTags(node: ContentNode, sourceSetRestriction: Set<DokkaSourceSet>? = null) { + node.takeIf { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } }?.let { + createPlatformTagBubbles(node.sourceSets.filter { + sourceSetRestriction == null || it in sourceSetRestriction + }) + } + } + + override fun FlowContent.buildTable( + node: ContentTable, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) { + when (node.dci.kind) { + ContentKind.Comment -> buildDefaultTable(node, pageContext, sourceSetRestriction) + else -> div(classes = "table") { + node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } + node.children.forEach { + buildRow(it, pageContext, sourceSetRestriction, node.style) + } + } + } + + } + + fun FlowContent.buildDefaultTable( + node: ContentTable, + pageContext: ContentPage, + sourceSetRestriction: Set<DokkaSourceSet>? + ) { + table { + thead { + node.header.forEach { + tr { + it.children.forEach { + th { + it.build(this@table, pageContext, sourceSetRestriction) + } + } + } + } + } + tbody { + node.children.forEach { + tr { + it.children.forEach { + td { + it.build(this, pageContext, sourceSetRestriction) + } + } + } + } + } + } + } + + + override fun FlowContent.buildHeader(level: Int, node: ContentHeader, content: FlowContent.() -> Unit) { + val anchor = node.extra[SimpleAttr.SimpleAttrKey("anchor")]?.extraValue + val classes = node.style.joinToString { it.toString() }.toLowerCase() + when (level) { + 1 -> h1(classes = classes) { withAnchor(anchor, content) } + 2 -> h2(classes = classes) { withAnchor(anchor, content) } + 3 -> h3(classes = classes) { withAnchor(anchor, content) } + 4 -> h4(classes = classes) { withAnchor(anchor, content) } + 5 -> h5(classes = classes) { withAnchor(anchor, content) } + else -> h6(classes = classes) { withAnchor(anchor, content) } + } + } + + private fun FlowContent.withAnchor(anchorName: String?, content: FlowContent.() -> Unit) { + a { + anchorName?.let { attributes["data-name"] = it } + } + content() + } + + + override fun FlowContent.buildNavigation(page: PageNode) = + div(classes = "breadcrumbs") { + locationProvider.ancestors(page).asReversed().forEach { node -> + text("/") + if (node.isNavigable) buildLink(node, page) + else text(node.name) + } + } + + private fun FlowContent.buildLink(to: PageNode, from: PageNode) = + buildLink(locationProvider.resolve(to, from)) { + text(to.name) + } + + private fun FlowContent.buildAnchor(pointingTo: String) { + span(classes = "anchor-wrapper") { + span(classes = "anchor-icon") { + attributes["pointing-to"] = pointingTo + unsafe { + raw( + """ + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M21.2496 5.3C20.3496 4.5 19.2496 4 18.0496 4C16.8496 4 15.6496 4.5 14.8496 5.3L10.3496 9.8L11.7496 11.2L16.2496 6.7C17.2496 5.7 18.8496 5.7 19.8496 6.7C20.8496 7.7 20.8496 9.3 19.8496 10.3L15.3496 14.8L16.7496 16.2L21.2496 11.7C22.1496 10.8 22.5496 9.7 22.5496 8.5C22.5496 7.3 22.1496 6.2 21.2496 5.3Z"/> + <path d="M8.35 16.7998C7.35 17.7998 5.75 17.7998 4.75 16.7998C3.75 15.7998 3.75 14.1998 4.75 13.1998L9.25 8.6998L7.85 7.2998L3.35 11.7998C1.55 13.5998 1.55 16.3998 3.35 18.1998C4.25 19.0998 5.35 19.4998 6.55 19.4998C7.75 19.4998 8.85 19.0998 9.75 18.1998L14.25 13.6998L12.85 12.2998L8.35 16.7998Z"/> + </svg> + """.trimIndent() + ) + } + } + copiedPopup("Link copied to clipboard") + } + } + + fun FlowContent.buildLink( + to: DRI, + platforms: List<DokkaSourceSet>, + from: PageNode? = null, + block: FlowContent.() -> Unit + ) = buildLink(locationProvider.resolve(to, platforms.toSet(), from), block) + + override fun buildError(node: ContentNode) { + context.logger.error("Unknown ContentNode type: $node") + } + + override fun FlowContent.buildNewLine() { + br() + } + + override fun FlowContent.buildLink(address: String, content: FlowContent.() -> Unit) = + a(href = address, block = content) + + override fun FlowContent.buildCodeBlock( + code: ContentCodeBlock, + pageContext: ContentPage + ) { + div("sample-container") { + code(code.style.joinToString(" ") { it.toString().toLowerCase() }) { + attributes["theme"] = "idea" + code.children.forEach { buildContentNode(it, pageContext) } + } + } + } + + override fun FlowContent.buildCodeInline( + code: ContentCodeInline, + pageContext: ContentPage + ) { + code { + code.children.forEach { buildContentNode(it, pageContext) } + } + } + + private fun getSymbolSignature(page: ContentPage) = page.content.dfs { it.dci.kind == ContentKind.Symbol } + + private fun flattenToText(node: ContentNode): String { + fun getContentTextNodes(node: ContentNode, sourceSetRestriction: DokkaSourceSet): List<ContentText> = + when (node) { + is ContentText -> listOf(node) + is ContentComposite -> node.children + .filter { sourceSetRestriction in it.sourceSets } + .flatMap { getContentTextNodes(it, sourceSetRestriction) } + .takeIf { node.dci.kind != ContentKind.Annotations } + .orEmpty() + else -> emptyList() + } + + val sourceSetRestriction = + node.sourceSets.find { it.analysisPlatform == Platform.common } ?: node.sourceSets.first() + return getContentTextNodes(node, sourceSetRestriction).joinToString("") { it.text } + } + + override suspend fun renderPage(page: PageNode) { + super.renderPage(page) + if (page is ContentPage && page !is ModulePageNode && page !is PackagePageNode) { + val signature = getSymbolSignature(page) + val textNodes = signature?.let { flattenToText(it) } + val documentable = page.documentable + if (documentable != null) { + listOf( + documentable.dri.packageName, + documentable.dri.classNames, + documentable.dri.callable?.name + ) + .filter { !it.isNullOrEmpty() } + .takeIf { it.isNotEmpty() } + ?.joinToString(".") + ?.let { + pageList.put(it, Pair(textNodes ?: page.name, locationProvider.resolve(page))) + } + + } + } + } + + override fun FlowContent.buildText(textNode: ContentText) { + when { + textNode.hasStyle(TextStyle.Indented) -> consumer.onTagContentEntity(Entities.nbsp) + } + text(textNode.text) + } + + private fun generatePagesList() = + pageList.entries + .filter { !it.key.isNullOrEmpty() } + .groupBy { it.key.substringAfterLast(".") } + .entries + .mapIndexed { topLevelIndex, entry -> + if (entry.value.size > 1) { + listOf( + "{\'name\': \'${entry.key}\', \'index\': \'$topLevelIndex\', \'disabled\': true, \'searchKey\':\'${entry.key}\' }" + ) + entry.value.mapIndexed { index, subentry -> + "{\'name\': \'${subentry.value.first}\', \'level\': 1, \'index\': \'$topLevelIndex.$index\', \'description\':\'${subentry.key}\', \'location\':\'${subentry.value.second}\', 'searchKey':'${entry.key}'}" + } + } else { + val subentry = entry.value.single() + listOf( + "{\'name\': \'${subentry.value.first}\', \'index\': \'$topLevelIndex\', \'description\':\'${subentry.key}\', \'location\':\'${subentry.value.second}\', 'searchKey':'${entry.key}'}" + ) + } + } + .flatten() + .joinToString(prefix = "[", separator = ",\n", postfix = "]") + + override fun render(root: RootPageNode) { + super.render(root) + runBlocking(Dispatchers.Default) { + launch { + outputWriter.write("scripts/pages", "var pages = ${generatePagesList()}", ".js") + } + } + } + + private fun PageNode.root(path: String) = locationProvider.resolveRoot(this) + path + + override fun buildPage(page: ContentPage, content: (FlowContent, ContentPage) -> Unit): String = + buildHtml(page, page.embeddedResources) { + div { + id = "content" + attributes["pageIds"] = page.dri.first().toString() + content(this, page) + } + } + + private fun resolveLink(link: String, page: PageNode): String = if (URI(link).isAbsolute) link else page.root(link) + + open fun buildHtml(page: PageNode, resources: List<String>, content: FlowContent.() -> Unit) = + createHTML().html { + head { + meta(name = "viewport", content = "width=device-width, initial-scale=1", charset = "UTF-8") + title(page.name) + resources.forEach { + when { + it.substringBefore('?').substringAfterLast('.') == "css" -> link( + rel = LinkRel.stylesheet, + href = resolveLink(it, page) + ) + it.substringBefore('?').substringAfterLast('.') == "js" -> script( + type = ScriptType.textJavaScript, + src = resolveLink(it, page) + ) { + async = true + } + else -> unsafe { +it } + } + } + script { unsafe { +"""var pathToRoot = "${locationProvider.resolveRoot(page)}";""" } } + } + body { + div { + id = "container" + div { + id = "leftColumn" + div { + id = "logo" + } + div { + id = "sideMenu" + } + } + div { + id = "main" + div { + id = "searchBar" + } + script(type = ScriptType.textJavaScript, src = page.root("scripts/pages.js")) {} + script(type = ScriptType.textJavaScript, src = page.root("scripts/main.js")) {} + content() + div(classes = "footer") { + span("go-to-top-icon") { + a(href = "#container") { + unsafe { + raw( + """ + <svg width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M11.3337 9.66683H0.666992L6.00033 3.66683L11.3337 9.66683Z" fill="black"/> + <path d="M0.666992 0.333496H11.3337V1.66683H0.666992V0.333496Z" fill="black"/> + </svg> + """.trimIndent() + ) + } + } + } + span { text("© 2020 Copyright") } + span("pull-right") { + span { text("Sponsored and developed by dokka") } + a(href= "https://github.com/Kotlin/dokka") { + span(classes = "padded-icon") { + unsafe { + raw( + """ + <svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M8 0H2.3949L4.84076 2.44586L0 7.28662L0.713376 8L5.55414 3.15924L8 5.6051V0Z" fill="black"/> + </svg> + """.trimIndent() + ) + } + } + } + } + } + } + } + } + } +} + +fun List<SimpleAttr>.joinAttr() = joinToString(" ") { it.extraKey + "=" + it.extraValue } + +private fun String.stripDiv() = drop(5).dropLast(6) // TODO: Find a way to do it without arbitrary trims + +private val PageNode.isNavigable: Boolean + get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing + +fun PropertyContainer<ContentNode>.extraHtmlAttributes() = allOfType<SimpleAttr>() diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt new file mode 100644 index 00000000..8fe2a6c8 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt @@ -0,0 +1,51 @@ +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.RendererSpecificPage +import org.jetbrains.dokka.pages.RenderingStrategy + +class NavigationPage(val root: NavigationNode) : RendererSpecificPage { + override val name = "navigation" + + override val children = emptyList<PageNode>() + + override fun modified(name: String, children: List<PageNode>) = this + + override val strategy = RenderingStrategy<HtmlRenderer> { + createHTML().visit(root, "nav-submenu", this) + } + + private fun <R> TagConsumer<R>.visit(node: NavigationNode, navId: String, renderer: HtmlRenderer): R = + with(renderer) { + div("sideMenuPart") { + id = navId + attributes["pageId"] = node.dri.toString() + div("overview") { + buildLink(node.dri, node.sourceSets.toList()) { +node.name } + if (node.children.isNotEmpty()) { + span("navButton") { + onClick = """document.getElementById("$navId").classList.toggle("hidden");""" + span("navButtonContent") + } + } + } + node.children.withIndex().forEach { (n, p) -> visit(p, "$navId-$n", renderer) } + } + } +} + +class NavigationNode( + val name: String, + val dri: DRI, + val sourceSets: Set<DokkaSourceSet>, + val children: List<NavigationNode> +) + +fun NavigationPage.transform(block: (NavigationNode) -> NavigationNode) = NavigationPage(root.transform(block)) + +fun NavigationNode.transform(block: (NavigationNode) -> NavigationNode) = + run(block).let { NavigationNode(it.name, it.dri, it.sourceSets, it.children.map(block)) } diff --git a/plugins/base/src/main/kotlin/renderers/html/Tags.kt b/plugins/base/src/main/kotlin/renderers/html/Tags.kt new file mode 100644 index 00000000..a3951eff --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/Tags.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.* + +@HtmlTagMarker +fun FlowOrPhrasingContent.wbr(classes : String? = null, block : WBR.() -> Unit = {}) : Unit = WBR(attributesMapOf("class", classes), consumer).visit(block) + +@Suppress("unused") +open class WBR(initialAttributes : Map<String, String>, override val consumer : TagConsumer<*>) : HTMLTag("wbr", consumer, initialAttributes, null, true, false), + HtmlBlockInlineTag { + +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt new file mode 100644 index 00000000..2f7c8ee1 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt @@ -0,0 +1,112 @@ +package org.jetbrains.dokka.base.renderers.html + +import kotlinx.html.h1 +import kotlinx.html.id +import kotlinx.html.table +import kotlinx.html.tbody +import org.jetbrains.dokka.base.renderers.sourceSets +import org.jetbrains.dokka.model.DEnum +import org.jetbrains.dokka.model.DEnumEntry +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer + + +object SearchPageInstaller : PageTransformer { + override fun invoke(input: RootPageNode) = input.modified(children = input.children + searchPage) + + private val searchPage = RendererSpecificResourcePage( + name = "Search", + children = emptyList(), + strategy = RenderingStrategy<HtmlRenderer> { + buildHtml(it, listOf("styles/style.css", "scripts/pages.js", "scripts/search.js")) { + h1 { + id = "searchTitle" + text("Search results for ") + } + table { + tbody { + id = "searchTable" + } + } + } + }) +} + +object NavigationPageInstaller : PageTransformer { + override fun invoke(input: RootPageNode) = input.modified( + children = input.children + NavigationPage( + input.children.filterIsInstance<ContentPage>().single() + .let(NavigationPageInstaller::visit) + ) + ) + + private fun visit(page: ContentPage): NavigationNode = + NavigationNode( + page.name, + page.dri.first(), + page.sourceSets(), + page.navigableChildren() + ) + + private fun ContentPage.navigableChildren(): List<NavigationNode> { + if (this !is ClasslikePageNode) { + return children.filterIsInstance<ContentPage>() + .map { visit(it) } + } else if (documentable is DEnum) { + return children.filter { it is ContentPage && it.documentable is DEnumEntry } + .map { visit(it as ContentPage) } + } + + return emptyList() + } +} + +object ResourceInstaller : PageTransformer { + override fun invoke(input: RootPageNode) = input.modified(children = input.children + resourcePages) + + private val resourcePages = listOf("styles", "scripts", "images").map { + RendererSpecificResourcePage(it, emptyList(), RenderingStrategy.Copy("/dokka/$it")) + } +} + +object StyleAndScriptsAppender : PageTransformer { + override fun invoke(input: RootPageNode) = input.transformContentPagesTree { + it.modified( + embeddedResources = it.embeddedResources + listOf( + "styles/style.css", + "scripts/navigationLoader.js", + "scripts/platformContentHandler.js", + "scripts/sourceset_dependencies.js", + "scripts/clipboard.js", + "styles/jetbrains-mono.css" + ) + ) + } +} + +class SourcesetDependencyAppender(val context: DokkaContext) : PageTransformer { + override fun invoke(input: RootPageNode): RootPageNode { + val dependenciesMap = context.configuration.sourceSets.map { + it.sourceSetID to it.dependentSourceSets + }.toMap() + + fun createDependenciesJson(): String = "sourceset_dependencies = '{${ + dependenciesMap.entries.joinToString(", ") { + "\"${it.key}\": [${it.value.joinToString(",") { + "\"$it\"" + }}]" + } + }}'" + + val deps = RendererSpecificResourcePage( + name = "scripts/sourceset_dependencies.js", + children = emptyList(), + strategy = RenderingStrategy.Write(createDependenciesJson()) + ) + + return input.modified( + children = input.children + deps + ) + } +} diff --git a/plugins/base/src/main/kotlin/renderers/preprocessors.kt b/plugins/base/src/main/kotlin/renderers/preprocessors.kt new file mode 100644 index 00000000..bf2a9eb4 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/preprocessors.kt @@ -0,0 +1,27 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer + +object RootCreator : PageTransformer { + override fun invoke(input: RootPageNode) = + RendererSpecificRootPage("", listOf(input), RenderingStrategy.DoNothing) +} + + +class PackageListCreator(val context: DokkaContext, val format: String, val linkExtension: String) : PageTransformer { + override fun invoke(input: RootPageNode) = + input.modified(children = input.children.map { + it.takeUnless { it is ModulePageNode } + ?: it.modified(children = it.children + packageList(input)) // TODO packageList should take module as an input + }) + + + private fun packageList(pageNode: RootPageNode) = + RendererSpecificResourcePage( + "${pageNode.name}/package-list", + emptyList(), + RenderingStrategy.Write(PackageListService(context).formatPackageList(pageNode, format, linkExtension)) + ) +} |