package org.jetbrains.dokka.Formats import com.google.inject.Inject import com.google.inject.name.Named import org.jetbrains.dokka.* import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatOutputBuilder.Page import org.jetbrains.dokka.NodeKind.Companion.classLike import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult import java.io.BufferedWriter import java.io.File import java.net.URI class JavaLayoutHtmlFormatGenerator @Inject constructor( @Named("outputDir") val root: File, val packageListService: PackageListService, val outputBuilderFactoryService: JavaLayoutHtmlFormatOutputBuilderFactory, private val options: DocumentationOptions, val logger: DokkaLogger, @Named("outlineRoot") val outlineRoot: String ) : Generator, JavaLayoutHtmlUriProvider { @set:Inject(optional = true) var outlineFactoryService: JavaLayoutHtmlFormatOutlineFactoryService? = null fun createOutputBuilderForNode(node: DocumentationNode, output: Appendable) = outputBuilderFactoryService.createOutputBuilder(output, node) fun DocumentationNode.getOwnerOrReport() = owner ?: run { error("Owner not found for $this") } override fun tryGetContainerUri(node: DocumentationNode): URI? { return when (node.kind) { NodeKind.Module -> URI("/").resolve(node.name + "/") NodeKind.Package -> tryGetContainerUri(node.getOwnerOrReport())?.resolve(node.name.replace('.', '/') + '/') NodeKind.GroupNode -> tryGetContainerUri(node.getOwnerOrReport()) in NodeKind.classLike -> tryGetContainerUri(node.getOwnerOrReport())?.resolve("${node.classNodeNameWithOuterClass()}.html") else -> null } } override fun tryGetMainUri(node: DocumentationNode): URI? { return when (node.kind) { NodeKind.Package -> tryGetContainerUri(node)?.resolve("package-summary.html") in NodeKind.classLike -> tryGetContainerUri(node)?.resolve("#") in NodeKind.memberLike -> { val owner = if (node.owner?.kind != NodeKind.ExternalClass) node.owner else node.owner?.owner if (owner!!.kind in classLike && (node.kind == NodeKind.CompanionObjectProperty || node.kind == NodeKind.CompanionObjectFunction) && owner.companion != null ) { val signature = node.detail(NodeKind.Signature) val originalFunction = owner.companion!!.members.first { it.detailOrNull(NodeKind.Signature)?.name == signature.name } tryGetMainUri(owner.companion!!)?.resolveInPage(originalFunction) } else { tryGetMainUri(owner)?.resolveInPage(node) } } NodeKind.TypeParameter, NodeKind.Parameter -> node.path.asReversed().drop(1).firstNotNullResult(this::tryGetMainUri)?.resolveInPage(node) NodeKind.AllTypes -> outlineRootUri(node).resolve ("classes.html") else -> null } } override fun tryGetOutlineRootUri(node: DocumentationNode): URI? { return when(node.kind) { NodeKind.AllTypes -> tryGetContainerUri(node.getOwnerOrReport()) else -> tryGetContainerUri(node) }?.resolve(outlineRoot) } fun URI.resolveInPage(node: DocumentationNode): URI = resolve("#${node.signatureForAnchor(logger).anchorEncoded()}") fun buildClass(node: DocumentationNode, parentDir: File) { val fileForClass = parentDir.resolve(node.classNodeNameWithOuterClass() + ".html") fileForClass.bufferedWriter().use { createOutputBuilderForNode(node, it).generatePage(Page.ClassPage(node)) } for (memberClass in node.members.filter { it.kind in NodeKind.classLike }) { buildClass(memberClass, parentDir) } } fun buildPackage(node: DocumentationNode, parentDir: File) { assert(node.kind == NodeKind.Package) var members = node.members val directoryForPackage = parentDir.resolve(node.name.replace('.', File.separatorChar)) directoryForPackage.mkdirsOrFail() directoryForPackage.resolve("package-summary.html").bufferedWriter().use { createOutputBuilderForNode(node, it).generatePage(Page.PackagePage(node)) } members.filter { it.kind == NodeKind.GroupNode }.forEach { members += it.members } members.filter { it.kind in NodeKind.classLike }.forEach { buildClass(it, directoryForPackage) } } fun buildClassIndex(node: DocumentationNode, parentDir: File) { val file = parentDir.resolve("classes.html") file.bufferedWriter().use { createOutputBuilderForNode(node, it).generatePage(Page.ClassIndex(node)) } } fun buildPackageIndex(module: DocumentationNode, nodes: List, parentDir: File) { val file = parentDir.resolve("packages.html") file.bufferedWriter().use { val uri = outlineRootUri(module).resolve("packages.html") outputBuilderFactoryService.createOutputBuilder(it, uri) .generatePage(Page.PackageIndex(nodes)) } } override fun buildPages(nodes: Iterable) { val module = nodes.single() val moduleRoot = root.resolve(module.name) val packages = module.members.filter { it.kind == NodeKind.Package } packages.forEach { buildPackage(it, moduleRoot) } val outlineRootFile = moduleRoot.resolve(outlineRoot) if (options.generateClassIndexPage) { buildClassIndex(module.members.single { it.kind == NodeKind.AllTypes }, outlineRootFile) } if (options.generatePackageIndexPage) { buildPackageIndex(module, packages, outlineRootFile) } } override fun buildOutlines(nodes: Iterable) { val uriToWriter = mutableMapOf() fun provideOutput(uri: URI): BufferedWriter { val normalized = uri.normalize() uriToWriter[normalized]?.let { return it } val file = root.resolve(normalized.path.removePrefix("/")) file.parentFile.mkdirsOrFail() val writer = file.bufferedWriter() uriToWriter[normalized] = writer return writer } outlineFactoryService?.generateOutlines(::provideOutput, nodes) uriToWriter.values.forEach { it.close() } } override fun buildSupportFiles() {} override fun buildPackageList(nodes: Iterable) { nodes.filter { it.kind == NodeKind.Module }.forEach { module -> val moduleRoot = root.resolve(module.name) val packageListFile = moduleRoot.resolve("package-list") packageListFile.writeText(packageListService.formatPackageList(module as DocumentationModule)) } } } interface JavaLayoutHtmlFormatOutputBuilderFactory { fun createOutputBuilder(output: Appendable, uri: URI): JavaLayoutHtmlFormatOutputBuilder fun createOutputBuilder(output: Appendable, node: DocumentationNode): JavaLayoutHtmlFormatOutputBuilder }