diff options
Diffstat (limited to 'core/src/main')
61 files changed, 6353 insertions, 0 deletions
diff --git a/core/src/main/kotlin/Analysis/AnalysisEnvironment.kt b/core/src/main/kotlin/Analysis/AnalysisEnvironment.kt new file mode 100644 index 00000000..a5e35a0e --- /dev/null +++ b/core/src/main/kotlin/Analysis/AnalysisEnvironment.kt @@ -0,0 +1,210 @@ +package org.jetbrains.dokka + +import com.intellij.core.CoreApplicationEnvironment +import com.intellij.core.CoreModuleManager +import com.intellij.mock.MockComponentManager +import com.intellij.openapi.Disposable +import com.intellij.openapi.extensions.Extensions +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.OrderEnumerationHandler +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.util.Disposer +import com.intellij.psi.PsiElement +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.analyzer.AnalysisResult +import org.jetbrains.kotlin.analyzer.ModuleContent +import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.analyzer.ResolverForModule +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoot +import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots +import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.ContentRoot +import org.jetbrains.kotlin.config.KotlinSourceRoot +import org.jetbrains.kotlin.container.getService +import org.jetbrains.kotlin.context.ProjectContext +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.idea.caches.resolve.KotlinOutOfBlockCompletionModificationTracker +import org.jetbrains.kotlin.idea.caches.resolve.LibraryModificationTracker +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.CompilerEnvironment +import org.jetbrains.kotlin.resolve.jvm.JvmAnalyzerFacade +import org.jetbrains.kotlin.resolve.jvm.JvmPlatformParameters +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import java.io.File + +/** + * Kotlin as a service entry point + * + * Configures environment, analyses files and provides facilities to perform code processing without emitting bytecode + * + * $messageCollector: required by compiler infrastructure and will receive all compiler messages + * $body: optional and can be used to configure environment without creating local variable + */ +public class AnalysisEnvironment(val messageCollector: MessageCollector) : Disposable { + val configuration = CompilerConfiguration(); + + init { + configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector) + } + + fun createCoreEnvironment(): KotlinCoreEnvironment { + val environment = KotlinCoreEnvironment.createForProduction(this, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) + val projectComponentManager = environment.project as MockComponentManager + + val projectFileIndex = CoreProjectFileIndex(environment.project, + environment.configuration.getList(CommonConfigurationKeys.CONTENT_ROOTS)) + + val moduleManager = object : CoreModuleManager(environment.project, this) { + override fun getModules(): Array<out Module> = arrayOf(projectFileIndex.module) + } + + CoreApplicationEnvironment.registerComponentInstance(projectComponentManager.picoContainer, + ModuleManager::class.java, moduleManager) + + Extensions.registerAreaClass("IDEA_MODULE", null) + CoreApplicationEnvironment.registerExtensionPoint(Extensions.getRootArea(), + OrderEnumerationHandler.EP_NAME, OrderEnumerationHandler.Factory::class.java) + + projectComponentManager.registerService(ProjectFileIndex::class.java, + projectFileIndex) + projectComponentManager.registerService(ProjectRootManager::class.java, + CoreProjectRootManager(projectFileIndex)) + projectComponentManager.registerService(LibraryModificationTracker::class.java, + LibraryModificationTracker(environment.project)) + projectComponentManager.registerService(KotlinCacheService::class.java, + KotlinCacheService(environment.project)) + projectComponentManager.registerService(KotlinOutOfBlockCompletionModificationTracker::class.java, + KotlinOutOfBlockCompletionModificationTracker()) + return environment + } + + fun createResolutionFacade(environment: KotlinCoreEnvironment): DokkaResolutionFacade { + val projectContext = ProjectContext(environment.project) + val sourceFiles = environment.getSourceFiles() + + val module = object : ModuleInfo { + override val name: Name = Name.special("<module>") + override fun dependencies(): List<ModuleInfo> = listOf(this) + } + val resolverForProject = JvmAnalyzerFacade.setupResolverForProject( + "Dokka", + projectContext, + listOf(module), + { ModuleContent(sourceFiles, GlobalSearchScope.allScope(environment.project)) }, + JvmPlatformParameters { module }, + CompilerEnvironment + ) + + val resolverForModule = resolverForProject.resolverForModule(module) + return DokkaResolutionFacade(environment.project, resolverForProject.descriptorForModule(module), resolverForModule) + } + + /** + * Classpath for this environment. + */ + public val classpath: List<File> + get() = configuration.jvmClasspathRoots + + /** + * Adds list of paths to classpath. + * $paths: collection of files to add + */ + public fun addClasspath(paths: List<File>) { + configuration.addJvmClasspathRoots(paths) + } + + /** + * Adds path to classpath. + * $path: path to add + */ + public fun addClasspath(path: File) { + configuration.addJvmClasspathRoot(path) + } + + /** + * List of source roots for this environment. + */ + public val sources: List<String> + get() = configuration.get(CommonConfigurationKeys.CONTENT_ROOTS) + ?.filterIsInstance<KotlinSourceRoot>() + ?.map { it.path } ?: emptyList() + + /** + * Adds list of paths to source roots. + * $list: collection of files to add + */ + public fun addSources(list: List<String>) { + list.forEach { + configuration.add(CommonConfigurationKeys.CONTENT_ROOTS, contentRootFromPath(it)) + } + } + + public fun addRoots(list: List<ContentRoot>) { + configuration.addAll(CommonConfigurationKeys.CONTENT_ROOTS, list) + } + + /** + * Disposes the environment and frees all associated resources. + */ + public override fun dispose() { + Disposer.dispose(this) + } +} + +public fun contentRootFromPath(path: String): ContentRoot { + val file = File(path) + return if (file.extension == "java") JavaSourceRoot(file, null) else KotlinSourceRoot(path) +} + + +class DokkaResolutionFacade(override val project: Project, + override val moduleDescriptor: ModuleDescriptor, + val resolverForModule: ResolverForModule) : ResolutionFacade { + + val resolveSession: ResolveSession get() = getFrontendService(ResolveSession::class.java) + + override fun analyze(element: KtElement, bodyResolveMode: BodyResolveMode): BindingContext { + throw UnsupportedOperationException() + } + + override fun analyzeFullyAndGetResult(elements: Collection<KtElement>): AnalysisResult { + throw UnsupportedOperationException() + } + + override fun <T : Any> getFrontendService(element: PsiElement, serviceClass: Class<T>): T { + throw UnsupportedOperationException() + } + + override fun <T : Any> getFrontendService(serviceClass: Class<T>): T { + return resolverForModule.componentProvider.getService(serviceClass) + } + + override fun <T : Any> getFrontendService(moduleDescriptor: ModuleDescriptor, serviceClass: Class<T>): T { + throw UnsupportedOperationException() + } + + override fun <T : Any> getIdeService(serviceClass: Class<T>): T { + throw UnsupportedOperationException() + } + + override fun resolveToDescriptor(declaration: KtDeclaration): DeclarationDescriptor { + return resolveSession.resolveToDescriptor(declaration) + } +} diff --git a/core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt b/core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt new file mode 100644 index 00000000..a1362fde --- /dev/null +++ b/core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt @@ -0,0 +1,550 @@ +package org.jetbrains.dokka + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.BaseComponent +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.SdkAdditionalData +import com.intellij.openapi.projectRoots.SdkModificator +import com.intellij.openapi.projectRoots.SdkTypeId +import com.intellij.openapi.roots.* +import com.intellij.openapi.roots.impl.ProjectOrderEnumerator +import com.intellij.openapi.util.Condition +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.messages.MessageBus +import org.jetbrains.jps.model.module.JpsModuleSourceRootType +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.cli.jvm.config.JvmContentRoot +import org.jetbrains.kotlin.config.ContentRoot +import org.jetbrains.kotlin.config.KotlinSourceRoot +import org.picocontainer.PicoContainer +import java.io.File + +/** + * Workaround for the lack of ability to create a ProjectFileIndex implementation using only + * classes from projectModel-{api,impl}. + */ +class CoreProjectFileIndex(val project: Project, contentRoots: List<ContentRoot>) : ProjectFileIndex, ModuleFileIndex { + val sourceRoots = contentRoots.filter { it !is JvmClasspathRoot } + val classpathRoots = contentRoots.filterIsInstance<JvmClasspathRoot>() + + val module: Module = object : UserDataHolderBase(), Module { + override fun isDisposed(): Boolean { + throw UnsupportedOperationException() + } + + override fun getOptionValue(p0: String): String? { + throw UnsupportedOperationException() + } + + override fun clearOption(p0: String) { + throw UnsupportedOperationException() + } + + override fun getName(): String = "<Dokka module>" + + override fun getModuleWithLibrariesScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleWithDependentsScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleContentScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun isLoaded(): Boolean { + throw UnsupportedOperationException() + } + + override fun setOption(p0: String, p1: String) { + throw UnsupportedOperationException() + } + + override fun getModuleWithDependenciesScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleWithDependenciesAndLibrariesScope(p0: Boolean): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getProject(): Project = project + + override fun getModuleContentWithDependenciesScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleFilePath(): String { + throw UnsupportedOperationException() + } + + override fun getModuleTestsWithDependentsScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleScope(p0: Boolean): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleRuntimeScope(p0: Boolean): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleFile(): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getExtensions(p0: ExtensionPointName<T>): Array<out T> { + throw UnsupportedOperationException() + } + + override fun getComponent(p0: String): BaseComponent? { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getComponent(p0: Class<T>, p1: T): T { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getComponent(interfaceClass: Class<T>): T? { + if (interfaceClass == ModuleRootManager::class.java) { + return moduleRootManager as T + } + throw UnsupportedOperationException() + } + + override fun getDisposed(): Condition<*> { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getComponents(p0: Class<T>): Array<out T> { + throw UnsupportedOperationException() + } + + override fun getPicoContainer(): PicoContainer { + throw UnsupportedOperationException() + } + + override fun hasComponent(p0: Class<*>): Boolean { + throw UnsupportedOperationException() + } + + override fun getMessageBus(): MessageBus { + throw UnsupportedOperationException() + } + + override fun dispose() { + throw UnsupportedOperationException() + } + } + + private val sdk: Sdk = object : Sdk, RootProvider { + override fun getFiles(rootType: OrderRootType): Array<out VirtualFile> = classpathRoots + .map { StandardFileSystems.local().findFileByPath(it.file.path) } + .filterNotNull() + .toTypedArray() + + override fun addRootSetChangedListener(p0: RootProvider.RootSetChangedListener) { + throw UnsupportedOperationException() + } + + override fun addRootSetChangedListener(p0: RootProvider.RootSetChangedListener, p1: Disposable) { + throw UnsupportedOperationException() + } + + override fun getUrls(p0: OrderRootType): Array<out String> { + throw UnsupportedOperationException() + } + + override fun removeRootSetChangedListener(p0: RootProvider.RootSetChangedListener) { + throw UnsupportedOperationException() + } + + override fun getSdkModificator(): SdkModificator { + throw UnsupportedOperationException() + } + + override fun getName(): String = "<dokka SDK>" + + override fun getRootProvider(): RootProvider = this + + override fun getHomePath(): String? { + throw UnsupportedOperationException() + } + + override fun getVersionString(): String? { + throw UnsupportedOperationException() + } + + override fun getSdkAdditionalData(): SdkAdditionalData? { + throw UnsupportedOperationException() + } + + override fun clone(): Any { + throw UnsupportedOperationException() + } + + override fun getSdkType(): SdkTypeId { + throw UnsupportedOperationException() + } + + override fun getHomeDirectory(): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getUserData(p0: Key<T>): T? { + throw UnsupportedOperationException() + } + + override fun <T : Any?> putUserData(p0: Key<T>, p1: T?) { + throw UnsupportedOperationException() + } + } + + private val moduleSourceOrderEntry = object : ModuleSourceOrderEntry { + override fun getFiles(p0: OrderRootType?): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getPresentableName(): String { + throw UnsupportedOperationException() + } + + override fun getUrls(p0: OrderRootType?): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getOwnerModule(): Module = module + + override fun <R : Any?> accept(p0: RootPolicy<R>?, p1: R?): R { + throw UnsupportedOperationException() + } + + override fun isValid(): Boolean { + throw UnsupportedOperationException() + } + + override fun compareTo(other: OrderEntry?): Int { + throw UnsupportedOperationException() + } + + override fun getRootModel(): ModuleRootModel = moduleRootManager + + override fun isSynthetic(): Boolean { + throw UnsupportedOperationException() + } + } + + private val sdkOrderEntry = object : JdkOrderEntry { + override fun getJdkName(): String? { + throw UnsupportedOperationException() + } + + override fun getJdk(): Sdk = sdk + + override fun getFiles(p0: OrderRootType?): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getPresentableName(): String { + throw UnsupportedOperationException() + } + + override fun getUrls(p0: OrderRootType?): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getOwnerModule(): Module { + throw UnsupportedOperationException() + } + + override fun <R : Any?> accept(p0: RootPolicy<R>?, p1: R?): R { + throw UnsupportedOperationException() + } + + override fun isValid(): Boolean { + throw UnsupportedOperationException() + } + + override fun getRootFiles(p0: OrderRootType?): Array<out VirtualFile>? { + throw UnsupportedOperationException() + } + + override fun getRootUrls(p0: OrderRootType?): Array<out String>? { + throw UnsupportedOperationException() + } + + override fun compareTo(other: OrderEntry?): Int { + throw UnsupportedOperationException() + } + + override fun isSynthetic(): Boolean { + throw UnsupportedOperationException() + } + + } + + inner class MyModuleRootManager : ModuleRootManager() { + override fun getExcludeRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getContentEntries(): Array<out ContentEntry> { + throw UnsupportedOperationException() + } + + override fun getExcludeRootUrls(): Array<out String> { + throw UnsupportedOperationException() + } + + override fun <R : Any?> processOrder(p0: RootPolicy<R>?, p1: R): R { + throw UnsupportedOperationException() + } + + override fun getSourceRoots(p0: Boolean): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getSourceRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getSourceRoots(p0: JpsModuleSourceRootType<*>): MutableList<VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getSourceRoots(p0: MutableSet<out JpsModuleSourceRootType<*>>): MutableList<VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getContentRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun orderEntries(): OrderEnumerator = + ProjectOrderEnumerator(project, null).using(object : RootModelProvider { + override fun getModules(): Array<out Module> = arrayOf(module) + + override fun getRootModel(p0: Module): ModuleRootModel = this@MyModuleRootManager + }) + + override fun <T : Any?> getModuleExtension(p0: Class<T>?): T { + throw UnsupportedOperationException() + } + + override fun getDependencyModuleNames(): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getModule(): Module = module + + override fun isSdkInherited(): Boolean { + throw UnsupportedOperationException() + } + + override fun getOrderEntries(): Array<out OrderEntry> = arrayOf(moduleSourceOrderEntry, sdkOrderEntry) + + override fun getSourceRootUrls(): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getSourceRootUrls(p0: Boolean): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getSdk(): Sdk? { + throw UnsupportedOperationException() + } + + override fun getContentRootUrls(): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getModuleDependencies(): Array<out Module> { + throw UnsupportedOperationException() + } + + override fun getModuleDependencies(p0: Boolean): Array<out Module> { + throw UnsupportedOperationException() + } + + override fun getModifiableModel(): ModifiableRootModel { + throw UnsupportedOperationException() + } + + override fun isDependsOn(p0: Module?): Boolean { + throw UnsupportedOperationException() + } + + override fun getFileIndex(): ModuleFileIndex { + return this@CoreProjectFileIndex + } + + override fun getDependencies(): Array<out Module> { + throw UnsupportedOperationException() + } + + override fun getDependencies(p0: Boolean): Array<out Module> { + throw UnsupportedOperationException() + } + } + + val moduleRootManager = MyModuleRootManager() + + override fun getContentRootForFile(p0: VirtualFile): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun getContentRootForFile(p0: VirtualFile, p1: Boolean): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun getPackageNameByDirectory(p0: VirtualFile): String? { + throw UnsupportedOperationException() + } + + override fun isInLibrarySource(file: VirtualFile): Boolean = false + + override fun getClassRootForFile(file: VirtualFile): VirtualFile? = + classpathRoots.firstOrNull { it.contains(file) }?.let { StandardFileSystems.local().findFileByPath(it.file.path) } + + override fun getOrderEntriesForFile(file: VirtualFile): List<OrderEntry> = + if (classpathRoots.contains(file)) listOf(sdkOrderEntry) else emptyList() + + override fun isInLibraryClasses(file: VirtualFile): Boolean = classpathRoots.contains(file) + + override fun isExcluded(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun getSourceRootForFile(p0: VirtualFile): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun isUnderIgnored(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun isLibraryClassFile(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun getModuleForFile(file: VirtualFile): Module? = + if (sourceRoots.contains(file)) module else null + + private fun List<ContentRoot>.contains(file: VirtualFile): Boolean = any { it.contains(file) } + + override fun getModuleForFile(p0: VirtualFile, p1: Boolean): Module? { + throw UnsupportedOperationException() + } + + override fun isInSource(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun isIgnored(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun isContentSourceFile(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun isInSourceContent(file: VirtualFile): Boolean = sourceRoots.contains(file) + + override fun iterateContent(p0: ContentIterator): Boolean { + throw UnsupportedOperationException() + } + + override fun isInContent(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun iterateContentUnderDirectory(p0: VirtualFile, p1: ContentIterator): Boolean { + throw UnsupportedOperationException() + } + + override fun isInTestSourceContent(file: VirtualFile): Boolean = false + + override fun isUnderSourceRootOfType(p0: VirtualFile, p1: MutableSet<out JpsModuleSourceRootType<*>>): Boolean { + throw UnsupportedOperationException() + } + + override fun getOrderEntryForFile(p0: VirtualFile): OrderEntry? { + throw UnsupportedOperationException() + } +} + +class CoreProjectRootManager(val projectFileIndex: CoreProjectFileIndex) : ProjectRootManager() { + override fun orderEntries(): OrderEnumerator { + throw UnsupportedOperationException() + } + + override fun orderEntries(p0: MutableCollection<out Module>): OrderEnumerator { + throw UnsupportedOperationException() + } + + override fun getContentRootsFromAllModules(): Array<out VirtualFile>? { + throw UnsupportedOperationException() + } + + override fun setProjectSdk(p0: Sdk?) { + throw UnsupportedOperationException() + } + + override fun setProjectSdkName(p0: String?) { + throw UnsupportedOperationException() + } + + override fun getModuleSourceRoots(p0: MutableSet<out JpsModuleSourceRootType<*>>): MutableList<VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getContentSourceRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getFileIndex(): ProjectFileIndex = projectFileIndex + + override fun getProjectSdkName(): String? { + throw UnsupportedOperationException() + } + + override fun getProjectSdk(): Sdk? { + throw UnsupportedOperationException() + } + + override fun getContentRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getContentRootUrls(): MutableList<String> { + throw UnsupportedOperationException() + } + +} + +fun ContentRoot.contains(file: VirtualFile) = when (this) { + is JvmContentRoot -> { + val path = if (file.fileSystem.protocol == StandardFileSystems.JAR_PROTOCOL) + StandardFileSystems.getVirtualFileForJar(file)?.path ?: file.path + else + file.path + File(path).startsWith(this.file.absoluteFile) + } + is KotlinSourceRoot -> File(file.path).startsWith(File(this.path).absoluteFile) + else -> false +} diff --git a/core/src/main/kotlin/Formats/FormatDescriptor.kt b/core/src/main/kotlin/Formats/FormatDescriptor.kt new file mode 100644 index 00000000..0c7ca794 --- /dev/null +++ b/core/src/main/kotlin/Formats/FormatDescriptor.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.Formats + +import org.jetbrains.dokka.* +import kotlin.reflect.KClass + +public interface FormatDescriptor { + val formatServiceClass: KClass<out FormatService>? + val outlineServiceClass: KClass<out OutlineFormatService>? + val generatorServiceClass: KClass<out Generator> + val packageDocumentationBuilderClass: KClass<out PackageDocumentationBuilder> + val javaDocumentationBuilderClass: KClass<out JavaDocumentationBuilder> +} diff --git a/core/src/main/kotlin/Formats/FormatService.kt b/core/src/main/kotlin/Formats/FormatService.kt new file mode 100644 index 00000000..7e66a6b7 --- /dev/null +++ b/core/src/main/kotlin/Formats/FormatService.kt @@ -0,0 +1,20 @@ +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 + * * [TextFormatService] – outputs documentation in Text format + */ +public interface FormatService { + /** Returns extension for output files */ + val extension: String + + /** Appends formatted content to [StringBuilder](to) using specified [location] */ + fun appendNodes(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) +} + +/** Format content to [String] using specified [location] */ +fun FormatService.format(location: Location, nodes: Iterable<DocumentationNode>): String = StringBuilder().apply { appendNodes(location, this, nodes) }.toString() diff --git a/core/src/main/kotlin/Formats/HtmlFormatService.kt b/core/src/main/kotlin/Formats/HtmlFormatService.kt new file mode 100644 index 00000000..4d45e6cb --- /dev/null +++ b/core/src/main/kotlin/Formats/HtmlFormatService.kt @@ -0,0 +1,169 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +public open class HtmlFormatService @Inject constructor(@Named("folders") locationService: LocationService, + signatureGenerator: LanguageService, + val templateService: HtmlTemplateService) +: StructuredFormatService(locationService, signatureGenerator, "html"), OutlineFormatService { + override public fun formatText(text: String): String { + return text.htmlEscape() + } + + override fun formatSymbol(text: String): String { + return "<span class=\"symbol\">${formatText(text)}</span>" + } + + override fun formatKeyword(text: String): String { + return "<span class=\"keyword\">${formatText(text)}</span>" + } + + override fun formatIdentifier(text: String, kind: IdentifierKind): String { + return "<span class=\"identifier\">${formatText(text)}</span>" + } + + override fun appendBlockCode(to: StringBuilder, line: String, language: String) { + to.append("<pre><code>") + to.append(line) + to.append("</code></pre>") + } + + override fun appendHeader(to: StringBuilder, text: String, level: Int) { + to.appendln("<h$level>${text}</h$level>") + } + + override fun appendParagraph(to: StringBuilder, text: String) { + to.appendln("<p>${text}</p>") + } + + override fun appendLine(to: StringBuilder, text: String) { + to.appendln("${text}<br/>") + } + + override fun appendLine(to: StringBuilder) { + to.appendln("<br/>") + } + + override fun appendAnchor(to: StringBuilder, anchor: String) { + to.appendln("<a name=\"${anchor.htmlEscape()}\"></a>") + } + + override fun appendTable(to: StringBuilder, body: () -> Unit) { + to.appendln("<table>") + body() + to.appendln("</table>") + } + + override fun appendTableHeader(to: StringBuilder, body: () -> Unit) { + to.appendln("<thead>") + body() + to.appendln("</thead>") + } + + override fun appendTableBody(to: StringBuilder, body: () -> Unit) { + to.appendln("<tbody>") + body() + to.appendln("</tbody>") + } + + override fun appendTableRow(to: StringBuilder, body: () -> Unit) { + to.appendln("<tr>") + body() + to.appendln("</tr>") + } + + override fun appendTableCell(to: StringBuilder, body: () -> Unit) { + to.appendln("<td>") + body() + to.appendln("</td>") + } + + override fun formatLink(text: String, href: String): String { + return "<a href=\"${href}\">${text}</a>" + } + + override fun formatStrong(text: String): String { + return "<strong>${text}</strong>" + } + + override fun formatEmphasis(text: String): String { + return "<emph>${text}</emph>" + } + + override fun formatStrikethrough(text: String): String { + return "<s>${text}</s>" + } + + override fun formatCode(code: String): String { + return "<code>${code}</code>" + } + + override fun formatUnorderedList(text: String): String = "<ul>${text}</ul>" + override fun formatOrderedList(text: String): String = "<ol>${text}</ol>" + + override fun formatListItem(text: String, kind: ListKind): String { + return "<li>${text}</li>" + } + + override fun formatBreadcrumbs(items: Iterable<FormatLink>): String { + return items.map { formatLink(it) }.joinToString(" / ") + } + + + override fun appendNodes(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + templateService.appendHeader(to, getPageTitle(nodes), calcPathToRoot(location)) + super.appendNodes(location, to, nodes) + templateService.appendFooter(to) + } + + override fun appendOutline(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + templateService.appendHeader(to, "Module Contents", calcPathToRoot(location)) + super.appendOutline(location, to, nodes) + templateService.appendFooter(to) + } + + private fun calcPathToRoot(location: Location): Path { + val path = Paths.get(location.path) + return path.parent?.relativize(Paths.get(locationService.root.path + '/')) ?: path + } + + 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 signature = formatText(location, link) + to.appendln("<a href=\"${location.path}\">${signature}</a><br/>") + } + + override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) { + to.appendln("<ul>") + body() + to.appendln("</ul>") + } + + override fun formatNonBreakingSpace(): String = " " +} + +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 + if (path.size == 1) { + return path.first().name + } + val qualifiedName = node.qualifiedName() + if (qualifiedName.length == 0 && path.size == 2) { + return path.first().name + " / root package" + } + return path.first().name + " / " + qualifiedName +} diff --git a/core/src/main/kotlin/Formats/HtmlTemplateService.kt b/core/src/main/kotlin/Formats/HtmlTemplateService.kt new file mode 100644 index 00000000..ae42a31b --- /dev/null +++ b/core/src/main/kotlin/Formats/HtmlTemplateService.kt @@ -0,0 +1,34 @@ +package org.jetbrains.dokka + +import java.nio.file.Path + +public interface HtmlTemplateService { + fun appendHeader(to: StringBuilder, title: String?, basePath: Path) + fun appendFooter(to: StringBuilder) + + companion object { + public fun default(css: String? = null): HtmlTemplateService { + return object : HtmlTemplateService { + override fun appendFooter(to: StringBuilder) { + to.appendln("</BODY>") + to.appendln("</HTML>") + } + override fun appendHeader(to: StringBuilder, title: String?, basePath: Path) { + to.appendln("<HTML>") + to.appendln("<HEAD>") + if (title != null) { + to.appendln("<title>$title</title>") + } + if (css != null) { + val cssPath = basePath.resolve(css) + to.appendln("<link rel=\"stylesheet\" href=\"$cssPath\">") + } + to.appendln("</HEAD>") + to.appendln("<BODY>") + } + } + } + } +} + + diff --git a/core/src/main/kotlin/Formats/JekyllFormatService.kt b/core/src/main/kotlin/Formats/JekyllFormatService.kt new file mode 100644 index 00000000..f81257d6 --- /dev/null +++ b/core/src/main/kotlin/Formats/JekyllFormatService.kt @@ -0,0 +1,22 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject + +open class JekyllFormatService + @Inject constructor(locationService: LocationService, + signatureGenerator: LanguageService, + linkExtension: String = "md") +: MarkdownFormatService(locationService, signatureGenerator, linkExtension) { + + override fun appendNodes(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + to.appendln("---") + appendFrontMatter(nodes, to) + to.appendln("---") + to.appendln("") + super.appendNodes(location, to, nodes) + } + + protected open fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) { + to.appendln("title: ${getPageTitle(nodes)}") + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt new file mode 100644 index 00000000..4eda7910 --- /dev/null +++ b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt @@ -0,0 +1,121 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject + +public class KotlinWebsiteFormatService @Inject constructor(locationService: LocationService, + signatureGenerator: LanguageService) +: JekyllFormatService(locationService, signatureGenerator, "html") { + private var needHardLineBreaks = false + + override fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) { + super.appendFrontMatter(nodes, to) + to.appendln("layout: api") + } + + override public fun formatBreadcrumbs(items: Iterable<FormatLink>): String { + items.drop(1) + + if (items.count() > 1) { + return "<div class='api-docs-breadcrumbs'>" + + items.map { formatLink(it) }.joinToString(" / ") + + "</div>" + } + + return "" + } + + override public fun formatCode(code: String): String = if (code.length > 0) "<code>$code</code>" else "" + + override fun formatStrikethrough(text: String): String = "<s>$text</s>" + + override fun appendAsSignature(to: StringBuilder, node: ContentNode, block: () -> Unit) { + val contentLength = node.textLength + if (contentLength == 0) return + to.append("<div class=\"signature\">") + needHardLineBreaks = contentLength >= 62 + try { + block() + } finally { + needHardLineBreaks = false + } + to.append("</div>") + } + + override fun appendAsOverloadGroup(to: StringBuilder, block: () -> Unit) { + to.append("<div class=\"overload-group\">\n") + block() + to.append("</div>\n") + } + + override fun formatLink(text: String, href: String): String { + return "<a href=\"${href}\">${text}</a>" + } + + override fun appendTable(to: StringBuilder, body: () -> Unit) { + to.appendln("<table class=\"api-docs-table\">") + body() + to.appendln("</table>") + } + + override fun appendTableHeader(to: StringBuilder, body: () -> Unit) { + to.appendln("<thead>") + body() + to.appendln("</thead>") + } + + override fun appendTableBody(to: StringBuilder, body: () -> Unit) { + to.appendln("<tbody>") + body() + to.appendln("</tbody>") + } + + override fun appendTableRow(to: StringBuilder, body: () -> Unit) { + to.appendln("<tr>") + body() + to.appendln("</tr>") + } + + override fun appendTableCell(to: StringBuilder, body: () -> Unit) { + to.appendln("<td markdown=\"1\">") + body() + to.appendln("\n</td>") + } + + override public fun appendBlockCode(to: StringBuilder, line: String, language: String) { + if (language.isNotEmpty()) { + super.appendBlockCode(to, line, language) + } else { + to.append("<pre markdown=\"1\">") + to.append(line.trimStart()) + to.append("</pre>") + } + } + + override fun formatSymbol(text: String): String { + return "<span class=\"symbol\">${formatText(text)}</span>" + } + + override fun formatKeyword(text: String): String { + return "<span class=\"keyword\">${formatText(text)}</span>" + } + + override fun formatIdentifier(text: String, kind: IdentifierKind): String { + return "<span class=\"${identifierClassName(kind)}\">${formatText(text)}</span>" + } + + override fun formatSoftLineBreak(): String = if (needHardLineBreaks) + "<br/>" + else + "" + + override fun formatIndentedSoftLineBreak(): String = if (needHardLineBreaks) + "<br/> " + else + "" + + private fun identifierClassName(kind: IdentifierKind) = when(kind) { + IdentifierKind.ParameterName -> "parameterName" + IdentifierKind.SummarizedTypeName -> "summarizedTypeName" + else -> "identifier" + } +} diff --git a/core/src/main/kotlin/Formats/MarkdownFormatService.kt b/core/src/main/kotlin/Formats/MarkdownFormatService.kt new file mode 100644 index 00000000..f694ae3e --- /dev/null +++ b/core/src/main/kotlin/Formats/MarkdownFormatService.kt @@ -0,0 +1,117 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject + + +public open class MarkdownFormatService + @Inject constructor(locationService: LocationService, + signatureGenerator: LanguageService, + linkExtension: String = "md") +: StructuredFormatService(locationService, signatureGenerator, "md", linkExtension) { + override public fun formatBreadcrumbs(items: Iterable<FormatLink>): String { + return items.map { formatLink(it) }.joinToString(" / ") + } + + override public fun formatText(text: String): String { + return text.htmlEscape() + } + + override fun formatSymbol(text: String): String { + return text.htmlEscape() + } + + override fun formatKeyword(text: String): String { + return text.htmlEscape() + } + override fun formatIdentifier(text: String, kind: IdentifierKind): String { + return text.htmlEscape() + } + + override public fun formatCode(code: String): String { + return "`$code`" + } + + override public fun formatUnorderedList(text: String): String = text + "\n" + override public fun formatOrderedList(text: String): String = text + "\n" + + override fun formatListItem(text: String, kind: ListKind): String { + val itemText = if (text.endsWith("\n")) text else text + "\n" + return if (kind == ListKind.Unordered) "* $itemText" else "1. $itemText" + } + + override public fun formatStrong(text: String): String { + return "**$text**" + } + + override fun formatEmphasis(text: String): String { + return "*$text*" + } + + override fun formatStrikethrough(text: String): String { + return "~~$text~~" + } + + override fun formatLink(text: String, href: String): String { + return "[$text]($href)" + } + + override public fun appendLine(to: StringBuilder) { + to.appendln() + } + + override public fun appendLine(to: StringBuilder, text: String) { + to.appendln(text) + } + + override fun appendAnchor(to: StringBuilder, anchor: String) { + // no anchors in Markdown + } + + override public fun appendParagraph(to: StringBuilder, text: String) { + to.appendln() + to.appendln(text) + to.appendln() + } + + override public fun appendHeader(to: StringBuilder, text: String, level: Int) { + appendLine(to) + appendLine(to, "${"#".repeat(level)} $text") + appendLine(to) + } + + override public fun appendBlockCode(to: StringBuilder, line: String, language: String) { + appendLine(to) + to.appendln("``` ${language}") + to.appendln(line) + to.appendln("```") + appendLine(to) + } + + override fun appendTable(to: StringBuilder, body: () -> Unit) { + to.appendln() + body() + to.appendln() + } + + override fun appendTableHeader(to: StringBuilder, body: () -> Unit) { + body() + } + + override fun appendTableBody(to: StringBuilder, body: () -> Unit) { + body() + } + + override fun appendTableRow(to: StringBuilder, body: () -> Unit) { + to.append("|") + body() + to.appendln() + } + + override fun appendTableCell(to: StringBuilder, body: () -> Unit) { + to.append(" ") + body() + to.append(" |") + } + + override fun formatNonBreakingSpace(): String = " " +} diff --git a/core/src/main/kotlin/Formats/OutlineService.kt b/core/src/main/kotlin/Formats/OutlineService.kt new file mode 100644 index 00000000..6626cf51 --- /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. + */ +public interface OutlineFormatService { + fun getOutlineFileName(location: Location): File + + public fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) + public fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) + + /** Appends formatted outline to [StringBuilder](to) using specified [location] */ + public 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 } + 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/StandardFormats.kt b/core/src/main/kotlin/Formats/StandardFormats.kt new file mode 100644 index 00000000..94e1b115 --- /dev/null +++ b/core/src/main/kotlin/Formats/StandardFormats.kt @@ -0,0 +1,38 @@ +package org.jetbrains.dokka.Formats + +import org.jetbrains.dokka.* + +abstract class KotlinFormatDescriptorBase : FormatDescriptor { + override val packageDocumentationBuilderClass = KotlinPackageDocumentationBuilder::class + override val javaDocumentationBuilderClass = KotlinJavaDocumentationBuilder::class + + override val generatorServiceClass = FileGenerator::class +} + +class HtmlFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = HtmlFormatService::class + override val outlineServiceClass = HtmlFormatService::class +} + +class HtmlAsJavaFormatDescriptor : FormatDescriptor { + override val formatServiceClass = HtmlFormatService::class + override val outlineServiceClass = HtmlFormatService::class + override val generatorServiceClass = FileGenerator::class + override val packageDocumentationBuilderClass = KotlinAsJavaDocumentationBuilder::class + override val javaDocumentationBuilderClass = JavaPsiDocumentationBuilder::class +} + +class KotlinWebsiteFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = KotlinWebsiteFormatService::class + override val outlineServiceClass = YamlOutlineService::class +} + +class JekyllFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = JekyllFormatService::class + override val outlineServiceClass = null +} + +class MarkdownFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = MarkdownFormatService::class + override val outlineServiceClass = null +} diff --git a/core/src/main/kotlin/Formats/StructuredFormatService.kt b/core/src/main/kotlin/Formats/StructuredFormatService.kt new file mode 100644 index 00000000..32a2b68a --- /dev/null +++ b/core/src/main/kotlin/Formats/StructuredFormatService.kt @@ -0,0 +1,367 @@ +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, + val linkExtension: String = extension) : FormatService { + val locationService: LocationService = locationService.withExtension(linkExtension) + + 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 + + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/YamlOutlineService.kt b/core/src/main/kotlin/Formats/YamlOutlineService.kt new file mode 100644 index 00000000..7968824c --- /dev/null +++ b/core/src/main/kotlin/Formats/YamlOutlineService.kt @@ -0,0 +1,24 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import java.io.File + +class YamlOutlineService @Inject constructor(val locationService: LocationService, + 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: ${locationService.location(node).path}") + } + + override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) { + val indent = " ".repeat(outlineLevel) + to.appendln("$indent content:") + outlineLevel++ + body() + outlineLevel-- + } +} diff --git a/core/src/main/kotlin/Generation/ConsoleGenerator.kt b/core/src/main/kotlin/Generation/ConsoleGenerator.kt new file mode 100644 index 00000000..803a16e4 --- /dev/null +++ b/core/src/main/kotlin/Generation/ConsoleGenerator.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka + +public class ConsoleGenerator(val signatureGenerator: LanguageService, val locationService: LocationService) { + val IndentStep = " " + + public fun generate(node: DocumentationNode, indent: String = "") { + println("@${locationService.location(node).path}") + generateHeader(node, indent) + //generateDetails(node, indent) + generateMembers(node, indent) + generateLinks(node, indent) + } + + public fun generateHeader(node: DocumentationNode, indent: String = "") { + println(indent + signatureGenerator.render(node)) + val docString = node.content.toString() + if (!docString.isEmpty()) + println("$indent\"${docString.replace("\n", "\n$indent")}\"") + println() + } + + public fun generateMembers(node: DocumentationNode, indent: String = "") { + val items = node.members.sortedBy { it.name } + for (child in items) + generate(child, indent + IndentStep) + } + + public fun generateDetails(node: DocumentationNode, indent: String = "") { + val items = node.details + for (child in items) + generate(child, indent + " ") + } + + public fun generateLinks(node: DocumentationNode, indent: String = "") { + val items = node.links + if (items.isEmpty()) + return + println("$indent Links") + for (child in items) + generate(child, indent + " ") + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Generation/FileGenerator.kt b/core/src/main/kotlin/Generation/FileGenerator.kt new file mode 100644 index 00000000..a762bae3 --- /dev/null +++ b/core/src/main/kotlin/Generation/FileGenerator.kt @@ -0,0 +1,57 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStreamWriter + +public class FileGenerator @Inject constructor(val locationService: FileLocationService) : Generator { + + @set:Inject(optional = true) var outlineService: OutlineFormatService? = null + @set:Inject(optional = true) lateinit var formatService: FormatService + + override fun buildPages(nodes: Iterable<DocumentationNode>) { + val specificLocationService = locationService.withExtension(formatService.extension) + + for ((location, items) in nodes.groupBy { specificLocationService.location(it) }) { + val file = location.file + file.parentFile?.mkdirsOrFail() + try { + FileOutputStream(file).use { + OutputStreamWriter(it, Charsets.UTF_8).use { + it.write(formatService.format(location, items)) + } + } + } catch (e: Throwable) { + println(e) + } + buildPages(items.flatMap { it.members }) + } + } + + override fun buildOutlines(nodes: Iterable<DocumentationNode>) { + val outlineService = this.outlineService ?: return + for ((location, items) in nodes.groupBy { locationService.location(it) }) { + val file = outlineService.getOutlineFileName(location) + file.parentFile?.mkdirsOrFail() + FileOutputStream(file).use { + OutputStreamWriter(it, Charsets.UTF_8).use { + it.write(outlineService.formatOutline(location, items)) + } + } + } + } + + override fun buildSupportFiles() { + FileOutputStream(locationService.location(listOf("style.css"), false).file).use { + javaClass.getResourceAsStream("/dokka/styles/style.css").copyTo(it) + } + } +} + +private fun File.mkdirsOrFail() { + if (!mkdirs() && !exists()) { + throw IOException("Failed to create directory $this") + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Generation/Generator.kt b/core/src/main/kotlin/Generation/Generator.kt new file mode 100644 index 00000000..ac10a6a5 --- /dev/null +++ b/core/src/main/kotlin/Generation/Generator.kt @@ -0,0 +1,19 @@ +package org.jetbrains.dokka + +public interface Generator { + fun buildPages(nodes: Iterable<DocumentationNode>) + fun buildOutlines(nodes: Iterable<DocumentationNode>) + fun buildSupportFiles() +} + +fun Generator.buildAll(nodes: Iterable<DocumentationNode>) { + buildPages(nodes) + buildOutlines(nodes) + buildSupportFiles() +} + +fun Generator.buildPage(node: DocumentationNode): Unit = buildPages(listOf(node)) + +fun Generator.buildOutline(node: DocumentationNode): Unit = buildOutlines(listOf(node)) + +fun Generator.buildAll(node: DocumentationNode): Unit = buildAll(listOf(node)) diff --git a/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt new file mode 100644 index 00000000..3c9875cd --- /dev/null +++ b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt @@ -0,0 +1,266 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.psi.* +import org.jetbrains.dokka.DocumentationNode.Kind + +fun getSignature(element: PsiElement?) = when(element) { + is PsiClass -> element.qualifiedName + is PsiField -> element.containingClass!!.qualifiedName + "#" + element.name + is PsiMethod -> + element.containingClass!!.qualifiedName + "#" + element.name + "(" + + element.parameterList.parameters.map { it.type.typeSignature() }.joinToString(",") + ")" + else -> null +} + +private fun PsiType.typeSignature(): String = when(this) { + is PsiArrayType -> "Array<${componentType.typeSignature()}>" + else -> mapTypeName(this) +} + +private fun mapTypeName(psiType: PsiType): String = when (psiType) { + is PsiPrimitiveType -> psiType.canonicalText + is PsiClassType -> psiType.resolve()?.qualifiedName ?: psiType.className + is PsiEllipsisType -> mapTypeName(psiType.componentType) + is PsiArrayType -> "Array" + else -> psiType.canonicalText +} + +interface JavaDocumentationBuilder { + fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) +} + +class JavaPsiDocumentationBuilder : JavaDocumentationBuilder { + private val options: DocumentationOptions + private val refGraph: NodeReferenceGraph + private val docParser: JavaDocumentationParser + + @Inject constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph) { + this.options = options + this.refGraph = refGraph + this.docParser = JavadocParser(refGraph) + } + + constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph, docParser: JavaDocumentationParser) { + this.options = options + this.refGraph = refGraph + this.docParser = docParser + } + + override fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) { + if (file.classes.all { skipElement(it) }) { + return + } + val packageNode = module.findOrCreatePackageNode(file.packageName, emptyMap()) + appendClasses(packageNode, file.classes) + } + + fun appendClasses(packageNode: DocumentationNode, classes: Array<PsiClass>) { + packageNode.appendChildren(classes) { build() } + } + + fun register(element: PsiElement, node: DocumentationNode) { + val signature = getSignature(element) + if (signature != null) { + refGraph.register(signature, node) + } + } + + fun link(node: DocumentationNode, element: PsiElement?) { + val qualifiedName = getSignature(element) + if (qualifiedName != null) { + refGraph.link(node, qualifiedName, DocumentationReference.Kind.Link) + } + } + + fun link(element: PsiElement?, node: DocumentationNode, kind: DocumentationReference.Kind) { + val qualifiedName = getSignature(element) + if (qualifiedName != null) { + refGraph.link(qualifiedName, node, kind) + } + } + + fun nodeForElement(element: PsiNamedElement, + kind: Kind, + name: String = element.name ?: "<anonymous>"): DocumentationNode { + val (docComment, deprecatedContent) = docParser.parseDocumentation(element) + val node = DocumentationNode(name, docComment, kind) + if (element is PsiModifierListOwner) { + node.appendModifiers(element) + val modifierList = element.modifierList + if (modifierList != null) { + modifierList.annotations.filter { !ignoreAnnotation(it) }.forEach { + val annotation = it.build() + node.append(annotation, + if (it.qualifiedName == "java.lang.Deprecated") DocumentationReference.Kind.Deprecation else DocumentationReference.Kind.Annotation) + } + } + } + if (deprecatedContent != null) { + val deprecationNode = DocumentationNode("", deprecatedContent, Kind.Modifier) + node.append(deprecationNode, DocumentationReference.Kind.Deprecation) + } + if (element is PsiDocCommentOwner && element.isDeprecated && node.deprecation == null) { + val deprecationNode = DocumentationNode("", Content.of(ContentText("Deprecated")), Kind.Modifier) + node.append(deprecationNode, DocumentationReference.Kind.Deprecation) + } + return node + } + + fun ignoreAnnotation(annotation: PsiAnnotation) = when(annotation.qualifiedName) { + "java.lang.SuppressWarnings" -> true + else -> false + } + + fun <T : Any> DocumentationNode.appendChildren(elements: Array<T>, + kind: DocumentationReference.Kind = DocumentationReference.Kind.Member, + buildFn: T.() -> DocumentationNode) { + elements.forEach { + if (!skipElement(it)) { + append(it.buildFn(), kind) + } + } + } + + private fun skipElement(element: Any) = skipElementByVisibility(element) || hasSuppressTag(element) + + private fun skipElementByVisibility(element: Any): Boolean = + !options.includeNonPublic && element is PsiModifierListOwner && + (element.hasModifierProperty(PsiModifier.PRIVATE) || element.hasModifierProperty(PsiModifier.PACKAGE_LOCAL)) + + private fun hasSuppressTag(element: Any) = + element is PsiDocCommentOwner && element.docComment?.let { it.findTagByName("suppress") != null } ?: false + + fun <T : Any> DocumentationNode.appendMembers(elements: Array<T>, buildFn: T.() -> DocumentationNode) = + appendChildren(elements, DocumentationReference.Kind.Member, buildFn) + + fun <T : Any> DocumentationNode.appendDetails(elements: Array<T>, buildFn: T.() -> DocumentationNode) = + appendChildren(elements, DocumentationReference.Kind.Detail, buildFn) + + fun PsiClass.build(): DocumentationNode { + val kind = when { + isInterface -> DocumentationNode.Kind.Interface + isEnum -> DocumentationNode.Kind.Enum + isAnnotationType -> DocumentationNode.Kind.AnnotationClass + else -> DocumentationNode.Kind.Class + } + val node = nodeForElement(this, kind) + superTypes.filter { !ignoreSupertype(it) }.forEach { + node.appendType(it, Kind.Supertype) + val superClass = it.resolve() + if (superClass != null) { + link(superClass, node, DocumentationReference.Kind.Inheritor) + } + } + node.appendDetails(typeParameters) { build() } + node.appendMembers(methods) { build() } + node.appendMembers(fields) { build() } + node.appendMembers(innerClasses) { build() } + register(this, node) + return node + } + + fun ignoreSupertype(psiType: PsiClassType): Boolean = + psiType.isClass("java.lang.Enum") || psiType.isClass("java.lang.Object") + + fun PsiClassType.isClass(qName: String): Boolean { + val shortName = qName.substringAfterLast('.') + if (className == shortName) { + val psiClass = resolve() + return psiClass?.qualifiedName == qName + } + return false + } + + fun PsiField.build(): DocumentationNode { + val node = nodeForElement(this, nodeKind()) + node.appendType(type) + node.appendModifiers(this) + register(this, node) + return node + } + + private fun PsiField.nodeKind(): Kind = when { + this is PsiEnumConstant -> Kind.EnumItem + else -> Kind.Field + } + + fun PsiMethod.build(): DocumentationNode { + val node = nodeForElement(this, nodeKind(), + if (isConstructor) "<init>" else name) + + if (!isConstructor) { + node.appendType(returnType) + } + node.appendDetails(parameterList.parameters) { build() } + node.appendDetails(typeParameters) { build() } + register(this, node) + return node + } + + private fun PsiMethod.nodeKind(): Kind = when { + isConstructor -> Kind.Constructor + else -> Kind.Function + } + + fun PsiParameter.build(): DocumentationNode { + val node = nodeForElement(this, Kind.Parameter) + node.appendType(type) + if (type is PsiEllipsisType) { + node.appendTextNode("vararg", Kind.Modifier, DocumentationReference.Kind.Detail) + } + return node + } + + fun PsiTypeParameter.build(): DocumentationNode { + val node = nodeForElement(this, Kind.TypeParameter) + extendsListTypes.forEach { node.appendType(it, Kind.UpperBound) } + implementsListTypes.forEach { node.appendType(it, Kind.UpperBound) } + return node + } + + fun DocumentationNode.appendModifiers(element: PsiModifierListOwner) { + val modifierList = element.modifierList ?: return + + PsiModifier.MODIFIERS.forEach { + if (modifierList.hasExplicitModifier(it)) { + appendTextNode(it, Kind.Modifier) + } + } + } + + fun DocumentationNode.appendType(psiType: PsiType?, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type) { + if (psiType == null) { + return + } + append(psiType.build(kind), DocumentationReference.Kind.Detail) + } + + fun PsiType.build(kind: DocumentationNode.Kind = DocumentationNode.Kind.Type): DocumentationNode { + val name = mapTypeName(this) + val node = DocumentationNode(name, Content.Empty, kind) + if (this is PsiClassType) { + node.appendDetails(parameters) { build(Kind.Type) } + link(node, resolve()) + } + if (this is PsiArrayType && this !is PsiEllipsisType) { + node.append(componentType.build(Kind.Type), DocumentationReference.Kind.Detail) + } + return node + } + + fun PsiAnnotation.build(): DocumentationNode { + val node = DocumentationNode(nameReferenceElement?.text ?: "<?>", Content.Empty, DocumentationNode.Kind.Annotation) + parameterList.attributes.forEach { + val parameter = DocumentationNode(it.name ?: "value", Content.Empty, DocumentationNode.Kind.Parameter) + val value = it.value + if (value != null) { + val valueText = (value as? PsiLiteralExpression)?.value as? String ?: value.text + val valueNode = DocumentationNode(valueText, Content.Empty, DocumentationNode.Kind.Value) + parameter.append(valueNode, DocumentationReference.Kind.Detail) + } + node.append(parameter, DocumentationReference.Kind.Detail) + } + return node + } +} diff --git a/core/src/main/kotlin/Java/JavadocParser.kt b/core/src/main/kotlin/Java/JavadocParser.kt new file mode 100644 index 00000000..1378a5a7 --- /dev/null +++ b/core/src/main/kotlin/Java/JavadocParser.kt @@ -0,0 +1,170 @@ +package org.jetbrains.dokka + +import com.intellij.psi.* +import com.intellij.psi.javadoc.PsiDocTag +import com.intellij.psi.javadoc.PsiDocTagValue +import com.intellij.psi.javadoc.PsiDocToken +import com.intellij.psi.javadoc.PsiInlineDocTag +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode + +data class JavadocParseResult(val content: Content, val deprecatedContent: Content?) { + companion object { + val Empty = JavadocParseResult(Content.Empty, null) + } +} + +interface JavaDocumentationParser { + fun parseDocumentation(element: PsiNamedElement): JavadocParseResult +} + +class JavadocParser(private val refGraph: NodeReferenceGraph) : JavaDocumentationParser { + override fun parseDocumentation(element: PsiNamedElement): JavadocParseResult { + val docComment = (element as? PsiDocCommentOwner)?.docComment + if (docComment == null) return JavadocParseResult.Empty + val result = MutableContent() + var deprecatedContent: Content? = null + val para = ContentParagraph() + result.append(para) + para.convertJavadocElements(docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() }) + docComment.tags.forEach { tag -> + when(tag.name) { + "see" -> result.convertSeeTag(tag) + "deprecated" -> { + deprecatedContent = Content() + deprecatedContent!!.convertJavadocElements(tag.contentElements()) + } + else -> { + val subjectName = tag.getSubjectName() + val section = result.addSection(javadocSectionDisplayName(tag.name), subjectName) + + section.convertJavadocElements(tag.contentElements()) + } + } + } + return JavadocParseResult(result, deprecatedContent) + } + + private fun PsiDocTag.contentElements(): Iterable<PsiElement> { + val tagValueElements = children + .dropWhile { it.node?.elementType == JavaDocTokenType.DOC_TAG_NAME } + .dropWhile { it is PsiWhiteSpace } + .filterNot { it.node?.elementType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS } + return if (getSubjectName() != null) tagValueElements.dropWhile { it is PsiDocTagValue } else tagValueElements + } + + private fun ContentBlock.convertJavadocElements(elements: Iterable<PsiElement>) { + val htmlBuilder = StringBuilder() + elements.forEach { + if (it is PsiInlineDocTag) { + htmlBuilder.append(convertInlineDocTag(it)) + } else { + htmlBuilder.append(it.text) + } + } + val doc = Jsoup.parse(htmlBuilder.toString().trimStart()) + doc.body().childNodes().forEach { + convertHtmlNode(it) + } + } + + private fun ContentBlock.convertHtmlNode(node: Node) { + if (node is TextNode) { + append(ContentText(node.text())) + } else if (node is Element) { + val childBlock = createBlock(node) + node.childNodes().forEach { + childBlock.convertHtmlNode(it) + } + append(childBlock) + } + } + + private fun createBlock(element: Element): ContentBlock = when(element.tagName()) { + "p" -> ContentParagraph() + "b", "strong" -> ContentStrong() + "i", "em" -> ContentEmphasis() + "s", "del" -> ContentStrikethrough() + "code" -> ContentCode() + "pre" -> ContentBlockCode() + "ul" -> ContentUnorderedList() + "ol" -> ContentOrderedList() + "li" -> ContentListItem() + "a" -> createLink(element) + else -> ContentBlock() + } + + private fun createLink(element: Element): ContentBlock { + val docref = element.attr("docref") + if (docref != null) { + return ContentNodeLazyLink(docref, { -> refGraph.lookup(docref)}) + } + val href = element.attr("href") + if (href != null) { + return ContentExternalLink(href) + } else { + return ContentBlock() + } + } + + private fun MutableContent.convertSeeTag(tag: PsiDocTag) { + val linkElement = tag.linkElement() + if (linkElement == null) { + return + } + val seeSection = findSectionByTag(ContentTags.SeeAlso) ?: addSection(ContentTags.SeeAlso, null) + val linkSignature = resolveLink(linkElement) + val text = ContentText(linkElement.text) + if (linkSignature != null) { + val linkNode = ContentNodeLazyLink(tag.valueElement!!.text, { -> refGraph.lookup(linkSignature)}) + linkNode.append(text) + seeSection.append(linkNode) + } else { + seeSection.append(text) + } + } + + private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) { + "link", "linkplain" -> { + val valueElement = tag.linkElement() + val linkSignature = resolveLink(valueElement) + if (linkSignature != null) { + val labelText = tag.dataElements.firstOrNull { it is PsiDocToken }?.text ?: valueElement!!.text + val link = "<a docref=\"$linkSignature\">${labelText.htmlEscape()}</a>" + if (tag.name == "link") "<code>$link</code>" else link + } + else if (valueElement != null) { + valueElement.text + } else { + "" + } + } + "code", "literal" -> { + val text = StringBuilder() + tag.dataElements.forEach { text.append(it.text) } + val escaped = text.toString().trimStart().htmlEscape() + if (tag.name == "code") "<code>$escaped</code>" else escaped + } + else -> tag.text + } + + private fun PsiDocTag.linkElement(): PsiElement? = + valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace } + + private fun resolveLink(valueElement: PsiElement?): String? { + val target = valueElement?.reference?.resolve() + if (target != null) { + return getSignature(target) + } + return null + } + + fun PsiDocTag.getSubjectName(): String? { + if (name == "param" || name == "throws" || name == "exception") { + return valueElement?.text + } + return null + } +} diff --git a/core/src/main/kotlin/Kotlin/ContentBuilder.kt b/core/src/main/kotlin/Kotlin/ContentBuilder.kt new file mode 100644 index 00000000..c4bb18de --- /dev/null +++ b/core/src/main/kotlin/Kotlin/ContentBuilder.kt @@ -0,0 +1,132 @@ +package org.jetbrains.dokka + +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.html.entities.EntityConverter +import java.util.* + +public fun buildContent(tree: MarkdownNode, linkResolver: (String) -> ContentBlock, inline: Boolean = false): MutableContent { + val result = MutableContent() + if (inline) { + buildInlineContentTo(tree, result, linkResolver) + } + else { + buildContentTo(tree, result, linkResolver) + } + return result +} + +public fun buildContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: (String) -> ContentBlock) { +// println(tree.toTestString()) + val nodeStack = ArrayDeque<ContentBlock>() + nodeStack.push(target) + + tree.visit {node, processChildren -> + val parent = nodeStack.peek() + + fun appendNodeWithChildren(content: ContentBlock) { + nodeStack.push(content) + processChildren() + parent.append(nodeStack.pop()) + } + + when (node.type) { + MarkdownElementTypes.ATX_1 -> appendNodeWithChildren(ContentHeading(1)) + MarkdownElementTypes.ATX_2 -> appendNodeWithChildren(ContentHeading(2)) + MarkdownElementTypes.ATX_3 -> appendNodeWithChildren(ContentHeading(3)) + MarkdownElementTypes.ATX_4 -> appendNodeWithChildren(ContentHeading(4)) + MarkdownElementTypes.ATX_5 -> appendNodeWithChildren(ContentHeading(5)) + MarkdownElementTypes.ATX_6 -> appendNodeWithChildren(ContentHeading(6)) + MarkdownElementTypes.UNORDERED_LIST -> appendNodeWithChildren(ContentUnorderedList()) + MarkdownElementTypes.ORDERED_LIST -> appendNodeWithChildren(ContentOrderedList()) + MarkdownElementTypes.LIST_ITEM -> appendNodeWithChildren(ContentListItem()) + MarkdownElementTypes.EMPH -> appendNodeWithChildren(ContentEmphasis()) + MarkdownElementTypes.STRONG -> appendNodeWithChildren(ContentStrong()) + MarkdownElementTypes.CODE_SPAN -> appendNodeWithChildren(ContentCode()) + MarkdownElementTypes.CODE_BLOCK, + MarkdownElementTypes.CODE_FENCE -> appendNodeWithChildren(ContentBlockCode()) + MarkdownElementTypes.PARAGRAPH -> appendNodeWithChildren(ContentParagraph()) + + MarkdownElementTypes.INLINE_LINK -> { + val label = node.child(MarkdownElementTypes.LINK_TEXT)?.child(MarkdownTokenTypes.TEXT) + val destination = node.child(MarkdownElementTypes.LINK_DESTINATION) + if (label != null) { + if (destination != null) { + val link = ContentExternalLink(destination.text) + link.append(ContentText(label.text)) + parent.append(link) + } else { + val link = ContentExternalLink(label.text) + link.append(ContentText(label.text)) + parent.append(link) + } + } + } + MarkdownElementTypes.SHORT_REFERENCE_LINK, + MarkdownElementTypes.FULL_REFERENCE_LINK -> { + val label = node.child(MarkdownElementTypes.LINK_LABEL)?.child(MarkdownTokenTypes.TEXT) + if (label != null) { + val link = linkResolver(label.text) + val linkText = node.child(MarkdownElementTypes.LINK_TEXT)?.child(MarkdownTokenTypes.TEXT) + link.append(ContentText(linkText?.text ?: label.text)) + parent.append(link) + } + } + MarkdownTokenTypes.WHITE_SPACE, + MarkdownTokenTypes.EOL -> { + if (keepWhitespace(nodeStack.peek()) && node.parent?.children?.last() != node) { + parent.append(ContentText(node.text)) + } + } + + MarkdownTokenTypes.CODE -> { + val block = ContentBlockCode() + block.append(ContentText(node.text)) + parent.append(block) + } + + MarkdownTokenTypes.TEXT -> { + fun createEntityOrText(text: String): ContentNode { + if (text == "&" || text == """ || text == "<" || text == ">") { + return ContentEntity(text) + } + if (text == "&") { + return ContentEntity("&") + } + val decodedText = EntityConverter.replaceEntities(text, true, true) + if (decodedText != text) { + return ContentEntity(text) + } + return ContentText(text) + } + + parent.append(createEntityOrText(node.text)) + } + + MarkdownTokenTypes.COLON, + MarkdownTokenTypes.DOUBLE_QUOTE, + MarkdownTokenTypes.LT, + MarkdownTokenTypes.GT, + MarkdownTokenTypes.LPAREN, + MarkdownTokenTypes.RPAREN, + MarkdownTokenTypes.LBRACKET, + MarkdownTokenTypes.RBRACKET, + MarkdownTokenTypes.CODE_FENCE_CONTENT -> { + parent.append(ContentText(node.text)) + } + else -> { + processChildren() + } + } + } +} + +private fun keepWhitespace(node: ContentNode) = node is ContentParagraph || node is ContentSection + +public fun buildInlineContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: (String) -> ContentBlock) { + val inlineContent = tree.children.singleOrNull { it.type == MarkdownElementTypes.PARAGRAPH }?.children ?: listOf(tree) + inlineContent.forEach { + buildContentTo(it, target, linkResolver) + } +} + diff --git a/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt b/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt new file mode 100644 index 00000000..2569bc71 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt @@ -0,0 +1,43 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink + +class DeclarationLinkResolver + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val refGraph: NodeReferenceGraph, + val logger: DokkaLogger) { + fun resolveContentLink(fromDescriptor: DeclarationDescriptor, href: String): ContentBlock { + val symbol = try { + val symbols = resolveKDocLink(resolutionFacade, fromDescriptor, null, href.split('.').toList()) + findTargetSymbol(symbols) + } catch(e: Exception) { + null + } + + // don't include unresolved links in generated doc + // assume that if an href doesn't contain '/', it's not an attempt to reference an external file + if (symbol != null) { + return ContentNodeLazyLink(href, { -> refGraph.lookup(symbol.signature()) }) + } + if ("/" in href) { + return ContentExternalLink(href) + } + logger.warn("Unresolved link to $href in doc comment of ${fromDescriptor.signatureWithSourceLocation()}") + return ContentExternalLink("#") + } + + fun findTargetSymbol(symbols: Collection<DeclarationDescriptor>): DeclarationDescriptor? { + if (symbols.isEmpty()) { + return null + } + val symbol = symbols.first() + if (symbol is CallableMemberDescriptor && symbol.kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE) { + return symbol.overriddenDescriptors.firstOrNull() + } + return symbol + } + +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt b/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt new file mode 100644 index 00000000..b7705ec9 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt @@ -0,0 +1,199 @@ +package org.jetbrains.dokka.Kotlin + +import com.google.inject.Inject +import com.intellij.psi.PsiDocCommentOwner +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.* +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.idea.kdoc.KDocFinder +import org.jetbrains.kotlin.idea.kdoc.getResolutionScope +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.load.java.descriptors.JavaCallableMemberDescriptor +import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.ResolutionScope +import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered +import org.jetbrains.kotlin.resolve.source.PsiSourceElement + +class DescriptorDocumentationParser + @Inject constructor(val options: DocumentationOptions, + val logger: DokkaLogger, + val linkResolver: DeclarationLinkResolver, + val resolutionFacade: DokkaResolutionFacade, + val refGraph: NodeReferenceGraph) +{ + fun parseDocumentation(descriptor: DeclarationDescriptor, inline: Boolean = false): Content = + parseDocumentationAndDetails(descriptor, inline).first + + fun parseDocumentationAndDetails(descriptor: DeclarationDescriptor, inline: Boolean = false): Pair<Content, (DocumentationNode) -> Unit> { + if (descriptor is JavaClassDescriptor || descriptor is JavaCallableMemberDescriptor) { + return parseJavadoc(descriptor) + } + + val kdoc = KDocFinder.findKDoc(descriptor) ?: findStdlibKDoc(descriptor) + if (kdoc == null) { + if (options.reportUndocumented && !descriptor.isDeprecated() && + descriptor !is ValueParameterDescriptor && descriptor !is TypeParameterDescriptor && + descriptor !is PropertyAccessorDescriptor) { + logger.warn("No documentation for ${descriptor.signatureWithSourceLocation()}") + } + return Content.Empty to { node -> } + } + var kdocText = kdoc.getContent() + // workaround for code fence parsing problem in IJ markdown parser + if (kdocText.endsWith("```") || kdocText.endsWith("~~~")) { + kdocText += "\n" + } + val tree = parseMarkdown(kdocText) + val content = buildContent(tree, { href -> linkResolver.resolveContentLink(descriptor, href) }, inline) + if (kdoc is KDocSection) { + val tags = kdoc.getTags() + tags.forEach { + when (it.name) { + "sample" -> + content.append(functionBody(descriptor, it.getSubjectName())) + "see" -> + content.addTagToSeeAlso(descriptor, it) + else -> { + val section = content.addSection(javadocSectionDisplayName(it.name), it.getSubjectName()) + val sectionContent = it.getContent() + val markdownNode = parseMarkdown(sectionContent) + buildInlineContentTo(markdownNode, section, { href -> linkResolver.resolveContentLink(descriptor, href) }) + } + } + } + } + return content to { node -> } + } + + /** + * Special case for generating stdlib documentation (the Any class to which the override chain will resolve + * is not the same one as the Any class included in the source scope). + */ + fun findStdlibKDoc(descriptor: DeclarationDescriptor): KDocTag? { + if (descriptor !is CallableMemberDescriptor) { + return null + } + val name = descriptor.name.asString() + if (name == "equals" || name == "hashCode" || name == "toString") { + var deepestDescriptor: CallableMemberDescriptor = descriptor + while (!deepestDescriptor.overriddenDescriptors.isEmpty()) { + deepestDescriptor = deepestDescriptor.overriddenDescriptors.first() + } + if (DescriptorUtils.getFqName(deepestDescriptor.containingDeclaration).asString() == "kotlin.Any") { + val anyClassDescriptors = resolutionFacade.resolveSession.getTopLevelClassDescriptors( + FqName.fromSegments(listOf("kotlin", "Any")), NoLookupLocation.FROM_IDE) + anyClassDescriptors.forEach { + val anyMethod = it.getMemberScope(listOf()) + .getDescriptorsFiltered(DescriptorKindFilter.FUNCTIONS, { it == descriptor.name }) + .single() + val kdoc = KDocFinder.findKDoc(anyMethod) + if (kdoc != null) { + return kdoc + } + } + } + } + return null + } + + fun parseJavadoc(descriptor: DeclarationDescriptor): Pair<Content, (DocumentationNode) -> Unit> { + val psi = ((descriptor as? DeclarationDescriptorWithSource)?.source as? PsiSourceElement)?.psi + if (psi is PsiDocCommentOwner) { + val parseResult = JavadocParser(refGraph).parseDocumentation(psi as PsiNamedElement) + return parseResult.content to { node -> + parseResult.deprecatedContent?.let { + val deprecationNode = DocumentationNode("", it, DocumentationNode.Kind.Modifier) + node.append(deprecationNode, DocumentationReference.Kind.Deprecation) + } + } + } + return Content.Empty to { node -> } + } + + fun KDocSection.getTags(): Array<KDocTag> = PsiTreeUtil.getChildrenOfType(this, KDocTag::class.java) ?: arrayOf() + + private fun MutableContent.addTagToSeeAlso(descriptor: DeclarationDescriptor, seeTag: KDocTag) { + val subjectName = seeTag.getSubjectName() + if (subjectName != null) { + val seeSection = findSectionByTag("See Also") ?: addSection("See Also", null) + val link = linkResolver.resolveContentLink(descriptor, subjectName) + link.append(ContentText(subjectName)) + val para = ContentParagraph() + para.append(link) + seeSection.append(para) + } + } + + private fun functionBody(descriptor: DeclarationDescriptor, functionName: String?): ContentNode { + if (functionName == null) { + logger.warn("Missing function name in @sample in ${descriptor.signature()}") + return ContentBlockCode().let() { it.append(ContentText("Missing function name in @sample")); it } + } + val scope = getResolutionScope(resolutionFacade, descriptor) + val rootPackage = resolutionFacade.moduleDescriptor.getPackage(FqName.ROOT) + val rootScope = rootPackage.memberScope + val symbol = resolveInScope(functionName, scope) ?: resolveInScope(functionName, rootScope) + if (symbol == null) { + logger.warn("Unresolved function $functionName in @sample in ${descriptor.signature()}") + return ContentBlockCode().let() { it.append(ContentText("Unresolved: $functionName")); it } + } + val psiElement = DescriptorToSourceUtils.descriptorToDeclaration(symbol) + if (psiElement == null) { + logger.warn("Can't find source for function $functionName in @sample in ${descriptor.signature()}") + return ContentBlockCode().let() { it.append(ContentText("Source not found: $functionName")); it } + } + + val text = when (psiElement) { + is KtDeclarationWithBody -> ContentBlockCode().let() { + val bodyExpression = psiElement.bodyExpression + when (bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> psiElement.text + } + + val lines = text.trimEnd().split("\n".toRegex()).toTypedArray().filterNot { it.length == 0 } + val indent = lines.map { it.takeWhile { it.isWhitespace() }.count() }.min() ?: 0 + val finalText = lines.map { it.drop(indent) }.joinToString("\n") + return ContentBlockCode("kotlin").let() { it.append(ContentText(finalText)); it } + } + + private fun resolveInScope(functionName: String, scope: ResolutionScope): DeclarationDescriptor? { + var currentScope = scope + val parts = functionName.split('.') + + var symbol: DeclarationDescriptor? = null + + for (part in parts) { + // short name + val symbolName = Name.guess(part) + val partSymbol = currentScope.getContributedDescriptors(DescriptorKindFilter.ALL, { it == symbolName }) + .filter { it.name == symbolName } + .firstOrNull() + + if (partSymbol == null) { + symbol = null + break + } + currentScope = if (partSymbol is ClassDescriptor) + partSymbol.defaultType.memberScope + else + getResolutionScope(resolutionFacade, partSymbol) + symbol = partSymbol + } + + return symbol + } +} diff --git a/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt b/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt new file mode 100644 index 00000000..6551ded6 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt @@ -0,0 +1,653 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiJavaFile +import org.jetbrains.dokka.DocumentationNode.Kind +import org.jetbrains.dokka.Kotlin.DescriptorDocumentationParser +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.annotations.Annotated +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.idea.caches.resolve.getModuleInfo +import org.jetbrains.kotlin.idea.kdoc.KDocFinder +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtModifierListOwner +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.constants.CompileTimeConstant +import org.jetbrains.kotlin.resolve.constants.ConstantValue +import org.jetbrains.kotlin.resolve.constants.TypedCompileTimeConstant +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.isDocumentedAnnotation +import org.jetbrains.kotlin.resolve.jvm.JavaDescriptorResolver +import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform +import org.jetbrains.kotlin.resolve.source.PsiSourceElement +import org.jetbrains.kotlin.resolve.source.getPsi +import org.jetbrains.kotlin.types.ErrorUtils +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeProjection + +public data class DocumentationOptions(val outputDir: String, + val outputFormat: String, + val includeNonPublic: Boolean = false, + val reportUndocumented: Boolean = true, + val skipEmptyPackages: Boolean = true, + val skipDeprecated: Boolean = false, + val sourceLinks: List<SourceLinkDefinition>) + +private fun isSamePackage(descriptor1: DeclarationDescriptor, descriptor2: DeclarationDescriptor): Boolean { + val package1 = DescriptorUtils.getParentOfType(descriptor1, PackageFragmentDescriptor::class.java) + val package2 = DescriptorUtils.getParentOfType(descriptor2, PackageFragmentDescriptor::class.java) + return package1 != null && package2 != null && package1.fqName == package2.fqName +} + +interface PackageDocumentationBuilder { + fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>) +} + +class DocumentationBuilder + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val descriptorDocumentationParser: DescriptorDocumentationParser, + val options: DocumentationOptions, + val refGraph: NodeReferenceGraph, + val logger: DokkaLogger) +{ + val visibleToDocumentation = setOf(Visibilities.PROTECTED, Visibilities.PUBLIC) + val boringBuiltinClasses = setOf( + "kotlin.Unit", "kotlin.Byte", "kotlin.Short", "kotlin.Int", "kotlin.Long", "kotlin.Char", "kotlin.Boolean", + "kotlin.Float", "kotlin.Double", "kotlin.String", "kotlin.Array", "kotlin.Any") + val knownModifiers = setOf( + KtTokens.PUBLIC_KEYWORD, KtTokens.PROTECTED_KEYWORD, KtTokens.INTERNAL_KEYWORD, KtTokens.PRIVATE_KEYWORD, + KtTokens.OPEN_KEYWORD, KtTokens.FINAL_KEYWORD, KtTokens.ABSTRACT_KEYWORD, KtTokens.SEALED_KEYWORD, + KtTokens.OVERRIDE_KEYWORD) + + fun link(node: DocumentationNode, descriptor: DeclarationDescriptor, kind: DocumentationReference.Kind) { + refGraph.link(node, descriptor.signature(), kind) + } + + fun link(fromDescriptor: DeclarationDescriptor?, toDescriptor: DeclarationDescriptor?, kind: DocumentationReference.Kind) { + if (fromDescriptor != null && toDescriptor != null) { + refGraph.link(fromDescriptor.signature(), toDescriptor.signature(), kind) + } + } + + fun register(descriptor: DeclarationDescriptor, node: DocumentationNode) { + refGraph.register(descriptor.signature(), node) + } + + fun <T> nodeForDescriptor(descriptor: T, kind: Kind): DocumentationNode where T : DeclarationDescriptor, T : Named { + val (doc, callback) = descriptorDocumentationParser.parseDocumentationAndDetails(descriptor, kind == Kind.Parameter) + val node = DocumentationNode(descriptor.name.asString(), doc, kind).withModifiers(descriptor) + callback(node) + return node + } + + private fun DocumentationNode.withModifiers(descriptor: DeclarationDescriptor) : DocumentationNode{ + if (descriptor is MemberDescriptor) { + appendVisibility(descriptor) + if (descriptor !is ConstructorDescriptor) { + appendModality(descriptor) + } + } + return this + } + + fun DocumentationNode.appendModality(descriptor: MemberDescriptor) { + var modality = descriptor.modality + if (modality == Modality.OPEN) { + val containingClass = descriptor.containingDeclaration as? ClassDescriptor + if (containingClass?.modality == Modality.FINAL) { + modality = Modality.FINAL + } + } + val modifier = modality.name.toLowerCase() + appendTextNode(modifier, DocumentationNode.Kind.Modifier) + } + + fun DocumentationNode.appendVisibility(descriptor: DeclarationDescriptorWithVisibility) { + val modifier = descriptor.visibility.normalize().displayName + appendTextNode(modifier, DocumentationNode.Kind.Modifier) + } + + fun DocumentationNode.appendSupertypes(descriptor: ClassDescriptor) { + val superTypes = descriptor.typeConstructor.supertypes + for (superType in superTypes) { + if (!ignoreSupertype(superType)) { + appendType(superType, DocumentationNode.Kind.Supertype) + val superclass = superType?.constructor?.declarationDescriptor + link(superclass, descriptor, DocumentationReference.Kind.Inheritor) + link(descriptor, superclass, DocumentationReference.Kind.Superclass) + } + } + } + + private fun ignoreSupertype(superType: KotlinType): Boolean { + val superClass = superType.constructor.declarationDescriptor as? ClassDescriptor + if (superClass != null) { + val fqName = DescriptorUtils.getFqNameSafe(superClass).asString() + return fqName == "kotlin.Annotation" || fqName == "kotlin.Enum" || fqName == "kotlin.Any" + } + return false + } + + fun DocumentationNode.appendProjection(projection: TypeProjection, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type) { + if (projection.isStarProjection) { + appendTextNode("*", Kind.Type) + } + else { + appendType(projection.type, kind, projection.projectionKind.label) + } + } + + fun DocumentationNode.appendType(kotlinType: KotlinType?, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type, prefix: String = "") { + if (kotlinType == null) + return + val classifierDescriptor = kotlinType.constructor.declarationDescriptor + val name = when (classifierDescriptor) { + is ClassDescriptor -> { + if (classifierDescriptor.isCompanionObject) { + classifierDescriptor.containingDeclaration.name.asString() + + "." + classifierDescriptor.name.asString() + } + else { + classifierDescriptor.name.asString() + } + } + is Named -> classifierDescriptor.name.asString() + else -> "<anonymous>" + } + val node = DocumentationNode(name, Content.Empty, kind) + if (prefix != "") { + node.appendTextNode(prefix, Kind.Modifier) + } + if (kotlinType.isMarkedNullable) { + node.appendTextNode("?", Kind.NullabilityModifier) + } + if (classifierDescriptor != null) { + link(node, classifierDescriptor, + if (classifierDescriptor.isBoringBuiltinClass()) DocumentationReference.Kind.HiddenLink else DocumentationReference.Kind.Link) + } + + append(node, DocumentationReference.Kind.Detail) + node.appendAnnotations(kotlinType) + for (typeArgument in kotlinType.arguments) { + node.appendProjection(typeArgument) + } + } + + fun ClassifierDescriptor.isBoringBuiltinClass(): Boolean = + DescriptorUtils.getFqName(this).asString() in boringBuiltinClasses + + fun DocumentationNode.appendAnnotations(annotated: Annotated) { + annotated.annotations.filter { it.isDocumented() }.forEach { + val annotationNode = it.build() + if (annotationNode != null) { + append(annotationNode, + if (annotationNode.isDeprecation()) DocumentationReference.Kind.Deprecation else DocumentationReference.Kind.Annotation) + } + } + } + + fun DocumentationNode.appendModifiers(descriptor: DeclarationDescriptor) { + val psi = (descriptor as DeclarationDescriptorWithSource).source.getPsi() as? KtModifierListOwner ?: return + KtTokens.MODIFIER_KEYWORDS_ARRAY.filter { it !in knownModifiers }.forEach { + if (psi.hasModifier(it)) { + appendTextNode(it.value, Kind.Modifier) + } + } + } + + fun DocumentationNode.isDeprecation() = name == "Deprecated" || name == "deprecated" + + fun DocumentationNode.appendSourceLink(sourceElement: SourceElement) { + appendSourceLink(sourceElement.getPsi(), options.sourceLinks) + } + + fun DocumentationNode.appendChild(descriptor: DeclarationDescriptor, kind: DocumentationReference.Kind): DocumentationNode? { + // do not include generated code + if (descriptor is CallableMemberDescriptor && descriptor.kind != CallableMemberDescriptor.Kind.DECLARATION) + return null + + if (descriptor.isDocumented()) { + val node = descriptor.build() + append(node, kind) + return node + } + return null + } + + fun DeclarationDescriptor.isDocumented(): Boolean { + return (options.includeNonPublic + || this !is MemberDescriptor + || this.visibility in visibleToDocumentation) && + !isDocumentationSuppressed() && + (!options.skipDeprecated || !isDeprecated()) + } + + fun DocumentationNode.appendMembers(descriptors: Iterable<DeclarationDescriptor>): List<DocumentationNode> { + val nodes = descriptors.map { descriptor -> + if (descriptor is CallableMemberDescriptor && descriptor.kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE) { + val baseDescriptor = descriptor.overriddenDescriptors.firstOrNull() + if (baseDescriptor != null) { + link(this, baseDescriptor, DocumentationReference.Kind.InheritedMember) + } + null + } + else { + val descriptorToUse = if (descriptor is ConstructorDescriptor) descriptor else descriptor.original + appendChild(descriptorToUse, DocumentationReference.Kind.Member) + } + } + return nodes.filterNotNull() + } + + fun DocumentationNode.appendInPageChildren(descriptors: Iterable<DeclarationDescriptor>, kind: DocumentationReference.Kind) { + descriptors.forEach { descriptor -> + val node = appendChild(descriptor, kind) + node?.addReferenceTo(this, DocumentationReference.Kind.TopLevelPage) + } + } + + fun DocumentationModule.appendFragments(fragments: Collection<PackageFragmentDescriptor>, + packageContent: Map<String, Content>, + packageDocumentationBuilder: PackageDocumentationBuilder) { + val allFqNames = fragments.map { it.fqName }.distinct() + + for (packageName in allFqNames) { + val declarations = fragments.filter { it.fqName == packageName }.flatMap { it.getMemberScope().getContributedDescriptors() } + + if (options.skipEmptyPackages && declarations.none { it.isDocumented() }) continue + logger.info(" package $packageName: ${declarations.count()} declarations") + val packageNode = findOrCreatePackageNode(packageName.asString(), packageContent) + packageDocumentationBuilder.buildPackageDocumentation(this@DocumentationBuilder, packageName, packageNode, declarations) + } + } + + fun DeclarationDescriptor.build(): DocumentationNode = when (this) { + is ClassDescriptor -> build() + is ConstructorDescriptor -> build() + is PropertyDescriptor -> build() + is FunctionDescriptor -> build() + is TypeParameterDescriptor -> build() + is ValueParameterDescriptor -> build() + is ReceiverParameterDescriptor -> build() + else -> throw IllegalStateException("Descriptor $this is not known") + } + + fun ClassDescriptor.build(): DocumentationNode { + val kind = when (kind) { + ClassKind.OBJECT -> Kind.Object + ClassKind.INTERFACE -> Kind.Interface + ClassKind.ENUM_CLASS -> Kind.Enum + ClassKind.ANNOTATION_CLASS -> Kind.AnnotationClass + ClassKind.ENUM_ENTRY -> Kind.EnumItem + else -> Kind.Class + } + val node = nodeForDescriptor(this, kind) + node.appendSupertypes(this) + if (getKind() != ClassKind.OBJECT && getKind() != ClassKind.ENUM_ENTRY) { + node.appendInPageChildren(typeConstructor.parameters, DocumentationReference.Kind.Detail) + val constructorsToDocument = if (getKind() == ClassKind.ENUM_CLASS) + constructors.filter { it.valueParameters.size > 0 } + else + constructors + node.appendMembers(constructorsToDocument) + } + val members = defaultType.memberScope.getContributedDescriptors().filter { it != companionObjectDescriptor } + node.appendMembers(members) + node.appendMembers(staticScope.getContributedDescriptors()).forEach { + it.appendTextNode("static", Kind.Modifier) + } + val companionObjectDescriptor = companionObjectDescriptor + if (companionObjectDescriptor != null) { + node.appendMembers(companionObjectDescriptor.defaultType.memberScope.getContributedDescriptors()) + } + node.appendAnnotations(this) + node.appendModifiers(this) + node.appendSourceLink(source) + register(this, node) + return node + } + + fun ConstructorDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, Kind.Constructor) + node.appendInPageChildren(valueParameters, DocumentationReference.Kind.Detail) + register(this, node) + return node + } + + private fun CallableMemberDescriptor.inCompanionObject(): Boolean { + val containingDeclaration = containingDeclaration + if ((containingDeclaration as? ClassDescriptor)?.isCompanionObject ?: false) { + return true + } + val receiver = extensionReceiverParameter + return (receiver?.type?.constructor?.declarationDescriptor as? ClassDescriptor)?.isCompanionObject ?: false + } + + fun FunctionDescriptor.build(): DocumentationNode { + if (ErrorUtils.containsErrorType(this)) { + logger.warn("Found an unresolved type in ${signatureWithSourceLocation()}") + } + + val node = nodeForDescriptor(this, if (inCompanionObject()) Kind.CompanionObjectFunction else Kind.Function) + + node.appendInPageChildren(typeParameters, DocumentationReference.Kind.Detail) + extensionReceiverParameter?.let { node.appendChild(it, DocumentationReference.Kind.Detail) } + node.appendInPageChildren(valueParameters, DocumentationReference.Kind.Detail) + node.appendType(returnType) + node.appendAnnotations(this) + node.appendModifiers(this) + node.appendSourceLink(source) + + overriddenDescriptors.forEach { + addOverrideLink(it, this) + } + + register(this, node) + return node + } + + fun addOverrideLink(baseClassFunction: CallableMemberDescriptor, overridingFunction: CallableMemberDescriptor) { + val source = baseClassFunction.original.source.getPsi() + if (source != null) { + link(overridingFunction, baseClassFunction, DocumentationReference.Kind.Override) + } else { + baseClassFunction.overriddenDescriptors.forEach { + addOverrideLink(it, overridingFunction) + } + } + } + + fun PropertyDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, if (inCompanionObject()) Kind.CompanionObjectProperty else Kind.Property) + node.appendInPageChildren(typeParameters, DocumentationReference.Kind.Detail) + extensionReceiverParameter?.let { node.appendChild(it, DocumentationReference.Kind.Detail) } + node.appendType(returnType) + node.appendAnnotations(this) + node.appendModifiers(this) + node.appendSourceLink(source) + if (isVar) { + node.appendTextNode("var", DocumentationNode.Kind.Modifier) + } + getter?.let { + if (!it.isDefault) { + node.addAccessorDocumentation(descriptorDocumentationParser.parseDocumentation(it), "Getter") + } + } + setter?.let { + if (!it.isDefault) { + node.addAccessorDocumentation(descriptorDocumentationParser.parseDocumentation(it), "Setter") + } + } + + overriddenDescriptors.forEach { + addOverrideLink(it, this) + } + + register(this, node) + return node + } + + fun DocumentationNode.addAccessorDocumentation(documentation: Content, prefix: String) { + if (documentation == Content.Empty) return + updateContent { + if (!documentation.children.isEmpty()) { + val section = addSection(prefix, null) + documentation.children.forEach { section.append(it) } + } + documentation.sections.forEach { + val section = addSection("$prefix ${it.tag}", it.subjectName) + it.children.forEach { section.append(it) } + } + } + } + + fun ValueParameterDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, Kind.Parameter) + node.appendType(varargElementType ?: type) + if (declaresDefaultValue()) { + val psi = source.getPsi() as? KtParameter + if (psi != null) { + val defaultValueText = psi.defaultValue?.text + if (defaultValueText != null) { + node.appendTextNode(defaultValueText, Kind.Value) + } + } + } + node.appendAnnotations(this) + node.appendModifiers(this) + if (varargElementType != null && node.details(Kind.Modifier).none { it.name == "vararg" }) { + node.appendTextNode("vararg", Kind.Modifier) + } + register(this, node) + return node + } + + fun TypeParameterDescriptor.build(): DocumentationNode { + val doc = descriptorDocumentationParser.parseDocumentation(this) + val name = name.asString() + val prefix = variance.label + + val node = DocumentationNode(name, doc, DocumentationNode.Kind.TypeParameter) + if (prefix != "") { + node.appendTextNode(prefix, Kind.Modifier) + } + if (isReified) { + node.appendTextNode("reified", Kind.Modifier) + } + + for (constraint in upperBounds) { + if (KotlinBuiltIns.isDefaultBound(constraint)) { + continue + } + node.appendType(constraint, Kind.UpperBound) + } + + for (constraint in lowerBounds) { + if (KotlinBuiltIns.isNothing(constraint)) + continue + node.appendType(constraint, Kind.LowerBound) + } + return node + } + + fun ReceiverParameterDescriptor.build(): DocumentationNode { + var receiverClass: DeclarationDescriptor = type.constructor.declarationDescriptor!! + if ((receiverClass as? ClassDescriptor)?.isCompanionObject ?: false) { + receiverClass = receiverClass.containingDeclaration!! + } + link(receiverClass, + containingDeclaration, + DocumentationReference.Kind.Extension) + + val node = DocumentationNode(name.asString(), Content.Empty, Kind.Receiver) + node.appendType(type) + return node + } + + fun AnnotationDescriptor.build(): DocumentationNode? { + val annotationClass = type.constructor.declarationDescriptor + if (annotationClass == null || ErrorUtils.isError(annotationClass)) { + return null + } + val node = DocumentationNode(annotationClass.name.asString(), Content.Empty, DocumentationNode.Kind.Annotation) + val arguments = allValueArguments.toList().sortedBy { it.first.index } + arguments.forEach { + val valueNode = it.second.toDocumentationNode() + if (valueNode != null) { + val paramNode = DocumentationNode(it.first.name.asString(), Content.Empty, DocumentationNode.Kind.Parameter) + paramNode.append(valueNode, DocumentationReference.Kind.Detail) + node.append(paramNode, DocumentationReference.Kind.Detail) + } + } + return node + } + + fun CompileTimeConstant<Any?>.build(): DocumentationNode? = when (this) { + is TypedCompileTimeConstant -> constantValue.toDocumentationNode() + else -> null + } + + fun ConstantValue<*>.toDocumentationNode(): DocumentationNode? = value?.let { value -> + when (value) { + is String -> + "\"" + StringUtil.escapeStringCharacters(value) + "\"" + is EnumEntrySyntheticClassDescriptor -> + value.containingDeclaration.name.asString() + "." + value.name.asString() + else -> value.toString() + }.let { valueString -> + DocumentationNode(valueString, Content.Empty, DocumentationNode.Kind.Value) + } + } +} + +class KotlinPackageDocumentationBuilder : PackageDocumentationBuilder { + override fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>) { + val externalClassNodes = hashMapOf<FqName, DocumentationNode>() + declarations.forEach { descriptor -> + with(documentationBuilder) { + if (descriptor.isDocumented()) { + val parent = packageNode.getParentForPackageMember(descriptor, externalClassNodes) + parent.appendChild(descriptor, DocumentationReference.Kind.Member) + } + } + } + } +} + +class KotlinJavaDocumentationBuilder + @Inject constructor(val documentationBuilder: DocumentationBuilder, + val logger: DokkaLogger) : JavaDocumentationBuilder +{ + override fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) { + val packageNode = module.findOrCreatePackageNode(file.packageName, packageContent) + + file.classes.forEach { + val javaDescriptorResolver = KotlinCacheService.getInstance(file.project).getProjectService(JvmPlatform, + it.getModuleInfo(), JavaDescriptorResolver::class.java) + + val descriptor = javaDescriptorResolver.resolveClass(JavaClassImpl(it)) + if (descriptor == null) { + logger.warn("Cannot find descriptor for Java class ${it.qualifiedName}") + } + else { + with(documentationBuilder) { + packageNode.appendChild(descriptor, DocumentationReference.Kind.Member) + } + } + } + } +} + +private fun AnnotationDescriptor.isDocumented(): Boolean { + if (source.getPsi() != null && mustBeDocumented()) return true + val annotationClassName = type.constructor.declarationDescriptor?.fqNameSafe?.asString() + return annotationClassName == "kotlin.Extension" +} + +fun AnnotationDescriptor.mustBeDocumented(): Boolean { + val annotationClass = type.constructor.declarationDescriptor as? Annotated ?: return false + return annotationClass.isDocumentedAnnotation() +} + +fun DeclarationDescriptor.isDocumentationSuppressed(): Boolean { + val doc = KDocFinder.findKDoc(this) + return doc is KDocSection && doc.findTagByName("suppress") != null +} + +fun DeclarationDescriptor.isDeprecated(): Boolean = annotations.any { + DescriptorUtils.getFqName(it.type.constructor.declarationDescriptor!!).asString() == "kotlin.Deprecated" +} || (this is ConstructorDescriptor && containingDeclaration.isDeprecated()) + +fun DocumentationNode.getParentForPackageMember(descriptor: DeclarationDescriptor, + externalClassNodes: MutableMap<FqName, DocumentationNode>): DocumentationNode { + if (descriptor is CallableMemberDescriptor) { + val extensionClassDescriptor = descriptor.getExtensionClassDescriptor() + if (extensionClassDescriptor != null && !isSamePackage(descriptor, extensionClassDescriptor) && + !ErrorUtils.isError(extensionClassDescriptor)) { + val fqName = DescriptorUtils.getFqNameSafe(extensionClassDescriptor) + return externalClassNodes.getOrPut(fqName, { + val newNode = DocumentationNode(fqName.asString(), Content.Empty, Kind.ExternalClass) + append(newNode, DocumentationReference.Kind.Member) + newNode + }) + } + } + return this +} + +fun CallableMemberDescriptor.getExtensionClassDescriptor(): ClassifierDescriptor? { + val extensionReceiver = extensionReceiverParameter + if (extensionReceiver != null) { + val type = extensionReceiver.type + return type.constructor.declarationDescriptor as? ClassDescriptor + } + return null +} + +fun DeclarationDescriptor.signature(): String = when(this) { + is ClassDescriptor, is PackageFragmentDescriptor -> DescriptorUtils.getFqName(this).asString() + is PropertyDescriptor -> containingDeclaration.signature() + "#" + name + receiverSignature() + is FunctionDescriptor -> containingDeclaration.signature() + "#" + name + parameterSignature() + is ValueParameterDescriptor -> containingDeclaration.signature() + ":" + name + is TypeParameterDescriptor -> containingDeclaration.signature() + "<" + name + + else -> throw UnsupportedOperationException("Don't know how to calculate signature for $this") +} + +fun PropertyDescriptor.receiverSignature(): String { + val receiver = extensionReceiverParameter + if (receiver != null) { + return "#" + receiver.type.signature() + } + return "" +} + +fun CallableMemberDescriptor.parameterSignature(): String { + val params = valueParameters.map { it.type }.toArrayList() + val extensionReceiver = extensionReceiverParameter + if (extensionReceiver != null) { + params.add(0, extensionReceiver.type) + } + return "(" + params.map { it.signature() }.joinToString() + ")" +} + +fun KotlinType.signature(): String { + val declarationDescriptor = constructor.declarationDescriptor ?: return "<null>" + val typeName = DescriptorUtils.getFqName(declarationDescriptor).asString() + if (typeName == "Array" && arguments.size == 1) { + return "Array<" + arguments.first().type.signature() + ">" + } + return typeName +} + +fun DeclarationDescriptor.signatureWithSourceLocation(): String { + val signature = signature() + val sourceLocation = sourceLocation() + return if (sourceLocation != null) "$signature ($sourceLocation)" else signature +} + +fun DeclarationDescriptor.sourceLocation(): String? { + if (this is DeclarationDescriptorWithSource) { + val psi = (this.source as? PsiSourceElement)?.getPsi() + if (psi != null) { + val fileName = psi.containingFile.name + val lineNumber = psi.lineNumber() + return if (lineNumber != null) "$fileName:$lineNumber" else fileName + } + } + return null +} diff --git a/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt b/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt new file mode 100644 index 00000000..7a1d591c --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt @@ -0,0 +1,64 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.Kotlin.DescriptorDocumentationParser +import org.jetbrains.kotlin.asJava.KtLightElement +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtPropertyAccessor + +class KotlinAsJavaDocumentationBuilder + @Inject constructor(val kotlinAsJavaDocumentationParser: KotlinAsJavaDocumentationParser) : PackageDocumentationBuilder +{ + override fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>) { + val project = documentationBuilder.resolutionFacade.project + val psiPackage = JavaPsiFacade.getInstance(project).findPackage(packageName.asString()) + if (psiPackage == null) { + documentationBuilder.logger.error("Cannot find Java package by qualified name: ${packageName.asString()}") + return + } + + val javaDocumentationBuilder = JavaPsiDocumentationBuilder(documentationBuilder.options, + documentationBuilder.refGraph, + kotlinAsJavaDocumentationParser) + + psiPackage.classes.filter { it is KtLightElement<*, *> }.filter { it.isVisibleInDocumentation() }.forEach { + javaDocumentationBuilder.appendClasses(packageNode, arrayOf(it)) + } + } + + fun PsiClass.isVisibleInDocumentation() : Boolean { + val origin: KtDeclaration? = (this as KtLightElement<*, *>).getOrigin() + return origin?.hasModifier(KtTokens.INTERNAL_KEYWORD) != true && + origin?.hasModifier(KtTokens.PRIVATE_KEYWORD) != true + } +} + +class KotlinAsJavaDocumentationParser + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val descriptorDocumentationParser: DescriptorDocumentationParser) : JavaDocumentationParser +{ + override fun parseDocumentation(element: PsiNamedElement): JavadocParseResult { + val kotlinLightElement = element as? KtLightElement<*, *> ?: return JavadocParseResult.Empty + val origin = kotlinLightElement.getOrigin() ?: return JavadocParseResult.Empty + if (origin is KtParameter) { + // LazyDeclarationResolver does not support setter parameters + val grandFather = origin.parent?.parent + if (grandFather is KtPropertyAccessor) { + return JavadocParseResult.Empty + } + } + val descriptor = resolutionFacade.resolveToDescriptor(origin) + val content = descriptorDocumentationParser.parseDocumentation(descriptor, origin is KtParameter) + return JavadocParseResult(content, null) + } +} diff --git a/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt b/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt new file mode 100644 index 00000000..0d39f410 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt @@ -0,0 +1,409 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.LanguageService.RenderMode + +/** + * Implements [LanguageService] and provides rendering of symbols in Kotlin language + */ +class KotlinLanguageService : LanguageService { + private val fullOnlyModifiers = setOf("public", "protected", "private", "inline", "noinline", "crossinline", "reified") + + override fun render(node: DocumentationNode, renderMode: RenderMode): ContentNode { + return content { + when (node.kind) { + DocumentationNode.Kind.Package -> if (renderMode == RenderMode.FULL) renderPackage(node) + in DocumentationNode.Kind.classLike -> renderClass(node, renderMode) + + DocumentationNode.Kind.EnumItem, + DocumentationNode.Kind.ExternalClass -> if (renderMode == RenderMode.FULL) identifier(node.name) + + DocumentationNode.Kind.TypeParameter -> renderTypeParameter(node, renderMode) + DocumentationNode.Kind.Type, + DocumentationNode.Kind.UpperBound -> renderType(node, renderMode) + + DocumentationNode.Kind.Modifier -> renderModifier(node) + DocumentationNode.Kind.Constructor, + DocumentationNode.Kind.Function, + DocumentationNode.Kind.CompanionObjectFunction -> renderFunction(node, renderMode) + DocumentationNode.Kind.Property, + DocumentationNode.Kind.CompanionObjectProperty -> renderProperty(node, renderMode) + else -> identifier(node.name) + } + } + } + + override fun renderName(node: DocumentationNode): String { + return when (node.kind) { + DocumentationNode.Kind.Constructor -> node.owner!!.name + else -> node.name + } + } + + override fun summarizeSignatures(nodes: List<DocumentationNode>): ContentNode? { + if (nodes.size < 2) return null + val receiverKind = nodes.getReceiverKind() ?: return null + val functionWithTypeParameter = nodes.firstOrNull { it.details(DocumentationNode.Kind.TypeParameter).any() } ?: return null + return content { + val typeParameter = functionWithTypeParameter.details(DocumentationNode.Kind.TypeParameter).first() + if (functionWithTypeParameter.kind == DocumentationNode.Kind.Function) { + renderFunction(functionWithTypeParameter, RenderMode.SUMMARY, SummarizingMapper(receiverKind, typeParameter.name)) + } + else { + renderProperty(functionWithTypeParameter, RenderMode.SUMMARY, SummarizingMapper(receiverKind, typeParameter.name)) + } + } + } + + private fun List<DocumentationNode>.getReceiverKind(): ReceiverKind? { + val qNames = map { it.getReceiverQName() }.filterNotNull() + if (qNames.size != size) + return null + + return ReceiverKind.values.firstOrNull { kind -> qNames.all { it in kind.classes } } + } + + private fun DocumentationNode.getReceiverQName(): String? { + if (kind != DocumentationNode.Kind.Function && kind != DocumentationNode.Kind.Property) return null + val receiver = details(DocumentationNode.Kind.Receiver).singleOrNull() ?: return null + return receiver.detail(DocumentationNode.Kind.Type).qualifiedNameFromType() + } + + companion object { + private val arrayClasses = setOf( + "kotlin.Array", + "kotlin.BooleanArray", + "kotlin.ByteArray", + "kotlin.CharArray", + "kotlin.ShortArray", + "kotlin.IntArray", + "kotlin.LongArray", + "kotlin.FloatArray", + "kotlin.DoubleArray" + ) + + private val arrayOrListClasses = setOf("kotlin.List") + arrayClasses + + private val iterableClasses = setOf( + "kotlin.Collection", + "kotlin.Sequence", + "kotlin.Iterable", + "kotlin.Map", + "kotlin.String", + "kotlin.CharSequence") + arrayOrListClasses + } + + private enum class ReceiverKind(val receiverName: String, val classes: Collection<String>) { + ARRAY("any_array", arrayClasses), + ARRAY_OR_LIST("any_array_or_list", arrayOrListClasses), + ITERABLE("any_iterable", iterableClasses), + } + + interface SignatureMapper { + fun renderReceiver(receiver: DocumentationNode, to: ContentBlock) + } + + private class SummarizingMapper(val kind: ReceiverKind, val typeParameterName: String): SignatureMapper { + override fun renderReceiver(receiver: DocumentationNode, to: ContentBlock) { + to.append(ContentIdentifier(kind.receiverName, IdentifierKind.SummarizedTypeName)) + to.text("<$typeParameterName>") + } + } + + private fun ContentBlock.renderPackage(node: DocumentationNode) { + keyword("package") + text(" ") + identifier(node.name) + } + + private fun ContentBlock.renderList(nodes: List<DocumentationNode>, separator: String = ", ", + noWrap: Boolean = false, renderItem: (DocumentationNode) -> Unit) { + if (nodes.none()) + return + renderItem(nodes.first()) + nodes.drop(1).forEach { + if (noWrap) { + symbol(separator.removeSuffix(" ")) + nbsp() + } else { + symbol(separator) + } + renderItem(it) + } + } + + private fun ContentBlock.renderLinked(node: DocumentationNode, body: ContentBlock.(DocumentationNode)->Unit) { + val to = node.links.firstOrNull() + if (to == null) + body(node) + else + link(to) { + body(node) + } + } + + private fun ContentBlock.renderType(node: DocumentationNode, renderMode: RenderMode) { + var typeArguments = node.details(DocumentationNode.Kind.Type) + if (node.name == "Function${typeArguments.count() - 1}") { + // lambda + val isExtension = node.annotations.any { it.name == "Extension" } + if (isExtension) { + renderType(typeArguments.first(), renderMode) + symbol(".") + typeArguments = typeArguments.drop(1) + } + symbol("(") + renderList(typeArguments.take(typeArguments.size - 1), noWrap = true) { + renderType(it, renderMode) + } + symbol(")") + nbsp() + symbol("->") + nbsp() + renderType(typeArguments.last(), renderMode) + return + } + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode, true) + renderLinked(node) { identifier(it.name, IdentifierKind.TypeName) } + if (typeArguments.any()) { + symbol("<") + renderList(typeArguments, noWrap = true) { + renderType(it, renderMode) + } + symbol(">") + } + val nullabilityModifier = node.details(DocumentationNode.Kind.NullabilityModifier).singleOrNull() + if (nullabilityModifier != null) { + symbol(nullabilityModifier.name) + } + } + + private fun ContentBlock.renderModifier(node: DocumentationNode, nowrap: Boolean = false) { + when (node.name) { + "final", "public", "var" -> {} + else -> { + keyword(node.name) + if (nowrap) { + nbsp() + } + else { + text(" ") + } + } + } + } + + private fun ContentBlock.renderTypeParameter(node: DocumentationNode, renderMode: RenderMode) { + renderModifiersForNode(node, renderMode, true) + + identifier(node.name) + + val constraints = node.details(DocumentationNode.Kind.UpperBound) + if (constraints.any()) { + nbsp() + symbol(":") + nbsp() + renderList(constraints, noWrap=true) { + renderType(it, renderMode) + } + } + } + private fun ContentBlock.renderParameter(node: DocumentationNode, renderMode: RenderMode) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + identifier(node.name, IdentifierKind.ParameterName) + symbol(":") + nbsp() + val parameterType = node.detail(DocumentationNode.Kind.Type) + renderType(parameterType, renderMode) + val valueNode = node.details(DocumentationNode.Kind.Value).firstOrNull() + if (valueNode != null) { + nbsp() + symbol("=") + nbsp() + text(valueNode.name) + } + } + + private fun ContentBlock.renderTypeParametersForNode(node: DocumentationNode, renderMode: RenderMode) { + val typeParameters = node.details(DocumentationNode.Kind.TypeParameter) + if (typeParameters.any()) { + symbol("<") + renderList(typeParameters) { + renderTypeParameter(it, renderMode) + } + symbol(">") + } + } + + private fun ContentBlock.renderSupertypesForNode(node: DocumentationNode, renderMode: RenderMode) { + val supertypes = node.details(DocumentationNode.Kind.Supertype) + if (supertypes.any()) { + nbsp() + symbol(":") + nbsp() + renderList(supertypes) { + indentedSoftLineBreak() + renderType(it, renderMode) + } + } + } + + private fun ContentBlock.renderModifiersForNode(node: DocumentationNode, + renderMode: RenderMode, + nowrap: Boolean = false) { + val modifiers = node.details(DocumentationNode.Kind.Modifier) + for (it in modifiers) { + if (node.kind == org.jetbrains.dokka.DocumentationNode.Kind.Interface && it.name == "abstract") + continue + if (renderMode == RenderMode.SUMMARY && it.name in fullOnlyModifiers) { + continue + } + renderModifier(it, nowrap) + } + } + + private fun ContentBlock.renderAnnotationsForNode(node: DocumentationNode) { + node.annotations.forEach { + renderAnnotation(it) + } + } + + private fun ContentBlock.renderAnnotation(node: DocumentationNode) { + identifier("@" + node.name, IdentifierKind.AnnotationName) + val parameters = node.details(DocumentationNode.Kind.Parameter) + if (!parameters.isEmpty()) { + symbol("(") + renderList(parameters) { + text(it.detail(DocumentationNode.Kind.Value).name) + } + symbol(")") + } + text(" ") + } + + private fun ContentBlock.renderClass(node: DocumentationNode, renderMode: RenderMode) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + DocumentationNode.Kind.Class, + DocumentationNode.Kind.AnnotationClass, + DocumentationNode.Kind.Enum -> keyword("class ") + DocumentationNode.Kind.Interface -> keyword("interface ") + DocumentationNode.Kind.EnumItem -> keyword("enum val ") + DocumentationNode.Kind.Object -> keyword("object ") + else -> throw IllegalArgumentException("Node $node is not a class-like object") + } + + identifierOrDeprecated(node) + renderTypeParametersForNode(node, renderMode) + renderSupertypesForNode(node, renderMode) + } + + private fun ContentBlock.renderFunction(node: DocumentationNode, + renderMode: RenderMode, + signatureMapper: SignatureMapper? = null) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + DocumentationNode.Kind.Constructor -> identifier(node.owner!!.name) + DocumentationNode.Kind.Function, + DocumentationNode.Kind.CompanionObjectFunction -> keyword("fun ") + else -> throw IllegalArgumentException("Node $node is not a function-like object") + } + renderTypeParametersForNode(node, renderMode) + if (node.details(DocumentationNode.Kind.TypeParameter).any()) { + text(" ") + } + + renderReceiver(node, renderMode, signatureMapper) + + if (node.kind != org.jetbrains.dokka.DocumentationNode.Kind.Constructor) + identifierOrDeprecated(node) + + symbol("(") + val parameters = node.details(DocumentationNode.Kind.Parameter) + renderList(parameters) { + indentedSoftLineBreak() + renderParameter(it, renderMode) + } + if (needReturnType(node)) { + if (parameters.isNotEmpty()) { + softLineBreak() + } + symbol(")") + symbol(": ") + renderType(node.detail(DocumentationNode.Kind.Type), renderMode) + } + else { + symbol(")") + } + } + + private fun ContentBlock.renderReceiver(node: DocumentationNode, renderMode: RenderMode, signatureMapper: SignatureMapper?) { + val receiver = node.details(DocumentationNode.Kind.Receiver).singleOrNull() + if (receiver != null) { + if (signatureMapper != null) { + signatureMapper.renderReceiver(receiver, this) + } else { + renderType(receiver.detail(DocumentationNode.Kind.Type), renderMode) + } + symbol(".") + } + } + + private fun needReturnType(node: DocumentationNode) = when(node.kind) { + DocumentationNode.Kind.Constructor -> false + else -> !node.isUnitReturnType() + } + + fun DocumentationNode.isUnitReturnType(): Boolean = + detail(DocumentationNode.Kind.Type).hiddenLinks.firstOrNull()?.qualifiedName() == "kotlin.Unit" + + private fun ContentBlock.renderProperty(node: DocumentationNode, + renderMode: RenderMode, + signatureMapper: SignatureMapper? = null) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + DocumentationNode.Kind.Property, + DocumentationNode.Kind.CompanionObjectProperty -> keyword("${node.getPropertyKeyword()} ") + else -> throw IllegalArgumentException("Node $node is not a property") + } + renderTypeParametersForNode(node, renderMode) + if (node.details(DocumentationNode.Kind.TypeParameter).any()) { + text(" ") + } + + renderReceiver(node, renderMode, signatureMapper) + + identifierOrDeprecated(node) + symbol(": ") + renderType(node.detail(DocumentationNode.Kind.Type), renderMode) + } + + fun DocumentationNode.getPropertyKeyword() = + if (details(DocumentationNode.Kind.Modifier).any { it.name == "var" }) "var" else "val" + + fun ContentBlock.identifierOrDeprecated(node: DocumentationNode) { + if (node.deprecation != null) { + val strike = ContentStrikethrough() + strike.identifier(node.name) + append(strike) + } else { + identifier(node.name) + } + } +} + +fun DocumentationNode.qualifiedNameFromType() = (links.firstOrNull() ?: hiddenLinks.firstOrNull())?.qualifiedName() ?: name diff --git a/core/src/main/kotlin/Languages/JavaLanguageService.kt b/core/src/main/kotlin/Languages/JavaLanguageService.kt new file mode 100644 index 00000000..7e40beff --- /dev/null +++ b/core/src/main/kotlin/Languages/JavaLanguageService.kt @@ -0,0 +1,162 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.DocumentationNode.Kind +import org.jetbrains.dokka.LanguageService.RenderMode + +/** + * Implements [LanguageService] and provides rendering of symbols in Java language + */ +public class JavaLanguageService : LanguageService { + override fun render(node: DocumentationNode, renderMode: RenderMode): ContentNode { + return ContentText(when (node.kind) { + Kind.Package -> renderPackage(node) + in Kind.classLike -> renderClass(node) + + Kind.TypeParameter -> renderTypeParameter(node) + Kind.Type, + Kind.UpperBound -> renderType(node) + + Kind.Constructor, + Kind.Function -> renderFunction(node) + Kind.Property -> renderProperty(node) + else -> "${node.kind}: ${node.name}" + }) + } + + override fun renderName(node: DocumentationNode): String { + return when (node.kind) { + Kind.Constructor -> node.owner!!.name + else -> node.name + } + } + + override fun summarizeSignatures(nodes: List<DocumentationNode>): ContentNode? = null + + private fun renderPackage(node: DocumentationNode): String { + return "package ${node.name}" + } + + private fun renderModifier(node: DocumentationNode): String { + return when (node.name) { + "open" -> "" + "internal" -> "" + else -> node.name + } + } + + public fun getArrayElementType(node: DocumentationNode): DocumentationNode? = when (node.name) { + "Array" -> node.details(Kind.Type).singleOrNull()?.let { et -> getArrayElementType(et) ?: et } ?: DocumentationNode("Object", node.content, DocumentationNode.Kind.ExternalClass) + "IntArray", "LongArray", "ShortArray", "ByteArray", "CharArray", "DoubleArray", "FloatArray", "BooleanArray" -> DocumentationNode(node.name.removeSuffix("Array").toLowerCase(), node.content, DocumentationNode.Kind.Type) + else -> null + } + + public fun getArrayDimension(node: DocumentationNode): Int = when (node.name) { + "Array" -> 1 + (node.details(DocumentationNode.Kind.Type).singleOrNull()?.let { getArrayDimension(it) } ?: 0) + "IntArray", "LongArray", "ShortArray", "ByteArray", "CharArray", "DoubleArray", "FloatArray", "BooleanArray" -> 1 + else -> 0 + } + + public fun renderType(node: DocumentationNode): String { + return when (node.name) { + "Unit" -> "void" + "Int" -> "int" + "Long" -> "long" + "Double" -> "double" + "Float" -> "float" + "Char" -> "char" + "Boolean" -> "bool" + // TODO: render arrays + else -> node.name + } + } + + private fun renderTypeParameter(node: DocumentationNode): String { + val constraints = node.details(Kind.UpperBound) + return if (constraints.none()) + node.name + else { + node.name + " extends " + constraints.map { renderType(node) }.joinToString() + } + } + + private fun renderParameter(node: DocumentationNode): String { + return "${renderType(node.detail(Kind.Type))} ${node.name}" + } + + private fun renderTypeParametersForNode(node: DocumentationNode): String { + return StringBuilder().apply { + val typeParameters = node.details(Kind.TypeParameter) + if (typeParameters.any()) { + append("<") + append(typeParameters.map { renderTypeParameter(it) }.joinToString()) + append("> ") + } + }.toString() + } + + private fun renderModifiersForNode(node: DocumentationNode): String { + val modifiers = node.details(Kind.Modifier).map { renderModifier(it) }.filter { it != "" } + if (modifiers.none()) + return "" + return modifiers.joinToString(" ", postfix = " ") + } + + private fun renderClass(node: DocumentationNode): String { + return StringBuilder().apply { + when (node.kind) { + Kind.Class -> append("class ") + Kind.Interface -> append("interface ") + Kind.Enum -> append("enum ") + Kind.EnumItem -> append("enum value ") + Kind.Object -> append("class ") + else -> throw IllegalArgumentException("Node $node is not a class-like object") + } + + append(node.name) + append(renderTypeParametersForNode(node)) + }.toString() + } + + private fun renderFunction(node: DocumentationNode): String { + return StringBuilder().apply { + when (node.kind) { + Kind.Constructor -> append(node.owner?.name) + Kind.Function -> { + append(renderTypeParametersForNode(node)) + append(renderType(node.detail(Kind.Type))) + append(" ") + append(node.name) + } + else -> throw IllegalArgumentException("Node $node is not a function-like object") + } + + val receiver = node.details(Kind.Receiver).singleOrNull() + append("(") + if (receiver != null) + (listOf(receiver) + node.details(Kind.Parameter)).map { renderParameter(it) }.joinTo(this) + else + node.details(Kind.Parameter).map { renderParameter(it) }.joinTo(this) + + append(")") + }.toString() + } + + private fun renderProperty(node: DocumentationNode): String { + return StringBuilder().apply { + when (node.kind) { + Kind.Property -> append("val ") + else -> throw IllegalArgumentException("Node $node is not a property") + } + append(renderTypeParametersForNode(node)) + val receiver = node.details(Kind.Receiver).singleOrNull() + if (receiver != null) { + append(renderType(receiver.detail(Kind.Type))) + append(".") + } + + append(node.name) + append(": ") + append(renderType(node.detail(Kind.Type))) + }.toString() + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Languages/LanguageService.kt b/core/src/main/kotlin/Languages/LanguageService.kt new file mode 100644 index 00000000..b0f4bbc9 --- /dev/null +++ b/core/src/main/kotlin/Languages/LanguageService.kt @@ -0,0 +1,41 @@ +package org.jetbrains.dokka + +/** + * Provides facility for rendering [DocumentationNode] as a language-dependent declaration + */ +interface LanguageService { + enum class RenderMode { + /** Brief signature (used in a list of all members of the class). */ + SUMMARY, + /** Full signature (used in the page describing the member itself */ + FULL + } + + /** + * Renders a [node] as a class, function, property or other signature in a target language. + * @param node A [DocumentationNode] to render + * @return [ContentNode] which is a root for a rich content tree suitable for formatting with [FormatService] + */ + fun render(node: DocumentationNode, renderMode: RenderMode = RenderMode.FULL): ContentNode + + /** + * Tries to summarize the signatures of the specified documentation nodes in a compact representation. + * Returns the representation if successful, or null if the signatures could not be summarized. + */ + fun summarizeSignatures(nodes: List<DocumentationNode>): ContentNode? + + /** + * Renders [node] as a named representation in the target language + * + * For example: + * ${code org.jetbrains.dokka.example} + * + * $node: A [DocumentationNode] to render + * $returns: [String] which is a string representation of the node's name + */ + fun renderName(node: DocumentationNode): String +} + +fun example(service: LanguageService, node: DocumentationNode) { + println("Node name: ${service.renderName(node)}") +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Locations/FoldersLocationService.kt b/core/src/main/kotlin/Locations/FoldersLocationService.kt new file mode 100644 index 00000000..89b34ed1 --- /dev/null +++ b/core/src/main/kotlin/Locations/FoldersLocationService.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import java.io.File + +public fun FoldersLocationService(root: String): FoldersLocationService = FoldersLocationService(File(root), "") +public class FoldersLocationService @Inject constructor(@Named("outputDir") val rootFile: File, val extension: String) : FileLocationService { + override val root: Location + get() = FileLocation(rootFile) + + override fun withExtension(newExtension: String): FileLocationService { + return if (extension.isEmpty()) FoldersLocationService(rootFile, newExtension) else this + } + + override fun location(qualifiedName: List<String>, hasMembers: Boolean): FileLocation { + return FileLocation(File(rootFile, relativePathToNode(qualifiedName, hasMembers)).appendExtension(extension)) + } +} + +fun relativePathToNode(qualifiedName: List<String>, hasMembers: Boolean): String { + val parts = qualifiedName.map { identifierToFilename(it) }.filterNot { it.isEmpty() } + return if (!hasMembers) { + // leaf node, use file in owner's folder + parts.joinToString("/") + } else { + parts.joinToString("/") + (if (parts.none()) "" else "/") + "index" + } +} diff --git a/core/src/main/kotlin/Locations/LocationService.kt b/core/src/main/kotlin/Locations/LocationService.kt new file mode 100644 index 00000000..80bc0236 --- /dev/null +++ b/core/src/main/kotlin/Locations/LocationService.kt @@ -0,0 +1,78 @@ +package org.jetbrains.dokka + +import java.io.File + +public interface Location { + val path: String get + fun relativePathTo(other: Location, anchor: String? = null): String +} + +/** + * Represents locations in the documentation in the form of [path](File). + * + * Locations are provided by [LocationService.location] function. + * + * $file: [File] for this location + * $path: [String] representing path of this location + */ +public data class FileLocation(val file: File): Location { + override val path : String + get() = file.path + + override fun relativePathTo(other: Location, anchor: String?): String { + if (other !is FileLocation) { + throw IllegalArgumentException("$other is not a FileLocation") + } + if (file.path.substringBeforeLast(".") == other.file.path.substringBeforeLast(".") && anchor == null) { + return "." + } + val ownerFolder = file.parentFile!! + val relativePath = ownerFolder.toPath().relativize(other.file.toPath()).toString() + return if (anchor == null) relativePath else relativePath + "#" + anchor + } +} + +/** + * Provides means of retrieving locations for [DocumentationNode](documentation nodes) + * + * `LocationService` determines where documentation for particular node should be generated + * + * * [FoldersLocationService] – represent packages and types as folders, members as files in those folders. + * * [SingleFolderLocationService] – all documentation is generated into single folder using fully qualified names + * for file names. + */ +public interface LocationService { + fun withExtension(newExtension: String) = this + + fun location(node: DocumentationNode): Location = location(node.path.map { it.name }, node.members.any()) + + /** + * Calculates a location corresponding to the specified [qualifiedName]. + * @param hasMembers if true, the node for which the location is calculated has member nodes. + */ + fun location(qualifiedName: List<String>, hasMembers: Boolean): Location + + val root: Location +} + + +public interface FileLocationService: LocationService { + override fun withExtension(newExtension: String): FileLocationService = this + + override fun location(node: DocumentationNode): FileLocation = location(node.path.map { it.name }, node.members.any()) + override fun location(qualifiedName: List<String>, hasMembers: Boolean): FileLocation +} + + +public fun identifierToFilename(path: String): String { + val escaped = path.replace('<', '-').replace('>', '-') + val lowercase = escaped.replace("[A-Z]".toRegex()) { matchResult -> "-" + matchResult.value.toLowerCase() } + return if (lowercase == "index") "--index--" else lowercase +} + +/** + * Returns relative location between two nodes. Used for relative links in documentation. + */ +fun LocationService.relativePathToLocation(owner: DocumentationNode, node: DocumentationNode): String { + return location(owner).relativePathTo(location(node), null) +} diff --git a/core/src/main/kotlin/Locations/SingleFolderLocationService.kt b/core/src/main/kotlin/Locations/SingleFolderLocationService.kt new file mode 100644 index 00000000..e313ac28 --- /dev/null +++ b/core/src/main/kotlin/Locations/SingleFolderLocationService.kt @@ -0,0 +1,19 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import java.io.File + +public fun SingleFolderLocationService(root: String): SingleFolderLocationService = SingleFolderLocationService(File(root), "") +public class SingleFolderLocationService @Inject constructor(@Named("outputDir") val rootFile: File, val extension: String) : FileLocationService { + override fun withExtension(newExtension: String): FileLocationService = + SingleFolderLocationService(rootFile, newExtension) + + override fun location(qualifiedName: List<String>, hasMembers: Boolean): FileLocation { + val filename = qualifiedName.map { identifierToFilename(it) }.joinToString("-") + return FileLocation(File(rootFile, filename).appendExtension(extension)) + } + + override val root: Location + get() = FileLocation(rootFile) +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Markdown/MarkdownProcessor.kt b/core/src/main/kotlin/Markdown/MarkdownProcessor.kt new file mode 100644 index 00000000..99caddc4 --- /dev/null +++ b/core/src/main/kotlin/Markdown/MarkdownProcessor.kt @@ -0,0 +1,50 @@ +package org.jetbrains.dokka + +import org.intellij.markdown.IElementType +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.LeafASTNode +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor +import org.intellij.markdown.parser.MarkdownParser + +class MarkdownNode(val node: ASTNode, val parent: MarkdownNode?, val markdown: String) { + val children: List<MarkdownNode> = node.children.map { MarkdownNode(it, this, markdown) } + val type: IElementType get() = node.type + val text: String get() = markdown.substring(node.startOffset, node.endOffset) + fun child(type: IElementType): MarkdownNode? = children.firstOrNull { it.type == type } + + override fun toString(): String = StringBuilder().apply { presentTo(this) }.toString() +} + +fun MarkdownNode.visit(action: (MarkdownNode, () -> Unit) -> Unit) { + action(this) { + for (child in children) { + child.visit(action) + } + } +} + +public fun MarkdownNode.toTestString(): String { + val sb = StringBuilder() + var level = 0 + visit { node, visitChildren -> + sb.append(" ".repeat(level * 2)) + node.presentTo(sb) + level++ + visitChildren() + level-- + } + return sb.toString() +} + +private fun MarkdownNode.presentTo(sb: StringBuilder) { + sb.append(type.toString()) + sb.append(":" + text.replace("\n", "\u23CE")) + sb.appendln() +} + +fun parseMarkdown(markdown: String): MarkdownNode { + if (markdown.isEmpty()) + return MarkdownNode(LeafASTNode(MarkdownElementTypes.MARKDOWN_FILE, 0, 0), null, markdown) + return MarkdownNode(MarkdownParser(CommonMarkFlavourDescriptor()).buildMarkdownTreeFromString(markdown), null, markdown) +} 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 diff --git a/core/src/main/kotlin/Utilities/DokkaModule.kt b/core/src/main/kotlin/Utilities/DokkaModule.kt new file mode 100644 index 00000000..1eb82313 --- /dev/null +++ b/core/src/main/kotlin/Utilities/DokkaModule.kt @@ -0,0 +1,73 @@ +package org.jetbrains.dokka.Utilities + +import com.google.inject.Binder +import com.google.inject.Module +import com.google.inject.Provider +import com.google.inject.name.Names +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.FormatDescriptor +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import java.io.File + +class DokkaModule(val environment: AnalysisEnvironment, + val options: DocumentationOptions, + val logger: DokkaLogger) : Module { + override fun configure(binder: Binder) { + binder.bind(File::class.java).annotatedWith(Names.named("outputDir")).toInstance(File(options.outputDir)) + + binder.bindNameAnnotated<LocationService, SingleFolderLocationService>("singleFolder") + binder.bindNameAnnotated<FileLocationService, SingleFolderLocationService>("singleFolder") + binder.bindNameAnnotated<LocationService, FoldersLocationService>("folders") + binder.bindNameAnnotated<FileLocationService, FoldersLocationService>("folders") + + // defaults + binder.bind(LocationService::class.java).to(FoldersLocationService::class.java) + binder.bind(FileLocationService::class.java).to(FoldersLocationService::class.java) + binder.bind(LanguageService::class.java).to(KotlinLanguageService::class.java) + + binder.bind(HtmlTemplateService::class.java).toProvider(object : Provider<HtmlTemplateService> { + override fun get(): HtmlTemplateService = HtmlTemplateService.default("style.css") + }) + + binder.registerCategory<LanguageService>("language") + binder.registerCategory<OutlineFormatService>("outline") + binder.registerCategory<FormatService>("format") + binder.registerCategory<Generator>("generator") + + val descriptor = ServiceLocator.lookup<FormatDescriptor>("format", options.outputFormat) + + descriptor.outlineServiceClass?.let { clazz -> + binder.bind(OutlineFormatService::class.java).to(clazz.java) + } + descriptor.formatServiceClass?.let { clazz -> + binder.bind(FormatService::class.java).to(clazz.java) + } + binder.bind<PackageDocumentationBuilder>().to(descriptor.packageDocumentationBuilderClass.java) + binder.bind<JavaDocumentationBuilder>().to(descriptor.javaDocumentationBuilderClass.java) + + binder.bind<Generator>().to(descriptor.generatorServiceClass.java) + + val coreEnvironment = environment.createCoreEnvironment() + binder.bind<KotlinCoreEnvironment>().toInstance(coreEnvironment) + + val dokkaResolutionFacade = environment.createResolutionFacade(coreEnvironment) + binder.bind<DokkaResolutionFacade>().toInstance(dokkaResolutionFacade) + + binder.bind<DocumentationOptions>().toInstance(options) + binder.bind<DokkaLogger>().toInstance(logger) + } +} + +private inline fun <reified T: Any> Binder.registerCategory(category: String) { + ServiceLocator.allServices(category).forEach { + @Suppress("UNCHECKED_CAST") + bind(T::class.java).annotatedWith(Names.named(it.name)).to(T::class.java.classLoader.loadClass(it.className) as Class<T>) + } +} + +private inline fun <reified Base : Any, reified T : Base> Binder.bindNameAnnotated(name: String) { + bind(Base::class.java).annotatedWith(Names.named(name)).to(T::class.java) +} + + +inline fun <reified T: Any> Binder.bind() = bind(T::class.java) diff --git a/core/src/main/kotlin/Utilities/Html.kt b/core/src/main/kotlin/Utilities/Html.kt new file mode 100644 index 00000000..ce3a1982 --- /dev/null +++ b/core/src/main/kotlin/Utilities/Html.kt @@ -0,0 +1,8 @@ +package org.jetbrains.dokka + + +/** + * Replaces symbols reserved in HTML with their respective entities. + * Replaces & with &, < with < and > with > + */ +public fun String.htmlEscape(): String = replace("&", "&").replace("<", "<").replace(">", ">") diff --git a/core/src/main/kotlin/Utilities/Path.kt b/core/src/main/kotlin/Utilities/Path.kt new file mode 100644 index 00000000..05838499 --- /dev/null +++ b/core/src/main/kotlin/Utilities/Path.kt @@ -0,0 +1,5 @@ +package org.jetbrains.dokka + +import java.io.File + +fun File.appendExtension(extension: String) = if (extension.isEmpty()) this else File(path + "." + extension) diff --git a/core/src/main/kotlin/Utilities/ServiceLocator.kt b/core/src/main/kotlin/Utilities/ServiceLocator.kt new file mode 100644 index 00000000..7a5aff79 --- /dev/null +++ b/core/src/main/kotlin/Utilities/ServiceLocator.kt @@ -0,0 +1,78 @@ +package org.jetbrains.dokka.Utilities + +import java.io.File +import java.util.* +import java.util.jar.JarFile +import java.util.zip.ZipEntry + +data class ServiceDescriptor(val name: String, val category: String, val description: String?, val className: String) + +class ServiceLookupException(message: String) : Exception(message) + +public object ServiceLocator { + public fun <T : Any> lookup(clazz: Class<T>, category: String, implementationName: String): T { + val descriptor = lookupDescriptor(category, implementationName) + val loadedClass = javaClass.classLoader.loadClass(descriptor.className) + val constructor = loadedClass.constructors + .filter { it.parameterTypes.isEmpty() } + .firstOrNull() ?: throw ServiceLookupException("Class ${descriptor.className} has no corresponding constructor") + + val implementationRawType: Any = if (constructor.parameterTypes.isEmpty()) constructor.newInstance() else constructor.newInstance(constructor) + + if (!clazz.isInstance(implementationRawType)) { + throw ServiceLookupException("Class ${descriptor.className} is not a subtype of ${clazz.name}") + } + + @Suppress("UNCHECKED_CAST") + return implementationRawType as T + } + + private fun lookupDescriptor(category: String, implementationName: String): ServiceDescriptor { + val properties = javaClass.classLoader.getResourceAsStream("dokka/$category/$implementationName.properties")?.use { stream -> + Properties().let { properties -> + properties.load(stream) + properties + } + } ?: throw ServiceLookupException("No implementation with name $implementationName found in category $category") + + val className = properties["class"]?.toString() ?: throw ServiceLookupException("Implementation $implementationName has no class configured") + + return ServiceDescriptor(implementationName, category, properties["description"]?.toString(), className) + } + + fun allServices(category: String): List<ServiceDescriptor> { + val entries = this.javaClass.classLoader.getResources("dokka/$category")?.toList() ?: emptyList() + + return entries.flatMap { + when (it.protocol) { + "file" -> File(it.file).listFiles()?.filter { it.extension == "properties" }?.map { lookupDescriptor(category, it.nameWithoutExtension) } ?: emptyList() + "jar" -> { + val file = JarFile(it.file.removePrefix("file:").substringBefore("!")) + try { + val jarPath = it.file.substringAfterLast("!").removePrefix("/") + file.entries() + .asSequence() + .filter { entry -> !entry.isDirectory && entry.path == jarPath && entry.extension == "properties" } + .map { entry -> + lookupDescriptor(category, entry.fileName.substringBeforeLast(".")) + }.toList() + } finally { + file.close() + } + } + else -> emptyList<ServiceDescriptor>() + } + } + } +} + +public inline fun <reified T : Any> ServiceLocator.lookup(category: String, implementationName: String): T = lookup(T::class.java, category, implementationName) + +private val ZipEntry.fileName: String + get() = name.substringAfterLast("/", name) + +private val ZipEntry.path: String + get() = name.substringBeforeLast("/", "").removePrefix("/") + +private val ZipEntry.extension: String? + get() = fileName.let { fn -> if ("." in fn) fn.substringAfterLast(".") else null } diff --git a/core/src/main/kotlin/ant/dokka.kt b/core/src/main/kotlin/ant/dokka.kt new file mode 100644 index 00000000..d78980f8 --- /dev/null +++ b/core/src/main/kotlin/ant/dokka.kt @@ -0,0 +1,108 @@ +package org.jetbrains.dokka.ant + +import org.apache.tools.ant.Task +import org.apache.tools.ant.types.Path +import org.apache.tools.ant.types.Reference +import org.apache.tools.ant.BuildException +import org.apache.tools.ant.Project +import org.jetbrains.dokka.DokkaLogger +import org.jetbrains.dokka.DokkaGenerator +import org.jetbrains.dokka.SourceLinkDefinition +import java.io.File + +class AntLogger(val task: Task): DokkaLogger { + override fun info(message: String) = task.log(message, Project.MSG_INFO) + override fun warn(message: String) = task.log(message, Project.MSG_WARN) + override fun error(message: String) = task.log(message, Project.MSG_ERR) +} + +class AntSourceLinkDefinition(var path: String? = null, var url: String? = null, var lineSuffix: String? = null) + +class DokkaAntTask(): Task() { + public var moduleName: String? = null + public var outputDir: String? = null + public var outputFormat: String = "html" + + public var skipDeprecated: Boolean = false + + public val compileClasspath: Path = Path(getProject()) + public val sourcePath: Path = Path(getProject()) + public val samplesPath: Path = Path(getProject()) + public val includesPath: Path = Path(getProject()) + + public val antSourceLinks: MutableList<AntSourceLinkDefinition> = arrayListOf() + + public fun setClasspath(classpath: Path) { + compileClasspath.append(classpath) + } + + public fun setClasspathRef(ref: Reference) { + compileClasspath.createPath().refid = ref + } + + public fun setSrc(src: Path) { + sourcePath.append(src) + } + + public fun setSrcRef(ref: Reference) { + sourcePath.createPath().refid = ref + } + + public fun setSamples(samples: Path) { + samplesPath.append(samples) + } + + public fun setSamplesRef(ref: Reference) { + samplesPath.createPath().refid = ref + } + + public fun setInclude(include: Path) { + includesPath.append(include) + } + + public fun createSourceLink(): AntSourceLinkDefinition { + val def = AntSourceLinkDefinition() + antSourceLinks.add(def) + return def + } + + override fun execute() { + if (sourcePath.list().size == 0) { + throw BuildException("At least one source path needs to be specified") + } + if (moduleName == null) { + throw BuildException("Module name needs to be specified") + } + if (outputDir == null) { + throw BuildException("Output directory needs to be specified") + } + val sourceLinks = antSourceLinks.map { + val path = it.path + if (path == null) { + throw BuildException("Path attribute of a <sourceLink> element is required") + } + val url = it.url + if (url == null) { + throw BuildException("Path attribute of a <sourceLink> element is required") + } + SourceLinkDefinition(File(path).canonicalFile.absolutePath, url, it.lineSuffix) + } + + val url = DokkaAntTask::class.java.getResource("/org/jetbrains/dokka/ant/DokkaAntTask.class") + val jarRoot = url.path.substringBefore("!/").removePrefix("file:") + + val generator = DokkaGenerator( + AntLogger(this), + listOf(jarRoot) + compileClasspath.list().toList(), + sourcePath.list().toList(), + samplesPath.list().toList(), + includesPath.list().toList(), + moduleName!!, + outputDir!!, + outputFormat, + sourceLinks, + skipDeprecated + ) + generator.generate() + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/javadoc/docbase.kt b/core/src/main/kotlin/javadoc/docbase.kt new file mode 100644 index 00000000..a0caca94 --- /dev/null +++ b/core/src/main/kotlin/javadoc/docbase.kt @@ -0,0 +1,501 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.* +import org.jetbrains.dokka.* +import java.lang.reflect.Modifier +import java.util.* +import kotlin.reflect.KClass + +private interface HasModule { + val module: ModuleNodeAdapter +} + +private interface HasDocumentationNode { + val node: DocumentationNode +} + +open class DocumentationNodeBareAdapter(override val node: DocumentationNode) : Doc, HasDocumentationNode { + private var rawCommentText_: String? = null + + override fun name(): String = node.name + override fun position(): SourcePosition? = SourcePositionAdapter(node) + + override fun inlineTags(): Array<out Tag>? = emptyArray() + override fun firstSentenceTags(): Array<out Tag>? = emptyArray() + override fun tags(): Array<out Tag> = emptyArray() + override fun tags(tagname: String?): Array<out Tag>? = tags().filter { it.kind() == tagname || it.kind() == "@$tagname" }.toTypedArray() + override fun seeTags(): Array<out SeeTag>? = tags().filterIsInstance<SeeTag>().toTypedArray() + override fun commentText(): String = "" + + override fun setRawCommentText(rawDocumentation: String?) { + rawCommentText_ = rawDocumentation ?: "" + } + + override fun getRawCommentText(): String = rawCommentText_ ?: "" + + override fun isError(): Boolean = false + override fun isException(): Boolean = node.kind == DocumentationNode.Kind.Exception + override fun isEnumConstant(): Boolean = node.kind == DocumentationNode.Kind.EnumItem + override fun isEnum(): Boolean = node.kind == DocumentationNode.Kind.Enum + override fun isMethod(): Boolean = node.kind == DocumentationNode.Kind.Function + override fun isInterface(): Boolean = node.kind == DocumentationNode.Kind.Interface + override fun isField(): Boolean = node.kind == DocumentationNode.Kind.Field + override fun isClass(): Boolean = node.kind == DocumentationNode.Kind.Class + override fun isAnnotationType(): Boolean = node.kind == DocumentationNode.Kind.AnnotationClass + override fun isConstructor(): Boolean = node.kind == DocumentationNode.Kind.Constructor + override fun isOrdinaryClass(): Boolean = node.kind == DocumentationNode.Kind.Class + override fun isAnnotationTypeElement(): Boolean = node.kind == DocumentationNode.Kind.Annotation + + override fun compareTo(other: Any?): Int = when (other) { + !is DocumentationNodeAdapter -> 1 + else -> node.name.compareTo(other.node.name) + } + + override fun equals(other: Any?): Boolean = node.qualifiedName() == (other as? DocumentationNodeAdapter)?.node?.qualifiedName() + override fun hashCode(): Int = node.name.hashCode() + + override fun isIncluded(): Boolean = node.kind != DocumentationNode.Kind.ExternalClass +} + + +// TODO think of source position instead of null +// TODO tags +open class DocumentationNodeAdapter(override val module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeBareAdapter(node), HasModule { + override fun inlineTags(): Array<out Tag> = buildInlineTags(module, this, node.content).toTypedArray() + override fun firstSentenceTags(): Array<out Tag> = buildInlineTags(module, this, node.summary).toTypedArray() + + override fun tags(): Array<out Tag> { + val result = ArrayList<Tag>(buildInlineTags(module, this, node.content)) + node.content.sections.flatMapTo(result) { + when (it.tag) { + ContentTags.SeeAlso -> buildInlineTags(module, this, it) + else -> emptyList<Tag>() + } + } + + node.deprecation?.let { + val content = it.content.asText() + if (content != null) { + result.add(TagImpl(this, "deprecated", content)) + } + } + + return result.toTypedArray() + } +} + +// should be extension property but can't because of KT-8745 +private fun <T> nodeAnnotations(self: T): List<AnnotationDescAdapter> where T : HasModule, T : HasDocumentationNode + = self.node.annotations.map { AnnotationDescAdapter(self.module, it) } + +private fun DocumentationNode.hasAnnotation(klass: KClass<*>) = klass.qualifiedName in annotations.map { it.qualifiedName() } +private fun DocumentationNode.hasModifier(name: String) = details(DocumentationNode.Kind.Modifier).any { it.name == name } + + +class PackageAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), PackageDoc { + private val allClasses = listOf(node).collectAllTypesRecursively() + + override fun findClass(className: String?): ClassDoc? = + allClasses.get(className)?.let { ClassDocumentationNodeAdapter(module, it) } + + override fun annotationTypes(): Array<out AnnotationTypeDoc> = emptyArray() + override fun annotations(): Array<out AnnotationDesc> = node.members(DocumentationNode.Kind.AnnotationClass).map { AnnotationDescAdapter(module, it) }.toTypedArray() + override fun exceptions(): Array<out ClassDoc> = node.members(DocumentationNode.Kind.Exception).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun ordinaryClasses(): Array<out ClassDoc> = node.members(DocumentationNode.Kind.Class).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun interfaces(): Array<out ClassDoc> = node.members(DocumentationNode.Kind.Interface).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun errors(): Array<out ClassDoc> = emptyArray() + override fun enums(): Array<out ClassDoc> = node.members(DocumentationNode.Kind.Enum).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun allClasses(filter: Boolean): Array<out ClassDoc> = allClasses.values.map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun allClasses(): Array<out ClassDoc> = allClasses(true) + + override fun isIncluded(): Boolean = node.name in module.allPackages +} + +class AnnotationTypeDocAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : ClassDocumentationNodeAdapter(module, node), AnnotationTypeDoc { + override fun elements(): Array<out AnnotationTypeElementDoc>? = emptyArray() // TODO +} + +class AnnotationDescAdapter(val module: ModuleNodeAdapter, val node: DocumentationNode) : AnnotationDesc { + override fun annotationType(): AnnotationTypeDoc? = AnnotationTypeDocAdapter(module, node) // TODO ????? + override fun isSynthesized(): Boolean = false + override fun elementValues(): Array<out AnnotationDesc.ElementValuePair>? = emptyArray() // TODO +} + +class ProgramElementAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), ProgramElementDoc { + override fun isPublic(): Boolean = true + override fun isPackagePrivate(): Boolean = false + override fun isStatic(): Boolean = node.hasModifier("static") + override fun modifierSpecifier(): Int = Modifier.PUBLIC + if (isStatic) Modifier.STATIC else 0 + override fun qualifiedName(): String? = if (node.kind == DocumentationNode.Kind.Type) node.qualifiedNameFromType() else node.qualifiedName() + override fun annotations(): Array<out AnnotationDesc>? = nodeAnnotations(this).toTypedArray() + override fun modifiers(): String? = "public ${if (isStatic) "static" else ""}".trim() + override fun isProtected(): Boolean = false + + override fun isFinal(): Boolean = node.hasModifier("final") + + override fun containingPackage(): PackageDoc? { + if (node.kind == DocumentationNode.Kind.Type) { + return null + } + + var owner: DocumentationNode? = node + while (owner != null) { + if (owner.kind == DocumentationNode.Kind.Package) { + return PackageAdapter(module, owner) + } + owner = owner.owner + } + + return null + } + + override fun containingClass(): ClassDoc? { + if (node.kind == DocumentationNode.Kind.Type) { + return null + } + + var owner = node.owner + while (owner != null) { + when (owner.kind) { + DocumentationNode.Kind.Class, + DocumentationNode.Kind.Interface, + DocumentationNode.Kind.Enum -> return ClassDocumentationNodeAdapter(module, owner) + else -> owner = owner.owner + } + } + + return null + } + + override fun isPrivate(): Boolean = false + override fun isIncluded(): Boolean = containingPackage()?.isIncluded ?: false && containingClass()?.let { it.isIncluded } ?: true +} + +open class TypeAdapter(override val module: ModuleNodeAdapter, override val node: DocumentationNode) : Type, HasDocumentationNode, HasModule { + private val javaLanguageService = JavaLanguageService() + + override fun qualifiedTypeName(): String = javaLanguageService.getArrayElementType(node)?.qualifiedNameFromType() ?: node.qualifiedNameFromType() + override fun typeName(): String = javaLanguageService.getArrayElementType(node)?.name ?: node.name + override fun simpleTypeName(): String = typeName() // TODO difference typeName() vs simpleTypeName() + + override fun dimension(): String = Collections.nCopies(javaLanguageService.getArrayDimension(node), "[]").joinToString("") + override fun isPrimitive(): Boolean = simpleTypeName() in setOf("int", "long", "short", "byte", "char", "double", "float", "boolean", "void") + + override fun asClassDoc(): ClassDoc? = if (isPrimitive) null else + elementType?.asClassDoc() ?: + when (node.kind) { + in DocumentationNode.Kind.classLike, + DocumentationNode.Kind.ExternalClass, + DocumentationNode.Kind.Exception -> module.classNamed(qualifiedTypeName()) ?: ClassDocumentationNodeAdapter(module, node) + + else -> when { + node.links.isNotEmpty() -> TypeAdapter(module, node.links.first()).asClassDoc() + else -> ClassDocumentationNodeAdapter(module, node) // TODO ? + } + } + + override fun asTypeVariable(): TypeVariable? = if (node.kind == DocumentationNode.Kind.TypeParameter) TypeVariableAdapter(module, node) else null + override fun asParameterizedType(): ParameterizedType? = + if (node.details(DocumentationNode.Kind.Type).isNotEmpty()) ParameterizedTypeAdapter(module, node) + else null // TODO it should ignore dimensions + + override fun asAnnotationTypeDoc(): AnnotationTypeDoc? = if (node.kind == DocumentationNode.Kind.AnnotationClass) AnnotationTypeDocAdapter(module, node) else null + override fun asAnnotatedType(): AnnotatedType? = if (node.annotations.isNotEmpty()) AnnotatedTypeAdapter(module, node) else null + override fun getElementType(): Type? = javaLanguageService.getArrayElementType(node)?.let { et -> TypeAdapter(module, et) } + override fun asWildcardType(): WildcardType? = null + + override fun toString(): String = qualifiedTypeName() + dimension() + override fun hashCode(): Int = node.name.hashCode() + override fun equals(other: Any?): Boolean = other is TypeAdapter && toString() == other.toString() +} + +class AnnotatedTypeAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : TypeAdapter(module, node), AnnotatedType { + override fun underlyingType(): Type? = this + override fun annotations(): Array<out AnnotationDesc> = nodeAnnotations(this).toTypedArray() +} + +class WildcardTypeAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : TypeAdapter(module, node), WildcardType { + override fun extendsBounds(): Array<out Type> = node.details(DocumentationNode.Kind.UpperBound).map { TypeAdapter(module, it) }.toTypedArray() + override fun superBounds(): Array<out Type> = node.details(DocumentationNode.Kind.LowerBound).map { TypeAdapter(module, it) }.toTypedArray() +} + +class TypeVariableAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : TypeAdapter(module, node), TypeVariable { + override fun owner(): ProgramElementDoc = node.owner!!.let<DocumentationNode, ProgramElementDoc> { owner -> + when (owner.kind) { + DocumentationNode.Kind.Function, + DocumentationNode.Kind.Constructor -> ExecutableMemberAdapter(module, owner) + + DocumentationNode.Kind.Class, + DocumentationNode.Kind.Interface, + DocumentationNode.Kind.Enum -> ClassDocumentationNodeAdapter(module, owner) + + else -> ProgramElementAdapter(module, node.owner!!) + } + } + + override fun bounds(): Array<out Type>? = node.details(DocumentationNode.Kind.UpperBound).map { TypeAdapter(module, it) }.toTypedArray() + override fun annotations(): Array<out AnnotationDesc>? = node.members(DocumentationNode.Kind.Annotation).map { AnnotationDescAdapter(module, it) }.toTypedArray() + + override fun qualifiedTypeName(): String = node.name + override fun simpleTypeName(): String = node.name + override fun typeName(): String = node.name + + override fun hashCode(): Int = node.name.hashCode() + override fun equals(other: Any?): Boolean = other is Type && other.typeName() == typeName() && other.asTypeVariable()?.owner() == owner() + + override fun asTypeVariable(): TypeVariableAdapter = this +} + +class ParameterizedTypeAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : TypeAdapter(module, node), ParameterizedType { + override fun typeArguments(): Array<out Type> = node.details(DocumentationNode.Kind.Type).map { TypeVariableAdapter(module, it) }.toTypedArray() + override fun superclassType(): Type? = + node.lookupSuperClasses(module) + .firstOrNull { it.kind == DocumentationNode.Kind.Class || it.kind == DocumentationNode.Kind.ExternalClass } + ?.let { ClassDocumentationNodeAdapter(module, it) } + + override fun interfaceTypes(): Array<out Type> = + node.lookupSuperClasses(module) + .filter { it.kind == DocumentationNode.Kind.Interface } + .map { ClassDocumentationNodeAdapter(module, it) } + .toTypedArray() + + override fun containingType(): Type? = when (node.owner?.kind) { + DocumentationNode.Kind.Package -> null + DocumentationNode.Kind.Class, + DocumentationNode.Kind.Interface, + DocumentationNode.Kind.Object, + DocumentationNode.Kind.Enum -> ClassDocumentationNodeAdapter(module, node.owner!!) + + else -> null + } +} + +class ParameterAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), Parameter { + override fun typeName(): String? = JavaLanguageService().renderType(node.detail(DocumentationNode.Kind.Type)) + override fun type(): Type? = TypeAdapter(module, node.detail(DocumentationNode.Kind.Type)) + override fun annotations(): Array<out AnnotationDesc> = nodeAnnotations(this).toTypedArray() +} + +class ReceiverParameterAdapter(module: ModuleNodeAdapter, val receiverType: DocumentationNode, val parent: ExecutableMemberAdapter) : DocumentationNodeAdapter(module, receiverType), Parameter { + override fun typeName(): String? = receiverType.name + override fun type(): Type? = TypeAdapter(module, receiverType) + override fun annotations(): Array<out AnnotationDesc> = nodeAnnotations(this).toTypedArray() + override fun name(): String = tryName("receiver") + + private tailrec fun tryName(name: String): String = when (name) { + in parent.parameters().drop(1).map { it.name() } -> tryName("$$name") + else -> name + } +} + +fun classOf(fqName: String, kind: DocumentationNode.Kind = DocumentationNode.Kind.Class) = DocumentationNode(fqName.substringAfterLast(".", fqName), Content.Empty, kind).let { node -> + val pkg = fqName.substringBeforeLast(".", "") + if (pkg.isNotEmpty()) { + node.append(DocumentationNode(pkg, Content.Empty, DocumentationNode.Kind.Package), DocumentationReference.Kind.Owner) + } + + node +} + +open class ExecutableMemberAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), ProgramElementDoc by ProgramElementAdapter(module, node), ExecutableMemberDoc { + + override fun isSynthetic(): Boolean = false + override fun isNative(): Boolean = node.annotations.any { it.name == "native" } + + override fun thrownExceptions(): Array<out ClassDoc> = emptyArray() // TODO + override fun throwsTags(): Array<out ThrowsTag> = + node.content.sections + .filter { it.tag == "Exceptions" } + .map { it.subjectName } + .filterNotNull() + .map { ThrowsTagAdapter(this, ClassDocumentationNodeAdapter(module, classOf(it, DocumentationNode.Kind.Exception))) } + .toTypedArray() + + override fun isVarArgs(): Boolean = node.details(DocumentationNode.Kind.Parameter).any { false } // TODO + + override fun isSynchronized(): Boolean = node.annotations.any { it.name == "synchronized" } + + override fun paramTags(): Array<out ParamTag> = node.details(DocumentationNode.Kind.Parameter) + .filter { it.content.summary !is ContentEmpty || it.content.description !is ContentEmpty || it.content.sections.isNotEmpty() } + .map { ParamTagAdapter(module, this, it.name, false, it.content.children) } + .toTypedArray() + + override fun thrownExceptionTypes(): Array<out Type> = emptyArray() + override fun receiverType(): Type? = receiverNode()?.let { receiver -> TypeAdapter(module, receiver) } + override fun flatSignature(): String = node.details(DocumentationNode.Kind.Parameter).map { JavaLanguageService().renderType(it) }.joinToString(", ", "(", ")") + override fun signature(): String = node.details(DocumentationNode.Kind.Parameter).map { JavaLanguageService().renderType(it) }.joinToString(", ", "(", ")") // TODO it should be FQ types + + override fun parameters(): Array<out Parameter> = + ((receiverNode()?.let { receiver -> listOf<Parameter>(ReceiverParameterAdapter(module, receiver, this)) } ?: emptyList()) + + node.details(DocumentationNode.Kind.Parameter).map { ParameterAdapter(module, it) } + ).toTypedArray() + + override fun typeParameters(): Array<out TypeVariable> = node.details(DocumentationNode.Kind.TypeParameter).map { TypeVariableAdapter(module, it) }.toTypedArray() + + override fun typeParamTags(): Array<out ParamTag> = node.details(DocumentationNode.Kind.TypeParameter).filter { it.content.summary !is ContentEmpty || it.content.description !is ContentEmpty || it.content.sections.isNotEmpty() }.map { + ParamTagAdapter(module, this, it.name, true, it.content.children) + }.toTypedArray() + + private fun receiverNode() = node.details(DocumentationNode.Kind.Receiver).let { receivers -> + when { + receivers.isNotEmpty() -> receivers.single().detail(DocumentationNode.Kind.Type) + else -> null + } + } +} + +class ConstructorAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : ExecutableMemberAdapter(module, node), ConstructorDoc { + override fun name(): String = node.owner?.name ?: throw IllegalStateException("No owner for $node") +} + +class MethodAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), ExecutableMemberDoc by ExecutableMemberAdapter(module, node), MethodDoc { + override fun overrides(meth: MethodDoc?): Boolean = false // TODO + + override fun overriddenType(): Type? = node.overrides.firstOrNull()?.owner?.let { owner -> TypeAdapter(module, owner) } + + override fun overriddenMethod(): MethodDoc? = node.overrides.map { MethodAdapter(module, it) }.firstOrNull() + override fun overriddenClass(): ClassDoc? = overriddenMethod()?.containingClass() + + override fun isAbstract(): Boolean = false // TODO + + override fun isDefault(): Boolean = false + + override fun returnType(): Type = TypeAdapter(module, node.detail(DocumentationNode.Kind.Type)) +} + +class FieldAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), ProgramElementDoc by ProgramElementAdapter(module, node), FieldDoc { + override fun isSynthetic(): Boolean = false + + override fun constantValueExpression(): String? = node.details(DocumentationNode.Kind.Value).firstOrNull()?.let { it.name } + override fun constantValue(): Any? = constantValueExpression() + + override fun type(): Type = TypeAdapter(module, node.detail(DocumentationNode.Kind.Type)) + override fun isTransient(): Boolean = node.hasAnnotation(Transient::class) + override fun serialFieldTags(): Array<out SerialFieldTag> = emptyArray() + + override fun isVolatile(): Boolean = node.hasAnnotation(Volatile::class) +} + +open class ClassDocumentationNodeAdapter(module: ModuleNodeAdapter, val classNode: DocumentationNode) + : DocumentationNodeAdapter(module, classNode), + Type by TypeAdapter(module, classNode), + ProgramElementDoc by ProgramElementAdapter(module, classNode), + ClassDoc { + + override fun name(): String { + val parent = classNode.owner + if (parent?.kind in DocumentationNode.Kind.classLike) { + return parent!!.name + "." + classNode.name + } + return classNode.name + } + + override fun constructors(filter: Boolean): Array<out ConstructorDoc> = classNode.members(DocumentationNode.Kind.Constructor).map { ConstructorAdapter(module, it) }.toTypedArray() + override fun constructors(): Array<out ConstructorDoc> = constructors(true) + override fun importedPackages(): Array<out PackageDoc> = emptyArray() + override fun importedClasses(): Array<out ClassDoc>? = emptyArray() + override fun typeParameters(): Array<out TypeVariable> = classNode.details(DocumentationNode.Kind.TypeParameter).map { TypeVariableAdapter(module, it) }.toTypedArray() + override fun asTypeVariable(): TypeVariable? = if (classNode.kind == DocumentationNode.Kind.Class) TypeVariableAdapter(module, classNode) else null + override fun isExternalizable(): Boolean = interfaces().any { it.qualifiedName() == "java.io.Externalizable" } + override fun definesSerializableFields(): Boolean = false + override fun methods(filter: Boolean): Array<out MethodDoc> = classNode.members(DocumentationNode.Kind.Function).map { MethodAdapter(module, it) }.toTypedArray() // TODO include get/set methods + override fun methods(): Array<out MethodDoc> = methods(true) + override fun enumConstants(): Array<out FieldDoc>? = classNode.members(DocumentationNode.Kind.EnumItem).map { FieldAdapter(module, it) }.toTypedArray() + override fun isAbstract(): Boolean = classNode.details(DocumentationNode.Kind.Modifier).any { it.name == "abstract" } + override fun interfaceTypes(): Array<out Type> = classNode.lookupSuperClasses(module) + .filter { it.kind == DocumentationNode.Kind.Interface } + .map { ClassDocumentationNodeAdapter(module, it) } + .toTypedArray() + + override fun interfaces(): Array<out ClassDoc> = classNode.lookupSuperClasses(module) + .filter { it.kind == DocumentationNode.Kind.Interface } + .map { ClassDocumentationNodeAdapter(module, it) } + .toTypedArray() + + override fun typeParamTags(): Array<out ParamTag> = (classNode.details(DocumentationNode.Kind.TypeParameter).filter { it.content.summary !is ContentEmpty || it.content.description !is ContentEmpty || it.content.sections.isNotEmpty() }.map { + ParamTagAdapter(module, this, it.name, true, it.content.children) + } + classNode.content.sections.filter { it.subjectName in typeParameters().map { it.simpleTypeName() } }.map { + ParamTagAdapter(module, this, it.subjectName ?: "?", true, it.children) + }).toTypedArray() + + override fun fields(): Array<out FieldDoc> = fields(true) + override fun fields(filter: Boolean): Array<out FieldDoc> = classNode.members(DocumentationNode.Kind.Field).map { FieldAdapter(module, it) }.toTypedArray() + + override fun findClass(className: String?): ClassDoc? = null // TODO !!! + override fun serializableFields(): Array<out FieldDoc> = emptyArray() + override fun superclassType(): Type? = classNode.lookupSuperClasses(module).singleOrNull { it.kind == DocumentationNode.Kind.Class }?.let { ClassDocumentationNodeAdapter(module, it) } + override fun serializationMethods(): Array<out MethodDoc> = emptyArray() // TODO + override fun superclass(): ClassDoc? = classNode.lookupSuperClasses(module).singleOrNull { it.kind == DocumentationNode.Kind.Class }?.let { ClassDocumentationNodeAdapter(module, it) } + override fun isSerializable(): Boolean = false // TODO + override fun subclassOf(cd: ClassDoc?): Boolean { + if (cd == null) { + return false + } + + val expectedFQName = cd.qualifiedName() + val types = arrayListOf(classNode) + val visitedTypes = HashSet<String>() + + while (types.isNotEmpty()) { + val type = types.removeAt(types.lastIndex) + val fqName = type.qualifiedName() + + if (expectedFQName == fqName) { + return true + } + + visitedTypes.add(fqName) + types.addAll(type.details(DocumentationNode.Kind.Supertype).filter { it.qualifiedName() !in visitedTypes }) + } + + return false + } + + override fun innerClasses(): Array<out ClassDoc> = classNode.members(DocumentationNode.Kind.Class).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun innerClasses(filter: Boolean): Array<out ClassDoc> = innerClasses() +} + +fun DocumentationNode.lookupSuperClasses(module: ModuleNodeAdapter) = + details(DocumentationNode.Kind.Supertype) + .map { it.links.firstOrNull() } + .map { module.allTypes[it?.qualifiedName()] } + .filterNotNull() + +fun List<DocumentationNode>.collectAllTypesRecursively(): Map<String, DocumentationNode> { + val result = hashMapOf<String, DocumentationNode>() + + fun DocumentationNode.collectTypesRecursively() { + val classLikeMembers = DocumentationNode.Kind.classLike.flatMap { members(it) } + classLikeMembers.forEach { + result.put(it.qualifiedName(), it) + it.collectTypesRecursively() + } + } + + forEach { it.collectTypesRecursively() } + return result +} + +class ModuleNodeAdapter(val module: DocumentationModule, val reporter: DocErrorReporter, val outputPath: String) : DocumentationNodeBareAdapter(module), DocErrorReporter by reporter, RootDoc { + val allPackages = module.members(DocumentationNode.Kind.Package).toMapBy { it.name } + val allTypes = module.members(DocumentationNode.Kind.Package).collectAllTypesRecursively() + + override fun packageNamed(name: String?): PackageDoc? = allPackages[name]?.let { PackageAdapter(this, it) } + + override fun classes(): Array<out ClassDoc> = + allTypes.values.map { ClassDocumentationNodeAdapter(this, it) }.toTypedArray() + + override fun options(): Array<out Array<String>> = arrayOf( + arrayOf("-d", outputPath), + arrayOf("-docencoding", "UTF-8"), + arrayOf("-charset", "UTF-8"), + arrayOf("-keywords") + ) + + override fun specifiedPackages(): Array<out PackageDoc>? = module.members(DocumentationNode.Kind.Package).map { PackageAdapter(this, it) }.toTypedArray() + + override fun classNamed(qualifiedName: String?): ClassDoc? = + allTypes[qualifiedName]?.let { ClassDocumentationNodeAdapter(this, it) } + + override fun specifiedClasses(): Array<out ClassDoc> = classes() +} diff --git a/core/src/main/kotlin/javadoc/dokka-adapters.kt b/core/src/main/kotlin/javadoc/dokka-adapters.kt new file mode 100644 index 00000000..23ee1702 --- /dev/null +++ b/core/src/main/kotlin/javadoc/dokka-adapters.kt @@ -0,0 +1,30 @@ +package org.jetbrains.dokka.javadoc + +import com.google.inject.Inject +import com.sun.tools.doclets.formats.html.HtmlDoclet +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.FormatDescriptor + +class JavadocGenerator @Inject constructor (val options: DocumentationOptions, val logger: DokkaLogger) : Generator { + override fun buildPages(nodes: Iterable<DocumentationNode>) { + val module = nodes.single() as DocumentationModule + + DokkaConsoleLogger.report() + HtmlDoclet.start(ModuleNodeAdapter(module, StandardReporter(logger), options.outputDir)) + } + + override fun buildOutlines(nodes: Iterable<DocumentationNode>) { + // no outline could be generated separately + } + + override fun buildSupportFiles() { + } +} + +class JavadocFormatDescriptor : FormatDescriptor { + override val formatServiceClass = null + override val outlineServiceClass = null + override val generatorServiceClass = JavadocGenerator::class + override val packageDocumentationBuilderClass = KotlinAsJavaDocumentationBuilder::class + override val javaDocumentationBuilderClass = JavaPsiDocumentationBuilder::class +} diff --git a/core/src/main/kotlin/javadoc/reporter.kt b/core/src/main/kotlin/javadoc/reporter.kt new file mode 100644 index 00000000..fc38368c --- /dev/null +++ b/core/src/main/kotlin/javadoc/reporter.kt @@ -0,0 +1,34 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.DocErrorReporter +import com.sun.javadoc.SourcePosition +import org.jetbrains.dokka.DokkaLogger + +class StandardReporter(val logger: DokkaLogger) : DocErrorReporter { + override fun printWarning(msg: String?) { + logger.warn(msg.toString()) + } + + override fun printWarning(pos: SourcePosition?, msg: String?) { + logger.warn(format(pos, msg)) + } + + override fun printError(msg: String?) { + logger.error(msg.toString()) + } + + override fun printError(pos: SourcePosition?, msg: String?) { + logger.error(format(pos, msg)) + } + + override fun printNotice(msg: String?) { + logger.info(msg.toString()) + } + + override fun printNotice(pos: SourcePosition?, msg: String?) { + logger.info(format(pos, msg)) + } + + private fun format(pos: SourcePosition?, msg: String?) = + if (pos == null) msg.toString() else "${pos.file()}:${pos.line()}:${pos.column()}: $msg" +}
\ No newline at end of file diff --git a/core/src/main/kotlin/javadoc/source-position.kt b/core/src/main/kotlin/javadoc/source-position.kt new file mode 100644 index 00000000..0e4c6e3c --- /dev/null +++ b/core/src/main/kotlin/javadoc/source-position.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.SourcePosition +import org.jetbrains.dokka.DocumentationNode +import java.io.File + +class SourcePositionAdapter(val docNode: DocumentationNode) : SourcePosition { + + private val sourcePositionParts: List<String> by lazy { + docNode.details(DocumentationNode.Kind.SourcePosition).firstOrNull()?.name?.split(":") ?: emptyList() + } + + override fun file(): File? = if (sourcePositionParts.isEmpty()) null else File(sourcePositionParts[0]) + + override fun line(): Int = sourcePositionParts.getOrNull(1)?.toInt() ?: -1 + + override fun column(): Int = sourcePositionParts.getOrNull(2)?.toInt() ?: -1 +} diff --git a/core/src/main/kotlin/javadoc/tags.kt b/core/src/main/kotlin/javadoc/tags.kt new file mode 100644 index 00000000..5872dbaa --- /dev/null +++ b/core/src/main/kotlin/javadoc/tags.kt @@ -0,0 +1,214 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.* +import org.jetbrains.dokka.* +import java.util.* + +class TagImpl(val holder: Doc, val name: String, val text: String): Tag { + override fun text(): String? = text + + override fun holder(): Doc = holder + override fun firstSentenceTags(): Array<out Tag>? = arrayOf() + override fun inlineTags(): Array<out Tag>? = arrayOf() + + override fun name(): String = name + override fun kind(): String = name + + override fun position(): SourcePosition = holder.position() +} + +class TextTag(val holder: Doc, val content: ContentText) : Tag { + val plainText: String + get() = content.text + + override fun name(): String = "Text" + override fun kind(): String = name() + override fun text(): String? = plainText + override fun inlineTags(): Array<out Tag> = arrayOf(this) + override fun holder(): Doc = holder + override fun firstSentenceTags(): Array<out Tag> = arrayOf(this) + override fun position(): SourcePosition = holder.position() +} + +abstract class SeeTagAdapter(val holder: Doc, val content: ContentNodeLink) : SeeTag { + override fun position(): SourcePosition? = holder.position() + override fun name(): String = "@see" + override fun kind(): String = "@see" + override fun holder(): Doc = holder + + override fun text(): String? = content.node?.name ?: "(?)" +} + +class SeeExternalLinkTagAdapter(val holder: Doc, val link: ContentExternalLink) : SeeTag { + override fun position(): SourcePosition = holder.position() + override fun text(): String = label() + override fun inlineTags(): Array<out Tag> = emptyArray() // TODO + + override fun label(): String { + val label = link.asText() ?: link.href + return "<a href=\"${link.href}\">$label</a>" + } + + override fun referencedPackage(): PackageDoc? = null + override fun referencedClass(): ClassDoc? = null + override fun referencedMemberName(): String? = null + override fun referencedClassName(): String? = null + override fun referencedMember(): MemberDoc? = null + override fun holder(): Doc = holder + override fun firstSentenceTags(): Array<out Tag> = inlineTags() + override fun name(): String = "@link" + override fun kind(): String = "@see" +} + +fun ContentBlock.asText(): String? { + val contentText = children.singleOrNull() as? ContentText + return contentText?.text +} + +class SeeMethodTagAdapter(holder: Doc, val method: MethodAdapter, content: ContentNodeLink) : SeeTagAdapter(holder, content) { + override fun referencedMember(): MemberDoc = method + override fun referencedMemberName(): String = method.name() + override fun referencedPackage(): PackageDoc? = null + override fun referencedClass(): ClassDoc = method.containingClass() + override fun referencedClassName(): String = method.containingClass().name() + override fun label(): String = "${method.containingClass().name()}.${method.name()}" + + override fun inlineTags(): Array<out Tag> = emptyArray() // TODO + override fun firstSentenceTags(): Array<out Tag> = inlineTags() // TODO +} + +class SeeClassTagAdapter(holder: Doc, val clazz: ClassDocumentationNodeAdapter, content: ContentNodeLink) : SeeTagAdapter(holder, content) { + override fun referencedMember(): MemberDoc? = null + override fun referencedMemberName(): String? = null + override fun referencedPackage(): PackageDoc? = null + override fun referencedClass(): ClassDoc = clazz + override fun referencedClassName(): String = clazz.name() + override fun label(): String = "${clazz.classNode.kind.name.toLowerCase()} ${clazz.name()}" + + override fun inlineTags(): Array<out Tag> = emptyArray() // TODO + override fun firstSentenceTags(): Array<out Tag> = inlineTags() // TODO +} + +class ParamTagAdapter(val module: ModuleNodeAdapter, + val holder: Doc, + val parameterName: String, + val typeParameter: Boolean, + val content: List<ContentNode>) : ParamTag { + + constructor(module: ModuleNodeAdapter, holder: Doc, parameterName: String, isTypeParameter: Boolean, content: ContentNode) + : this(module, holder, parameterName, isTypeParameter, listOf(content)) { + } + + override fun name(): String = "@param" + override fun kind(): String = name() + override fun holder(): Doc = holder + override fun position(): SourcePosition? = holder.position() + + override fun text(): String = "@param $parameterName ..." + override fun inlineTags(): Array<out Tag> = content.flatMap { buildInlineTags(module, holder, it) }.toTypedArray() + override fun firstSentenceTags(): Array<out Tag> = arrayOf(TextTag(holder, ContentText(text()))) + + override fun isTypeParameter(): Boolean = typeParameter + override fun parameterComment(): String = content.toString() // TODO + override fun parameterName(): String = parameterName +} + + +class ThrowsTagAdapter(val holder: Doc, val type: ClassDocumentationNodeAdapter) : ThrowsTag { + override fun name(): String = "@throws" + override fun kind(): String = name() + override fun holder(): Doc = holder + override fun position(): SourcePosition? = holder.position() + + override fun text(): String = "@throws ${type.qualifiedTypeName()}" + override fun inlineTags(): Array<out Tag> = emptyArray() + override fun firstSentenceTags(): Array<out Tag> = emptyArray() + + override fun exceptionComment(): String = "" + override fun exceptionType(): Type = type + override fun exception(): ClassDoc = type + override fun exceptionName(): String = type.qualifiedName() +} + +fun buildInlineTags(module: ModuleNodeAdapter, holder: Doc, root: ContentNode): List<Tag> = ArrayList<Tag>().let { buildInlineTags(module, holder, root, it); it } + +private fun buildInlineTags(module: ModuleNodeAdapter, holder: Doc, nodes: List<ContentNode>, result: MutableList<Tag>) { + nodes.forEach { + buildInlineTags(module, holder, it, result) + } +} + + +private fun buildInlineTags(module: ModuleNodeAdapter, holder: Doc, node: ContentNode, result: MutableList<Tag>) { + fun surroundWith(module: ModuleNodeAdapter, holder: Doc, prefix: String, postfix: String, node: ContentBlock, result: MutableList<Tag>) { + if (node.children.isNotEmpty()) { + val open = TextTag(holder, ContentText(prefix)) + val close = TextTag(holder, ContentText(postfix)) + + result.add(open) + buildInlineTags(module, holder, node.children, result) + + if (result.last() === open) { + result.removeAt(result.lastIndex) + } else { + result.add(close) + } + } + } + + fun surroundWith(module: ModuleNodeAdapter, holder: Doc, prefix: String, postfix: String, node: ContentNode, result: MutableList<Tag>) { + if (node !is ContentEmpty) { + val open = TextTag(holder, ContentText(prefix)) + val close = TextTag(holder, ContentText(postfix)) + + result.add(open) + buildInlineTags(module, holder, node, result) + if (result.last() === open) { + result.removeAt(result.lastIndex) + } else { + result.add(close) + } + } + } + + when (node) { + is ContentText -> result.add(TextTag(holder, node)) + is ContentNodeLink -> { + val target = node.node + when (target?.kind) { + DocumentationNode.Kind.Function -> result.add(SeeMethodTagAdapter(holder, MethodAdapter(module, node.node!!), node)) + + in DocumentationNode.Kind.classLike -> result.add(SeeClassTagAdapter(holder, ClassDocumentationNodeAdapter(module, node.node!!), node)) + + else -> buildInlineTags(module, holder, node.children, result) + } + } + is ContentExternalLink -> result.add(SeeExternalLinkTagAdapter(holder, node)) + is ContentCode -> surroundWith(module, holder, "<code>", "</code>", node, result) + is ContentBlockCode -> surroundWith(module, holder, "<code><pre>", "</pre></code>", node, result) + is ContentEmpty -> {} + is ContentEmphasis -> surroundWith(module, holder, "<em>", "</em>", node, result) + is ContentHeading -> surroundWith(module, holder, "<h${node.level}>", "</h${node.level}>", node, result) + is ContentEntity -> result.add(TextTag(holder, ContentText(node.text))) // TODO ?? + is ContentIdentifier -> result.add(TextTag(holder, ContentText(node.text))) // TODO + is ContentKeyword -> result.add(TextTag(holder, ContentText(node.text))) // TODO + is ContentListItem -> surroundWith(module, holder, "<li>", "</li>", node, result) + is ContentOrderedList -> surroundWith(module, holder, "<ol>", "</ol>", node, result) + is ContentUnorderedList -> surroundWith(module, holder, "<ul>", "</ul>", node, result) + is ContentParagraph -> surroundWith(module, holder, "<p>", "</p>", node, result) + is ContentSection -> surroundWith(module, holder, "<p>", "</p>", node, result) // TODO how section should be represented? + is ContentNonBreakingSpace -> result.add(TextTag(holder, ContentText(" "))) + is ContentStrikethrough -> surroundWith(module, holder, "<strike>", "</strike>", node, result) + is ContentStrong -> surroundWith(module, holder, "<strong>", "</strong>", node, result) + is ContentSymbol -> result.add(TextTag(holder, ContentText(node.text))) // TODO? + is Content -> { + surroundWith(module, holder, "<p>", "</p>", node.summary, result) + surroundWith(module, holder, "<p>", "</p>", node.description, result) +// node.sections.forEach { +// buildInlineTags(module, holder, it, result) +// } + } + + else -> result.add(TextTag(holder, ContentText("$node"))) + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/main.kt b/core/src/main/kotlin/main.kt new file mode 100644 index 00000000..22e82991 --- /dev/null +++ b/core/src/main/kotlin/main.kt @@ -0,0 +1,262 @@ +package org.jetbrains.dokka + +import com.google.inject.Guice +import com.google.inject.Injector +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiManager +import com.sampullara.cli.Args +import com.sampullara.cli.Argument +import org.jetbrains.dokka.Utilities.DokkaModule +import org.jetbrains.kotlin.cli.common.arguments.ValueDescription +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.resolve.LazyTopDownAnalyzerForTopLevel +import org.jetbrains.kotlin.resolve.TopDownAnalysisMode +import org.jetbrains.kotlin.utils.PathUtil +import java.io.File +import kotlin.util.measureTimeMillis + +class DokkaArguments { + @set:Argument(value = "src", description = "Source file or directory (allows many paths separated by the system path separator)") + @ValueDescription("<path>") + public var src: String = "" + + @set:Argument(value = "srcLink", description = "Mapping between a source directory and a Web site for browsing the code") + @ValueDescription("<path>=<url>[#lineSuffix]") + public var srcLink: String = "" + + @set:Argument(value = "include", description = "Markdown files to load (allows many paths separated by the system path separator)") + @ValueDescription("<path>") + public var include: String = "" + + @set:Argument(value = "samples", description = "Source root for samples") + @ValueDescription("<path>") + public var samples: String = "" + + @set:Argument(value = "output", description = "Output directory path") + @ValueDescription("<path>") + public var outputDir: String = "out/doc/" + + @set:Argument(value = "format", description = "Output format (text, html, markdown, jekyll, kotlin-website)") + @ValueDescription("<name>") + public var outputFormat: String = "html" + + @set:Argument(value = "module", description = "Name of the documentation module") + @ValueDescription("<name>") + public var moduleName: String = "" + + @set:Argument(value = "classpath", description = "Classpath for symbol resolution") + @ValueDescription("<path>") + public var classpath: String = "" + + @set:Argument(value = "nodeprecated", description = "Exclude deprecated members from documentation") + public var nodeprecated: Boolean = false + +} + +private fun parseSourceLinkDefinition(srcLink: String): SourceLinkDefinition { + val (path, urlAndLine) = srcLink.split('=') + return SourceLinkDefinition(File(path).absolutePath, + urlAndLine.substringBefore("#"), + urlAndLine.substringAfter("#", "").let { if (it.isEmpty()) null else "#" + it }) +} + +public fun main(args: Array<String>) { + val arguments = DokkaArguments() + val freeArgs: List<String> = Args.parse(arguments, args) ?: listOf() + val sources = if (arguments.src.isNotEmpty()) arguments.src.split(File.pathSeparatorChar).toList() + freeArgs else freeArgs + val samples = if (arguments.samples.isNotEmpty()) arguments.samples.split(File.pathSeparatorChar).toList() else listOf() + val includes = if (arguments.include.isNotEmpty()) arguments.include.split(File.pathSeparatorChar).toList() else listOf() + + val sourceLinks = if (arguments.srcLink.isNotEmpty() && arguments.srcLink.contains("=")) + listOf(parseSourceLinkDefinition(arguments.srcLink)) + else { + if (arguments.srcLink.isNotEmpty()) { + println("Warning: Invalid -srcLink syntax. Expected: <path>=<url>[#lineSuffix]. No source links will be generated.") + } + listOf() + } + + val classPath = arguments.classpath.split(File.pathSeparatorChar).toList() + val generator = DokkaGenerator( + DokkaConsoleLogger, + classPath, + sources, + samples, + includes, + arguments.moduleName, + arguments.outputDir.let { if (it.endsWith('/')) it else it + '/' }, + arguments.outputFormat, + sourceLinks, + arguments.nodeprecated) + + generator.generate() + DokkaConsoleLogger.report() +} + +interface DokkaLogger { + fun info(message: String) + fun warn(message: String) + fun error(message: String) +} + +object DokkaConsoleLogger: DokkaLogger { + var warningCount: Int = 0 + + override fun info(message: String) = println(message) + override fun warn(message: String) { + println("WARN: $message") + warningCount++ + } + + override fun error(message: String) = println("ERROR: $message") + + fun report() { + if (warningCount > 0) { + println("generation completed with $warningCount warnings") + } else { + println("generation completed successfully") + } + } +} + +class DokkaMessageCollector(val logger: DokkaLogger): MessageCollector { + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation) { + logger.error(MessageRenderer.PLAIN_FULL_PATHS.render(severity, message, location)) + } +} + +class DokkaGenerator(val logger: DokkaLogger, + val classpath: List<String>, + val sources: List<String>, + val samples: List<String>, + val includes: List<String>, + val moduleName: String, + val outputDir: String, + val outputFormat: String, + val sourceLinks: List<SourceLinkDefinition>, + val skipDeprecated: Boolean = false) { + fun generate() { + val environment = createAnalysisEnvironment() + + logger.info("Module: $moduleName") + logger.info("Output: ${File(outputDir)}") + logger.info("Sources: ${environment.sources.joinToString()}") + logger.info("Classpath: ${environment.classpath.joinToString()}") + + logger.info("Analysing sources and libraries... ") + val startAnalyse = System.currentTimeMillis() + + val options = DocumentationOptions(outputDir, outputFormat, false, sourceLinks = sourceLinks, skipDeprecated = skipDeprecated) + + val injector = Guice.createInjector(DokkaModule(environment, options, logger)) + + val documentation = buildDocumentationModule(injector, moduleName, { isSample(it) }, includes) + + val timeAnalyse = System.currentTimeMillis() - startAnalyse + logger.info("done in ${timeAnalyse / 1000} secs") + + val timeBuild = measureTimeMillis { + logger.info("Generating pages... ") + injector.getInstance(Generator::class.java).buildAll(documentation) + } + logger.info("done in ${timeBuild / 1000} secs") + + Disposer.dispose(environment) + } + + fun createAnalysisEnvironment(): AnalysisEnvironment { + val environment = AnalysisEnvironment(DokkaMessageCollector(logger)) + + environment.apply { + addClasspath(PathUtil.getJdkClassesRoots()) + // addClasspath(PathUtil.getKotlinPathsForCompiler().getRuntimePath()) + for (element in this@DokkaGenerator.classpath) { + addClasspath(File(element)) + } + + addSources(this@DokkaGenerator.sources) + addSources(this@DokkaGenerator.samples) + } + + return environment + } + + fun isSample(file: PsiFile): Boolean { + val sourceFile = File(file.virtualFile!!.path) + return samples.none { sample -> + val canonicalSample = File(sample).canonicalPath + val canonicalSource = sourceFile.canonicalPath + canonicalSource.startsWith(canonicalSample) + } + } +} + +fun buildDocumentationModule(injector: Injector, + moduleName: String, + filesToDocumentFilter: (PsiFile) -> Boolean = { file -> true }, + includes: List<String> = listOf()): DocumentationModule { + + val coreEnvironment = injector.getInstance(KotlinCoreEnvironment::class.java) + val fragmentFiles = coreEnvironment.getSourceFiles().filter(filesToDocumentFilter) + + val resolutionFacade = injector.getInstance(DokkaResolutionFacade::class.java) + val analyzer = resolutionFacade.getFrontendService(LazyTopDownAnalyzerForTopLevel::class.java) + analyzer.analyzeDeclarations(TopDownAnalysisMode.TopLevelDeclarations, fragmentFiles) + + val fragments = fragmentFiles + .map { resolutionFacade.resolveSession.getPackageFragment(it.packageFqName) } + .filterNotNull() + .distinct() + + val packageDocs = injector.getInstance(PackageDocs::class.java) + for (include in includes) { + packageDocs.parse(include, fragments.firstOrNull()) + } + val documentationModule = DocumentationModule(moduleName, packageDocs.moduleContent) + + with(injector.getInstance(DocumentationBuilder::class.java)) { + documentationModule.appendFragments(fragments, packageDocs.packageContent, + injector.getInstance(PackageDocumentationBuilder::class.java)) + } + + val javaFiles = coreEnvironment.getJavaSourceFiles().filter(filesToDocumentFilter) + with(injector.getInstance(JavaDocumentationBuilder::class.java)) { + javaFiles.map { appendFile(it, documentationModule, packageDocs.packageContent) } + } + + injector.getInstance(NodeReferenceGraph::class.java).resolveReferences() + + return documentationModule +} + + +fun KotlinCoreEnvironment.getJavaSourceFiles(): List<PsiJavaFile> { + val sourceRoots = configuration.get(CommonConfigurationKeys.CONTENT_ROOTS) + ?.filterIsInstance<JavaSourceRoot>() + ?.map { it.file } + ?: listOf() + + val result = arrayListOf<PsiJavaFile>() + val localFileSystem = VirtualFileManager.getInstance().getFileSystem("file") + sourceRoots.forEach { sourceRoot -> + sourceRoot.absoluteFile.walkTopDown().forEach { + val vFile = localFileSystem.findFileByPath(it.path) + if (vFile != null) { + val psiFile = PsiManager.getInstance(project).findFile(vFile) + if (psiFile is PsiJavaFile) { + result.add(psiFile) + } + } + } + } + return result +} diff --git a/core/src/main/resources/META-INF/MANIFEST.MF b/core/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 00000000..78fabddc --- /dev/null +++ b/core/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Class-Path: kotlin-plugin.jar +Main-Class: org.jetbrains.dokka.DokkaPackage + diff --git a/core/src/main/resources/dokka-antlib.xml b/core/src/main/resources/dokka-antlib.xml new file mode 100644 index 00000000..9c3373d5 --- /dev/null +++ b/core/src/main/resources/dokka-antlib.xml @@ -0,0 +1,3 @@ +<antlib> + <taskdef name="dokka" classname="org.jetbrains.dokka.ant.DokkaAntTask"/> +</antlib> diff --git a/core/src/main/resources/dokka/format/html-as-java.properties b/core/src/main/resources/dokka/format/html-as-java.properties new file mode 100644 index 00000000..f598f377 --- /dev/null +++ b/core/src/main/resources/dokka/format/html-as-java.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.HtmlAsJavaFormatDescriptor +description=Produces output in HTML format using Java syntax
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/html.properties b/core/src/main/resources/dokka/format/html.properties new file mode 100644 index 00000000..7881dfae --- /dev/null +++ b/core/src/main/resources/dokka/format/html.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.HtmlFormatDescriptor +description=Produces output in HTML format
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/javadoc.properties b/core/src/main/resources/dokka/format/javadoc.properties new file mode 100644 index 00000000..a58317fc --- /dev/null +++ b/core/src/main/resources/dokka/format/javadoc.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.javadoc.JavadocFormatDescriptor
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/jekyll.properties b/core/src/main/resources/dokka/format/jekyll.properties new file mode 100644 index 00000000..b11401a4 --- /dev/null +++ b/core/src/main/resources/dokka/format/jekyll.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.JekyllFormatDescriptor +description=Produces documentation in Jekyll format
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/kotlin-website.properties b/core/src/main/resources/dokka/format/kotlin-website.properties new file mode 100644 index 00000000..c13e7675 --- /dev/null +++ b/core/src/main/resources/dokka/format/kotlin-website.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.KotlinWebsiteFormatDescriptor +description=Generates Kotlin website documentation
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/markdown.properties b/core/src/main/resources/dokka/format/markdown.properties new file mode 100644 index 00000000..6217a6df --- /dev/null +++ b/core/src/main/resources/dokka/format/markdown.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.MarkdownFormatDescriptor +description=Produces documentation in markdown format
\ No newline at end of file diff --git a/core/src/main/resources/dokka/generator/default.properties b/core/src/main/resources/dokka/generator/default.properties new file mode 100644 index 00000000..a4a16200 --- /dev/null +++ b/core/src/main/resources/dokka/generator/default.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.FileGenerator +description=Default documentation generator
\ No newline at end of file diff --git a/core/src/main/resources/dokka/generator/javadoc.properties b/core/src/main/resources/dokka/generator/javadoc.properties new file mode 100644 index 00000000..4075704f --- /dev/null +++ b/core/src/main/resources/dokka/generator/javadoc.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.javadoc.JavadocGenerator +description=Produces output via JDK javadoc tool
\ No newline at end of file diff --git a/core/src/main/resources/dokka/language/java.properties b/core/src/main/resources/dokka/language/java.properties new file mode 100644 index 00000000..ab42f532 --- /dev/null +++ b/core/src/main/resources/dokka/language/java.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.JavaLanguageService
\ No newline at end of file diff --git a/core/src/main/resources/dokka/language/kotlin.properties b/core/src/main/resources/dokka/language/kotlin.properties new file mode 100644 index 00000000..16092007 --- /dev/null +++ b/core/src/main/resources/dokka/language/kotlin.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.KotlinLanguageService
\ No newline at end of file diff --git a/core/src/main/resources/dokka/outline/yaml.properties b/core/src/main/resources/dokka/outline/yaml.properties new file mode 100644 index 00000000..7268af37 --- /dev/null +++ b/core/src/main/resources/dokka/outline/yaml.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.YamlOutlineService
\ No newline at end of file diff --git a/core/src/main/resources/dokka/styles/style.css b/core/src/main/resources/dokka/styles/style.css new file mode 100644 index 00000000..09586237 --- /dev/null +++ b/core/src/main/resources/dokka/styles/style.css @@ -0,0 +1,280 @@ +@import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700); + +body, table { + padding:50px; + font:14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; + color:#555; + font-weight:300; +} + +.keyword { + color:black; + font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + font-size:12px; +} + +.symbol { + font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + font-size:12px; +} + +.identifier { + color: darkblue; + font-size:12px; + font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; +} + +h1, h2, h3, h4, h5, h6 { + color:#222; + margin:0 0 20px; +} + +p, ul, ol, table, pre, dl { + margin:0 0 20px; +} + +h1, h2, h3 { + line-height:1.1; +} + +h1 { + font-size:28px; +} + +h2 { + color:#393939; +} + +h3, h4, h5, h6 { + color:#494949; +} + +a { + color:#258aaf; + font-weight:400; + text-decoration:none; +} + +a:hover { + color: inherit; + text-decoration:underline; +} + +a small { + font-size:11px; + color:#555; + margin-top:-0.6em; + display:block; +} + +.wrapper { + width:860px; + margin:0 auto; +} + +blockquote { + border-left:1px solid #e5e5e5; + margin:0; + padding:0 0 0 20px; + font-style:italic; +} + +code, pre { + font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + color:#333; + font-size:12px; +} + +pre { + display: block; +/* + padding:8px 8px; + background: #f8f8f8; + border-radius:5px; + border:1px solid #e5e5e5; +*/ + overflow-x: auto; +} + +table { + width:100%; + border-collapse:collapse; +} + +th, td { + text-align:left; + vertical-align: top; + padding:5px 10px; +} + +dt { + color:#444; + font-weight:700; +} + +th { + color:#444; +} + +img { + max-width:100%; +} + +header { + width:270px; + float:left; + position:fixed; +} + +header ul { + list-style:none; + height:40px; + + padding:0; + + background: #eee; + background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); + background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + + border-radius:5px; + border:1px solid #d2d2d2; + box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; + width:270px; +} + +header li { + width:89px; + float:left; + border-right:1px solid #d2d2d2; + height:40px; +} + +header ul a { + line-height:1; + font-size:11px; + color:#999; + display:block; + text-align:center; + padding-top:6px; + height:40px; +} + +strong { + color:#222; + font-weight:700; +} + +header ul li + li { + width:88px; + border-left:1px solid #fff; +} + +header ul li + li + li { + border-right:none; + width:89px; +} + +header ul a strong { + font-size:14px; + display:block; + color:#222; +} + +section { + width:500px; + float:right; + padding-bottom:50px; +} + +small { + font-size:11px; +} + +hr { + border:0; + background:#e5e5e5; + height:1px; + margin:0 0 20px; +} + +footer { + width:270px; + float:left; + position:fixed; + bottom:50px; +} + +@media print, screen and (max-width: 960px) { + + div.wrapper { + width:auto; + margin:0; + } + + header, section, footer { + float:none; + position:static; + width:auto; + } + + header { + padding-right:320px; + } + + section { + border:1px solid #e5e5e5; + border-width:1px 0; + padding:20px 0; + margin:0 0 20px; + } + + header a small { + display:inline; + } + + header ul { + position:absolute; + right:50px; + top:52px; + } +} + +@media print, screen and (max-width: 720px) { + body { + word-wrap:break-word; + } + + header { + padding:0; + } + + header ul, header p.view { + position:static; + } + + pre, code { + word-wrap:normal; + } +} + +@media print, screen and (max-width: 480px) { + body { + padding:15px; + } + + header ul { + display:none; + } +} + +@media print { + body { + padding:0.4in; + font-size:12pt; + color:#444; + } +} diff --git a/core/src/main/resources/format/javadoc.properties b/core/src/main/resources/format/javadoc.properties new file mode 100644 index 00000000..a58317fc --- /dev/null +++ b/core/src/main/resources/format/javadoc.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.javadoc.JavadocFormatDescriptor
\ No newline at end of file |