aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/kotlin/Formats
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/main/kotlin/Formats')
-rw-r--r--core/src/main/kotlin/Formats/AnalysisComponents.kt45
-rw-r--r--core/src/main/kotlin/Formats/DacHtmlFormat.kt949
-rw-r--r--core/src/main/kotlin/Formats/DacOutlineService.kt395
-rw-r--r--core/src/main/kotlin/Formats/ExtraOutlineServices.kt20
-rw-r--r--core/src/main/kotlin/Formats/FormatDescriptor.kt42
-rw-r--r--core/src/main/kotlin/Formats/FormatService.kt32
-rw-r--r--core/src/main/kotlin/Formats/GFMFormatService.kt61
-rw-r--r--core/src/main/kotlin/Formats/HtmlFormatService.kt168
-rw-r--r--core/src/main/kotlin/Formats/HtmlTemplateService.kt38
-rw-r--r--core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt141
-rw-r--r--core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt1171
-rw-r--r--core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt165
-rw-r--r--core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt154
-rw-r--r--core/src/main/kotlin/Formats/JekyllFormatService.kt44
-rw-r--r--core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt224
-rw-r--r--core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt186
-rw-r--r--core/src/main/kotlin/Formats/MarkdownFormatService.kt239
-rw-r--r--core/src/main/kotlin/Formats/OutlineService.kt29
-rw-r--r--core/src/main/kotlin/Formats/PackageListService.kt63
-rw-r--r--core/src/main/kotlin/Formats/StandardFormats.kt66
-rw-r--r--core/src/main/kotlin/Formats/StructuredFormatService.kt691
-rw-r--r--core/src/main/kotlin/Formats/YamlOutlineService.kt26
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("&nbsp;&nbsp;")
+ }
+ }
+ +"\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("&nbsp;/&nbsp;")
+ }
+
+ 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("&nbsp;")
+ }
+
+ 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/>&nbsp;&nbsp;&nbsp;&nbsp;")
+ }
+ }
+
+ 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/>&nbsp;&nbsp;&nbsp;&nbsp;")
+ }
+ }
+
+ 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("&nbsp;")
+ }
+ }
+}
+
+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--
+ }
+}