blob: 6dccb06ba9ed7053256a36b5d582853bab43d1fa [file] [log] [blame]
package org.jetbrains.dokka.Formats
import com.google.inject.Binder
import com.google.inject.Inject
import com.google.inject.name.Named
import kotlinx.html.*
import kotlinx.html.Entities.nbsp
import kotlinx.html.stream.appendHTML
import org.jetbrains.dokka.*
import org.jetbrains.dokka.LanguageService.RenderMode.FULL
import org.jetbrains.dokka.LanguageService.RenderMode.SUMMARY
import org.jetbrains.dokka.NodeKind.Companion.classLike
import org.jetbrains.dokka.NodeKind.Companion.memberLike
import org.jetbrains.dokka.Utilities.bind
import org.jetbrains.dokka.Utilities.lazyBind
import org.jetbrains.dokka.Utilities.toOptional
import org.jetbrains.dokka.Utilities.toType
import org.jetbrains.kotlin.preprocessor.mkdirsOrFail
import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult
import java.io.BufferedWriter
import java.io.File
import java.net.URI
import java.net.URLEncoder
import kotlin.reflect.KClass
abstract class JavaLayoutHtmlFormatDescriptorBase : FormatDescriptor, DefaultAnalysisComponent {
override fun configureOutput(binder: Binder): Unit = with(binder) {
bind<Generator>() toType generatorServiceClass
bind<LanguageService>() toType languageServiceClass
bind<JavaLayoutHtmlTemplateService>() toType templateServiceClass
bind<JavaLayoutHtmlUriProvider>() toType generatorServiceClass
lazyBind<JavaLayoutHtmlFormatOutlineFactoryService>() toOptional outlineFactoryClass
}
val generatorServiceClass = JavaLayoutHtmlFormatGenerator::class
abstract val languageServiceClass: KClass<out LanguageService>
abstract val templateServiceClass: KClass<out JavaLayoutHtmlTemplateService>
abstract val outlineFactoryClass: KClass<out JavaLayoutHtmlFormatOutlineFactoryService>?
}
class JavaLayoutHtmlFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin {
override val languageServiceClass = KotlinLanguageService::class
override val templateServiceClass = JavaLayoutHtmlTemplateService.Default::class
override val outlineFactoryClass = null
}
interface JavaLayoutHtmlTemplateService {
fun composePage(
nodes: List<DocumentationNode>,
tagConsumer: TagConsumer<Appendable>,
headContent: HEAD.() -> Unit,
bodyContent: BODY.() -> Unit
)
class Default : JavaLayoutHtmlTemplateService {
override fun composePage(
nodes: List<DocumentationNode>,
tagConsumer: TagConsumer<Appendable>,
headContent: HEAD.() -> Unit,
bodyContent: BODY.() -> Unit
) {
tagConsumer.html {
head(headContent)
body(block = bodyContent)
}
}
}
}
interface JavaLayoutHtmlFormatOutlineFactoryService {
fun generateOutlines(outputProvider: (URI) -> Appendable, nodes: Iterable<DocumentationNode>)
}
class JavaLayoutHtmlFormatOutputBuilder(
val output: Appendable,
val languageService: LanguageService,
val uriProvider: JavaLayoutHtmlUriProvider,
val templateService: JavaLayoutHtmlTemplateService,
val logger: DokkaLogger,
val uri: URI
) {
val htmlConsumer = output.appendHTML()
val contentToHtmlBuilder = ContentToHtmlBuilder(uriProvider, uri)
private fun <T> FlowContent.summaryNodeGroup(nodes: Iterable<T>, header: String, headerAsRow: Boolean = false, row: TBODY.(T) -> Unit) {
if (nodes.none()) return
if (!headerAsRow) {
h2 { +header }
}
table {
if (headerAsRow) thead { tr { td { h3 { +header } } } }
tbody {
nodes.forEach { node ->
row(node)
}
}
}
}
fun FlowContent.metaMarkup(content: ContentNode) = with(contentToHtmlBuilder) {
appendContent(content)
}
fun FlowContent.metaMarkup(content: List<ContentNode>) = with(contentToHtmlBuilder) {
appendContent(content)
}
private fun TBODY.formatClassLikeRow(node: DocumentationNode) = tr {
td { a(href = uriProvider.linkTo(node, uri)) { +node.simpleName() } }
td { metaMarkup(node.summary) }
}
private fun TBODY.functionSummaryRow(node: DocumentationNode) = tr {
td {
for (modifier in node.details(NodeKind.Modifier)) {
renderedSignature(modifier, SUMMARY)
}
renderedSignature(node.detail(NodeKind.Type), SUMMARY)
}
td {
div {
a(href = uriProvider.linkTo(node, uri)) { +node.name }
}
metaMarkup(node.summary)
}
}
private fun TBODY.formatInheritRow(entry: Map.Entry<DocumentationNode, List<DocumentationNode>>) = tr {
td {
val (from, nodes) = entry
+"From class "
a(href = uriProvider.linkTo(from.owner!!, uri)) { +from.qualifiedName() }
table {
tbody {
for (node in nodes) {
functionSummaryRow(node)
}
}
}
}
}
private fun FlowContent.renderedSignature(node: DocumentationNode, mode: LanguageService.RenderMode = SUMMARY) {
metaMarkup(languageService.render(node, mode))
}
private fun FlowContent.fullFunctionDocs(node: DocumentationNode) {
div {
id = node.signatureForAnchor(logger)
h3 { +node.name }
pre { renderedSignature(node, FULL) }
metaMarkup(node.content)
for ((name, sections) in node.content.sections.groupBy { it.tag }) {
table {
thead { tr { td { h3 { +name } } } }
tbody {
sections.forEach {
tr {
td { it.subjectName?.let { +it } }
td {
metaMarkup(it.children)
}
}
}
}
}
}
}
}
private fun FlowContent.fullPropertyDocs(node: DocumentationNode) {
fullFunctionDocs(node)
}
fun appendPackage(node: DocumentationNode) = templateService.composePage(
listOf(node),
htmlConsumer,
headContent = {
},
bodyContent = {
h1 { +node.name }
metaMarkup(node.content)
summaryNodeGroup(node.members(NodeKind.Class), "Classes") { formatClassLikeRow(it) }
summaryNodeGroup(node.members(NodeKind.Exception), "Exceptions") { formatClassLikeRow(it) }
summaryNodeGroup(node.members(NodeKind.TypeAlias), "Type-aliases") { formatClassLikeRow(it) }
summaryNodeGroup(node.members(NodeKind.AnnotationClass), "Annotations") { formatClassLikeRow(it) }
summaryNodeGroup(node.members(NodeKind.Enum), "Enums") { formatClassLikeRow(it) }
summaryNodeGroup(node.members(NodeKind.Function), "Top-level functions summary") { functionSummaryRow(it) }
summaryNodeGroup(node.members(NodeKind.Property), "Top-level properties summary") { functionSummaryRow(it) }
fullDocs(node.members(NodeKind.Function), { h2 { +"Top-level functions" } }) { fullFunctionDocs(it) }
fullDocs(node.members(NodeKind.Property), { h2 { +"Top-level properties" } }) { fullPropertyDocs(it) }
}
)
fun FlowContent.classHierarchy(node: DocumentationNode) {
val superclassSequence = generateSequence(node) { it.superclass }
table {
superclassSequence.toList().asReversed().forEach {
tr {
td {
a(href = uriProvider.linkTo(it, uri)) { +it.qualifiedName() }
}
}
}
}
}
fun appendClassLike(node: DocumentationNode) = templateService.composePage(
listOf(node),
htmlConsumer,
headContent = {
},
bodyContent = {
h1 { +node.name }
pre { renderedSignature(node, FULL) }
classHierarchy(node)
metaMarkup(node.content)
h2 { +"Summary" }
fun DocumentationNode.isFunction() = kind == NodeKind.Function || kind == NodeKind.CompanionObjectFunction
fun DocumentationNode.isProperty() = kind == NodeKind.Property || kind == NodeKind.CompanionObjectProperty
val functionsToDisplay = node.members.filter(DocumentationNode::isFunction)
val properties = node.members.filter(DocumentationNode::isProperty)
val inheritedFunctionsByReceiver = node.inheritedMembers.filter(DocumentationNode::isFunction).groupBy { it.owner!! }
val inheritedPropertiesByReceiver = node.inheritedMembers.filter(DocumentationNode::isProperty).groupBy { it.owner!! }
val extensionProperties = node.extensions.filter(DocumentationNode::isProperty)
val extensionFunctions = node.extensions.filter(DocumentationNode::isFunction)
summaryNodeGroup(functionsToDisplay, "Functions", headerAsRow = true) { functionSummaryRow(it) }
summaryNodeGroup(inheritedFunctionsByReceiver.entries, "Inherited functions", headerAsRow = true) { formatInheritRow(it) }
summaryNodeGroup(extensionFunctions, "Extension functions", headerAsRow = true) { functionSummaryRow(it) }
summaryNodeGroup(properties, "Properties", headerAsRow = true) { functionSummaryRow(it) }
summaryNodeGroup(inheritedPropertiesByReceiver.entries, "Inherited properties", headerAsRow = true) { formatInheritRow(it) }
summaryNodeGroup(extensionProperties, "Extension properties", headerAsRow = true) { functionSummaryRow(it) }
fullDocs(functionsToDisplay, { h2 { +"Functions" } }) { fullFunctionDocs(it) }
fullDocs(extensionFunctions, { h2 { +"Extension functions" } }) { fullFunctionDocs(it) }
fullDocs(properties, { h2 { +"Properties" } }) { fullPropertyDocs(it) }
fullDocs(extensionProperties, { h2 { +"Extension properties" } }) { fullPropertyDocs(it) }
}
)
private fun FlowContent.fullDocs(
nodes: List<DocumentationNode>,
header: FlowContent.() -> Unit,
renderNode: FlowContent.(DocumentationNode) -> Unit
) {
if (nodes.none()) return
header()
for (node in nodes) {
renderNode(node)
}
}
}
class ContentToHtmlBuilder(val uriProvider: JavaLayoutHtmlUriProvider, val uri: URI) {
fun FlowContent.appendContent(content: List<ContentNode>): Unit = content.forEach { appendContent(it) }
private fun FlowContent.hN(level: Int, classes: String? = null, block: CommonAttributeGroupFacadeFlowHeadingPhrasingContent.() -> Unit) {
when (level) {
1 -> h1(classes, block)
2 -> h2(classes, block)
3 -> h3(classes, block)
4 -> h4(classes, block)
5 -> h5(classes, block)
6 -> h6(classes, block)
}
}
fun FlowContent.appendContent(content: ContentNode) {
when (content) {
is ContentText -> +content.text
is ContentSymbol -> span("symbol") { +content.text }
is ContentKeyword -> span("keyword") { +content.text }
is ContentIdentifier -> span("identifier") {
content.signature?.let { id = it }
+content.text
}
is ContentHeading -> hN(level = content.level) { appendContent(content.children) }
is ContentEntity -> +content.text
is ContentStrong -> strong { appendContent(content.children) }
is ContentStrikethrough -> del { appendContent(content.children) }
is ContentEmphasis -> em { appendContent(content.children) }
is ContentOrderedList -> ol { appendContent(content.children) }
is ContentUnorderedList -> ul { appendContent(content.children) }
is ContentListItem -> consumer.li {
(content.children.singleOrNull() as? ContentParagraph)
?.let { paragraph -> appendContent(paragraph.children) }
?: appendContent(content.children)
}
is ContentCode -> pre { code { appendContent(content.children) } }
is ContentBlockSampleCode -> pre { code {} }
is ContentBlockCode -> pre { code {} }
is ContentNonBreakingSpace -> +nbsp
is ContentSoftLineBreak, is ContentIndentedSoftLineBreak -> {
}
is ContentParagraph -> p { appendContent(content.children) }
is ContentNodeLink -> {
a(href = uriProvider.linkTo(content.node!!, uri)) { appendContent(content.children) }
}
is ContentExternalLink -> {
a(href = content.href) { appendContent(content.children) }
}
is ContentBlock -> appendContent(content.children)
}
}
}
interface JavaLayoutHtmlUriProvider {
fun tryGetContainerUri(node: DocumentationNode): URI?
fun tryGetMainUri(node: DocumentationNode): URI?
fun containerUri(node: DocumentationNode): URI = tryGetContainerUri(node) ?: error("Unsupported ${node.kind}")
fun mainUri(node: DocumentationNode): URI = tryGetMainUri(node) ?: error("Unsupported ${node.kind}")
fun linkTo(to: DocumentationNode, from: URI): String {
return mainUri(to).relativeTo(from).toString()
}
fun mainUriOrWarn(node: DocumentationNode): URI? = tryGetMainUri(node) ?: (null).also {
AssertionError("Not implemented mainUri for ${node.kind}").printStackTrace()
}
}
class JavaLayoutHtmlFormatGenerator @Inject constructor(
@Named("outputDir") val root: File,
val languageService: LanguageService,
val templateService: JavaLayoutHtmlTemplateService,
val logger: DokkaLogger
) : Generator, JavaLayoutHtmlUriProvider {
@set:Inject(optional = true)
var outlineFactoryService: JavaLayoutHtmlFormatOutlineFactoryService? = null
fun createOutputBuilderForNode(node: DocumentationNode, output: Appendable)
= JavaLayoutHtmlFormatOutputBuilder(output, languageService, this, templateService, logger, mainUri(node))
override fun tryGetContainerUri(node: DocumentationNode): URI? {
return when (node.kind) {
NodeKind.Module -> URI("/").resolve(node.name + "/")
NodeKind.Package -> tryGetContainerUri(node.owner!!)?.resolve(node.name.replace('.', '/') + '/')
in classLike -> tryGetContainerUri(node.owner!!)?.resolve("${node.name}.html")
else -> null
}
}
override fun tryGetMainUri(node: DocumentationNode): URI? {
return when (node.kind) {
NodeKind.Package -> tryGetContainerUri(node)?.resolve("package-summary.html")
in classLike -> tryGetContainerUri(node)?.resolve("#")
in memberLike -> tryGetMainUri(node.owner!!)?.resolveInPage(node)
NodeKind.TypeParameter -> node.path.asReversed().drop(1).firstNotNullResult(this::tryGetMainUri)?.resolveInPage(node)
NodeKind.AllTypes -> tryGetContainerUri(node.owner!!)?.resolve("allclasses.html")
else -> null
}
}
fun URI.resolveInPage(node: DocumentationNode): URI = resolve("#${node.signatureUrlEncoded(logger)}")
fun buildClass(node: DocumentationNode, parentDir: File) {
val fileForClass = parentDir.resolve(node.simpleName() + ".html")
fileForClass.bufferedWriter().use {
createOutputBuilderForNode(node, it).appendClassLike(node)
}
}
fun buildPackage(node: DocumentationNode, parentDir: File) {
assert(node.kind == NodeKind.Package)
val members = node.members
val directoryForPackage = parentDir.resolve(node.name.replace('.', File.separatorChar))
directoryForPackage.mkdirsOrFail()
directoryForPackage.resolve("package-summary.html").bufferedWriter().use {
createOutputBuilderForNode(node, it).appendPackage(node)
}
members.filter { it.kind in classLike }.forEach {
buildClass(it, directoryForPackage)
}
}
override fun buildPages(nodes: Iterable<DocumentationNode>) {
val module = nodes.single()
val moduleRoot = root.resolve(module.name)
module.members.filter { it.kind == NodeKind.Package }.forEach { buildPackage(it, moduleRoot) }
}
override fun buildOutlines(nodes: Iterable<DocumentationNode>) {
val uriToWriter = mutableMapOf<URI, BufferedWriter>()
fun provideOutput(uri: URI): BufferedWriter {
val normalized = uri.normalize()
uriToWriter[normalized]?.let { return it }
val file = root.resolve(normalized.path.removePrefix("/"))
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<DocumentationNode>) {
}
}
fun DocumentationNode.signatureForAnchor(logger: DokkaLogger): String = when (kind) {
NodeKind.Function -> buildString {
detailOrNull(NodeKind.Receiver)?.let {
append("(")
append(it.detail(NodeKind.Type).qualifiedNameFromType())
append(").")
}
append(name)
details(NodeKind.Parameter).joinTo(this, prefix = "(", postfix = ")") { it.detail(NodeKind.Type).qualifiedNameFromType() }
}
NodeKind.Property ->
"$name:${detail(NodeKind.Type).qualifiedNameFromType()}"
NodeKind.TypeParameter, NodeKind.Parameter -> owner!!.signatureForAnchor(logger) + "/" + name
else -> "Not implemented signatureForAnchor $this".also { logger.warn(it) }
}
fun DocumentationNode.signatureUrlEncoded(logger: DokkaLogger) = URLEncoder.encode(signatureForAnchor(logger), "UTF-8")
fun URI.relativeTo(uri: URI): URI {
// Normalize paths to remove . and .. segments
val base = uri.normalize()
val child = this.normalize()
// Split paths into segments
var bParts = base.path.split('/').dropLastWhile { it.isEmpty() }
val cParts = child.path.split('/').dropLastWhile { it.isEmpty() }
// Discard trailing segment of base path
if (bParts.isNotEmpty() && !base.path.endsWith("/")) {
bParts = bParts.dropLast(1)
}
// Compute common prefix
val commonPartsSize = bParts.zip(cParts).count { (basePart, childPart) -> basePart == childPart }
return URI.create(buildString {
bParts.drop(commonPartsSize).joinTo(this, separator = "") { "../" }
cParts.drop(commonPartsSize).joinTo(this, separator = "/")
child.rawQuery?.let {
append("?")
append(it)
}
child.rawFragment?.let {
append("#")
append(it)
}
})
}