diff options
Diffstat (limited to 'plugins')
7 files changed, 620 insertions, 0 deletions
diff --git a/plugins/base/build.gradle.kts b/plugins/base/build.gradle.kts index ba0b5753..b42d7e28 100644 --- a/plugins/base/build.gradle.kts +++ b/plugins/base/build.gradle.kts @@ -7,3 +7,7 @@ publishing { } } +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.10") +} + diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt index 34c0ffae..bafd30ff 100644 --- a/plugins/base/src/main/kotlin/DokkaBase.kt +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -4,13 +4,19 @@ import org.jetbrains.dokka.CoreExtensions import org.jetbrains.dokka.base.transformers.descriptors.DefaultDescriptorToDocumentationTranslator import org.jetbrains.dokka.base.transformers.documentables.DefaultDocumentableMerger import org.jetbrains.dokka.base.transformers.documentables.DefaultDocumentablesToPageTranslator +import org.jetbrains.dokka.base.transformers.psi.DefaultPsiToDocumentationTranslator import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.renderers.html.HtmlRenderer class DokkaBase: DokkaPlugin() { val descriptorToDocumentationTranslator by extending(isFallback = true) { CoreExtensions.descriptorToDocumentationTranslator providing ::DefaultDescriptorToDocumentationTranslator } + val psiToDocumentationTranslator by extending(isFallback = true) { + CoreExtensions.psiToDocumentationTranslator with DefaultPsiToDocumentationTranslator + } + val documentableMerger by extending(isFallback = true) { CoreExtensions.documentableMerger with DefaultDocumentableMerger } @@ -18,4 +24,8 @@ class DokkaBase: DokkaPlugin() { val documentablesToPageTranslator by extending(isFallback = true) { CoreExtensions.documentablesToPageTranslator with DefaultDocumentablesToPageTranslator } + + val htmlRenderer by extending { + CoreExtensions.renderer providing ::HtmlRenderer + } }
\ No newline at end of file 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..c6183cf3 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt @@ -0,0 +1,127 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.renderers.OutputWriter +import org.jetbrains.dokka.renderers.Renderer +import org.jetbrains.dokka.resolvers.LocationProvider +import org.jetbrains.dokka.transformers.pages.PageNodeTransformer + +abstract class DefaultRenderer<T>( + protected val context: DokkaContext +) : Renderer { + + protected val outputWriter = context.single(CoreExtensions.outputWriter) + + protected lateinit var locationProvider: LocationProvider + private set + + protected open val preprocessors: Iterable<PageNodeTransformer> = emptyList() + + abstract fun T.buildHeader(level: Int, content: T.() -> Unit) + abstract fun T.buildLink(address: String, content: T.() -> Unit) + abstract fun T.buildList(node: ContentList, pageContext: ContentPage) + abstract fun T.buildNewLine() + abstract fun T.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) + abstract fun T.buildTable(node: ContentTable, pageContext: ContentPage) + 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.buildGroup(node: ContentGroup, pageContext: ContentPage) { + node.children.forEach { it.build(this, pageContext) } + } + + open fun T.buildLinkText(nodes: List<ContentNode>, pageContext: ContentPage) { + nodes.forEach { it.build(this, pageContext) } + } + + open fun T.buildCode(code: List<ContentNode>, language: String, pageContext: ContentPage) { + code.forEach { it.build(this, pageContext) } + } + + open fun T.buildHeader(node: ContentHeader, pageContext: ContentPage) { + buildHeader(node.level) { node.children.forEach { it.build(this, pageContext) } } + } + + open fun ContentNode.build(builder: T, pageContext: ContentPage) = + builder.buildContentNode(this, pageContext) + + open fun T.buildContentNode(node: ContentNode, pageContext: ContentPage) { + when (node) { + is ContentText -> buildText(node) + is ContentHeader -> buildHeader(node, pageContext) + is ContentCode -> buildCode(node.children, node.language, pageContext) + is ContentDRILink -> buildLink( + locationProvider.resolve(node.address, node.platforms.toList(), pageContext) + ) { + buildLinkText(node.children, pageContext) + } + is ContentResolvedLink -> buildLink(node.address) { buildLinkText(node.children, pageContext) } + is ContentEmbeddedResource -> buildResource(node, pageContext) + is ContentList -> buildList(node, pageContext) + is ContentTable -> buildTable(node, pageContext) + is ContentGroup -> buildGroup(node, pageContext) + else -> buildError(node) + } + } + + open fun buildPageContent(context: T, page: ContentPage) { + context.buildNavigation(page) + page.content.build(context, page) + } + + open 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" + ) + } + } + + open fun renderPages(root: PageNode) { + renderPage(root) + root.children.forEach { renderPages(it) } + } + + // reimplement this as preprocessor + open fun renderPackageList(root: ContentPage) = + getPackageNamesAndPlatforms(root) + .keys + .joinToString("\n") + .also { outputWriter.write("${root.name}/package-list", it, "") } + + open fun getPackageNamesAndPlatforms(root: PageNode): Map<String, List<PlatformData>> = + root.children + .map(::getPackageNamesAndPlatforms) + .fold(emptyMap<String, List<PlatformData>>()) { e, acc -> acc + e } + + if (root is PackagePageNode) { + mapOf(root.name to root.platforms()) + } else { + emptyMap() + } + + override fun render(root: RootPageNode) { + val newRoot = preprocessors.fold(root) { acc, t -> t(acc) } + + locationProvider = + context.single(CoreExtensions.locationProviderFactory).getLocationProvider(newRoot) + + root.children<ModulePageNode>().forEach { renderPackageList(it) } + + renderPages(newRoot) + } +} + +fun ContentPage.platforms() = this.content.platforms.toList()
\ 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..c9270681 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -0,0 +1,216 @@ +package org.jetbrains.dokka.renderers.html + +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.base.renderers.DefaultRenderer +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.Function +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.renderers.OutputWriter +import java.io.File + +open class HtmlRenderer( + context: DokkaContext +) : DefaultRenderer<FlowContent>(context) { + + private val pageList = mutableListOf<String>() + + override val preprocessors = listOf( + RootCreator, + SearchPageInstaller, + ResourceInstaller, + NavigationPageInstaller, + StyleAndScriptsAppender + ) + + override fun FlowContent.buildList(node: ContentList, pageContext: ContentPage) = + if (node.ordered) ol { + buildListItems(node.children, pageContext) + } + else ul { + buildListItems(node.children, pageContext) + } + + open fun OL.buildListItems(items: List<ContentNode>, pageContext: ContentPage) { + items.forEach { + if (it is ContentList) + buildList(it, pageContext) + else + li { it.build(this, pageContext) } + } + } + + open fun UL.buildListItems(items: List<ContentNode>, pageContext: ContentPage) { + 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.extras.filterIsInstance<HTMLSimpleAttr>().joinAttr() + img(src = node.address, alt = node.altText) + } else { + println("Unrecognized resource type: $node") + } + } + + override fun FlowContent.buildTable(node: ContentTable, pageContext: ContentPage) { + table { + thead { + node.header.forEach { + tr { + it.children.forEach { + th { + it.build(this@table, pageContext) + } + } + } + } + } + tbody { + node.children.forEach { + tr { + it.children.forEach { + td { + it.build(this, pageContext) + } + } + } + } + } + } + } + + override fun FlowContent.buildHeader(level: Int, content: FlowContent.() -> Unit) { + when (level) { + 1 -> h1(block = content) + 2 -> h2(block = content) + 3 -> h3(block = content) + 4 -> h4(block = content) + 5 -> h5(block = content) + else -> h6(block = content) + } + } + + override fun FlowContent.buildNavigation(page: PageNode) = + 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) + } + + fun FlowContent.buildLink( + to: DRI, + platforms: List<PlatformData>, + from: PageNode? = null, + block: FlowContent.() -> Unit + ) = buildLink(locationProvider.resolve(to, platforms, 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.buildCode(code: List<ContentNode>, language: String, pageContext: ContentPage) { + buildNewLine() + code.forEach { + +((it as? ContentText)?.text ?: run { context.logger.error("Cannot cast $it as ContentText!"); "" }) + buildNewLine() + } + } + + override fun renderPage(page: PageNode) { + super.renderPage(page) + if (page is ContentPage) { + pageList.add( + """{ "name": "${page.name}", ${if (page is ClasslikePageNode) "\"class\": \"${page.name}\"," else ""} "location": "${locationProvider.resolve( + page + )}" }""" + ) + } + } + + override fun FlowContent.buildText(textNode: ContentText) { + text(textNode.text) + } + + override fun render(root: RootPageNode) { + super.render(root) + outputWriter.write("scripts/pages", "var pages = [\n${pageList.joinToString(",\n")}\n]", ".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) { content(this, page) } + + open fun buildHtml(page: PageNode, resources: List<String>, content: FlowContent.() -> Unit) = + createHTML().html { + head { + title(page.name) + with(resources) { + filter { it.substringBefore('?').substringAfterLast('.') == "css" } + .forEach { link(rel = LinkRel.stylesheet, href = page.root(it)) } + filter { it.substringBefore('?').substringAfterLast('.') == "js" } + .forEach { script(type = ScriptType.textJavaScript, src = page.root(it)) { async = true } } + } + script { unsafe { +"""var pathToRoot = "${locationProvider.resolveRoot(page)}";""" } } + } + body { + div { + id = "navigation" + div { + id = "searchBar" + form(action = page.root("-search.html"), method = FormMethod.get) { + id = "searchForm" + input(type = InputType.search, name = "query") + input(type = InputType.submit) { value = "Search" } + } + } + div { + id = "sideMenu" + } + } + div { + id = "content" + content() + } + } + } +} + +fun List<HTMLMetadata>.joinAttr() = joinToString(" ") { it.key + "=" + it.value } + +private fun PageNode.pageKind() = when (this) { + is PackagePageNode -> "package" + is ClasslikePageNode -> "class" + is MemberPageNode -> when (this.documentable) { + is Function -> "function" + else -> "other" + } + else -> "other" +} + +private val PageNode.isNavigable: Boolean + get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing
\ No newline at end of file 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..4a2fb40d --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt @@ -0,0 +1,50 @@ +package org.jetbrains.dokka.renderers.html + +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.PlatformData +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 + div("overview") { + buildLink(node.dri, node.platforms) { +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 platforms: List<PlatformData>, + 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.platforms, it.children.map(block)) } 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..09164d97 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt @@ -0,0 +1,68 @@ +package org.jetbrains.dokka.renderers.html + +import kotlinx.html.h1 +import kotlinx.html.id +import kotlinx.html.table +import kotlinx.html.tbody +import org.jetbrains.dokka.base.renderers.platforms +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.transformers.pages.PageNodeTransformer + +object RootCreator : PageNodeTransformer { + override fun invoke(input: RootPageNode) = + RendererSpecificRootPage("", listOf(input), RenderingStrategy.DoNothing) +} + +object SearchPageInstaller : PageNodeTransformer { + 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")) { + h1 { + id = "searchTitle" + text("Search results for ") + } + table { + tbody { + id = "searchTable" + } + } + } + }) +} + +object NavigationPageInstaller : PageNodeTransformer { + override fun invoke(input: RootPageNode) = input.modified( + children = input.children + NavigationPage( + input.children.filterIsInstance<ContentPage>().single().let(::visit) + ) + ) + + private fun visit(page: ContentPage): NavigationNode = NavigationNode( + page.name, + page.dri.first(), + page.platforms(), + page.children.filterIsInstance<ContentPage>().map { visit(it) }) +} + +object ResourceInstaller : PageNodeTransformer { + 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 : PageNodeTransformer { + override fun invoke(input: RootPageNode) = input.transformContentPagesTree { + it.modified( + embeddedResources = it.embeddedResources + listOf( + "styles/style.css", + "scripts/navigationLoader.js" + ) + ) + } +} diff --git a/plugins/base/src/main/kotlin/transformers/psi/DefaultPsiToDocumentationTranslator.kt b/plugins/base/src/main/kotlin/transformers/psi/DefaultPsiToDocumentationTranslator.kt new file mode 100644 index 00000000..b913ae88 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/psi/DefaultPsiToDocumentationTranslator.kt @@ -0,0 +1,145 @@ +package org.jetbrains.dokka.base.transformers.psi + +import com.intellij.psi.* +import org.jetbrains.dokka.JavadocParser +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.JavaClassReference +import org.jetbrains.dokka.links.withClass +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.Function +import org.jetbrains.dokka.pages.PlatformData +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.psi.PsiToDocumentationTranslator +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.descriptors.Visibilities + +object DefaultPsiToDocumentationTranslator : PsiToDocumentationTranslator { + + override fun invoke( + moduleName: String, + psiFiles: List<PsiJavaFile>, + platformData: PlatformData, + context: DokkaContext + ): Module { + val docParser = + DokkaPsiParser( + platformData, + context.logger + ) + return Module(moduleName, + psiFiles.map { psiFile -> + val dri = DRI(packageName = psiFile.packageName) + Package( + dri, + emptyList(), + emptyList(), + psiFile.classes.map { docParser.parseClass(it, dri) } + ) + } + ) + } + + class DokkaPsiParser( + private val platformData: PlatformData, + logger: DokkaLogger + ) { + + private val javadocParser: JavadocParser = JavadocParser(logger) + + private fun getComment(psi: PsiNamedElement): List<PlatformInfo> { + val comment = javadocParser.parseDocumentation(psi) + return listOf(BasePlatformInfo(comment, listOf(platformData))) + } + + private fun PsiModifierListOwner.getVisibility() = modifierList?.children?.toList()?.let { ml -> + when { + ml.any { it.text == PsiKeyword.PUBLIC } -> Visibilities.PUBLIC + ml.any { it.text == PsiKeyword.PROTECTED } -> Visibilities.PROTECTED + else -> Visibilities.PRIVATE + } + } ?: Visibilities.PRIVATE + + fun parseClass(psi: PsiClass, parent: DRI): Class = with(psi) { + val kind = when { + isAnnotationType -> JavaClassKindTypes.ANNOTATION_CLASS + isInterface -> JavaClassKindTypes.INTERFACE + isEnum -> JavaClassKindTypes.ENUM_CLASS + else -> JavaClassKindTypes.CLASS + } + val dri = parent.withClass(name.toString()) + /*superTypes.filter { !ignoreSupertype(it) }.forEach { + node.appendType(it, NodeKind.Supertype) + val superClass = it.resolve() + if (superClass != null) { + link(superClass, node, RefKind.Inheritor) + } + }*/ + val inherited = emptyList<DRI>() //listOf(psi.superClass) + psi.interfaces // TODO DRIs of inherited + val actual = getComment(psi).map { ClassPlatformInfo(it, inherited) } + + return Class( + dri = dri, + name = name.orEmpty(), + kind = kind, + constructors = constructors.map { parseFunction(it, dri, true) }, + functions = methods.mapNotNull { if (!it.isConstructor) parseFunction(it, dri) else null }, + properties = fields.mapNotNull { parseField(it, dri) }, + classlikes = innerClasses.map { parseClass(it, dri) }, + expected = null, + actual = actual, + extra = mutableSetOf(), + visibility = mapOf(platformData to psi.getVisibility()) + ) + } + + private fun parseFunction(psi: PsiMethod, parent: DRI, isConstructor: Boolean = false): Function { + val dri = parent.copy(callable = Callable( + psi.name, + JavaClassReference(psi.containingClass?.name.orEmpty()), + psi.parameterList.parameters.map { parameter -> + JavaClassReference(parameter.type.canonicalText) + } + ) + ) + return Function( + dri, + if (isConstructor) "<init>" else psi.name, + psi.returnType?.let { JavaTypeWrapper(type = it) }, + isConstructor, + null, + psi.parameterList.parameters.mapIndexed { index, psiParameter -> + Parameter( + dri.copy(target = index + 1), + psiParameter.name, + JavaTypeWrapper(psiParameter.type), + null, + getComment(psi) + ) + }, + null, + getComment(psi), + visibility = mapOf(platformData to psi.getVisibility()) + ) + } + + private fun parseField(psi: PsiField, parent: DRI): Property { + val dri = parent.copy( + callable = Callable( + psi.name!!, // TODO: Investigate if this is indeed nullable + JavaClassReference(psi.containingClass?.name.orEmpty()), + emptyList() + ) + ) + return Property( + dri, + psi.name!!, // TODO: Investigate if this is indeed nullable + null, + null, + getComment(psi), + accessors = emptyList(), + visibility = mapOf(platformData to psi.getVisibility()) + ) + } + } +} |