diff options
Diffstat (limited to 'core/src/main/kotlin/Formats')
22 files changed, 4949 insertions, 0 deletions
diff --git a/core/src/main/kotlin/Formats/AnalysisComponents.kt b/core/src/main/kotlin/Formats/AnalysisComponents.kt new file mode 100644 index 000000000..d78d4a0c1 --- /dev/null +++ b/core/src/main/kotlin/Formats/AnalysisComponents.kt @@ -0,0 +1,45 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Binder +import org.jetbrains.dokka.* +import org.jetbrains.dokka.KotlinAsJavaElementSignatureProvider +import org.jetbrains.dokka.KotlinElementSignatureProvider +import org.jetbrains.dokka.ElementSignatureProvider +import org.jetbrains.dokka.Samples.DefaultSampleProcessingService +import org.jetbrains.dokka.Samples.SampleProcessingService +import org.jetbrains.dokka.Utilities.bind +import org.jetbrains.dokka.Utilities.toType +import kotlin.reflect.KClass + + +interface DefaultAnalysisComponentServices { + val packageDocumentationBuilderClass: KClass<out PackageDocumentationBuilder> + val javaDocumentationBuilderClass: KClass<out JavaDocumentationBuilder> + val sampleProcessingService: KClass<out SampleProcessingService> + val elementSignatureProvider: KClass<out ElementSignatureProvider> +} + +interface DefaultAnalysisComponent : FormatDescriptorAnalysisComponent, DefaultAnalysisComponentServices { + override fun configureAnalysis(binder: Binder): Unit = with(binder) { + bind<ElementSignatureProvider>() toType elementSignatureProvider + bind<PackageDocumentationBuilder>() toType packageDocumentationBuilderClass + bind<JavaDocumentationBuilder>() toType javaDocumentationBuilderClass + bind<SampleProcessingService>() toType sampleProcessingService + } +} + + +object KotlinAsJava : DefaultAnalysisComponentServices { + override val packageDocumentationBuilderClass = KotlinAsJavaDocumentationBuilder::class + override val javaDocumentationBuilderClass = JavaPsiDocumentationBuilder::class + override val sampleProcessingService = DefaultSampleProcessingService::class + override val elementSignatureProvider = KotlinAsJavaElementSignatureProvider::class +} + + +object KotlinAsKotlin : DefaultAnalysisComponentServices { + override val packageDocumentationBuilderClass = KotlinPackageDocumentationBuilder::class + override val javaDocumentationBuilderClass = KotlinJavaDocumentationBuilder::class + override val sampleProcessingService = DefaultSampleProcessingService::class + override val elementSignatureProvider = KotlinElementSignatureProvider::class +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/DacHtmlFormat.kt b/core/src/main/kotlin/Formats/DacHtmlFormat.kt new file mode 100644 index 000000000..e2399435b --- /dev/null +++ b/core/src/main/kotlin/Formats/DacHtmlFormat.kt @@ -0,0 +1,949 @@ +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<Appendable>, 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 <td> and + // <code> 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<DocumentationNode>, + 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 <T> FlowContent.summaryNodeGroup( + nodes: Iterable<T>, + 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<Map.Entry<DocumentationNode, List<DocumentationNode>>>, + 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<Map.Entry<DocumentationNode, List<DocumentationNode>>> + ) { + 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<DocumentationNode>) { + 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<DocumentationNode>, 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<ContentNode>.flatten(): Sequence<ContentNode> { + 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<ContentNode>, 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<out JavaLayoutHtmlTemplateService> = DevsiteHtmlTemplateService::class + + override val outlineFactoryClass = DacOutlineFormatter::class + override val languageServiceClass = KotlinLanguageService::class + override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class + override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl::class + override val sampleProcessingService = DevsiteSampleProcessingService::class +} + + +class DacAsJavaFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava { + override val templateServiceClass: KClass<out JavaLayoutHtmlTemplateService> = DevsiteHtmlTemplateService::class + + override val outlineFactoryClass = DacOutlineFormatter::class + override val languageServiceClass = NewJavaLanguageService::class + override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class + override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl::class +} diff --git a/core/src/main/kotlin/Formats/DacOutlineService.kt b/core/src/main/kotlin/Formats/DacOutlineService.kt new file mode 100644 index 000000000..e249c39f7 --- /dev/null +++ b/core/src/main/kotlin/Formats/DacOutlineService.kt @@ -0,0 +1,395 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Inject +import org.jetbrains.dokka.* +import java.net.URI +import com.google.inject.name.Named +import org.jetbrains.kotlin.cfg.pseudocode.AllTypes + + +interface DacOutlineFormatService { + fun computeOutlineURI(node: DocumentationNode): URI + fun format(to: Appendable, node: DocumentationNode) +} + +class DacOutlineFormatter @Inject constructor( + uriProvider: JavaLayoutHtmlUriProvider, + languageService: LanguageService, + @Named("dacRoot") dacRoot: String, + @Named("generateClassIndex") generateClassIndex: Boolean, + @Named("generatePackageIndex") generatePackageIndex: Boolean +) : JavaLayoutHtmlFormatOutlineFactoryService { + val tocOutline = TocOutlineService(uriProvider, languageService, dacRoot, generateClassIndex, generatePackageIndex) + val outlines = listOf(tocOutline) + + override fun generateOutlines(outputProvider: (URI) -> Appendable, nodes: Iterable<DocumentationNode>) { + for (node in nodes) { + for (outline in outlines) { + val uri = outline.computeOutlineURI(node) + val output = outputProvider(uri) + outline.format(output, node) + } + } + } +} + +/** + * Outline service for generating a _toc.yaml file, responsible for pointing to the paths of each + * index.html file in the doc tree. + */ +class BookOutlineService( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val dacRoot: String, + val generateClassIndex: Boolean, + val generatePackageIndex: Boolean +) : DacOutlineFormatService { + override fun computeOutlineURI(node: DocumentationNode): URI = uriProvider.outlineRootUri(node).resolve("_book.yaml") + + override fun format(to: Appendable, node: DocumentationNode) { + appendOutline(to, listOf(node)) + } + + var outlineLevel = 0 + + /** Appends formatted outline to [StringBuilder](to) using specified [location] */ + fun appendOutline(to: Appendable, nodes: Iterable<DocumentationNode>) { + if (outlineLevel == 0) to.appendln("reference:") + for (node in nodes) { + appendOutlineHeader(node, to) + val subPackages = node.members.filter { + it.kind == NodeKind.Package + } + if (subPackages.any()) { + val sortedMembers = subPackages.sortedBy { it.name.toLowerCase() } + appendOutlineLevel(to) { + appendOutline(to, sortedMembers) + } + } + + } + } + + fun appendOutlineHeader(node: DocumentationNode, to: Appendable) { + if (node is DocumentationModule) { + to.appendln("- title: Package Index") + to.appendln(" path: $dacRoot${uriProvider.outlineRootUri(node).resolve("packages.html")}") + to.appendln(" status_text: no-toggle") + } else { + to.appendln("- title: ${languageService.renderName(node)}") + to.appendln(" path: $dacRoot${uriProvider.mainUriOrWarn(node)}") + to.appendln(" status_text: no-toggle") + } + } + + fun appendOutlineLevel(to: Appendable, body: () -> Unit) { + outlineLevel++ + body() + outlineLevel-- + } +} + +/** + * Outline service for generating a _toc.yaml file, responsible for pointing to the paths of each + * index.html file in the doc tree. + */ +class TocOutlineService( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val dacRoot: String, + val generateClassIndex: Boolean, + val generatePackageIndex: Boolean +) : DacOutlineFormatService { + override fun computeOutlineURI(node: DocumentationNode): URI = uriProvider.outlineRootUri(node).resolve("_toc.yaml") + + override fun format(to: Appendable, node: DocumentationNode) { + appendOutline(to, listOf(node)) + } + + var outlineLevel = 0 + + /** Appends formatted outline to [StringBuilder](to) using specified [location] */ + fun appendOutline(to: Appendable, nodes: Iterable<DocumentationNode>) { + if (outlineLevel == 0) to.appendln("toc:") + for (node in nodes) { + appendOutlineHeader(node, to) + val subPackages = node.members.filter { + it.kind == NodeKind.Package + } + if (subPackages.any()) { + val sortedMembers = subPackages.sortedBy { it.nameWithOuterClass() } + appendOutlineLevel { + appendOutline(to, sortedMembers) + } + } + } + } + + fun appendOutlineHeader(node: DocumentationNode, to: Appendable) { + if (node is DocumentationModule) { + if (generateClassIndex) { + node.members.filter { it.kind == NodeKind.AllTypes }.firstOrNull()?.let { + to.appendln("- title: Class Index") + to.appendln(" path: $dacRoot${uriProvider.outlineRootUri(it).resolve("classes.html")}") + to.appendln() + } + } + if (generatePackageIndex) { + to.appendln("- title: Package Index") + to.appendln(" path: $dacRoot${uriProvider.outlineRootUri(node).resolve("packages.html")}") + to.appendln() + } + } else if (node.kind != NodeKind.AllTypes && !(node is DocumentationModule)) { + to.appendln("- title: ${languageService.renderName(node)}") + to.appendln(" path: $dacRoot${uriProvider.mainUriOrWarn(node)}") + to.appendln() + var addedSectionHeader = false + for (kind in NodeKind.classLike) { + val members = node.getMembersOfKinds(kind) + if (members.isNotEmpty()) { + if (!addedSectionHeader) { + to.appendln(" section:") + addedSectionHeader = true + } + to.appendln(" - title: ${kind.pluralizedName()}") + to.appendln() + to.appendln(" section:") + members.sortedBy { it.nameWithOuterClass().toLowerCase() }.forEach { member -> + to.appendln(" - title: ${languageService.renderNameWithOuterClass(member)}") + to.appendln(" path: $dacRoot${uriProvider.mainUriOrWarn(member)}".trimEnd('#')) + to.appendln() + } + } + } + to.appendln().appendln() + } + } + + fun appendOutlineLevel(body: () -> Unit) { + outlineLevel++ + body() + outlineLevel-- + } +} + +class DacNavOutlineService constructor( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val dacRoot: String +) : DacOutlineFormatService { + override fun computeOutlineURI(node: DocumentationNode): URI = + uriProvider.outlineRootUri(node).resolve("navtree_data.js") + + override fun format(to: Appendable, node: DocumentationNode) { + to.append("var NAVTREE_DATA = ").appendNavTree(node.members).append(";") + } + + private fun Appendable.appendNavTree(nodes: Iterable<DocumentationNode>): Appendable { + append("[ ") + var first = true + for (node in nodes) { + if (!first) append(", ") + first = false + val interfaces = node.getMembersOfKinds(NodeKind.Interface) + val classes = node.getMembersOfKinds(NodeKind.Class) + val objects = node.getMembersOfKinds(NodeKind.Object) + val annotations = node.getMembersOfKinds(NodeKind.AnnotationClass) + val enums = node.getMembersOfKinds(NodeKind.Enum) + val exceptions = node.getMembersOfKinds(NodeKind.Exception) + + append("[ \"${node.name}\", \"$dacRoot${uriProvider.tryGetMainUri(node)}\", [ ") + var needComma = false + if (interfaces.firstOrNull() != null) { + appendNavTreePagesOfKind("Interfaces", interfaces) + needComma = true + } + if (classes.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Classes", classes) + needComma = true + } + if (objects.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Objects", objects) + } + if (annotations.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Annotations", annotations) + needComma = true + } + if (enums.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Enums", enums) + needComma = true + } + if (exceptions.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Exceptions", exceptions) + } + append(" ] ]") + } + append(" ]") + return this + } + + private fun Appendable.appendNavTreePagesOfKind(kindTitle: String, + nodesOfKind: Iterable<DocumentationNode>): Appendable { + append("[ \"$kindTitle\", null, [ ") + var started = false + for (node in nodesOfKind) { + if (started) append(", ") + started = true + appendNavTreeChild(node) + } + append(" ], null, null ]") + return this + } + + private fun Appendable.appendNavTreeChild(node: DocumentationNode): Appendable { + append("[ \"${node.nameWithOuterClass()}\", \"${dacRoot}${uriProvider.tryGetMainUri(node)}\"") + append(", null, null, null ]") + return this + } +} + +class DacSearchOutlineService( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val dacRoot: String +) : DacOutlineFormatService { + + override fun computeOutlineURI(node: DocumentationNode): URI = + uriProvider.outlineRootUri(node).resolve("lists.js") + + override fun format(to: Appendable, node: DocumentationNode) { + val pageNodes = node.getAllPageNodes() + var id = 0 + to.append("var KTX_CORE_DATA = [\n") + var first = true + for (pageNode in pageNodes) { + if (pageNode.kind == NodeKind.Module) continue + if (!first) to.append(", \n") + first = false + to.append(" { " + + "id:$id, " + + "label:\"${pageNode.qualifiedName()}\", " + + "link:\"${dacRoot}${uriProvider.tryGetMainUri(pageNode)}\", " + + "type:\"${pageNode.getClassOrPackage()}\", " + + "deprecated:\"false\" }") + id++ + } + to.append("\n];") + } + + private fun DocumentationNode.getClassOrPackage(): String = + if (hasOwnPage()) + "class" + else if (isPackage()) { + "package" + } else { + "" + } + + private fun DocumentationNode.getAllPageNodes(): Iterable<DocumentationNode> { + val allPageNodes = mutableListOf<DocumentationNode>() + recursiveSetAllPageNodes(allPageNodes) + return allPageNodes + } + + private fun DocumentationNode.recursiveSetAllPageNodes( + allPageNodes: MutableList<DocumentationNode>) { + for (child in members) { + if (child.hasOwnPage() || child.isPackage()) { + allPageNodes.add(child) + child.qualifiedName() + child.recursiveSetAllPageNodes(allPageNodes) + } + } + } + +} + +/** + * Return all children of the node who are one of the selected `NodeKind`s. It recursively fetches + * all offspring, not just immediate children. + */ +fun DocumentationNode.getMembersOfKinds(vararg kinds: NodeKind): MutableList<DocumentationNode> { + val membersOfKind = mutableListOf<DocumentationNode>() + recursiveSetMembersOfKinds(kinds, membersOfKind) + return membersOfKind +} + +private fun DocumentationNode.recursiveSetMembersOfKinds(kinds: Array<out NodeKind>, + membersOfKind: MutableList<DocumentationNode>) { + for (member in members) { + if (member.kind in kinds) { + membersOfKind.add(member) + } + member.recursiveSetMembersOfKinds(kinds, membersOfKind) + } +} + +/** + * Returns whether or not this node owns a page. The criteria for whether a node owns a page is + * similar to the way javadoc is structured. Classes, Interfaces, Enums, AnnotationClasses, + * Exceptions, and Objects (Kotlin-specific) meet the criteria. + */ +fun DocumentationNode.hasOwnPage() = + kind == NodeKind.Class || kind == NodeKind.Interface || kind == NodeKind.Enum || + kind == NodeKind.AnnotationClass || kind == NodeKind.Exception || + kind == NodeKind.Object + +/** + * In most cases, this returns the short name of the `Type`. When the Type is an inner Type, it + * prepends the name with the containing Type name(s). + * + * For example, if you have a class named OuterClass and an inner class named InnerClass, this would + * return OuterClass.InnerClass. + * + */ +fun DocumentationNode.nameWithOuterClass(): String { + val nameBuilder = StringBuilder(name) + var parent = owner + if (hasOwnPage()) { + while (parent != null && parent.hasOwnPage()) { + nameBuilder.insert(0, "${parent.name}.") + parent = parent.owner + } + } + return nameBuilder.toString() +} + +/** + * Return whether the node is a package. + */ +fun DocumentationNode.isPackage(): Boolean { + return kind == NodeKind.Package +} + +/** + * Return the 'page owner' of this node. `DocumentationNode.hasOwnPage()` defines the criteria for + * a page owner. If this node is not a page owner, then it iterates up through its ancestors to + * find the first page owner. + */ +fun DocumentationNode.pageOwner(): DocumentationNode { + if (hasOwnPage() || owner == null) { + return this + } else { + var parent: DocumentationNode = owner!! + while (!parent.hasOwnPage() && !parent.isPackage()) { + parent = parent.owner!! + } + return parent + } +} + +fun NodeKind.pluralizedName() = when(this) { + NodeKind.Class -> "Classes" + NodeKind.Interface -> "Interfaces" + NodeKind.AnnotationClass -> "Annotations" + NodeKind.Enum -> "Enums" + NodeKind.Exception -> "Exceptions" + NodeKind.Object -> "Objects" + NodeKind.TypeAlias -> "TypeAliases" + else -> "${name}s" +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/ExtraOutlineServices.kt b/core/src/main/kotlin/Formats/ExtraOutlineServices.kt new file mode 100644 index 000000000..e4eeac01a --- /dev/null +++ b/core/src/main/kotlin/Formats/ExtraOutlineServices.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka + +import java.io.File + +/** + * Outline service that is responsible for generating a single outline format. + * + * TODO: port existing implementations of ExtraOutlineService to OutlineService, and remove this. + */ +interface ExtraOutlineService { + fun getFileName(): String + fun getFile(location: Location): File + fun format(node: DocumentationNode): String +} + +/** + * Holder of all of the extra outline services needed for a StandardFormat, in addition to the main + * [OutlineFormatService]. + */ +abstract class ExtraOutlineServices(vararg val services: ExtraOutlineService) diff --git a/core/src/main/kotlin/Formats/FormatDescriptor.kt b/core/src/main/kotlin/Formats/FormatDescriptor.kt new file mode 100644 index 000000000..b497fb0f5 --- /dev/null +++ b/core/src/main/kotlin/Formats/FormatDescriptor.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Binder +import org.jetbrains.dokka.* +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 kotlin.reflect.KClass + + +interface FormatDescriptorAnalysisComponent { + fun configureAnalysis(binder: Binder) +} + +interface FormatDescriptorOutputComponent { + fun configureOutput(binder: Binder) +} + +interface FormatDescriptor : FormatDescriptorAnalysisComponent, FormatDescriptorOutputComponent + + +abstract class FileGeneratorBasedFormatDescriptor : FormatDescriptor { + + override fun configureOutput(binder: Binder): Unit = with(binder) { + bind<Generator>() toType NodeLocationAwareGenerator::class + bind<NodeLocationAwareGenerator>() toType generatorServiceClass + + bind<LanguageService>() toType languageServiceClass + + lazyBind<OutlineFormatService>() toOptional (outlineServiceClass) + lazyBind<FormatService>() toOptional formatServiceClass + lazyBind<PackageListService>() toOptional packageListServiceClass + } + + abstract val formatServiceClass: KClass<out FormatService>? + abstract val outlineServiceClass: KClass<out OutlineFormatService>? + abstract val generatorServiceClass: KClass<out FileGenerator> + abstract val packageListServiceClass: KClass<out PackageListService>? + + open val languageServiceClass: KClass<out LanguageService> = KotlinLanguageService::class +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/FormatService.kt b/core/src/main/kotlin/Formats/FormatService.kt new file mode 100644 index 000000000..63f25008f --- /dev/null +++ b/core/src/main/kotlin/Formats/FormatService.kt @@ -0,0 +1,32 @@ +package org.jetbrains.dokka + +/** + * Abstract representation of a formatting service used to output documentation in desired format + * + * Bundled Formatters: + * * [HtmlFormatService] – outputs documentation to HTML format + * * [MarkdownFormatService] – outputs documentation in Markdown format + */ +interface FormatService { + /** Returns extension for output files */ + val extension: String + + /** extension which will be used for internal and external linking */ + val linkExtension: String + get() = extension + + fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder + + fun enumerateSupportFiles(callback: (resource: String, targetPath: String) -> Unit) { + } +} + +interface FormattedOutputBuilder { + /** Appends formatted content to [StringBuilder](to) using specified [location] */ + fun appendNodes(nodes: Iterable<DocumentationNode>) +} + +/** Format content to [String] using specified [location] */ +fun FormatService.format(location: Location, nodes: Iterable<DocumentationNode>): String = StringBuilder().apply { + createOutputBuilder(this, location).appendNodes(nodes) +}.toString() diff --git a/core/src/main/kotlin/Formats/GFMFormatService.kt b/core/src/main/kotlin/Formats/GFMFormatService.kt new file mode 100644 index 000000000..036ec8564 --- /dev/null +++ b/core/src/main/kotlin/Formats/GFMFormatService.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName + +open class GFMOutputBuilder( + to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String> +) : MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) { + override fun appendTable(vararg columns: String, body: () -> Unit) { + to.appendln(columns.joinToString(" | ", "| ", " |")) + to.appendln("|" + "---|".repeat(columns.size)) + body() + } + + override fun appendUnorderedList(body: () -> Unit) { + if (inTableCell) { + wrapInTag("ul", body) + } else { + super.appendUnorderedList(body) + } + } + + override fun appendOrderedList(body: () -> Unit) { + if (inTableCell) { + wrapInTag("ol", body) + } else { + super.appendOrderedList(body) + } + } + + override fun appendListItem(body: () -> Unit) { + if (inTableCell) { + wrapInTag("li", body) + } else { + super.appendListItem(body) + } + } +} + +open class GFMFormatService( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + linkExtension: String, + impliedPlatforms: List<String> +) : MarkdownFormatService(generator, signatureGenerator, linkExtension, impliedPlatforms) { + + @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String> + ) : this(generator, signatureGenerator, "md", impliedPlatforms) + + override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder = + GFMOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +} diff --git a/core/src/main/kotlin/Formats/HtmlFormatService.kt b/core/src/main/kotlin/Formats/HtmlFormatService.kt new file mode 100644 index 000000000..0ad946be2 --- /dev/null +++ b/core/src/main/kotlin/Formats/HtmlFormatService.kt @@ -0,0 +1,168 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName +import java.io.File + +open class HtmlOutputBuilder(to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String>, + val templateService: HtmlTemplateService) + : StructuredOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +{ + override fun appendText(text: String) { + to.append(text.htmlEscape()) + } + + override fun appendSymbol(text: String) { + to.append("<span class=\"symbol\">${text.htmlEscape()}</span>") + } + + override fun appendKeyword(text: String) { + to.append("<span class=\"keyword\">${text.htmlEscape()}</span>") + } + + override fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) { + val id = signature?.let { " id=\"$it\"" }.orEmpty() + to.append("<span class=\"identifier\"$id>${text.htmlEscape()}</span>") + } + + override fun appendBlockCode(language: String, body: () -> Unit) { + val openTags = if (language.isNotBlank()) + "<pre><code class=\"lang-$language\">" + else + "<pre><code>" + wrap(openTags, "</code></pre>", body) + } + + override fun appendHeader(level: Int, body: () -> Unit) = + wrapInTag("h$level", body, newlineBeforeOpen = true, newlineAfterClose = true) + override fun appendParagraph(body: () -> Unit) = + wrapInTag("p", body, newlineBeforeOpen = true, newlineAfterClose = true) + + override fun appendSoftParagraph(body: () -> Unit) = appendParagraph(body) + + override fun appendLine() { + to.appendln("<br/>") + } + + override fun appendAnchor(anchor: String) { + to.appendln("<a name=\"${anchor.htmlEscape()}\"></a>") + } + + override fun appendTable(vararg columns: String, body: () -> Unit) = + wrapInTag("table", body, newlineAfterOpen = true, newlineAfterClose = true) + override fun appendTableBody(body: () -> Unit) = + wrapInTag("tbody", body, newlineAfterOpen = true, newlineAfterClose = true) + override fun appendTableRow(body: () -> Unit) = + wrapInTag("tr", body, newlineAfterOpen = true, newlineAfterClose = true) + override fun appendTableCell(body: () -> Unit) = + wrapInTag("td", body, newlineAfterOpen = true, newlineAfterClose = true) + + override fun appendLink(href: String, body: () -> Unit) = wrap("<a href=\"$href\">", "</a>", body) + + override fun appendStrong(body: () -> Unit) = wrapInTag("strong", body) + override fun appendEmphasis(body: () -> Unit) = wrapInTag("em", body) + override fun appendStrikethrough(body: () -> Unit) = wrapInTag("s", body) + override fun appendCode(body: () -> Unit) = wrapInTag("code", body) + + override fun appendUnorderedList(body: () -> Unit) = wrapInTag("ul", body, newlineAfterClose = true) + override fun appendOrderedList(body: () -> Unit) = wrapInTag("ol", body, newlineAfterClose = true) + override fun appendListItem(body: () -> Unit) = wrapInTag("li", body, newlineAfterClose = true) + + override fun appendBreadcrumbSeparator() { + to.append(" / ") + } + + override fun appendNodes(nodes: Iterable<DocumentationNode>) { + templateService.appendHeader(to, getPageTitle(nodes), generator.relativePathToRoot(location)) + super.appendNodes(nodes) + templateService.appendFooter(to) + } + + override fun appendNonBreakingSpace() { + to.append(" ") + } + + override fun ensureParagraph() { + + } +} + +open class HtmlFormatService @Inject constructor(generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + val templateService: HtmlTemplateService, + @Named(impliedPlatformsName) val impliedPlatforms: List<String>) +: StructuredFormatService(generator, signatureGenerator, "html"), OutlineFormatService { + + override fun enumerateSupportFiles(callback: (String, String) -> Unit) { + callback("/dokka/styles/style.css", "style.css") + } + + override fun createOutputBuilder(to: StringBuilder, location: Location) = + HtmlOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms, templateService) + + override fun appendOutline(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + templateService.appendHeader(to, "Module Contents", generator.relativePathToRoot(location)) + super.appendOutline(location, to, nodes) + templateService.appendFooter(to) + } + + override fun getOutlineFileName(location: Location): File { + return File("${location.path}-outline.html") + } + + override fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) { + val link = ContentNodeDirectLink(node) + link.append(languageService.render(node, LanguageService.RenderMode.FULL)) + val tempBuilder = StringBuilder() + createOutputBuilder(tempBuilder, location).appendContent(link) + to.appendln("<a href=\"${location.path}\">$tempBuilder</a><br/>") + } + + override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) { + to.appendln("<ul>") + body() + to.appendln("</ul>") + } +} + +fun getPageTitle(nodes: Iterable<DocumentationNode>): String? { + val breakdownByLocation = nodes.groupBy { node -> formatPageTitle(node) } + return breakdownByLocation.keys.singleOrNull() +} + +fun formatPageTitle(node: DocumentationNode): String { + val path = node.path + val moduleName = path.first().name + if (path.size == 1) { + return moduleName + } + + val qName = qualifiedNameForPageTitle(node) + return qName + " - " + moduleName +} + +private fun qualifiedNameForPageTitle(node: DocumentationNode): String { + if (node.kind == NodeKind.Package) { + var packageName = node.qualifiedName() + if (packageName.isEmpty()) { + packageName = "root package" + } + return packageName + } + + val path = node.path + var pathFromToplevelMember = path.dropWhile { it.kind !in NodeKind.classLike } + if (pathFromToplevelMember.isEmpty()) { + pathFromToplevelMember = path.dropWhile { it.kind != NodeKind.Property && it.kind != NodeKind.Function } + } + if (pathFromToplevelMember.isNotEmpty()) { + return pathFromToplevelMember.map { it.name }.filter { it.length > 0 }.joinToString(".") + } + return node.qualifiedName() +} diff --git a/core/src/main/kotlin/Formats/HtmlTemplateService.kt b/core/src/main/kotlin/Formats/HtmlTemplateService.kt new file mode 100644 index 000000000..a65a7b18c --- /dev/null +++ b/core/src/main/kotlin/Formats/HtmlTemplateService.kt @@ -0,0 +1,38 @@ +package org.jetbrains.dokka + +import java.io.File + +interface HtmlTemplateService { + fun appendHeader(to: StringBuilder, title: String?, basePath: File) + fun appendFooter(to: StringBuilder) + + companion object { + fun default(css: String? = null): HtmlTemplateService { + return object : HtmlTemplateService { + override fun appendFooter(to: StringBuilder) { + if (!to.endsWith('\n')) { + to.append('\n') + } + to.appendln("</BODY>") + to.appendln("</HTML>") + } + override fun appendHeader(to: StringBuilder, title: String?, basePath: File) { + to.appendln("<HTML>") + to.appendln("<HEAD>") + to.appendln("<meta charset=\"UTF-8\">") + if (title != null) { + to.appendln("<title>$title</title>") + } + if (css != null) { + val cssPath = basePath.resolve(css).toUnixString() + to.appendln("<link rel=\"stylesheet\" href=\"$cssPath\">") + } + to.appendln("</HEAD>") + to.appendln("<BODY>") + } + } + } + } +} + + diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt new file mode 100644 index 000000000..b94886693 --- /dev/null +++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt @@ -0,0 +1,141 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Binder +import kotlinx.html.* +import org.jetbrains.dokka.* +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 java.net.URI +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 + bind<PackageListService>() toType packageListServiceClass + bind<JavaLayoutHtmlFormatOutputBuilderFactory>() toType outputBuilderFactoryClass + } + + val generatorServiceClass = JavaLayoutHtmlFormatGenerator::class + abstract val languageServiceClass: KClass<out LanguageService> + abstract val templateServiceClass: KClass<out JavaLayoutHtmlTemplateService> + abstract val outlineFactoryClass: KClass<out JavaLayoutHtmlFormatOutlineFactoryService>? + abstract val packageListServiceClass: KClass<out PackageListService> + abstract val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> +} + +class JavaLayoutHtmlFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin { + override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = JavaLayoutHtmlFormatOutputBuilderFactoryImpl::class + override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class + override val languageServiceClass = KotlinLanguageService::class + override val templateServiceClass = JavaLayoutHtmlTemplateService.Default::class + override val outlineFactoryClass = null +} + +class JavaLayoutHtmlAsJavaFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava { + override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = JavaLayoutHtmlFormatOutputBuilderFactoryImpl::class + override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class + override val languageServiceClass = NewJavaLanguageService::class + override val templateServiceClass = JavaLayoutHtmlTemplateService.Default::class + override val outlineFactoryClass = null +} + +interface JavaLayoutHtmlFormatOutlineFactoryService { + fun generateOutlines(outputProvider: (URI) -> Appendable, nodes: Iterable<DocumentationNode>) +} + + +interface JavaLayoutHtmlUriProvider { + fun tryGetContainerUri(node: DocumentationNode): URI? + fun tryGetMainUri(node: DocumentationNode): URI? + fun tryGetOutlineRootUri(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 outlineRootUri(node: DocumentationNode): URI = tryGetOutlineRootUri(node) ?: error("Unsupported ${node.kind}") + + + fun linkTo(to: DocumentationNode, from: URI): String { + return mainUri(to).relativeTo(from).toString() + } + + fun linkToFromOutline(to: DocumentationNode, from: URI): String { + return outlineRootUri(to).relativeTo(from).toString() + } + + fun mainUriOrWarn(node: DocumentationNode): URI? = tryGetMainUri(node) ?: (null).also { + AssertionError("Not implemented mainUri for ${node.kind} (${node})").printStackTrace() + } +} + + +interface JavaLayoutHtmlTemplateService { + fun composePage( + page: JavaLayoutHtmlFormatOutputBuilder.Page, + tagConsumer: TagConsumer<Appendable>, + headContent: HEAD.() -> Unit, + bodyContent: BODY.() -> Unit + ) + + class Default : JavaLayoutHtmlTemplateService { + override fun composePage( + page: JavaLayoutHtmlFormatOutputBuilder.Page, + tagConsumer: TagConsumer<Appendable>, + headContent: HEAD.() -> Unit, + bodyContent: BODY.() -> Unit + ) { + tagConsumer.html { + head { + meta(charset = "UTF-8") + headContent() + } + body(block = bodyContent) + } + } + } +} + +val DocumentationNode.companion get() = members(NodeKind.Object).find { it.details(NodeKind.Modifier).any { it.name == "companion" } } + +fun DocumentationNode.signatureForAnchor(logger: DokkaLogger): String { + + fun StringBuilder.appendReceiverIfSo() { + detailOrNull(NodeKind.Receiver)?.let { + append("(") + append(it.detail(NodeKind.Type).qualifiedNameFromType()) + append(").") + } + } + + return when (kind) { + NodeKind.Function, NodeKind.Constructor, NodeKind.CompanionObjectFunction -> buildString { + if (kind == NodeKind.CompanionObjectFunction) { + append("Companion.") + } + appendReceiverIfSo() + append(prettyName) + details(NodeKind.Parameter).joinTo(this, prefix = "(", postfix = ")") { it.detail(NodeKind.Type).qualifiedNameFromType() } + } + NodeKind.Property, NodeKind.CompanionObjectProperty -> buildString { + if (kind == NodeKind.CompanionObjectProperty) { + append("Companion.") + } + appendReceiverIfSo() + append(name) + append(":") + append(detail(NodeKind.Type).qualifiedNameFromType()) + } + NodeKind.TypeParameter, NodeKind.Parameter -> this.detail(NodeKind.Signature).name // Todo Why not signatureForAnchor + NodeKind.Field -> name + NodeKind.EnumItem -> "ENUM_VALUE:$name" + NodeKind.Attribute -> "attr_$name" + else -> "Not implemented signatureForAnchor $this".also { logger.warn(it) } + } +} + diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt new file mode 100644 index 000000000..59d898a2a --- /dev/null +++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt @@ -0,0 +1,1171 @@ +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 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<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.kind == 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 ?: "" diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt new file mode 100644 index 000000000..9928a8e9e --- /dev/null +++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt @@ -0,0 +1,165 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatOutputBuilder.Page +import org.jetbrains.dokka.NodeKind.Companion.classLike +import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult +import java.io.BufferedWriter +import java.io.File +import java.net.URI + +class JavaLayoutHtmlFormatGenerator @Inject constructor( + @Named("outputDir") val root: File, + val packageListService: PackageListService, + val outputBuilderFactoryService: JavaLayoutHtmlFormatOutputBuilderFactory, + private val options: DocumentationOptions, + val logger: DokkaLogger, + @Named("outlineRoot") val outlineRoot: String +) : Generator, JavaLayoutHtmlUriProvider { + + @set:Inject(optional = true) + var outlineFactoryService: JavaLayoutHtmlFormatOutlineFactoryService? = null + + fun createOutputBuilderForNode(node: DocumentationNode, output: Appendable) = outputBuilderFactoryService.createOutputBuilder(output, node) + + fun DocumentationNode.getOwnerOrReport() = owner ?: run { + error("Owner not found for $this") + } + + override fun tryGetContainerUri(node: DocumentationNode): URI? { + return when (node.kind) { + NodeKind.Module -> URI("/").resolve(node.name + "/") + NodeKind.Package -> tryGetContainerUri(node.getOwnerOrReport())?.resolve(node.name.replace('.', '/') + '/') + NodeKind.GroupNode -> tryGetContainerUri(node.getOwnerOrReport()) + in NodeKind.classLike -> tryGetContainerUri(node.getOwnerOrReport())?.resolve("${node.classNodeNameWithOuterClass()}.html") + else -> null + } + } + + override fun tryGetMainUri(node: DocumentationNode): URI? { + return when (node.kind) { + NodeKind.Package -> tryGetContainerUri(node)?.resolve("package-summary.html") + in NodeKind.classLike -> tryGetContainerUri(node)?.resolve("#") + in NodeKind.memberLike -> { + val owner = if (node.owner?.kind != NodeKind.ExternalClass) node.owner else node.owner?.owner + if (owner!!.kind in classLike && + (node.kind == NodeKind.CompanionObjectProperty || node.kind == NodeKind.CompanionObjectFunction) && + owner.companion != null + ) { + val signature = node.detail(NodeKind.Signature) + val originalFunction = owner.companion!!.members.first { it.detailOrNull(NodeKind.Signature)?.name == signature.name } + tryGetMainUri(owner.companion!!)?.resolveInPage(originalFunction) + } else { + tryGetMainUri(owner)?.resolveInPage(node) + } + } + NodeKind.TypeParameter, NodeKind.Parameter -> node.path.asReversed().drop(1).firstNotNullResult(this::tryGetMainUri)?.resolveInPage(node) + NodeKind.AllTypes -> outlineRootUri(node).resolve ("classes.html") + else -> null + } + } + + override fun tryGetOutlineRootUri(node: DocumentationNode): URI? { + return when(node.kind) { + NodeKind.AllTypes -> tryGetContainerUri(node.getOwnerOrReport()) + else -> tryGetContainerUri(node) + }?.resolve(outlineRoot) + } + + fun URI.resolveInPage(node: DocumentationNode): URI = resolve("#${node.signatureForAnchor(logger).anchorEncoded()}") + + fun buildClass(node: DocumentationNode, parentDir: File) { + val fileForClass = parentDir.resolve(node.classNodeNameWithOuterClass() + ".html") + fileForClass.bufferedWriter().use { + createOutputBuilderForNode(node, it).generatePage(Page.ClassPage(node)) + } + for (memberClass in node.members.filter { it.kind in NodeKind.classLike }) { + buildClass(memberClass, parentDir) + } + } + + fun buildPackage(node: DocumentationNode, parentDir: File) { + assert(node.kind == NodeKind.Package) + var members = node.members + val directoryForPackage = parentDir.resolve(node.name.replace('.', File.separatorChar)) + directoryForPackage.mkdirsOrFail() + + directoryForPackage.resolve("package-summary.html").bufferedWriter().use { + createOutputBuilderForNode(node, it).generatePage(Page.PackagePage(node)) + } + + members.filter { it.kind == NodeKind.GroupNode }.forEach { + members += it.members + } + members.filter { it.kind in NodeKind.classLike }.forEach { + buildClass(it, directoryForPackage) + } + } + + fun buildClassIndex(node: DocumentationNode, parentDir: File) { + val file = parentDir.resolve("classes.html") + file.bufferedWriter().use { + createOutputBuilderForNode(node, it).generatePage(Page.ClassIndex(node)) + } + } + + fun buildPackageIndex(module: DocumentationNode, nodes: List<DocumentationNode>, parentDir: File) { + val file = parentDir.resolve("packages.html") + file.bufferedWriter().use { + val uri = outlineRootUri(module).resolve("packages.html") + outputBuilderFactoryService.createOutputBuilder(it, uri) + .generatePage(Page.PackageIndex(nodes)) + } + } + + override fun buildPages(nodes: Iterable<DocumentationNode>) { + val module = nodes.single() + + val moduleRoot = root.resolve(module.name) + val packages = module.members.filter { it.kind == NodeKind.Package } + packages.forEach { buildPackage(it, moduleRoot) } + val outlineRootFile = moduleRoot.resolve(outlineRoot) + if (options.generateClassIndexPage) { + buildClassIndex(module.members.single { it.kind == NodeKind.AllTypes }, outlineRootFile) + } + + if (options.generatePackageIndexPage) { + buildPackageIndex(module, packages, outlineRootFile) + } + } + + override fun buildOutlines(nodes: Iterable<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("/")) + file.parentFile.mkdirsOrFail() + val writer = file.bufferedWriter() + uriToWriter[normalized] = writer + return writer + } + + outlineFactoryService?.generateOutlines(::provideOutput, nodes) + + uriToWriter.values.forEach { it.close() } + } + + override fun buildSupportFiles() {} + + override fun buildPackageList(nodes: Iterable<DocumentationNode>) { + nodes.filter { it.kind == NodeKind.Module }.forEach { module -> + val moduleRoot = root.resolve(module.name) + val packageListFile = moduleRoot.resolve("package-list") + packageListFile.writeText(packageListService.formatPackageList(module as DocumentationModule)) + } + } +} + +interface JavaLayoutHtmlFormatOutputBuilderFactory { + fun createOutputBuilder(output: Appendable, uri: URI): JavaLayoutHtmlFormatOutputBuilder + fun createOutputBuilder(output: Appendable, node: DocumentationNode): JavaLayoutHtmlFormatOutputBuilder +} diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt new file mode 100644 index 000000000..ce05fe897 --- /dev/null +++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt @@ -0,0 +1,154 @@ +package org.jetbrains.dokka.Formats + +import com.intellij.psi.PsiMember +import com.intellij.psi.PsiParameter +import org.jetbrains.dokka.* +import org.jetbrains.dokka.ExternalDocumentationLinkResolver.Companion.DOKKA_PARAM_PREFIX +import org.jetbrains.kotlin.asJava.toLightElements +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor +import org.jetbrains.kotlin.load.java.descriptors.JavaMethodDescriptor +import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe +import org.jetbrains.kotlin.resolve.descriptorUtil.isCompanionObject +import org.jetbrains.kotlin.types.KotlinType + +class JavaLayoutHtmlPackageListService: PackageListService { + + private fun StringBuilder.appendParam(name: String, value: String) { + append(DOKKA_PARAM_PREFIX) + append(name) + append(":") + appendln(value) + } + + override fun formatPackageList(module: DocumentationModule): String { + val packages = module.members(NodeKind.Package).map { it.name } + + return buildString { + appendParam("format", "java-layout-html") + appendParam("mode", "kotlin") + for (p in packages) { + appendln(p) + } + } + } + +} + +class JavaLayoutHtmlInboundLinkResolutionService(private val paramMap: Map<String, List<String>>, + private val resolutionFacade: DokkaResolutionFacade) : InboundExternalLinkResolutionService { + + constructor(asJava: Boolean, resolutionFacade: DokkaResolutionFacade) : + this(mapOf("mode" to listOf(if (asJava) "java" else "kotlin")), resolutionFacade) + + + private val isJavaMode = paramMap["mode"]!!.single() == "java" + + private fun getContainerPath(symbol: DeclarationDescriptor): String? { + return when (symbol) { + is PackageFragmentDescriptor -> symbol.fqName.asString().replace('.', '/') + "/" + is ClassifierDescriptor -> getContainerPath(symbol.findPackage()) + symbol.nameWithOuter() + ".html" + else -> null + } + } + + private fun DeclarationDescriptor.findPackage(): PackageFragmentDescriptor = + generateSequence(this) { it.containingDeclaration }.filterIsInstance<PackageFragmentDescriptor>().first() + + private fun ClassifierDescriptor.nameWithOuter(): String = + generateSequence(this) { it.containingDeclaration as? ClassifierDescriptor } + .toList().asReversed().joinToString(".") { it.name.asString() } + + private fun getJavaPagePath(symbol: DeclarationDescriptor): String? { + + val sourcePsi = symbol.sourcePsi() ?: return null + val source = (if (sourcePsi is KtDeclaration) { + sourcePsi.toLightElements().firstOrNull() + } else { + sourcePsi + }) as? PsiMember ?: return null + val desc = source.getJavaMemberDescriptor(resolutionFacade) ?: return null + return getPagePath(desc) + } + + private fun getPagePath(symbol: DeclarationDescriptor): String? { + return when (symbol) { + is PackageFragmentDescriptor -> getContainerPath(symbol) + "package-summary.html" + is EnumEntrySyntheticClassDescriptor -> getContainerPath(symbol.containingDeclaration) + "#" + symbol.signatureForAnchorUrlEncoded() + is ClassifierDescriptor -> getContainerPath(symbol) + "#" + is FunctionDescriptor, is PropertyDescriptor -> getContainerPath(symbol.containingDeclaration!!) + "#" + symbol.signatureForAnchorUrlEncoded() + else -> null + } + } + + private fun DeclarationDescriptor.signatureForAnchor(): String? { + + fun ReceiverParameterDescriptor.extractReceiverName(): String { + var receiverClass: DeclarationDescriptor = type.constructor.declarationDescriptor!! + if (receiverClass.isCompanionObject()) { + receiverClass = receiverClass.containingDeclaration!! + } else if (receiverClass is TypeParameterDescriptor) { + val upperBoundClass = receiverClass.upperBounds.singleOrNull()?.constructor?.declarationDescriptor + if (upperBoundClass != null) { + receiverClass = upperBoundClass + } + } + + return receiverClass.name.asString() + } + + fun KotlinType.qualifiedNameForSignature(): String { + val desc = constructor.declarationDescriptor + return desc?.fqNameUnsafe?.asString() ?: "<ERROR TYPE NAME>" + } + + fun StringBuilder.appendReceiverAndCompanion(desc: CallableDescriptor) { + if (desc.containingDeclaration.isCompanionObject()) { + append("Companion.") + } + desc.extensionReceiverParameter?.let { + append("(") + append(it.extractReceiverName()) + append(").") + } + } + + return when (this) { + is EnumEntrySyntheticClassDescriptor -> buildString { + append("ENUM_VALUE:") + append(name.asString()) + } + is JavaMethodDescriptor -> buildString { + append(name.asString()) + valueParameters.joinTo(this, prefix = "(", postfix = ")") { + val param = it.sourcePsi() as PsiParameter + param.type.canonicalText + } + } + is JavaPropertyDescriptor -> buildString { + append(name.asString()) + } + is FunctionDescriptor -> buildString { + appendReceiverAndCompanion(this@signatureForAnchor) + append(name.asString()) + valueParameters.joinTo(this, prefix = "(", postfix = ")") { + it.type.qualifiedNameForSignature() + } + } + is PropertyDescriptor -> buildString { + appendReceiverAndCompanion(this@signatureForAnchor) + append(name.asString()) + append(":") + + append(returnType?.qualifiedNameForSignature()) + } + else -> null + } + } + + private fun DeclarationDescriptor.signatureForAnchorUrlEncoded(): String? = signatureForAnchor()?.anchorEncoded() + + override fun getPath(symbol: DeclarationDescriptor) = if (isJavaMode) getJavaPagePath(symbol) else getPagePath(symbol) +} diff --git a/core/src/main/kotlin/Formats/JekyllFormatService.kt b/core/src/main/kotlin/Formats/JekyllFormatService.kt new file mode 100644 index 000000000..a948dfa93 --- /dev/null +++ b/core/src/main/kotlin/Formats/JekyllFormatService.kt @@ -0,0 +1,44 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName + +open class JekyllOutputBuilder(to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String>) + : MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) { + override fun appendNodes(nodes: Iterable<DocumentationNode>) { + to.appendln("---") + appendFrontMatter(nodes, to) + to.appendln("---") + to.appendln("") + super.appendNodes(nodes) + } + + protected open fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) { + to.appendln("title: ${getPageTitle(nodes)}") + } +} + + +open class JekyllFormatService( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + linkExtension: String, + impliedPlatforms: List<String> +) : MarkdownFormatService(generator, signatureGenerator, linkExtension, impliedPlatforms) { + + @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String> + ) : this(generator, signatureGenerator, "html", impliedPlatforms) + + override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder = + JekyllOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) + +} diff --git a/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt new file mode 100644 index 000000000..a98002d49 --- /dev/null +++ b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt @@ -0,0 +1,224 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName +import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty + + +open class KotlinWebsiteOutputBuilder( + to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String> +) : JekyllOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) { + private var needHardLineBreaks = false + private var insideDiv = 0 + + override fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) { + super.appendFrontMatter(nodes, to) + to.appendln("layout: api") + } + + override fun appendBreadcrumbs(path: Iterable<FormatLink>) { + if (path.count() > 1) { + to.append("<div class='api-docs-breadcrumbs'>") + super.appendBreadcrumbs(path) + to.append("</div>") + } + } + + override fun appendCode(body: () -> Unit) = wrapIfNotEmpty("<code>", "</code>", body) + + override fun appendStrikethrough(body: () -> Unit) = wrapInTag("s", body) + + protected fun div(to: StringBuilder, cssClass: String, otherAttributes: String = "", markdown: Boolean = false, block: () -> Unit) { + to.append("<div class=\"$cssClass\"$otherAttributes") + if (markdown) to.append(" markdown=\"1\"") + to.append(">") + if (!markdown) insideDiv++ + block() + if (!markdown) insideDiv-- + to.append("</div>\n") + } + + override fun appendAsSignature(node: ContentNode, block: () -> Unit) { + val contentLength = node.textLength + if (contentLength == 0) return + div(to, "signature") { + needHardLineBreaks = contentLength >= 62 + try { + block() + } finally { + needHardLineBreaks = false + } + } + } + + override fun appendAsOverloadGroup(to: StringBuilder, platforms: Set<String>, block: () -> Unit) { + div(to, "overload-group", calculateDataAttributes(platforms), true) { + ensureParagraph() + block() + ensureParagraph() + } + } + + override fun appendLink(href: String, body: () -> Unit) = wrap("<a href=\"$href\">", "</a>", body) + + override fun appendHeader(level: Int, body: () -> Unit) { + if (insideDiv > 0) { + wrapInTag("p", body, newlineAfterClose = true) + } else { + super.appendHeader(level, body) + } + } + + override fun appendLine() { + if (insideDiv > 0) { + to.appendln("<br/>") + } else { + super.appendLine() + } + } + + override fun appendTable(vararg columns: String, body: () -> Unit) { + to.appendln("<table class=\"api-docs-table\">") + body() + to.appendln("</table>") + } + + override fun appendTableBody(body: () -> Unit) { + to.appendln("<tbody>") + body() + to.appendln("</tbody>") + } + + override fun appendTableRow(body: () -> Unit) { + to.appendln("<tr>") + body() + to.appendln("</tr>") + } + + override fun appendTableCell(body: () -> Unit) { + to.appendln("<td markdown=\"1\">") + body() + to.appendln("\n</td>") + } + + override fun appendBlockCode(language: String, body: () -> Unit) { + if (language.isNotEmpty()) { + super.appendBlockCode(language, body) + } else { + wrap("<pre markdown=\"1\">", "</pre>", body) + } + } + + override fun appendSymbol(text: String) { + to.append("<span class=\"symbol\">${text.htmlEscape()}</span>") + } + + override fun appendKeyword(text: String) { + to.append("<span class=\"keyword\">${text.htmlEscape()}</span>") + } + + override fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) { + val id = signature?.let { " id=\"$it\"" }.orEmpty() + to.append("<span class=\"${identifierClassName(kind)}\"$id>${text.htmlEscape()}</span>") + } + + override fun appendSoftLineBreak() { + if (needHardLineBreaks) + to.append("<br/>") + + } + + override fun appendIndentedSoftLineBreak() { + if (needHardLineBreaks) { + to.append("<br/> ") + } + } + + private fun identifierClassName(kind: IdentifierKind) = when (kind) { + IdentifierKind.ParameterName -> "parameterName" + IdentifierKind.SummarizedTypeName -> "summarizedTypeName" + else -> "identifier" + } + + fun calculateDataAttributes(platforms: Set<String>): String { + fun String.isKotlinVersion() = this.startsWith("Kotlin") + fun String.isJREVersion() = this.startsWith("JRE") + val kotlinVersion = platforms.singleOrNull(String::isKotlinVersion) + val jreVersion = platforms.singleOrNull(String::isJREVersion) + val targetPlatforms = platforms.filterNot { it.isKotlinVersion() || it.isJREVersion() } + + val kotlinVersionAttr = kotlinVersion?.let { " data-kotlin-version=\"$it\"" } ?: "" + val jreVersionAttr = jreVersion?.let { " data-jre-version=\"$it\"" } ?: "" + val platformsAttr = targetPlatforms.ifNotEmpty { " data-platform=\"${targetPlatforms.joinToString()}\"" } ?: "" + return "$platformsAttr$kotlinVersionAttr$jreVersionAttr" + } + + override fun appendIndexRow(platforms: Set<String>, block: () -> Unit) { + if (platforms.isNotEmpty()) + wrap("<tr${calculateDataAttributes(platforms)}>", "</tr>", block) + else + appendTableRow(block) + } + + override fun appendPlatforms(platforms: Set<String>) { + + } +} + +class KotlinWebsiteFormatService @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String>, + logger: DokkaLogger +) : JekyllFormatService(generator, signatureGenerator, "html", impliedPlatforms) { + init { + logger.warn("Format kotlin-website deprecated and will be removed in next release") + } + + override fun createOutputBuilder(to: StringBuilder, location: Location) = + KotlinWebsiteOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +} + + +class KotlinWebsiteRunnableSamplesOutputBuilder( + to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String> +) : KotlinWebsiteOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) { + + override fun appendSampleBlockCode(language: String, imports: () -> Unit, body: () -> Unit) { + div(to, "sample", markdown = true) { + appendBlockCode(language) { + imports() + wrap("\n\nfun main(args: Array<String>) {", "}") { + wrap("\n//sampleStart\n", "\n//sampleEnd\n", body) + } + } + } + } +} + +class KotlinWebsiteRunnableSamplesFormatService @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String>, + logger: DokkaLogger +) : JekyllFormatService(generator, signatureGenerator, "html", impliedPlatforms) { + + init { + logger.warn("Format kotlin-website-samples deprecated and will be removed in next release") + } + + override fun createOutputBuilder(to: StringBuilder, location: Location) = + KotlinWebsiteRunnableSamplesOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +} + diff --git a/core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt b/core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt new file mode 100644 index 000000000..6ced75b55 --- /dev/null +++ b/core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt @@ -0,0 +1,186 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName +import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty +import java.io.File + + +object EmptyHtmlTemplateService : HtmlTemplateService { + override fun appendFooter(to: StringBuilder) {} + + override fun appendHeader(to: StringBuilder, title: String?, basePath: File) {} +} + + +open class KotlinWebsiteHtmlOutputBuilder( + to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String>, + templateService: HtmlTemplateService +) : HtmlOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms, templateService) { + private var needHardLineBreaks = false + private var insideDiv = 0 + + override fun appendLine() {} + + override fun appendBreadcrumbs(path: Iterable<FormatLink>) { + if (path.count() > 1) { + to.append("<div class='api-docs-breadcrumbs'>") + super.appendBreadcrumbs(path) + to.append("</div>") + } + } + + override fun appendCode(body: () -> Unit) = wrapIfNotEmpty("<code>", "</code>", body) + + protected fun div(to: StringBuilder, cssClass: String, otherAttributes: String = "", block: () -> Unit) { + to.append("<div class=\"$cssClass\"$otherAttributes") + to.append(">") + insideDiv++ + block() + insideDiv-- + to.append("</div>\n") + } + + override fun appendAsSignature(node: ContentNode, block: () -> Unit) { + val contentLength = node.textLength + if (contentLength == 0) return + div(to, "signature") { + needHardLineBreaks = contentLength >= 62 + try { + block() + } finally { + needHardLineBreaks = false + } + } + } + + override fun appendAsOverloadGroup(to: StringBuilder, platforms: Set<String>, block: () -> Unit) { + div(to, "overload-group", calculateDataAttributes(platforms)) { + block() + } + } + + override fun appendLink(href: String, body: () -> Unit) = wrap("<a href=\"$href\">", "</a>", body) + + override fun appendTable(vararg columns: String, body: () -> Unit) { + to.appendln("<table class=\"api-docs-table\">") + body() + to.appendln("</table>") + } + + override fun appendTableBody(body: () -> Unit) { + to.appendln("<tbody>") + body() + to.appendln("</tbody>") + } + + override fun appendTableRow(body: () -> Unit) { + to.appendln("<tr>") + body() + to.appendln("</tr>") + } + + override fun appendTableCell(body: () -> Unit) { + to.appendln("<td>") + body() + to.appendln("\n</td>") + } + + override fun appendSymbol(text: String) { + to.append("<span class=\"symbol\">${text.htmlEscape()}</span>") + } + + override fun appendKeyword(text: String) { + to.append("<span class=\"keyword\">${text.htmlEscape()}</span>") + } + + override fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) { + val id = signature?.let { " id=\"$it\"" }.orEmpty() + to.append("<span class=\"${identifierClassName(kind)}\"$id>${text.htmlEscape()}</span>") + } + + override fun appendSoftLineBreak() { + if (needHardLineBreaks) + to.append("<br/>") + } + + override fun appendIndentedSoftLineBreak() { + if (needHardLineBreaks) { + to.append("<br/> ") + } + } + + private fun identifierClassName(kind: IdentifierKind) = when (kind) { + IdentifierKind.ParameterName -> "parameterName" + IdentifierKind.SummarizedTypeName -> "summarizedTypeName" + else -> "identifier" + } + + fun calculateDataAttributes(platforms: Set<String>): String { + fun String.isKotlinVersion() = this.startsWith("Kotlin") + fun String.isJREVersion() = this.startsWith("JRE") + val kotlinVersion = platforms.singleOrNull(String::isKotlinVersion) + val jreVersion = platforms.singleOrNull(String::isJREVersion) + val targetPlatforms = platforms.filterNot { it.isKotlinVersion() || it.isJREVersion() } + + val kotlinVersionAttr = kotlinVersion?.let { " data-kotlin-version=\"$it\"" } ?: "" + val jreVersionAttr = jreVersion?.let { " data-jre-version=\"$it\"" } ?: "" + val platformsAttr = targetPlatforms.ifNotEmpty { " data-platform=\"${targetPlatforms.joinToString()}\"" } ?: "" + return "$platformsAttr$kotlinVersionAttr$jreVersionAttr" + } + + override fun appendIndexRow(platforms: Set<String>, block: () -> Unit) { + if (platforms.isNotEmpty()) + wrap("<tr${calculateDataAttributes(platforms)}>", "</tr>", block) + else + appendTableRow(block) + } + + override fun appendPlatforms(platforms: Set<String>) {} + + override fun appendBreadcrumbSeparator() { + to.append(" / ") + } + + override fun appendSampleBlockCode(language: String, imports: () -> Unit, body: () -> Unit) { + div(to, "sample") { + appendBlockCode(language) { + imports() + wrap("\n\nfun main(args: Array<String>) {".htmlEscape(), "}") { + wrap("\n//sampleStart\n", "\n//sampleEnd\n", body) + } + } + } + } + + override fun appendSoftParagraph(body: () -> Unit) = appendParagraph(body) + + + override fun appendSectionWithTag(section: ContentSection) { + appendParagraph { + appendStrong { appendText(section.tag) } + appendText(" ") + appendContent(section) + } + } +} + +class KotlinWebsiteHtmlFormatService @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String>, + templateService: HtmlTemplateService +) : HtmlFormatService(generator, signatureGenerator, templateService, impliedPlatforms) { + + override fun enumerateSupportFiles(callback: (String, String) -> Unit) {} + + override fun createOutputBuilder(to: StringBuilder, location: Location) = + KotlinWebsiteHtmlOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms, templateService) +} + diff --git a/core/src/main/kotlin/Formats/MarkdownFormatService.kt b/core/src/main/kotlin/Formats/MarkdownFormatService.kt new file mode 100644 index 000000000..4265394f2 --- /dev/null +++ b/core/src/main/kotlin/Formats/MarkdownFormatService.kt @@ -0,0 +1,239 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName +import java.util.* + +enum class ListKind { + Ordered, + Unordered +} + +private class ListState(val kind: ListKind, var size: Int = 1) { + fun getTagAndIncrement() = when (kind) { + ListKind.Ordered -> "${size++}. " + else -> "* " + } +} + +private val TWO_LINE_BREAKS = System.lineSeparator() + System.lineSeparator() + +open class MarkdownOutputBuilder(to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String>) + : StructuredOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +{ + private val listStack = ArrayDeque<ListState>() + protected var inTableCell = false + protected var inCodeBlock = false + private var lastTableCellStart = -1 + private var maxBackticksInCodeBlock = 0 + + private fun appendNewline() { + while (to.endsWith(' ')) { + to.setLength(to.length - 1) + } + to.appendln() + } + + private fun ensureNewline() { + if (inTableCell && listStack.isEmpty()) { + if (to.length != lastTableCellStart && !to.endsWith("<br>")) { + to.append("<br>") + } + } + else { + if (!endsWithNewline()) { + appendNewline() + } + } + } + + private fun endsWithNewline(): Boolean { + var index = to.length - 1 + while (index > 0) { + val c = to[index] + if (c != ' ') { + return c == '\n' + } + index-- + } + return false + } + + override fun ensureParagraph() { + if (!to.endsWith(TWO_LINE_BREAKS)) { + if (!to.endsWith('\n')) { + appendNewline() + } + appendNewline() + } + } + override fun appendBreadcrumbSeparator() { + to.append(" / ") + } + + private val backTickFindingRegex = """(`+)""".toRegex() + + override fun appendText(text: String) { + if (inCodeBlock) { + to.append(text) + val backTicks = backTickFindingRegex.findAll(text) + val longestBackTickRun = backTicks.map { it.value.length }.max() ?: 0 + maxBackticksInCodeBlock = maxBackticksInCodeBlock.coerceAtLeast(longestBackTickRun) + } + else { + if (text == "\n" && inTableCell) { + to.append(" ") + } else { + to.append(text.htmlEscape()) + } + } + } + + override fun appendCode(body: () -> Unit) { + inCodeBlock = true + val codeBlockStart = to.length + maxBackticksInCodeBlock = 0 + + wrapIfNotEmpty("`", "`", body, checkEndsWith = true) + + if (maxBackticksInCodeBlock > 0) { + val extraBackticks = "`".repeat(maxBackticksInCodeBlock) + to.insert(codeBlockStart, extraBackticks) + to.append(extraBackticks) + } + + inCodeBlock = false + } + + override fun appendUnorderedList(body: () -> Unit) { + listStack.push(ListState(ListKind.Unordered)) + body() + listStack.pop() + ensureNewline() + } + + override fun appendOrderedList(body: () -> Unit) { + listStack.push(ListState(ListKind.Ordered)) + body() + listStack.pop() + ensureNewline() + } + + override fun appendListItem(body: () -> Unit) { + ensureNewline() + to.append(listStack.peek()?.getTagAndIncrement()) + body() + ensureNewline() + } + + override fun appendStrong(body: () -> Unit) = wrap("**", "**", body) + override fun appendEmphasis(body: () -> Unit) = wrap("*", "*", body) + override fun appendStrikethrough(body: () -> Unit) = wrap("~~", "~~", body) + + override fun appendLink(href: String, body: () -> Unit) { + if (inCodeBlock) { + wrap("`[`", "`]($href)`", body) + } + else { + wrap("[", "]($href)", body) + } + } + + override fun appendLine() { + if (inTableCell) { + to.append("<br>") + } + else { + appendNewline() + } + } + + override fun appendAnchor(anchor: String) { + // no anchors in Markdown + } + + override fun appendParagraph(body: () -> Unit) { + if (inTableCell) { + ensureNewline() + body() + } else if (listStack.isNotEmpty()) { + body() + ensureNewline() + } else { + ensureParagraph() + body() + ensureParagraph() + } + } + + override fun appendHeader(level: Int, body: () -> Unit) { + ensureParagraph() + to.append("${"#".repeat(level)} ") + body() + ensureParagraph() + } + + override fun appendBlockCode(language: String, body: () -> Unit) { + inCodeBlock = true + ensureParagraph() + to.appendln(if (language.isEmpty()) "```" else "``` $language") + body() + ensureNewline() + to.appendln("```") + appendLine() + inCodeBlock = false + } + + override fun appendTable(vararg columns: String, body: () -> Unit) { + ensureParagraph() + body() + ensureParagraph() + } + + override fun appendTableBody(body: () -> Unit) { + body() + } + + override fun appendTableRow(body: () -> Unit) { + to.append("|") + body() + appendNewline() + } + + override fun appendTableCell(body: () -> Unit) { + to.append(" ") + inTableCell = true + lastTableCellStart = to.length + body() + inTableCell = false + to.append(" |") + } + + override fun appendNonBreakingSpace() { + if (inCodeBlock) { + to.append(" ") + } + else { + to.append(" ") + } + } +} + +open class MarkdownFormatService(generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + linkExtension: String, + val impliedPlatforms: List<String>) +: StructuredFormatService(generator, signatureGenerator, "md", linkExtension) { + @Inject constructor(generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String>): this(generator, signatureGenerator, "md", impliedPlatforms) + + override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder = + MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +} diff --git a/core/src/main/kotlin/Formats/OutlineService.kt b/core/src/main/kotlin/Formats/OutlineService.kt new file mode 100644 index 000000000..958e93aff --- /dev/null +++ b/core/src/main/kotlin/Formats/OutlineService.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka + +import java.io.File + +/** + * Service for building the outline of the package contents. + */ +interface OutlineFormatService { + fun getOutlineFileName(location: Location): File + + fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) + fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) + + /** Appends formatted outline to [StringBuilder](to) using specified [location] */ + fun appendOutline(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + for (node in nodes) { + appendOutlineHeader(location, node, to) + if (node.members.any()) { + val sortedMembers = node.members.sortedBy { it.name.toLowerCase() } + appendOutlineLevel(to) { + appendOutline(location, to, sortedMembers) + } + } + } + } + + fun formatOutline(location: Location, nodes: Iterable<DocumentationNode>): String = + StringBuilder().apply { appendOutline(location, this, nodes) }.toString() +} diff --git a/core/src/main/kotlin/Formats/PackageListService.kt b/core/src/main/kotlin/Formats/PackageListService.kt new file mode 100644 index 000000000..7b68098e8 --- /dev/null +++ b/core/src/main/kotlin/Formats/PackageListService.kt @@ -0,0 +1,63 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject + + +interface PackageListService { + fun formatPackageList(module: DocumentationModule): String +} + +class DefaultPackageListService @Inject constructor( + val generator: NodeLocationAwareGenerator, + val formatService: FormatService +) : PackageListService { + + override fun formatPackageList(module: DocumentationModule): String { + val packages = mutableSetOf<String>() + val nonStandardLocations = mutableMapOf<String, String>() + + fun visit(node: DocumentationNode, relocated: Boolean = false) { + val nodeKind = node.kind + + when (nodeKind) { + NodeKind.Package -> { + packages.add(node.qualifiedName()) + node.members.forEach { visit(it) } + } + NodeKind.Signature -> { + if (relocated) + nonStandardLocations[node.name] = generator.relativePathToLocation(module, node.owner!!) + } + NodeKind.ExternalClass -> { + node.members.forEach { visit(it, relocated = true) } + } + NodeKind.GroupNode -> { + //only children of top-level GN records interesting for us, since link to top-level ones should point to GN + node.members.forEach { it.members.forEach { visit(it, relocated = true) } } + //record signature of GN as signature of type alias and class merged to GN, so link to it should point to GN + node.detailOrNull(NodeKind.Signature)?.let { visit(it, relocated = true) } + } + else -> { + if (nodeKind in NodeKind.classLike || nodeKind in NodeKind.memberLike) { + node.details(NodeKind.Signature).forEach { visit(it, relocated) } + node.members.forEach { visit(it, relocated) } + } + } + } + } + + module.members.forEach { visit(it) } + + return buildString { + appendln("\$dokka.linkExtension:${formatService.linkExtension}") + + nonStandardLocations.map { (signature, location) -> "\$dokka.location:$signature\u001f$location" } + .sorted().joinTo(this, separator = "\n", postfix = "\n") + + packages.sorted().joinTo(this, separator = "\n", postfix = "\n") + } + + } + +} + diff --git a/core/src/main/kotlin/Formats/StandardFormats.kt b/core/src/main/kotlin/Formats/StandardFormats.kt new file mode 100644 index 000000000..dd67ac972 --- /dev/null +++ b/core/src/main/kotlin/Formats/StandardFormats.kt @@ -0,0 +1,66 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Binder +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Samples.KotlinWebsiteSampleProcessingService +import org.jetbrains.dokka.Utilities.bind +import kotlin.reflect.KClass + +abstract class KotlinFormatDescriptorBase + : FileGeneratorBasedFormatDescriptor(), + DefaultAnalysisComponent, + DefaultAnalysisComponentServices by KotlinAsKotlin { + override val generatorServiceClass = FileGenerator::class + override val outlineServiceClass: KClass<out OutlineFormatService>? = null + override val packageListServiceClass: KClass<out PackageListService>? = DefaultPackageListService::class +} + +abstract class HtmlFormatDescriptorBase : FileGeneratorBasedFormatDescriptor(), DefaultAnalysisComponent { + override val formatServiceClass = HtmlFormatService::class + override val outlineServiceClass = HtmlFormatService::class + override val generatorServiceClass = FileGenerator::class + override val packageListServiceClass = DefaultPackageListService::class + + override fun configureOutput(binder: Binder): Unit = with(binder) { + super.configureOutput(binder) + bind<HtmlTemplateService>().toProvider { HtmlTemplateService.default("style.css") } + } +} + +class HtmlFormatDescriptor : HtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin + +class HtmlAsJavaFormatDescriptor : HtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava + +class KotlinWebsiteFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = KotlinWebsiteFormatService::class + override val outlineServiceClass = YamlOutlineService::class +} + +class KotlinWebsiteFormatRunnableSamplesDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = KotlinWebsiteRunnableSamplesFormatService::class + override val sampleProcessingService = KotlinWebsiteSampleProcessingService::class + override val outlineServiceClass = YamlOutlineService::class +} + +class KotlinWebsiteHtmlFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = KotlinWebsiteHtmlFormatService::class + override val sampleProcessingService = KotlinWebsiteSampleProcessingService::class + override val outlineServiceClass = YamlOutlineService::class + + override fun configureOutput(binder: Binder) = with(binder) { + super.configureOutput(binder) + bind<HtmlTemplateService>().toInstance(EmptyHtmlTemplateService) + } +} + +class JekyllFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = JekyllFormatService::class +} + +class MarkdownFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = MarkdownFormatService::class +} + +class GFMFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = GFMFormatService::class +} diff --git a/core/src/main/kotlin/Formats/StructuredFormatService.kt b/core/src/main/kotlin/Formats/StructuredFormatService.kt new file mode 100644 index 000000000..a2c9078f3 --- /dev/null +++ b/core/src/main/kotlin/Formats/StructuredFormatService.kt @@ -0,0 +1,691 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.LanguageService.RenderMode +import java.util.* + +data class FormatLink(val text: String, val href: String) + +abstract class StructuredOutputBuilder(val to: StringBuilder, + val location: Location, + val generator: NodeLocationAwareGenerator, + val languageService: LanguageService, + val extension: String, + val impliedPlatforms: List<String>) : FormattedOutputBuilder { + + protected fun DocumentationNode.location() = generator.location(this) + + protected fun wrap(prefix: String, suffix: String, body: () -> Unit) { + to.append(prefix) + body() + to.append(suffix) + } + + protected fun wrapIfNotEmpty(prefix: String, suffix: String, body: () -> Unit, checkEndsWith: Boolean = false) { + val startLength = to.length + to.append(prefix) + body() + if (checkEndsWith && to.endsWith(suffix)) { + to.setLength(to.length - suffix.length) + } else if (to.length > startLength + prefix.length) { + to.append(suffix) + } else { + to.setLength(startLength) + } + } + + protected fun wrapInTag(tag: String, + body: () -> Unit, + newlineBeforeOpen: Boolean = false, + newlineAfterOpen: Boolean = false, + newlineAfterClose: Boolean = false) { + if (newlineBeforeOpen && !to.endsWith('\n')) to.appendln() + to.append("<$tag>") + if (newlineAfterOpen) to.appendln() + body() + to.append("</$tag>") + if (newlineAfterClose) to.appendln() + } + + protected abstract fun ensureParagraph() + + open fun appendSampleBlockCode(language: String, imports: () -> Unit, body: () -> Unit) = appendBlockCode(language, body) + abstract fun appendBlockCode(language: String, body: () -> Unit) + abstract fun appendHeader(level: Int = 1, body: () -> Unit) + abstract fun appendParagraph(body: () -> Unit) + + open fun appendSoftParagraph(body: () -> Unit) { + ensureParagraph() + body() + } + + abstract fun appendLine() + abstract fun appendAnchor(anchor: String) + + abstract fun appendTable(vararg columns: String, body: () -> Unit) + abstract fun appendTableBody(body: () -> Unit) + abstract fun appendTableRow(body: () -> Unit) + abstract fun appendTableCell(body: () -> Unit) + + abstract fun appendText(text: String) + + open fun appendSinceKotlin(version: String) { + appendParagraph { + appendText("Available since Kotlin: ") + appendCode { appendText(version) } + } + } + + open fun appendSectionWithTag(section: ContentSection) { + appendParagraph { + appendStrong { appendText(section.tag) } + appendLine() + appendContent(section) + } + } + + open fun appendSymbol(text: String) { + appendText(text) + } + + open fun appendKeyword(text: String) { + appendText(text) + } + + open fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) { + appendText(text) + } + + fun appendEntity(text: String) { + to.append(text) + } + + abstract fun appendLink(href: String, body: () -> Unit) + + open fun appendLink(link: FormatLink) { + appendLink(link.href) { appendText(link.text) } + } + + abstract fun appendStrong(body: () -> Unit) + abstract fun appendStrikethrough(body: () -> Unit) + abstract fun appendEmphasis(body: () -> Unit) + abstract fun appendCode(body: () -> Unit) + abstract fun appendUnorderedList(body: () -> Unit) + abstract fun appendOrderedList(body: () -> Unit) + abstract fun appendListItem(body: () -> Unit) + + abstract fun appendBreadcrumbSeparator() + abstract fun appendNonBreakingSpace() + open fun appendSoftLineBreak() { + } + + open fun appendIndentedSoftLineBreak() { + } + + fun appendContent(content: List<ContentNode>) { + for (contentNode in content) { + appendContent(contentNode) + } + } + + open fun appendContent(content: ContentNode) { + when (content) { + is ContentText -> appendText(content.text) + is ContentSymbol -> appendSymbol(content.text) + is ContentKeyword -> appendKeyword(content.text) + is ContentIdentifier -> appendIdentifier(content.text, content.kind, content.signature) + is ContentNonBreakingSpace -> appendNonBreakingSpace() + is ContentSoftLineBreak -> appendSoftLineBreak() + is ContentIndentedSoftLineBreak -> appendIndentedSoftLineBreak() + is ContentEntity -> appendEntity(content.text) + is ContentStrong -> appendStrong { appendContent(content.children) } + is ContentStrikethrough -> appendStrikethrough { appendContent(content.children) } + is ContentCode -> appendCode { appendContent(content.children) } + is ContentEmphasis -> appendEmphasis { appendContent(content.children) } + is ContentUnorderedList -> appendUnorderedList { appendContent(content.children) } + is ContentOrderedList -> appendOrderedList { appendContent(content.children) } + is ContentListItem -> appendListItem { + val child = content.children.singleOrNull() + if (child is ContentParagraph) { + appendContent(child.children) + } else { + appendContent(content.children) + } + } + + is ContentNodeLink -> { + val node = content.node + val linkTo = if (node != null) locationHref(location, node) else "#" + appendLinkIfNotThisPage(linkTo, content) + } + is ContentExternalLink -> appendLinkIfNotThisPage(content.href, content) + + is ContentParagraph -> { + if (!content.isEmpty()) { + appendParagraph { appendContent(content.children) } + } + } + + is ContentSpecialReference -> wrapInTag(tag = "aside class=\"note\"", body = { + if (!content.isEmpty()) { + appendContent(content.children) + } + }) + + is ContentBlockSampleCode, is ContentBlockCode -> { + content as ContentBlockCode + fun ContentBlockCode.appendBlockCodeContent() { + children + .dropWhile { it is ContentText && it.text.isBlank() } + .forEach { appendContent(it) } + } + when (content) { + is ContentBlockSampleCode -> + appendSampleBlockCode(content.language, content.importsBlock::appendBlockCodeContent, { content.appendBlockCodeContent() }) + is ContentBlockCode -> + appendBlockCode(content.language, { content.appendBlockCodeContent() }) + } + } + is ContentHeading -> appendHeader(content.level) { appendContent(content.children) } + is ContentBlock -> appendContent(content.children) + } + } + + private fun appendLinkIfNotThisPage(href: String, content: ContentBlock) { + if (href == ".") { + appendContent(content.children) + } else { + appendLink(href) { appendContent(content.children) } + } + } + + open fun link(from: DocumentationNode, + to: DocumentationNode, + name: (DocumentationNode) -> String = DocumentationNode::name): FormatLink = link(from, to, extension, name) + + open fun link(from: DocumentationNode, + to: DocumentationNode, + extension: String, + name: (DocumentationNode) -> String = DocumentationNode::name): FormatLink { + if (to.owner?.kind == NodeKind.GroupNode) + return link(from, to.owner!!, extension, name) + + if (from.owner?.kind == NodeKind.GroupNode) + return link(from.owner!!, to, extension, name) + + return FormatLink(name(to), from.location().relativePathTo(to.location())) + } + + fun locationHref(from: Location, to: DocumentationNode): String { + val topLevelPage = to.references(RefKind.TopLevelPage).singleOrNull()?.to + if (topLevelPage != null) { + val signature = to.detailOrNull(NodeKind.Signature) + return from.relativePathTo(topLevelPage.location(), signature?.name ?: to.name) + } + return from.relativePathTo(to.location()) + } + + private fun DocumentationNode.isModuleOrPackage(): Boolean = + kind == NodeKind.Module || kind == NodeKind.Package + + protected open fun appendAsSignature(node: ContentNode, block: () -> Unit) { + block() + } + + protected open fun appendAsOverloadGroup(to: StringBuilder, platforms: Set<String>, block: () -> Unit) { + block() + } + + protected open fun appendIndexRow(platforms: Set<String>, block: () -> Unit) { + appendTableRow(block) + } + + protected open fun appendPlatforms(platforms: Set<String>) { + if (platforms.isNotEmpty()) { + appendLine() + appendText(platforms.joinToString(prefix = "(", postfix = ")")) + } + } + + protected open fun appendBreadcrumbs(path: Iterable<FormatLink>) { + for ((index, item) in path.withIndex()) { + if (index > 0) { + appendBreadcrumbSeparator() + } + appendLink(item) + } + } + + fun Content.getSectionsWithSubjects(): Map<String, List<ContentSection>> = + sections.filter { it.subjectName != null }.groupBy { it.tag } + + private fun ContentNode.appendSignature() { + if (this is ContentBlock && this.isEmpty()) { + return + } + + val signatureAsCode = ContentCode() + signatureAsCode.append(this) + appendContent(signatureAsCode) + } + + open inner class PageBuilder(val nodes: Iterable<DocumentationNode>, val noHeader: Boolean = false) { + open fun build() { + val breakdownByLocation = nodes.groupBy { node -> + node.path.filterNot { it.name.isEmpty() }.map { link(node, it) }.distinct() + } + + for ((path, nodes) in breakdownByLocation) { + if (!noHeader && path.isNotEmpty()) { + appendBreadcrumbs(path) + appendLine() + appendLine() + } + appendLocation(nodes.filter { it.kind != NodeKind.ExternalClass }) + } + } + + private fun appendLocation(nodes: Iterable<DocumentationNode>) { + val singleNode = nodes.singleOrNull() + if (singleNode != null && singleNode.isModuleOrPackage()) { + if (singleNode.kind == NodeKind.Package) { + val packageName = if (singleNode.name.isEmpty()) "<root>" else singleNode.name + appendHeader(2) { appendText("Package $packageName") } + } + singleNode.appendPlatforms() + appendContent(singleNode.content) + } else { + val breakdownByName = nodes.groupBy { node -> node.name } + for ((name, items) in breakdownByName) { + if (!noHeader) + appendHeader { appendText(name) } + appendDocumentation(items, singleNode != null) + } + } + } + + private fun appendDocumentation(overloads: Iterable<DocumentationNode>, isSingleNode: Boolean) { + val breakdownBySummary = overloads.groupByTo(LinkedHashMap()) { node -> node.content } + + if (breakdownBySummary.size == 1) { + formatOverloadGroup(breakdownBySummary.values.single(), isSingleNode) + } else { + for ((_, items) in breakdownBySummary) { + + appendAsOverloadGroup(to, platformsOfItems(items)) { + formatOverloadGroup(items) + } + + } + } + } + + private fun formatOverloadGroup(items: List<DocumentationNode>, isSingleNode: Boolean = false) { + for ((index, item) in items.withIndex()) { + if (index > 0) appendLine() + val rendered = languageService.render(item) + item.detailOrNull(NodeKind.Signature)?.let { + if (item.kind !in NodeKind.classLike || !isSingleNode) + appendAnchor(it.name) + } + appendAsSignature(rendered) { + appendCode { appendContent(rendered) } + item.appendSourceLink() + } + item.appendOverrides() + item.appendDeprecation() + item.appendPlatforms() + } + // All items have exactly the same documentation, so we can use any item to render it + val item = items.first() + item.details(NodeKind.OverloadGroupNote).forEach { + appendContent(it.content) + } + + appendContent(item.content.summary) + item.appendDescription() + } + + private fun DocumentationNode.appendSourceLink() { + val sourceUrl = details(NodeKind.SourceUrl).firstOrNull() + if (sourceUrl != null) { + to.append(" ") + appendLink(sourceUrl.name) { to.append("(source)") } + } + } + + private fun DocumentationNode.appendOverrides() { + overrides.forEach { + appendParagraph { + to.append("Overrides ") + val location = location().relativePathTo(it.location()) + + appendLink(FormatLink(it.owner!!.name + "." + it.name, location)) + } + } + } + + private fun DocumentationNode.appendDeprecation() { + if (deprecation != null) { + val deprecationParameter = deprecation!!.details(NodeKind.Parameter).firstOrNull() + val deprecationValue = deprecationParameter?.details(NodeKind.Value)?.firstOrNull() + appendLine() + if (deprecationValue != null) { + appendStrong { to.append("Deprecated:") } + appendText(" " + deprecationValue.name.removeSurrounding("\"")) + appendLine() + appendLine() + } else if (deprecation?.content != Content.Empty) { + appendStrong { to.append("Deprecated:") } + to.append(" ") + appendContent(deprecation!!.content) + } else { + appendStrong { to.append("Deprecated") } + appendLine() + appendLine() + } + } + } + + private fun DocumentationNode.appendPlatforms() { + val platforms = if (isModuleOrPackage()) + platformsToShow.toSet() + platformsOfItems(members) + else + platformsToShow + + if (platforms.isEmpty()) return + + appendParagraph { + appendStrong { to.append("Platform and version requirements:") } + to.append(" " + platforms.joinToString()) + } + } + + protected fun platformsOfItems(items: List<DocumentationNode>): Set<String> { + val platforms = items.asSequence().map { + when (it.kind) { + NodeKind.ExternalClass, NodeKind.Package, NodeKind.Module, NodeKind.GroupNode -> platformsOfItems(it.members) + else -> it.platformsToShow.toSet() + } + } + + fun String.isKotlinVersion() = this.startsWith("Kotlin") + + // Calculating common platforms for items + return platforms.reduce { result, platformsOfItem -> + val otherKotlinVersion = result.find { it.isKotlinVersion() } + val (kotlinVersions, otherPlatforms) = platformsOfItem.partition { it.isKotlinVersion() } + + // When no Kotlin version specified, it means that version is 1.0 + if (otherKotlinVersion != null && kotlinVersions.isNotEmpty()) { + val allKotlinVersions = (kotlinVersions + otherKotlinVersion).distinct() + + val minVersion = allKotlinVersions.min()!! + val resultVersion = when { + allKotlinVersions.size == 1 -> allKotlinVersions.single() + minVersion.endsWith("+") -> minVersion + else -> minVersion + "+" + } + + result.intersect(otherPlatforms) + resultVersion + } else { + result.intersect(platformsOfItem) + } + } + } + + val DocumentationNode.platformsToShow: List<String> + get() = platforms.let { if (it.containsAll(impliedPlatforms)) it - impliedPlatforms else it } + + private fun DocumentationNode.appendDescription() { + if (content.description != ContentEmpty) { + appendContent(content.description) + } + content.getSectionsWithSubjects().forEach { + appendSectionWithSubject(it.key, it.value) + } + + for (section in content.sections.filter { it.subjectName == null }) { + appendSectionWithTag(section) + } + } + + fun appendSectionWithSubject(title: String, subjectSections: List<ContentSection>) { + appendHeader(3) { appendText(title) } + subjectSections.forEach { + val subjectName = it.subjectName + if (subjectName != null) { + appendSoftParagraph { + appendAnchor(subjectName) + appendCode { to.append(subjectName) } + to.append(" - ") + appendContent(it) + } + } + } + } + } + + inner class GroupNodePageBuilder(val node: DocumentationNode) : PageBuilder(listOf(node)) { + + override fun build() { + val breakdownByLocation = node.path.filterNot { it.name.isEmpty() }.map { link(node, it) } + + appendBreadcrumbs(breakdownByLocation) + appendLine() + appendLine() + appendHeader { appendText(node.name) } + + fun DocumentationNode.priority(): Int = when (kind) { + NodeKind.TypeAlias -> 1 + NodeKind.Class -> 2 + else -> 3 + } + + for (member in node.members.sortedBy(DocumentationNode::priority)) { + + appendAsOverloadGroup(to, platformsOfItems(listOf(member))) { + formatSubNodeOfGroup(member) + } + + } + } + + fun formatSubNodeOfGroup(member: DocumentationNode) { + SingleNodePageBuilder(member, true).build() + } + } + + inner class SingleNodePageBuilder(val node: DocumentationNode, noHeader: Boolean = false) + : PageBuilder(listOf(node), noHeader) { + + override fun build() { + super.build() + + if (node.kind == NodeKind.ExternalClass) { + appendSection("Extensions for ${node.name}", node.members) + return + } + + fun DocumentationNode.membersOrGroupMembers(predicate: (DocumentationNode) -> Boolean): List<DocumentationNode> { + return members.filter(predicate) + members(NodeKind.GroupNode).flatMap { it.members.filter(predicate) } + } + + fun DocumentationNode.membersOrGroupMembers(kind: NodeKind): List<DocumentationNode> { + return membersOrGroupMembers { it.kind == kind } + } + + appendSection("Packages", node.members(NodeKind.Package), platformsBasedOnMembers = true) + appendSection("Types", node.membersOrGroupMembers { it.kind in NodeKind.classLike && it.kind != NodeKind.TypeAlias && it.kind != NodeKind.AnnotationClass && it.kind != NodeKind.Exception }) + appendSection("Annotations", node.membersOrGroupMembers(NodeKind.AnnotationClass)) + appendSection("Exceptions", node.membersOrGroupMembers(NodeKind.Exception)) + appendSection("Type Aliases", node.membersOrGroupMembers(NodeKind.TypeAlias)) + appendSection("Extensions for External Classes", node.members(NodeKind.ExternalClass)) + appendSection("Enum Values", node.members(NodeKind.EnumItem), sortMembers = false, omitSamePlatforms = true) + appendSection("Constructors", node.members(NodeKind.Constructor), omitSamePlatforms = true) + appendSection("Properties", node.members(NodeKind.Property), omitSamePlatforms = true) + appendSection("Inherited Properties", node.inheritedMembers(NodeKind.Property)) + appendSection("Functions", node.members(NodeKind.Function), omitSamePlatforms = true) + appendSection("Inherited Functions", node.inheritedMembers(NodeKind.Function)) + appendSection("Companion Object Properties", node.members(NodeKind.CompanionObjectProperty), omitSamePlatforms = true) + appendSection("Inherited Companion Object Properties", node.inheritedCompanionObjectMembers(NodeKind.Property)) + appendSection("Companion Object Functions", node.members(NodeKind.CompanionObjectFunction), omitSamePlatforms = true) + appendSection("Inherited Companion Object Functions", node.inheritedCompanionObjectMembers(NodeKind.Function)) + appendSection("Other members", node.members.filter { + it.kind !in setOf( + NodeKind.Class, + NodeKind.Interface, + NodeKind.Enum, + NodeKind.Object, + NodeKind.AnnotationClass, + NodeKind.Exception, + NodeKind.TypeAlias, + NodeKind.Constructor, + NodeKind.Property, + NodeKind.Package, + NodeKind.Function, + NodeKind.CompanionObjectProperty, + NodeKind.CompanionObjectFunction, + NodeKind.ExternalClass, + NodeKind.EnumItem, + NodeKind.AllTypes, + NodeKind.GroupNode + ) + }) + + val allExtensions = node.extensions + appendSection("Extension Properties", allExtensions.filter { it.kind == NodeKind.Property }) + appendSection("Extension Functions", allExtensions.filter { it.kind == NodeKind.Function }) + appendSection("Companion Object Extension Properties", allExtensions.filter { it.kind == NodeKind.CompanionObjectProperty }) + appendSection("Companion Object Extension Functions", allExtensions.filter { it.kind == NodeKind.CompanionObjectFunction }) + appendSection("Inheritors", + node.inheritors.filter { it.kind != NodeKind.EnumItem }) + + if (node.kind == NodeKind.Module) { + appendHeader(3) { to.append("Index") } + node.members(NodeKind.AllTypes).singleOrNull()?.let { allTypes -> + appendLink(link(node, allTypes, { "All Types" })) + } + } + } + + private fun appendSection(caption: String, members: List<DocumentationNode>, + sortMembers: Boolean = true, + omitSamePlatforms: Boolean = false, + platformsBasedOnMembers: Boolean = false) { + if (members.isEmpty()) return + + appendHeader(3) { appendText(caption) } + + val children = if (sortMembers) members.sortedBy { it.name.toLowerCase() } else members + val membersMap = children.groupBy { link(node, it) } + + + + appendTable("Name", "Summary") { + appendTableBody { + for ((memberLocation, members) in membersMap) { + val elementPlatforms = platformsOfItems(members, omitSamePlatforms) + val platforms = if (platformsBasedOnMembers) + members.flatMapTo(mutableSetOf()) { platformsOfItems(it.members) } + elementPlatforms + else + elementPlatforms + appendIndexRow(platforms) { + appendTableCell { + appendParagraph { + appendLink(memberLocation) + if (members.singleOrNull()?.kind != NodeKind.ExternalClass) { + appendPlatforms(platforms) + } + } + } + appendTableCell { + val breakdownBySummary = members.groupBy { it.summary } + for ((summary, items) in breakdownBySummary) { + appendSummarySignatures(items) + appendContent(summary) + } + } + } + } + } + } + } + + private fun platformsOfItems(items: List<DocumentationNode>, omitSamePlatforms: Boolean = true): Set<String> { + val platforms = platformsOfItems(items) + if (platforms.isNotEmpty() && (platforms != node.platformsToShow.toSet() || !omitSamePlatforms)) { + return platforms + } + return emptySet() + } + + private fun appendSummarySignatures(items: List<DocumentationNode>) { + val summarySignature = languageService.summarizeSignatures(items) + if (summarySignature != null) { + appendAsSignature(summarySignature) { + summarySignature.appendSignature() + } + return + } + val renderedSignatures = items.map { languageService.render(it, RenderMode.SUMMARY) } + renderedSignatures.subList(0, renderedSignatures.size - 1).forEach { + appendAsSignature(it) { + it.appendSignature() + } + appendLine() + } + appendAsSignature(renderedSignatures.last()) { + renderedSignatures.last().appendSignature() + } + } + } + + inner class AllTypesNodeBuilder(val node: DocumentationNode) + : PageBuilder(listOf(node)) { + + override fun build() { + appendContent(node.owner!!.summary) + appendHeader(3) { to.append("All Types") } + + appendTable("Name", "Summary") { + appendTableBody { + for (type in node.members) { + appendTableRow { + appendTableCell { + appendLink(link(node, type) { + if (it.kind == NodeKind.ExternalClass) it.name else it.qualifiedName() + }) + if (type.kind == NodeKind.ExternalClass) { + val packageName = type.owner?.name + if (packageName != null) { + appendText(" (extensions in package $packageName)") + } + } + } + appendTableCell { + appendContent(type.summary) + } + } + } + } + } + } + } + + override fun appendNodes(nodes: Iterable<DocumentationNode>) { + val singleNode = nodes.singleOrNull() + when (singleNode?.kind) { + NodeKind.AllTypes -> AllTypesNodeBuilder(singleNode).build() + NodeKind.GroupNode -> GroupNodePageBuilder(singleNode).build() + null -> PageBuilder(nodes).build() + else -> SingleNodePageBuilder(singleNode).build() + } + } +} + +abstract class StructuredFormatService(val generator: NodeLocationAwareGenerator, + val languageService: LanguageService, + override val extension: String, + override final val linkExtension: String = extension) : FormatService { + +} diff --git a/core/src/main/kotlin/Formats/YamlOutlineService.kt b/core/src/main/kotlin/Formats/YamlOutlineService.kt new file mode 100644 index 000000000..c36f98eb0 --- /dev/null +++ b/core/src/main/kotlin/Formats/YamlOutlineService.kt @@ -0,0 +1,26 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import java.io.File + +class YamlOutlineService @Inject constructor( + val generator: NodeLocationAwareGenerator, + val languageService: LanguageService +) : OutlineFormatService { + override fun getOutlineFileName(location: Location): File = File("${location.path}.yml") + + var outlineLevel = 0 + override fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) { + val indent = " ".repeat(outlineLevel) + to.appendln("$indent- title: ${languageService.renderName(node)}") + to.appendln("$indent url: ${generator.location(node).path}") + } + + override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) { + val indent = " ".repeat(outlineLevel) + to.appendln("$indent content:") + outlineLevel++ + body() + outlineLevel-- + } +} |