blob: 6bc28522e05ebfadc20152d5aae72afac0add2ec [file] [log] [blame]
package org.jetbrains.dokka
import org.jetbrains.dokka.LanguageService.RenderMode
import java.util.*
data class FormatLink(val text: String, val href: String)
enum class ListKind {
Ordered,
Unordered
}
abstract class StructuredFormatService(locationService: LocationService,
val languageService: LanguageService,
override val extension: String) : FormatService {
val locationService: LocationService = locationService.withExtension(extension)
abstract fun appendBlockCode(to: StringBuilder, line: String, language: String)
abstract fun appendHeader(to: StringBuilder, text: String, level: Int = 1)
abstract fun appendParagraph(to: StringBuilder, text: String)
abstract fun appendLine(to: StringBuilder, text: String)
abstract fun appendLine(to: StringBuilder)
abstract fun appendAnchor(to: StringBuilder, anchor: String)
abstract fun appendTable(to: StringBuilder, body: () -> Unit)
abstract fun appendTableHeader(to: StringBuilder, body: () -> Unit)
abstract fun appendTableBody(to: StringBuilder, body: () -> Unit)
abstract fun appendTableRow(to: StringBuilder, body: () -> Unit)
abstract fun appendTableCell(to: StringBuilder, body: () -> Unit)
abstract fun formatText(text: String): String
abstract fun formatSymbol(text: String): String
abstract fun formatKeyword(text: String): String
abstract fun formatIdentifier(text: String, kind: IdentifierKind): String
fun formatEntity(text: String): String = text
abstract fun formatLink(text: String, href: String): String
open fun formatLink(link: FormatLink): String = formatLink(formatText(link.text), link.href)
abstract fun formatStrong(text: String): String
abstract fun formatStrikethrough(text: String): String
abstract fun formatEmphasis(text: String): String
abstract fun formatCode(code: String): String
abstract fun formatUnorderedList(text: String): String
abstract fun formatOrderedList(text: String): String
abstract fun formatListItem(text: String, kind: ListKind): String
abstract fun formatBreadcrumbs(items: Iterable<FormatLink>): String
abstract fun formatNonBreakingSpace(): String
open fun formatSoftLineBreak(): String = ""
open fun formatIndentedSoftLineBreak(): String = ""
open fun formatText(location: Location, nodes: Iterable<ContentNode>, listKind: ListKind = ListKind.Unordered): String {
return nodes.map { formatText(location, it, listKind) }.joinToString("")
}
open fun formatText(location: Location, content: ContentNode, listKind: ListKind = ListKind.Unordered): String {
return StringBuilder().apply {
when (content) {
is ContentText -> append(formatText(content.text))
is ContentSymbol -> append(formatSymbol(content.text))
is ContentKeyword -> append(formatKeyword(content.text))
is ContentIdentifier -> append(formatIdentifier(content.text, content.kind))
is ContentNonBreakingSpace -> append(formatNonBreakingSpace())
is ContentSoftLineBreak -> append(formatSoftLineBreak())
is ContentIndentedSoftLineBreak -> append(formatIndentedSoftLineBreak())
is ContentEntity -> append(formatEntity(content.text))
is ContentStrong -> append(formatStrong(formatText(location, content.children)))
is ContentStrikethrough -> append(formatStrikethrough(formatText(location, content.children)))
is ContentCode -> append(formatCode(formatText(location, content.children)))
is ContentEmphasis -> append(formatEmphasis(formatText(location, content.children)))
is ContentUnorderedList -> append(formatUnorderedList(formatText(location, content.children, ListKind.Unordered)))
is ContentOrderedList -> append(formatOrderedList(formatText(location, content.children, ListKind.Ordered)))
is ContentListItem -> append(formatListItem(formatText(location, content.children), listKind))
is ContentNodeLink -> {
val node = content.node
val linkTo = if (node != null) locationHref(location, node) else "#"
val linkText = formatText(location, content.children)
if (linkTo == ".") {
append(linkText)
} else {
append(formatLink(linkText, linkTo))
}
}
is ContentExternalLink -> {
val linkText = formatText(location, content.children)
if (content.href == ".") {
append(linkText)
} else {
append(formatLink(linkText, content.href))
}
}
is ContentParagraph -> appendParagraph(this, formatText(location, content.children))
is ContentBlockCode -> appendBlockCode(this, formatText(location, content.children), content.language)
is ContentHeading -> appendHeader(this, formatText(location, content.children), content.level)
is ContentBlock -> append(formatText(location, content.children))
}
}.toString()
}
open fun link(from: DocumentationNode, to: DocumentationNode): FormatLink = link(from, to, extension)
open fun link(from: DocumentationNode, to: DocumentationNode, extension: String): FormatLink {
return FormatLink(to.name, locationService.relativePathToLocation(from, to))
}
fun locationHref(from: Location, to: DocumentationNode): String {
val topLevelPage = to.references(DocumentationReference.Kind.TopLevelPage).singleOrNull()?.to
if (topLevelPage != null) {
return from.relativePathTo(locationService.location(topLevelPage), to.name)
}
return from.relativePathTo(locationService.location(to))
}
fun appendDocumentation(location: Location, to: StringBuilder, overloads: Iterable<DocumentationNode>) {
val breakdownBySummary = overloads.groupByTo(LinkedHashMap()) { node -> node.content }
for ((summary, items) in breakdownBySummary) {
appendAsOverloadGroup(to) {
items.forEach {
val rendered = languageService.render(it)
appendAsSignature(to, rendered) {
to.append(formatCode(formatText(location, rendered)))
it.appendSourceLink(to)
}
it.appendOverrides(to)
it.appendDeprecation(location, to)
}
// All items have exactly the same documentation, so we can use any item to render it
val item = items.first()
item.details(DocumentationNode.Kind.OverloadGroupNote).forEach {
to.append(formatText(location, it.content))
}
to.append(formatText(location, item.content.summary))
appendDescription(location, to, item)
appendLine(to)
appendLine(to)
}
}
}
private fun DocumentationNode.isModuleOrPackage(): Boolean =
kind == DocumentationNode.Kind.Module || kind == DocumentationNode.Kind.Package
protected open fun appendAsSignature(to: StringBuilder, node: ContentNode, block: () -> Unit) {
block()
}
protected open fun appendAsOverloadGroup(to: StringBuilder, block: () -> Unit) {
block()
}
fun appendDescription(location: Location, to: StringBuilder, node: DocumentationNode) {
if (node.content.description != ContentEmpty) {
appendLine(to, formatText(location, node.content.description))
appendLine(to)
}
node.content.getSectionsWithSubjects().forEach {
appendSectionWithSubject(it.key, location, it.value, to)
}
for (section in node.content.sections.filter { it.subjectName == null }) {
appendLine(to, formatStrong(formatText(section.tag)))
appendLine(to, formatText(location, section))
}
}
fun Content.getSectionsWithSubjects(): Map<String, List<ContentSection>> =
sections.filter { it.subjectName != null }.groupBy { it.tag }
fun appendSectionWithSubject(title: String, location: Location, subjectSections: List<ContentSection>, to: StringBuilder) {
appendHeader(to, title, 3)
subjectSections.forEach {
val subjectName = it.subjectName
if (subjectName != null) {
appendAnchor(to, subjectName)
to.append(formatCode(subjectName)).append(" - ")
to.append(formatText(location, it))
appendLine(to)
}
}
}
private fun DocumentationNode.appendOverrides(to: StringBuilder) {
overrides.forEach {
to.append("Overrides ")
val location = locationService.relativePathToLocation(this, it)
appendLine(to, formatLink(FormatLink(it.owner!!.name + "." + it.name, location)))
}
}
private fun DocumentationNode.appendDeprecation(location: Location, to: StringBuilder) {
if (deprecation != null) {
val deprecationParameter = deprecation!!.details(DocumentationNode.Kind.Parameter).firstOrNull()
val deprecationValue = deprecationParameter?.details(DocumentationNode.Kind.Value)?.firstOrNull()
if (deprecationValue != null) {
to.append(formatStrong("Deprecated:")).append(" ")
appendLine(to, formatText(deprecationValue.name.removeSurrounding("\"")))
appendLine(to)
} else if (deprecation?.content != Content.Empty) {
to.append(formatStrong("Deprecated:")).append(" ")
to.append(formatText(location, deprecation!!.content))
} else {
appendLine(to, formatStrong("Deprecated"))
appendLine(to)
}
}
}
private fun DocumentationNode.appendSourceLink(to: StringBuilder) {
val sourceUrl = details(DocumentationNode.Kind.SourceUrl).firstOrNull()
if (sourceUrl != null) {
to.append(" ")
appendLine(to, formatLink("(source)", sourceUrl.name))
} else {
appendLine(to)
}
}
fun appendLocation(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) {
val singleNode = nodes.singleOrNull()
if (singleNode != null && singleNode.isModuleOrPackage()) {
if (singleNode.kind == DocumentationNode.Kind.Package) {
appendHeader(to, "Package " + formatText(singleNode.name), 2)
}
to.append(formatText(location, singleNode.content))
} else {
val breakdownByName = nodes.groupBy { node -> node.name }
for ((name, items) in breakdownByName) {
appendHeader(to, formatText(name))
appendDocumentation(location, to, items)
}
}
}
private fun appendSection(location: Location, caption: String, nodes: List<DocumentationNode>, node: DocumentationNode, to: StringBuilder) {
if (nodes.any()) {
appendHeader(to, caption, 3)
val children = nodes.sortedBy { it.name }
val membersMap = children.groupBy { link(node, it) }
appendTable(to) {
appendTableBody(to) {
for ((memberLocation, members) in membersMap) {
appendTableRow(to) {
appendTableCell(to) {
to.append(formatLink(memberLocation))
}
appendTableCell(to) {
val breakdownBySummary = members.groupBy { formatText(location, it.summary) }
for ((summary, items) in breakdownBySummary) {
appendSummarySignatures(items, location, to)
if (!summary.isEmpty()) {
to.append(summary)
}
}
}
}
}
}
}
}
}
private fun appendSummarySignatures(items: List<DocumentationNode>, location: Location, to: StringBuilder) {
val summarySignature = languageService.summarizeSignatures(items)
if (summarySignature != null) {
appendAsSignature(to, summarySignature) {
appendLine(to, summarySignature.signatureToText(location))
}
return
}
val renderedSignatures = items.map { languageService.render(it, RenderMode.SUMMARY) }
renderedSignatures.subList(0, renderedSignatures.size - 1).forEach {
appendAsSignature(to, it) {
appendLine(to, it.signatureToText(location))
}
}
appendAsSignature(to, renderedSignatures.last()) {
to.append(renderedSignatures.last().signatureToText(location))
}
}
private fun ContentNode.signatureToText(location: Location): String {
return if (this is ContentBlock && this.isEmpty()) {
""
} else {
val signatureAsCode = ContentCode()
signatureAsCode.append(this)
formatText(location, signatureAsCode)
}
}
override fun appendNodes(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) {
val breakdownByLocation = nodes.groupBy { node ->
formatBreadcrumbs(node.path.filterNot { it.name.isEmpty() }.map { link(node, it) })
}
for ((breadcrumbs, items) in breakdownByLocation) {
appendLine(to, breadcrumbs)
appendLine(to)
appendLocation(location, to, items.filter { it.kind != DocumentationNode.Kind.ExternalClass })
}
for (node in nodes) {
if (node.kind == DocumentationNode.Kind.ExternalClass) {
appendSection(location, "Extensions for ${node.name}", node.members, node, to)
continue
}
appendSection(location, "Packages", node.members(DocumentationNode.Kind.Package), node, to)
appendSection(location, "Types", node.members.filter { it.kind in DocumentationNode.Kind.classLike }, node, to)
appendSection(location, "Extensions for External Classes", node.members(DocumentationNode.Kind.ExternalClass), node, to)
appendSection(location, "Enum Values", node.members(DocumentationNode.Kind.EnumItem), node, to)
appendSection(location, "Constructors", node.members(DocumentationNode.Kind.Constructor), node, to)
appendSection(location, "Properties", node.members(DocumentationNode.Kind.Property), node, to)
appendSection(location, "Inherited Properties", node.inheritedMembers(DocumentationNode.Kind.Property), node, to)
appendSection(location, "Functions", node.members(DocumentationNode.Kind.Function), node, to)
appendSection(location, "Inherited Functions", node.inheritedMembers(DocumentationNode.Kind.Function), node, to)
appendSection(location, "Companion Object Properties", node.members(DocumentationNode.Kind.CompanionObjectProperty), node, to)
appendSection(location, "Companion Object Functions", node.members(DocumentationNode.Kind.CompanionObjectFunction), node, to)
appendSection(location, "Other members", node.members.filter {
it.kind !in setOf(
DocumentationNode.Kind.Class,
DocumentationNode.Kind.Interface,
DocumentationNode.Kind.Enum,
DocumentationNode.Kind.Object,
DocumentationNode.Kind.AnnotationClass,
DocumentationNode.Kind.Constructor,
DocumentationNode.Kind.Property,
DocumentationNode.Kind.Package,
DocumentationNode.Kind.Function,
DocumentationNode.Kind.CompanionObjectProperty,
DocumentationNode.Kind.CompanionObjectFunction,
DocumentationNode.Kind.ExternalClass,
DocumentationNode.Kind.EnumItem
)
}, node, to)
val allExtensions = collectAllExtensions(node)
appendSection(location, "Extension Properties", allExtensions.filter { it.kind == DocumentationNode.Kind.Property }, node, to)
appendSection(location, "Extension Functions", allExtensions.filter { it.kind == DocumentationNode.Kind.Function }, node, to)
appendSection(location, "Companion Object Extension Properties", allExtensions.filter { it.kind == DocumentationNode.Kind.CompanionObjectProperty }, node, to)
appendSection(location, "Companion Object Extension Functions", allExtensions.filter { it.kind == DocumentationNode.Kind.CompanionObjectFunction }, node, to)
appendSection(location, "Inheritors",
node.inheritors.filter { it.kind != DocumentationNode.Kind.EnumItem }, node, to)
appendSection(location, "Links", node.links, node, to)
}
}
private fun collectAllExtensions(node: DocumentationNode): Collection<DocumentationNode> {
val result = LinkedHashSet<DocumentationNode>()
val visited = hashSetOf<DocumentationNode>()
fun collect(node: DocumentationNode) {
if (!visited.add(node)) return
result.addAll(node.extensions)
node.references(DocumentationReference.Kind.Superclass).forEach { collect(it.to) }
}
collect(node)
return result
}
}