diff options
Diffstat (limited to 'core/src')
119 files changed, 17500 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 000000000..9fea67407 --- /dev/null +++ b/core/src/main/kotlin/Analysis/AnalysisEnvironment.kt @@ -0,0 +1,371 @@ +package org.jetbrains.dokka + +import com.google.common.collect.ImmutableMap +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.openapi.vfs.StandardFileSystems +import com.intellij.psi.PsiElement +import com.intellij.psi.impl.source.javadoc.JavadocManagerImpl +import com.intellij.psi.javadoc.CustomJavadocTagProvider +import com.intellij.psi.javadoc.JavadocManager +import com.intellij.psi.javadoc.JavadocTagInfo +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.analyzer.* +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.builtins.jvm.JvmBuiltIns +import org.jetbrains.kotlin.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.config.ContentRoot +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot +import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoot +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.JvmPackagePartProvider +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM +import org.jetbrains.kotlin.cli.jvm.config.* +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.config.* +import org.jetbrains.kotlin.container.getService +import org.jetbrains.kotlin.container.tryGetService +import org.jetbrains.kotlin.context.ProjectContext +import org.jetbrains.kotlin.context.withModule +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.platform.konan.KonanPlatforms +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.CompilerEnvironment +import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices +import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics +import org.jetbrains.kotlin.resolve.jvm.JvmPlatformParameters +import org.jetbrains.kotlin.resolve.jvm.JvmResolverForModuleFactory +import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatformAnalyzerServices +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.util.slicedMap.ReadOnlySlice +import org.jetbrains.kotlin.util.slicedMap.WritableSlice +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 + */ +class AnalysisEnvironment(val messageCollector: MessageCollector) : Disposable { + val configuration = CompilerConfiguration() + + init { + configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector) + } + + fun createCoreEnvironment(): KotlinCoreEnvironment { + System.setProperty("idea.io.use.fallback", "true") + val environment = KotlinCoreEnvironment.createForProduction(this, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) + val projectComponentManager = environment.project as MockComponentManager + + val projectFileIndex = CoreProjectFileIndex(environment.project, + environment.configuration.getList(CLIConfigurationKeys.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) + + CoreApplicationEnvironment.registerExtensionPoint(Extensions.getArea(environment.project), + JavadocTagInfo.EP_NAME, JavadocTagInfo::class.java) + CoreApplicationEnvironment.registerExtensionPoint(Extensions.getRootArea(), + CustomJavadocTagProvider.EP_NAME, CustomJavadocTagProvider::class.java) + + projectComponentManager.registerService(ProjectFileIndex::class.java, + projectFileIndex) + projectComponentManager.registerService(ProjectRootManager::class.java, + CoreProjectRootManager(projectFileIndex)) + projectComponentManager.registerService(JavadocManager::class.java, + JavadocManagerImpl(environment.project)) + projectComponentManager.registerService(CustomJavadocTagProvider::class.java, + CustomJavadocTagProvider { emptyList() }) + return environment + } + + fun createSourceModuleSearchScope(project: Project, sourceFiles: List<KtFile>): GlobalSearchScope { + // TODO: Fix when going to implement dokka for JS + return TopDownAnalyzerFacadeForJVM.newModuleSearchScope(project, sourceFiles) + } + + + fun createResolutionFacade(environment: KotlinCoreEnvironment): Pair<DokkaResolutionFacade, DokkaResolutionFacade> { + + val projectContext = ProjectContext(environment.project, "Dokka") + val sourceFiles = environment.getSourceFiles() + + + val library = object : ModuleInfo { + override val name: Name = Name.special("<library>") + override val platform: TargetPlatform + get() = JvmPlatforms.defaultJvmPlatform + override val analyzerServices: PlatformDependentAnalyzerServices = + JvmPlatformAnalyzerServices + override fun dependencies(): List<ModuleInfo> = listOf(this) + } + val module = object : ModuleInfo { + override val name: Name = Name.special("<module>") + override val platform: TargetPlatform + get() = JvmPlatforms.defaultJvmPlatform + override val analyzerServices: PlatformDependentAnalyzerServices = + JvmPlatformAnalyzerServices + override fun dependencies(): List<ModuleInfo> = listOf(this, library) + } + + val sourcesScope = createSourceModuleSearchScope(environment.project, sourceFiles) + + val builtIns = JvmBuiltIns( + projectContext.storageManager, + JvmBuiltIns.Kind.FROM_CLASS_LOADER + ) + + + val javaRoots = classpath + .mapNotNull { + val rootFile = when { + it.extension == "jar" -> + StandardFileSystems.jar().findFileByPath("${it.absolutePath}${"!/"}") + else -> + StandardFileSystems.local().findFileByPath(it.absolutePath) + } + + rootFile?.let { JavaRoot(it, JavaRoot.RootType.BINARY) } + } + + val resolverForProject = object : AbstractResolverForProject<ModuleInfo>( + "Dokka", + projectContext, + modules = listOf(module, library) + ) { + override fun modulesContent(module: ModuleInfo): ModuleContent<ModuleInfo> = + when (module) { + library -> ModuleContent(module, emptyList(), GlobalSearchScope.notScope(sourcesScope)) + module -> ModuleContent(module, emptyList(), sourcesScope) + else -> throw IllegalArgumentException("Unexpected module info") + } + + override fun builtInsForModule(module: ModuleInfo): KotlinBuiltIns = builtIns + + override fun createResolverForModule( + descriptor: ModuleDescriptor, + moduleInfo: ModuleInfo + ): ResolverForModule = JvmResolverForModuleFactory( + JvmPlatformParameters({ content -> + JvmPackagePartProvider( + configuration.languageVersionSettings, + content.moduleContentScope + ) + .apply { + addRoots(javaRoots, messageCollector) + } + }, { + val file = (it as JavaClassImpl).psi.containingFile.virtualFile + if (file in sourcesScope) + module + else + library + }), + CompilerEnvironment, + KonanPlatforms.defaultKonanPlatform + ).createResolverForModule( + descriptor as ModuleDescriptorImpl, + projectContext.withModule(descriptor), + modulesContent(moduleInfo), + this, + LanguageVersionSettingsImpl.DEFAULT + ) + + override fun sdkDependency(module: ModuleInfo): ModuleInfo? = null + } + + val resolverForLibrary = resolverForProject.resolverForModule(library) // Required before module to initialize library properly + val resolverForModule = resolverForProject.resolverForModule(module) + val libraryModuleDescriptor = resolverForProject.descriptorForModule(library) + val moduleDescriptor = resolverForProject.descriptorForModule(module) + builtIns.initialize(moduleDescriptor, true) + val libraryResolutionFacade = DokkaResolutionFacade(environment.project, libraryModuleDescriptor, resolverForLibrary) + val created = DokkaResolutionFacade(environment.project, moduleDescriptor, resolverForModule) + val projectComponentManager = environment.project as MockComponentManager + projectComponentManager.registerService(KotlinCacheService::class.java, CoreKotlinCacheService(created)) + + return created to libraryResolutionFacade + } + + fun loadLanguageVersionSettings(languageVersionString: String?, apiVersionString: String?) { + val languageVersion = LanguageVersion.fromVersionString(languageVersionString) ?: LanguageVersion.LATEST_STABLE + val apiVersion = apiVersionString?.let { ApiVersion.parse(it) } ?: ApiVersion.createByLanguageVersion(languageVersion) + configuration.languageVersionSettings = LanguageVersionSettingsImpl(languageVersion, apiVersion) + } + + /** + * Classpath for this environment. + */ + val classpath: List<File> + get() = configuration.jvmClasspathRoots + + /** + * Adds list of paths to classpath. + * $paths: collection of files to add + */ + fun addClasspath(paths: List<File>) { + configuration.addJvmClasspathRoots(paths) + } + + /** + * Adds path to classpath. + * $path: path to add + */ + fun addClasspath(path: File) { + configuration.addJvmClasspathRoot(path) + } + + /** + * List of source roots for this environment. + */ + val sources: List<String> + get() = configuration.get(CLIConfigurationKeys.CONTENT_ROOTS) + ?.filterIsInstance<KotlinSourceRoot>() + ?.map { it.path } ?: emptyList() + + /** + * Adds list of paths to source roots. + * $list: collection of files to add + */ + fun addSources(list: List<String>) { + list.forEach { + configuration.addKotlinSourceRoot(it) + val file = File(it) + if (file.isDirectory || file.extension == ".java") { + configuration.addJavaSourceRoot(file) + } + } + } + + fun addRoots(list: List<ContentRoot>) { + configuration.addAll(CLIConfigurationKeys.CONTENT_ROOTS, list) + } + + /** + * Disposes the environment and frees all associated resources. + */ + override fun dispose() { + Disposer.dispose(this) + } +} + +fun contentRootFromPath(path: String): ContentRoot { + val file = File(path) + return if (file.extension == "java") JavaSourceRoot(file, null) else KotlinSourceRoot(path, false) +} + + +class DokkaResolutionFacade(override val project: Project, + override val moduleDescriptor: ModuleDescriptor, + val resolverForModule: ResolverForModule) : ResolutionFacade { + override fun analyzeWithAllCompilerChecks(elements: Collection<KtElement>): AnalysisResult { + throw UnsupportedOperationException() + } + + override fun <T : Any> tryGetFrontendService(element: PsiElement, serviceClass: Class<T>): T? { + return resolverForModule.componentProvider.tryGetService(serviceClass) + } + + override fun resolveToDescriptor(declaration: KtDeclaration, bodyResolveMode: BodyResolveMode): DeclarationDescriptor { + return resolveSession.resolveToDescriptor(declaration) + } + + override fun analyze(elements: Collection<KtElement>, bodyResolveMode: BodyResolveMode): BindingContext { + throw UnsupportedOperationException() + } + + val resolveSession: ResolveSession get() = getFrontendService(ResolveSession::class.java) + + override fun analyze(element: KtElement, bodyResolveMode: BodyResolveMode): BindingContext { + if (element is KtDeclaration) { + val descriptor = resolveToDescriptor(element) + return object : BindingContext { + override fun <K : Any?, V : Any?> getKeys(p0: WritableSlice<K, V>?): Collection<K> { + throw UnsupportedOperationException() + } + + override fun getType(p0: KtExpression): KotlinType? { + throw UnsupportedOperationException() + } + + override fun <K : Any?, V : Any?> get(slice: ReadOnlySlice<K, V>?, key: K): V? { + if (key != element) { + throw UnsupportedOperationException() + } + return when { + slice == BindingContext.DECLARATION_TO_DESCRIPTOR -> descriptor as V + slice == BindingContext.PRIMARY_CONSTRUCTOR_PARAMETER && (element as KtParameter).hasValOrVar() -> descriptor as V + else -> null + } + } + + override fun getDiagnostics(): Diagnostics { + throw UnsupportedOperationException() + } + + override fun addOwnDataTo(p0: BindingTrace, p1: Boolean) { + throw UnsupportedOperationException() + } + + override fun <K : Any?, V : Any?> getSliceContents(p0: ReadOnlySlice<K, V>): ImmutableMap<K, V> { + throw UnsupportedOperationException() + } + + } + } + 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 { + return resolverForModule.componentProvider.getService(serviceClass) + } + + override fun <T : Any> getIdeService(serviceClass: Class<T>): T { + throw UnsupportedOperationException() + } + +} diff --git a/core/src/main/kotlin/Analysis/CoreKotlinCacheService.kt b/core/src/main/kotlin/Analysis/CoreKotlinCacheService.kt new file mode 100644 index 000000000..d9093760c --- /dev/null +++ b/core/src/main/kotlin/Analysis/CoreKotlinCacheService.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka + +import com.intellij.psi.PsiFile +import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.resolve.diagnostics.KotlinSuppressCache + + +class CoreKotlinCacheService(private val resolutionFacade: DokkaResolutionFacade) : KotlinCacheService { + override fun getResolutionFacade(elements: List<KtElement>): ResolutionFacade { + return resolutionFacade + } + + override fun getResolutionFacade( + elements: List<KtElement>, + platform: org.jetbrains.kotlin.platform.TargetPlatform + ): ResolutionFacade { + return resolutionFacade + } + + override fun getResolutionFacadeByFile( + file: PsiFile, + platform: org.jetbrains.kotlin.platform.TargetPlatform + ): ResolutionFacade? { + return resolutionFacade + } + + override fun getResolutionFacadeByModuleInfo( + moduleInfo: ModuleInfo, + platform: org.jetbrains.kotlin.platform.TargetPlatform + ): ResolutionFacade? { + return resolutionFacade + } + + override fun getSuppressionCache(): KotlinSuppressCache { + throw UnsupportedOperationException() + } + +} + diff --git a/core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt b/core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt new file mode 100644 index 000000000..4ece8d300 --- /dev/null +++ b/core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt @@ -0,0 +1,569 @@ +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.VfsUtilCore.getVirtualFileForJar +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileFilter +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.messages.MessageBus +import org.jetbrains.jps.model.module.JpsModuleSourceRootType +import org.jetbrains.kotlin.cli.common.config.ContentRoot +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.cli.jvm.config.JvmContentRoot +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(private val project: Project, contentRoots: List<ContentRoot>) : ProjectFileIndex, ModuleFileIndex { + override fun iterateContent(p0: ContentIterator, p1: VirtualFileFilter?): Boolean { + throw UnsupportedOperationException() + } + + override fun iterateContentUnderDirectory(p0: VirtualFile, p1: ContentIterator, p2: VirtualFileFilter?): Boolean { + throw UnsupportedOperationException() + } + + override fun isInLibrary(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + 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 = this@CoreProjectFileIndex.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 + .mapNotNull { StandardFileSystems.local().findFileByPath(it.file.path) } + .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<VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getUrls(p0: OrderRootType): Array<String> { + throw UnsupportedOperationException() + } + + override fun <R : Any?> accept(p0: RootPolicy<R>, p1: R?): R { + throw UnsupportedOperationException() + } + + + override fun getPresentableName(): String { + throw UnsupportedOperationException() + } + + override fun getOwnerModule(): Module = module + + + 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 getFiles(p0: OrderRootType): Array<VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getUrls(p0: OrderRootType): Array<String> { + throw UnsupportedOperationException() + } + + override fun <R : Any?> accept(p0: RootPolicy<R>, p1: R?): R { + throw UnsupportedOperationException() + } + + override fun getJdkName(): String? { + throw UnsupportedOperationException() + } + + override fun getJdk(): Sdk = sdk + + override fun getPresentableName(): String { + throw UnsupportedOperationException() + } + + override fun getOwnerModule(): Module { + 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 getExternalSource(): ProjectModelExternalSource? { + throw UnsupportedOperationException() + } + + 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 = this@CoreProjectFileIndex.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) + 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 +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Analysis/JavaResolveExtension.kt b/core/src/main/kotlin/Analysis/JavaResolveExtension.kt new file mode 100644 index 000000000..4a4c78e56 --- /dev/null +++ b/core/src/main/kotlin/Analysis/JavaResolveExtension.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("JavaResolutionUtils") + +package org.jetbrains.dokka + +import com.intellij.psi.* +import org.jetbrains.kotlin.asJava.unwrapped +import org.jetbrains.kotlin.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.load.java.sources.JavaSourceElement +import org.jetbrains.kotlin.load.java.structure.* +import org.jetbrains.kotlin.load.java.structure.impl.* +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.resolve.jvm.JavaDescriptorResolver +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.MemberScope + +// TODO: Remove that file + +@JvmOverloads +fun PsiMethod.getJavaMethodDescriptor(resolutionFacade: ResolutionFacade = javaResolutionFacade()): DeclarationDescriptor? { + val method = originalElement as? PsiMethod ?: return null + if (method.containingClass == null || !Name.isValidIdentifier(method.name)) return null + val resolver = method.getJavaDescriptorResolver(resolutionFacade) + return when { + method.isConstructor -> resolver?.resolveConstructor(JavaConstructorImpl(method)) + else -> resolver?.resolveMethod(JavaMethodImpl(method)) + } +} + +@JvmOverloads +fun PsiClass.getJavaClassDescriptor(resolutionFacade: ResolutionFacade = javaResolutionFacade()): ClassDescriptor? { + val psiClass = originalElement as? PsiClass ?: return null + return psiClass.getJavaDescriptorResolver(resolutionFacade)?.resolveClass(JavaClassImpl(psiClass)) +} + +@JvmOverloads +fun PsiField.getJavaFieldDescriptor(resolutionFacade: ResolutionFacade = javaResolutionFacade()): PropertyDescriptor? { + val field = originalElement as? PsiField ?: return null + return field.getJavaDescriptorResolver(resolutionFacade)?.resolveField(JavaFieldImpl(field)) +} + +@JvmOverloads +fun PsiMember.getJavaMemberDescriptor(resolutionFacade: ResolutionFacade = javaResolutionFacade()): DeclarationDescriptor? { + return when (this) { + is PsiEnumConstant -> containingClass?.getJavaClassDescriptor(resolutionFacade) + is PsiClass -> getJavaClassDescriptor(resolutionFacade) + is PsiMethod -> getJavaMethodDescriptor(resolutionFacade) + is PsiField -> getJavaFieldDescriptor(resolutionFacade) + else -> null + } +} + +@JvmOverloads +fun PsiMember.getJavaOrKotlinMemberDescriptor(resolutionFacade: ResolutionFacade = javaResolutionFacade()): DeclarationDescriptor? { + val callable = unwrapped + return when (callable) { + is PsiMember -> getJavaMemberDescriptor(resolutionFacade) + is KtDeclaration -> { + val descriptor = resolutionFacade.resolveToDescriptor(callable) + if (descriptor is ClassDescriptor && this is PsiMethod) descriptor.unsubstitutedPrimaryConstructor else descriptor + } + else -> null + } +} + +private fun PsiElement.getJavaDescriptorResolver(resolutionFacade: ResolutionFacade): JavaDescriptorResolver? { + return resolutionFacade.tryGetFrontendService(this, JavaDescriptorResolver::class.java) +} + +private fun JavaDescriptorResolver.resolveMethod(method: JavaMethod): DeclarationDescriptor? { + return getContainingScope(method) + ?.getContributedDescriptors(nameFilter = { true }, kindFilter = DescriptorKindFilter.CALLABLES) + ?.filterIsInstance<DeclarationDescriptorWithSource>() + ?.findByJavaElement(method) +} + +private fun JavaDescriptorResolver.resolveConstructor(constructor: JavaConstructor): ConstructorDescriptor? { + return resolveClass(constructor.containingClass)?.constructors?.findByJavaElement(constructor) +} + +private fun JavaDescriptorResolver.resolveField(field: JavaField): PropertyDescriptor? { + return getContainingScope(field)?.getContributedVariables(field.name, NoLookupLocation.FROM_IDE)?.findByJavaElement(field) +} + +private fun JavaDescriptorResolver.getContainingScope(member: JavaMember): MemberScope? { + val containingClass = resolveClass(member.containingClass) + return if (member.isStatic) + containingClass?.staticScope + else + containingClass?.defaultType?.memberScope +} + +private fun <T : DeclarationDescriptorWithSource> Collection<T>.findByJavaElement(javaElement: JavaElement): T? { + return firstOrNull { member -> + val memberJavaElement = (member.original.source as? JavaSourceElement)?.javaElement + when { + memberJavaElement == javaElement -> + true + memberJavaElement is JavaElementImpl<*> && javaElement is JavaElementImpl<*> -> + memberJavaElement.psi.isEquivalentTo(javaElement.psi) + else -> + false + } + } +} + +fun PsiElement.javaResolutionFacade() = + KotlinCacheService.getInstance(project).getResolutionFacadeByFile(this.originalElement.containingFile, JvmPlatforms.defaultJvmPlatform)!! diff --git a/core/src/main/kotlin/DokkaBootstrapImpl.kt b/core/src/main/kotlin/DokkaBootstrapImpl.kt new file mode 100644 index 000000000..402018102 --- /dev/null +++ b/core/src/main/kotlin/DokkaBootstrapImpl.kt @@ -0,0 +1,83 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.DokkaConfiguration.PackageOptions +import ru.yole.jkid.deserialization.deserialize +import java.io.File +import java.util.function.BiConsumer + + +fun parsePerPackageOptions(arg: String): List<PackageOptions> { + if (arg.isBlank()) return emptyList() + + return arg.split(";").map { it.split(",") }.map { + val prefix = it.first() + if (prefix == "") + throw IllegalArgumentException("Please do not register packageOptions with all match pattern, use global settings instead") + val args = it.subList(1, it.size) + val deprecated = args.find { it.endsWith("deprecated") }?.startsWith("+") ?: true + val reportUndocumented = args.find { it.endsWith("warnUndocumented") }?.startsWith("+") ?: true + val privateApi = args.find { it.endsWith("privateApi") }?.startsWith("+") ?: false + val suppress = args.find { it.endsWith("suppress") }?.startsWith("+") ?: false + PackageOptionsImpl(prefix, includeNonPublic = privateApi, reportUndocumented = reportUndocumented, skipDeprecated = !deprecated, suppress = suppress) + } +} + +class DokkaBootstrapImpl : DokkaBootstrap { + + private class DokkaProxyLogger(val consumer: BiConsumer<String, String>) : DokkaLogger { + override fun info(message: String) { + consumer.accept("info", message) + } + + override fun warn(message: String) { + consumer.accept("warn", message) + } + + override fun error(message: String) { + consumer.accept("error", message) + } + } + + lateinit var generator: DokkaGenerator + + override fun configure(logger: BiConsumer<String, String>, serializedConfigurationJSON: String) + = configure(DokkaProxyLogger(logger), deserialize<DokkaConfigurationImpl>(serializedConfigurationJSON)) + + fun configure(logger: DokkaLogger, configuration: DokkaConfiguration) = with(configuration) { + generator = DokkaGenerator( + logger, + classpath, + sourceRoots, + samples, + includes, + moduleName, + DocumentationOptions( + outputDir = outputDir, + outputFormat = format, + includeNonPublic = includeNonPublic, + includeRootPackage = includeRootPackage, + reportUndocumented = reportUndocumented, + skipEmptyPackages = skipEmptyPackages, + skipDeprecated = skipDeprecated, + jdkVersion = jdkVersion, + generateClassIndexPage = generateClassIndexPage, + generatePackageIndexPage = generatePackageIndexPage, + sourceLinks = sourceLinks, + impliedPlatforms = impliedPlatforms, + perPackageOptions = perPackageOptions, + externalDocumentationLinks = externalDocumentationLinks, + noStdlibLink = noStdlibLink, + noJdkLink = noJdkLink, + languageVersion = languageVersion, + apiVersion = apiVersion, + cacheRoot = cacheRoot, + suppressedFiles = suppressedFiles.map { File(it) }.toSet(), + collectInheritedExtensionsFromLibraries = collectInheritedExtensionsFromLibraries, + outlineRoot = outlineRoot, + dacRoot = dacRoot + ) + ) + } + + override fun generate() = generator.generate() +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/AnalysisComponents.kt b/core/src/main/kotlin/Formats/AnalysisComponents.kt new file mode 100644 index 000000000..d78d4a0c1 --- /dev/null +++ b/core/src/main/kotlin/Formats/AnalysisComponents.kt @@ -0,0 +1,45 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Binder +import org.jetbrains.dokka.* +import org.jetbrains.dokka.KotlinAsJavaElementSignatureProvider +import org.jetbrains.dokka.KotlinElementSignatureProvider +import org.jetbrains.dokka.ElementSignatureProvider +import org.jetbrains.dokka.Samples.DefaultSampleProcessingService +import org.jetbrains.dokka.Samples.SampleProcessingService +import org.jetbrains.dokka.Utilities.bind +import org.jetbrains.dokka.Utilities.toType +import kotlin.reflect.KClass + + +interface DefaultAnalysisComponentServices { + val packageDocumentationBuilderClass: KClass<out PackageDocumentationBuilder> + val javaDocumentationBuilderClass: KClass<out JavaDocumentationBuilder> + val sampleProcessingService: KClass<out SampleProcessingService> + val elementSignatureProvider: KClass<out ElementSignatureProvider> +} + +interface DefaultAnalysisComponent : FormatDescriptorAnalysisComponent, DefaultAnalysisComponentServices { + override fun configureAnalysis(binder: Binder): Unit = with(binder) { + bind<ElementSignatureProvider>() toType elementSignatureProvider + bind<PackageDocumentationBuilder>() toType packageDocumentationBuilderClass + bind<JavaDocumentationBuilder>() toType javaDocumentationBuilderClass + bind<SampleProcessingService>() toType sampleProcessingService + } +} + + +object KotlinAsJava : DefaultAnalysisComponentServices { + override val packageDocumentationBuilderClass = KotlinAsJavaDocumentationBuilder::class + override val javaDocumentationBuilderClass = JavaPsiDocumentationBuilder::class + override val sampleProcessingService = DefaultSampleProcessingService::class + override val elementSignatureProvider = KotlinAsJavaElementSignatureProvider::class +} + + +object KotlinAsKotlin : DefaultAnalysisComponentServices { + override val packageDocumentationBuilderClass = KotlinPackageDocumentationBuilder::class + override val javaDocumentationBuilderClass = KotlinJavaDocumentationBuilder::class + override val sampleProcessingService = DefaultSampleProcessingService::class + override val elementSignatureProvider = KotlinElementSignatureProvider::class +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/DacHtmlFormat.kt b/core/src/main/kotlin/Formats/DacHtmlFormat.kt new file mode 100644 index 000000000..e2399435b --- /dev/null +++ b/core/src/main/kotlin/Formats/DacHtmlFormat.kt @@ -0,0 +1,949 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Inject +import com.google.inject.name.Named +import kotlinx.html.* +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Samples.DevsiteSampleProcessingService +import org.jetbrains.dokka.Kotlin.ParameterInfoNode +import org.jetbrains.dokka.Utilities.firstSentence +import java.lang.Math.max +import java.net.URI +import kotlin.reflect.KClass + +/** + * On Devsite, certain headers and footers are needed for generating Devsite metadata. + */ +class DevsiteHtmlTemplateService @Inject constructor( + @Named("outlineRoot") val outlineRoot: String, + @Named("dacRoot") val dacRoot: String +) : JavaLayoutHtmlTemplateService { + override fun composePage(page: JavaLayoutHtmlFormatOutputBuilder.Page, tagConsumer: TagConsumer<Appendable>, headContent: HEAD.() -> Unit, bodyContent: BODY.() -> Unit) { + tagConsumer.html { + attributes["devsite"] = "true" + head { + headContent() + title { + +when (page) { + is JavaLayoutHtmlFormatOutputBuilder.Page.ClassIndex -> "Class Index" + is JavaLayoutHtmlFormatOutputBuilder.Page.ClassPage -> page.node.nameWithOuterClass() + is JavaLayoutHtmlFormatOutputBuilder.Page.PackageIndex -> "Package Index" + is JavaLayoutHtmlFormatOutputBuilder.Page.PackagePage -> page.node.nameWithOuterClass() + } + } + unsafe { +"{% setvar book_path %}${dacRoot}/${outlineRoot}_book.yaml{% endsetvar %}\n{% include \"_shared/_reference-head-tags.html\" %}\n" } + } + body { + bodyContent() + } + } + } +} + +class DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl @javax.inject.Inject constructor( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val templateService: JavaLayoutHtmlTemplateService, + val logger: DokkaLogger +) : JavaLayoutHtmlFormatOutputBuilderFactory { + override fun createOutputBuilder(output: Appendable, node: DocumentationNode): JavaLayoutHtmlFormatOutputBuilder { + return createOutputBuilder(output, uriProvider.mainUri(node)) + } + + override fun createOutputBuilder(output: Appendable, uri: URI): JavaLayoutHtmlFormatOutputBuilder { + return DevsiteLayoutHtmlFormatOutputBuilder(output, languageService, uriProvider, templateService, logger, uri) + } +} + +class DevsiteLayoutHtmlFormatOutputBuilder( + output: Appendable, + languageService: LanguageService, + uriProvider: JavaLayoutHtmlUriProvider, + templateService: JavaLayoutHtmlTemplateService, + logger: DokkaLogger, + uri: URI +) : JavaLayoutHtmlFormatOutputBuilder(output, languageService, uriProvider, templateService, logger, uri) { + override fun FlowContent.fullMemberDocs(node: DocumentationNode) { + fullMemberDocs(node, node) + } + + override fun FlowContent.fullMemberDocs(node: DocumentationNode, uriNode: DocumentationNode) { + a { + attributes["name"] = uriNode.signatureForAnchor(logger).anchorEncoded() + } + div(classes = "api apilevel-${node.apiLevel.name}") { + attributes["data-version-added"] = node.apiLevel.name + h3(classes = "api-name") { + //id = node.signatureForAnchor(logger).urlEncoded() + +node.prettyName + } + apiAndDeprecatedVersions(node) + pre(classes = "api-signature no-pretty-print") { renderedSignature(node, LanguageService.RenderMode.FULL) } + deprecationWarningToMarkup(node, prefix = true) + nodeContent(node, uriNode) + node.constantValue()?.let { value -> + pre { + +"Value: " + code { +value } + } + } + for ((name, sections) in node.content.sections.groupBy { it.tag }) { + when (name) { + ContentTags.Return -> { + table(classes = "responsive") { + tbody { + tr { + th { + colSpan = "2" + +name + } + } + sections.forEach { + tr { + if (it.children.size > 0) { + td { + val firstChild = it.children.first() + if (firstChild is ContentBlock && + firstChild.children.size == 3 && + firstChild.children[0] is NodeRenderContent && + firstChild.children[1] is ContentSymbol && + firstChild.children[2] is ContentText) { + // it.children is expected to have two items + // First should have 3 children of its own: + // - NodeRenderContent is the return type + // - ContentSymbol - ":" + // - ContentText - " " + // We want to only use NodeRenderContent in a separate <td> and + // <code> to get proper formatting in DAC. + code { + metaMarkup(listOf(firstChild.children[0])) + } + } else { + metaMarkup(listOf(firstChild)) + } + } + td { + if (it.children.size > 1) { + metaMarkup(it.children.subList(1, it.children.size)) + } + } + } + } + } + } + } + } + ContentTags.Parameters -> { + table(classes = "responsive") { + tbody { + tr { + th { + colSpan = "2" + +name + } + } + sections.forEach { section -> + tr { + td { + val parameterInfoNode = section.children.find { it is ParameterInfoNode } as? ParameterInfoNode + // If there is no info found, just display the parameter + // name. + if (parameterInfoNode?.parameterContent == null) { + code { + section.subjectName?.let { +it } + } + } else { + // Add already marked up type information here + metaMarkup( + listOf(parameterInfoNode.parameterContent!!) + ) + } + } + td { + metaMarkup(section.children) + } + } + } + } + } + } + ContentTags.SeeAlso -> { + div { + p { + b { + +name + } + } + ul(classes = "nolist") { + sections.filter {it.tag == "See Also"}.forEach { + it.children.forEach { child -> + if (child is ContentNodeLazyLink || child is ContentExternalLink) { + li { + code { + contentNodeToMarkup(child) // Wrap bare links in listItems. + } // bare links come from the java-to-kotlin parser. + } + } + else if (child is ContentUnorderedList) { + metaMarkup(child.children) // Already wrapped in listItems. + } // this is how we want things to look. No parser currently does this (yet). + else if (child is ContentParagraph) { + li{ + code { + metaMarkup (child.children) // Replace paragraphs with listItems. + } // paragraph-wrapped links come from the kotlin parser + } + } // NOTE: currently the java-to-java parser does not add See Also links! + } + } + } + } + } + ContentTags.Exceptions -> { + table(classes = "responsive") { + tbody { + tr { + th { + colSpan = "2" + +name + } + } + sections.forEach { + tr { + td { + code { + it.subjectName?.let { +it } + } + } + td { + metaMarkup(it.children) + } + } + } + } + } + } + } + } + } + } + + override fun summary(node: DocumentationNode) = node.firstSentenceOfSummary() + + fun TBODY.xmlAttributeRow(attr: DocumentationNode) = tr { + td { + a(href = attr) { + code { + +attr.attributeRef!!.name + } + } + } + td { + +attr.attributeRef!!.firstSentence() + } + } + + protected fun FlowContent.fullAttributeDocs( + attributes: List<DocumentationNode>, + header: String + ) { + if (attributes.none()) return + h2 { + +header + } + attributes.forEach { + fullMemberDocs(it.attributeRef!!, it) + } + } + + override fun FlowContent.classLikeFullMemberDocs(page: Page.ClassPage) = with(page) { + fullAttributeDocs(attributes, "XML attributes") + fullMemberDocs(enumValues, "Enum values") + fullMemberDocs(constants, "Constants") + + constructors.forEach { (visibility, group) -> + fullMemberDocs(group, "${visibility.capitalize()} constructors") + } + + functions.forEach { (visibility, group) -> + fullMemberDocs(group, "${visibility.capitalize()} methods") + } + + fullMemberDocs(properties, "Properties") + + fields.forEach { (visibility, group) -> + fullMemberDocs(group, "${visibility.capitalize()} fields") + } + if (!hasMeaningfulCompanion) { + fullMemberDocs(companionFunctions, "Companion functions") + fullMemberDocs(companionProperties, "Companion properties") + } + } + + override fun FlowContent.classLikeSummaries(page: Page.ClassPage) = with(page) { + this@classLikeSummaries.summaryNodeGroup( + nestedClasses, + header = "Nested classes", + summaryId = "nestedclasses", + tableClass = "responsive", + headerAsRow = true + ) { + nestedClassSummaryRow(it) + } + + this@classLikeSummaries.summaryNodeGroup( + attributes, + header="XML attributes", + summaryId="lattrs", + tableClass = "responsive", + headerAsRow = true + ) { + xmlAttributeRow(it) + } + + this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( + superClasses = inheritedAttributes.entries, + header="Inherited XML attributes", + tableId="inhattrs", + tableClass = "responsive", + row = { inheritedXmlAttributeRow(it)} + ) + + this@classLikeSummaries.summaryNodeGroup( + constants, + header = "Constants", + summaryId = "constants", + tableClass = "responsive", + headerAsRow = true + ) { propertyLikeSummaryRow(it) } + + this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( + superClasses = inheritedConstants.entries, + header = "Inherited constants", + tableId = "inhconstants", + tableClass = "responsive constants inhtable", + row = { inheritedMemberRow(it) } + ) + + constructors.forEach { (visibility, group) -> + this@classLikeSummaries.summaryNodeGroup( + group, + header = "${visibility.capitalize()} constructors", + summaryId = "${visibility.take(3)}ctors", + tableClass = "responsive", + headerAsRow = true + ) { + functionLikeSummaryRow(it) + } + } + + this@classLikeSummaries.summaryNodeGroup( + enumValues, + header = "Enum values", + summaryId = "enumvalues", + tableClass = "responsive", + headerAsRow = true + ) { + propertyLikeSummaryRow(it, showSignature = false) + } + + functions.forEach { (visibility, group) -> + this@classLikeSummaries.summaryNodeGroup( + group, + header = "${visibility.capitalize()} methods", + summaryId = "${visibility.take(3)}methods", + tableClass = "responsive", + headerAsRow = true + ) { + functionLikeSummaryRow(it) + } + } + + this@classLikeSummaries.summaryNodeGroup( + companionFunctions, + header = "Companion functions", + summaryId = "compmethods", + tableClass = "responsive", + headerAsRow = true + ) { + functionLikeSummaryRow(it) + } + + this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( + superClasses = inheritedFunctionsByReceiver.entries, + header = "Inherited functions", + tableId = "inhmethods", + tableClass = "responsive", + row = { inheritedMemberRow(it) } + ) + + this@classLikeSummaries.summaryNodeGroup( + extensionFunctions.entries, + header = "Extension functions", + summaryId = "extmethods", + tableClass = "responsive", + headerAsRow = true + ) { + extensionRow(it) { + functionLikeSummaryRow(it) + } + } + this@classLikeSummaries.summaryNodeGroup( + inheritedExtensionFunctions.entries, + header = "Inherited extension functions", + summaryId = "inhextmethods", + tableClass = "responsive", + headerAsRow = true + ) { + extensionRow(it) { + functionLikeSummaryRow(it) + } + } + + fields.forEach { (visibility, group) -> + this@classLikeSummaries.summaryNodeGroup( + group, + header = "${visibility.capitalize()} fields", + summaryId = "${visibility.take(3)}fields", + tableClass = "responsive", + headerAsRow = true + ) { propertyLikeSummaryRow(it) } + } + + this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( + superClasses = inheritedFieldsByReceiver.entries, + header = "Inherited fields", + tableId = "inhfields", + tableClass = "responsive properties inhtable", + row = { inheritedMemberRow(it) } + ) + + this@classLikeSummaries.summaryNodeGroup( + properties, + header = "Properties", + summaryId = "properties", + tableClass = "responsive", + headerAsRow = true + ) { propertyLikeSummaryRow(it) } + + + this@classLikeSummaries.summaryNodeGroup( + companionProperties, + "Companion properties", + headerAsRow = true + ) { + propertyLikeSummaryRow(it) + } + + this@classLikeSummaries.expandableSummaryNodeGroupForInheritedMembers( + superClasses = inheritedPropertiesByReceiver.entries, + header = "Inherited properties", + tableId = "inhfields", + tableClass = "responsive properties inhtable", + row = { inheritedMemberRow(it) } + ) + + this@classLikeSummaries.summaryNodeGroup( + extensionProperties.entries, + "Extension properties", + headerAsRow = true + ) { + extensionRow(it) { + propertyLikeSummaryRow(it) + } + } + + this@classLikeSummaries.summaryNodeGroup( + inheritedExtensionProperties.entries, + "Inherited extension properties", + headerAsRow = true + ) { + extensionRow(it) { + propertyLikeSummaryRow(it) + } + } + } + + fun <T> FlowContent.summaryNodeGroup( + nodes: Iterable<T>, + header: String, + headerAsRow: Boolean, + summaryId: String, + tableClass: String = "responsive", + row: TBODY.(T) -> Unit + ) { + if (nodes.none()) return + if (!headerAsRow) { + h2 { +header } + } + table(classes = tableClass) { + id = summaryId + tbody { + if (headerAsRow) { + developerHeading(header) + } + nodes.forEach { node -> + row(node) + } + } + } + } + + override fun FlowContent.contentBlockCode(content: ContentBlockCode) { + pre { + attributes["class"] = "prettyprint" + contentNodesToMarkup(content.children) + } + } + + override fun FlowContent.contentBlockSampleCode(content: ContentBlockSampleCode) { + pre { + attributes["class"] = "prettyprint" + contentNodesToMarkup(content.importsBlock.children) + +"\n\n" + contentNodesToMarkup(content.children) + } + } + + override fun generatePackage(page: Page.PackagePage) = templateService.composePage( + page, + htmlConsumer, + headContent = { + + }, + bodyContent = { + h1 { +page.node.name } + nodeContent(page.node) + this@composePage.summaryNodeGroup(page.interfaces.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Interfaces", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.classes.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Classes", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.exceptions.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Exceptions", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.typeAliases.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Type-aliases", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.annotations.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Annotations", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.enums.sortedBy { it.nameWithOuterClass().toLowerCase() }, "Enums", headerAsRow = false) { classLikeRow(it) } + + this@composePage.summaryNodeGroup( + page.constants.sortedBy { it.name }, + "Top-level constants summary", + headerAsRow = false + ) { + propertyLikeSummaryRow(it) + } + + this@composePage.summaryNodeGroup( + page.functions.sortedBy { it.name }, + "Top-level functions summary", + headerAsRow = false + ) { + functionLikeSummaryRow(it) + } + + this@composePage.summaryNodeGroup( + page.properties.sortedBy { it.name }, + "Top-level properties summary", + headerAsRow = false + ) { + propertyLikeSummaryRow(it) + } + + summaryNodeGroupForExtensions("Extension functions summary", page.extensionFunctions.entries) + summaryNodeGroupForExtensions("Extension properties summary", page.extensionProperties.entries) + + fullMemberDocs(page.constants.sortedBy { it.name }, "Top-level constants") + fullMemberDocs(page.functions.sortedBy { it.name }, "Top-level functions") + fullMemberDocs(page.properties.sortedBy { it.name }, "Top-level properties") + fullMemberDocs(page.extensionFunctions.values.flatten().sortedBy { it.name }, "Extension functions") + fullMemberDocs(page.extensionProperties.values.flatten().sortedBy { it.name }, "Extension properties") + } + ) + + private fun TBODY.inheritedXmlAttributeRow(inheritedMember: DocumentationNode) { + tr(classes = "api apilevel-${inheritedMember.attributeRef!!.apiLevel.name}") { + attributes["data-version-added"] = "${inheritedMember.apiLevel}" + td { + code { + a(href = inheritedMember) { +inheritedMember.attributeRef!!.name } + } + } + td { + attributes["width"] = "100%" + p { + nodeContent(inheritedMember.attributeRef!!, inheritedMember) + } + } + } + } + + private fun TBODY.inheritedMemberRow(inheritedMember: DocumentationNode) { + tr(classes = "api apilevel-${inheritedMember.apiLevel.name}") { + attributes["data-version-added"] = "${inheritedMember.apiLevel}" + val type = inheritedMember.detailOrNull(NodeKind.Type) + td { + code { + type?.let { + renderedSignature(it, LanguageService.RenderMode.SUMMARY) + } + } + } + td { + attributes["width"] = "100%" + code { + a(href = inheritedMember) { +inheritedMember.name } + if (inheritedMember.kind == NodeKind.Function) { + shortFunctionParametersList(inheritedMember) + } + } + p { + nodeContent(inheritedMember) + } + } + } + } + + private fun FlowContent.expandableSummaryNodeGroupForInheritedMembers( + tableId: String, + header: String, + tableClass: String, + superClasses: Set<Map.Entry<DocumentationNode, List<DocumentationNode>>>, + row: TBODY.(inheritedMember: DocumentationNode) -> Unit + ) { + if (superClasses.none()) return + table(classes = tableClass) { + attributes["id"] = tableId + tbody { + developerHeading(header) + superClasses.forEach { (superClass, members) -> + tr(classes = "api apilevel-${superClass.apiLevel.name}") { + td { + attributes["colSpan"] = "2" + div(classes = "expandable jd-inherited-apis") { + span(classes = "expand-control exw-expanded") { + +"From class " + code { + a(href = superClass) { +superClass.name } + } + } + table(classes = "responsive exw-expanded-content") { + tbody { + members.forEach { inheritedMember -> + row(inheritedMember) + } + } + } + } + } + } + } + } + } + } + + private fun FlowContent.summaryNodeGroupForExtensions( + header: String, + receivers: Set<Map.Entry<DocumentationNode, List<DocumentationNode>>> + ) { + if (receivers.none()) return + h2 { +header } + div { + receivers.forEach { + table { + tr { + td { + attributes["colSpan"] = "2" + +"For " + a(href = it.key) { +it.key.name } + } + } + it.value.forEach { node -> + tr { + if (node.kind != NodeKind.Constructor) { + td { + modifiers(node) + renderedSignature(node.detail(NodeKind.Type), LanguageService.RenderMode.SUMMARY) + } + } + td { + div { + code { + val receiver = node.detailOrNull(NodeKind.Receiver) + if (receiver != null) { + renderedSignature(receiver.detail(NodeKind.Type), LanguageService.RenderMode.SUMMARY) + +"." + } + a(href = node) { +node.name } + shortFunctionParametersList(node) + } + } + + nodeSummary(node) + } + } + } + } + } + } + } + + + override fun generatePackageIndex(page: Page.PackageIndex) = templateService.composePage( + page, + htmlConsumer, + headContent = { + + }, + bodyContent = { + h1 { +"Package Index" } + table { + tbody { + for (node in page.packages) { + tr { + td { + a(href = uriProvider.linkTo(node, uri)) { +node.name } + } + } + } + } + } + } + ) + + override fun generateClassIndex(page: Page.ClassIndex) = templateService.composePage( + page, + htmlConsumer, + headContent = { + + }, + bodyContent = { + h1 { +"Class Index" } + + p { + +"These are all the API classes. See all " + a(href="packages.html") { + +"API packages." + } + } + + div(classes = "jd-letterlist") { + page.classesByFirstLetter.forEach { (letter) -> + +"\n " + a(href = "#letter_$letter") { +letter } + unsafe { + raw(" ") + } + } + +"\n " + } + + page.classesByFirstLetter.forEach { (letter, classes) -> + h2 { + id = "letter_$letter" + +letter + } + table { + tbody { + for (node in classes) { + tr { + td { + a(href = uriProvider.linkTo(node, uri)) { +node.classNodeNameWithOuterClass() } + } + td { + if (!deprecatedIndexSummary(node)) { + nodeSummary(node) + } + } + } + } + } + } + } + } + ) + + override fun FlowContent.classHierarchy(superclasses: List<DocumentationNode>) { + table(classes = "jd-inheritance-table") { + var level = superclasses.size + superclasses.forEach { + tr { + var spaceColumns = max(superclasses.size - 1 - level, 0) + while (spaceColumns > 0) { + td(classes = "jd-inheritance-space") { + +" " + } + spaceColumns-- + } + if (it != superclasses.first()) { + td(classes = "jd-inheritance-space") { + +"   ↳" + } + } + td(classes = "jd-inheritance-class-cell") { + attributes["colSpan"] = "$level" + qualifiedTypeReference(it) + } + } + level-- + } + } + } + + override fun FlowContent.subclasses(inheritors: List<DocumentationNode>, direct: Boolean) { + if (inheritors.isEmpty()) return + + // The number of subclasses in collapsed view before truncating and adding a "and xx others". + // See https://developer.android.com/reference/android/view/View for an example. + val numToShow = 12 + + table(classes = "jd-sumtable jd-sumtable-subclasses") { + tbody { + tr { + td { + div(classes = "expandable") { + span(classes = "expand-control") { + if (direct) + +"Known Direct Subclasses" + else + +"Known Indirect Subclasses" + } + div(classes = "showalways") { + attributes["id"] = if (direct) "subclasses-direct" else "subclasses-indirect" + + inheritors.take(numToShow).forEach { inheritor -> + a(href = inheritor) { +inheritor.classNodeNameWithOuterClass() } + if (inheritor != inheritors.last()) +", " + } + + if (inheritors.size > numToShow) { + +"and ${inheritors.size - numToShow} others." + } + } + div(classes = "exw-expanded-content") { + attributes["id"] = if (direct) "subclasses-direct-summary" else "subclasses-indirect-summary" + table(classes = "jd-sumtable-expando") { + inheritors.forEach { inheritor -> + tr(classes = "api api-level-${inheritor.apiLevel.name}") { + attributes["data-version-added"] = inheritor.apiLevel.name + td(classes = "jd-linkcol") { + a(href = inheritor) { +inheritor.classNodeNameWithOuterClass() } + } + td(classes = "jd-descrcol") { + attributes["width"] = "100%" + nodeSummary(inheritor) + } + } + } + } + } + } + } + } + } + } + } + + + fun DocumentationNode.firstSentenceOfSummary(): ContentNode { + + fun Sequence<ContentNode>.flatten(): Sequence<ContentNode> { + return flatMap { + when (it) { + is ContentParagraph -> it.children.asSequence().flatten() + else -> sequenceOf(it) + } + } + } + + fun ContentNode.firstSentence(): ContentText? = when(this) { + is ContentText -> ContentText(text.firstSentence()) + else -> null + } + + val elements = sequenceOf(summary).flatten() + fun containsDot(it: ContentNode) = (it as? ContentText)?.text?.contains(".") == true + + val paragraph = ContentParagraph() + (elements.takeWhile { !containsDot(it) } + elements.firstOrNull { containsDot(it) }?.firstSentence()).forEach { + if (it != null) { + paragraph.append(it) + } + } + if (paragraph.isEmpty()) { + return ContentEmpty + } + + return paragraph + } + + fun DocumentationNode.firstSentence(): String { + val sb = StringBuilder() + addContentNodeToStringBuilder(content, sb) + return sb.toString().firstSentence() + } + + private fun addContentNodesToStringBuilder(content: List<ContentNode>, sb: StringBuilder): Unit = + content.forEach { addContentNodeToStringBuilder(it, sb) } + + private fun addContentNodeToStringBuilder(content: ContentNode, sb: StringBuilder) { + when (content) { + is ContentText -> sb.appendWith(content.text) + is ContentSymbol -> sb.appendWith(content.text) + is ContentKeyword -> sb.appendWith(content.text) + is ContentIdentifier -> sb.appendWith(content.text) + is ContentEntity -> sb.appendWith(content.text) + + is ContentHeading -> addContentNodesToStringBuilder(content.children, sb) + is ContentStrong -> addContentNodesToStringBuilder(content.children, sb) + is ContentStrikethrough -> addContentNodesToStringBuilder(content.children, sb) + is ContentEmphasis -> addContentNodesToStringBuilder(content.children, sb) + is ContentOrderedList -> addContentNodesToStringBuilder(content.children, sb) + is ContentUnorderedList -> addContentNodesToStringBuilder(content.children, sb) + is ContentListItem -> addContentNodesToStringBuilder(content.children, sb) + is ContentCode -> addContentNodesToStringBuilder(content.children, sb) + is ContentBlockSampleCode -> addContentNodesToStringBuilder(content.children, sb) + is ContentBlockCode -> addContentNodesToStringBuilder(content.children, sb) + is ContentParagraph -> addContentNodesToStringBuilder(content.children, sb) + is ContentNodeLink -> addContentNodesToStringBuilder(content.children, sb) + is ContentBookmark -> addContentNodesToStringBuilder(content.children, sb) + is ContentExternalLink -> addContentNodesToStringBuilder(content.children, sb) + is ContentLocalLink -> addContentNodesToStringBuilder(content.children, sb) + is ContentSection -> { } + is ContentBlock -> addContentNodesToStringBuilder(content.children, sb) + } + } + + private fun StringBuilder.appendWith(text: String, delimiter: String = " ") { + if (this.length == 0) { + append(text) + } else { + append(delimiter) + append(text) + } + } +} + +fun TBODY.developerHeading(header: String) { + tr { + th { + attributes["colSpan"] = "2" + +header + } + } +} + +class DacFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin { + override val templateServiceClass: KClass<out JavaLayoutHtmlTemplateService> = DevsiteHtmlTemplateService::class + + override val outlineFactoryClass = DacOutlineFormatter::class + override val languageServiceClass = KotlinLanguageService::class + override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class + override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl::class + override val sampleProcessingService = DevsiteSampleProcessingService::class +} + + +class DacAsJavaFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava { + override val templateServiceClass: KClass<out JavaLayoutHtmlTemplateService> = DevsiteHtmlTemplateService::class + + override val outlineFactoryClass = DacOutlineFormatter::class + override val languageServiceClass = NewJavaLanguageService::class + override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class + override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl::class +} diff --git a/core/src/main/kotlin/Formats/DacOutlineService.kt b/core/src/main/kotlin/Formats/DacOutlineService.kt new file mode 100644 index 000000000..e249c39f7 --- /dev/null +++ b/core/src/main/kotlin/Formats/DacOutlineService.kt @@ -0,0 +1,395 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Inject +import org.jetbrains.dokka.* +import java.net.URI +import com.google.inject.name.Named +import org.jetbrains.kotlin.cfg.pseudocode.AllTypes + + +interface DacOutlineFormatService { + fun computeOutlineURI(node: DocumentationNode): URI + fun format(to: Appendable, node: DocumentationNode) +} + +class DacOutlineFormatter @Inject constructor( + uriProvider: JavaLayoutHtmlUriProvider, + languageService: LanguageService, + @Named("dacRoot") dacRoot: String, + @Named("generateClassIndex") generateClassIndex: Boolean, + @Named("generatePackageIndex") generatePackageIndex: Boolean +) : JavaLayoutHtmlFormatOutlineFactoryService { + val tocOutline = TocOutlineService(uriProvider, languageService, dacRoot, generateClassIndex, generatePackageIndex) + val outlines = listOf(tocOutline) + + override fun generateOutlines(outputProvider: (URI) -> Appendable, nodes: Iterable<DocumentationNode>) { + for (node in nodes) { + for (outline in outlines) { + val uri = outline.computeOutlineURI(node) + val output = outputProvider(uri) + outline.format(output, node) + } + } + } +} + +/** + * Outline service for generating a _toc.yaml file, responsible for pointing to the paths of each + * index.html file in the doc tree. + */ +class BookOutlineService( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val dacRoot: String, + val generateClassIndex: Boolean, + val generatePackageIndex: Boolean +) : DacOutlineFormatService { + override fun computeOutlineURI(node: DocumentationNode): URI = uriProvider.outlineRootUri(node).resolve("_book.yaml") + + override fun format(to: Appendable, node: DocumentationNode) { + appendOutline(to, listOf(node)) + } + + var outlineLevel = 0 + + /** Appends formatted outline to [StringBuilder](to) using specified [location] */ + fun appendOutline(to: Appendable, nodes: Iterable<DocumentationNode>) { + if (outlineLevel == 0) to.appendln("reference:") + for (node in nodes) { + appendOutlineHeader(node, to) + val subPackages = node.members.filter { + it.kind == NodeKind.Package + } + if (subPackages.any()) { + val sortedMembers = subPackages.sortedBy { it.name.toLowerCase() } + appendOutlineLevel(to) { + appendOutline(to, sortedMembers) + } + } + + } + } + + fun appendOutlineHeader(node: DocumentationNode, to: Appendable) { + if (node is DocumentationModule) { + to.appendln("- title: Package Index") + to.appendln(" path: $dacRoot${uriProvider.outlineRootUri(node).resolve("packages.html")}") + to.appendln(" status_text: no-toggle") + } else { + to.appendln("- title: ${languageService.renderName(node)}") + to.appendln(" path: $dacRoot${uriProvider.mainUriOrWarn(node)}") + to.appendln(" status_text: no-toggle") + } + } + + fun appendOutlineLevel(to: Appendable, body: () -> Unit) { + outlineLevel++ + body() + outlineLevel-- + } +} + +/** + * Outline service for generating a _toc.yaml file, responsible for pointing to the paths of each + * index.html file in the doc tree. + */ +class TocOutlineService( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val dacRoot: String, + val generateClassIndex: Boolean, + val generatePackageIndex: Boolean +) : DacOutlineFormatService { + override fun computeOutlineURI(node: DocumentationNode): URI = uriProvider.outlineRootUri(node).resolve("_toc.yaml") + + override fun format(to: Appendable, node: DocumentationNode) { + appendOutline(to, listOf(node)) + } + + var outlineLevel = 0 + + /** Appends formatted outline to [StringBuilder](to) using specified [location] */ + fun appendOutline(to: Appendable, nodes: Iterable<DocumentationNode>) { + if (outlineLevel == 0) to.appendln("toc:") + for (node in nodes) { + appendOutlineHeader(node, to) + val subPackages = node.members.filter { + it.kind == NodeKind.Package + } + if (subPackages.any()) { + val sortedMembers = subPackages.sortedBy { it.nameWithOuterClass() } + appendOutlineLevel { + appendOutline(to, sortedMembers) + } + } + } + } + + fun appendOutlineHeader(node: DocumentationNode, to: Appendable) { + if (node is DocumentationModule) { + if (generateClassIndex) { + node.members.filter { it.kind == NodeKind.AllTypes }.firstOrNull()?.let { + to.appendln("- title: Class Index") + to.appendln(" path: $dacRoot${uriProvider.outlineRootUri(it).resolve("classes.html")}") + to.appendln() + } + } + if (generatePackageIndex) { + to.appendln("- title: Package Index") + to.appendln(" path: $dacRoot${uriProvider.outlineRootUri(node).resolve("packages.html")}") + to.appendln() + } + } else if (node.kind != NodeKind.AllTypes && !(node is DocumentationModule)) { + to.appendln("- title: ${languageService.renderName(node)}") + to.appendln(" path: $dacRoot${uriProvider.mainUriOrWarn(node)}") + to.appendln() + var addedSectionHeader = false + for (kind in NodeKind.classLike) { + val members = node.getMembersOfKinds(kind) + if (members.isNotEmpty()) { + if (!addedSectionHeader) { + to.appendln(" section:") + addedSectionHeader = true + } + to.appendln(" - title: ${kind.pluralizedName()}") + to.appendln() + to.appendln(" section:") + members.sortedBy { it.nameWithOuterClass().toLowerCase() }.forEach { member -> + to.appendln(" - title: ${languageService.renderNameWithOuterClass(member)}") + to.appendln(" path: $dacRoot${uriProvider.mainUriOrWarn(member)}".trimEnd('#')) + to.appendln() + } + } + } + to.appendln().appendln() + } + } + + fun appendOutlineLevel(body: () -> Unit) { + outlineLevel++ + body() + outlineLevel-- + } +} + +class DacNavOutlineService constructor( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val dacRoot: String +) : DacOutlineFormatService { + override fun computeOutlineURI(node: DocumentationNode): URI = + uriProvider.outlineRootUri(node).resolve("navtree_data.js") + + override fun format(to: Appendable, node: DocumentationNode) { + to.append("var NAVTREE_DATA = ").appendNavTree(node.members).append(";") + } + + private fun Appendable.appendNavTree(nodes: Iterable<DocumentationNode>): Appendable { + append("[ ") + var first = true + for (node in nodes) { + if (!first) append(", ") + first = false + val interfaces = node.getMembersOfKinds(NodeKind.Interface) + val classes = node.getMembersOfKinds(NodeKind.Class) + val objects = node.getMembersOfKinds(NodeKind.Object) + val annotations = node.getMembersOfKinds(NodeKind.AnnotationClass) + val enums = node.getMembersOfKinds(NodeKind.Enum) + val exceptions = node.getMembersOfKinds(NodeKind.Exception) + + append("[ \"${node.name}\", \"$dacRoot${uriProvider.tryGetMainUri(node)}\", [ ") + var needComma = false + if (interfaces.firstOrNull() != null) { + appendNavTreePagesOfKind("Interfaces", interfaces) + needComma = true + } + if (classes.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Classes", classes) + needComma = true + } + if (objects.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Objects", objects) + } + if (annotations.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Annotations", annotations) + needComma = true + } + if (enums.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Enums", enums) + needComma = true + } + if (exceptions.firstOrNull() != null) { + if (needComma) append(", ") + appendNavTreePagesOfKind("Exceptions", exceptions) + } + append(" ] ]") + } + append(" ]") + return this + } + + private fun Appendable.appendNavTreePagesOfKind(kindTitle: String, + nodesOfKind: Iterable<DocumentationNode>): Appendable { + append("[ \"$kindTitle\", null, [ ") + var started = false + for (node in nodesOfKind) { + if (started) append(", ") + started = true + appendNavTreeChild(node) + } + append(" ], null, null ]") + return this + } + + private fun Appendable.appendNavTreeChild(node: DocumentationNode): Appendable { + append("[ \"${node.nameWithOuterClass()}\", \"${dacRoot}${uriProvider.tryGetMainUri(node)}\"") + append(", null, null, null ]") + return this + } +} + +class DacSearchOutlineService( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val dacRoot: String +) : DacOutlineFormatService { + + override fun computeOutlineURI(node: DocumentationNode): URI = + uriProvider.outlineRootUri(node).resolve("lists.js") + + override fun format(to: Appendable, node: DocumentationNode) { + val pageNodes = node.getAllPageNodes() + var id = 0 + to.append("var KTX_CORE_DATA = [\n") + var first = true + for (pageNode in pageNodes) { + if (pageNode.kind == NodeKind.Module) continue + if (!first) to.append(", \n") + first = false + to.append(" { " + + "id:$id, " + + "label:\"${pageNode.qualifiedName()}\", " + + "link:\"${dacRoot}${uriProvider.tryGetMainUri(pageNode)}\", " + + "type:\"${pageNode.getClassOrPackage()}\", " + + "deprecated:\"false\" }") + id++ + } + to.append("\n];") + } + + private fun DocumentationNode.getClassOrPackage(): String = + if (hasOwnPage()) + "class" + else if (isPackage()) { + "package" + } else { + "" + } + + private fun DocumentationNode.getAllPageNodes(): Iterable<DocumentationNode> { + val allPageNodes = mutableListOf<DocumentationNode>() + recursiveSetAllPageNodes(allPageNodes) + return allPageNodes + } + + private fun DocumentationNode.recursiveSetAllPageNodes( + allPageNodes: MutableList<DocumentationNode>) { + for (child in members) { + if (child.hasOwnPage() || child.isPackage()) { + allPageNodes.add(child) + child.qualifiedName() + child.recursiveSetAllPageNodes(allPageNodes) + } + } + } + +} + +/** + * Return all children of the node who are one of the selected `NodeKind`s. It recursively fetches + * all offspring, not just immediate children. + */ +fun DocumentationNode.getMembersOfKinds(vararg kinds: NodeKind): MutableList<DocumentationNode> { + val membersOfKind = mutableListOf<DocumentationNode>() + recursiveSetMembersOfKinds(kinds, membersOfKind) + return membersOfKind +} + +private fun DocumentationNode.recursiveSetMembersOfKinds(kinds: Array<out NodeKind>, + membersOfKind: MutableList<DocumentationNode>) { + for (member in members) { + if (member.kind in kinds) { + membersOfKind.add(member) + } + member.recursiveSetMembersOfKinds(kinds, membersOfKind) + } +} + +/** + * Returns whether or not this node owns a page. The criteria for whether a node owns a page is + * similar to the way javadoc is structured. Classes, Interfaces, Enums, AnnotationClasses, + * Exceptions, and Objects (Kotlin-specific) meet the criteria. + */ +fun DocumentationNode.hasOwnPage() = + kind == NodeKind.Class || kind == NodeKind.Interface || kind == NodeKind.Enum || + kind == NodeKind.AnnotationClass || kind == NodeKind.Exception || + kind == NodeKind.Object + +/** + * In most cases, this returns the short name of the `Type`. When the Type is an inner Type, it + * prepends the name with the containing Type name(s). + * + * For example, if you have a class named OuterClass and an inner class named InnerClass, this would + * return OuterClass.InnerClass. + * + */ +fun DocumentationNode.nameWithOuterClass(): String { + val nameBuilder = StringBuilder(name) + var parent = owner + if (hasOwnPage()) { + while (parent != null && parent.hasOwnPage()) { + nameBuilder.insert(0, "${parent.name}.") + parent = parent.owner + } + } + return nameBuilder.toString() +} + +/** + * Return whether the node is a package. + */ +fun DocumentationNode.isPackage(): Boolean { + return kind == NodeKind.Package +} + +/** + * Return the 'page owner' of this node. `DocumentationNode.hasOwnPage()` defines the criteria for + * a page owner. If this node is not a page owner, then it iterates up through its ancestors to + * find the first page owner. + */ +fun DocumentationNode.pageOwner(): DocumentationNode { + if (hasOwnPage() || owner == null) { + return this + } else { + var parent: DocumentationNode = owner!! + while (!parent.hasOwnPage() && !parent.isPackage()) { + parent = parent.owner!! + } + return parent + } +} + +fun NodeKind.pluralizedName() = when(this) { + NodeKind.Class -> "Classes" + NodeKind.Interface -> "Interfaces" + NodeKind.AnnotationClass -> "Annotations" + NodeKind.Enum -> "Enums" + NodeKind.Exception -> "Exceptions" + NodeKind.Object -> "Objects" + NodeKind.TypeAlias -> "TypeAliases" + else -> "${name}s" +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/ExtraOutlineServices.kt b/core/src/main/kotlin/Formats/ExtraOutlineServices.kt new file mode 100644 index 000000000..e4eeac01a --- /dev/null +++ b/core/src/main/kotlin/Formats/ExtraOutlineServices.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka + +import java.io.File + +/** + * Outline service that is responsible for generating a single outline format. + * + * TODO: port existing implementations of ExtraOutlineService to OutlineService, and remove this. + */ +interface ExtraOutlineService { + fun getFileName(): String + fun getFile(location: Location): File + fun format(node: DocumentationNode): String +} + +/** + * Holder of all of the extra outline services needed for a StandardFormat, in addition to the main + * [OutlineFormatService]. + */ +abstract class ExtraOutlineServices(vararg val services: ExtraOutlineService) diff --git a/core/src/main/kotlin/Formats/FormatDescriptor.kt b/core/src/main/kotlin/Formats/FormatDescriptor.kt new file mode 100644 index 000000000..b497fb0f5 --- /dev/null +++ b/core/src/main/kotlin/Formats/FormatDescriptor.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Binder +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Utilities.bind +import org.jetbrains.dokka.Utilities.lazyBind +import org.jetbrains.dokka.Utilities.toOptional +import org.jetbrains.dokka.Utilities.toType +import kotlin.reflect.KClass + + +interface FormatDescriptorAnalysisComponent { + fun configureAnalysis(binder: Binder) +} + +interface FormatDescriptorOutputComponent { + fun configureOutput(binder: Binder) +} + +interface FormatDescriptor : FormatDescriptorAnalysisComponent, FormatDescriptorOutputComponent + + +abstract class FileGeneratorBasedFormatDescriptor : FormatDescriptor { + + override fun configureOutput(binder: Binder): Unit = with(binder) { + bind<Generator>() toType NodeLocationAwareGenerator::class + bind<NodeLocationAwareGenerator>() toType generatorServiceClass + + bind<LanguageService>() toType languageServiceClass + + lazyBind<OutlineFormatService>() toOptional (outlineServiceClass) + lazyBind<FormatService>() toOptional formatServiceClass + lazyBind<PackageListService>() toOptional packageListServiceClass + } + + abstract val formatServiceClass: KClass<out FormatService>? + abstract val outlineServiceClass: KClass<out OutlineFormatService>? + abstract val generatorServiceClass: KClass<out FileGenerator> + abstract val packageListServiceClass: KClass<out PackageListService>? + + open val languageServiceClass: KClass<out LanguageService> = KotlinLanguageService::class +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/FormatService.kt b/core/src/main/kotlin/Formats/FormatService.kt new file mode 100644 index 000000000..63f25008f --- /dev/null +++ b/core/src/main/kotlin/Formats/FormatService.kt @@ -0,0 +1,32 @@ +package org.jetbrains.dokka + +/** + * Abstract representation of a formatting service used to output documentation in desired format + * + * Bundled Formatters: + * * [HtmlFormatService] – outputs documentation to HTML format + * * [MarkdownFormatService] – outputs documentation in Markdown format + */ +interface FormatService { + /** Returns extension for output files */ + val extension: String + + /** extension which will be used for internal and external linking */ + val linkExtension: String + get() = extension + + fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder + + fun enumerateSupportFiles(callback: (resource: String, targetPath: String) -> Unit) { + } +} + +interface FormattedOutputBuilder { + /** Appends formatted content to [StringBuilder](to) using specified [location] */ + fun appendNodes(nodes: Iterable<DocumentationNode>) +} + +/** Format content to [String] using specified [location] */ +fun FormatService.format(location: Location, nodes: Iterable<DocumentationNode>): String = StringBuilder().apply { + createOutputBuilder(this, location).appendNodes(nodes) +}.toString() diff --git a/core/src/main/kotlin/Formats/GFMFormatService.kt b/core/src/main/kotlin/Formats/GFMFormatService.kt new file mode 100644 index 000000000..036ec8564 --- /dev/null +++ b/core/src/main/kotlin/Formats/GFMFormatService.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName + +open class GFMOutputBuilder( + to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String> +) : MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) { + override fun appendTable(vararg columns: String, body: () -> Unit) { + to.appendln(columns.joinToString(" | ", "| ", " |")) + to.appendln("|" + "---|".repeat(columns.size)) + body() + } + + override fun appendUnorderedList(body: () -> Unit) { + if (inTableCell) { + wrapInTag("ul", body) + } else { + super.appendUnorderedList(body) + } + } + + override fun appendOrderedList(body: () -> Unit) { + if (inTableCell) { + wrapInTag("ol", body) + } else { + super.appendOrderedList(body) + } + } + + override fun appendListItem(body: () -> Unit) { + if (inTableCell) { + wrapInTag("li", body) + } else { + super.appendListItem(body) + } + } +} + +open class GFMFormatService( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + linkExtension: String, + impliedPlatforms: List<String> +) : MarkdownFormatService(generator, signatureGenerator, linkExtension, impliedPlatforms) { + + @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String> + ) : this(generator, signatureGenerator, "md", impliedPlatforms) + + override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder = + GFMOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +} diff --git a/core/src/main/kotlin/Formats/HtmlFormatService.kt b/core/src/main/kotlin/Formats/HtmlFormatService.kt new file mode 100644 index 000000000..0ad946be2 --- /dev/null +++ b/core/src/main/kotlin/Formats/HtmlFormatService.kt @@ -0,0 +1,168 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName +import java.io.File + +open class HtmlOutputBuilder(to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String>, + val templateService: HtmlTemplateService) + : StructuredOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +{ + override fun appendText(text: String) { + to.append(text.htmlEscape()) + } + + override fun appendSymbol(text: String) { + to.append("<span class=\"symbol\">${text.htmlEscape()}</span>") + } + + override fun appendKeyword(text: String) { + to.append("<span class=\"keyword\">${text.htmlEscape()}</span>") + } + + override fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) { + val id = signature?.let { " id=\"$it\"" }.orEmpty() + to.append("<span class=\"identifier\"$id>${text.htmlEscape()}</span>") + } + + override fun appendBlockCode(language: String, body: () -> Unit) { + val openTags = if (language.isNotBlank()) + "<pre><code class=\"lang-$language\">" + else + "<pre><code>" + wrap(openTags, "</code></pre>", body) + } + + override fun appendHeader(level: Int, body: () -> Unit) = + wrapInTag("h$level", body, newlineBeforeOpen = true, newlineAfterClose = true) + override fun appendParagraph(body: () -> Unit) = + wrapInTag("p", body, newlineBeforeOpen = true, newlineAfterClose = true) + + override fun appendSoftParagraph(body: () -> Unit) = appendParagraph(body) + + override fun appendLine() { + to.appendln("<br/>") + } + + override fun appendAnchor(anchor: String) { + to.appendln("<a name=\"${anchor.htmlEscape()}\"></a>") + } + + override fun appendTable(vararg columns: String, body: () -> Unit) = + wrapInTag("table", body, newlineAfterOpen = true, newlineAfterClose = true) + override fun appendTableBody(body: () -> Unit) = + wrapInTag("tbody", body, newlineAfterOpen = true, newlineAfterClose = true) + override fun appendTableRow(body: () -> Unit) = + wrapInTag("tr", body, newlineAfterOpen = true, newlineAfterClose = true) + override fun appendTableCell(body: () -> Unit) = + wrapInTag("td", body, newlineAfterOpen = true, newlineAfterClose = true) + + override fun appendLink(href: String, body: () -> Unit) = wrap("<a href=\"$href\">", "</a>", body) + + override fun appendStrong(body: () -> Unit) = wrapInTag("strong", body) + override fun appendEmphasis(body: () -> Unit) = wrapInTag("em", body) + override fun appendStrikethrough(body: () -> Unit) = wrapInTag("s", body) + override fun appendCode(body: () -> Unit) = wrapInTag("code", body) + + override fun appendUnorderedList(body: () -> Unit) = wrapInTag("ul", body, newlineAfterClose = true) + override fun appendOrderedList(body: () -> Unit) = wrapInTag("ol", body, newlineAfterClose = true) + override fun appendListItem(body: () -> Unit) = wrapInTag("li", body, newlineAfterClose = true) + + override fun appendBreadcrumbSeparator() { + to.append(" / ") + } + + override fun appendNodes(nodes: Iterable<DocumentationNode>) { + templateService.appendHeader(to, getPageTitle(nodes), generator.relativePathToRoot(location)) + super.appendNodes(nodes) + templateService.appendFooter(to) + } + + override fun appendNonBreakingSpace() { + to.append(" ") + } + + override fun ensureParagraph() { + + } +} + +open class HtmlFormatService @Inject constructor(generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + val templateService: HtmlTemplateService, + @Named(impliedPlatformsName) val impliedPlatforms: List<String>) +: StructuredFormatService(generator, signatureGenerator, "html"), OutlineFormatService { + + override fun enumerateSupportFiles(callback: (String, String) -> Unit) { + callback("/dokka/styles/style.css", "style.css") + } + + override fun createOutputBuilder(to: StringBuilder, location: Location) = + HtmlOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms, templateService) + + override fun appendOutline(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + templateService.appendHeader(to, "Module Contents", generator.relativePathToRoot(location)) + super.appendOutline(location, to, nodes) + templateService.appendFooter(to) + } + + override fun getOutlineFileName(location: Location): File { + return File("${location.path}-outline.html") + } + + override fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) { + val link = ContentNodeDirectLink(node) + link.append(languageService.render(node, LanguageService.RenderMode.FULL)) + val tempBuilder = StringBuilder() + createOutputBuilder(tempBuilder, location).appendContent(link) + to.appendln("<a href=\"${location.path}\">$tempBuilder</a><br/>") + } + + override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) { + to.appendln("<ul>") + body() + to.appendln("</ul>") + } +} + +fun getPageTitle(nodes: Iterable<DocumentationNode>): String? { + val breakdownByLocation = nodes.groupBy { node -> formatPageTitle(node) } + return breakdownByLocation.keys.singleOrNull() +} + +fun formatPageTitle(node: DocumentationNode): String { + val path = node.path + val moduleName = path.first().name + if (path.size == 1) { + return moduleName + } + + val qName = qualifiedNameForPageTitle(node) + return qName + " - " + moduleName +} + +private fun qualifiedNameForPageTitle(node: DocumentationNode): String { + if (node.kind == NodeKind.Package) { + var packageName = node.qualifiedName() + if (packageName.isEmpty()) { + packageName = "root package" + } + return packageName + } + + val path = node.path + var pathFromToplevelMember = path.dropWhile { it.kind !in NodeKind.classLike } + if (pathFromToplevelMember.isEmpty()) { + pathFromToplevelMember = path.dropWhile { it.kind != NodeKind.Property && it.kind != NodeKind.Function } + } + if (pathFromToplevelMember.isNotEmpty()) { + return pathFromToplevelMember.map { it.name }.filter { it.length > 0 }.joinToString(".") + } + return node.qualifiedName() +} diff --git a/core/src/main/kotlin/Formats/HtmlTemplateService.kt b/core/src/main/kotlin/Formats/HtmlTemplateService.kt new file mode 100644 index 000000000..a65a7b18c --- /dev/null +++ b/core/src/main/kotlin/Formats/HtmlTemplateService.kt @@ -0,0 +1,38 @@ +package org.jetbrains.dokka + +import java.io.File + +interface HtmlTemplateService { + fun appendHeader(to: StringBuilder, title: String?, basePath: File) + fun appendFooter(to: StringBuilder) + + companion object { + fun default(css: String? = null): HtmlTemplateService { + return object : HtmlTemplateService { + override fun appendFooter(to: StringBuilder) { + if (!to.endsWith('\n')) { + to.append('\n') + } + to.appendln("</BODY>") + to.appendln("</HTML>") + } + override fun appendHeader(to: StringBuilder, title: String?, basePath: File) { + to.appendln("<HTML>") + to.appendln("<HEAD>") + to.appendln("<meta charset=\"UTF-8\">") + if (title != null) { + to.appendln("<title>$title</title>") + } + if (css != null) { + val cssPath = basePath.resolve(css).toUnixString() + to.appendln("<link rel=\"stylesheet\" href=\"$cssPath\">") + } + to.appendln("</HEAD>") + to.appendln("<BODY>") + } + } + } + } +} + + diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt new file mode 100644 index 000000000..b94886693 --- /dev/null +++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt @@ -0,0 +1,141 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Binder +import kotlinx.html.* +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Utilities.bind +import org.jetbrains.dokka.Utilities.lazyBind +import org.jetbrains.dokka.Utilities.toOptional +import org.jetbrains.dokka.Utilities.toType +import java.net.URI +import kotlin.reflect.KClass + + +abstract class JavaLayoutHtmlFormatDescriptorBase : FormatDescriptor, DefaultAnalysisComponent { + + override fun configureOutput(binder: Binder): Unit = with(binder) { + bind<Generator>() toType generatorServiceClass + bind<LanguageService>() toType languageServiceClass + bind<JavaLayoutHtmlTemplateService>() toType templateServiceClass + bind<JavaLayoutHtmlUriProvider>() toType generatorServiceClass + lazyBind<JavaLayoutHtmlFormatOutlineFactoryService>() toOptional outlineFactoryClass + bind<PackageListService>() toType packageListServiceClass + bind<JavaLayoutHtmlFormatOutputBuilderFactory>() toType outputBuilderFactoryClass + } + + val generatorServiceClass = JavaLayoutHtmlFormatGenerator::class + abstract val languageServiceClass: KClass<out LanguageService> + abstract val templateServiceClass: KClass<out JavaLayoutHtmlTemplateService> + abstract val outlineFactoryClass: KClass<out JavaLayoutHtmlFormatOutlineFactoryService>? + abstract val packageListServiceClass: KClass<out PackageListService> + abstract val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> +} + +class JavaLayoutHtmlFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin { + override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = JavaLayoutHtmlFormatOutputBuilderFactoryImpl::class + override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class + override val languageServiceClass = KotlinLanguageService::class + override val templateServiceClass = JavaLayoutHtmlTemplateService.Default::class + override val outlineFactoryClass = null +} + +class JavaLayoutHtmlAsJavaFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava { + override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = JavaLayoutHtmlFormatOutputBuilderFactoryImpl::class + override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class + override val languageServiceClass = NewJavaLanguageService::class + override val templateServiceClass = JavaLayoutHtmlTemplateService.Default::class + override val outlineFactoryClass = null +} + +interface JavaLayoutHtmlFormatOutlineFactoryService { + fun generateOutlines(outputProvider: (URI) -> Appendable, nodes: Iterable<DocumentationNode>) +} + + +interface JavaLayoutHtmlUriProvider { + fun tryGetContainerUri(node: DocumentationNode): URI? + fun tryGetMainUri(node: DocumentationNode): URI? + fun tryGetOutlineRootUri(node: DocumentationNode): URI? + fun containerUri(node: DocumentationNode): URI = tryGetContainerUri(node) ?: error("Unsupported ${node.kind}") + fun mainUri(node: DocumentationNode): URI = tryGetMainUri(node) ?: error("Unsupported ${node.kind}") + fun outlineRootUri(node: DocumentationNode): URI = tryGetOutlineRootUri(node) ?: error("Unsupported ${node.kind}") + + + fun linkTo(to: DocumentationNode, from: URI): String { + return mainUri(to).relativeTo(from).toString() + } + + fun linkToFromOutline(to: DocumentationNode, from: URI): String { + return outlineRootUri(to).relativeTo(from).toString() + } + + fun mainUriOrWarn(node: DocumentationNode): URI? = tryGetMainUri(node) ?: (null).also { + AssertionError("Not implemented mainUri for ${node.kind} (${node})").printStackTrace() + } +} + + +interface JavaLayoutHtmlTemplateService { + fun composePage( + page: JavaLayoutHtmlFormatOutputBuilder.Page, + tagConsumer: TagConsumer<Appendable>, + headContent: HEAD.() -> Unit, + bodyContent: BODY.() -> Unit + ) + + class Default : JavaLayoutHtmlTemplateService { + override fun composePage( + page: JavaLayoutHtmlFormatOutputBuilder.Page, + tagConsumer: TagConsumer<Appendable>, + headContent: HEAD.() -> Unit, + bodyContent: BODY.() -> Unit + ) { + tagConsumer.html { + head { + meta(charset = "UTF-8") + headContent() + } + body(block = bodyContent) + } + } + } +} + +val DocumentationNode.companion get() = members(NodeKind.Object).find { it.details(NodeKind.Modifier).any { it.name == "companion" } } + +fun DocumentationNode.signatureForAnchor(logger: DokkaLogger): String { + + fun StringBuilder.appendReceiverIfSo() { + detailOrNull(NodeKind.Receiver)?.let { + append("(") + append(it.detail(NodeKind.Type).qualifiedNameFromType()) + append(").") + } + } + + return when (kind) { + NodeKind.Function, NodeKind.Constructor, NodeKind.CompanionObjectFunction -> buildString { + if (kind == NodeKind.CompanionObjectFunction) { + append("Companion.") + } + appendReceiverIfSo() + append(prettyName) + details(NodeKind.Parameter).joinTo(this, prefix = "(", postfix = ")") { it.detail(NodeKind.Type).qualifiedNameFromType() } + } + NodeKind.Property, NodeKind.CompanionObjectProperty -> buildString { + if (kind == NodeKind.CompanionObjectProperty) { + append("Companion.") + } + appendReceiverIfSo() + append(name) + append(":") + append(detail(NodeKind.Type).qualifiedNameFromType()) + } + NodeKind.TypeParameter, NodeKind.Parameter -> this.detail(NodeKind.Signature).name // Todo Why not signatureForAnchor + NodeKind.Field -> name + NodeKind.EnumItem -> "ENUM_VALUE:$name" + NodeKind.Attribute -> "attr_$name" + else -> "Not implemented signatureForAnchor $this".also { logger.warn(it) } + } +} + diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt new file mode 100644 index 000000000..59d898a2a --- /dev/null +++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt @@ -0,0 +1,1171 @@ +package org.jetbrains.dokka.Formats + +import com.google.common.base.Throwables +import kotlinx.html.* +import kotlinx.html.Entities.nbsp +import kotlinx.html.stream.appendHTML +import org.jetbrains.dokka.* +import org.jetbrains.dokka.LanguageService.RenderMode.FULL +import org.jetbrains.dokka.LanguageService.RenderMode.SUMMARY +import org.jetbrains.dokka.NodeKind.Companion.classLike +import java.net.URI +import javax.inject.Inject + + +open class JavaLayoutHtmlFormatOutputBuilder( + val output: Appendable, + val languageService: LanguageService, + val uriProvider: JavaLayoutHtmlUriProvider, + val templateService: JavaLayoutHtmlTemplateService, + val logger: DokkaLogger, + val uri: URI +) { + + val htmlConsumer = output.appendHTML() + + + private fun FlowContent.hN( + level: Int, + classes: String? = null, + block: CommonAttributeGroupFacadeFlowHeadingPhrasingContent.() -> Unit + ) { + when (level) { + 1 -> h1(classes, block) + 2 -> h2(classes, block) + 3 -> h3(classes, block) + 4 -> h4(classes, block) + 5 -> h5(classes, block) + 6 -> h6(classes, block) + } + } + + protected open fun FlowContent.metaMarkup(content: List<ContentNode>, contextUri: URI = uri) = + contentNodesToMarkup(content, contextUri) + + protected fun FlowContent.nodeContent(node: DocumentationNode, uriNode: DocumentationNode) = + contentNodeToMarkup(node.content, uriProvider.mainUriOrWarn(uriNode) ?: uri) + + protected fun FlowContent.nodeContent(node: DocumentationNode) = + nodeContent(node, node) + + protected fun FlowContent.contentNodesToMarkup(content: List<ContentNode>, contextUri: URI = uri): Unit = + content.forEach { contentNodeToMarkup(it, contextUri) } + + protected fun FlowContent.contentNodeToMarkup(content: ContentNode, contextUri: URI = uri) { + when (content) { + is ContentText -> +content.text + is ContentSymbol -> span("symbol") { +content.text } + is ContentKeyword -> span("keyword") { +content.text } + is ContentIdentifier -> span("identifier") { + content.signature?.let { id = it } + +content.text + } + + is ContentHeading -> hN(level = content.level) { contentNodesToMarkup(content.children, contextUri) } + + is ContentEntity -> +content.text + + is ContentStrong -> strong { contentNodesToMarkup(content.children, contextUri) } + is ContentStrikethrough -> del { contentNodesToMarkup(content.children, contextUri) } + is ContentEmphasis -> em { contentNodesToMarkup(content.children, contextUri) } + + is ContentOrderedList -> ol { contentNodesToMarkup(content.children, contextUri) } + is ContentUnorderedList -> ul { contentNodesToMarkup(content.children, contextUri) } + is ContentListItem -> consumer.li { + (content.children.singleOrNull() as? ContentParagraph) + ?.let { paragraph -> contentNodesToMarkup(paragraph.children, contextUri) } + ?: contentNodesToMarkup(content.children, contextUri) + } + + is ContentDescriptionList -> dl { contentNodesToMarkup(content.children, contextUri) } + is ContentDescriptionTerm -> consumer.dt { + (content.children.singleOrNull() as? ContentParagraph) + ?.let { paragraph -> this@contentNodeToMarkup.contentNodesToMarkup(paragraph.children, contextUri) } + ?: this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) + } + is ContentDescriptionDefinition -> consumer.dd { + (content.children.singleOrNull() as? ContentParagraph) + ?.let { paragraph -> contentNodesToMarkup(paragraph.children, contextUri) } + ?: contentNodesToMarkup(content.children, contextUri) + } + + is ContentTable -> table { contentNodesToMarkup(content.children, contextUri) } + is ContentTableBody -> consumer.tbody { this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) } + is ContentTableRow -> consumer.tr { this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) } + is ContentTableHeader -> consumer.th { + content.colspan?.let { + if (it.isNotBlank()) { + attributes["colspan"] = content.colspan + } + } + content.rowspan?.let { + if (it.isNotBlank()) { + attributes["rowspan"] = content.rowspan + } + } + (content.children.singleOrNull() as? ContentParagraph) + ?.let { paragraph -> this@contentNodeToMarkup.contentNodesToMarkup(paragraph.children, contextUri) } + ?: this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) + } + is ContentTableCell -> consumer.td { + content.colspan?.let { + if (it.isNotBlank()) { + attributes["colspan"] = content.colspan + } + } + content.rowspan?.let { + if (it.isNotBlank()) { + attributes["rowspan"] = content.rowspan + } + } + (content.children.singleOrNull() as? ContentParagraph) + ?.let { paragraph -> contentNodesToMarkup(paragraph.children, contextUri) } + ?: contentNodesToMarkup(content.children, contextUri) + } + + is ContentSpecialReference -> aside(classes = "note") { + contentNodesToMarkup(content.children, contextUri) + } + + is ContentCode -> contentInlineCode(content) + is ContentBlockSampleCode -> contentBlockSampleCode(content) + is ContentBlockCode -> contentBlockCode(content) + + ContentNonBreakingSpace -> +nbsp + ContentSoftLineBreak, ContentIndentedSoftLineBreak -> { + } + ContentHardLineBreak -> br + + is ContentParagraph -> p(classes = content.label) { contentNodesToMarkup(content.children, contextUri) } + + is NodeRenderContent -> renderedSignature(content.node, mode = content.mode) + is ContentNodeLink -> { + fun FlowContent.body() = contentNodesToMarkup(content.children, contextUri) + + when (content.node?.kind) { + NodeKind.TypeParameter -> body() + else -> a(href = content.node, block = FlowContent::body) + } + } + is ContentBookmark -> a { + id = content.name + contentNodesToMarkup(content.children, contextUri) + } + is ContentExternalLink -> contentExternalLink(content) + is ContentLocalLink -> a(href = contextUri.resolve(content.href).relativeTo(uri).toString()) { + contentNodesToMarkup(content.children, contextUri) + } + is ContentSection -> { + } + is ScriptBlock -> script(content.type, content.src) {} + is ContentBlock -> contentNodesToMarkup(content.children, contextUri) + } + } + + protected open fun FlowContent.contentInlineCode(content: ContentCode) { + code { contentNodesToMarkup(content.children) } + } + + protected open fun FlowContent.contentBlockSampleCode(content: ContentBlockSampleCode) { + pre { + code { + attributes["data-language"] = content.language + contentNodesToMarkup(content.importsBlock.children) + +"\n\n" + contentNodesToMarkup(content.children) + } + } + } + + protected open fun FlowContent.contentBlockCode(content: ContentBlockCode) { + pre { + code { + attributes["data-language"] = content.language + contentNodesToMarkup(content.children) + } + } + } + + protected open fun FlowContent.contentExternalLink(content: ContentExternalLink) { + a(href = content.href) { contentNodesToMarkup(content.children) } + } + + protected open fun <T> FlowContent.summaryNodeGroup( + nodes: Iterable<T>, + header: String, + headerAsRow: Boolean = true, + row: TBODY.(T) -> Unit + ) { + if (nodes.none()) return + if (!headerAsRow) { + h2 { +header } + } + table { + tbody { + if (headerAsRow) { + developerHeading(header) + } + nodes.forEach { node -> + row(node) + } + } + } + } + + + protected open fun summary(node: DocumentationNode) = node.summary + + protected open fun TBODY.classLikeRow(node: DocumentationNode) = tr { + td { a(href = uriProvider.linkTo(node, uri)) { +node.simpleName() } } + td { nodeSummary(node) } + } + + protected fun FlowContent.modifiers(node: DocumentationNode) { + for (modifier in node.details(NodeKind.Modifier)) { + renderedSignature(modifier, SUMMARY) + } + } + + protected fun FlowContent.shortFunctionParametersList(func: DocumentationNode) { + val params = func.details(NodeKind.Parameter) + .map { languageService.render(it, FULL) } + .run { + drop(1).fold(listOfNotNull(firstOrNull())) { acc, node -> + acc + ContentText(", ") + node + } + } + metaMarkup(listOf(ContentText("(")) + params + listOf(ContentText(")"))) + } + + + protected open fun TBODY.functionLikeSummaryRow(node: DocumentationNode) = tr { + if (node.kind != NodeKind.Constructor) { + td { + modifiers(node) + renderedSignature(node.detail(NodeKind.Type), SUMMARY) + } + } + td { + div { + code { + val receiver = node.detailOrNull(NodeKind.Receiver) + if (receiver != null) { + renderedSignature(receiver.detail(NodeKind.Type), SUMMARY) + +"." + } + a(href = node) { +node.prettyName } + shortFunctionParametersList(node) + } + } + + nodeSummary(node) + } + } + + protected open fun TBODY.propertyLikeSummaryRow(node: DocumentationNode, showSignature: Boolean = true) = tr { + if (showSignature) { + td { + modifiers(node) + renderedSignature(node.detail(NodeKind.Type), SUMMARY) + } + } + td { + div { + code { + a(href = node) { +node.name } + } + } + + nodeSummary(node) + } + } + + protected open fun TBODY.nestedClassSummaryRow(node: DocumentationNode) = tr { + td { + modifiers(node) + } + td { + div { + code { + a(href = node) { +node.name } + } + } + + nodeSummary(node) + } + } + + protected fun HtmlBlockTag.nodeSummary(node: DocumentationNode, uriNode: DocumentationNode) { + contentNodeToMarkup(summary(node), uriProvider.mainUriOrWarn(uriNode) ?: uri) + } + + protected fun HtmlBlockTag.nodeSummary(node: DocumentationNode) { + nodeSummary(node, node) + } + + protected open fun TBODY.inheritRow( + entry: Map.Entry<DocumentationNode, List<DocumentationNode>>, + summaryRow: TBODY.(DocumentationNode) -> Unit + ) = tr { + td { + val (from, nodes) = entry + +"From class " + a(href = from.owner!!) { +from.qualifiedName() } + table { + tbody { + for (node in nodes) { + summaryRow(node) + } + } + } + } + } + + protected open fun TBODY.groupedRow( + entry: Map.Entry<DocumentationNode, List<DocumentationNode>>, + groupHeader: HtmlBlockTag.(DocumentationNode) -> Unit, + summaryRow: TBODY.(DocumentationNode) -> Unit + ) = tr { + td { + val (from, nodes) = entry + groupHeader(from) + table { + tbody { + for (node in nodes) { + summaryRow(node) + } + } + } + } + } + + protected open fun TBODY.extensionRow( + entry: Map.Entry<DocumentationNode, List<DocumentationNode>>, + summaryRow: TBODY.(DocumentationNode) -> Unit + ) = groupedRow(entry, { from -> + +"From " + a(href = from) { +from.qualifiedName() } + }, summaryRow) + + + protected open fun TBODY.extensionByReceiverRow( + entry: Map.Entry<DocumentationNode, List<DocumentationNode>>, + summaryRow: TBODY.(DocumentationNode) -> Unit + ) = groupedRow(entry, { from -> + +"For " + a(href = from) { +from.name } + }, summaryRow) + + protected open fun FlowOrInteractiveOrPhrasingContent.a(href: DocumentationNode?, classes: String? = null, block: HtmlBlockInlineTag.() -> Unit) { + if (href == null) { + return a(href = "#", classes = classes, block = block) + } + + val hrefText = try { + href.name.takeIf { href.kind == NodeKind.ExternalLink } + ?: href.links.firstOrNull { it.kind == NodeKind.ExternalLink }?.name + ?: "#".takeIf { href.kind == NodeKind.ExternalClass } // When external class unresolved + ?: uriProvider.linkTo(href, uri) + } catch (e: Exception) { + val owners = generateSequence(href) { it.owner }.toList().reversed() + logger.warn("Exception while resolving link to ${owners.joinToString(separator = " ")}\n" + + Throwables.getStackTraceAsString(e)) + "#" + } + + a(href = hrefText, classes = classes, block = block) + } + + protected open fun FlowContent.renderedSignature( + node: DocumentationNode, + mode: LanguageService.RenderMode = SUMMARY + ) { + contentNodeToMarkup(languageService.render(node, mode), uri) + } + + protected open fun generatePackage(page: Page.PackagePage) = templateService.composePage( + page, + htmlConsumer, + headContent = { + + }, + bodyContent = { + h1 { +page.node.name } + nodeContent(page.node) + this@composePage.summaryNodeGroup(page.interfaces, "Interfaces", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.classes, "Classes", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.exceptions, "Exceptions", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.typeAliases, "Type-aliases", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.annotations, "Annotations", headerAsRow = false) { classLikeRow(it) } + this@composePage.summaryNodeGroup(page.enums, "Enums", headerAsRow = false) { classLikeRow(it) } + + this@composePage.summaryNodeGroup( + page.constants, + "Top-level constants summary", + headerAsRow = false + ) { + propertyLikeSummaryRow(it) + } + + this@composePage.summaryNodeGroup( + page.functions, + "Top-level functions summary", + headerAsRow = false + ) { + functionLikeSummaryRow(it) + } + + this@composePage.summaryNodeGroup( + page.properties, + "Top-level properties summary", + headerAsRow = false + ) { + propertyLikeSummaryRow(it) + } + + this@composePage.summaryNodeGroup( + page.extensionFunctions.entries, + "Extension functions summary", + headerAsRow = false + ) { + extensionByReceiverRow(it) { + functionLikeSummaryRow(it) + } + } + + this@composePage.summaryNodeGroup( + page.extensionProperties.entries, + "Extension properties summary", + headerAsRow = false + ) { + extensionByReceiverRow(it) { + functionLikeSummaryRow(it) + } + } + + fullMemberDocs(page.constants, "Top-level constants") + fullMemberDocs(page.functions, "Top-level functions") + fullMemberDocs(page.properties, "Top-level properties") + fullMemberDocs(page.extensionFunctions.values.flatten(), "Extension functions") + fullMemberDocs(page.extensionProperties.values.flatten(), "Extension properties") + } + ) + + protected fun FlowContent.qualifiedTypeReference(node: DocumentationNode) { + if (node.kind in classLike) { + a(href = node) { +node.qualifiedName() } + return + } + + val targetLink = node.links.firstOrNull() + + if (targetLink?.kind == NodeKind.TypeParameter) { + +node.name + return + } + + a(href = targetLink) { + +node.qualifiedNameFromType() + } + val typeParameters = node.details(NodeKind.Type) + if (typeParameters.isNotEmpty()) { + +"<" + typeParameters.forEach { + if (it != typeParameters.first()) { + +", " + } + qualifiedTypeReference(it) + } + +">" + } + } + + protected open fun FlowContent.classHierarchy(superclasses: List<DocumentationNode>) { + table { + superclasses.forEach { + tr { + if (it != superclasses.first()) { + td { + +"   ↳" + } + } + td { + qualifiedTypeReference(it) + } + } + } + } + } + + protected open fun FlowContent.subclasses(inheritors: List<DocumentationNode>, direct: Boolean) { + if (inheritors.isEmpty()) return + div { + table { + thead { + tr { + td { + if (direct) + +"Known Direct Subclasses" + else + +"Known Indirect Subclasses" + } + } + } + tbody { + inheritors.forEach { inheritor -> + tr { + td { + a(href = inheritor) { +inheritor.classNodeNameWithOuterClass() } + } + td { + nodeSummary(inheritor) + } + } + } + } + } + } + } + + protected open fun FlowContent.classLikeSummaries(page: Page.ClassPage) = with(page) { + this@classLikeSummaries.summaryNodeGroup( + nestedClasses, + "Nested classes", + headerAsRow = true + ) { + nestedClassSummaryRow(it) + } + + this@classLikeSummaries.summaryNodeGroup(enumValues, "Enum values") { + propertyLikeSummaryRow(it) + } + + this@classLikeSummaries.summaryNodeGroup(constants, "Constants") { propertyLikeSummaryRow(it) } + + constructors.forEach { (visibility, group) -> + this@classLikeSummaries.summaryNodeGroup( + group, + "${visibility.capitalize()} constructors", + headerAsRow = true + ) { + functionLikeSummaryRow(it) + } + } + + functions.forEach { (visibility, group) -> + this@classLikeSummaries.summaryNodeGroup( + group, + "${visibility.capitalize()} functions", + headerAsRow = true + ) { + functionLikeSummaryRow(it) + } + } + + this@classLikeSummaries.summaryNodeGroup( + companionFunctions, + "Companion functions", + headerAsRow = true + ) { + functionLikeSummaryRow(it) + } + this@classLikeSummaries.summaryNodeGroup( + inheritedFunctionsByReceiver.entries, + "Inherited functions", + headerAsRow = true + ) { + inheritRow(it) { + functionLikeSummaryRow(it) + } + } + this@classLikeSummaries.summaryNodeGroup( + extensionFunctions.entries, + "Extension functions", + headerAsRow = true + ) { + extensionRow(it) { + functionLikeSummaryRow(it) + } + } + this@classLikeSummaries.summaryNodeGroup( + inheritedExtensionFunctions.entries, + "Inherited extension functions", + headerAsRow = true + ) { + extensionRow(it) { + functionLikeSummaryRow(it) + } + } + + + this@classLikeSummaries.summaryNodeGroup(properties, "Properties", headerAsRow = true) { propertyLikeSummaryRow(it) } + this@classLikeSummaries.summaryNodeGroup( + companionProperties, + "Companion properties", + headerAsRow = true + ) { + propertyLikeSummaryRow(it) + } + + this@classLikeSummaries.summaryNodeGroup( + inheritedPropertiesByReceiver.entries, + "Inherited properties", + headerAsRow = true + ) { + inheritRow(it) { + propertyLikeSummaryRow(it) + } + } + this@classLikeSummaries.summaryNodeGroup( + extensionProperties.entries, + "Extension properties", + headerAsRow = true + ) { + extensionRow(it) { + propertyLikeSummaryRow(it) + } + } + this@classLikeSummaries.summaryNodeGroup( + inheritedExtensionProperties.entries, + "Inherited extension properties", + headerAsRow = true + ) { + extensionRow(it) { + propertyLikeSummaryRow(it) + } + } + } + + protected open fun FlowContent.classLikeFullMemberDocs(page: Page.ClassPage) = with(page) { + fullMemberDocs(enumValues, "Enum values") + fullMemberDocs(constants, "Constants") + + constructors.forEach { (visibility, group) -> + fullMemberDocs(group, "${visibility.capitalize()} constructors") + } + + functions.forEach { (visibility, group) -> + fullMemberDocs(group, "${visibility.capitalize()} methods") + } + + fullMemberDocs(properties, "Properties") + if (!hasMeaningfulCompanion) { + fullMemberDocs(companionFunctions, "Companion functions") + fullMemberDocs(companionProperties, "Companion properties") + } + } + + protected open fun generateClassLike(page: Page.ClassPage) = templateService.composePage( + page, + htmlConsumer, + headContent = { + + }, + bodyContent = { + val node = page.node + with(page) { + + div { + id = "api-info-block" + apiAndDeprecatedVersions(node) + } + + if (node.artifactId.name.isNotEmpty()) { + div(classes = "api-level") { br { +"belongs to Maven artifact ${node.artifactId}" } } + } + h1 { +node.name } + pre { renderedSignature(node, FULL) } + classHierarchy(page.superclasses) + + subclasses(page.directInheritors, true) + subclasses(page.indirectInheritors, false) + + deprecatedClassCallOut(node) + nodeContent(node) + + h2 { +"Summary" } + classLikeSummaries(page) + classLikeFullMemberDocs(page) + } + } + ) + + protected open fun FlowContent.classIndexSummary(node: DocumentationNode) { + nodeContent(node) + } + + protected open fun generateClassIndex(page: Page.ClassIndex) = templateService.composePage( + page, + htmlConsumer, + headContent = { + + }, + bodyContent = { + h1 { +"Class Index" } + + + ul { + page.classesByFirstLetter.forEach { (letter) -> + li { a(href = "#letter_$letter") { +letter } } + } + } + + page.classesByFirstLetter.forEach { (letter, classes) -> + h2 { + id = "letter_$letter" + +letter + } + table { + tbody { + for (node in classes) { + tr { + td { + a(href = uriProvider.linkTo(node, uri)) { +node.classNodeNameWithOuterClass() } + } + td { + if (!deprecatedIndexSummary(node)) { + classIndexSummary(node) + } + } + } + } + } + } + } + } + ) + + protected open fun generatePackageIndex(page: Page.PackageIndex) = templateService.composePage( + page, + htmlConsumer, + headContent = { + + }, + bodyContent = { + h1 { +"Package Index" } + table { + tbody { + for (node in page.packages) { + tr { + td { + a(href = uriProvider.linkTo(node, uri)) { +node.name } + } + td { + nodeContent(node) + } + } + } + } + } + } + ) + + fun generatePage(page: Page) { + when (page) { + is Page.PackageIndex -> generatePackageIndex(page) + is Page.ClassIndex -> generateClassIndex(page) + is Page.ClassPage -> generateClassLike(page) + is Page.PackagePage -> generatePackage(page) + } + } + + protected fun FlowContent.fullMemberDocs( + nodes: List<DocumentationNode>, + header: String + ) { + if (nodes.none()) return + h2 { + +header + } + for (node in nodes) { + fullMemberDocs(node) + } + } + + protected open fun FlowContent.seeAlsoSection(links: List<List<ContentNode>>) { + p { b { +"See Also" } } + ul { + links.forEach { linkParts -> + li { code { metaMarkup(linkParts) } } + } + } + } + + protected open fun FlowContent.regularSection(name: String, entries: List<ContentSection>) { + table { + thead { + tr { + th { + colSpan = "2" + +name + } + } + } + tbody { + entries.forEach { + tr { + if (it.subjectName != null) { + td { +it.subjectName } + } + td { + metaMarkup(it.children) + } + } + } + } + } + } + + protected open fun FlowContent.deprecationWarningToMarkup( + node: DocumentationNode, + prefix: Boolean = false, + emphasis: Boolean = true + ): Boolean { + val deprecated = formatDeprecationOrNull(node, prefix, emphasis) + deprecated?.let { + contentNodeToMarkup(deprecated, uriProvider.mainUri(node)) + return true + } + return false + } + + protected open fun FlowContent.deprecatedClassCallOut(node: DocumentationNode) { + val deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty() + if (deprecatedLevelExists) { + hr { } + aside(classes = "caution") { + strong { +node.deprecatedLevelMessage() } + deprecationWarningToMarkup(node, emphasis = false) + } + } + } + + protected open fun FlowContent.deprecatedIndexSummary(node: DocumentationNode): Boolean { + val deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty() + if (deprecatedLevelExists) { + val em = ContentEmphasis() + em.append(ContentText(node.deprecatedLevelMessage())) + em.append(ContentText(" ")) + for (child in node.deprecation?.content?.children ?: emptyList<ContentNode>()) { + em.append(child) + } + contentNodeToMarkup(em, uriProvider.mainUri(node)) + return true + } + return false + } + + protected open fun FlowContent.apiAndDeprecatedVersions(node: DocumentationNode) { + val apiLevelExists = node.apiLevel.name.isNotEmpty() + val sdkExtSinceExists = node.sdkExtSince.name.isNotEmpty() + val deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty() + if (apiLevelExists || sdkExtSinceExists || deprecatedLevelExists) { + div(classes = "api-level") { + if (apiLevelExists) { + +"Added in " + a(href = "https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels") { + +"API level ${node.apiLevel.name}" + } + } + if (sdkExtSinceExists) { + if (apiLevelExists) { + br + +"Also in " + } else { + +"Added in " + } + a(href = "https://developer.android.com/sdkExtensions") { + +"${node.sdkExtSince.name}" + } + } + if (deprecatedLevelExists) { + if (apiLevelExists || sdkExtSinceExists) { + br + } + +"Deprecated in " + a(href = "https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels") { + +"API level ${node.deprecatedLevel.name}" + } + } + } + } + } + + protected open fun formatDeprecationOrNull( + node: DocumentationNode, + prefix: Boolean = false, + emphasis: Boolean = true): ContentNode? { + val deprecated = node.deprecation + deprecated?.let { + return ContentParagraph("caution").apply { + if (prefix) { + append(ContentStrong().apply { text( + if (deprecated.content.children.size == 0) "Deprecated." + else "Deprecated: " + ) }) + } + val em = if (emphasis) ContentEmphasis() else ContentBlock() + for (child in deprecated.content.children) { + em.append(child) + } + append(em) + } + } + return null + } + + protected open fun FlowContent.section(name: String, sectionParts: List<ContentSection>) { + when (name) { + ContentTags.SeeAlso -> seeAlsoSection(sectionParts.map { it.children.flatMap { (it as? ContentParagraph)?.children ?: listOf(it) } }) + else -> regularSection(name, sectionParts) + } + } + + protected open fun FlowContent.sections(content: Content) { + val sectionsByTag = content.sections.groupByTo(mutableMapOf()) { it.tag } + + val seeAlso = sectionsByTag.remove(ContentTags.SeeAlso) + + for ((name, entries) in sectionsByTag) { + section(name, entries) + } + + seeAlso?.let { section(ContentTags.SeeAlso, it) } + } + + protected open fun FlowContent.fullMemberDocs(node: DocumentationNode, uriNode: DocumentationNode) { + div { + id = node.signatureForAnchor(logger) + h3 { +node.name } + pre { renderedSignature(node, FULL) } + deprecationWarningToMarkup(node, prefix = true) + nodeContent(node) + node.constantValue()?.let { value -> + pre { + +"Value: " + code { +value } + } + } + + sections(node.content) + } + } + + protected open fun FlowContent.fullMemberDocs(node: DocumentationNode) { + fullMemberDocs(node, node) + } + + sealed class Page { + class PackageIndex(packages: List<DocumentationNode>) : Page() { + init { + assert(packages.all { it.kind == NodeKind.Package }) + } + + val packages = packages.sortedBy { it.name } + } + + class ClassIndex(allTypesNode: DocumentationNode) : Page() { + init { + assert(allTypesNode.kind == NodeKind.AllTypes) + } + + // Wide-collect all nested classes + val classes: List<DocumentationNode> = + generateSequence(listOf(allTypesNode)) { nodes -> + nodes + .flatMap { it.members.filter { it.kind in NodeKind.classLike } } + .takeUnless { it.isEmpty() } + }.drop(1) + .flatten() + .sortedBy { it.classNodeNameWithOuterClass().toLowerCase() } + .toList() + + + // Group all classes by it's first letter and sort + val classesByFirstLetter = + classes + .groupBy { + it.classNodeNameWithOuterClass().first().toString() + } + .entries + .sortedBy { (letter) -> + val x = letter.toLowerCase() + x + } + } + + class ClassPage(val node: DocumentationNode) : Page() { + + init { + assert(node.kind in NodeKind.classLike) + } + + val superclasses = (sequenceOf(node) + node.superclassTypeSequence).toList().asReversed() + + val enumValues = node.members(NodeKind.EnumItem).sortedBy { it.name } + + val directInheritors: List<DocumentationNode> + val indirectInheritors: List<DocumentationNode> + + init { + // Wide-collect all inheritors + val inheritors = generateSequence(node.inheritors) { inheritors -> + inheritors + .flatMap { it.inheritors } + .takeUnless { it.isEmpty() } + } + directInheritors = inheritors.first().sortedBy { it.classNodeNameWithOuterClass() } + indirectInheritors = inheritors.drop(1).flatten().toList().sortedBy { it.classNodeNameWithOuterClass() } + } + + val isCompanion = node.details(NodeKind.Modifier).any { it.name == "companion" } + val hasMeaningfulCompanion = !isCompanion && node.companion != null + + private fun DocumentationNode.thisTypeExtension() = + detail(NodeKind.Receiver).detail(NodeKind.Type).links.any { it == node } + + val functionKind = if (!isCompanion) NodeKind.Function else NodeKind.CompanionObjectFunction + val propertyKind = if (!isCompanion) NodeKind.Property else NodeKind.CompanionObjectProperty + + private fun DocumentationNode.isFunction() = kind == functionKind + private fun DocumentationNode.isProperty() = kind == propertyKind + + + val nestedClasses = node.members.filter { it.kind in NodeKind.classLike } - enumValues + + val attributes = node.attributes + + val inheritedAttributes = + node.superclassTypeSequence + .toList() + .sortedBy { it.name } + .flatMap { it.typeDeclarationClass?.attributes.orEmpty() } + .distinctBy { it.attributeRef!!.name } + .groupBy { it.owner!! } + + val allInheritedMembers = node.allInheritedMembers + val constants = node.members.filter { it.constantValue() != null } + val inheritedConstants = allInheritedMembers.filter { it.constantValue() != null }.groupBy { it.owner!! } + + + fun compareVisibilities(a: String, b: String): Int { + return visibilityNames.indexOf(a) - visibilityNames.indexOf(b) + } + + fun Collection<DocumentationNode>.groupByVisibility() = + groupBy { it.visibility() }.toSortedMap(Comparator { a, b -> compareVisibilities(a, b) }) + + + val constructors = node.members(NodeKind.Constructor).groupByVisibility() + val functions = node.members(functionKind).groupByVisibility() + val fields = (node.members(NodeKind.Field) - constants).groupByVisibility() + val properties = node.members(propertyKind) - constants + val inheritedFunctionsByReceiver = allInheritedMembers.filter { it.kind == functionKind }.groupBy { it.owner!! } + val inheritedPropertiesByReceiver = + allInheritedMembers.filter { + it.kind == propertyKind && it.constantValue() == null + }.groupBy { it.owner!! } + + val inheritedFieldsByReceiver = + allInheritedMembers.filter { + it.kind == NodeKind.Field && it.constantValue() != null + }.groupBy { it.owner!! } + + val originalExtensions = if (!isCompanion) node.extensions else node.owner!!.extensions + + val extensionFunctions: Map<DocumentationNode, List<DocumentationNode>> + val extensionProperties: Map<DocumentationNode, List<DocumentationNode>> + val inheritedExtensionFunctions: Map<DocumentationNode, List<DocumentationNode>> + val inheritedExtensionProperties: Map<DocumentationNode, List<DocumentationNode>> + + init { + val (extensions, inheritedExtensions) = originalExtensions.partition { it.thisTypeExtension() } + extensionFunctions = extensions.filter { it.isFunction() }.sortedBy { it.name }.groupBy { it.owner!! } + extensionProperties = extensions.filter { it.isProperty() }.sortedBy { it.name }.groupBy { it.owner!! } + inheritedExtensionFunctions = + inheritedExtensions.filter { it.isFunction() }.sortedBy { it.name }.groupBy { it.owner!! } + inheritedExtensionProperties = + inheritedExtensions.filter { it.isProperty() }.sortedBy { it.name }.groupBy { it.owner!! } + } + + val companionFunctions = node.members(NodeKind.CompanionObjectFunction).takeUnless { isCompanion }.orEmpty() + val companionProperties = + node.members(NodeKind.CompanionObjectProperty).takeUnless { isCompanion }.orEmpty() - constants + + + } + + class PackagePage(val node: DocumentationNode) : Page() { + + init { + assert(node.kind == NodeKind.Package) + } + + val interfaces = node.members(NodeKind.Interface) + + node.members(NodeKind.Class).flatMap { it.members(NodeKind.Interface) } + val classes = node.members(NodeKind.Class) + val exceptions = node.members(NodeKind.Exception) + val typeAliases = node.members(NodeKind.TypeAlias) + val annotations = node.members(NodeKind.AnnotationClass) + val enums = node.members(NodeKind.Enum) + + val constants = node.members(NodeKind.Property).filter { it.constantValue() != null } + + + private fun DocumentationNode.getClassExtensionReceiver() = + detailOrNull(NodeKind.Receiver)?.detailOrNull(NodeKind.Type)?.takeIf { + it.links.any { it.kind == NodeKind.ExternalLink || it.kind in NodeKind.classLike } + } + + private fun List<DocumentationNode>.groupedExtensions() = + filter { it.getClassExtensionReceiver() != null } + .groupBy { + val receiverType = it.getClassExtensionReceiver()!! + receiverType.links.filter { it.kind != NodeKind.ExternalLink}.firstOrNull() ?: + receiverType.links(NodeKind.ExternalLink).first() + } + + private fun List<DocumentationNode>.externalExtensions(kind: NodeKind) = + associateBy({ it }, { it.members(kind) }) + .filterNot { (_, values) -> values.isEmpty() } + + val extensionFunctions = + node.members(NodeKind.ExternalClass).externalExtensions(NodeKind.Function) + + node.members(NodeKind.Function).groupedExtensions() + + val extensionProperties = + node.members(NodeKind.ExternalClass).externalExtensions(NodeKind.Property) + + node.members(NodeKind.Property).groupedExtensions() + + val functions = node.members(NodeKind.Function) - extensionFunctions.values.flatten() + val properties = node.members(NodeKind.Property) - constants - extensionProperties.values.flatten() + + } + } +} + +class JavaLayoutHtmlFormatOutputBuilderFactoryImpl @Inject constructor( + val uriProvider: JavaLayoutHtmlUriProvider, + val languageService: LanguageService, + val templateService: JavaLayoutHtmlTemplateService, + val logger: DokkaLogger +) : JavaLayoutHtmlFormatOutputBuilderFactory { + override fun createOutputBuilder(output: Appendable, node: DocumentationNode): JavaLayoutHtmlFormatOutputBuilder { + return createOutputBuilder(output, uriProvider.mainUri(node)) + } + + override fun createOutputBuilder(output: Appendable, uri: URI): JavaLayoutHtmlFormatOutputBuilder { + return JavaLayoutHtmlFormatOutputBuilder(output, languageService, uriProvider, templateService, logger, uri) + } +} + +fun DocumentationNode.constantValue(): String? = + detailOrNull(NodeKind.Value)?.name.takeIf { + kind == NodeKind.Field || kind == NodeKind.Property || kind == NodeKind.CompanionObjectProperty + } + + +private val visibilityNames = setOf("public", "protected", "internal", "package-local", "private") + +fun DocumentationNode.visibility(): String = + details(NodeKind.Modifier).firstOrNull { it.name in visibilityNames }?.name ?: "" diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt new file mode 100644 index 000000000..9928a8e9e --- /dev/null +++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt @@ -0,0 +1,165 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatOutputBuilder.Page +import org.jetbrains.dokka.NodeKind.Companion.classLike +import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult +import java.io.BufferedWriter +import java.io.File +import java.net.URI + +class JavaLayoutHtmlFormatGenerator @Inject constructor( + @Named("outputDir") val root: File, + val packageListService: PackageListService, + val outputBuilderFactoryService: JavaLayoutHtmlFormatOutputBuilderFactory, + private val options: DocumentationOptions, + val logger: DokkaLogger, + @Named("outlineRoot") val outlineRoot: String +) : Generator, JavaLayoutHtmlUriProvider { + + @set:Inject(optional = true) + var outlineFactoryService: JavaLayoutHtmlFormatOutlineFactoryService? = null + + fun createOutputBuilderForNode(node: DocumentationNode, output: Appendable) = outputBuilderFactoryService.createOutputBuilder(output, node) + + fun DocumentationNode.getOwnerOrReport() = owner ?: run { + error("Owner not found for $this") + } + + override fun tryGetContainerUri(node: DocumentationNode): URI? { + return when (node.kind) { + NodeKind.Module -> URI("/").resolve(node.name + "/") + NodeKind.Package -> tryGetContainerUri(node.getOwnerOrReport())?.resolve(node.name.replace('.', '/') + '/') + NodeKind.GroupNode -> tryGetContainerUri(node.getOwnerOrReport()) + in NodeKind.classLike -> tryGetContainerUri(node.getOwnerOrReport())?.resolve("${node.classNodeNameWithOuterClass()}.html") + else -> null + } + } + + override fun tryGetMainUri(node: DocumentationNode): URI? { + return when (node.kind) { + NodeKind.Package -> tryGetContainerUri(node)?.resolve("package-summary.html") + in NodeKind.classLike -> tryGetContainerUri(node)?.resolve("#") + in NodeKind.memberLike -> { + val owner = if (node.owner?.kind != NodeKind.ExternalClass) node.owner else node.owner?.owner + if (owner!!.kind in classLike && + (node.kind == NodeKind.CompanionObjectProperty || node.kind == NodeKind.CompanionObjectFunction) && + owner.companion != null + ) { + val signature = node.detail(NodeKind.Signature) + val originalFunction = owner.companion!!.members.first { it.detailOrNull(NodeKind.Signature)?.name == signature.name } + tryGetMainUri(owner.companion!!)?.resolveInPage(originalFunction) + } else { + tryGetMainUri(owner)?.resolveInPage(node) + } + } + NodeKind.TypeParameter, NodeKind.Parameter -> node.path.asReversed().drop(1).firstNotNullResult(this::tryGetMainUri)?.resolveInPage(node) + NodeKind.AllTypes -> outlineRootUri(node).resolve ("classes.html") + else -> null + } + } + + override fun tryGetOutlineRootUri(node: DocumentationNode): URI? { + return when(node.kind) { + NodeKind.AllTypes -> tryGetContainerUri(node.getOwnerOrReport()) + else -> tryGetContainerUri(node) + }?.resolve(outlineRoot) + } + + fun URI.resolveInPage(node: DocumentationNode): URI = resolve("#${node.signatureForAnchor(logger).anchorEncoded()}") + + fun buildClass(node: DocumentationNode, parentDir: File) { + val fileForClass = parentDir.resolve(node.classNodeNameWithOuterClass() + ".html") + fileForClass.bufferedWriter().use { + createOutputBuilderForNode(node, it).generatePage(Page.ClassPage(node)) + } + for (memberClass in node.members.filter { it.kind in NodeKind.classLike }) { + buildClass(memberClass, parentDir) + } + } + + fun buildPackage(node: DocumentationNode, parentDir: File) { + assert(node.kind == NodeKind.Package) + var members = node.members + val directoryForPackage = parentDir.resolve(node.name.replace('.', File.separatorChar)) + directoryForPackage.mkdirsOrFail() + + directoryForPackage.resolve("package-summary.html").bufferedWriter().use { + createOutputBuilderForNode(node, it).generatePage(Page.PackagePage(node)) + } + + members.filter { it.kind == NodeKind.GroupNode }.forEach { + members += it.members + } + members.filter { it.kind in NodeKind.classLike }.forEach { + buildClass(it, directoryForPackage) + } + } + + fun buildClassIndex(node: DocumentationNode, parentDir: File) { + val file = parentDir.resolve("classes.html") + file.bufferedWriter().use { + createOutputBuilderForNode(node, it).generatePage(Page.ClassIndex(node)) + } + } + + fun buildPackageIndex(module: DocumentationNode, nodes: List<DocumentationNode>, parentDir: File) { + val file = parentDir.resolve("packages.html") + file.bufferedWriter().use { + val uri = outlineRootUri(module).resolve("packages.html") + outputBuilderFactoryService.createOutputBuilder(it, uri) + .generatePage(Page.PackageIndex(nodes)) + } + } + + override fun buildPages(nodes: Iterable<DocumentationNode>) { + val module = nodes.single() + + val moduleRoot = root.resolve(module.name) + val packages = module.members.filter { it.kind == NodeKind.Package } + packages.forEach { buildPackage(it, moduleRoot) } + val outlineRootFile = moduleRoot.resolve(outlineRoot) + if (options.generateClassIndexPage) { + buildClassIndex(module.members.single { it.kind == NodeKind.AllTypes }, outlineRootFile) + } + + if (options.generatePackageIndexPage) { + buildPackageIndex(module, packages, outlineRootFile) + } + } + + override fun buildOutlines(nodes: Iterable<DocumentationNode>) { + val uriToWriter = mutableMapOf<URI, BufferedWriter>() + + fun provideOutput(uri: URI): BufferedWriter { + val normalized = uri.normalize() + uriToWriter[normalized]?.let { return it } + val file = root.resolve(normalized.path.removePrefix("/")) + file.parentFile.mkdirsOrFail() + val writer = file.bufferedWriter() + uriToWriter[normalized] = writer + return writer + } + + outlineFactoryService?.generateOutlines(::provideOutput, nodes) + + uriToWriter.values.forEach { it.close() } + } + + override fun buildSupportFiles() {} + + override fun buildPackageList(nodes: Iterable<DocumentationNode>) { + nodes.filter { it.kind == NodeKind.Module }.forEach { module -> + val moduleRoot = root.resolve(module.name) + val packageListFile = moduleRoot.resolve("package-list") + packageListFile.writeText(packageListService.formatPackageList(module as DocumentationModule)) + } + } +} + +interface JavaLayoutHtmlFormatOutputBuilderFactory { + fun createOutputBuilder(output: Appendable, uri: URI): JavaLayoutHtmlFormatOutputBuilder + fun createOutputBuilder(output: Appendable, node: DocumentationNode): JavaLayoutHtmlFormatOutputBuilder +} diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt new file mode 100644 index 000000000..ce05fe897 --- /dev/null +++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt @@ -0,0 +1,154 @@ +package org.jetbrains.dokka.Formats + +import com.intellij.psi.PsiMember +import com.intellij.psi.PsiParameter +import org.jetbrains.dokka.* +import org.jetbrains.dokka.ExternalDocumentationLinkResolver.Companion.DOKKA_PARAM_PREFIX +import org.jetbrains.kotlin.asJava.toLightElements +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor +import org.jetbrains.kotlin.load.java.descriptors.JavaMethodDescriptor +import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe +import org.jetbrains.kotlin.resolve.descriptorUtil.isCompanionObject +import org.jetbrains.kotlin.types.KotlinType + +class JavaLayoutHtmlPackageListService: PackageListService { + + private fun StringBuilder.appendParam(name: String, value: String) { + append(DOKKA_PARAM_PREFIX) + append(name) + append(":") + appendln(value) + } + + override fun formatPackageList(module: DocumentationModule): String { + val packages = module.members(NodeKind.Package).map { it.name } + + return buildString { + appendParam("format", "java-layout-html") + appendParam("mode", "kotlin") + for (p in packages) { + appendln(p) + } + } + } + +} + +class JavaLayoutHtmlInboundLinkResolutionService(private val paramMap: Map<String, List<String>>, + private val resolutionFacade: DokkaResolutionFacade) : InboundExternalLinkResolutionService { + + constructor(asJava: Boolean, resolutionFacade: DokkaResolutionFacade) : + this(mapOf("mode" to listOf(if (asJava) "java" else "kotlin")), resolutionFacade) + + + private val isJavaMode = paramMap["mode"]!!.single() == "java" + + private fun getContainerPath(symbol: DeclarationDescriptor): String? { + return when (symbol) { + is PackageFragmentDescriptor -> symbol.fqName.asString().replace('.', '/') + "/" + is ClassifierDescriptor -> getContainerPath(symbol.findPackage()) + symbol.nameWithOuter() + ".html" + else -> null + } + } + + private fun DeclarationDescriptor.findPackage(): PackageFragmentDescriptor = + generateSequence(this) { it.containingDeclaration }.filterIsInstance<PackageFragmentDescriptor>().first() + + private fun ClassifierDescriptor.nameWithOuter(): String = + generateSequence(this) { it.containingDeclaration as? ClassifierDescriptor } + .toList().asReversed().joinToString(".") { it.name.asString() } + + private fun getJavaPagePath(symbol: DeclarationDescriptor): String? { + + val sourcePsi = symbol.sourcePsi() ?: return null + val source = (if (sourcePsi is KtDeclaration) { + sourcePsi.toLightElements().firstOrNull() + } else { + sourcePsi + }) as? PsiMember ?: return null + val desc = source.getJavaMemberDescriptor(resolutionFacade) ?: return null + return getPagePath(desc) + } + + private fun getPagePath(symbol: DeclarationDescriptor): String? { + return when (symbol) { + is PackageFragmentDescriptor -> getContainerPath(symbol) + "package-summary.html" + is EnumEntrySyntheticClassDescriptor -> getContainerPath(symbol.containingDeclaration) + "#" + symbol.signatureForAnchorUrlEncoded() + is ClassifierDescriptor -> getContainerPath(symbol) + "#" + is FunctionDescriptor, is PropertyDescriptor -> getContainerPath(symbol.containingDeclaration!!) + "#" + symbol.signatureForAnchorUrlEncoded() + else -> null + } + } + + private fun DeclarationDescriptor.signatureForAnchor(): String? { + + fun ReceiverParameterDescriptor.extractReceiverName(): String { + var receiverClass: DeclarationDescriptor = type.constructor.declarationDescriptor!! + if (receiverClass.isCompanionObject()) { + receiverClass = receiverClass.containingDeclaration!! + } else if (receiverClass is TypeParameterDescriptor) { + val upperBoundClass = receiverClass.upperBounds.singleOrNull()?.constructor?.declarationDescriptor + if (upperBoundClass != null) { + receiverClass = upperBoundClass + } + } + + return receiverClass.name.asString() + } + + fun KotlinType.qualifiedNameForSignature(): String { + val desc = constructor.declarationDescriptor + return desc?.fqNameUnsafe?.asString() ?: "<ERROR TYPE NAME>" + } + + fun StringBuilder.appendReceiverAndCompanion(desc: CallableDescriptor) { + if (desc.containingDeclaration.isCompanionObject()) { + append("Companion.") + } + desc.extensionReceiverParameter?.let { + append("(") + append(it.extractReceiverName()) + append(").") + } + } + + return when (this) { + is EnumEntrySyntheticClassDescriptor -> buildString { + append("ENUM_VALUE:") + append(name.asString()) + } + is JavaMethodDescriptor -> buildString { + append(name.asString()) + valueParameters.joinTo(this, prefix = "(", postfix = ")") { + val param = it.sourcePsi() as PsiParameter + param.type.canonicalText + } + } + is JavaPropertyDescriptor -> buildString { + append(name.asString()) + } + is FunctionDescriptor -> buildString { + appendReceiverAndCompanion(this@signatureForAnchor) + append(name.asString()) + valueParameters.joinTo(this, prefix = "(", postfix = ")") { + it.type.qualifiedNameForSignature() + } + } + is PropertyDescriptor -> buildString { + appendReceiverAndCompanion(this@signatureForAnchor) + append(name.asString()) + append(":") + + append(returnType?.qualifiedNameForSignature()) + } + else -> null + } + } + + private fun DeclarationDescriptor.signatureForAnchorUrlEncoded(): String? = signatureForAnchor()?.anchorEncoded() + + override fun getPath(symbol: DeclarationDescriptor) = if (isJavaMode) getJavaPagePath(symbol) else getPagePath(symbol) +} diff --git a/core/src/main/kotlin/Formats/JekyllFormatService.kt b/core/src/main/kotlin/Formats/JekyllFormatService.kt new file mode 100644 index 000000000..a948dfa93 --- /dev/null +++ b/core/src/main/kotlin/Formats/JekyllFormatService.kt @@ -0,0 +1,44 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName + +open class JekyllOutputBuilder(to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String>) + : MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) { + override fun appendNodes(nodes: Iterable<DocumentationNode>) { + to.appendln("---") + appendFrontMatter(nodes, to) + to.appendln("---") + to.appendln("") + super.appendNodes(nodes) + } + + protected open fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) { + to.appendln("title: ${getPageTitle(nodes)}") + } +} + + +open class JekyllFormatService( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + linkExtension: String, + impliedPlatforms: List<String> +) : MarkdownFormatService(generator, signatureGenerator, linkExtension, impliedPlatforms) { + + @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String> + ) : this(generator, signatureGenerator, "html", impliedPlatforms) + + override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder = + JekyllOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) + +} diff --git a/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt new file mode 100644 index 000000000..a98002d49 --- /dev/null +++ b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt @@ -0,0 +1,224 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName +import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty + + +open class KotlinWebsiteOutputBuilder( + to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String> +) : JekyllOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) { + private var needHardLineBreaks = false + private var insideDiv = 0 + + override fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) { + super.appendFrontMatter(nodes, to) + to.appendln("layout: api") + } + + override fun appendBreadcrumbs(path: Iterable<FormatLink>) { + if (path.count() > 1) { + to.append("<div class='api-docs-breadcrumbs'>") + super.appendBreadcrumbs(path) + to.append("</div>") + } + } + + override fun appendCode(body: () -> Unit) = wrapIfNotEmpty("<code>", "</code>", body) + + override fun appendStrikethrough(body: () -> Unit) = wrapInTag("s", body) + + protected fun div(to: StringBuilder, cssClass: String, otherAttributes: String = "", markdown: Boolean = false, block: () -> Unit) { + to.append("<div class=\"$cssClass\"$otherAttributes") + if (markdown) to.append(" markdown=\"1\"") + to.append(">") + if (!markdown) insideDiv++ + block() + if (!markdown) insideDiv-- + to.append("</div>\n") + } + + override fun appendAsSignature(node: ContentNode, block: () -> Unit) { + val contentLength = node.textLength + if (contentLength == 0) return + div(to, "signature") { + needHardLineBreaks = contentLength >= 62 + try { + block() + } finally { + needHardLineBreaks = false + } + } + } + + override fun appendAsOverloadGroup(to: StringBuilder, platforms: Set<String>, block: () -> Unit) { + div(to, "overload-group", calculateDataAttributes(platforms), true) { + ensureParagraph() + block() + ensureParagraph() + } + } + + override fun appendLink(href: String, body: () -> Unit) = wrap("<a href=\"$href\">", "</a>", body) + + override fun appendHeader(level: Int, body: () -> Unit) { + if (insideDiv > 0) { + wrapInTag("p", body, newlineAfterClose = true) + } else { + super.appendHeader(level, body) + } + } + + override fun appendLine() { + if (insideDiv > 0) { + to.appendln("<br/>") + } else { + super.appendLine() + } + } + + override fun appendTable(vararg columns: String, body: () -> Unit) { + to.appendln("<table class=\"api-docs-table\">") + body() + to.appendln("</table>") + } + + override fun appendTableBody(body: () -> Unit) { + to.appendln("<tbody>") + body() + to.appendln("</tbody>") + } + + override fun appendTableRow(body: () -> Unit) { + to.appendln("<tr>") + body() + to.appendln("</tr>") + } + + override fun appendTableCell(body: () -> Unit) { + to.appendln("<td markdown=\"1\">") + body() + to.appendln("\n</td>") + } + + override fun appendBlockCode(language: String, body: () -> Unit) { + if (language.isNotEmpty()) { + super.appendBlockCode(language, body) + } else { + wrap("<pre markdown=\"1\">", "</pre>", body) + } + } + + override fun appendSymbol(text: String) { + to.append("<span class=\"symbol\">${text.htmlEscape()}</span>") + } + + override fun appendKeyword(text: String) { + to.append("<span class=\"keyword\">${text.htmlEscape()}</span>") + } + + override fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) { + val id = signature?.let { " id=\"$it\"" }.orEmpty() + to.append("<span class=\"${identifierClassName(kind)}\"$id>${text.htmlEscape()}</span>") + } + + override fun appendSoftLineBreak() { + if (needHardLineBreaks) + to.append("<br/>") + + } + + override fun appendIndentedSoftLineBreak() { + if (needHardLineBreaks) { + to.append("<br/> ") + } + } + + private fun identifierClassName(kind: IdentifierKind) = when (kind) { + IdentifierKind.ParameterName -> "parameterName" + IdentifierKind.SummarizedTypeName -> "summarizedTypeName" + else -> "identifier" + } + + fun calculateDataAttributes(platforms: Set<String>): String { + fun String.isKotlinVersion() = this.startsWith("Kotlin") + fun String.isJREVersion() = this.startsWith("JRE") + val kotlinVersion = platforms.singleOrNull(String::isKotlinVersion) + val jreVersion = platforms.singleOrNull(String::isJREVersion) + val targetPlatforms = platforms.filterNot { it.isKotlinVersion() || it.isJREVersion() } + + val kotlinVersionAttr = kotlinVersion?.let { " data-kotlin-version=\"$it\"" } ?: "" + val jreVersionAttr = jreVersion?.let { " data-jre-version=\"$it\"" } ?: "" + val platformsAttr = targetPlatforms.ifNotEmpty { " data-platform=\"${targetPlatforms.joinToString()}\"" } ?: "" + return "$platformsAttr$kotlinVersionAttr$jreVersionAttr" + } + + override fun appendIndexRow(platforms: Set<String>, block: () -> Unit) { + if (platforms.isNotEmpty()) + wrap("<tr${calculateDataAttributes(platforms)}>", "</tr>", block) + else + appendTableRow(block) + } + + override fun appendPlatforms(platforms: Set<String>) { + + } +} + +class KotlinWebsiteFormatService @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String>, + logger: DokkaLogger +) : JekyllFormatService(generator, signatureGenerator, "html", impliedPlatforms) { + init { + logger.warn("Format kotlin-website deprecated and will be removed in next release") + } + + override fun createOutputBuilder(to: StringBuilder, location: Location) = + KotlinWebsiteOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +} + + +class KotlinWebsiteRunnableSamplesOutputBuilder( + to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String> +) : KotlinWebsiteOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) { + + override fun appendSampleBlockCode(language: String, imports: () -> Unit, body: () -> Unit) { + div(to, "sample", markdown = true) { + appendBlockCode(language) { + imports() + wrap("\n\nfun main(args: Array<String>) {", "}") { + wrap("\n//sampleStart\n", "\n//sampleEnd\n", body) + } + } + } + } +} + +class KotlinWebsiteRunnableSamplesFormatService @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String>, + logger: DokkaLogger +) : JekyllFormatService(generator, signatureGenerator, "html", impliedPlatforms) { + + init { + logger.warn("Format kotlin-website-samples deprecated and will be removed in next release") + } + + override fun createOutputBuilder(to: StringBuilder, location: Location) = + KotlinWebsiteRunnableSamplesOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +} + diff --git a/core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt b/core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt new file mode 100644 index 000000000..6ced75b55 --- /dev/null +++ b/core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt @@ -0,0 +1,186 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName +import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty +import java.io.File + + +object EmptyHtmlTemplateService : HtmlTemplateService { + override fun appendFooter(to: StringBuilder) {} + + override fun appendHeader(to: StringBuilder, title: String?, basePath: File) {} +} + + +open class KotlinWebsiteHtmlOutputBuilder( + to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String>, + templateService: HtmlTemplateService +) : HtmlOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms, templateService) { + private var needHardLineBreaks = false + private var insideDiv = 0 + + override fun appendLine() {} + + override fun appendBreadcrumbs(path: Iterable<FormatLink>) { + if (path.count() > 1) { + to.append("<div class='api-docs-breadcrumbs'>") + super.appendBreadcrumbs(path) + to.append("</div>") + } + } + + override fun appendCode(body: () -> Unit) = wrapIfNotEmpty("<code>", "</code>", body) + + protected fun div(to: StringBuilder, cssClass: String, otherAttributes: String = "", block: () -> Unit) { + to.append("<div class=\"$cssClass\"$otherAttributes") + to.append(">") + insideDiv++ + block() + insideDiv-- + to.append("</div>\n") + } + + override fun appendAsSignature(node: ContentNode, block: () -> Unit) { + val contentLength = node.textLength + if (contentLength == 0) return + div(to, "signature") { + needHardLineBreaks = contentLength >= 62 + try { + block() + } finally { + needHardLineBreaks = false + } + } + } + + override fun appendAsOverloadGroup(to: StringBuilder, platforms: Set<String>, block: () -> Unit) { + div(to, "overload-group", calculateDataAttributes(platforms)) { + block() + } + } + + override fun appendLink(href: String, body: () -> Unit) = wrap("<a href=\"$href\">", "</a>", body) + + override fun appendTable(vararg columns: String, body: () -> Unit) { + to.appendln("<table class=\"api-docs-table\">") + body() + to.appendln("</table>") + } + + override fun appendTableBody(body: () -> Unit) { + to.appendln("<tbody>") + body() + to.appendln("</tbody>") + } + + override fun appendTableRow(body: () -> Unit) { + to.appendln("<tr>") + body() + to.appendln("</tr>") + } + + override fun appendTableCell(body: () -> Unit) { + to.appendln("<td>") + body() + to.appendln("\n</td>") + } + + override fun appendSymbol(text: String) { + to.append("<span class=\"symbol\">${text.htmlEscape()}</span>") + } + + override fun appendKeyword(text: String) { + to.append("<span class=\"keyword\">${text.htmlEscape()}</span>") + } + + override fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) { + val id = signature?.let { " id=\"$it\"" }.orEmpty() + to.append("<span class=\"${identifierClassName(kind)}\"$id>${text.htmlEscape()}</span>") + } + + override fun appendSoftLineBreak() { + if (needHardLineBreaks) + to.append("<br/>") + } + + override fun appendIndentedSoftLineBreak() { + if (needHardLineBreaks) { + to.append("<br/> ") + } + } + + private fun identifierClassName(kind: IdentifierKind) = when (kind) { + IdentifierKind.ParameterName -> "parameterName" + IdentifierKind.SummarizedTypeName -> "summarizedTypeName" + else -> "identifier" + } + + fun calculateDataAttributes(platforms: Set<String>): String { + fun String.isKotlinVersion() = this.startsWith("Kotlin") + fun String.isJREVersion() = this.startsWith("JRE") + val kotlinVersion = platforms.singleOrNull(String::isKotlinVersion) + val jreVersion = platforms.singleOrNull(String::isJREVersion) + val targetPlatforms = platforms.filterNot { it.isKotlinVersion() || it.isJREVersion() } + + val kotlinVersionAttr = kotlinVersion?.let { " data-kotlin-version=\"$it\"" } ?: "" + val jreVersionAttr = jreVersion?.let { " data-jre-version=\"$it\"" } ?: "" + val platformsAttr = targetPlatforms.ifNotEmpty { " data-platform=\"${targetPlatforms.joinToString()}\"" } ?: "" + return "$platformsAttr$kotlinVersionAttr$jreVersionAttr" + } + + override fun appendIndexRow(platforms: Set<String>, block: () -> Unit) { + if (platforms.isNotEmpty()) + wrap("<tr${calculateDataAttributes(platforms)}>", "</tr>", block) + else + appendTableRow(block) + } + + override fun appendPlatforms(platforms: Set<String>) {} + + override fun appendBreadcrumbSeparator() { + to.append(" / ") + } + + override fun appendSampleBlockCode(language: String, imports: () -> Unit, body: () -> Unit) { + div(to, "sample") { + appendBlockCode(language) { + imports() + wrap("\n\nfun main(args: Array<String>) {".htmlEscape(), "}") { + wrap("\n//sampleStart\n", "\n//sampleEnd\n", body) + } + } + } + } + + override fun appendSoftParagraph(body: () -> Unit) = appendParagraph(body) + + + override fun appendSectionWithTag(section: ContentSection) { + appendParagraph { + appendStrong { appendText(section.tag) } + appendText(" ") + appendContent(section) + } + } +} + +class KotlinWebsiteHtmlFormatService @Inject constructor( + generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String>, + templateService: HtmlTemplateService +) : HtmlFormatService(generator, signatureGenerator, templateService, impliedPlatforms) { + + override fun enumerateSupportFiles(callback: (String, String) -> Unit) {} + + override fun createOutputBuilder(to: StringBuilder, location: Location) = + KotlinWebsiteHtmlOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms, templateService) +} + diff --git a/core/src/main/kotlin/Formats/MarkdownFormatService.kt b/core/src/main/kotlin/Formats/MarkdownFormatService.kt new file mode 100644 index 000000000..4265394f2 --- /dev/null +++ b/core/src/main/kotlin/Formats/MarkdownFormatService.kt @@ -0,0 +1,239 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.dokka.Utilities.impliedPlatformsName +import java.util.* + +enum class ListKind { + Ordered, + Unordered +} + +private class ListState(val kind: ListKind, var size: Int = 1) { + fun getTagAndIncrement() = when (kind) { + ListKind.Ordered -> "${size++}. " + else -> "* " + } +} + +private val TWO_LINE_BREAKS = System.lineSeparator() + System.lineSeparator() + +open class MarkdownOutputBuilder(to: StringBuilder, + location: Location, + generator: NodeLocationAwareGenerator, + languageService: LanguageService, + extension: String, + impliedPlatforms: List<String>) + : StructuredOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +{ + private val listStack = ArrayDeque<ListState>() + protected var inTableCell = false + protected var inCodeBlock = false + private var lastTableCellStart = -1 + private var maxBackticksInCodeBlock = 0 + + private fun appendNewline() { + while (to.endsWith(' ')) { + to.setLength(to.length - 1) + } + to.appendln() + } + + private fun ensureNewline() { + if (inTableCell && listStack.isEmpty()) { + if (to.length != lastTableCellStart && !to.endsWith("<br>")) { + to.append("<br>") + } + } + else { + if (!endsWithNewline()) { + appendNewline() + } + } + } + + private fun endsWithNewline(): Boolean { + var index = to.length - 1 + while (index > 0) { + val c = to[index] + if (c != ' ') { + return c == '\n' + } + index-- + } + return false + } + + override fun ensureParagraph() { + if (!to.endsWith(TWO_LINE_BREAKS)) { + if (!to.endsWith('\n')) { + appendNewline() + } + appendNewline() + } + } + override fun appendBreadcrumbSeparator() { + to.append(" / ") + } + + private val backTickFindingRegex = """(`+)""".toRegex() + + override fun appendText(text: String) { + if (inCodeBlock) { + to.append(text) + val backTicks = backTickFindingRegex.findAll(text) + val longestBackTickRun = backTicks.map { it.value.length }.max() ?: 0 + maxBackticksInCodeBlock = maxBackticksInCodeBlock.coerceAtLeast(longestBackTickRun) + } + else { + if (text == "\n" && inTableCell) { + to.append(" ") + } else { + to.append(text.htmlEscape()) + } + } + } + + override fun appendCode(body: () -> Unit) { + inCodeBlock = true + val codeBlockStart = to.length + maxBackticksInCodeBlock = 0 + + wrapIfNotEmpty("`", "`", body, checkEndsWith = true) + + if (maxBackticksInCodeBlock > 0) { + val extraBackticks = "`".repeat(maxBackticksInCodeBlock) + to.insert(codeBlockStart, extraBackticks) + to.append(extraBackticks) + } + + inCodeBlock = false + } + + override fun appendUnorderedList(body: () -> Unit) { + listStack.push(ListState(ListKind.Unordered)) + body() + listStack.pop() + ensureNewline() + } + + override fun appendOrderedList(body: () -> Unit) { + listStack.push(ListState(ListKind.Ordered)) + body() + listStack.pop() + ensureNewline() + } + + override fun appendListItem(body: () -> Unit) { + ensureNewline() + to.append(listStack.peek()?.getTagAndIncrement()) + body() + ensureNewline() + } + + override fun appendStrong(body: () -> Unit) = wrap("**", "**", body) + override fun appendEmphasis(body: () -> Unit) = wrap("*", "*", body) + override fun appendStrikethrough(body: () -> Unit) = wrap("~~", "~~", body) + + override fun appendLink(href: String, body: () -> Unit) { + if (inCodeBlock) { + wrap("`[`", "`]($href)`", body) + } + else { + wrap("[", "]($href)", body) + } + } + + override fun appendLine() { + if (inTableCell) { + to.append("<br>") + } + else { + appendNewline() + } + } + + override fun appendAnchor(anchor: String) { + // no anchors in Markdown + } + + override fun appendParagraph(body: () -> Unit) { + if (inTableCell) { + ensureNewline() + body() + } else if (listStack.isNotEmpty()) { + body() + ensureNewline() + } else { + ensureParagraph() + body() + ensureParagraph() + } + } + + override fun appendHeader(level: Int, body: () -> Unit) { + ensureParagraph() + to.append("${"#".repeat(level)} ") + body() + ensureParagraph() + } + + override fun appendBlockCode(language: String, body: () -> Unit) { + inCodeBlock = true + ensureParagraph() + to.appendln(if (language.isEmpty()) "```" else "``` $language") + body() + ensureNewline() + to.appendln("```") + appendLine() + inCodeBlock = false + } + + override fun appendTable(vararg columns: String, body: () -> Unit) { + ensureParagraph() + body() + ensureParagraph() + } + + override fun appendTableBody(body: () -> Unit) { + body() + } + + override fun appendTableRow(body: () -> Unit) { + to.append("|") + body() + appendNewline() + } + + override fun appendTableCell(body: () -> Unit) { + to.append(" ") + inTableCell = true + lastTableCellStart = to.length + body() + inTableCell = false + to.append(" |") + } + + override fun appendNonBreakingSpace() { + if (inCodeBlock) { + to.append(" ") + } + else { + to.append(" ") + } + } +} + +open class MarkdownFormatService(generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + linkExtension: String, + val impliedPlatforms: List<String>) +: StructuredFormatService(generator, signatureGenerator, "md", linkExtension) { + @Inject constructor(generator: NodeLocationAwareGenerator, + signatureGenerator: LanguageService, + @Named(impliedPlatformsName) impliedPlatforms: List<String>): this(generator, signatureGenerator, "md", impliedPlatforms) + + override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder = + MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) +} diff --git a/core/src/main/kotlin/Formats/OutlineService.kt b/core/src/main/kotlin/Formats/OutlineService.kt new file mode 100644 index 000000000..958e93aff --- /dev/null +++ b/core/src/main/kotlin/Formats/OutlineService.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka + +import java.io.File + +/** + * Service for building the outline of the package contents. + */ +interface OutlineFormatService { + fun getOutlineFileName(location: Location): File + + fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) + fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) + + /** Appends formatted outline to [StringBuilder](to) using specified [location] */ + fun appendOutline(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + for (node in nodes) { + appendOutlineHeader(location, node, to) + if (node.members.any()) { + val sortedMembers = node.members.sortedBy { it.name.toLowerCase() } + appendOutlineLevel(to) { + appendOutline(location, to, sortedMembers) + } + } + } + } + + fun formatOutline(location: Location, nodes: Iterable<DocumentationNode>): String = + StringBuilder().apply { appendOutline(location, this, nodes) }.toString() +} diff --git a/core/src/main/kotlin/Formats/PackageListService.kt b/core/src/main/kotlin/Formats/PackageListService.kt new file mode 100644 index 000000000..7b68098e8 --- /dev/null +++ b/core/src/main/kotlin/Formats/PackageListService.kt @@ -0,0 +1,63 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject + + +interface PackageListService { + fun formatPackageList(module: DocumentationModule): String +} + +class DefaultPackageListService @Inject constructor( + val generator: NodeLocationAwareGenerator, + val formatService: FormatService +) : PackageListService { + + override fun formatPackageList(module: DocumentationModule): String { + val packages = mutableSetOf<String>() + val nonStandardLocations = mutableMapOf<String, String>() + + fun visit(node: DocumentationNode, relocated: Boolean = false) { + val nodeKind = node.kind + + when (nodeKind) { + NodeKind.Package -> { + packages.add(node.qualifiedName()) + node.members.forEach { visit(it) } + } + NodeKind.Signature -> { + if (relocated) + nonStandardLocations[node.name] = generator.relativePathToLocation(module, node.owner!!) + } + NodeKind.ExternalClass -> { + node.members.forEach { visit(it, relocated = true) } + } + NodeKind.GroupNode -> { + //only children of top-level GN records interesting for us, since link to top-level ones should point to GN + node.members.forEach { it.members.forEach { visit(it, relocated = true) } } + //record signature of GN as signature of type alias and class merged to GN, so link to it should point to GN + node.detailOrNull(NodeKind.Signature)?.let { visit(it, relocated = true) } + } + else -> { + if (nodeKind in NodeKind.classLike || nodeKind in NodeKind.memberLike) { + node.details(NodeKind.Signature).forEach { visit(it, relocated) } + node.members.forEach { visit(it, relocated) } + } + } + } + } + + module.members.forEach { visit(it) } + + return buildString { + appendln("\$dokka.linkExtension:${formatService.linkExtension}") + + nonStandardLocations.map { (signature, location) -> "\$dokka.location:$signature\u001f$location" } + .sorted().joinTo(this, separator = "\n", postfix = "\n") + + packages.sorted().joinTo(this, separator = "\n", postfix = "\n") + } + + } + +} + diff --git a/core/src/main/kotlin/Formats/StandardFormats.kt b/core/src/main/kotlin/Formats/StandardFormats.kt new file mode 100644 index 000000000..dd67ac972 --- /dev/null +++ b/core/src/main/kotlin/Formats/StandardFormats.kt @@ -0,0 +1,66 @@ +package org.jetbrains.dokka.Formats + +import com.google.inject.Binder +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Samples.KotlinWebsiteSampleProcessingService +import org.jetbrains.dokka.Utilities.bind +import kotlin.reflect.KClass + +abstract class KotlinFormatDescriptorBase + : FileGeneratorBasedFormatDescriptor(), + DefaultAnalysisComponent, + DefaultAnalysisComponentServices by KotlinAsKotlin { + override val generatorServiceClass = FileGenerator::class + override val outlineServiceClass: KClass<out OutlineFormatService>? = null + override val packageListServiceClass: KClass<out PackageListService>? = DefaultPackageListService::class +} + +abstract class HtmlFormatDescriptorBase : FileGeneratorBasedFormatDescriptor(), DefaultAnalysisComponent { + override val formatServiceClass = HtmlFormatService::class + override val outlineServiceClass = HtmlFormatService::class + override val generatorServiceClass = FileGenerator::class + override val packageListServiceClass = DefaultPackageListService::class + + override fun configureOutput(binder: Binder): Unit = with(binder) { + super.configureOutput(binder) + bind<HtmlTemplateService>().toProvider { HtmlTemplateService.default("style.css") } + } +} + +class HtmlFormatDescriptor : HtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin + +class HtmlAsJavaFormatDescriptor : HtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava + +class KotlinWebsiteFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = KotlinWebsiteFormatService::class + override val outlineServiceClass = YamlOutlineService::class +} + +class KotlinWebsiteFormatRunnableSamplesDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = KotlinWebsiteRunnableSamplesFormatService::class + override val sampleProcessingService = KotlinWebsiteSampleProcessingService::class + override val outlineServiceClass = YamlOutlineService::class +} + +class KotlinWebsiteHtmlFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = KotlinWebsiteHtmlFormatService::class + override val sampleProcessingService = KotlinWebsiteSampleProcessingService::class + override val outlineServiceClass = YamlOutlineService::class + + override fun configureOutput(binder: Binder) = with(binder) { + super.configureOutput(binder) + bind<HtmlTemplateService>().toInstance(EmptyHtmlTemplateService) + } +} + +class JekyllFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = JekyllFormatService::class +} + +class MarkdownFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = MarkdownFormatService::class +} + +class GFMFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = GFMFormatService::class +} diff --git a/core/src/main/kotlin/Formats/StructuredFormatService.kt b/core/src/main/kotlin/Formats/StructuredFormatService.kt new file mode 100644 index 000000000..a2c9078f3 --- /dev/null +++ b/core/src/main/kotlin/Formats/StructuredFormatService.kt @@ -0,0 +1,691 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.LanguageService.RenderMode +import java.util.* + +data class FormatLink(val text: String, val href: String) + +abstract class StructuredOutputBuilder(val to: StringBuilder, + val location: Location, + val generator: NodeLocationAwareGenerator, + val languageService: LanguageService, + val extension: String, + val impliedPlatforms: List<String>) : FormattedOutputBuilder { + + protected fun DocumentationNode.location() = generator.location(this) + + protected fun wrap(prefix: String, suffix: String, body: () -> Unit) { + to.append(prefix) + body() + to.append(suffix) + } + + protected fun wrapIfNotEmpty(prefix: String, suffix: String, body: () -> Unit, checkEndsWith: Boolean = false) { + val startLength = to.length + to.append(prefix) + body() + if (checkEndsWith && to.endsWith(suffix)) { + to.setLength(to.length - suffix.length) + } else if (to.length > startLength + prefix.length) { + to.append(suffix) + } else { + to.setLength(startLength) + } + } + + protected fun wrapInTag(tag: String, + body: () -> Unit, + newlineBeforeOpen: Boolean = false, + newlineAfterOpen: Boolean = false, + newlineAfterClose: Boolean = false) { + if (newlineBeforeOpen && !to.endsWith('\n')) to.appendln() + to.append("<$tag>") + if (newlineAfterOpen) to.appendln() + body() + to.append("</$tag>") + if (newlineAfterClose) to.appendln() + } + + protected abstract fun ensureParagraph() + + open fun appendSampleBlockCode(language: String, imports: () -> Unit, body: () -> Unit) = appendBlockCode(language, body) + abstract fun appendBlockCode(language: String, body: () -> Unit) + abstract fun appendHeader(level: Int = 1, body: () -> Unit) + abstract fun appendParagraph(body: () -> Unit) + + open fun appendSoftParagraph(body: () -> Unit) { + ensureParagraph() + body() + } + + abstract fun appendLine() + abstract fun appendAnchor(anchor: String) + + abstract fun appendTable(vararg columns: String, body: () -> Unit) + abstract fun appendTableBody(body: () -> Unit) + abstract fun appendTableRow(body: () -> Unit) + abstract fun appendTableCell(body: () -> Unit) + + abstract fun appendText(text: String) + + open fun appendSinceKotlin(version: String) { + appendParagraph { + appendText("Available since Kotlin: ") + appendCode { appendText(version) } + } + } + + open fun appendSectionWithTag(section: ContentSection) { + appendParagraph { + appendStrong { appendText(section.tag) } + appendLine() + appendContent(section) + } + } + + open fun appendSymbol(text: String) { + appendText(text) + } + + open fun appendKeyword(text: String) { + appendText(text) + } + + open fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) { + appendText(text) + } + + fun appendEntity(text: String) { + to.append(text) + } + + abstract fun appendLink(href: String, body: () -> Unit) + + open fun appendLink(link: FormatLink) { + appendLink(link.href) { appendText(link.text) } + } + + abstract fun appendStrong(body: () -> Unit) + abstract fun appendStrikethrough(body: () -> Unit) + abstract fun appendEmphasis(body: () -> Unit) + abstract fun appendCode(body: () -> Unit) + abstract fun appendUnorderedList(body: () -> Unit) + abstract fun appendOrderedList(body: () -> Unit) + abstract fun appendListItem(body: () -> Unit) + + abstract fun appendBreadcrumbSeparator() + abstract fun appendNonBreakingSpace() + open fun appendSoftLineBreak() { + } + + open fun appendIndentedSoftLineBreak() { + } + + fun appendContent(content: List<ContentNode>) { + for (contentNode in content) { + appendContent(contentNode) + } + } + + open fun appendContent(content: ContentNode) { + when (content) { + is ContentText -> appendText(content.text) + is ContentSymbol -> appendSymbol(content.text) + is ContentKeyword -> appendKeyword(content.text) + is ContentIdentifier -> appendIdentifier(content.text, content.kind, content.signature) + is ContentNonBreakingSpace -> appendNonBreakingSpace() + is ContentSoftLineBreak -> appendSoftLineBreak() + is ContentIndentedSoftLineBreak -> appendIndentedSoftLineBreak() + is ContentEntity -> appendEntity(content.text) + is ContentStrong -> appendStrong { appendContent(content.children) } + is ContentStrikethrough -> appendStrikethrough { appendContent(content.children) } + is ContentCode -> appendCode { appendContent(content.children) } + is ContentEmphasis -> appendEmphasis { appendContent(content.children) } + is ContentUnorderedList -> appendUnorderedList { appendContent(content.children) } + is ContentOrderedList -> appendOrderedList { appendContent(content.children) } + is ContentListItem -> appendListItem { + val child = content.children.singleOrNull() + if (child is ContentParagraph) { + appendContent(child.children) + } else { + appendContent(content.children) + } + } + + is ContentNodeLink -> { + val node = content.node + val linkTo = if (node != null) locationHref(location, node) else "#" + appendLinkIfNotThisPage(linkTo, content) + } + is ContentExternalLink -> appendLinkIfNotThisPage(content.href, content) + + is ContentParagraph -> { + if (!content.isEmpty()) { + appendParagraph { appendContent(content.children) } + } + } + + is ContentSpecialReference -> wrapInTag(tag = "aside class=\"note\"", body = { + if (!content.isEmpty()) { + appendContent(content.children) + } + }) + + is ContentBlockSampleCode, is ContentBlockCode -> { + content as ContentBlockCode + fun ContentBlockCode.appendBlockCodeContent() { + children + .dropWhile { it is ContentText && it.text.isBlank() } + .forEach { appendContent(it) } + } + when (content) { + is ContentBlockSampleCode -> + appendSampleBlockCode(content.language, content.importsBlock::appendBlockCodeContent, { content.appendBlockCodeContent() }) + is ContentBlockCode -> + appendBlockCode(content.language, { content.appendBlockCodeContent() }) + } + } + is ContentHeading -> appendHeader(content.level) { appendContent(content.children) } + is ContentBlock -> appendContent(content.children) + } + } + + private fun appendLinkIfNotThisPage(href: String, content: ContentBlock) { + if (href == ".") { + appendContent(content.children) + } else { + appendLink(href) { appendContent(content.children) } + } + } + + open fun link(from: DocumentationNode, + to: DocumentationNode, + name: (DocumentationNode) -> String = DocumentationNode::name): FormatLink = link(from, to, extension, name) + + open fun link(from: DocumentationNode, + to: DocumentationNode, + extension: String, + name: (DocumentationNode) -> String = DocumentationNode::name): FormatLink { + if (to.owner?.kind == NodeKind.GroupNode) + return link(from, to.owner!!, extension, name) + + if (from.owner?.kind == NodeKind.GroupNode) + return link(from.owner!!, to, extension, name) + + return FormatLink(name(to), from.location().relativePathTo(to.location())) + } + + fun locationHref(from: Location, to: DocumentationNode): String { + val topLevelPage = to.references(RefKind.TopLevelPage).singleOrNull()?.to + if (topLevelPage != null) { + val signature = to.detailOrNull(NodeKind.Signature) + return from.relativePathTo(topLevelPage.location(), signature?.name ?: to.name) + } + return from.relativePathTo(to.location()) + } + + private fun DocumentationNode.isModuleOrPackage(): Boolean = + kind == NodeKind.Module || kind == NodeKind.Package + + protected open fun appendAsSignature(node: ContentNode, block: () -> Unit) { + block() + } + + protected open fun appendAsOverloadGroup(to: StringBuilder, platforms: Set<String>, block: () -> Unit) { + block() + } + + protected open fun appendIndexRow(platforms: Set<String>, block: () -> Unit) { + appendTableRow(block) + } + + protected open fun appendPlatforms(platforms: Set<String>) { + if (platforms.isNotEmpty()) { + appendLine() + appendText(platforms.joinToString(prefix = "(", postfix = ")")) + } + } + + protected open fun appendBreadcrumbs(path: Iterable<FormatLink>) { + for ((index, item) in path.withIndex()) { + if (index > 0) { + appendBreadcrumbSeparator() + } + appendLink(item) + } + } + + fun Content.getSectionsWithSubjects(): Map<String, List<ContentSection>> = + sections.filter { it.subjectName != null }.groupBy { it.tag } + + private fun ContentNode.appendSignature() { + if (this is ContentBlock && this.isEmpty()) { + return + } + + val signatureAsCode = ContentCode() + signatureAsCode.append(this) + appendContent(signatureAsCode) + } + + open inner class PageBuilder(val nodes: Iterable<DocumentationNode>, val noHeader: Boolean = false) { + open fun build() { + val breakdownByLocation = nodes.groupBy { node -> + node.path.filterNot { it.name.isEmpty() }.map { link(node, it) }.distinct() + } + + for ((path, nodes) in breakdownByLocation) { + if (!noHeader && path.isNotEmpty()) { + appendBreadcrumbs(path) + appendLine() + appendLine() + } + appendLocation(nodes.filter { it.kind != NodeKind.ExternalClass }) + } + } + + private fun appendLocation(nodes: Iterable<DocumentationNode>) { + val singleNode = nodes.singleOrNull() + if (singleNode != null && singleNode.isModuleOrPackage()) { + if (singleNode.kind == NodeKind.Package) { + val packageName = if (singleNode.name.isEmpty()) "<root>" else singleNode.name + appendHeader(2) { appendText("Package $packageName") } + } + singleNode.appendPlatforms() + appendContent(singleNode.content) + } else { + val breakdownByName = nodes.groupBy { node -> node.name } + for ((name, items) in breakdownByName) { + if (!noHeader) + appendHeader { appendText(name) } + appendDocumentation(items, singleNode != null) + } + } + } + + private fun appendDocumentation(overloads: Iterable<DocumentationNode>, isSingleNode: Boolean) { + val breakdownBySummary = overloads.groupByTo(LinkedHashMap()) { node -> node.content } + + if (breakdownBySummary.size == 1) { + formatOverloadGroup(breakdownBySummary.values.single(), isSingleNode) + } else { + for ((_, items) in breakdownBySummary) { + + appendAsOverloadGroup(to, platformsOfItems(items)) { + formatOverloadGroup(items) + } + + } + } + } + + private fun formatOverloadGroup(items: List<DocumentationNode>, isSingleNode: Boolean = false) { + for ((index, item) in items.withIndex()) { + if (index > 0) appendLine() + val rendered = languageService.render(item) + item.detailOrNull(NodeKind.Signature)?.let { + if (item.kind !in NodeKind.classLike || !isSingleNode) + appendAnchor(it.name) + } + appendAsSignature(rendered) { + appendCode { appendContent(rendered) } + item.appendSourceLink() + } + item.appendOverrides() + item.appendDeprecation() + item.appendPlatforms() + } + // All items have exactly the same documentation, so we can use any item to render it + val item = items.first() + item.details(NodeKind.OverloadGroupNote).forEach { + appendContent(it.content) + } + + appendContent(item.content.summary) + item.appendDescription() + } + + private fun DocumentationNode.appendSourceLink() { + val sourceUrl = details(NodeKind.SourceUrl).firstOrNull() + if (sourceUrl != null) { + to.append(" ") + appendLink(sourceUrl.name) { to.append("(source)") } + } + } + + private fun DocumentationNode.appendOverrides() { + overrides.forEach { + appendParagraph { + to.append("Overrides ") + val location = location().relativePathTo(it.location()) + + appendLink(FormatLink(it.owner!!.name + "." + it.name, location)) + } + } + } + + private fun DocumentationNode.appendDeprecation() { + if (deprecation != null) { + val deprecationParameter = deprecation!!.details(NodeKind.Parameter).firstOrNull() + val deprecationValue = deprecationParameter?.details(NodeKind.Value)?.firstOrNull() + appendLine() + if (deprecationValue != null) { + appendStrong { to.append("Deprecated:") } + appendText(" " + deprecationValue.name.removeSurrounding("\"")) + appendLine() + appendLine() + } else if (deprecation?.content != Content.Empty) { + appendStrong { to.append("Deprecated:") } + to.append(" ") + appendContent(deprecation!!.content) + } else { + appendStrong { to.append("Deprecated") } + appendLine() + appendLine() + } + } + } + + private fun DocumentationNode.appendPlatforms() { + val platforms = if (isModuleOrPackage()) + platformsToShow.toSet() + platformsOfItems(members) + else + platformsToShow + + if (platforms.isEmpty()) return + + appendParagraph { + appendStrong { to.append("Platform and version requirements:") } + to.append(" " + platforms.joinToString()) + } + } + + protected fun platformsOfItems(items: List<DocumentationNode>): Set<String> { + val platforms = items.asSequence().map { + when (it.kind) { + NodeKind.ExternalClass, NodeKind.Package, NodeKind.Module, NodeKind.GroupNode -> platformsOfItems(it.members) + else -> it.platformsToShow.toSet() + } + } + + fun String.isKotlinVersion() = this.startsWith("Kotlin") + + // Calculating common platforms for items + return platforms.reduce { result, platformsOfItem -> + val otherKotlinVersion = result.find { it.isKotlinVersion() } + val (kotlinVersions, otherPlatforms) = platformsOfItem.partition { it.isKotlinVersion() } + + // When no Kotlin version specified, it means that version is 1.0 + if (otherKotlinVersion != null && kotlinVersions.isNotEmpty()) { + val allKotlinVersions = (kotlinVersions + otherKotlinVersion).distinct() + + val minVersion = allKotlinVersions.min()!! + val resultVersion = when { + allKotlinVersions.size == 1 -> allKotlinVersions.single() + minVersion.endsWith("+") -> minVersion + else -> minVersion + "+" + } + + result.intersect(otherPlatforms) + resultVersion + } else { + result.intersect(platformsOfItem) + } + } + } + + val DocumentationNode.platformsToShow: List<String> + get() = platforms.let { if (it.containsAll(impliedPlatforms)) it - impliedPlatforms else it } + + private fun DocumentationNode.appendDescription() { + if (content.description != ContentEmpty) { + appendContent(content.description) + } + content.getSectionsWithSubjects().forEach { + appendSectionWithSubject(it.key, it.value) + } + + for (section in content.sections.filter { it.subjectName == null }) { + appendSectionWithTag(section) + } + } + + fun appendSectionWithSubject(title: String, subjectSections: List<ContentSection>) { + appendHeader(3) { appendText(title) } + subjectSections.forEach { + val subjectName = it.subjectName + if (subjectName != null) { + appendSoftParagraph { + appendAnchor(subjectName) + appendCode { to.append(subjectName) } + to.append(" - ") + appendContent(it) + } + } + } + } + } + + inner class GroupNodePageBuilder(val node: DocumentationNode) : PageBuilder(listOf(node)) { + + override fun build() { + val breakdownByLocation = node.path.filterNot { it.name.isEmpty() }.map { link(node, it) } + + appendBreadcrumbs(breakdownByLocation) + appendLine() + appendLine() + appendHeader { appendText(node.name) } + + fun DocumentationNode.priority(): Int = when (kind) { + NodeKind.TypeAlias -> 1 + NodeKind.Class -> 2 + else -> 3 + } + + for (member in node.members.sortedBy(DocumentationNode::priority)) { + + appendAsOverloadGroup(to, platformsOfItems(listOf(member))) { + formatSubNodeOfGroup(member) + } + + } + } + + fun formatSubNodeOfGroup(member: DocumentationNode) { + SingleNodePageBuilder(member, true).build() + } + } + + inner class SingleNodePageBuilder(val node: DocumentationNode, noHeader: Boolean = false) + : PageBuilder(listOf(node), noHeader) { + + override fun build() { + super.build() + + if (node.kind == NodeKind.ExternalClass) { + appendSection("Extensions for ${node.name}", node.members) + return + } + + fun DocumentationNode.membersOrGroupMembers(predicate: (DocumentationNode) -> Boolean): List<DocumentationNode> { + return members.filter(predicate) + members(NodeKind.GroupNode).flatMap { it.members.filter(predicate) } + } + + fun DocumentationNode.membersOrGroupMembers(kind: NodeKind): List<DocumentationNode> { + return membersOrGroupMembers { it.kind == kind } + } + + appendSection("Packages", node.members(NodeKind.Package), platformsBasedOnMembers = true) + appendSection("Types", node.membersOrGroupMembers { it.kind in NodeKind.classLike && it.kind != NodeKind.TypeAlias && it.kind != NodeKind.AnnotationClass && it.kind != NodeKind.Exception }) + appendSection("Annotations", node.membersOrGroupMembers(NodeKind.AnnotationClass)) + appendSection("Exceptions", node.membersOrGroupMembers(NodeKind.Exception)) + appendSection("Type Aliases", node.membersOrGroupMembers(NodeKind.TypeAlias)) + appendSection("Extensions for External Classes", node.members(NodeKind.ExternalClass)) + appendSection("Enum Values", node.members(NodeKind.EnumItem), sortMembers = false, omitSamePlatforms = true) + appendSection("Constructors", node.members(NodeKind.Constructor), omitSamePlatforms = true) + appendSection("Properties", node.members(NodeKind.Property), omitSamePlatforms = true) + appendSection("Inherited Properties", node.inheritedMembers(NodeKind.Property)) + appendSection("Functions", node.members(NodeKind.Function), omitSamePlatforms = true) + appendSection("Inherited Functions", node.inheritedMembers(NodeKind.Function)) + appendSection("Companion Object Properties", node.members(NodeKind.CompanionObjectProperty), omitSamePlatforms = true) + appendSection("Inherited Companion Object Properties", node.inheritedCompanionObjectMembers(NodeKind.Property)) + appendSection("Companion Object Functions", node.members(NodeKind.CompanionObjectFunction), omitSamePlatforms = true) + appendSection("Inherited Companion Object Functions", node.inheritedCompanionObjectMembers(NodeKind.Function)) + appendSection("Other members", node.members.filter { + it.kind !in setOf( + NodeKind.Class, + NodeKind.Interface, + NodeKind.Enum, + NodeKind.Object, + NodeKind.AnnotationClass, + NodeKind.Exception, + NodeKind.TypeAlias, + NodeKind.Constructor, + NodeKind.Property, + NodeKind.Package, + NodeKind.Function, + NodeKind.CompanionObjectProperty, + NodeKind.CompanionObjectFunction, + NodeKind.ExternalClass, + NodeKind.EnumItem, + NodeKind.AllTypes, + NodeKind.GroupNode + ) + }) + + val allExtensions = node.extensions + appendSection("Extension Properties", allExtensions.filter { it.kind == NodeKind.Property }) + appendSection("Extension Functions", allExtensions.filter { it.kind == NodeKind.Function }) + appendSection("Companion Object Extension Properties", allExtensions.filter { it.kind == NodeKind.CompanionObjectProperty }) + appendSection("Companion Object Extension Functions", allExtensions.filter { it.kind == NodeKind.CompanionObjectFunction }) + appendSection("Inheritors", + node.inheritors.filter { it.kind != NodeKind.EnumItem }) + + if (node.kind == NodeKind.Module) { + appendHeader(3) { to.append("Index") } + node.members(NodeKind.AllTypes).singleOrNull()?.let { allTypes -> + appendLink(link(node, allTypes, { "All Types" })) + } + } + } + + private fun appendSection(caption: String, members: List<DocumentationNode>, + sortMembers: Boolean = true, + omitSamePlatforms: Boolean = false, + platformsBasedOnMembers: Boolean = false) { + if (members.isEmpty()) return + + appendHeader(3) { appendText(caption) } + + val children = if (sortMembers) members.sortedBy { it.name.toLowerCase() } else members + val membersMap = children.groupBy { link(node, it) } + + + + appendTable("Name", "Summary") { + appendTableBody { + for ((memberLocation, members) in membersMap) { + val elementPlatforms = platformsOfItems(members, omitSamePlatforms) + val platforms = if (platformsBasedOnMembers) + members.flatMapTo(mutableSetOf()) { platformsOfItems(it.members) } + elementPlatforms + else + elementPlatforms + appendIndexRow(platforms) { + appendTableCell { + appendParagraph { + appendLink(memberLocation) + if (members.singleOrNull()?.kind != NodeKind.ExternalClass) { + appendPlatforms(platforms) + } + } + } + appendTableCell { + val breakdownBySummary = members.groupBy { it.summary } + for ((summary, items) in breakdownBySummary) { + appendSummarySignatures(items) + appendContent(summary) + } + } + } + } + } + } + } + + private fun platformsOfItems(items: List<DocumentationNode>, omitSamePlatforms: Boolean = true): Set<String> { + val platforms = platformsOfItems(items) + if (platforms.isNotEmpty() && (platforms != node.platformsToShow.toSet() || !omitSamePlatforms)) { + return platforms + } + return emptySet() + } + + private fun appendSummarySignatures(items: List<DocumentationNode>) { + val summarySignature = languageService.summarizeSignatures(items) + if (summarySignature != null) { + appendAsSignature(summarySignature) { + summarySignature.appendSignature() + } + return + } + val renderedSignatures = items.map { languageService.render(it, RenderMode.SUMMARY) } + renderedSignatures.subList(0, renderedSignatures.size - 1).forEach { + appendAsSignature(it) { + it.appendSignature() + } + appendLine() + } + appendAsSignature(renderedSignatures.last()) { + renderedSignatures.last().appendSignature() + } + } + } + + inner class AllTypesNodeBuilder(val node: DocumentationNode) + : PageBuilder(listOf(node)) { + + override fun build() { + appendContent(node.owner!!.summary) + appendHeader(3) { to.append("All Types") } + + appendTable("Name", "Summary") { + appendTableBody { + for (type in node.members) { + appendTableRow { + appendTableCell { + appendLink(link(node, type) { + if (it.kind == NodeKind.ExternalClass) it.name else it.qualifiedName() + }) + if (type.kind == NodeKind.ExternalClass) { + val packageName = type.owner?.name + if (packageName != null) { + appendText(" (extensions in package $packageName)") + } + } + } + appendTableCell { + appendContent(type.summary) + } + } + } + } + } + } + } + + override fun appendNodes(nodes: Iterable<DocumentationNode>) { + val singleNode = nodes.singleOrNull() + when (singleNode?.kind) { + NodeKind.AllTypes -> AllTypesNodeBuilder(singleNode).build() + NodeKind.GroupNode -> GroupNodePageBuilder(singleNode).build() + null -> PageBuilder(nodes).build() + else -> SingleNodePageBuilder(singleNode).build() + } + } +} + +abstract class StructuredFormatService(val generator: NodeLocationAwareGenerator, + val languageService: LanguageService, + override val extension: String, + override final val linkExtension: String = extension) : FormatService { + +} diff --git a/core/src/main/kotlin/Formats/YamlOutlineService.kt b/core/src/main/kotlin/Formats/YamlOutlineService.kt new file mode 100644 index 000000000..c36f98eb0 --- /dev/null +++ b/core/src/main/kotlin/Formats/YamlOutlineService.kt @@ -0,0 +1,26 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import java.io.File + +class YamlOutlineService @Inject constructor( + val generator: NodeLocationAwareGenerator, + val languageService: LanguageService +) : OutlineFormatService { + override fun getOutlineFileName(location: Location): File = File("${location.path}.yml") + + var outlineLevel = 0 + override fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) { + val indent = " ".repeat(outlineLevel) + to.appendln("$indent- title: ${languageService.renderName(node)}") + to.appendln("$indent url: ${generator.location(node).path}") + } + + override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) { + val indent = " ".repeat(outlineLevel) + to.appendln("$indent content:") + outlineLevel++ + body() + outlineLevel-- + } +} diff --git a/core/src/main/kotlin/Generation/DokkaGenerator.kt b/core/src/main/kotlin/Generation/DokkaGenerator.kt new file mode 100644 index 000000000..6f063b587 --- /dev/null +++ b/core/src/main/kotlin/Generation/DokkaGenerator.kt @@ -0,0 +1,207 @@ +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 org.jetbrains.dokka.DokkaConfiguration.SourceRoot +import org.jetbrains.dokka.Utilities.DokkaAnalysisModule +import org.jetbrains.dokka.Utilities.DokkaOutputModule +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +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.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.resolve.LazyTopDownAnalyzer +import org.jetbrains.kotlin.resolve.TopDownAnalysisMode +import java.io.File +import kotlin.system.measureTimeMillis + +class DokkaGenerator(val logger: DokkaLogger, + val classpath: List<String>, + val sources: List<SourceRoot>, + val samples: List<String>, + val includes: List<String>, + val moduleName: String, + val options: DocumentationOptions) { + + private val documentationModule = DocumentationModule(moduleName) + + fun generate() { + val sourcesGroupedByPlatform = sources.groupBy { it.platforms.firstOrNull() } + for ((platform, roots) in sourcesGroupedByPlatform) { + appendSourceModule(platform, roots) + } + documentationModule.prepareForGeneration(options) + + val timeBuild = measureTimeMillis { + logger.info("Generating pages... ") + val outputInjector = Guice.createInjector(DokkaOutputModule(options, logger)) + outputInjector.getInstance(Generator::class.java).buildAll(documentationModule) + } + logger.info("done in ${timeBuild / 1000} secs") + } + + private fun appendSourceModule(defaultPlatform: String?, sourceRoots: List<SourceRoot>) { + val sourcePaths = sourceRoots.map { it.path } + val environment = createAnalysisEnvironment(sourcePaths) + + logger.info("Module: $moduleName") + logger.info("Output: ${File(options.outputDir)}") + logger.info("Sources: ${sourcePaths.joinToString()}") + logger.info("Classpath: ${environment.classpath.joinToString()}") + + logger.info("Analysing sources and libraries... ") + val startAnalyse = System.currentTimeMillis() + + val defaultPlatformAsList = defaultPlatform?.let { listOf(it) }.orEmpty() + val defaultPlatformsProvider = object : DefaultPlatformsProvider { + override fun getDefaultPlatforms(descriptor: DeclarationDescriptor): List<String> { + val containingFilePath = descriptor.sourcePsi()?.containingFile?.virtualFile?.canonicalPath + ?.let { File(it).absolutePath } + val sourceRoot = containingFilePath?.let { path -> sourceRoots.find { path.startsWith(it.path) } } + return sourceRoot?.platforms ?: defaultPlatformAsList + } + } + + val injector = Guice.createInjector( + DokkaAnalysisModule(environment, options, defaultPlatformsProvider, documentationModule.nodeRefGraph, logger)) + + buildDocumentationModule(injector, documentationModule, { isNotSample(it) }, includes) + + val timeAnalyse = System.currentTimeMillis() - startAnalyse + logger.info("done in ${timeAnalyse / 1000} secs") + + Disposer.dispose(environment) + } + + fun createAnalysisEnvironment(sourcePaths: List<String>): AnalysisEnvironment { + val environment = AnalysisEnvironment(DokkaMessageCollector(logger)) + + environment.apply { + //addClasspath(PathUtil.getJdkClassesRootsFromCurrentJre()) + // addClasspath(PathUtil.getKotlinPathsForCompiler().getRuntimePath()) + for (element in this@DokkaGenerator.classpath) { + addClasspath(File(element)) + } + + addSources(sourcePaths) + addSources(this@DokkaGenerator.samples) + + loadLanguageVersionSettings(options.languageVersion, options.apiVersion) + } + + return environment + } + + fun isNotSample(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) + } + } +} + +class DokkaMessageCollector(val logger: DokkaLogger) : MessageCollector { + override fun clear() { + seenErrors = false + } + + private var seenErrors = false + + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation?) { + if (severity == CompilerMessageSeverity.ERROR) { + seenErrors = true + } + logger.error(MessageRenderer.PLAIN_FULL_PATHS.render(severity, message, location)) + } + + override fun hasErrors() = seenErrors +} + +fun buildDocumentationModule(injector: Injector, + documentationModule: DocumentationModule, + filesToDocumentFilter: (PsiFile) -> Boolean = { file -> true }, + includes: List<String> = listOf()) { + + val coreEnvironment = injector.getInstance(KotlinCoreEnvironment::class.java) + val fragmentFiles = coreEnvironment.getSourceFiles().filter(filesToDocumentFilter) + + val resolutionFacade = injector.getInstance(DokkaResolutionFacade::class.java) + val analyzer = resolutionFacade.getFrontendService(LazyTopDownAnalyzer::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) + } + if (documentationModule.content.isEmpty()) { + documentationModule.updateContent { + for (node in packageDocs.moduleContent.children) { + append(node) + } + } + } + + parseJavaPackageDocs(packageDocs, coreEnvironment) + + with(injector.getInstance(DocumentationBuilder::class.java)) { + documentationModule.appendFragments(fragments, packageDocs.packageContent, + injector.getInstance(PackageDocumentationBuilder::class.java)) + + propagateExtensionFunctionsToSubclasses(fragments, resolutionFacade) + } + + val javaFiles = coreEnvironment.getJavaSourceFiles().filter(filesToDocumentFilter) + with(injector.getInstance(JavaDocumentationBuilder::class.java)) { + javaFiles.map { appendFile(it, documentationModule, packageDocs.packageContent) } + } +} + +fun parseJavaPackageDocs(packageDocs: PackageDocs, coreEnvironment: KotlinCoreEnvironment) { + val contentRoots = coreEnvironment.configuration.get(CLIConfigurationKeys.CONTENT_ROOTS) + ?.filterIsInstance<JavaSourceRoot>() + ?.map { it.file } + ?: listOf() + contentRoots.forEach { root -> + root.walkTopDown().filter { it.name == "overview.html" }.forEach { + packageDocs.parseJava(it.path, it.relativeTo(root).parent.replace("/", ".")) + } + } +} + + +fun KotlinCoreEnvironment.getJavaSourceFiles(): List<PsiJavaFile> { + val sourceRoots = configuration.get(CLIConfigurationKeys.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/kotlin/Generation/FileGenerator.kt b/core/src/main/kotlin/Generation/FileGenerator.kt new file mode 100644 index 000000000..2d202db84 --- /dev/null +++ b/core/src/main/kotlin/Generation/FileGenerator.kt @@ -0,0 +1,89 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import org.jetbrains.kotlin.utils.fileUtils.withReplacedExtensionOrNull +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStreamWriter + +class FileGenerator @Inject constructor(@Named("outputDir") override val root: File) : NodeLocationAwareGenerator { + + @set:Inject(optional = true) var outlineService: OutlineFormatService? = null + @set:Inject(optional = true) lateinit var formatService: FormatService + @set:Inject(optional = true) lateinit var options: DocumentationOptions + @set:Inject(optional = true) var packageListService: PackageListService? = null + + override fun location(node: DocumentationNode): FileLocation { + return FileLocation(fileForNode(node, formatService.linkExtension)) + } + + private fun fileForNode(node: DocumentationNode, extension: String = ""): File { + return File(root, relativePathToNode(node)).appendExtension(extension) + } + + fun locationWithoutExtension(node: DocumentationNode): FileLocation { + return FileLocation(fileForNode(node)) + } + + override fun buildPages(nodes: Iterable<DocumentationNode>) { + + for ((file, items) in nodes.groupBy { fileForNode(it, formatService.extension) }) { + + file.parentFile?.mkdirsOrFail() + try { + FileOutputStream(file).use { + OutputStreamWriter(it, Charsets.UTF_8).use { + it.write(formatService.format(location(items.first()), 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 { locationWithoutExtension(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() { + formatService.enumerateSupportFiles { resource, targetPath -> + FileOutputStream(File(root, relativePathToNode(listOf(targetPath), false))).use { + javaClass.getResourceAsStream(resource).copyTo(it) + } + } + } + + override fun buildPackageList(nodes: Iterable<DocumentationNode>) { + if (packageListService == null) return + + for (module in nodes) { + + val moduleRoot = location(module).file.parentFile + val packageListFile = File(moduleRoot, "package-list") + + packageListFile.writeText("\$dokka.format:${options.outputFormat}\n" + + packageListService!!.formatPackageList(module as DocumentationModule)) + } + + } + +} + +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 000000000..23286e299 --- /dev/null +++ b/core/src/main/kotlin/Generation/Generator.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka + +import java.io.File + +interface Generator { + fun buildPages(nodes: Iterable<DocumentationNode>) + fun buildOutlines(nodes: Iterable<DocumentationNode>) + fun buildSupportFiles() + fun buildPackageList(nodes: Iterable<DocumentationNode>) +} + +fun Generator.buildAll(nodes: Iterable<DocumentationNode>) { + buildPages(nodes) + buildOutlines(nodes) + buildSupportFiles() + buildPackageList(nodes) +} + +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)) + + +interface NodeLocationAwareGenerator: Generator { + fun location(node: DocumentationNode): Location + val root: File +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Generation/configurationImpl.kt b/core/src/main/kotlin/Generation/configurationImpl.kt new file mode 100644 index 000000000..c8f93c519 --- /dev/null +++ b/core/src/main/kotlin/Generation/configurationImpl.kt @@ -0,0 +1,67 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.DokkaConfiguration.SourceLinkDefinition +import org.jetbrains.dokka.DokkaConfiguration.SourceRoot +import java.io.File + + +data class SourceLinkDefinitionImpl(override val path: String, + override val url: String, + override val lineSuffix: String?) : SourceLinkDefinition { + companion object { + fun parseSourceLinkDefinition(srcLink: String): SourceLinkDefinition { + val (path, urlAndLine) = srcLink.split('=') + return SourceLinkDefinitionImpl(File(path).absolutePath, + urlAndLine.substringBefore("#"), + urlAndLine.substringAfter("#", "").let { if (it.isEmpty()) null else "#" + it }) + } + } +} + +class SourceRootImpl(path: String, override val platforms: List<String> = emptyList()) : SourceRoot { + override val path: String = File(path).absolutePath + + companion object { + fun parseSourceRoot(sourceRoot: String): SourceRoot { + val components = sourceRoot.split("::", limit = 2) + return SourceRootImpl(components.last(), if (components.size == 1) listOf() else components[0].split(',')) + } + } +} + +data class PackageOptionsImpl(override val prefix: String, + override val includeNonPublic: Boolean = false, + override val reportUndocumented: Boolean = true, + override val skipDeprecated: Boolean = false, + override val suppress: Boolean = false) : DokkaConfiguration.PackageOptions + +data class DokkaConfigurationImpl( + override val moduleName: String, + override val classpath: List<String>, + override val sourceRoots: List<SourceRootImpl>, + override val samples: List<String>, + override val includes: List<String>, + override val outputDir: String, + override val format: String, + override val includeNonPublic: Boolean, + override val includeRootPackage: Boolean, + override val reportUndocumented: Boolean, + override val skipEmptyPackages: Boolean, + override val skipDeprecated: Boolean, + override val jdkVersion: Int, + override val generateClassIndexPage: Boolean, + override val generatePackageIndexPage: Boolean, + override val sourceLinks: List<SourceLinkDefinitionImpl>, + override val impliedPlatforms: List<String>, + override val perPackageOptions: List<PackageOptionsImpl>, + override val externalDocumentationLinks: List<ExternalDocumentationLinkImpl>, + override val noStdlibLink: Boolean, + override val noJdkLink: Boolean, + override val cacheRoot: String?, + override val suppressedFiles: List<String>, + override val languageVersion: String?, + override val apiVersion: String?, + override val collectInheritedExtensionsFromLibraries: Boolean, + override val outlineRoot: String, + override val dacRoot: String +) : DokkaConfiguration
\ No newline at end of file diff --git a/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt new file mode 100644 index 000000000..94bb0455d --- /dev/null +++ b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt @@ -0,0 +1,392 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.* +import com.intellij.psi.impl.JavaConstantExpressionEvaluator +import com.intellij.psi.impl.source.PsiClassReferenceType +import com.intellij.psi.util.InheritanceUtil +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.asJava.elements.KtLightDeclaration +import org.jetbrains.kotlin.asJava.elements.KtLightElement +import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtModifierListOwner +import java.io.File + +fun getSignature(element: PsiElement?) = when(element) { + is PsiPackage -> element.qualifiedName + 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()}))" + is PsiPrimitiveType -> "kotlin." + canonicalText.capitalize() + is PsiClassType -> resolve()?.qualifiedName ?: className + else -> mapTypeName(this) +} + +private fun mapTypeName(psiType: PsiType): String = when (psiType) { + is PsiPrimitiveType -> psiType.canonicalText + is PsiClassType -> psiType.resolve()?.name ?: psiType.className + is PsiEllipsisType -> mapTypeName(psiType.componentType) + is PsiArrayType -> "kotlin.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 + private val externalDocumentationLinkResolver: ExternalDocumentationLinkResolver + + @Inject constructor( + options: DocumentationOptions, + refGraph: NodeReferenceGraph, + logger: DokkaLogger, + signatureProvider: ElementSignatureProvider, + externalDocumentationLinkResolver: ExternalDocumentationLinkResolver + ) { + this.options = options + this.refGraph = refGraph + this.docParser = JavadocParser(refGraph, logger, signatureProvider, externalDocumentationLinkResolver) + this.externalDocumentationLinkResolver = externalDocumentationLinkResolver + } + + constructor( + options: DocumentationOptions, + refGraph: NodeReferenceGraph, + docParser: JavaDocumentationParser, + externalDocumentationLinkResolver: ExternalDocumentationLinkResolver + ) { + this.options = options + this.refGraph = refGraph + this.docParser = docParser + this.externalDocumentationLinkResolver = externalDocumentationLinkResolver + } + + override fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) { + if (skipFile(file) || file.classes.all { skipElement(it) }) { + return + } + val packageNode = findOrCreatePackageNode(module, file.packageName, emptyMap(), refGraph) + 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, RefKind.Link) + } + } + + fun link(element: PsiElement?, node: DocumentationNode, kind: RefKind) { + val qualifiedName = getSignature(element) + if (qualifiedName != null) { + refGraph.link(qualifiedName, node, kind) + } + } + + fun nodeForElement(element: PsiNamedElement, + kind: NodeKind, + name: String = element.name ?: "<anonymous>"): DocumentationNode { + val (docComment, deprecatedContent, attrs, apiLevel, sdkExtSince, deprecatedLevel, artifactId, attribute) = 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() + if (it.qualifiedName == "java.lang.Deprecated" || it.qualifiedName == "kotlin.Deprecated") { + node.append(annotation, RefKind.Deprecation) + annotation.convertDeprecationDetailsToChildren() + } else { + node.append(annotation, RefKind.Annotation) + } + } + } + } + if (deprecatedContent != null) { + val deprecationNode = DocumentationNode("", deprecatedContent, NodeKind.Modifier) + node.append(deprecationNode, RefKind.Deprecation) + } + if (element is PsiDocCommentOwner && element.isDeprecated && node.deprecation == null) { + val deprecationNode = DocumentationNode("", Content.of(ContentText("Deprecated")), NodeKind.Modifier) + node.append(deprecationNode, RefKind.Deprecation) + } + apiLevel?.let { + node.append(it, RefKind.Detail) + } + sdkExtSince?.let { + node.append(it, RefKind.Detail) + } + deprecatedLevel?.let { + node.append(it, RefKind.Detail) + } + artifactId?.let { + node.append(it, RefKind.Detail) + } + attrs.forEach { + refGraph.link(node, it, RefKind.Detail) + refGraph.link(it, node, RefKind.Owner) + } + attribute?.let { + val attrName = node.qualifiedName() + refGraph.register("Attr:$attrName", attribute) + } + return node + } + + fun ignoreAnnotation(annotation: PsiAnnotation) = when(annotation.qualifiedName) { + "java.lang.SuppressWarnings" -> true + else -> false + } + + fun <T : Any> DocumentationNode.appendChildren(elements: Array<T>, + kind: RefKind = RefKind.Member, + buildFn: T.() -> DocumentationNode) { + elements.forEach { + if (!skipElement(it)) { + append(it.buildFn(), kind) + } + } + } + + private fun skipFile(javaFile: PsiJavaFile): Boolean = options.effectivePackageOptions(javaFile.packageName).suppress + + private fun skipElement(element: Any) = + skipElementByVisibility(element) || + hasSuppressDocTag(element) || + hasHideAnnotation(element) || + skipElementBySuppressedFiles(element) + + private fun skipElementByVisibility(element: Any): Boolean = + element is PsiModifierListOwner && + element !is PsiParameter && + !(options.effectivePackageOptions((element.containingFile as? PsiJavaFile)?.packageName ?: "").includeNonPublic) && + (element.hasModifierProperty(PsiModifier.PRIVATE) || + element.hasModifierProperty(PsiModifier.PACKAGE_LOCAL) || + element.isInternal()) + + private fun skipElementBySuppressedFiles(element: Any): Boolean = + element is PsiElement && element.containingFile.virtualFile != null && File(element.containingFile.virtualFile.path).absoluteFile in options.suppressedFiles + + private fun PsiElement.isInternal(): Boolean { + val ktElement = (this as? KtLightElement<*, *>)?.kotlinOrigin ?: return false + return (ktElement as? KtModifierListOwner)?.hasModifier(KtTokens.INTERNAL_KEYWORD) ?: false + } + + fun <T : Any> DocumentationNode.appendMembers(elements: Array<T>, buildFn: T.() -> DocumentationNode) = + appendChildren(elements, RefKind.Member, buildFn) + + fun <T : Any> DocumentationNode.appendDetails(elements: Array<T>, buildFn: T.() -> DocumentationNode) = + appendChildren(elements, RefKind.Detail, buildFn) + + fun PsiClass.build(): DocumentationNode { + val kind = when { + isInterface -> NodeKind.Interface + isEnum -> NodeKind.Enum + isAnnotationType -> NodeKind.AnnotationClass + isException() -> NodeKind.Exception + else -> NodeKind.Class + } + val node = nodeForElement(this, kind) + superTypes.filter { !ignoreSupertype(it) }.forEach { superType -> + node.appendType(superType, NodeKind.Supertype) + val superClass = superType.resolve() + if (superClass != null) { + link(superClass, node, RefKind.Inheritor) + } + } + + var methodsAndConstructors = methods + + if (constructors.isEmpty()) { + // Having no constructor represents a class that only has an implicit/default constructor + // so we create one synthetically for documentation + val factory = JavaPsiFacade.getElementFactory(this.project) + methodsAndConstructors += factory.createMethodFromText("public $name() {}", this) + } + node.appendDetails(typeParameters) { build() } + node.appendMembers(methodsAndConstructors) { build() } + node.appendMembers(fields) { build() } + node.appendMembers(innerClasses) { build() } + register(this, node) + return node + } + + fun PsiClass.isException() = InheritanceUtil.isInheritor(this, "java.lang.Throwable") + + fun ignoreSupertype(psiType: PsiClassType): Boolean = false +// 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.appendConstantValueIfAny(this) + register(this, node) + return node + } + + private fun DocumentationNode.appendConstantValueIfAny(field: PsiField) { + val modifierList = field.modifierList ?: return + val initializer = field.initializer ?: return + if (modifierList.hasExplicitModifier(PsiModifier.FINAL) && + modifierList.hasExplicitModifier(PsiModifier.STATIC)) { + val value = JavaConstantExpressionEvaluator.computeConstantExpression(initializer, false) ?: return + val text = when(value) { + is String -> "\"${StringUtil.escapeStringCharacters(value)}\"" + else -> value.toString() + } + append(DocumentationNode(text, Content.Empty, NodeKind.Value), RefKind.Detail) + } + } + + private fun PsiField.nodeKind(): NodeKind = when { + this is PsiEnumConstant -> NodeKind.EnumItem + else -> NodeKind.Field + } + + fun PsiMethod.build(): DocumentationNode { + val node = nodeForElement(this, nodeKind(), name) + + if (!isConstructor) { + node.appendType(returnType) + } + node.appendDetails(parameterList.parameters) { build() } + node.appendDetails(typeParameters) { build() } + register(this, node) + return node + } + + private fun PsiMethod.nodeKind(): NodeKind = when { + isConstructor -> NodeKind.Constructor + else -> NodeKind.Function + } + + fun PsiParameter.build(): DocumentationNode { + val node = nodeForElement(this, NodeKind.Parameter) + node.appendType(type) + if (type is PsiEllipsisType) { + node.appendTextNode("vararg", NodeKind.Modifier, RefKind.Detail) + } + return node + } + + fun PsiTypeParameter.build(): DocumentationNode { + val node = nodeForElement(this, NodeKind.TypeParameter) + extendsListTypes.forEach { node.appendType(it, NodeKind.UpperBound) } + implementsListTypes.forEach { node.appendType(it, NodeKind.UpperBound) } + return node + } + + fun DocumentationNode.appendModifiers(element: PsiModifierListOwner) { + val modifierList = element.modifierList ?: return + + PsiModifier.MODIFIERS.forEach { + if (modifierList.hasExplicitModifier(it)) { + appendTextNode(it, NodeKind.Modifier) + } + } + } + + fun DocumentationNode.appendType(psiType: PsiType?, kind: NodeKind = NodeKind.Type) { + if (psiType == null) { + return + } + + val node = psiType.build(kind) + append(node, RefKind.Detail) + + // Attempt to create an external link if the psiType is one + if (psiType is PsiClassReferenceType) { + val target = psiType.reference.resolve() + if (target != null) { + val externalLink = externalDocumentationLinkResolver.buildExternalDocumentationLink(target) + if (externalLink != null) { + node.append(DocumentationNode(externalLink, Content.Empty, NodeKind.ExternalLink), RefKind.Link) + } + } + } + } + + fun PsiType.build(kind: NodeKind = NodeKind.Type): DocumentationNode { + val name = mapTypeName(this) + val node = DocumentationNode(name, Content.Empty, kind) + if (this is PsiClassType) { + node.appendDetails(parameters) { build(NodeKind.Type) } + link(node, resolve()) + } + if (this is PsiArrayType && this !is PsiEllipsisType) { + node.append(componentType.build(NodeKind.Type), RefKind.Detail) + } + return node + } + + fun PsiAnnotation.build(): DocumentationNode { + val node = DocumentationNode(nameReferenceElement?.text ?: "<?>", Content.Empty, NodeKind.Annotation) + parameterList.attributes.forEach { + val parameter = DocumentationNode(it.name ?: "value", Content.Empty, NodeKind.Parameter) + val value = it.value + if (value != null) { + val valueText = (value as? PsiLiteralExpression)?.value as? String ?: value.text + val valueNode = DocumentationNode(valueText, Content.Empty, NodeKind.Value) + parameter.append(valueNode, RefKind.Detail) + } + node.append(parameter, RefKind.Detail) + } + return node + } +} + +fun hasSuppressDocTag(element: Any?): Boolean { + val declaration = (element as? KtLightDeclaration<*, *>)?.kotlinOrigin as? KtDeclaration ?: return false + return PsiTreeUtil.findChildrenOfType(declaration.docComment, KDocTag::class.java).any { it.knownTag == KDocKnownTag.SUPPRESS } +} + +/** + * Determines if the @hide annotation is present in a Javadoc comment. + * + * @param element a doc element to analyze for the presence of @hide + * + * @return true if @hide is present, otherwise false + * + * Note: this does not process @hide annotations in KDoc. For KDoc, use the @suppress tag instead, which is processed + * by [hasSuppressDocTag]. + */ +fun hasHideAnnotation(element: Any?): Boolean { + return element is PsiDocCommentOwner && element.docComment?.run { findTagByName("hide") != null } ?: false +} diff --git a/core/src/main/kotlin/Java/JavadocParser.kt b/core/src/main/kotlin/Java/JavadocParser.kt new file mode 100644 index 000000000..0c73e7661 --- /dev/null +++ b/core/src/main/kotlin/Java/JavadocParser.kt @@ -0,0 +1,709 @@ +package org.jetbrains.dokka + +import com.intellij.psi.* +import com.intellij.psi.impl.source.javadoc.PsiDocTagValueImpl +import com.intellij.psi.impl.source.tree.JavaDocElementType +import com.intellij.psi.javadoc.* +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.IncorrectOperationException +import org.jetbrains.dokka.Model.CodeNode +import org.jetbrains.kotlin.utils.join +import org.jetbrains.kotlin.utils.keysToMap +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode +import java.io.File +import java.net.URI +import java.util.regex.Pattern + +private val NAME_TEXT = Pattern.compile("(\\S+)(.*)", Pattern.DOTALL) +private val TEXT = Pattern.compile("(\\S+)\\s*(.*)", Pattern.DOTALL) + +data class JavadocParseResult( + val content: Content, + val deprecatedContent: Content?, + val attributeRefs: List<String>, + val apiLevel: DocumentationNode? = null, + val sdkExtSince: DocumentationNode? = null, + val deprecatedLevel: DocumentationNode? = null, + val artifactId: DocumentationNode? = null, + val attribute: DocumentationNode? = null +) { + companion object { + val Empty = JavadocParseResult(Content.Empty, + null, + emptyList(), + null, + null, + null, + null + ) + } +} + +interface JavaDocumentationParser { + fun parseDocumentation(element: PsiNamedElement): JavadocParseResult +} + +class JavadocParser( + private val refGraph: NodeReferenceGraph, + private val logger: DokkaLogger, + private val signatureProvider: ElementSignatureProvider, + private val externalDocumentationLinkResolver: ExternalDocumentationLinkResolver +) : JavaDocumentationParser { + + private fun ContentSection.appendTypeElement( + signature: String, + selector: (DocumentationNode) -> DocumentationNode? + ) { + append(LazyContentBlock { + val node = refGraph.lookupOrWarn(signature, logger)?.let(selector) + if (node != null) { + it.append(NodeRenderContent(node, LanguageService.RenderMode.SUMMARY)) + it.symbol(":") + it.text(" ") + } + }) + } + + 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 firstParagraph = ContentParagraph() + firstParagraph.convertJavadocElements( + docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() }, + element + ) + val paragraphs = firstParagraph.children.dropWhile { it !is ContentParagraph } + firstParagraph.children.removeAll(paragraphs) + if (!firstParagraph.isEmpty()) { + result.append(firstParagraph) + } + paragraphs.forEach { + result.append(it) + } + + if (element is PsiMethod) { + val tagsByName = element.searchInheritedTags() + for ((tagName, tags) in tagsByName) { + for ((tag, context) in tags) { + val section = result.addSection(javadocSectionDisplayName(tagName), tag.getSubjectName()) + val signature = signatureProvider.signature(element) + when (tagName) { + "param" -> { + section.appendTypeElement(signature) { + it.details + .find { node -> node.kind == NodeKind.Parameter && node.name == tag.getSubjectName() } + ?.detailOrNull(NodeKind.Type) + } + } + "return" -> { + section.appendTypeElement(signature) { it.detailOrNull(NodeKind.Type) } + } + } + section.convertJavadocElements(tag.contentElements(), context) + } + } + } + + val attrRefSignatures = mutableListOf<String>() + var since: DocumentationNode? = null + var sdkextsince: DocumentationNode? = null + var deprecated: DocumentationNode? = null + var artifactId: DocumentationNode? = null + var attrName: String? = null + var attrDesc: Content? = null + var attr: DocumentationNode? = null + docComment.tags.forEach { tag -> + when (tag.name.toLowerCase()) { + "see" -> result.convertSeeTag(tag) + "deprecated" -> { + deprecatedContent = Content().apply { + convertJavadocElements(tag.contentElements(), element) + } + } + "attr" -> { + when (tag.valueElement?.text) { + "ref" -> + tag.getAttrRef(element)?.let { + attrRefSignatures.add(it) + } + "name" -> attrName = tag.getAttrName() + "description" -> attrDesc = tag.getAttrDesc(element) + } + } + "since", "apisince" -> { + since = DocumentationNode(tag.getApiLevel() ?: "", Content.Empty, NodeKind.ApiLevel) + } + "sdkextsince" -> { + sdkextsince = DocumentationNode(tag.getSdkExtSince() ?: "", Content.Empty, NodeKind.SdkExtSince) + } + "deprecatedsince" -> { + deprecated = DocumentationNode(tag.getApiLevel() ?: "", Content.Empty, NodeKind.DeprecatedLevel) + } + "artifactid" -> { + artifactId = DocumentationNode(tag.artifactId() ?: "", Content.Empty, NodeKind.ArtifactId) + } + in tagsToInherit -> { + } + else -> { + val subjectName = tag.getSubjectName() + val section = result.addSection(javadocSectionDisplayName(tag.name), subjectName) + section.convertJavadocElements(tag.contentElements(), element) + } + } + } + attrName?.let { name -> + attr = DocumentationNode(name, attrDesc ?: Content.Empty, NodeKind.AttributeRef) + } + return JavadocParseResult(result, deprecatedContent, attrRefSignatures, since, sdkextsince, deprecated, artifactId, attr) + } + + private val tagsToInherit = setOf("param", "return", "throws") + + private data class TagWithContext(val tag: PsiDocTag, val context: PsiNamedElement) + + fun PsiDocTag.artifactId(): String? { + var artifactName: String? = null + if (dataElements.isNotEmpty()) { + artifactName = join(dataElements.map { it.text }, "") + } + return artifactName + } + + fun PsiDocTag.getApiLevel(): String? { + if (dataElements.isNotEmpty()) { + val data = dataElements + if (data[0] is PsiDocTagValueImpl) { + val docTagValue = data[0] + if (docTagValue.firstChild != null) { + val apiLevel = docTagValue.firstChild + return apiLevel.text + } + } + } + return null + } + + fun PsiDocTag.getSdkExtSince(): String? { + if (dataElements.isNotEmpty()) { + return join(dataElements.map { it.text }, " ") + } + return null + } + + private fun PsiDocTag.getAttrRef(element: PsiNamedElement): String? { + if (dataElements.size > 1) { + val elementText = dataElements[1].text + try { + val linkComment = JavaPsiFacade.getInstance(project).elementFactory + .createDocCommentFromText("/** {@link $elementText} */", element) + val linkElement = PsiTreeUtil.getChildOfType(linkComment, PsiInlineDocTag::class.java)?.linkElement() + val signature = resolveInternalLink(linkElement) + val attrSignature = "AttrMain:$signature" + return attrSignature + } catch (e: IncorrectOperationException) { + return null + } + } else return null + } + + private fun PsiDocTag.getAttrName(): String? { + if (dataElements.size > 1) { + val nameMatcher = NAME_TEXT.matcher(dataElements[1].text) + if (nameMatcher.matches()) { + return nameMatcher.group(1) + } else { + return null + } + } else return null + } + + private fun PsiDocTag.getAttrDesc(element: PsiNamedElement): Content? { + return Content().apply { + convertJavadocElementsToAttrDesc(contentElements(), element) + } + } + + private fun PsiMethod.searchInheritedTags(): Map<String, Collection<TagWithContext>> { + + val output = tagsToInherit.keysToMap { mutableMapOf<String?, TagWithContext>() } + + fun recursiveSearch(methods: Array<PsiMethod>) { + for (method in methods) { + recursiveSearch(method.findSuperMethods()) + } + for (method in methods) { + for (tag in method.docComment?.tags.orEmpty()) { + if (tag.name in tagsToInherit) { + output[tag.name]!![tag.getSubjectName()] = TagWithContext(tag, method) + } + } + } + } + + recursiveSearch(arrayOf(this)) + return output.mapValues { it.value.values } + } + + + 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>, element: PsiNamedElement) { + val doc = Jsoup.parse(expandAllForElements(elements, element)) + doc.body().childNodes().forEach { + convertHtmlNode(it)?.let { append(it) } + } + doc.head().childNodes().forEach { + convertHtmlNode(it)?.let { append(it) } + } + } + + private fun ContentBlock.convertJavadocElementsToAttrDesc(elements: Iterable<PsiElement>, element: PsiNamedElement) { + val doc = Jsoup.parse(expandAllForElements(elements, element)) + doc.body().childNodes().forEach { + convertHtmlNode(it)?.let { + var content = it + if (content is ContentText) { + var description = content.text + val matcher = TEXT.matcher(content.text) + if (matcher.matches()) { + val command = matcher.group(1) + if (command == "description") { + description = matcher.group(2) + content = ContentText(description) + } + } + } + append(content) + } + } + } + + private fun expandAllForElements(elements: Iterable<PsiElement>, element: PsiNamedElement): String { + val htmlBuilder = StringBuilder() + elements.forEach { + if (it is PsiInlineDocTag) { + htmlBuilder.append(convertInlineDocTag(it, element)) + } else { + htmlBuilder.append(it.text) + } + } + return htmlBuilder.toString().trim() + } + + private fun convertHtmlNode(node: Node, isBlockCode: Boolean = false): ContentNode? { + if (isBlockCode) { + return if (node is TextNode) { // Fixes b/129762453 + val codeNode = CodeNode(node.wholeText, "") + ContentText(codeNode.text().removePrefix("#")) + } else { // Fixes b/129857975 + ContentText(node.toString()) + } + } + if (node is TextNode) { + return ContentText(node.text().removePrefix("#")) + } else if (node is Element) { + val childBlock = createBlock(node) + node.childNodes().forEach { + val child = convertHtmlNode(it, isBlockCode = childBlock is ContentBlockCode) + if (child != null) { + childBlock.append(child) + } + } + return (childBlock) + } + return null + } + + 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) + "br" -> ContentBlock().apply { hardLineBreak() } + + "dl" -> ContentDescriptionList() + "dt" -> ContentDescriptionTerm() + "dd" -> ContentDescriptionDefinition() + + "table" -> ContentTable() + "tbody" -> ContentTableBody() + "tr" -> ContentTableRow() + "th" -> { + val colspan = element.attr("colspan") + val rowspan = element.attr("rowspan") + ContentTableHeader(colspan, rowspan) + } + "td" -> { + val colspan = element.attr("colspan") + val rowspan = element.attr("rowspan") + ContentTableCell(colspan, rowspan) + } + + "h1" -> ContentHeading(1) + "h2" -> ContentHeading(2) + "h3" -> ContentHeading(3) + "h4" -> ContentHeading(4) + "h5" -> ContentHeading(5) + "h6" -> ContentHeading(6) + + "div" -> { + val divClass = element.attr("class") + if (divClass == "special reference" || divClass == "note") ContentSpecialReference() + else ContentParagraph() + } + + "script" -> { + + // If the `type` attr is an empty string, we want to use null instead so that the resulting generated + // Javascript does not contain a `type` attr. + // + // Example: + // type == "" => <script type="" src="..."> + // type == null => <script src="..."> + val type = if (element.attr("type").isNotEmpty()) { + element.attr("type") + } else { + null + } + ScriptBlock(type, element.attr("src")) + } + + else -> ContentBlock() + } + + private fun createLink(element: Element): ContentBlock { + return when { + element.hasAttr("docref") -> { + val docref = element.attr("docref") + ContentNodeLazyLink(docref, { -> refGraph.lookupOrWarn(docref, logger) }) + } + element.hasAttr("href") -> { + val href = element.attr("href") + + val uri = try { + URI(href) + } catch (_: Exception) { + null + } + + if (uri?.isAbsolute == false) { + ContentLocalLink(href) + } else { + ContentExternalLink(href) + } + } + element.hasAttr("name") -> { + ContentBookmark(element.attr("name")) + } + else -> ContentBlock() + } + } + + private fun MutableContent.convertSeeTag(tag: PsiDocTag) { + val linkElement = tag.linkElement() ?: return + val seeSection = findSectionByTag(ContentTags.SeeAlso) ?: addSection(ContentTags.SeeAlso, null) + + val valueElement = tag.referenceElement() + val externalLink = resolveExternalLink(valueElement) + val text = ContentText(linkElement.text) + + val linkSignature by lazy { resolveInternalLink(valueElement) } + val node = when { + externalLink != null -> { + val linkNode = ContentExternalLink(externalLink) + linkNode.append(text) + linkNode + } + linkSignature != null -> { + @Suppress("USELESS_CAST") + val signature: String = linkSignature as String + val linkNode = + ContentNodeLazyLink( + (tag.valueElement ?: linkElement).text + ) { refGraph.lookupOrWarn(signature, logger) } + linkNode.append(text) + linkNode + } + else -> text + } + seeSection.append(node) + } + + private fun convertInlineDocTag(tag: PsiInlineDocTag, element: PsiNamedElement) = when (tag.name) { + "link", "linkplain" -> { + val valueElement = tag.referenceElement() + val externalLink = resolveExternalLink(valueElement) + val linkSignature by lazy { resolveInternalLink(valueElement) } + if (externalLink != null || linkSignature != null) { + + // sometimes `dataElements` contains multiple `PsiDocToken` elements and some have whitespace in them + // this is best effort to find the first non-empty one before falling back to using the symbol name. + val labelText = tag.dataElements.firstOrNull { + it is PsiDocToken && it.text?.trim()?.isNotEmpty() ?: false + }?.text ?: valueElement!!.text + + val linkTarget = if (externalLink != null) "href=\"$externalLink\"" else "docref=\"$linkSignature\"" + val link = "<a $linkTarget>$labelText</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 + } + "inheritDoc" -> { + val result = (element as? PsiMethod)?.let { + // @{inheritDoc} is only allowed on functions + val parent = tag.parent + when (parent) { + is PsiDocComment -> element.findSuperDocCommentOrWarn() + is PsiDocTag -> element.findSuperDocTagOrWarn(parent) + else -> null + } + } + result ?: tag.text + } + "docRoot" -> { + // TODO: fix that + "https://developer.android.com/" + } + "sample" -> { + tag.text?.let { tagText -> + val (absolutePath, delimiter) = getSampleAnnotationInformation(tagText) + val code = retrieveCodeInFile(absolutePath, delimiter) + return if (code != null && code.isNotEmpty()) { + "<pre is-upgraded>$code</pre>" + } else { + "" + } + } + } + + // Loads MathJax script from local source, which then updates MathJax HTML code + "usesMathJax" -> { + "<script src=\"/_static/js/managed/mathjax/MathJax.js?config=TeX-AMS_SVG\"></script>" + } + + else -> tag.text + } + + private fun PsiDocTag.referenceElement(): PsiElement? = + linkElement()?.let { + if (it.node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) { + PsiTreeUtil.findChildOfType(it, PsiJavaCodeReferenceElement::class.java) + } else { + it + } + } + + private fun PsiDocTag.linkElement(): PsiElement? = + valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace } + + private fun resolveExternalLink(valueElement: PsiElement?): String? { + val target = valueElement?.reference?.resolve() + if (target != null) { + return externalDocumentationLinkResolver.buildExternalDocumentationLink(target) + } + return null + } + + private fun resolveInternalLink(valueElement: PsiElement?): String? { + val target = valueElement?.reference?.resolve() + if (target != null) { + return signatureProvider.signature(target) + } + return null + } + + fun PsiDocTag.getSubjectName(): String? { + if (name == "param" || name == "throws" || name == "exception") { + return valueElement?.text + } + return null + } + + private fun PsiMethod.findSuperDocCommentOrWarn(): String { + val method = findFirstSuperMethodWithDocumentation(this) + if (method != null) { + val descriptionElements = method.docComment?.descriptionElements?.dropWhile { + it.text.trim().isEmpty() + } ?: return "" + + return expandAllForElements(descriptionElements, method) + } + logger.warn("No docs found on supertype with {@inheritDoc} method ${this.name} in ${this.containingFile.name}:${this.lineNumber()}") + return "" + } + + + private fun PsiMethod.findSuperDocTagOrWarn(elementToExpand: PsiDocTag): String { + val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, this) + + if (result != null) { + val (method, tag) = result + + val contentElements = tag.contentElements().dropWhile { it.text.trim().isEmpty() } + + val expandedString = expandAllForElements(contentElements, method) + + return expandedString + } + logger.warn("No docs found on supertype for @${elementToExpand.name} ${elementToExpand.getSubjectName()} with {@inheritDoc} method ${this.name} in ${this.containingFile.name}:${this.lineNumber()}") + return "" + } + + private fun findFirstSuperMethodWithDocumentation(current: PsiMethod): PsiMethod? { + val superMethods = current.findSuperMethods() + for (method in superMethods) { + val docs = method.docComment?.descriptionElements?.dropWhile { it.text.trim().isEmpty() } + if (docs?.isNotEmpty() == true) { + return method + } + } + for (method in superMethods) { + val result = findFirstSuperMethodWithDocumentation(method) + if (result != null) { + return result + } + } + + return null + } + + private fun findFirstSuperMethodWithDocumentationforTag( + elementToExpand: PsiDocTag, + current: PsiMethod + ): Pair<PsiMethod, PsiDocTag>? { + val superMethods = current.findSuperMethods() + val mappedFilteredTags = superMethods.map { + it to it.docComment?.tags?.filter { it.name == elementToExpand.name } + } + + for ((method, tags) in mappedFilteredTags) { + tags ?: continue + for (tag in tags) { + val (tagSubject, elementSubject) = when (tag.name) { + "throws" -> { + // match class names only for throws, ignore possibly fully qualified path + // TODO: Always match exactly here + tag.getSubjectName()?.split(".")?.last() to elementToExpand.getSubjectName()?.split(".")?.last() + } + else -> { + tag.getSubjectName() to elementToExpand.getSubjectName() + } + } + + if (tagSubject == elementSubject) { + return method to tag + } + } + } + + for (method in superMethods) { + val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, method) + if (result != null) { + return result + } + } + return null + } + + /** + * Returns information inside @sample + * + * Component1 is the absolute path to the file + * Component2 is the delimiter if exists in the file + */ + private fun getSampleAnnotationInformation(tagText: String): Pair<String, String> { + val pathContent = tagText + .trim { it == '{' || it == '}' } + .removePrefix("@sample ") + + val formattedPath = pathContent.substringBefore(" ").trim() + val potentialDelimiter = pathContent.substringAfterLast(" ").trim() + + val delimiter = if (potentialDelimiter == formattedPath) "" else potentialDelimiter + val path = "samples/$formattedPath" + + return Pair(path, delimiter) + } + + /** + * Retrieves the code inside a file. + * + * If betweenTag is not empty, it retrieves the code between + * BEGIN_INCLUDE($betweenTag) and END_INCLUDE($betweenTag) comments. + * + * Also, the method will trim every line with the number of spaces in the first line + */ + private fun retrieveCodeInFile(path: String, betweenTag: String = "") = StringBuilder().apply { + try { + if (betweenTag.isEmpty()) { + appendContent(path) + } else { + appendContentBetweenIncludes(path, betweenTag) + } + } catch (e: java.lang.Exception) { + logger.error("No file found when processing Java @sample. Path to sample: $path\n") + } + } + + private fun StringBuilder.appendContent(path: String) { + val spaces = InitialSpaceIndent() + File(path).forEachLine { + appendWithoutInitialIndent(it, spaces) + } + } + + private fun StringBuilder.appendContentBetweenIncludes(path: String, includeTag: String) { + var shouldAppend = false + val beginning = "BEGIN_INCLUDE($includeTag)" + val end = "END_INCLUDE($includeTag)" + val spaces = InitialSpaceIndent() + File(path).forEachLine { + if (shouldAppend) { + if (it.contains(end)) { + shouldAppend = false + } else { + appendWithoutInitialIndent(it, spaces) + } + } else { + if (it.contains(beginning)) shouldAppend = true + } + } + } + + private fun StringBuilder.appendWithoutInitialIndent(it: String, spaces: InitialSpaceIndent) { + if (spaces.value == -1) { + spaces.value = (it.length - it.trimStart().length).coerceAtLeast(0) + appendln(it) + } else { + appendln(if (it.isBlank()) it else it.substring(spaces.value, it.length)) + } + } + + private data class InitialSpaceIndent(var value: Int = -1) +} diff --git a/core/src/main/kotlin/Kotlin/ContentBuilder.kt b/core/src/main/kotlin/Kotlin/ContentBuilder.kt new file mode 100644 index 000000000..c60625a4a --- /dev/null +++ b/core/src/main/kotlin/Kotlin/ContentBuilder.kt @@ -0,0 +1,188 @@ +package org.jetbrains.dokka + +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.html.entities.EntityConverter +import org.intellij.markdown.parser.LinkMap +import java.util.* + +class LinkResolver(private val linkMap: LinkMap, private val contentFactory: (String) -> ContentBlock) { + fun getLinkInfo(refLabel: String) = linkMap.getLinkInfo(refLabel) + fun resolve(href: String): ContentBlock = contentFactory(href) +} + +fun buildContent(tree: MarkdownNode, linkResolver: LinkResolver, inline: Boolean = false): MutableContent { + val result = MutableContent() + if (inline) { + buildInlineContentTo(tree, result, linkResolver) + } else { + buildContentTo(tree, result, linkResolver) + } + return result +} + +fun buildContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: LinkResolver) { +// 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 -> { + val startDelimiter = node.child(MarkdownTokenTypes.BACKTICK)?.text + if (startDelimiter != null) { + val text = node.text.substring(startDelimiter.length).removeSuffix(startDelimiter) + val codeSpan = ContentCode().apply { append(ContentText(text)) } + parent.append(codeSpan) + } + } + MarkdownElementTypes.CODE_BLOCK, + MarkdownElementTypes.CODE_FENCE -> { + val language = node.child(MarkdownTokenTypes.FENCE_LANG)?.text?.trim() ?: "" + appendNodeWithChildren(ContentBlockCode(language)) + } + MarkdownElementTypes.PARAGRAPH -> appendNodeWithChildren(ContentParagraph()) + + MarkdownElementTypes.INLINE_LINK -> { + val linkTextNode = node.child(MarkdownElementTypes.LINK_TEXT) + val destination = node.child(MarkdownElementTypes.LINK_DESTINATION) + if (linkTextNode != null) { + if (destination != null) { + val link = ContentExternalLink(destination.text) + renderLinkTextTo(linkTextNode, link, linkResolver) + parent.append(link) + } else { + val link = ContentExternalLink(linkTextNode.getLabelText()) + renderLinkTextTo(linkTextNode, link, linkResolver) + parent.append(link) + } + } + } + MarkdownElementTypes.SHORT_REFERENCE_LINK, + MarkdownElementTypes.FULL_REFERENCE_LINK -> { + val labelElement = node.child(MarkdownElementTypes.LINK_LABEL) + if (labelElement != null) { + val linkInfo = linkResolver.getLinkInfo(labelElement.text) + val labelText = labelElement.getLabelText() + val link = linkInfo?.let { linkResolver.resolve(it.destination.toString()) } ?: linkResolver.resolve(labelText) + val linkText = node.child(MarkdownElementTypes.LINK_TEXT) + if (linkText != null) { + renderLinkTextTo(linkText, link, linkResolver) + } else { + link.append(ContentText(labelText)) + } + parent.append(link) + } + } + MarkdownTokenTypes.WHITE_SPACE -> { + // Don't append first space if start of header (it is added during formatting later) + // v + // #### Some Heading + if (nodeStack.peek() !is ContentHeading || node.parent?.children?.first() != node) { + parent.append(ContentText(node.text)) + } + } + MarkdownTokenTypes.EOL -> { + if ((keepEol(nodeStack.peek()) && node.parent?.children?.last() != node) || + // Keep extra blank lines when processing lists (affects Markdown formatting) + (processingList(nodeStack.peek()) && node.previous?.type == MarkdownTokenTypes.EOL)) { + parent.append(ContentText(node.text)) + } + } + + MarkdownTokenTypes.CODE_LINE -> { + val content = ContentText(node.text) + if (parent is ContentBlockCode) { + parent.append(content) + } else { + parent.append(ContentBlockCode().apply { append(content) }) + } + } + + 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.EMPH -> { + val parentNodeType = node.parent?.type + if (parentNodeType != MarkdownElementTypes.EMPH && parentNodeType != MarkdownElementTypes.STRONG) { + parent.append(ContentText(node.text)) + } + } + + MarkdownTokenTypes.COLON, + MarkdownTokenTypes.SINGLE_QUOTE, + MarkdownTokenTypes.DOUBLE_QUOTE, + MarkdownTokenTypes.LT, + MarkdownTokenTypes.GT, + MarkdownTokenTypes.LPAREN, + MarkdownTokenTypes.RPAREN, + MarkdownTokenTypes.LBRACKET, + MarkdownTokenTypes.RBRACKET, + MarkdownTokenTypes.EXCLAMATION_MARK, + MarkdownTokenTypes.BACKTICK, + MarkdownTokenTypes.CODE_FENCE_CONTENT -> { + parent.append(ContentText(node.text)) + } + + MarkdownElementTypes.LINK_DEFINITION -> { + } + + else -> { + processChildren() + } + } + } +} + +private fun MarkdownNode.getLabelText() = children.filter { it.type == MarkdownTokenTypes.TEXT || it.type == MarkdownTokenTypes.EMPH }.joinToString("") { it.text } + +private fun keepEol(node: ContentNode) = node is ContentParagraph || node is ContentSection || node is ContentBlockCode +private fun processingList(node: ContentNode) = node is ContentOrderedList || node is ContentUnorderedList + +fun buildInlineContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: LinkResolver) { + val inlineContent = tree.children.singleOrNull { it.type == MarkdownElementTypes.PARAGRAPH }?.children ?: listOf(tree) + inlineContent.forEach { + buildContentTo(it, target, linkResolver) + } +} + +fun renderLinkTextTo(tree: MarkdownNode, target: ContentBlock, linkResolver: LinkResolver) { + val linkTextNodes = tree.children.drop(1).dropLast(1) + linkTextNodes.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 000000000..d73bef4a5 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt @@ -0,0 +1,72 @@ +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.descriptors.TypeAliasDescriptor +import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink + +class DeclarationLinkResolver + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val refGraph: NodeReferenceGraph, + val logger: DokkaLogger, + val options: DocumentationOptions, + val externalDocumentationLinkResolver: ExternalDocumentationLinkResolver, + val elementSignatureProvider: ElementSignatureProvider) { + + + fun tryResolveContentLink(fromDescriptor: DeclarationDescriptor, href: String): ContentBlock? { + val symbol = try { + val symbols = resolveKDocLink(resolutionFacade.resolveSession.bindingContext, + 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) { + val externalHref = externalDocumentationLinkResolver.buildExternalDocumentationLink(symbol) + if (externalHref != null) { + return ContentExternalLink(externalHref) + } + val signature = elementSignatureProvider.signature(symbol) + val referencedAt = fromDescriptor.signatureWithSourceLocation() + + return ContentNodeLazyLink(href, { -> + val target = refGraph.lookup(signature) + + if (target == null) { + logger.warn("Can't find node by signature `$signature`, referenced at $referencedAt") + } + target + }) + } + if ("/" in href) { + return ContentExternalLink(href) + } + return null + } + + fun resolveContentLink(fromDescriptor: DeclarationDescriptor, href: String) = + tryResolveContentLink(fromDescriptor, href) ?: run { + logger.warn("Unresolved link to $href in doc comment of ${fromDescriptor.signatureWithSourceLocation()}") + 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() + } + if (symbol is TypeAliasDescriptor && !symbol.isDocumented(options)) { + return symbol.classDescriptor + } + return symbol + } + +} diff --git a/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt b/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt new file mode 100644 index 000000000..098a17f97 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt @@ -0,0 +1,339 @@ +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.intellij.markdown.parser.LinkMap +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Samples.SampleProcessingService +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny +import org.jetbrains.kotlin.idea.kdoc.findKDoc +import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag +import org.jetbrains.kotlin.kdoc.psi.api.KDoc +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.psi.KtBinaryExpressionWithTypeRHS +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.annotations.argumentValue +import org.jetbrains.kotlin.resolve.constants.StringValue +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered +import org.jetbrains.kotlin.resolve.source.PsiSourceElement +import java.util.regex.Pattern + +private val REF_COMMAND = "ref" +private val NAME_COMMAND = "name" +private val DESCRIPTION_COMMAND = "description" +private val TEXT = Pattern.compile("(\\S+)\\s*(.*)", Pattern.DOTALL) +private val NAME_TEXT = Pattern.compile("(\\S+)(.*)", Pattern.DOTALL) + +class DescriptorDocumentationParser @Inject constructor( + val options: DocumentationOptions, + val logger: DokkaLogger, + val linkResolver: DeclarationLinkResolver, + val resolutionFacade: DokkaResolutionFacade, + val refGraph: NodeReferenceGraph, + val sampleService: SampleProcessingService, + val signatureProvider: KotlinElementSignatureProvider, + val externalDocumentationLinkResolver: ExternalDocumentationLinkResolver +) { + 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 || + descriptor is EnumEntrySyntheticClassDescriptor) { + return parseJavadoc(descriptor) + } + + val kdoc = descriptor.findKDoc() ?: findStdlibKDoc(descriptor) + if (kdoc == null) { + if (options.effectivePackageOptions(descriptor.fqNameSafe).reportUndocumented && !descriptor.isDeprecated() && + descriptor !is ValueParameterDescriptor && descriptor !is TypeParameterDescriptor && + descriptor !is PropertyAccessorDescriptor && !descriptor.isSuppressWarning()) { + logger.warn("No documentation for ${descriptor.signatureWithSourceLocation()}") + } + return Content.Empty to { node -> } + } + + val contextDescriptor = + (PsiTreeUtil.getParentOfType(kdoc, KDoc::class.java)?.context as? KtDeclaration) + ?.takeIf { it != descriptor.original.sourcePsi() } + ?.resolveToDescriptorIfAny() + ?: descriptor + + // This will build the initial node for all content above the tags, however we also sometimes have @Sample + // tags between content, so we handle that case below + 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 linkMap = LinkMap.buildLinkMap(tree.node, kdocText) + val content = buildContent(tree, LinkResolver(linkMap, { href -> linkResolver.resolveContentLink(contextDescriptor, href) }), inline) + if (kdoc is KDocSection) { + val tags = kdoc.getTags() + tags.forEach { + when (it.knownTag) { + KDocKnownTag.SAMPLE -> { + content.append(sampleService.resolveSample(contextDescriptor, it.getSubjectName(), it)) + // If the sample tag has text below it, it will be considered as the child of the tag, so add it + val tagSubContent = it.getContent() + if (tagSubContent.isNotBlank()) { + val markdownNode = parseMarkdown(tagSubContent) + buildInlineContentTo(markdownNode, content, LinkResolver(linkMap, { href -> linkResolver.resolveContentLink(contextDescriptor, href) })) + } + } + KDocKnownTag.SEE -> + content.addTagToSeeAlso(contextDescriptor, it) + KDocKnownTag.PARAM -> { + val section = content.addSection(javadocSectionDisplayName(it.name), it.getSubjectName()) + section.append(ParameterInfoNode { + val signature = signatureProvider.signature(descriptor) + refGraph.lookupOrWarn(signature, logger)?.details?.find { node -> + node.kind == NodeKind.Parameter && node.name == it.getSubjectName() + } + }) + val sectionContent = it.getContent() + val markdownNode = parseMarkdown(sectionContent) + buildInlineContentTo(markdownNode, section, LinkResolver(linkMap, { href -> linkResolver.resolveContentLink(contextDescriptor, href) })) + } + else -> { + val section = content.addSection(javadocSectionDisplayName(it.name), it.getSubjectName()) + val sectionContent = it.getContent() + val markdownNode = parseMarkdown(sectionContent) + buildInlineContentTo(markdownNode, section, LinkResolver(linkMap, { href -> linkResolver.resolveContentLink(contextDescriptor, href) })) + } + } + } + } + return content to { node -> + if (kdoc is KDocSection) { + val tags = kdoc.getTags() + node.addExtraTags(tags, descriptor) + } + } + } + + /** + * Adds @attr tag. There are 3 types of syntax for this: + * *@attr ref <android.>R.styleable.<attribute_name> + * *@attr name <attribute_name> + * *@attr description <attribute_description> + * This also adds the @since and @apiSince tags. + */ + private fun DocumentationNode.addExtraTags(tags: Array<KDocTag>, descriptor: DeclarationDescriptor) { + tags.forEach { + val name = it.name + if (name?.toLowerCase() == "attr") { + it.getAttr(descriptor)?.let { append(it, RefKind.Detail) } + } else if (name?.toLowerCase() == "since" || name?.toLowerCase() == "apisince") { + val apiLevel = DocumentationNode(it.getContent(), Content.Empty, NodeKind.ApiLevel) + append(apiLevel, RefKind.Detail) + } else if (name?.toLowerCase() == "sdkextsince") { + val sdkExtSince = DocumentationNode(it.getContent(), Content.Empty, NodeKind.SdkExtSince) + append(sdkExtSince, RefKind.Detail) + } else if (name?.toLowerCase() == "deprecatedsince") { + val deprecatedLevel = DocumentationNode(it.getContent(), Content.Empty, NodeKind.DeprecatedLevel) + append(deprecatedLevel, RefKind.Detail) + } else if (name?.toLowerCase() == "artifactid") { + val artifactId = DocumentationNode(it.getContent(), Content.Empty, NodeKind.ArtifactId) + append(artifactId, RefKind.Detail) + } + } + } + + private fun DeclarationDescriptor.isSuppressWarning(): Boolean { + val suppressAnnotation = annotations.findAnnotation(FqName(Suppress::class.qualifiedName!!)) + return if (suppressAnnotation != null) { + @Suppress("UNCHECKED_CAST") + (suppressAnnotation.argumentValue("names")?.value as List<StringValue>).any { it.value == "NOT_DOCUMENTED" } + } else containingDeclaration?.isSuppressWarning() ?: false + } + + /** + * 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.getTopLevelClassifierDescriptors( + FqName.fromSegments(listOf("kotlin", "Any")), NoLookupLocation.FROM_IDE) + anyClassDescriptors.forEach { + val anyMethod = (it as ClassDescriptor).getMemberScope(listOf()) + .getDescriptorsFiltered(DescriptorKindFilter.FUNCTIONS, { it == descriptor.name }) + .single() + val kdoc = anyMethod.findKDoc() + 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, + logger, + signatureProvider, + externalDocumentationLinkResolver + ).parseDocumentation(psi as PsiNamedElement) + return parseResult.content to { node -> + parseResult.deprecatedContent?.let { + val deprecationNode = DocumentationNode("", it, NodeKind.Modifier) + node.append(deprecationNode, RefKind.Deprecation) + } + if (node.kind in NodeKind.classLike) { + parseResult.attributeRefs.forEach { + val signature = node.detailOrNull(NodeKind.Signature) + val signatureName = signature?.name + val classAttrSignature = "${signatureName}:$it" + refGraph.register(classAttrSignature, DocumentationNode(node.name, Content.Empty, NodeKind.Attribute)) + refGraph.link(node, classAttrSignature, RefKind.Detail) + refGraph.link(classAttrSignature, node, RefKind.Owner) + refGraph.link(classAttrSignature, it, RefKind.AttributeRef) + } + } else if (node.kind in NodeKind.memberLike) { + parseResult.attributeRefs.forEach { + refGraph.link(node, it, RefKind.HiddenLink) + } + } + parseResult.apiLevel?.let { + node.append(it, RefKind.Detail) + } + parseResult.sdkExtSince?.let { + node.append(it, RefKind.Detail) + } + parseResult.deprecatedLevel?.let { + node.append(it, RefKind.Detail) + } + parseResult.artifactId?.let { + node.append(it, RefKind.Detail) + } + parseResult.attribute?.let { + val signature = node.detailOrNull(NodeKind.Signature) + val signatureName = signature?.name + val attrSignature = "AttrMain:$signatureName" + refGraph.register(attrSignature, it) + refGraph.link(attrSignature, node, RefKind.AttributeSource) + } + } + } + return Content.Empty to { _ -> } + } + + fun KDocSection.getTags(): Array<KDocTag> = PsiTreeUtil.getChildrenOfType(this, KDocTag::class.java) + ?: arrayOf() + + private fun MutableContent.addTagToSeeAlso(descriptor: DeclarationDescriptor, seeTag: KDocTag) { + addTagToSection(seeTag, descriptor, "See Also") + } + + private fun MutableContent.addTagToSection(seeTag: KDocTag, descriptor: DeclarationDescriptor, sectionName: String) { + val subjectName = seeTag.getSubjectName() + if (subjectName != null) { + val section = findSectionByTag(sectionName) ?: addSection(sectionName, null) + val link = linkResolver.resolveContentLink(descriptor, subjectName) + link.append(ContentText(subjectName)) + val para = ContentParagraph() + para.append(link) + section.append(para) + } + } + + private fun KDocTag.getAttr(descriptor: DeclarationDescriptor): DocumentationNode? { + var attribute: DocumentationNode? = null + val matcher = TEXT.matcher(getContent()) + if (matcher.matches()) { + val command = matcher.group(1) + val more = matcher.group(2) + attribute = when (command) { + REF_COMMAND -> { + val attrRef = more.trim() + val qualified = attrRef.split('.', '#') + val targetDescriptor = resolveKDocLink(resolutionFacade.resolveSession.bindingContext, resolutionFacade, descriptor, this, qualified) + DocumentationNode(attrRef, Content.Empty, NodeKind.Attribute).also { + if (targetDescriptor.isNotEmpty()) { + refGraph.link(it, targetDescriptor.first().signature(), RefKind.Detail) + } + } + } + NAME_COMMAND -> { + val nameMatcher = NAME_TEXT.matcher(more) + if (nameMatcher.matches()) { + val attrName = nameMatcher.group(1) + DocumentationNode(attrName, Content.Empty, NodeKind.Attribute) + } else { + null + } + } + DESCRIPTION_COMMAND -> { + val attrDescription = more + DocumentationNode(attrDescription, Content.Empty, NodeKind.Attribute) + } + else -> null + } + } + return attribute + } + +} + +/** + * Lazily executed wrapper node holding a [NodeKind.Parameter] node that will be used to add type + * and default value information to + * [org.jetbrains.dokka.Formats.DevsiteLayoutHtmlFormatOutputBuilder]. + * + * We make this a [ContentBlock] instead of a [ContentNode] so we won't fallback to calling + * [toString] on this and trying to add it to documentation somewhere - returning an empty list + * should make this a no-op. + * + * @property wrappedNode lazily executable lambda that will return the matching documentation node + * for this parameter (if it exists) + */ +class ParameterInfoNode(private val wrappedNode: () -> DocumentationNode?) : ContentBlock() { + private var computed = false + + val parameterContent: NodeRenderContent? + get() = lazyNode + + private var lazyNode: NodeRenderContent? = null + get() { + if (!computed) { + computed = true + + val node = wrappedNode() + if (node != null) { + field = NodeRenderContent(node, LanguageService.RenderMode.SUMMARY) + } + } + return field + } + + override val children = arrayListOf<ContentNode>() +} diff --git a/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt b/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt new file mode 100644 index 000000000..b9fe8483e --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt @@ -0,0 +1,1177 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiField +import com.intellij.psi.PsiJavaFile +import org.jetbrains.dokka.DokkaConfiguration.* +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.annotations.AnnotationDescriptorImpl +import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor +import org.jetbrains.kotlin.idea.kdoc.findKDoc +import org.jetbrains.kotlin.idea.util.fuzzyExtensionReceiverType +import org.jetbrains.kotlin.idea.util.makeNotNullable +import org.jetbrains.kotlin.idea.util.toFuzzyType +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtModifierListOwner +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtVariableDeclaration +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.constants.ConstantValue +import org.jetbrains.kotlin.resolve.descriptorUtil.* +import org.jetbrains.kotlin.resolve.findTopMostOverriddenDescriptors +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered +import org.jetbrains.kotlin.resolve.source.PsiSourceElement +import org.jetbrains.kotlin.resolve.source.getPsi +import org.jetbrains.kotlin.types.* +import org.jetbrains.kotlin.types.typeUtil.supertypes +import org.jetbrains.kotlin.util.supertypesWithAny +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import com.google.inject.name.Named as GuiceNamed + +class DocumentationOptions(val outputDir: String, + val outputFormat: String, + includeNonPublic: Boolean = false, + val includeRootPackage: Boolean = false, + reportUndocumented: Boolean = true, + val skipEmptyPackages: Boolean = true, + skipDeprecated: Boolean = false, + jdkVersion: Int = 6, + val generateClassIndexPage: Boolean = true, + val generatePackageIndexPage: Boolean = true, + val sourceLinks: List<SourceLinkDefinition> = emptyList(), + val impliedPlatforms: List<String> = emptyList(), + // Sorted by pattern length + perPackageOptions: List<PackageOptions> = emptyList(), + externalDocumentationLinks: List<ExternalDocumentationLink> = emptyList(), + noStdlibLink: Boolean, + noJdkLink: Boolean = false, + val languageVersion: String?, + val apiVersion: String?, + cacheRoot: String? = null, + val suppressedFiles: Set<File> = emptySet(), + val collectInheritedExtensionsFromLibraries: Boolean = false, + val outlineRoot: String = "", + val dacRoot: String = "") { + init { + if (perPackageOptions.any { it.prefix == "" }) + throw IllegalArgumentException("Please do not register packageOptions with all match pattern, use global settings instead") + } + + val perPackageOptions = perPackageOptions.sortedByDescending { it.prefix.length } + val rootPackageOptions = PackageOptionsImpl("", includeNonPublic, reportUndocumented, skipDeprecated) + + fun effectivePackageOptions(pack: String): PackageOptions = perPackageOptions.firstOrNull { pack == it.prefix || pack.startsWith(it.prefix + ".") } ?: rootPackageOptions + fun effectivePackageOptions(pack: FqName): PackageOptions = effectivePackageOptions(pack.asString()) + + val defaultLinks = run { + val links = mutableListOf<ExternalDocumentationLink>() + //links += ExternalDocumentationLink.Builder("https://developer.android.com/reference/").build() + if (!noJdkLink) + links += ExternalDocumentationLink.Builder("http://docs.oracle.com/javase/$jdkVersion/docs/api/").build() + + if (!noStdlibLink) + links += ExternalDocumentationLink.Builder("https://kotlinlang.org/api/latest/jvm/stdlib/").build() + links + } + + val externalDocumentationLinks = defaultLinks + externalDocumentationLinks + + val cacheRoot: Path? = when { + cacheRoot == "default" -> Paths.get(System.getProperty("user.home"), ".cache", "dokka") + cacheRoot != null -> Paths.get(cacheRoot) + else -> null + } +} + +private fun isExtensionForExternalClass(extensionFunctionDescriptor: DeclarationDescriptor, + extensionReceiverDescriptor: DeclarationDescriptor, + allFqNames: Collection<FqName>): Boolean { + val extensionFunctionPackage = DescriptorUtils.getParentOfType(extensionFunctionDescriptor, PackageFragmentDescriptor::class.java) + val extensionReceiverPackage = DescriptorUtils.getParentOfType(extensionReceiverDescriptor, PackageFragmentDescriptor::class.java) + return extensionFunctionPackage != null && extensionReceiverPackage != null && + extensionFunctionPackage.fqName != extensionReceiverPackage.fqName && + extensionReceiverPackage.fqName !in allFqNames +} + +interface PackageDocumentationBuilder { + fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>, + allFqNames: Collection<FqName>) +} + +interface DefaultPlatformsProvider { + fun getDefaultPlatforms(descriptor: DeclarationDescriptor): List<String> +} + +val ignoredSupertypes = setOf( + "kotlin.Annotation", "kotlin.Enum", "kotlin.Any" +) + +class DocumentationBuilder +@Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val descriptorDocumentationParser: DescriptorDocumentationParser, + val options: DocumentationOptions, + val refGraph: NodeReferenceGraph, + val platformNodeRegistry: PlatformNodeRegistry, + val logger: DokkaLogger, + val linkResolver: DeclarationLinkResolver, + val defaultPlatformsProvider: DefaultPlatformsProvider) { + 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: RefKind) { + refGraph.link(node, descriptor.signature(), kind) + } + + fun link(fromDescriptor: DeclarationDescriptor?, toDescriptor: DeclarationDescriptor?, kind: RefKind) { + 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: NodeKind, + external: Boolean = false + ): DocumentationNode where T : DeclarationDescriptor, T : Named { + val (doc, callback) = + if (external) { + Content.Empty to { node -> } + } else { + descriptorDocumentationParser.parseDocumentationAndDetails( + descriptor, + kind == NodeKind.Parameter + ) + } + val node = DocumentationNode(descriptor.name.asString(), doc, kind).withModifiers(descriptor) + node.appendSignature(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, NodeKind.Modifier) + } + + fun DocumentationNode.appendVisibility(descriptor: DeclarationDescriptorWithVisibility) { + val modifier = descriptor.visibility.normalize().displayName + appendTextNode(modifier, NodeKind.Modifier) + } + + fun DocumentationNode.appendSupertype(descriptor: ClassDescriptor, superType: KotlinType, backref: Boolean) { + val unwrappedType = superType.unwrap() + if (unwrappedType is AbbreviatedType) { + appendSupertype(descriptor, unwrappedType.abbreviation, backref) + } else { + appendType(unwrappedType, NodeKind.Supertype) + val superclass = unwrappedType.constructor.declarationDescriptor + if (backref) { + link(superclass, descriptor, RefKind.Inheritor) + } + link(descriptor, superclass, RefKind.Superclass) + } + } + + fun DocumentationNode.appendProjection(projection: TypeProjection, kind: NodeKind = NodeKind.Type) { + if (projection.isStarProjection) { + appendTextNode("*", NodeKind.Type) + } else { + appendType(projection.type, kind, projection.projectionKind.label) + } + } + + fun DocumentationNode.appendType(kotlinType: KotlinType?, kind: NodeKind = NodeKind.Type, prefix: String = "") { + if (kotlinType == null) + return + (kotlinType.unwrap() as? AbbreviatedType)?.let { + return appendType(it.abbreviation) + } + + if (kotlinType.isDynamic()) { + append(DocumentationNode("dynamic", Content.Empty, kind), RefKind.Detail) + 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, NodeKind.Modifier) + } + if (kotlinType.isNullabilityFlexible()) { + node.appendTextNode("!", NodeKind.NullabilityModifier) + } else if (kotlinType.isMarkedNullable) { + node.appendTextNode("?", NodeKind.NullabilityModifier) + } + if (classifierDescriptor != null) { + val externalLink = + linkResolver.externalDocumentationLinkResolver.buildExternalDocumentationLink(classifierDescriptor) + if (externalLink != null) { + if (classifierDescriptor !is TypeParameterDescriptor) { + val targetNode = + refGraph.lookup(classifierDescriptor.signature()) ?: classifierDescriptor.build(true) + node.append(targetNode, RefKind.ExternalType) + node.append(DocumentationNode(externalLink, Content.Empty, NodeKind.ExternalLink), RefKind.Link) + } + } + link( + node, classifierDescriptor, + if (classifierDescriptor.isBoringBuiltinClass()) RefKind.HiddenLink else RefKind.Link + ) + if (classifierDescriptor !is TypeParameterDescriptor) { + node.append( + DocumentationNode( + classifierDescriptor.fqNameUnsafe.asString(), + Content.Empty, + NodeKind.QualifiedName + ), RefKind.Detail + ) + } + } + + + append(node, RefKind.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.forEach { + it.build()?.let { annotationNode -> + if (annotationNode.isSinceKotlin()) { + appendSinceKotlin(annotationNode) + } + else { + val refKind = when { + it.isDocumented() -> + when { + annotationNode.isDeprecation() -> RefKind.Deprecation + else -> RefKind.Annotation + } + it.isHiddenInDocumentation() -> RefKind.HiddenAnnotation + else -> return@forEach + } + append(annotationNode, refKind) + if (refKind == RefKind.Deprecation) annotationNode.convertDeprecationDetailsToChildren() + } + } + } + } + + fun DocumentationNode.appendExternalLink(externalLink: String) { + append(DocumentationNode(externalLink, Content.Empty, NodeKind.ExternalLink), RefKind.Link) + } + + fun DocumentationNode.appendExternalLink(descriptor: DeclarationDescriptor) { + val target = linkResolver.externalDocumentationLinkResolver.buildExternalDocumentationLink(descriptor) + if (target != null) { + appendExternalLink(target) + } + } + + fun DocumentationNode.appendSinceKotlin(annotation: DocumentationNode) { + val kotlinVersion = annotation + .detail(NodeKind.Parameter) + .detail(NodeKind.Value) + .name.removeSurrounding("\"") + + append(platformNodeRegistry["Kotlin " + kotlinVersion], RefKind.Platform) + } + + 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, NodeKind.Modifier) + } + } + } + + fun DocumentationNode.appendDefaultPlatforms(descriptor: DeclarationDescriptor) { + for (platform in defaultPlatformsProvider.getDefaultPlatforms(descriptor)) { + append(platformNodeRegistry[platform], RefKind.Platform) + } + } + + fun DocumentationNode.isDeprecation() = name == "Deprecated" || name == "deprecated" + + fun DocumentationNode.isSinceKotlin() = name == "SinceKotlin" && kind == NodeKind.Annotation + + fun DocumentationNode.appendSourceLink(sourceElement: SourceElement) { + appendSourceLink(sourceElement.getPsi(), options.sourceLinks) + } + + fun DocumentationNode.appendSignature(descriptor: DeclarationDescriptor) { + appendTextNode(descriptor.signature(), NodeKind.Signature, RefKind.Detail) + } + + fun DocumentationNode.appendChild(descriptor: DeclarationDescriptor, kind: RefKind): DocumentationNode? { + if (!descriptor.isGenerated() && descriptor.isDocumented(options)) { + val node = descriptor.build() + append(node, kind) + return node + } + return null + } + + fun createGroupNode(signature: String, nodes: List<DocumentationNode>) = (nodes.find { it.kind == NodeKind.GroupNode } ?: + DocumentationNode(nodes.first().name, Content.Empty, NodeKind.GroupNode).apply { + appendTextNode(signature, NodeKind.Signature, RefKind.Detail) + }) + .also { groupNode -> + nodes.forEach { node -> + if (node != groupNode) { + node.owner?.let { owner -> + node.dropReferences { it.to == owner && it.kind == RefKind.Owner } + owner.dropReferences { it.to == node && it.kind == RefKind.Member } + owner.append(groupNode, RefKind.Member) + } + groupNode.append(node, RefKind.Member) + } + } + } + + + fun DocumentationNode.appendOrUpdateMember(descriptor: DeclarationDescriptor) { + if (descriptor.isGenerated() || !descriptor.isDocumented(options)) return + + val existingNode = refGraph.lookup(descriptor.signature()) + if (existingNode != null) { + if (existingNode.kind == NodeKind.TypeAlias && descriptor is ClassDescriptor + || existingNode.kind == NodeKind.Class && descriptor is TypeAliasDescriptor) { + val node = createGroupNode(descriptor.signature(), listOf(existingNode, descriptor.build())) + register(descriptor, node) + return + } + + existingNode.updatePlatforms(descriptor) + + if (descriptor is ClassDescriptor) { + val membersToDocument = descriptor.collectMembersToDocument() + for ((memberDescriptor, inheritedLinkKind, extraModifier) in membersToDocument) { + if (memberDescriptor is ClassDescriptor) { + existingNode.appendOrUpdateMember(memberDescriptor) // recurse into nested classes + } + else { + val existingMemberNode = refGraph.lookup(memberDescriptor.signature()) + if (existingMemberNode != null) { + existingMemberNode.updatePlatforms(memberDescriptor) + } + else { + existingNode.appendClassMember(memberDescriptor, inheritedLinkKind, extraModifier) + } + } + } + } + } + else { + appendChild(descriptor, RefKind.Member) + } + } + + private fun DocumentationNode.updatePlatforms(descriptor: DeclarationDescriptor) { + for (platform in defaultPlatformsProvider.getDefaultPlatforms(descriptor) - platforms) { + append(platformNodeRegistry[platform], RefKind.Platform) + } + } + + fun DocumentationNode.appendClassMember(descriptor: DeclarationDescriptor, + inheritedLinkKind: RefKind = RefKind.InheritedMember, + extraModifier: String?) { + if (descriptor is CallableMemberDescriptor && descriptor.kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE) { + val baseDescriptor = descriptor.overriddenDescriptors.firstOrNull() + if (baseDescriptor != null) { + link(this, baseDescriptor, inheritedLinkKind) + } + } else { + val descriptorToUse = if (descriptor is ConstructorDescriptor) descriptor else descriptor.original + val child = appendChild(descriptorToUse, RefKind.Member) + if (extraModifier != null) { + child?.appendTextNode("static", NodeKind.Modifier) + } + } + } + + fun DocumentationNode.appendInPageChildren(descriptors: Iterable<DeclarationDescriptor>, kind: RefKind) { + descriptors.forEach { descriptor -> + val node = appendChild(descriptor, kind) + node?.addReferenceTo(this, RefKind.TopLevelPage) + } + } + + fun DocumentationModule.appendFragments(fragments: Collection<PackageFragmentDescriptor>, + packageContent: Map<String, Content>, + packageDocumentationBuilder: PackageDocumentationBuilder) { + val allFqNames = fragments.map { it.fqName }.distinct() + + for (packageName in allFqNames) { + if (packageName.isRoot && !options.includeRootPackage) continue + val declarations = fragments.filter { it.fqName == packageName }.flatMap { it.getMemberScope().getContributedDescriptors() } + + if (options.skipEmptyPackages && declarations.none { it.isDocumented(options) }) continue + logger.info(" package $packageName: ${declarations.count()} declarations") + val packageNode = findOrCreatePackageNode(this, packageName.asString(), packageContent, this@DocumentationBuilder.refGraph) + packageDocumentationBuilder.buildPackageDocumentation(this@DocumentationBuilder, packageName, packageNode, + declarations, allFqNames) + } + + } + + fun propagateExtensionFunctionsToSubclasses( + fragments: Collection<PackageFragmentDescriptor>, + resolutionFacade: DokkaResolutionFacade + ) { + + val moduleDescriptor = resolutionFacade.moduleDescriptor + + // Wide-collect all view descriptors + val allPackageViewDescriptors = generateSequence(listOf(moduleDescriptor.getPackage(FqName.ROOT))) { packages -> + packages + .flatMap { pkg -> + moduleDescriptor.getSubPackagesOf(pkg.fqName) { true } + }.map { fqName -> + moduleDescriptor.getPackage(fqName) + }.takeUnless { it.isEmpty() } + }.flatten() + + val allDescriptors = + if (options.collectInheritedExtensionsFromLibraries) { + allPackageViewDescriptors.map { it.memberScope } + } else { + fragments.asSequence().map { it.getMemberScope() } + }.flatMap { + it.getDescriptorsFiltered( + DescriptorKindFilter.CALLABLES + ).asSequence() + } + + + val documentingDescriptors = fragments.flatMap { it.getMemberScope().getContributedDescriptors() } + val documentingClasses = documentingDescriptors.filterIsInstance<ClassDescriptor>() + + val classHierarchy = buildClassHierarchy(documentingClasses) + + val allExtensionFunctions = + allDescriptors + .filterIsInstance<CallableMemberDescriptor>() + .filter { it.extensionReceiverParameter != null } + val extensionFunctionsByName = allExtensionFunctions.groupBy { it.name } + + for (extensionFunction in allExtensionFunctions) { + if (extensionFunction.dispatchReceiverParameter != null) continue + val possiblyShadowingFunctions = extensionFunctionsByName[extensionFunction.name] + ?.filter { fn -> fn.canShadow(extensionFunction) } + ?: emptyList() + + if (extensionFunction.extensionReceiverParameter?.type?.isDynamic() == true) continue + val subclasses = + classHierarchy.filter { (key) -> key.isExtensionApplicable(extensionFunction) } + if (subclasses.isEmpty()) continue + subclasses.values.flatten().forEach { subclass -> + if (subclass.isExtensionApplicable(extensionFunction) && + possiblyShadowingFunctions.none { subclass.isExtensionApplicable(it) }) { + + val hasExternalLink = + linkResolver.externalDocumentationLinkResolver.buildExternalDocumentationLink( + extensionFunction + ) != null + if (hasExternalLink) { + val containerDesc = + extensionFunction.containingDeclaration as? PackageFragmentDescriptor + if (containerDesc != null) { + val container = refGraph.lookup(containerDesc.signature()) + ?: containerDesc.buildExternal() + container.append(extensionFunction.buildExternal(), RefKind.Member) + } + } + + refGraph.link(subclass.signature(), extensionFunction.signature(), RefKind.Extension) + } + } + } + } + + private fun ClassDescriptor.isExtensionApplicable(extensionFunction: CallableMemberDescriptor): Boolean { + val receiverType = extensionFunction.fuzzyExtensionReceiverType()?.makeNotNullable() + val classType = defaultType.toFuzzyType(declaredTypeParameters) + return receiverType != null && classType.checkIsSubtypeOf(receiverType) != null + } + + private fun buildClassHierarchy(classes: List<ClassDescriptor>): Map<ClassDescriptor, List<ClassDescriptor>> { + val result = hashMapOf<ClassDescriptor, MutableList<ClassDescriptor>>() + classes.forEach { cls -> + TypeUtils.getAllSupertypes(cls.defaultType).forEach { supertype -> + val classDescriptor = supertype.constructor.declarationDescriptor as? ClassDescriptor + if (classDescriptor != null) { + val subtypesList = result.getOrPut(classDescriptor) { arrayListOf() } + subtypesList.add(cls) + } + } + } + return result + } + + private fun CallableMemberDescriptor.canShadow(other: CallableMemberDescriptor): Boolean { + if (this == other) return false + if (this is PropertyDescriptor && other is PropertyDescriptor) { + return true + } + if (this is FunctionDescriptor && other is FunctionDescriptor) { + val parameters1 = valueParameters + val parameters2 = other.valueParameters + if (parameters1.size != parameters2.size) { + return false + } + for ((p1, p2) in parameters1 zip parameters2) { + if (p1.type != p2.type) { + return false + } + } + return true + } + return false + } + + fun DeclarationDescriptor.build(): DocumentationNode = when (this) { + is ClassifierDescriptor -> build() + is ConstructorDescriptor -> build() + is PropertyDescriptor -> build() + is FunctionDescriptor -> build() + is ValueParameterDescriptor -> build() + is ReceiverParameterDescriptor -> build() + else -> throw IllegalStateException("Descriptor $this is not known") + } + + fun PackageFragmentDescriptor.buildExternal(): DocumentationNode { + val node = DocumentationNode(fqName.asString(), Content.Empty, NodeKind.Package) + + val externalLink = linkResolver.externalDocumentationLinkResolver.buildExternalDocumentationLink(this) + if (externalLink != null) { + node.append(DocumentationNode(externalLink, Content.Empty, NodeKind.ExternalLink), RefKind.Link) + } + register(this, node) + return node + } + + fun CallableDescriptor.buildExternal(): DocumentationNode = when(this) { + is FunctionDescriptor -> build(true) + is PropertyDescriptor -> build(true) + else -> throw IllegalStateException("Descriptor $this is not known") + } + + + fun ClassifierDescriptor.build(external: Boolean = false): DocumentationNode = when (this) { + is ClassDescriptor -> build(external) + is TypeAliasDescriptor -> build(external) + is TypeParameterDescriptor -> build() + else -> throw IllegalStateException("Descriptor $this is not known") + } + + fun TypeAliasDescriptor.build(external: Boolean = false): DocumentationNode { + val node = nodeForDescriptor(this, NodeKind.TypeAlias) + + if (!external) { + node.appendAnnotations(this) + } + node.appendModifiers(this) + node.appendInPageChildren(typeConstructor.parameters, RefKind.Detail) + + node.appendType(underlyingType, NodeKind.TypeAliasUnderlyingType) + + if (!external) { + node.appendSourceLink(source) + node.appendDefaultPlatforms(this) + } + register(this, node) + return node + } + + fun ClassDescriptor.build(external: Boolean = false): DocumentationNode { + val kind = when { + kind == ClassKind.OBJECT -> NodeKind.Object + kind == ClassKind.INTERFACE -> NodeKind.Interface + kind == ClassKind.ENUM_CLASS -> NodeKind.Enum + kind == ClassKind.ANNOTATION_CLASS -> NodeKind.AnnotationClass + kind == ClassKind.ENUM_ENTRY -> NodeKind.EnumItem + isSubclassOfThrowable() -> NodeKind.Exception + else -> NodeKind.Class + } + val node = nodeForDescriptor(this, kind, external) + register(this, node) + supertypesWithAnyPrecise().forEach { + node.appendSupertype(this, it, !external) + } + if (getKind() != ClassKind.OBJECT && getKind() != ClassKind.ENUM_ENTRY) { + node.appendInPageChildren(typeConstructor.parameters, RefKind.Detail) + } + if (!external) { + for ((descriptor, inheritedLinkKind, extraModifier) in collectMembersToDocument()) { + node.appendClassMember(descriptor, inheritedLinkKind, extraModifier) + } + node.appendAnnotations(this) + } + node.appendModifiers(this) + if (!external) { + node.appendSourceLink(source) + node.appendDefaultPlatforms(this) + } + return node + } + + data class ClassMember(val descriptor: DeclarationDescriptor, + val inheritedLinkKind: RefKind = RefKind.InheritedMember, + val extraModifier: String? = null) + + fun ClassDescriptor.collectMembersToDocument(): List<ClassMember> { + val result = arrayListOf<ClassMember>() + if (kind != ClassKind.OBJECT && kind != ClassKind.ENUM_ENTRY) { + val constructorsToDocument = if (kind == ClassKind.ENUM_CLASS) + constructors.filter { it.valueParameters.size > 0 } + else + constructors + constructorsToDocument.mapTo(result) { ClassMember(it) } + } + + defaultType.memberScope.getContributedDescriptors() + .filter { it != companionObjectDescriptor } + .mapTo(result) { ClassMember(it) } + + staticScope.getContributedDescriptors() + .mapTo(result) { ClassMember(it, extraModifier = "static") } + + val companionObjectDescriptor = companionObjectDescriptor + if (companionObjectDescriptor != null && companionObjectDescriptor.isDocumented(options)) { + val descriptors = companionObjectDescriptor.defaultType.memberScope.getContributedDescriptors() + val descriptorsToDocument = descriptors.filter { it !is CallableDescriptor || !it.isInheritedFromAny() } + descriptorsToDocument.mapTo(result) { + ClassMember(it, inheritedLinkKind = RefKind.InheritedCompanionObjectMember) + } + + if (companionObjectDescriptor.getAllSuperclassesWithoutAny().isNotEmpty() + || companionObjectDescriptor.getSuperInterfaces().isNotEmpty()) { + result += ClassMember(companionObjectDescriptor) + } + } + return result + } + + fun CallableDescriptor.isInheritedFromAny(): Boolean { + return findTopMostOverriddenDescriptors().any { + DescriptorUtils.getFqNameSafe(it.containingDeclaration).asString() == "kotlin.Any" + } + } + + fun ClassDescriptor.isSubclassOfThrowable(): Boolean = + defaultType.supertypes().any { it.constructor.declarationDescriptor == builtIns.throwable } + + fun ConstructorDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, NodeKind.Constructor) + node.appendInPageChildren(valueParameters, RefKind.Detail) + node.appendDefaultPlatforms(this) + 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(external: Boolean = false): DocumentationNode { + if (ErrorUtils.containsErrorTypeInParameters(this) || ErrorUtils.containsErrorType(this.returnType)) { + logger.warn("Found an unresolved type in ${signatureWithSourceLocation()}") + } + + val node = nodeForDescriptor(this, if (inCompanionObject()) NodeKind.CompanionObjectFunction else NodeKind.Function, external) + + node.appendInPageChildren(typeParameters, RefKind.Detail) + extensionReceiverParameter?.let { node.appendChild(it, RefKind.Detail) } + node.appendInPageChildren(valueParameters, RefKind.Detail) + node.appendType(returnType) + node.appendAnnotations(this) + node.appendModifiers(this) + if (!external) { + node.appendSourceLink(source) + node.appendDefaultPlatforms(this) + } else { + node.appendExternalLink(this) + } + + 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, RefKind.Override) + } else { + baseClassFunction.overriddenDescriptors.forEach { + addOverrideLink(it, overridingFunction) + } + } + } + + fun PropertyDescriptor.build(external: Boolean = false): DocumentationNode { + val node = nodeForDescriptor( + this, + if (inCompanionObject()) NodeKind.CompanionObjectProperty else NodeKind.Property, + external + ) + node.appendInPageChildren(typeParameters, RefKind.Detail) + extensionReceiverParameter?.let { node.appendChild(it, RefKind.Detail) } + node.appendType(returnType) + node.appendAnnotations(this) + node.appendModifiers(this) + if (!external) { + node.appendSourceLink(source) + if (isVar) { + node.appendTextNode("var", NodeKind.Modifier) + } + + if (isConst) { + val psi = sourcePsi() + val valueText = when (psi) { + is KtVariableDeclaration -> psi.initializer?.text + is PsiField -> psi.initializer?.text + else -> null + } + valueText?.let { node.appendTextNode(it, NodeKind.Value) } + } + + + getter?.let { + if (!it.isDefault) { + node.addAccessorDocumentation(descriptorDocumentationParser.parseDocumentation(it), "Getter") + } + } + setter?.let { + if (!it.isDefault) { + node.addAccessorDocumentation(descriptorDocumentationParser.parseDocumentation(it), "Setter") + } + } + node.appendDefaultPlatforms(this) + } + if (external) { + node.appendExternalLink(this) + } + + 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, NodeKind.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, NodeKind.Value) + } + } + } + node.appendAnnotations(this) + node.appendModifiers(this) + if (varargElementType != null && node.details(NodeKind.Modifier).none { it.name == "vararg" }) { + node.appendTextNode("vararg", NodeKind.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, NodeKind.TypeParameter) + if (prefix != "") { + node.appendTextNode(prefix, NodeKind.Modifier) + } + if (isReified) { + node.appendTextNode("reified", NodeKind.Modifier) + } + + for (constraint in upperBounds) { + if (KotlinBuiltIns.isDefaultBound(constraint)) { + continue + } + node.appendType(constraint, NodeKind.UpperBound) + } + register(this, node) + return node + } + + fun ReceiverParameterDescriptor.build(): DocumentationNode { + var receiverClass: DeclarationDescriptor = type.constructor.declarationDescriptor!! + if ((receiverClass as? ClassDescriptor)?.isCompanionObject ?: false) { + receiverClass = receiverClass.containingDeclaration!! + } else if (receiverClass is TypeParameterDescriptor) { + val upperBoundClass = receiverClass.upperBounds.singleOrNull()?.constructor?.declarationDescriptor + if (upperBoundClass != null) { + receiverClass = upperBoundClass + } + } + + if ((containingDeclaration as? FunctionDescriptor)?.dispatchReceiverParameter == null) { + link(receiverClass, containingDeclaration, RefKind.Extension) + } + + val node = DocumentationNode(name.asString(), Content.Empty, NodeKind.Receiver) + node.appendType(type) + register(this, node) + return node + } + + fun AnnotationDescriptor.build(isWithinReplaceWith: Boolean = false): DocumentationNode? { + val annotationClass = type.constructor.declarationDescriptor + if (annotationClass == null || ErrorUtils.isError(annotationClass)) { + return null + } + val node = DocumentationNode(annotationClass.name.asString(), Content.Empty, NodeKind.Annotation) + allValueArguments.forEach foreach@{ (name, value) -> + if (name.toString() == "imports" && value.toString() == "[]") return@foreach + var valueNode: DocumentationNode? = null + if (value.toString() == "@kotlin.ReplaceWith") { + valueNode = (value.value as AnnotationDescriptor).build(true) + } + else valueNode = value.toDocumentationNode(isWithinReplaceWith) + if (valueNode != null) { + val paramNode = DocumentationNode(name.asString(), Content.Empty, NodeKind.Parameter) + paramNode.append(valueNode, RefKind.Detail) + node.append(paramNode, RefKind.Detail) + } + } + return node + } + + fun ConstantValue<*>.toDocumentationNode(isWithinReplaceWith: Boolean = false): DocumentationNode? = value?.let { value -> + when (value) { + is String -> + (if (isWithinReplaceWith) "Replace with: " else "") + "\"" + StringUtil.escapeStringCharacters(value) + "\"" + is EnumEntrySyntheticClassDescriptor -> + value.containingDeclaration.name.asString() + "." + value.name.asString() + is Pair<*, *> -> { + val (classId, name) = value + if (classId is ClassId && name is Name) { + classId.shortClassName.asString() + "." + name.asString() + } else { + value.toString() + } + } + else -> value.toString() + }.let { valueString -> + DocumentationNode(valueString, Content.Empty, NodeKind.Value) + } + } + + + fun DocumentationNode.getParentForPackageMember(descriptor: DeclarationDescriptor, + externalClassNodes: MutableMap<FqName, DocumentationNode>, + allFqNames: Collection<FqName>): DocumentationNode { + if (descriptor is CallableMemberDescriptor) { + val extensionClassDescriptor = descriptor.getExtensionClassDescriptor() + if (extensionClassDescriptor != null && isExtensionForExternalClass(descriptor, extensionClassDescriptor, allFqNames) && + !ErrorUtils.isError(extensionClassDescriptor)) { + val fqName = DescriptorUtils.getFqNameSafe(extensionClassDescriptor) + return externalClassNodes.getOrPut(fqName, { + val newNode = DocumentationNode(fqName.asString(), Content.Empty, NodeKind.ExternalClass) + val externalLink = linkResolver.externalDocumentationLinkResolver.buildExternalDocumentationLink(extensionClassDescriptor) + if (externalLink != null) { + newNode.append(DocumentationNode(externalLink, Content.Empty, NodeKind.ExternalLink), RefKind.Link) + } + append(newNode, RefKind.Member) + newNode + }) + } + } + return this + } + +} + +fun DeclarationDescriptor.isDocumented(options: DocumentationOptions): Boolean { + return (options.effectivePackageOptions(fqNameSafe).includeNonPublic + || this !is MemberDescriptor + || this.visibility.isPublicAPI) + && !isDocumentationSuppressed(options) + && (!options.effectivePackageOptions(fqNameSafe).skipDeprecated || !isDeprecated()) +} + +private fun DeclarationDescriptor.isGenerated() = this is CallableMemberDescriptor && kind != CallableMemberDescriptor.Kind.DECLARATION + +class KotlinPackageDocumentationBuilder : PackageDocumentationBuilder { + override fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>, + allFqNames: Collection<FqName>) { + val externalClassNodes = hashMapOf<FqName, DocumentationNode>() + declarations.forEach { descriptor -> + with(documentationBuilder) { + if (descriptor.isDocumented(options)) { + val parent = packageNode.getParentForPackageMember(descriptor, externalClassNodes, allFqNames) + parent.appendOrUpdateMember(descriptor) + } + } + } + } +} + +class KotlinJavaDocumentationBuilder +@Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val documentationBuilder: DocumentationBuilder, + val options: DocumentationOptions, + val logger: DokkaLogger) : JavaDocumentationBuilder { + override fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) { + val classDescriptors = file.classes.map { + it.getJavaClassDescriptor(resolutionFacade) + } + + if (classDescriptors.any { it != null && it.isDocumented(options) }) { + val packageNode = findOrCreatePackageNode(module, file.packageName, packageContent, documentationBuilder.refGraph) + + for (descriptor in classDescriptors.filterNotNull()) { + with(documentationBuilder) { + packageNode.appendChild(descriptor, RefKind.Member) + } + } + } + } +} + +private val hiddenAnnotations = setOf( + KotlinBuiltIns.FQ_NAMES.parameterName.asString() +) + +private fun AnnotationDescriptor.isHiddenInDocumentation() = + type.constructor.declarationDescriptor?.fqNameSafe?.asString() in hiddenAnnotations + +private fun AnnotationDescriptor.isDocumented(): Boolean { + if (source.getPsi() != null && mustBeDocumented()) return true + val annotationClassName = type.constructor.declarationDescriptor?.fqNameSafe?.asString() + return annotationClassName == KotlinBuiltIns.FQ_NAMES.extensionFunctionType.asString() +} + +fun AnnotationDescriptor.mustBeDocumented(): Boolean { + val annotationClass = type.constructor.declarationDescriptor as? Annotated ?: return false + return annotationClass.isDocumentedAnnotation() +} + +fun DeclarationDescriptor.isDocumentationSuppressed(options: DocumentationOptions): Boolean { + + if (options.effectivePackageOptions(fqNameSafe).suppress) return true + + val path = this.findPsi()?.containingFile?.virtualFile?.path + if (path != null) { + if (File(path).absoluteFile in options.suppressedFiles) return true + } + + val doc = findKDoc() + if (doc is KDocSection && doc.findTagByName("suppress") != null) return true + + return hasSuppressDocTag(sourcePsi()) || hasHideAnnotation(sourcePsi()) +} + +fun DeclarationDescriptor.sourcePsi() = + ((original as? DeclarationDescriptorWithSource)?.source as? PsiSourceElement)?.psi + +fun DeclarationDescriptor.isDeprecated(): Boolean = annotations.any { + DescriptorUtils.getFqName(it.type.constructor.declarationDescriptor!!).asString() == "kotlin.Deprecated" +} || (this is ConstructorDescriptor && containingDeclaration.isDeprecated()) + +fun CallableMemberDescriptor.getExtensionClassDescriptor(): ClassifierDescriptor? { + val extensionReceiver = extensionReceiverParameter + if (extensionReceiver != null) { + val type = extensionReceiver.type + val receiverClass = type.constructor.declarationDescriptor as? ClassDescriptor + if (receiverClass?.isCompanionObject ?: false) { + return receiverClass?.containingDeclaration as? ClassifierDescriptor + } + return receiverClass + } + return null +} + +fun DeclarationDescriptor.signature(): String { + if (this != original) return original.signature() + return when (this) { + is ClassDescriptor, + is PackageFragmentDescriptor, + is PackageViewDescriptor, + is TypeAliasDescriptor -> 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 + is ReceiverParameterDescriptor -> 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 }.toMutableList() + val extensionReceiver = extensionReceiverParameter + if (extensionReceiver != null) { + params.add(0, extensionReceiver.type) + } + return params.joinToString(prefix = "(", postfix = ")") { it.signature() } +} + +fun KotlinType.signature(): String { + val visited = hashSetOf<KotlinType>() + + fun KotlinType.signatureRecursive(): String { + if (this in visited) { + return "" + } + visited.add(this) + + val declarationDescriptor = constructor.declarationDescriptor ?: return "<null>" + val typeName = DescriptorUtils.getFqName(declarationDescriptor).asString() + if (arguments.isEmpty()) { + return typeName + } + return typeName + arguments.joinToString(prefix = "((", postfix = "))") { it.type.signatureRecursive() } + } + + return signatureRecursive() +} + +fun DeclarationDescriptor.signatureWithSourceLocation(): String { + val signature = signature() + val sourceLocation = sourceLocation() + return if (sourceLocation != null) "$signature ($sourceLocation)" else signature +} + +fun DeclarationDescriptor.sourceLocation(): String? { + val psi = sourcePsi() + if (psi != null) { + val fileName = psi.containingFile.name + val lineNumber = psi.lineNumber() + return if (lineNumber != null) "$fileName:$lineNumber" else fileName + } + return null +} + +fun DocumentationModule.prepareForGeneration(options: DocumentationOptions) { + if (options.generateClassIndexPage) { + generateAllTypesNode() + } + nodeRefGraph.resolveReferences() +} + +fun DocumentationNode.generateAllTypesNode() { + val allTypes = members(NodeKind.Package) + .flatMap { it.members.filter { it.kind in NodeKind.classLike || it.kind == NodeKind.ExternalClass } } + .sortedBy { if (it.kind == NodeKind.ExternalClass) it.name.substringAfterLast('.').toLowerCase() else it.name.toLowerCase() } + + val allTypesNode = DocumentationNode("alltypes", Content.Empty, NodeKind.AllTypes) + for (typeNode in allTypes) { + allTypesNode.addReferenceTo(typeNode, RefKind.Member) + } + + append(allTypesNode, RefKind.Member) +} + +fun ClassDescriptor.supertypesWithAnyPrecise(): Collection<KotlinType> { + if (KotlinBuiltIns.isAny(this)) { + return emptyList() + } + return typeConstructor.supertypesWithAny() +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Kotlin/ExternalDocumentationLinkResolver.kt b/core/src/main/kotlin/Kotlin/ExternalDocumentationLinkResolver.kt new file mode 100644 index 000000000..d09bc1c9f --- /dev/null +++ b/core/src/main/kotlin/Kotlin/ExternalDocumentationLinkResolver.kt @@ -0,0 +1,258 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.Singleton +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.util.io.* +import org.jetbrains.dokka.Formats.FileGeneratorBasedFormatDescriptor +import org.jetbrains.dokka.Formats.FormatDescriptor +import org.jetbrains.dokka.Utilities.ServiceLocator +import org.jetbrains.dokka.Utilities.lookup +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor +import org.jetbrains.kotlin.load.java.descriptors.* +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.parents +import java.io.ByteArrayOutputStream +import java.io.PrintWriter +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection +import java.nio.file.Path +import java.security.MessageDigest +import javax.inject.Named +import kotlin.reflect.full.findAnnotation + +fun ByteArray.toHexString() = this.joinToString(separator = "") { "%02x".format(it) } + +@Singleton +class ExternalDocumentationLinkResolver @Inject constructor( + val options: DocumentationOptions, + @Named("libraryResolutionFacade") val libraryResolutionFacade: DokkaResolutionFacade, + val logger: DokkaLogger +) { + + val packageFqNameToLocation = mutableMapOf<FqName, ExternalDocumentationRoot>() + val formats = mutableMapOf<String, InboundExternalLinkResolutionService>() + + class ExternalDocumentationRoot(val rootUrl: URL, val resolver: InboundExternalLinkResolutionService, val locations: Map<String, String>) { + override fun toString(): String = rootUrl.toString() + } + + val cacheDir: Path? = options.cacheRoot?.resolve("packageListCache")?.apply { toFile().mkdirs() } + + val cachedProtocols = setOf("http", "https", "ftp") + + fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection { + val connection = this.openConnection() + connection.connectTimeout = timeout + connection.readTimeout = timeout + + when (connection) { + is HttpURLConnection -> { + return when (connection.responseCode) { + in 200..299 -> { + connection + } + HttpURLConnection.HTTP_MOVED_PERM, + HttpURLConnection.HTTP_MOVED_TEMP, + HttpURLConnection.HTTP_SEE_OTHER -> { + if (redirectsAllowed > 0) { + val newUrl = connection.getHeaderField("Location") + URL(newUrl).doOpenConnectionToReadContent(timeout, redirectsAllowed - 1) + } else { + throw RuntimeException("Too many redirects") + } + } + else -> { + throw RuntimeException("Unhandled http code: ${connection.responseCode}") + } + } + } + else -> return connection + } + } + + fun loadPackageList(link: DokkaConfiguration.ExternalDocumentationLink) { + + val packageListUrl = link.packageListUrl + val needsCache = packageListUrl.protocol in cachedProtocols + + val packageListStream = if (cacheDir != null && needsCache) { + val packageListLink = packageListUrl.toExternalForm() + + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(packageListLink.toByteArray(Charsets.UTF_8)).toHexString() + val cacheEntry = cacheDir.resolve(hash).toFile() + + if (cacheEntry.exists()) { + try { + val connection = packageListUrl.doOpenConnectionToReadContent() + val originModifiedDate = connection.date + val cacheDate = cacheEntry.lastModified() + if (originModifiedDate > cacheDate || originModifiedDate == 0L) { + if (originModifiedDate == 0L) + logger.warn("No date header for $packageListUrl, downloading anyway") + else + logger.info("Renewing package-list from $packageListUrl") + connection.getInputStream().copyTo(cacheEntry.outputStream()) + } + } catch (e: Exception) { + logger.error("Failed to update package-list cache for $link") + val baos = ByteArrayOutputStream() + PrintWriter(baos).use { + e.printStackTrace(it) + } + baos.flush() + logger.error(baos.toString()) + } + } else { + logger.info("Downloading package-list from $packageListUrl") + packageListUrl.openStream().copyTo(cacheEntry.outputStream()) + } + cacheEntry.inputStream() + } else { + packageListUrl.doOpenConnectionToReadContent().getInputStream() + } + + val (params, packages) = + packageListStream + .bufferedReader() + .useLines { lines -> lines.partition { it.startsWith(DOKKA_PARAM_PREFIX) } } + + val paramsMap = params.asSequence() + .map { it.removePrefix(DOKKA_PARAM_PREFIX).split(":", limit = 2) } + .groupBy({ (key, _) -> key }, { (_, value) -> value }) + + val format = paramsMap["format"]?.singleOrNull() ?: "javadoc" + + val locations = paramsMap["location"].orEmpty() + .map { it.split("\u001f", limit = 2) } + .map { (key, value) -> key to value } + .toMap() + + + val defaultResolverDesc = services["dokka-default"]!! + val resolverDesc = services[format] + ?: defaultResolverDesc.takeIf { format in formatsWithDefaultResolver } + ?: defaultResolverDesc.also { + logger.warn("Couldn't find InboundExternalLinkResolutionService(format = `$format`) for $link, using Dokka default") + } + + + val resolverClass = javaClass.classLoader.loadClass(resolverDesc.className).kotlin + + val constructors = resolverClass.constructors + + val constructor = constructors.singleOrNull() + ?: constructors.first { it.findAnnotation<Inject>() != null } + val resolver = constructor.call(paramsMap) as InboundExternalLinkResolutionService + + val rootInfo = ExternalDocumentationRoot(link.url, resolver, locations) + + packages.map { FqName(it) }.forEach { packageFqNameToLocation[it] = rootInfo } + } + + init { + options.externalDocumentationLinks.forEach { + try { + loadPackageList(it) + } catch (e: Exception) { + throw RuntimeException("Exception while loading package-list from $it", e) + } + } + } + + fun buildExternalDocumentationLink(element: PsiElement): String? { + return element.extractDescriptor(libraryResolutionFacade)?.let { + buildExternalDocumentationLink(it) + } + } + + fun buildExternalDocumentationLink(symbol: DeclarationDescriptor): String? { + val packageFqName: FqName = + when (symbol) { + is PackageFragmentDescriptor -> symbol.fqName + is DeclarationDescriptorNonRoot -> symbol.parents.firstOrNull { it is PackageFragmentDescriptor }?.fqNameSafe ?: return null + else -> return null + } + + val externalLocation = packageFqNameToLocation[packageFqName] ?: return null + + val path = externalLocation.locations[symbol.signature()] ?: + externalLocation.resolver.getPath(symbol) ?: return null + + return URL(externalLocation.rootUrl, path).toExternalForm() + } + + companion object { + const val DOKKA_PARAM_PREFIX = "\$dokka." + val services = ServiceLocator.allServices("inbound-link-resolver").associateBy { it.name } + private val formatsWithDefaultResolver = + ServiceLocator + .allServices("format") + .filter { + val desc = ServiceLocator.lookup<FormatDescriptor>(it) as? FileGeneratorBasedFormatDescriptor + desc?.generatorServiceClass == FileGenerator::class + }.map { it.name } + .toSet() + } +} + + +interface InboundExternalLinkResolutionService { + fun getPath(symbol: DeclarationDescriptor): String? + + class Javadoc(paramsMap: Map<String, List<String>>) : InboundExternalLinkResolutionService { + override fun getPath(symbol: DeclarationDescriptor): String? { + if (symbol is EnumEntrySyntheticClassDescriptor) { + return getPath(symbol.containingDeclaration)?.let { it + "#" + symbol.name.asString() } + } else if (symbol is ClassDescriptor) { + return DescriptorUtils.getFqName(symbol).asString().replace(".", "/") + ".html" + } else if (symbol is JavaCallableMemberDescriptor) { + val containingClass = symbol.containingDeclaration as? JavaClassDescriptor ?: return null + val containingClassLink = getPath(containingClass) + if (containingClassLink != null) { + if (symbol is JavaMethodDescriptor || symbol is JavaClassConstructorDescriptor) { + val psi = symbol.sourcePsi() as? PsiMethod + if (psi != null) { + val params = psi.parameterList.parameters.joinToString { it.type.canonicalText } + return containingClassLink + "#" + symbol.name + "(" + params + ")" + } + } else if (symbol is JavaPropertyDescriptor) { + return "$containingClassLink#${symbol.name}" + } + } + } + // TODO Kotlin javadoc + return null + } + } + + class Dokka(val paramsMap: Map<String, List<String>>) : InboundExternalLinkResolutionService { + val extension = paramsMap["linkExtension"]?.singleOrNull() ?: error("linkExtension not provided for Dokka resolver") + + override fun getPath(symbol: DeclarationDescriptor): String? { + val leafElement = when (symbol) { + is CallableDescriptor, is TypeAliasDescriptor -> true + else -> false + } + val path = getPathWithoutExtension(symbol) + if (leafElement) return "$path.$extension" + else return "$path/index.$extension" + } + + private fun getPathWithoutExtension(symbol: DeclarationDescriptor): String { + return when { + symbol.containingDeclaration == null -> identifierToFilename(symbol.name.asString()) + symbol is PackageFragmentDescriptor -> identifierToFilename(symbol.fqName.asString()) + else -> getPathWithoutExtension(symbol.containingDeclaration!!) + '/' + identifierToFilename(symbol.name.asString()) + } + } + + } +} + diff --git a/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt b/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt new file mode 100644 index 000000000..c5fb15385 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt @@ -0,0 +1,68 @@ +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.elements.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>, + allFqNames: Collection<FqName>) { + 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, + documentationBuilder.linkResolver.externalDocumentationLinkResolver + ) + + 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<*, *>).kotlinOrigin as? KtDeclaration ?: return true + + 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.kotlinOrigin as? KtDeclaration ?: 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, emptyList(), null, null) + } +} diff --git a/core/src/main/kotlin/Kotlin/KotlinAsJavaElementSignatureProvider.kt b/core/src/main/kotlin/Kotlin/KotlinAsJavaElementSignatureProvider.kt new file mode 100644 index 000000000..20ea179ee --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinAsJavaElementSignatureProvider.kt @@ -0,0 +1,25 @@ +package org.jetbrains.dokka + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.asJava.toLightElements +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.psi.KtElement + +class KotlinAsJavaElementSignatureProvider : ElementSignatureProvider { + + private fun PsiElement.javaLikePsi() = when { + this is KtElement -> toLightElements().firstOrNull() + else -> this + } + + override fun signature(forPsi: PsiElement): String { + return getSignature(forPsi.javaLikePsi()) ?: + "not implemented for $forPsi" + } + + override fun signature(forDesc: DeclarationDescriptor): String { + val sourcePsi = forDesc.sourcePsi() + return getSignature(sourcePsi?.javaLikePsi()) ?: + "not implemented for $forDesc with psi: $sourcePsi" + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Kotlin/KotlinElementSignatureProvider.kt b/core/src/main/kotlin/Kotlin/KotlinElementSignatureProvider.kt new file mode 100644 index 000000000..bcac01829 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinElementSignatureProvider.kt @@ -0,0 +1,34 @@ +package org.jetbrains.dokka + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMember +import com.intellij.psi.PsiPackage +import org.jetbrains.kotlin.asJava.elements.KtLightElement +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.BindingContext +import javax.inject.Inject + +class KotlinElementSignatureProvider @Inject constructor( + val resolutionFacade: DokkaResolutionFacade +) : ElementSignatureProvider { + override fun signature(forPsi: PsiElement): String { + return forPsi.extractDescriptor(resolutionFacade) + ?.let { signature(it) } + ?: run { "no desc for $forPsi in ${(forPsi as? PsiMember)?.containingClass}" } + } + + override fun signature(forDesc: DeclarationDescriptor): String = forDesc.signature() +} + + +fun PsiElement.extractDescriptor(resolutionFacade: DokkaResolutionFacade): DeclarationDescriptor? { + val forPsi = this + + return when (forPsi) { + is KtLightElement<*, *> -> return (forPsi.kotlinOrigin!!).extractDescriptor(resolutionFacade) + is PsiPackage -> resolutionFacade.moduleDescriptor.getPackage(FqName(forPsi.qualifiedName)) + is PsiMember -> forPsi.getJavaOrKotlinMemberDescriptor(resolutionFacade) + else -> resolutionFacade.resolveSession.bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, forPsi] + } +} diff --git a/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt b/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt new file mode 100644 index 000000000..8a33ff8b7 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt @@ -0,0 +1,463 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.classNodeNameWithOuterClass +import org.jetbrains.dokka.LanguageService.RenderMode + +/** + * Implements [LanguageService] and provides rendering of symbols in Kotlin language + */ +class KotlinLanguageService : CommonLanguageService() { + override fun showModifierInSummary(node: DocumentationNode): Boolean { + return node.name !in fullOnlyModifiers + } + + private val fullOnlyModifiers = + setOf("public", "protected", "private", "inline", "noinline", "crossinline", "reified") + + override fun render(node: DocumentationNode, renderMode: RenderMode): ContentNode { + return content { + when (node.kind) { + NodeKind.Package -> if (renderMode == RenderMode.FULL) renderPackage(node) + in NodeKind.classLike -> renderClass(node, renderMode) + + NodeKind.EnumItem -> renderClass(node, renderMode) + NodeKind.ExternalClass -> if (renderMode == RenderMode.FULL) identifier(node.name) + + NodeKind.Parameter -> renderParameter(node, renderMode) + NodeKind.TypeParameter -> renderTypeParameter(node, renderMode) + NodeKind.Type, + NodeKind.UpperBound -> renderType(node, renderMode) + + NodeKind.Modifier -> renderModifier(this, node, renderMode) + NodeKind.Constructor, + NodeKind.Function, + NodeKind.CompanionObjectFunction -> renderFunction(node, renderMode) + NodeKind.Property, + NodeKind.CompanionObjectProperty -> renderProperty(node, renderMode) + else -> identifier(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(NodeKind.TypeParameter).any() } ?: return null + return content { + val typeParameter = functionWithTypeParameter.details(NodeKind.TypeParameter).first() + if (functionWithTypeParameter.kind == NodeKind.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 != NodeKind.Function && kind != NodeKind.Property) return null + val receiver = details(NodeKind.Receiver).singleOrNull() ?: return null + return receiver.detail(NodeKind.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.renderFunctionalTypeParameterName(node: DocumentationNode, renderMode: RenderMode) { + node.references(RefKind.HiddenAnnotation).map { it.to } + .find { it.name == "ParameterName" }?.let { + val parameterNameValue = it.detail(NodeKind.Parameter).detail(NodeKind.Value) + identifier(parameterNameValue.name.removeSurrounding("\""), IdentifierKind.ParameterName) + symbol(":") + nbsp() + } + } + + private fun ContentBlock.renderFunctionalType(node: DocumentationNode, renderMode: RenderMode) { + var typeArguments = node.details(NodeKind.Type) + + if (node.name.startsWith("Suspend")) { + keyword("suspend ") + } + + // lambda + val isExtension = node.annotations.any { it.name == "ExtensionFunctionType" } + if (isExtension) { + renderType(typeArguments.first(), renderMode) + symbol(".") + typeArguments = typeArguments.drop(1) + } + symbol("(") + renderList(typeArguments.take(typeArguments.size - 1), noWrap = true) { + renderFunctionalTypeParameterName(it, renderMode) + renderType(it, renderMode) + } + symbol(")") + nbsp() + symbol("->") + nbsp() + renderType(typeArguments.last(), renderMode) + + } + + private fun DocumentationNode.isFunctionalType(): Boolean { + val typeArguments = details(NodeKind.Type) + val functionalTypeName = "Function${typeArguments.count() - 1}" + val suspendFunctionalTypeName = "Suspend$functionalTypeName" + return name == functionalTypeName || name == suspendFunctionalTypeName + } + + private fun ContentBlock.renderType(node: DocumentationNode, renderMode: RenderMode) { + if (node.name == "dynamic") { + keyword("dynamic") + return + } + if (node.isFunctionalType()) { + renderFunctionalType(node, renderMode) + return + } + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode, true) + renderLinked(this, node) { + identifier(it.typeDeclarationClass?.classNodeNameWithOuterClass() ?: it.name, IdentifierKind.TypeName) + } + val typeArguments = node.details(NodeKind.Type) + if (typeArguments.isNotEmpty()) { + symbol("<") + renderList(typeArguments, noWrap = true) { + renderType(it, renderMode) + } + symbol(">") + } + val nullabilityModifier = node.details(NodeKind.NullabilityModifier).singleOrNull() + if (nullabilityModifier != null) { + symbol(nullabilityModifier.name) + } + } + + override fun renderModifier( + block: ContentBlock, + node: DocumentationNode, + renderMode: RenderMode, + nowrap: Boolean + ) { + when (node.name) { + "final", "public", "var" -> { + } + else -> { + if (node.name !in fullOnlyModifiers || renderMode == RenderMode.FULL) { + super.renderModifier(block, node, renderMode, nowrap) + } + } + } + } + + private fun ContentBlock.renderTypeParameter(node: DocumentationNode, renderMode: RenderMode) { + renderModifiersForNode(node, renderMode, true) + + identifier(node.name) + + val constraints = node.details(NodeKind.UpperBound) + if (constraints.size == 1) { + 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, node.detailOrNull(NodeKind.Signature)?.name) + symbol(":") + nbsp() + val parameterType = node.detail(NodeKind.Type) + renderType(parameterType, renderMode) + val valueNode = node.details(NodeKind.Value).firstOrNull() + if (valueNode != null) { + nbsp() + symbol("=") + nbsp() + text(valueNode.name) + } + } + + private fun ContentBlock.renderTypeParametersForNode(node: DocumentationNode, renderMode: RenderMode) { + val typeParameters = node.details(NodeKind.TypeParameter) + if (typeParameters.any()) { + symbol("<") + renderList(typeParameters) { + renderTypeParameter(it, renderMode) + } + symbol(">") + } + } + + private fun ContentBlock.renderExtraTypeParameterConstraints(node: DocumentationNode, renderMode: RenderMode) { + val parametersWithMultipleConstraints = + node.details(NodeKind.TypeParameter).filter { it.details(NodeKind.UpperBound).size > 1 } + val parametersWithConstraints = parametersWithMultipleConstraints + .flatMap { parameter -> + parameter.details(NodeKind.UpperBound).map { constraint -> parameter to constraint } + } + if (parametersWithMultipleConstraints.isNotEmpty()) { + keyword(" where ") + renderList(parametersWithConstraints) { + identifier(it.first.name) + nbsp() + symbol(":") + nbsp() + renderType(it.second, renderMode) + } + } + } + + private fun ContentBlock.renderSupertypesForNode(node: DocumentationNode, renderMode: RenderMode) { + val supertypes = node.details(NodeKind.Supertype).filterNot { it.qualifiedNameFromType() in ignoredSupertypes } + if (supertypes.any()) { + nbsp() + symbol(":") + nbsp() + renderList(supertypes) { + indentedSoftLineBreak() + renderType(it, renderMode) + } + } + } + + 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(NodeKind.Parameter) + if (!parameters.isEmpty()) { + symbol("(") + renderList(parameters) { + text(it.detail(NodeKind.Value).name) + } + symbol(")") + } + text(" ") + } + + private fun ContentBlock.renderClass(node: DocumentationNode, renderMode: RenderMode) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + NodeKind.Class, + NodeKind.AnnotationClass, + NodeKind.Exception, + NodeKind.Enum -> keyword("class ") + NodeKind.Interface -> keyword("interface ") + NodeKind.EnumItem -> keyword("enum val ") + NodeKind.Object -> keyword("object ") + NodeKind.TypeAlias -> keyword("typealias ") + else -> throw IllegalArgumentException("Node $node is not a class-like object") + } + + identifierOrDeprecated(node) + renderTypeParametersForNode(node, renderMode) + renderSupertypesForNode(node, renderMode) + renderExtraTypeParameterConstraints(node, renderMode) + + if (node.kind == NodeKind.TypeAlias) { + nbsp() + symbol("=") + nbsp() + renderType(node.detail(NodeKind.TypeAliasUnderlyingType), renderMode) + } + } + + private fun ContentBlock.renderFunction( + node: DocumentationNode, + renderMode: RenderMode, + signatureMapper: SignatureMapper? = null + ) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + NodeKind.Constructor -> identifier(node.owner!!.name) + NodeKind.Function, + NodeKind.CompanionObjectFunction -> keyword("fun ") + else -> throw IllegalArgumentException("Node $node is not a function-like object") + } + renderTypeParametersForNode(node, renderMode) + if (node.details(NodeKind.TypeParameter).any()) { + text(" ") + } + + renderReceiver(node, renderMode, signatureMapper) + + if (node.kind != NodeKind.Constructor) + identifierOrDeprecated(node) + + symbol("(") + val parameters = node.details(NodeKind.Parameter) + renderHardWrappingList(parameters) { + renderParameter(it, renderMode) + } + if (needReturnType(node)) { + if (parameters.size > 1) { + hardLineBreak() + } + symbol(")") + symbol(": ") + renderType(node.detail(NodeKind.Type), renderMode) + } else { + symbol(")") + } + renderExtraTypeParameterConstraints(node, renderMode) + } + + private fun ContentBlock.renderReceiver( + node: DocumentationNode, + renderMode: RenderMode, + signatureMapper: SignatureMapper? + ) { + val receiver = node.details(NodeKind.Receiver).singleOrNull() + if (receiver != null) { + if (signatureMapper != null) { + signatureMapper.renderReceiver(receiver, this) + } else { + val type = receiver.detail(NodeKind.Type) + + if (type.isFunctionalType()) { + symbol("(") + renderFunctionalType(type, renderMode) + symbol(")") + } else { + renderType(type, renderMode) + } + } + symbol(".") + } + } + + private fun needReturnType(node: DocumentationNode) = when (node.kind) { + NodeKind.Constructor -> false + else -> !node.isUnitReturnType() + } + + fun DocumentationNode.isUnitReturnType(): Boolean = + detail(NodeKind.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) { + NodeKind.Property, + NodeKind.CompanionObjectProperty -> keyword("${node.getPropertyKeyword()} ") + else -> throw IllegalArgumentException("Node $node is not a property") + } + renderTypeParametersForNode(node, renderMode) + if (node.details(NodeKind.TypeParameter).any()) { + text(" ") + } + + renderReceiver(node, renderMode, signatureMapper) + + identifierOrDeprecated(node) + symbol(": ") + renderType(node.detail(NodeKind.Type), renderMode) + renderExtraTypeParameterConstraints(node, renderMode) + } + + fun DocumentationNode.getPropertyKeyword() = + if (details(NodeKind.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(): String { + return details.firstOrNull { it.kind == NodeKind.QualifiedName }?.name + ?: (links.firstOrNull { it.kind != NodeKind.ExternalLink } ?: hiddenLinks.firstOrNull())?.qualifiedName() + ?: name +} + + +val DocumentationNode.typeDeclarationClass + get() = (links.firstOrNull { it.kind in NodeKind.classLike } ?: externalType) diff --git a/core/src/main/kotlin/Languages/CommonLanguageService.kt b/core/src/main/kotlin/Languages/CommonLanguageService.kt new file mode 100644 index 000000000..c6e3cd37f --- /dev/null +++ b/core/src/main/kotlin/Languages/CommonLanguageService.kt @@ -0,0 +1,118 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.Formats.nameWithOuterClass + + +abstract class CommonLanguageService : LanguageService { + + protected fun ContentBlock.renderPackage(node: DocumentationNode) { + keyword("package") + nbsp() + identifier(node.name) + } + + override fun renderName(node: DocumentationNode): String { + return when (node.kind) { + NodeKind.Constructor -> node.owner!!.name + else -> node.name + } + } + + override fun renderNameWithOuterClass(node: DocumentationNode): String { + return when (node.kind) { + NodeKind.Constructor -> node.owner!!.nameWithOuterClass() + else -> node.nameWithOuterClass() + } + } + + open fun renderModifier( + block: ContentBlock, + node: DocumentationNode, + renderMode: LanguageService.RenderMode, + nowrap: Boolean = false + ) = with(block) { + keyword(node.name) + if (nowrap) { + nbsp() + } else { + text(" ") + } + } + + protected fun renderLinked( + block: ContentBlock, + node: DocumentationNode, + body: ContentBlock.(DocumentationNode) -> Unit + ) = with(block) { + val to = node.links.firstOrNull() + if (to == null) + body(node) + else + link(to) { + this.body(node) + } + } + + protected fun <T> ContentBlock.renderHardWrappingList( + nodes: List<T>, separator: String = ", ", + renderItem: (T) -> Unit + ) { + if (nodes.none()) + return + + if (nodes.count() > 1) { + hardLineBreak() + repeat(4) { + nbsp() + } + } + + renderItem(nodes.first()) + nodes.drop(1).forEach { + symbol(separator) + hardLineBreak() + repeat(4) { + nbsp() + } + renderItem(it) + } + } + + protected fun <T> ContentBlock.renderList( + nodes: List<T>, separator: String = ", ", + noWrap: Boolean = false, renderItem: (T) -> Unit + ) { + if (nodes.none()) + return + renderItem(nodes.first()) + nodes.drop(1).forEach { + if (noWrap) { + symbol(separator.removeSuffix(" ")) + nbsp() + } else { + symbol(separator) + } + renderItem(it) + } + } + + abstract fun showModifierInSummary(node: DocumentationNode): Boolean + + protected fun ContentBlock.renderModifiersForNode( + node: DocumentationNode, + renderMode: LanguageService.RenderMode, + nowrap: Boolean = false + ) { + val modifiers = node.details(NodeKind.Modifier) + for (it in modifiers) { + if (node.kind == NodeKind.Interface && it.name == "abstract") + continue + if (renderMode == LanguageService.RenderMode.SUMMARY && !showModifierInSummary(it)) { + continue + } + renderModifier(this, it, renderMode, nowrap) + } + } + + +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Languages/JavaLanguageService.kt b/core/src/main/kotlin/Languages/JavaLanguageService.kt new file mode 100644 index 000000000..4b3979b54 --- /dev/null +++ b/core/src/main/kotlin/Languages/JavaLanguageService.kt @@ -0,0 +1,179 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.Formats.nameWithOuterClass +import org.jetbrains.dokka.LanguageService.RenderMode + +/** + * Implements [LanguageService] and provides rendering of symbols in Java language + */ +class JavaLanguageService : LanguageService { + override fun render(node: DocumentationNode, renderMode: RenderMode): ContentNode { + return ContentText(when (node.kind) { + NodeKind.Package -> renderPackage(node) + in NodeKind.classLike -> renderClass(node) + + NodeKind.TypeParameter -> renderTypeParameter(node) + NodeKind.Type, + NodeKind.UpperBound -> renderType(node) + + NodeKind.Constructor, + NodeKind.Function -> renderFunction(node) + NodeKind.Property -> renderProperty(node) + else -> "${node.kind}: ${node.name}" + }) + } + + override fun renderName(node: DocumentationNode): String { + return when (node.kind) { + NodeKind.Constructor -> node.owner!!.name + else -> node.name + } + } + + override fun renderNameWithOuterClass(node: DocumentationNode): String { + return when (node.kind) { + NodeKind.Constructor -> node.owner!!.nameWithOuterClass() + else -> node.nameWithOuterClass() + } + } + + 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 + } + } + + fun getArrayElementType(node: DocumentationNode): DocumentationNode? = when (node.qualifiedName()) { + "kotlin.Array" -> + node.details(NodeKind.Type).singleOrNull()?.let { et -> getArrayElementType(et) ?: et } ?: + DocumentationNode("Object", node.content, NodeKind.ExternalClass) + + "kotlin.IntArray", "kotlin.LongArray", "kotlin.ShortArray", "kotlin.ByteArray", + "kotlin.CharArray", "kotlin.DoubleArray", "kotlin.FloatArray", "kotlin.BooleanArray" -> + DocumentationNode(node.name.removeSuffix("Array").toLowerCase(), node.content, NodeKind.Type) + + else -> null + } + + fun getArrayDimension(node: DocumentationNode): Int = when (node.qualifiedName()) { + "kotlin.Array" -> + 1 + (node.details(NodeKind.Type).singleOrNull()?.let { getArrayDimension(it) } ?: 0) + + "kotlin.IntArray", "kotlin.LongArray", "kotlin.ShortArray", "kotlin.ByteArray", + "kotlin.CharArray", "kotlin.DoubleArray", "kotlin.FloatArray", "kotlin.BooleanArray" -> + 1 + else -> 0 + } + + 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(NodeKind.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(NodeKind.Type))} ${node.name}" + } + + private fun renderTypeParametersForNode(node: DocumentationNode): String { + return StringBuilder().apply { + val typeParameters = node.details(NodeKind.TypeParameter) + if (typeParameters.any()) { + append("<") + append(typeParameters.map { renderTypeParameter(it) }.joinToString()) + append("> ") + } + }.toString() + } + + private fun renderModifiersForNode(node: DocumentationNode): String { + val modifiers = node.details(NodeKind.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) { + NodeKind.Class -> append("class ") + NodeKind.Interface -> append("interface ") + NodeKind.Enum -> append("enum ") + NodeKind.EnumItem -> append("enum value ") + NodeKind.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) { + NodeKind.Constructor -> append(node.owner?.name) + NodeKind.Function -> { + append(renderTypeParametersForNode(node)) + append(renderType(node.detail(NodeKind.Type))) + append(" ") + append(node.name) + } + else -> throw IllegalArgumentException("Node $node is not a function-like object") + } + + val receiver = node.details(NodeKind.Receiver).singleOrNull() + append("(") + if (receiver != null) + (listOf(receiver) + node.details(NodeKind.Parameter)).map { renderParameter(it) }.joinTo(this) + else + node.details(NodeKind.Parameter).map { renderParameter(it) }.joinTo(this) + + append(")") + }.toString() + } + + private fun renderProperty(node: DocumentationNode): String { + return StringBuilder().apply { + when (node.kind) { + NodeKind.Property -> append("val ") + else -> throw IllegalArgumentException("Node $node is not a property") + } + append(renderTypeParametersForNode(node)) + val receiver = node.details(NodeKind.Receiver).singleOrNull() + if (receiver != null) { + append(renderType(receiver.detail(NodeKind.Type))) + append(".") + } + + append(node.name) + append(": ") + append(renderType(node.detail(NodeKind.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 000000000..e43081993 --- /dev/null +++ b/core/src/main/kotlin/Languages/LanguageService.kt @@ -0,0 +1,43 @@ +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 renderNameWithOuterClass(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/Languages/NewJavaLanguageService.kt b/core/src/main/kotlin/Languages/NewJavaLanguageService.kt new file mode 100644 index 000000000..c4b5fa090 --- /dev/null +++ b/core/src/main/kotlin/Languages/NewJavaLanguageService.kt @@ -0,0 +1,250 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.classNodeNameWithOuterClass +import org.jetbrains.dokka.LanguageService.RenderMode + +/** + * Implements [LanguageService] and provides rendering of symbols in Java language + */ +class NewJavaLanguageService : CommonLanguageService() { + override fun showModifierInSummary(node: DocumentationNode): Boolean { + return node.name !in fullOnlyModifiers + } + + private val fullOnlyModifiers = setOf("public", "protected", "private") + + override fun render(node: DocumentationNode, renderMode: RenderMode): ContentNode { + return content { + (when (node.kind) { + NodeKind.Package -> renderPackage(node) + in NodeKind.classLike -> renderClass(node, renderMode) + + NodeKind.Modifier -> renderModifier(this, node, renderMode) + NodeKind.TypeParameter -> renderTypeParameter(node) + NodeKind.Type, + NodeKind.UpperBound -> renderType(node) + NodeKind.Parameter -> renderParameter(node) + NodeKind.Constructor, + NodeKind.Function -> renderFunction(node, renderMode) + NodeKind.Property -> renderProperty(node) + NodeKind.Field -> renderField(node, renderMode) + NodeKind.EnumItem -> renderClass(node, renderMode) + else -> "${node.kind}: ${node.name}" + }) + } + } + + override fun summarizeSignatures(nodes: List<DocumentationNode>): ContentNode? = null + + + override fun renderModifier(block: ContentBlock, node: DocumentationNode, renderMode: RenderMode, nowrap: Boolean) { + when (node.name) { + "open", "internal" -> { + } + else -> { + if (node.name !in fullOnlyModifiers || renderMode == RenderMode.FULL) { + super.renderModifier(block, node, renderMode, nowrap) + } + } + } + } + + fun getArrayElementType(node: DocumentationNode): DocumentationNode? = when (node.qualifiedName()) { + "kotlin.Array" -> + node.details(NodeKind.Type).singleOrNull()?.let { et -> getArrayElementType(et) ?: et } + ?: DocumentationNode("Object", node.content, NodeKind.ExternalClass) + + "kotlin.IntArray", "kotlin.LongArray", "kotlin.ShortArray", "kotlin.ByteArray", + "kotlin.CharArray", "kotlin.DoubleArray", "kotlin.FloatArray", "kotlin.BooleanArray" -> + DocumentationNode(node.name.removeSuffix("Array").toLowerCase(), node.content, NodeKind.Type) + + else -> null + } + + fun getArrayDimension(node: DocumentationNode): Int = when (node.qualifiedName()) { + "kotlin.Array" -> + 1 + (node.details(NodeKind.Type).singleOrNull()?.let { getArrayDimension(it) } ?: 0) + + "kotlin.IntArray", "kotlin.LongArray", "kotlin.ShortArray", "kotlin.ByteArray", + "kotlin.CharArray", "kotlin.DoubleArray", "kotlin.FloatArray", "kotlin.BooleanArray" -> + 1 + else -> 0 + } + + fun ContentBlock.renderType(node: DocumentationNode) { + when (node.name) { + "Unit" -> identifier("void") + "Int" -> identifier("int") + "Long" -> identifier("long") + "Double" -> identifier("double") + "Float" -> identifier("float") + "Char" -> identifier("char") + "Boolean" -> identifier("bool") + // TODO: render arrays + else -> { + renderLinked(this, node) { + identifier( + it.typeDeclarationClass?.classNodeNameWithOuterClass() ?: it.name, + IdentifierKind.TypeName + ) + } + renderTypeArgumentsForType(node) + } + } + } + + private fun ContentBlock.renderTypeParameter(node: DocumentationNode) { + val constraints = node.details(NodeKind.UpperBound) + if (constraints.none()) + identifier(node.name) + else { + identifier(node.name) + text(" ") + keyword("extends") + text(" ") + constraints.forEach { renderType(node) } + } + } + + private fun ContentBlock.renderParameter(node: DocumentationNode) { + renderType(node.detail(NodeKind.Type)) + text(" ") + identifier(node.name) + } + + private fun ContentBlock.renderTypeParametersForNode(node: DocumentationNode) { + val typeParameters = node.details(NodeKind.TypeParameter) + if (typeParameters.any()) { + symbol("<") + renderList(typeParameters, noWrap = true) { + renderTypeParameter(it) + } + symbol(">") + text(" ") + } + } + + private fun ContentBlock.renderTypeArgumentsForType(node: DocumentationNode) { + val typeArguments = node.details(NodeKind.Type) + if (typeArguments.any()) { + symbol("<") + renderList(typeArguments, noWrap = true) { + renderType(it) + } + symbol(">") + } + } + +// private fun renderModifiersForNode(node: DocumentationNode): String { +// val modifiers = node.details(NodeKind.Modifier).map { renderModifier(it) }.filter { it != "" } +// if (modifiers.none()) +// return "" +// return modifiers.joinToString(" ", postfix = " ") +// } + + private fun ContentBlock.renderClassKind(node: DocumentationNode) { + when (node.kind) { + NodeKind.Interface -> { + keyword("interface") + } + NodeKind.EnumItem -> { + keyword("enum value") + } + NodeKind.Enum -> { + keyword("enum") + } + NodeKind.Class, NodeKind.Exception, NodeKind.Object -> { + keyword("class") + } + else -> throw IllegalArgumentException("Node $node is not a class-like object") + } + text(" ") + } + + private fun ContentBlock.renderClass(node: DocumentationNode, renderMode: RenderMode) { + renderModifiersForNode(node, renderMode) + renderClassKind(node) + + identifier(node.name) + renderTypeParametersForNode(node) + val superClassType = node.superclassType + val interfaces = node.supertypes - superClassType + if (superClassType != null) { + text(" ") + keyword("extends") + text(" ") + renderType(superClassType) + } + if (interfaces.isNotEmpty()) { + text(" ") + keyword("implements") + text(" ") + renderList(interfaces.filterNotNull()) { + renderType(it) + } + } + } + + private fun ContentBlock.renderParameters(nodes: List<DocumentationNode>) { + renderList(nodes) { + renderParameter(it) + } + } + + private fun ContentBlock.renderFunction( + node: DocumentationNode, + renderMode: RenderMode + ) { + renderModifiersForNode(node, renderMode) + when (node.kind) { + NodeKind.Constructor -> identifier(node.owner?.name ?: "") + NodeKind.Function -> { + renderTypeParametersForNode(node) + renderType(node.detail(NodeKind.Type)) + text(" ") + identifier(node.name) + + } + else -> throw IllegalArgumentException("Node $node is not a function-like object") + } + + val receiver = node.details(NodeKind.Receiver).singleOrNull() + symbol("(") + if (receiver != null) + renderParameters(listOf(receiver) + node.details(NodeKind.Parameter)) + else + renderParameters(node.details(NodeKind.Parameter)) + + symbol(")") + } + + private fun ContentBlock.renderProperty(node: DocumentationNode) { + + when (node.kind) { + NodeKind.Property -> { + keyword("val") + text(" ") + } + else -> throw IllegalArgumentException("Node $node is not a property") + } + renderTypeParametersForNode(node) + val receiver = node.details(NodeKind.Receiver).singleOrNull() + if (receiver != null) { + renderType(receiver.detail(NodeKind.Type)) + symbol(".") + } + + identifier(node.name) + symbol(":") + text(" ") + renderType(node.detail(NodeKind.Type)) + + } + + private fun ContentBlock.renderField(node: DocumentationNode, renderMode: RenderMode) { + renderModifiersForNode(node, renderMode) + renderType(node.detail(NodeKind.Type)) + text(" ") + identifier(node.name) + } +} diff --git a/core/src/main/kotlin/Locations/Location.kt b/core/src/main/kotlin/Locations/Location.kt new file mode 100644 index 000000000..4cb0ac39c --- /dev/null +++ b/core/src/main/kotlin/Locations/Location.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka + +import java.io.File + +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). + * + * $file: [File] for this location + * $path: [String] representing path of this location + */ +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 "./${file.name}" + } + val ownerFolder = file.parentFile!! + val relativePath = ownerFolder.toPath().relativize(other.file.toPath()).toString().replace(File.separatorChar, '/') + return if (anchor == null) relativePath else relativePath + "#" + anchor + } +} + +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" + } +} + + +fun relativePathToNode(node: DocumentationNode) = relativePathToNode(node.path.map { it.name }, node.members.any()) + +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 +} + +fun NodeLocationAwareGenerator.relativePathToLocation(owner: DocumentationNode, node: DocumentationNode): String { + return location(owner).relativePathTo(location(node), null) +} + +fun NodeLocationAwareGenerator.relativePathToRoot(from: Location): File { + val file = File(from.path).parentFile + return root.relativeTo(file) +} + +fun File.toUnixString() = toString().replace(File.separatorChar, '/') diff --git a/core/src/main/kotlin/Markdown/MarkdownProcessor.kt b/core/src/main/kotlin/Markdown/MarkdownProcessor.kt new file mode 100644 index 000000000..2c8f7a739 --- /dev/null +++ b/core/src/main/kotlin/Markdown/MarkdownProcessor.kt @@ -0,0 +1,53 @@ +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.ast.getTextInNode +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() = node.getTextInNode(markdown).toString() + fun child(type: IElementType): MarkdownNode? = children.firstOrNull { it.type == type } + + val previous get() = parent?.children?.getOrNull(parent.children.indexOf(this) - 1) + + 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) + } + } +} + +fun MarkdownNode.toTestString(): String { + val sb = StringBuilder() + var level = 0 + visit { node, visitChildren -> + sb.append(" ".repeat(level * 2)) + node.presentTo(sb) + sb.appendln() + level++ + visitChildren() + level-- + } + return sb.toString() +} + +private fun MarkdownNode.presentTo(sb: StringBuilder) { + sb.append(type.toString()) + sb.append(":" + text.replace("\n", "\u23CE")) +} + +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/CodeNode.kt b/core/src/main/kotlin/Model/CodeNode.kt new file mode 100644 index 000000000..17b506276 --- /dev/null +++ b/core/src/main/kotlin/Model/CodeNode.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.Model + +import org.jsoup.helper.StringUtil.isWhitespace +import org.jsoup.nodes.TextNode + +class CodeNode(text: String, baseUri: String): TextNode(text, baseUri) { + + override fun text(): String { + return normaliseInitialWhitespace(wholeText.removePrefix("<code>") + .removeSuffix("</code>")) + } + + private fun normaliseInitialWhitespace(text: String): String { + val sb = StringBuilder(text.length) + removeInitialWhitespace(sb, text) + return sb.toString() + } + + /** + * Remove initial whitespace. + * @param accum builder to append to + * @param string string to remove the initial whitespace + */ + private fun removeInitialWhitespace(accum: StringBuilder, string: String) { + var reachedNonWhite = false + + val len = string.length + var c: Int + var i = 0 + while (i < len) { + c = string.codePointAt(i) + if (isWhitespace(c) && !reachedNonWhite) { + i += Character.charCount(c) + continue + } else { + accum.appendCodePoint(c) + reachedNonWhite = true + } + i += Character.charCount(c) + } + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Model/Content.kt b/core/src/main/kotlin/Model/Content.kt new file mode 100644 index 000000000..a4c78fabf --- /dev/null +++ b/core/src/main/kotlin/Model/Content.kt @@ -0,0 +1,296 @@ +package org.jetbrains.dokka + +interface ContentNode { + val textLength: Int +} + +object ContentEmpty : ContentNode { + override val textLength: Int get() = 0 +} + +open class ContentBlock() : ContentNode { + open 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 } +} + +class NodeRenderContent( + val node: DocumentationNode, + val mode: LanguageService.RenderMode +): ContentNode { + override val textLength: Int + get() = 0 //TODO: Clarify? +} + +class LazyContentBlock(private val fillChildren: (ContentBlock) -> Unit) : ContentBlock() { + private var computed = false + override val children: ArrayList<ContentNode> + get() { + if (!computed) { + computed = true + fillChildren(this) + } + return super.children + } + + override fun equals(other: Any?): Boolean { + return other is LazyContentBlock && other.fillChildren == fillChildren && super.equals(other) + } + + override fun hashCode(): Int { + return super.hashCode() + 31 * fillChildren.hashCode() + } +} + +enum class IdentifierKind { + TypeName, + ParameterName, + AnnotationName, + SummarizedTypeName, + Other +} + +data class ContentText(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +data class ContentKeyword(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +data class ContentIdentifier(val text: String, + val kind: IdentifierKind = IdentifierKind.Other, + val signature: String? = null) : ContentNode { + override val textLength: Int + get() = text.length +} + +data class ContentSymbol(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +data class ContentEntity(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +object ContentNonBreakingSpace: ContentNode { + override val textLength: Int + get() = 1 +} + +object ContentSoftLineBreak: ContentNode { + override val textLength: Int + get() = 0 +} + +object ContentIndentedSoftLineBreak: ContentNode { + override val textLength: Int + get() = 0 +} +class ScriptBlock(val type: String?, val src: String) : ContentBlock() + +class ContentParagraph(val label: String? = null) : ContentBlock() +class ContentEmphasis() : ContentBlock() +class ContentStrong() : ContentBlock() +class ContentStrikethrough() : ContentBlock() +class ContentCode() : ContentBlock() + +class ContentDescriptionList() : ContentBlock() +class ContentDescriptionTerm() : ContentBlock() +class ContentDescriptionDefinition() : ContentBlock() + +class ContentTable() : ContentBlock() +class ContentTableBody() : ContentBlock() +class ContentTableRow() : ContentBlock() +class ContentTableHeader(val colspan: String? = null, val rowspan: String? = null) : ContentBlock() +class ContentTableCell(val colspan: String? = null, val rowspan: String? = null) : ContentBlock() + +class ContentSpecialReference() : ContentBlock() + +open class ContentBlockCode(val language: String = "") : ContentBlock() +class ContentBlockSampleCode(language: String = "kotlin", val importsBlock: ContentBlockCode = ContentBlockCode(language)) : ContentBlockCode(language) + +abstract class ContentNodeLink() : ContentBlock() { + abstract val node: DocumentationNode? +} + +object ContentHardLineBreak : ContentNode { + override val textLength: Int + get() = 0 +} + +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() +} + +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() +} + +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() +} + +data class ContentBookmark(val name: String): ContentBlock() +data class ContentLocalLink(val href: String) : ContentBlock() + +class ContentUnorderedList() : ContentBlock() +class ContentOrderedList() : ContentBlock() +class ContentListItem() : ContentBlock() + +class ContentHeading(val level: Int) : ContentBlock() + +class ContentSection(val tag: String, 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) +} + +object ContentTags { + val Description = "Description" + val SeeAlso = "See Also" + val Return = "Return" + val Exceptions = "Exceptions" + val Parameters = "Parameters" +} + +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, signature: String? = null) { + append(ContentIdentifier(value, kind, signature)) +} + +fun ContentBlock.nbsp() = append(ContentNonBreakingSpace) +fun ContentBlock.softLineBreak() = append(ContentSoftLineBreak) +fun ContentBlock.indentedSoftLineBreak() = append(ContentIndentedSoftLineBreak) +fun ContentBlock.hardLineBreak() = append(ContentHardLineBreak) + +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 = if (to.kind == NodeKind.ExternalLink) + ContentExternalLink(to.name) + else + ContentNodeDirectLink(to) + + block.body() + append(block) +} + +open class Content(): ContentBlock() { + open val sections: List<ContentSection> get() = emptyList() + open val summary: ContentNode get() = ContentEmpty + 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 + } + } +} + +open class MutableContent() : Content() { + private val sectionList = arrayListOf<ContentSection>() + override val sections: List<ContentSection> + get() = sectionList + + fun addSection(tag: String?, subjectName: String?): ContentSection { + val section = ContentSection(tag ?: "", subjectName) + sectionList.add(section) + return section + } + + override val summary: ContentNode get() = children.firstOrNull() ?: ContentEmpty + + 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" -> ContentTags.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 000000000..7ac6d8f4a --- /dev/null +++ b/core/src/main/kotlin/Model/DocumentationNode.kt @@ -0,0 +1,311 @@ +package org.jetbrains.dokka + +import java.util.* + +enum class NodeKind { + Unknown, + + Package, + Class, + Interface, + Enum, + AnnotationClass, + Exception, + EnumItem, + Object, + TypeAlias, + + Constructor, + Function, + Property, + Field, + + CompanionObjectProperty, + CompanionObjectFunction, + + Parameter, + Receiver, + TypeParameter, + Type, + Supertype, + UpperBound, + LowerBound, + + TypeAliasUnderlyingType, + + Modifier, + NullabilityModifier, + + Module, + + ExternalClass, + Annotation, + + Value, + + SourceUrl, + SourcePosition, + Signature, + + ExternalLink, + QualifiedName, + Platform, + + AllTypes, + + /** + * A note which is rendered once on a page documenting a group of overloaded functions. + * Needs to be generated equally on all overloads. + */ + OverloadGroupNote, + + Attribute, + + AttributeRef, + + ApiLevel, + SdkExtSince, + + DeprecatedLevel, + + ArtifactId, + + GroupNode; + + companion object { + val classLike = setOf(Class, Interface, Enum, AnnotationClass, Exception, Object, TypeAlias) + val memberLike = setOf(Function, Property, Field, Constructor, CompanionObjectFunction, CompanionObjectProperty, EnumItem, Attribute) + } +} + +open class DocumentationNode(val name: String, + content: Content, + val kind: NodeKind) { + + private val references = LinkedHashSet<DocumentationReference>() + + var content: Content = content + private set + + val summary: ContentNode get() = content.summary + + val owner: DocumentationNode? + get() = references(RefKind.Owner).singleOrNull()?.to + val details: List<DocumentationNode> + get() = references(RefKind.Detail).map { it.to } + val members: List<DocumentationNode> + get() = references(RefKind.Member).map { it.to }.sortedBy { it.name } + val inheritedMembers: List<DocumentationNode> + get() = references(RefKind.InheritedMember).map { it.to } + val allInheritedMembers: List<DocumentationNode> + get() = recursiveInheritedMembers().sortedBy { it.name } + val inheritedCompanionObjectMembers: List<DocumentationNode> + get() = references(RefKind.InheritedCompanionObjectMember).map { it.to } + val extensions: List<DocumentationNode> + get() = references(RefKind.Extension).map { it.to } + val inheritors: List<DocumentationNode> + get() = references(RefKind.Inheritor).map { it.to } + val overrides: List<DocumentationNode> + get() = references(RefKind.Override).map { it.to } + val links: List<DocumentationNode> + get() = references(RefKind.Link).map { it.to } + val hiddenLinks: List<DocumentationNode> + get() = references(RefKind.HiddenLink).map { it.to } + val annotations: List<DocumentationNode> + get() = references(RefKind.Annotation).map { it.to } + val deprecation: DocumentationNode? + get() = references(RefKind.Deprecation).map { it.to }.firstOrNull() + val platforms: List<String> + get() = references(RefKind.Platform).map { it.to.name } + val externalType: DocumentationNode? + get() = references(RefKind.ExternalType).map { it.to }.firstOrNull() + val apiLevel: DocumentationNode + get() = detailOrNull(NodeKind.ApiLevel) ?: DocumentationNode("", Content.Empty, NodeKind.ApiLevel) + val sdkExtSince: DocumentationNode + get() = detailOrNull(NodeKind.SdkExtSince) ?: DocumentationNode("", Content.Empty, NodeKind.SdkExtSince) + val deprecatedLevel: DocumentationNode + get() = detailOrNull(NodeKind.DeprecatedLevel) ?: DocumentationNode("", Content.Empty, NodeKind.DeprecatedLevel) + val artifactId: DocumentationNode + get() = detailOrNull(NodeKind.ArtifactId) ?: DocumentationNode("", Content.Empty, NodeKind.ArtifactId) + val attributes: List<DocumentationNode> + get() = details(NodeKind.Attribute).sortedBy { it.attributeRef!!.name } + val attributeRef: DocumentationNode? + get() = references(RefKind.AttributeRef).map { it.to }.firstOrNull() + val relatedAttributes: List<DocumentationNode> + get() = hiddenLinks(NodeKind.Attribute) + val supertypes: List<DocumentationNode> + get() = details(NodeKind.Supertype) + val signatureName = detailOrNull(NodeKind.Signature)?.name + + val prettyName : String + get() = when(kind) { + NodeKind.Constructor -> owner!!.name + else -> name + } + + val superclassType: DocumentationNode? + get() = when (kind) { + NodeKind.Supertype -> { + (links + listOfNotNull(externalType)).firstOrNull { it.kind in NodeKind.classLike }?.superclassType + } + NodeKind.Interface -> null + in NodeKind.classLike -> supertypes.firstOrNull { + (it.links + listOfNotNull(it.externalType)).any { it.isSuperclassFor(this) } + } + else -> null + } + + val superclassTypeSequence: Sequence<DocumentationNode> + get() = generateSequence(superclassType) { + it.superclassType + } + + // TODO: Should we allow node mutation? Model merge will copy by ref, so references are transparent, which could nice + fun addReferenceTo(to: DocumentationNode, kind: RefKind) { + references.add(DocumentationReference(this, to, kind)) + } + + fun dropReferences(predicate: (DocumentationReference) -> Boolean) { + references.removeAll(predicate) + } + + fun addAllReferencesFrom(other: DocumentationNode) { + references.addAll(other.references) + } + + fun updateContent(body: MutableContent.() -> Unit) { + if (content !is MutableContent) { + content = MutableContent() + } + (content as MutableContent).body() + } + fun details(kind: NodeKind): List<DocumentationNode> = details.filter { it.kind == kind } + fun members(kind: NodeKind): List<DocumentationNode> = members.filter { it.kind == kind } + fun hiddenLinks(kind: NodeKind): List<DocumentationNode> = hiddenLinks.filter { it.kind == kind } + fun inheritedMembers(kind: NodeKind): List<DocumentationNode> = inheritedMembers.filter { it.kind == kind } + fun inheritedCompanionObjectMembers(kind: NodeKind): List<DocumentationNode> = inheritedCompanionObjectMembers.filter { it.kind == kind } + fun links(kind: NodeKind): List<DocumentationNode> = links.filter { it.kind == kind } + + fun detail(kind: NodeKind): DocumentationNode = details.filter { it.kind == kind }.single() + fun detailOrNull(kind: NodeKind): DocumentationNode? = details.filter { it.kind == kind }.singleOrNull() + fun member(kind: NodeKind): DocumentationNode = members.filter { it.kind == kind }.single() + fun link(kind: NodeKind): DocumentationNode = links.filter { it.kind == kind }.single() + + fun references(kind: RefKind): List<DocumentationReference> = references.filter { it.kind == kind } + fun allReferences(): Set<DocumentationReference> = references + + override fun toString(): String { + return "$kind:$name" + } +} + +class DocumentationModule(name: String, content: Content = Content.Empty) + : DocumentationNode(name, content, NodeKind.Module) { + val nodeRefGraph = NodeReferenceGraph() +} + +val DocumentationNode.path: List<DocumentationNode> + get() { + val parent = owner ?: return listOf(this) + return parent.path + this + } + +fun findOrCreatePackageNode(module: DocumentationNode?, packageName: String, packageContent: Map<String, Content>, refGraph: NodeReferenceGraph): DocumentationNode { + val existingNode = refGraph.lookup(packageName) + if (existingNode != null) { + return existingNode + } + val newNode = DocumentationNode(packageName, + packageContent.getOrElse(packageName) { Content.Empty }, + NodeKind.Package) + + refGraph.register(packageName, newNode) + module?.append(newNode, RefKind.Member) + return newNode +} + +fun DocumentationNode.append(child: DocumentationNode, kind: RefKind) { + addReferenceTo(child, kind) + when (kind) { + RefKind.Detail -> child.addReferenceTo(this, RefKind.Owner) + RefKind.Member -> child.addReferenceTo(this, RefKind.Owner) + RefKind.Owner -> child.addReferenceTo(this, RefKind.Member) + else -> { /* Do not add any links back for other types */ + } + } +} + +fun DocumentationNode.appendTextNode(text: String, + kind: NodeKind, + refKind: RefKind = RefKind.Detail) { + append(DocumentationNode(text, Content.Empty, kind), refKind) +} + +fun DocumentationNode.qualifiedName(): String { + if (kind == NodeKind.Type) { + return qualifiedNameFromType() + } else if (kind == NodeKind.Package) { + return name + } + return path.drop(1).map { it.name }.filter { it.length > 0 }.joinToString(".") +} + +fun DocumentationNode.simpleName() = name.substringAfterLast('.') + +private fun DocumentationNode.recursiveInheritedMembers(): List<DocumentationNode> { + val allInheritedMembers = mutableListOf<DocumentationNode>() + recursiveInheritedMembers(allInheritedMembers) + return allInheritedMembers +} + +private fun DocumentationNode.recursiveInheritedMembers(allInheritedMembers: MutableList<DocumentationNode>) { + allInheritedMembers.addAll(inheritedMembers) + inheritedMembers.groupBy { it.owner!! } .forEach { (node, _) -> + node.recursiveInheritedMembers(allInheritedMembers) + } +} + +private fun DocumentationNode.isSuperclassFor(node: DocumentationNode): Boolean { + return when(node.kind) { + NodeKind.Object, NodeKind.Class, NodeKind.Enum -> kind == NodeKind.Class + NodeKind.Exception -> kind == NodeKind.Class || kind == NodeKind.Exception + else -> false + } +} + +fun DocumentationNode.classNodeNameWithOuterClass(): String { + assert(kind in NodeKind.classLike) + return path.dropWhile { it.kind !in NodeKind.classLike }.joinToString(separator = ".") { it.name } +} + +fun DocumentationNode.deprecatedLevelMessage(): String { + val kindName = when(kind) { + NodeKind.Enum -> "enum" + NodeKind.Interface -> "interface" + NodeKind.AnnotationClass -> "annotation" + NodeKind.Exception -> "exception" + else -> "class" + } + return "This $kindName was deprecated in API level ${deprecatedLevel.name}." +} + +fun DocumentationNode.convertDeprecationDetailsToChildren() { + val toProcess = details.toMutableList() + while (!toProcess.isEmpty()) { + var child = toProcess.removeAt(0) + if (child.details.isEmpty() && child.name != "") { + updateContent { text(child.name.cleanForAppending()) } + } + else toProcess.addAll(0, child.details) + } +} + + /* + * Removes extraneous quotation marks and adds a ". " to make appending children readable. + */ +fun String.cleanForAppending(): String { + var result = this + if (this.first() == this.last() && this.first() == '"') result = result.substring(1, result.length - 1) + if (result[result.length - 2] != '.' && result.last() != ' ') result += ". " + return result +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Model/DocumentationReference.kt b/core/src/main/kotlin/Model/DocumentationReference.kt new file mode 100644 index 000000000..cc582a0fd --- /dev/null +++ b/core/src/main/kotlin/Model/DocumentationReference.kt @@ -0,0 +1,86 @@ +package org.jetbrains.dokka + +import com.google.inject.Singleton + +enum class RefKind { + Owner, + Member, + InheritedMember, + InheritedCompanionObjectMember, + Detail, + Link, + HiddenLink, + Extension, + Inheritor, + Superclass, + Override, + Annotation, + HiddenAnnotation, + Deprecation, + TopLevelPage, + Platform, + ExternalType, + AttributeRef, + AttributeSource +} + +data class DocumentationReference(val from: DocumentationNode, val to: DocumentationNode, val kind: RefKind) { +} + +class PendingDocumentationReference(val lazyNodeFrom: () -> DocumentationNode?, + val lazyNodeTo: () -> DocumentationNode?, + val kind: RefKind) { + fun resolve() { + val fromNode = lazyNodeFrom() + val toNode = lazyNodeTo() + if (fromNode != null && toNode != null) { + fromNode.addReferenceTo(toNode, kind) + } + } +} + +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: RefKind) { + references.add(PendingDocumentationReference({ -> fromNode}, { -> nodeMap[toSignature]}, kind)) + } + + fun link(fromSignature: String, toNode: DocumentationNode, kind: RefKind) { + references.add(PendingDocumentationReference({ -> nodeMap[fromSignature]}, { -> toNode}, kind)) + } + + fun link(fromSignature: String, toSignature: String, kind: RefKind) { + references.add(PendingDocumentationReference({ -> nodeMap[fromSignature]}, { -> nodeMap[toSignature]}, kind)) + } + + fun lookup(signature: String) = nodeMap[signature] + + fun lookupOrWarn(signature: String, logger: DokkaLogger): DocumentationNode? { + val result = nodeMap[signature] + if (result == null) { + logger.warn("Can't find node by signature `$signature`") + } + return result + } + + fun resolveReferences() { + references.forEach { it.resolve() } + } +} + +@Singleton +class PlatformNodeRegistry { + private val platformNodes = hashMapOf<String, DocumentationNode>() + + operator fun get(platform: String): DocumentationNode { + return platformNodes.getOrPut(platform) { + DocumentationNode(platform, Content.Empty, NodeKind.Platform) + } + } +} diff --git a/core/src/main/kotlin/Model/ElementSignatureProvider.kt b/core/src/main/kotlin/Model/ElementSignatureProvider.kt new file mode 100644 index 000000000..e8fdde6e3 --- /dev/null +++ b/core/src/main/kotlin/Model/ElementSignatureProvider.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor + +interface ElementSignatureProvider { + fun signature(forDesc: DeclarationDescriptor): String + fun signature(forPsi: PsiElement): String +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Model/PackageDocs.kt b/core/src/main/kotlin/Model/PackageDocs.kt new file mode 100644 index 000000000..5b6289146 --- /dev/null +++ b/core/src/main/kotlin/Model/PackageDocs.kt @@ -0,0 +1,135 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.Singleton +import com.intellij.ide.highlighter.JavaFileType +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.LocalTimeCounter +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.parser.LinkMap +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor +import java.io.File + +@Singleton +class PackageDocs + @Inject constructor(val linkResolver: DeclarationLinkResolver?, + val logger: DokkaLogger, + val environment: KotlinCoreEnvironment, + val refGraph: NodeReferenceGraph, + val elementSignatureProvider: ElementSignatureProvider) +{ + val moduleContent: MutableContent = MutableContent() + private val _packageContent: MutableMap<String, MutableContent> = hashMapOf() + val packageContent: Map<String, Content> + get() = _packageContent + + fun parse(fileName: String, linkResolveContext: List<PackageFragmentDescriptor>) { + val file = File(fileName) + if (file.exists()) { + val text = file.readText() + val tree = parseMarkdown(text) + val linkMap = LinkMap.buildLinkMap(tree.node, 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, LinkResolver(linkMap, { resolveContentLink(fileName, it, linkResolveContext) })) + } + } + } else { + logger.warn("Include file $file was not found.") + } + } + + private fun parseHtmlAsJavadoc(text: String, packageName: String, file: File) { + val javadocText = text + .replace("*/", "*/") + .removeSurrounding("<html>", "</html>", true).trim() + .removeSurrounding("<body>", "</body>", true) + .lineSequence() + .map { "* $it" } + .joinToString (separator = "\n", prefix = "/**\n", postfix = "\n*/") + parseJavadoc(javadocText, packageName, file) + } + + private fun CharSequence.removeSurrounding(prefix: CharSequence, suffix: CharSequence, ignoringCase: Boolean = false): CharSequence { + if ((length >= prefix.length + suffix.length) && startsWith(prefix, ignoringCase) && endsWith(suffix, ignoringCase)) { + return subSequence(prefix.length, length - suffix.length) + } + return subSequence(0, length) + } + + + private fun parseJavadoc(text: String, packageName: String, file: File) { + + val psiFileFactory = PsiFileFactory.getInstance(environment.project) + val psiFile = psiFileFactory.createFileFromText( + file.nameWithoutExtension + ".java", + JavaFileType.INSTANCE, + "package $packageName; $text\npublic class C {}", + LocalTimeCounter.currentTime(), + false, + true + ) + + val psiClass = PsiTreeUtil.getChildOfType(psiFile, PsiClass::class.java)!! + val parser = JavadocParser(refGraph, logger, elementSignatureProvider, linkResolver?.externalDocumentationLinkResolver!!) + findOrCreatePackageContent(packageName).apply { + val content = parser.parseDocumentation(psiClass).content + children.addAll(content.children) + content.sections.forEach { + addSection(it.tag, it.subjectName).children.addAll(it.children) + } + } + } + + + fun parseJava(fileName: String, packageName: String) { + val file = File(fileName) + if (file.exists()) { + val text = file.readText() + + val trimmedText = text.trim() + + if (trimmedText.startsWith("/**")) { + parseJavadoc(text, packageName, file) + } else if (trimmedText.toLowerCase().startsWith("<html>")) { + parseHtmlAsJavadoc(trimmedText, packageName, file) + } + } + } + + 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(fileName: String, href: String, linkResolveContext: List<PackageFragmentDescriptor>): ContentBlock { + if (linkResolver != null) { + linkResolveContext + .asSequence() + .map { p -> linkResolver.tryResolveContentLink(p, href) } + .filterNotNull() + .firstOrNull() + ?.let { return it } + } + logger.warn("Unresolved link to `$href` in include ($fileName)") + 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 000000000..2c75cfdac --- /dev/null +++ b/core/src/main/kotlin/Model/SourceLinks.kt @@ -0,0 +1,56 @@ +package org.jetbrains.dokka + +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNameIdentifierOwner +import org.jetbrains.dokka.DokkaConfiguration.SourceLinkDefinition +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import java.io.File + + +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, NodeKind.SourceUrl), + RefKind.Detail); + } + + if (target != null) { + append(DocumentationNode(target.sourcePosition(), Content.Empty, NodeKind.SourcePosition), RefKind.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/Samples/DefaultSampleProcessingService.kt b/core/src/main/kotlin/Samples/DefaultSampleProcessingService.kt new file mode 100644 index 000000000..116a5c02f --- /dev/null +++ b/core/src/main/kotlin/Samples/DefaultSampleProcessingService.kt @@ -0,0 +1,105 @@ +package org.jetbrains.dokka.Samples + +import com.google.inject.Inject +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.* +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.PackageViewDescriptor +import org.jetbrains.kotlin.idea.kdoc.getKDocLinkResolutionScope +import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.ResolutionScope + + +open class DefaultSampleProcessingService +@Inject constructor(val options: DocumentationOptions, + val logger: DokkaLogger, + val resolutionFacade: DokkaResolutionFacade) + : SampleProcessingService { + + override fun resolveSample(descriptor: DeclarationDescriptor, functionName: String?, kdocTag: KDocTag): ContentNode { + if (functionName == null) { + logger.warn("Missing function name in @sample in ${descriptor.signature()}") + return ContentBlockSampleCode().apply { append(ContentText("//Missing function name in @sample")) } + } + val bindingContext = BindingContext.EMPTY + val symbol = resolveKDocLink(bindingContext, resolutionFacade, descriptor, kdocTag, functionName.split(".")).firstOrNull() + if (symbol == null) { + logger.warn("Unresolved function $functionName in @sample in ${descriptor.signature()}") + return ContentBlockSampleCode().apply { append(ContentText("//Unresolved: $functionName")) } + } + val psiElement = DescriptorToSourceUtils.descriptorToDeclaration(symbol) + if (psiElement == null) { + logger.warn("Can't find source for function $functionName in @sample in ${descriptor.signature()}") + return ContentBlockSampleCode().apply { append(ContentText("//Source not found: $functionName")) } + } + + val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() + val lines = text.split("\n") + val indent = lines.filter(String::isNotBlank).map { it.takeWhile(Char::isWhitespace).count() }.min() ?: 0 + val finalText = lines.map { it.drop(indent) }.joinToString("\n") + + return ContentBlockSampleCode(importsBlock = processImports(psiElement)).apply { append(ContentText(finalText)) } + } + + protected open fun processSampleBody(psiElement: PsiElement): String = when (psiElement) { + is KtDeclarationWithBody -> { + val bodyExpression = psiElement.bodyExpression + when (bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> psiElement.text + } + + protected open fun processImports(psiElement: PsiElement): ContentBlockCode { + val psiFile = psiElement.containingFile + if (psiFile is KtFile) { + return ContentBlockCode("kotlin").apply { + append(ContentText(psiFile.importList?.text ?: "")) + } + } else { + return ContentBlockCode("") + } + } + + 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.identifier(part) + val partSymbol = currentScope.getContributedDescriptors(DescriptorKindFilter.ALL, { it == symbolName }) + .filter { it.name == symbolName } + .firstOrNull() + + if (partSymbol == null) { + symbol = null + break + } + @Suppress("IfThenToElvis") + currentScope = if (partSymbol is ClassDescriptor) + partSymbol.defaultType.memberScope + else if (partSymbol is PackageViewDescriptor) + partSymbol.memberScope + else + getKDocLinkResolutionScope(resolutionFacade, partSymbol) + symbol = partSymbol + } + + return symbol + } +} + diff --git a/core/src/main/kotlin/Samples/DevsiteSampleProcessingService.kt b/core/src/main/kotlin/Samples/DevsiteSampleProcessingService.kt new file mode 100644 index 000000000..33d6cfeba --- /dev/null +++ b/core/src/main/kotlin/Samples/DevsiteSampleProcessingService.kt @@ -0,0 +1,52 @@ +package org.jetbrains.dokka.Samples + +import com.google.inject.Inject +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.* +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.utils.addIfNotNull + +open class DevsiteSampleProcessingService +@Inject constructor( + options: DocumentationOptions, + logger: DokkaLogger, + resolutionFacade: DokkaResolutionFacade +) : DefaultSampleProcessingService(options, logger, resolutionFacade) { + + override fun processImports(psiElement: PsiElement): ContentBlockCode { + // List of expression calls inside this sample, so we can trim the imports to only show relevant expressions + val sampleExpressionCalls = mutableSetOf<String>() + val psiFile = psiElement.containingFile + (psiElement as KtDeclarationWithBody).bodyExpression!!.accept(object : KtTreeVisitorVoid() { + override fun visitCallExpression(expression: KtCallExpression) { + sampleExpressionCalls.addIfNotNull(expression.calleeExpression?.text) + super.visitCallExpression(expression) + } + }) + val androidxPackage = Name.identifier("androidx") + if (psiFile is KtFile) { + val filteredImports = psiFile.importList?.imports?.filter { element -> + val fqImportName = element.importPath?.fqName ?: return@filter false + + val shortName = fqImportName.shortName().identifier + // Hide all non-androidx imports + if (!fqImportName.startsWith(androidxPackage)) return@filter false + + sampleExpressionCalls.any { call -> + call == shortName + } + } + + return ContentBlockCode("kotlin").apply { + filteredImports?.forEach { import -> + if (import != filteredImports.first()) { + append(ContentText("\n")) + } + append(ContentText(import.text)) + } + } + } + return super.processImports(psiElement) + } +} diff --git a/core/src/main/kotlin/Samples/KotlinWebsiteSampleProcessingService.kt b/core/src/main/kotlin/Samples/KotlinWebsiteSampleProcessingService.kt new file mode 100644 index 000000000..b0988c352 --- /dev/null +++ b/core/src/main/kotlin/Samples/KotlinWebsiteSampleProcessingService.kt @@ -0,0 +1,137 @@ +package org.jetbrains.dokka.Samples + +import com.google.inject.Inject +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.impl.source.tree.LeafPsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.* +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.allChildren +import org.jetbrains.kotlin.psi.psiUtil.prevLeaf +import org.jetbrains.kotlin.resolve.ImportPath + +open class KotlinWebsiteSampleProcessingService +@Inject constructor(options: DocumentationOptions, + logger: DokkaLogger, + resolutionFacade: DokkaResolutionFacade) + : DefaultSampleProcessingService(options, logger, resolutionFacade) { + + private class SampleBuilder : KtTreeVisitorVoid() { + val builder = StringBuilder() + val text: String + get() = builder.toString() + + fun KtValueArgument.extractStringArgumentValue() = + (getArgumentExpression() as KtStringTemplateExpression) + .entries.joinToString("") { it.text } + + + fun convertAssertPrints(expression: KtCallExpression) { + val (argument, commentArgument) = expression.valueArguments + builder.apply { + append("println(") + append(argument.text) + append(") // ") + append(commentArgument.extractStringArgumentValue()) + } + } + + fun convertAssertTrueFalse(expression: KtCallExpression, expectedResult: Boolean) { + val (argument) = expression.valueArguments + builder.apply { + expression.valueArguments.getOrNull(1)?.let { + append("// ${it.extractStringArgumentValue()}") + val ws = expression.prevLeaf { it is PsiWhiteSpace } + append(ws?.text ?: "\n") + } + append("println(\"") + append(argument.text) + append(" is \${") + append(argument.text) + append("}\") // $expectedResult") + } + } + + fun convertAssertFails(expression: KtCallExpression) { + val (message, funcArgument) = expression.valueArguments + builder.apply { + val argument = if (funcArgument.getArgumentExpression() is KtLambdaExpression) + PsiTreeUtil.findChildOfType(funcArgument, KtBlockExpression::class.java)?.text ?: "" + else + funcArgument.text + append(argument.lines().joinToString(separator = "\n") { "// $it" }) + append(" // ") + append(message.extractStringArgumentValue()) + append(" will fail") + } + } + + fun convertAssertFailsWith(expression: KtCallExpression) { + val (funcArgument) = expression.valueArguments + val (exceptionType) = expression.typeArguments + builder.apply { + val argument = if (funcArgument.firstChild is KtLambdaExpression) + PsiTreeUtil.findChildOfType(funcArgument, KtBlockExpression::class.java)?.text ?: "" + else + funcArgument.text + append(argument.lines().joinToString(separator = "\n") { "// $it" }) + append(" // will fail with ") + append(exceptionType.text) + } + } + + override fun visitCallExpression(expression: KtCallExpression) { + when (expression.calleeExpression?.text) { + "assertPrints" -> convertAssertPrints(expression) + "assertTrue" -> convertAssertTrueFalse(expression, expectedResult = true) + "assertFalse" -> convertAssertTrueFalse(expression, expectedResult = false) + "assertFails" -> convertAssertFails(expression) + "assertFailsWith" -> convertAssertFailsWith(expression) + else -> super.visitCallExpression(expression) + } + } + + override fun visitElement(element: PsiElement) { + if (element is LeafPsiElement) + builder.append(element.text) + super.visitElement(element) + } + } + + private fun PsiElement.buildSampleText(): String { + val sampleBuilder = SampleBuilder() + this.accept(sampleBuilder) + return sampleBuilder.text + } + + val importsToIgnore = arrayOf("samples.*").map { ImportPath.fromString(it) } + + override fun processImports(psiElement: PsiElement): ContentBlockCode { + val psiFile = psiElement.containingFile + if (psiFile is KtFile) { + return ContentBlockCode("kotlin").apply { + append(ContentText("\n")) + psiFile.importList?.let { + it.allChildren.filter { + it !is KtImportDirective || it.importPath !in importsToIgnore + }.forEach { append(ContentText(it.text)) } + } + } + } + return super.processImports(psiElement) + } + + override fun processSampleBody(psiElement: PsiElement) = when (psiElement) { + is KtDeclarationWithBody -> { + val bodyExpression = psiElement.bodyExpression + val bodyExpressionText = bodyExpression!!.buildSampleText() + when (bodyExpression) { + is KtBlockExpression -> bodyExpressionText.removeSurrounding("{", "}") + else -> bodyExpressionText + } + } + else -> psiElement.buildSampleText() + } +} + diff --git a/core/src/main/kotlin/Samples/SampleProcessingService.kt b/core/src/main/kotlin/Samples/SampleProcessingService.kt new file mode 100644 index 000000000..86c917cf5 --- /dev/null +++ b/core/src/main/kotlin/Samples/SampleProcessingService.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.Samples + +import org.jetbrains.dokka.ContentNode +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag + +interface SampleProcessingService { + fun resolveSample(descriptor: DeclarationDescriptor, functionName: String?, kdocTag: KDocTag): ContentNode +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Utilities/DokkaLogging.kt b/core/src/main/kotlin/Utilities/DokkaLogging.kt new file mode 100644 index 000000000..1ef528378 --- /dev/null +++ b/core/src/main/kotlin/Utilities/DokkaLogging.kt @@ -0,0 +1,27 @@ +package org.jetbrains.dokka + +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") + } + } +} diff --git a/core/src/main/kotlin/Utilities/DokkaModules.kt b/core/src/main/kotlin/Utilities/DokkaModules.kt new file mode 100644 index 000000000..7c8e5c347 --- /dev/null +++ b/core/src/main/kotlin/Utilities/DokkaModules.kt @@ -0,0 +1,81 @@ +package org.jetbrains.dokka.Utilities + +import com.google.inject.Binder +import com.google.inject.Module +import com.google.inject.TypeLiteral +import com.google.inject.binder.AnnotatedBindingBuilder +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 +import kotlin.reflect.KClass + +const val impliedPlatformsName = "impliedPlatforms" + +class DokkaAnalysisModule(val environment: AnalysisEnvironment, + val options: DocumentationOptions, + val defaultPlatformsProvider: DefaultPlatformsProvider, + val nodeReferenceGraph: NodeReferenceGraph, + val logger: DokkaLogger) : Module { + override fun configure(binder: Binder) { + binder.bind<DokkaLogger>().toInstance(logger) + + val coreEnvironment = environment.createCoreEnvironment() + binder.bind<KotlinCoreEnvironment>().toInstance(coreEnvironment) + + val (dokkaResolutionFacade, libraryResolutionFacade) = environment.createResolutionFacade(coreEnvironment) + binder.bind<DokkaResolutionFacade>().toInstance(dokkaResolutionFacade) + binder.bind<DokkaResolutionFacade>().annotatedWith(Names.named("libraryResolutionFacade")).toInstance(libraryResolutionFacade) + + binder.bind<DocumentationOptions>().toInstance(options) + + binder.bind<DefaultPlatformsProvider>().toInstance(defaultPlatformsProvider) + + binder.bind<NodeReferenceGraph>().toInstance(nodeReferenceGraph) + + val descriptor = ServiceLocator.lookup<FormatDescriptor>("format", options.outputFormat) + descriptor.configureAnalysis(binder) + } +} + +object StringListType : TypeLiteral<@JvmSuppressWildcards List<String>>() + +class DokkaOutputModule(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.bind<DocumentationOptions>().toInstance(options) + binder.bind<DokkaLogger>().toInstance(logger) + binder.bind(StringListType).annotatedWith(Names.named(impliedPlatformsName)).toInstance(options.impliedPlatforms) + binder.bind<String>().annotatedWith(Names.named("outlineRoot")).toInstance(options.outlineRoot) + binder.bind<String>().annotatedWith(Names.named("dacRoot")).toInstance(options.dacRoot) + binder.bind<Boolean>().annotatedWith(Names.named("generateClassIndex")).toInstance(options.generateClassIndexPage) + binder.bind<Boolean>().annotatedWith(Names.named("generatePackageIndex")).toInstance(options.generatePackageIndexPage) + val descriptor = ServiceLocator.lookup<FormatDescriptor>("format", options.outputFormat) + + descriptor.configureOutput(binder) + } +} + +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(): AnnotatedBindingBuilder<T> = bind(T::class.java) + +inline fun <reified T: Any> Binder.lazyBind(): Lazy<AnnotatedBindingBuilder<T>> = lazy { bind(T::class.java) } + +inline infix fun <reified T: Any, TKClass: KClass<out T>> Lazy<AnnotatedBindingBuilder<T>>.toOptional(kClass: TKClass?) = + kClass?.let { value toType it } + +inline infix fun <reified T: Any, TKClass: KClass<out T>> AnnotatedBindingBuilder<T>.toType(kClass: TKClass) = to(kClass.java) diff --git a/core/src/main/kotlin/Utilities/DownloadSamples.kt b/core/src/main/kotlin/Utilities/DownloadSamples.kt new file mode 100644 index 000000000..3c28d6cc7 --- /dev/null +++ b/core/src/main/kotlin/Utilities/DownloadSamples.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.dokka.Utilities + +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File +import java.io.FileOutputStream + +object DownloadSamples { + + /** HTTP Client to make requests **/ + val client = OkHttpClient() + + /** + * Function that downloads samples based on the directory structure described in hashmap + */ + fun downloadSamples(): Boolean { + + //loop through each directory of AOSP code in SamplesPathsToURLs.kt + filepathsToUrls.forEach { (filepath, url) -> + + //build request using each URL + val request = Request.Builder() + .url(url) + .build() + + val response = client.newCall(request).execute() + + if (response.isSuccessful) { + + //save .tar.gz file to filepath designated by map + val currentFile = File(filepath) + currentFile.mkdirs() + + val fos = FileOutputStream("$filepath.tar.gz") + fos.write(response.body?.bytes()) + fos.close() + + //Unzip, Untar, and delete compressed file after + extractFiles(filepath) + + } else { + println("Error Downloading Samples: $response") + return false + } + } + + println("Successfully completed download of samples.") + return true + + } + + /** + * Execute bash commands to extract file, then delete archive file + */ + private fun extractFiles(pathToFile: String) { + + ProcessBuilder() + .command("tar","-zxf", "$pathToFile.tar.gz", "-C", pathToFile) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start() + .waitFor() + + ProcessBuilder() + .command("rm", "$pathToFile.tar.gz") + .redirectError(ProcessBuilder.Redirect.INHERIT) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start() + .waitFor() + } + +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Utilities/Html.kt b/core/src/main/kotlin/Utilities/Html.kt new file mode 100644 index 000000000..d9463c595 --- /dev/null +++ b/core/src/main/kotlin/Utilities/Html.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka + +import java.net.URI + + +/** + * Replaces symbols reserved in HTML with their respective entities. + * Replaces & with &, < with < and > with > + */ +fun String.htmlEscape(): String = replace("&", "&").replace("<", "<").replace(">", ">") + +// A URI consists of several parts (as described in https://docs.oracle.com/javase/7/docs/api/java/net/URI.html ): +// [scheme:][//authority][path][?query][#fragment] +// +// The anchorEnchoded() function encodes the given string to make it a legal value for <fragment> +fun String.anchorEncoded(): String { + return URI(null, null, this).getRawFragment() +} diff --git a/core/src/main/kotlin/Utilities/Path.kt b/core/src/main/kotlin/Utilities/Path.kt new file mode 100644 index 000000000..058384993 --- /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/SamplesPathsToURLs.kt b/core/src/main/kotlin/Utilities/SamplesPathsToURLs.kt new file mode 100644 index 000000000..173389d5e --- /dev/null +++ b/core/src/main/kotlin/Utilities/SamplesPathsToURLs.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.dokka.Utilities + +//HashMap of all filepaths to the URLs that should be downloaded to that filepath +val filepathsToUrls: HashMap<String, String> = hashMapOf( + "./samples/development/samples/ApiDemos" to "https://android.googlesource.com/platform/development/+archive/refs/heads/master/samples/ApiDemos.tar.gz", + "./samples/development/samples/NotePad" to "https://android.googlesource.com/platform/development/+archive/refs/heads/master/samples/NotePad.tar.gz", + "./samples/external/icu/android_icu4j/src/samples/java/android/icu/samples/text" to "https://android.googlesource.com/platform/external/icu/+archive/refs/heads/master/android_icu4j/src/samples/java/android/icu/samples/text.tar.gz", + "./samples/frameworks/base/core/java/android/content" to "https://android.googlesource.com/platform/frameworks/base/+archive/refs/heads/master/core/java/android/content.tar.gz", + "./samples/frameworks/base/tests/appwidgets/AppWidgetHostTest/src/com/android/tests/appwidgethost" to "https://android.googlesource.com/platform/frameworks/base/+archive/refs/heads/master/tests/appwidgets/AppWidgetHostTest/src/com/android/tests/appwidgethost.tar.gz" + ) diff --git a/core/src/main/kotlin/Utilities/ServiceLocator.kt b/core/src/main/kotlin/Utilities/ServiceLocator.kt new file mode 100644 index 000000000..83c4c65c1 --- /dev/null +++ b/core/src/main/kotlin/Utilities/ServiceLocator.kt @@ -0,0 +1,100 @@ +package org.jetbrains.dokka.Utilities + +import java.io.File +import java.net.URISyntaxException +import java.net.URL +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) + +object ServiceLocator { + fun <T : Any> lookup(clazz: Class<T>, category: String, implementationName: String): T { + val descriptor = lookupDescriptor(category, implementationName) + return lookup(clazz, descriptor) + } + + fun <T : Any> lookup( + clazz: Class<T>, + descriptor: ServiceDescriptor + ): T { + 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 URL.toFile(): File { + assert(protocol == "file") + + return try { + File(toURI()) + } catch (e: URISyntaxException) { //Try to handle broken URLs, with unescaped spaces + File(path) + } + } + + fun allServices(category: String): List<ServiceDescriptor> { + val entries = this.javaClass.classLoader.getResources("dokka/$category")?.toList() ?: emptyList() + + return entries.flatMap { + when (it.protocol) { + "file" -> it.toFile().listFiles()?.filter { it.extension == "properties" }?.map { lookupDescriptor(category, it.nameWithoutExtension) } ?: emptyList() + "jar" -> { + val file = JarFile(URL(it.file.substringBefore("!")).toFile()) + 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>() + } + } + } +} + +inline fun <reified T : Any> ServiceLocator.lookup(category: String, implementationName: String): T = lookup(T::class.java, category, implementationName) +inline fun <reified T : Any> ServiceLocator.lookup(desc: ServiceDescriptor): T = lookup(T::class.java, desc) + +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/Utilities/StringExtensions.kt b/core/src/main/kotlin/Utilities/StringExtensions.kt new file mode 100644 index 000000000..98f8c8036 --- /dev/null +++ b/core/src/main/kotlin/Utilities/StringExtensions.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.dokka.Utilities + +/** + * Finds the first sentence of a string, accounting for periods that may occur in parenthesis. + */ +fun String.firstSentence(): String { + + // First, search for location of first period and first parenthesis. + val firstPeriodIndex = this.indexOf('.') + val openParenIndex = this.indexOf('(') + + // If there is no opening parenthesis found or if it occurs after the occurrence of the first period, just return + // the first sentence, or the entire string if no period is found. + if (openParenIndex == -1 || openParenIndex > firstPeriodIndex) { + return if (firstPeriodIndex != -1) { + this.substring(0, firstPeriodIndex + 1) + } else { + this + } + } + + // At this point we know that the opening parenthesis occurs before the first period, so we look for the matching + // closing parenthesis. + val closeParenIndex = this.indexOf(')', openParenIndex) + + // If a matching closing parenthesis is found, take that substring and recursively process the rest of the string. + // This is to accommodate periods inside of parenthesis. If a matching closing parenthesis is not found, return the + // original string. + return if (closeParenIndex != -1) { + this.substring(0, closeParenIndex) + this.substring(closeParenIndex, this.length).firstSentence() + } else { + this + } + +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Utilities/Uri.kt b/core/src/main/kotlin/Utilities/Uri.kt new file mode 100644 index 000000000..9827c624c --- /dev/null +++ b/core/src/main/kotlin/Utilities/Uri.kt @@ -0,0 +1,40 @@ +package org.jetbrains.dokka + +import java.net.URI + + +fun URI.relativeTo(uri: URI): URI { + // Normalize paths to remove . and .. segments + val base = uri.normalize() + val child = this.normalize() + + fun StringBuilder.appendRelativePath() { + // Split paths into segments + var bParts = base.path.split('/').dropLastWhile { it.isEmpty() } + val cParts = child.path.split('/').dropLastWhile { it.isEmpty() } + + // Discard trailing segment of base path + if (bParts.isNotEmpty() && !base.path.endsWith("/")) { + bParts = bParts.dropLast(1) + } + + // Compute common prefix + val commonPartsSize = bParts.zip(cParts).takeWhile { (basePart, childPart) -> basePart == childPart }.count() + bParts.drop(commonPartsSize).joinTo(this, separator = "") { "../" } + cParts.drop(commonPartsSize).joinTo(this, separator = "/") + } + + return URI.create(buildString { + if (base.path != child.path) { + appendRelativePath() + } + child.rawQuery?.let { + append("?") + append(it) + } + child.rawFragment?.let { + append("#") + append(it) + } + }) +}
\ 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 000000000..12f571bee --- /dev/null +++ b/core/src/main/kotlin/javadoc/docbase.kt @@ -0,0 +1,525 @@ +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 == NodeKind.Exception + override fun isEnumConstant(): Boolean = node.kind == NodeKind.EnumItem + override fun isEnum(): Boolean = node.kind == NodeKind.Enum + override fun isMethod(): Boolean = node.kind == NodeKind.Function + override fun isInterface(): Boolean = node.kind == NodeKind.Interface + override fun isField(): Boolean = node.kind == NodeKind.Field + override fun isClass(): Boolean = node.kind == NodeKind.Class + override fun isAnnotationType(): Boolean = node.kind == NodeKind.AnnotationClass + override fun isConstructor(): Boolean = node.kind == NodeKind.Constructor + override fun isOrdinaryClass(): Boolean = node.kind == NodeKind.Class + override fun isAnnotationTypeElement(): Boolean = node.kind == NodeKind.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 != NodeKind.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() + 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(NodeKind.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(NodeKind.AnnotationClass).map { AnnotationDescAdapter(module, it) }.toTypedArray() + override fun exceptions(): Array<out ClassDoc> = node.members(NodeKind.Exception).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun ordinaryClasses(): Array<out ClassDoc> = node.members(NodeKind.Class).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun interfaces(): Array<out ClassDoc> = node.members(NodeKind.Interface).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun errors(): Array<out ClassDoc> = emptyArray() + override fun enums(): Array<out ClassDoc> = node.members(NodeKind.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 +} + +open 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? = 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 == NodeKind.Type) { + return null + } + + var owner: DocumentationNode? = node + while (owner != null) { + if (owner.kind == NodeKind.Package) { + return PackageAdapter(module, owner) + } + owner = owner.owner + } + + return null + } + + override fun containingClass(): ClassDoc? { + if (node.kind == NodeKind.Type) { + return null + } + + var owner = node.owner + while (owner != null) { + if (owner.kind in NodeKind.classLike) { + return ClassDocumentationNodeAdapter(module, owner) + } + 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)?.simpleName() ?: node.simpleName() + 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 NodeKind.classLike, + NodeKind.ExternalClass, + NodeKind.Exception -> module.classNamed(qualifiedTypeName()) ?: ClassDocumentationNodeAdapter(module, node) + + else -> when { + node.links.firstOrNull { it.kind != NodeKind.ExternalLink } != null -> { + TypeAdapter(module, node.links.firstOrNull { it.kind != NodeKind.ExternalLink }!!).asClassDoc() + } + else -> ClassDocumentationNodeAdapter(module, node) // TODO ? + } + } + + override fun asTypeVariable(): TypeVariable? = if (node.kind == NodeKind.TypeParameter) TypeVariableAdapter(module, node) else null + override fun asParameterizedType(): ParameterizedType? = + if (node.details(NodeKind.Type).isNotEmpty() && javaLanguageService.getArrayElementType(node) == null) + ParameterizedTypeAdapter(module, node) + else + null + + override fun asAnnotationTypeDoc(): AnnotationTypeDoc? = if (node.kind == NodeKind.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 NotAnnotatedTypeAdapter(typeAdapter: AnnotatedTypeAdapter) : Type by typeAdapter { + override fun asAnnotatedType() = null +} + +class AnnotatedTypeAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : TypeAdapter(module, node), AnnotatedType { + override fun underlyingType(): Type? = NotAnnotatedTypeAdapter(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(NodeKind.UpperBound).map { TypeAdapter(module, it) }.toTypedArray() + override fun superBounds(): Array<out Type> = node.details(NodeKind.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) { + NodeKind.Function, + NodeKind.Constructor -> ExecutableMemberAdapter(module, owner) + + NodeKind.Class, + NodeKind.Interface, + NodeKind.Enum -> ClassDocumentationNodeAdapter(module, owner) + + else -> ProgramElementAdapter(module, node.owner!!) + } + } + + override fun bounds(): Array<out Type>? = node.details(NodeKind.UpperBound).map { TypeAdapter(module, it) }.toTypedArray() + override fun annotations(): Array<out AnnotationDesc>? = node.members(NodeKind.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(NodeKind.Type).map { TypeVariableAdapter(module, it) }.toTypedArray() + override fun superclassType(): Type? = + node.lookupSuperClasses(module) + .firstOrNull { it.kind == NodeKind.Class || it.kind == NodeKind.ExternalClass } + ?.let { ClassDocumentationNodeAdapter(module, it) } + + override fun interfaceTypes(): Array<out Type> = + node.lookupSuperClasses(module) + .filter { it.kind == NodeKind.Interface } + .map { ClassDocumentationNodeAdapter(module, it) } + .toTypedArray() + + override fun containingType(): Type? = when (node.owner?.kind) { + NodeKind.Package -> null + NodeKind.Class, + NodeKind.Interface, + NodeKind.Object, + NodeKind.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(NodeKind.Type)) + override fun type(): Type? = TypeAdapter(module, node.detail(NodeKind.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: NodeKind = NodeKind.Class) = DocumentationNode(fqName.substringAfterLast(".", fqName), Content.Empty, kind).let { node -> + val pkg = fqName.substringBeforeLast(".", "") + if (pkg.isNotEmpty()) { + node.append(DocumentationNode(pkg, Content.Empty, NodeKind.Package), RefKind.Owner) + } + + node +} + +private fun DocumentationNode.hasNonEmptyContent() = + this.content.summary !is ContentEmpty || this.content.description !is ContentEmpty || this.content.sections.isNotEmpty() + + +open class ExecutableMemberAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : 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 == ContentTags.Exceptions && it.subjectName != null } + .map { ThrowsTagAdapter(this, ClassDocumentationNodeAdapter(module, classOf(it.subjectName!!, NodeKind.Exception)), it.children) } + .toTypedArray() + + override fun isVarArgs(): Boolean = node.details(NodeKind.Parameter).any { false } // TODO + + override fun isSynchronized(): Boolean = node.annotations.any { it.name == "synchronized" } + + override fun paramTags(): Array<out ParamTag> = + collectParamTags(NodeKind.Parameter, sectionFilter = { it.subjectName in parameters().map { it.name() } }) + + override fun thrownExceptionTypes(): Array<out Type> = emptyArray() + override fun receiverType(): Type? = receiverNode()?.let { receiver -> TypeAdapter(module, receiver) } + override fun flatSignature(): String = node.details(NodeKind.Parameter).map { JavaLanguageService().renderType(it) }.joinToString(", ", "(", ")") + override fun signature(): String = node.details(NodeKind.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(NodeKind.Parameter).map { ParameterAdapter(module, it) } + ).toTypedArray() + + override fun typeParameters(): Array<out TypeVariable> = node.details(NodeKind.TypeParameter).map { TypeVariableAdapter(module, it) }.toTypedArray() + + override fun typeParamTags(): Array<out ParamTag> = + collectParamTags(NodeKind.TypeParameter, sectionFilter = { it.subjectName in typeParameters().map { it.simpleTypeName() } }) + + private fun receiverNode() = node.details(NodeKind.Receiver).let { receivers -> + when { + receivers.isNotEmpty() -> receivers.single().detail(NodeKind.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") + + override fun containingClass(): ClassDoc? { + return super.containingClass() + } +} + +class MethodAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : 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(NodeKind.Type)) + + override fun tags(tagname: String?) = super.tags(tagname) + + override fun tags(): Array<out Tag> { + val tags = super.tags().toMutableList() + node.content.findSectionByTag(ContentTags.Return)?.let { + tags += ReturnTagAdapter(module, this, it.children) + } + + return tags.toTypedArray() + } +} + +class FieldAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : ProgramElementAdapter(module, node), FieldDoc { + override fun isSynthetic(): Boolean = false + + override fun constantValueExpression(): String? = node.detailOrNull(NodeKind.Value)?.let { it.name } + override fun constantValue(): Any? = constantValueExpression() + + override fun type(): Type = TypeAdapter(module, node.detail(NodeKind.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) + : ProgramElementAdapter(module, classNode), + Type by TypeAdapter(module, classNode), + ClassDoc { + + override fun name(): String { + val parent = classNode.owner + if (parent?.kind in NodeKind.classLike) { + return parent!!.name + "." + classNode.name + } + return classNode.simpleName() + } + + override fun constructors(filter: Boolean): Array<out ConstructorDoc> = classNode.members(NodeKind.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(NodeKind.TypeParameter).map { TypeVariableAdapter(module, it) }.toTypedArray() + override fun asTypeVariable(): TypeVariable? = if (classNode.kind == NodeKind.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(NodeKind.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(NodeKind.EnumItem).map { FieldAdapter(module, it) }.toTypedArray() + override fun isAbstract(): Boolean = classNode.details(NodeKind.Modifier).any { it.name == "abstract" } + override fun interfaceTypes(): Array<out Type> = classNode.lookupSuperClasses(module) + .filter { it.kind == NodeKind.Interface } + .map { ClassDocumentationNodeAdapter(module, it) } + .toTypedArray() + + override fun interfaces(): Array<out ClassDoc> = classNode.lookupSuperClasses(module) + .filter { it.kind == NodeKind.Interface } + .map { ClassDocumentationNodeAdapter(module, it) } + .toTypedArray() + + override fun typeParamTags(): Array<out ParamTag> = + collectParamTags(NodeKind.TypeParameter, sectionFilter = { it.subjectName in typeParameters().map { it.simpleTypeName() } }) + + override fun fields(): Array<out FieldDoc> = fields(true) + override fun fields(filter: Boolean): Array<out FieldDoc> = classNode.members(NodeKind.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 == NodeKind.Class }?.let { ClassDocumentationNodeAdapter(module, it) } + override fun serializationMethods(): Array<out MethodDoc> = emptyArray() // TODO + override fun superclass(): ClassDoc? = classNode.lookupSuperClasses(module).singleOrNull { it.kind == NodeKind.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(NodeKind.Supertype).filter { it.qualifiedName() !in visitedTypes }) + } + + return false + } + + override fun innerClasses(): Array<out ClassDoc> = classNode.members(NodeKind.Class).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun innerClasses(filter: Boolean): Array<out ClassDoc> = innerClasses() +} + +fun DocumentationNode.lookupSuperClasses(module: ModuleNodeAdapter) = + details(NodeKind.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 = NodeKind.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(NodeKind.Package).associateBy { it.name } + val allTypes = module.members(NodeKind.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(NodeKind.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() +} + +private fun DocumentationNodeAdapter.collectParamTags(kind: NodeKind, sectionFilter: (ContentSection) -> Boolean) = + (node.details(kind) + .filter(DocumentationNode::hasNonEmptyContent) + .map { ParamTagAdapter(module, this, it.name, true, it.content.children) } + + + node.content.sections + .filter(sectionFilter) + .map { ParamTagAdapter(module, this, it.subjectName ?: "?", true, it.children) }) + + .toTypedArray()
\ No newline at end of file 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 000000000..483fb3cdc --- /dev/null +++ b/core/src/main/kotlin/javadoc/dokka-adapters.kt @@ -0,0 +1,39 @@ +package org.jetbrains.dokka.javadoc + +import com.google.inject.Binder +import com.google.inject.Inject +import com.sun.tools.doclets.formats.html.HtmlDoclet +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.* +import org.jetbrains.dokka.Utilities.bind +import org.jetbrains.dokka.Utilities.toType + +class JavadocGenerator @Inject constructor(val options: DocumentationOptions, val logger: DokkaLogger) : Generator { + + override fun buildPages(nodes: Iterable<DocumentationNode>) { + val module = nodes.single() as DocumentationModule + + HtmlDoclet.start(ModuleNodeAdapter(module, StandardReporter(logger), options.outputDir)) + } + + override fun buildOutlines(nodes: Iterable<DocumentationNode>) { + // no outline could be generated separately + } + + override fun buildSupportFiles() { + } + + override fun buildPackageList(nodes: Iterable<DocumentationNode>) { + // handled by javadoc itself + } +} + +class JavadocFormatDescriptor : + FormatDescriptor, + DefaultAnalysisComponent, + DefaultAnalysisComponentServices by KotlinAsJava { + + override fun configureOutput(binder: Binder): Unit = with(binder) { + bind<Generator>() toType JavadocGenerator::class + } +} diff --git a/core/src/main/kotlin/javadoc/reporter.kt b/core/src/main/kotlin/javadoc/reporter.kt new file mode 100644 index 000000000..fc38368c9 --- /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 000000000..6125f9689 --- /dev/null +++ b/core/src/main/kotlin/javadoc/source-position.kt @@ -0,0 +1,19 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.SourcePosition +import org.jetbrains.dokka.DocumentationNode +import org.jetbrains.dokka.NodeKind +import java.io.File + +class SourcePositionAdapter(val docNode: DocumentationNode) : SourcePosition { + + private val sourcePositionParts: List<String> by lazy { + docNode.details(NodeKind.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 000000000..95c6e87fc --- /dev/null +++ b/core/src/main/kotlin/javadoc/tags.kt @@ -0,0 +1,240 @@ +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 ${parameterComment()}" // Seems has no effect, so used for debug + override fun inlineTags(): Array<out Tag> = buildInlineTags(module, holder, content).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, val content: List<ContentNode>) : 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 = "${name()} ${exceptionName()} ${exceptionComment()}" + override fun inlineTags(): Array<out Tag> = buildInlineTags(type.module, holder, content).toTypedArray() + override fun firstSentenceTags(): Array<out Tag> = emptyArray() + + override fun exceptionComment(): String = content.toString() + override fun exceptionType(): Type = type + override fun exception(): ClassDoc = type + override fun exceptionName(): String = type.qualifiedTypeName() +} + +class ReturnTagAdapter(val module: ModuleNodeAdapter, val holder: Doc, val content: List<ContentNode>) : Tag { + override fun name(): String = "@return" + override fun kind() = name() + override fun holder() = holder + override fun position(): SourcePosition? = holder.position() + + override fun text(): String = "@return $content" // Seems has no effect, so used for debug + override fun inlineTags(): Array<Tag> = buildInlineTags(module, holder, content).toTypedArray() + override fun firstSentenceTags(): Array<Tag> = inlineTags() +} + +fun buildInlineTags(module: ModuleNodeAdapter, holder: Doc, tags: List<ContentNode>): List<Tag> = ArrayList<Tag>().apply { tags.forEach { buildInlineTags(module, holder, it, this) } } + +fun buildInlineTags(module: ModuleNodeAdapter, holder: Doc, root: ContentNode): List<Tag> = ArrayList<Tag>().apply { buildInlineTags(module, holder, root, this) } + +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) { + NodeKind.Function -> result.add(SeeMethodTagAdapter(holder, MethodAdapter(module, node.node!!), node)) + + in NodeKind.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 ContentSpecialReference -> surroundWith(module, holder, "<aside class=\"note\">", "</aside>", node, result) + 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 ContentDescriptionList -> surroundWith(module, holder, "<dl>", "</dl>", node, result) + is ContentDescriptionTerm -> surroundWith(module, holder, "<dt>", "</dt>", node, result) + is ContentDescriptionDefinition -> surroundWith(module, holder, "<dd>", "</dd>", node, result) + + is ContentTable -> surroundWith(module, holder, "<table>", "</table>", node, result) + is ContentTableBody -> surroundWith(module, holder, "<tbody>", "</tbody>", node, result) + is ContentTableRow -> surroundWith(module, holder, "<tr>", "</tr>", node, result) + is ContentTableHeader -> surroundWith(module, holder, "<th>", "</th>", node, result) + is ContentTableCell -> surroundWith(module, holder, "<td>", "</td>", 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) + } + is ContentBlock -> { + surroundWith(module, holder, "", "", node, result) + } + is ContentHardLineBreak -> result.add(TextTag(holder, ContentText("<br/>"))) + + else -> result.add(TextTag(holder, ContentText("$node"))) + } +}
\ No newline at end of file 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 000000000..87807e1e3 --- /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.MainKt + diff --git a/core/src/main/resources/dokka/format/dac-as-java.properties b/core/src/main/resources/dokka/format/dac-as-java.properties new file mode 100644 index 000000000..29e05b3fd --- /dev/null +++ b/core/src/main/resources/dokka/format/dac-as-java.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.DacAsJavaFormatDescriptor +description=Generates developer.android.com website documentation
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/dac.properties b/core/src/main/resources/dokka/format/dac.properties new file mode 100644 index 000000000..52b19097f --- /dev/null +++ b/core/src/main/resources/dokka/format/dac.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.DacFormatDescriptor +description=Generates developer.android.com website documentation
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/gfm.properties b/core/src/main/resources/dokka/format/gfm.properties new file mode 100644 index 000000000..5e8f7aa8c --- /dev/null +++ b/core/src/main/resources/dokka/format/gfm.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.GFMFormatDescriptor +description=Produces documentation in GitHub-flavored markdown format 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 000000000..f598f3771 --- /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 000000000..7881dfae8 --- /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/java-layout-html-as-java.properties b/core/src/main/resources/dokka/format/java-layout-html-as-java.properties new file mode 100644 index 000000000..7d178ba4a --- /dev/null +++ b/core/src/main/resources/dokka/format/java-layout-html-as-java.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.JavaLayoutHtmlAsJavaFormatDescriptor +description=Produces Java Style Docs with Javadoc like layout
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/java-layout-html.properties b/core/src/main/resources/dokka/format/java-layout-html.properties new file mode 100644 index 000000000..fbb2bbedc --- /dev/null +++ b/core/src/main/resources/dokka/format/java-layout-html.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatDescriptor +description=Produces Kotlin Style Docs with Javadoc like layout
\ 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 000000000..a0d8a945d --- /dev/null +++ b/core/src/main/resources/dokka/format/javadoc.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.javadoc.JavadocFormatDescriptor +description=Produces Javadoc, with Kotlin declarations as Java view
\ 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 000000000..b11401a4b --- /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-html.properties b/core/src/main/resources/dokka/format/kotlin-website-html.properties new file mode 100644 index 000000000..f4c320b9f --- /dev/null +++ b/core/src/main/resources/dokka/format/kotlin-website-html.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.KotlinWebsiteHtmlFormatDescriptor +description=Generates Kotlin website documentation
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/kotlin-website-samples.properties b/core/src/main/resources/dokka/format/kotlin-website-samples.properties new file mode 100644 index 000000000..bda616a41 --- /dev/null +++ b/core/src/main/resources/dokka/format/kotlin-website-samples.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.KotlinWebsiteFormatRunnableSamplesDescriptor +description=Generates Kotlin website documentation
\ 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 000000000..c13e76754 --- /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 000000000..6217a6df1 --- /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/inbound-link-resolver/dokka-default.properties b/core/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties new file mode 100644 index 000000000..c484a920d --- /dev/null +++ b/core/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.InboundExternalLinkResolutionService$Dokka +description=Uses Dokka Default resolver
\ No newline at end of file diff --git a/core/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties b/core/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties new file mode 100644 index 000000000..3b61eabe7 --- /dev/null +++ b/core/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.JavaLayoutHtmlInboundLinkResolutionService +description=Resolver for JavaLayoutHtml
\ No newline at end of file diff --git a/core/src/main/resources/dokka/inbound-link-resolver/javadoc.properties b/core/src/main/resources/dokka/inbound-link-resolver/javadoc.properties new file mode 100644 index 000000000..0d5d7d17c --- /dev/null +++ b/core/src/main/resources/dokka/inbound-link-resolver/javadoc.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.InboundExternalLinkResolutionService$Javadoc +description=Uses Javadoc Default resolver
\ 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 000000000..914be69d6 --- /dev/null +++ b/core/src/main/resources/dokka/styles/style.css @@ -0,0 +1,283 @@ +@import url(https://fonts.googleapis.com/css?family=Open+Sans:300i,400,700); + +body, table { + padding:50px; + font:14px/1.5 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; + color:#555; + font-weight:300; + margin-left: auto; + margin-right: auto; + max-width: 1440px; +} + +.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/test/kotlin/Model/CodeNodeTest.kt b/core/src/test/kotlin/Model/CodeNodeTest.kt new file mode 100644 index 000000000..ae3e67183 --- /dev/null +++ b/core/src/test/kotlin/Model/CodeNodeTest.kt @@ -0,0 +1,14 @@ +package Model + +import org.jetbrains.dokka.Model.CodeNode +import org.junit.Test +import kotlin.test.assertEquals + +class CodeNodeTest { + + @Test fun text_normalisesInitialWhitespace() { + val expected = "Expected\ntext in this\ttest" + val sut = CodeNode("\n \t \r $expected", "") + assertEquals(expected, sut.text()) + } +}
\ No newline at end of file diff --git a/core/src/test/kotlin/TestAPI.kt b/core/src/test/kotlin/TestAPI.kt new file mode 100644 index 000000000..ef2923cce --- /dev/null +++ b/core/src/test/kotlin/TestAPI.kt @@ -0,0 +1,302 @@ +package org.jetbrains.dokka.tests + +import com.google.inject.Guice +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.FileUtil +import com.intellij.rt.execution.junit.FileComparisonFailure +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Utilities.DokkaAnalysisModule +import org.jetbrains.kotlin.cli.common.config.ContentRoot +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot +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.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.junit.Assert +import org.junit.Assert.fail +import java.io.File + +fun verifyModel(vararg roots: ContentRoot, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + format: String = "html", + includeNonPublic: Boolean = true, + perPackageOptions: List<DokkaConfiguration.PackageOptions> = emptyList(), + noStdlibLink: Boolean = true, + collectInheritedExtensionsFromLibraries: Boolean = false, + verifier: (DocumentationModule) -> Unit) { + val documentation = DocumentationModule("test") + + val options = DocumentationOptions( + "", + format, + includeNonPublic = includeNonPublic, + skipEmptyPackages = false, + includeRootPackage = true, + sourceLinks = listOf(), + perPackageOptions = perPackageOptions, + generateClassIndexPage = false, + generatePackageIndexPage = false, + noStdlibLink = noStdlibLink, + noJdkLink = false, + cacheRoot = "default", + languageVersion = null, + apiVersion = null, + collectInheritedExtensionsFromLibraries = collectInheritedExtensionsFromLibraries + ) + + appendDocumentation(documentation, *roots, + withJdk = withJdk, + withKotlinRuntime = withKotlinRuntime, + options = options) + documentation.prepareForGeneration(options) + + verifier(documentation) +} + +fun appendDocumentation(documentation: DocumentationModule, + vararg roots: ContentRoot, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + options: DocumentationOptions, + defaultPlatforms: List<String> = emptyList()) { + val messageCollector = object : MessageCollector { + override fun clear() { + + } + + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation?) { + when (severity) { + CompilerMessageSeverity.STRONG_WARNING, + CompilerMessageSeverity.WARNING, + CompilerMessageSeverity.LOGGING, + CompilerMessageSeverity.OUTPUT, + CompilerMessageSeverity.INFO, + CompilerMessageSeverity.ERROR -> { + println("$severity: $message at $location") + } + CompilerMessageSeverity.EXCEPTION -> { + fail("$severity: $message at $location") + } + } + } + + override fun hasErrors() = false + } + + val environment = AnalysisEnvironment(messageCollector) + environment.apply { + if (withJdk || withKotlinRuntime) { + val stringRoot = PathManager.getResourceRoot(String::class.java, "/java/lang/String.class") + addClasspath(File(stringRoot)) + } + if (withKotlinRuntime) { + val kotlinStrictfpRoot = PathManager.getResourceRoot(Strictfp::class.java, "/kotlin/jvm/Strictfp.class") + addClasspath(File(kotlinStrictfpRoot)) + } + addRoots(roots.toList()) + + loadLanguageVersionSettings(options.languageVersion, options.apiVersion) + } + val defaultPlatformsProvider = object : DefaultPlatformsProvider { + override fun getDefaultPlatforms(descriptor: DeclarationDescriptor) = defaultPlatforms + } + val injector = Guice.createInjector( + DokkaAnalysisModule(environment, options, defaultPlatformsProvider, documentation.nodeRefGraph, DokkaConsoleLogger)) + buildDocumentationModule(injector, documentation) + Disposer.dispose(environment) +} + +fun verifyModel(source: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + format: String = "html", + includeNonPublic: Boolean = true, + verifier: (DocumentationModule) -> Unit) { + if (!File(source).exists()) { + throw IllegalArgumentException("Can't find test data file $source") + } + verifyModel(contentRootFromPath(source), + withJdk = withJdk, + withKotlinRuntime = withKotlinRuntime, + format = format, + includeNonPublic = includeNonPublic, + verifier = verifier) +} + +fun verifyPackageMember(source: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + verifier: (DocumentationNode) -> Unit) { + verifyModel(source, withJdk = withJdk, withKotlinRuntime = withKotlinRuntime) { model -> + val pkg = model.members.single() + verifier(pkg.members.single()) + } +} + +fun verifyJavaModel(source: String, + withKotlinRuntime: Boolean = false, + format: String = "html", + verifier: (DocumentationModule) -> Unit) { + val tempDir = FileUtil.createTempDirectory("dokka", "") + try { + val sourceFile = File(source) + FileUtil.copy(sourceFile, File(tempDir, sourceFile.name)) + verifyModel(JavaSourceRoot(tempDir, null), format = format, withJdk = true, withKotlinRuntime = withKotlinRuntime, verifier = verifier) + } + finally { + FileUtil.delete(tempDir) + } +} + +fun verifyJavaPackageMember(source: String, + withKotlinRuntime: Boolean = false, + verifier: (DocumentationNode) -> Unit) { + verifyJavaModel(source, withKotlinRuntime) { model -> + val pkg = model.members.single() + verifier(pkg.members.single()) + } +} + +fun verifyOutput(roots: Array<ContentRoot>, + outputExtension: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + format: String = "html", + includeNonPublic: Boolean = true, + noStdlibLink: Boolean = true, + collectInheritedExtensionsFromLibraries: Boolean = false, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + verifyModel( + *roots, + withJdk = withJdk, + withKotlinRuntime = withKotlinRuntime, + format = format, + includeNonPublic = includeNonPublic, + noStdlibLink = noStdlibLink, + collectInheritedExtensionsFromLibraries = collectInheritedExtensionsFromLibraries + ) { + verifyModelOutput(it, outputExtension, roots.first().path, outputGenerator) + } +} + +fun verifyModelOutput(it: DocumentationModule, + outputExtension: String, + sourcePath: String, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + val output = StringBuilder() + outputGenerator(it, output) + val ext = outputExtension.removePrefix(".") + val expectedFile = File(sourcePath.replaceAfterLast(".", ext, sourcePath + "." + ext)) + assertEqualsIgnoringSeparators(expectedFile, output.toString()) +} + +fun verifyOutput( + path: String, + outputExtension: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + format: String = "html", + includeNonPublic: Boolean = true, + noStdlibLink: Boolean = true, + collectInheritedExtensionsFromLibraries: Boolean = false, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit +) { + verifyOutput( + arrayOf(contentRootFromPath(path)), + outputExtension, + withJdk, + withKotlinRuntime, + format, + includeNonPublic, + noStdlibLink, + collectInheritedExtensionsFromLibraries, + outputGenerator + ) +} + +fun verifyJavaOutput(path: String, + outputExtension: String, + withKotlinRuntime: Boolean = false, + format: String = "html", + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + verifyJavaModel(path, withKotlinRuntime, format) { model -> + verifyModelOutput(model, outputExtension, path, outputGenerator) + } +} + +fun assertEqualsIgnoringSeparators(expectedFile: File, output: String) { + if (!expectedFile.exists()) expectedFile.createNewFile() + val expectedText = expectedFile.readText().replace("\r\n", "\n") + val actualText = output.replace("\r\n", "\n") + + if(expectedText != actualText) + throw FileComparisonFailure("", expectedText, actualText, expectedFile.canonicalPath) +} + +fun assertEqualsIgnoringSeparators(expectedOutput: String, output: String) { + Assert.assertEquals(expectedOutput.replace("\r\n", "\n"), output.replace("\r\n", "\n")) +} + +fun StringBuilder.appendChildren(node: ContentBlock): StringBuilder { + for (child in node.children) { + val childText = child.toTestString() + append(childText) + } + return this +} + +fun StringBuilder.appendNode(node: ContentNode): StringBuilder { + when (node) { + is ContentText -> { + append(node.text) + } + is ContentEmphasis -> append("*").appendChildren(node).append("*") + is ContentBlockCode -> { + if (node.language.isNotBlank()) + appendln("[code lang=${node.language}]") + else + appendln("[code]") + appendChildren(node) + appendln() + appendln("[/code]") + } + is ContentNodeLink -> { + append("[") + appendChildren(node) + append(" -> ") + append(node.node.toString()) + append("]") + } + is ContentBlock -> { + appendChildren(node) + } + is NodeRenderContent -> { + append("render(") + append(node.node) + append(",") + append(node.mode) + append(")") + } + is ContentSymbol -> { append(node.text) } + is ContentEmpty -> { /* nothing */ } + else -> throw IllegalStateException("Don't know how to format node $node") + } + return this +} + +fun ContentNode.toTestString(): String { + val node = this + return StringBuilder().apply { + appendNode(node) + }.toString() +} + +val ContentRoot.path: String + get() = when(this) { + is KotlinSourceRoot -> path + is JavaSourceRoot -> file.path + else -> throw UnsupportedOperationException() + } diff --git a/core/src/test/kotlin/format/DacFormatTest.kt b/core/src/test/kotlin/format/DacFormatTest.kt new file mode 100644 index 000000000..5d8babc3d --- /dev/null +++ b/core/src/test/kotlin/format/DacFormatTest.kt @@ -0,0 +1,58 @@ +package org.jetbrains.dokka.tests.format + +import org.jetbrains.dokka.Formats.DacAsJavaFormatDescriptor +import org.jetbrains.dokka.Formats.DacFormatDescriptor +import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatDescriptorBase +import org.junit.Test + +class DacFormatTest: DacFormatTestCase() { + val dacFormatDescriptor = DacFormatDescriptor() + val dacAsJavaFormatDescriptor = DacAsJavaFormatDescriptor() + val dacFormat = "dac" + val dacAsJavaFormat = "dac-as-java" + + private fun verifyBothFormats(directory: String) { + verifyDirectory(directory, dacFormatDescriptor, dacFormat) + verifyDirectory(directory, dacAsJavaFormatDescriptor, dacAsJavaFormat) + } + + @Test fun javaSeeTag() { + verifyBothFormats("javaSeeTag") + } + + @Test fun javaConstructor() { + verifyBothFormats("javaConstructor") + } + + @Test + fun javaSeeTagAsJava() { + verifyBothFormats("javaSeeTag") + } + + @Test + fun javaConstructorAsJava() { + verifyBothFormats("javaConstructor") + } + + @Test + fun javaDefaultConstructor() { + verifyBothFormats("javaDefaultConstructor") + } + + @Test + fun javaInheritedMethods() { + verifyBothFormats("inheritedMethods") + } + + @Test fun javaMethodVisibilities() { + verifyBothFormats("javaMethodVisibilities") + } + + @Test fun javaClassLinks() { + verifyBothFormats("javaClassLinks") + } + + @Test fun deprecation() { + verifyBothFormats("deprecation") + } +}
\ No newline at end of file diff --git a/core/src/test/kotlin/format/DacFormatTestCase.kt b/core/src/test/kotlin/format/DacFormatTestCase.kt new file mode 100644 index 000000000..922b58097 --- /dev/null +++ b/core/src/test/kotlin/format/DacFormatTestCase.kt @@ -0,0 +1,90 @@ +package org.jetbrains.dokka.tests.format + +import com.google.inject.Guice +import com.google.inject.Injector +import com.google.inject.Module +import com.google.inject.name.Names +import org.jetbrains.dokka.DocumentationOptions +import org.jetbrains.dokka.DokkaLogger +import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatDescriptorBase +import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatGenerator +import org.jetbrains.dokka.Generator +import org.jetbrains.dokka.Utilities.bind +import org.jetbrains.dokka.tests.assertEqualsIgnoringSeparators +import org.jetbrains.dokka.tests.verifyModel +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import java.io.File +import java.net.URI + +abstract class DacFormatTestCase { + @get:Rule + var folder = TemporaryFolder() + + protected fun verifyDirectory(directory: String, formatDescriptor: JavaLayoutHtmlFormatDescriptorBase, dokkaFormat: String) { + val injector: Injector by lazy { + val options = + DocumentationOptions( + folder.toString(), + dokkaFormat, + apiVersion = null, + languageVersion = null, + generateClassIndexPage = false, + generatePackageIndexPage = false, + noStdlibLink = false, + noJdkLink = false, + collectInheritedExtensionsFromLibraries = true + ) + + Guice.createInjector(Module { binder -> + + binder.bind<Boolean>().annotatedWith(Names.named("generateClassIndex")).toInstance(false) + binder.bind<Boolean>().annotatedWith(Names.named("generatePackageIndex")).toInstance(false) + + binder.bind<String>().annotatedWith(Names.named("dacRoot")).toInstance("") + binder.bind<String>().annotatedWith(Names.named("outlineRoot")).toInstance("") + binder.bind<File>().annotatedWith(Names.named("outputDir")).toInstance(folder.root) + + binder.bind<DocumentationOptions>().toProvider { options } + binder.bind<DokkaLogger>().toInstance(object : DokkaLogger { + override fun info(message: String) { + println(message) + } + + override fun warn(message: String) { + println("WARN: $message") + } + + override fun error(message: String) { + println("ERROR: $message") + } + }) + + formatDescriptor.configureOutput(binder) + }) + } + + + val directoryFile = File("testdata/format/dac/$directory") + verifyModel( + JavaSourceRoot(directoryFile, null), KotlinSourceRoot(directoryFile.path, false), + format = dokkaFormat + ) { documentationModule -> + val nodes = documentationModule.members.single().members + with(injector.getInstance(Generator::class.java)) { + this as JavaLayoutHtmlFormatGenerator + buildPages(listOf(documentationModule)) + val byLocations = nodes.groupBy { mainUri(it) } + val tmpFolder = folder.root.toURI().resolve("${documentationModule.name}/") + byLocations.forEach { (loc, node) -> + val output = StringBuilder() + output.append(tmpFolder.resolve(URI("/").relativize(loc)).toURL().readText()) + val expectedFile = File(File(directoryFile, dokkaFormat), "${node.first().name}.html") + assertEqualsIgnoringSeparators(expectedFile, output.toString()) + } + } + } + } +}
\ No newline at end of file diff --git a/core/src/test/kotlin/format/FileGeneratorTestCase.kt b/core/src/test/kotlin/format/FileGeneratorTestCase.kt new file mode 100644 index 000000000..ef9e815d2 --- /dev/null +++ b/core/src/test/kotlin/format/FileGeneratorTestCase.kt @@ -0,0 +1,35 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.* +import org.junit.Before +import org.junit.Rule +import org.junit.rules.TemporaryFolder + + +abstract class FileGeneratorTestCase { + abstract val formatService: FormatService + + @get:Rule + var folder = TemporaryFolder() + + val fileGenerator = FileGenerator(folder.apply { create() }.root) + + @Before + fun bindGenerator() { + fileGenerator.formatService = formatService + } + + fun buildPagesAndReadInto(nodes: List<DocumentationNode>, sb: StringBuilder) = with(fileGenerator) { + buildPages(nodes) + val byLocations = nodes.groupBy { location(it) } + byLocations.forEach { (loc, _) -> + if (byLocations.size > 1) { + if (sb.isNotBlank() && !sb.endsWith('\n')) { + sb.appendln() + } + sb.appendln("<!-- File: ${loc.file.relativeTo(root).toUnixString()} -->") + } + sb.append(loc.file.readText()) + } + } +}
\ No newline at end of file diff --git a/core/src/test/kotlin/format/GFMFormatTest.kt b/core/src/test/kotlin/format/GFMFormatTest.kt new file mode 100644 index 000000000..b90ab2bf2 --- /dev/null +++ b/core/src/test/kotlin/format/GFMFormatTest.kt @@ -0,0 +1,28 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.GFMFormatService +import org.jetbrains.dokka.KotlinLanguageService +import org.junit.Test + +class GFMFormatTest : FileGeneratorTestCase() { + override val formatService = GFMFormatService(fileGenerator, KotlinLanguageService(), listOf()) + + @Test + fun sample() { + verifyGFMNodeByName("sample", "Foo") + } + + @Test + fun listInTableCell() { + verifyGFMNodeByName("listInTableCell", "Foo") + } + + private fun verifyGFMNodeByName(fileName: String, name: String) { + verifyOutput("testdata/format/gfm/$fileName.kt", ".md") { model, output -> + buildPagesAndReadInto( + model.members.single().members.filter { it.name == name }, + output + ) + } + } +} diff --git a/core/src/test/kotlin/format/HtmlFormatTest.kt b/core/src/test/kotlin/format/HtmlFormatTest.kt new file mode 100644 index 000000000..01e9b3c5f --- /dev/null +++ b/core/src/test/kotlin/format/HtmlFormatTest.kt @@ -0,0 +1,182 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.* +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.junit.Test +import java.io.File + +// TODO: add tests back +class HtmlFormatTest: FileGeneratorTestCase() { + override val formatService = HtmlFormatService(fileGenerator, KotlinLanguageService(), HtmlTemplateService.default(), listOf()) + + @Test fun classWithCompanionObject() { + verifyHtmlNode("classWithCompanionObject") + } + + @Test fun htmlEscaping() { + verifyHtmlNode("htmlEscaping") + } + + @Test fun overloads() { + verifyHtmlNodes("overloads") { model -> model.members } + } + + @Test fun overloadsWithDescription() { + verifyHtmlNode("overloadsWithDescription") + } + + @Test fun overloadsWithDifferentDescriptions() { + verifyHtmlNode("overloadsWithDifferentDescriptions") + } + + @Test fun deprecated() { + verifyOutput("testdata/format/deprecated.kt", ".package.html") { model, output -> + buildPagesAndReadInto(model.members, output) + } + verifyOutput("testdata/format/deprecated.kt", ".class.html") { model, output -> + buildPagesAndReadInto(model.members.single().members, output) + } + } + + @Test fun brokenLink() { + verifyHtmlNode("brokenLink") + } + + @Test fun codeSpan() { + verifyHtmlNode("codeSpan") + } + + @Test fun parenthesis() { + verifyHtmlNode("parenthesis") + } + + @Test fun bracket() { + verifyHtmlNode("bracket") + } + + @Test fun see() { + verifyHtmlNode("see") + } + + @Test fun tripleBackticks() { + verifyHtmlNode("tripleBackticks") + } + + @Test fun typeLink() { + verifyHtmlNodes("typeLink") { model -> model.members.single().members.filter { it.name == "Bar" } } + } + + @Test fun parameterAnchor() { + verifyHtmlNode("parameterAnchor") + } + + @Test fun javaSupertypeLink() { + verifyJavaHtmlNodes("JavaSupertype") { model -> + model.members.single().members.single { it.name == "JavaSupertype" }.members.filter { it.name == "Bar" } + } + } + + @Test fun codeBlock() { + verifyHtmlNode("codeBlock") + } + + @Test fun javaLinkTag() { + verifyJavaHtmlNode("javaLinkTag") + } + + @Test fun javaLinkTagWithLabel() { + verifyJavaHtmlNode("javaLinkTagWithLabel") + } + + @Test fun javaSeeTag() { + verifyJavaHtmlNode("javaSeeTag") + } + + @Test fun javaDeprecated() { + verifyJavaHtmlNodes("javaDeprecated") { model -> + model.members.single().members.single { it.name == "Foo" }.members.filter { it.name == "foo" } + } + } + + @Test fun crossLanguageKotlinExtendsJava() { + verifyOutput(arrayOf( + KotlinSourceRoot("testdata/format/crossLanguage/kotlinExtendsJava/Bar.kt", false), + JavaSourceRoot(File("testdata/format/crossLanguage/kotlinExtendsJava"), null)), + ".html") { model, output -> + buildPagesAndReadInto( + model.members.single().members.filter { it.name == "Bar" }, + output + ) + } + } + + @Test fun orderedList() { + verifyHtmlNodes("orderedList") { model -> model.members.single().members.filter { it.name == "Bar" } } + } + + @Test fun linkWithLabel() { + verifyHtmlNodes("linkWithLabel") { model -> model.members.single().members.filter { it.name == "Bar" } } + } + + @Test fun entity() { + verifyHtmlNodes("entity") { model -> model.members.single().members.filter { it.name == "Bar" } } + } + + @Test fun uninterpretedEmphasisCharacters() { + verifyHtmlNode("uninterpretedEmphasisCharacters") + } + + @Test fun markdownInLinks() { + verifyHtmlNode("markdownInLinks") + } + + @Test fun returnWithLink() { + verifyHtmlNode("returnWithLink") + } + + @Test fun linkWithStarProjection() { + verifyHtmlNode("linkWithStarProjection", withKotlinRuntime = true) + } + + @Test fun functionalTypeWithNamedParameters() { + verifyHtmlNode("functionalTypeWithNamedParameters") + } + + @Test fun sinceKotlin() { + verifyHtmlNode("sinceKotlin") + } + + @Test fun blankLineInsideCodeBlock() { + verifyHtmlNode("blankLineInsideCodeBlock") + } + + @Test fun indentedCodeBlock() { + verifyHtmlNode("indentedCodeBlock") + } + + private fun verifyHtmlNode(fileName: String, withKotlinRuntime: Boolean = false) { + verifyHtmlNodes(fileName, withKotlinRuntime) { model -> model.members.single().members } + } + + private fun verifyHtmlNodes(fileName: String, + withKotlinRuntime: Boolean = false, + nodeFilter: (DocumentationModule) -> List<DocumentationNode>) { + verifyOutput("testdata/format/$fileName.kt", ".html", withKotlinRuntime = withKotlinRuntime) { model, output -> + buildPagesAndReadInto(nodeFilter(model), output) + } + } + + private fun verifyJavaHtmlNode(fileName: String, withKotlinRuntime: Boolean = false) { + verifyJavaHtmlNodes(fileName, withKotlinRuntime) { model -> model.members.single().members } + } + + private fun verifyJavaHtmlNodes(fileName: String, + withKotlinRuntime: Boolean = false, + nodeFilter: (DocumentationModule) -> List<DocumentationNode>) { + verifyJavaOutput("testdata/format/$fileName.java", ".html", withKotlinRuntime = withKotlinRuntime) { model, output -> + buildPagesAndReadInto(nodeFilter(model), output) + } + } +} + diff --git a/core/src/test/kotlin/format/JavaLayoutHtmlFormatTest.kt b/core/src/test/kotlin/format/JavaLayoutHtmlFormatTest.kt new file mode 100644 index 000000000..59746b10f --- /dev/null +++ b/core/src/test/kotlin/format/JavaLayoutHtmlFormatTest.kt @@ -0,0 +1,114 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatDescriptor +import org.junit.Test +import java.io.File +import java.net.URL + +class JavaLayoutHtmlFormatTest : JavaLayoutHtmlFormatTestCase() { + override val formatDescriptor = JavaLayoutHtmlFormatDescriptor() + +// @Test +// fun simple() { +// verifyNode("simple.kt") +// } +// +//// @Test +//// fun topLevel() { +//// verifyPackageNode("topLevel.kt") +//// } +// +// @Test +// fun codeBlocks() { +// verifyNode("codeBlocks.kt") { model -> +// listOf(model.members.single().members.single { it.name == "foo" }) +// } +// } +// +// @Test +// fun const() { +// verifyPackageNode("const.kt", noStdlibLink = true) +// verifyNode("const.kt", noStdlibLink = true) { model -> +// model.members.single().members.filter { it.kind in NodeKind.classLike } +// } +// } +// +// @Test +// fun externalClassExtension() { +// verifyPackageNode("externalClassExtension.kt") +// } +// +// @Test +// fun unresolvedExternalClass() { +// verifyNode("unresolvedExternalClass.kt", noStdlibLink = true) { model -> +// listOf(model.members.single().members.single { it.name == "MyException" }) +// } +// } +// +// @Test +// fun genericExtension() { +// verifyNode("genericExtension.kt", noStdlibLink = true) { model -> +// model.members.single().members(NodeKind.Class) +// } +// } +// +// +// @Test +// fun sections() { +// verifyNode("sections.kt", noStdlibLink = true) { model -> +// model.members.single().members.filter { it.name == "sectionsTest" } +// } +// } +// +// @Test +// fun constJava() { +// verifyNode("ConstJava.java", noStdlibLink = true) +// } +// +// @Test +// fun inboundLinksInKotlinMode() { +// val root = "./testdata/format/java-layout-html" +// +// val options = DocumentationOptions( +// "", +// "java-layout-html", +// sourceLinks = listOf(), +// generateClassIndexPage = false, +// generatePackageIndexPage = false, +// noStdlibLink = true, +// apiVersion = null, +// languageVersion = null, +// perPackageOptions = listOf(PackageOptionsImpl("foo", suppress = true)), +// externalDocumentationLinks = +// listOf( +// DokkaConfiguration.ExternalDocumentationLink.Builder( +// URL("file:///"), +// File(root, "inboundLinksTestPackageList").toURI().toURL() +// ).build() +// ) +// ) +// +// +// val sourcePath = "$root/inboundLinksInKotlinMode.kt" +// val documentation = DocumentationModule("test") +// +// appendDocumentation( +// documentation, +// contentRootFromPath(sourcePath), +// contentRootFromPath("$root/inboundLinksInKotlinMode.Dep.kt"), +// withJdk = false, +// withKotlinRuntime = false, +// options = options +// ) +// documentation.prepareForGeneration(options) +// +// verifyModelOutput(documentation, ".html", sourcePath) { model, output -> +// buildPagesAndReadInto( +// model, +// model.members.single { it.name == "bar" }.members, +// output +// ) +// } +// } +}
\ No newline at end of file diff --git a/core/src/test/kotlin/format/JavaLayoutHtmlFormatTestCase.kt b/core/src/test/kotlin/format/JavaLayoutHtmlFormatTestCase.kt new file mode 100644 index 000000000..620f10dda --- /dev/null +++ b/core/src/test/kotlin/format/JavaLayoutHtmlFormatTestCase.kt @@ -0,0 +1,117 @@ +package org.jetbrains.dokka.tests + +import com.google.inject.Guice +import com.google.inject.Injector +import com.google.inject.Module +import com.google.inject.name.Names +import org.jetbrains.dokka.DocumentationNode +import org.jetbrains.dokka.DocumentationOptions +import org.jetbrains.dokka.DokkaLogger +import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatDescriptorBase +import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatGenerator +import org.jetbrains.dokka.Generator +import org.jetbrains.dokka.Utilities.bind +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import java.io.File +import java.net.URI + +abstract class JavaLayoutHtmlFormatTestCase { + + abstract val formatDescriptor: JavaLayoutHtmlFormatDescriptorBase + + @get:Rule + var folder = TemporaryFolder() + + var options = + DocumentationOptions( + "", + "java-layout-html", + apiVersion = null, + languageVersion = null, + generateClassIndexPage = false, + generatePackageIndexPage = false, + noStdlibLink = false, + noJdkLink = false, + collectInheritedExtensionsFromLibraries = true + ) + + val injector: Injector by lazy { + Guice.createInjector(Module { binder -> + binder.bind<File>().annotatedWith(Names.named("outputDir")).toInstance(folder.apply { create() }.root) + + binder.bind<DocumentationOptions>().toProvider { options } + binder.bind<DokkaLogger>().toInstance(object : DokkaLogger { + override fun info(message: String) { + println(message) + } + + override fun warn(message: String) { + println("WARN: $message") + } + + override fun error(message: String) { + println("ERROR: $message") + } + + }) + + formatDescriptor.configureOutput(binder) + }) + } + + + protected fun buildPagesAndReadInto(model: DocumentationNode, nodes: List<DocumentationNode>, sb: StringBuilder) = + with(injector.getInstance(Generator::class.java)) { + this as JavaLayoutHtmlFormatGenerator + buildPages(listOf(model)) + val byLocations = nodes.groupBy { mainUri(it) } + byLocations.forEach { (loc, _) -> + sb.appendln("<!-- File: $loc -->") + sb.append(folder.root.toURI().resolve(URI("/").relativize(loc)).toURL().readText()) + } + } + + + protected fun verifyNode( + fileName: String, + noStdlibLink: Boolean = false, + fileExtension: String = ".html", + select: (model: DocumentationNode) -> List<DocumentationNode> + ) { + verifyOutput( + "testdata/format/java-layout-html/$fileName", + fileExtension, + format = "java-layout-html", + withKotlinRuntime = true, + noStdlibLink = noStdlibLink, + collectInheritedExtensionsFromLibraries = true + ) { model, output -> + buildPagesAndReadInto( + model, + select(model), + output + ) + } + } + + protected fun verifyNode(fileName: String, noStdlibLink: Boolean = false) { + verifyNode(fileName, noStdlibLink) { model -> listOf(model.members.single().members.single()) } + } + + protected fun verifyPackageNode(fileName: String, noStdlibLink: Boolean = false) { + verifyOutput( + "testdata/format/java-layout-html/$fileName", + ".package-summary.html", + format = "java-layout-html", + withKotlinRuntime = true, + noStdlibLink = noStdlibLink + ) { model, output -> + buildPagesAndReadInto( + model, + listOf(model.members.single()), + output + ) + } + } +}
\ No newline at end of file diff --git a/core/src/test/kotlin/format/KotlinWebSiteFormatTest.kt b/core/src/test/kotlin/format/KotlinWebSiteFormatTest.kt new file mode 100644 index 000000000..01ac58da4 --- /dev/null +++ b/core/src/test/kotlin/format/KotlinWebSiteFormatTest.kt @@ -0,0 +1,74 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.* +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +@Ignore +class KotlinWebSiteFormatTest: FileGeneratorTestCase() { + override val formatService = KotlinWebsiteFormatService(fileGenerator, KotlinLanguageService(), listOf(), DokkaConsoleLogger) + + @Test fun sample() { + verifyKWSNodeByName("sample", "foo") + } + + @Test fun returnTag() { + verifyKWSNodeByName("returnTag", "indexOf") + } + + @Test fun overloadGroup() { + verifyKWSNodeByName("overloadGroup", "magic") + } + + @Test fun dataTags() { + val module = buildMultiplePlatforms("dataTags") + verifyMultiplatformPackage(module, "dataTags") + } + + @Test fun dataTagsInGroupNode() { + val path = "dataTagsInGroupNode" + val module = buildMultiplePlatforms(path) + verifyModelOutput(module, ".md", "testdata/format/website/$path/multiplatform.kt") { model, output -> + buildPagesAndReadInto( + listOfNotNull(model.members.single().members.find { it.kind == NodeKind.GroupNode }), + output + ) + } + verifyMultiplatformPackage(module, path) + } + + private fun verifyKWSNodeByName(fileName: String, name: String) { + verifyOutput("testdata/format/website/$fileName.kt", ".md", format = "kotlin-website") { model, output -> + buildPagesAndReadInto( + model.members.single().members.filter { it.name == name }, + output + ) + } + } + + private fun buildMultiplePlatforms(path: String): DocumentationModule { + val module = DocumentationModule("test") + val options = DocumentationOptions( + outputDir = "", + outputFormat = "html", + generateClassIndexPage = false, + generatePackageIndexPage = false, + noStdlibLink = true, + noJdkLink = true, + languageVersion = null, + apiVersion = null + ) + appendDocumentation(module, contentRootFromPath("testdata/format/website/$path/jvm.kt"), defaultPlatforms = listOf("JVM"), options = options) + appendDocumentation(module, contentRootFromPath("testdata/format/website/$path/jre7.kt"), defaultPlatforms = listOf("JVM", "JRE7"), options = options) + appendDocumentation(module, contentRootFromPath("testdata/format/website/$path/js.kt"), defaultPlatforms = listOf("JS"), options = options) + return module + } + + private fun verifyMultiplatformPackage(module: DocumentationModule, path: String) { + verifyModelOutput(module, ".package.md", "testdata/format/website/$path/multiplatform.kt") { model, output -> + buildPagesAndReadInto(model.members, output) + } + } + +} diff --git a/core/src/test/kotlin/format/KotlinWebSiteHtmlFormatTest.kt b/core/src/test/kotlin/format/KotlinWebSiteHtmlFormatTest.kt new file mode 100644 index 000000000..63d7d5766 --- /dev/null +++ b/core/src/test/kotlin/format/KotlinWebSiteHtmlFormatTest.kt @@ -0,0 +1,85 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.* +import org.junit.Before +import org.junit.Test + +class KotlinWebSiteHtmlFormatTest: FileGeneratorTestCase() { + override val formatService = KotlinWebsiteHtmlFormatService(fileGenerator, KotlinLanguageService(), listOf(), EmptyHtmlTemplateService) + + @Test fun dropImport() { + verifyKWSNodeByName("dropImport", "foo") + } + + @Test fun sample() { + verifyKWSNodeByName("sample", "foo") + } + + @Test fun sampleWithAsserts() { + verifyKWSNodeByName("sampleWithAsserts", "a") + } + + @Test fun newLinesInSamples() { + verifyKWSNodeByName("newLinesInSamples", "foo") + } + + @Test fun newLinesInImportList() { + verifyKWSNodeByName("newLinesInImportList", "foo") + } + + @Test fun returnTag() { + verifyKWSNodeByName("returnTag", "indexOf") + } + + @Test fun overloadGroup() { + verifyKWSNodeByName("overloadGroup", "magic") + } + + @Test fun dataTags() { + val module = buildMultiplePlatforms("dataTags") + verifyMultiplatformPackage(module, "dataTags") + } + + @Test fun dataTagsInGroupNode() { + val path = "dataTagsInGroupNode" + val module = buildMultiplePlatforms(path) + verifyModelOutput(module, ".html", "testdata/format/website-html/$path/multiplatform.kt") { model, output -> + buildPagesAndReadInto( + listOfNotNull(model.members.single().members.find { it.kind == NodeKind.GroupNode }), + output + ) + } + verifyMultiplatformPackage(module, path) + } + + private fun verifyKWSNodeByName(fileName: String, name: String) { + verifyOutput("testdata/format/website-html/$fileName.kt", ".html", format = "kotlin-website-html") { model, output -> + buildPagesAndReadInto(model.members.single().members.filter { it.name == name }, output) + } + } + + private fun buildMultiplePlatforms(path: String): DocumentationModule { + val module = DocumentationModule("test") + val options = DocumentationOptions( + outputDir = "", + outputFormat = "kotlin-website-html", + generateClassIndexPage = false, + generatePackageIndexPage = false, + noStdlibLink = true, + noJdkLink = true, + languageVersion = null, + apiVersion = null + ) + appendDocumentation(module, contentRootFromPath("testdata/format/website-html/$path/jvm.kt"), defaultPlatforms = listOf("JVM"), options = options) + appendDocumentation(module, contentRootFromPath("testdata/format/website-html/$path/jre7.kt"), defaultPlatforms = listOf("JVM", "JRE7"), options = options) + appendDocumentation(module, contentRootFromPath("testdata/format/website-html/$path/js.kt"), defaultPlatforms = listOf("JS"), options = options) + return module + } + + private fun verifyMultiplatformPackage(module: DocumentationModule, path: String) { + verifyModelOutput(module, ".package.html", "testdata/format/website-html/$path/multiplatform.kt") { model, output -> + buildPagesAndReadInto(model.members, output) + } + } + +} diff --git a/core/src/test/kotlin/format/KotlinWebSiteRunnableSamplesFormatTest.kt b/core/src/test/kotlin/format/KotlinWebSiteRunnableSamplesFormatTest.kt new file mode 100644 index 000000000..453b1de85 --- /dev/null +++ b/core/src/test/kotlin/format/KotlinWebSiteRunnableSamplesFormatTest.kt @@ -0,0 +1,39 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.DokkaConsoleLogger +import org.jetbrains.dokka.KotlinLanguageService +import org.jetbrains.dokka.KotlinWebsiteRunnableSamplesFormatService +import org.junit.Ignore +import org.junit.Test + +@Ignore +class KotlinWebSiteRunnableSamplesFormatTest { +// private val kwsService = KotlinWebsiteRunnableSamplesFormatService(InMemoryLocationService, KotlinLanguageService(), listOf(), DokkaConsoleLogger) +// +// +// @Test fun dropImport() { +// verifyKWSNodeByName("dropImport", "foo") +// } +// +// @Test fun sample() { +// verifyKWSNodeByName("sample", "foo") +// } +// +// @Test fun sampleWithAsserts() { +// verifyKWSNodeByName("sampleWithAsserts", "a") +// } +// +// @Test fun newLinesInSamples() { +// verifyKWSNodeByName("newLinesInSamples", "foo") +// } +// +// @Test fun newLinesInImportList() { +// verifyKWSNodeByName("newLinesInImportList", "foo") +// } +// +// private fun verifyKWSNodeByName(fileName: String, name: String) { +// verifyOutput("testdata/format/website-samples/$fileName.kt", ".md", format = "kotlin-website-samples") { model, output -> +// kwsService.createOutputBuilder(output, tempLocation).appendNodes(model.members.single().members.filter { it.name == name }) +// } +// } +} diff --git a/core/src/test/kotlin/format/MarkdownFormatTest.kt b/core/src/test/kotlin/format/MarkdownFormatTest.kt new file mode 100644 index 000000000..08d467995 --- /dev/null +++ b/core/src/test/kotlin/format/MarkdownFormatTest.kt @@ -0,0 +1,547 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.* +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +class MarkdownFormatTest: FileGeneratorTestCase() { + override val formatService = MarkdownFormatService(fileGenerator, KotlinLanguageService(), listOf()) + + @Test fun emptyDescription() { + verifyMarkdownNode("emptyDescription") + } + + @Test fun classWithCompanionObject() { + verifyMarkdownNode("classWithCompanionObject") + } + + @Test fun annotations() { + verifyMarkdownNode("annotations") + } + + @Test fun annotationClass() { + verifyMarkdownNode("annotationClass", withKotlinRuntime = true) + verifyMarkdownPackage("annotationClass", withKotlinRuntime = true) + } + + @Test fun exceptionClass() { + verifyMarkdownNode("exceptionClass", withKotlinRuntime = true) + verifyMarkdownPackage("exceptionClass", withKotlinRuntime = true) + } + + @Test fun annotationParams() { + verifyMarkdownNode("annotationParams", withKotlinRuntime = true) + } + + @Test fun extensions() { + verifyOutput("testdata/format/extensions.kt", ".package.md") { model, output -> + buildPagesAndReadInto(model.members, output) + } + verifyOutput("testdata/format/extensions.kt", ".class.md") { model, output -> + buildPagesAndReadInto(model.members.single().members, output) + } + } + + @Test fun enumClass() { + verifyOutput("testdata/format/enumClass.kt", ".md") { model, output -> + buildPagesAndReadInto(model.members.single().members, output) + } + verifyOutput("testdata/format/enumClass.kt", ".value.md") { model, output -> + val enumClassNode = model.members.single().members[0] + buildPagesAndReadInto( + enumClassNode.members.filter { it.name == "LOCAL_CONTINUE_AND_BREAK" }, + output + ) + } + } + + @Test fun varargsFunction() { + verifyMarkdownNode("varargsFunction") + } + + @Test fun overridingFunction() { + verifyMarkdownNodes("overridingFunction") { model-> + val classMembers = model.members.single().members.first { it.name == "D" }.members + classMembers.filter { it.name == "f" } + } + } + + @Test fun propertyVar() { + verifyMarkdownNode("propertyVar") + } + + @Test fun functionWithDefaultParameter() { + verifyMarkdownNode("functionWithDefaultParameter") + } + + @Test fun accessor() { + verifyMarkdownNodes("accessor") { model -> + model.members.single().members.first { it.name == "C" }.members.filter { it.name == "x" } + } + } + + @Test fun paramTag() { + verifyMarkdownNode("paramTag") + } + + @Test fun throwsTag() { + verifyMarkdownNode("throwsTag") + } + + @Test fun typeParameterBounds() { + verifyMarkdownNode("typeParameterBounds") + } + + @Test fun typeParameterVariance() { + verifyMarkdownNode("typeParameterVariance") + } + + @Test fun typeProjectionVariance() { + verifyMarkdownNode("typeProjectionVariance") + } + + @Test + fun javadocCodeMultiline() { + verifyJavaMarkdownNode("javadocCodeMultiline") + } + + // TODO: FIXME + @Ignore + @Test + fun javadocHtml() { + verifyJavaMarkdownNode("javadocHtml") + } + + // TODO: FIXME + @Ignore + @Test + fun javaCodeLiteralTags() { + verifyJavaMarkdownNode("javaCodeLiteralTags") + } + + @Test + fun javaSample() { + verifyJavaMarkdownNode("javaSample") + } + + // TODO: FIXME + @Ignore + @Test + fun javaCodeInParam() { + verifyJavaMarkdownNode("javaCodeInParam") + } + + @Test fun javaSpaceInAuthor() { + verifyJavaMarkdownNode("javaSpaceInAuthor") + } + + @Test fun nullability() { + verifyMarkdownNode("nullability") + } + + @Test fun operatorOverloading() { + verifyMarkdownNodes("operatorOverloading") { model-> + model.members.single().members.single { it.name == "C" }.members.filter { it.name == "plus" } + } + } + + @Test fun javadocOrderedList() { + verifyJavaMarkdownNodes("javadocOrderedList") { model -> + model.members.single().members.filter { it.name == "Bar" } + } + } + + @Test fun codeBlockNoHtmlEscape() { + verifyMarkdownNodeByName("codeBlockNoHtmlEscape", "hackTheArithmetic") + } + + @Test fun companionObjectExtension() { + verifyMarkdownNodeByName("companionObjectExtension", "Foo") + } + + @Test fun starProjection() { + verifyMarkdownNode("starProjection") + } + + @Test fun extensionFunctionParameter() { + verifyMarkdownNode("extensionFunctionParameter") + } + + @Test fun summarizeSignatures() { + verifyMarkdownNodes("summarizeSignatures") { model -> model.members } + } + + @Test fun summarizeSignaturesProperty() { + verifyMarkdownNodes("summarizeSignaturesProperty") { model -> model.members } + } + + @Test fun reifiedTypeParameter() { + verifyMarkdownNode("reifiedTypeParameter", withKotlinRuntime = true) + } + + @Test fun annotatedTypeParameter() { + verifyMarkdownNode("annotatedTypeParameter", withKotlinRuntime = true) + } + + @Test fun inheritedMembers() { + verifyMarkdownNodeByName("inheritedMembers", "Bar") + } + + @Test fun inheritedExtensions() { + verifyMarkdownNodeByName("inheritedExtensions", "Bar") + } + + @Test fun genericInheritedExtensions() { + verifyMarkdownNodeByName("genericInheritedExtensions", "Bar") + } + + @Test fun arrayAverage() { + verifyMarkdownNodeByName("arrayAverage", "XArray") + } + + @Test fun multipleTypeParameterConstraints() { + verifyMarkdownNode("multipleTypeParameterConstraints", withKotlinRuntime = true) + } + + @Test fun inheritedCompanionObjectProperties() { + verifyMarkdownNodeByName("inheritedCompanionObjectProperties", "C") + } + + @Test fun shadowedExtensionFunctions() { + verifyMarkdownNodeByName("shadowedExtensionFunctions", "Bar") + } + + @Test fun inapplicableExtensionFunctions() { + verifyMarkdownNodeByName("inapplicableExtensionFunctions", "Bar") + } + + @Test fun receiverParameterTypeBound() { + verifyMarkdownNodeByName("receiverParameterTypeBound", "Foo") + } + + @Test fun extensionWithDocumentedReceiver() { + verifyMarkdownNodes("extensionWithDocumentedReceiver") { model -> + model.members.single().members.single().members.filter { it.name == "fn" } + } + } + + @Test fun codeBlock() { + verifyMarkdownNode("codeBlock") + } + + @Test fun exclInCodeBlock() { + verifyMarkdownNodeByName("exclInCodeBlock", "foo") + } + + @Test fun backtickInCodeBlock() { + verifyMarkdownNodeByName("backtickInCodeBlock", "foo") + } + + @Test fun qualifiedNameLink() { + verifyMarkdownNodeByName("qualifiedNameLink", "foo", withKotlinRuntime = true) + } + + @Test fun functionalTypeWithNamedParameters() { + verifyMarkdownNode("functionalTypeWithNamedParameters") + } + + @Test fun typeAliases() { + verifyMarkdownNode("typeAliases") + verifyMarkdownPackage("typeAliases") + } + + @Test fun sampleByFQName() { + verifyMarkdownNode("sampleByFQName") + } + + @Test fun sampleByShortName() { + verifyMarkdownNode("sampleByShortName") + } + + + @Test fun suspendParam() { + verifyMarkdownNode("suspendParam") + verifyMarkdownPackage("suspendParam") + } + + @Test fun sinceKotlin() { + verifyMarkdownNode("sinceKotlin") + verifyMarkdownPackage("sinceKotlin") + } + + @Test fun sinceKotlinWide() { + verifyMarkdownPackage("sinceKotlinWide") + } + + @Test fun dynamicType() { + verifyMarkdownNode("dynamicType") + } + + @Test fun dynamicExtension() { + verifyMarkdownNodes("dynamicExtension") { model -> model.members.single().members.filter { it.name == "Foo" } } + } + + @Test fun memberExtension() { + verifyMarkdownNodes("memberExtension") { model -> model.members.single().members.filter { it.name == "Foo" } } + } + + @Test fun renderFunctionalTypeInParenthesisWhenItIsReceiver() { + verifyMarkdownNode("renderFunctionalTypeInParenthesisWhenItIsReceiver") + } + + @Test fun multiplePlatforms() { + verifyMultiplatformPackage(buildMultiplePlatforms("multiplatform/simple"), "multiplatform/simple") + } + + @Test fun multiplePlatformsMerge() { + verifyMultiplatformPackage(buildMultiplePlatforms("multiplatform/merge"), "multiplatform/merge") + } + + @Test fun multiplePlatformsMergeMembers() { + val module = buildMultiplePlatforms("multiplatform/mergeMembers") + verifyModelOutput(module, ".md", "testdata/format/multiplatform/mergeMembers/foo.kt") { model, output -> + buildPagesAndReadInto(model.members.single().members, output) + } + } + + @Test fun multiplePlatformsOmitRedundant() { + val module = buildMultiplePlatforms("multiplatform/omitRedundant") + verifyModelOutput(module, ".md", "testdata/format/multiplatform/omitRedundant/foo.kt") { model, output -> + buildPagesAndReadInto(model.members.single().members, output) + } + } + + @Test fun multiplePlatformsImplied() { + val module = buildMultiplePlatforms("multiplatform/implied") + verifyModelOutput(module, ".md", "testdata/format/multiplatform/implied/foo.kt") { model, output -> + val service = MarkdownFormatService(fileGenerator, KotlinLanguageService(), listOf("JVM", "JS")) + fileGenerator.formatService = service + buildPagesAndReadInto(model.members.single().members, output) + } + } + + @Test fun packagePlatformsWithExtExtensions() { + val path = "multiplatform/packagePlatformsWithExtExtensions" + val module = DocumentationModule("test") + val options = DocumentationOptions( + outputDir = "", + outputFormat = "html", + generateClassIndexPage = false, + generatePackageIndexPage = false, + noStdlibLink = true, + noJdkLink = true, + languageVersion = null, + apiVersion = null + ) + appendDocumentation(module, contentRootFromPath("testdata/format/$path/jvm.kt"), defaultPlatforms = listOf("JVM"), withKotlinRuntime = true, options = options) + verifyMultiplatformIndex(module, path) + verifyMultiplatformPackage(module, path) + } + + @Test fun multiplePlatformsPackagePlatformFromMembers() { + val path = "multiplatform/packagePlatformsFromMembers" + val module = buildMultiplePlatforms(path) + verifyMultiplatformIndex(module, path) + verifyMultiplatformPackage(module, path) + } + + @Test fun multiplePlatformsGroupNode() { + val path = "multiplatform/groupNode" + val module = buildMultiplePlatforms(path) + verifyModelOutput(module, ".md", "testdata/format/$path/multiplatform.kt") { model, output -> + buildPagesAndReadInto( + listOfNotNull(model.members.single().members.find { it.kind == NodeKind.GroupNode }), + output + ) + } + verifyMultiplatformPackage(module, path) + } + + @Test fun multiplePlatformsBreadcrumbsInMemberOfMemberOfGroupNode() { + val path = "multiplatform/breadcrumbsInMemberOfMemberOfGroupNode" + val module = buildMultiplePlatforms(path) + verifyModelOutput(module, ".md", "testdata/format/$path/multiplatform.kt") { model, output -> + buildPagesAndReadInto( + listOfNotNull(model.members.single().members.find { it.kind == NodeKind.GroupNode }?.member(NodeKind.Class)?.member(NodeKind.Function)), + output + ) + } + } + + @Test fun linksInEmphasis() { + verifyMarkdownNode("linksInEmphasis") + } + + @Test fun linksInStrong() { + verifyMarkdownNode("linksInStrong") + } + + @Test fun linksInHeaders() { + verifyMarkdownNode("linksInHeaders") + } + + @Test fun tokensInEmphasis() { + verifyMarkdownNode("tokensInEmphasis") + } + + @Test fun tokensInStrong() { + verifyMarkdownNode("tokensInStrong") + } + + @Test fun tokensInHeaders() { + verifyMarkdownNode("tokensInHeaders") + } + + @Test fun unorderedLists() { + verifyMarkdownNode("unorderedLists") + } + + @Test fun nestedLists() { + verifyMarkdownNode("nestedLists") + } + + @Test fun referenceLink() { + verifyMarkdownNode("referenceLink") + } + + @Test fun externalReferenceLink() { + verifyMarkdownNode("externalReferenceLink") + } + + @Test fun newlineInTableCell() { + verifyMarkdownPackage("newlineInTableCell") + } + + @Test fun indentedCodeBlock() { + verifyMarkdownNode("indentedCodeBlock") + } + + @Test fun receiverReference() { + verifyMarkdownNode("receiverReference") + } + + @Test fun extensionScope() { + verifyMarkdownNodeByName("extensionScope", "test") + } + + @Test fun typeParameterReference() { + verifyMarkdownNode("typeParameterReference") + } + + @Test fun notPublishedTypeAliasAutoExpansion() { + verifyMarkdownNodeByName("notPublishedTypeAliasAutoExpansion", "foo", includeNonPublic = false) + } + + @Test fun companionImplements() { + verifyMarkdownNodeByName("companionImplements", "Foo") + } + + @Test fun enumRef() { + verifyMarkdownNode("enumRef") + } + + @Test fun inheritedLink() { + val filePath = "testdata/format/inheritedLink" + verifyOutput( + arrayOf( + contentRootFromPath("$filePath.kt"), + contentRootFromPath("$filePath.1.kt") + ), + ".md", + withJdk = true, + withKotlinRuntime = true, + includeNonPublic = false + ) { model, output -> + buildPagesAndReadInto(model.members.single { it.name == "p2" }.members.single().members, output) + } + } + + + private fun buildMultiplePlatforms(path: String): DocumentationModule { + val module = DocumentationModule("test") + val options = DocumentationOptions( + outputDir = "", + outputFormat = "html", + generateClassIndexPage = false, + generatePackageIndexPage = false, + noStdlibLink = true, + noJdkLink = true, + languageVersion = null, + apiVersion = null + ) + appendDocumentation(module, contentRootFromPath("testdata/format/$path/jvm.kt"), defaultPlatforms = listOf("JVM"), options = options) + appendDocumentation(module, contentRootFromPath("testdata/format/$path/js.kt"), defaultPlatforms = listOf("JS"), options = options) + return module + } + + private fun verifyMultiplatformPackage(module: DocumentationModule, path: String) { + verifyModelOutput(module, ".package.md", "testdata/format/$path/multiplatform.kt") { model, output -> + buildPagesAndReadInto(model.members, output) + } + } + + private fun verifyMultiplatformIndex(module: DocumentationModule, path: String) { + verifyModelOutput(module, ".md", "testdata/format/$path/multiplatform.index.kt") { + model, output -> + val service = MarkdownFormatService(fileGenerator, KotlinLanguageService(), listOf()) + fileGenerator.formatService = service + buildPagesAndReadInto(listOf(model), output) + } + } + + @Test fun blankLineInsideCodeBlock() { + verifyMarkdownNode("blankLineInsideCodeBlock") + } + + private fun verifyMarkdownPackage(fileName: String, withKotlinRuntime: Boolean = false) { + verifyOutput("testdata/format/$fileName.kt", ".package.md", withKotlinRuntime = withKotlinRuntime) { model, output -> + buildPagesAndReadInto(model.members, output) + } + } + + private fun verifyMarkdownNode(fileName: String, withKotlinRuntime: Boolean = false) { + verifyMarkdownNodes(fileName, withKotlinRuntime) { model -> model.members.single().members } + } + + private fun verifyMarkdownNodes( + fileName: String, + withKotlinRuntime: Boolean = false, + includeNonPublic: Boolean = true, + nodeFilter: (DocumentationModule) -> List<DocumentationNode> + ) { + verifyOutput( + "testdata/format/$fileName.kt", + ".md", + withKotlinRuntime = withKotlinRuntime, + includeNonPublic = includeNonPublic + ) { model, output -> + buildPagesAndReadInto(nodeFilter(model), output) + } + } + + private fun verifyJavaMarkdownNode(fileName: String, withKotlinRuntime: Boolean = false) { + verifyJavaMarkdownNodes(fileName, withKotlinRuntime) { model -> model.members.single().members } + } + + private fun verifyJavaMarkdownNodes(fileName: String, withKotlinRuntime: Boolean = false, nodeFilter: (DocumentationModule) -> List<DocumentationNode>) { + verifyJavaOutput("testdata/format/$fileName.java", ".md", withKotlinRuntime = withKotlinRuntime) { model, output -> + buildPagesAndReadInto(nodeFilter(model), output) + } + } + + private fun verifyMarkdownNodeByName( + fileName: String, + name: String, + withKotlinRuntime: Boolean = false, + includeNonPublic: Boolean = true + ) { + verifyMarkdownNodes(fileName, withKotlinRuntime, includeNonPublic) { model-> + val nodesWithName = model.members.single().members.filter { it.name == name } + if (nodesWithName.isEmpty()) { + throw IllegalArgumentException("Found no nodes named $name") + } + nodesWithName + } + } +} diff --git a/core/src/test/kotlin/format/PackageDocsTest.kt b/core/src/test/kotlin/format/PackageDocsTest.kt new file mode 100644 index 000000000..b7fff1e2e --- /dev/null +++ b/core/src/test/kotlin/format/PackageDocsTest.kt @@ -0,0 +1,92 @@ +package org.jetbrains.dokka.tests.format + +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doAnswer +import com.nhaarman.mockito_kotlin.eq +import com.nhaarman.mockito_kotlin.mock +import org.jetbrains.dokka.* +import org.jetbrains.dokka.tests.assertEqualsIgnoringSeparators +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.io.File + +class PackageDocsTest { + + private lateinit var testDisposable: Disposable + + @Before + fun setup() { + testDisposable = Disposer.newDisposable() + } + + @After + fun cleanup() { + Disposer.dispose(testDisposable) + } + + fun createPackageDocs(linkResolver: DeclarationLinkResolver?): PackageDocs { + val environment = KotlinCoreEnvironment.createForTests(testDisposable, CompilerConfiguration.EMPTY, EnvironmentConfigFiles.JVM_CONFIG_FILES) + return PackageDocs(linkResolver, DokkaConsoleLogger, environment, mock(), mock()) + } + + @Test fun verifyParse() { + + val docs = createPackageDocs(null) + docs.parse("testdata/packagedocs/stdlib.md", emptyList()) + val packageContent = docs.packageContent["kotlin"]!! + val block = (packageContent.children.single() as ContentBlock).children.first() as ContentText + assertEquals("Core functions and types", block.text) + } + + @Test fun testReferenceLinksInPackageDocs() { + val mockLinkResolver = mock<DeclarationLinkResolver> { + val exampleCom = "http://example.com" + on { tryResolveContentLink(any(), eq(exampleCom)) } doAnswer { ContentExternalLink(exampleCom) } + } + + val mockPackageDescriptor = mock<PackageFragmentDescriptor> {} + + val docs = createPackageDocs(mockLinkResolver) + docs.parse("testdata/packagedocs/referenceLinks.md", listOf(mockPackageDescriptor)) + + checkMarkdownOutput(docs, "testdata/packagedocs/referenceLinks") + } + + fun checkMarkdownOutput(docs: PackageDocs, expectedFilePrefix: String) { + + val generator = FileGenerator(File("")) + + val out = StringBuilder() + val outputBuilder = MarkdownOutputBuilder( + out, + FileLocation(generator.root), + generator, + KotlinLanguageService(), + ".md", + emptyList() + ) + fun checkOutput(content: Content, filePostfix: String) { + outputBuilder.appendContent(content) + val expectedFile = File(expectedFilePrefix + filePostfix) + assertEqualsIgnoringSeparators(expectedFile, out.toString()) + out.setLength(0) + } + + checkOutput(docs.moduleContent, ".module.md") + + docs.packageContent.forEach { + (name, content) -> + checkOutput(content, ".$name.md") + } + + } +} diff --git a/core/src/test/kotlin/issues/IssuesTest.kt b/core/src/test/kotlin/issues/IssuesTest.kt new file mode 100644 index 000000000..d61088180 --- /dev/null +++ b/core/src/test/kotlin/issues/IssuesTest.kt @@ -0,0 +1,28 @@ +package issues + +import org.jetbrains.dokka.DocumentationNode +import org.jetbrains.dokka.NodeKind +import org.jetbrains.dokka.tests.toTestString +import org.jetbrains.dokka.tests.verifyModel +import org.junit.Test +import kotlin.test.assertEquals + + +class IssuesTest { + + @Test + fun errorClasses() { + verifyModel("testdata/issues/errorClasses.kt", withJdk = true, withKotlinRuntime = true) { model -> + val cls = model.members.single().members.single() + + fun DocumentationNode.returnType() = this.details.find { it.kind == NodeKind.Type }?.name + assertEquals("Test", cls.members[1].returnType()) + assertEquals("List", cls.members[2].returnType()) + assertEquals("Test", cls.members[3].returnType()) + assertEquals("Test", cls.members[4].returnType()) + assertEquals("String", cls.members[5].returnType()) + assertEquals("String", cls.members[6].returnType()) + assertEquals("String", cls.members[7].returnType()) + } + } +} diff --git a/core/src/test/kotlin/javadoc/JavadocTest.kt b/core/src/test/kotlin/javadoc/JavadocTest.kt new file mode 100644 index 000000000..a42d63933 --- /dev/null +++ b/core/src/test/kotlin/javadoc/JavadocTest.kt @@ -0,0 +1,185 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.Tag +import com.sun.javadoc.Type +import org.jetbrains.dokka.DokkaConsoleLogger +import org.jetbrains.dokka.tests.assertEqualsIgnoringSeparators +import org.jetbrains.dokka.tests.verifyModel +import org.junit.Assert.* +import org.junit.Test + +class JavadocTest { + @Test fun testTypes() { + verifyJavadoc("testdata/javadoc/types.kt", withJdk = true) { doc -> + val classDoc = doc.classNamed("foo.TypesKt")!! + val method = classDoc.methods().find { it.name() == "foo" }!! + + val type = method.returnType() + assertFalse(type.asClassDoc().isIncluded) + assertEquals("String", type.qualifiedTypeName()) + assertEquals("String", type.asClassDoc().qualifiedName()) + + val params = method.parameters() + assertTrue(params[0].type().isPrimitive) + assertFalse(params[1].type().asClassDoc().isIncluded) + } + } + + @Test fun testObject() { + verifyJavadoc("testdata/javadoc/obj.kt") { doc -> + val classDoc = doc.classNamed("foo.O") + assertNotNull(classDoc) + + val companionDoc = doc.classNamed("foo.O.Companion") + assertNotNull(companionDoc) + + val pkgDoc = doc.packageNamed("foo")!! + assertEquals(2, pkgDoc.allClasses().size) + } + } + + @Test fun testException() { + verifyJavadoc("testdata/javadoc/exception.kt", withKotlinRuntime = true) { doc -> + val classDoc = doc.classNamed("foo.MyException")!! + val member = classDoc.methods().find { it.name() == "foo" } + assertEquals(classDoc, member!!.containingClass()) + } + } + + @Test fun testByteArray() { + verifyJavadoc("testdata/javadoc/bytearr.kt", withKotlinRuntime = true) { doc -> + val classDoc = doc.classNamed("foo.ByteArray")!! + assertNotNull(classDoc.asClassDoc()) + + val member = classDoc.methods().find { it.name() == "foo" }!! + assertEquals("[]", member.returnType().dimension()) + } + } + + @Test fun testStringArray() { + verifyJavadoc("testdata/javadoc/stringarr.kt", withKotlinRuntime = true) { doc -> + val classDoc = doc.classNamed("foo.Foo")!! + assertNotNull(classDoc.asClassDoc()) + + val member = classDoc.methods().find { it.name() == "main" }!! + val paramType = member.parameters()[0].type() + assertNull(paramType.asParameterizedType()) + assertEquals("String", paramType.typeName()) + assertEquals("String", paramType.asClassDoc().name()) + } + } + + @Test fun testJvmName() { + verifyJavadoc("testdata/javadoc/jvmname.kt", withKotlinRuntime = true) { doc -> + val classDoc = doc.classNamed("foo.Apple")!! + assertNotNull(classDoc.asClassDoc()) + + val member = classDoc.methods().find { it.name() == "_tree" } + assertNotNull(member) + } + } + + @Test fun testLinkWithParam() { + verifyJavadoc("testdata/javadoc/paramlink.kt", withKotlinRuntime = true) { doc -> + val classDoc = doc.classNamed("demo.Apple")!! + assertNotNull(classDoc.asClassDoc()) + val tags = classDoc.inlineTags().filterIsInstance<SeeTagAdapter>() + assertEquals(2, tags.size) + val linkTag = tags[1] as SeeMethodTagAdapter + assertEquals("cutIntoPieces", linkTag.method.name()) + } + } + + @Test fun testInternalVisibility() { + verifyJavadoc("testdata/javadoc/internal.kt", withKotlinRuntime = true, includeNonPublic = false) { doc -> + val classDoc = doc.classNamed("foo.Person")!! + val constructors = classDoc.constructors() + assertEquals(1, constructors.size) + assertEquals(1, constructors.single().parameters().size) + } + } + + @Test fun testSuppress() { + verifyJavadoc("testdata/javadoc/suppress.kt", withKotlinRuntime = true) { doc -> + assertNull(doc.classNamed("Some")) + assertNull(doc.classNamed("SomeAgain")) + assertNull(doc.classNamed("Interface")) + val classSame = doc.classNamed("Same")!! + assertTrue(classSame.fields().isEmpty()) + assertTrue(classSame.methods().isEmpty()) + } + } + + @Test fun testTypeAliases() { + verifyJavadoc("testdata/javadoc/typealiases.kt", withKotlinRuntime = true) { doc -> + assertNull(doc.classNamed("B")) + assertNull(doc.classNamed("D")) + + assertEquals("A", doc.classNamed("C")!!.superclass().name()) + val methodParamType = doc.classNamed("TypealiasesKt")!!.methods() + .find { it.name() == "some" }!!.parameters().first() + .type() + assertEquals("Function1", methodParamType.qualifiedTypeName()) + assertEquals("? super A, C", methodParamType.asParameterizedType().typeArguments() + .map(Type::qualifiedTypeName).joinToString()) + } + } + + @Test fun testKDocKeywordsOnMethod() { + verifyJavadoc("testdata/javadoc/kdocKeywordsOnMethod.kt", withKotlinRuntime = true) { doc -> + val method = doc.classNamed("KdocKeywordsOnMethodKt")!!.methods()[0] + assertEquals("@return [ContentText(text=value of a)]", method.tags("return").first().text()) + assertEquals("@param a [ContentText(text=Some string)]", method.paramTags().first().text()) + assertEquals("@throws FireException [ContentText(text=in case of fire)]", method.throwsTags().first().text()) + } + } + + @Test + fun testBlankLineInsideCodeBlock() { + verifyJavadoc("testdata/javadoc/blankLineInsideCodeBlock.kt", withKotlinRuntime = true) { doc -> + val method = doc.classNamed("BlankLineInsideCodeBlockKt")!!.methods()[0] + val text = method.inlineTags().joinToString(separator = "", transform = Tag::text) + assertEqualsIgnoringSeparators(""" + <p><code><pre> + This is a test + of Dokka's code blocks. + Here is a blank line. + + The previous line was blank. + </pre></code></p> + """.trimIndent(), text) + } + } + + @Test + fun testCompanionMethodReference() { + verifyJavadoc("testdata/javadoc/companionMethodReference.kt") { doc -> + val classDoc = doc.classNamed("foo.TestClass")!! + val tag = classDoc.inlineTags().filterIsInstance<SeeMethodTagAdapter>().first() + assertEquals("TestClass.Companion", tag.referencedClassName()) + assertEquals("test", tag.referencedMemberName()) + } + } + + @Test fun shouldHaveAllFunctionMarkedAsDeprecated() { + verifyJavadoc("testdata/javadoc/deprecated.java") { doc -> + val classDoc = doc.classNamed("bar.Banana")!! + + classDoc.methods().forEach { method -> + assertTrue(method.tags().any { it.kind() == "deprecated" }) + } + } + } + + private fun verifyJavadoc(name: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + includeNonPublic: Boolean = true, + callback: (ModuleNodeAdapter) -> Unit) { + + verifyModel(name, format = "javadoc", withJdk = withJdk, withKotlinRuntime = withKotlinRuntime, includeNonPublic = includeNonPublic) { model -> + val doc = ModuleNodeAdapter(model, StandardReporter(DokkaConsoleLogger), "") + callback(doc) + } + } +} diff --git a/core/src/test/kotlin/markdown/ParserTest.kt b/core/src/test/kotlin/markdown/ParserTest.kt new file mode 100644 index 000000000..b0ec68ff9 --- /dev/null +++ b/core/src/test/kotlin/markdown/ParserTest.kt @@ -0,0 +1,154 @@ +package org.jetbrains.dokka.tests + +import org.junit.Test +import org.jetbrains.dokka.toTestString +import org.jetbrains.dokka.parseMarkdown +import org.junit.Ignore + +@Ignore public class ParserTest { + fun runTestFor(text : String) { + println("MD: ---") + println(text) + val markdownTree = parseMarkdown(text) + println("AST: ---") + println(markdownTree.toTestString()) + println() + } + + @Test fun text() { + runTestFor("text") + } + + @Test fun textWithSpaces() { + runTestFor("text and string") + } + + @Test fun textWithColon() { + runTestFor("text and string: cool!") + } + + @Test fun link() { + runTestFor("text [links]") + } + + @Test fun linkWithHref() { + runTestFor("text [links](http://google.com)") + } + + @Test fun multiline() { + runTestFor( + """ +text +and +string +""") + } + + @Test fun para() { + runTestFor( + """ +paragraph number +one + +paragraph +number two +""") + } + + @Test fun bulletList() { + runTestFor( + """* list item 1 +* list item 2 +""") + } + + @Test fun bulletListWithLines() { + runTestFor( + """ +* list item 1 + continue 1 +* list item 2 + continue 2 + """) + } + + @Test fun bulletListStrong() { + runTestFor( + """ +* list *item* 1 + continue 1 +* list *item* 2 + continue 2 + """) + } + + @Test fun emph() { + runTestFor("*text*") + } + + @Test fun underscoresNoEmph() { + runTestFor("text_with_underscores") + } + + @Test fun emphUnderscores() { + runTestFor("_text_") + } + + @Test fun singleStar() { + runTestFor("Embedded*Star") + } + + @Test fun directive() { + runTestFor("A text \${code with.another.value} with directive") + } + + @Test fun emphAndEmptySection() { + runTestFor("*text*\n\$sec:\n") + } + + @Test fun emphAndSection() { + runTestFor("*text*\n\$sec: some text\n") + } + + @Test fun emphAndBracedSection() { + runTestFor("Text *bold* text \n\${sec}: some text") + } + + @Test fun section() { + runTestFor( + "Plain text \n\$one: Summary \n\${two}: Description with *emphasis* \n\${An example of a section}: Example") + } + + @Test fun anonymousSection() { + runTestFor("Summary\n\nDescription\n") + } + + @Test fun specialSection() { + runTestFor( + "Plain text \n\$\$summary: Summary \n\${\$description}: Description \n\${\$An example of a section}: Example") + } + + @Test fun emptySection() { + runTestFor( + "Plain text \n\$summary:") + } + + val b = "$" + @Test fun pair() { + runTestFor( + """Represents a generic pair of two values. + +There is no meaning attached to values in this class, it can be used for any purpose. +Pair exhibits value semantics, i.e. two pairs are equal if both components are equal. + +An example of decomposing it into values: +${b}{code test.tuples.PairTest.pairMultiAssignment} + +${b}constructor: Creates new instance of [Pair] +${b}first: First value +${b}second: Second value"""" + ) + } + +} + diff --git a/core/src/test/kotlin/model/ClassTest.kt b/core/src/test/kotlin/model/ClassTest.kt new file mode 100644 index 000000000..6bc45db10 --- /dev/null +++ b/core/src/test/kotlin/model/ClassTest.kt @@ -0,0 +1,293 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.Content +import org.jetbrains.dokka.NodeKind +import org.jetbrains.dokka.RefKind +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class ClassTest { + @Test fun emptyClass() { + verifyModel("testdata/classes/emptyClass.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(NodeKind.Class, kind) + assertEquals("Klass", name) + assertEquals(Content.Empty, content) + assertEquals("<init>", members.single().name) + assertTrue(links.none()) + } + } + } + + @Test fun emptyObject() { + verifyModel("testdata/classes/emptyObject.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(NodeKind.Object, kind) + assertEquals("Obj", name) + assertEquals(Content.Empty, content) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun classWithConstructor() { + verifyModel("testdata/classes/classWithConstructor.kt") { model -> + with (model.members.single().members.single()) { + assertEquals(NodeKind.Class, kind) + assertEquals("Klass", name) + assertEquals(Content.Empty, content) + assertTrue(links.none()) + + assertEquals(1, members.count()) + with(members.elementAt(0)) { + assertEquals("<init>", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Constructor, kind) + assertEquals(3, details.count()) + assertEquals("public", details.elementAt(0).name) + with(details.elementAt(2)) { + assertEquals("name", name) + assertEquals(NodeKind.Parameter, kind) + assertEquals(Content.Empty, content) + assertEquals("String", detail(NodeKind.Type).name) + assertTrue(links.none()) + assertTrue(members.none()) + } + assertTrue(links.none()) + assertTrue(members.none()) + } + } + } + } + + @Test fun classWithFunction() { + verifyModel("testdata/classes/classWithFunction.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(NodeKind.Class, kind) + assertEquals("Klass", name) + assertEquals(Content.Empty, content) + assertTrue(links.none()) + + assertEquals(2, members.count()) + with(members.elementAt(0)) { + assertEquals("<init>", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Constructor, kind) + assertEquals(2, details.count()) + assertEquals("public", details.elementAt(0).name) + assertTrue(links.none()) + assertTrue(members.none()) + } + with(members.elementAt(1)) { + assertEquals("fn", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Function, kind) + assertEquals("Unit", detail(NodeKind.Type).name) + assertTrue(links.none()) + assertTrue(members.none()) + } + } + } + } + + @Test fun classWithProperty() { + verifyModel("testdata/classes/classWithProperty.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(NodeKind.Class, kind) + assertEquals("Klass", name) + assertEquals(Content.Empty, content) + assertTrue(links.none()) + + assertEquals(2, members.count()) + with(members.elementAt(0)) { + assertEquals("<init>", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Constructor, kind) + assertEquals(2, details.count()) + assertEquals("public", details.elementAt(0).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + with(members.elementAt(1)) { + assertEquals("name", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Property, kind) + assertEquals("String", detail(NodeKind.Type).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + } + + @Test fun classWithCompanionObject() { + verifyModel("testdata/classes/classWithCompanionObject.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(NodeKind.Class, kind) + assertEquals("Klass", name) + assertEquals(Content.Empty, content) + assertTrue(links.none()) + + assertEquals(3, members.count()) + with(members.elementAt(0)) { + assertEquals("<init>", name) + assertEquals(Content.Empty, content) + } + with(members.elementAt(1)) { + assertEquals("foo", name) + assertEquals(NodeKind.CompanionObjectFunction, kind) + assertTrue(members.none()) + assertTrue(links.none()) + } + with(members.elementAt(2)) { + assertEquals("x", name) + assertEquals(NodeKind.CompanionObjectProperty, kind) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + } + + @Test fun annotatedClass() { + verifyPackageMember("testdata/classes/annotatedClass.kt", withKotlinRuntime = true) { cls -> + assertEquals(1, cls.annotations.count()) + with(cls.annotations[0]) { + assertEquals("Strictfp", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Annotation, kind) + } + } + } + + @Test fun dataClass() { + verifyPackageMember("testdata/classes/dataClass.kt") { cls -> + val modifiers = cls.details(NodeKind.Modifier).map { it.name } + assertTrue("data" in modifiers) + } + } + + @Test fun sealedClass() { + verifyPackageMember("testdata/classes/sealedClass.kt") { cls -> + val modifiers = cls.details(NodeKind.Modifier).map { it.name } + assertEquals(1, modifiers.count { it == "sealed" }) + } + } + + @Test fun annotatedClassWithAnnotationParameters() { + verifyModel("testdata/classes/annotatedClassWithAnnotationParameters.kt") { model -> + with(model.members.single().members.single()) { + with(deprecation!!) { + assertEquals("Deprecated", name) + // assertEquals(Content.Empty, content) // this is now an empty MutableContent instead + assertEquals(NodeKind.Annotation, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(NodeKind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(NodeKind.Value, kind) + assertEquals("\"should no longer be used\"", name) + } + } + } + } + } + } + + @Test fun javaAnnotationClass() { + verifyModel("testdata/classes/javaAnnotationClass.kt", withJdk = true) { model -> + with(model.members.single().members.single()) { + assertEquals(1, annotations.count()) + with(annotations[0]) { + assertEquals("Retention", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Annotation, kind) + with(details[0]) { + assertEquals(NodeKind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(NodeKind.Value, kind) + assertEquals("RetentionPolicy.SOURCE", name) + } + } + } + } + } + } + + @Test fun notOpenClass() { + verifyModel("testdata/classes/notOpenClass.kt") { model -> + with(model.members.single().members.first { it.name == "D"}.members.first { it.name == "f" }) { + val modifiers = details(NodeKind.Modifier) + assertEquals(2, modifiers.size) + assertEquals("final", modifiers[1].name) + + val overrideReferences = references(RefKind.Override) + assertEquals(1, overrideReferences.size) + } + } + } + + @Test fun indirectOverride() { + verifyModel("testdata/classes/indirectOverride.kt") { model -> + with(model.members.single().members.first { it.name == "E"}.members.first { it.name == "foo" }) { + val modifiers = details(NodeKind.Modifier) + assertEquals(2, modifiers.size) + assertEquals("final", modifiers[1].name) + + val overrideReferences = references(RefKind.Override) + assertEquals(1, overrideReferences.size) + } + } + } + + @Test fun innerClass() { + verifyPackageMember("testdata/classes/innerClass.kt") { cls -> + val innerClass = cls.members.single { it.name == "D" } + val modifiers = innerClass.details(NodeKind.Modifier) + assertEquals(3, modifiers.size) + assertEquals("inner", modifiers[2].name) + } + } + + @Test fun companionObjectExtension() { + verifyModel("testdata/classes/companionObjectExtension.kt") { model -> + val pkg = model.members.single() + val cls = pkg.members.single { it.name == "Foo" } + val extensions = cls.extensions.filter { it.kind == NodeKind.CompanionObjectProperty } + assertEquals(1, extensions.size) + } + } + + @Test fun secondaryConstructor() { + verifyPackageMember("testdata/classes/secondaryConstructor.kt") { cls -> + val constructors = cls.members(NodeKind.Constructor) + assertEquals(2, constructors.size) + with (constructors.first { it.details(NodeKind.Parameter).size == 1}) { + assertEquals("<init>", name) + assertEquals("This is a secondary constructor.", summary.toTestString()) + } + } + } + + @Test fun sinceKotlin() { + verifyModel("testdata/classes/sinceKotlin.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(listOf("Kotlin 1.1"), platforms) + } + } + } + + @Test fun privateCompanionObject() { + verifyModel("testdata/classes/privateCompanionObject.kt", includeNonPublic = false) { model -> + with(model.members.single().members.single()) { + assertEquals(0, members(NodeKind.CompanionObjectFunction).size) + assertEquals(0, members(NodeKind.CompanionObjectProperty).size) + } + } + } + +} diff --git a/core/src/test/kotlin/model/CommentTest.kt b/core/src/test/kotlin/model/CommentTest.kt new file mode 100644 index 000000000..7869837c1 --- /dev/null +++ b/core/src/test/kotlin/model/CommentTest.kt @@ -0,0 +1,186 @@ +package org.jetbrains.dokka.tests + +import org.junit.Test +import org.junit.Assert.* +import org.jetbrains.dokka.* + +public class CommentTest { + + @Test fun codeBlockComment() { + verifyModel("testdata/comments/codeBlockComment.kt") { model -> + with(model.members.single().members.first()) { + assertEqualsIgnoringSeparators("""[code lang=brainfuck] + | + |++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>. + | + |[/code] + |""".trimMargin(), + content.toTestString()) + } + with(model.members.single().members.last()) { + assertEqualsIgnoringSeparators("""[code] + | + |a + b - c + | + |[/code] + |""".trimMargin(), + content.toTestString()) + } + } + } + + @Test fun emptyDoc() { + verifyModel("testdata/comments/emptyDoc.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(Content.Empty, content) + } + } + } + + @Test fun emptyDocButComment() { + verifyModel("testdata/comments/emptyDocButComment.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(Content.Empty, content) + } + } + } + + @Test fun multilineDoc() { + verifyModel("testdata/comments/multilineDoc.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("doc1", content.summary.toTestString()) + assertEquals("doc2\ndoc3", content.description.toTestString()) + } + } + } + + @Test fun multilineDocWithComment() { + verifyModel("testdata/comments/multilineDocWithComment.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("doc1", content.summary.toTestString()) + assertEquals("doc2\ndoc3", content.description.toTestString()) + } + } + } + + @Test fun oneLineDoc() { + verifyModel("testdata/comments/oneLineDoc.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("doc", content.summary.toTestString()) + } + } + } + + @Test fun oneLineDocWithComment() { + verifyModel("testdata/comments/oneLineDocWithComment.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("doc", content.summary.toTestString()) + } + } + } + + @Test fun oneLineDocWithEmptyLine() { + verifyModel("testdata/comments/oneLineDocWithEmptyLine.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("doc", content.summary.toTestString()) + } + } + } + + @Test fun emptySection() { + verifyModel("testdata/comments/emptySection.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Summary", content.summary.toTestString()) + assertEquals(1, content.sections.count()) + with (content.findSectionByTag("one")!!) { + assertEquals("One", tag) + assertEquals("", toTestString()) + } + } + } + } + + @Test fun quotes() { + verifyModel("testdata/comments/quotes.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("it's \"useful\"", content.summary.toTestString()) + } + } + } + + @Test fun section1() { + verifyModel("testdata/comments/section1.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Summary", content.summary.toTestString()) + assertEquals(1, content.sections.count()) + with (content.findSectionByTag("one")!!) { + assertEquals("One", tag) + assertEquals("section one", toTestString()) + } + } + } + } + + @Test fun section2() { + verifyModel("testdata/comments/section2.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Summary", content.summary.toTestString()) + assertEquals(2, content.sections.count()) + with (content.findSectionByTag("one")!!) { + assertEquals("One", tag) + assertEquals("section one", toTestString()) + } + with (content.findSectionByTag("two")!!) { + assertEquals("Two", tag) + assertEquals("section two", toTestString()) + } + } + } + } + + @Test fun multilineSection() { + verifyModel("testdata/comments/multilineSection.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Summary", content.summary.toTestString()) + assertEquals(1, content.sections.count()) + with (content.findSectionByTag("one")!!) { + assertEquals("One", tag) + assertEquals("""line one +line two""", toTestString()) + } + } + } + } + + @Test fun directive() { + verifyModel("testdata/comments/directive.kt") { model -> + with(model.members.single().members[3]) { + assertEquals("Summary", content.summary.toTestString()) + with (content.description) { + assertEqualsIgnoringSeparators(""" + |[code lang=kotlin] + |if (true) { + | println(property) + |} + |[/code] + |[code lang=kotlin] + |if (true) { + | println(property) + |} + |[/code] + |[code lang=kotlin] + |if (true) { + | println(property) + |} + |[/code] + |[code lang=kotlin] + |if (true) { + | println(property) + |} + |[/code] + |""".trimMargin(), toTestString()) + } + } + } + } +} diff --git a/core/src/test/kotlin/model/FunctionTest.kt b/core/src/test/kotlin/model/FunctionTest.kt new file mode 100644 index 000000000..c94d7e990 --- /dev/null +++ b/core/src/test/kotlin/model/FunctionTest.kt @@ -0,0 +1,251 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.Content +import org.jetbrains.dokka.NodeKind +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.test.assertNotNull + +class FunctionTest { + @Test fun function() { + verifyModel("testdata/functions/function.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("fn", name) + assertEquals(NodeKind.Function, kind) + assertEquals("Function fn", content.summary.toTestString()) + assertEquals("Unit", detail(NodeKind.Type).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun functionWithReceiver() { + verifyModel("testdata/functions/functionWithReceiver.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("kotlin.String", name) + assertEquals(NodeKind.ExternalClass, kind) + assertEquals(2, members.count()) + with(members[0]) { + assertEquals("fn", name) + assertEquals(NodeKind.Function, kind) + assertEquals("Function with receiver", content.summary.toTestString()) + assertEquals("public", details.elementAt(0).name) + assertEquals("final", details.elementAt(1).name) + with(details.elementAt(3)) { + assertEquals("<this>", name) + assertEquals(NodeKind.Receiver, kind) + assertEquals(Content.Empty, content) + assertEquals("String", details.single().name) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertEquals("Unit", details.elementAt(4).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + with(members[1]) { + assertEquals("fn", name) + assertEquals(NodeKind.Function, kind) + } + } + } + } + + @Test fun genericFunction() { + verifyModel("testdata/functions/genericFunction.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("generic", name) + assertEquals(NodeKind.Function, kind) + assertEquals("generic function", content.summary.toTestString()) + + assertEquals("private", details.elementAt(0).name) + assertEquals("final", details.elementAt(1).name) + with(details.elementAt(3)) { + assertEquals("T", name) + assertEquals(NodeKind.TypeParameter, kind) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertEquals("Unit", details.elementAt(4).name) + + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + @Test fun genericFunctionWithConstraints() { + verifyModel("testdata/functions/genericFunctionWithConstraints.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("generic", name) + assertEquals(NodeKind.Function, kind) + assertEquals("generic function", content.summary.toTestString()) + + val functionDetails = details + assertEquals("public", functionDetails.elementAt(0).name) + assertEquals("final", functionDetails.elementAt(1).name) + with(functionDetails.elementAt(3)) { + assertEquals("T", name) + assertEquals(NodeKind.TypeParameter, kind) + assertEquals(Content.Empty, content) + with(details.single()) { + assertEquals("R", name) + assertEquals(NodeKind.UpperBound, kind) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.singleOrNull() == functionDetails.elementAt(4)) + } + assertTrue(members.none()) + assertTrue(links.none()) + } + with(functionDetails.elementAt(4)) { + assertEquals("R", name) + assertEquals(NodeKind.TypeParameter, kind) + assertEquals(Content.Empty, content) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertEquals("Unit", functionDetails.elementAt(5).name) + + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun functionWithParams() { + verifyModel("testdata/functions/functionWithParams.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("function", name) + assertEquals(NodeKind.Function, kind) + assertEquals("Multiline", content.summary.toTestString()) + assertEquals("""Function +Documentation""", content.description.toTestString()) + + assertEquals("public", details.elementAt(0).name) + assertEquals("final", details.elementAt(1).name) + with(details.elementAt(3)) { + assertEquals("x", name) + assertEquals(NodeKind.Parameter, kind) + assertEquals("parameter", content.summary.toTestString()) + assertEquals("Int", detail(NodeKind.Type).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertEquals("Unit", details.elementAt(4).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun annotatedFunction() { + verifyPackageMember("testdata/functions/annotatedFunction.kt", withKotlinRuntime = true) { func -> + assertEquals(1, func.annotations.count()) + with(func.annotations[0]) { + assertEquals("Strictfp", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Annotation, kind) + } + } + } + + @Test fun functionWithNotDocumentedAnnotation() { + verifyPackageMember("testdata/functions/functionWithNotDocumentedAnnotation.kt") { func -> + assertEquals(0, func.annotations.count()) + } + } + + @Test fun inlineFunction() { + verifyPackageMember("testdata/functions/inlineFunction.kt") { func -> + val modifiers = func.details(NodeKind.Modifier).map { it.name } + assertTrue("inline" in modifiers) + } + } + + @Test fun functionWithAnnotatedParam() { + verifyModel("testdata/functions/functionWithAnnotatedParam.kt") { model -> + with(model.members.single().members.single { it.name == "function" }) { + with(details(NodeKind.Parameter).first()) { + assertEquals(1, annotations.count()) + with(annotations[0]) { + assertEquals("Fancy", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Annotation, kind) + } + } + } + } + } + + @Test fun functionWithNoinlineParam() { + verifyPackageMember("testdata/functions/functionWithNoinlineParam.kt") { func -> + with(func.details(NodeKind.Parameter).first()) { + val modifiers = details(NodeKind.Modifier).map { it.name } + assertTrue("noinline" in modifiers) + } + } + } + + @Test fun annotatedFunctionWithAnnotationParameters() { + verifyModel("testdata/functions/annotatedFunctionWithAnnotationParameters.kt") { model -> + with(model.members.single().members.single { it.name == "f" }) { + assertEquals(1, annotations.count()) + with(annotations[0]) { + assertEquals("Fancy", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Annotation, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(NodeKind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(NodeKind.Value, kind) + assertEquals("1", name) + } + } + } + } + } + } + + @Test fun functionWithDefaultParameter() { + verifyModel("testdata/functions/functionWithDefaultParameter.kt") { model -> + with(model.members.single().members.single()) { + with(details.elementAt(3)) { + val value = details(NodeKind.Value) + assertEquals(1, value.count()) + with(value[0]) { + assertEquals("\"\"", name) + } + } + } + } + } + + @Test fun sinceKotlin() { + verifyModel("testdata/functions/sinceKotlin.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(listOf("Kotlin 1.1"), platforms) + } + } + } + + // Test for b/159470920, to ensure that we correctly parse annotated function types without resolving 'ERROR CLASS' + // types. Note that the actual annotation is not included in the type information, this is tracked in b/145517104. + @Test fun functionWithAnnotatedLambdaParam() { + verifyModel("testdata/functions/functionWithAnnotatedLambdaParam.kt") { model -> + with(model.members.single().members.single { it.name == "function" }) { + with(details(NodeKind.Parameter).first()) { + with(details(NodeKind.Type).first()) { + assertEquals("Function0", name) + } + } + } + } + } +} diff --git a/core/src/test/kotlin/model/JavaTest.kt b/core/src/test/kotlin/model/JavaTest.kt new file mode 100644 index 000000000..c00d8dc37 --- /dev/null +++ b/core/src/test/kotlin/model/JavaTest.kt @@ -0,0 +1,219 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.NodeKind +import org.jetbrains.dokka.RefKind +import org.junit.Assert.* +import org.junit.Ignore +import org.junit.Test + +public class JavaTest { + @Test fun function() { + verifyJavaPackageMember("testdata/java/member.java") { cls -> + assertEquals("Test", cls.name) + assertEquals(NodeKind.Class, cls.kind) + with(cls.members(NodeKind.Function).single()) { + assertEquals("fn", name) + assertEquals("Summary for Function", content.summary.toTestString().trimEnd()) + assertEquals(3, content.sections.size) + with(content.sections[0]) { + assertEquals("Parameters", tag) + assertEquals("name", subjectName) + assertEquals("render(Type:String,SUMMARY): is String parameter", toTestString()) + } + with(content.sections[1]) { + assertEquals("Parameters", tag) + assertEquals("value", subjectName) + assertEquals("render(Type:Int,SUMMARY): is int parameter", toTestString()) + } + with(content.sections[2]) { + assertEquals("Author", tag) + assertEquals("yole", toTestString()) + } + assertEquals("Unit", detail(NodeKind.Type).name) + assertTrue(members.none()) + assertTrue(links.none()) + with(details.first { it.name == "name" }) { + assertEquals(NodeKind.Parameter, kind) + assertEquals("String", detail(NodeKind.Type).name) + } + with(details.first { it.name == "value" }) { + assertEquals(NodeKind.Parameter, kind) + assertEquals("Int", detail(NodeKind.Type).name) + } + } + } + } + + @Test fun memberWithModifiers() { + verifyJavaPackageMember("testdata/java/memberWithModifiers.java") { cls -> + val modifiers = cls.details(NodeKind.Modifier).map { it.name } + assertTrue("abstract" in modifiers) + with(cls.members.single { it.name == "fn" }) { + assertEquals("protected", details[0].name) + } + with(cls.members.single { it.name == "openFn" }) { + assertEquals("open", details[1].name) + } + } + } + + @Test fun superClass() { + verifyJavaPackageMember("testdata/java/superClass.java") { cls -> + val superTypes = cls.details(NodeKind.Supertype) + assertEquals(2, superTypes.size) + assertEquals("Exception", superTypes[0].name) + assertEquals("Cloneable", superTypes[1].name) + } + } + + @Test fun arrayType() { + verifyJavaPackageMember("testdata/java/arrayType.java") { cls -> + with(cls.members(NodeKind.Function).single()) { + val type = detail(NodeKind.Type) + assertEquals("Array", type.name) + assertEquals("String", type.detail(NodeKind.Type).name) + with(details(NodeKind.Parameter).single()) { + val parameterType = detail(NodeKind.Type) + assertEquals("IntArray", parameterType.name) + } + } + } + } + + @Test fun typeParameter() { + verifyJavaPackageMember("testdata/java/typeParameter.java") { cls -> + val typeParameters = cls.details(NodeKind.TypeParameter) + with(typeParameters.single()) { + assertEquals("T", name) + with(detail(NodeKind.UpperBound)) { + assertEquals("Comparable", name) + assertEquals("T", detail(NodeKind.Type).name) + } + } + with(cls.members(NodeKind.Function).single()) { + val methodTypeParameters = details(NodeKind.TypeParameter) + with(methodTypeParameters.single()) { + assertEquals("E", name) + } + } + } + } + + @Test fun constructors() { + verifyJavaPackageMember("testdata/java/constructors.java") { cls -> + val constructors = cls.members(NodeKind.Constructor) + assertEquals(2, constructors.size) + with(constructors[0]) { + assertEquals("<init>", name) + } + } + } + + @Test fun innerClass() { + verifyJavaPackageMember("testdata/java/InnerClass.java") { cls -> + val innerClass = cls.members(NodeKind.Class).single() + assertEquals("D", innerClass.name) + } + } + + @Test fun varargs() { + verifyJavaPackageMember("testdata/java/varargs.java") { cls -> + val fn = cls.members(NodeKind.Function).single() + val param = fn.detail(NodeKind.Parameter) + assertEquals("vararg", param.details(NodeKind.Modifier).first().name) + val psiType = param.detail(NodeKind.Type) + assertEquals("String", psiType.name) + assertTrue(psiType.details(NodeKind.Type).isEmpty()) + } + } + + @Test fun fields() { + verifyJavaPackageMember("testdata/java/field.java") { cls -> + val i = cls.members(NodeKind.Property).single { it.name == "i" } + assertEquals("Int", i.detail(NodeKind.Type).name) + assertTrue("var" in i.details(NodeKind.Modifier).map { it.name }) + + val s = cls.members(NodeKind.Property).single { it.name == "s" } + assertEquals("String", s.detail(NodeKind.Type).name) + assertFalse("var" in s.details(NodeKind.Modifier).map { it.name }) + assertTrue("static" in s.details(NodeKind.Modifier).map { it.name }) + } + } + + @Test fun staticMethod() { + verifyJavaPackageMember("testdata/java/staticMethod.java") { cls -> + val m = cls.members(NodeKind.Function).single { it.name == "foo" } + assertTrue("static" in m.details(NodeKind.Modifier).map { it.name }) + } + } + + /** + * `@suppress` not supported in Java! + * + * [Proposed tags](http://www.oracle.com/technetwork/java/javase/documentation/proposed-tags-142378.html) + * Proposed tag `@exclude` for it, but not supported yet + */ + @Ignore("@suppress not supported in Java!") @Test fun suppressTag() { + verifyJavaPackageMember("testdata/java/suppressTag.java") { cls -> + assertEquals(1, cls.members(NodeKind.Function).size) + } + } + + @Test fun hideAnnotation() { + verifyJavaPackageMember("testdata/java/hideAnnotation.java") { cls -> + assertEquals(1, cls.members(NodeKind.Function).size) + assertEquals(1, cls.members(NodeKind.Property).size) + + // The test file contains two classes, one of which is hidden. + // The test for @hide annotation on classes is via verifyJavaPackageMember(), + // which will throw an IllegalArgumentException if it detects more than one class. + } + } + + @Test fun annotatedAnnotation() { + verifyJavaPackageMember("testdata/java/annotatedAnnotation.java") { cls -> + assertEquals(1, cls.annotations.size) + with(cls.annotations[0]) { + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(NodeKind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(NodeKind.Value, kind) + assertEquals("[AnnotationTarget.FIELD, AnnotationTarget.CLASS, AnnotationTarget.FILE, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER]", name) + } + } + } + } + } + + @Test fun deprecation() { + verifyJavaPackageMember("testdata/java/deprecation.java") { cls -> + val fn = cls.members(NodeKind.Function).single() + assertEquals("This should no longer be used", fn.deprecation!!.content.toTestString()) + } + } + + @Test fun javaLangObject() { + verifyJavaPackageMember("testdata/java/javaLangObject.java") { cls -> + val fn = cls.members(NodeKind.Function).single() + assertEquals("Any", fn.detail(NodeKind.Type).name) + } + } + + @Test fun enumValues() { + verifyJavaPackageMember("testdata/java/enumValues.java") { cls -> + val superTypes = cls.details(NodeKind.Supertype) + assertEquals(1, superTypes.size) + assertEquals(1, cls.members(NodeKind.EnumItem).size) + } + } + + @Test fun inheritorLinks() { + verifyJavaPackageMember("testdata/java/InheritorLinks.java") { cls -> + val fooClass = cls.members.single { it.name == "Foo" } + val inheritors = fooClass.references(RefKind.Inheritor) + assertEquals(1, inheritors.size) + } + } +} diff --git a/core/src/test/kotlin/model/KotlinAsJavaTest.kt b/core/src/test/kotlin/model/KotlinAsJavaTest.kt new file mode 100644 index 000000000..4a054a880 --- /dev/null +++ b/core/src/test/kotlin/model/KotlinAsJavaTest.kt @@ -0,0 +1,39 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.DocumentationModule +import org.jetbrains.dokka.NodeKind +import org.junit.Test +import org.junit.Assert.assertEquals + +class KotlinAsJavaTest { + @Test fun function() { + verifyModelAsJava("testdata/functions/function.kt") { model -> + val pkg = model.members.single() + + val facadeClass = pkg.members.single { it.name == "FunctionKt" } + assertEquals(NodeKind.Class, facadeClass.kind) + + val fn = facadeClass.members.single { it.kind == NodeKind.Function} + assertEquals("fn", fn.name) + } + } + + @Test fun propertyWithComment() { + verifyModelAsJava("testdata/comments/oneLineDoc.kt") { model -> + val facadeClass = model.members.single().members.single { it.name == "OneLineDocKt" } + val getter = facadeClass.members.single { it.name == "getProperty" } + assertEquals(NodeKind.Function, getter.kind) + assertEquals("doc", getter.content.summary.toTestString()) + } + } +} + +fun verifyModelAsJava(source: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + verifier: (DocumentationModule) -> Unit) { + verifyModel(source, + withJdk = withJdk, withKotlinRuntime = withKotlinRuntime, + format = "html-as-java", + verifier = verifier) +} diff --git a/core/src/test/kotlin/model/LinkTest.kt b/core/src/test/kotlin/model/LinkTest.kt new file mode 100644 index 000000000..6b72525fd --- /dev/null +++ b/core/src/test/kotlin/model/LinkTest.kt @@ -0,0 +1,75 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.ContentBlock +import org.jetbrains.dokka.ContentNodeLazyLink +import org.jetbrains.dokka.NodeKind +import org.junit.Assert.assertEquals +import org.junit.Test + +class LinkTest { + @Test fun linkToSelf() { + verifyModel("testdata/links/linkToSelf.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Foo", name) + assertEquals(NodeKind.Class, kind) + assertEquals("This is link to [Foo -> Class:Foo]", content.summary.toTestString()) + } + } + } + + @Test fun linkToMember() { + verifyModel("testdata/links/linkToMember.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Foo", name) + assertEquals(NodeKind.Class, kind) + assertEquals("This is link to [member -> Function:member]", content.summary.toTestString()) + } + } + } + + @Test fun linkToConstantWithUnderscores() { + verifyModel("testdata/links/linkToConstantWithUnderscores.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Foo", name) + assertEquals(NodeKind.Class, kind) + assertEquals("This is link to [MY_CONSTANT_VALUE -> CompanionObjectProperty:MY_CONSTANT_VALUE]", content.summary.toTestString()) + } + } + } + + @Test fun linkToQualifiedMember() { + verifyModel("testdata/links/linkToQualifiedMember.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Foo", name) + assertEquals(NodeKind.Class, kind) + assertEquals("This is link to [Foo.member -> Function:member]", content.summary.toTestString()) + } + } + } + + @Test fun linkToParam() { + verifyModel("testdata/links/linkToParam.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Foo", name) + assertEquals(NodeKind.Function, kind) + assertEquals("This is link to [param -> Parameter:param]", content.summary.toTestString()) + } + } + } + + @Test fun linkToPackage() { + verifyModel("testdata/links/linkToPackage.kt") { model -> + val packageNode = model.members.single() + with(packageNode) { + assertEquals(this.name, "test.magic") + } + with(packageNode.members.single()) { + assertEquals("Magic", name) + assertEquals(NodeKind.Class, kind) + assertEquals("Basic implementations of [Magic -> Class:Magic] are located in [test.magic -> Package:test.magic] package", content.summary.toTestString()) + assertEquals(packageNode, ((this.content.summary as ContentBlock).children.filterIsInstance<ContentNodeLazyLink>().last()).lazyNode.invoke()) + } + } + } + +}
\ No newline at end of file diff --git a/core/src/test/kotlin/model/PackageTest.kt b/core/src/test/kotlin/model/PackageTest.kt new file mode 100644 index 000000000..3936fb4f3 --- /dev/null +++ b/core/src/test/kotlin/model/PackageTest.kt @@ -0,0 +1,116 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.Content +import org.jetbrains.dokka.NodeKind +import org.jetbrains.dokka.PackageOptionsImpl +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot +import org.junit.Assert.* +import org.junit.Test + +public class PackageTest { + @Test fun rootPackage() { + verifyModel("testdata/packages/rootPackage.kt") { model -> + with(model.members.single()) { + assertEquals(NodeKind.Package, kind) + assertEquals("", name) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun simpleNamePackage() { + verifyModel("testdata/packages/simpleNamePackage.kt") { model -> + with(model.members.single()) { + assertEquals(NodeKind.Package, kind) + assertEquals("simple", name) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun dottedNamePackage() { + verifyModel("testdata/packages/dottedNamePackage.kt") { model -> + with(model.members.single()) { + assertEquals(NodeKind.Package, kind) + assertEquals("dot.name", name) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun multipleFiles() { + verifyModel(KotlinSourceRoot("testdata/packages/dottedNamePackage.kt", false), + KotlinSourceRoot("testdata/packages/simpleNamePackage.kt", false) + ) { model -> + assertEquals(2, model.members.count()) + with(model.members.single { it.name == "simple" }) { + assertEquals(NodeKind.Package, kind) + assertEquals("simple", name) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + with(model.members.single { it.name == "dot.name" }) { + assertEquals(NodeKind.Package, kind) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun multipleFilesSamePackage() { + verifyModel(KotlinSourceRoot("testdata/packages/simpleNamePackage.kt", false), + KotlinSourceRoot("testdata/packages/simpleNamePackage2.kt", false)) { model -> + assertEquals(1, model.members.count()) + with(model.members.elementAt(0)) { + assertEquals(NodeKind.Package, kind) + assertEquals("simple", name) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun classAtPackageLevel() { + verifyModel(KotlinSourceRoot("testdata/packages/classInPackage.kt", false)) { model -> + assertEquals(1, model.members.count()) + with(model.members.elementAt(0)) { + assertEquals(NodeKind.Package, kind) + assertEquals("simple.name", name) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertEquals(1, members.size) + assertTrue(links.none()) + } + } + } + + @Test fun suppressAtPackageLevel() { + verifyModel(KotlinSourceRoot("testdata/packages/classInPackage.kt", false), + perPackageOptions = listOf(PackageOptionsImpl(prefix = "simple.name", suppress = true))) { model -> + assertEquals(1, model.members.count()) + with(model.members.elementAt(0)) { + assertEquals(NodeKind.Package, kind) + assertEquals("simple.name", name) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } +} diff --git a/core/src/test/kotlin/model/PropertyTest.kt b/core/src/test/kotlin/model/PropertyTest.kt new file mode 100644 index 000000000..41c3a4c8f --- /dev/null +++ b/core/src/test/kotlin/model/PropertyTest.kt @@ -0,0 +1,112 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.Content +import org.jetbrains.dokka.NodeKind +import org.jetbrains.dokka.RefKind +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Ignore +import org.junit.Test + +class PropertyTest { + @Test fun valueProperty() { + verifyModel("testdata/properties/valueProperty.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("property", name) + assertEquals(NodeKind.Property, kind) + assertEquals(Content.Empty, content) + assertEquals("String", detail(NodeKind.Type).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun variableProperty() { + verifyModel("testdata/properties/variableProperty.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("property", name) + assertEquals(NodeKind.Property, kind) + assertEquals(Content.Empty, content) + assertEquals("String", detail(NodeKind.Type).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun valuePropertyWithGetter() { + verifyModel("testdata/properties/valuePropertyWithGetter.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("property", name) + assertEquals(NodeKind.Property, kind) + assertEquals(Content.Empty, content) + assertEquals("String", detail(NodeKind.Type).name) + assertTrue(links.none()) + assertTrue(members.none()) + } + } + } + + @Test fun variablePropertyWithAccessors() { + verifyModel("testdata/properties/variablePropertyWithAccessors.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("property", name) + assertEquals(NodeKind.Property, kind) + assertEquals(Content.Empty, content) + assertEquals("String", detail(NodeKind.Type).name) + val modifiers = details(NodeKind.Modifier).map { it.name } + assertTrue("final" in modifiers) + assertTrue("public" in modifiers) + assertTrue("var" in modifiers) + assertTrue(links.none()) + assertTrue(members.none()) + } + } + } + + @Test fun annotatedProperty() { + verifyModel("testdata/properties/annotatedProperty.kt", withKotlinRuntime = true) { model -> + with(model.members.single().members.single()) { + assertEquals(1, annotations.count()) + with(annotations[0]) { + assertEquals("Strictfp", name) + assertEquals(Content.Empty, content) + assertEquals(NodeKind.Annotation, kind) + } + } + } + } + + @Test fun propertyWithReceiver() { + verifyModel("testdata/properties/propertyWithReceiver.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("kotlin.String", name) + assertEquals(NodeKind.ExternalClass, kind) + with(members.single()) { + assertEquals("foobar", name) + assertEquals(NodeKind.Property, kind) + } + } + } + } + + @Test fun propertyOverride() { + verifyModel("testdata/properties/propertyOverride.kt") { model -> + with(model.members.single().members.single { it.name == "Bar" }.members.single { it.name == "xyzzy"}) { + assertEquals("xyzzy", name) + val override = references(RefKind.Override).single().to + assertEquals("xyzzy", override.name) + assertEquals("Foo", override.owner!!.name) + } + } + } + + @Test fun sinceKotlin() { + verifyModel("testdata/properties/sinceKotlin.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(listOf("Kotlin 1.1"), platforms) + } + } + } +} diff --git a/core/src/test/kotlin/model/TypeAliasTest.kt b/core/src/test/kotlin/model/TypeAliasTest.kt new file mode 100644 index 000000000..c653ac83a --- /dev/null +++ b/core/src/test/kotlin/model/TypeAliasTest.kt @@ -0,0 +1,132 @@ +package org.jetbrains.dokka.tests + +import junit.framework.TestCase.assertEquals +import org.jetbrains.dokka.Content +import org.jetbrains.dokka.NodeKind +import org.junit.Test + +class TypeAliasTest { + @Test + fun testSimple() { + verifyModel("testdata/typealias/simple.kt") { + val pkg = it.members.single() + with(pkg.member(NodeKind.TypeAlias)) { + assertEquals(Content.Empty, content) + assertEquals("B", name) + assertEquals("A", detail(NodeKind.TypeAliasUnderlyingType).name) + } + } + } + + @Test + fun testInheritanceFromTypeAlias() { + verifyModel("testdata/typealias/inheritanceFromTypeAlias.kt") { + val pkg = it.members.single() + with(pkg.member(NodeKind.TypeAlias)) { + assertEquals(Content.Empty, content) + assertEquals("Same", name) + assertEquals("Some", detail(NodeKind.TypeAliasUnderlyingType).name) + assertEquals("My", inheritors.single().name) + } + with(pkg.members(NodeKind.Class).find { it.name == "My" }!!) { + assertEquals("Same", detail(NodeKind.Supertype).name) + } + } + } + + @Test + fun testChain() { + verifyModel("testdata/typealias/chain.kt") { + val pkg = it.members.single() + with(pkg.members(NodeKind.TypeAlias).find { it.name == "B" }!!) { + assertEquals(Content.Empty, content) + assertEquals("A", detail(NodeKind.TypeAliasUnderlyingType).name) + } + with(pkg.members(NodeKind.TypeAlias).find { it.name == "C" }!!) { + assertEquals(Content.Empty, content) + assertEquals("B", detail(NodeKind.TypeAliasUnderlyingType).name) + } + } + } + + @Test + fun testDocumented() { + verifyModel("testdata/typealias/documented.kt") { + val pkg = it.members.single() + with(pkg.member(NodeKind.TypeAlias)) { + assertEquals("Just typealias", content.summary.toTestString()) + } + } + } + + @Test + fun testDeprecated() { + verifyModel("testdata/typealias/deprecated.kt") { + val pkg = it.members.single() + with(pkg.member(NodeKind.TypeAlias)) { + assertEquals(Content.Empty, content) + assertEquals("Deprecated", deprecation!!.name) + assertEquals("\"Not mainstream now\"", deprecation!!.detail(NodeKind.Parameter).detail(NodeKind.Value).name) + } + } + } + + @Test + fun testGeneric() { + verifyModel("testdata/typealias/generic.kt") { + val pkg = it.members.single() + with(pkg.members(NodeKind.TypeAlias).find { it.name == "B" }!!) { + assertEquals("Any", detail(NodeKind.TypeAliasUnderlyingType).detail(NodeKind.Type).name) + } + + with(pkg.members(NodeKind.TypeAlias).find { it.name == "C" }!!) { + assertEquals("T", detail(NodeKind.TypeAliasUnderlyingType).detail(NodeKind.Type).name) + assertEquals("T", detail(NodeKind.TypeParameter).name) + } + } + } + + @Test + fun testFunctional() { + verifyModel("testdata/typealias/functional.kt") { + val pkg = it.members.single() + with(pkg.member(NodeKind.TypeAlias)) { + assertEquals("Function1", detail(NodeKind.TypeAliasUnderlyingType).name) + val typeParams = detail(NodeKind.TypeAliasUnderlyingType).details(NodeKind.Type) + assertEquals("A", typeParams.first().name) + assertEquals("B", typeParams.last().name) + } + + with(pkg.member(NodeKind.Function)) { + assertEquals("Spell", detail(NodeKind.Parameter).detail(NodeKind.Type).name) + } + } + } + + @Test + fun testAsTypeBoundWithVariance() { + verifyModel("testdata/typealias/asTypeBoundWithVariance.kt") { + val pkg = it.members.single() + with(pkg.members(NodeKind.Class).find { it.name == "C" }!!) { + val tParam = detail(NodeKind.TypeParameter) + assertEquals("out", tParam.detail(NodeKind.Modifier).name) + assertEquals("B", tParam.detail(NodeKind.Type).link(NodeKind.TypeAlias).name) + } + + with(pkg.members(NodeKind.Class).find { it.name == "D" }!!) { + val tParam = detail(NodeKind.TypeParameter) + assertEquals("in", tParam.detail(NodeKind.Modifier).name) + assertEquals("B", tParam.detail(NodeKind.Type).link(NodeKind.TypeAlias).name) + } + } + } + + @Test + fun sinceKotlin() { + verifyModel("testdata/typealias/sinceKotlin.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(listOf("Kotlin 1.1"), platforms) + } + } + } +}
\ No newline at end of file diff --git a/core/src/test/kotlin/utilities/StringExtensionsTest.kt b/core/src/test/kotlin/utilities/StringExtensionsTest.kt new file mode 100644 index 000000000..80c18df6d --- /dev/null +++ b/core/src/test/kotlin/utilities/StringExtensionsTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.dokka.tests.utilities + +import org.jetbrains.dokka.Utilities.firstSentence +import org.junit.Assert.assertEquals +import org.junit.Test + +class StringExtensionsTest { + + @Test + fun firstSentence_emptyString() { + assertEquals("", "".firstSentence()) + } + + @Test + fun incompleteSentence() { + assertEquals("Hello there", "Hello there".firstSentence()) + } + + @Test + fun incompleteSentence_withParenthesis() { + assertEquals("Hello there (hi)", "Hello there (hi)".firstSentence()) + assertEquals("Hello there (hi.)", "Hello there (hi.)".firstSentence()) + } + + @Test + fun incompleteSentence_apiLevel() { + assertEquals("API level 8 (Android 2.2, Froyo)", "API level 8 (Android 2.2, Froyo)".firstSentence()) + } + + @Test + fun unmatchedClosingParen() { + assertEquals( + "A notation either declares, by name, the format of an unparsed entity (see \n", + "A notation either declares, by name, the format of an unparsed entity (see \n".firstSentence() + ) + } + + @Test + fun unmatchedClosingParen_withFullFirstSentence() { + assertEquals( + "This interface represents a notation declared in the DTD.", + ("This interface represents a notation declared in the DTD. A notation either declares, by name, " + + "the format of an unparsed entity (see \n").firstSentence() + ) + } + + @Test + fun firstSentence_singleSentence() { + assertEquals("Hello there.", "Hello there.".firstSentence()) + } + + @Test + fun firstSentence_multipleSentences() { + assertEquals("Hello there.", "Hello there. How are you?".firstSentence()) + } + + @Test + fun firstSentence_singleSentence_withParenthesis() { + assertEquals("API level 28 (Android Pie).", "API level 28 (Android Pie).".firstSentence()) + } + + @Test + fun firstSentence_multipleSentences_withParenthesis() { + assertEquals( + "API level 28 (Android Pie).", + "API level 28 (Android Pie). API level 27 (Android Oreo)".firstSentence() + ) + } + + @Test + fun firstSentence_singleSentence_withPeriodInParenthesis() { + assertEquals("API level 28 (Android 9.0 Pie).", "API level 28 (Android 9.0 Pie).".firstSentence()) + } + + @Test + fun firstSentence_multipleSentences_withPeriodInParenthesis() { + assertEquals( + "API level 28 (Android 9.0 Pie).", + "API level 28 (Android 9.0 Pie). API level 27 (Android 8.0 Oreo).".firstSentence() + ) + } + + @Test + fun parenthesisWithperiod_notFirstSentence() { + assertEquals("Foo bar.", "Foo bar. Baz (Wow)".firstSentence()) + assertEquals("Foo bar.", "Foo bar. Baz (Wow).".firstSentence()) + } + + @Test + fun periodInsideParenthesis() { + assertEquals( + "A ViewGroup is a special view that can contain other views (called children.) " + + "The view group is the base class for layouts and views containers.", + ("A ViewGroup is a special view that can contain other views (called children.) " + + "The view group is the base class for layouts and views containers. " + + "This class also defines the android.view.ViewGroup.LayoutParams class " + + "which serves as the base class for layouts parameters.").firstSentence() + ) + assertEquals("Foo (Foo.) bar.", "Foo (Foo.) bar. Baz.".firstSentence()) + assertEquals("Foo (Foo.) bar (bar.) baz.", "Foo (Foo.) bar (bar.) baz. Wow".firstSentence()) + assertEquals("Foo (Foo.) bar (bar.) baz (baz.) Wow", "Foo (Foo.) bar (bar.) baz (baz.) Wow".firstSentence()) + } +}
\ No newline at end of file diff --git a/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline
\ No newline at end of file |