package org.jetbrains.dokka.Formats import com.google.inject.Inject import com.google.inject.name.Named import kotlinx.html.* import org.jetbrains.dokka.* import org.jetbrains.dokka.Samples.DevsiteSampleProcessingService import org.jetbrains.dokka.Kotlin.ParameterInfoNode import org.jetbrains.dokka.Utilities.firstSentence import java.lang.Math.max import java.net.URI import kotlin.reflect.KClass /** * On Devsite, certain headers and footers are needed for generating Devsite metadata. */ class DevsiteHtmlTemplateService @Inject constructor( @Named("outlineRoot") val outlineRoot: String, @Named("dacRoot") val dacRoot: String ) : JavaLayoutHtmlTemplateService { override fun composePage(page: JavaLayoutHtmlFormatOutputBuilder.Page, tagConsumer: TagConsumer, headContent: HEAD.() -> Unit, bodyContent: BODY.() -> Unit) { tagConsumer.html { attributes["devsite"] = "true" head { headContent() title { +when (page) { is JavaLayoutHtmlFormatOutputBuilder.Page.ClassIndex -> "Class Index" is JavaLayoutHtmlFormatOutputBuilder.Page.ClassPage -> page.node.nameWithOuterClass() is JavaLayoutHtmlFormatOutputBuilder.Page.PackageIndex -> "Package Index" is JavaLayoutHtmlFormatOutputBuilder.Page.PackagePage -> page.node.nameWithOuterClass() } } unsafe { +"{% setvar book_path %}${dacRoot}/${outlineRoot}_book.yaml{% endsetvar %}\n{% include \"_shared/_reference-head-tags.html\" %}\n" } } body { bodyContent() } } } } class DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl @javax.inject.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 DevsiteLayoutHtmlFormatOutputBuilder(output, languageService, uriProvider, templateService, logger, uri) } } class DevsiteLayoutHtmlFormatOutputBuilder( output: Appendable, languageService: LanguageService, uriProvider: JavaLayoutHtmlUriProvider, templateService: JavaLayoutHtmlTemplateService, logger: DokkaLogger, uri: URI ) : JavaLayoutHtmlFormatOutputBuilder(output, languageService, uriProvider, templateService, logger, uri) { override fun FlowContent.fullMemberDocs(node: DocumentationNode) { fullMemberDocs(node, node) } override fun FlowContent.fullMemberDocs(node: DocumentationNode, uriNode: DocumentationNode) { a { attributes["name"] = uriNode.signatureForAnchor(logger).anchorEncoded() } div(classes = "api apilevel-${node.apiLevel.name}") { attributes["data-version-added"] = node.apiLevel.name h3(classes = "api-name") { //id = node.signatureForAnchor(logger).urlEncoded() +node.prettyName } apiAndDeprecatedVersions(node) pre(classes = "api-signature no-pretty-print") { renderedSignature(node, LanguageService.RenderMode.FULL) } deprecationWarningToMarkup(node, prefix = true) nodeContent(node, uriNode) node.constantValue()?.let { value -> pre { +"Value: " code { +value } } } for ((name, sections) in node.content.sections.groupBy { it.tag }) { when (name) { ContentTags.Return -> { table(classes = "responsive") { tbody { tr { th { colSpan = "2" +name } } sections.forEach { tr { if (it.children.size > 0) { td { val firstChild = it.children.first() if (firstChild is ContentBlock && firstChild.children.size == 3 && firstChild.children[0] is NodeRenderContent && firstChild.children[1] is ContentSymbol && firstChild.children[2] is ContentText) { // it.children is expected to have two items // First should have 3 children of its own: // - NodeRenderContent is the return type // - ContentSymbol - ":" // - ContentText - " " // We want to only use NodeRenderContent in a separate and // to get proper formatting in DAC. code { metaMarkup(listOf(firstChild.children[0])) } } else { metaMarkup(listOf(firstChild)) } } td { if (it.children.size > 1) { metaMarkup(it.children.subList(1, it.children.size)) } } } } } } } } ContentTags.Parameters -> { table(classes = "responsive") { tbody { tr { th { colSpan = "2" +name } } sections.forEach { section -> tr { td { val parameterInfoNode = section.children.find { it is ParameterInfoNode } as? ParameterInfoNode // If there is no info found, just display the parameter // name. if (parameterInfoNode?.parameterContent == null) { code { section.subjectName?.let { +it } } } else { // Add already marked up type information here metaMarkup( listOf(parameterInfoNode.parameterContent!!) ) } } td { metaMarkup(section.children) } } } } } } ContentTags.SeeAlso -> { div { p { b { +name } } ul(classes = "nolist") { sections.filter {it.tag == "See Also"}.forEach { it.children.forEach { child -> if (child is ContentNodeLazyLink || child is ContentExternalLink) { li { code { contentNodeToMarkup(child) // Wrap bare links in listItems. } // bare links come from the java-to-kotlin parser. } } else if (child is ContentUnorderedList) { metaMarkup(child.children) // Already wrapped in listItems. } // this is how we want things to look. No parser currently does this (yet). else if (child is ContentParagraph) { li{ code { metaMarkup (child.children) // Replace paragraphs with listItems. } // paragraph-wrapped links come from the kotlin parser } } // NOTE: currently the java-to-java parser does not add See Also links! } } } } } ContentTags.Exceptions -> { table(classes = "responsive") { tbody { tr { th { colSpan = "2" +name } } sections.forEach { tr { td { code { it.subjectName?.let { +it } } } td { metaMarkup(it.children) } } } } } } } } } } override fun summary(node: DocumentationNode) = node.firstSentenceOfSummary() fun TBODY.xmlAttributeRow(attr: DocumentationNode) = tr { td { a(href = attr) { code { +attr.attributeRef!!.name } } } td { +attr.attributeRef!!.firstSentence() } } protected fun FlowContent.fullAttributeDocs( attributes: List, header: String ) { if (attributes.none()) return h2 { +header } attributes.forEach { fullMemberDocs(it.attributeRef!!, it) } } override fun FlowContent.classLikeFullMemberDocs(page: Page.ClassPage) = with(page) { fullAttributeDocs(attributes, "XML attributes") 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") fields.forEach { (visibility, group) -> fullMemberDocs(group, "${visibility.capitalize()} fields") } if (!hasMeaningfulCompanion) { fullMemberDocs(companionFunctions, "Companion functions") fullMemberDocs(companionProperties, "Companion properties") } } override fun FlowContent.classLikeSummaries(page: Page.ClassPage) = with(page) { this@classLikeSummaries.summaryNodeGroup( nestedClasses, header = "Nested classes", summaryId = "nestedclasses", tableClass = "responsive", headerAsRow = true ) { nestedClassSummaryRow(it) } this@classLikeSummaries.summaryNodeGroup( attributes, header="XML attributes", summaryId="lattrs", tableClass = "responsive", headerAsRow = true ) { xmlAttributeRow(it) } this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( superClasses = inheritedAttributes.entries, header="Inherited XML attributes", tableId="inhattrs", tableClass = "responsive", row = { inheritedXmlAttributeRow(it)} ) this@classLikeSummaries.summaryNodeGroup( constants, header = "Constants", summaryId = "constants", tableClass = "responsive", headerAsRow = true ) { propertyLikeSummaryRow(it) } this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( superClasses = inheritedConstants.entries, header = "Inherited constants", tableId = "inhconstants", tableClass = "responsive constants inhtable", row = { inheritedMemberRow(it) } ) constructors.forEach { (visibility, group) -> this@classLikeSummaries.summaryNodeGroup( group, header = "${visibility.capitalize()} constructors", summaryId = "${visibility.take(3)}ctors", tableClass = "responsive", headerAsRow = true ) { functionLikeSummaryRow(it) } } this@classLikeSummaries.summaryNodeGroup( enumValues, header = "Enum values", summaryId = "enumvalues", tableClass = "responsive", headerAsRow = true ) { propertyLikeSummaryRow(it, showSignature = false) } functions.forEach { (visibility, group) -> this@classLikeSummaries.summaryNodeGroup( group, header = "${visibility.capitalize()} methods", summaryId = "${visibility.take(3)}methods", tableClass = "responsive", headerAsRow = true ) { functionLikeSummaryRow(it) } } this@classLikeSummaries.summaryNodeGroup( companionFunctions, header = "Companion functions", summaryId = "compmethods", tableClass = "responsive", headerAsRow = true ) { functionLikeSummaryRow(it) } this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( superClasses = inheritedFunctionsByReceiver.entries, header = "Inherited functions", tableId = "inhmethods", tableClass = "responsive", row = { inheritedMemberRow(it) } ) this@classLikeSummaries.summaryNodeGroup( extensionFunctions.entries, header = "Extension functions", summaryId = "extmethods", tableClass = "responsive", headerAsRow = true ) { extensionRow(it) { functionLikeSummaryRow(it) } } this@classLikeSummaries.summaryNodeGroup( inheritedExtensionFunctions.entries, header = "Inherited extension functions", summaryId = "inhextmethods", tableClass = "responsive", headerAsRow = true ) { extensionRow(it) { functionLikeSummaryRow(it) } } fields.forEach { (visibility, group) -> this@classLikeSummaries.summaryNodeGroup( group, header = "${visibility.capitalize()} fields", summaryId = "${visibility.take(3)}fields", tableClass = "responsive", headerAsRow = true ) { propertyLikeSummaryRow(it) } } this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( superClasses = inheritedFieldsByReceiver.entries, header = "Inherited fields", tableId = "inhfields", tableClass = "responsive properties inhtable", row = { inheritedMemberRow(it) } ) this@classLikeSummaries.summaryNodeGroup( properties, header = "Properties", summaryId = "properties", tableClass = "responsive", headerAsRow = true ) { propertyLikeSummaryRow(it) } this@classLikeSummaries.summaryNodeGroup( companionProperties, "Companion properties", headerAsRow = true ) { propertyLikeSummaryRow(it) } this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( superClasses = inheritedPropertiesByReceiver.entries, header = "Inherited properties", tableId = "inhfields", tableClass = "responsive properties inhtable", row = { inheritedMemberRow(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) } } } fun FlowContent.summaryNodeGroup( nodes: Iterable, header: String, headerAsRow: Boolean, summaryId: String, tableClass: String = "responsive", row: TBODY.(T) -> Unit ) { if (nodes.none()) return if (!headerAsRow) { h2 { +header } } table(classes = tableClass) { id = summaryId tbody { if (headerAsRow) { developerHeading(header) } nodes.forEach { node -> row(node) } } } } override fun FlowContent.contentBlockCode(content: ContentBlockCode) { pre { attributes["class"] = "prettyprint" contentNodesToMarkup(content.children) } } override fun FlowContent.contentBlockSampleCode(content: ContentBlockSampleCode) { pre { attributes["class"] = "prettyprint" contentNodesToMarkup(content.importsBlock.children) +"\n\n" contentNodesToMarkup(content.children) } } override fun generatePackage(page: Page.PackagePage) = templateService.composePage( page, htmlConsumer, headContent = { }, bodyContent = { h1 { +page.node.name } nodeContent(page.node) this@composePage.summaryNodeGroup(page.interfaces.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Interfaces", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.classes.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Classes", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.exceptions.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Exceptions", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.typeAliases.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Type-aliases", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.annotations.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Annotations", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup(page.enums.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Enums", headerAsRow = false) { classLikeRow(it) } this@composePage.summaryNodeGroup( page.constants.sortedBy { it.name }, "Top-level constants summary", headerAsRow = false ) { propertyLikeSummaryRow(it) } this@composePage.summaryNodeGroup( page.functions.sortedBy { it.name }, "Top-level functions summary", headerAsRow = false ) { functionLikeSummaryRow(it) } this@composePage.summaryNodeGroup( page.properties.sortedBy { it.name }, "Top-level properties summary", headerAsRow = false ) { propertyLikeSummaryRow(it) } summaryNodeGroupForExtensions("Extension functions summary", page.extensionFunctions.entries) summaryNodeGroupForExtensions("Extension properties summary", page.extensionProperties.entries) fullMemberDocs(page.constants.sortedBy { it.name }, "Top-level constants") fullMemberDocs(page.functions.sortedBy { it.name }, "Top-level functions") fullMemberDocs(page.properties.sortedBy { it.name }, "Top-level properties") fullMemberDocs(page.extensionFunctions.values.flatten().sortedBy { it.name }, "Extension functions") fullMemberDocs(page.extensionProperties.values.flatten().sortedBy { it.name }, "Extension properties") } ) private fun TBODY.inheritedXmlAttributeRow(inheritedMember: DocumentationNode) { tr(classes = "api apilevel-${inheritedMember.attributeRef!!.apiLevel.name}") { attributes["data-version-added"] = "${inheritedMember.apiLevel}" td { code { a(href = inheritedMember) { +inheritedMember.attributeRef!!.name } } } td { attributes["width"] = "100%" p { nodeContent(inheritedMember.attributeRef!!, inheritedMember) } } } } private fun TBODY.inheritedMemberRow(inheritedMember: DocumentationNode) { tr(classes = "api apilevel-${inheritedMember.apiLevel.name}") { attributes["data-version-added"] = "${inheritedMember.apiLevel}" val type = inheritedMember.detailOrNull(NodeKind.Type) td { code { type?.let { renderedSignature(it, LanguageService.RenderMode.SUMMARY) } } } td { attributes["width"] = "100%" code { a(href = inheritedMember) { +inheritedMember.name } if (inheritedMember.kind == NodeKind.Function) { shortFunctionParametersList(inheritedMember) } } p { nodeContent(inheritedMember) } } } } private fun FlowContent.expandableSummaryNodeGroupForInheritedMembers( tableId: String, header: String, tableClass: String, superClasses: Set>>, row: TBODY.(inheritedMember: DocumentationNode) -> Unit ) { if (superClasses.none()) return table(classes = tableClass) { attributes["id"] = tableId tbody { developerHeading(header) superClasses.forEach { (superClass, members) -> tr(classes = "api apilevel-${superClass.apiLevel.name}") { td { attributes["colSpan"] = "2" div(classes = "expandable jd-inherited-apis") { span(classes = "expand-control exw-expanded") { +"From class " code { a(href = superClass) { +superClass.name } } } table(classes = "responsive exw-expanded-content") { tbody { members.forEach { inheritedMember -> row(inheritedMember) } } } } } } } } } } private fun FlowContent.summaryNodeGroupForExtensions( header: String, receivers: Set>> ) { if (receivers.none()) return h2 { +header } div { receivers.forEach { table { tr { td { attributes["colSpan"] = "2" +"For " a(href = it.key) { +it.key.name } } } it.value.forEach { node -> tr { if (node.kind != NodeKind.Constructor) { td { modifiers(node) renderedSignature(node.detail(NodeKind.Type), LanguageService.RenderMode.SUMMARY) } } td { div { code { val receiver = node.detailOrNull(NodeKind.Receiver) if (receiver != null) { renderedSignature(receiver.detail(NodeKind.Type), LanguageService.RenderMode.SUMMARY) +"." } a(href = node) { +node.name } shortFunctionParametersList(node) } } nodeSummary(node) } } } } } } } override 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 } } } } } } } ) override fun generateClassIndex(page: Page.ClassIndex) = templateService.composePage( page, htmlConsumer, headContent = { }, bodyContent = { h1 { +"Class Index" } p { +"These are all the API classes. See all " a(href="packages.html") { +"API packages." } } div(classes = "jd-letterlist") { page.classesByFirstLetter.forEach { (letter) -> +"\n " a(href = "#letter_$letter") { +letter } unsafe { raw("  ") } } +"\n " } 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)) { nodeSummary(node) } } } } } } } } ) override fun FlowContent.classHierarchy(superclasses: List) { table(classes = "jd-inheritance-table") { var level = superclasses.size superclasses.forEach { tr { var spaceColumns = max(superclasses.size - 1 - level, 0) while (spaceColumns > 0) { td(classes = "jd-inheritance-space") { +" " } spaceColumns-- } if (it != superclasses.first()) { td(classes = "jd-inheritance-space") { +"   ↳" } } td(classes = "jd-inheritance-class-cell") { attributes["colSpan"] = "$level" qualifiedTypeReference(it) } } level-- } } } override fun FlowContent.subclasses(inheritors: List, direct: Boolean) { if (inheritors.isEmpty()) return // The number of subclasses in collapsed view before truncating and adding a "and xx others". // See https://developer.android.com/reference/android/view/View for an example. val numToShow = 12 table(classes = "jd-sumtable jd-sumtable-subclasses") { tbody { tr { td { div(classes = "expandable") { span(classes = "expand-control") { if (direct) +"Known Direct Subclasses" else +"Known Indirect Subclasses" } div(classes = "showalways") { attributes["id"] = if (direct) "subclasses-direct" else "subclasses-indirect" inheritors.take(numToShow).forEach { inheritor -> a(href = inheritor) { +inheritor.classNodeNameWithOuterClass() } if (inheritor != inheritors.last()) +", " } if (inheritors.size > numToShow) { +"and ${inheritors.size - numToShow} others." } } div(classes = "exw-expanded-content") { attributes["id"] = if (direct) "subclasses-direct-summary" else "subclasses-indirect-summary" table(classes = "jd-sumtable-expando") { inheritors.forEach { inheritor -> tr(classes = "api api-level-${inheritor.apiLevel.name}") { attributes["data-version-added"] = inheritor.apiLevel.name td(classes = "jd-linkcol") { a(href = inheritor) { +inheritor.classNodeNameWithOuterClass() } } td(classes = "jd-descrcol") { attributes["width"] = "100%" nodeSummary(inheritor) } } } } } } } } } } } fun DocumentationNode.firstSentenceOfSummary(): ContentNode { fun Sequence.flatten(): Sequence { return flatMap { when (it) { is ContentParagraph -> it.children.asSequence().flatten() else -> sequenceOf(it) } } } fun ContentNode.firstSentence(): ContentText? = when(this) { is ContentText -> ContentText(text.firstSentence()) else -> null } val elements = sequenceOf(summary).flatten() fun containsDot(it: ContentNode) = (it as? ContentText)?.text?.contains(".") == true val paragraph = ContentParagraph() (elements.takeWhile { !containsDot(it) } + elements.firstOrNull { containsDot(it) }?.firstSentence()).forEach { if (it != null) { paragraph.append(it) } } if (paragraph.isEmpty()) { return ContentEmpty } return paragraph } fun DocumentationNode.firstSentence(): String { val sb = StringBuilder() addContentNodeToStringBuilder(content, sb) return sb.toString().firstSentence() } private fun addContentNodesToStringBuilder(content: List, sb: StringBuilder): Unit = content.forEach { addContentNodeToStringBuilder(it, sb) } private fun addContentNodeToStringBuilder(content: ContentNode, sb: StringBuilder) { when (content) { is ContentText -> sb.appendWith(content.text) is ContentSymbol -> sb.appendWith(content.text) is ContentKeyword -> sb.appendWith(content.text) is ContentIdentifier -> sb.appendWith(content.text) is ContentEntity -> sb.appendWith(content.text) is ContentHeading -> addContentNodesToStringBuilder(content.children, sb) is ContentStrong -> addContentNodesToStringBuilder(content.children, sb) is ContentStrikethrough -> addContentNodesToStringBuilder(content.children, sb) is ContentEmphasis -> addContentNodesToStringBuilder(content.children, sb) is ContentOrderedList -> addContentNodesToStringBuilder(content.children, sb) is ContentUnorderedList -> addContentNodesToStringBuilder(content.children, sb) is ContentListItem -> addContentNodesToStringBuilder(content.children, sb) is ContentCode -> addContentNodesToStringBuilder(content.children, sb) is ContentBlockSampleCode -> addContentNodesToStringBuilder(content.children, sb) is ContentBlockCode -> addContentNodesToStringBuilder(content.children, sb) is ContentParagraph -> addContentNodesToStringBuilder(content.children, sb) is ContentNodeLink -> addContentNodesToStringBuilder(content.children, sb) is ContentBookmark -> addContentNodesToStringBuilder(content.children, sb) is ContentExternalLink -> addContentNodesToStringBuilder(content.children, sb) is ContentLocalLink -> addContentNodesToStringBuilder(content.children, sb) is ContentSection -> { } is ContentBlock -> addContentNodesToStringBuilder(content.children, sb) } } private fun StringBuilder.appendWith(text: String, delimiter: String = " ") { if (this.length == 0) { append(text) } else { append(delimiter) append(text) } } } fun TBODY.developerHeading(header: String) { tr { th { attributes["colSpan"] = "2" +header } } } class DacFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin { override val templateServiceClass: KClass = DevsiteHtmlTemplateService::class override val outlineFactoryClass = DacOutlineFormatter::class override val languageServiceClass = KotlinLanguageService::class override val packageListServiceClass: KClass = JavaLayoutHtmlPackageListService::class override val outputBuilderFactoryClass: KClass = DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl::class override val sampleProcessingService = DevsiteSampleProcessingService::class } class DacAsJavaFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava { override val templateServiceClass: KClass = DevsiteHtmlTemplateService::class override val outlineFactoryClass = DacOutlineFormatter::class override val languageServiceClass = NewJavaLanguageService::class override val packageListServiceClass: KClass = JavaLayoutHtmlPackageListService::class override val outputBuilderFactoryClass: KClass = DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl::class }