aboutsummaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/src')
-rw-r--r--core/src/main/kotlin/Analysis/AnalysisEnvironment.kt371
-rw-r--r--core/src/main/kotlin/Analysis/CoreKotlinCacheService.kt42
-rw-r--r--core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt569
-rw-r--r--core/src/main/kotlin/Analysis/JavaResolveExtension.kt128
-rw-r--r--core/src/main/kotlin/DokkaBootstrapImpl.kt83
-rw-r--r--core/src/main/kotlin/Formats/AnalysisComponents.kt45
-rw-r--r--core/src/main/kotlin/Formats/DacHtmlFormat.kt949
-rw-r--r--core/src/main/kotlin/Formats/DacOutlineService.kt395
-rw-r--r--core/src/main/kotlin/Formats/ExtraOutlineServices.kt20
-rw-r--r--core/src/main/kotlin/Formats/FormatDescriptor.kt42
-rw-r--r--core/src/main/kotlin/Formats/FormatService.kt32
-rw-r--r--core/src/main/kotlin/Formats/GFMFormatService.kt61
-rw-r--r--core/src/main/kotlin/Formats/HtmlFormatService.kt168
-rw-r--r--core/src/main/kotlin/Formats/HtmlTemplateService.kt38
-rw-r--r--core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt141
-rw-r--r--core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt1171
-rw-r--r--core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt165
-rw-r--r--core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt154
-rw-r--r--core/src/main/kotlin/Formats/JekyllFormatService.kt44
-rw-r--r--core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt224
-rw-r--r--core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt186
-rw-r--r--core/src/main/kotlin/Formats/MarkdownFormatService.kt239
-rw-r--r--core/src/main/kotlin/Formats/OutlineService.kt29
-rw-r--r--core/src/main/kotlin/Formats/PackageListService.kt63
-rw-r--r--core/src/main/kotlin/Formats/StandardFormats.kt66
-rw-r--r--core/src/main/kotlin/Formats/StructuredFormatService.kt691
-rw-r--r--core/src/main/kotlin/Formats/YamlOutlineService.kt26
-rw-r--r--core/src/main/kotlin/Generation/DokkaGenerator.kt207
-rw-r--r--core/src/main/kotlin/Generation/FileGenerator.kt89
-rw-r--r--core/src/main/kotlin/Generation/Generator.kt29
-rw-r--r--core/src/main/kotlin/Generation/configurationImpl.kt67
-rw-r--r--core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt392
-rw-r--r--core/src/main/kotlin/Java/JavadocParser.kt709
-rw-r--r--core/src/main/kotlin/Kotlin/ContentBuilder.kt188
-rw-r--r--core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt72
-rw-r--r--core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt339
-rw-r--r--core/src/main/kotlin/Kotlin/DocumentationBuilder.kt1177
-rw-r--r--core/src/main/kotlin/Kotlin/ExternalDocumentationLinkResolver.kt258
-rw-r--r--core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt68
-rw-r--r--core/src/main/kotlin/Kotlin/KotlinAsJavaElementSignatureProvider.kt25
-rw-r--r--core/src/main/kotlin/Kotlin/KotlinElementSignatureProvider.kt34
-rw-r--r--core/src/main/kotlin/Kotlin/KotlinLanguageService.kt463
-rw-r--r--core/src/main/kotlin/Languages/CommonLanguageService.kt118
-rw-r--r--core/src/main/kotlin/Languages/JavaLanguageService.kt179
-rw-r--r--core/src/main/kotlin/Languages/LanguageService.kt43
-rw-r--r--core/src/main/kotlin/Languages/NewJavaLanguageService.kt250
-rw-r--r--core/src/main/kotlin/Locations/Location.kt61
-rw-r--r--core/src/main/kotlin/Markdown/MarkdownProcessor.kt53
-rw-r--r--core/src/main/kotlin/Model/CodeNode.kt42
-rw-r--r--core/src/main/kotlin/Model/Content.kt296
-rw-r--r--core/src/main/kotlin/Model/DocumentationNode.kt311
-rw-r--r--core/src/main/kotlin/Model/DocumentationReference.kt86
-rw-r--r--core/src/main/kotlin/Model/ElementSignatureProvider.kt9
-rw-r--r--core/src/main/kotlin/Model/PackageDocs.kt135
-rw-r--r--core/src/main/kotlin/Model/SourceLinks.kt56
-rw-r--r--core/src/main/kotlin/Samples/DefaultSampleProcessingService.kt105
-rw-r--r--core/src/main/kotlin/Samples/DevsiteSampleProcessingService.kt52
-rw-r--r--core/src/main/kotlin/Samples/KotlinWebsiteSampleProcessingService.kt137
-rw-r--r--core/src/main/kotlin/Samples/SampleProcessingService.kt9
-rw-r--r--core/src/main/kotlin/Utilities/DokkaLogging.kt27
-rw-r--r--core/src/main/kotlin/Utilities/DokkaModules.kt81
-rw-r--r--core/src/main/kotlin/Utilities/DownloadSamples.kt88
-rw-r--r--core/src/main/kotlin/Utilities/Html.kt18
-rw-r--r--core/src/main/kotlin/Utilities/Path.kt5
-rw-r--r--core/src/main/kotlin/Utilities/SamplesPathsToURLs.kt26
-rw-r--r--core/src/main/kotlin/Utilities/ServiceLocator.kt100
-rw-r--r--core/src/main/kotlin/Utilities/StringExtensions.kt51
-rw-r--r--core/src/main/kotlin/Utilities/Uri.kt40
-rw-r--r--core/src/main/kotlin/javadoc/docbase.kt525
-rw-r--r--core/src/main/kotlin/javadoc/dokka-adapters.kt39
-rw-r--r--core/src/main/kotlin/javadoc/reporter.kt34
-rw-r--r--core/src/main/kotlin/javadoc/source-position.kt19
-rw-r--r--core/src/main/kotlin/javadoc/tags.kt240
-rw-r--r--core/src/main/resources/META-INF/MANIFEST.MF4
-rw-r--r--core/src/main/resources/dokka/format/dac-as-java.properties2
-rw-r--r--core/src/main/resources/dokka/format/dac.properties2
-rw-r--r--core/src/main/resources/dokka/format/gfm.properties2
-rw-r--r--core/src/main/resources/dokka/format/html-as-java.properties2
-rw-r--r--core/src/main/resources/dokka/format/html.properties2
-rw-r--r--core/src/main/resources/dokka/format/java-layout-html-as-java.properties2
-rw-r--r--core/src/main/resources/dokka/format/java-layout-html.properties2
-rw-r--r--core/src/main/resources/dokka/format/javadoc.properties2
-rw-r--r--core/src/main/resources/dokka/format/jekyll.properties2
-rw-r--r--core/src/main/resources/dokka/format/kotlin-website-html.properties2
-rw-r--r--core/src/main/resources/dokka/format/kotlin-website-samples.properties2
-rw-r--r--core/src/main/resources/dokka/format/kotlin-website.properties2
-rw-r--r--core/src/main/resources/dokka/format/markdown.properties2
-rw-r--r--core/src/main/resources/dokka/inbound-link-resolver/dokka-default.properties2
-rw-r--r--core/src/main/resources/dokka/inbound-link-resolver/java-layout-html.properties2
-rw-r--r--core/src/main/resources/dokka/inbound-link-resolver/javadoc.properties2
-rw-r--r--core/src/main/resources/dokka/styles/style.css283
-rw-r--r--core/src/test/kotlin/Model/CodeNodeTest.kt14
-rw-r--r--core/src/test/kotlin/TestAPI.kt302
-rw-r--r--core/src/test/kotlin/format/DacFormatTest.kt58
-rw-r--r--core/src/test/kotlin/format/DacFormatTestCase.kt90
-rw-r--r--core/src/test/kotlin/format/FileGeneratorTestCase.kt35
-rw-r--r--core/src/test/kotlin/format/GFMFormatTest.kt28
-rw-r--r--core/src/test/kotlin/format/HtmlFormatTest.kt182
-rw-r--r--core/src/test/kotlin/format/JavaLayoutHtmlFormatTest.kt114
-rw-r--r--core/src/test/kotlin/format/JavaLayoutHtmlFormatTestCase.kt117
-rw-r--r--core/src/test/kotlin/format/KotlinWebSiteFormatTest.kt74
-rw-r--r--core/src/test/kotlin/format/KotlinWebSiteHtmlFormatTest.kt85
-rw-r--r--core/src/test/kotlin/format/KotlinWebSiteRunnableSamplesFormatTest.kt39
-rw-r--r--core/src/test/kotlin/format/MarkdownFormatTest.kt547
-rw-r--r--core/src/test/kotlin/format/PackageDocsTest.kt92
-rw-r--r--core/src/test/kotlin/issues/IssuesTest.kt28
-rw-r--r--core/src/test/kotlin/javadoc/JavadocTest.kt185
-rw-r--r--core/src/test/kotlin/markdown/ParserTest.kt154
-rw-r--r--core/src/test/kotlin/model/ClassTest.kt293
-rw-r--r--core/src/test/kotlin/model/CommentTest.kt186
-rw-r--r--core/src/test/kotlin/model/FunctionTest.kt251
-rw-r--r--core/src/test/kotlin/model/JavaTest.kt219
-rw-r--r--core/src/test/kotlin/model/KotlinAsJavaTest.kt39
-rw-r--r--core/src/test/kotlin/model/LinkTest.kt75
-rw-r--r--core/src/test/kotlin/model/PackageTest.kt116
-rw-r--r--core/src/test/kotlin/model/PropertyTest.kt112
-rw-r--r--core/src/test/kotlin/model/TypeAliasTest.kt132
-rw-r--r--core/src/test/kotlin/utilities/StringExtensionsTest.kt119
-rw-r--r--core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
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("&nbsp;&nbsp;")
+ }
+ }
+ +"\n "
+ }
+
+ page.classesByFirstLetter.forEach { (letter, classes) ->
+ h2 {
+ id = "letter_$letter"
+ +letter
+ }
+ table {
+ tbody {
+ for (node in classes) {
+ tr {
+ td {
+ a(href = uriProvider.linkTo(node, uri)) { +node.classNodeNameWithOuterClass() }
+ }
+ td {
+ if (!deprecatedIndexSummary(node)) {
+ nodeSummary(node)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+
+ override fun FlowContent.classHierarchy(superclasses: List<DocumentationNode>) {
+ table(classes = "jd-inheritance-table") {
+ var level = superclasses.size
+ superclasses.forEach {
+ tr {
+ var spaceColumns = max(superclasses.size - 1 - level, 0)
+ while (spaceColumns > 0) {
+ td(classes = "jd-inheritance-space") {
+ +" "
+ }
+ spaceColumns--
+ }
+ if (it != superclasses.first()) {
+ td(classes = "jd-inheritance-space") {
+ +"   ↳"
+ }
+ }
+ td(classes = "jd-inheritance-class-cell") {
+ attributes["colSpan"] = "$level"
+ qualifiedTypeReference(it)
+ }
+ }
+ level--
+ }
+ }
+ }
+
+ override fun FlowContent.subclasses(inheritors: List<DocumentationNode>, direct: Boolean) {
+ if (inheritors.isEmpty()) return
+
+ // The number of subclasses in collapsed view before truncating and adding a "and xx others".
+ // See https://developer.android.com/reference/android/view/View for an example.
+ val numToShow = 12
+
+ table(classes = "jd-sumtable jd-sumtable-subclasses") {
+ tbody {
+ tr {
+ td {
+ div(classes = "expandable") {
+ span(classes = "expand-control") {
+ if (direct)
+ +"Known Direct Subclasses"
+ else
+ +"Known Indirect Subclasses"
+ }
+ div(classes = "showalways") {
+ attributes["id"] = if (direct) "subclasses-direct" else "subclasses-indirect"
+
+ inheritors.take(numToShow).forEach { inheritor ->
+ a(href = inheritor) { +inheritor.classNodeNameWithOuterClass() }
+ if (inheritor != inheritors.last()) +", "
+ }
+
+ if (inheritors.size > numToShow) {
+ +"and ${inheritors.size - numToShow} others."
+ }
+ }
+ div(classes = "exw-expanded-content") {
+ attributes["id"] = if (direct) "subclasses-direct-summary" else "subclasses-indirect-summary"
+ table(classes = "jd-sumtable-expando") {
+ inheritors.forEach { inheritor ->
+ tr(classes = "api api-level-${inheritor.apiLevel.name}") {
+ attributes["data-version-added"] = inheritor.apiLevel.name
+ td(classes = "jd-linkcol") {
+ a(href = inheritor) { +inheritor.classNodeNameWithOuterClass() }
+ }
+ td(classes = "jd-descrcol") {
+ attributes["width"] = "100%"
+ nodeSummary(inheritor)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ fun DocumentationNode.firstSentenceOfSummary(): ContentNode {
+
+ fun Sequence<ContentNode>.flatten(): Sequence<ContentNode> {
+ return flatMap {
+ when (it) {
+ is ContentParagraph -> it.children.asSequence().flatten()
+ else -> sequenceOf(it)
+ }
+ }
+ }
+
+ fun ContentNode.firstSentence(): ContentText? = when(this) {
+ is ContentText -> ContentText(text.firstSentence())
+ else -> null
+ }
+
+ val elements = sequenceOf(summary).flatten()
+ fun containsDot(it: ContentNode) = (it as? ContentText)?.text?.contains(".") == true
+
+ val paragraph = ContentParagraph()
+ (elements.takeWhile { !containsDot(it) } + elements.firstOrNull { containsDot(it) }?.firstSentence()).forEach {
+ if (it != null) {
+ paragraph.append(it)
+ }
+ }
+ if (paragraph.isEmpty()) {
+ return ContentEmpty
+ }
+
+ return paragraph
+ }
+
+ fun DocumentationNode.firstSentence(): String {
+ val sb = StringBuilder()
+ addContentNodeToStringBuilder(content, sb)
+ return sb.toString().firstSentence()
+ }
+
+ private fun addContentNodesToStringBuilder(content: List<ContentNode>, sb: StringBuilder): Unit =
+ content.forEach { addContentNodeToStringBuilder(it, sb) }
+
+ private fun addContentNodeToStringBuilder(content: ContentNode, sb: StringBuilder) {
+ when (content) {
+ is ContentText -> sb.appendWith(content.text)
+ is ContentSymbol -> sb.appendWith(content.text)
+ is ContentKeyword -> sb.appendWith(content.text)
+ is ContentIdentifier -> sb.appendWith(content.text)
+ is ContentEntity -> sb.appendWith(content.text)
+
+ is ContentHeading -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentStrong -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentStrikethrough -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentEmphasis -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentOrderedList -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentUnorderedList -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentListItem -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentCode -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentBlockSampleCode -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentBlockCode -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentParagraph -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentNodeLink -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentBookmark -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentExternalLink -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentLocalLink -> addContentNodesToStringBuilder(content.children, sb)
+ is ContentSection -> { }
+ is ContentBlock -> addContentNodesToStringBuilder(content.children, sb)
+ }
+ }
+
+ private fun StringBuilder.appendWith(text: String, delimiter: String = " ") {
+ if (this.length == 0) {
+ append(text)
+ } else {
+ append(delimiter)
+ append(text)
+ }
+ }
+}
+
+fun TBODY.developerHeading(header: String) {
+ tr {
+ th {
+ attributes["colSpan"] = "2"
+ +header
+ }
+ }
+}
+
+class DacFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin {
+ override val templateServiceClass: KClass<out JavaLayoutHtmlTemplateService> = DevsiteHtmlTemplateService::class
+
+ override val outlineFactoryClass = DacOutlineFormatter::class
+ override val languageServiceClass = KotlinLanguageService::class
+ override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class
+ override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl::class
+ override val sampleProcessingService = DevsiteSampleProcessingService::class
+}
+
+
+class DacAsJavaFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava {
+ override val templateServiceClass: KClass<out JavaLayoutHtmlTemplateService> = DevsiteHtmlTemplateService::class
+
+ override val outlineFactoryClass = DacOutlineFormatter::class
+ override val languageServiceClass = NewJavaLanguageService::class
+ override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class
+ override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = DevsiteLayoutHtmlFormatOutputBuilderFactoryImpl::class
+}
diff --git a/core/src/main/kotlin/Formats/DacOutlineService.kt b/core/src/main/kotlin/Formats/DacOutlineService.kt
new file mode 100644
index 000000000..e249c39f7
--- /dev/null
+++ b/core/src/main/kotlin/Formats/DacOutlineService.kt
@@ -0,0 +1,395 @@
+package org.jetbrains.dokka.Formats
+
+import com.google.inject.Inject
+import org.jetbrains.dokka.*
+import java.net.URI
+import com.google.inject.name.Named
+import org.jetbrains.kotlin.cfg.pseudocode.AllTypes
+
+
+interface DacOutlineFormatService {
+ fun computeOutlineURI(node: DocumentationNode): URI
+ fun format(to: Appendable, node: DocumentationNode)
+}
+
+class DacOutlineFormatter @Inject constructor(
+ uriProvider: JavaLayoutHtmlUriProvider,
+ languageService: LanguageService,
+ @Named("dacRoot") dacRoot: String,
+ @Named("generateClassIndex") generateClassIndex: Boolean,
+ @Named("generatePackageIndex") generatePackageIndex: Boolean
+) : JavaLayoutHtmlFormatOutlineFactoryService {
+ val tocOutline = TocOutlineService(uriProvider, languageService, dacRoot, generateClassIndex, generatePackageIndex)
+ val outlines = listOf(tocOutline)
+
+ override fun generateOutlines(outputProvider: (URI) -> Appendable, nodes: Iterable<DocumentationNode>) {
+ for (node in nodes) {
+ for (outline in outlines) {
+ val uri = outline.computeOutlineURI(node)
+ val output = outputProvider(uri)
+ outline.format(output, node)
+ }
+ }
+ }
+}
+
+/**
+ * Outline service for generating a _toc.yaml file, responsible for pointing to the paths of each
+ * index.html file in the doc tree.
+ */
+class BookOutlineService(
+ val uriProvider: JavaLayoutHtmlUriProvider,
+ val languageService: LanguageService,
+ val dacRoot: String,
+ val generateClassIndex: Boolean,
+ val generatePackageIndex: Boolean
+) : DacOutlineFormatService {
+ override fun computeOutlineURI(node: DocumentationNode): URI = uriProvider.outlineRootUri(node).resolve("_book.yaml")
+
+ override fun format(to: Appendable, node: DocumentationNode) {
+ appendOutline(to, listOf(node))
+ }
+
+ var outlineLevel = 0
+
+ /** Appends formatted outline to [StringBuilder](to) using specified [location] */
+ fun appendOutline(to: Appendable, nodes: Iterable<DocumentationNode>) {
+ if (outlineLevel == 0) to.appendln("reference:")
+ for (node in nodes) {
+ appendOutlineHeader(node, to)
+ val subPackages = node.members.filter {
+ it.kind == NodeKind.Package
+ }
+ if (subPackages.any()) {
+ val sortedMembers = subPackages.sortedBy { it.name.toLowerCase() }
+ appendOutlineLevel(to) {
+ appendOutline(to, sortedMembers)
+ }
+ }
+
+ }
+ }
+
+ fun appendOutlineHeader(node: DocumentationNode, to: Appendable) {
+ if (node is DocumentationModule) {
+ to.appendln("- title: Package Index")
+ to.appendln(" path: $dacRoot${uriProvider.outlineRootUri(node).resolve("packages.html")}")
+ to.appendln(" status_text: no-toggle")
+ } else {
+ to.appendln("- title: ${languageService.renderName(node)}")
+ to.appendln(" path: $dacRoot${uriProvider.mainUriOrWarn(node)}")
+ to.appendln(" status_text: no-toggle")
+ }
+ }
+
+ fun appendOutlineLevel(to: Appendable, body: () -> Unit) {
+ outlineLevel++
+ body()
+ outlineLevel--
+ }
+}
+
+/**
+ * Outline service for generating a _toc.yaml file, responsible for pointing to the paths of each
+ * index.html file in the doc tree.
+ */
+class TocOutlineService(
+ val uriProvider: JavaLayoutHtmlUriProvider,
+ val languageService: LanguageService,
+ val dacRoot: String,
+ val generateClassIndex: Boolean,
+ val generatePackageIndex: Boolean
+) : DacOutlineFormatService {
+ override fun computeOutlineURI(node: DocumentationNode): URI = uriProvider.outlineRootUri(node).resolve("_toc.yaml")
+
+ override fun format(to: Appendable, node: DocumentationNode) {
+ appendOutline(to, listOf(node))
+ }
+
+ var outlineLevel = 0
+
+ /** Appends formatted outline to [StringBuilder](to) using specified [location] */
+ fun appendOutline(to: Appendable, nodes: Iterable<DocumentationNode>) {
+ if (outlineLevel == 0) to.appendln("toc:")
+ for (node in nodes) {
+ appendOutlineHeader(node, to)
+ val subPackages = node.members.filter {
+ it.kind == NodeKind.Package
+ }
+ if (subPackages.any()) {
+ val sortedMembers = subPackages.sortedBy { it.nameWithOuterClass() }
+ appendOutlineLevel {
+ appendOutline(to, sortedMembers)
+ }
+ }
+ }
+ }
+
+ fun appendOutlineHeader(node: DocumentationNode, to: Appendable) {
+ if (node is DocumentationModule) {
+ if (generateClassIndex) {
+ node.members.filter { it.kind == NodeKind.AllTypes }.firstOrNull()?.let {
+ to.appendln("- title: Class Index")
+ to.appendln(" path: $dacRoot${uriProvider.outlineRootUri(it).resolve("classes.html")}")
+ to.appendln()
+ }
+ }
+ if (generatePackageIndex) {
+ to.appendln("- title: Package Index")
+ to.appendln(" path: $dacRoot${uriProvider.outlineRootUri(node).resolve("packages.html")}")
+ to.appendln()
+ }
+ } else if (node.kind != NodeKind.AllTypes && !(node is DocumentationModule)) {
+ to.appendln("- title: ${languageService.renderName(node)}")
+ to.appendln(" path: $dacRoot${uriProvider.mainUriOrWarn(node)}")
+ to.appendln()
+ var addedSectionHeader = false
+ for (kind in NodeKind.classLike) {
+ val members = node.getMembersOfKinds(kind)
+ if (members.isNotEmpty()) {
+ if (!addedSectionHeader) {
+ to.appendln(" section:")
+ addedSectionHeader = true
+ }
+ to.appendln(" - title: ${kind.pluralizedName()}")
+ to.appendln()
+ to.appendln(" section:")
+ members.sortedBy { it.nameWithOuterClass().toLowerCase() }.forEach { member ->
+ to.appendln(" - title: ${languageService.renderNameWithOuterClass(member)}")
+ to.appendln(" path: $dacRoot${uriProvider.mainUriOrWarn(member)}".trimEnd('#'))
+ to.appendln()
+ }
+ }
+ }
+ to.appendln().appendln()
+ }
+ }
+
+ fun appendOutlineLevel(body: () -> Unit) {
+ outlineLevel++
+ body()
+ outlineLevel--
+ }
+}
+
+class DacNavOutlineService constructor(
+ val uriProvider: JavaLayoutHtmlUriProvider,
+ val languageService: LanguageService,
+ val dacRoot: String
+) : DacOutlineFormatService {
+ override fun computeOutlineURI(node: DocumentationNode): URI =
+ uriProvider.outlineRootUri(node).resolve("navtree_data.js")
+
+ override fun format(to: Appendable, node: DocumentationNode) {
+ to.append("var NAVTREE_DATA = ").appendNavTree(node.members).append(";")
+ }
+
+ private fun Appendable.appendNavTree(nodes: Iterable<DocumentationNode>): Appendable {
+ append("[ ")
+ var first = true
+ for (node in nodes) {
+ if (!first) append(", ")
+ first = false
+ val interfaces = node.getMembersOfKinds(NodeKind.Interface)
+ val classes = node.getMembersOfKinds(NodeKind.Class)
+ val objects = node.getMembersOfKinds(NodeKind.Object)
+ val annotations = node.getMembersOfKinds(NodeKind.AnnotationClass)
+ val enums = node.getMembersOfKinds(NodeKind.Enum)
+ val exceptions = node.getMembersOfKinds(NodeKind.Exception)
+
+ append("[ \"${node.name}\", \"$dacRoot${uriProvider.tryGetMainUri(node)}\", [ ")
+ var needComma = false
+ if (interfaces.firstOrNull() != null) {
+ appendNavTreePagesOfKind("Interfaces", interfaces)
+ needComma = true
+ }
+ if (classes.firstOrNull() != null) {
+ if (needComma) append(", ")
+ appendNavTreePagesOfKind("Classes", classes)
+ needComma = true
+ }
+ if (objects.firstOrNull() != null) {
+ if (needComma) append(", ")
+ appendNavTreePagesOfKind("Objects", objects)
+ }
+ if (annotations.firstOrNull() != null) {
+ if (needComma) append(", ")
+ appendNavTreePagesOfKind("Annotations", annotations)
+ needComma = true
+ }
+ if (enums.firstOrNull() != null) {
+ if (needComma) append(", ")
+ appendNavTreePagesOfKind("Enums", enums)
+ needComma = true
+ }
+ if (exceptions.firstOrNull() != null) {
+ if (needComma) append(", ")
+ appendNavTreePagesOfKind("Exceptions", exceptions)
+ }
+ append(" ] ]")
+ }
+ append(" ]")
+ return this
+ }
+
+ private fun Appendable.appendNavTreePagesOfKind(kindTitle: String,
+ nodesOfKind: Iterable<DocumentationNode>): Appendable {
+ append("[ \"$kindTitle\", null, [ ")
+ var started = false
+ for (node in nodesOfKind) {
+ if (started) append(", ")
+ started = true
+ appendNavTreeChild(node)
+ }
+ append(" ], null, null ]")
+ return this
+ }
+
+ private fun Appendable.appendNavTreeChild(node: DocumentationNode): Appendable {
+ append("[ \"${node.nameWithOuterClass()}\", \"${dacRoot}${uriProvider.tryGetMainUri(node)}\"")
+ append(", null, null, null ]")
+ return this
+ }
+}
+
+class DacSearchOutlineService(
+ val uriProvider: JavaLayoutHtmlUriProvider,
+ val languageService: LanguageService,
+ val dacRoot: String
+) : DacOutlineFormatService {
+
+ override fun computeOutlineURI(node: DocumentationNode): URI =
+ uriProvider.outlineRootUri(node).resolve("lists.js")
+
+ override fun format(to: Appendable, node: DocumentationNode) {
+ val pageNodes = node.getAllPageNodes()
+ var id = 0
+ to.append("var KTX_CORE_DATA = [\n")
+ var first = true
+ for (pageNode in pageNodes) {
+ if (pageNode.kind == NodeKind.Module) continue
+ if (!first) to.append(", \n")
+ first = false
+ to.append(" { " +
+ "id:$id, " +
+ "label:\"${pageNode.qualifiedName()}\", " +
+ "link:\"${dacRoot}${uriProvider.tryGetMainUri(pageNode)}\", " +
+ "type:\"${pageNode.getClassOrPackage()}\", " +
+ "deprecated:\"false\" }")
+ id++
+ }
+ to.append("\n];")
+ }
+
+ private fun DocumentationNode.getClassOrPackage(): String =
+ if (hasOwnPage())
+ "class"
+ else if (isPackage()) {
+ "package"
+ } else {
+ ""
+ }
+
+ private fun DocumentationNode.getAllPageNodes(): Iterable<DocumentationNode> {
+ val allPageNodes = mutableListOf<DocumentationNode>()
+ recursiveSetAllPageNodes(allPageNodes)
+ return allPageNodes
+ }
+
+ private fun DocumentationNode.recursiveSetAllPageNodes(
+ allPageNodes: MutableList<DocumentationNode>) {
+ for (child in members) {
+ if (child.hasOwnPage() || child.isPackage()) {
+ allPageNodes.add(child)
+ child.qualifiedName()
+ child.recursiveSetAllPageNodes(allPageNodes)
+ }
+ }
+ }
+
+}
+
+/**
+ * Return all children of the node who are one of the selected `NodeKind`s. It recursively fetches
+ * all offspring, not just immediate children.
+ */
+fun DocumentationNode.getMembersOfKinds(vararg kinds: NodeKind): MutableList<DocumentationNode> {
+ val membersOfKind = mutableListOf<DocumentationNode>()
+ recursiveSetMembersOfKinds(kinds, membersOfKind)
+ return membersOfKind
+}
+
+private fun DocumentationNode.recursiveSetMembersOfKinds(kinds: Array<out NodeKind>,
+ membersOfKind: MutableList<DocumentationNode>) {
+ for (member in members) {
+ if (member.kind in kinds) {
+ membersOfKind.add(member)
+ }
+ member.recursiveSetMembersOfKinds(kinds, membersOfKind)
+ }
+}
+
+/**
+ * Returns whether or not this node owns a page. The criteria for whether a node owns a page is
+ * similar to the way javadoc is structured. Classes, Interfaces, Enums, AnnotationClasses,
+ * Exceptions, and Objects (Kotlin-specific) meet the criteria.
+ */
+fun DocumentationNode.hasOwnPage() =
+ kind == NodeKind.Class || kind == NodeKind.Interface || kind == NodeKind.Enum ||
+ kind == NodeKind.AnnotationClass || kind == NodeKind.Exception ||
+ kind == NodeKind.Object
+
+/**
+ * In most cases, this returns the short name of the `Type`. When the Type is an inner Type, it
+ * prepends the name with the containing Type name(s).
+ *
+ * For example, if you have a class named OuterClass and an inner class named InnerClass, this would
+ * return OuterClass.InnerClass.
+ *
+ */
+fun DocumentationNode.nameWithOuterClass(): String {
+ val nameBuilder = StringBuilder(name)
+ var parent = owner
+ if (hasOwnPage()) {
+ while (parent != null && parent.hasOwnPage()) {
+ nameBuilder.insert(0, "${parent.name}.")
+ parent = parent.owner
+ }
+ }
+ return nameBuilder.toString()
+}
+
+/**
+ * Return whether the node is a package.
+ */
+fun DocumentationNode.isPackage(): Boolean {
+ return kind == NodeKind.Package
+}
+
+/**
+ * Return the 'page owner' of this node. `DocumentationNode.hasOwnPage()` defines the criteria for
+ * a page owner. If this node is not a page owner, then it iterates up through its ancestors to
+ * find the first page owner.
+ */
+fun DocumentationNode.pageOwner(): DocumentationNode {
+ if (hasOwnPage() || owner == null) {
+ return this
+ } else {
+ var parent: DocumentationNode = owner!!
+ while (!parent.hasOwnPage() && !parent.isPackage()) {
+ parent = parent.owner!!
+ }
+ return parent
+ }
+}
+
+fun NodeKind.pluralizedName() = when(this) {
+ NodeKind.Class -> "Classes"
+ NodeKind.Interface -> "Interfaces"
+ NodeKind.AnnotationClass -> "Annotations"
+ NodeKind.Enum -> "Enums"
+ NodeKind.Exception -> "Exceptions"
+ NodeKind.Object -> "Objects"
+ NodeKind.TypeAlias -> "TypeAliases"
+ else -> "${name}s"
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/Formats/ExtraOutlineServices.kt b/core/src/main/kotlin/Formats/ExtraOutlineServices.kt
new file mode 100644
index 000000000..e4eeac01a
--- /dev/null
+++ b/core/src/main/kotlin/Formats/ExtraOutlineServices.kt
@@ -0,0 +1,20 @@
+package org.jetbrains.dokka
+
+import java.io.File
+
+/**
+ * Outline service that is responsible for generating a single outline format.
+ *
+ * TODO: port existing implementations of ExtraOutlineService to OutlineService, and remove this.
+ */
+interface ExtraOutlineService {
+ fun getFileName(): String
+ fun getFile(location: Location): File
+ fun format(node: DocumentationNode): String
+}
+
+/**
+ * Holder of all of the extra outline services needed for a StandardFormat, in addition to the main
+ * [OutlineFormatService].
+ */
+abstract class ExtraOutlineServices(vararg val services: ExtraOutlineService)
diff --git a/core/src/main/kotlin/Formats/FormatDescriptor.kt b/core/src/main/kotlin/Formats/FormatDescriptor.kt
new file mode 100644
index 000000000..b497fb0f5
--- /dev/null
+++ b/core/src/main/kotlin/Formats/FormatDescriptor.kt
@@ -0,0 +1,42 @@
+package org.jetbrains.dokka.Formats
+
+import com.google.inject.Binder
+import org.jetbrains.dokka.*
+import org.jetbrains.dokka.Utilities.bind
+import org.jetbrains.dokka.Utilities.lazyBind
+import org.jetbrains.dokka.Utilities.toOptional
+import org.jetbrains.dokka.Utilities.toType
+import kotlin.reflect.KClass
+
+
+interface FormatDescriptorAnalysisComponent {
+ fun configureAnalysis(binder: Binder)
+}
+
+interface FormatDescriptorOutputComponent {
+ fun configureOutput(binder: Binder)
+}
+
+interface FormatDescriptor : FormatDescriptorAnalysisComponent, FormatDescriptorOutputComponent
+
+
+abstract class FileGeneratorBasedFormatDescriptor : FormatDescriptor {
+
+ override fun configureOutput(binder: Binder): Unit = with(binder) {
+ bind<Generator>() toType NodeLocationAwareGenerator::class
+ bind<NodeLocationAwareGenerator>() toType generatorServiceClass
+
+ bind<LanguageService>() toType languageServiceClass
+
+ lazyBind<OutlineFormatService>() toOptional (outlineServiceClass)
+ lazyBind<FormatService>() toOptional formatServiceClass
+ lazyBind<PackageListService>() toOptional packageListServiceClass
+ }
+
+ abstract val formatServiceClass: KClass<out FormatService>?
+ abstract val outlineServiceClass: KClass<out OutlineFormatService>?
+ abstract val generatorServiceClass: KClass<out FileGenerator>
+ abstract val packageListServiceClass: KClass<out PackageListService>?
+
+ open val languageServiceClass: KClass<out LanguageService> = KotlinLanguageService::class
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/Formats/FormatService.kt b/core/src/main/kotlin/Formats/FormatService.kt
new file mode 100644
index 000000000..63f25008f
--- /dev/null
+++ b/core/src/main/kotlin/Formats/FormatService.kt
@@ -0,0 +1,32 @@
+package org.jetbrains.dokka
+
+/**
+ * Abstract representation of a formatting service used to output documentation in desired format
+ *
+ * Bundled Formatters:
+ * * [HtmlFormatService] – outputs documentation to HTML format
+ * * [MarkdownFormatService] – outputs documentation in Markdown format
+ */
+interface FormatService {
+ /** Returns extension for output files */
+ val extension: String
+
+ /** extension which will be used for internal and external linking */
+ val linkExtension: String
+ get() = extension
+
+ fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder
+
+ fun enumerateSupportFiles(callback: (resource: String, targetPath: String) -> Unit) {
+ }
+}
+
+interface FormattedOutputBuilder {
+ /** Appends formatted content to [StringBuilder](to) using specified [location] */
+ fun appendNodes(nodes: Iterable<DocumentationNode>)
+}
+
+/** Format content to [String] using specified [location] */
+fun FormatService.format(location: Location, nodes: Iterable<DocumentationNode>): String = StringBuilder().apply {
+ createOutputBuilder(this, location).appendNodes(nodes)
+}.toString()
diff --git a/core/src/main/kotlin/Formats/GFMFormatService.kt b/core/src/main/kotlin/Formats/GFMFormatService.kt
new file mode 100644
index 000000000..036ec8564
--- /dev/null
+++ b/core/src/main/kotlin/Formats/GFMFormatService.kt
@@ -0,0 +1,61 @@
+package org.jetbrains.dokka
+
+import com.google.inject.Inject
+import com.google.inject.name.Named
+import org.jetbrains.dokka.Utilities.impliedPlatformsName
+
+open class GFMOutputBuilder(
+ to: StringBuilder,
+ location: Location,
+ generator: NodeLocationAwareGenerator,
+ languageService: LanguageService,
+ extension: String,
+ impliedPlatforms: List<String>
+) : MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) {
+ override fun appendTable(vararg columns: String, body: () -> Unit) {
+ to.appendln(columns.joinToString(" | ", "| ", " |"))
+ to.appendln("|" + "---|".repeat(columns.size))
+ body()
+ }
+
+ override fun appendUnorderedList(body: () -> Unit) {
+ if (inTableCell) {
+ wrapInTag("ul", body)
+ } else {
+ super.appendUnorderedList(body)
+ }
+ }
+
+ override fun appendOrderedList(body: () -> Unit) {
+ if (inTableCell) {
+ wrapInTag("ol", body)
+ } else {
+ super.appendOrderedList(body)
+ }
+ }
+
+ override fun appendListItem(body: () -> Unit) {
+ if (inTableCell) {
+ wrapInTag("li", body)
+ } else {
+ super.appendListItem(body)
+ }
+ }
+}
+
+open class GFMFormatService(
+ generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ linkExtension: String,
+ impliedPlatforms: List<String>
+) : MarkdownFormatService(generator, signatureGenerator, linkExtension, impliedPlatforms) {
+
+ @Inject constructor(
+ generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ @Named(impliedPlatformsName) impliedPlatforms: List<String>
+ ) : this(generator, signatureGenerator, "md", impliedPlatforms)
+
+ override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder =
+ GFMOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms)
+}
diff --git a/core/src/main/kotlin/Formats/HtmlFormatService.kt b/core/src/main/kotlin/Formats/HtmlFormatService.kt
new file mode 100644
index 000000000..0ad946be2
--- /dev/null
+++ b/core/src/main/kotlin/Formats/HtmlFormatService.kt
@@ -0,0 +1,168 @@
+package org.jetbrains.dokka
+
+import com.google.inject.Inject
+import com.google.inject.name.Named
+import org.jetbrains.dokka.Utilities.impliedPlatformsName
+import java.io.File
+
+open class HtmlOutputBuilder(to: StringBuilder,
+ location: Location,
+ generator: NodeLocationAwareGenerator,
+ languageService: LanguageService,
+ extension: String,
+ impliedPlatforms: List<String>,
+ val templateService: HtmlTemplateService)
+ : StructuredOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms)
+{
+ override fun appendText(text: String) {
+ to.append(text.htmlEscape())
+ }
+
+ override fun appendSymbol(text: String) {
+ to.append("<span class=\"symbol\">${text.htmlEscape()}</span>")
+ }
+
+ override fun appendKeyword(text: String) {
+ to.append("<span class=\"keyword\">${text.htmlEscape()}</span>")
+ }
+
+ override fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) {
+ val id = signature?.let { " id=\"$it\"" }.orEmpty()
+ to.append("<span class=\"identifier\"$id>${text.htmlEscape()}</span>")
+ }
+
+ override fun appendBlockCode(language: String, body: () -> Unit) {
+ val openTags = if (language.isNotBlank())
+ "<pre><code class=\"lang-$language\">"
+ else
+ "<pre><code>"
+ wrap(openTags, "</code></pre>", body)
+ }
+
+ override fun appendHeader(level: Int, body: () -> Unit) =
+ wrapInTag("h$level", body, newlineBeforeOpen = true, newlineAfterClose = true)
+ override fun appendParagraph(body: () -> Unit) =
+ wrapInTag("p", body, newlineBeforeOpen = true, newlineAfterClose = true)
+
+ override fun appendSoftParagraph(body: () -> Unit) = appendParagraph(body)
+
+ override fun appendLine() {
+ to.appendln("<br/>")
+ }
+
+ override fun appendAnchor(anchor: String) {
+ to.appendln("<a name=\"${anchor.htmlEscape()}\"></a>")
+ }
+
+ override fun appendTable(vararg columns: String, body: () -> Unit) =
+ wrapInTag("table", body, newlineAfterOpen = true, newlineAfterClose = true)
+ override fun appendTableBody(body: () -> Unit) =
+ wrapInTag("tbody", body, newlineAfterOpen = true, newlineAfterClose = true)
+ override fun appendTableRow(body: () -> Unit) =
+ wrapInTag("tr", body, newlineAfterOpen = true, newlineAfterClose = true)
+ override fun appendTableCell(body: () -> Unit) =
+ wrapInTag("td", body, newlineAfterOpen = true, newlineAfterClose = true)
+
+ override fun appendLink(href: String, body: () -> Unit) = wrap("<a href=\"$href\">", "</a>", body)
+
+ override fun appendStrong(body: () -> Unit) = wrapInTag("strong", body)
+ override fun appendEmphasis(body: () -> Unit) = wrapInTag("em", body)
+ override fun appendStrikethrough(body: () -> Unit) = wrapInTag("s", body)
+ override fun appendCode(body: () -> Unit) = wrapInTag("code", body)
+
+ override fun appendUnorderedList(body: () -> Unit) = wrapInTag("ul", body, newlineAfterClose = true)
+ override fun appendOrderedList(body: () -> Unit) = wrapInTag("ol", body, newlineAfterClose = true)
+ override fun appendListItem(body: () -> Unit) = wrapInTag("li", body, newlineAfterClose = true)
+
+ override fun appendBreadcrumbSeparator() {
+ to.append("&nbsp;/&nbsp;")
+ }
+
+ override fun appendNodes(nodes: Iterable<DocumentationNode>) {
+ templateService.appendHeader(to, getPageTitle(nodes), generator.relativePathToRoot(location))
+ super.appendNodes(nodes)
+ templateService.appendFooter(to)
+ }
+
+ override fun appendNonBreakingSpace() {
+ to.append("&nbsp;")
+ }
+
+ override fun ensureParagraph() {
+
+ }
+}
+
+open class HtmlFormatService @Inject constructor(generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ val templateService: HtmlTemplateService,
+ @Named(impliedPlatformsName) val impliedPlatforms: List<String>)
+: StructuredFormatService(generator, signatureGenerator, "html"), OutlineFormatService {
+
+ override fun enumerateSupportFiles(callback: (String, String) -> Unit) {
+ callback("/dokka/styles/style.css", "style.css")
+ }
+
+ override fun createOutputBuilder(to: StringBuilder, location: Location) =
+ HtmlOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms, templateService)
+
+ override fun appendOutline(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) {
+ templateService.appendHeader(to, "Module Contents", generator.relativePathToRoot(location))
+ super.appendOutline(location, to, nodes)
+ templateService.appendFooter(to)
+ }
+
+ override fun getOutlineFileName(location: Location): File {
+ return File("${location.path}-outline.html")
+ }
+
+ override fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) {
+ val link = ContentNodeDirectLink(node)
+ link.append(languageService.render(node, LanguageService.RenderMode.FULL))
+ val tempBuilder = StringBuilder()
+ createOutputBuilder(tempBuilder, location).appendContent(link)
+ to.appendln("<a href=\"${location.path}\">$tempBuilder</a><br/>")
+ }
+
+ override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) {
+ to.appendln("<ul>")
+ body()
+ to.appendln("</ul>")
+ }
+}
+
+fun getPageTitle(nodes: Iterable<DocumentationNode>): String? {
+ val breakdownByLocation = nodes.groupBy { node -> formatPageTitle(node) }
+ return breakdownByLocation.keys.singleOrNull()
+}
+
+fun formatPageTitle(node: DocumentationNode): String {
+ val path = node.path
+ val moduleName = path.first().name
+ if (path.size == 1) {
+ return moduleName
+ }
+
+ val qName = qualifiedNameForPageTitle(node)
+ return qName + " - " + moduleName
+}
+
+private fun qualifiedNameForPageTitle(node: DocumentationNode): String {
+ if (node.kind == NodeKind.Package) {
+ var packageName = node.qualifiedName()
+ if (packageName.isEmpty()) {
+ packageName = "root package"
+ }
+ return packageName
+ }
+
+ val path = node.path
+ var pathFromToplevelMember = path.dropWhile { it.kind !in NodeKind.classLike }
+ if (pathFromToplevelMember.isEmpty()) {
+ pathFromToplevelMember = path.dropWhile { it.kind != NodeKind.Property && it.kind != NodeKind.Function }
+ }
+ if (pathFromToplevelMember.isNotEmpty()) {
+ return pathFromToplevelMember.map { it.name }.filter { it.length > 0 }.joinToString(".")
+ }
+ return node.qualifiedName()
+}
diff --git a/core/src/main/kotlin/Formats/HtmlTemplateService.kt b/core/src/main/kotlin/Formats/HtmlTemplateService.kt
new file mode 100644
index 000000000..a65a7b18c
--- /dev/null
+++ b/core/src/main/kotlin/Formats/HtmlTemplateService.kt
@@ -0,0 +1,38 @@
+package org.jetbrains.dokka
+
+import java.io.File
+
+interface HtmlTemplateService {
+ fun appendHeader(to: StringBuilder, title: String?, basePath: File)
+ fun appendFooter(to: StringBuilder)
+
+ companion object {
+ fun default(css: String? = null): HtmlTemplateService {
+ return object : HtmlTemplateService {
+ override fun appendFooter(to: StringBuilder) {
+ if (!to.endsWith('\n')) {
+ to.append('\n')
+ }
+ to.appendln("</BODY>")
+ to.appendln("</HTML>")
+ }
+ override fun appendHeader(to: StringBuilder, title: String?, basePath: File) {
+ to.appendln("<HTML>")
+ to.appendln("<HEAD>")
+ to.appendln("<meta charset=\"UTF-8\">")
+ if (title != null) {
+ to.appendln("<title>$title</title>")
+ }
+ if (css != null) {
+ val cssPath = basePath.resolve(css).toUnixString()
+ to.appendln("<link rel=\"stylesheet\" href=\"$cssPath\">")
+ }
+ to.appendln("</HEAD>")
+ to.appendln("<BODY>")
+ }
+ }
+ }
+ }
+}
+
+
diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt
new file mode 100644
index 000000000..b94886693
--- /dev/null
+++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormat.kt
@@ -0,0 +1,141 @@
+package org.jetbrains.dokka.Formats
+
+import com.google.inject.Binder
+import kotlinx.html.*
+import org.jetbrains.dokka.*
+import org.jetbrains.dokka.Utilities.bind
+import org.jetbrains.dokka.Utilities.lazyBind
+import org.jetbrains.dokka.Utilities.toOptional
+import org.jetbrains.dokka.Utilities.toType
+import java.net.URI
+import kotlin.reflect.KClass
+
+
+abstract class JavaLayoutHtmlFormatDescriptorBase : FormatDescriptor, DefaultAnalysisComponent {
+
+ override fun configureOutput(binder: Binder): Unit = with(binder) {
+ bind<Generator>() toType generatorServiceClass
+ bind<LanguageService>() toType languageServiceClass
+ bind<JavaLayoutHtmlTemplateService>() toType templateServiceClass
+ bind<JavaLayoutHtmlUriProvider>() toType generatorServiceClass
+ lazyBind<JavaLayoutHtmlFormatOutlineFactoryService>() toOptional outlineFactoryClass
+ bind<PackageListService>() toType packageListServiceClass
+ bind<JavaLayoutHtmlFormatOutputBuilderFactory>() toType outputBuilderFactoryClass
+ }
+
+ val generatorServiceClass = JavaLayoutHtmlFormatGenerator::class
+ abstract val languageServiceClass: KClass<out LanguageService>
+ abstract val templateServiceClass: KClass<out JavaLayoutHtmlTemplateService>
+ abstract val outlineFactoryClass: KClass<out JavaLayoutHtmlFormatOutlineFactoryService>?
+ abstract val packageListServiceClass: KClass<out PackageListService>
+ abstract val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory>
+}
+
+class JavaLayoutHtmlFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin {
+ override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = JavaLayoutHtmlFormatOutputBuilderFactoryImpl::class
+ override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class
+ override val languageServiceClass = KotlinLanguageService::class
+ override val templateServiceClass = JavaLayoutHtmlTemplateService.Default::class
+ override val outlineFactoryClass = null
+}
+
+class JavaLayoutHtmlAsJavaFormatDescriptor : JavaLayoutHtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava {
+ override val outputBuilderFactoryClass: KClass<out JavaLayoutHtmlFormatOutputBuilderFactory> = JavaLayoutHtmlFormatOutputBuilderFactoryImpl::class
+ override val packageListServiceClass: KClass<out PackageListService> = JavaLayoutHtmlPackageListService::class
+ override val languageServiceClass = NewJavaLanguageService::class
+ override val templateServiceClass = JavaLayoutHtmlTemplateService.Default::class
+ override val outlineFactoryClass = null
+}
+
+interface JavaLayoutHtmlFormatOutlineFactoryService {
+ fun generateOutlines(outputProvider: (URI) -> Appendable, nodes: Iterable<DocumentationNode>)
+}
+
+
+interface JavaLayoutHtmlUriProvider {
+ fun tryGetContainerUri(node: DocumentationNode): URI?
+ fun tryGetMainUri(node: DocumentationNode): URI?
+ fun tryGetOutlineRootUri(node: DocumentationNode): URI?
+ fun containerUri(node: DocumentationNode): URI = tryGetContainerUri(node) ?: error("Unsupported ${node.kind}")
+ fun mainUri(node: DocumentationNode): URI = tryGetMainUri(node) ?: error("Unsupported ${node.kind}")
+ fun outlineRootUri(node: DocumentationNode): URI = tryGetOutlineRootUri(node) ?: error("Unsupported ${node.kind}")
+
+
+ fun linkTo(to: DocumentationNode, from: URI): String {
+ return mainUri(to).relativeTo(from).toString()
+ }
+
+ fun linkToFromOutline(to: DocumentationNode, from: URI): String {
+ return outlineRootUri(to).relativeTo(from).toString()
+ }
+
+ fun mainUriOrWarn(node: DocumentationNode): URI? = tryGetMainUri(node) ?: (null).also {
+ AssertionError("Not implemented mainUri for ${node.kind} (${node})").printStackTrace()
+ }
+}
+
+
+interface JavaLayoutHtmlTemplateService {
+ fun composePage(
+ page: JavaLayoutHtmlFormatOutputBuilder.Page,
+ tagConsumer: TagConsumer<Appendable>,
+ headContent: HEAD.() -> Unit,
+ bodyContent: BODY.() -> Unit
+ )
+
+ class Default : JavaLayoutHtmlTemplateService {
+ override fun composePage(
+ page: JavaLayoutHtmlFormatOutputBuilder.Page,
+ tagConsumer: TagConsumer<Appendable>,
+ headContent: HEAD.() -> Unit,
+ bodyContent: BODY.() -> Unit
+ ) {
+ tagConsumer.html {
+ head {
+ meta(charset = "UTF-8")
+ headContent()
+ }
+ body(block = bodyContent)
+ }
+ }
+ }
+}
+
+val DocumentationNode.companion get() = members(NodeKind.Object).find { it.details(NodeKind.Modifier).any { it.name == "companion" } }
+
+fun DocumentationNode.signatureForAnchor(logger: DokkaLogger): String {
+
+ fun StringBuilder.appendReceiverIfSo() {
+ detailOrNull(NodeKind.Receiver)?.let {
+ append("(")
+ append(it.detail(NodeKind.Type).qualifiedNameFromType())
+ append(").")
+ }
+ }
+
+ return when (kind) {
+ NodeKind.Function, NodeKind.Constructor, NodeKind.CompanionObjectFunction -> buildString {
+ if (kind == NodeKind.CompanionObjectFunction) {
+ append("Companion.")
+ }
+ appendReceiverIfSo()
+ append(prettyName)
+ details(NodeKind.Parameter).joinTo(this, prefix = "(", postfix = ")") { it.detail(NodeKind.Type).qualifiedNameFromType() }
+ }
+ NodeKind.Property, NodeKind.CompanionObjectProperty -> buildString {
+ if (kind == NodeKind.CompanionObjectProperty) {
+ append("Companion.")
+ }
+ appendReceiverIfSo()
+ append(name)
+ append(":")
+ append(detail(NodeKind.Type).qualifiedNameFromType())
+ }
+ NodeKind.TypeParameter, NodeKind.Parameter -> this.detail(NodeKind.Signature).name // Todo Why not signatureForAnchor
+ NodeKind.Field -> name
+ NodeKind.EnumItem -> "ENUM_VALUE:$name"
+ NodeKind.Attribute -> "attr_$name"
+ else -> "Not implemented signatureForAnchor $this".also { logger.warn(it) }
+ }
+}
+
diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt
new file mode 100644
index 000000000..59d898a2a
--- /dev/null
+++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlFormatOutputBuilder.kt
@@ -0,0 +1,1171 @@
+package org.jetbrains.dokka.Formats
+
+import com.google.common.base.Throwables
+import kotlinx.html.*
+import kotlinx.html.Entities.nbsp
+import kotlinx.html.stream.appendHTML
+import org.jetbrains.dokka.*
+import org.jetbrains.dokka.LanguageService.RenderMode.FULL
+import org.jetbrains.dokka.LanguageService.RenderMode.SUMMARY
+import org.jetbrains.dokka.NodeKind.Companion.classLike
+import java.net.URI
+import javax.inject.Inject
+
+
+open class JavaLayoutHtmlFormatOutputBuilder(
+ val output: Appendable,
+ val languageService: LanguageService,
+ val uriProvider: JavaLayoutHtmlUriProvider,
+ val templateService: JavaLayoutHtmlTemplateService,
+ val logger: DokkaLogger,
+ val uri: URI
+) {
+
+ val htmlConsumer = output.appendHTML()
+
+
+ private fun FlowContent.hN(
+ level: Int,
+ classes: String? = null,
+ block: CommonAttributeGroupFacadeFlowHeadingPhrasingContent.() -> Unit
+ ) {
+ when (level) {
+ 1 -> h1(classes, block)
+ 2 -> h2(classes, block)
+ 3 -> h3(classes, block)
+ 4 -> h4(classes, block)
+ 5 -> h5(classes, block)
+ 6 -> h6(classes, block)
+ }
+ }
+
+ protected open fun FlowContent.metaMarkup(content: List<ContentNode>, contextUri: URI = uri) =
+ contentNodesToMarkup(content, contextUri)
+
+ protected fun FlowContent.nodeContent(node: DocumentationNode, uriNode: DocumentationNode) =
+ contentNodeToMarkup(node.content, uriProvider.mainUriOrWarn(uriNode) ?: uri)
+
+ protected fun FlowContent.nodeContent(node: DocumentationNode) =
+ nodeContent(node, node)
+
+ protected fun FlowContent.contentNodesToMarkup(content: List<ContentNode>, contextUri: URI = uri): Unit =
+ content.forEach { contentNodeToMarkup(it, contextUri) }
+
+ protected fun FlowContent.contentNodeToMarkup(content: ContentNode, contextUri: URI = uri) {
+ when (content) {
+ is ContentText -> +content.text
+ is ContentSymbol -> span("symbol") { +content.text }
+ is ContentKeyword -> span("keyword") { +content.text }
+ is ContentIdentifier -> span("identifier") {
+ content.signature?.let { id = it }
+ +content.text
+ }
+
+ is ContentHeading -> hN(level = content.level) { contentNodesToMarkup(content.children, contextUri) }
+
+ is ContentEntity -> +content.text
+
+ is ContentStrong -> strong { contentNodesToMarkup(content.children, contextUri) }
+ is ContentStrikethrough -> del { contentNodesToMarkup(content.children, contextUri) }
+ is ContentEmphasis -> em { contentNodesToMarkup(content.children, contextUri) }
+
+ is ContentOrderedList -> ol { contentNodesToMarkup(content.children, contextUri) }
+ is ContentUnorderedList -> ul { contentNodesToMarkup(content.children, contextUri) }
+ is ContentListItem -> consumer.li {
+ (content.children.singleOrNull() as? ContentParagraph)
+ ?.let { paragraph -> contentNodesToMarkup(paragraph.children, contextUri) }
+ ?: contentNodesToMarkup(content.children, contextUri)
+ }
+
+ is ContentDescriptionList -> dl { contentNodesToMarkup(content.children, contextUri) }
+ is ContentDescriptionTerm -> consumer.dt {
+ (content.children.singleOrNull() as? ContentParagraph)
+ ?.let { paragraph -> this@contentNodeToMarkup.contentNodesToMarkup(paragraph.children, contextUri) }
+ ?: this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri)
+ }
+ is ContentDescriptionDefinition -> consumer.dd {
+ (content.children.singleOrNull() as? ContentParagraph)
+ ?.let { paragraph -> contentNodesToMarkup(paragraph.children, contextUri) }
+ ?: contentNodesToMarkup(content.children, contextUri)
+ }
+
+ is ContentTable -> table { contentNodesToMarkup(content.children, contextUri) }
+ is ContentTableBody -> consumer.tbody { this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) }
+ is ContentTableRow -> consumer.tr { this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri) }
+ is ContentTableHeader -> consumer.th {
+ content.colspan?.let {
+ if (it.isNotBlank()) {
+ attributes["colspan"] = content.colspan
+ }
+ }
+ content.rowspan?.let {
+ if (it.isNotBlank()) {
+ attributes["rowspan"] = content.rowspan
+ }
+ }
+ (content.children.singleOrNull() as? ContentParagraph)
+ ?.let { paragraph -> this@contentNodeToMarkup.contentNodesToMarkup(paragraph.children, contextUri) }
+ ?: this@contentNodeToMarkup.contentNodesToMarkup(content.children, contextUri)
+ }
+ is ContentTableCell -> consumer.td {
+ content.colspan?.let {
+ if (it.isNotBlank()) {
+ attributes["colspan"] = content.colspan
+ }
+ }
+ content.rowspan?.let {
+ if (it.isNotBlank()) {
+ attributes["rowspan"] = content.rowspan
+ }
+ }
+ (content.children.singleOrNull() as? ContentParagraph)
+ ?.let { paragraph -> contentNodesToMarkup(paragraph.children, contextUri) }
+ ?: contentNodesToMarkup(content.children, contextUri)
+ }
+
+ is ContentSpecialReference -> aside(classes = "note") {
+ contentNodesToMarkup(content.children, contextUri)
+ }
+
+ is ContentCode -> contentInlineCode(content)
+ is ContentBlockSampleCode -> contentBlockSampleCode(content)
+ is ContentBlockCode -> contentBlockCode(content)
+
+ ContentNonBreakingSpace -> +nbsp
+ ContentSoftLineBreak, ContentIndentedSoftLineBreak -> {
+ }
+ ContentHardLineBreak -> br
+
+ is ContentParagraph -> p(classes = content.label) { contentNodesToMarkup(content.children, contextUri) }
+
+ is NodeRenderContent -> renderedSignature(content.node, mode = content.mode)
+ is ContentNodeLink -> {
+ fun FlowContent.body() = contentNodesToMarkup(content.children, contextUri)
+
+ when (content.node?.kind) {
+ NodeKind.TypeParameter -> body()
+ else -> a(href = content.node, block = FlowContent::body)
+ }
+ }
+ is ContentBookmark -> a {
+ id = content.name
+ contentNodesToMarkup(content.children, contextUri)
+ }
+ is ContentExternalLink -> contentExternalLink(content)
+ is ContentLocalLink -> a(href = contextUri.resolve(content.href).relativeTo(uri).toString()) {
+ contentNodesToMarkup(content.children, contextUri)
+ }
+ is ContentSection -> {
+ }
+ is ScriptBlock -> script(content.type, content.src) {}
+ is ContentBlock -> contentNodesToMarkup(content.children, contextUri)
+ }
+ }
+
+ protected open fun FlowContent.contentInlineCode(content: ContentCode) {
+ code { contentNodesToMarkup(content.children) }
+ }
+
+ protected open fun FlowContent.contentBlockSampleCode(content: ContentBlockSampleCode) {
+ pre {
+ code {
+ attributes["data-language"] = content.language
+ contentNodesToMarkup(content.importsBlock.children)
+ +"\n\n"
+ contentNodesToMarkup(content.children)
+ }
+ }
+ }
+
+ protected open fun FlowContent.contentBlockCode(content: ContentBlockCode) {
+ pre {
+ code {
+ attributes["data-language"] = content.language
+ contentNodesToMarkup(content.children)
+ }
+ }
+ }
+
+ protected open fun FlowContent.contentExternalLink(content: ContentExternalLink) {
+ a(href = content.href) { contentNodesToMarkup(content.children) }
+ }
+
+ protected open fun <T> FlowContent.summaryNodeGroup(
+ nodes: Iterable<T>,
+ header: String,
+ headerAsRow: Boolean = true,
+ row: TBODY.(T) -> Unit
+ ) {
+ if (nodes.none()) return
+ if (!headerAsRow) {
+ h2 { +header }
+ }
+ table {
+ tbody {
+ if (headerAsRow) {
+ developerHeading(header)
+ }
+ nodes.forEach { node ->
+ row(node)
+ }
+ }
+ }
+ }
+
+
+ protected open fun summary(node: DocumentationNode) = node.summary
+
+ protected open fun TBODY.classLikeRow(node: DocumentationNode) = tr {
+ td { a(href = uriProvider.linkTo(node, uri)) { +node.simpleName() } }
+ td { nodeSummary(node) }
+ }
+
+ protected fun FlowContent.modifiers(node: DocumentationNode) {
+ for (modifier in node.details(NodeKind.Modifier)) {
+ renderedSignature(modifier, SUMMARY)
+ }
+ }
+
+ protected fun FlowContent.shortFunctionParametersList(func: DocumentationNode) {
+ val params = func.details(NodeKind.Parameter)
+ .map { languageService.render(it, FULL) }
+ .run {
+ drop(1).fold(listOfNotNull(firstOrNull())) { acc, node ->
+ acc + ContentText(", ") + node
+ }
+ }
+ metaMarkup(listOf(ContentText("(")) + params + listOf(ContentText(")")))
+ }
+
+
+ protected open fun TBODY.functionLikeSummaryRow(node: DocumentationNode) = tr {
+ if (node.kind != NodeKind.Constructor) {
+ td {
+ modifiers(node)
+ renderedSignature(node.detail(NodeKind.Type), SUMMARY)
+ }
+ }
+ td {
+ div {
+ code {
+ val receiver = node.detailOrNull(NodeKind.Receiver)
+ if (receiver != null) {
+ renderedSignature(receiver.detail(NodeKind.Type), SUMMARY)
+ +"."
+ }
+ a(href = node) { +node.prettyName }
+ shortFunctionParametersList(node)
+ }
+ }
+
+ nodeSummary(node)
+ }
+ }
+
+ protected open fun TBODY.propertyLikeSummaryRow(node: DocumentationNode, showSignature: Boolean = true) = tr {
+ if (showSignature) {
+ td {
+ modifiers(node)
+ renderedSignature(node.detail(NodeKind.Type), SUMMARY)
+ }
+ }
+ td {
+ div {
+ code {
+ a(href = node) { +node.name }
+ }
+ }
+
+ nodeSummary(node)
+ }
+ }
+
+ protected open fun TBODY.nestedClassSummaryRow(node: DocumentationNode) = tr {
+ td {
+ modifiers(node)
+ }
+ td {
+ div {
+ code {
+ a(href = node) { +node.name }
+ }
+ }
+
+ nodeSummary(node)
+ }
+ }
+
+ protected fun HtmlBlockTag.nodeSummary(node: DocumentationNode, uriNode: DocumentationNode) {
+ contentNodeToMarkup(summary(node), uriProvider.mainUriOrWarn(uriNode) ?: uri)
+ }
+
+ protected fun HtmlBlockTag.nodeSummary(node: DocumentationNode) {
+ nodeSummary(node, node)
+ }
+
+ protected open fun TBODY.inheritRow(
+ entry: Map.Entry<DocumentationNode, List<DocumentationNode>>,
+ summaryRow: TBODY.(DocumentationNode) -> Unit
+ ) = tr {
+ td {
+ val (from, nodes) = entry
+ +"From class "
+ a(href = from.owner!!) { +from.qualifiedName() }
+ table {
+ tbody {
+ for (node in nodes) {
+ summaryRow(node)
+ }
+ }
+ }
+ }
+ }
+
+ protected open fun TBODY.groupedRow(
+ entry: Map.Entry<DocumentationNode, List<DocumentationNode>>,
+ groupHeader: HtmlBlockTag.(DocumentationNode) -> Unit,
+ summaryRow: TBODY.(DocumentationNode) -> Unit
+ ) = tr {
+ td {
+ val (from, nodes) = entry
+ groupHeader(from)
+ table {
+ tbody {
+ for (node in nodes) {
+ summaryRow(node)
+ }
+ }
+ }
+ }
+ }
+
+ protected open fun TBODY.extensionRow(
+ entry: Map.Entry<DocumentationNode, List<DocumentationNode>>,
+ summaryRow: TBODY.(DocumentationNode) -> Unit
+ ) = groupedRow(entry, { from ->
+ +"From "
+ a(href = from) { +from.qualifiedName() }
+ }, summaryRow)
+
+
+ protected open fun TBODY.extensionByReceiverRow(
+ entry: Map.Entry<DocumentationNode, List<DocumentationNode>>,
+ summaryRow: TBODY.(DocumentationNode) -> Unit
+ ) = groupedRow(entry, { from ->
+ +"For "
+ a(href = from) { +from.name }
+ }, summaryRow)
+
+ protected open fun FlowOrInteractiveOrPhrasingContent.a(href: DocumentationNode?, classes: String? = null, block: HtmlBlockInlineTag.() -> Unit) {
+ if (href == null) {
+ return a(href = "#", classes = classes, block = block)
+ }
+
+ val hrefText = try {
+ href.name.takeIf { href.kind == NodeKind.ExternalLink }
+ ?: href.links.firstOrNull { it.kind == NodeKind.ExternalLink }?.name
+ ?: "#".takeIf { href.kind == NodeKind.ExternalClass } // When external class unresolved
+ ?: uriProvider.linkTo(href, uri)
+ } catch (e: Exception) {
+ val owners = generateSequence(href) { it.owner }.toList().reversed()
+ logger.warn("Exception while resolving link to ${owners.joinToString(separator = " ")}\n"
+ + Throwables.getStackTraceAsString(e))
+ "#"
+ }
+
+ a(href = hrefText, classes = classes, block = block)
+ }
+
+ protected open fun FlowContent.renderedSignature(
+ node: DocumentationNode,
+ mode: LanguageService.RenderMode = SUMMARY
+ ) {
+ contentNodeToMarkup(languageService.render(node, mode), uri)
+ }
+
+ protected open fun generatePackage(page: Page.PackagePage) = templateService.composePage(
+ page,
+ htmlConsumer,
+ headContent = {
+
+ },
+ bodyContent = {
+ h1 { +page.node.name }
+ nodeContent(page.node)
+ this@composePage.summaryNodeGroup(page.interfaces, "Interfaces", headerAsRow = false) { classLikeRow(it) }
+ this@composePage.summaryNodeGroup(page.classes, "Classes", headerAsRow = false) { classLikeRow(it) }
+ this@composePage.summaryNodeGroup(page.exceptions, "Exceptions", headerAsRow = false) { classLikeRow(it) }
+ this@composePage.summaryNodeGroup(page.typeAliases, "Type-aliases", headerAsRow = false) { classLikeRow(it) }
+ this@composePage.summaryNodeGroup(page.annotations, "Annotations", headerAsRow = false) { classLikeRow(it) }
+ this@composePage.summaryNodeGroup(page.enums, "Enums", headerAsRow = false) { classLikeRow(it) }
+
+ this@composePage.summaryNodeGroup(
+ page.constants,
+ "Top-level constants summary",
+ headerAsRow = false
+ ) {
+ propertyLikeSummaryRow(it)
+ }
+
+ this@composePage.summaryNodeGroup(
+ page.functions,
+ "Top-level functions summary",
+ headerAsRow = false
+ ) {
+ functionLikeSummaryRow(it)
+ }
+
+ this@composePage.summaryNodeGroup(
+ page.properties,
+ "Top-level properties summary",
+ headerAsRow = false
+ ) {
+ propertyLikeSummaryRow(it)
+ }
+
+ this@composePage.summaryNodeGroup(
+ page.extensionFunctions.entries,
+ "Extension functions summary",
+ headerAsRow = false
+ ) {
+ extensionByReceiverRow(it) {
+ functionLikeSummaryRow(it)
+ }
+ }
+
+ this@composePage.summaryNodeGroup(
+ page.extensionProperties.entries,
+ "Extension properties summary",
+ headerAsRow = false
+ ) {
+ extensionByReceiverRow(it) {
+ functionLikeSummaryRow(it)
+ }
+ }
+
+ fullMemberDocs(page.constants, "Top-level constants")
+ fullMemberDocs(page.functions, "Top-level functions")
+ fullMemberDocs(page.properties, "Top-level properties")
+ fullMemberDocs(page.extensionFunctions.values.flatten(), "Extension functions")
+ fullMemberDocs(page.extensionProperties.values.flatten(), "Extension properties")
+ }
+ )
+
+ protected fun FlowContent.qualifiedTypeReference(node: DocumentationNode) {
+ if (node.kind in classLike) {
+ a(href = node) { +node.qualifiedName() }
+ return
+ }
+
+ val targetLink = node.links.firstOrNull()
+
+ if (targetLink?.kind == NodeKind.TypeParameter) {
+ +node.name
+ return
+ }
+
+ a(href = targetLink) {
+ +node.qualifiedNameFromType()
+ }
+ val typeParameters = node.details(NodeKind.Type)
+ if (typeParameters.isNotEmpty()) {
+ +"<"
+ typeParameters.forEach {
+ if (it != typeParameters.first()) {
+ +", "
+ }
+ qualifiedTypeReference(it)
+ }
+ +">"
+ }
+ }
+
+ protected open fun FlowContent.classHierarchy(superclasses: List<DocumentationNode>) {
+ table {
+ superclasses.forEach {
+ tr {
+ if (it != superclasses.first()) {
+ td {
+ +"   ↳"
+ }
+ }
+ td {
+ qualifiedTypeReference(it)
+ }
+ }
+ }
+ }
+ }
+
+ protected open fun FlowContent.subclasses(inheritors: List<DocumentationNode>, direct: Boolean) {
+ if (inheritors.isEmpty()) return
+ div {
+ table {
+ thead {
+ tr {
+ td {
+ if (direct)
+ +"Known Direct Subclasses"
+ else
+ +"Known Indirect Subclasses"
+ }
+ }
+ }
+ tbody {
+ inheritors.forEach { inheritor ->
+ tr {
+ td {
+ a(href = inheritor) { +inheritor.classNodeNameWithOuterClass() }
+ }
+ td {
+ nodeSummary(inheritor)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected open fun FlowContent.classLikeSummaries(page: Page.ClassPage) = with(page) {
+ this@classLikeSummaries.summaryNodeGroup(
+ nestedClasses,
+ "Nested classes",
+ headerAsRow = true
+ ) {
+ nestedClassSummaryRow(it)
+ }
+
+ this@classLikeSummaries.summaryNodeGroup(enumValues, "Enum values") {
+ propertyLikeSummaryRow(it)
+ }
+
+ this@classLikeSummaries.summaryNodeGroup(constants, "Constants") { propertyLikeSummaryRow(it) }
+
+ constructors.forEach { (visibility, group) ->
+ this@classLikeSummaries.summaryNodeGroup(
+ group,
+ "${visibility.capitalize()} constructors",
+ headerAsRow = true
+ ) {
+ functionLikeSummaryRow(it)
+ }
+ }
+
+ functions.forEach { (visibility, group) ->
+ this@classLikeSummaries.summaryNodeGroup(
+ group,
+ "${visibility.capitalize()} functions",
+ headerAsRow = true
+ ) {
+ functionLikeSummaryRow(it)
+ }
+ }
+
+ this@classLikeSummaries.summaryNodeGroup(
+ companionFunctions,
+ "Companion functions",
+ headerAsRow = true
+ ) {
+ functionLikeSummaryRow(it)
+ }
+ this@classLikeSummaries.summaryNodeGroup(
+ inheritedFunctionsByReceiver.entries,
+ "Inherited functions",
+ headerAsRow = true
+ ) {
+ inheritRow(it) {
+ functionLikeSummaryRow(it)
+ }
+ }
+ this@classLikeSummaries.summaryNodeGroup(
+ extensionFunctions.entries,
+ "Extension functions",
+ headerAsRow = true
+ ) {
+ extensionRow(it) {
+ functionLikeSummaryRow(it)
+ }
+ }
+ this@classLikeSummaries.summaryNodeGroup(
+ inheritedExtensionFunctions.entries,
+ "Inherited extension functions",
+ headerAsRow = true
+ ) {
+ extensionRow(it) {
+ functionLikeSummaryRow(it)
+ }
+ }
+
+
+ this@classLikeSummaries.summaryNodeGroup(properties, "Properties", headerAsRow = true) { propertyLikeSummaryRow(it) }
+ this@classLikeSummaries.summaryNodeGroup(
+ companionProperties,
+ "Companion properties",
+ headerAsRow = true
+ ) {
+ propertyLikeSummaryRow(it)
+ }
+
+ this@classLikeSummaries.summaryNodeGroup(
+ inheritedPropertiesByReceiver.entries,
+ "Inherited properties",
+ headerAsRow = true
+ ) {
+ inheritRow(it) {
+ propertyLikeSummaryRow(it)
+ }
+ }
+ this@classLikeSummaries.summaryNodeGroup(
+ extensionProperties.entries,
+ "Extension properties",
+ headerAsRow = true
+ ) {
+ extensionRow(it) {
+ propertyLikeSummaryRow(it)
+ }
+ }
+ this@classLikeSummaries.summaryNodeGroup(
+ inheritedExtensionProperties.entries,
+ "Inherited extension properties",
+ headerAsRow = true
+ ) {
+ extensionRow(it) {
+ propertyLikeSummaryRow(it)
+ }
+ }
+ }
+
+ protected open fun FlowContent.classLikeFullMemberDocs(page: Page.ClassPage) = with(page) {
+ fullMemberDocs(enumValues, "Enum values")
+ fullMemberDocs(constants, "Constants")
+
+ constructors.forEach { (visibility, group) ->
+ fullMemberDocs(group, "${visibility.capitalize()} constructors")
+ }
+
+ functions.forEach { (visibility, group) ->
+ fullMemberDocs(group, "${visibility.capitalize()} methods")
+ }
+
+ fullMemberDocs(properties, "Properties")
+ if (!hasMeaningfulCompanion) {
+ fullMemberDocs(companionFunctions, "Companion functions")
+ fullMemberDocs(companionProperties, "Companion properties")
+ }
+ }
+
+ protected open fun generateClassLike(page: Page.ClassPage) = templateService.composePage(
+ page,
+ htmlConsumer,
+ headContent = {
+
+ },
+ bodyContent = {
+ val node = page.node
+ with(page) {
+
+ div {
+ id = "api-info-block"
+ apiAndDeprecatedVersions(node)
+ }
+
+ if (node.artifactId.name.isNotEmpty()) {
+ div(classes = "api-level") { br { +"belongs to Maven artifact ${node.artifactId}" } }
+ }
+ h1 { +node.name }
+ pre { renderedSignature(node, FULL) }
+ classHierarchy(page.superclasses)
+
+ subclasses(page.directInheritors, true)
+ subclasses(page.indirectInheritors, false)
+
+ deprecatedClassCallOut(node)
+ nodeContent(node)
+
+ h2 { +"Summary" }
+ classLikeSummaries(page)
+ classLikeFullMemberDocs(page)
+ }
+ }
+ )
+
+ protected open fun FlowContent.classIndexSummary(node: DocumentationNode) {
+ nodeContent(node)
+ }
+
+ protected open fun generateClassIndex(page: Page.ClassIndex) = templateService.composePage(
+ page,
+ htmlConsumer,
+ headContent = {
+
+ },
+ bodyContent = {
+ h1 { +"Class Index" }
+
+
+ ul {
+ page.classesByFirstLetter.forEach { (letter) ->
+ li { a(href = "#letter_$letter") { +letter } }
+ }
+ }
+
+ page.classesByFirstLetter.forEach { (letter, classes) ->
+ h2 {
+ id = "letter_$letter"
+ +letter
+ }
+ table {
+ tbody {
+ for (node in classes) {
+ tr {
+ td {
+ a(href = uriProvider.linkTo(node, uri)) { +node.classNodeNameWithOuterClass() }
+ }
+ td {
+ if (!deprecatedIndexSummary(node)) {
+ classIndexSummary(node)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+
+ protected open fun generatePackageIndex(page: Page.PackageIndex) = templateService.composePage(
+ page,
+ htmlConsumer,
+ headContent = {
+
+ },
+ bodyContent = {
+ h1 { +"Package Index" }
+ table {
+ tbody {
+ for (node in page.packages) {
+ tr {
+ td {
+ a(href = uriProvider.linkTo(node, uri)) { +node.name }
+ }
+ td {
+ nodeContent(node)
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+
+ fun generatePage(page: Page) {
+ when (page) {
+ is Page.PackageIndex -> generatePackageIndex(page)
+ is Page.ClassIndex -> generateClassIndex(page)
+ is Page.ClassPage -> generateClassLike(page)
+ is Page.PackagePage -> generatePackage(page)
+ }
+ }
+
+ protected fun FlowContent.fullMemberDocs(
+ nodes: List<DocumentationNode>,
+ header: String
+ ) {
+ if (nodes.none()) return
+ h2 {
+ +header
+ }
+ for (node in nodes) {
+ fullMemberDocs(node)
+ }
+ }
+
+ protected open fun FlowContent.seeAlsoSection(links: List<List<ContentNode>>) {
+ p { b { +"See Also" } }
+ ul {
+ links.forEach { linkParts ->
+ li { code { metaMarkup(linkParts) } }
+ }
+ }
+ }
+
+ protected open fun FlowContent.regularSection(name: String, entries: List<ContentSection>) {
+ table {
+ thead {
+ tr {
+ th {
+ colSpan = "2"
+ +name
+ }
+ }
+ }
+ tbody {
+ entries.forEach {
+ tr {
+ if (it.subjectName != null) {
+ td { +it.subjectName }
+ }
+ td {
+ metaMarkup(it.children)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected open fun FlowContent.deprecationWarningToMarkup(
+ node: DocumentationNode,
+ prefix: Boolean = false,
+ emphasis: Boolean = true
+ ): Boolean {
+ val deprecated = formatDeprecationOrNull(node, prefix, emphasis)
+ deprecated?.let {
+ contentNodeToMarkup(deprecated, uriProvider.mainUri(node))
+ return true
+ }
+ return false
+ }
+
+ protected open fun FlowContent.deprecatedClassCallOut(node: DocumentationNode) {
+ val deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty()
+ if (deprecatedLevelExists) {
+ hr { }
+ aside(classes = "caution") {
+ strong { +node.deprecatedLevelMessage() }
+ deprecationWarningToMarkup(node, emphasis = false)
+ }
+ }
+ }
+
+ protected open fun FlowContent.deprecatedIndexSummary(node: DocumentationNode): Boolean {
+ val deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty()
+ if (deprecatedLevelExists) {
+ val em = ContentEmphasis()
+ em.append(ContentText(node.deprecatedLevelMessage()))
+ em.append(ContentText(" "))
+ for (child in node.deprecation?.content?.children ?: emptyList<ContentNode>()) {
+ em.append(child)
+ }
+ contentNodeToMarkup(em, uriProvider.mainUri(node))
+ return true
+ }
+ return false
+ }
+
+ protected open fun FlowContent.apiAndDeprecatedVersions(node: DocumentationNode) {
+ val apiLevelExists = node.apiLevel.name.isNotEmpty()
+ val sdkExtSinceExists = node.sdkExtSince.name.isNotEmpty()
+ val deprecatedLevelExists = node.deprecatedLevel.name.isNotEmpty()
+ if (apiLevelExists || sdkExtSinceExists || deprecatedLevelExists) {
+ div(classes = "api-level") {
+ if (apiLevelExists) {
+ +"Added in "
+ a(href = "https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels") {
+ +"API level ${node.apiLevel.name}"
+ }
+ }
+ if (sdkExtSinceExists) {
+ if (apiLevelExists) {
+ br
+ +"Also in "
+ } else {
+ +"Added in "
+ }
+ a(href = "https://developer.android.com/sdkExtensions") {
+ +"${node.sdkExtSince.name}"
+ }
+ }
+ if (deprecatedLevelExists) {
+ if (apiLevelExists || sdkExtSinceExists) {
+ br
+ }
+ +"Deprecated in "
+ a(href = "https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels") {
+ +"API level ${node.deprecatedLevel.name}"
+ }
+ }
+ }
+ }
+ }
+
+ protected open fun formatDeprecationOrNull(
+ node: DocumentationNode,
+ prefix: Boolean = false,
+ emphasis: Boolean = true): ContentNode? {
+ val deprecated = node.deprecation
+ deprecated?.let {
+ return ContentParagraph("caution").apply {
+ if (prefix) {
+ append(ContentStrong().apply { text(
+ if (deprecated.content.children.size == 0) "Deprecated."
+ else "Deprecated: "
+ ) })
+ }
+ val em = if (emphasis) ContentEmphasis() else ContentBlock()
+ for (child in deprecated.content.children) {
+ em.append(child)
+ }
+ append(em)
+ }
+ }
+ return null
+ }
+
+ protected open fun FlowContent.section(name: String, sectionParts: List<ContentSection>) {
+ when (name) {
+ ContentTags.SeeAlso -> seeAlsoSection(sectionParts.map { it.children.flatMap { (it as? ContentParagraph)?.children ?: listOf(it) } })
+ else -> regularSection(name, sectionParts)
+ }
+ }
+
+ protected open fun FlowContent.sections(content: Content) {
+ val sectionsByTag = content.sections.groupByTo(mutableMapOf()) { it.tag }
+
+ val seeAlso = sectionsByTag.remove(ContentTags.SeeAlso)
+
+ for ((name, entries) in sectionsByTag) {
+ section(name, entries)
+ }
+
+ seeAlso?.let { section(ContentTags.SeeAlso, it) }
+ }
+
+ protected open fun FlowContent.fullMemberDocs(node: DocumentationNode, uriNode: DocumentationNode) {
+ div {
+ id = node.signatureForAnchor(logger)
+ h3 { +node.name }
+ pre { renderedSignature(node, FULL) }
+ deprecationWarningToMarkup(node, prefix = true)
+ nodeContent(node)
+ node.constantValue()?.let { value ->
+ pre {
+ +"Value: "
+ code { +value }
+ }
+ }
+
+ sections(node.content)
+ }
+ }
+
+ protected open fun FlowContent.fullMemberDocs(node: DocumentationNode) {
+ fullMemberDocs(node, node)
+ }
+
+ sealed class Page {
+ class PackageIndex(packages: List<DocumentationNode>) : Page() {
+ init {
+ assert(packages.all { it.kind == NodeKind.Package })
+ }
+
+ val packages = packages.sortedBy { it.name }
+ }
+
+ class ClassIndex(allTypesNode: DocumentationNode) : Page() {
+ init {
+ assert(allTypesNode.kind == NodeKind.AllTypes)
+ }
+
+ // Wide-collect all nested classes
+ val classes: List<DocumentationNode> =
+ generateSequence(listOf(allTypesNode)) { nodes ->
+ nodes
+ .flatMap { it.members.filter { it.kind in NodeKind.classLike } }
+ .takeUnless { it.isEmpty() }
+ }.drop(1)
+ .flatten()
+ .sortedBy { it.classNodeNameWithOuterClass().toLowerCase() }
+ .toList()
+
+
+ // Group all classes by it's first letter and sort
+ val classesByFirstLetter =
+ classes
+ .groupBy {
+ it.classNodeNameWithOuterClass().first().toString()
+ }
+ .entries
+ .sortedBy { (letter) ->
+ val x = letter.toLowerCase()
+ x
+ }
+ }
+
+ class ClassPage(val node: DocumentationNode) : Page() {
+
+ init {
+ assert(node.kind in NodeKind.classLike)
+ }
+
+ val superclasses = (sequenceOf(node) + node.superclassTypeSequence).toList().asReversed()
+
+ val enumValues = node.members(NodeKind.EnumItem).sortedBy { it.name }
+
+ val directInheritors: List<DocumentationNode>
+ val indirectInheritors: List<DocumentationNode>
+
+ init {
+ // Wide-collect all inheritors
+ val inheritors = generateSequence(node.inheritors) { inheritors ->
+ inheritors
+ .flatMap { it.inheritors }
+ .takeUnless { it.isEmpty() }
+ }
+ directInheritors = inheritors.first().sortedBy { it.classNodeNameWithOuterClass() }
+ indirectInheritors = inheritors.drop(1).flatten().toList().sortedBy { it.classNodeNameWithOuterClass() }
+ }
+
+ val isCompanion = node.details(NodeKind.Modifier).any { it.name == "companion" }
+ val hasMeaningfulCompanion = !isCompanion && node.companion != null
+
+ private fun DocumentationNode.thisTypeExtension() =
+ detail(NodeKind.Receiver).detail(NodeKind.Type).links.any { it == node }
+
+ val functionKind = if (!isCompanion) NodeKind.Function else NodeKind.CompanionObjectFunction
+ val propertyKind = if (!isCompanion) NodeKind.Property else NodeKind.CompanionObjectProperty
+
+ private fun DocumentationNode.isFunction() = kind == functionKind
+ private fun DocumentationNode.isProperty() = kind == propertyKind
+
+
+ val nestedClasses = node.members.filter { it.kind in NodeKind.classLike } - enumValues
+
+ val attributes = node.attributes
+
+ val inheritedAttributes =
+ node.superclassTypeSequence
+ .toList()
+ .sortedBy { it.name }
+ .flatMap { it.typeDeclarationClass?.attributes.orEmpty() }
+ .distinctBy { it.attributeRef!!.name }
+ .groupBy { it.owner!! }
+
+ val allInheritedMembers = node.allInheritedMembers
+ val constants = node.members.filter { it.constantValue() != null }
+ val inheritedConstants = allInheritedMembers.filter { it.constantValue() != null }.groupBy { it.owner!! }
+
+
+ fun compareVisibilities(a: String, b: String): Int {
+ return visibilityNames.indexOf(a) - visibilityNames.indexOf(b)
+ }
+
+ fun Collection<DocumentationNode>.groupByVisibility() =
+ groupBy { it.visibility() }.toSortedMap(Comparator { a, b -> compareVisibilities(a, b) })
+
+
+ val constructors = node.members(NodeKind.Constructor).groupByVisibility()
+ val functions = node.members(functionKind).groupByVisibility()
+ val fields = (node.members(NodeKind.Field) - constants).groupByVisibility()
+ val properties = node.members(propertyKind) - constants
+ val inheritedFunctionsByReceiver = allInheritedMembers.filter { it.kind == functionKind }.groupBy { it.owner!! }
+ val inheritedPropertiesByReceiver =
+ allInheritedMembers.filter {
+ it.kind == propertyKind && it.constantValue() == null
+ }.groupBy { it.owner!! }
+
+ val inheritedFieldsByReceiver =
+ allInheritedMembers.filter {
+ it.kind == NodeKind.Field && it.constantValue() != null
+ }.groupBy { it.owner!! }
+
+ val originalExtensions = if (!isCompanion) node.extensions else node.owner!!.extensions
+
+ val extensionFunctions: Map<DocumentationNode, List<DocumentationNode>>
+ val extensionProperties: Map<DocumentationNode, List<DocumentationNode>>
+ val inheritedExtensionFunctions: Map<DocumentationNode, List<DocumentationNode>>
+ val inheritedExtensionProperties: Map<DocumentationNode, List<DocumentationNode>>
+
+ init {
+ val (extensions, inheritedExtensions) = originalExtensions.partition { it.thisTypeExtension() }
+ extensionFunctions = extensions.filter { it.isFunction() }.sortedBy { it.name }.groupBy { it.owner!! }
+ extensionProperties = extensions.filter { it.isProperty() }.sortedBy { it.name }.groupBy { it.owner!! }
+ inheritedExtensionFunctions =
+ inheritedExtensions.filter { it.isFunction() }.sortedBy { it.name }.groupBy { it.owner!! }
+ inheritedExtensionProperties =
+ inheritedExtensions.filter { it.isProperty() }.sortedBy { it.name }.groupBy { it.owner!! }
+ }
+
+ val companionFunctions = node.members(NodeKind.CompanionObjectFunction).takeUnless { isCompanion }.orEmpty()
+ val companionProperties =
+ node.members(NodeKind.CompanionObjectProperty).takeUnless { isCompanion }.orEmpty() - constants
+
+
+ }
+
+ class PackagePage(val node: DocumentationNode) : Page() {
+
+ init {
+ assert(node.kind == NodeKind.Package)
+ }
+
+ val interfaces = node.members(NodeKind.Interface) +
+ node.members(NodeKind.Class).flatMap { it.members(NodeKind.Interface) }
+ val classes = node.members(NodeKind.Class)
+ val exceptions = node.members(NodeKind.Exception)
+ val typeAliases = node.members(NodeKind.TypeAlias)
+ val annotations = node.members(NodeKind.AnnotationClass)
+ val enums = node.members(NodeKind.Enum)
+
+ val constants = node.members(NodeKind.Property).filter { it.constantValue() != null }
+
+
+ private fun DocumentationNode.getClassExtensionReceiver() =
+ detailOrNull(NodeKind.Receiver)?.detailOrNull(NodeKind.Type)?.takeIf {
+ it.links.any { it.kind == NodeKind.ExternalLink || it.kind in NodeKind.classLike }
+ }
+
+ private fun List<DocumentationNode>.groupedExtensions() =
+ filter { it.getClassExtensionReceiver() != null }
+ .groupBy {
+ val receiverType = it.getClassExtensionReceiver()!!
+ receiverType.links.filter { it.kind != NodeKind.ExternalLink}.firstOrNull() ?:
+ receiverType.links(NodeKind.ExternalLink).first()
+ }
+
+ private fun List<DocumentationNode>.externalExtensions(kind: NodeKind) =
+ associateBy({ it }, { it.members(kind) })
+ .filterNot { (_, values) -> values.isEmpty() }
+
+ val extensionFunctions =
+ node.members(NodeKind.ExternalClass).externalExtensions(NodeKind.Function) +
+ node.members(NodeKind.Function).groupedExtensions()
+
+ val extensionProperties =
+ node.members(NodeKind.ExternalClass).externalExtensions(NodeKind.Property) +
+ node.members(NodeKind.Property).groupedExtensions()
+
+ val functions = node.members(NodeKind.Function) - extensionFunctions.values.flatten()
+ val properties = node.members(NodeKind.Property) - constants - extensionProperties.values.flatten()
+
+ }
+ }
+}
+
+class JavaLayoutHtmlFormatOutputBuilderFactoryImpl @Inject constructor(
+ val uriProvider: JavaLayoutHtmlUriProvider,
+ val languageService: LanguageService,
+ val templateService: JavaLayoutHtmlTemplateService,
+ val logger: DokkaLogger
+) : JavaLayoutHtmlFormatOutputBuilderFactory {
+ override fun createOutputBuilder(output: Appendable, node: DocumentationNode): JavaLayoutHtmlFormatOutputBuilder {
+ return createOutputBuilder(output, uriProvider.mainUri(node))
+ }
+
+ override fun createOutputBuilder(output: Appendable, uri: URI): JavaLayoutHtmlFormatOutputBuilder {
+ return JavaLayoutHtmlFormatOutputBuilder(output, languageService, uriProvider, templateService, logger, uri)
+ }
+}
+
+fun DocumentationNode.constantValue(): String? =
+ detailOrNull(NodeKind.Value)?.name.takeIf {
+ kind == NodeKind.Field || kind == NodeKind.Property || kind == NodeKind.CompanionObjectProperty
+ }
+
+
+private val visibilityNames = setOf("public", "protected", "internal", "package-local", "private")
+
+fun DocumentationNode.visibility(): String =
+ details(NodeKind.Modifier).firstOrNull { it.name in visibilityNames }?.name ?: ""
diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt
new file mode 100644
index 000000000..9928a8e9e
--- /dev/null
+++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlGenerator.kt
@@ -0,0 +1,165 @@
+package org.jetbrains.dokka.Formats
+
+import com.google.inject.Inject
+import com.google.inject.name.Named
+import org.jetbrains.dokka.*
+import org.jetbrains.dokka.Formats.JavaLayoutHtmlFormatOutputBuilder.Page
+import org.jetbrains.dokka.NodeKind.Companion.classLike
+import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult
+import java.io.BufferedWriter
+import java.io.File
+import java.net.URI
+
+class JavaLayoutHtmlFormatGenerator @Inject constructor(
+ @Named("outputDir") val root: File,
+ val packageListService: PackageListService,
+ val outputBuilderFactoryService: JavaLayoutHtmlFormatOutputBuilderFactory,
+ private val options: DocumentationOptions,
+ val logger: DokkaLogger,
+ @Named("outlineRoot") val outlineRoot: String
+) : Generator, JavaLayoutHtmlUriProvider {
+
+ @set:Inject(optional = true)
+ var outlineFactoryService: JavaLayoutHtmlFormatOutlineFactoryService? = null
+
+ fun createOutputBuilderForNode(node: DocumentationNode, output: Appendable) = outputBuilderFactoryService.createOutputBuilder(output, node)
+
+ fun DocumentationNode.getOwnerOrReport() = owner ?: run {
+ error("Owner not found for $this")
+ }
+
+ override fun tryGetContainerUri(node: DocumentationNode): URI? {
+ return when (node.kind) {
+ NodeKind.Module -> URI("/").resolve(node.name + "/")
+ NodeKind.Package -> tryGetContainerUri(node.getOwnerOrReport())?.resolve(node.name.replace('.', '/') + '/')
+ NodeKind.GroupNode -> tryGetContainerUri(node.getOwnerOrReport())
+ in NodeKind.classLike -> tryGetContainerUri(node.getOwnerOrReport())?.resolve("${node.classNodeNameWithOuterClass()}.html")
+ else -> null
+ }
+ }
+
+ override fun tryGetMainUri(node: DocumentationNode): URI? {
+ return when (node.kind) {
+ NodeKind.Package -> tryGetContainerUri(node)?.resolve("package-summary.html")
+ in NodeKind.classLike -> tryGetContainerUri(node)?.resolve("#")
+ in NodeKind.memberLike -> {
+ val owner = if (node.owner?.kind != NodeKind.ExternalClass) node.owner else node.owner?.owner
+ if (owner!!.kind in classLike &&
+ (node.kind == NodeKind.CompanionObjectProperty || node.kind == NodeKind.CompanionObjectFunction) &&
+ owner.companion != null
+ ) {
+ val signature = node.detail(NodeKind.Signature)
+ val originalFunction = owner.companion!!.members.first { it.detailOrNull(NodeKind.Signature)?.name == signature.name }
+ tryGetMainUri(owner.companion!!)?.resolveInPage(originalFunction)
+ } else {
+ tryGetMainUri(owner)?.resolveInPage(node)
+ }
+ }
+ NodeKind.TypeParameter, NodeKind.Parameter -> node.path.asReversed().drop(1).firstNotNullResult(this::tryGetMainUri)?.resolveInPage(node)
+ NodeKind.AllTypes -> outlineRootUri(node).resolve ("classes.html")
+ else -> null
+ }
+ }
+
+ override fun tryGetOutlineRootUri(node: DocumentationNode): URI? {
+ return when(node.kind) {
+ NodeKind.AllTypes -> tryGetContainerUri(node.getOwnerOrReport())
+ else -> tryGetContainerUri(node)
+ }?.resolve(outlineRoot)
+ }
+
+ fun URI.resolveInPage(node: DocumentationNode): URI = resolve("#${node.signatureForAnchor(logger).anchorEncoded()}")
+
+ fun buildClass(node: DocumentationNode, parentDir: File) {
+ val fileForClass = parentDir.resolve(node.classNodeNameWithOuterClass() + ".html")
+ fileForClass.bufferedWriter().use {
+ createOutputBuilderForNode(node, it).generatePage(Page.ClassPage(node))
+ }
+ for (memberClass in node.members.filter { it.kind in NodeKind.classLike }) {
+ buildClass(memberClass, parentDir)
+ }
+ }
+
+ fun buildPackage(node: DocumentationNode, parentDir: File) {
+ assert(node.kind == NodeKind.Package)
+ var members = node.members
+ val directoryForPackage = parentDir.resolve(node.name.replace('.', File.separatorChar))
+ directoryForPackage.mkdirsOrFail()
+
+ directoryForPackage.resolve("package-summary.html").bufferedWriter().use {
+ createOutputBuilderForNode(node, it).generatePage(Page.PackagePage(node))
+ }
+
+ members.filter { it.kind == NodeKind.GroupNode }.forEach {
+ members += it.members
+ }
+ members.filter { it.kind in NodeKind.classLike }.forEach {
+ buildClass(it, directoryForPackage)
+ }
+ }
+
+ fun buildClassIndex(node: DocumentationNode, parentDir: File) {
+ val file = parentDir.resolve("classes.html")
+ file.bufferedWriter().use {
+ createOutputBuilderForNode(node, it).generatePage(Page.ClassIndex(node))
+ }
+ }
+
+ fun buildPackageIndex(module: DocumentationNode, nodes: List<DocumentationNode>, parentDir: File) {
+ val file = parentDir.resolve("packages.html")
+ file.bufferedWriter().use {
+ val uri = outlineRootUri(module).resolve("packages.html")
+ outputBuilderFactoryService.createOutputBuilder(it, uri)
+ .generatePage(Page.PackageIndex(nodes))
+ }
+ }
+
+ override fun buildPages(nodes: Iterable<DocumentationNode>) {
+ val module = nodes.single()
+
+ val moduleRoot = root.resolve(module.name)
+ val packages = module.members.filter { it.kind == NodeKind.Package }
+ packages.forEach { buildPackage(it, moduleRoot) }
+ val outlineRootFile = moduleRoot.resolve(outlineRoot)
+ if (options.generateClassIndexPage) {
+ buildClassIndex(module.members.single { it.kind == NodeKind.AllTypes }, outlineRootFile)
+ }
+
+ if (options.generatePackageIndexPage) {
+ buildPackageIndex(module, packages, outlineRootFile)
+ }
+ }
+
+ override fun buildOutlines(nodes: Iterable<DocumentationNode>) {
+ val uriToWriter = mutableMapOf<URI, BufferedWriter>()
+
+ fun provideOutput(uri: URI): BufferedWriter {
+ val normalized = uri.normalize()
+ uriToWriter[normalized]?.let { return it }
+ val file = root.resolve(normalized.path.removePrefix("/"))
+ file.parentFile.mkdirsOrFail()
+ val writer = file.bufferedWriter()
+ uriToWriter[normalized] = writer
+ return writer
+ }
+
+ outlineFactoryService?.generateOutlines(::provideOutput, nodes)
+
+ uriToWriter.values.forEach { it.close() }
+ }
+
+ override fun buildSupportFiles() {}
+
+ override fun buildPackageList(nodes: Iterable<DocumentationNode>) {
+ nodes.filter { it.kind == NodeKind.Module }.forEach { module ->
+ val moduleRoot = root.resolve(module.name)
+ val packageListFile = moduleRoot.resolve("package-list")
+ packageListFile.writeText(packageListService.formatPackageList(module as DocumentationModule))
+ }
+ }
+}
+
+interface JavaLayoutHtmlFormatOutputBuilderFactory {
+ fun createOutputBuilder(output: Appendable, uri: URI): JavaLayoutHtmlFormatOutputBuilder
+ fun createOutputBuilder(output: Appendable, node: DocumentationNode): JavaLayoutHtmlFormatOutputBuilder
+}
diff --git a/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt
new file mode 100644
index 000000000..ce05fe897
--- /dev/null
+++ b/core/src/main/kotlin/Formats/JavaLayoutHtml/JavaLayoutHtmlPackageListService.kt
@@ -0,0 +1,154 @@
+package org.jetbrains.dokka.Formats
+
+import com.intellij.psi.PsiMember
+import com.intellij.psi.PsiParameter
+import org.jetbrains.dokka.*
+import org.jetbrains.dokka.ExternalDocumentationLinkResolver.Companion.DOKKA_PARAM_PREFIX
+import org.jetbrains.kotlin.asJava.toLightElements
+import org.jetbrains.kotlin.descriptors.*
+import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor
+import org.jetbrains.kotlin.load.java.descriptors.JavaMethodDescriptor
+import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor
+import org.jetbrains.kotlin.psi.KtDeclaration
+import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
+import org.jetbrains.kotlin.resolve.descriptorUtil.isCompanionObject
+import org.jetbrains.kotlin.types.KotlinType
+
+class JavaLayoutHtmlPackageListService: PackageListService {
+
+ private fun StringBuilder.appendParam(name: String, value: String) {
+ append(DOKKA_PARAM_PREFIX)
+ append(name)
+ append(":")
+ appendln(value)
+ }
+
+ override fun formatPackageList(module: DocumentationModule): String {
+ val packages = module.members(NodeKind.Package).map { it.name }
+
+ return buildString {
+ appendParam("format", "java-layout-html")
+ appendParam("mode", "kotlin")
+ for (p in packages) {
+ appendln(p)
+ }
+ }
+ }
+
+}
+
+class JavaLayoutHtmlInboundLinkResolutionService(private val paramMap: Map<String, List<String>>,
+ private val resolutionFacade: DokkaResolutionFacade) : InboundExternalLinkResolutionService {
+
+ constructor(asJava: Boolean, resolutionFacade: DokkaResolutionFacade) :
+ this(mapOf("mode" to listOf(if (asJava) "java" else "kotlin")), resolutionFacade)
+
+
+ private val isJavaMode = paramMap["mode"]!!.single() == "java"
+
+ private fun getContainerPath(symbol: DeclarationDescriptor): String? {
+ return when (symbol) {
+ is PackageFragmentDescriptor -> symbol.fqName.asString().replace('.', '/') + "/"
+ is ClassifierDescriptor -> getContainerPath(symbol.findPackage()) + symbol.nameWithOuter() + ".html"
+ else -> null
+ }
+ }
+
+ private fun DeclarationDescriptor.findPackage(): PackageFragmentDescriptor =
+ generateSequence(this) { it.containingDeclaration }.filterIsInstance<PackageFragmentDescriptor>().first()
+
+ private fun ClassifierDescriptor.nameWithOuter(): String =
+ generateSequence(this) { it.containingDeclaration as? ClassifierDescriptor }
+ .toList().asReversed().joinToString(".") { it.name.asString() }
+
+ private fun getJavaPagePath(symbol: DeclarationDescriptor): String? {
+
+ val sourcePsi = symbol.sourcePsi() ?: return null
+ val source = (if (sourcePsi is KtDeclaration) {
+ sourcePsi.toLightElements().firstOrNull()
+ } else {
+ sourcePsi
+ }) as? PsiMember ?: return null
+ val desc = source.getJavaMemberDescriptor(resolutionFacade) ?: return null
+ return getPagePath(desc)
+ }
+
+ private fun getPagePath(symbol: DeclarationDescriptor): String? {
+ return when (symbol) {
+ is PackageFragmentDescriptor -> getContainerPath(symbol) + "package-summary.html"
+ is EnumEntrySyntheticClassDescriptor -> getContainerPath(symbol.containingDeclaration) + "#" + symbol.signatureForAnchorUrlEncoded()
+ is ClassifierDescriptor -> getContainerPath(symbol) + "#"
+ is FunctionDescriptor, is PropertyDescriptor -> getContainerPath(symbol.containingDeclaration!!) + "#" + symbol.signatureForAnchorUrlEncoded()
+ else -> null
+ }
+ }
+
+ private fun DeclarationDescriptor.signatureForAnchor(): String? {
+
+ fun ReceiverParameterDescriptor.extractReceiverName(): String {
+ var receiverClass: DeclarationDescriptor = type.constructor.declarationDescriptor!!
+ if (receiverClass.isCompanionObject()) {
+ receiverClass = receiverClass.containingDeclaration!!
+ } else if (receiverClass is TypeParameterDescriptor) {
+ val upperBoundClass = receiverClass.upperBounds.singleOrNull()?.constructor?.declarationDescriptor
+ if (upperBoundClass != null) {
+ receiverClass = upperBoundClass
+ }
+ }
+
+ return receiverClass.name.asString()
+ }
+
+ fun KotlinType.qualifiedNameForSignature(): String {
+ val desc = constructor.declarationDescriptor
+ return desc?.fqNameUnsafe?.asString() ?: "<ERROR TYPE NAME>"
+ }
+
+ fun StringBuilder.appendReceiverAndCompanion(desc: CallableDescriptor) {
+ if (desc.containingDeclaration.isCompanionObject()) {
+ append("Companion.")
+ }
+ desc.extensionReceiverParameter?.let {
+ append("(")
+ append(it.extractReceiverName())
+ append(").")
+ }
+ }
+
+ return when (this) {
+ is EnumEntrySyntheticClassDescriptor -> buildString {
+ append("ENUM_VALUE:")
+ append(name.asString())
+ }
+ is JavaMethodDescriptor -> buildString {
+ append(name.asString())
+ valueParameters.joinTo(this, prefix = "(", postfix = ")") {
+ val param = it.sourcePsi() as PsiParameter
+ param.type.canonicalText
+ }
+ }
+ is JavaPropertyDescriptor -> buildString {
+ append(name.asString())
+ }
+ is FunctionDescriptor -> buildString {
+ appendReceiverAndCompanion(this@signatureForAnchor)
+ append(name.asString())
+ valueParameters.joinTo(this, prefix = "(", postfix = ")") {
+ it.type.qualifiedNameForSignature()
+ }
+ }
+ is PropertyDescriptor -> buildString {
+ appendReceiverAndCompanion(this@signatureForAnchor)
+ append(name.asString())
+ append(":")
+
+ append(returnType?.qualifiedNameForSignature())
+ }
+ else -> null
+ }
+ }
+
+ private fun DeclarationDescriptor.signatureForAnchorUrlEncoded(): String? = signatureForAnchor()?.anchorEncoded()
+
+ override fun getPath(symbol: DeclarationDescriptor) = if (isJavaMode) getJavaPagePath(symbol) else getPagePath(symbol)
+}
diff --git a/core/src/main/kotlin/Formats/JekyllFormatService.kt b/core/src/main/kotlin/Formats/JekyllFormatService.kt
new file mode 100644
index 000000000..a948dfa93
--- /dev/null
+++ b/core/src/main/kotlin/Formats/JekyllFormatService.kt
@@ -0,0 +1,44 @@
+package org.jetbrains.dokka
+
+import com.google.inject.Inject
+import com.google.inject.name.Named
+import org.jetbrains.dokka.Utilities.impliedPlatformsName
+
+open class JekyllOutputBuilder(to: StringBuilder,
+ location: Location,
+ generator: NodeLocationAwareGenerator,
+ languageService: LanguageService,
+ extension: String,
+ impliedPlatforms: List<String>)
+ : MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) {
+ override fun appendNodes(nodes: Iterable<DocumentationNode>) {
+ to.appendln("---")
+ appendFrontMatter(nodes, to)
+ to.appendln("---")
+ to.appendln("")
+ super.appendNodes(nodes)
+ }
+
+ protected open fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) {
+ to.appendln("title: ${getPageTitle(nodes)}")
+ }
+}
+
+
+open class JekyllFormatService(
+ generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ linkExtension: String,
+ impliedPlatforms: List<String>
+) : MarkdownFormatService(generator, signatureGenerator, linkExtension, impliedPlatforms) {
+
+ @Inject constructor(
+ generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ @Named(impliedPlatformsName) impliedPlatforms: List<String>
+ ) : this(generator, signatureGenerator, "html", impliedPlatforms)
+
+ override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder =
+ JekyllOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms)
+
+}
diff --git a/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt
new file mode 100644
index 000000000..a98002d49
--- /dev/null
+++ b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt
@@ -0,0 +1,224 @@
+package org.jetbrains.dokka
+
+import com.google.inject.Inject
+import com.google.inject.name.Named
+import org.jetbrains.dokka.Utilities.impliedPlatformsName
+import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
+
+
+open class KotlinWebsiteOutputBuilder(
+ to: StringBuilder,
+ location: Location,
+ generator: NodeLocationAwareGenerator,
+ languageService: LanguageService,
+ extension: String,
+ impliedPlatforms: List<String>
+) : JekyllOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) {
+ private var needHardLineBreaks = false
+ private var insideDiv = 0
+
+ override fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) {
+ super.appendFrontMatter(nodes, to)
+ to.appendln("layout: api")
+ }
+
+ override fun appendBreadcrumbs(path: Iterable<FormatLink>) {
+ if (path.count() > 1) {
+ to.append("<div class='api-docs-breadcrumbs'>")
+ super.appendBreadcrumbs(path)
+ to.append("</div>")
+ }
+ }
+
+ override fun appendCode(body: () -> Unit) = wrapIfNotEmpty("<code>", "</code>", body)
+
+ override fun appendStrikethrough(body: () -> Unit) = wrapInTag("s", body)
+
+ protected fun div(to: StringBuilder, cssClass: String, otherAttributes: String = "", markdown: Boolean = false, block: () -> Unit) {
+ to.append("<div class=\"$cssClass\"$otherAttributes")
+ if (markdown) to.append(" markdown=\"1\"")
+ to.append(">")
+ if (!markdown) insideDiv++
+ block()
+ if (!markdown) insideDiv--
+ to.append("</div>\n")
+ }
+
+ override fun appendAsSignature(node: ContentNode, block: () -> Unit) {
+ val contentLength = node.textLength
+ if (contentLength == 0) return
+ div(to, "signature") {
+ needHardLineBreaks = contentLength >= 62
+ try {
+ block()
+ } finally {
+ needHardLineBreaks = false
+ }
+ }
+ }
+
+ override fun appendAsOverloadGroup(to: StringBuilder, platforms: Set<String>, block: () -> Unit) {
+ div(to, "overload-group", calculateDataAttributes(platforms), true) {
+ ensureParagraph()
+ block()
+ ensureParagraph()
+ }
+ }
+
+ override fun appendLink(href: String, body: () -> Unit) = wrap("<a href=\"$href\">", "</a>", body)
+
+ override fun appendHeader(level: Int, body: () -> Unit) {
+ if (insideDiv > 0) {
+ wrapInTag("p", body, newlineAfterClose = true)
+ } else {
+ super.appendHeader(level, body)
+ }
+ }
+
+ override fun appendLine() {
+ if (insideDiv > 0) {
+ to.appendln("<br/>")
+ } else {
+ super.appendLine()
+ }
+ }
+
+ override fun appendTable(vararg columns: String, body: () -> Unit) {
+ to.appendln("<table class=\"api-docs-table\">")
+ body()
+ to.appendln("</table>")
+ }
+
+ override fun appendTableBody(body: () -> Unit) {
+ to.appendln("<tbody>")
+ body()
+ to.appendln("</tbody>")
+ }
+
+ override fun appendTableRow(body: () -> Unit) {
+ to.appendln("<tr>")
+ body()
+ to.appendln("</tr>")
+ }
+
+ override fun appendTableCell(body: () -> Unit) {
+ to.appendln("<td markdown=\"1\">")
+ body()
+ to.appendln("\n</td>")
+ }
+
+ override fun appendBlockCode(language: String, body: () -> Unit) {
+ if (language.isNotEmpty()) {
+ super.appendBlockCode(language, body)
+ } else {
+ wrap("<pre markdown=\"1\">", "</pre>", body)
+ }
+ }
+
+ override fun appendSymbol(text: String) {
+ to.append("<span class=\"symbol\">${text.htmlEscape()}</span>")
+ }
+
+ override fun appendKeyword(text: String) {
+ to.append("<span class=\"keyword\">${text.htmlEscape()}</span>")
+ }
+
+ override fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) {
+ val id = signature?.let { " id=\"$it\"" }.orEmpty()
+ to.append("<span class=\"${identifierClassName(kind)}\"$id>${text.htmlEscape()}</span>")
+ }
+
+ override fun appendSoftLineBreak() {
+ if (needHardLineBreaks)
+ to.append("<br/>")
+
+ }
+
+ override fun appendIndentedSoftLineBreak() {
+ if (needHardLineBreaks) {
+ to.append("<br/>&nbsp;&nbsp;&nbsp;&nbsp;")
+ }
+ }
+
+ private fun identifierClassName(kind: IdentifierKind) = when (kind) {
+ IdentifierKind.ParameterName -> "parameterName"
+ IdentifierKind.SummarizedTypeName -> "summarizedTypeName"
+ else -> "identifier"
+ }
+
+ fun calculateDataAttributes(platforms: Set<String>): String {
+ fun String.isKotlinVersion() = this.startsWith("Kotlin")
+ fun String.isJREVersion() = this.startsWith("JRE")
+ val kotlinVersion = platforms.singleOrNull(String::isKotlinVersion)
+ val jreVersion = platforms.singleOrNull(String::isJREVersion)
+ val targetPlatforms = platforms.filterNot { it.isKotlinVersion() || it.isJREVersion() }
+
+ val kotlinVersionAttr = kotlinVersion?.let { " data-kotlin-version=\"$it\"" } ?: ""
+ val jreVersionAttr = jreVersion?.let { " data-jre-version=\"$it\"" } ?: ""
+ val platformsAttr = targetPlatforms.ifNotEmpty { " data-platform=\"${targetPlatforms.joinToString()}\"" } ?: ""
+ return "$platformsAttr$kotlinVersionAttr$jreVersionAttr"
+ }
+
+ override fun appendIndexRow(platforms: Set<String>, block: () -> Unit) {
+ if (platforms.isNotEmpty())
+ wrap("<tr${calculateDataAttributes(platforms)}>", "</tr>", block)
+ else
+ appendTableRow(block)
+ }
+
+ override fun appendPlatforms(platforms: Set<String>) {
+
+ }
+}
+
+class KotlinWebsiteFormatService @Inject constructor(
+ generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ @Named(impliedPlatformsName) impliedPlatforms: List<String>,
+ logger: DokkaLogger
+) : JekyllFormatService(generator, signatureGenerator, "html", impliedPlatforms) {
+ init {
+ logger.warn("Format kotlin-website deprecated and will be removed in next release")
+ }
+
+ override fun createOutputBuilder(to: StringBuilder, location: Location) =
+ KotlinWebsiteOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms)
+}
+
+
+class KotlinWebsiteRunnableSamplesOutputBuilder(
+ to: StringBuilder,
+ location: Location,
+ generator: NodeLocationAwareGenerator,
+ languageService: LanguageService,
+ extension: String,
+ impliedPlatforms: List<String>
+) : KotlinWebsiteOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) {
+
+ override fun appendSampleBlockCode(language: String, imports: () -> Unit, body: () -> Unit) {
+ div(to, "sample", markdown = true) {
+ appendBlockCode(language) {
+ imports()
+ wrap("\n\nfun main(args: Array<String>) {", "}") {
+ wrap("\n//sampleStart\n", "\n//sampleEnd\n", body)
+ }
+ }
+ }
+ }
+}
+
+class KotlinWebsiteRunnableSamplesFormatService @Inject constructor(
+ generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ @Named(impliedPlatformsName) impliedPlatforms: List<String>,
+ logger: DokkaLogger
+) : JekyllFormatService(generator, signatureGenerator, "html", impliedPlatforms) {
+
+ init {
+ logger.warn("Format kotlin-website-samples deprecated and will be removed in next release")
+ }
+
+ override fun createOutputBuilder(to: StringBuilder, location: Location) =
+ KotlinWebsiteRunnableSamplesOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms)
+}
+
diff --git a/core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt b/core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt
new file mode 100644
index 000000000..6ced75b55
--- /dev/null
+++ b/core/src/main/kotlin/Formats/KotlinWebsiteHtmlFormatService.kt
@@ -0,0 +1,186 @@
+package org.jetbrains.dokka
+
+import com.google.inject.Inject
+import com.google.inject.name.Named
+import org.jetbrains.dokka.Utilities.impliedPlatformsName
+import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
+import java.io.File
+
+
+object EmptyHtmlTemplateService : HtmlTemplateService {
+ override fun appendFooter(to: StringBuilder) {}
+
+ override fun appendHeader(to: StringBuilder, title: String?, basePath: File) {}
+}
+
+
+open class KotlinWebsiteHtmlOutputBuilder(
+ to: StringBuilder,
+ location: Location,
+ generator: NodeLocationAwareGenerator,
+ languageService: LanguageService,
+ extension: String,
+ impliedPlatforms: List<String>,
+ templateService: HtmlTemplateService
+) : HtmlOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms, templateService) {
+ private var needHardLineBreaks = false
+ private var insideDiv = 0
+
+ override fun appendLine() {}
+
+ override fun appendBreadcrumbs(path: Iterable<FormatLink>) {
+ if (path.count() > 1) {
+ to.append("<div class='api-docs-breadcrumbs'>")
+ super.appendBreadcrumbs(path)
+ to.append("</div>")
+ }
+ }
+
+ override fun appendCode(body: () -> Unit) = wrapIfNotEmpty("<code>", "</code>", body)
+
+ protected fun div(to: StringBuilder, cssClass: String, otherAttributes: String = "", block: () -> Unit) {
+ to.append("<div class=\"$cssClass\"$otherAttributes")
+ to.append(">")
+ insideDiv++
+ block()
+ insideDiv--
+ to.append("</div>\n")
+ }
+
+ override fun appendAsSignature(node: ContentNode, block: () -> Unit) {
+ val contentLength = node.textLength
+ if (contentLength == 0) return
+ div(to, "signature") {
+ needHardLineBreaks = contentLength >= 62
+ try {
+ block()
+ } finally {
+ needHardLineBreaks = false
+ }
+ }
+ }
+
+ override fun appendAsOverloadGroup(to: StringBuilder, platforms: Set<String>, block: () -> Unit) {
+ div(to, "overload-group", calculateDataAttributes(platforms)) {
+ block()
+ }
+ }
+
+ override fun appendLink(href: String, body: () -> Unit) = wrap("<a href=\"$href\">", "</a>", body)
+
+ override fun appendTable(vararg columns: String, body: () -> Unit) {
+ to.appendln("<table class=\"api-docs-table\">")
+ body()
+ to.appendln("</table>")
+ }
+
+ override fun appendTableBody(body: () -> Unit) {
+ to.appendln("<tbody>")
+ body()
+ to.appendln("</tbody>")
+ }
+
+ override fun appendTableRow(body: () -> Unit) {
+ to.appendln("<tr>")
+ body()
+ to.appendln("</tr>")
+ }
+
+ override fun appendTableCell(body: () -> Unit) {
+ to.appendln("<td>")
+ body()
+ to.appendln("\n</td>")
+ }
+
+ override fun appendSymbol(text: String) {
+ to.append("<span class=\"symbol\">${text.htmlEscape()}</span>")
+ }
+
+ override fun appendKeyword(text: String) {
+ to.append("<span class=\"keyword\">${text.htmlEscape()}</span>")
+ }
+
+ override fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) {
+ val id = signature?.let { " id=\"$it\"" }.orEmpty()
+ to.append("<span class=\"${identifierClassName(kind)}\"$id>${text.htmlEscape()}</span>")
+ }
+
+ override fun appendSoftLineBreak() {
+ if (needHardLineBreaks)
+ to.append("<br/>")
+ }
+
+ override fun appendIndentedSoftLineBreak() {
+ if (needHardLineBreaks) {
+ to.append("<br/>&nbsp;&nbsp;&nbsp;&nbsp;")
+ }
+ }
+
+ private fun identifierClassName(kind: IdentifierKind) = when (kind) {
+ IdentifierKind.ParameterName -> "parameterName"
+ IdentifierKind.SummarizedTypeName -> "summarizedTypeName"
+ else -> "identifier"
+ }
+
+ fun calculateDataAttributes(platforms: Set<String>): String {
+ fun String.isKotlinVersion() = this.startsWith("Kotlin")
+ fun String.isJREVersion() = this.startsWith("JRE")
+ val kotlinVersion = platforms.singleOrNull(String::isKotlinVersion)
+ val jreVersion = platforms.singleOrNull(String::isJREVersion)
+ val targetPlatforms = platforms.filterNot { it.isKotlinVersion() || it.isJREVersion() }
+
+ val kotlinVersionAttr = kotlinVersion?.let { " data-kotlin-version=\"$it\"" } ?: ""
+ val jreVersionAttr = jreVersion?.let { " data-jre-version=\"$it\"" } ?: ""
+ val platformsAttr = targetPlatforms.ifNotEmpty { " data-platform=\"${targetPlatforms.joinToString()}\"" } ?: ""
+ return "$platformsAttr$kotlinVersionAttr$jreVersionAttr"
+ }
+
+ override fun appendIndexRow(platforms: Set<String>, block: () -> Unit) {
+ if (platforms.isNotEmpty())
+ wrap("<tr${calculateDataAttributes(platforms)}>", "</tr>", block)
+ else
+ appendTableRow(block)
+ }
+
+ override fun appendPlatforms(platforms: Set<String>) {}
+
+ override fun appendBreadcrumbSeparator() {
+ to.append(" / ")
+ }
+
+ override fun appendSampleBlockCode(language: String, imports: () -> Unit, body: () -> Unit) {
+ div(to, "sample") {
+ appendBlockCode(language) {
+ imports()
+ wrap("\n\nfun main(args: Array<String>) {".htmlEscape(), "}") {
+ wrap("\n//sampleStart\n", "\n//sampleEnd\n", body)
+ }
+ }
+ }
+ }
+
+ override fun appendSoftParagraph(body: () -> Unit) = appendParagraph(body)
+
+
+ override fun appendSectionWithTag(section: ContentSection) {
+ appendParagraph {
+ appendStrong { appendText(section.tag) }
+ appendText(" ")
+ appendContent(section)
+ }
+ }
+}
+
+class KotlinWebsiteHtmlFormatService @Inject constructor(
+ generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ @Named(impliedPlatformsName) impliedPlatforms: List<String>,
+ templateService: HtmlTemplateService
+) : HtmlFormatService(generator, signatureGenerator, templateService, impliedPlatforms) {
+
+ override fun enumerateSupportFiles(callback: (String, String) -> Unit) {}
+
+ override fun createOutputBuilder(to: StringBuilder, location: Location) =
+ KotlinWebsiteHtmlOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms, templateService)
+}
+
diff --git a/core/src/main/kotlin/Formats/MarkdownFormatService.kt b/core/src/main/kotlin/Formats/MarkdownFormatService.kt
new file mode 100644
index 000000000..4265394f2
--- /dev/null
+++ b/core/src/main/kotlin/Formats/MarkdownFormatService.kt
@@ -0,0 +1,239 @@
+package org.jetbrains.dokka
+
+import com.google.inject.Inject
+import com.google.inject.name.Named
+import org.jetbrains.dokka.Utilities.impliedPlatformsName
+import java.util.*
+
+enum class ListKind {
+ Ordered,
+ Unordered
+}
+
+private class ListState(val kind: ListKind, var size: Int = 1) {
+ fun getTagAndIncrement() = when (kind) {
+ ListKind.Ordered -> "${size++}. "
+ else -> "* "
+ }
+}
+
+private val TWO_LINE_BREAKS = System.lineSeparator() + System.lineSeparator()
+
+open class MarkdownOutputBuilder(to: StringBuilder,
+ location: Location,
+ generator: NodeLocationAwareGenerator,
+ languageService: LanguageService,
+ extension: String,
+ impliedPlatforms: List<String>)
+ : StructuredOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms)
+{
+ private val listStack = ArrayDeque<ListState>()
+ protected var inTableCell = false
+ protected var inCodeBlock = false
+ private var lastTableCellStart = -1
+ private var maxBackticksInCodeBlock = 0
+
+ private fun appendNewline() {
+ while (to.endsWith(' ')) {
+ to.setLength(to.length - 1)
+ }
+ to.appendln()
+ }
+
+ private fun ensureNewline() {
+ if (inTableCell && listStack.isEmpty()) {
+ if (to.length != lastTableCellStart && !to.endsWith("<br>")) {
+ to.append("<br>")
+ }
+ }
+ else {
+ if (!endsWithNewline()) {
+ appendNewline()
+ }
+ }
+ }
+
+ private fun endsWithNewline(): Boolean {
+ var index = to.length - 1
+ while (index > 0) {
+ val c = to[index]
+ if (c != ' ') {
+ return c == '\n'
+ }
+ index--
+ }
+ return false
+ }
+
+ override fun ensureParagraph() {
+ if (!to.endsWith(TWO_LINE_BREAKS)) {
+ if (!to.endsWith('\n')) {
+ appendNewline()
+ }
+ appendNewline()
+ }
+ }
+ override fun appendBreadcrumbSeparator() {
+ to.append(" / ")
+ }
+
+ private val backTickFindingRegex = """(`+)""".toRegex()
+
+ override fun appendText(text: String) {
+ if (inCodeBlock) {
+ to.append(text)
+ val backTicks = backTickFindingRegex.findAll(text)
+ val longestBackTickRun = backTicks.map { it.value.length }.max() ?: 0
+ maxBackticksInCodeBlock = maxBackticksInCodeBlock.coerceAtLeast(longestBackTickRun)
+ }
+ else {
+ if (text == "\n" && inTableCell) {
+ to.append(" ")
+ } else {
+ to.append(text.htmlEscape())
+ }
+ }
+ }
+
+ override fun appendCode(body: () -> Unit) {
+ inCodeBlock = true
+ val codeBlockStart = to.length
+ maxBackticksInCodeBlock = 0
+
+ wrapIfNotEmpty("`", "`", body, checkEndsWith = true)
+
+ if (maxBackticksInCodeBlock > 0) {
+ val extraBackticks = "`".repeat(maxBackticksInCodeBlock)
+ to.insert(codeBlockStart, extraBackticks)
+ to.append(extraBackticks)
+ }
+
+ inCodeBlock = false
+ }
+
+ override fun appendUnorderedList(body: () -> Unit) {
+ listStack.push(ListState(ListKind.Unordered))
+ body()
+ listStack.pop()
+ ensureNewline()
+ }
+
+ override fun appendOrderedList(body: () -> Unit) {
+ listStack.push(ListState(ListKind.Ordered))
+ body()
+ listStack.pop()
+ ensureNewline()
+ }
+
+ override fun appendListItem(body: () -> Unit) {
+ ensureNewline()
+ to.append(listStack.peek()?.getTagAndIncrement())
+ body()
+ ensureNewline()
+ }
+
+ override fun appendStrong(body: () -> Unit) = wrap("**", "**", body)
+ override fun appendEmphasis(body: () -> Unit) = wrap("*", "*", body)
+ override fun appendStrikethrough(body: () -> Unit) = wrap("~~", "~~", body)
+
+ override fun appendLink(href: String, body: () -> Unit) {
+ if (inCodeBlock) {
+ wrap("`[`", "`]($href)`", body)
+ }
+ else {
+ wrap("[", "]($href)", body)
+ }
+ }
+
+ override fun appendLine() {
+ if (inTableCell) {
+ to.append("<br>")
+ }
+ else {
+ appendNewline()
+ }
+ }
+
+ override fun appendAnchor(anchor: String) {
+ // no anchors in Markdown
+ }
+
+ override fun appendParagraph(body: () -> Unit) {
+ if (inTableCell) {
+ ensureNewline()
+ body()
+ } else if (listStack.isNotEmpty()) {
+ body()
+ ensureNewline()
+ } else {
+ ensureParagraph()
+ body()
+ ensureParagraph()
+ }
+ }
+
+ override fun appendHeader(level: Int, body: () -> Unit) {
+ ensureParagraph()
+ to.append("${"#".repeat(level)} ")
+ body()
+ ensureParagraph()
+ }
+
+ override fun appendBlockCode(language: String, body: () -> Unit) {
+ inCodeBlock = true
+ ensureParagraph()
+ to.appendln(if (language.isEmpty()) "```" else "``` $language")
+ body()
+ ensureNewline()
+ to.appendln("```")
+ appendLine()
+ inCodeBlock = false
+ }
+
+ override fun appendTable(vararg columns: String, body: () -> Unit) {
+ ensureParagraph()
+ body()
+ ensureParagraph()
+ }
+
+ override fun appendTableBody(body: () -> Unit) {
+ body()
+ }
+
+ override fun appendTableRow(body: () -> Unit) {
+ to.append("|")
+ body()
+ appendNewline()
+ }
+
+ override fun appendTableCell(body: () -> Unit) {
+ to.append(" ")
+ inTableCell = true
+ lastTableCellStart = to.length
+ body()
+ inTableCell = false
+ to.append(" |")
+ }
+
+ override fun appendNonBreakingSpace() {
+ if (inCodeBlock) {
+ to.append(" ")
+ }
+ else {
+ to.append("&nbsp;")
+ }
+ }
+}
+
+open class MarkdownFormatService(generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ linkExtension: String,
+ val impliedPlatforms: List<String>)
+: StructuredFormatService(generator, signatureGenerator, "md", linkExtension) {
+ @Inject constructor(generator: NodeLocationAwareGenerator,
+ signatureGenerator: LanguageService,
+ @Named(impliedPlatformsName) impliedPlatforms: List<String>): this(generator, signatureGenerator, "md", impliedPlatforms)
+
+ override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder =
+ MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms)
+}
diff --git a/core/src/main/kotlin/Formats/OutlineService.kt b/core/src/main/kotlin/Formats/OutlineService.kt
new file mode 100644
index 000000000..958e93aff
--- /dev/null
+++ b/core/src/main/kotlin/Formats/OutlineService.kt
@@ -0,0 +1,29 @@
+package org.jetbrains.dokka
+
+import java.io.File
+
+/**
+ * Service for building the outline of the package contents.
+ */
+interface OutlineFormatService {
+ fun getOutlineFileName(location: Location): File
+
+ fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder)
+ fun appendOutlineLevel(to: StringBuilder, body: () -> Unit)
+
+ /** Appends formatted outline to [StringBuilder](to) using specified [location] */
+ fun appendOutline(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) {
+ for (node in nodes) {
+ appendOutlineHeader(location, node, to)
+ if (node.members.any()) {
+ val sortedMembers = node.members.sortedBy { it.name.toLowerCase() }
+ appendOutlineLevel(to) {
+ appendOutline(location, to, sortedMembers)
+ }
+ }
+ }
+ }
+
+ fun formatOutline(location: Location, nodes: Iterable<DocumentationNode>): String =
+ StringBuilder().apply { appendOutline(location, this, nodes) }.toString()
+}
diff --git a/core/src/main/kotlin/Formats/PackageListService.kt b/core/src/main/kotlin/Formats/PackageListService.kt
new file mode 100644
index 000000000..7b68098e8
--- /dev/null
+++ b/core/src/main/kotlin/Formats/PackageListService.kt
@@ -0,0 +1,63 @@
+package org.jetbrains.dokka
+
+import com.google.inject.Inject
+
+
+interface PackageListService {
+ fun formatPackageList(module: DocumentationModule): String
+}
+
+class DefaultPackageListService @Inject constructor(
+ val generator: NodeLocationAwareGenerator,
+ val formatService: FormatService
+) : PackageListService {
+
+ override fun formatPackageList(module: DocumentationModule): String {
+ val packages = mutableSetOf<String>()
+ val nonStandardLocations = mutableMapOf<String, String>()
+
+ fun visit(node: DocumentationNode, relocated: Boolean = false) {
+ val nodeKind = node.kind
+
+ when (nodeKind) {
+ NodeKind.Package -> {
+ packages.add(node.qualifiedName())
+ node.members.forEach { visit(it) }
+ }
+ NodeKind.Signature -> {
+ if (relocated)
+ nonStandardLocations[node.name] = generator.relativePathToLocation(module, node.owner!!)
+ }
+ NodeKind.ExternalClass -> {
+ node.members.forEach { visit(it, relocated = true) }
+ }
+ NodeKind.GroupNode -> {
+ //only children of top-level GN records interesting for us, since link to top-level ones should point to GN
+ node.members.forEach { it.members.forEach { visit(it, relocated = true) } }
+ //record signature of GN as signature of type alias and class merged to GN, so link to it should point to GN
+ node.detailOrNull(NodeKind.Signature)?.let { visit(it, relocated = true) }
+ }
+ else -> {
+ if (nodeKind in NodeKind.classLike || nodeKind in NodeKind.memberLike) {
+ node.details(NodeKind.Signature).forEach { visit(it, relocated) }
+ node.members.forEach { visit(it, relocated) }
+ }
+ }
+ }
+ }
+
+ module.members.forEach { visit(it) }
+
+ return buildString {
+ appendln("\$dokka.linkExtension:${formatService.linkExtension}")
+
+ nonStandardLocations.map { (signature, location) -> "\$dokka.location:$signature\u001f$location" }
+ .sorted().joinTo(this, separator = "\n", postfix = "\n")
+
+ packages.sorted().joinTo(this, separator = "\n", postfix = "\n")
+ }
+
+ }
+
+}
+
diff --git a/core/src/main/kotlin/Formats/StandardFormats.kt b/core/src/main/kotlin/Formats/StandardFormats.kt
new file mode 100644
index 000000000..dd67ac972
--- /dev/null
+++ b/core/src/main/kotlin/Formats/StandardFormats.kt
@@ -0,0 +1,66 @@
+package org.jetbrains.dokka.Formats
+
+import com.google.inject.Binder
+import org.jetbrains.dokka.*
+import org.jetbrains.dokka.Samples.KotlinWebsiteSampleProcessingService
+import org.jetbrains.dokka.Utilities.bind
+import kotlin.reflect.KClass
+
+abstract class KotlinFormatDescriptorBase
+ : FileGeneratorBasedFormatDescriptor(),
+ DefaultAnalysisComponent,
+ DefaultAnalysisComponentServices by KotlinAsKotlin {
+ override val generatorServiceClass = FileGenerator::class
+ override val outlineServiceClass: KClass<out OutlineFormatService>? = null
+ override val packageListServiceClass: KClass<out PackageListService>? = DefaultPackageListService::class
+}
+
+abstract class HtmlFormatDescriptorBase : FileGeneratorBasedFormatDescriptor(), DefaultAnalysisComponent {
+ override val formatServiceClass = HtmlFormatService::class
+ override val outlineServiceClass = HtmlFormatService::class
+ override val generatorServiceClass = FileGenerator::class
+ override val packageListServiceClass = DefaultPackageListService::class
+
+ override fun configureOutput(binder: Binder): Unit = with(binder) {
+ super.configureOutput(binder)
+ bind<HtmlTemplateService>().toProvider { HtmlTemplateService.default("style.css") }
+ }
+}
+
+class HtmlFormatDescriptor : HtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsKotlin
+
+class HtmlAsJavaFormatDescriptor : HtmlFormatDescriptorBase(), DefaultAnalysisComponentServices by KotlinAsJava
+
+class KotlinWebsiteFormatDescriptor : KotlinFormatDescriptorBase() {
+ override val formatServiceClass = KotlinWebsiteFormatService::class
+ override val outlineServiceClass = YamlOutlineService::class
+}
+
+class KotlinWebsiteFormatRunnableSamplesDescriptor : KotlinFormatDescriptorBase() {
+ override val formatServiceClass = KotlinWebsiteRunnableSamplesFormatService::class
+ override val sampleProcessingService = KotlinWebsiteSampleProcessingService::class
+ override val outlineServiceClass = YamlOutlineService::class
+}
+
+class KotlinWebsiteHtmlFormatDescriptor : KotlinFormatDescriptorBase() {
+ override val formatServiceClass = KotlinWebsiteHtmlFormatService::class
+ override val sampleProcessingService = KotlinWebsiteSampleProcessingService::class
+ override val outlineServiceClass = YamlOutlineService::class
+
+ override fun configureOutput(binder: Binder) = with(binder) {
+ super.configureOutput(binder)
+ bind<HtmlTemplateService>().toInstance(EmptyHtmlTemplateService)
+ }
+}
+
+class JekyllFormatDescriptor : KotlinFormatDescriptorBase() {
+ override val formatServiceClass = JekyllFormatService::class
+}
+
+class MarkdownFormatDescriptor : KotlinFormatDescriptorBase() {
+ override val formatServiceClass = MarkdownFormatService::class
+}
+
+class GFMFormatDescriptor : KotlinFormatDescriptorBase() {
+ override val formatServiceClass = GFMFormatService::class
+}
diff --git a/core/src/main/kotlin/Formats/StructuredFormatService.kt b/core/src/main/kotlin/Formats/StructuredFormatService.kt
new file mode 100644
index 000000000..a2c9078f3
--- /dev/null
+++ b/core/src/main/kotlin/Formats/StructuredFormatService.kt
@@ -0,0 +1,691 @@
+package org.jetbrains.dokka
+
+import org.jetbrains.dokka.LanguageService.RenderMode
+import java.util.*
+
+data class FormatLink(val text: String, val href: String)
+
+abstract class StructuredOutputBuilder(val to: StringBuilder,
+ val location: Location,
+ val generator: NodeLocationAwareGenerator,
+ val languageService: LanguageService,
+ val extension: String,
+ val impliedPlatforms: List<String>) : FormattedOutputBuilder {
+
+ protected fun DocumentationNode.location() = generator.location(this)
+
+ protected fun wrap(prefix: String, suffix: String, body: () -> Unit) {
+ to.append(prefix)
+ body()
+ to.append(suffix)
+ }
+
+ protected fun wrapIfNotEmpty(prefix: String, suffix: String, body: () -> Unit, checkEndsWith: Boolean = false) {
+ val startLength = to.length
+ to.append(prefix)
+ body()
+ if (checkEndsWith && to.endsWith(suffix)) {
+ to.setLength(to.length - suffix.length)
+ } else if (to.length > startLength + prefix.length) {
+ to.append(suffix)
+ } else {
+ to.setLength(startLength)
+ }
+ }
+
+ protected fun wrapInTag(tag: String,
+ body: () -> Unit,
+ newlineBeforeOpen: Boolean = false,
+ newlineAfterOpen: Boolean = false,
+ newlineAfterClose: Boolean = false) {
+ if (newlineBeforeOpen && !to.endsWith('\n')) to.appendln()
+ to.append("<$tag>")
+ if (newlineAfterOpen) to.appendln()
+ body()
+ to.append("</$tag>")
+ if (newlineAfterClose) to.appendln()
+ }
+
+ protected abstract fun ensureParagraph()
+
+ open fun appendSampleBlockCode(language: String, imports: () -> Unit, body: () -> Unit) = appendBlockCode(language, body)
+ abstract fun appendBlockCode(language: String, body: () -> Unit)
+ abstract fun appendHeader(level: Int = 1, body: () -> Unit)
+ abstract fun appendParagraph(body: () -> Unit)
+
+ open fun appendSoftParagraph(body: () -> Unit) {
+ ensureParagraph()
+ body()
+ }
+
+ abstract fun appendLine()
+ abstract fun appendAnchor(anchor: String)
+
+ abstract fun appendTable(vararg columns: String, body: () -> Unit)
+ abstract fun appendTableBody(body: () -> Unit)
+ abstract fun appendTableRow(body: () -> Unit)
+ abstract fun appendTableCell(body: () -> Unit)
+
+ abstract fun appendText(text: String)
+
+ open fun appendSinceKotlin(version: String) {
+ appendParagraph {
+ appendText("Available since Kotlin: ")
+ appendCode { appendText(version) }
+ }
+ }
+
+ open fun appendSectionWithTag(section: ContentSection) {
+ appendParagraph {
+ appendStrong { appendText(section.tag) }
+ appendLine()
+ appendContent(section)
+ }
+ }
+
+ open fun appendSymbol(text: String) {
+ appendText(text)
+ }
+
+ open fun appendKeyword(text: String) {
+ appendText(text)
+ }
+
+ open fun appendIdentifier(text: String, kind: IdentifierKind, signature: String?) {
+ appendText(text)
+ }
+
+ fun appendEntity(text: String) {
+ to.append(text)
+ }
+
+ abstract fun appendLink(href: String, body: () -> Unit)
+
+ open fun appendLink(link: FormatLink) {
+ appendLink(link.href) { appendText(link.text) }
+ }
+
+ abstract fun appendStrong(body: () -> Unit)
+ abstract fun appendStrikethrough(body: () -> Unit)
+ abstract fun appendEmphasis(body: () -> Unit)
+ abstract fun appendCode(body: () -> Unit)
+ abstract fun appendUnorderedList(body: () -> Unit)
+ abstract fun appendOrderedList(body: () -> Unit)
+ abstract fun appendListItem(body: () -> Unit)
+
+ abstract fun appendBreadcrumbSeparator()
+ abstract fun appendNonBreakingSpace()
+ open fun appendSoftLineBreak() {
+ }
+
+ open fun appendIndentedSoftLineBreak() {
+ }
+
+ fun appendContent(content: List<ContentNode>) {
+ for (contentNode in content) {
+ appendContent(contentNode)
+ }
+ }
+
+ open fun appendContent(content: ContentNode) {
+ when (content) {
+ is ContentText -> appendText(content.text)
+ is ContentSymbol -> appendSymbol(content.text)
+ is ContentKeyword -> appendKeyword(content.text)
+ is ContentIdentifier -> appendIdentifier(content.text, content.kind, content.signature)
+ is ContentNonBreakingSpace -> appendNonBreakingSpace()
+ is ContentSoftLineBreak -> appendSoftLineBreak()
+ is ContentIndentedSoftLineBreak -> appendIndentedSoftLineBreak()
+ is ContentEntity -> appendEntity(content.text)
+ is ContentStrong -> appendStrong { appendContent(content.children) }
+ is ContentStrikethrough -> appendStrikethrough { appendContent(content.children) }
+ is ContentCode -> appendCode { appendContent(content.children) }
+ is ContentEmphasis -> appendEmphasis { appendContent(content.children) }
+ is ContentUnorderedList -> appendUnorderedList { appendContent(content.children) }
+ is ContentOrderedList -> appendOrderedList { appendContent(content.children) }
+ is ContentListItem -> appendListItem {
+ val child = content.children.singleOrNull()
+ if (child is ContentParagraph) {
+ appendContent(child.children)
+ } else {
+ appendContent(content.children)
+ }
+ }
+
+ is ContentNodeLink -> {
+ val node = content.node
+ val linkTo = if (node != null) locationHref(location, node) else "#"
+ appendLinkIfNotThisPage(linkTo, content)
+ }
+ is ContentExternalLink -> appendLinkIfNotThisPage(content.href, content)
+
+ is ContentParagraph -> {
+ if (!content.isEmpty()) {
+ appendParagraph { appendContent(content.children) }
+ }
+ }
+
+ is ContentSpecialReference -> wrapInTag(tag = "aside class=\"note\"", body = {
+ if (!content.isEmpty()) {
+ appendContent(content.children)
+ }
+ })
+
+ is ContentBlockSampleCode, is ContentBlockCode -> {
+ content as ContentBlockCode
+ fun ContentBlockCode.appendBlockCodeContent() {
+ children
+ .dropWhile { it is ContentText && it.text.isBlank() }
+ .forEach { appendContent(it) }
+ }
+ when (content) {
+ is ContentBlockSampleCode ->
+ appendSampleBlockCode(content.language, content.importsBlock::appendBlockCodeContent, { content.appendBlockCodeContent() })
+ is ContentBlockCode ->
+ appendBlockCode(content.language, { content.appendBlockCodeContent() })
+ }
+ }
+ is ContentHeading -> appendHeader(content.level) { appendContent(content.children) }
+ is ContentBlock -> appendContent(content.children)
+ }
+ }
+
+ private fun appendLinkIfNotThisPage(href: String, content: ContentBlock) {
+ if (href == ".") {
+ appendContent(content.children)
+ } else {
+ appendLink(href) { appendContent(content.children) }
+ }
+ }
+
+ open fun link(from: DocumentationNode,
+ to: DocumentationNode,
+ name: (DocumentationNode) -> String = DocumentationNode::name): FormatLink = link(from, to, extension, name)
+
+ open fun link(from: DocumentationNode,
+ to: DocumentationNode,
+ extension: String,
+ name: (DocumentationNode) -> String = DocumentationNode::name): FormatLink {
+ if (to.owner?.kind == NodeKind.GroupNode)
+ return link(from, to.owner!!, extension, name)
+
+ if (from.owner?.kind == NodeKind.GroupNode)
+ return link(from.owner!!, to, extension, name)
+
+ return FormatLink(name(to), from.location().relativePathTo(to.location()))
+ }
+
+ fun locationHref(from: Location, to: DocumentationNode): String {
+ val topLevelPage = to.references(RefKind.TopLevelPage).singleOrNull()?.to
+ if (topLevelPage != null) {
+ val signature = to.detailOrNull(NodeKind.Signature)
+ return from.relativePathTo(topLevelPage.location(), signature?.name ?: to.name)
+ }
+ return from.relativePathTo(to.location())
+ }
+
+ private fun DocumentationNode.isModuleOrPackage(): Boolean =
+ kind == NodeKind.Module || kind == NodeKind.Package
+
+ protected open fun appendAsSignature(node: ContentNode, block: () -> Unit) {
+ block()
+ }
+
+ protected open fun appendAsOverloadGroup(to: StringBuilder, platforms: Set<String>, block: () -> Unit) {
+ block()
+ }
+
+ protected open fun appendIndexRow(platforms: Set<String>, block: () -> Unit) {
+ appendTableRow(block)
+ }
+
+ protected open fun appendPlatforms(platforms: Set<String>) {
+ if (platforms.isNotEmpty()) {
+ appendLine()
+ appendText(platforms.joinToString(prefix = "(", postfix = ")"))
+ }
+ }
+
+ protected open fun appendBreadcrumbs(path: Iterable<FormatLink>) {
+ for ((index, item) in path.withIndex()) {
+ if (index > 0) {
+ appendBreadcrumbSeparator()
+ }
+ appendLink(item)
+ }
+ }
+
+ fun Content.getSectionsWithSubjects(): Map<String, List<ContentSection>> =
+ sections.filter { it.subjectName != null }.groupBy { it.tag }
+
+ private fun ContentNode.appendSignature() {
+ if (this is ContentBlock && this.isEmpty()) {
+ return
+ }
+
+ val signatureAsCode = ContentCode()
+ signatureAsCode.append(this)
+ appendContent(signatureAsCode)
+ }
+
+ open inner class PageBuilder(val nodes: Iterable<DocumentationNode>, val noHeader: Boolean = false) {
+ open fun build() {
+ val breakdownByLocation = nodes.groupBy { node ->
+ node.path.filterNot { it.name.isEmpty() }.map { link(node, it) }.distinct()
+ }
+
+ for ((path, nodes) in breakdownByLocation) {
+ if (!noHeader && path.isNotEmpty()) {
+ appendBreadcrumbs(path)
+ appendLine()
+ appendLine()
+ }
+ appendLocation(nodes.filter { it.kind != NodeKind.ExternalClass })
+ }
+ }
+
+ private fun appendLocation(nodes: Iterable<DocumentationNode>) {
+ val singleNode = nodes.singleOrNull()
+ if (singleNode != null && singleNode.isModuleOrPackage()) {
+ if (singleNode.kind == NodeKind.Package) {
+ val packageName = if (singleNode.name.isEmpty()) "<root>" else singleNode.name
+ appendHeader(2) { appendText("Package $packageName") }
+ }
+ singleNode.appendPlatforms()
+ appendContent(singleNode.content)
+ } else {
+ val breakdownByName = nodes.groupBy { node -> node.name }
+ for ((name, items) in breakdownByName) {
+ if (!noHeader)
+ appendHeader { appendText(name) }
+ appendDocumentation(items, singleNode != null)
+ }
+ }
+ }
+
+ private fun appendDocumentation(overloads: Iterable<DocumentationNode>, isSingleNode: Boolean) {
+ val breakdownBySummary = overloads.groupByTo(LinkedHashMap()) { node -> node.content }
+
+ if (breakdownBySummary.size == 1) {
+ formatOverloadGroup(breakdownBySummary.values.single(), isSingleNode)
+ } else {
+ for ((_, items) in breakdownBySummary) {
+
+ appendAsOverloadGroup(to, platformsOfItems(items)) {
+ formatOverloadGroup(items)
+ }
+
+ }
+ }
+ }
+
+ private fun formatOverloadGroup(items: List<DocumentationNode>, isSingleNode: Boolean = false) {
+ for ((index, item) in items.withIndex()) {
+ if (index > 0) appendLine()
+ val rendered = languageService.render(item)
+ item.detailOrNull(NodeKind.Signature)?.let {
+ if (item.kind !in NodeKind.classLike || !isSingleNode)
+ appendAnchor(it.name)
+ }
+ appendAsSignature(rendered) {
+ appendCode { appendContent(rendered) }
+ item.appendSourceLink()
+ }
+ item.appendOverrides()
+ item.appendDeprecation()
+ item.appendPlatforms()
+ }
+ // All items have exactly the same documentation, so we can use any item to render it
+ val item = items.first()
+ item.details(NodeKind.OverloadGroupNote).forEach {
+ appendContent(it.content)
+ }
+
+ appendContent(item.content.summary)
+ item.appendDescription()
+ }
+
+ private fun DocumentationNode.appendSourceLink() {
+ val sourceUrl = details(NodeKind.SourceUrl).firstOrNull()
+ if (sourceUrl != null) {
+ to.append(" ")
+ appendLink(sourceUrl.name) { to.append("(source)") }
+ }
+ }
+
+ private fun DocumentationNode.appendOverrides() {
+ overrides.forEach {
+ appendParagraph {
+ to.append("Overrides ")
+ val location = location().relativePathTo(it.location())
+
+ appendLink(FormatLink(it.owner!!.name + "." + it.name, location))
+ }
+ }
+ }
+
+ private fun DocumentationNode.appendDeprecation() {
+ if (deprecation != null) {
+ val deprecationParameter = deprecation!!.details(NodeKind.Parameter).firstOrNull()
+ val deprecationValue = deprecationParameter?.details(NodeKind.Value)?.firstOrNull()
+ appendLine()
+ if (deprecationValue != null) {
+ appendStrong { to.append("Deprecated:") }
+ appendText(" " + deprecationValue.name.removeSurrounding("\""))
+ appendLine()
+ appendLine()
+ } else if (deprecation?.content != Content.Empty) {
+ appendStrong { to.append("Deprecated:") }
+ to.append(" ")
+ appendContent(deprecation!!.content)
+ } else {
+ appendStrong { to.append("Deprecated") }
+ appendLine()
+ appendLine()
+ }
+ }
+ }
+
+ private fun DocumentationNode.appendPlatforms() {
+ val platforms = if (isModuleOrPackage())
+ platformsToShow.toSet() + platformsOfItems(members)
+ else
+ platformsToShow
+
+ if (platforms.isEmpty()) return
+
+ appendParagraph {
+ appendStrong { to.append("Platform and version requirements:") }
+ to.append(" " + platforms.joinToString())
+ }
+ }
+
+ protected fun platformsOfItems(items: List<DocumentationNode>): Set<String> {
+ val platforms = items.asSequence().map {
+ when (it.kind) {
+ NodeKind.ExternalClass, NodeKind.Package, NodeKind.Module, NodeKind.GroupNode -> platformsOfItems(it.members)
+ else -> it.platformsToShow.toSet()
+ }
+ }
+
+ fun String.isKotlinVersion() = this.startsWith("Kotlin")
+
+ // Calculating common platforms for items
+ return platforms.reduce { result, platformsOfItem ->
+ val otherKotlinVersion = result.find { it.isKotlinVersion() }
+ val (kotlinVersions, otherPlatforms) = platformsOfItem.partition { it.isKotlinVersion() }
+
+ // When no Kotlin version specified, it means that version is 1.0
+ if (otherKotlinVersion != null && kotlinVersions.isNotEmpty()) {
+ val allKotlinVersions = (kotlinVersions + otherKotlinVersion).distinct()
+
+ val minVersion = allKotlinVersions.min()!!
+ val resultVersion = when {
+ allKotlinVersions.size == 1 -> allKotlinVersions.single()
+ minVersion.endsWith("+") -> minVersion
+ else -> minVersion + "+"
+ }
+
+ result.intersect(otherPlatforms) + resultVersion
+ } else {
+ result.intersect(platformsOfItem)
+ }
+ }
+ }
+
+ val DocumentationNode.platformsToShow: List<String>
+ get() = platforms.let { if (it.containsAll(impliedPlatforms)) it - impliedPlatforms else it }
+
+ private fun DocumentationNode.appendDescription() {
+ if (content.description != ContentEmpty) {
+ appendContent(content.description)
+ }
+ content.getSectionsWithSubjects().forEach {
+ appendSectionWithSubject(it.key, it.value)
+ }
+
+ for (section in content.sections.filter { it.subjectName == null }) {
+ appendSectionWithTag(section)
+ }
+ }
+
+ fun appendSectionWithSubject(title: String, subjectSections: List<ContentSection>) {
+ appendHeader(3) { appendText(title) }
+ subjectSections.forEach {
+ val subjectName = it.subjectName
+ if (subjectName != null) {
+ appendSoftParagraph {
+ appendAnchor(subjectName)
+ appendCode { to.append(subjectName) }
+ to.append(" - ")
+ appendContent(it)
+ }
+ }
+ }
+ }
+ }
+
+ inner class GroupNodePageBuilder(val node: DocumentationNode) : PageBuilder(listOf(node)) {
+
+ override fun build() {
+ val breakdownByLocation = node.path.filterNot { it.name.isEmpty() }.map { link(node, it) }
+
+ appendBreadcrumbs(breakdownByLocation)
+ appendLine()
+ appendLine()
+ appendHeader { appendText(node.name) }
+
+ fun DocumentationNode.priority(): Int = when (kind) {
+ NodeKind.TypeAlias -> 1
+ NodeKind.Class -> 2
+ else -> 3
+ }
+
+ for (member in node.members.sortedBy(DocumentationNode::priority)) {
+
+ appendAsOverloadGroup(to, platformsOfItems(listOf(member))) {
+ formatSubNodeOfGroup(member)
+ }
+
+ }
+ }
+
+ fun formatSubNodeOfGroup(member: DocumentationNode) {
+ SingleNodePageBuilder(member, true).build()
+ }
+ }
+
+ inner class SingleNodePageBuilder(val node: DocumentationNode, noHeader: Boolean = false)
+ : PageBuilder(listOf(node), noHeader) {
+
+ override fun build() {
+ super.build()
+
+ if (node.kind == NodeKind.ExternalClass) {
+ appendSection("Extensions for ${node.name}", node.members)
+ return
+ }
+
+ fun DocumentationNode.membersOrGroupMembers(predicate: (DocumentationNode) -> Boolean): List<DocumentationNode> {
+ return members.filter(predicate) + members(NodeKind.GroupNode).flatMap { it.members.filter(predicate) }
+ }
+
+ fun DocumentationNode.membersOrGroupMembers(kind: NodeKind): List<DocumentationNode> {
+ return membersOrGroupMembers { it.kind == kind }
+ }
+
+ appendSection("Packages", node.members(NodeKind.Package), platformsBasedOnMembers = true)
+ appendSection("Types", node.membersOrGroupMembers { it.kind in NodeKind.classLike && it.kind != NodeKind.TypeAlias && it.kind != NodeKind.AnnotationClass && it.kind != NodeKind.Exception })
+ appendSection("Annotations", node.membersOrGroupMembers(NodeKind.AnnotationClass))
+ appendSection("Exceptions", node.membersOrGroupMembers(NodeKind.Exception))
+ appendSection("Type Aliases", node.membersOrGroupMembers(NodeKind.TypeAlias))
+ appendSection("Extensions for External Classes", node.members(NodeKind.ExternalClass))
+ appendSection("Enum Values", node.members(NodeKind.EnumItem), sortMembers = false, omitSamePlatforms = true)
+ appendSection("Constructors", node.members(NodeKind.Constructor), omitSamePlatforms = true)
+ appendSection("Properties", node.members(NodeKind.Property), omitSamePlatforms = true)
+ appendSection("Inherited Properties", node.inheritedMembers(NodeKind.Property))
+ appendSection("Functions", node.members(NodeKind.Function), omitSamePlatforms = true)
+ appendSection("Inherited Functions", node.inheritedMembers(NodeKind.Function))
+ appendSection("Companion Object Properties", node.members(NodeKind.CompanionObjectProperty), omitSamePlatforms = true)
+ appendSection("Inherited Companion Object Properties", node.inheritedCompanionObjectMembers(NodeKind.Property))
+ appendSection("Companion Object Functions", node.members(NodeKind.CompanionObjectFunction), omitSamePlatforms = true)
+ appendSection("Inherited Companion Object Functions", node.inheritedCompanionObjectMembers(NodeKind.Function))
+ appendSection("Other members", node.members.filter {
+ it.kind !in setOf(
+ NodeKind.Class,
+ NodeKind.Interface,
+ NodeKind.Enum,
+ NodeKind.Object,
+ NodeKind.AnnotationClass,
+ NodeKind.Exception,
+ NodeKind.TypeAlias,
+ NodeKind.Constructor,
+ NodeKind.Property,
+ NodeKind.Package,
+ NodeKind.Function,
+ NodeKind.CompanionObjectProperty,
+ NodeKind.CompanionObjectFunction,
+ NodeKind.ExternalClass,
+ NodeKind.EnumItem,
+ NodeKind.AllTypes,
+ NodeKind.GroupNode
+ )
+ })
+
+ val allExtensions = node.extensions
+ appendSection("Extension Properties", allExtensions.filter { it.kind == NodeKind.Property })
+ appendSection("Extension Functions", allExtensions.filter { it.kind == NodeKind.Function })
+ appendSection("Companion Object Extension Properties", allExtensions.filter { it.kind == NodeKind.CompanionObjectProperty })
+ appendSection("Companion Object Extension Functions", allExtensions.filter { it.kind == NodeKind.CompanionObjectFunction })
+ appendSection("Inheritors",
+ node.inheritors.filter { it.kind != NodeKind.EnumItem })
+
+ if (node.kind == NodeKind.Module) {
+ appendHeader(3) { to.append("Index") }
+ node.members(NodeKind.AllTypes).singleOrNull()?.let { allTypes ->
+ appendLink(link(node, allTypes, { "All Types" }))
+ }
+ }
+ }
+
+ private fun appendSection(caption: String, members: List<DocumentationNode>,
+ sortMembers: Boolean = true,
+ omitSamePlatforms: Boolean = false,
+ platformsBasedOnMembers: Boolean = false) {
+ if (members.isEmpty()) return
+
+ appendHeader(3) { appendText(caption) }
+
+ val children = if (sortMembers) members.sortedBy { it.name.toLowerCase() } else members
+ val membersMap = children.groupBy { link(node, it) }
+
+
+
+ appendTable("Name", "Summary") {
+ appendTableBody {
+ for ((memberLocation, members) in membersMap) {
+ val elementPlatforms = platformsOfItems(members, omitSamePlatforms)
+ val platforms = if (platformsBasedOnMembers)
+ members.flatMapTo(mutableSetOf()) { platformsOfItems(it.members) } + elementPlatforms
+ else
+ elementPlatforms
+ appendIndexRow(platforms) {
+ appendTableCell {
+ appendParagraph {
+ appendLink(memberLocation)
+ if (members.singleOrNull()?.kind != NodeKind.ExternalClass) {
+ appendPlatforms(platforms)
+ }
+ }
+ }
+ appendTableCell {
+ val breakdownBySummary = members.groupBy { it.summary }
+ for ((summary, items) in breakdownBySummary) {
+ appendSummarySignatures(items)
+ appendContent(summary)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun platformsOfItems(items: List<DocumentationNode>, omitSamePlatforms: Boolean = true): Set<String> {
+ val platforms = platformsOfItems(items)
+ if (platforms.isNotEmpty() && (platforms != node.platformsToShow.toSet() || !omitSamePlatforms)) {
+ return platforms
+ }
+ return emptySet()
+ }
+
+ private fun appendSummarySignatures(items: List<DocumentationNode>) {
+ val summarySignature = languageService.summarizeSignatures(items)
+ if (summarySignature != null) {
+ appendAsSignature(summarySignature) {
+ summarySignature.appendSignature()
+ }
+ return
+ }
+ val renderedSignatures = items.map { languageService.render(it, RenderMode.SUMMARY) }
+ renderedSignatures.subList(0, renderedSignatures.size - 1).forEach {
+ appendAsSignature(it) {
+ it.appendSignature()
+ }
+ appendLine()
+ }
+ appendAsSignature(renderedSignatures.last()) {
+ renderedSignatures.last().appendSignature()
+ }
+ }
+ }
+
+ inner class AllTypesNodeBuilder(val node: DocumentationNode)
+ : PageBuilder(listOf(node)) {
+
+ override fun build() {
+ appendContent(node.owner!!.summary)
+ appendHeader(3) { to.append("All Types") }
+
+ appendTable("Name", "Summary") {
+ appendTableBody {
+ for (type in node.members) {
+ appendTableRow {
+ appendTableCell {
+ appendLink(link(node, type) {
+ if (it.kind == NodeKind.ExternalClass) it.name else it.qualifiedName()
+ })
+ if (type.kind == NodeKind.ExternalClass) {
+ val packageName = type.owner?.name
+ if (packageName != null) {
+ appendText(" (extensions in package $packageName)")
+ }
+ }
+ }
+ appendTableCell {
+ appendContent(type.summary)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun appendNodes(nodes: Iterable<DocumentationNode>) {
+ val singleNode = nodes.singleOrNull()
+ when (singleNode?.kind) {
+ NodeKind.AllTypes -> AllTypesNodeBuilder(singleNode).build()
+ NodeKind.GroupNode -> GroupNodePageBuilder(singleNode).build()
+ null -> PageBuilder(nodes).build()
+ else -> SingleNodePageBuilder(singleNode).build()
+ }
+ }
+}
+
+abstract class StructuredFormatService(val generator: NodeLocationAwareGenerator,
+ val languageService: LanguageService,
+ override val extension: String,
+ override final val linkExtension: String = extension) : FormatService {
+
+}
diff --git a/core/src/main/kotlin/Formats/YamlOutlineService.kt b/core/src/main/kotlin/Formats/YamlOutlineService.kt
new file mode 100644
index 000000000..c36f98eb0
--- /dev/null
+++ b/core/src/main/kotlin/Formats/YamlOutlineService.kt
@@ -0,0 +1,26 @@
+package org.jetbrains.dokka
+
+import com.google.inject.Inject
+import java.io.File
+
+class YamlOutlineService @Inject constructor(
+ val generator: NodeLocationAwareGenerator,
+ val languageService: LanguageService
+) : OutlineFormatService {
+ override fun getOutlineFileName(location: Location): File = File("${location.path}.yml")
+
+ var outlineLevel = 0
+ override fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) {
+ val indent = " ".repeat(outlineLevel)
+ to.appendln("$indent- title: ${languageService.renderName(node)}")
+ to.appendln("$indent url: ${generator.location(node).path}")
+ }
+
+ override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) {
+ val indent = " ".repeat(outlineLevel)
+ to.appendln("$indent content:")
+ outlineLevel++
+ body()
+ outlineLevel--
+ }
+}
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 == "&amp;" || text == "&quot;" || text == "&lt;" || text == "&gt;") {
+ return ContentEntity(text)
+ }
+ if (text == "&") {
+ return ContentEntity("&amp;")
+ }
+ 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("*/", "*&#47;")
+ .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 &amp;, < with &lt; and > with &gt;
+ */
+fun String.htmlEscape(): String = replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+
+// 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("&nbsp;")))
+ 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