package org.jetbrains.dokka.Formats import com.google.common.base.Throwables 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 java.net.URI import javax.inject.Inject open class JavaLayoutHtmlFormatOutputBuilder( val output: Appendable, val languageService: LanguageService, val uriProvider: JavaLayoutHtmlUriProvider, val templateService: JavaLayoutHtmlTemplateService, val logger: DokkaLogger, val uri: URI ) { val htmlConsumer = output.appendHTML() 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) } } protected open fun FlowContent.metaMarkup(content: List, contextUri: URI = uri) = contentNodesToMarkup(content, contextUri) protected fun FlowContent.nodeContent(node: DocumentationNode, uriNode: DocumentationNode) = contentNodeToMarkup(node.content, uriProvider.mainUriOrWarn(uriNode) ?: uri) protected fun FlowContent.nodeContent(node: DocumentationNode) = nodeContent(node, node) protected fun FlowContent.contentNodesToMarkup(content: List, contextUri: URI = uri): Unit = content.forEach { contentNodeToMarkup(it, contextUri) } protected fun FlowContent.contentNodeToMarkup(content: ContentNode, contextUri: URI = uri) { 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) { contentNodesToMarkup(content.children, contextUri) } is ContentEntity -> +content.text is ContentStrong -> strong { contentNodesToMarkup(content.children, contextUri) } is ContentStrikethrough -> del { contentNodesToMarkup(content.children, contextUri) } is ContentEmphasis -> em { contentNodesToMarkup(content.children, contextUri) } is ContentOrderedList -> ol { contentNodesToMarkup(content.children, contextUri) } is ContentUnorderedList -> ul { contentNodesToMarkup(content.children, contextUri) } is ContentListItem -> consumer.li { (content.children.singleOrNull() as? ContentParagraph) ?.let { paragraph -> contentNodesToMarkup(paragraph.children, contextUri) } ?: contentNodesToMarkup(content.children, contextUri) } is ContentDescriptionList -> dl { contentNodesToMarkup(content.children, contextUri) } is ContentDescriptionTerm -> consumer.dt { (content.children.singleOrNull() as? ContentParagraph) ?.let { paragraph -> this@contentNodeToMarkup.contentNodesToMarkup(paragraph.children, contextUri) } ?: this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) } is ContentDescriptionDefinition -> consumer.dd { (content.children.singleOrNull() as? ContentParagraph) ?.let { paragraph -> contentNodesToMarkup(paragraph.children, contextUri) } ?: contentNodesToMarkup(content.children, contextUri) } is ContentTable -> table { contentNodesToMarkup(content.children, contextUri) } is ContentTableBody -> consumer.tbody { this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) } is ContentTableRow -> consumer.tr { this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) } is ContentTableHeader -> consumer.th { content.colspan?.let { if (it.isNotBlank()) { attributes["colspan"] = content.colspan } } content.rowspan?.let { if (it.isNotBlank()) { attributes["rowspan"] = content.rowspan } } (content.children.singleOrNull() as? ContentParagraph) ?.let { paragraph -> this@contentNodeToMarkup.contentNodesToMarkup(paragraph.children, contextUri) } ?: this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) } is ContentTableCell -> consumer.td { content.colspan?.let { if (it.isNotBlank()) { attributes["colspan"] = content.colspan } } content.rowspan?.let { if (it.isNotBlank()) { attributes["rowspan"] = content.rowspan } } (content.children.singleOrNull() as? ContentParagraph) ?.let { paragraph -> contentNodesToMarkup(paragraph.children, contextUri) } ?: contentNodesToMarkup(content.children, contextUri) } is ContentSpecialReference -> aside(classes = "note") { contentNodesToMarkup(content.children, contextUri) } is ContentCode -> contentInlineCode(content) is ContentBlockSampleCode -> contentBlockSampleCode(content) is ContentBlockCode -> contentBlockCode(content) ContentNonBreakingSpace -> +nbsp ContentSoftLineBreak, ContentIndentedSoftLineBreak -> { } ContentHardLineBreak -> br is ContentParagraph -> p(classes = content.label) { contentNodesToMarkup(content.children, contextUri) } is NodeRenderContent -> renderedSignature(content.node, mode = content.mode) is ContentNodeLink -> { fun FlowContent.body() = contentNodesToMarkup(content.children, contextUri) when (content.node?.kind) { NodeKind.TypeParameter -> body() else -> a(href = content.node, block = FlowContent::body) } } is ContentBookmark -> a { id = content.name contentNodesToMarkup(content.children, contextUri) } is ContentExternalLink -> contentExternalLink(content) is ContentLocalLink -> a(href = contextUri.resolve(content.href).relativeTo(uri).toString()) { contentNodesToMarkup(content.children, contextUri) } is ContentSection -> { } is ScriptBlock -> script(content.type, content.src) {} is ContentBlock -> contentNodesToMarkup(content.children, contextUri) } } protected open fun FlowContent.contentInlineCode(content: ContentCode) { code { contentNodesToMarkup(content.children) } } protected open fun FlowContent.contentBlockSampleCode(content: ContentBlockSampleCode) { pre { code { attributes["data-language"] = content.language contentNodesToMarkup(content.importsBlock.children) +"\n\n" contentNodesToMarkup(content.children) } } } protected open fun FlowContent.contentBlockCode(content: ContentBlockCode) { pre { code { attributes["data-language"] = content.language contentNodesToMarkup(content.children) } } } protected open fun FlowContent.contentExternalLink(content: ContentExternalLink) { a(href = content.href) { contentNodesToMarkup(content.children) } } protected open fun FlowContent.summaryNodeGroup( nodes: Iterable, header: String, headerAsRow: Boolean = true, row: TBODY.(T) -> Unit ) { if (nodes.none()) return if (!headerAsRow) { h2 { +header } } table { tbody { if (headerAsRow) { developerHeading(header) } nodes.forEach { node -> row(node) } } } } protected open fun summary(node: DocumentationNode) = node.summary protected open fun TBODY.classLikeRow(node: DocumentationNode) = tr { td { a(href = uriProvider.linkTo(node, uri)) { +node.simpleName() } } td { nodeSummary(node) } } protected fun FlowContent.modifiers(node: DocumentationNode) { for (modifier in node.details(NodeKind.Modifier)) { renderedSignature(modifier, SUMMARY) } } protected fun FlowContent.shortFunctionParametersList(func: DocumentationNode) { val params = func.details(NodeKind.Parameter) .map { languageService.render(it, FULL) } .run { drop(1).fold(listOfNotNull(firstOrNull())) { acc, node -> acc + ContentText(", ") + node } } metaMarkup(listOf(ContentText("(")) + params + listOf(ContentText(")"))) } protected open fun TBODY.functionLikeSummaryRow(node: DocumentationNode) = tr { if (node.kind != NodeKind.Constructor) { td { modifiers(node) renderedSignature(node.detail(NodeKind.Type), SUMMARY) } } td { div { code { val receiver = node.detailOrNull(NodeKind.Receiver) if (receiver != null) { renderedSignature(receiver.detail(NodeKind.Type), SUMMARY) +"." } a(href = node) { +node.prettyName } shortFunctionParametersList(node) } } nodeSummary(node) } } protected open fun TBODY.propertyLikeSummaryRow(node: DocumentationNode, showSignature: Boolean = true) = tr { if (showSignature) { td { modifiers(node) renderedSignature(node.detail(NodeKind.Type), SUMMARY) } } td { div { code { a(href = node) { +node.name } } } nodeSummary(node) } } protected open fun TBODY.nestedClassSummaryRow(node: DocumentationNode) = tr { td { modifiers(node) } td { div { code { a(href = node) { +node.name } } } nodeSummary(node) } } protected fun HtmlBlockTag.nodeSummary(node: DocumentationNode, uriNode: DocumentationNode) { contentNodeToMarkup(summary(node), uriProvider.mainUriOrWarn(uriNode) ?: uri) } protected fun HtmlBlockTag.nodeSummary(node: DocumentationNode) { nodeSummary(node, node) } protected open fun TBODY.inheritRow( entry: Map.Entry>, summaryRow: TBODY.(DocumentationNode) -> Unit ) = tr { td { val (from, nodes) = entry +"From class " a(href = from.owner!!) { +from.qualifiedName() } table { tbody { for (node in nodes) { summaryRow(node) } } } } } protected open fun TBODY.groupedRow( entry: Map.Entry>, groupHeader: HtmlBlockTag.(DocumentationNode) -> Unit, summaryRow: TBODY.(DocumentationNode) -> Unit ) = tr { td { val (from, nodes) = entry groupHeader(from) table { tbody { for (node in nodes) { summaryRow(node) } } } } } protected open fun TBODY.extensionRow( entry: Map.Entry>, summaryRow: TBODY.(DocumentationNode) -> Unit ) = groupedRow(entry, { from -> +"From " a(href = from) { +from.qualifiedName() } }, summaryRow) protected open fun TBODY.extensionByReceiverRow( entry: Map.Entry>, summaryRow: TBODY.(DocumentationNode) -> Unit ) = groupedRow(entry, { from -> +"For " a(href = from) { +from.name } }, summaryRow) protected open fun FlowOrInteractiveOrPhrasingContent.a(href: DocumentationNode?, classes: String? = null, block: HtmlBlockInlineTag.() -> Unit) { if (href == null) { return a(href = "#", classes = classes, block = block) } val hrefText = try { href.name.takeIf { href.kind == NodeKind.ExternalLink } ?: href.links.firstOrNull { it.kind == NodeKind.ExternalLink }?.name ?: "#".takeIf { href.kind == NodeKind.ExternalClass } // When external class unresolved ?: uriProvider.linkTo(href, uri) } catch (e: Exception) { val owners = generateSequence(href) { it.owner }.toList().reversed() logger.warn("Exception while resolving link to ${owners.joinToString(separator = " ")}\n" + Throwables.getStackTraceAsString(e)) "#" } a(href = hrefText, classes = classes, block = block) } protected open fun FlowContent.renderedSignature( node: DocumentationNode, mode: LanguageService.RenderMode = SUMMARY ) { contentNodeToMarkup(languageService.render(node, mode), uri) } protected open fun generatePackage(page: Page.PackagePage) = templateService.composePage( page, htmlConsumer, headContent = { }, bodyContent = { h1 { +page.node.name } nodeContent(page.node) this@composePage.summaryNodeGroup(page.interfaces, "Interfaces", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.classes, "Classes", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.exceptions, "Exceptions", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.typeAliases, "Type-aliases", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.annotations, "Annotations", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.enums, "Enums", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup( page.constants, "Top-level constants summary", headerAsRow = false ) { propertyLikeSummaryRow(it) } this@composePage.summaryNodeGroup( page.functions, "Top-level functions summary", headerAsRow = false ) { functionLikeSummaryRow(it) } this@composePage.summaryNodeGroup( page.properties, "Top-level properties summary", headerAsRow = false ) { propertyLikeSummaryRow(it) } this@composePage.summaryNodeGroup( page.extensionFunctions.entries, "Extension functions summary", headerAsRow = false ) { extensionByReceiverRow(it) { functionLikeSummaryRow(it) } } this@composePage.summaryNodeGroup( page.extensionProperties.entries, "Extension properties summary", headerAsRow = false ) { extensionByReceiverRow(it) { functionLikeSummaryRow(it) } } fullMemberDocs(page.constants, "Top-level constants") fullMemberDocs(page.functions, "Top-level functions") fullMemberDocs(page.properties, "Top-level properties") fullMemberDocs(page.extensionFunctions.values.flatten(), "Extension functions") fullMemberDocs(page.extensionProperties.values.flatten(), "Extension properties") } ) protected fun FlowContent.qualifiedTypeReference(node: DocumentationNode) { if (node.kind in classLike) { a(href = node) { +node.qualifiedName() } return } val targetLink = node.links.firstOrNull() if (targetLink?.kind == NodeKind.TypeParameter) { +node.name return } a(href = targetLink) { +node.qualifiedNameFromType() } val typeParameters = node.details(NodeKind.Type) if (typeParameters.isNotEmpty()) { +"<" typeParameters.forEach { if (it != typeParameters.first()) { +", " } qualifiedTypeReference(it) } +">" } } protected open fun FlowContent.classHierarchy(superclasses: List) { table { superclasses.forEach { tr { if (it != superclasses.first()) { td { +"   ↳" } } td { qualifiedTypeReference(it) } } } } } protected open fun FlowContent.subclasses(inheritors: List, direct: Boolean) { if (inheritors.isEmpty()) return div { table { thead { tr { td { if (direct) +"Known Direct Subclasses" else +"Known Indirect Subclasses" } } } tbody { inheritors.forEach { inheritor -> tr { td { a(href = inheritor) { +inheritor.classNodeNameWithOuterClass() } } td { nodeSummary(inheritor) } } } } } } } protected open fun FlowContent.classLikeSummaries(page: Page.ClassPage) = with(page) { this@classLikeSummaries.summaryNodeGroup( nestedClasses, "Nested classes", headerAsRow = true ) { nestedClassSummaryRow(it) } this@classLikeSummaries.summaryNodeGroup(enumValues, "Enum values") { propertyLikeSummaryRow(it) } this@classLikeSummaries.summaryNodeGroup(constants, "Constants") { propertyLikeSummaryRow(it) } constructors.forEach { (visibility, group) -> this@classLikeSummaries.summaryNodeGroup( group, "${visibility.capitalize()} constructors", headerAsRow = true ) { functionLikeSummaryRow(it) } } functions.forEach { (visibility, group) -> this@classLikeSummaries.summaryNodeGroup( group, "${visibility.capitalize()} functions", headerAsRow = true ) { functionLikeSummaryRow(it) } } this@classLikeSummaries.summaryNodeGroup( companionFunctions, "Companion functions", headerAsRow = true ) { functionLikeSummaryRow(it) } this@classLikeSummaries.summaryNodeGroup( inheritedFunctionsByReceiver.entries, "Inherited functions", headerAsRow = true ) { inheritRow(it) { functionLikeSummaryRow(it) } } this@classLikeSummaries.summaryNodeGroup( extensionFunctions.entries, "Extension functions", headerAsRow = true ) { extensionRow(it) { functionLikeSummaryRow(it) } } this@classLikeSummaries.summaryNodeGroup( inheritedExtensionFunctions.entries, "Inherited extension functions", headerAsRow = true ) { extensionRow(it) { functionLikeSummaryRow(it) } } this@classLikeSummaries.summaryNodeGroup(properties, "Properties", headerAsRow = true) { propertyLikeSummaryRow(it) } this@classLikeSummaries.summaryNodeGroup( companionProperties, "Companion properties", headerAsRow = true ) { propertyLikeSummaryRow(it) } this@classLikeSummaries.summaryNodeGroup( inheritedPropertiesByReceiver.entries, "Inherited properties", headerAsRow = true ) { inheritRow(it) { propertyLikeSummaryRow(it) } } this@classLikeSummaries.summaryNodeGroup( extensionProperties.entries, "Extension properties", headerAsRow = true ) { extensionRow(it) { propertyLikeSummaryRow(it) } } this@classLikeSummaries.summaryNodeGroup( inheritedExtensionProperties.entries, "Inherited extension properties", headerAsRow = true ) { extensionRow(it) { propertyLikeSummaryRow(it) } } } protected open fun FlowContent.classLikeFullMemberDocs(page: Page.ClassPage) = with(page) { fullMemberDocs(enumValues, "Enum values") fullMemberDocs(constants, "Constants") constructors.forEach { (visibility, group) -> fullMemberDocs(group, "${visibility.capitalize()} constructors") } functions.forEach { (visibility, group) -> fullMemberDocs(group, "${visibility.capitalize()} methods") } fullMemberDocs(properties, "Properties") if (!hasMeaningfulCompanion) { fullMemberDocs(companionFunctions, "Companion functions") fullMemberDocs(companionProperties, "Companion properties") } } protected open fun generateClassLike(page: Page.ClassPage) = templateService.composePage( page, htmlConsumer, headContent = { }, bodyContent = { val node = page.node with(page) { div { id = "api-info-block" apiAndDeprecatedVersions(node) } if (node.artifactId.name.isNotEmpty()) { div(classes = "api-level") { br { +"belongs to Maven artifact ${node.artifactId}" } } } h1 { +node.name } pre { renderedSignature(node, FULL) } classHierarchy(page.superclasses) subclasses(page.directInheritors, true) subclasses(page.indirectInheritors, false) deprecatedClassCallOut(node) nodeContent(node) h2 { +"Summary" } classLikeSummaries(page) classLikeFullMemberDocs(page) } } ) protected open fun FlowContent.classIndexSummary(node: DocumentationNode) { nodeContent(node) } protected open fun generateClassIndex(page: Page.ClassIndex) = templateService.composePage( page, htmlConsumer, headContent = { }, bodyContent = { h1 { +"Class Index" } ul { page.classesByFirstLetter.forEach { (letter) -> li { a(href = "#letter_$letter") { +letter } } } } page.classesByFirstLetter.forEach { (letter, classes) -> h2 { id = "letter_$letter" +letter } table { tbody { for (node in classes) { tr { td { a(href = uriProvider.linkTo(node, uri)) { +node.classNodeNameWithOuterClass() } } td { if (!deprecatedIndexSummary(node)) { classIndexSummary(node) } } } } } } } } ) protected open fun generatePackageIndex(page: Page.PackageIndex) = templateService.composePage( page, htmlConsumer, headContent = { }, bodyContent = { h1 { +"Package Index" } table { tbody { for (node in page.packages) { tr { td { a(href = uriProvider.linkTo(node, uri)) { +node.name } } td { nodeContent(node) } } } } } } ) fun generatePage(page: Page) { when (page) { is Page.PackageIndex -> generatePackageIndex(page) is Page.ClassIndex -> generateClassIndex(page) is Page.ClassPage -> generateClassLike(page) is Page.PackagePage -> generatePackage(page) } } protected fun FlowContent.fullMemberDocs( nodes: List, header: String ) { if (nodes.none()) return h2 { +header } for (node in nodes) { fullMemberDocs(node) } } protected open fun FlowContent.seeAlsoSection(links: List>) { p { b { +"See Also" } } ul { links.forEach { linkParts -> li { code { metaMarkup(linkParts) } } } } } protected open fun FlowContent.regularSection(name: String, entries: List) { table { thead { tr { th { colSpan = "2" +name } } } tbody { entries.forEach { tr { if (it.subjectName != null) { td { +it.subjectName } } td { metaMarkup(it.children) } } } } } } protected open fun FlowContent.deprecationWarningToMarkup( node: DocumentationNode, prefix: Boolean = false, emphasis: Boolean = true ): Boolean { val deprecated = formatDeprecationOrNull(node, prefix, emphasis) deprecated?.let { contentNodeToMarkup(deprecated, uriProvider.mainUri(node)) return true } return false } protected open fun FlowContent.deprecatedClassCallOut(node: DocumentationNode) { val deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty() if (deprecatedLevelExists) { hr { } aside(classes = "caution") { strong { +node.deprecatedLevelMessage() } deprecationWarningToMarkup(node, emphasis = false) } } } protected open fun FlowContent.deprecatedIndexSummary(node: DocumentationNode): Boolean { val deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty() if (deprecatedLevelExists) { val em = ContentEmphasis() em.append(ContentText(node.deprecatedLevelMessage())) em.append(ContentText(" ")) for (child in node.deprecation?.content?.children ?: emptyList()) { em.append(child) } contentNodeToMarkup(em, uriProvider.mainUri(node)) return true } return false } protected open fun FlowContent.apiAndDeprecatedVersions(node: DocumentationNode) { val apiLevelExists = node.apiLevel.name.isNotEmpty() val sdkExtSinceExists = node.sdkExtSince.name.isNotEmpty() val deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty() if (apiLevelExists || sdkExtSinceExists || deprecatedLevelExists) { div(classes = "api-level") { if (apiLevelExists) { +"Added in " a(href = "https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels") { +"API level ${node.apiLevel.name}" } } if (sdkExtSinceExists) { if (apiLevelExists) { br +"Also in " } else { +"Added in " } a(href = "https://developer.android.com/sdkExtensions") { +"${node.sdkExtSince.name}" } } if (deprecatedLevelExists) { if (apiLevelExists || sdkExtSinceExists) { br } +"Deprecated in " a(href = "https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels") { +"API level ${node.deprecatedLevel.name}" } } } } } protected open fun formatDeprecationOrNull( node: DocumentationNode, prefix: Boolean = false, emphasis: Boolean = true): ContentNode? { val deprecated = node.deprecation deprecated?.let { return ContentParagraph("caution").apply { if (prefix) { append(ContentStrong().apply { text( if (deprecated.content.children.size == 0) "Deprecated." else "Deprecated: " ) }) } val em = if (emphasis) ContentEmphasis() else ContentBlock() for (child in deprecated.content.children) { em.append(child) } append(em) } } return null } protected open fun FlowContent.section(name: String, sectionParts: List) { when (name) { ContentTags.SeeAlso -> seeAlsoSection(sectionParts.map { it.children.flatMap { (it as? ContentParagraph)?.children ?: listOf(it) } }) else -> regularSection(name, sectionParts) } } protected open fun FlowContent.sections(content: Content) { val sectionsByTag = content.sections.groupByTo(mutableMapOf()) { it.tag } val seeAlso = sectionsByTag.remove(ContentTags.SeeAlso) for ((name, entries) in sectionsByTag) { section(name, entries) } seeAlso?.let { section(ContentTags.SeeAlso, it) } } protected open fun FlowContent.fullMemberDocs(node: DocumentationNode, uriNode: DocumentationNode) { div { id = node.signatureForAnchor(logger) h3 { +node.name } pre { renderedSignature(node, FULL) } deprecationWarningToMarkup(node, prefix = true) nodeContent(node) node.constantValue()?.let { value -> pre { +"Value: " code { +value } } } sections(node.content) } } protected open fun FlowContent.fullMemberDocs(node: DocumentationNode) { fullMemberDocs(node, node) } sealed class Page { class PackageIndex(packages: List) : Page() { init { assert(packages.all { it.kind == NodeKind.Package }) } val packages = packages.sortedBy { it.name } } class ClassIndex(allTypesNode: DocumentationNode) : Page() { init { assert(allTypesNode.kind == NodeKind.AllTypes) } // Wide-collect all nested classes val classes: List = generateSequence(listOf(allTypesNode)) { nodes -> nodes .flatMap { it.members.filter { it.kind in NodeKind.classLike } } .takeUnless { it.isEmpty() } }.drop(1) .flatten() .sortedBy { it.classNodeNameWithOuterClass().toLowerCase() } .toList() // Group all classes by it's first letter and sort val classesByFirstLetter = classes .groupBy { it.classNodeNameWithOuterClass().first().toString() } .entries .sortedBy { (letter) -> val x = letter.toLowerCase() x } } class ClassPage(val node: DocumentationNode) : Page() { init { assert(node.kind in NodeKind.classLike) } val superclasses = (sequenceOf(node) + node.superclassTypeSequence).toList().asReversed() val enumValues = node.members(NodeKind.EnumItem).sortedBy { it.name } val directInheritors: List val indirectInheritors: List init { // Wide-collect all inheritors val inheritors = generateSequence(node.inheritors) { inheritors -> inheritors .flatMap { it.inheritors } .takeUnless { it.isEmpty() } } directInheritors = inheritors.first().sortedBy { it.classNodeNameWithOuterClass() } indirectInheritors = inheritors.drop(1).flatten().toList().sortedBy { it.classNodeNameWithOuterClass() } } val isCompanion = node.details(NodeKind.Modifier).any { it.name == "companion" } val hasMeaningfulCompanion = !isCompanion && node.companion != null private fun DocumentationNode.thisTypeExtension() = detail(NodeKind.Receiver).detail(NodeKind.Type).links.any { it == node } val functionKind = if (!isCompanion) NodeKind.Function else NodeKind.CompanionObjectFunction val propertyKind = if (!isCompanion) NodeKind.Property else NodeKind.CompanionObjectProperty private fun DocumentationNode.isFunction() = kind == functionKind private fun DocumentationNode.isProperty() = kind == propertyKind val nestedClasses = node.members.filter { it.kind in NodeKind.classLike } - enumValues val attributes = node.attributes val inheritedAttributes = node.superclassTypeSequence .toList() .sortedBy { it.name } .flatMap { it.typeDeclarationClass?.attributes.orEmpty() } .distinctBy { it.attributeRef!!.name } .groupBy { it.owner!! } val allInheritedMembers = node.allInheritedMembers val constants = node.members.filter { it.constantValue() != null } val inheritedConstants = allInheritedMembers.filter { it.constantValue() != null }.groupBy { it.owner!! } fun compareVisibilities(a: String, b: String): Int { return visibilityNames.indexOf(a) - visibilityNames.indexOf(b) } fun Collection.groupByVisibility() = groupBy { it.visibility() }.toSortedMap(Comparator { a, b -> compareVisibilities(a, b) }) val constructors = node.members(NodeKind.Constructor).groupByVisibility() val functions = node.members(functionKind).groupByVisibility() val fields = (node.members(NodeKind.Field) - constants).groupByVisibility() val properties = node.members(propertyKind) - constants val inheritedFunctionsByReceiver = allInheritedMembers.filter { it.kind == functionKind }.groupBy { it.owner!! } val inheritedPropertiesByReceiver = allInheritedMembers.filter { it.kind == propertyKind && it.constantValue() == null }.groupBy { it.owner!! } val inheritedFieldsByReceiver = allInheritedMembers.filter { it.kind == NodeKind.Field && it.constantValue() != null }.groupBy { it.owner!! } val originalExtensions = if (!isCompanion) node.extensions else node.owner!!.extensions val extensionFunctions: Map> val extensionProperties: Map> val inheritedExtensionFunctions: Map> val inheritedExtensionProperties: Map> init { val (extensions, inheritedExtensions) = originalExtensions.partition { it.thisTypeExtension() } extensionFunctions = extensions.filter { it.isFunction() }.sortedBy { it.name }.groupBy { it.owner!! } extensionProperties = extensions.filter { it.isProperty() }.sortedBy { it.name }.groupBy { it.owner!! } inheritedExtensionFunctions = inheritedExtensions.filter { it.isFunction() }.sortedBy { it.name }.groupBy { it.owner!! } inheritedExtensionProperties = inheritedExtensions.filter { it.isProperty() }.sortedBy { it.name }.groupBy { it.owner!! } } val companionFunctions = node.members(NodeKind.CompanionObjectFunction).takeUnless { isCompanion }.orEmpty() val companionProperties = node.members(NodeKind.CompanionObjectProperty).takeUnless { isCompanion }.orEmpty() - constants } class PackagePage(val node: DocumentationNode) : Page() { init { assert(node.kind == NodeKind.Package) } val interfaces = node.members(NodeKind.Interface) + node.members(NodeKind.Class).flatMap { it.members(NodeKind.Interface) } val classes = node.members(NodeKind.Class) val exceptions = node.members(NodeKind.Exception) val typeAliases = node.members(NodeKind.TypeAlias) val annotations = node.members(NodeKind.AnnotationClass) val enums = node.members(NodeKind.Enum) val constants = node.members(NodeKind.Property).filter { it.constantValue() != null } private fun DocumentationNode.getClassExtensionReceiver() = detailOrNull(NodeKind.Receiver)?.detailOrNull(NodeKind.Type)?.takeIf { it.links.any { it.kind == NodeKind.ExternalLink || it.kind in NodeKind.classLike } } private fun List.groupedExtensions() = filter { it.getClassExtensionReceiver() != null } .groupBy { val receiverType = it.getClassExtensionReceiver()!! receiverType.links.filter { it.kind != NodeKind.ExternalLink}.firstOrNull() ?: receiverType.links(NodeKind.ExternalLink).first() } private fun List.externalExtensions(kind: NodeKind) = associateBy({ it }, { it.members(kind) }) .filterNot { (_, values) -> values.isEmpty() } val extensionFunctions = node.members(NodeKind.ExternalClass).externalExtensions(NodeKind.Function) + node.members(NodeKind.Function).groupedExtensions() val extensionProperties = node.members(NodeKind.ExternalClass).externalExtensions(NodeKind.Property) + node.members(NodeKind.Property).groupedExtensions() val functions = node.members(NodeKind.Function) - extensionFunctions.values.flatten() val properties = node.members(NodeKind.Property) - constants - extensionProperties.values.flatten() } } } class JavaLayoutHtmlFormatOutputBuilderFactoryImpl @Inject constructor( val uriProvider: JavaLayoutHtmlUriProvider, val languageService: LanguageService, val templateService: JavaLayoutHtmlTemplateService, val logger: DokkaLogger ) : JavaLayoutHtmlFormatOutputBuilderFactory { override fun createOutputBuilder(output: Appendable, node: DocumentationNode): JavaLayoutHtmlFormatOutputBuilder { return createOutputBuilder(output, uriProvider.mainUri(node)) } override fun createOutputBuilder(output: Appendable, uri: URI): JavaLayoutHtmlFormatOutputBuilder { return JavaLayoutHtmlFormatOutputBuilder(output, languageService, uriProvider, templateService, logger, uri) } } fun DocumentationNode.constantValue(): String? = detailOrNull(NodeKind.Value)?.name.takeIf { kind == NodeKind.Field || kind == NodeKind.Property || kind == NodeKind.CompanionObjectProperty } private val visibilityNames = setOf("public", "protected", "internal", "package-local", "private") fun DocumentationNode.visibility(): String = details(NodeKind.Modifier).firstOrNull { it.name in visibilityNames }?.name ?: ""