| 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<ContentNode>, 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<ContentNode>, 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 <T> FlowContent.summaryNodeGroup( |
| nodes: Iterable<T>, |
| 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<DocumentationNode, List<DocumentationNode>>, |
| 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<DocumentationNode, List<DocumentationNode>>, |
| 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<DocumentationNode, List<DocumentationNode>>, |
| summaryRow: TBODY.(DocumentationNode) -> Unit |
| ) = groupedRow(entry, { from -> |
| +"From " |
| a(href = from) { +from.qualifiedName() } |
| }, summaryRow) |
| |
| |
| protected open fun TBODY.extensionByReceiverRow( |
| entry: Map.Entry<DocumentationNode, List<DocumentationNode>>, |
| 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<DocumentationNode>) { |
| table { |
| superclasses.forEach { |
| tr { |
| if (it != superclasses.first()) { |
| td { |
| +"   ↳" |
| } |
| } |
| td { |
| qualifiedTypeReference(it) |
| } |
| } |
| } |
| } |
| } |
| |
| protected open fun FlowContent.subclasses(inheritors: List<DocumentationNode>, 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<DocumentationNode>, |
| header: String |
| ) { |
| if (nodes.none()) return |
| h2 { |
| +header |
| } |
| for (node in nodes) { |
| fullMemberDocs(node) |
| } |
| } |
| |
| protected open fun FlowContent.seeAlsoSection(links: List<List<ContentNode>>) { |
| p { b { +"See Also" } } |
| ul { |
| links.forEach { linkParts -> |
| li { code { metaMarkup(linkParts) } } |
| } |
| } |
| } |
| |
| protected open fun FlowContent.regularSection(name: String, entries: List<ContentSection>) { |
| 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<ContentNode>()) { |
| 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 deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty() |
| if (apiLevelExists || 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 (deprecatedLevelExists) { |
| br |
| } |
| } |
| if (deprecatedLevelExists) { |
| +"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<ContentSection>) { |
| 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<DocumentationNode>) : 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<DocumentationNode> = |
| 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<DocumentationNode> |
| val indirectInheritors: List<DocumentationNode> |
| |
| 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<DocumentationNode>.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 == NodeKind.Field && it.constantValue() != null |
| }.groupBy { it.owner!! } |
| |
| val originalExtensions = if (!isCompanion) node.extensions else node.owner!!.extensions |
| |
| val extensionFunctions: Map<DocumentationNode, List<DocumentationNode>> |
| val extensionProperties: Map<DocumentationNode, List<DocumentationNode>> |
| val inheritedExtensionFunctions: Map<DocumentationNode, List<DocumentationNode>> |
| val inheritedExtensionProperties: Map<DocumentationNode, List<DocumentationNode>> |
| |
| 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<DocumentationNode>.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<DocumentationNode>.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 ?: "" |