diff options
Diffstat (limited to 'core/src/main/kotlin/Model')
-rw-r--r-- | core/src/main/kotlin/Model/Content.kt | 231 | ||||
-rw-r--r-- | core/src/main/kotlin/Model/DocumentationNode.kt | 162 | ||||
-rw-r--r-- | core/src/main/kotlin/Model/DocumentationReference.kt | 61 | ||||
-rw-r--r-- | core/src/main/kotlin/Model/PackageDocs.kt | 60 | ||||
-rw-r--r-- | core/src/main/kotlin/Model/SourceLinks.kt | 56 |
5 files changed, 570 insertions, 0 deletions
diff --git a/core/src/main/kotlin/Model/Content.kt b/core/src/main/kotlin/Model/Content.kt new file mode 100644 index 00000000..6556b09e --- /dev/null +++ b/core/src/main/kotlin/Model/Content.kt @@ -0,0 +1,231 @@ +package org.jetbrains.dokka + +public interface ContentNode { + val textLength: Int +} + +public object ContentEmpty : ContentNode { + override val textLength: Int get() = 0 +} + +public open class ContentBlock() : ContentNode { + val children = arrayListOf<ContentNode>() + + fun append(node: ContentNode) { + children.add(node) + } + + fun isEmpty() = children.isEmpty() + + override fun equals(other: Any?): Boolean = + other is ContentBlock && javaClass == other.javaClass && children == other.children + + override fun hashCode(): Int = + children.hashCode() + + override val textLength: Int + get() = children.sumBy { it.textLength } +} + +enum class IdentifierKind { + TypeName, + ParameterName, + AnnotationName, + SummarizedTypeName, + Other +} + +public data class ContentText(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +public data class ContentKeyword(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +public data class ContentIdentifier(val text: String, val kind: IdentifierKind = IdentifierKind.Other) : ContentNode { + override val textLength: Int + get() = text.length +} + +public data class ContentSymbol(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +public data class ContentEntity(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +public object ContentNonBreakingSpace: ContentNode { + override val textLength: Int + get() = 1 +} + +public object ContentSoftLineBreak: ContentNode { + override val textLength: Int + get() = 0 +} + +public object ContentIndentedSoftLineBreak: ContentNode { + override val textLength: Int + get() = 0 +} + +public class ContentParagraph() : ContentBlock() +public class ContentEmphasis() : ContentBlock() +public class ContentStrong() : ContentBlock() +public class ContentStrikethrough() : ContentBlock() +public class ContentCode() : ContentBlock() +public class ContentBlockCode(val language: String = "") : ContentBlock() + +public abstract class ContentNodeLink() : ContentBlock() { + abstract val node: DocumentationNode? +} + +public class ContentNodeDirectLink(override val node: DocumentationNode): ContentNodeLink() { + override fun equals(other: Any?): Boolean = + super.equals(other) && other is ContentNodeDirectLink && node.name == other.node.name + + override fun hashCode(): Int = + children.hashCode() * 31 + node.name.hashCode() +} + +public class ContentNodeLazyLink(val linkText: String, val lazyNode: () -> DocumentationNode?): ContentNodeLink() { + override val node: DocumentationNode? get() = lazyNode() + + override fun equals(other: Any?): Boolean = + super.equals(other) && other is ContentNodeLazyLink && linkText == other.linkText + + override fun hashCode(): Int = + children.hashCode() * 31 + linkText.hashCode() +} + +public class ContentExternalLink(val href : String) : ContentBlock() { + override fun equals(other: Any?): Boolean = + super.equals(other) && other is ContentExternalLink && href == other.href + + override fun hashCode(): Int = + children.hashCode() * 31 + href.hashCode() +} + +public class ContentUnorderedList() : ContentBlock() +public class ContentOrderedList() : ContentBlock() +public class ContentListItem() : ContentBlock() + +public class ContentHeading(val level: Int) : ContentBlock() + +public class ContentSection(public val tag: String, public val subjectName: String?) : ContentBlock() { + override fun equals(other: Any?): Boolean = + super.equals(other) && other is ContentSection && tag == other.tag && subjectName == other.subjectName + + override fun hashCode(): Int = + children.hashCode() * 31 * 31 + tag.hashCode() * 31 + (subjectName?.hashCode() ?: 0) +} + +public object ContentTags { + val Description = "Description" + val SeeAlso = "See Also" +} + +fun content(body: ContentBlock.() -> Unit): ContentBlock { + val block = ContentBlock() + block.body() + return block +} + +fun ContentBlock.text(value: String) = append(ContentText(value)) +fun ContentBlock.keyword(value: String) = append(ContentKeyword(value)) +fun ContentBlock.symbol(value: String) = append(ContentSymbol(value)) +fun ContentBlock.identifier(value: String, kind: IdentifierKind = IdentifierKind.Other) = append(ContentIdentifier(value, kind)) +fun ContentBlock.nbsp() = append(ContentNonBreakingSpace) +fun ContentBlock.softLineBreak() = append(ContentSoftLineBreak) +fun ContentBlock.indentedSoftLineBreak() = append(ContentIndentedSoftLineBreak) + +fun ContentBlock.strong(body: ContentBlock.() -> Unit) { + val strong = ContentStrong() + strong.body() + append(strong) +} + +fun ContentBlock.code(body: ContentBlock.() -> Unit) { + val code = ContentCode() + code.body() + append(code) +} + +fun ContentBlock.link(to: DocumentationNode, body: ContentBlock.() -> Unit) { + val block = ContentNodeDirectLink(to) + block.body() + append(block) +} + +public open class Content(): ContentBlock() { + public open val sections: List<ContentSection> get() = emptyList() + public open val summary: ContentNode get() = ContentEmpty + public open val description: ContentNode get() = ContentEmpty + + fun findSectionByTag(tag: String): ContentSection? = + sections.firstOrNull { tag.equals(it.tag, ignoreCase = true) } + + companion object { + val Empty = Content() + + fun of(vararg child: ContentNode): Content { + val result = MutableContent() + child.forEach { result.append(it) } + return result + } + } +} + +public open class MutableContent() : Content() { + private val sectionList = arrayListOf<ContentSection>() + public override val sections: List<ContentSection> + get() = sectionList + + fun addSection(tag: String?, subjectName: String?): ContentSection { + val section = ContentSection(tag ?: "", subjectName) + sectionList.add(section) + return section + } + + public override val summary: ContentNode get() = children.firstOrNull() ?: ContentEmpty + + public override val description: ContentNode by lazy { + val descriptionNodes = children.drop(1) + if (descriptionNodes.isEmpty()) { + ContentEmpty + } else { + val result = ContentSection(ContentTags.Description, null) + result.children.addAll(descriptionNodes) + result + } + } + + override fun equals(other: Any?): Boolean { + if (other !is Content) + return false + return sections == other.sections && children == other.children + } + + override fun hashCode(): Int { + return sections.map { it.hashCode() }.sum() + } + + override fun toString(): String { + if (sections.isEmpty()) + return "<empty>" + return (listOf(summary, description) + sections).joinToString() + } +} + +fun javadocSectionDisplayName(sectionName: String?): String? = + when(sectionName) { + "param" -> "Parameters" + "throws", "exception" -> "Exceptions" + else -> sectionName?.capitalize() + } diff --git a/core/src/main/kotlin/Model/DocumentationNode.kt b/core/src/main/kotlin/Model/DocumentationNode.kt new file mode 100644 index 00000000..52881f65 --- /dev/null +++ b/core/src/main/kotlin/Model/DocumentationNode.kt @@ -0,0 +1,162 @@ +package org.jetbrains.dokka + +import java.util.* + +public open class DocumentationNode(val name: String, + content: Content, + val kind: DocumentationNode.Kind) { + + private val references = LinkedHashSet<DocumentationReference>() + + var content: Content = content + private set + + public val summary: ContentNode get() = content.summary + + public val owner: DocumentationNode? + get() = references(DocumentationReference.Kind.Owner).singleOrNull()?.to + public val details: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Detail).map { it.to } + public val members: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Member).map { it.to } + public val inheritedMembers: List<DocumentationNode> + get() = references(DocumentationReference.Kind.InheritedMember).map { it.to } + public val extensions: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Extension).map { it.to } + public val inheritors: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Inheritor).map { it.to } + public val overrides: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Override).map { it.to } + public val links: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Link).map { it.to } + public val hiddenLinks: List<DocumentationNode> + get() = references(DocumentationReference.Kind.HiddenLink).map { it.to } + public val annotations: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Annotation).map { it.to } + public val deprecation: DocumentationNode? + get() = references(DocumentationReference.Kind.Deprecation).singleOrNull()?.to + + // TODO: Should we allow node mutation? Model merge will copy by ref, so references are transparent, which could nice + public fun addReferenceTo(to: DocumentationNode, kind: DocumentationReference.Kind) { + references.add(DocumentationReference(this, to, kind)) + } + + public fun addAllReferencesFrom(other: DocumentationNode) { + references.addAll(other.references) + } + + public fun updateContent(body: MutableContent.() -> Unit) { + if (content !is MutableContent) { + content = MutableContent() + } + (content as MutableContent).body() + } + + public fun details(kind: DocumentationNode.Kind): List<DocumentationNode> = details.filter { it.kind == kind } + public fun members(kind: DocumentationNode.Kind): List<DocumentationNode> = members.filter { it.kind == kind } + public fun inheritedMembers(kind: DocumentationNode.Kind): List<DocumentationNode> = inheritedMembers.filter { it.kind == kind } + public fun links(kind: DocumentationNode.Kind): List<DocumentationNode> = links.filter { it.kind == kind } + + public fun detail(kind: DocumentationNode.Kind): DocumentationNode = details.filter { it.kind == kind }.single() + public fun member(kind: DocumentationNode.Kind): DocumentationNode = members.filter { it.kind == kind }.single() + public fun link(kind: DocumentationNode.Kind): DocumentationNode = links.filter { it.kind == kind }.single() + + public fun references(kind: DocumentationReference.Kind): List<DocumentationReference> = references.filter { it.kind == kind } + public fun allReferences(): Set<DocumentationReference> = references + + public override fun toString(): String { + return "$kind:$name" + } + + public enum class Kind { + Unknown, + + Package, + Class, + Interface, + Enum, + AnnotationClass, + EnumItem, + Object, + + Constructor, + Function, + Property, + Field, + + CompanionObjectProperty, + CompanionObjectFunction, + + Parameter, + Receiver, + TypeParameter, + Type, + Supertype, + UpperBound, + LowerBound, + Exception, + + Modifier, + NullabilityModifier, + + Module, + + ExternalClass, + Annotation, + + Value, + + SourceUrl, + SourcePosition, + + /** + * A note which is rendered once on a page documenting a group of overloaded functions. + * Needs to be generated equally on all overloads. + */ + OverloadGroupNote; + + companion object { + val classLike = setOf(Class, Interface, Enum, AnnotationClass, Object) + } + } +} + +public class DocumentationModule(name: String, content: Content = Content.Empty) + : DocumentationNode(name, content, DocumentationNode.Kind.Module) { +} + +val DocumentationNode.path: List<DocumentationNode> + get() { + val parent = owner ?: return listOf(this) + return parent.path + this + } + +fun DocumentationNode.findOrCreatePackageNode(packageName: String, packageContent: Map<String, Content>): DocumentationNode { + val existingNode = members(DocumentationNode.Kind.Package).firstOrNull { it.name == packageName } + if (existingNode != null) { + return existingNode + } + val newNode = DocumentationNode(packageName, + packageContent.getOrElse(packageName) { Content.Empty }, + DocumentationNode.Kind.Package) + append(newNode, DocumentationReference.Kind.Member) + return newNode +} + +fun DocumentationNode.append(child: DocumentationNode, kind: DocumentationReference.Kind) { + addReferenceTo(child, kind) + when (kind) { + DocumentationReference.Kind.Detail -> child.addReferenceTo(this, DocumentationReference.Kind.Owner) + DocumentationReference.Kind.Member -> child.addReferenceTo(this, DocumentationReference.Kind.Owner) + DocumentationReference.Kind.Owner -> child.addReferenceTo(this, DocumentationReference.Kind.Member) + else -> { /* Do not add any links back for other types */ } + } +} + +fun DocumentationNode.appendTextNode(text: String, + kind: DocumentationNode.Kind, + refKind: DocumentationReference.Kind = DocumentationReference.Kind.Detail) { + append(DocumentationNode(text, Content.Empty, kind), refKind) +} + +fun DocumentationNode.qualifiedName() = path.drop(1).map { it.name }.filter { it.length > 0 }.joinToString(".") diff --git a/core/src/main/kotlin/Model/DocumentationReference.kt b/core/src/main/kotlin/Model/DocumentationReference.kt new file mode 100644 index 00000000..898c92d7 --- /dev/null +++ b/core/src/main/kotlin/Model/DocumentationReference.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka + +import com.google.inject.Singleton + +public data class DocumentationReference(val from: DocumentationNode, val to: DocumentationNode, val kind: DocumentationReference.Kind) { + public enum class Kind { + Owner, + Member, + InheritedMember, + Detail, + Link, + HiddenLink, + Extension, + Inheritor, + Superclass, + Override, + Annotation, + Deprecation, + TopLevelPage + } +} + +class PendingDocumentationReference(val lazyNodeFrom: () -> DocumentationNode?, + val lazyNodeTo: () -> DocumentationNode?, + val kind: DocumentationReference.Kind) { + fun resolve() { + val fromNode = lazyNodeFrom() + val toNode = lazyNodeTo() + if (fromNode != null && toNode != null) { + fromNode.addReferenceTo(toNode, kind) + } + } +} + +@Singleton +class NodeReferenceGraph() { + private val nodeMap = hashMapOf<String, DocumentationNode>() + val references = arrayListOf<PendingDocumentationReference>() + + fun register(signature: String, node: DocumentationNode) { + nodeMap.put(signature, node) + } + + fun link(fromNode: DocumentationNode, toSignature: String, kind: DocumentationReference.Kind) { + references.add(PendingDocumentationReference({ -> fromNode}, { -> nodeMap[toSignature]}, kind)) + } + + fun link(fromSignature: String, toNode: DocumentationNode, kind: DocumentationReference.Kind) { + references.add(PendingDocumentationReference({ -> nodeMap[fromSignature]}, { -> toNode}, kind)) + } + + fun link(fromSignature: String, toSignature: String, kind: DocumentationReference.Kind) { + references.add(PendingDocumentationReference({ -> nodeMap[fromSignature]}, { -> nodeMap[toSignature]}, kind)) + } + + fun lookup(signature: String): DocumentationNode? = nodeMap[signature] + + fun resolveReferences() { + references.forEach { it.resolve() } + } +} diff --git a/core/src/main/kotlin/Model/PackageDocs.kt b/core/src/main/kotlin/Model/PackageDocs.kt new file mode 100644 index 00000000..044c73d8 --- /dev/null +++ b/core/src/main/kotlin/Model/PackageDocs.kt @@ -0,0 +1,60 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.Singleton +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyPackageDescriptor +import java.io.File + +@Singleton +public class PackageDocs + @Inject constructor(val linkResolver: DeclarationLinkResolver?, + val logger: DokkaLogger) +{ + public val moduleContent: MutableContent = MutableContent() + private val _packageContent: MutableMap<String, MutableContent> = hashMapOf() + public val packageContent: Map<String, Content> + get() = _packageContent + + fun parse(fileName: String, linkResolveContext: LazyPackageDescriptor?) { + val file = File(fileName) + if (file.exists()) { + val text = file.readText() + val tree = parseMarkdown(text) + var targetContent: MutableContent = moduleContent + tree.children.forEach { + if (it.type == MarkdownElementTypes.ATX_1) { + val headingText = it.child(MarkdownTokenTypes.ATX_CONTENT)?.text + if (headingText != null) { + targetContent = findTargetContent(headingText.trimStart()) + } + } else { + buildContentTo(it, targetContent, { resolveContentLink(it, linkResolveContext) }) + } + } + } else { + logger.warn("Include file $file was not found.") + } + } + + private fun findTargetContent(heading: String): MutableContent { + if (heading.startsWith("Module") || heading.startsWith("module")) { + return moduleContent + } + if (heading.startsWith("Package") || heading.startsWith("package")) { + return findOrCreatePackageContent(heading.substring("package".length).trim()) + } + return findOrCreatePackageContent(heading) + } + + private fun findOrCreatePackageContent(packageName: String) = + _packageContent.getOrPut(packageName) { -> MutableContent() } + + private fun resolveContentLink(href: String, linkResolveContext: LazyPackageDescriptor?): ContentBlock { + if (linkResolveContext != null && linkResolver != null) { + return linkResolver.resolveContentLink(linkResolveContext, href) + } + return ContentExternalLink("#") + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Model/SourceLinks.kt b/core/src/main/kotlin/Model/SourceLinks.kt new file mode 100644 index 00000000..956bfe4b --- /dev/null +++ b/core/src/main/kotlin/Model/SourceLinks.kt @@ -0,0 +1,56 @@ +package org.jetbrains.dokka + +import com.intellij.psi.PsiElement +import java.io.File +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiNameIdentifierOwner +import org.jetbrains.kotlin.psi.psiUtil.startOffset + +class SourceLinkDefinition(val path: String, val url: String, val lineSuffix: String?) + +fun DocumentationNode.appendSourceLink(psi: PsiElement?, sourceLinks: List<SourceLinkDefinition>) { + val path = psi?.containingFile?.virtualFile?.path ?: return + + val target = if (psi is PsiNameIdentifierOwner) psi.nameIdentifier else psi + val absPath = File(path).absolutePath + val linkDef = sourceLinks.firstOrNull { absPath.startsWith(it.path) } + if (linkDef != null) { + var url = linkDef.url + path.substring(linkDef.path.length) + if (linkDef.lineSuffix != null) { + val line = target?.lineNumber() + if (line != null) { + url += linkDef.lineSuffix + line.toString() + } + } + append(DocumentationNode(url, Content.Empty, DocumentationNode.Kind.SourceUrl), + DocumentationReference.Kind.Detail); + } + + if (target != null) { + append(DocumentationNode(target.sourcePosition(), Content.Empty, DocumentationNode.Kind.SourcePosition), DocumentationReference.Kind.Detail) + } +} + +private fun PsiElement.sourcePosition(): String { + val path = containingFile.virtualFile.path + val lineNumber = lineNumber() + val columnNumber = columnNumber() + + return when { + lineNumber == null -> path + columnNumber == null -> "$path:$lineNumber" + else -> "$path:$lineNumber:$columnNumber" + } +} + +fun PsiElement.lineNumber(): Int? { + val doc = PsiDocumentManager.getInstance(project).getDocument(containingFile) + // IJ uses 0-based line-numbers; external source browsers use 1-based + return doc?.getLineNumber(textRange.startOffset)?.plus(1) +} + +fun PsiElement.columnNumber(): Int? { + val doc = PsiDocumentManager.getInstance(project).getDocument(containingFile) ?: return null + val lineNumber = doc.getLineNumber(textRange.startOffset) + return startOffset - doc.getLineStartOffset(lineNumber) +}
\ No newline at end of file |