diff options
Diffstat (limited to 'core')
270 files changed, 16689 insertions, 0 deletions
diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 00000000..49d6d4d6 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,103 @@ +import javax.tools.ToolProvider + +group 'org.jetbrains.dokka' +version dokka_version + +buildscript { + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2' + } + repositories { + jcenter() + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' +apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'maven-publish' +apply plugin: 'com.jfrog.bintray' + +sourceCompatibility = 1.5 + +repositories { + mavenCentral() +} + +configurations { + provided +} + +dependencies { + compile "com.google.inject:guice:4.0" + compile "com.github.spullara.cli-parser:cli-parser:1.1.1" + compile "org.jsoup:jsoup:1.8.3" + + compile files("../lib/intellij-core-analysis.jar") + compile files("../lib/kotlin-for-upsource.jar") + compile files("../lib/markdown.jar") + compile files("../lib/picocontainer.jar") + + provided files(((URLClassLoader) ToolProvider.getSystemToolClassLoader()).getURLs().findAll { it.path.endsWith("jar") }) + provided "org.apache.ant:ant:1.9.6" + + runtime "org.fusesource.jansi:jansi:1.11" + + runtime files("../lib/trove4j.jar") + runtime files("../lib/jdom.jar") + runtime files("../lib/protobuf-2.5.0-lite.jar") + runtime files("../lib/asm-all.jar") + runtime files("../lib/jps-model.jar") + + testCompile group: 'junit', name: 'junit', version: '4.11' +} + +tasks.withType(AbstractCompile) { + classpath += configurations.provided +} + +tasks.withType(Test) { + classpath += configurations.provided +} + +jar { + manifest { + attributes "Implementation-Title": "Dokka Kotlin Documentation tool" + attributes "Implementation-Version": version + attributes "Main-Class": "org.jetbrains.dokka.MainKt" + } +} + +shadowJar { + baseName = 'dokka-fatjar' + classifier = '' +} + +publishing { + publications { + shadow(MavenPublication) { + from components.shadow + artifactId = 'dokka-fatjar' + } + } +} + +bintray { + user = System.getenv('BINTRAY_USER') + key = System.getenv('BINTRAY_KEY') + + pkg { + repo = 'dokka' + name = 'dokka' + userOrg = 'kotlin' + desc = 'Dokka, the Kotlin documentation tool' + vcsUrl = 'https://github.com/kotlin/dokka.git' + licenses = ['Apache-2.0'] + version { + name = dokka_version + } + } + + publications = ['shadow'] +} diff --git a/core/gradlew b/core/gradlew new file mode 100755 index 00000000..91a7e269 --- /dev/null +++ b/core/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/core/gradlew.bat b/core/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/core/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/core/settings.gradle b/core/settings.gradle new file mode 100644 index 00000000..17bdad4c --- /dev/null +++ b/core/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'core' + diff --git a/core/src/main/kotlin/Analysis/AnalysisEnvironment.kt b/core/src/main/kotlin/Analysis/AnalysisEnvironment.kt new file mode 100644 index 00000000..a5e35a0e --- /dev/null +++ b/core/src/main/kotlin/Analysis/AnalysisEnvironment.kt @@ -0,0 +1,210 @@ +package org.jetbrains.dokka + +import com.intellij.core.CoreApplicationEnvironment +import com.intellij.core.CoreModuleManager +import com.intellij.mock.MockComponentManager +import com.intellij.openapi.Disposable +import com.intellij.openapi.extensions.Extensions +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.OrderEnumerationHandler +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.util.Disposer +import com.intellij.psi.PsiElement +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.analyzer.AnalysisResult +import org.jetbrains.kotlin.analyzer.ModuleContent +import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.analyzer.ResolverForModule +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoot +import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots +import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.ContentRoot +import org.jetbrains.kotlin.config.KotlinSourceRoot +import org.jetbrains.kotlin.container.getService +import org.jetbrains.kotlin.context.ProjectContext +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.idea.caches.resolve.KotlinOutOfBlockCompletionModificationTracker +import org.jetbrains.kotlin.idea.caches.resolve.LibraryModificationTracker +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.CompilerEnvironment +import org.jetbrains.kotlin.resolve.jvm.JvmAnalyzerFacade +import org.jetbrains.kotlin.resolve.jvm.JvmPlatformParameters +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import java.io.File + +/** + * Kotlin as a service entry point + * + * Configures environment, analyses files and provides facilities to perform code processing without emitting bytecode + * + * $messageCollector: required by compiler infrastructure and will receive all compiler messages + * $body: optional and can be used to configure environment without creating local variable + */ +public class AnalysisEnvironment(val messageCollector: MessageCollector) : Disposable { + val configuration = CompilerConfiguration(); + + init { + configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector) + } + + fun createCoreEnvironment(): KotlinCoreEnvironment { + val environment = KotlinCoreEnvironment.createForProduction(this, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) + val projectComponentManager = environment.project as MockComponentManager + + val projectFileIndex = CoreProjectFileIndex(environment.project, + environment.configuration.getList(CommonConfigurationKeys.CONTENT_ROOTS)) + + val moduleManager = object : CoreModuleManager(environment.project, this) { + override fun getModules(): Array<out Module> = arrayOf(projectFileIndex.module) + } + + CoreApplicationEnvironment.registerComponentInstance(projectComponentManager.picoContainer, + ModuleManager::class.java, moduleManager) + + Extensions.registerAreaClass("IDEA_MODULE", null) + CoreApplicationEnvironment.registerExtensionPoint(Extensions.getRootArea(), + OrderEnumerationHandler.EP_NAME, OrderEnumerationHandler.Factory::class.java) + + projectComponentManager.registerService(ProjectFileIndex::class.java, + projectFileIndex) + projectComponentManager.registerService(ProjectRootManager::class.java, + CoreProjectRootManager(projectFileIndex)) + projectComponentManager.registerService(LibraryModificationTracker::class.java, + LibraryModificationTracker(environment.project)) + projectComponentManager.registerService(KotlinCacheService::class.java, + KotlinCacheService(environment.project)) + projectComponentManager.registerService(KotlinOutOfBlockCompletionModificationTracker::class.java, + KotlinOutOfBlockCompletionModificationTracker()) + return environment + } + + fun createResolutionFacade(environment: KotlinCoreEnvironment): DokkaResolutionFacade { + val projectContext = ProjectContext(environment.project) + val sourceFiles = environment.getSourceFiles() + + val module = object : ModuleInfo { + override val name: Name = Name.special("<module>") + override fun dependencies(): List<ModuleInfo> = listOf(this) + } + val resolverForProject = JvmAnalyzerFacade.setupResolverForProject( + "Dokka", + projectContext, + listOf(module), + { ModuleContent(sourceFiles, GlobalSearchScope.allScope(environment.project)) }, + JvmPlatformParameters { module }, + CompilerEnvironment + ) + + val resolverForModule = resolverForProject.resolverForModule(module) + return DokkaResolutionFacade(environment.project, resolverForProject.descriptorForModule(module), resolverForModule) + } + + /** + * Classpath for this environment. + */ + public val classpath: List<File> + get() = configuration.jvmClasspathRoots + + /** + * Adds list of paths to classpath. + * $paths: collection of files to add + */ + public fun addClasspath(paths: List<File>) { + configuration.addJvmClasspathRoots(paths) + } + + /** + * Adds path to classpath. + * $path: path to add + */ + public fun addClasspath(path: File) { + configuration.addJvmClasspathRoot(path) + } + + /** + * List of source roots for this environment. + */ + public val sources: List<String> + get() = configuration.get(CommonConfigurationKeys.CONTENT_ROOTS) + ?.filterIsInstance<KotlinSourceRoot>() + ?.map { it.path } ?: emptyList() + + /** + * Adds list of paths to source roots. + * $list: collection of files to add + */ + public fun addSources(list: List<String>) { + list.forEach { + configuration.add(CommonConfigurationKeys.CONTENT_ROOTS, contentRootFromPath(it)) + } + } + + public fun addRoots(list: List<ContentRoot>) { + configuration.addAll(CommonConfigurationKeys.CONTENT_ROOTS, list) + } + + /** + * Disposes the environment and frees all associated resources. + */ + public override fun dispose() { + Disposer.dispose(this) + } +} + +public fun contentRootFromPath(path: String): ContentRoot { + val file = File(path) + return if (file.extension == "java") JavaSourceRoot(file, null) else KotlinSourceRoot(path) +} + + +class DokkaResolutionFacade(override val project: Project, + override val moduleDescriptor: ModuleDescriptor, + val resolverForModule: ResolverForModule) : ResolutionFacade { + + val resolveSession: ResolveSession get() = getFrontendService(ResolveSession::class.java) + + override fun analyze(element: KtElement, bodyResolveMode: BodyResolveMode): BindingContext { + throw UnsupportedOperationException() + } + + override fun analyzeFullyAndGetResult(elements: Collection<KtElement>): AnalysisResult { + throw UnsupportedOperationException() + } + + override fun <T : Any> getFrontendService(element: PsiElement, serviceClass: Class<T>): T { + throw UnsupportedOperationException() + } + + override fun <T : Any> getFrontendService(serviceClass: Class<T>): T { + return resolverForModule.componentProvider.getService(serviceClass) + } + + override fun <T : Any> getFrontendService(moduleDescriptor: ModuleDescriptor, serviceClass: Class<T>): T { + throw UnsupportedOperationException() + } + + override fun <T : Any> getIdeService(serviceClass: Class<T>): T { + throw UnsupportedOperationException() + } + + override fun resolveToDescriptor(declaration: KtDeclaration): DeclarationDescriptor { + return resolveSession.resolveToDescriptor(declaration) + } +} diff --git a/core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt b/core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt new file mode 100644 index 00000000..a1362fde --- /dev/null +++ b/core/src/main/kotlin/Analysis/CoreProjectFileIndex.kt @@ -0,0 +1,550 @@ +package org.jetbrains.dokka + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.BaseComponent +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.SdkAdditionalData +import com.intellij.openapi.projectRoots.SdkModificator +import com.intellij.openapi.projectRoots.SdkTypeId +import com.intellij.openapi.roots.* +import com.intellij.openapi.roots.impl.ProjectOrderEnumerator +import com.intellij.openapi.util.Condition +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.messages.MessageBus +import org.jetbrains.jps.model.module.JpsModuleSourceRootType +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.cli.jvm.config.JvmContentRoot +import org.jetbrains.kotlin.config.ContentRoot +import org.jetbrains.kotlin.config.KotlinSourceRoot +import org.picocontainer.PicoContainer +import java.io.File + +/** + * Workaround for the lack of ability to create a ProjectFileIndex implementation using only + * classes from projectModel-{api,impl}. + */ +class CoreProjectFileIndex(val project: Project, contentRoots: List<ContentRoot>) : ProjectFileIndex, ModuleFileIndex { + val sourceRoots = contentRoots.filter { it !is JvmClasspathRoot } + val classpathRoots = contentRoots.filterIsInstance<JvmClasspathRoot>() + + val module: Module = object : UserDataHolderBase(), Module { + override fun isDisposed(): Boolean { + throw UnsupportedOperationException() + } + + override fun getOptionValue(p0: String): String? { + throw UnsupportedOperationException() + } + + override fun clearOption(p0: String) { + throw UnsupportedOperationException() + } + + override fun getName(): String = "<Dokka module>" + + override fun getModuleWithLibrariesScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleWithDependentsScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleContentScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun isLoaded(): Boolean { + throw UnsupportedOperationException() + } + + override fun setOption(p0: String, p1: String) { + throw UnsupportedOperationException() + } + + override fun getModuleWithDependenciesScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleWithDependenciesAndLibrariesScope(p0: Boolean): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getProject(): Project = project + + override fun getModuleContentWithDependenciesScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleFilePath(): String { + throw UnsupportedOperationException() + } + + override fun getModuleTestsWithDependentsScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleScope(): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleScope(p0: Boolean): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleRuntimeScope(p0: Boolean): GlobalSearchScope { + throw UnsupportedOperationException() + } + + override fun getModuleFile(): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getExtensions(p0: ExtensionPointName<T>): Array<out T> { + throw UnsupportedOperationException() + } + + override fun getComponent(p0: String): BaseComponent? { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getComponent(p0: Class<T>, p1: T): T { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getComponent(interfaceClass: Class<T>): T? { + if (interfaceClass == ModuleRootManager::class.java) { + return moduleRootManager as T + } + throw UnsupportedOperationException() + } + + override fun getDisposed(): Condition<*> { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getComponents(p0: Class<T>): Array<out T> { + throw UnsupportedOperationException() + } + + override fun getPicoContainer(): PicoContainer { + throw UnsupportedOperationException() + } + + override fun hasComponent(p0: Class<*>): Boolean { + throw UnsupportedOperationException() + } + + override fun getMessageBus(): MessageBus { + throw UnsupportedOperationException() + } + + override fun dispose() { + throw UnsupportedOperationException() + } + } + + private val sdk: Sdk = object : Sdk, RootProvider { + override fun getFiles(rootType: OrderRootType): Array<out VirtualFile> = classpathRoots + .map { StandardFileSystems.local().findFileByPath(it.file.path) } + .filterNotNull() + .toTypedArray() + + override fun addRootSetChangedListener(p0: RootProvider.RootSetChangedListener) { + throw UnsupportedOperationException() + } + + override fun addRootSetChangedListener(p0: RootProvider.RootSetChangedListener, p1: Disposable) { + throw UnsupportedOperationException() + } + + override fun getUrls(p0: OrderRootType): Array<out String> { + throw UnsupportedOperationException() + } + + override fun removeRootSetChangedListener(p0: RootProvider.RootSetChangedListener) { + throw UnsupportedOperationException() + } + + override fun getSdkModificator(): SdkModificator { + throw UnsupportedOperationException() + } + + override fun getName(): String = "<dokka SDK>" + + override fun getRootProvider(): RootProvider = this + + override fun getHomePath(): String? { + throw UnsupportedOperationException() + } + + override fun getVersionString(): String? { + throw UnsupportedOperationException() + } + + override fun getSdkAdditionalData(): SdkAdditionalData? { + throw UnsupportedOperationException() + } + + override fun clone(): Any { + throw UnsupportedOperationException() + } + + override fun getSdkType(): SdkTypeId { + throw UnsupportedOperationException() + } + + override fun getHomeDirectory(): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun <T : Any?> getUserData(p0: Key<T>): T? { + throw UnsupportedOperationException() + } + + override fun <T : Any?> putUserData(p0: Key<T>, p1: T?) { + throw UnsupportedOperationException() + } + } + + private val moduleSourceOrderEntry = object : ModuleSourceOrderEntry { + override fun getFiles(p0: OrderRootType?): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getPresentableName(): String { + throw UnsupportedOperationException() + } + + override fun getUrls(p0: OrderRootType?): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getOwnerModule(): Module = module + + override fun <R : Any?> accept(p0: RootPolicy<R>?, p1: R?): R { + throw UnsupportedOperationException() + } + + override fun isValid(): Boolean { + throw UnsupportedOperationException() + } + + override fun compareTo(other: OrderEntry?): Int { + throw UnsupportedOperationException() + } + + override fun getRootModel(): ModuleRootModel = moduleRootManager + + override fun isSynthetic(): Boolean { + throw UnsupportedOperationException() + } + } + + private val sdkOrderEntry = object : JdkOrderEntry { + override fun getJdkName(): String? { + throw UnsupportedOperationException() + } + + override fun getJdk(): Sdk = sdk + + override fun getFiles(p0: OrderRootType?): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getPresentableName(): String { + throw UnsupportedOperationException() + } + + override fun getUrls(p0: OrderRootType?): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getOwnerModule(): Module { + throw UnsupportedOperationException() + } + + override fun <R : Any?> accept(p0: RootPolicy<R>?, p1: R?): R { + throw UnsupportedOperationException() + } + + override fun isValid(): Boolean { + throw UnsupportedOperationException() + } + + override fun getRootFiles(p0: OrderRootType?): Array<out VirtualFile>? { + throw UnsupportedOperationException() + } + + override fun getRootUrls(p0: OrderRootType?): Array<out String>? { + throw UnsupportedOperationException() + } + + override fun compareTo(other: OrderEntry?): Int { + throw UnsupportedOperationException() + } + + override fun isSynthetic(): Boolean { + throw UnsupportedOperationException() + } + + } + + inner class MyModuleRootManager : ModuleRootManager() { + override fun getExcludeRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getContentEntries(): Array<out ContentEntry> { + throw UnsupportedOperationException() + } + + override fun getExcludeRootUrls(): Array<out String> { + throw UnsupportedOperationException() + } + + override fun <R : Any?> processOrder(p0: RootPolicy<R>?, p1: R): R { + throw UnsupportedOperationException() + } + + override fun getSourceRoots(p0: Boolean): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getSourceRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getSourceRoots(p0: JpsModuleSourceRootType<*>): MutableList<VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getSourceRoots(p0: MutableSet<out JpsModuleSourceRootType<*>>): MutableList<VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getContentRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun orderEntries(): OrderEnumerator = + ProjectOrderEnumerator(project, null).using(object : RootModelProvider { + override fun getModules(): Array<out Module> = arrayOf(module) + + override fun getRootModel(p0: Module): ModuleRootModel = this@MyModuleRootManager + }) + + override fun <T : Any?> getModuleExtension(p0: Class<T>?): T { + throw UnsupportedOperationException() + } + + override fun getDependencyModuleNames(): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getModule(): Module = module + + override fun isSdkInherited(): Boolean { + throw UnsupportedOperationException() + } + + override fun getOrderEntries(): Array<out OrderEntry> = arrayOf(moduleSourceOrderEntry, sdkOrderEntry) + + override fun getSourceRootUrls(): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getSourceRootUrls(p0: Boolean): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getSdk(): Sdk? { + throw UnsupportedOperationException() + } + + override fun getContentRootUrls(): Array<out String> { + throw UnsupportedOperationException() + } + + override fun getModuleDependencies(): Array<out Module> { + throw UnsupportedOperationException() + } + + override fun getModuleDependencies(p0: Boolean): Array<out Module> { + throw UnsupportedOperationException() + } + + override fun getModifiableModel(): ModifiableRootModel { + throw UnsupportedOperationException() + } + + override fun isDependsOn(p0: Module?): Boolean { + throw UnsupportedOperationException() + } + + override fun getFileIndex(): ModuleFileIndex { + return this@CoreProjectFileIndex + } + + override fun getDependencies(): Array<out Module> { + throw UnsupportedOperationException() + } + + override fun getDependencies(p0: Boolean): Array<out Module> { + throw UnsupportedOperationException() + } + } + + val moduleRootManager = MyModuleRootManager() + + override fun getContentRootForFile(p0: VirtualFile): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun getContentRootForFile(p0: VirtualFile, p1: Boolean): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun getPackageNameByDirectory(p0: VirtualFile): String? { + throw UnsupportedOperationException() + } + + override fun isInLibrarySource(file: VirtualFile): Boolean = false + + override fun getClassRootForFile(file: VirtualFile): VirtualFile? = + classpathRoots.firstOrNull { it.contains(file) }?.let { StandardFileSystems.local().findFileByPath(it.file.path) } + + override fun getOrderEntriesForFile(file: VirtualFile): List<OrderEntry> = + if (classpathRoots.contains(file)) listOf(sdkOrderEntry) else emptyList() + + override fun isInLibraryClasses(file: VirtualFile): Boolean = classpathRoots.contains(file) + + override fun isExcluded(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun getSourceRootForFile(p0: VirtualFile): VirtualFile? { + throw UnsupportedOperationException() + } + + override fun isUnderIgnored(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun isLibraryClassFile(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun getModuleForFile(file: VirtualFile): Module? = + if (sourceRoots.contains(file)) module else null + + private fun List<ContentRoot>.contains(file: VirtualFile): Boolean = any { it.contains(file) } + + override fun getModuleForFile(p0: VirtualFile, p1: Boolean): Module? { + throw UnsupportedOperationException() + } + + override fun isInSource(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun isIgnored(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun isContentSourceFile(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun isInSourceContent(file: VirtualFile): Boolean = sourceRoots.contains(file) + + override fun iterateContent(p0: ContentIterator): Boolean { + throw UnsupportedOperationException() + } + + override fun isInContent(p0: VirtualFile): Boolean { + throw UnsupportedOperationException() + } + + override fun iterateContentUnderDirectory(p0: VirtualFile, p1: ContentIterator): Boolean { + throw UnsupportedOperationException() + } + + override fun isInTestSourceContent(file: VirtualFile): Boolean = false + + override fun isUnderSourceRootOfType(p0: VirtualFile, p1: MutableSet<out JpsModuleSourceRootType<*>>): Boolean { + throw UnsupportedOperationException() + } + + override fun getOrderEntryForFile(p0: VirtualFile): OrderEntry? { + throw UnsupportedOperationException() + } +} + +class CoreProjectRootManager(val projectFileIndex: CoreProjectFileIndex) : ProjectRootManager() { + override fun orderEntries(): OrderEnumerator { + throw UnsupportedOperationException() + } + + override fun orderEntries(p0: MutableCollection<out Module>): OrderEnumerator { + throw UnsupportedOperationException() + } + + override fun getContentRootsFromAllModules(): Array<out VirtualFile>? { + throw UnsupportedOperationException() + } + + override fun setProjectSdk(p0: Sdk?) { + throw UnsupportedOperationException() + } + + override fun setProjectSdkName(p0: String?) { + throw UnsupportedOperationException() + } + + override fun getModuleSourceRoots(p0: MutableSet<out JpsModuleSourceRootType<*>>): MutableList<VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getContentSourceRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getFileIndex(): ProjectFileIndex = projectFileIndex + + override fun getProjectSdkName(): String? { + throw UnsupportedOperationException() + } + + override fun getProjectSdk(): Sdk? { + throw UnsupportedOperationException() + } + + override fun getContentRoots(): Array<out VirtualFile> { + throw UnsupportedOperationException() + } + + override fun getContentRootUrls(): MutableList<String> { + throw UnsupportedOperationException() + } + +} + +fun ContentRoot.contains(file: VirtualFile) = when (this) { + is JvmContentRoot -> { + val path = if (file.fileSystem.protocol == StandardFileSystems.JAR_PROTOCOL) + StandardFileSystems.getVirtualFileForJar(file)?.path ?: file.path + else + file.path + File(path).startsWith(this.file.absoluteFile) + } + is KotlinSourceRoot -> File(file.path).startsWith(File(this.path).absoluteFile) + else -> false +} diff --git a/core/src/main/kotlin/Formats/FormatDescriptor.kt b/core/src/main/kotlin/Formats/FormatDescriptor.kt new file mode 100644 index 00000000..0c7ca794 --- /dev/null +++ b/core/src/main/kotlin/Formats/FormatDescriptor.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.Formats + +import org.jetbrains.dokka.* +import kotlin.reflect.KClass + +public interface FormatDescriptor { + val formatServiceClass: KClass<out FormatService>? + val outlineServiceClass: KClass<out OutlineFormatService>? + val generatorServiceClass: KClass<out Generator> + val packageDocumentationBuilderClass: KClass<out PackageDocumentationBuilder> + val javaDocumentationBuilderClass: KClass<out JavaDocumentationBuilder> +} diff --git a/core/src/main/kotlin/Formats/FormatService.kt b/core/src/main/kotlin/Formats/FormatService.kt new file mode 100644 index 00000000..7e66a6b7 --- /dev/null +++ b/core/src/main/kotlin/Formats/FormatService.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka + +/** + * Abstract representation of a formatting service used to output documentation in desired format + * + * Bundled Formatters: + * * [HtmlFormatService] – outputs documentation to HTML format + * * [MarkdownFormatService] – outputs documentation in Markdown format + * * [TextFormatService] – outputs documentation in Text format + */ +public interface FormatService { + /** Returns extension for output files */ + val extension: String + + /** Appends formatted content to [StringBuilder](to) using specified [location] */ + fun appendNodes(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) +} + +/** Format content to [String] using specified [location] */ +fun FormatService.format(location: Location, nodes: Iterable<DocumentationNode>): String = StringBuilder().apply { appendNodes(location, this, nodes) }.toString() diff --git a/core/src/main/kotlin/Formats/HtmlFormatService.kt b/core/src/main/kotlin/Formats/HtmlFormatService.kt new file mode 100644 index 00000000..4d45e6cb --- /dev/null +++ b/core/src/main/kotlin/Formats/HtmlFormatService.kt @@ -0,0 +1,169 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +public open class HtmlFormatService @Inject constructor(@Named("folders") locationService: LocationService, + signatureGenerator: LanguageService, + val templateService: HtmlTemplateService) +: StructuredFormatService(locationService, signatureGenerator, "html"), OutlineFormatService { + override public fun formatText(text: String): String { + return text.htmlEscape() + } + + override fun formatSymbol(text: String): String { + return "<span class=\"symbol\">${formatText(text)}</span>" + } + + override fun formatKeyword(text: String): String { + return "<span class=\"keyword\">${formatText(text)}</span>" + } + + override fun formatIdentifier(text: String, kind: IdentifierKind): String { + return "<span class=\"identifier\">${formatText(text)}</span>" + } + + override fun appendBlockCode(to: StringBuilder, line: String, language: String) { + to.append("<pre><code>") + to.append(line) + to.append("</code></pre>") + } + + override fun appendHeader(to: StringBuilder, text: String, level: Int) { + to.appendln("<h$level>${text}</h$level>") + } + + override fun appendParagraph(to: StringBuilder, text: String) { + to.appendln("<p>${text}</p>") + } + + override fun appendLine(to: StringBuilder, text: String) { + to.appendln("${text}<br/>") + } + + override fun appendLine(to: StringBuilder) { + to.appendln("<br/>") + } + + override fun appendAnchor(to: StringBuilder, anchor: String) { + to.appendln("<a name=\"${anchor.htmlEscape()}\"></a>") + } + + override fun appendTable(to: StringBuilder, body: () -> Unit) { + to.appendln("<table>") + body() + to.appendln("</table>") + } + + override fun appendTableHeader(to: StringBuilder, body: () -> Unit) { + to.appendln("<thead>") + body() + to.appendln("</thead>") + } + + override fun appendTableBody(to: StringBuilder, body: () -> Unit) { + to.appendln("<tbody>") + body() + to.appendln("</tbody>") + } + + override fun appendTableRow(to: StringBuilder, body: () -> Unit) { + to.appendln("<tr>") + body() + to.appendln("</tr>") + } + + override fun appendTableCell(to: StringBuilder, body: () -> Unit) { + to.appendln("<td>") + body() + to.appendln("</td>") + } + + override fun formatLink(text: String, href: String): String { + return "<a href=\"${href}\">${text}</a>" + } + + override fun formatStrong(text: String): String { + return "<strong>${text}</strong>" + } + + override fun formatEmphasis(text: String): String { + return "<emph>${text}</emph>" + } + + override fun formatStrikethrough(text: String): String { + return "<s>${text}</s>" + } + + override fun formatCode(code: String): String { + return "<code>${code}</code>" + } + + override fun formatUnorderedList(text: String): String = "<ul>${text}</ul>" + override fun formatOrderedList(text: String): String = "<ol>${text}</ol>" + + override fun formatListItem(text: String, kind: ListKind): String { + return "<li>${text}</li>" + } + + override fun formatBreadcrumbs(items: Iterable<FormatLink>): String { + return items.map { formatLink(it) }.joinToString(" / ") + } + + + override fun appendNodes(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + templateService.appendHeader(to, getPageTitle(nodes), calcPathToRoot(location)) + super.appendNodes(location, to, nodes) + templateService.appendFooter(to) + } + + override fun appendOutline(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + templateService.appendHeader(to, "Module Contents", calcPathToRoot(location)) + super.appendOutline(location, to, nodes) + templateService.appendFooter(to) + } + + private fun calcPathToRoot(location: Location): Path { + val path = Paths.get(location.path) + return path.parent?.relativize(Paths.get(locationService.root.path + '/')) ?: path + } + + override fun getOutlineFileName(location: Location): File { + return File("${location.path}-outline.html") + } + + override fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) { + val link = ContentNodeDirectLink(node) + link.append(languageService.render(node, LanguageService.RenderMode.FULL)) + val signature = formatText(location, link) + to.appendln("<a href=\"${location.path}\">${signature}</a><br/>") + } + + override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) { + to.appendln("<ul>") + body() + to.appendln("</ul>") + } + + override fun formatNonBreakingSpace(): String = " " +} + +fun getPageTitle(nodes: Iterable<DocumentationNode>): String? { + val breakdownByLocation = nodes.groupBy { node -> formatPageTitle(node) } + return breakdownByLocation.keys.singleOrNull() +} + +fun formatPageTitle(node: DocumentationNode): String { + val path = node.path + if (path.size == 1) { + return path.first().name + } + val qualifiedName = node.qualifiedName() + if (qualifiedName.length == 0 && path.size == 2) { + return path.first().name + " / root package" + } + return path.first().name + " / " + qualifiedName +} diff --git a/core/src/main/kotlin/Formats/HtmlTemplateService.kt b/core/src/main/kotlin/Formats/HtmlTemplateService.kt new file mode 100644 index 00000000..ae42a31b --- /dev/null +++ b/core/src/main/kotlin/Formats/HtmlTemplateService.kt @@ -0,0 +1,34 @@ +package org.jetbrains.dokka + +import java.nio.file.Path + +public interface HtmlTemplateService { + fun appendHeader(to: StringBuilder, title: String?, basePath: Path) + fun appendFooter(to: StringBuilder) + + companion object { + public fun default(css: String? = null): HtmlTemplateService { + return object : HtmlTemplateService { + override fun appendFooter(to: StringBuilder) { + to.appendln("</BODY>") + to.appendln("</HTML>") + } + override fun appendHeader(to: StringBuilder, title: String?, basePath: Path) { + to.appendln("<HTML>") + to.appendln("<HEAD>") + if (title != null) { + to.appendln("<title>$title</title>") + } + if (css != null) { + val cssPath = basePath.resolve(css) + to.appendln("<link rel=\"stylesheet\" href=\"$cssPath\">") + } + to.appendln("</HEAD>") + to.appendln("<BODY>") + } + } + } + } +} + + diff --git a/core/src/main/kotlin/Formats/JekyllFormatService.kt b/core/src/main/kotlin/Formats/JekyllFormatService.kt new file mode 100644 index 00000000..f81257d6 --- /dev/null +++ b/core/src/main/kotlin/Formats/JekyllFormatService.kt @@ -0,0 +1,22 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject + +open class JekyllFormatService + @Inject constructor(locationService: LocationService, + signatureGenerator: LanguageService, + linkExtension: String = "md") +: MarkdownFormatService(locationService, signatureGenerator, linkExtension) { + + override fun appendNodes(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + to.appendln("---") + appendFrontMatter(nodes, to) + to.appendln("---") + to.appendln("") + super.appendNodes(location, to, nodes) + } + + protected open fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) { + to.appendln("title: ${getPageTitle(nodes)}") + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt new file mode 100644 index 00000000..4eda7910 --- /dev/null +++ b/core/src/main/kotlin/Formats/KotlinWebsiteFormatService.kt @@ -0,0 +1,121 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject + +public class KotlinWebsiteFormatService @Inject constructor(locationService: LocationService, + signatureGenerator: LanguageService) +: JekyllFormatService(locationService, signatureGenerator, "html") { + private var needHardLineBreaks = false + + override fun appendFrontMatter(nodes: Iterable<DocumentationNode>, to: StringBuilder) { + super.appendFrontMatter(nodes, to) + to.appendln("layout: api") + } + + override public fun formatBreadcrumbs(items: Iterable<FormatLink>): String { + items.drop(1) + + if (items.count() > 1) { + return "<div class='api-docs-breadcrumbs'>" + + items.map { formatLink(it) }.joinToString(" / ") + + "</div>" + } + + return "" + } + + override public fun formatCode(code: String): String = if (code.length > 0) "<code>$code</code>" else "" + + override fun formatStrikethrough(text: String): String = "<s>$text</s>" + + override fun appendAsSignature(to: StringBuilder, node: ContentNode, block: () -> Unit) { + val contentLength = node.textLength + if (contentLength == 0) return + to.append("<div class=\"signature\">") + needHardLineBreaks = contentLength >= 62 + try { + block() + } finally { + needHardLineBreaks = false + } + to.append("</div>") + } + + override fun appendAsOverloadGroup(to: StringBuilder, block: () -> Unit) { + to.append("<div class=\"overload-group\">\n") + block() + to.append("</div>\n") + } + + override fun formatLink(text: String, href: String): String { + return "<a href=\"${href}\">${text}</a>" + } + + override fun appendTable(to: StringBuilder, body: () -> Unit) { + to.appendln("<table class=\"api-docs-table\">") + body() + to.appendln("</table>") + } + + override fun appendTableHeader(to: StringBuilder, body: () -> Unit) { + to.appendln("<thead>") + body() + to.appendln("</thead>") + } + + override fun appendTableBody(to: StringBuilder, body: () -> Unit) { + to.appendln("<tbody>") + body() + to.appendln("</tbody>") + } + + override fun appendTableRow(to: StringBuilder, body: () -> Unit) { + to.appendln("<tr>") + body() + to.appendln("</tr>") + } + + override fun appendTableCell(to: StringBuilder, body: () -> Unit) { + to.appendln("<td markdown=\"1\">") + body() + to.appendln("\n</td>") + } + + override public fun appendBlockCode(to: StringBuilder, line: String, language: String) { + if (language.isNotEmpty()) { + super.appendBlockCode(to, line, language) + } else { + to.append("<pre markdown=\"1\">") + to.append(line.trimStart()) + to.append("</pre>") + } + } + + override fun formatSymbol(text: String): String { + return "<span class=\"symbol\">${formatText(text)}</span>" + } + + override fun formatKeyword(text: String): String { + return "<span class=\"keyword\">${formatText(text)}</span>" + } + + override fun formatIdentifier(text: String, kind: IdentifierKind): String { + return "<span class=\"${identifierClassName(kind)}\">${formatText(text)}</span>" + } + + override fun formatSoftLineBreak(): String = if (needHardLineBreaks) + "<br/>" + else + "" + + override fun formatIndentedSoftLineBreak(): String = if (needHardLineBreaks) + "<br/> " + else + "" + + private fun identifierClassName(kind: IdentifierKind) = when(kind) { + IdentifierKind.ParameterName -> "parameterName" + IdentifierKind.SummarizedTypeName -> "summarizedTypeName" + else -> "identifier" + } +} diff --git a/core/src/main/kotlin/Formats/MarkdownFormatService.kt b/core/src/main/kotlin/Formats/MarkdownFormatService.kt new file mode 100644 index 00000000..f694ae3e --- /dev/null +++ b/core/src/main/kotlin/Formats/MarkdownFormatService.kt @@ -0,0 +1,117 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject + + +public open class MarkdownFormatService + @Inject constructor(locationService: LocationService, + signatureGenerator: LanguageService, + linkExtension: String = "md") +: StructuredFormatService(locationService, signatureGenerator, "md", linkExtension) { + override public fun formatBreadcrumbs(items: Iterable<FormatLink>): String { + return items.map { formatLink(it) }.joinToString(" / ") + } + + override public fun formatText(text: String): String { + return text.htmlEscape() + } + + override fun formatSymbol(text: String): String { + return text.htmlEscape() + } + + override fun formatKeyword(text: String): String { + return text.htmlEscape() + } + override fun formatIdentifier(text: String, kind: IdentifierKind): String { + return text.htmlEscape() + } + + override public fun formatCode(code: String): String { + return "`$code`" + } + + override public fun formatUnorderedList(text: String): String = text + "\n" + override public fun formatOrderedList(text: String): String = text + "\n" + + override fun formatListItem(text: String, kind: ListKind): String { + val itemText = if (text.endsWith("\n")) text else text + "\n" + return if (kind == ListKind.Unordered) "* $itemText" else "1. $itemText" + } + + override public fun formatStrong(text: String): String { + return "**$text**" + } + + override fun formatEmphasis(text: String): String { + return "*$text*" + } + + override fun formatStrikethrough(text: String): String { + return "~~$text~~" + } + + override fun formatLink(text: String, href: String): String { + return "[$text]($href)" + } + + override public fun appendLine(to: StringBuilder) { + to.appendln() + } + + override public fun appendLine(to: StringBuilder, text: String) { + to.appendln(text) + } + + override fun appendAnchor(to: StringBuilder, anchor: String) { + // no anchors in Markdown + } + + override public fun appendParagraph(to: StringBuilder, text: String) { + to.appendln() + to.appendln(text) + to.appendln() + } + + override public fun appendHeader(to: StringBuilder, text: String, level: Int) { + appendLine(to) + appendLine(to, "${"#".repeat(level)} $text") + appendLine(to) + } + + override public fun appendBlockCode(to: StringBuilder, line: String, language: String) { + appendLine(to) + to.appendln("``` ${language}") + to.appendln(line) + to.appendln("```") + appendLine(to) + } + + override fun appendTable(to: StringBuilder, body: () -> Unit) { + to.appendln() + body() + to.appendln() + } + + override fun appendTableHeader(to: StringBuilder, body: () -> Unit) { + body() + } + + override fun appendTableBody(to: StringBuilder, body: () -> Unit) { + body() + } + + override fun appendTableRow(to: StringBuilder, body: () -> Unit) { + to.append("|") + body() + to.appendln() + } + + override fun appendTableCell(to: StringBuilder, body: () -> Unit) { + to.append(" ") + body() + to.append(" |") + } + + override fun formatNonBreakingSpace(): String = " " +} diff --git a/core/src/main/kotlin/Formats/OutlineService.kt b/core/src/main/kotlin/Formats/OutlineService.kt new file mode 100644 index 00000000..6626cf51 --- /dev/null +++ b/core/src/main/kotlin/Formats/OutlineService.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka + +import java.io.File + +/** + * Service for building the outline of the package contents. + */ +public interface OutlineFormatService { + fun getOutlineFileName(location: Location): File + + public fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) + public fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) + + /** Appends formatted outline to [StringBuilder](to) using specified [location] */ + public fun appendOutline(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + for (node in nodes) { + appendOutlineHeader(location, node, to) + if (node.members.any()) { + val sortedMembers = node.members.sortedBy { it.name } + appendOutlineLevel(to) { + appendOutline(location, to, sortedMembers) + } + } + } + } + + fun formatOutline(location: Location, nodes: Iterable<DocumentationNode>): String = + StringBuilder().apply { appendOutline(location, this, nodes) }.toString() +} diff --git a/core/src/main/kotlin/Formats/StandardFormats.kt b/core/src/main/kotlin/Formats/StandardFormats.kt new file mode 100644 index 00000000..94e1b115 --- /dev/null +++ b/core/src/main/kotlin/Formats/StandardFormats.kt @@ -0,0 +1,38 @@ +package org.jetbrains.dokka.Formats + +import org.jetbrains.dokka.* + +abstract class KotlinFormatDescriptorBase : FormatDescriptor { + override val packageDocumentationBuilderClass = KotlinPackageDocumentationBuilder::class + override val javaDocumentationBuilderClass = KotlinJavaDocumentationBuilder::class + + override val generatorServiceClass = FileGenerator::class +} + +class HtmlFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = HtmlFormatService::class + override val outlineServiceClass = HtmlFormatService::class +} + +class HtmlAsJavaFormatDescriptor : FormatDescriptor { + override val formatServiceClass = HtmlFormatService::class + override val outlineServiceClass = HtmlFormatService::class + override val generatorServiceClass = FileGenerator::class + override val packageDocumentationBuilderClass = KotlinAsJavaDocumentationBuilder::class + override val javaDocumentationBuilderClass = JavaPsiDocumentationBuilder::class +} + +class KotlinWebsiteFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = KotlinWebsiteFormatService::class + override val outlineServiceClass = YamlOutlineService::class +} + +class JekyllFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = JekyllFormatService::class + override val outlineServiceClass = null +} + +class MarkdownFormatDescriptor : KotlinFormatDescriptorBase() { + override val formatServiceClass = MarkdownFormatService::class + override val outlineServiceClass = null +} diff --git a/core/src/main/kotlin/Formats/StructuredFormatService.kt b/core/src/main/kotlin/Formats/StructuredFormatService.kt new file mode 100644 index 00000000..32a2b68a --- /dev/null +++ b/core/src/main/kotlin/Formats/StructuredFormatService.kt @@ -0,0 +1,367 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.LanguageService.RenderMode +import java.util.* + +data class FormatLink(val text: String, val href: String) + +enum class ListKind { + Ordered, + Unordered +} + +abstract class StructuredFormatService(locationService: LocationService, + val languageService: LanguageService, + override val extension: String, + val linkExtension: String = extension) : FormatService { + val locationService: LocationService = locationService.withExtension(linkExtension) + + abstract fun appendBlockCode(to: StringBuilder, line: String, language: String) + abstract fun appendHeader(to: StringBuilder, text: String, level: Int = 1) + abstract fun appendParagraph(to: StringBuilder, text: String) + abstract fun appendLine(to: StringBuilder, text: String) + abstract fun appendLine(to: StringBuilder) + abstract fun appendAnchor(to: StringBuilder, anchor: String) + + abstract fun appendTable(to: StringBuilder, body: () -> Unit) + abstract fun appendTableHeader(to: StringBuilder, body: () -> Unit) + abstract fun appendTableBody(to: StringBuilder, body: () -> Unit) + abstract fun appendTableRow(to: StringBuilder, body: () -> Unit) + abstract fun appendTableCell(to: StringBuilder, body: () -> Unit) + + abstract fun formatText(text: String): String + abstract fun formatSymbol(text: String): String + abstract fun formatKeyword(text: String): String + abstract fun formatIdentifier(text: String, kind: IdentifierKind): String + fun formatEntity(text: String): String = text + abstract fun formatLink(text: String, href: String): String + open fun formatLink(link: FormatLink): String = formatLink(formatText(link.text), link.href) + abstract fun formatStrong(text: String): String + abstract fun formatStrikethrough(text: String): String + abstract fun formatEmphasis(text: String): String + abstract fun formatCode(code: String): String + abstract fun formatUnorderedList(text: String): String + abstract fun formatOrderedList(text: String): String + abstract fun formatListItem(text: String, kind: ListKind): String + abstract fun formatBreadcrumbs(items: Iterable<FormatLink>): String + abstract fun formatNonBreakingSpace(): String + open fun formatSoftLineBreak(): String = "" + open fun formatIndentedSoftLineBreak(): String = "" + + open fun formatText(location: Location, nodes: Iterable<ContentNode>, listKind: ListKind = ListKind.Unordered): String { + return nodes.map { formatText(location, it, listKind) }.joinToString("") + } + + open fun formatText(location: Location, content: ContentNode, listKind: ListKind = ListKind.Unordered): String { + return StringBuilder().apply { + when (content) { + is ContentText -> append(formatText(content.text)) + is ContentSymbol -> append(formatSymbol(content.text)) + is ContentKeyword -> append(formatKeyword(content.text)) + is ContentIdentifier -> append(formatIdentifier(content.text, content.kind)) + is ContentNonBreakingSpace -> append(formatNonBreakingSpace()) + is ContentSoftLineBreak -> append(formatSoftLineBreak()) + is ContentIndentedSoftLineBreak -> append(formatIndentedSoftLineBreak()) + is ContentEntity -> append(formatEntity(content.text)) + is ContentStrong -> append(formatStrong(formatText(location, content.children))) + is ContentStrikethrough -> append(formatStrikethrough(formatText(location, content.children))) + is ContentCode -> append(formatCode(formatText(location, content.children))) + is ContentEmphasis -> append(formatEmphasis(formatText(location, content.children))) + is ContentUnorderedList -> append(formatUnorderedList(formatText(location, content.children, ListKind.Unordered))) + is ContentOrderedList -> append(formatOrderedList(formatText(location, content.children, ListKind.Ordered))) + is ContentListItem -> append(formatListItem(formatText(location, content.children), listKind)) + + is ContentNodeLink -> { + val node = content.node + val linkTo = if (node != null) locationHref(location, node) else "#" + val linkText = formatText(location, content.children) + if (linkTo == ".") { + append(linkText) + } else { + append(formatLink(linkText, linkTo)) + } + } + is ContentExternalLink -> { + val linkText = formatText(location, content.children) + if (content.href == ".") { + append(linkText) + } else { + append(formatLink(linkText, content.href)) + } + } + is ContentParagraph -> appendParagraph(this, formatText(location, content.children)) + is ContentBlockCode -> appendBlockCode(this, formatText(location, content.children), content.language) + is ContentHeading -> appendHeader(this, formatText(location, content.children), content.level) + is ContentBlock -> append(formatText(location, content.children)) + } + }.toString() + } + + open fun link(from: DocumentationNode, to: DocumentationNode): FormatLink = link(from, to, extension) + + open fun link(from: DocumentationNode, to: DocumentationNode, extension: String): FormatLink { + return FormatLink(to.name, locationService.relativePathToLocation(from, to)) + } + + fun locationHref(from: Location, to: DocumentationNode): String { + val topLevelPage = to.references(DocumentationReference.Kind.TopLevelPage).singleOrNull()?.to + if (topLevelPage != null) { + return from.relativePathTo(locationService.location(topLevelPage), to.name) + } + return from.relativePathTo(locationService.location(to)) + } + + fun appendDocumentation(location: Location, to: StringBuilder, overloads: Iterable<DocumentationNode>) { + val breakdownBySummary = overloads.groupByTo(LinkedHashMap()) { node -> node.content } + + for ((summary, items) in breakdownBySummary) { + appendAsOverloadGroup(to) { + items.forEach { + val rendered = languageService.render(it) + appendAsSignature(to, rendered) { + to.append(formatCode(formatText(location, rendered))) + it.appendSourceLink(to) + } + it.appendOverrides(to) + it.appendDeprecation(location, to) + } + // All items have exactly the same documentation, so we can use any item to render it + val item = items.first() + item.details(DocumentationNode.Kind.OverloadGroupNote).forEach { + to.append(formatText(location, it.content)) + } + to.append(formatText(location, item.content.summary)) + appendDescription(location, to, item) + appendLine(to) + appendLine(to) + } + } + } + + private fun DocumentationNode.isModuleOrPackage(): Boolean = + kind == DocumentationNode.Kind.Module || kind == DocumentationNode.Kind.Package + + protected open fun appendAsSignature(to: StringBuilder, node: ContentNode, block: () -> Unit) { + block() + } + + protected open fun appendAsOverloadGroup(to: StringBuilder, block: () -> Unit) { + block() + } + + fun appendDescription(location: Location, to: StringBuilder, node: DocumentationNode) { + if (node.content.description != ContentEmpty) { + appendLine(to, formatText(location, node.content.description)) + appendLine(to) + } + node.content.getSectionsWithSubjects().forEach { + appendSectionWithSubject(it.key, location, it.value, to) + } + + for (section in node.content.sections.filter { it.subjectName == null }) { + appendLine(to, formatStrong(formatText(section.tag))) + appendLine(to, formatText(location, section)) + } + } + + fun Content.getSectionsWithSubjects(): Map<String, List<ContentSection>> = + sections.filter { it.subjectName != null }.groupBy { it.tag } + + fun appendSectionWithSubject(title: String, location: Location, subjectSections: List<ContentSection>, to: StringBuilder) { + appendHeader(to, title, 3) + subjectSections.forEach { + val subjectName = it.subjectName + if (subjectName != null) { + appendAnchor(to, subjectName) + to.append(formatCode(subjectName)).append(" - ") + to.append(formatText(location, it)) + appendLine(to) + } + } + } + + private fun DocumentationNode.appendOverrides(to: StringBuilder) { + overrides.forEach { + to.append("Overrides ") + val location = locationService.relativePathToLocation(this, it) + appendLine(to, formatLink(FormatLink(it.owner!!.name + "." + it.name, location))) + } + } + + private fun DocumentationNode.appendDeprecation(location: Location, to: StringBuilder) { + if (deprecation != null) { + val deprecationParameter = deprecation!!.details(DocumentationNode.Kind.Parameter).firstOrNull() + val deprecationValue = deprecationParameter?.details(DocumentationNode.Kind.Value)?.firstOrNull() + if (deprecationValue != null) { + to.append(formatStrong("Deprecated:")).append(" ") + appendLine(to, formatText(deprecationValue.name.removeSurrounding("\""))) + appendLine(to) + } else if (deprecation?.content != Content.Empty) { + to.append(formatStrong("Deprecated:")).append(" ") + to.append(formatText(location, deprecation!!.content)) + } else { + appendLine(to, formatStrong("Deprecated")) + appendLine(to) + } + } + } + + private fun DocumentationNode.appendSourceLink(to: StringBuilder) { + val sourceUrl = details(DocumentationNode.Kind.SourceUrl).firstOrNull() + if (sourceUrl != null) { + to.append(" ") + appendLine(to, formatLink("(source)", sourceUrl.name)) + } else { + appendLine(to) + } + } + + fun appendLocation(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + val singleNode = nodes.singleOrNull() + if (singleNode != null && singleNode.isModuleOrPackage()) { + if (singleNode.kind == DocumentationNode.Kind.Package) { + appendHeader(to, "Package " + formatText(singleNode.name), 2) + } + to.append(formatText(location, singleNode.content)) + } else { + val breakdownByName = nodes.groupBy { node -> node.name } + for ((name, items) in breakdownByName) { + appendHeader(to, formatText(name)) + appendDocumentation(location, to, items) + } + } + } + + private fun appendSection(location: Location, caption: String, nodes: List<DocumentationNode>, node: DocumentationNode, to: StringBuilder) { + if (nodes.any()) { + appendHeader(to, caption, 3) + + val children = nodes.sortedBy { it.name } + val membersMap = children.groupBy { link(node, it) } + + appendTable(to) { + appendTableBody(to) { + for ((memberLocation, members) in membersMap) { + appendTableRow(to) { + appendTableCell(to) { + to.append(formatLink(memberLocation)) + } + appendTableCell(to) { + val breakdownBySummary = members.groupBy { formatText(location, it.summary) } + for ((summary, items) in breakdownBySummary) { + appendSummarySignatures(items, location, to) + if (!summary.isEmpty()) { + to.append(summary) + } + } + } + } + } + } + } + } + } + + private fun appendSummarySignatures(items: List<DocumentationNode>, location: Location, to: StringBuilder) { + val summarySignature = languageService.summarizeSignatures(items) + if (summarySignature != null) { + appendAsSignature(to, summarySignature) { + appendLine(to, summarySignature.signatureToText(location)) + } + return + } + val renderedSignatures = items.map { languageService.render(it, RenderMode.SUMMARY) } + renderedSignatures.subList(0, renderedSignatures.size - 1).forEach { + appendAsSignature(to, it) { + appendLine(to, it.signatureToText(location)) + } + } + appendAsSignature(to, renderedSignatures.last()) { + to.append(renderedSignatures.last().signatureToText(location)) + } + } + + private fun ContentNode.signatureToText(location: Location): String { + return if (this is ContentBlock && this.isEmpty()) { + "" + } else { + val signatureAsCode = ContentCode() + signatureAsCode.append(this) + formatText(location, signatureAsCode) + } + } + + override fun appendNodes(location: Location, to: StringBuilder, nodes: Iterable<DocumentationNode>) { + val breakdownByLocation = nodes.groupBy { node -> + formatBreadcrumbs(node.path.filterNot { it.name.isEmpty() }.map { link(node, it) }) + } + + for ((breadcrumbs, items) in breakdownByLocation) { + appendLine(to, breadcrumbs) + appendLine(to) + appendLocation(location, to, items.filter { it.kind != DocumentationNode.Kind.ExternalClass }) + } + + for (node in nodes) { + if (node.kind == DocumentationNode.Kind.ExternalClass) { + appendSection(location, "Extensions for ${node.name}", node.members, node, to) + continue + } + + appendSection(location, "Packages", node.members(DocumentationNode.Kind.Package), node, to) + appendSection(location, "Types", node.members.filter { it.kind in DocumentationNode.Kind.classLike }, node, to) + appendSection(location, "Extensions for External Classes", node.members(DocumentationNode.Kind.ExternalClass), node, to) + appendSection(location, "Enum Values", node.members(DocumentationNode.Kind.EnumItem), node, to) + appendSection(location, "Constructors", node.members(DocumentationNode.Kind.Constructor), node, to) + appendSection(location, "Properties", node.members(DocumentationNode.Kind.Property), node, to) + appendSection(location, "Inherited Properties", node.inheritedMembers(DocumentationNode.Kind.Property), node, to) + appendSection(location, "Functions", node.members(DocumentationNode.Kind.Function), node, to) + appendSection(location, "Inherited Functions", node.inheritedMembers(DocumentationNode.Kind.Function), node, to) + appendSection(location, "Companion Object Properties", node.members(DocumentationNode.Kind.CompanionObjectProperty), node, to) + appendSection(location, "Companion Object Functions", node.members(DocumentationNode.Kind.CompanionObjectFunction), node, to) + appendSection(location, "Other members", node.members.filter { + it.kind !in setOf( + DocumentationNode.Kind.Class, + DocumentationNode.Kind.Interface, + DocumentationNode.Kind.Enum, + DocumentationNode.Kind.Object, + DocumentationNode.Kind.AnnotationClass, + DocumentationNode.Kind.Constructor, + DocumentationNode.Kind.Property, + DocumentationNode.Kind.Package, + DocumentationNode.Kind.Function, + DocumentationNode.Kind.CompanionObjectProperty, + DocumentationNode.Kind.CompanionObjectFunction, + DocumentationNode.Kind.ExternalClass, + DocumentationNode.Kind.EnumItem + ) + }, node, to) + + val allExtensions = collectAllExtensions(node) + appendSection(location, "Extension Properties", allExtensions.filter { it.kind == DocumentationNode.Kind.Property }, node, to) + appendSection(location, "Extension Functions", allExtensions.filter { it.kind == DocumentationNode.Kind.Function }, node, to) + appendSection(location, "Companion Object Extension Properties", allExtensions.filter { it.kind == DocumentationNode.Kind.CompanionObjectProperty }, node, to) + appendSection(location, "Companion Object Extension Functions", allExtensions.filter { it.kind == DocumentationNode.Kind.CompanionObjectFunction }, node, to) + appendSection(location, "Inheritors", + node.inheritors.filter { it.kind != DocumentationNode.Kind.EnumItem }, node, to) + appendSection(location, "Links", node.links, node, to) + + } + } + + private fun collectAllExtensions(node: DocumentationNode): Collection<DocumentationNode> { + val result = LinkedHashSet<DocumentationNode>() + val visited = hashSetOf<DocumentationNode>() + + fun collect(node: DocumentationNode) { + if (!visited.add(node)) return + result.addAll(node.extensions) + node.references(DocumentationReference.Kind.Superclass).forEach { collect(it.to) } + } + + collect(node) + + return result + + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Formats/YamlOutlineService.kt b/core/src/main/kotlin/Formats/YamlOutlineService.kt new file mode 100644 index 00000000..7968824c --- /dev/null +++ b/core/src/main/kotlin/Formats/YamlOutlineService.kt @@ -0,0 +1,24 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import java.io.File + +class YamlOutlineService @Inject constructor(val locationService: LocationService, + val languageService: LanguageService) : OutlineFormatService { + override fun getOutlineFileName(location: Location): File = File("${location.path}.yml") + + var outlineLevel = 0 + override fun appendOutlineHeader(location: Location, node: DocumentationNode, to: StringBuilder) { + val indent = " ".repeat(outlineLevel) + to.appendln("$indent- title: ${languageService.renderName(node)}") + to.appendln("$indent url: ${locationService.location(node).path}") + } + + override fun appendOutlineLevel(to: StringBuilder, body: () -> Unit) { + val indent = " ".repeat(outlineLevel) + to.appendln("$indent content:") + outlineLevel++ + body() + outlineLevel-- + } +} diff --git a/core/src/main/kotlin/Generation/ConsoleGenerator.kt b/core/src/main/kotlin/Generation/ConsoleGenerator.kt new file mode 100644 index 00000000..803a16e4 --- /dev/null +++ b/core/src/main/kotlin/Generation/ConsoleGenerator.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka + +public class ConsoleGenerator(val signatureGenerator: LanguageService, val locationService: LocationService) { + val IndentStep = " " + + public fun generate(node: DocumentationNode, indent: String = "") { + println("@${locationService.location(node).path}") + generateHeader(node, indent) + //generateDetails(node, indent) + generateMembers(node, indent) + generateLinks(node, indent) + } + + public fun generateHeader(node: DocumentationNode, indent: String = "") { + println(indent + signatureGenerator.render(node)) + val docString = node.content.toString() + if (!docString.isEmpty()) + println("$indent\"${docString.replace("\n", "\n$indent")}\"") + println() + } + + public fun generateMembers(node: DocumentationNode, indent: String = "") { + val items = node.members.sortedBy { it.name } + for (child in items) + generate(child, indent + IndentStep) + } + + public fun generateDetails(node: DocumentationNode, indent: String = "") { + val items = node.details + for (child in items) + generate(child, indent + " ") + } + + public fun generateLinks(node: DocumentationNode, indent: String = "") { + val items = node.links + if (items.isEmpty()) + return + println("$indent Links") + for (child in items) + generate(child, indent + " ") + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Generation/FileGenerator.kt b/core/src/main/kotlin/Generation/FileGenerator.kt new file mode 100644 index 00000000..a762bae3 --- /dev/null +++ b/core/src/main/kotlin/Generation/FileGenerator.kt @@ -0,0 +1,57 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStreamWriter + +public class FileGenerator @Inject constructor(val locationService: FileLocationService) : Generator { + + @set:Inject(optional = true) var outlineService: OutlineFormatService? = null + @set:Inject(optional = true) lateinit var formatService: FormatService + + override fun buildPages(nodes: Iterable<DocumentationNode>) { + val specificLocationService = locationService.withExtension(formatService.extension) + + for ((location, items) in nodes.groupBy { specificLocationService.location(it) }) { + val file = location.file + file.parentFile?.mkdirsOrFail() + try { + FileOutputStream(file).use { + OutputStreamWriter(it, Charsets.UTF_8).use { + it.write(formatService.format(location, items)) + } + } + } catch (e: Throwable) { + println(e) + } + buildPages(items.flatMap { it.members }) + } + } + + override fun buildOutlines(nodes: Iterable<DocumentationNode>) { + val outlineService = this.outlineService ?: return + for ((location, items) in nodes.groupBy { locationService.location(it) }) { + val file = outlineService.getOutlineFileName(location) + file.parentFile?.mkdirsOrFail() + FileOutputStream(file).use { + OutputStreamWriter(it, Charsets.UTF_8).use { + it.write(outlineService.formatOutline(location, items)) + } + } + } + } + + override fun buildSupportFiles() { + FileOutputStream(locationService.location(listOf("style.css"), false).file).use { + javaClass.getResourceAsStream("/dokka/styles/style.css").copyTo(it) + } + } +} + +private fun File.mkdirsOrFail() { + if (!mkdirs() && !exists()) { + throw IOException("Failed to create directory $this") + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Generation/Generator.kt b/core/src/main/kotlin/Generation/Generator.kt new file mode 100644 index 00000000..ac10a6a5 --- /dev/null +++ b/core/src/main/kotlin/Generation/Generator.kt @@ -0,0 +1,19 @@ +package org.jetbrains.dokka + +public interface Generator { + fun buildPages(nodes: Iterable<DocumentationNode>) + fun buildOutlines(nodes: Iterable<DocumentationNode>) + fun buildSupportFiles() +} + +fun Generator.buildAll(nodes: Iterable<DocumentationNode>) { + buildPages(nodes) + buildOutlines(nodes) + buildSupportFiles() +} + +fun Generator.buildPage(node: DocumentationNode): Unit = buildPages(listOf(node)) + +fun Generator.buildOutline(node: DocumentationNode): Unit = buildOutlines(listOf(node)) + +fun Generator.buildAll(node: DocumentationNode): Unit = buildAll(listOf(node)) diff --git a/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt new file mode 100644 index 00000000..3c9875cd --- /dev/null +++ b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt @@ -0,0 +1,266 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.psi.* +import org.jetbrains.dokka.DocumentationNode.Kind + +fun getSignature(element: PsiElement?) = when(element) { + is PsiClass -> element.qualifiedName + is PsiField -> element.containingClass!!.qualifiedName + "#" + element.name + is PsiMethod -> + element.containingClass!!.qualifiedName + "#" + element.name + "(" + + element.parameterList.parameters.map { it.type.typeSignature() }.joinToString(",") + ")" + else -> null +} + +private fun PsiType.typeSignature(): String = when(this) { + is PsiArrayType -> "Array<${componentType.typeSignature()}>" + else -> mapTypeName(this) +} + +private fun mapTypeName(psiType: PsiType): String = when (psiType) { + is PsiPrimitiveType -> psiType.canonicalText + is PsiClassType -> psiType.resolve()?.qualifiedName ?: psiType.className + is PsiEllipsisType -> mapTypeName(psiType.componentType) + is PsiArrayType -> "Array" + else -> psiType.canonicalText +} + +interface JavaDocumentationBuilder { + fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) +} + +class JavaPsiDocumentationBuilder : JavaDocumentationBuilder { + private val options: DocumentationOptions + private val refGraph: NodeReferenceGraph + private val docParser: JavaDocumentationParser + + @Inject constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph) { + this.options = options + this.refGraph = refGraph + this.docParser = JavadocParser(refGraph) + } + + constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph, docParser: JavaDocumentationParser) { + this.options = options + this.refGraph = refGraph + this.docParser = docParser + } + + override fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) { + if (file.classes.all { skipElement(it) }) { + return + } + val packageNode = module.findOrCreatePackageNode(file.packageName, emptyMap()) + appendClasses(packageNode, file.classes) + } + + fun appendClasses(packageNode: DocumentationNode, classes: Array<PsiClass>) { + packageNode.appendChildren(classes) { build() } + } + + fun register(element: PsiElement, node: DocumentationNode) { + val signature = getSignature(element) + if (signature != null) { + refGraph.register(signature, node) + } + } + + fun link(node: DocumentationNode, element: PsiElement?) { + val qualifiedName = getSignature(element) + if (qualifiedName != null) { + refGraph.link(node, qualifiedName, DocumentationReference.Kind.Link) + } + } + + fun link(element: PsiElement?, node: DocumentationNode, kind: DocumentationReference.Kind) { + val qualifiedName = getSignature(element) + if (qualifiedName != null) { + refGraph.link(qualifiedName, node, kind) + } + } + + fun nodeForElement(element: PsiNamedElement, + kind: Kind, + name: String = element.name ?: "<anonymous>"): DocumentationNode { + val (docComment, deprecatedContent) = docParser.parseDocumentation(element) + val node = DocumentationNode(name, docComment, kind) + if (element is PsiModifierListOwner) { + node.appendModifiers(element) + val modifierList = element.modifierList + if (modifierList != null) { + modifierList.annotations.filter { !ignoreAnnotation(it) }.forEach { + val annotation = it.build() + node.append(annotation, + if (it.qualifiedName == "java.lang.Deprecated") DocumentationReference.Kind.Deprecation else DocumentationReference.Kind.Annotation) + } + } + } + if (deprecatedContent != null) { + val deprecationNode = DocumentationNode("", deprecatedContent, Kind.Modifier) + node.append(deprecationNode, DocumentationReference.Kind.Deprecation) + } + if (element is PsiDocCommentOwner && element.isDeprecated && node.deprecation == null) { + val deprecationNode = DocumentationNode("", Content.of(ContentText("Deprecated")), Kind.Modifier) + node.append(deprecationNode, DocumentationReference.Kind.Deprecation) + } + return node + } + + fun ignoreAnnotation(annotation: PsiAnnotation) = when(annotation.qualifiedName) { + "java.lang.SuppressWarnings" -> true + else -> false + } + + fun <T : Any> DocumentationNode.appendChildren(elements: Array<T>, + kind: DocumentationReference.Kind = DocumentationReference.Kind.Member, + buildFn: T.() -> DocumentationNode) { + elements.forEach { + if (!skipElement(it)) { + append(it.buildFn(), kind) + } + } + } + + private fun skipElement(element: Any) = skipElementByVisibility(element) || hasSuppressTag(element) + + private fun skipElementByVisibility(element: Any): Boolean = + !options.includeNonPublic && element is PsiModifierListOwner && + (element.hasModifierProperty(PsiModifier.PRIVATE) || element.hasModifierProperty(PsiModifier.PACKAGE_LOCAL)) + + private fun hasSuppressTag(element: Any) = + element is PsiDocCommentOwner && element.docComment?.let { it.findTagByName("suppress") != null } ?: false + + fun <T : Any> DocumentationNode.appendMembers(elements: Array<T>, buildFn: T.() -> DocumentationNode) = + appendChildren(elements, DocumentationReference.Kind.Member, buildFn) + + fun <T : Any> DocumentationNode.appendDetails(elements: Array<T>, buildFn: T.() -> DocumentationNode) = + appendChildren(elements, DocumentationReference.Kind.Detail, buildFn) + + fun PsiClass.build(): DocumentationNode { + val kind = when { + isInterface -> DocumentationNode.Kind.Interface + isEnum -> DocumentationNode.Kind.Enum + isAnnotationType -> DocumentationNode.Kind.AnnotationClass + else -> DocumentationNode.Kind.Class + } + val node = nodeForElement(this, kind) + superTypes.filter { !ignoreSupertype(it) }.forEach { + node.appendType(it, Kind.Supertype) + val superClass = it.resolve() + if (superClass != null) { + link(superClass, node, DocumentationReference.Kind.Inheritor) + } + } + node.appendDetails(typeParameters) { build() } + node.appendMembers(methods) { build() } + node.appendMembers(fields) { build() } + node.appendMembers(innerClasses) { build() } + register(this, node) + return node + } + + fun ignoreSupertype(psiType: PsiClassType): Boolean = + psiType.isClass("java.lang.Enum") || psiType.isClass("java.lang.Object") + + fun PsiClassType.isClass(qName: String): Boolean { + val shortName = qName.substringAfterLast('.') + if (className == shortName) { + val psiClass = resolve() + return psiClass?.qualifiedName == qName + } + return false + } + + fun PsiField.build(): DocumentationNode { + val node = nodeForElement(this, nodeKind()) + node.appendType(type) + node.appendModifiers(this) + register(this, node) + return node + } + + private fun PsiField.nodeKind(): Kind = when { + this is PsiEnumConstant -> Kind.EnumItem + else -> Kind.Field + } + + fun PsiMethod.build(): DocumentationNode { + val node = nodeForElement(this, nodeKind(), + if (isConstructor) "<init>" else name) + + if (!isConstructor) { + node.appendType(returnType) + } + node.appendDetails(parameterList.parameters) { build() } + node.appendDetails(typeParameters) { build() } + register(this, node) + return node + } + + private fun PsiMethod.nodeKind(): Kind = when { + isConstructor -> Kind.Constructor + else -> Kind.Function + } + + fun PsiParameter.build(): DocumentationNode { + val node = nodeForElement(this, Kind.Parameter) + node.appendType(type) + if (type is PsiEllipsisType) { + node.appendTextNode("vararg", Kind.Modifier, DocumentationReference.Kind.Detail) + } + return node + } + + fun PsiTypeParameter.build(): DocumentationNode { + val node = nodeForElement(this, Kind.TypeParameter) + extendsListTypes.forEach { node.appendType(it, Kind.UpperBound) } + implementsListTypes.forEach { node.appendType(it, Kind.UpperBound) } + return node + } + + fun DocumentationNode.appendModifiers(element: PsiModifierListOwner) { + val modifierList = element.modifierList ?: return + + PsiModifier.MODIFIERS.forEach { + if (modifierList.hasExplicitModifier(it)) { + appendTextNode(it, Kind.Modifier) + } + } + } + + fun DocumentationNode.appendType(psiType: PsiType?, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type) { + if (psiType == null) { + return + } + append(psiType.build(kind), DocumentationReference.Kind.Detail) + } + + fun PsiType.build(kind: DocumentationNode.Kind = DocumentationNode.Kind.Type): DocumentationNode { + val name = mapTypeName(this) + val node = DocumentationNode(name, Content.Empty, kind) + if (this is PsiClassType) { + node.appendDetails(parameters) { build(Kind.Type) } + link(node, resolve()) + } + if (this is PsiArrayType && this !is PsiEllipsisType) { + node.append(componentType.build(Kind.Type), DocumentationReference.Kind.Detail) + } + return node + } + + fun PsiAnnotation.build(): DocumentationNode { + val node = DocumentationNode(nameReferenceElement?.text ?: "<?>", Content.Empty, DocumentationNode.Kind.Annotation) + parameterList.attributes.forEach { + val parameter = DocumentationNode(it.name ?: "value", Content.Empty, DocumentationNode.Kind.Parameter) + val value = it.value + if (value != null) { + val valueText = (value as? PsiLiteralExpression)?.value as? String ?: value.text + val valueNode = DocumentationNode(valueText, Content.Empty, DocumentationNode.Kind.Value) + parameter.append(valueNode, DocumentationReference.Kind.Detail) + } + node.append(parameter, DocumentationReference.Kind.Detail) + } + return node + } +} diff --git a/core/src/main/kotlin/Java/JavadocParser.kt b/core/src/main/kotlin/Java/JavadocParser.kt new file mode 100644 index 00000000..1378a5a7 --- /dev/null +++ b/core/src/main/kotlin/Java/JavadocParser.kt @@ -0,0 +1,170 @@ +package org.jetbrains.dokka + +import com.intellij.psi.* +import com.intellij.psi.javadoc.PsiDocTag +import com.intellij.psi.javadoc.PsiDocTagValue +import com.intellij.psi.javadoc.PsiDocToken +import com.intellij.psi.javadoc.PsiInlineDocTag +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode + +data class JavadocParseResult(val content: Content, val deprecatedContent: Content?) { + companion object { + val Empty = JavadocParseResult(Content.Empty, null) + } +} + +interface JavaDocumentationParser { + fun parseDocumentation(element: PsiNamedElement): JavadocParseResult +} + +class JavadocParser(private val refGraph: NodeReferenceGraph) : JavaDocumentationParser { + override fun parseDocumentation(element: PsiNamedElement): JavadocParseResult { + val docComment = (element as? PsiDocCommentOwner)?.docComment + if (docComment == null) return JavadocParseResult.Empty + val result = MutableContent() + var deprecatedContent: Content? = null + val para = ContentParagraph() + result.append(para) + para.convertJavadocElements(docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() }) + docComment.tags.forEach { tag -> + when(tag.name) { + "see" -> result.convertSeeTag(tag) + "deprecated" -> { + deprecatedContent = Content() + deprecatedContent!!.convertJavadocElements(tag.contentElements()) + } + else -> { + val subjectName = tag.getSubjectName() + val section = result.addSection(javadocSectionDisplayName(tag.name), subjectName) + + section.convertJavadocElements(tag.contentElements()) + } + } + } + return JavadocParseResult(result, deprecatedContent) + } + + private fun PsiDocTag.contentElements(): Iterable<PsiElement> { + val tagValueElements = children + .dropWhile { it.node?.elementType == JavaDocTokenType.DOC_TAG_NAME } + .dropWhile { it is PsiWhiteSpace } + .filterNot { it.node?.elementType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS } + return if (getSubjectName() != null) tagValueElements.dropWhile { it is PsiDocTagValue } else tagValueElements + } + + private fun ContentBlock.convertJavadocElements(elements: Iterable<PsiElement>) { + val htmlBuilder = StringBuilder() + elements.forEach { + if (it is PsiInlineDocTag) { + htmlBuilder.append(convertInlineDocTag(it)) + } else { + htmlBuilder.append(it.text) + } + } + val doc = Jsoup.parse(htmlBuilder.toString().trimStart()) + doc.body().childNodes().forEach { + convertHtmlNode(it) + } + } + + private fun ContentBlock.convertHtmlNode(node: Node) { + if (node is TextNode) { + append(ContentText(node.text())) + } else if (node is Element) { + val childBlock = createBlock(node) + node.childNodes().forEach { + childBlock.convertHtmlNode(it) + } + append(childBlock) + } + } + + private fun createBlock(element: Element): ContentBlock = when(element.tagName()) { + "p" -> ContentParagraph() + "b", "strong" -> ContentStrong() + "i", "em" -> ContentEmphasis() + "s", "del" -> ContentStrikethrough() + "code" -> ContentCode() + "pre" -> ContentBlockCode() + "ul" -> ContentUnorderedList() + "ol" -> ContentOrderedList() + "li" -> ContentListItem() + "a" -> createLink(element) + else -> ContentBlock() + } + + private fun createLink(element: Element): ContentBlock { + val docref = element.attr("docref") + if (docref != null) { + return ContentNodeLazyLink(docref, { -> refGraph.lookup(docref)}) + } + val href = element.attr("href") + if (href != null) { + return ContentExternalLink(href) + } else { + return ContentBlock() + } + } + + private fun MutableContent.convertSeeTag(tag: PsiDocTag) { + val linkElement = tag.linkElement() + if (linkElement == null) { + return + } + val seeSection = findSectionByTag(ContentTags.SeeAlso) ?: addSection(ContentTags.SeeAlso, null) + val linkSignature = resolveLink(linkElement) + val text = ContentText(linkElement.text) + if (linkSignature != null) { + val linkNode = ContentNodeLazyLink(tag.valueElement!!.text, { -> refGraph.lookup(linkSignature)}) + linkNode.append(text) + seeSection.append(linkNode) + } else { + seeSection.append(text) + } + } + + private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) { + "link", "linkplain" -> { + val valueElement = tag.linkElement() + val linkSignature = resolveLink(valueElement) + if (linkSignature != null) { + val labelText = tag.dataElements.firstOrNull { it is PsiDocToken }?.text ?: valueElement!!.text + val link = "<a docref=\"$linkSignature\">${labelText.htmlEscape()}</a>" + if (tag.name == "link") "<code>$link</code>" else link + } + else if (valueElement != null) { + valueElement.text + } else { + "" + } + } + "code", "literal" -> { + val text = StringBuilder() + tag.dataElements.forEach { text.append(it.text) } + val escaped = text.toString().trimStart().htmlEscape() + if (tag.name == "code") "<code>$escaped</code>" else escaped + } + else -> tag.text + } + + private fun PsiDocTag.linkElement(): PsiElement? = + valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace } + + private fun resolveLink(valueElement: PsiElement?): String? { + val target = valueElement?.reference?.resolve() + if (target != null) { + return getSignature(target) + } + return null + } + + fun PsiDocTag.getSubjectName(): String? { + if (name == "param" || name == "throws" || name == "exception") { + return valueElement?.text + } + return null + } +} diff --git a/core/src/main/kotlin/Kotlin/ContentBuilder.kt b/core/src/main/kotlin/Kotlin/ContentBuilder.kt new file mode 100644 index 00000000..c4bb18de --- /dev/null +++ b/core/src/main/kotlin/Kotlin/ContentBuilder.kt @@ -0,0 +1,132 @@ +package org.jetbrains.dokka + +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.html.entities.EntityConverter +import java.util.* + +public fun buildContent(tree: MarkdownNode, linkResolver: (String) -> ContentBlock, inline: Boolean = false): MutableContent { + val result = MutableContent() + if (inline) { + buildInlineContentTo(tree, result, linkResolver) + } + else { + buildContentTo(tree, result, linkResolver) + } + return result +} + +public fun buildContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: (String) -> ContentBlock) { +// println(tree.toTestString()) + val nodeStack = ArrayDeque<ContentBlock>() + nodeStack.push(target) + + tree.visit {node, processChildren -> + val parent = nodeStack.peek() + + fun appendNodeWithChildren(content: ContentBlock) { + nodeStack.push(content) + processChildren() + parent.append(nodeStack.pop()) + } + + when (node.type) { + MarkdownElementTypes.ATX_1 -> appendNodeWithChildren(ContentHeading(1)) + MarkdownElementTypes.ATX_2 -> appendNodeWithChildren(ContentHeading(2)) + MarkdownElementTypes.ATX_3 -> appendNodeWithChildren(ContentHeading(3)) + MarkdownElementTypes.ATX_4 -> appendNodeWithChildren(ContentHeading(4)) + MarkdownElementTypes.ATX_5 -> appendNodeWithChildren(ContentHeading(5)) + MarkdownElementTypes.ATX_6 -> appendNodeWithChildren(ContentHeading(6)) + MarkdownElementTypes.UNORDERED_LIST -> appendNodeWithChildren(ContentUnorderedList()) + MarkdownElementTypes.ORDERED_LIST -> appendNodeWithChildren(ContentOrderedList()) + MarkdownElementTypes.LIST_ITEM -> appendNodeWithChildren(ContentListItem()) + MarkdownElementTypes.EMPH -> appendNodeWithChildren(ContentEmphasis()) + MarkdownElementTypes.STRONG -> appendNodeWithChildren(ContentStrong()) + MarkdownElementTypes.CODE_SPAN -> appendNodeWithChildren(ContentCode()) + MarkdownElementTypes.CODE_BLOCK, + MarkdownElementTypes.CODE_FENCE -> appendNodeWithChildren(ContentBlockCode()) + MarkdownElementTypes.PARAGRAPH -> appendNodeWithChildren(ContentParagraph()) + + MarkdownElementTypes.INLINE_LINK -> { + val label = node.child(MarkdownElementTypes.LINK_TEXT)?.child(MarkdownTokenTypes.TEXT) + val destination = node.child(MarkdownElementTypes.LINK_DESTINATION) + if (label != null) { + if (destination != null) { + val link = ContentExternalLink(destination.text) + link.append(ContentText(label.text)) + parent.append(link) + } else { + val link = ContentExternalLink(label.text) + link.append(ContentText(label.text)) + parent.append(link) + } + } + } + MarkdownElementTypes.SHORT_REFERENCE_LINK, + MarkdownElementTypes.FULL_REFERENCE_LINK -> { + val label = node.child(MarkdownElementTypes.LINK_LABEL)?.child(MarkdownTokenTypes.TEXT) + if (label != null) { + val link = linkResolver(label.text) + val linkText = node.child(MarkdownElementTypes.LINK_TEXT)?.child(MarkdownTokenTypes.TEXT) + link.append(ContentText(linkText?.text ?: label.text)) + parent.append(link) + } + } + MarkdownTokenTypes.WHITE_SPACE, + MarkdownTokenTypes.EOL -> { + if (keepWhitespace(nodeStack.peek()) && node.parent?.children?.last() != node) { + parent.append(ContentText(node.text)) + } + } + + MarkdownTokenTypes.CODE -> { + val block = ContentBlockCode() + block.append(ContentText(node.text)) + parent.append(block) + } + + MarkdownTokenTypes.TEXT -> { + fun createEntityOrText(text: String): ContentNode { + if (text == "&" || text == """ || text == "<" || text == ">") { + return ContentEntity(text) + } + if (text == "&") { + return ContentEntity("&") + } + val decodedText = EntityConverter.replaceEntities(text, true, true) + if (decodedText != text) { + return ContentEntity(text) + } + return ContentText(text) + } + + parent.append(createEntityOrText(node.text)) + } + + MarkdownTokenTypes.COLON, + MarkdownTokenTypes.DOUBLE_QUOTE, + MarkdownTokenTypes.LT, + MarkdownTokenTypes.GT, + MarkdownTokenTypes.LPAREN, + MarkdownTokenTypes.RPAREN, + MarkdownTokenTypes.LBRACKET, + MarkdownTokenTypes.RBRACKET, + MarkdownTokenTypes.CODE_FENCE_CONTENT -> { + parent.append(ContentText(node.text)) + } + else -> { + processChildren() + } + } + } +} + +private fun keepWhitespace(node: ContentNode) = node is ContentParagraph || node is ContentSection + +public fun buildInlineContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: (String) -> ContentBlock) { + val inlineContent = tree.children.singleOrNull { it.type == MarkdownElementTypes.PARAGRAPH }?.children ?: listOf(tree) + inlineContent.forEach { + buildContentTo(it, target, linkResolver) + } +} + diff --git a/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt b/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt new file mode 100644 index 00000000..2569bc71 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt @@ -0,0 +1,43 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink + +class DeclarationLinkResolver + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val refGraph: NodeReferenceGraph, + val logger: DokkaLogger) { + fun resolveContentLink(fromDescriptor: DeclarationDescriptor, href: String): ContentBlock { + val symbol = try { + val symbols = resolveKDocLink(resolutionFacade, fromDescriptor, null, href.split('.').toList()) + findTargetSymbol(symbols) + } catch(e: Exception) { + null + } + + // don't include unresolved links in generated doc + // assume that if an href doesn't contain '/', it's not an attempt to reference an external file + if (symbol != null) { + return ContentNodeLazyLink(href, { -> refGraph.lookup(symbol.signature()) }) + } + if ("/" in href) { + return ContentExternalLink(href) + } + logger.warn("Unresolved link to $href in doc comment of ${fromDescriptor.signatureWithSourceLocation()}") + return ContentExternalLink("#") + } + + fun findTargetSymbol(symbols: Collection<DeclarationDescriptor>): DeclarationDescriptor? { + if (symbols.isEmpty()) { + return null + } + val symbol = symbols.first() + if (symbol is CallableMemberDescriptor && symbol.kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE) { + return symbol.overriddenDescriptors.firstOrNull() + } + return symbol + } + +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt b/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt new file mode 100644 index 00000000..b7705ec9 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt @@ -0,0 +1,199 @@ +package org.jetbrains.dokka.Kotlin + +import com.google.inject.Inject +import com.intellij.psi.PsiDocCommentOwner +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.* +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.idea.kdoc.KDocFinder +import org.jetbrains.kotlin.idea.kdoc.getResolutionScope +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.load.java.descriptors.JavaCallableMemberDescriptor +import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.ResolutionScope +import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered +import org.jetbrains.kotlin.resolve.source.PsiSourceElement + +class DescriptorDocumentationParser + @Inject constructor(val options: DocumentationOptions, + val logger: DokkaLogger, + val linkResolver: DeclarationLinkResolver, + val resolutionFacade: DokkaResolutionFacade, + val refGraph: NodeReferenceGraph) +{ + fun parseDocumentation(descriptor: DeclarationDescriptor, inline: Boolean = false): Content = + parseDocumentationAndDetails(descriptor, inline).first + + fun parseDocumentationAndDetails(descriptor: DeclarationDescriptor, inline: Boolean = false): Pair<Content, (DocumentationNode) -> Unit> { + if (descriptor is JavaClassDescriptor || descriptor is JavaCallableMemberDescriptor) { + return parseJavadoc(descriptor) + } + + val kdoc = KDocFinder.findKDoc(descriptor) ?: findStdlibKDoc(descriptor) + if (kdoc == null) { + if (options.reportUndocumented && !descriptor.isDeprecated() && + descriptor !is ValueParameterDescriptor && descriptor !is TypeParameterDescriptor && + descriptor !is PropertyAccessorDescriptor) { + logger.warn("No documentation for ${descriptor.signatureWithSourceLocation()}") + } + return Content.Empty to { node -> } + } + var kdocText = kdoc.getContent() + // workaround for code fence parsing problem in IJ markdown parser + if (kdocText.endsWith("```") || kdocText.endsWith("~~~")) { + kdocText += "\n" + } + val tree = parseMarkdown(kdocText) + val content = buildContent(tree, { href -> linkResolver.resolveContentLink(descriptor, href) }, inline) + if (kdoc is KDocSection) { + val tags = kdoc.getTags() + tags.forEach { + when (it.name) { + "sample" -> + content.append(functionBody(descriptor, it.getSubjectName())) + "see" -> + content.addTagToSeeAlso(descriptor, it) + else -> { + val section = content.addSection(javadocSectionDisplayName(it.name), it.getSubjectName()) + val sectionContent = it.getContent() + val markdownNode = parseMarkdown(sectionContent) + buildInlineContentTo(markdownNode, section, { href -> linkResolver.resolveContentLink(descriptor, href) }) + } + } + } + } + return content to { node -> } + } + + /** + * Special case for generating stdlib documentation (the Any class to which the override chain will resolve + * is not the same one as the Any class included in the source scope). + */ + fun findStdlibKDoc(descriptor: DeclarationDescriptor): KDocTag? { + if (descriptor !is CallableMemberDescriptor) { + return null + } + val name = descriptor.name.asString() + if (name == "equals" || name == "hashCode" || name == "toString") { + var deepestDescriptor: CallableMemberDescriptor = descriptor + while (!deepestDescriptor.overriddenDescriptors.isEmpty()) { + deepestDescriptor = deepestDescriptor.overriddenDescriptors.first() + } + if (DescriptorUtils.getFqName(deepestDescriptor.containingDeclaration).asString() == "kotlin.Any") { + val anyClassDescriptors = resolutionFacade.resolveSession.getTopLevelClassDescriptors( + FqName.fromSegments(listOf("kotlin", "Any")), NoLookupLocation.FROM_IDE) + anyClassDescriptors.forEach { + val anyMethod = it.getMemberScope(listOf()) + .getDescriptorsFiltered(DescriptorKindFilter.FUNCTIONS, { it == descriptor.name }) + .single() + val kdoc = KDocFinder.findKDoc(anyMethod) + if (kdoc != null) { + return kdoc + } + } + } + } + return null + } + + fun parseJavadoc(descriptor: DeclarationDescriptor): Pair<Content, (DocumentationNode) -> Unit> { + val psi = ((descriptor as? DeclarationDescriptorWithSource)?.source as? PsiSourceElement)?.psi + if (psi is PsiDocCommentOwner) { + val parseResult = JavadocParser(refGraph).parseDocumentation(psi as PsiNamedElement) + return parseResult.content to { node -> + parseResult.deprecatedContent?.let { + val deprecationNode = DocumentationNode("", it, DocumentationNode.Kind.Modifier) + node.append(deprecationNode, DocumentationReference.Kind.Deprecation) + } + } + } + return Content.Empty to { node -> } + } + + fun KDocSection.getTags(): Array<KDocTag> = PsiTreeUtil.getChildrenOfType(this, KDocTag::class.java) ?: arrayOf() + + private fun MutableContent.addTagToSeeAlso(descriptor: DeclarationDescriptor, seeTag: KDocTag) { + val subjectName = seeTag.getSubjectName() + if (subjectName != null) { + val seeSection = findSectionByTag("See Also") ?: addSection("See Also", null) + val link = linkResolver.resolveContentLink(descriptor, subjectName) + link.append(ContentText(subjectName)) + val para = ContentParagraph() + para.append(link) + seeSection.append(para) + } + } + + private fun functionBody(descriptor: DeclarationDescriptor, functionName: String?): ContentNode { + if (functionName == null) { + logger.warn("Missing function name in @sample in ${descriptor.signature()}") + return ContentBlockCode().let() { it.append(ContentText("Missing function name in @sample")); it } + } + val scope = getResolutionScope(resolutionFacade, descriptor) + val rootPackage = resolutionFacade.moduleDescriptor.getPackage(FqName.ROOT) + val rootScope = rootPackage.memberScope + val symbol = resolveInScope(functionName, scope) ?: resolveInScope(functionName, rootScope) + if (symbol == null) { + logger.warn("Unresolved function $functionName in @sample in ${descriptor.signature()}") + return ContentBlockCode().let() { it.append(ContentText("Unresolved: $functionName")); it } + } + val psiElement = DescriptorToSourceUtils.descriptorToDeclaration(symbol) + if (psiElement == null) { + logger.warn("Can't find source for function $functionName in @sample in ${descriptor.signature()}") + return ContentBlockCode().let() { it.append(ContentText("Source not found: $functionName")); it } + } + + val text = when (psiElement) { + is KtDeclarationWithBody -> ContentBlockCode().let() { + val bodyExpression = psiElement.bodyExpression + when (bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> psiElement.text + } + + val lines = text.trimEnd().split("\n".toRegex()).toTypedArray().filterNot { it.length == 0 } + val indent = lines.map { it.takeWhile { it.isWhitespace() }.count() }.min() ?: 0 + val finalText = lines.map { it.drop(indent) }.joinToString("\n") + return ContentBlockCode("kotlin").let() { it.append(ContentText(finalText)); it } + } + + private fun resolveInScope(functionName: String, scope: ResolutionScope): DeclarationDescriptor? { + var currentScope = scope + val parts = functionName.split('.') + + var symbol: DeclarationDescriptor? = null + + for (part in parts) { + // short name + val symbolName = Name.guess(part) + val partSymbol = currentScope.getContributedDescriptors(DescriptorKindFilter.ALL, { it == symbolName }) + .filter { it.name == symbolName } + .firstOrNull() + + if (partSymbol == null) { + symbol = null + break + } + currentScope = if (partSymbol is ClassDescriptor) + partSymbol.defaultType.memberScope + else + getResolutionScope(resolutionFacade, partSymbol) + symbol = partSymbol + } + + return symbol + } +} diff --git a/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt b/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt new file mode 100644 index 00000000..6551ded6 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt @@ -0,0 +1,653 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiJavaFile +import org.jetbrains.dokka.DocumentationNode.Kind +import org.jetbrains.dokka.Kotlin.DescriptorDocumentationParser +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.annotations.Annotated +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.idea.caches.resolve.getModuleInfo +import org.jetbrains.kotlin.idea.kdoc.KDocFinder +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtModifierListOwner +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.constants.CompileTimeConstant +import org.jetbrains.kotlin.resolve.constants.ConstantValue +import org.jetbrains.kotlin.resolve.constants.TypedCompileTimeConstant +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.isDocumentedAnnotation +import org.jetbrains.kotlin.resolve.jvm.JavaDescriptorResolver +import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform +import org.jetbrains.kotlin.resolve.source.PsiSourceElement +import org.jetbrains.kotlin.resolve.source.getPsi +import org.jetbrains.kotlin.types.ErrorUtils +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeProjection + +public data class DocumentationOptions(val outputDir: String, + val outputFormat: String, + val includeNonPublic: Boolean = false, + val reportUndocumented: Boolean = true, + val skipEmptyPackages: Boolean = true, + val skipDeprecated: Boolean = false, + val sourceLinks: List<SourceLinkDefinition>) + +private fun isSamePackage(descriptor1: DeclarationDescriptor, descriptor2: DeclarationDescriptor): Boolean { + val package1 = DescriptorUtils.getParentOfType(descriptor1, PackageFragmentDescriptor::class.java) + val package2 = DescriptorUtils.getParentOfType(descriptor2, PackageFragmentDescriptor::class.java) + return package1 != null && package2 != null && package1.fqName == package2.fqName +} + +interface PackageDocumentationBuilder { + fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>) +} + +class DocumentationBuilder + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val descriptorDocumentationParser: DescriptorDocumentationParser, + val options: DocumentationOptions, + val refGraph: NodeReferenceGraph, + val logger: DokkaLogger) +{ + val visibleToDocumentation = setOf(Visibilities.PROTECTED, Visibilities.PUBLIC) + val boringBuiltinClasses = setOf( + "kotlin.Unit", "kotlin.Byte", "kotlin.Short", "kotlin.Int", "kotlin.Long", "kotlin.Char", "kotlin.Boolean", + "kotlin.Float", "kotlin.Double", "kotlin.String", "kotlin.Array", "kotlin.Any") + val knownModifiers = setOf( + KtTokens.PUBLIC_KEYWORD, KtTokens.PROTECTED_KEYWORD, KtTokens.INTERNAL_KEYWORD, KtTokens.PRIVATE_KEYWORD, + KtTokens.OPEN_KEYWORD, KtTokens.FINAL_KEYWORD, KtTokens.ABSTRACT_KEYWORD, KtTokens.SEALED_KEYWORD, + KtTokens.OVERRIDE_KEYWORD) + + fun link(node: DocumentationNode, descriptor: DeclarationDescriptor, kind: DocumentationReference.Kind) { + refGraph.link(node, descriptor.signature(), kind) + } + + fun link(fromDescriptor: DeclarationDescriptor?, toDescriptor: DeclarationDescriptor?, kind: DocumentationReference.Kind) { + if (fromDescriptor != null && toDescriptor != null) { + refGraph.link(fromDescriptor.signature(), toDescriptor.signature(), kind) + } + } + + fun register(descriptor: DeclarationDescriptor, node: DocumentationNode) { + refGraph.register(descriptor.signature(), node) + } + + fun <T> nodeForDescriptor(descriptor: T, kind: Kind): DocumentationNode where T : DeclarationDescriptor, T : Named { + val (doc, callback) = descriptorDocumentationParser.parseDocumentationAndDetails(descriptor, kind == Kind.Parameter) + val node = DocumentationNode(descriptor.name.asString(), doc, kind).withModifiers(descriptor) + callback(node) + return node + } + + private fun DocumentationNode.withModifiers(descriptor: DeclarationDescriptor) : DocumentationNode{ + if (descriptor is MemberDescriptor) { + appendVisibility(descriptor) + if (descriptor !is ConstructorDescriptor) { + appendModality(descriptor) + } + } + return this + } + + fun DocumentationNode.appendModality(descriptor: MemberDescriptor) { + var modality = descriptor.modality + if (modality == Modality.OPEN) { + val containingClass = descriptor.containingDeclaration as? ClassDescriptor + if (containingClass?.modality == Modality.FINAL) { + modality = Modality.FINAL + } + } + val modifier = modality.name.toLowerCase() + appendTextNode(modifier, DocumentationNode.Kind.Modifier) + } + + fun DocumentationNode.appendVisibility(descriptor: DeclarationDescriptorWithVisibility) { + val modifier = descriptor.visibility.normalize().displayName + appendTextNode(modifier, DocumentationNode.Kind.Modifier) + } + + fun DocumentationNode.appendSupertypes(descriptor: ClassDescriptor) { + val superTypes = descriptor.typeConstructor.supertypes + for (superType in superTypes) { + if (!ignoreSupertype(superType)) { + appendType(superType, DocumentationNode.Kind.Supertype) + val superclass = superType?.constructor?.declarationDescriptor + link(superclass, descriptor, DocumentationReference.Kind.Inheritor) + link(descriptor, superclass, DocumentationReference.Kind.Superclass) + } + } + } + + private fun ignoreSupertype(superType: KotlinType): Boolean { + val superClass = superType.constructor.declarationDescriptor as? ClassDescriptor + if (superClass != null) { + val fqName = DescriptorUtils.getFqNameSafe(superClass).asString() + return fqName == "kotlin.Annotation" || fqName == "kotlin.Enum" || fqName == "kotlin.Any" + } + return false + } + + fun DocumentationNode.appendProjection(projection: TypeProjection, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type) { + if (projection.isStarProjection) { + appendTextNode("*", Kind.Type) + } + else { + appendType(projection.type, kind, projection.projectionKind.label) + } + } + + fun DocumentationNode.appendType(kotlinType: KotlinType?, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type, prefix: String = "") { + if (kotlinType == null) + return + val classifierDescriptor = kotlinType.constructor.declarationDescriptor + val name = when (classifierDescriptor) { + is ClassDescriptor -> { + if (classifierDescriptor.isCompanionObject) { + classifierDescriptor.containingDeclaration.name.asString() + + "." + classifierDescriptor.name.asString() + } + else { + classifierDescriptor.name.asString() + } + } + is Named -> classifierDescriptor.name.asString() + else -> "<anonymous>" + } + val node = DocumentationNode(name, Content.Empty, kind) + if (prefix != "") { + node.appendTextNode(prefix, Kind.Modifier) + } + if (kotlinType.isMarkedNullable) { + node.appendTextNode("?", Kind.NullabilityModifier) + } + if (classifierDescriptor != null) { + link(node, classifierDescriptor, + if (classifierDescriptor.isBoringBuiltinClass()) DocumentationReference.Kind.HiddenLink else DocumentationReference.Kind.Link) + } + + append(node, DocumentationReference.Kind.Detail) + node.appendAnnotations(kotlinType) + for (typeArgument in kotlinType.arguments) { + node.appendProjection(typeArgument) + } + } + + fun ClassifierDescriptor.isBoringBuiltinClass(): Boolean = + DescriptorUtils.getFqName(this).asString() in boringBuiltinClasses + + fun DocumentationNode.appendAnnotations(annotated: Annotated) { + annotated.annotations.filter { it.isDocumented() }.forEach { + val annotationNode = it.build() + if (annotationNode != null) { + append(annotationNode, + if (annotationNode.isDeprecation()) DocumentationReference.Kind.Deprecation else DocumentationReference.Kind.Annotation) + } + } + } + + fun DocumentationNode.appendModifiers(descriptor: DeclarationDescriptor) { + val psi = (descriptor as DeclarationDescriptorWithSource).source.getPsi() as? KtModifierListOwner ?: return + KtTokens.MODIFIER_KEYWORDS_ARRAY.filter { it !in knownModifiers }.forEach { + if (psi.hasModifier(it)) { + appendTextNode(it.value, Kind.Modifier) + } + } + } + + fun DocumentationNode.isDeprecation() = name == "Deprecated" || name == "deprecated" + + fun DocumentationNode.appendSourceLink(sourceElement: SourceElement) { + appendSourceLink(sourceElement.getPsi(), options.sourceLinks) + } + + fun DocumentationNode.appendChild(descriptor: DeclarationDescriptor, kind: DocumentationReference.Kind): DocumentationNode? { + // do not include generated code + if (descriptor is CallableMemberDescriptor && descriptor.kind != CallableMemberDescriptor.Kind.DECLARATION) + return null + + if (descriptor.isDocumented()) { + val node = descriptor.build() + append(node, kind) + return node + } + return null + } + + fun DeclarationDescriptor.isDocumented(): Boolean { + return (options.includeNonPublic + || this !is MemberDescriptor + || this.visibility in visibleToDocumentation) && + !isDocumentationSuppressed() && + (!options.skipDeprecated || !isDeprecated()) + } + + fun DocumentationNode.appendMembers(descriptors: Iterable<DeclarationDescriptor>): List<DocumentationNode> { + val nodes = descriptors.map { descriptor -> + if (descriptor is CallableMemberDescriptor && descriptor.kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE) { + val baseDescriptor = descriptor.overriddenDescriptors.firstOrNull() + if (baseDescriptor != null) { + link(this, baseDescriptor, DocumentationReference.Kind.InheritedMember) + } + null + } + else { + val descriptorToUse = if (descriptor is ConstructorDescriptor) descriptor else descriptor.original + appendChild(descriptorToUse, DocumentationReference.Kind.Member) + } + } + return nodes.filterNotNull() + } + + fun DocumentationNode.appendInPageChildren(descriptors: Iterable<DeclarationDescriptor>, kind: DocumentationReference.Kind) { + descriptors.forEach { descriptor -> + val node = appendChild(descriptor, kind) + node?.addReferenceTo(this, DocumentationReference.Kind.TopLevelPage) + } + } + + fun DocumentationModule.appendFragments(fragments: Collection<PackageFragmentDescriptor>, + packageContent: Map<String, Content>, + packageDocumentationBuilder: PackageDocumentationBuilder) { + val allFqNames = fragments.map { it.fqName }.distinct() + + for (packageName in allFqNames) { + val declarations = fragments.filter { it.fqName == packageName }.flatMap { it.getMemberScope().getContributedDescriptors() } + + if (options.skipEmptyPackages && declarations.none { it.isDocumented() }) continue + logger.info(" package $packageName: ${declarations.count()} declarations") + val packageNode = findOrCreatePackageNode(packageName.asString(), packageContent) + packageDocumentationBuilder.buildPackageDocumentation(this@DocumentationBuilder, packageName, packageNode, declarations) + } + } + + fun DeclarationDescriptor.build(): DocumentationNode = when (this) { + is ClassDescriptor -> build() + is ConstructorDescriptor -> build() + is PropertyDescriptor -> build() + is FunctionDescriptor -> build() + is TypeParameterDescriptor -> build() + is ValueParameterDescriptor -> build() + is ReceiverParameterDescriptor -> build() + else -> throw IllegalStateException("Descriptor $this is not known") + } + + fun ClassDescriptor.build(): DocumentationNode { + val kind = when (kind) { + ClassKind.OBJECT -> Kind.Object + ClassKind.INTERFACE -> Kind.Interface + ClassKind.ENUM_CLASS -> Kind.Enum + ClassKind.ANNOTATION_CLASS -> Kind.AnnotationClass + ClassKind.ENUM_ENTRY -> Kind.EnumItem + else -> Kind.Class + } + val node = nodeForDescriptor(this, kind) + node.appendSupertypes(this) + if (getKind() != ClassKind.OBJECT && getKind() != ClassKind.ENUM_ENTRY) { + node.appendInPageChildren(typeConstructor.parameters, DocumentationReference.Kind.Detail) + val constructorsToDocument = if (getKind() == ClassKind.ENUM_CLASS) + constructors.filter { it.valueParameters.size > 0 } + else + constructors + node.appendMembers(constructorsToDocument) + } + val members = defaultType.memberScope.getContributedDescriptors().filter { it != companionObjectDescriptor } + node.appendMembers(members) + node.appendMembers(staticScope.getContributedDescriptors()).forEach { + it.appendTextNode("static", Kind.Modifier) + } + val companionObjectDescriptor = companionObjectDescriptor + if (companionObjectDescriptor != null) { + node.appendMembers(companionObjectDescriptor.defaultType.memberScope.getContributedDescriptors()) + } + node.appendAnnotations(this) + node.appendModifiers(this) + node.appendSourceLink(source) + register(this, node) + return node + } + + fun ConstructorDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, Kind.Constructor) + node.appendInPageChildren(valueParameters, DocumentationReference.Kind.Detail) + register(this, node) + return node + } + + private fun CallableMemberDescriptor.inCompanionObject(): Boolean { + val containingDeclaration = containingDeclaration + if ((containingDeclaration as? ClassDescriptor)?.isCompanionObject ?: false) { + return true + } + val receiver = extensionReceiverParameter + return (receiver?.type?.constructor?.declarationDescriptor as? ClassDescriptor)?.isCompanionObject ?: false + } + + fun FunctionDescriptor.build(): DocumentationNode { + if (ErrorUtils.containsErrorType(this)) { + logger.warn("Found an unresolved type in ${signatureWithSourceLocation()}") + } + + val node = nodeForDescriptor(this, if (inCompanionObject()) Kind.CompanionObjectFunction else Kind.Function) + + node.appendInPageChildren(typeParameters, DocumentationReference.Kind.Detail) + extensionReceiverParameter?.let { node.appendChild(it, DocumentationReference.Kind.Detail) } + node.appendInPageChildren(valueParameters, DocumentationReference.Kind.Detail) + node.appendType(returnType) + node.appendAnnotations(this) + node.appendModifiers(this) + node.appendSourceLink(source) + + overriddenDescriptors.forEach { + addOverrideLink(it, this) + } + + register(this, node) + return node + } + + fun addOverrideLink(baseClassFunction: CallableMemberDescriptor, overridingFunction: CallableMemberDescriptor) { + val source = baseClassFunction.original.source.getPsi() + if (source != null) { + link(overridingFunction, baseClassFunction, DocumentationReference.Kind.Override) + } else { + baseClassFunction.overriddenDescriptors.forEach { + addOverrideLink(it, overridingFunction) + } + } + } + + fun PropertyDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, if (inCompanionObject()) Kind.CompanionObjectProperty else Kind.Property) + node.appendInPageChildren(typeParameters, DocumentationReference.Kind.Detail) + extensionReceiverParameter?.let { node.appendChild(it, DocumentationReference.Kind.Detail) } + node.appendType(returnType) + node.appendAnnotations(this) + node.appendModifiers(this) + node.appendSourceLink(source) + if (isVar) { + node.appendTextNode("var", DocumentationNode.Kind.Modifier) + } + getter?.let { + if (!it.isDefault) { + node.addAccessorDocumentation(descriptorDocumentationParser.parseDocumentation(it), "Getter") + } + } + setter?.let { + if (!it.isDefault) { + node.addAccessorDocumentation(descriptorDocumentationParser.parseDocumentation(it), "Setter") + } + } + + overriddenDescriptors.forEach { + addOverrideLink(it, this) + } + + register(this, node) + return node + } + + fun DocumentationNode.addAccessorDocumentation(documentation: Content, prefix: String) { + if (documentation == Content.Empty) return + updateContent { + if (!documentation.children.isEmpty()) { + val section = addSection(prefix, null) + documentation.children.forEach { section.append(it) } + } + documentation.sections.forEach { + val section = addSection("$prefix ${it.tag}", it.subjectName) + it.children.forEach { section.append(it) } + } + } + } + + fun ValueParameterDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, Kind.Parameter) + node.appendType(varargElementType ?: type) + if (declaresDefaultValue()) { + val psi = source.getPsi() as? KtParameter + if (psi != null) { + val defaultValueText = psi.defaultValue?.text + if (defaultValueText != null) { + node.appendTextNode(defaultValueText, Kind.Value) + } + } + } + node.appendAnnotations(this) + node.appendModifiers(this) + if (varargElementType != null && node.details(Kind.Modifier).none { it.name == "vararg" }) { + node.appendTextNode("vararg", Kind.Modifier) + } + register(this, node) + return node + } + + fun TypeParameterDescriptor.build(): DocumentationNode { + val doc = descriptorDocumentationParser.parseDocumentation(this) + val name = name.asString() + val prefix = variance.label + + val node = DocumentationNode(name, doc, DocumentationNode.Kind.TypeParameter) + if (prefix != "") { + node.appendTextNode(prefix, Kind.Modifier) + } + if (isReified) { + node.appendTextNode("reified", Kind.Modifier) + } + + for (constraint in upperBounds) { + if (KotlinBuiltIns.isDefaultBound(constraint)) { + continue + } + node.appendType(constraint, Kind.UpperBound) + } + + for (constraint in lowerBounds) { + if (KotlinBuiltIns.isNothing(constraint)) + continue + node.appendType(constraint, Kind.LowerBound) + } + return node + } + + fun ReceiverParameterDescriptor.build(): DocumentationNode { + var receiverClass: DeclarationDescriptor = type.constructor.declarationDescriptor!! + if ((receiverClass as? ClassDescriptor)?.isCompanionObject ?: false) { + receiverClass = receiverClass.containingDeclaration!! + } + link(receiverClass, + containingDeclaration, + DocumentationReference.Kind.Extension) + + val node = DocumentationNode(name.asString(), Content.Empty, Kind.Receiver) + node.appendType(type) + return node + } + + fun AnnotationDescriptor.build(): DocumentationNode? { + val annotationClass = type.constructor.declarationDescriptor + if (annotationClass == null || ErrorUtils.isError(annotationClass)) { + return null + } + val node = DocumentationNode(annotationClass.name.asString(), Content.Empty, DocumentationNode.Kind.Annotation) + val arguments = allValueArguments.toList().sortedBy { it.first.index } + arguments.forEach { + val valueNode = it.second.toDocumentationNode() + if (valueNode != null) { + val paramNode = DocumentationNode(it.first.name.asString(), Content.Empty, DocumentationNode.Kind.Parameter) + paramNode.append(valueNode, DocumentationReference.Kind.Detail) + node.append(paramNode, DocumentationReference.Kind.Detail) + } + } + return node + } + + fun CompileTimeConstant<Any?>.build(): DocumentationNode? = when (this) { + is TypedCompileTimeConstant -> constantValue.toDocumentationNode() + else -> null + } + + fun ConstantValue<*>.toDocumentationNode(): DocumentationNode? = value?.let { value -> + when (value) { + is String -> + "\"" + StringUtil.escapeStringCharacters(value) + "\"" + is EnumEntrySyntheticClassDescriptor -> + value.containingDeclaration.name.asString() + "." + value.name.asString() + else -> value.toString() + }.let { valueString -> + DocumentationNode(valueString, Content.Empty, DocumentationNode.Kind.Value) + } + } +} + +class KotlinPackageDocumentationBuilder : PackageDocumentationBuilder { + override fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>) { + val externalClassNodes = hashMapOf<FqName, DocumentationNode>() + declarations.forEach { descriptor -> + with(documentationBuilder) { + if (descriptor.isDocumented()) { + val parent = packageNode.getParentForPackageMember(descriptor, externalClassNodes) + parent.appendChild(descriptor, DocumentationReference.Kind.Member) + } + } + } + } +} + +class KotlinJavaDocumentationBuilder + @Inject constructor(val documentationBuilder: DocumentationBuilder, + val logger: DokkaLogger) : JavaDocumentationBuilder +{ + override fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) { + val packageNode = module.findOrCreatePackageNode(file.packageName, packageContent) + + file.classes.forEach { + val javaDescriptorResolver = KotlinCacheService.getInstance(file.project).getProjectService(JvmPlatform, + it.getModuleInfo(), JavaDescriptorResolver::class.java) + + val descriptor = javaDescriptorResolver.resolveClass(JavaClassImpl(it)) + if (descriptor == null) { + logger.warn("Cannot find descriptor for Java class ${it.qualifiedName}") + } + else { + with(documentationBuilder) { + packageNode.appendChild(descriptor, DocumentationReference.Kind.Member) + } + } + } + } +} + +private fun AnnotationDescriptor.isDocumented(): Boolean { + if (source.getPsi() != null && mustBeDocumented()) return true + val annotationClassName = type.constructor.declarationDescriptor?.fqNameSafe?.asString() + return annotationClassName == "kotlin.Extension" +} + +fun AnnotationDescriptor.mustBeDocumented(): Boolean { + val annotationClass = type.constructor.declarationDescriptor as? Annotated ?: return false + return annotationClass.isDocumentedAnnotation() +} + +fun DeclarationDescriptor.isDocumentationSuppressed(): Boolean { + val doc = KDocFinder.findKDoc(this) + return doc is KDocSection && doc.findTagByName("suppress") != null +} + +fun DeclarationDescriptor.isDeprecated(): Boolean = annotations.any { + DescriptorUtils.getFqName(it.type.constructor.declarationDescriptor!!).asString() == "kotlin.Deprecated" +} || (this is ConstructorDescriptor && containingDeclaration.isDeprecated()) + +fun DocumentationNode.getParentForPackageMember(descriptor: DeclarationDescriptor, + externalClassNodes: MutableMap<FqName, DocumentationNode>): DocumentationNode { + if (descriptor is CallableMemberDescriptor) { + val extensionClassDescriptor = descriptor.getExtensionClassDescriptor() + if (extensionClassDescriptor != null && !isSamePackage(descriptor, extensionClassDescriptor) && + !ErrorUtils.isError(extensionClassDescriptor)) { + val fqName = DescriptorUtils.getFqNameSafe(extensionClassDescriptor) + return externalClassNodes.getOrPut(fqName, { + val newNode = DocumentationNode(fqName.asString(), Content.Empty, Kind.ExternalClass) + append(newNode, DocumentationReference.Kind.Member) + newNode + }) + } + } + return this +} + +fun CallableMemberDescriptor.getExtensionClassDescriptor(): ClassifierDescriptor? { + val extensionReceiver = extensionReceiverParameter + if (extensionReceiver != null) { + val type = extensionReceiver.type + return type.constructor.declarationDescriptor as? ClassDescriptor + } + return null +} + +fun DeclarationDescriptor.signature(): String = when(this) { + is ClassDescriptor, is PackageFragmentDescriptor -> DescriptorUtils.getFqName(this).asString() + is PropertyDescriptor -> containingDeclaration.signature() + "#" + name + receiverSignature() + is FunctionDescriptor -> containingDeclaration.signature() + "#" + name + parameterSignature() + is ValueParameterDescriptor -> containingDeclaration.signature() + ":" + name + is TypeParameterDescriptor -> containingDeclaration.signature() + "<" + name + + else -> throw UnsupportedOperationException("Don't know how to calculate signature for $this") +} + +fun PropertyDescriptor.receiverSignature(): String { + val receiver = extensionReceiverParameter + if (receiver != null) { + return "#" + receiver.type.signature() + } + return "" +} + +fun CallableMemberDescriptor.parameterSignature(): String { + val params = valueParameters.map { it.type }.toArrayList() + val extensionReceiver = extensionReceiverParameter + if (extensionReceiver != null) { + params.add(0, extensionReceiver.type) + } + return "(" + params.map { it.signature() }.joinToString() + ")" +} + +fun KotlinType.signature(): String { + val declarationDescriptor = constructor.declarationDescriptor ?: return "<null>" + val typeName = DescriptorUtils.getFqName(declarationDescriptor).asString() + if (typeName == "Array" && arguments.size == 1) { + return "Array<" + arguments.first().type.signature() + ">" + } + return typeName +} + +fun DeclarationDescriptor.signatureWithSourceLocation(): String { + val signature = signature() + val sourceLocation = sourceLocation() + return if (sourceLocation != null) "$signature ($sourceLocation)" else signature +} + +fun DeclarationDescriptor.sourceLocation(): String? { + if (this is DeclarationDescriptorWithSource) { + val psi = (this.source as? PsiSourceElement)?.getPsi() + if (psi != null) { + val fileName = psi.containingFile.name + val lineNumber = psi.lineNumber() + return if (lineNumber != null) "$fileName:$lineNumber" else fileName + } + } + return null +} diff --git a/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt b/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt new file mode 100644 index 00000000..7a1d591c --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt @@ -0,0 +1,64 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.Kotlin.DescriptorDocumentationParser +import org.jetbrains.kotlin.asJava.KtLightElement +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtPropertyAccessor + +class KotlinAsJavaDocumentationBuilder + @Inject constructor(val kotlinAsJavaDocumentationParser: KotlinAsJavaDocumentationParser) : PackageDocumentationBuilder +{ + override fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>) { + val project = documentationBuilder.resolutionFacade.project + val psiPackage = JavaPsiFacade.getInstance(project).findPackage(packageName.asString()) + if (psiPackage == null) { + documentationBuilder.logger.error("Cannot find Java package by qualified name: ${packageName.asString()}") + return + } + + val javaDocumentationBuilder = JavaPsiDocumentationBuilder(documentationBuilder.options, + documentationBuilder.refGraph, + kotlinAsJavaDocumentationParser) + + psiPackage.classes.filter { it is KtLightElement<*, *> }.filter { it.isVisibleInDocumentation() }.forEach { + javaDocumentationBuilder.appendClasses(packageNode, arrayOf(it)) + } + } + + fun PsiClass.isVisibleInDocumentation() : Boolean { + val origin: KtDeclaration? = (this as KtLightElement<*, *>).getOrigin() + return origin?.hasModifier(KtTokens.INTERNAL_KEYWORD) != true && + origin?.hasModifier(KtTokens.PRIVATE_KEYWORD) != true + } +} + +class KotlinAsJavaDocumentationParser + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val descriptorDocumentationParser: DescriptorDocumentationParser) : JavaDocumentationParser +{ + override fun parseDocumentation(element: PsiNamedElement): JavadocParseResult { + val kotlinLightElement = element as? KtLightElement<*, *> ?: return JavadocParseResult.Empty + val origin = kotlinLightElement.getOrigin() ?: return JavadocParseResult.Empty + if (origin is KtParameter) { + // LazyDeclarationResolver does not support setter parameters + val grandFather = origin.parent?.parent + if (grandFather is KtPropertyAccessor) { + return JavadocParseResult.Empty + } + } + val descriptor = resolutionFacade.resolveToDescriptor(origin) + val content = descriptorDocumentationParser.parseDocumentation(descriptor, origin is KtParameter) + return JavadocParseResult(content, null) + } +} diff --git a/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt b/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt new file mode 100644 index 00000000..0d39f410 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt @@ -0,0 +1,409 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.LanguageService.RenderMode + +/** + * Implements [LanguageService] and provides rendering of symbols in Kotlin language + */ +class KotlinLanguageService : LanguageService { + private val fullOnlyModifiers = setOf("public", "protected", "private", "inline", "noinline", "crossinline", "reified") + + override fun render(node: DocumentationNode, renderMode: RenderMode): ContentNode { + return content { + when (node.kind) { + DocumentationNode.Kind.Package -> if (renderMode == RenderMode.FULL) renderPackage(node) + in DocumentationNode.Kind.classLike -> renderClass(node, renderMode) + + DocumentationNode.Kind.EnumItem, + DocumentationNode.Kind.ExternalClass -> if (renderMode == RenderMode.FULL) identifier(node.name) + + DocumentationNode.Kind.TypeParameter -> renderTypeParameter(node, renderMode) + DocumentationNode.Kind.Type, + DocumentationNode.Kind.UpperBound -> renderType(node, renderMode) + + DocumentationNode.Kind.Modifier -> renderModifier(node) + DocumentationNode.Kind.Constructor, + DocumentationNode.Kind.Function, + DocumentationNode.Kind.CompanionObjectFunction -> renderFunction(node, renderMode) + DocumentationNode.Kind.Property, + DocumentationNode.Kind.CompanionObjectProperty -> renderProperty(node, renderMode) + else -> identifier(node.name) + } + } + } + + override fun renderName(node: DocumentationNode): String { + return when (node.kind) { + DocumentationNode.Kind.Constructor -> node.owner!!.name + else -> node.name + } + } + + override fun summarizeSignatures(nodes: List<DocumentationNode>): ContentNode? { + if (nodes.size < 2) return null + val receiverKind = nodes.getReceiverKind() ?: return null + val functionWithTypeParameter = nodes.firstOrNull { it.details(DocumentationNode.Kind.TypeParameter).any() } ?: return null + return content { + val typeParameter = functionWithTypeParameter.details(DocumentationNode.Kind.TypeParameter).first() + if (functionWithTypeParameter.kind == DocumentationNode.Kind.Function) { + renderFunction(functionWithTypeParameter, RenderMode.SUMMARY, SummarizingMapper(receiverKind, typeParameter.name)) + } + else { + renderProperty(functionWithTypeParameter, RenderMode.SUMMARY, SummarizingMapper(receiverKind, typeParameter.name)) + } + } + } + + private fun List<DocumentationNode>.getReceiverKind(): ReceiverKind? { + val qNames = map { it.getReceiverQName() }.filterNotNull() + if (qNames.size != size) + return null + + return ReceiverKind.values.firstOrNull { kind -> qNames.all { it in kind.classes } } + } + + private fun DocumentationNode.getReceiverQName(): String? { + if (kind != DocumentationNode.Kind.Function && kind != DocumentationNode.Kind.Property) return null + val receiver = details(DocumentationNode.Kind.Receiver).singleOrNull() ?: return null + return receiver.detail(DocumentationNode.Kind.Type).qualifiedNameFromType() + } + + companion object { + private val arrayClasses = setOf( + "kotlin.Array", + "kotlin.BooleanArray", + "kotlin.ByteArray", + "kotlin.CharArray", + "kotlin.ShortArray", + "kotlin.IntArray", + "kotlin.LongArray", + "kotlin.FloatArray", + "kotlin.DoubleArray" + ) + + private val arrayOrListClasses = setOf("kotlin.List") + arrayClasses + + private val iterableClasses = setOf( + "kotlin.Collection", + "kotlin.Sequence", + "kotlin.Iterable", + "kotlin.Map", + "kotlin.String", + "kotlin.CharSequence") + arrayOrListClasses + } + + private enum class ReceiverKind(val receiverName: String, val classes: Collection<String>) { + ARRAY("any_array", arrayClasses), + ARRAY_OR_LIST("any_array_or_list", arrayOrListClasses), + ITERABLE("any_iterable", iterableClasses), + } + + interface SignatureMapper { + fun renderReceiver(receiver: DocumentationNode, to: ContentBlock) + } + + private class SummarizingMapper(val kind: ReceiverKind, val typeParameterName: String): SignatureMapper { + override fun renderReceiver(receiver: DocumentationNode, to: ContentBlock) { + to.append(ContentIdentifier(kind.receiverName, IdentifierKind.SummarizedTypeName)) + to.text("<$typeParameterName>") + } + } + + private fun ContentBlock.renderPackage(node: DocumentationNode) { + keyword("package") + text(" ") + identifier(node.name) + } + + private fun ContentBlock.renderList(nodes: List<DocumentationNode>, separator: String = ", ", + noWrap: Boolean = false, renderItem: (DocumentationNode) -> Unit) { + if (nodes.none()) + return + renderItem(nodes.first()) + nodes.drop(1).forEach { + if (noWrap) { + symbol(separator.removeSuffix(" ")) + nbsp() + } else { + symbol(separator) + } + renderItem(it) + } + } + + private fun ContentBlock.renderLinked(node: DocumentationNode, body: ContentBlock.(DocumentationNode)->Unit) { + val to = node.links.firstOrNull() + if (to == null) + body(node) + else + link(to) { + body(node) + } + } + + private fun ContentBlock.renderType(node: DocumentationNode, renderMode: RenderMode) { + var typeArguments = node.details(DocumentationNode.Kind.Type) + if (node.name == "Function${typeArguments.count() - 1}") { + // lambda + val isExtension = node.annotations.any { it.name == "Extension" } + if (isExtension) { + renderType(typeArguments.first(), renderMode) + symbol(".") + typeArguments = typeArguments.drop(1) + } + symbol("(") + renderList(typeArguments.take(typeArguments.size - 1), noWrap = true) { + renderType(it, renderMode) + } + symbol(")") + nbsp() + symbol("->") + nbsp() + renderType(typeArguments.last(), renderMode) + return + } + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode, true) + renderLinked(node) { identifier(it.name, IdentifierKind.TypeName) } + if (typeArguments.any()) { + symbol("<") + renderList(typeArguments, noWrap = true) { + renderType(it, renderMode) + } + symbol(">") + } + val nullabilityModifier = node.details(DocumentationNode.Kind.NullabilityModifier).singleOrNull() + if (nullabilityModifier != null) { + symbol(nullabilityModifier.name) + } + } + + private fun ContentBlock.renderModifier(node: DocumentationNode, nowrap: Boolean = false) { + when (node.name) { + "final", "public", "var" -> {} + else -> { + keyword(node.name) + if (nowrap) { + nbsp() + } + else { + text(" ") + } + } + } + } + + private fun ContentBlock.renderTypeParameter(node: DocumentationNode, renderMode: RenderMode) { + renderModifiersForNode(node, renderMode, true) + + identifier(node.name) + + val constraints = node.details(DocumentationNode.Kind.UpperBound) + if (constraints.any()) { + nbsp() + symbol(":") + nbsp() + renderList(constraints, noWrap=true) { + renderType(it, renderMode) + } + } + } + private fun ContentBlock.renderParameter(node: DocumentationNode, renderMode: RenderMode) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + identifier(node.name, IdentifierKind.ParameterName) + symbol(":") + nbsp() + val parameterType = node.detail(DocumentationNode.Kind.Type) + renderType(parameterType, renderMode) + val valueNode = node.details(DocumentationNode.Kind.Value).firstOrNull() + if (valueNode != null) { + nbsp() + symbol("=") + nbsp() + text(valueNode.name) + } + } + + private fun ContentBlock.renderTypeParametersForNode(node: DocumentationNode, renderMode: RenderMode) { + val typeParameters = node.details(DocumentationNode.Kind.TypeParameter) + if (typeParameters.any()) { + symbol("<") + renderList(typeParameters) { + renderTypeParameter(it, renderMode) + } + symbol(">") + } + } + + private fun ContentBlock.renderSupertypesForNode(node: DocumentationNode, renderMode: RenderMode) { + val supertypes = node.details(DocumentationNode.Kind.Supertype) + if (supertypes.any()) { + nbsp() + symbol(":") + nbsp() + renderList(supertypes) { + indentedSoftLineBreak() + renderType(it, renderMode) + } + } + } + + private fun ContentBlock.renderModifiersForNode(node: DocumentationNode, + renderMode: RenderMode, + nowrap: Boolean = false) { + val modifiers = node.details(DocumentationNode.Kind.Modifier) + for (it in modifiers) { + if (node.kind == org.jetbrains.dokka.DocumentationNode.Kind.Interface && it.name == "abstract") + continue + if (renderMode == RenderMode.SUMMARY && it.name in fullOnlyModifiers) { + continue + } + renderModifier(it, nowrap) + } + } + + private fun ContentBlock.renderAnnotationsForNode(node: DocumentationNode) { + node.annotations.forEach { + renderAnnotation(it) + } + } + + private fun ContentBlock.renderAnnotation(node: DocumentationNode) { + identifier("@" + node.name, IdentifierKind.AnnotationName) + val parameters = node.details(DocumentationNode.Kind.Parameter) + if (!parameters.isEmpty()) { + symbol("(") + renderList(parameters) { + text(it.detail(DocumentationNode.Kind.Value).name) + } + symbol(")") + } + text(" ") + } + + private fun ContentBlock.renderClass(node: DocumentationNode, renderMode: RenderMode) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + DocumentationNode.Kind.Class, + DocumentationNode.Kind.AnnotationClass, + DocumentationNode.Kind.Enum -> keyword("class ") + DocumentationNode.Kind.Interface -> keyword("interface ") + DocumentationNode.Kind.EnumItem -> keyword("enum val ") + DocumentationNode.Kind.Object -> keyword("object ") + else -> throw IllegalArgumentException("Node $node is not a class-like object") + } + + identifierOrDeprecated(node) + renderTypeParametersForNode(node, renderMode) + renderSupertypesForNode(node, renderMode) + } + + private fun ContentBlock.renderFunction(node: DocumentationNode, + renderMode: RenderMode, + signatureMapper: SignatureMapper? = null) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + DocumentationNode.Kind.Constructor -> identifier(node.owner!!.name) + DocumentationNode.Kind.Function, + DocumentationNode.Kind.CompanionObjectFunction -> keyword("fun ") + else -> throw IllegalArgumentException("Node $node is not a function-like object") + } + renderTypeParametersForNode(node, renderMode) + if (node.details(DocumentationNode.Kind.TypeParameter).any()) { + text(" ") + } + + renderReceiver(node, renderMode, signatureMapper) + + if (node.kind != org.jetbrains.dokka.DocumentationNode.Kind.Constructor) + identifierOrDeprecated(node) + + symbol("(") + val parameters = node.details(DocumentationNode.Kind.Parameter) + renderList(parameters) { + indentedSoftLineBreak() + renderParameter(it, renderMode) + } + if (needReturnType(node)) { + if (parameters.isNotEmpty()) { + softLineBreak() + } + symbol(")") + symbol(": ") + renderType(node.detail(DocumentationNode.Kind.Type), renderMode) + } + else { + symbol(")") + } + } + + private fun ContentBlock.renderReceiver(node: DocumentationNode, renderMode: RenderMode, signatureMapper: SignatureMapper?) { + val receiver = node.details(DocumentationNode.Kind.Receiver).singleOrNull() + if (receiver != null) { + if (signatureMapper != null) { + signatureMapper.renderReceiver(receiver, this) + } else { + renderType(receiver.detail(DocumentationNode.Kind.Type), renderMode) + } + symbol(".") + } + } + + private fun needReturnType(node: DocumentationNode) = when(node.kind) { + DocumentationNode.Kind.Constructor -> false + else -> !node.isUnitReturnType() + } + + fun DocumentationNode.isUnitReturnType(): Boolean = + detail(DocumentationNode.Kind.Type).hiddenLinks.firstOrNull()?.qualifiedName() == "kotlin.Unit" + + private fun ContentBlock.renderProperty(node: DocumentationNode, + renderMode: RenderMode, + signatureMapper: SignatureMapper? = null) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + DocumentationNode.Kind.Property, + DocumentationNode.Kind.CompanionObjectProperty -> keyword("${node.getPropertyKeyword()} ") + else -> throw IllegalArgumentException("Node $node is not a property") + } + renderTypeParametersForNode(node, renderMode) + if (node.details(DocumentationNode.Kind.TypeParameter).any()) { + text(" ") + } + + renderReceiver(node, renderMode, signatureMapper) + + identifierOrDeprecated(node) + symbol(": ") + renderType(node.detail(DocumentationNode.Kind.Type), renderMode) + } + + fun DocumentationNode.getPropertyKeyword() = + if (details(DocumentationNode.Kind.Modifier).any { it.name == "var" }) "var" else "val" + + fun ContentBlock.identifierOrDeprecated(node: DocumentationNode) { + if (node.deprecation != null) { + val strike = ContentStrikethrough() + strike.identifier(node.name) + append(strike) + } else { + identifier(node.name) + } + } +} + +fun DocumentationNode.qualifiedNameFromType() = (links.firstOrNull() ?: hiddenLinks.firstOrNull())?.qualifiedName() ?: name diff --git a/core/src/main/kotlin/Languages/JavaLanguageService.kt b/core/src/main/kotlin/Languages/JavaLanguageService.kt new file mode 100644 index 00000000..7e40beff --- /dev/null +++ b/core/src/main/kotlin/Languages/JavaLanguageService.kt @@ -0,0 +1,162 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.DocumentationNode.Kind +import org.jetbrains.dokka.LanguageService.RenderMode + +/** + * Implements [LanguageService] and provides rendering of symbols in Java language + */ +public class JavaLanguageService : LanguageService { + override fun render(node: DocumentationNode, renderMode: RenderMode): ContentNode { + return ContentText(when (node.kind) { + Kind.Package -> renderPackage(node) + in Kind.classLike -> renderClass(node) + + Kind.TypeParameter -> renderTypeParameter(node) + Kind.Type, + Kind.UpperBound -> renderType(node) + + Kind.Constructor, + Kind.Function -> renderFunction(node) + Kind.Property -> renderProperty(node) + else -> "${node.kind}: ${node.name}" + }) + } + + override fun renderName(node: DocumentationNode): String { + return when (node.kind) { + Kind.Constructor -> node.owner!!.name + else -> node.name + } + } + + override fun summarizeSignatures(nodes: List<DocumentationNode>): ContentNode? = null + + private fun renderPackage(node: DocumentationNode): String { + return "package ${node.name}" + } + + private fun renderModifier(node: DocumentationNode): String { + return when (node.name) { + "open" -> "" + "internal" -> "" + else -> node.name + } + } + + public fun getArrayElementType(node: DocumentationNode): DocumentationNode? = when (node.name) { + "Array" -> node.details(Kind.Type).singleOrNull()?.let { et -> getArrayElementType(et) ?: et } ?: DocumentationNode("Object", node.content, DocumentationNode.Kind.ExternalClass) + "IntArray", "LongArray", "ShortArray", "ByteArray", "CharArray", "DoubleArray", "FloatArray", "BooleanArray" -> DocumentationNode(node.name.removeSuffix("Array").toLowerCase(), node.content, DocumentationNode.Kind.Type) + else -> null + } + + public fun getArrayDimension(node: DocumentationNode): Int = when (node.name) { + "Array" -> 1 + (node.details(DocumentationNode.Kind.Type).singleOrNull()?.let { getArrayDimension(it) } ?: 0) + "IntArray", "LongArray", "ShortArray", "ByteArray", "CharArray", "DoubleArray", "FloatArray", "BooleanArray" -> 1 + else -> 0 + } + + public fun renderType(node: DocumentationNode): String { + return when (node.name) { + "Unit" -> "void" + "Int" -> "int" + "Long" -> "long" + "Double" -> "double" + "Float" -> "float" + "Char" -> "char" + "Boolean" -> "bool" + // TODO: render arrays + else -> node.name + } + } + + private fun renderTypeParameter(node: DocumentationNode): String { + val constraints = node.details(Kind.UpperBound) + return if (constraints.none()) + node.name + else { + node.name + " extends " + constraints.map { renderType(node) }.joinToString() + } + } + + private fun renderParameter(node: DocumentationNode): String { + return "${renderType(node.detail(Kind.Type))} ${node.name}" + } + + private fun renderTypeParametersForNode(node: DocumentationNode): String { + return StringBuilder().apply { + val typeParameters = node.details(Kind.TypeParameter) + if (typeParameters.any()) { + append("<") + append(typeParameters.map { renderTypeParameter(it) }.joinToString()) + append("> ") + } + }.toString() + } + + private fun renderModifiersForNode(node: DocumentationNode): String { + val modifiers = node.details(Kind.Modifier).map { renderModifier(it) }.filter { it != "" } + if (modifiers.none()) + return "" + return modifiers.joinToString(" ", postfix = " ") + } + + private fun renderClass(node: DocumentationNode): String { + return StringBuilder().apply { + when (node.kind) { + Kind.Class -> append("class ") + Kind.Interface -> append("interface ") + Kind.Enum -> append("enum ") + Kind.EnumItem -> append("enum value ") + Kind.Object -> append("class ") + else -> throw IllegalArgumentException("Node $node is not a class-like object") + } + + append(node.name) + append(renderTypeParametersForNode(node)) + }.toString() + } + + private fun renderFunction(node: DocumentationNode): String { + return StringBuilder().apply { + when (node.kind) { + Kind.Constructor -> append(node.owner?.name) + Kind.Function -> { + append(renderTypeParametersForNode(node)) + append(renderType(node.detail(Kind.Type))) + append(" ") + append(node.name) + } + else -> throw IllegalArgumentException("Node $node is not a function-like object") + } + + val receiver = node.details(Kind.Receiver).singleOrNull() + append("(") + if (receiver != null) + (listOf(receiver) + node.details(Kind.Parameter)).map { renderParameter(it) }.joinTo(this) + else + node.details(Kind.Parameter).map { renderParameter(it) }.joinTo(this) + + append(")") + }.toString() + } + + private fun renderProperty(node: DocumentationNode): String { + return StringBuilder().apply { + when (node.kind) { + Kind.Property -> append("val ") + else -> throw IllegalArgumentException("Node $node is not a property") + } + append(renderTypeParametersForNode(node)) + val receiver = node.details(Kind.Receiver).singleOrNull() + if (receiver != null) { + append(renderType(receiver.detail(Kind.Type))) + append(".") + } + + append(node.name) + append(": ") + append(renderType(node.detail(Kind.Type))) + }.toString() + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Languages/LanguageService.kt b/core/src/main/kotlin/Languages/LanguageService.kt new file mode 100644 index 00000000..b0f4bbc9 --- /dev/null +++ b/core/src/main/kotlin/Languages/LanguageService.kt @@ -0,0 +1,41 @@ +package org.jetbrains.dokka + +/** + * Provides facility for rendering [DocumentationNode] as a language-dependent declaration + */ +interface LanguageService { + enum class RenderMode { + /** Brief signature (used in a list of all members of the class). */ + SUMMARY, + /** Full signature (used in the page describing the member itself */ + FULL + } + + /** + * Renders a [node] as a class, function, property or other signature in a target language. + * @param node A [DocumentationNode] to render + * @return [ContentNode] which is a root for a rich content tree suitable for formatting with [FormatService] + */ + fun render(node: DocumentationNode, renderMode: RenderMode = RenderMode.FULL): ContentNode + + /** + * Tries to summarize the signatures of the specified documentation nodes in a compact representation. + * Returns the representation if successful, or null if the signatures could not be summarized. + */ + fun summarizeSignatures(nodes: List<DocumentationNode>): ContentNode? + + /** + * Renders [node] as a named representation in the target language + * + * For example: + * ${code org.jetbrains.dokka.example} + * + * $node: A [DocumentationNode] to render + * $returns: [String] which is a string representation of the node's name + */ + fun renderName(node: DocumentationNode): String +} + +fun example(service: LanguageService, node: DocumentationNode) { + println("Node name: ${service.renderName(node)}") +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Locations/FoldersLocationService.kt b/core/src/main/kotlin/Locations/FoldersLocationService.kt new file mode 100644 index 00000000..89b34ed1 --- /dev/null +++ b/core/src/main/kotlin/Locations/FoldersLocationService.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import java.io.File + +public fun FoldersLocationService(root: String): FoldersLocationService = FoldersLocationService(File(root), "") +public class FoldersLocationService @Inject constructor(@Named("outputDir") val rootFile: File, val extension: String) : FileLocationService { + override val root: Location + get() = FileLocation(rootFile) + + override fun withExtension(newExtension: String): FileLocationService { + return if (extension.isEmpty()) FoldersLocationService(rootFile, newExtension) else this + } + + override fun location(qualifiedName: List<String>, hasMembers: Boolean): FileLocation { + return FileLocation(File(rootFile, relativePathToNode(qualifiedName, hasMembers)).appendExtension(extension)) + } +} + +fun relativePathToNode(qualifiedName: List<String>, hasMembers: Boolean): String { + val parts = qualifiedName.map { identifierToFilename(it) }.filterNot { it.isEmpty() } + return if (!hasMembers) { + // leaf node, use file in owner's folder + parts.joinToString("/") + } else { + parts.joinToString("/") + (if (parts.none()) "" else "/") + "index" + } +} diff --git a/core/src/main/kotlin/Locations/LocationService.kt b/core/src/main/kotlin/Locations/LocationService.kt new file mode 100644 index 00000000..80bc0236 --- /dev/null +++ b/core/src/main/kotlin/Locations/LocationService.kt @@ -0,0 +1,78 @@ +package org.jetbrains.dokka + +import java.io.File + +public interface Location { + val path: String get + fun relativePathTo(other: Location, anchor: String? = null): String +} + +/** + * Represents locations in the documentation in the form of [path](File). + * + * Locations are provided by [LocationService.location] function. + * + * $file: [File] for this location + * $path: [String] representing path of this location + */ +public data class FileLocation(val file: File): Location { + override val path : String + get() = file.path + + override fun relativePathTo(other: Location, anchor: String?): String { + if (other !is FileLocation) { + throw IllegalArgumentException("$other is not a FileLocation") + } + if (file.path.substringBeforeLast(".") == other.file.path.substringBeforeLast(".") && anchor == null) { + return "." + } + val ownerFolder = file.parentFile!! + val relativePath = ownerFolder.toPath().relativize(other.file.toPath()).toString() + return if (anchor == null) relativePath else relativePath + "#" + anchor + } +} + +/** + * Provides means of retrieving locations for [DocumentationNode](documentation nodes) + * + * `LocationService` determines where documentation for particular node should be generated + * + * * [FoldersLocationService] – represent packages and types as folders, members as files in those folders. + * * [SingleFolderLocationService] – all documentation is generated into single folder using fully qualified names + * for file names. + */ +public interface LocationService { + fun withExtension(newExtension: String) = this + + fun location(node: DocumentationNode): Location = location(node.path.map { it.name }, node.members.any()) + + /** + * Calculates a location corresponding to the specified [qualifiedName]. + * @param hasMembers if true, the node for which the location is calculated has member nodes. + */ + fun location(qualifiedName: List<String>, hasMembers: Boolean): Location + + val root: Location +} + + +public interface FileLocationService: LocationService { + override fun withExtension(newExtension: String): FileLocationService = this + + override fun location(node: DocumentationNode): FileLocation = location(node.path.map { it.name }, node.members.any()) + override fun location(qualifiedName: List<String>, hasMembers: Boolean): FileLocation +} + + +public fun identifierToFilename(path: String): String { + val escaped = path.replace('<', '-').replace('>', '-') + val lowercase = escaped.replace("[A-Z]".toRegex()) { matchResult -> "-" + matchResult.value.toLowerCase() } + return if (lowercase == "index") "--index--" else lowercase +} + +/** + * Returns relative location between two nodes. Used for relative links in documentation. + */ +fun LocationService.relativePathToLocation(owner: DocumentationNode, node: DocumentationNode): String { + return location(owner).relativePathTo(location(node), null) +} diff --git a/core/src/main/kotlin/Locations/SingleFolderLocationService.kt b/core/src/main/kotlin/Locations/SingleFolderLocationService.kt new file mode 100644 index 00000000..e313ac28 --- /dev/null +++ b/core/src/main/kotlin/Locations/SingleFolderLocationService.kt @@ -0,0 +1,19 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.name.Named +import java.io.File + +public fun SingleFolderLocationService(root: String): SingleFolderLocationService = SingleFolderLocationService(File(root), "") +public class SingleFolderLocationService @Inject constructor(@Named("outputDir") val rootFile: File, val extension: String) : FileLocationService { + override fun withExtension(newExtension: String): FileLocationService = + SingleFolderLocationService(rootFile, newExtension) + + override fun location(qualifiedName: List<String>, hasMembers: Boolean): FileLocation { + val filename = qualifiedName.map { identifierToFilename(it) }.joinToString("-") + return FileLocation(File(rootFile, filename).appendExtension(extension)) + } + + override val root: Location + get() = FileLocation(rootFile) +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Markdown/MarkdownProcessor.kt b/core/src/main/kotlin/Markdown/MarkdownProcessor.kt new file mode 100644 index 00000000..99caddc4 --- /dev/null +++ b/core/src/main/kotlin/Markdown/MarkdownProcessor.kt @@ -0,0 +1,50 @@ +package org.jetbrains.dokka + +import org.intellij.markdown.IElementType +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.LeafASTNode +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor +import org.intellij.markdown.parser.MarkdownParser + +class MarkdownNode(val node: ASTNode, val parent: MarkdownNode?, val markdown: String) { + val children: List<MarkdownNode> = node.children.map { MarkdownNode(it, this, markdown) } + val type: IElementType get() = node.type + val text: String get() = markdown.substring(node.startOffset, node.endOffset) + fun child(type: IElementType): MarkdownNode? = children.firstOrNull { it.type == type } + + override fun toString(): String = StringBuilder().apply { presentTo(this) }.toString() +} + +fun MarkdownNode.visit(action: (MarkdownNode, () -> Unit) -> Unit) { + action(this) { + for (child in children) { + child.visit(action) + } + } +} + +public fun MarkdownNode.toTestString(): String { + val sb = StringBuilder() + var level = 0 + visit { node, visitChildren -> + sb.append(" ".repeat(level * 2)) + node.presentTo(sb) + level++ + visitChildren() + level-- + } + return sb.toString() +} + +private fun MarkdownNode.presentTo(sb: StringBuilder) { + sb.append(type.toString()) + sb.append(":" + text.replace("\n", "\u23CE")) + sb.appendln() +} + +fun parseMarkdown(markdown: String): MarkdownNode { + if (markdown.isEmpty()) + return MarkdownNode(LeafASTNode(MarkdownElementTypes.MARKDOWN_FILE, 0, 0), null, markdown) + return MarkdownNode(MarkdownParser(CommonMarkFlavourDescriptor()).buildMarkdownTreeFromString(markdown), null, markdown) +} diff --git a/core/src/main/kotlin/Model/Content.kt b/core/src/main/kotlin/Model/Content.kt new file mode 100644 index 00000000..6556b09e --- /dev/null +++ b/core/src/main/kotlin/Model/Content.kt @@ -0,0 +1,231 @@ +package org.jetbrains.dokka + +public interface ContentNode { + val textLength: Int +} + +public object ContentEmpty : ContentNode { + override val textLength: Int get() = 0 +} + +public open class ContentBlock() : ContentNode { + val children = arrayListOf<ContentNode>() + + fun append(node: ContentNode) { + children.add(node) + } + + fun isEmpty() = children.isEmpty() + + override fun equals(other: Any?): Boolean = + other is ContentBlock && javaClass == other.javaClass && children == other.children + + override fun hashCode(): Int = + children.hashCode() + + override val textLength: Int + get() = children.sumBy { it.textLength } +} + +enum class IdentifierKind { + TypeName, + ParameterName, + AnnotationName, + SummarizedTypeName, + Other +} + +public data class ContentText(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +public data class ContentKeyword(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +public data class ContentIdentifier(val text: String, val kind: IdentifierKind = IdentifierKind.Other) : ContentNode { + override val textLength: Int + get() = text.length +} + +public data class ContentSymbol(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +public data class ContentEntity(val text: String) : ContentNode { + override val textLength: Int + get() = text.length +} + +public object ContentNonBreakingSpace: ContentNode { + override val textLength: Int + get() = 1 +} + +public object ContentSoftLineBreak: ContentNode { + override val textLength: Int + get() = 0 +} + +public object ContentIndentedSoftLineBreak: ContentNode { + override val textLength: Int + get() = 0 +} + +public class ContentParagraph() : ContentBlock() +public class ContentEmphasis() : ContentBlock() +public class ContentStrong() : ContentBlock() +public class ContentStrikethrough() : ContentBlock() +public class ContentCode() : ContentBlock() +public class ContentBlockCode(val language: String = "") : ContentBlock() + +public abstract class ContentNodeLink() : ContentBlock() { + abstract val node: DocumentationNode? +} + +public class ContentNodeDirectLink(override val node: DocumentationNode): ContentNodeLink() { + override fun equals(other: Any?): Boolean = + super.equals(other) && other is ContentNodeDirectLink && node.name == other.node.name + + override fun hashCode(): Int = + children.hashCode() * 31 + node.name.hashCode() +} + +public class ContentNodeLazyLink(val linkText: String, val lazyNode: () -> DocumentationNode?): ContentNodeLink() { + override val node: DocumentationNode? get() = lazyNode() + + override fun equals(other: Any?): Boolean = + super.equals(other) && other is ContentNodeLazyLink && linkText == other.linkText + + override fun hashCode(): Int = + children.hashCode() * 31 + linkText.hashCode() +} + +public class ContentExternalLink(val href : String) : ContentBlock() { + override fun equals(other: Any?): Boolean = + super.equals(other) && other is ContentExternalLink && href == other.href + + override fun hashCode(): Int = + children.hashCode() * 31 + href.hashCode() +} + +public class ContentUnorderedList() : ContentBlock() +public class ContentOrderedList() : ContentBlock() +public class ContentListItem() : ContentBlock() + +public class ContentHeading(val level: Int) : ContentBlock() + +public class ContentSection(public val tag: String, public val subjectName: String?) : ContentBlock() { + override fun equals(other: Any?): Boolean = + super.equals(other) && other is ContentSection && tag == other.tag && subjectName == other.subjectName + + override fun hashCode(): Int = + children.hashCode() * 31 * 31 + tag.hashCode() * 31 + (subjectName?.hashCode() ?: 0) +} + +public object ContentTags { + val Description = "Description" + val SeeAlso = "See Also" +} + +fun content(body: ContentBlock.() -> Unit): ContentBlock { + val block = ContentBlock() + block.body() + return block +} + +fun ContentBlock.text(value: String) = append(ContentText(value)) +fun ContentBlock.keyword(value: String) = append(ContentKeyword(value)) +fun ContentBlock.symbol(value: String) = append(ContentSymbol(value)) +fun ContentBlock.identifier(value: String, kind: IdentifierKind = IdentifierKind.Other) = append(ContentIdentifier(value, kind)) +fun ContentBlock.nbsp() = append(ContentNonBreakingSpace) +fun ContentBlock.softLineBreak() = append(ContentSoftLineBreak) +fun ContentBlock.indentedSoftLineBreak() = append(ContentIndentedSoftLineBreak) + +fun ContentBlock.strong(body: ContentBlock.() -> Unit) { + val strong = ContentStrong() + strong.body() + append(strong) +} + +fun ContentBlock.code(body: ContentBlock.() -> Unit) { + val code = ContentCode() + code.body() + append(code) +} + +fun ContentBlock.link(to: DocumentationNode, body: ContentBlock.() -> Unit) { + val block = ContentNodeDirectLink(to) + block.body() + append(block) +} + +public open class Content(): ContentBlock() { + public open val sections: List<ContentSection> get() = emptyList() + public open val summary: ContentNode get() = ContentEmpty + public open val description: ContentNode get() = ContentEmpty + + fun findSectionByTag(tag: String): ContentSection? = + sections.firstOrNull { tag.equals(it.tag, ignoreCase = true) } + + companion object { + val Empty = Content() + + fun of(vararg child: ContentNode): Content { + val result = MutableContent() + child.forEach { result.append(it) } + return result + } + } +} + +public open class MutableContent() : Content() { + private val sectionList = arrayListOf<ContentSection>() + public override val sections: List<ContentSection> + get() = sectionList + + fun addSection(tag: String?, subjectName: String?): ContentSection { + val section = ContentSection(tag ?: "", subjectName) + sectionList.add(section) + return section + } + + public override val summary: ContentNode get() = children.firstOrNull() ?: ContentEmpty + + public override val description: ContentNode by lazy { + val descriptionNodes = children.drop(1) + if (descriptionNodes.isEmpty()) { + ContentEmpty + } else { + val result = ContentSection(ContentTags.Description, null) + result.children.addAll(descriptionNodes) + result + } + } + + override fun equals(other: Any?): Boolean { + if (other !is Content) + return false + return sections == other.sections && children == other.children + } + + override fun hashCode(): Int { + return sections.map { it.hashCode() }.sum() + } + + override fun toString(): String { + if (sections.isEmpty()) + return "<empty>" + return (listOf(summary, description) + sections).joinToString() + } +} + +fun javadocSectionDisplayName(sectionName: String?): String? = + when(sectionName) { + "param" -> "Parameters" + "throws", "exception" -> "Exceptions" + else -> sectionName?.capitalize() + } diff --git a/core/src/main/kotlin/Model/DocumentationNode.kt b/core/src/main/kotlin/Model/DocumentationNode.kt new file mode 100644 index 00000000..52881f65 --- /dev/null +++ b/core/src/main/kotlin/Model/DocumentationNode.kt @@ -0,0 +1,162 @@ +package org.jetbrains.dokka + +import java.util.* + +public open class DocumentationNode(val name: String, + content: Content, + val kind: DocumentationNode.Kind) { + + private val references = LinkedHashSet<DocumentationReference>() + + var content: Content = content + private set + + public val summary: ContentNode get() = content.summary + + public val owner: DocumentationNode? + get() = references(DocumentationReference.Kind.Owner).singleOrNull()?.to + public val details: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Detail).map { it.to } + public val members: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Member).map { it.to } + public val inheritedMembers: List<DocumentationNode> + get() = references(DocumentationReference.Kind.InheritedMember).map { it.to } + public val extensions: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Extension).map { it.to } + public val inheritors: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Inheritor).map { it.to } + public val overrides: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Override).map { it.to } + public val links: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Link).map { it.to } + public val hiddenLinks: List<DocumentationNode> + get() = references(DocumentationReference.Kind.HiddenLink).map { it.to } + public val annotations: List<DocumentationNode> + get() = references(DocumentationReference.Kind.Annotation).map { it.to } + public val deprecation: DocumentationNode? + get() = references(DocumentationReference.Kind.Deprecation).singleOrNull()?.to + + // TODO: Should we allow node mutation? Model merge will copy by ref, so references are transparent, which could nice + public fun addReferenceTo(to: DocumentationNode, kind: DocumentationReference.Kind) { + references.add(DocumentationReference(this, to, kind)) + } + + public fun addAllReferencesFrom(other: DocumentationNode) { + references.addAll(other.references) + } + + public fun updateContent(body: MutableContent.() -> Unit) { + if (content !is MutableContent) { + content = MutableContent() + } + (content as MutableContent).body() + } + + public fun details(kind: DocumentationNode.Kind): List<DocumentationNode> = details.filter { it.kind == kind } + public fun members(kind: DocumentationNode.Kind): List<DocumentationNode> = members.filter { it.kind == kind } + public fun inheritedMembers(kind: DocumentationNode.Kind): List<DocumentationNode> = inheritedMembers.filter { it.kind == kind } + public fun links(kind: DocumentationNode.Kind): List<DocumentationNode> = links.filter { it.kind == kind } + + public fun detail(kind: DocumentationNode.Kind): DocumentationNode = details.filter { it.kind == kind }.single() + public fun member(kind: DocumentationNode.Kind): DocumentationNode = members.filter { it.kind == kind }.single() + public fun link(kind: DocumentationNode.Kind): DocumentationNode = links.filter { it.kind == kind }.single() + + public fun references(kind: DocumentationReference.Kind): List<DocumentationReference> = references.filter { it.kind == kind } + public fun allReferences(): Set<DocumentationReference> = references + + public override fun toString(): String { + return "$kind:$name" + } + + public enum class Kind { + Unknown, + + Package, + Class, + Interface, + Enum, + AnnotationClass, + EnumItem, + Object, + + Constructor, + Function, + Property, + Field, + + CompanionObjectProperty, + CompanionObjectFunction, + + Parameter, + Receiver, + TypeParameter, + Type, + Supertype, + UpperBound, + LowerBound, + Exception, + + Modifier, + NullabilityModifier, + + Module, + + ExternalClass, + Annotation, + + Value, + + SourceUrl, + SourcePosition, + + /** + * A note which is rendered once on a page documenting a group of overloaded functions. + * Needs to be generated equally on all overloads. + */ + OverloadGroupNote; + + companion object { + val classLike = setOf(Class, Interface, Enum, AnnotationClass, Object) + } + } +} + +public class DocumentationModule(name: String, content: Content = Content.Empty) + : DocumentationNode(name, content, DocumentationNode.Kind.Module) { +} + +val DocumentationNode.path: List<DocumentationNode> + get() { + val parent = owner ?: return listOf(this) + return parent.path + this + } + +fun DocumentationNode.findOrCreatePackageNode(packageName: String, packageContent: Map<String, Content>): DocumentationNode { + val existingNode = members(DocumentationNode.Kind.Package).firstOrNull { it.name == packageName } + if (existingNode != null) { + return existingNode + } + val newNode = DocumentationNode(packageName, + packageContent.getOrElse(packageName) { Content.Empty }, + DocumentationNode.Kind.Package) + append(newNode, DocumentationReference.Kind.Member) + return newNode +} + +fun DocumentationNode.append(child: DocumentationNode, kind: DocumentationReference.Kind) { + addReferenceTo(child, kind) + when (kind) { + DocumentationReference.Kind.Detail -> child.addReferenceTo(this, DocumentationReference.Kind.Owner) + DocumentationReference.Kind.Member -> child.addReferenceTo(this, DocumentationReference.Kind.Owner) + DocumentationReference.Kind.Owner -> child.addReferenceTo(this, DocumentationReference.Kind.Member) + else -> { /* Do not add any links back for other types */ } + } +} + +fun DocumentationNode.appendTextNode(text: String, + kind: DocumentationNode.Kind, + refKind: DocumentationReference.Kind = DocumentationReference.Kind.Detail) { + append(DocumentationNode(text, Content.Empty, kind), refKind) +} + +fun DocumentationNode.qualifiedName() = path.drop(1).map { it.name }.filter { it.length > 0 }.joinToString(".") diff --git a/core/src/main/kotlin/Model/DocumentationReference.kt b/core/src/main/kotlin/Model/DocumentationReference.kt new file mode 100644 index 00000000..898c92d7 --- /dev/null +++ b/core/src/main/kotlin/Model/DocumentationReference.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka + +import com.google.inject.Singleton + +public data class DocumentationReference(val from: DocumentationNode, val to: DocumentationNode, val kind: DocumentationReference.Kind) { + public enum class Kind { + Owner, + Member, + InheritedMember, + Detail, + Link, + HiddenLink, + Extension, + Inheritor, + Superclass, + Override, + Annotation, + Deprecation, + TopLevelPage + } +} + +class PendingDocumentationReference(val lazyNodeFrom: () -> DocumentationNode?, + val lazyNodeTo: () -> DocumentationNode?, + val kind: DocumentationReference.Kind) { + fun resolve() { + val fromNode = lazyNodeFrom() + val toNode = lazyNodeTo() + if (fromNode != null && toNode != null) { + fromNode.addReferenceTo(toNode, kind) + } + } +} + +@Singleton +class NodeReferenceGraph() { + private val nodeMap = hashMapOf<String, DocumentationNode>() + val references = arrayListOf<PendingDocumentationReference>() + + fun register(signature: String, node: DocumentationNode) { + nodeMap.put(signature, node) + } + + fun link(fromNode: DocumentationNode, toSignature: String, kind: DocumentationReference.Kind) { + references.add(PendingDocumentationReference({ -> fromNode}, { -> nodeMap[toSignature]}, kind)) + } + + fun link(fromSignature: String, toNode: DocumentationNode, kind: DocumentationReference.Kind) { + references.add(PendingDocumentationReference({ -> nodeMap[fromSignature]}, { -> toNode}, kind)) + } + + fun link(fromSignature: String, toSignature: String, kind: DocumentationReference.Kind) { + references.add(PendingDocumentationReference({ -> nodeMap[fromSignature]}, { -> nodeMap[toSignature]}, kind)) + } + + fun lookup(signature: String): DocumentationNode? = nodeMap[signature] + + fun resolveReferences() { + references.forEach { it.resolve() } + } +} diff --git a/core/src/main/kotlin/Model/PackageDocs.kt b/core/src/main/kotlin/Model/PackageDocs.kt new file mode 100644 index 00000000..044c73d8 --- /dev/null +++ b/core/src/main/kotlin/Model/PackageDocs.kt @@ -0,0 +1,60 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.google.inject.Singleton +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyPackageDescriptor +import java.io.File + +@Singleton +public class PackageDocs + @Inject constructor(val linkResolver: DeclarationLinkResolver?, + val logger: DokkaLogger) +{ + public val moduleContent: MutableContent = MutableContent() + private val _packageContent: MutableMap<String, MutableContent> = hashMapOf() + public val packageContent: Map<String, Content> + get() = _packageContent + + fun parse(fileName: String, linkResolveContext: LazyPackageDescriptor?) { + val file = File(fileName) + if (file.exists()) { + val text = file.readText() + val tree = parseMarkdown(text) + var targetContent: MutableContent = moduleContent + tree.children.forEach { + if (it.type == MarkdownElementTypes.ATX_1) { + val headingText = it.child(MarkdownTokenTypes.ATX_CONTENT)?.text + if (headingText != null) { + targetContent = findTargetContent(headingText.trimStart()) + } + } else { + buildContentTo(it, targetContent, { resolveContentLink(it, linkResolveContext) }) + } + } + } else { + logger.warn("Include file $file was not found.") + } + } + + private fun findTargetContent(heading: String): MutableContent { + if (heading.startsWith("Module") || heading.startsWith("module")) { + return moduleContent + } + if (heading.startsWith("Package") || heading.startsWith("package")) { + return findOrCreatePackageContent(heading.substring("package".length).trim()) + } + return findOrCreatePackageContent(heading) + } + + private fun findOrCreatePackageContent(packageName: String) = + _packageContent.getOrPut(packageName) { -> MutableContent() } + + private fun resolveContentLink(href: String, linkResolveContext: LazyPackageDescriptor?): ContentBlock { + if (linkResolveContext != null && linkResolver != null) { + return linkResolver.resolveContentLink(linkResolveContext, href) + } + return ContentExternalLink("#") + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Model/SourceLinks.kt b/core/src/main/kotlin/Model/SourceLinks.kt new file mode 100644 index 00000000..956bfe4b --- /dev/null +++ b/core/src/main/kotlin/Model/SourceLinks.kt @@ -0,0 +1,56 @@ +package org.jetbrains.dokka + +import com.intellij.psi.PsiElement +import java.io.File +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiNameIdentifierOwner +import org.jetbrains.kotlin.psi.psiUtil.startOffset + +class SourceLinkDefinition(val path: String, val url: String, val lineSuffix: String?) + +fun DocumentationNode.appendSourceLink(psi: PsiElement?, sourceLinks: List<SourceLinkDefinition>) { + val path = psi?.containingFile?.virtualFile?.path ?: return + + val target = if (psi is PsiNameIdentifierOwner) psi.nameIdentifier else psi + val absPath = File(path).absolutePath + val linkDef = sourceLinks.firstOrNull { absPath.startsWith(it.path) } + if (linkDef != null) { + var url = linkDef.url + path.substring(linkDef.path.length) + if (linkDef.lineSuffix != null) { + val line = target?.lineNumber() + if (line != null) { + url += linkDef.lineSuffix + line.toString() + } + } + append(DocumentationNode(url, Content.Empty, DocumentationNode.Kind.SourceUrl), + DocumentationReference.Kind.Detail); + } + + if (target != null) { + append(DocumentationNode(target.sourcePosition(), Content.Empty, DocumentationNode.Kind.SourcePosition), DocumentationReference.Kind.Detail) + } +} + +private fun PsiElement.sourcePosition(): String { + val path = containingFile.virtualFile.path + val lineNumber = lineNumber() + val columnNumber = columnNumber() + + return when { + lineNumber == null -> path + columnNumber == null -> "$path:$lineNumber" + else -> "$path:$lineNumber:$columnNumber" + } +} + +fun PsiElement.lineNumber(): Int? { + val doc = PsiDocumentManager.getInstance(project).getDocument(containingFile) + // IJ uses 0-based line-numbers; external source browsers use 1-based + return doc?.getLineNumber(textRange.startOffset)?.plus(1) +} + +fun PsiElement.columnNumber(): Int? { + val doc = PsiDocumentManager.getInstance(project).getDocument(containingFile) ?: return null + val lineNumber = doc.getLineNumber(textRange.startOffset) + return startOffset - doc.getLineStartOffset(lineNumber) +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Utilities/DokkaModule.kt b/core/src/main/kotlin/Utilities/DokkaModule.kt new file mode 100644 index 00000000..1eb82313 --- /dev/null +++ b/core/src/main/kotlin/Utilities/DokkaModule.kt @@ -0,0 +1,73 @@ +package org.jetbrains.dokka.Utilities + +import com.google.inject.Binder +import com.google.inject.Module +import com.google.inject.Provider +import com.google.inject.name.Names +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.FormatDescriptor +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import java.io.File + +class DokkaModule(val environment: AnalysisEnvironment, + val options: DocumentationOptions, + val logger: DokkaLogger) : Module { + override fun configure(binder: Binder) { + binder.bind(File::class.java).annotatedWith(Names.named("outputDir")).toInstance(File(options.outputDir)) + + binder.bindNameAnnotated<LocationService, SingleFolderLocationService>("singleFolder") + binder.bindNameAnnotated<FileLocationService, SingleFolderLocationService>("singleFolder") + binder.bindNameAnnotated<LocationService, FoldersLocationService>("folders") + binder.bindNameAnnotated<FileLocationService, FoldersLocationService>("folders") + + // defaults + binder.bind(LocationService::class.java).to(FoldersLocationService::class.java) + binder.bind(FileLocationService::class.java).to(FoldersLocationService::class.java) + binder.bind(LanguageService::class.java).to(KotlinLanguageService::class.java) + + binder.bind(HtmlTemplateService::class.java).toProvider(object : Provider<HtmlTemplateService> { + override fun get(): HtmlTemplateService = HtmlTemplateService.default("style.css") + }) + + binder.registerCategory<LanguageService>("language") + binder.registerCategory<OutlineFormatService>("outline") + binder.registerCategory<FormatService>("format") + binder.registerCategory<Generator>("generator") + + val descriptor = ServiceLocator.lookup<FormatDescriptor>("format", options.outputFormat) + + descriptor.outlineServiceClass?.let { clazz -> + binder.bind(OutlineFormatService::class.java).to(clazz.java) + } + descriptor.formatServiceClass?.let { clazz -> + binder.bind(FormatService::class.java).to(clazz.java) + } + binder.bind<PackageDocumentationBuilder>().to(descriptor.packageDocumentationBuilderClass.java) + binder.bind<JavaDocumentationBuilder>().to(descriptor.javaDocumentationBuilderClass.java) + + binder.bind<Generator>().to(descriptor.generatorServiceClass.java) + + val coreEnvironment = environment.createCoreEnvironment() + binder.bind<KotlinCoreEnvironment>().toInstance(coreEnvironment) + + val dokkaResolutionFacade = environment.createResolutionFacade(coreEnvironment) + binder.bind<DokkaResolutionFacade>().toInstance(dokkaResolutionFacade) + + binder.bind<DocumentationOptions>().toInstance(options) + binder.bind<DokkaLogger>().toInstance(logger) + } +} + +private inline fun <reified T: Any> Binder.registerCategory(category: String) { + ServiceLocator.allServices(category).forEach { + @Suppress("UNCHECKED_CAST") + bind(T::class.java).annotatedWith(Names.named(it.name)).to(T::class.java.classLoader.loadClass(it.className) as Class<T>) + } +} + +private inline fun <reified Base : Any, reified T : Base> Binder.bindNameAnnotated(name: String) { + bind(Base::class.java).annotatedWith(Names.named(name)).to(T::class.java) +} + + +inline fun <reified T: Any> Binder.bind() = bind(T::class.java) diff --git a/core/src/main/kotlin/Utilities/Html.kt b/core/src/main/kotlin/Utilities/Html.kt new file mode 100644 index 00000000..ce3a1982 --- /dev/null +++ b/core/src/main/kotlin/Utilities/Html.kt @@ -0,0 +1,8 @@ +package org.jetbrains.dokka + + +/** + * Replaces symbols reserved in HTML with their respective entities. + * Replaces & with &, < with < and > with > + */ +public fun String.htmlEscape(): String = replace("&", "&").replace("<", "<").replace(">", ">") diff --git a/core/src/main/kotlin/Utilities/Path.kt b/core/src/main/kotlin/Utilities/Path.kt new file mode 100644 index 00000000..05838499 --- /dev/null +++ b/core/src/main/kotlin/Utilities/Path.kt @@ -0,0 +1,5 @@ +package org.jetbrains.dokka + +import java.io.File + +fun File.appendExtension(extension: String) = if (extension.isEmpty()) this else File(path + "." + extension) diff --git a/core/src/main/kotlin/Utilities/ServiceLocator.kt b/core/src/main/kotlin/Utilities/ServiceLocator.kt new file mode 100644 index 00000000..7a5aff79 --- /dev/null +++ b/core/src/main/kotlin/Utilities/ServiceLocator.kt @@ -0,0 +1,78 @@ +package org.jetbrains.dokka.Utilities + +import java.io.File +import java.util.* +import java.util.jar.JarFile +import java.util.zip.ZipEntry + +data class ServiceDescriptor(val name: String, val category: String, val description: String?, val className: String) + +class ServiceLookupException(message: String) : Exception(message) + +public object ServiceLocator { + public fun <T : Any> lookup(clazz: Class<T>, category: String, implementationName: String): T { + val descriptor = lookupDescriptor(category, implementationName) + val loadedClass = javaClass.classLoader.loadClass(descriptor.className) + val constructor = loadedClass.constructors + .filter { it.parameterTypes.isEmpty() } + .firstOrNull() ?: throw ServiceLookupException("Class ${descriptor.className} has no corresponding constructor") + + val implementationRawType: Any = if (constructor.parameterTypes.isEmpty()) constructor.newInstance() else constructor.newInstance(constructor) + + if (!clazz.isInstance(implementationRawType)) { + throw ServiceLookupException("Class ${descriptor.className} is not a subtype of ${clazz.name}") + } + + @Suppress("UNCHECKED_CAST") + return implementationRawType as T + } + + private fun lookupDescriptor(category: String, implementationName: String): ServiceDescriptor { + val properties = javaClass.classLoader.getResourceAsStream("dokka/$category/$implementationName.properties")?.use { stream -> + Properties().let { properties -> + properties.load(stream) + properties + } + } ?: throw ServiceLookupException("No implementation with name $implementationName found in category $category") + + val className = properties["class"]?.toString() ?: throw ServiceLookupException("Implementation $implementationName has no class configured") + + return ServiceDescriptor(implementationName, category, properties["description"]?.toString(), className) + } + + fun allServices(category: String): List<ServiceDescriptor> { + val entries = this.javaClass.classLoader.getResources("dokka/$category")?.toList() ?: emptyList() + + return entries.flatMap { + when (it.protocol) { + "file" -> File(it.file).listFiles()?.filter { it.extension == "properties" }?.map { lookupDescriptor(category, it.nameWithoutExtension) } ?: emptyList() + "jar" -> { + val file = JarFile(it.file.removePrefix("file:").substringBefore("!")) + try { + val jarPath = it.file.substringAfterLast("!").removePrefix("/") + file.entries() + .asSequence() + .filter { entry -> !entry.isDirectory && entry.path == jarPath && entry.extension == "properties" } + .map { entry -> + lookupDescriptor(category, entry.fileName.substringBeforeLast(".")) + }.toList() + } finally { + file.close() + } + } + else -> emptyList<ServiceDescriptor>() + } + } + } +} + +public inline fun <reified T : Any> ServiceLocator.lookup(category: String, implementationName: String): T = lookup(T::class.java, category, implementationName) + +private val ZipEntry.fileName: String + get() = name.substringAfterLast("/", name) + +private val ZipEntry.path: String + get() = name.substringBeforeLast("/", "").removePrefix("/") + +private val ZipEntry.extension: String? + get() = fileName.let { fn -> if ("." in fn) fn.substringAfterLast(".") else null } diff --git a/core/src/main/kotlin/ant/dokka.kt b/core/src/main/kotlin/ant/dokka.kt new file mode 100644 index 00000000..d78980f8 --- /dev/null +++ b/core/src/main/kotlin/ant/dokka.kt @@ -0,0 +1,108 @@ +package org.jetbrains.dokka.ant + +import org.apache.tools.ant.Task +import org.apache.tools.ant.types.Path +import org.apache.tools.ant.types.Reference +import org.apache.tools.ant.BuildException +import org.apache.tools.ant.Project +import org.jetbrains.dokka.DokkaLogger +import org.jetbrains.dokka.DokkaGenerator +import org.jetbrains.dokka.SourceLinkDefinition +import java.io.File + +class AntLogger(val task: Task): DokkaLogger { + override fun info(message: String) = task.log(message, Project.MSG_INFO) + override fun warn(message: String) = task.log(message, Project.MSG_WARN) + override fun error(message: String) = task.log(message, Project.MSG_ERR) +} + +class AntSourceLinkDefinition(var path: String? = null, var url: String? = null, var lineSuffix: String? = null) + +class DokkaAntTask(): Task() { + public var moduleName: String? = null + public var outputDir: String? = null + public var outputFormat: String = "html" + + public var skipDeprecated: Boolean = false + + public val compileClasspath: Path = Path(getProject()) + public val sourcePath: Path = Path(getProject()) + public val samplesPath: Path = Path(getProject()) + public val includesPath: Path = Path(getProject()) + + public val antSourceLinks: MutableList<AntSourceLinkDefinition> = arrayListOf() + + public fun setClasspath(classpath: Path) { + compileClasspath.append(classpath) + } + + public fun setClasspathRef(ref: Reference) { + compileClasspath.createPath().refid = ref + } + + public fun setSrc(src: Path) { + sourcePath.append(src) + } + + public fun setSrcRef(ref: Reference) { + sourcePath.createPath().refid = ref + } + + public fun setSamples(samples: Path) { + samplesPath.append(samples) + } + + public fun setSamplesRef(ref: Reference) { + samplesPath.createPath().refid = ref + } + + public fun setInclude(include: Path) { + includesPath.append(include) + } + + public fun createSourceLink(): AntSourceLinkDefinition { + val def = AntSourceLinkDefinition() + antSourceLinks.add(def) + return def + } + + override fun execute() { + if (sourcePath.list().size == 0) { + throw BuildException("At least one source path needs to be specified") + } + if (moduleName == null) { + throw BuildException("Module name needs to be specified") + } + if (outputDir == null) { + throw BuildException("Output directory needs to be specified") + } + val sourceLinks = antSourceLinks.map { + val path = it.path + if (path == null) { + throw BuildException("Path attribute of a <sourceLink> element is required") + } + val url = it.url + if (url == null) { + throw BuildException("Path attribute of a <sourceLink> element is required") + } + SourceLinkDefinition(File(path).canonicalFile.absolutePath, url, it.lineSuffix) + } + + val url = DokkaAntTask::class.java.getResource("/org/jetbrains/dokka/ant/DokkaAntTask.class") + val jarRoot = url.path.substringBefore("!/").removePrefix("file:") + + val generator = DokkaGenerator( + AntLogger(this), + listOf(jarRoot) + compileClasspath.list().toList(), + sourcePath.list().toList(), + samplesPath.list().toList(), + includesPath.list().toList(), + moduleName!!, + outputDir!!, + outputFormat, + sourceLinks, + skipDeprecated + ) + generator.generate() + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/javadoc/docbase.kt b/core/src/main/kotlin/javadoc/docbase.kt new file mode 100644 index 00000000..a0caca94 --- /dev/null +++ b/core/src/main/kotlin/javadoc/docbase.kt @@ -0,0 +1,501 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.* +import org.jetbrains.dokka.* +import java.lang.reflect.Modifier +import java.util.* +import kotlin.reflect.KClass + +private interface HasModule { + val module: ModuleNodeAdapter +} + +private interface HasDocumentationNode { + val node: DocumentationNode +} + +open class DocumentationNodeBareAdapter(override val node: DocumentationNode) : Doc, HasDocumentationNode { + private var rawCommentText_: String? = null + + override fun name(): String = node.name + override fun position(): SourcePosition? = SourcePositionAdapter(node) + + override fun inlineTags(): Array<out Tag>? = emptyArray() + override fun firstSentenceTags(): Array<out Tag>? = emptyArray() + override fun tags(): Array<out Tag> = emptyArray() + override fun tags(tagname: String?): Array<out Tag>? = tags().filter { it.kind() == tagname || it.kind() == "@$tagname" }.toTypedArray() + override fun seeTags(): Array<out SeeTag>? = tags().filterIsInstance<SeeTag>().toTypedArray() + override fun commentText(): String = "" + + override fun setRawCommentText(rawDocumentation: String?) { + rawCommentText_ = rawDocumentation ?: "" + } + + override fun getRawCommentText(): String = rawCommentText_ ?: "" + + override fun isError(): Boolean = false + override fun isException(): Boolean = node.kind == DocumentationNode.Kind.Exception + override fun isEnumConstant(): Boolean = node.kind == DocumentationNode.Kind.EnumItem + override fun isEnum(): Boolean = node.kind == DocumentationNode.Kind.Enum + override fun isMethod(): Boolean = node.kind == DocumentationNode.Kind.Function + override fun isInterface(): Boolean = node.kind == DocumentationNode.Kind.Interface + override fun isField(): Boolean = node.kind == DocumentationNode.Kind.Field + override fun isClass(): Boolean = node.kind == DocumentationNode.Kind.Class + override fun isAnnotationType(): Boolean = node.kind == DocumentationNode.Kind.AnnotationClass + override fun isConstructor(): Boolean = node.kind == DocumentationNode.Kind.Constructor + override fun isOrdinaryClass(): Boolean = node.kind == DocumentationNode.Kind.Class + override fun isAnnotationTypeElement(): Boolean = node.kind == DocumentationNode.Kind.Annotation + + override fun compareTo(other: Any?): Int = when (other) { + !is DocumentationNodeAdapter -> 1 + else -> node.name.compareTo(other.node.name) + } + + override fun equals(other: Any?): Boolean = node.qualifiedName() == (other as? DocumentationNodeAdapter)?.node?.qualifiedName() + override fun hashCode(): Int = node.name.hashCode() + + override fun isIncluded(): Boolean = node.kind != DocumentationNode.Kind.ExternalClass +} + + +// TODO think of source position instead of null +// TODO tags +open class DocumentationNodeAdapter(override val module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeBareAdapter(node), HasModule { + override fun inlineTags(): Array<out Tag> = buildInlineTags(module, this, node.content).toTypedArray() + override fun firstSentenceTags(): Array<out Tag> = buildInlineTags(module, this, node.summary).toTypedArray() + + override fun tags(): Array<out Tag> { + val result = ArrayList<Tag>(buildInlineTags(module, this, node.content)) + node.content.sections.flatMapTo(result) { + when (it.tag) { + ContentTags.SeeAlso -> buildInlineTags(module, this, it) + else -> emptyList<Tag>() + } + } + + node.deprecation?.let { + val content = it.content.asText() + if (content != null) { + result.add(TagImpl(this, "deprecated", content)) + } + } + + return result.toTypedArray() + } +} + +// should be extension property but can't because of KT-8745 +private fun <T> nodeAnnotations(self: T): List<AnnotationDescAdapter> where T : HasModule, T : HasDocumentationNode + = self.node.annotations.map { AnnotationDescAdapter(self.module, it) } + +private fun DocumentationNode.hasAnnotation(klass: KClass<*>) = klass.qualifiedName in annotations.map { it.qualifiedName() } +private fun DocumentationNode.hasModifier(name: String) = details(DocumentationNode.Kind.Modifier).any { it.name == name } + + +class PackageAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), PackageDoc { + private val allClasses = listOf(node).collectAllTypesRecursively() + + override fun findClass(className: String?): ClassDoc? = + allClasses.get(className)?.let { ClassDocumentationNodeAdapter(module, it) } + + override fun annotationTypes(): Array<out AnnotationTypeDoc> = emptyArray() + override fun annotations(): Array<out AnnotationDesc> = node.members(DocumentationNode.Kind.AnnotationClass).map { AnnotationDescAdapter(module, it) }.toTypedArray() + override fun exceptions(): Array<out ClassDoc> = node.members(DocumentationNode.Kind.Exception).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun ordinaryClasses(): Array<out ClassDoc> = node.members(DocumentationNode.Kind.Class).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun interfaces(): Array<out ClassDoc> = node.members(DocumentationNode.Kind.Interface).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun errors(): Array<out ClassDoc> = emptyArray() + override fun enums(): Array<out ClassDoc> = node.members(DocumentationNode.Kind.Enum).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun allClasses(filter: Boolean): Array<out ClassDoc> = allClasses.values.map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun allClasses(): Array<out ClassDoc> = allClasses(true) + + override fun isIncluded(): Boolean = node.name in module.allPackages +} + +class AnnotationTypeDocAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : ClassDocumentationNodeAdapter(module, node), AnnotationTypeDoc { + override fun elements(): Array<out AnnotationTypeElementDoc>? = emptyArray() // TODO +} + +class AnnotationDescAdapter(val module: ModuleNodeAdapter, val node: DocumentationNode) : AnnotationDesc { + override fun annotationType(): AnnotationTypeDoc? = AnnotationTypeDocAdapter(module, node) // TODO ????? + override fun isSynthesized(): Boolean = false + override fun elementValues(): Array<out AnnotationDesc.ElementValuePair>? = emptyArray() // TODO +} + +class ProgramElementAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), ProgramElementDoc { + override fun isPublic(): Boolean = true + override fun isPackagePrivate(): Boolean = false + override fun isStatic(): Boolean = node.hasModifier("static") + override fun modifierSpecifier(): Int = Modifier.PUBLIC + if (isStatic) Modifier.STATIC else 0 + override fun qualifiedName(): String? = if (node.kind == DocumentationNode.Kind.Type) node.qualifiedNameFromType() else node.qualifiedName() + override fun annotations(): Array<out AnnotationDesc>? = nodeAnnotations(this).toTypedArray() + override fun modifiers(): String? = "public ${if (isStatic) "static" else ""}".trim() + override fun isProtected(): Boolean = false + + override fun isFinal(): Boolean = node.hasModifier("final") + + override fun containingPackage(): PackageDoc? { + if (node.kind == DocumentationNode.Kind.Type) { + return null + } + + var owner: DocumentationNode? = node + while (owner != null) { + if (owner.kind == DocumentationNode.Kind.Package) { + return PackageAdapter(module, owner) + } + owner = owner.owner + } + + return null + } + + override fun containingClass(): ClassDoc? { + if (node.kind == DocumentationNode.Kind.Type) { + return null + } + + var owner = node.owner + while (owner != null) { + when (owner.kind) { + DocumentationNode.Kind.Class, + DocumentationNode.Kind.Interface, + DocumentationNode.Kind.Enum -> return ClassDocumentationNodeAdapter(module, owner) + else -> owner = owner.owner + } + } + + return null + } + + override fun isPrivate(): Boolean = false + override fun isIncluded(): Boolean = containingPackage()?.isIncluded ?: false && containingClass()?.let { it.isIncluded } ?: true +} + +open class TypeAdapter(override val module: ModuleNodeAdapter, override val node: DocumentationNode) : Type, HasDocumentationNode, HasModule { + private val javaLanguageService = JavaLanguageService() + + override fun qualifiedTypeName(): String = javaLanguageService.getArrayElementType(node)?.qualifiedNameFromType() ?: node.qualifiedNameFromType() + override fun typeName(): String = javaLanguageService.getArrayElementType(node)?.name ?: node.name + override fun simpleTypeName(): String = typeName() // TODO difference typeName() vs simpleTypeName() + + override fun dimension(): String = Collections.nCopies(javaLanguageService.getArrayDimension(node), "[]").joinToString("") + override fun isPrimitive(): Boolean = simpleTypeName() in setOf("int", "long", "short", "byte", "char", "double", "float", "boolean", "void") + + override fun asClassDoc(): ClassDoc? = if (isPrimitive) null else + elementType?.asClassDoc() ?: + when (node.kind) { + in DocumentationNode.Kind.classLike, + DocumentationNode.Kind.ExternalClass, + DocumentationNode.Kind.Exception -> module.classNamed(qualifiedTypeName()) ?: ClassDocumentationNodeAdapter(module, node) + + else -> when { + node.links.isNotEmpty() -> TypeAdapter(module, node.links.first()).asClassDoc() + else -> ClassDocumentationNodeAdapter(module, node) // TODO ? + } + } + + override fun asTypeVariable(): TypeVariable? = if (node.kind == DocumentationNode.Kind.TypeParameter) TypeVariableAdapter(module, node) else null + override fun asParameterizedType(): ParameterizedType? = + if (node.details(DocumentationNode.Kind.Type).isNotEmpty()) ParameterizedTypeAdapter(module, node) + else null // TODO it should ignore dimensions + + override fun asAnnotationTypeDoc(): AnnotationTypeDoc? = if (node.kind == DocumentationNode.Kind.AnnotationClass) AnnotationTypeDocAdapter(module, node) else null + override fun asAnnotatedType(): AnnotatedType? = if (node.annotations.isNotEmpty()) AnnotatedTypeAdapter(module, node) else null + override fun getElementType(): Type? = javaLanguageService.getArrayElementType(node)?.let { et -> TypeAdapter(module, et) } + override fun asWildcardType(): WildcardType? = null + + override fun toString(): String = qualifiedTypeName() + dimension() + override fun hashCode(): Int = node.name.hashCode() + override fun equals(other: Any?): Boolean = other is TypeAdapter && toString() == other.toString() +} + +class AnnotatedTypeAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : TypeAdapter(module, node), AnnotatedType { + override fun underlyingType(): Type? = this + override fun annotations(): Array<out AnnotationDesc> = nodeAnnotations(this).toTypedArray() +} + +class WildcardTypeAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : TypeAdapter(module, node), WildcardType { + override fun extendsBounds(): Array<out Type> = node.details(DocumentationNode.Kind.UpperBound).map { TypeAdapter(module, it) }.toTypedArray() + override fun superBounds(): Array<out Type> = node.details(DocumentationNode.Kind.LowerBound).map { TypeAdapter(module, it) }.toTypedArray() +} + +class TypeVariableAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : TypeAdapter(module, node), TypeVariable { + override fun owner(): ProgramElementDoc = node.owner!!.let<DocumentationNode, ProgramElementDoc> { owner -> + when (owner.kind) { + DocumentationNode.Kind.Function, + DocumentationNode.Kind.Constructor -> ExecutableMemberAdapter(module, owner) + + DocumentationNode.Kind.Class, + DocumentationNode.Kind.Interface, + DocumentationNode.Kind.Enum -> ClassDocumentationNodeAdapter(module, owner) + + else -> ProgramElementAdapter(module, node.owner!!) + } + } + + override fun bounds(): Array<out Type>? = node.details(DocumentationNode.Kind.UpperBound).map { TypeAdapter(module, it) }.toTypedArray() + override fun annotations(): Array<out AnnotationDesc>? = node.members(DocumentationNode.Kind.Annotation).map { AnnotationDescAdapter(module, it) }.toTypedArray() + + override fun qualifiedTypeName(): String = node.name + override fun simpleTypeName(): String = node.name + override fun typeName(): String = node.name + + override fun hashCode(): Int = node.name.hashCode() + override fun equals(other: Any?): Boolean = other is Type && other.typeName() == typeName() && other.asTypeVariable()?.owner() == owner() + + override fun asTypeVariable(): TypeVariableAdapter = this +} + +class ParameterizedTypeAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : TypeAdapter(module, node), ParameterizedType { + override fun typeArguments(): Array<out Type> = node.details(DocumentationNode.Kind.Type).map { TypeVariableAdapter(module, it) }.toTypedArray() + override fun superclassType(): Type? = + node.lookupSuperClasses(module) + .firstOrNull { it.kind == DocumentationNode.Kind.Class || it.kind == DocumentationNode.Kind.ExternalClass } + ?.let { ClassDocumentationNodeAdapter(module, it) } + + override fun interfaceTypes(): Array<out Type> = + node.lookupSuperClasses(module) + .filter { it.kind == DocumentationNode.Kind.Interface } + .map { ClassDocumentationNodeAdapter(module, it) } + .toTypedArray() + + override fun containingType(): Type? = when (node.owner?.kind) { + DocumentationNode.Kind.Package -> null + DocumentationNode.Kind.Class, + DocumentationNode.Kind.Interface, + DocumentationNode.Kind.Object, + DocumentationNode.Kind.Enum -> ClassDocumentationNodeAdapter(module, node.owner!!) + + else -> null + } +} + +class ParameterAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), Parameter { + override fun typeName(): String? = JavaLanguageService().renderType(node.detail(DocumentationNode.Kind.Type)) + override fun type(): Type? = TypeAdapter(module, node.detail(DocumentationNode.Kind.Type)) + override fun annotations(): Array<out AnnotationDesc> = nodeAnnotations(this).toTypedArray() +} + +class ReceiverParameterAdapter(module: ModuleNodeAdapter, val receiverType: DocumentationNode, val parent: ExecutableMemberAdapter) : DocumentationNodeAdapter(module, receiverType), Parameter { + override fun typeName(): String? = receiverType.name + override fun type(): Type? = TypeAdapter(module, receiverType) + override fun annotations(): Array<out AnnotationDesc> = nodeAnnotations(this).toTypedArray() + override fun name(): String = tryName("receiver") + + private tailrec fun tryName(name: String): String = when (name) { + in parent.parameters().drop(1).map { it.name() } -> tryName("$$name") + else -> name + } +} + +fun classOf(fqName: String, kind: DocumentationNode.Kind = DocumentationNode.Kind.Class) = DocumentationNode(fqName.substringAfterLast(".", fqName), Content.Empty, kind).let { node -> + val pkg = fqName.substringBeforeLast(".", "") + if (pkg.isNotEmpty()) { + node.append(DocumentationNode(pkg, Content.Empty, DocumentationNode.Kind.Package), DocumentationReference.Kind.Owner) + } + + node +} + +open class ExecutableMemberAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), ProgramElementDoc by ProgramElementAdapter(module, node), ExecutableMemberDoc { + + override fun isSynthetic(): Boolean = false + override fun isNative(): Boolean = node.annotations.any { it.name == "native" } + + override fun thrownExceptions(): Array<out ClassDoc> = emptyArray() // TODO + override fun throwsTags(): Array<out ThrowsTag> = + node.content.sections + .filter { it.tag == "Exceptions" } + .map { it.subjectName } + .filterNotNull() + .map { ThrowsTagAdapter(this, ClassDocumentationNodeAdapter(module, classOf(it, DocumentationNode.Kind.Exception))) } + .toTypedArray() + + override fun isVarArgs(): Boolean = node.details(DocumentationNode.Kind.Parameter).any { false } // TODO + + override fun isSynchronized(): Boolean = node.annotations.any { it.name == "synchronized" } + + override fun paramTags(): Array<out ParamTag> = node.details(DocumentationNode.Kind.Parameter) + .filter { it.content.summary !is ContentEmpty || it.content.description !is ContentEmpty || it.content.sections.isNotEmpty() } + .map { ParamTagAdapter(module, this, it.name, false, it.content.children) } + .toTypedArray() + + override fun thrownExceptionTypes(): Array<out Type> = emptyArray() + override fun receiverType(): Type? = receiverNode()?.let { receiver -> TypeAdapter(module, receiver) } + override fun flatSignature(): String = node.details(DocumentationNode.Kind.Parameter).map { JavaLanguageService().renderType(it) }.joinToString(", ", "(", ")") + override fun signature(): String = node.details(DocumentationNode.Kind.Parameter).map { JavaLanguageService().renderType(it) }.joinToString(", ", "(", ")") // TODO it should be FQ types + + override fun parameters(): Array<out Parameter> = + ((receiverNode()?.let { receiver -> listOf<Parameter>(ReceiverParameterAdapter(module, receiver, this)) } ?: emptyList()) + + node.details(DocumentationNode.Kind.Parameter).map { ParameterAdapter(module, it) } + ).toTypedArray() + + override fun typeParameters(): Array<out TypeVariable> = node.details(DocumentationNode.Kind.TypeParameter).map { TypeVariableAdapter(module, it) }.toTypedArray() + + override fun typeParamTags(): Array<out ParamTag> = node.details(DocumentationNode.Kind.TypeParameter).filter { it.content.summary !is ContentEmpty || it.content.description !is ContentEmpty || it.content.sections.isNotEmpty() }.map { + ParamTagAdapter(module, this, it.name, true, it.content.children) + }.toTypedArray() + + private fun receiverNode() = node.details(DocumentationNode.Kind.Receiver).let { receivers -> + when { + receivers.isNotEmpty() -> receivers.single().detail(DocumentationNode.Kind.Type) + else -> null + } + } +} + +class ConstructorAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : ExecutableMemberAdapter(module, node), ConstructorDoc { + override fun name(): String = node.owner?.name ?: throw IllegalStateException("No owner for $node") +} + +class MethodAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), ExecutableMemberDoc by ExecutableMemberAdapter(module, node), MethodDoc { + override fun overrides(meth: MethodDoc?): Boolean = false // TODO + + override fun overriddenType(): Type? = node.overrides.firstOrNull()?.owner?.let { owner -> TypeAdapter(module, owner) } + + override fun overriddenMethod(): MethodDoc? = node.overrides.map { MethodAdapter(module, it) }.firstOrNull() + override fun overriddenClass(): ClassDoc? = overriddenMethod()?.containingClass() + + override fun isAbstract(): Boolean = false // TODO + + override fun isDefault(): Boolean = false + + override fun returnType(): Type = TypeAdapter(module, node.detail(DocumentationNode.Kind.Type)) +} + +class FieldAdapter(module: ModuleNodeAdapter, node: DocumentationNode) : DocumentationNodeAdapter(module, node), ProgramElementDoc by ProgramElementAdapter(module, node), FieldDoc { + override fun isSynthetic(): Boolean = false + + override fun constantValueExpression(): String? = node.details(DocumentationNode.Kind.Value).firstOrNull()?.let { it.name } + override fun constantValue(): Any? = constantValueExpression() + + override fun type(): Type = TypeAdapter(module, node.detail(DocumentationNode.Kind.Type)) + override fun isTransient(): Boolean = node.hasAnnotation(Transient::class) + override fun serialFieldTags(): Array<out SerialFieldTag> = emptyArray() + + override fun isVolatile(): Boolean = node.hasAnnotation(Volatile::class) +} + +open class ClassDocumentationNodeAdapter(module: ModuleNodeAdapter, val classNode: DocumentationNode) + : DocumentationNodeAdapter(module, classNode), + Type by TypeAdapter(module, classNode), + ProgramElementDoc by ProgramElementAdapter(module, classNode), + ClassDoc { + + override fun name(): String { + val parent = classNode.owner + if (parent?.kind in DocumentationNode.Kind.classLike) { + return parent!!.name + "." + classNode.name + } + return classNode.name + } + + override fun constructors(filter: Boolean): Array<out ConstructorDoc> = classNode.members(DocumentationNode.Kind.Constructor).map { ConstructorAdapter(module, it) }.toTypedArray() + override fun constructors(): Array<out ConstructorDoc> = constructors(true) + override fun importedPackages(): Array<out PackageDoc> = emptyArray() + override fun importedClasses(): Array<out ClassDoc>? = emptyArray() + override fun typeParameters(): Array<out TypeVariable> = classNode.details(DocumentationNode.Kind.TypeParameter).map { TypeVariableAdapter(module, it) }.toTypedArray() + override fun asTypeVariable(): TypeVariable? = if (classNode.kind == DocumentationNode.Kind.Class) TypeVariableAdapter(module, classNode) else null + override fun isExternalizable(): Boolean = interfaces().any { it.qualifiedName() == "java.io.Externalizable" } + override fun definesSerializableFields(): Boolean = false + override fun methods(filter: Boolean): Array<out MethodDoc> = classNode.members(DocumentationNode.Kind.Function).map { MethodAdapter(module, it) }.toTypedArray() // TODO include get/set methods + override fun methods(): Array<out MethodDoc> = methods(true) + override fun enumConstants(): Array<out FieldDoc>? = classNode.members(DocumentationNode.Kind.EnumItem).map { FieldAdapter(module, it) }.toTypedArray() + override fun isAbstract(): Boolean = classNode.details(DocumentationNode.Kind.Modifier).any { it.name == "abstract" } + override fun interfaceTypes(): Array<out Type> = classNode.lookupSuperClasses(module) + .filter { it.kind == DocumentationNode.Kind.Interface } + .map { ClassDocumentationNodeAdapter(module, it) } + .toTypedArray() + + override fun interfaces(): Array<out ClassDoc> = classNode.lookupSuperClasses(module) + .filter { it.kind == DocumentationNode.Kind.Interface } + .map { ClassDocumentationNodeAdapter(module, it) } + .toTypedArray() + + override fun typeParamTags(): Array<out ParamTag> = (classNode.details(DocumentationNode.Kind.TypeParameter).filter { it.content.summary !is ContentEmpty || it.content.description !is ContentEmpty || it.content.sections.isNotEmpty() }.map { + ParamTagAdapter(module, this, it.name, true, it.content.children) + } + classNode.content.sections.filter { it.subjectName in typeParameters().map { it.simpleTypeName() } }.map { + ParamTagAdapter(module, this, it.subjectName ?: "?", true, it.children) + }).toTypedArray() + + override fun fields(): Array<out FieldDoc> = fields(true) + override fun fields(filter: Boolean): Array<out FieldDoc> = classNode.members(DocumentationNode.Kind.Field).map { FieldAdapter(module, it) }.toTypedArray() + + override fun findClass(className: String?): ClassDoc? = null // TODO !!! + override fun serializableFields(): Array<out FieldDoc> = emptyArray() + override fun superclassType(): Type? = classNode.lookupSuperClasses(module).singleOrNull { it.kind == DocumentationNode.Kind.Class }?.let { ClassDocumentationNodeAdapter(module, it) } + override fun serializationMethods(): Array<out MethodDoc> = emptyArray() // TODO + override fun superclass(): ClassDoc? = classNode.lookupSuperClasses(module).singleOrNull { it.kind == DocumentationNode.Kind.Class }?.let { ClassDocumentationNodeAdapter(module, it) } + override fun isSerializable(): Boolean = false // TODO + override fun subclassOf(cd: ClassDoc?): Boolean { + if (cd == null) { + return false + } + + val expectedFQName = cd.qualifiedName() + val types = arrayListOf(classNode) + val visitedTypes = HashSet<String>() + + while (types.isNotEmpty()) { + val type = types.removeAt(types.lastIndex) + val fqName = type.qualifiedName() + + if (expectedFQName == fqName) { + return true + } + + visitedTypes.add(fqName) + types.addAll(type.details(DocumentationNode.Kind.Supertype).filter { it.qualifiedName() !in visitedTypes }) + } + + return false + } + + override fun innerClasses(): Array<out ClassDoc> = classNode.members(DocumentationNode.Kind.Class).map { ClassDocumentationNodeAdapter(module, it) }.toTypedArray() + override fun innerClasses(filter: Boolean): Array<out ClassDoc> = innerClasses() +} + +fun DocumentationNode.lookupSuperClasses(module: ModuleNodeAdapter) = + details(DocumentationNode.Kind.Supertype) + .map { it.links.firstOrNull() } + .map { module.allTypes[it?.qualifiedName()] } + .filterNotNull() + +fun List<DocumentationNode>.collectAllTypesRecursively(): Map<String, DocumentationNode> { + val result = hashMapOf<String, DocumentationNode>() + + fun DocumentationNode.collectTypesRecursively() { + val classLikeMembers = DocumentationNode.Kind.classLike.flatMap { members(it) } + classLikeMembers.forEach { + result.put(it.qualifiedName(), it) + it.collectTypesRecursively() + } + } + + forEach { it.collectTypesRecursively() } + return result +} + +class ModuleNodeAdapter(val module: DocumentationModule, val reporter: DocErrorReporter, val outputPath: String) : DocumentationNodeBareAdapter(module), DocErrorReporter by reporter, RootDoc { + val allPackages = module.members(DocumentationNode.Kind.Package).toMapBy { it.name } + val allTypes = module.members(DocumentationNode.Kind.Package).collectAllTypesRecursively() + + override fun packageNamed(name: String?): PackageDoc? = allPackages[name]?.let { PackageAdapter(this, it) } + + override fun classes(): Array<out ClassDoc> = + allTypes.values.map { ClassDocumentationNodeAdapter(this, it) }.toTypedArray() + + override fun options(): Array<out Array<String>> = arrayOf( + arrayOf("-d", outputPath), + arrayOf("-docencoding", "UTF-8"), + arrayOf("-charset", "UTF-8"), + arrayOf("-keywords") + ) + + override fun specifiedPackages(): Array<out PackageDoc>? = module.members(DocumentationNode.Kind.Package).map { PackageAdapter(this, it) }.toTypedArray() + + override fun classNamed(qualifiedName: String?): ClassDoc? = + allTypes[qualifiedName]?.let { ClassDocumentationNodeAdapter(this, it) } + + override fun specifiedClasses(): Array<out ClassDoc> = classes() +} diff --git a/core/src/main/kotlin/javadoc/dokka-adapters.kt b/core/src/main/kotlin/javadoc/dokka-adapters.kt new file mode 100644 index 00000000..23ee1702 --- /dev/null +++ b/core/src/main/kotlin/javadoc/dokka-adapters.kt @@ -0,0 +1,30 @@ +package org.jetbrains.dokka.javadoc + +import com.google.inject.Inject +import com.sun.tools.doclets.formats.html.HtmlDoclet +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.FormatDescriptor + +class JavadocGenerator @Inject constructor (val options: DocumentationOptions, val logger: DokkaLogger) : Generator { + override fun buildPages(nodes: Iterable<DocumentationNode>) { + val module = nodes.single() as DocumentationModule + + DokkaConsoleLogger.report() + HtmlDoclet.start(ModuleNodeAdapter(module, StandardReporter(logger), options.outputDir)) + } + + override fun buildOutlines(nodes: Iterable<DocumentationNode>) { + // no outline could be generated separately + } + + override fun buildSupportFiles() { + } +} + +class JavadocFormatDescriptor : FormatDescriptor { + override val formatServiceClass = null + override val outlineServiceClass = null + override val generatorServiceClass = JavadocGenerator::class + override val packageDocumentationBuilderClass = KotlinAsJavaDocumentationBuilder::class + override val javaDocumentationBuilderClass = JavaPsiDocumentationBuilder::class +} diff --git a/core/src/main/kotlin/javadoc/reporter.kt b/core/src/main/kotlin/javadoc/reporter.kt new file mode 100644 index 00000000..fc38368c --- /dev/null +++ b/core/src/main/kotlin/javadoc/reporter.kt @@ -0,0 +1,34 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.DocErrorReporter +import com.sun.javadoc.SourcePosition +import org.jetbrains.dokka.DokkaLogger + +class StandardReporter(val logger: DokkaLogger) : DocErrorReporter { + override fun printWarning(msg: String?) { + logger.warn(msg.toString()) + } + + override fun printWarning(pos: SourcePosition?, msg: String?) { + logger.warn(format(pos, msg)) + } + + override fun printError(msg: String?) { + logger.error(msg.toString()) + } + + override fun printError(pos: SourcePosition?, msg: String?) { + logger.error(format(pos, msg)) + } + + override fun printNotice(msg: String?) { + logger.info(msg.toString()) + } + + override fun printNotice(pos: SourcePosition?, msg: String?) { + logger.info(format(pos, msg)) + } + + private fun format(pos: SourcePosition?, msg: String?) = + if (pos == null) msg.toString() else "${pos.file()}:${pos.line()}:${pos.column()}: $msg" +}
\ No newline at end of file diff --git a/core/src/main/kotlin/javadoc/source-position.kt b/core/src/main/kotlin/javadoc/source-position.kt new file mode 100644 index 00000000..0e4c6e3c --- /dev/null +++ b/core/src/main/kotlin/javadoc/source-position.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.SourcePosition +import org.jetbrains.dokka.DocumentationNode +import java.io.File + +class SourcePositionAdapter(val docNode: DocumentationNode) : SourcePosition { + + private val sourcePositionParts: List<String> by lazy { + docNode.details(DocumentationNode.Kind.SourcePosition).firstOrNull()?.name?.split(":") ?: emptyList() + } + + override fun file(): File? = if (sourcePositionParts.isEmpty()) null else File(sourcePositionParts[0]) + + override fun line(): Int = sourcePositionParts.getOrNull(1)?.toInt() ?: -1 + + override fun column(): Int = sourcePositionParts.getOrNull(2)?.toInt() ?: -1 +} diff --git a/core/src/main/kotlin/javadoc/tags.kt b/core/src/main/kotlin/javadoc/tags.kt new file mode 100644 index 00000000..5872dbaa --- /dev/null +++ b/core/src/main/kotlin/javadoc/tags.kt @@ -0,0 +1,214 @@ +package org.jetbrains.dokka.javadoc + +import com.sun.javadoc.* +import org.jetbrains.dokka.* +import java.util.* + +class TagImpl(val holder: Doc, val name: String, val text: String): Tag { + override fun text(): String? = text + + override fun holder(): Doc = holder + override fun firstSentenceTags(): Array<out Tag>? = arrayOf() + override fun inlineTags(): Array<out Tag>? = arrayOf() + + override fun name(): String = name + override fun kind(): String = name + + override fun position(): SourcePosition = holder.position() +} + +class TextTag(val holder: Doc, val content: ContentText) : Tag { + val plainText: String + get() = content.text + + override fun name(): String = "Text" + override fun kind(): String = name() + override fun text(): String? = plainText + override fun inlineTags(): Array<out Tag> = arrayOf(this) + override fun holder(): Doc = holder + override fun firstSentenceTags(): Array<out Tag> = arrayOf(this) + override fun position(): SourcePosition = holder.position() +} + +abstract class SeeTagAdapter(val holder: Doc, val content: ContentNodeLink) : SeeTag { + override fun position(): SourcePosition? = holder.position() + override fun name(): String = "@see" + override fun kind(): String = "@see" + override fun holder(): Doc = holder + + override fun text(): String? = content.node?.name ?: "(?)" +} + +class SeeExternalLinkTagAdapter(val holder: Doc, val link: ContentExternalLink) : SeeTag { + override fun position(): SourcePosition = holder.position() + override fun text(): String = label() + override fun inlineTags(): Array<out Tag> = emptyArray() // TODO + + override fun label(): String { + val label = link.asText() ?: link.href + return "<a href=\"${link.href}\">$label</a>" + } + + override fun referencedPackage(): PackageDoc? = null + override fun referencedClass(): ClassDoc? = null + override fun referencedMemberName(): String? = null + override fun referencedClassName(): String? = null + override fun referencedMember(): MemberDoc? = null + override fun holder(): Doc = holder + override fun firstSentenceTags(): Array<out Tag> = inlineTags() + override fun name(): String = "@link" + override fun kind(): String = "@see" +} + +fun ContentBlock.asText(): String? { + val contentText = children.singleOrNull() as? ContentText + return contentText?.text +} + +class SeeMethodTagAdapter(holder: Doc, val method: MethodAdapter, content: ContentNodeLink) : SeeTagAdapter(holder, content) { + override fun referencedMember(): MemberDoc = method + override fun referencedMemberName(): String = method.name() + override fun referencedPackage(): PackageDoc? = null + override fun referencedClass(): ClassDoc = method.containingClass() + override fun referencedClassName(): String = method.containingClass().name() + override fun label(): String = "${method.containingClass().name()}.${method.name()}" + + override fun inlineTags(): Array<out Tag> = emptyArray() // TODO + override fun firstSentenceTags(): Array<out Tag> = inlineTags() // TODO +} + +class SeeClassTagAdapter(holder: Doc, val clazz: ClassDocumentationNodeAdapter, content: ContentNodeLink) : SeeTagAdapter(holder, content) { + override fun referencedMember(): MemberDoc? = null + override fun referencedMemberName(): String? = null + override fun referencedPackage(): PackageDoc? = null + override fun referencedClass(): ClassDoc = clazz + override fun referencedClassName(): String = clazz.name() + override fun label(): String = "${clazz.classNode.kind.name.toLowerCase()} ${clazz.name()}" + + override fun inlineTags(): Array<out Tag> = emptyArray() // TODO + override fun firstSentenceTags(): Array<out Tag> = inlineTags() // TODO +} + +class ParamTagAdapter(val module: ModuleNodeAdapter, + val holder: Doc, + val parameterName: String, + val typeParameter: Boolean, + val content: List<ContentNode>) : ParamTag { + + constructor(module: ModuleNodeAdapter, holder: Doc, parameterName: String, isTypeParameter: Boolean, content: ContentNode) + : this(module, holder, parameterName, isTypeParameter, listOf(content)) { + } + + override fun name(): String = "@param" + override fun kind(): String = name() + override fun holder(): Doc = holder + override fun position(): SourcePosition? = holder.position() + + override fun text(): String = "@param $parameterName ..." + override fun inlineTags(): Array<out Tag> = content.flatMap { buildInlineTags(module, holder, it) }.toTypedArray() + override fun firstSentenceTags(): Array<out Tag> = arrayOf(TextTag(holder, ContentText(text()))) + + override fun isTypeParameter(): Boolean = typeParameter + override fun parameterComment(): String = content.toString() // TODO + override fun parameterName(): String = parameterName +} + + +class ThrowsTagAdapter(val holder: Doc, val type: ClassDocumentationNodeAdapter) : ThrowsTag { + override fun name(): String = "@throws" + override fun kind(): String = name() + override fun holder(): Doc = holder + override fun position(): SourcePosition? = holder.position() + + override fun text(): String = "@throws ${type.qualifiedTypeName()}" + override fun inlineTags(): Array<out Tag> = emptyArray() + override fun firstSentenceTags(): Array<out Tag> = emptyArray() + + override fun exceptionComment(): String = "" + override fun exceptionType(): Type = type + override fun exception(): ClassDoc = type + override fun exceptionName(): String = type.qualifiedName() +} + +fun buildInlineTags(module: ModuleNodeAdapter, holder: Doc, root: ContentNode): List<Tag> = ArrayList<Tag>().let { buildInlineTags(module, holder, root, it); it } + +private fun buildInlineTags(module: ModuleNodeAdapter, holder: Doc, nodes: List<ContentNode>, result: MutableList<Tag>) { + nodes.forEach { + buildInlineTags(module, holder, it, result) + } +} + + +private fun buildInlineTags(module: ModuleNodeAdapter, holder: Doc, node: ContentNode, result: MutableList<Tag>) { + fun surroundWith(module: ModuleNodeAdapter, holder: Doc, prefix: String, postfix: String, node: ContentBlock, result: MutableList<Tag>) { + if (node.children.isNotEmpty()) { + val open = TextTag(holder, ContentText(prefix)) + val close = TextTag(holder, ContentText(postfix)) + + result.add(open) + buildInlineTags(module, holder, node.children, result) + + if (result.last() === open) { + result.removeAt(result.lastIndex) + } else { + result.add(close) + } + } + } + + fun surroundWith(module: ModuleNodeAdapter, holder: Doc, prefix: String, postfix: String, node: ContentNode, result: MutableList<Tag>) { + if (node !is ContentEmpty) { + val open = TextTag(holder, ContentText(prefix)) + val close = TextTag(holder, ContentText(postfix)) + + result.add(open) + buildInlineTags(module, holder, node, result) + if (result.last() === open) { + result.removeAt(result.lastIndex) + } else { + result.add(close) + } + } + } + + when (node) { + is ContentText -> result.add(TextTag(holder, node)) + is ContentNodeLink -> { + val target = node.node + when (target?.kind) { + DocumentationNode.Kind.Function -> result.add(SeeMethodTagAdapter(holder, MethodAdapter(module, node.node!!), node)) + + in DocumentationNode.Kind.classLike -> result.add(SeeClassTagAdapter(holder, ClassDocumentationNodeAdapter(module, node.node!!), node)) + + else -> buildInlineTags(module, holder, node.children, result) + } + } + is ContentExternalLink -> result.add(SeeExternalLinkTagAdapter(holder, node)) + is ContentCode -> surroundWith(module, holder, "<code>", "</code>", node, result) + is ContentBlockCode -> surroundWith(module, holder, "<code><pre>", "</pre></code>", node, result) + is ContentEmpty -> {} + is ContentEmphasis -> surroundWith(module, holder, "<em>", "</em>", node, result) + is ContentHeading -> surroundWith(module, holder, "<h${node.level}>", "</h${node.level}>", node, result) + is ContentEntity -> result.add(TextTag(holder, ContentText(node.text))) // TODO ?? + is ContentIdentifier -> result.add(TextTag(holder, ContentText(node.text))) // TODO + is ContentKeyword -> result.add(TextTag(holder, ContentText(node.text))) // TODO + is ContentListItem -> surroundWith(module, holder, "<li>", "</li>", node, result) + is ContentOrderedList -> surroundWith(module, holder, "<ol>", "</ol>", node, result) + is ContentUnorderedList -> surroundWith(module, holder, "<ul>", "</ul>", node, result) + is ContentParagraph -> surroundWith(module, holder, "<p>", "</p>", node, result) + is ContentSection -> surroundWith(module, holder, "<p>", "</p>", node, result) // TODO how section should be represented? + is ContentNonBreakingSpace -> result.add(TextTag(holder, ContentText(" "))) + is ContentStrikethrough -> surroundWith(module, holder, "<strike>", "</strike>", node, result) + is ContentStrong -> surroundWith(module, holder, "<strong>", "</strong>", node, result) + is ContentSymbol -> result.add(TextTag(holder, ContentText(node.text))) // TODO? + is Content -> { + surroundWith(module, holder, "<p>", "</p>", node.summary, result) + surroundWith(module, holder, "<p>", "</p>", node.description, result) +// node.sections.forEach { +// buildInlineTags(module, holder, it, result) +// } + } + + else -> result.add(TextTag(holder, ContentText("$node"))) + } +}
\ No newline at end of file diff --git a/core/src/main/kotlin/main.kt b/core/src/main/kotlin/main.kt new file mode 100644 index 00000000..22e82991 --- /dev/null +++ b/core/src/main/kotlin/main.kt @@ -0,0 +1,262 @@ +package org.jetbrains.dokka + +import com.google.inject.Guice +import com.google.inject.Injector +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiManager +import com.sampullara.cli.Args +import com.sampullara.cli.Argument +import org.jetbrains.dokka.Utilities.DokkaModule +import org.jetbrains.kotlin.cli.common.arguments.ValueDescription +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.resolve.LazyTopDownAnalyzerForTopLevel +import org.jetbrains.kotlin.resolve.TopDownAnalysisMode +import org.jetbrains.kotlin.utils.PathUtil +import java.io.File +import kotlin.util.measureTimeMillis + +class DokkaArguments { + @set:Argument(value = "src", description = "Source file or directory (allows many paths separated by the system path separator)") + @ValueDescription("<path>") + public var src: String = "" + + @set:Argument(value = "srcLink", description = "Mapping between a source directory and a Web site for browsing the code") + @ValueDescription("<path>=<url>[#lineSuffix]") + public var srcLink: String = "" + + @set:Argument(value = "include", description = "Markdown files to load (allows many paths separated by the system path separator)") + @ValueDescription("<path>") + public var include: String = "" + + @set:Argument(value = "samples", description = "Source root for samples") + @ValueDescription("<path>") + public var samples: String = "" + + @set:Argument(value = "output", description = "Output directory path") + @ValueDescription("<path>") + public var outputDir: String = "out/doc/" + + @set:Argument(value = "format", description = "Output format (text, html, markdown, jekyll, kotlin-website)") + @ValueDescription("<name>") + public var outputFormat: String = "html" + + @set:Argument(value = "module", description = "Name of the documentation module") + @ValueDescription("<name>") + public var moduleName: String = "" + + @set:Argument(value = "classpath", description = "Classpath for symbol resolution") + @ValueDescription("<path>") + public var classpath: String = "" + + @set:Argument(value = "nodeprecated", description = "Exclude deprecated members from documentation") + public var nodeprecated: Boolean = false + +} + +private fun parseSourceLinkDefinition(srcLink: String): SourceLinkDefinition { + val (path, urlAndLine) = srcLink.split('=') + return SourceLinkDefinition(File(path).absolutePath, + urlAndLine.substringBefore("#"), + urlAndLine.substringAfter("#", "").let { if (it.isEmpty()) null else "#" + it }) +} + +public fun main(args: Array<String>) { + val arguments = DokkaArguments() + val freeArgs: List<String> = Args.parse(arguments, args) ?: listOf() + val sources = if (arguments.src.isNotEmpty()) arguments.src.split(File.pathSeparatorChar).toList() + freeArgs else freeArgs + val samples = if (arguments.samples.isNotEmpty()) arguments.samples.split(File.pathSeparatorChar).toList() else listOf() + val includes = if (arguments.include.isNotEmpty()) arguments.include.split(File.pathSeparatorChar).toList() else listOf() + + val sourceLinks = if (arguments.srcLink.isNotEmpty() && arguments.srcLink.contains("=")) + listOf(parseSourceLinkDefinition(arguments.srcLink)) + else { + if (arguments.srcLink.isNotEmpty()) { + println("Warning: Invalid -srcLink syntax. Expected: <path>=<url>[#lineSuffix]. No source links will be generated.") + } + listOf() + } + + val classPath = arguments.classpath.split(File.pathSeparatorChar).toList() + val generator = DokkaGenerator( + DokkaConsoleLogger, + classPath, + sources, + samples, + includes, + arguments.moduleName, + arguments.outputDir.let { if (it.endsWith('/')) it else it + '/' }, + arguments.outputFormat, + sourceLinks, + arguments.nodeprecated) + + generator.generate() + DokkaConsoleLogger.report() +} + +interface DokkaLogger { + fun info(message: String) + fun warn(message: String) + fun error(message: String) +} + +object DokkaConsoleLogger: DokkaLogger { + var warningCount: Int = 0 + + override fun info(message: String) = println(message) + override fun warn(message: String) { + println("WARN: $message") + warningCount++ + } + + override fun error(message: String) = println("ERROR: $message") + + fun report() { + if (warningCount > 0) { + println("generation completed with $warningCount warnings") + } else { + println("generation completed successfully") + } + } +} + +class DokkaMessageCollector(val logger: DokkaLogger): MessageCollector { + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation) { + logger.error(MessageRenderer.PLAIN_FULL_PATHS.render(severity, message, location)) + } +} + +class DokkaGenerator(val logger: DokkaLogger, + val classpath: List<String>, + val sources: List<String>, + val samples: List<String>, + val includes: List<String>, + val moduleName: String, + val outputDir: String, + val outputFormat: String, + val sourceLinks: List<SourceLinkDefinition>, + val skipDeprecated: Boolean = false) { + fun generate() { + val environment = createAnalysisEnvironment() + + logger.info("Module: $moduleName") + logger.info("Output: ${File(outputDir)}") + logger.info("Sources: ${environment.sources.joinToString()}") + logger.info("Classpath: ${environment.classpath.joinToString()}") + + logger.info("Analysing sources and libraries... ") + val startAnalyse = System.currentTimeMillis() + + val options = DocumentationOptions(outputDir, outputFormat, false, sourceLinks = sourceLinks, skipDeprecated = skipDeprecated) + + val injector = Guice.createInjector(DokkaModule(environment, options, logger)) + + val documentation = buildDocumentationModule(injector, moduleName, { isSample(it) }, includes) + + val timeAnalyse = System.currentTimeMillis() - startAnalyse + logger.info("done in ${timeAnalyse / 1000} secs") + + val timeBuild = measureTimeMillis { + logger.info("Generating pages... ") + injector.getInstance(Generator::class.java).buildAll(documentation) + } + logger.info("done in ${timeBuild / 1000} secs") + + Disposer.dispose(environment) + } + + fun createAnalysisEnvironment(): AnalysisEnvironment { + val environment = AnalysisEnvironment(DokkaMessageCollector(logger)) + + environment.apply { + addClasspath(PathUtil.getJdkClassesRoots()) + // addClasspath(PathUtil.getKotlinPathsForCompiler().getRuntimePath()) + for (element in this@DokkaGenerator.classpath) { + addClasspath(File(element)) + } + + addSources(this@DokkaGenerator.sources) + addSources(this@DokkaGenerator.samples) + } + + return environment + } + + fun isSample(file: PsiFile): Boolean { + val sourceFile = File(file.virtualFile!!.path) + return samples.none { sample -> + val canonicalSample = File(sample).canonicalPath + val canonicalSource = sourceFile.canonicalPath + canonicalSource.startsWith(canonicalSample) + } + } +} + +fun buildDocumentationModule(injector: Injector, + moduleName: String, + filesToDocumentFilter: (PsiFile) -> Boolean = { file -> true }, + includes: List<String> = listOf()): DocumentationModule { + + val coreEnvironment = injector.getInstance(KotlinCoreEnvironment::class.java) + val fragmentFiles = coreEnvironment.getSourceFiles().filter(filesToDocumentFilter) + + val resolutionFacade = injector.getInstance(DokkaResolutionFacade::class.java) + val analyzer = resolutionFacade.getFrontendService(LazyTopDownAnalyzerForTopLevel::class.java) + analyzer.analyzeDeclarations(TopDownAnalysisMode.TopLevelDeclarations, fragmentFiles) + + val fragments = fragmentFiles + .map { resolutionFacade.resolveSession.getPackageFragment(it.packageFqName) } + .filterNotNull() + .distinct() + + val packageDocs = injector.getInstance(PackageDocs::class.java) + for (include in includes) { + packageDocs.parse(include, fragments.firstOrNull()) + } + val documentationModule = DocumentationModule(moduleName, packageDocs.moduleContent) + + with(injector.getInstance(DocumentationBuilder::class.java)) { + documentationModule.appendFragments(fragments, packageDocs.packageContent, + injector.getInstance(PackageDocumentationBuilder::class.java)) + } + + val javaFiles = coreEnvironment.getJavaSourceFiles().filter(filesToDocumentFilter) + with(injector.getInstance(JavaDocumentationBuilder::class.java)) { + javaFiles.map { appendFile(it, documentationModule, packageDocs.packageContent) } + } + + injector.getInstance(NodeReferenceGraph::class.java).resolveReferences() + + return documentationModule +} + + +fun KotlinCoreEnvironment.getJavaSourceFiles(): List<PsiJavaFile> { + val sourceRoots = configuration.get(CommonConfigurationKeys.CONTENT_ROOTS) + ?.filterIsInstance<JavaSourceRoot>() + ?.map { it.file } + ?: listOf() + + val result = arrayListOf<PsiJavaFile>() + val localFileSystem = VirtualFileManager.getInstance().getFileSystem("file") + sourceRoots.forEach { sourceRoot -> + sourceRoot.absoluteFile.walkTopDown().forEach { + val vFile = localFileSystem.findFileByPath(it.path) + if (vFile != null) { + val psiFile = PsiManager.getInstance(project).findFile(vFile) + if (psiFile is PsiJavaFile) { + result.add(psiFile) + } + } + } + } + return result +} diff --git a/core/src/main/resources/META-INF/MANIFEST.MF b/core/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 00000000..78fabddc --- /dev/null +++ b/core/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Class-Path: kotlin-plugin.jar +Main-Class: org.jetbrains.dokka.DokkaPackage + diff --git a/core/src/main/resources/dokka-antlib.xml b/core/src/main/resources/dokka-antlib.xml new file mode 100644 index 00000000..9c3373d5 --- /dev/null +++ b/core/src/main/resources/dokka-antlib.xml @@ -0,0 +1,3 @@ +<antlib> + <taskdef name="dokka" classname="org.jetbrains.dokka.ant.DokkaAntTask"/> +</antlib> diff --git a/core/src/main/resources/dokka/format/html-as-java.properties b/core/src/main/resources/dokka/format/html-as-java.properties new file mode 100644 index 00000000..f598f377 --- /dev/null +++ b/core/src/main/resources/dokka/format/html-as-java.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.HtmlAsJavaFormatDescriptor +description=Produces output in HTML format using Java syntax
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/html.properties b/core/src/main/resources/dokka/format/html.properties new file mode 100644 index 00000000..7881dfae --- /dev/null +++ b/core/src/main/resources/dokka/format/html.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.HtmlFormatDescriptor +description=Produces output in HTML format
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/javadoc.properties b/core/src/main/resources/dokka/format/javadoc.properties new file mode 100644 index 00000000..a58317fc --- /dev/null +++ b/core/src/main/resources/dokka/format/javadoc.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.javadoc.JavadocFormatDescriptor
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/jekyll.properties b/core/src/main/resources/dokka/format/jekyll.properties new file mode 100644 index 00000000..b11401a4 --- /dev/null +++ b/core/src/main/resources/dokka/format/jekyll.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.JekyllFormatDescriptor +description=Produces documentation in Jekyll format
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/kotlin-website.properties b/core/src/main/resources/dokka/format/kotlin-website.properties new file mode 100644 index 00000000..c13e7675 --- /dev/null +++ b/core/src/main/resources/dokka/format/kotlin-website.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.KotlinWebsiteFormatDescriptor +description=Generates Kotlin website documentation
\ No newline at end of file diff --git a/core/src/main/resources/dokka/format/markdown.properties b/core/src/main/resources/dokka/format/markdown.properties new file mode 100644 index 00000000..6217a6df --- /dev/null +++ b/core/src/main/resources/dokka/format/markdown.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.Formats.MarkdownFormatDescriptor +description=Produces documentation in markdown format
\ No newline at end of file diff --git a/core/src/main/resources/dokka/generator/default.properties b/core/src/main/resources/dokka/generator/default.properties new file mode 100644 index 00000000..a4a16200 --- /dev/null +++ b/core/src/main/resources/dokka/generator/default.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.FileGenerator +description=Default documentation generator
\ No newline at end of file diff --git a/core/src/main/resources/dokka/generator/javadoc.properties b/core/src/main/resources/dokka/generator/javadoc.properties new file mode 100644 index 00000000..4075704f --- /dev/null +++ b/core/src/main/resources/dokka/generator/javadoc.properties @@ -0,0 +1,2 @@ +class=org.jetbrains.dokka.javadoc.JavadocGenerator +description=Produces output via JDK javadoc tool
\ No newline at end of file diff --git a/core/src/main/resources/dokka/language/java.properties b/core/src/main/resources/dokka/language/java.properties new file mode 100644 index 00000000..ab42f532 --- /dev/null +++ b/core/src/main/resources/dokka/language/java.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.JavaLanguageService
\ No newline at end of file diff --git a/core/src/main/resources/dokka/language/kotlin.properties b/core/src/main/resources/dokka/language/kotlin.properties new file mode 100644 index 00000000..16092007 --- /dev/null +++ b/core/src/main/resources/dokka/language/kotlin.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.KotlinLanguageService
\ No newline at end of file diff --git a/core/src/main/resources/dokka/outline/yaml.properties b/core/src/main/resources/dokka/outline/yaml.properties new file mode 100644 index 00000000..7268af37 --- /dev/null +++ b/core/src/main/resources/dokka/outline/yaml.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.YamlOutlineService
\ No newline at end of file diff --git a/core/src/main/resources/dokka/styles/style.css b/core/src/main/resources/dokka/styles/style.css new file mode 100644 index 00000000..09586237 --- /dev/null +++ b/core/src/main/resources/dokka/styles/style.css @@ -0,0 +1,280 @@ +@import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700); + +body, table { + padding:50px; + font:14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; + color:#555; + font-weight:300; +} + +.keyword { + color:black; + font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + font-size:12px; +} + +.symbol { + font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + font-size:12px; +} + +.identifier { + color: darkblue; + font-size:12px; + font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; +} + +h1, h2, h3, h4, h5, h6 { + color:#222; + margin:0 0 20px; +} + +p, ul, ol, table, pre, dl { + margin:0 0 20px; +} + +h1, h2, h3 { + line-height:1.1; +} + +h1 { + font-size:28px; +} + +h2 { + color:#393939; +} + +h3, h4, h5, h6 { + color:#494949; +} + +a { + color:#258aaf; + font-weight:400; + text-decoration:none; +} + +a:hover { + color: inherit; + text-decoration:underline; +} + +a small { + font-size:11px; + color:#555; + margin-top:-0.6em; + display:block; +} + +.wrapper { + width:860px; + margin:0 auto; +} + +blockquote { + border-left:1px solid #e5e5e5; + margin:0; + padding:0 0 0 20px; + font-style:italic; +} + +code, pre { + font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + color:#333; + font-size:12px; +} + +pre { + display: block; +/* + padding:8px 8px; + background: #f8f8f8; + border-radius:5px; + border:1px solid #e5e5e5; +*/ + overflow-x: auto; +} + +table { + width:100%; + border-collapse:collapse; +} + +th, td { + text-align:left; + vertical-align: top; + padding:5px 10px; +} + +dt { + color:#444; + font-weight:700; +} + +th { + color:#444; +} + +img { + max-width:100%; +} + +header { + width:270px; + float:left; + position:fixed; +} + +header ul { + list-style:none; + height:40px; + + padding:0; + + background: #eee; + background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); + background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + + border-radius:5px; + border:1px solid #d2d2d2; + box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; + width:270px; +} + +header li { + width:89px; + float:left; + border-right:1px solid #d2d2d2; + height:40px; +} + +header ul a { + line-height:1; + font-size:11px; + color:#999; + display:block; + text-align:center; + padding-top:6px; + height:40px; +} + +strong { + color:#222; + font-weight:700; +} + +header ul li + li { + width:88px; + border-left:1px solid #fff; +} + +header ul li + li + li { + border-right:none; + width:89px; +} + +header ul a strong { + font-size:14px; + display:block; + color:#222; +} + +section { + width:500px; + float:right; + padding-bottom:50px; +} + +small { + font-size:11px; +} + +hr { + border:0; + background:#e5e5e5; + height:1px; + margin:0 0 20px; +} + +footer { + width:270px; + float:left; + position:fixed; + bottom:50px; +} + +@media print, screen and (max-width: 960px) { + + div.wrapper { + width:auto; + margin:0; + } + + header, section, footer { + float:none; + position:static; + width:auto; + } + + header { + padding-right:320px; + } + + section { + border:1px solid #e5e5e5; + border-width:1px 0; + padding:20px 0; + margin:0 0 20px; + } + + header a small { + display:inline; + } + + header ul { + position:absolute; + right:50px; + top:52px; + } +} + +@media print, screen and (max-width: 720px) { + body { + word-wrap:break-word; + } + + header { + padding:0; + } + + header ul, header p.view { + position:static; + } + + pre, code { + word-wrap:normal; + } +} + +@media print, screen and (max-width: 480px) { + body { + padding:15px; + } + + header ul { + display:none; + } +} + +@media print { + body { + padding:0.4in; + font-size:12pt; + color:#444; + } +} diff --git a/core/src/main/resources/format/javadoc.properties b/core/src/main/resources/format/javadoc.properties new file mode 100644 index 00000000..a58317fc --- /dev/null +++ b/core/src/main/resources/format/javadoc.properties @@ -0,0 +1 @@ +class=org.jetbrains.dokka.javadoc.JavadocFormatDescriptor
\ No newline at end of file diff --git a/core/src/test/kotlin/TestAPI.kt b/core/src/test/kotlin/TestAPI.kt new file mode 100644 index 00000000..d7833e36 --- /dev/null +++ b/core/src/test/kotlin/TestAPI.kt @@ -0,0 +1,214 @@ +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 org.jetbrains.dokka.* +import org.jetbrains.dokka.Utilities.DokkaModule +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.config.ContentRoot +import org.jetbrains.kotlin.config.KotlinSourceRoot +import org.junit.Assert +import java.io.File +import kotlin.test.fail + +public fun verifyModel(vararg roots: ContentRoot, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + format: String = "html", + verifier: (DocumentationModule) -> Unit) { + val messageCollector = object : MessageCollector { + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation) { + when (severity) { + CompilerMessageSeverity.WARNING, + CompilerMessageSeverity.LOGGING, + CompilerMessageSeverity.OUTPUT, + CompilerMessageSeverity.INFO, + CompilerMessageSeverity.ERROR -> { + println("$severity: $message at $location") + } + CompilerMessageSeverity.EXCEPTION -> { + fail("$severity: $message at $location") + } + } + } + } + + 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 kotlinPairRoot = PathManager.getResourceRoot(Pair::class.java, "/kotlin/Pair.class") + addClasspath(File(kotlinPairRoot)) + } + addRoots(roots.toList()) + } + val options = DocumentationOptions("", format, includeNonPublic = true, skipEmptyPackages = false, sourceLinks = listOf<SourceLinkDefinition>()) + val injector = Guice.createInjector(DokkaModule(environment, options, DokkaConsoleLogger)) + val documentation = buildDocumentationModule(injector, "test") + verifier(documentation) + Disposer.dispose(environment) +} + +public fun verifyModel(source: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + format: String = "html", + 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, + verifier = verifier) +} + +public 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()) + } +} + +public fun verifyJavaModel(source: String, + withKotlinRuntime: Boolean = false, + verifier: (DocumentationModule) -> Unit) { + val tempDir = FileUtil.createTempDirectory("dokka", "") + try { + val sourceFile = File(source) + FileUtil.copy(sourceFile, File(tempDir, sourceFile.name)) + verifyModel(JavaSourceRoot(tempDir, null), withJdk = true, withKotlinRuntime = withKotlinRuntime, verifier = verifier) + } + finally { + FileUtil.delete(tempDir) + } +} + +public fun verifyJavaPackageMember(source: String, + withKotlinRuntime: Boolean = false, + verifier: (DocumentationNode) -> Unit) { + verifyJavaModel(source, withKotlinRuntime) { model -> + val pkg = model.members.single() + verifier(pkg.members.single()) + } +} + +public fun verifyOutput(roots: Array<ContentRoot>, + outputExtension: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + verifyModel(*roots, withJdk = withJdk, withKotlinRuntime = withKotlinRuntime) { + verifyModelOutput(it, outputExtension, outputGenerator, roots.first().path) + } +} + +private fun verifyModelOutput(it: DocumentationModule, + outputExtension: String, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit, + sourcePath: String) { + val output = StringBuilder() + outputGenerator(it, output) + val ext = outputExtension.removePrefix(".") + val path = sourcePath + val expectedOutput = File(path.replaceAfterLast(".", ext, path + "." + ext)).readText() + assertEqualsIgnoringSeparators(expectedOutput, output.toString()) +} + +public fun verifyOutput(path: String, + outputExtension: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + verifyOutput(arrayOf(contentRootFromPath(path)), outputExtension, withJdk, withKotlinRuntime, outputGenerator) +} + +public fun verifyJavaOutput(path: String, + outputExtension: String, + withKotlinRuntime: Boolean = false, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + verifyJavaModel(path, withKotlinRuntime) { model -> + verifyModelOutput(model, outputExtension, outputGenerator, path) + } +} + +public 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 -> { + appendln("[code]") + appendChildren(node) + appendln() + appendln("[/code]") + } + is ContentNodeLink -> { + append("[") + appendChildren(node) + append(" -> ") + append(node.node.toString()) + append("]") + } + is ContentBlock -> { + appendChildren(node) + } + 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() +} + +class InMemoryLocation(override val path: String): Location { + override fun relativePathTo(other: Location, anchor: String?): String = + if (anchor != null) other.path + "#" + anchor else other.path +} + +object InMemoryLocationService: LocationService { + override fun location(qualifiedName: List<String>, hasMembers: Boolean) = + InMemoryLocation(relativePathToNode(qualifiedName, hasMembers)) + + override val root: Location + get() = InMemoryLocation("") +} + +val tempLocation = InMemoryLocation("") + +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/HtmlFormatTest.kt b/core/src/test/kotlin/format/HtmlFormatTest.kt new file mode 100644 index 00000000..593dbfe6 --- /dev/null +++ b/core/src/test/kotlin/format/HtmlFormatTest.kt @@ -0,0 +1,157 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.HtmlFormatService +import org.jetbrains.dokka.HtmlTemplateService +import org.jetbrains.dokka.KotlinLanguageService +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.config.KotlinSourceRoot +import org.junit.Test +import java.io.File + +public class HtmlFormatTest { + private val htmlService = HtmlFormatService(InMemoryLocationService, KotlinLanguageService(), HtmlTemplateService.default()) + + @Test fun classWithCompanionObject() { + verifyOutput("testdata/format/classWithCompanionObject.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun htmlEscaping() { + verifyOutput("testdata/format/htmlEscaping.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun overloads() { + verifyOutput("testdata/format/overloads.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members) + } + } + + @Test fun overloadsWithDescription() { + verifyOutput("testdata/format/overloadsWithDescription.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun overloadsWithDifferentDescriptions() { + verifyOutput("testdata/format/overloadsWithDifferentDescriptions.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun deprecated() { + verifyOutput("testdata/format/deprecated.kt", ".package.html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members) + } + verifyOutput("testdata/format/deprecated.kt", ".class.html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun brokenLink() { + verifyOutput("testdata/format/brokenLink.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun codeSpan() { + verifyOutput("testdata/format/codeSpan.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun parenthesis() { + verifyOutput("testdata/format/parenthesis.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun bracket() { + verifyOutput("testdata/format/bracket.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun see() { + verifyOutput("testdata/format/see.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun tripleBackticks() { + verifyOutput("testdata/format/tripleBackticks.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun typeLink() { + verifyOutput("testdata/format/typeLink.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members.filter { it.name == "Bar"} ) + } + } + + @Test fun parameterAnchor() { + verifyOutput("testdata/format/parameterAnchor.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun javaSupertypeLink() { + verifyJavaOutput("testdata/format/javaSupertype.java", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members.single { it.name == "C"}.members.filter { it.name == "Bar"} ) + } + } + + @Test fun javaLinkTag() { + verifyJavaOutput("testdata/format/javaLinkTag.java", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun javaLinkTagWithLabel() { + verifyJavaOutput("testdata/format/javaLinkTagWithLabel.java", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun javaSeeTag() { + verifyJavaOutput("testdata/format/javaSeeTag.java", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun javaDeprecated() { + verifyJavaOutput("testdata/format/javaDeprecated.java", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, 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"), + JavaSourceRoot(File("testdata/format/crossLanguage/kotlinExtendsJava"), null)), + ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members.filter { it.name == "Bar" }) + } + } + + @Test fun orderedList() { + verifyOutput("testdata/format/orderedList.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members.filter { it.name == "Bar" }) + } + } + + @Test fun linkWithLabel() { + verifyOutput("testdata/format/linkWithLabel.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members.filter { it.name == "Bar" }) + } + } + + @Test fun entity() { + verifyOutput("testdata/format/entity.kt", ".html") { model, output -> + htmlService.appendNodes(tempLocation, output, model.members.single().members.filter { it.name == "Bar" }) + } + } +} + diff --git a/core/src/test/kotlin/format/MarkdownFormatTest.kt b/core/src/test/kotlin/format/MarkdownFormatTest.kt new file mode 100644 index 00000000..e2339707 --- /dev/null +++ b/core/src/test/kotlin/format/MarkdownFormatTest.kt @@ -0,0 +1,218 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.KotlinLanguageService +import org.jetbrains.dokka.MarkdownFormatService +import org.junit.Test + +public class MarkdownFormatTest { + private val markdownService = MarkdownFormatService(InMemoryLocationService, KotlinLanguageService()) + + @Test fun emptyDescription() { + verifyOutput("testdata/format/emptyDescription.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun classWithCompanionObject() { + verifyOutput("testdata/format/classWithCompanionObject.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun annotations() { + verifyOutput("testdata/format/annotations.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun annotationClass() { + verifyOutput("testdata/format/annotationClass.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun annotationParams() { + verifyOutput("testdata/format/annotationParams.kt", ".md", withKotlinRuntime = true) { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun extensions() { + verifyOutput("testdata/format/extensions.kt", ".package.md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members) + } + verifyOutput("testdata/format/extensions.kt", ".class.md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun enumClass() { + verifyOutput("testdata/format/enumClass.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + verifyOutput("testdata/format/enumClass.kt", ".value.md") { model, output -> + val enumClassNode = model.members.single().members[0] + markdownService.appendNodes(tempLocation, output, + enumClassNode.members.filter { it.name == "LOCAL_CONTINUE_AND_BREAK" }) + } + } + + @Test fun varargsFunction() { + verifyOutput("testdata/format/varargsFunction.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun overridingFunction() { + verifyOutput("testdata/format/overridingFunction.kt", ".md") { model, output -> + val classMembers = model.members.single().members.first { it.name == "D" }.members + markdownService.appendNodes(tempLocation, output, classMembers.filter { it.name == "f" }) + } + + } + + @Test fun propertyVar() { + verifyOutput("testdata/format/propertyVar.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun functionWithDefaultParameter() { + verifyOutput("testdata/format/functionWithDefaultParameter.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun accessor() { + verifyOutput("testdata/format/accessor.kt", ".md") { model, output -> + val propertyNode = model.members.single().members.first { it.name == "C" }.members.filter { it.name == "x" } + markdownService.appendNodes(tempLocation, output, propertyNode) + } + } + + @Test fun paramTag() { + verifyOutput("testdata/format/paramTag.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun throwsTag() { + verifyOutput("testdata/format/throwsTag.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun typeParameterBounds() { + verifyOutput("testdata/format/typeParameterBounds.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun typeParameterVariance() { + verifyOutput("testdata/format/typeParameterVariance.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun typeProjectionVariance() { + verifyOutput("testdata/format/typeProjectionVariance.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun javadocHtml() { + verifyJavaOutput("testdata/format/javadocHtml.java", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun javaCodeLiteralTags() { + verifyJavaOutput("testdata/format/javaCodeLiteralTags.java", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun javaCodeInParam() { + verifyJavaOutput("testdata/format/javaCodeInParam.java", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun javaSpaceInAuthor() { + verifyJavaOutput("testdata/format/javaSpaceInAuthor.java", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun nullability() { + verifyOutput("testdata/format/nullability.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun operatorOverloading() { + verifyOutput("testdata/format/operatorOverloading.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members.single { it.name == "C" }.members.filter { it.name == "plus" }) + } + } + + @Test fun javadocOrderedList() { + verifyJavaOutput("testdata/format/javadocOrderedList.java", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members.filter { it.name == "Bar" }) + } + } + + @Test fun companionObjectExtension() { + verifyOutput("testdata/format/companionObjectExtension.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members.filter { it.name == "Foo" }) + } + } + + @Test fun starProjection() { + verifyOutput("testdata/format/starProjection.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun extensionFunctionParameter() { + verifyOutput("testdata/format/extensionFunctionParameter.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun summarizeSignatures() { + verifyOutput("testdata/format/summarizeSignatures.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members) + } + } + + @Test fun summarizeSignaturesProperty() { + verifyOutput("testdata/format/summarizeSignaturesProperty.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members) + } + } + + @Test fun reifiedTypeParameter() { + verifyOutput("testdata/format/reifiedTypeParameter.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun annotatedTypeParameter() { + verifyOutput("testdata/format/annotatedTypeParameter.kt", ".md", withKotlinRuntime = true) { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } + + @Test fun inheritedMembers() { + verifyOutput("testdata/format/inheritedMembers.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members.filter { it.name == "Bar" }) + } + } + + @Test fun inheritedExtensions() { + verifyOutput("testdata/format/inheritedExtensions.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members.filter { it.name == "Bar" }) + } + } +} diff --git a/core/src/test/kotlin/format/PackageDocsTest.kt b/core/src/test/kotlin/format/PackageDocsTest.kt new file mode 100644 index 00000000..4d7852da --- /dev/null +++ b/core/src/test/kotlin/format/PackageDocsTest.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka.tests.format + +import org.jetbrains.dokka.ContentBlock +import org.jetbrains.dokka.ContentText +import org.jetbrains.dokka.DokkaConsoleLogger +import org.jetbrains.dokka.PackageDocs +import org.junit.Test +import kotlin.test.assertEquals + +public class PackageDocsTest { + @Test fun verifyParse() { + val docs = PackageDocs(null, DokkaConsoleLogger) + docs.parse("testdata/packagedocs/stdlib.md", null) + val packageContent = docs.packageContent["kotlin"]!! + val block = (packageContent.children.single() as ContentBlock).children.first() as ContentText + assertEquals("Core functions and types", block.text) + } +} diff --git a/core/src/test/kotlin/javadoc/JavadocTest.kt b/core/src/test/kotlin/javadoc/JavadocTest.kt new file mode 100644 index 00000000..4f0049ac --- /dev/null +++ b/core/src/test/kotlin/javadoc/JavadocTest.kt @@ -0,0 +1,44 @@ +package org.jetbrains.dokka.javadoc + +import org.jetbrains.dokka.DokkaConsoleLogger +import org.jetbrains.dokka.tests.verifyModel +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class JavadocTest { + @Test fun testTypes() { + verifyModel("testdata/javadoc/types.kt", format = "javadoc", withJdk = true) { model -> + val doc = ModuleNodeAdapter(model, StandardReporter(DokkaConsoleLogger), "") + val classDoc = doc.classNamed("foo.TypesKt")!! + val method = classDoc.methods().find { it.name() == "foo" }!! + + val type = method.returnType() + assertFalse(type.asClassDoc().isIncluded) + assertEquals("java.lang.String", type.qualifiedTypeName()) + assertEquals("java.lang.String", type.asClassDoc().qualifiedName()) + + val params = method.parameters() + assertTrue(params[0].type().isPrimitive) + assertFalse(params[1].type().asClassDoc().isIncluded) + } + } + + @Test fun testObject() { + verifyModel("testdata/javadoc/obj.kt", format = "javadoc") { model -> + val doc = ModuleNodeAdapter(model, StandardReporter(DokkaConsoleLogger), "") + + 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) + } + } + +} diff --git a/core/src/test/kotlin/markdown/ParserTest.kt b/core/src/test/kotlin/markdown/ParserTest.kt new file mode 100644 index 00000000..5a7adf05 --- /dev/null +++ b/core/src/test/kotlin/markdown/ParserTest.kt @@ -0,0 +1,142 @@ +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 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 00000000..981791c4 --- /dev/null +++ b/core/src/test/kotlin/model/ClassTest.kt @@ -0,0 +1,275 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.Content +import org.jetbrains.dokka.DocumentationNode +import org.jetbrains.dokka.DocumentationReference +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +public class ClassTest { + @Test fun emptyClass() { + verifyModel("testdata/classes/emptyClass.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(DocumentationNode.Kind.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(DocumentationNode.Kind.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(DocumentationNode.Kind.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(DocumentationNode.Kind.Constructor, kind) + assertEquals(2, details.count()) + assertEquals("public", details.elementAt(0).name) + with(details.elementAt(1)) { + assertEquals("name", name) + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals(Content.Empty, content) + assertEquals("String", details.single().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(DocumentationNode.Kind.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(DocumentationNode.Kind.Constructor, kind) + assertEquals(1, 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(DocumentationNode.Kind.Function, kind) + assertEquals("Unit", detail(DocumentationNode.Kind.Type).name) + assertTrue(links.none()) + assertTrue(members.none()) + } + } + } + } + + @Test fun classWithProperty() { + verifyModel("testdata/classes/classWithProperty.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(DocumentationNode.Kind.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(DocumentationNode.Kind.Constructor, kind) + assertEquals(1, 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(DocumentationNode.Kind.Property, kind) + assertEquals("String", detail(DocumentationNode.Kind.Type).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + } + + @Test fun classWithCompanionObject() { + verifyModel("testdata/classes/classWithCompanionObject.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(DocumentationNode.Kind.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("x", name) + assertEquals(DocumentationNode.Kind.CompanionObjectProperty, kind) + assertTrue(members.none()) + assertTrue(links.none()) + } + with(members.elementAt(2)) { + assertEquals("foo", name) + assertEquals(DocumentationNode.Kind.CompanionObjectFunction, 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(DocumentationNode.Kind.Annotation, kind) + } + } + } + + @Test fun dataClass() { + verifyPackageMember("testdata/classes/dataClass.kt") { cls -> + val modifiers = cls.details(DocumentationNode.Kind.Modifier).map { it.name } + assertTrue("data" in modifiers) + } + } + + @Test fun sealedClass() { + verifyPackageMember("testdata/classes/sealedClass.kt") { cls -> + val modifiers = cls.details(DocumentationNode.Kind.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) + assertEquals(DocumentationNode.Kind.Annotation, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.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(DocumentationNode.Kind.Annotation, kind) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.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(DocumentationNode.Kind.Modifier) + assertEquals(2, modifiers.size) + assertEquals("final", modifiers[1].name) + + val overrideReferences = references(DocumentationReference.Kind.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(DocumentationNode.Kind.Modifier) + assertEquals(2, modifiers.size) + assertEquals("final", modifiers[1].name) + + val overrideReferences = references(DocumentationReference.Kind.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(DocumentationNode.Kind.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 == DocumentationNode.Kind.CompanionObjectProperty } + assertEquals(1, extensions.size) + } + } + + @Test fun secondaryConstructor() { + verifyPackageMember("testdata/classes/secondaryConstructor.kt") { cls -> + val constructors = cls.members(DocumentationNode.Kind.Constructor) + assertEquals(2, constructors.size) + with (constructors.first { it.details(DocumentationNode.Kind.Parameter).size == 1}) { + assertEquals("<init>", name) + assertEquals("This is a secondary constructor.", summary.toTestString()) + } + } + } +} diff --git a/core/src/test/kotlin/model/CommentTest.kt b/core/src/test/kotlin/model/CommentTest.kt new file mode 100644 index 00000000..f3792610 --- /dev/null +++ b/core/src/test/kotlin/model/CommentTest.kt @@ -0,0 +1,153 @@ +package org.jetbrains.dokka.tests + +import org.junit.Test +import kotlin.test.* +import org.jetbrains.dokka.* + +public class CommentTest { + @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 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.first()) { + assertEquals("Summary", content.summary.toTestString()) + with (content.description) { + assertEqualsIgnoringSeparators("""[code] +if (true) { + println(property) +} +[/code] +[code] +if (true) { + println(property) +} +[/code] +[code] +if (true) { + println(property) +} +[/code] +[code] +if (true) { + println(property) +} +[/code] +""", toTestString()) + } + } + } + } +} diff --git a/core/src/test/kotlin/model/FunctionTest.kt b/core/src/test/kotlin/model/FunctionTest.kt new file mode 100644 index 00000000..83fd8223 --- /dev/null +++ b/core/src/test/kotlin/model/FunctionTest.kt @@ -0,0 +1,227 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.Content +import org.jetbrains.dokka.DocumentationNode +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +public class FunctionTest { + @Test fun function() { + verifyModel("testdata/functions/function.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("fn", name) + assertEquals(DocumentationNode.Kind.Function, kind) + assertEquals("Function fn", content.summary.toTestString()) + assertEquals("Unit", detail(DocumentationNode.Kind.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(DocumentationNode.Kind.ExternalClass, kind) + assertEquals(2, members.count()) + with(members[0]) { + assertEquals("fn", name) + assertEquals(DocumentationNode.Kind.Function, kind) + assertEquals("Function with receiver", content.summary.toTestString()) + assertEquals("public", details.elementAt(0).name) + assertEquals("final", details.elementAt(1).name) + with(details.elementAt(2)) { + assertEquals("<this>", name) + assertEquals(DocumentationNode.Kind.Receiver, kind) + assertEquals(Content.Empty, content) + assertEquals("String", details.single().name) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertEquals("Unit", details.elementAt(3).name) + assertTrue(members.none()) + assertTrue(links.none()) + } + with(members[1]) { + assertEquals("fn", name) + assertEquals(DocumentationNode.Kind.Function, kind) + } + } + } + } + + @Test fun genericFunction() { + verifyModel("testdata/functions/genericFunction.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("generic", name) + assertEquals(DocumentationNode.Kind.Function, kind) + assertEquals("generic function", content.summary.toTestString()) + + assertEquals("private", details.elementAt(0).name) + assertEquals("final", details.elementAt(1).name) + with(details.elementAt(2)) { + assertEquals("T", name) + assertEquals(DocumentationNode.Kind.TypeParameter, kind) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertEquals("Unit", details.elementAt(3).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(DocumentationNode.Kind.Function, kind) + assertEquals("generic function", content.summary.toTestString()) + + assertEquals("public", details.elementAt(0).name) + assertEquals("final", details.elementAt(1).name) + with(details.elementAt(2)) { + assertEquals("T", name) + assertEquals(DocumentationNode.Kind.TypeParameter, kind) + assertEquals(Content.Empty, content) + with(details.single()) { + assertEquals("R", name) + assertEquals(DocumentationNode.Kind.UpperBound, kind) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertTrue(members.none()) + assertTrue(links.none()) + } + with(details.elementAt(3)) { + assertEquals("R", name) + assertEquals(DocumentationNode.Kind.TypeParameter, kind) + assertEquals(Content.Empty, content) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertEquals("Unit", details.elementAt(4).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(DocumentationNode.Kind.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(2)) { + assertEquals("x", name) + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals("parameter", content.summary.toTestString()) + assertEquals("Int", details.single().name) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertEquals("Unit", details.elementAt(3).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(DocumentationNode.Kind.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(DocumentationNode.Kind.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.elementAt(2)) { + assertEquals(1, annotations.count()) + with(annotations[0]) { + assertEquals("Fancy", name) + assertEquals(Content.Empty, content) + assertEquals(DocumentationNode.Kind.Annotation, kind) + } + } + } + } + } + + @Test fun functionWithNoinlineParam() { + verifyPackageMember("testdata/functions/functionWithNoinlineParam.kt") { func -> + with(func.details.elementAt(2)) { + val modifiers = details(DocumentationNode.Kind.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(DocumentationNode.Kind.Annotation, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Value, kind) + assertEquals("1", name) + } + } + } + } + } + } + + @Test fun functionWithDefaultParameter() { + verifyModel("testdata/functions/functionWithDefaultParameter.kt") { model -> + with(model.members.single().members.single()) { + with(details.elementAt(2)) { + val value = details(DocumentationNode.Kind.Value) + assertEquals(1, value.count()) + with(value[0]) { + assertEquals("\"\"", name) + } + } + } + } + } +} diff --git a/core/src/test/kotlin/model/JavaTest.kt b/core/src/test/kotlin/model/JavaTest.kt new file mode 100644 index 00000000..903260d3 --- /dev/null +++ b/core/src/test/kotlin/model/JavaTest.kt @@ -0,0 +1,197 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.DocumentationNode +import org.jetbrains.dokka.DocumentationReference +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +public class JavaTest { + @Test fun function() { + verifyJavaPackageMember("testdata/java/member.java") { cls -> + assertEquals("Test", cls.name) + assertEquals(DocumentationNode.Kind.Class, cls.kind) + with(cls.members(DocumentationNode.Kind.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("is String parameter ", toTestString()) + } + with(content.sections[1]) { + assertEquals("Parameters", tag) + assertEquals("value", subjectName) + assertEquals("is int parameter ", toTestString()) + } + with(content.sections[2]) { + assertEquals("Author", tag) + assertEquals("yole", toTestString()) + } + assertEquals("Unit", detail(DocumentationNode.Kind.Type).name) + assertTrue(members.none()) + assertTrue(links.none()) + with(details.first { it.name == "name" }) { + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals("String", detail(DocumentationNode.Kind.Type).name) + } + with(details.first { it.name == "value" }) { + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals("Int", detail(DocumentationNode.Kind.Type).name) + } + } + } + } + + @Test fun memberWithModifiers() { + verifyJavaPackageMember("testdata/java/memberWithModifiers.java") { cls -> + val modifiers = cls.details(DocumentationNode.Kind.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(DocumentationNode.Kind.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(DocumentationNode.Kind.Function).single()) { + val type = detail(DocumentationNode.Kind.Type) + assertEquals("Array", type.name) + assertEquals("String", type.detail(DocumentationNode.Kind.Type).name) + with(details(DocumentationNode.Kind.Parameter).single()) { + val parameterType = detail(DocumentationNode.Kind.Type) + assertEquals("IntArray", parameterType.name) + } + } + } + } + + @Test fun typeParameter() { + verifyJavaPackageMember("testdata/java/typeParameter.java") { cls -> + val typeParameters = cls.details(DocumentationNode.Kind.TypeParameter) + with(typeParameters.single()) { + assertEquals("T", name) + with(detail(DocumentationNode.Kind.UpperBound)) { + assertEquals("Comparable", name) + assertEquals("T", detail(DocumentationNode.Kind.Type).name) + } + } + with(cls.members(DocumentationNode.Kind.Function).single()) { + val methodTypeParameters = details(DocumentationNode.Kind.TypeParameter) + with(methodTypeParameters.single()) { + assertEquals("E", name) + } + } + } + } + + @Test fun constructors() { + verifyJavaPackageMember("testdata/java/constructors.java") { cls -> + val constructors = cls.members(DocumentationNode.Kind.Constructor) + assertEquals(2, constructors.size) + with(constructors[0]) { + assertEquals("<init>", name) + } + } + } + + @Test fun innerClass() { + verifyJavaPackageMember("testdata/java/innerClass.java") { cls -> + val innerClass = cls.members(DocumentationNode.Kind.Class).single() + assertEquals("D", innerClass.name) + } + } + + @Test fun varargs() { + verifyJavaPackageMember("testdata/java/varargs.java") { cls -> + val fn = cls.members(DocumentationNode.Kind.Function).single() + val param = fn.detail(DocumentationNode.Kind.Parameter) + assertEquals("vararg", param.details(DocumentationNode.Kind.Modifier).first().name) + val psiType = param.detail(DocumentationNode.Kind.Type) + assertEquals("String", psiType.name) + assertTrue(psiType.details(DocumentationNode.Kind.Type).isEmpty()) + } + } + + @Test fun fields() { + verifyJavaPackageMember("testdata/java/field.java") { cls -> + val i = cls.members(DocumentationNode.Kind.Property).single { it.name == "i" } + assertEquals("Int", i.detail(DocumentationNode.Kind.Type).name) + assertTrue("var" in i.details(DocumentationNode.Kind.Modifier).map { it.name }) + + val s = cls.members(DocumentationNode.Kind.Property).single { it.name == "s" } + assertEquals("String", s.detail(DocumentationNode.Kind.Type).name) + assertFalse("var" in s.details(DocumentationNode.Kind.Modifier).map { it.name }) + assertTrue("static" in s.details(DocumentationNode.Kind.Modifier).map { it.name }) + } + } + + @Test fun staticMethod() { + verifyJavaPackageMember("testdata/java/staticMethod.java") { cls -> + val m = cls.members(DocumentationNode.Kind.Function).single { it.name == "foo" } + assertTrue("static" in m.details(DocumentationNode.Kind.Modifier).map { it.name }) + } + } + + @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(DocumentationNode.Kind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.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(DocumentationNode.Kind.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(DocumentationNode.Kind.Function).single() + assertEquals("Any", fn.detail(DocumentationNode.Kind.Type).name) + } + } + + @Test fun enumValues() { + verifyJavaPackageMember("testdata/java/enumValues.java") { cls -> + val superTypes = cls.details(DocumentationNode.Kind.Supertype) + assertEquals(0, superTypes.size) + assertEquals(1, cls.members(DocumentationNode.Kind.EnumItem).size) + } + } + + @Test fun inheritorLinks() { + verifyJavaPackageMember("testdata/java/inheritorLinks.java") { cls -> + val fooClass = cls.members.single { it.name == "Foo" } + val inheritors = fooClass.references(DocumentationReference.Kind.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 00000000..b439d399 --- /dev/null +++ b/core/src/test/kotlin/model/KotlinAsJavaTest.kt @@ -0,0 +1,40 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.DocumentationModule +import org.jetbrains.dokka.DocumentationNode +import org.junit.Test +import kotlin.test.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(DocumentationNode.Kind.Class, facadeClass.kind) + + val fn = facadeClass.members.single() + assertEquals("fn", fn.name) + assertEquals(DocumentationNode.Kind.Function, fn.kind) + } + } + + @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(DocumentationNode.Kind.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 00000000..c30e1c10 --- /dev/null +++ b/core/src/test/kotlin/model/LinkTest.kt @@ -0,0 +1,48 @@ +package org.jetbrains.dokka.tests + +import org.junit.Test +import kotlin.test.* +import org.jetbrains.dokka.* + +public class LinkTest { + @Test fun linkToSelf() { + verifyModel("testdata/links/linkToSelf.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Foo", name) + assertEquals(DocumentationNode.Kind.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(DocumentationNode.Kind.Class, kind) + assertEquals("This is link to [member -> Function:member]", content.summary.toTestString()) + } + } + } + + @Test fun linkToQualifiedMember() { + verifyModel("testdata/links/linkToQualifiedMember.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("Foo", name) + assertEquals(DocumentationNode.Kind.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(DocumentationNode.Kind.Function, kind) + assertEquals("This is link to [param -> Parameter:param]", content.summary.toTestString()) + } + } + } + +}
\ 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 00000000..aa5a059a --- /dev/null +++ b/core/src/test/kotlin/model/PackageTest.kt @@ -0,0 +1,86 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.Content +import org.jetbrains.dokka.DocumentationNode +import org.jetbrains.kotlin.config.KotlinSourceRoot +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +public class PackageTest { + @Test fun rootPackage() { + verifyModel("testdata/packages/rootPackage.kt") { model -> + with(model.members.single()) { + assertEquals(DocumentationNode.Kind.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(DocumentationNode.Kind.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(DocumentationNode.Kind.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"), + KotlinSourceRoot("testdata/packages/simpleNamePackage.kt")) { model -> + assertEquals(2, model.members.count()) + with(model.members.single { it.name == "simple" }) { + assertEquals(DocumentationNode.Kind.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(DocumentationNode.Kind.Package, kind) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } + + @Test fun multipleFilesSamePackage() { + verifyModel(KotlinSourceRoot("testdata/packages/simpleNamePackage.kt"), + KotlinSourceRoot("testdata/packages/simpleNamePackage2.kt")) { model -> + assertEquals(1, model.members.count()) + with(model.members.elementAt(0)) { + assertEquals(DocumentationNode.Kind.Package, kind) + assertEquals("simple", name) + assertEquals(Content.Empty, content) + assertTrue(details.none()) + assertTrue(members.none()) + assertTrue(links.none()) + } + } + } +}
\ No newline at end of file diff --git a/core/src/test/kotlin/model/PropertyTest.kt b/core/src/test/kotlin/model/PropertyTest.kt new file mode 100644 index 00000000..716aac54 --- /dev/null +++ b/core/src/test/kotlin/model/PropertyTest.kt @@ -0,0 +1,103 @@ +package org.jetbrains.dokka.tests + +import org.jetbrains.dokka.Content +import org.jetbrains.dokka.DocumentationNode +import org.jetbrains.dokka.DocumentationReference +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +public class PropertyTest { + @Test fun valueProperty() { + verifyModel("testdata/properties/valueProperty.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("property", name) + assertEquals(DocumentationNode.Kind.Property, kind) + assertEquals(Content.Empty, content) + assertEquals("String", detail(DocumentationNode.Kind.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(DocumentationNode.Kind.Property, kind) + assertEquals(Content.Empty, content) + assertEquals("String", detail(DocumentationNode.Kind.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(DocumentationNode.Kind.Property, kind) + assertEquals(Content.Empty, content) + assertEquals("String", detail(DocumentationNode.Kind.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(DocumentationNode.Kind.Property, kind) + assertEquals(Content.Empty, content) + assertEquals("String", detail(DocumentationNode.Kind.Type).name) + val modifiers = details(DocumentationNode.Kind.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("Volatile", name) + assertEquals(Content.Empty, content) + assertEquals(DocumentationNode.Kind.Annotation, kind) + } + } + } + } + + @Test fun propertyWithReceiver() { + verifyModel("testdata/properties/propertyWithReceiver.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("kotlin.String", name) + assertEquals(DocumentationNode.Kind.ExternalClass, kind) + with(members.single()) { + assertEquals("foobar", name) + assertEquals(DocumentationNode.Kind.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(DocumentationReference.Kind.Override).single().to + assertEquals("xyzzy", override.name) + assertEquals("Foo", override.owner!!.name) + } + } + } +} diff --git a/core/testdata/classes/annotatedClass.kt b/core/testdata/classes/annotatedClass.kt new file mode 100644 index 00000000..1b58f56c --- /dev/null +++ b/core/testdata/classes/annotatedClass.kt @@ -0,0 +1 @@ +@Strictfp class Foo() {} diff --git a/core/testdata/classes/annotatedClassWithAnnotationParameters.kt b/core/testdata/classes/annotatedClassWithAnnotationParameters.kt new file mode 100644 index 00000000..930d6a62 --- /dev/null +++ b/core/testdata/classes/annotatedClassWithAnnotationParameters.kt @@ -0,0 +1 @@ +@Deprecated("should no longer be used") class Foo() {} diff --git a/core/testdata/classes/classWithCompanionObject.kt b/core/testdata/classes/classWithCompanionObject.kt new file mode 100644 index 00000000..fdbd915d --- /dev/null +++ b/core/testdata/classes/classWithCompanionObject.kt @@ -0,0 +1,7 @@ +class Klass() { + companion object { + val x = 1 + + fun foo() {} + } +} diff --git a/core/testdata/classes/classWithConstructor.kt b/core/testdata/classes/classWithConstructor.kt new file mode 100644 index 00000000..0751d570 --- /dev/null +++ b/core/testdata/classes/classWithConstructor.kt @@ -0,0 +1 @@ +class Klass(name: String)
\ No newline at end of file diff --git a/core/testdata/classes/classWithFunction.kt b/core/testdata/classes/classWithFunction.kt new file mode 100644 index 00000000..a981cfb6 --- /dev/null +++ b/core/testdata/classes/classWithFunction.kt @@ -0,0 +1,4 @@ +class Klass { + fun fn() { + } +} diff --git a/core/testdata/classes/classWithProperty.kt b/core/testdata/classes/classWithProperty.kt new file mode 100644 index 00000000..2a849572 --- /dev/null +++ b/core/testdata/classes/classWithProperty.kt @@ -0,0 +1,3 @@ +class Klass { + val name: String = "" +}
\ No newline at end of file diff --git a/core/testdata/classes/companionObjectExtension.kt b/core/testdata/classes/companionObjectExtension.kt new file mode 100644 index 00000000..4b471376 --- /dev/null +++ b/core/testdata/classes/companionObjectExtension.kt @@ -0,0 +1,10 @@ +class Foo { + companion object Default { + } +} + + +/** + * The def + */ +val Foo.Default.x: Int get() = 1 diff --git a/core/testdata/classes/dataClass.kt b/core/testdata/classes/dataClass.kt new file mode 100644 index 00000000..62c6f0ec --- /dev/null +++ b/core/testdata/classes/dataClass.kt @@ -0,0 +1 @@ +data class Foo() {} diff --git a/core/testdata/classes/emptyClass.kt b/core/testdata/classes/emptyClass.kt new file mode 100644 index 00000000..abd20cc8 --- /dev/null +++ b/core/testdata/classes/emptyClass.kt @@ -0,0 +1,3 @@ +class Klass { + +}
\ No newline at end of file diff --git a/core/testdata/classes/emptyObject.kt b/core/testdata/classes/emptyObject.kt new file mode 100644 index 00000000..4138bf31 --- /dev/null +++ b/core/testdata/classes/emptyObject.kt @@ -0,0 +1,3 @@ +object Obj { + +}
\ No newline at end of file diff --git a/core/testdata/classes/genericClass.kt b/core/testdata/classes/genericClass.kt new file mode 100644 index 00000000..db20ff7e --- /dev/null +++ b/core/testdata/classes/genericClass.kt @@ -0,0 +1,3 @@ +class Klass<T> { + +}
\ No newline at end of file diff --git a/core/testdata/classes/indirectOverride.kt b/core/testdata/classes/indirectOverride.kt new file mode 100644 index 00000000..8d091b80 --- /dev/null +++ b/core/testdata/classes/indirectOverride.kt @@ -0,0 +1,9 @@ +abstract class C() { + abstract fun foo() +} + +abstract class D(): C() + +class E(): D() { + override fun foo() {} +} diff --git a/core/testdata/classes/innerClass.kt b/core/testdata/classes/innerClass.kt new file mode 100644 index 00000000..3c6e497d --- /dev/null +++ b/core/testdata/classes/innerClass.kt @@ -0,0 +1,5 @@ +class C { + inner class D { + + } +}
\ No newline at end of file diff --git a/core/testdata/classes/javaAnnotationClass.kt b/core/testdata/classes/javaAnnotationClass.kt new file mode 100644 index 00000000..95600147 --- /dev/null +++ b/core/testdata/classes/javaAnnotationClass.kt @@ -0,0 +1,5 @@ +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy + +@Retention(RetentionPolicy.SOURCE) +public annotation class throws() diff --git a/core/testdata/classes/notOpenClass.kt b/core/testdata/classes/notOpenClass.kt new file mode 100644 index 00000000..edee2c1a --- /dev/null +++ b/core/testdata/classes/notOpenClass.kt @@ -0,0 +1,7 @@ +open class C() { + open fun f() {} +} + +class D() : C() { + override fun f() {} +} diff --git a/core/testdata/classes/sealedClass.kt b/core/testdata/classes/sealedClass.kt new file mode 100644 index 00000000..93350393 --- /dev/null +++ b/core/testdata/classes/sealedClass.kt @@ -0,0 +1 @@ +sealed class Foo() {} diff --git a/core/testdata/classes/secondaryConstructor.kt b/core/testdata/classes/secondaryConstructor.kt new file mode 100644 index 00000000..e5cb2557 --- /dev/null +++ b/core/testdata/classes/secondaryConstructor.kt @@ -0,0 +1,5 @@ +class C() { + /** This is a secondary constructor. */ + constructor(s: String): this() { + } +} diff --git a/core/testdata/comments/directive.kt b/core/testdata/comments/directive.kt new file mode 100644 index 00000000..b27f5a48 --- /dev/null +++ b/core/testdata/comments/directive.kt @@ -0,0 +1,35 @@ +/** + * Summary + * + * @sample example1 + * @sample example2 + * @sample X.example3 + * @sample X.Y.example4 + */ +val property = "test" + +fun example1(node: String) = if (true) { + println(property) +} + +fun example2(node: String) { + if (true) { + println(property) + } +} + +class X { + fun example3(node: String) { + if (true) { + println(property) + } + } + + class Y { + fun example4(node: String) { + if (true) { + println(property) + } + } + } +} diff --git a/core/testdata/comments/emptyDoc.kt b/core/testdata/comments/emptyDoc.kt new file mode 100644 index 00000000..b87cce57 --- /dev/null +++ b/core/testdata/comments/emptyDoc.kt @@ -0,0 +1 @@ +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/emptyDocButComment.kt b/core/testdata/comments/emptyDocButComment.kt new file mode 100644 index 00000000..ceb24753 --- /dev/null +++ b/core/testdata/comments/emptyDocButComment.kt @@ -0,0 +1,2 @@ +/* comment */ +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/emptySection.kt b/core/testdata/comments/emptySection.kt new file mode 100644 index 00000000..47d6b1a5 --- /dev/null +++ b/core/testdata/comments/emptySection.kt @@ -0,0 +1,6 @@ + +/** + * Summary + * @one + */ +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/multilineDoc.kt b/core/testdata/comments/multilineDoc.kt new file mode 100644 index 00000000..31cfa3a7 --- /dev/null +++ b/core/testdata/comments/multilineDoc.kt @@ -0,0 +1,7 @@ +/** + * doc1 + * + * doc2 + * doc3 + */ +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/multilineDocWithComment.kt b/core/testdata/comments/multilineDocWithComment.kt new file mode 100644 index 00000000..88d22642 --- /dev/null +++ b/core/testdata/comments/multilineDocWithComment.kt @@ -0,0 +1,8 @@ +/** + * doc1 + * + * doc2 + * doc3 + */ +// comment +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/multilineSection.kt b/core/testdata/comments/multilineSection.kt new file mode 100644 index 00000000..6ef4df2c --- /dev/null +++ b/core/testdata/comments/multilineSection.kt @@ -0,0 +1,7 @@ +/** + * Summary + * @one + * line one + * line two + */ +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/oneLineDoc.kt b/core/testdata/comments/oneLineDoc.kt new file mode 100644 index 00000000..92a40c67 --- /dev/null +++ b/core/testdata/comments/oneLineDoc.kt @@ -0,0 +1,2 @@ +/** doc */ +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/oneLineDocWithComment.kt b/core/testdata/comments/oneLineDocWithComment.kt new file mode 100644 index 00000000..c8467933 --- /dev/null +++ b/core/testdata/comments/oneLineDocWithComment.kt @@ -0,0 +1,3 @@ +/** doc */ +// comment +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/oneLineDocWithEmptyLine.kt b/core/testdata/comments/oneLineDocWithEmptyLine.kt new file mode 100644 index 00000000..e364c416 --- /dev/null +++ b/core/testdata/comments/oneLineDocWithEmptyLine.kt @@ -0,0 +1,3 @@ +/** doc */ + +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/section1.kt b/core/testdata/comments/section1.kt new file mode 100644 index 00000000..7c763b4c --- /dev/null +++ b/core/testdata/comments/section1.kt @@ -0,0 +1,5 @@ +/** + * Summary + * @one section one + */ +val property = "test"
\ No newline at end of file diff --git a/core/testdata/comments/section2.kt b/core/testdata/comments/section2.kt new file mode 100644 index 00000000..e280793e --- /dev/null +++ b/core/testdata/comments/section2.kt @@ -0,0 +1,6 @@ +/** + * Summary + * @one section one + * @two section two + */ +val property = "test"
\ No newline at end of file diff --git a/core/testdata/format/accessor.kt b/core/testdata/format/accessor.kt new file mode 100644 index 00000000..5a4d1742 --- /dev/null +++ b/core/testdata/format/accessor.kt @@ -0,0 +1,5 @@ +class C() { + var x: String + /** The getter returns an empty string. */ get() = "" + /** The setter does nothing. */ set(value) { } +} diff --git a/core/testdata/format/accessor.md b/core/testdata/format/accessor.md new file mode 100644 index 00000000..8279f452 --- /dev/null +++ b/core/testdata/format/accessor.md @@ -0,0 +1,18 @@ +[test](test/index) / [C](test/-c/index) / [x](test/-c/x) + + +# x + +`var x: String` +**Getter** + +The getter returns an empty string. + + +**Setter** + +The setter does nothing. + + + + diff --git a/core/testdata/format/annotatedTypeParameter.kt b/core/testdata/format/annotatedTypeParameter.kt new file mode 100644 index 00000000..cc3bfc1a --- /dev/null +++ b/core/testdata/format/annotatedTypeParameter.kt @@ -0,0 +1,2 @@ +public fun <E> containsAll(elements: Collection<@UnsafeVariance E>): @UnsafeVariance E { +} diff --git a/core/testdata/format/annotatedTypeParameter.md b/core/testdata/format/annotatedTypeParameter.md new file mode 100644 index 00000000..7d26a162 --- /dev/null +++ b/core/testdata/format/annotatedTypeParameter.md @@ -0,0 +1,8 @@ +[test](test/index) / [containsAll](test/contains-all) + + +# containsAll + +`fun <E> containsAll(elements: Collection<@UnsafeVariance E>): @UnsafeVariance E` + + diff --git a/core/testdata/format/annotationClass.kt b/core/testdata/format/annotationClass.kt new file mode 100644 index 00000000..89d494fb --- /dev/null +++ b/core/testdata/format/annotationClass.kt @@ -0,0 +1 @@ +annotation class fancy diff --git a/core/testdata/format/annotationClass.md b/core/testdata/format/annotationClass.md new file mode 100644 index 00000000..03548aa3 --- /dev/null +++ b/core/testdata/format/annotationClass.md @@ -0,0 +1,14 @@ +[test](test/index) / [fancy](test/fancy/index) + + +# fancy + +`annotation class fancy` + + + +### Constructors + + +| [<init>](test/fancy/-init-) | `fancy()` | + diff --git a/core/testdata/format/annotationParams.kt b/core/testdata/format/annotationParams.kt new file mode 100644 index 00000000..f259a740 --- /dev/null +++ b/core/testdata/format/annotationParams.kt @@ -0,0 +1 @@ +@JvmName("FFF") fun f() {} diff --git a/core/testdata/format/annotationParams.md b/core/testdata/format/annotationParams.md new file mode 100644 index 00000000..8cdd6e96 --- /dev/null +++ b/core/testdata/format/annotationParams.md @@ -0,0 +1,8 @@ +[test](test/index) / [f](test/f) + + +# f + +`@JvmName("FFF") fun f(): Unit` + + diff --git a/core/testdata/format/annotations.kt b/core/testdata/format/annotations.kt new file mode 100644 index 00000000..57f76249 --- /dev/null +++ b/core/testdata/format/annotations.kt @@ -0,0 +1,6 @@ +data class Foo { + inline fun bar(noinline notInlined: () -> Unit) { + } + + inline val x: Int +} diff --git a/core/testdata/format/annotations.md b/core/testdata/format/annotations.md new file mode 100644 index 00000000..b898d55c --- /dev/null +++ b/core/testdata/format/annotations.md @@ -0,0 +1,26 @@ +[test](test/index) / [Foo](test/-foo/index) + + +# Foo + +`data class Foo` + + + +### Constructors + + +| [<init>](test/-foo/-init-) | `Foo()` | + + +### Properties + + +| [x](test/-foo/x) | `val x: Int` | + + +### Functions + + +| [bar](test/-foo/bar) | `fun bar(notInlined: () -> Unit): Unit` | + diff --git a/core/testdata/format/bracket.html b/core/testdata/format/bracket.html new file mode 100644 index 00000000..5630073d --- /dev/null +++ b/core/testdata/format/bracket.html @@ -0,0 +1,14 @@ +<HTML> +<HEAD> +<title>test / foo</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/foo">foo</a><br/> +<br/> +<h1>foo</h1> +<code><span class="keyword">fun </span><span class="identifier">foo</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<p>bar[]</p> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/bracket.kt b/core/testdata/format/bracket.kt new file mode 100644 index 00000000..d41b0073 --- /dev/null +++ b/core/testdata/format/bracket.kt @@ -0,0 +1,4 @@ +/** + * bar[] + */ +fun foo() {} diff --git a/core/testdata/format/brokenLink.html b/core/testdata/format/brokenLink.html new file mode 100644 index 00000000..32135787 --- /dev/null +++ b/core/testdata/format/brokenLink.html @@ -0,0 +1,14 @@ +<HTML> +<HEAD> +<title>test / f</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/f">f</a><br/> +<br/> +<h1>f</h1> +<code><span class="keyword">fun </span><span class="identifier">f</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<p>This references <a href="#">noSuchIdentifier</a>.</p> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/brokenLink.kt b/core/testdata/format/brokenLink.kt new file mode 100644 index 00000000..268a986e --- /dev/null +++ b/core/testdata/format/brokenLink.kt @@ -0,0 +1,4 @@ +/** + * This references [noSuchIdentifier]. + */ +fun f() { } diff --git a/core/testdata/format/classWithCompanionObject.html b/core/testdata/format/classWithCompanionObject.html new file mode 100644 index 00000000..be9247ea --- /dev/null +++ b/core/testdata/format/classWithCompanionObject.html @@ -0,0 +1,46 @@ +<HTML> +<HEAD> +<title>test / Klass</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-klass/index">Klass</a><br/> +<br/> +<h1>Klass</h1> +<code><span class="keyword">class </span><span class="identifier">Klass</span></code><br/> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-klass/-init-"><init></a></td> +<td> +<code><span class="identifier">Klass</span><span class="symbol">(</span><span class="symbol">)</span></code></td> +</tr> +</tbody> +</table> +<h3>Companion Object Properties</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-klass/x">x</a></td> +<td> +<code><span class="keyword">val </span><span class="identifier">x</span><span class="symbol">: </span><span class="identifier">Int</span></code></td> +</tr> +</tbody> +</table> +<h3>Companion Object Functions</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-klass/foo">foo</a></td> +<td> +<code><span class="keyword">fun </span><span class="identifier">foo</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/classWithCompanionObject.kt b/core/testdata/format/classWithCompanionObject.kt new file mode 100644 index 00000000..fdbd915d --- /dev/null +++ b/core/testdata/format/classWithCompanionObject.kt @@ -0,0 +1,7 @@ +class Klass() { + companion object { + val x = 1 + + fun foo() {} + } +} diff --git a/core/testdata/format/classWithCompanionObject.md b/core/testdata/format/classWithCompanionObject.md new file mode 100644 index 00000000..9398c3d3 --- /dev/null +++ b/core/testdata/format/classWithCompanionObject.md @@ -0,0 +1,26 @@ +[test](test/index) / [Klass](test/-klass/index) + + +# Klass + +`class Klass` + + + +### Constructors + + +| [<init>](test/-klass/-init-) | `Klass()` | + + +### Companion Object Properties + + +| [x](test/-klass/x) | `val x: Int` | + + +### Companion Object Functions + + +| [foo](test/-klass/foo) | `fun foo(): Unit` | + diff --git a/core/testdata/format/codeSpan.html b/core/testdata/format/codeSpan.html new file mode 100644 index 00000000..cc553043 --- /dev/null +++ b/core/testdata/format/codeSpan.html @@ -0,0 +1,14 @@ +<HTML> +<HEAD> +<title>test / foo</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/foo">foo</a><br/> +<br/> +<h1>foo</h1> +<code><span class="keyword">fun </span><span class="identifier">foo</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<p>This is a <code>code span</code>.</p> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/codeSpan.kt b/core/testdata/format/codeSpan.kt new file mode 100644 index 00000000..645f454a --- /dev/null +++ b/core/testdata/format/codeSpan.kt @@ -0,0 +1,4 @@ +/** + * This is a `code span`. + */ +fun foo() {}
\ No newline at end of file diff --git a/core/testdata/format/companionObjectExtension.kt b/core/testdata/format/companionObjectExtension.kt new file mode 100644 index 00000000..f452de2c --- /dev/null +++ b/core/testdata/format/companionObjectExtension.kt @@ -0,0 +1,10 @@ +class Foo { + companion object Default { + } +} + + +/** + * The default object property. + */ +val Foo.Default.x: Int get() = 1 diff --git a/core/testdata/format/companionObjectExtension.md b/core/testdata/format/companionObjectExtension.md new file mode 100644 index 00000000..271da5b0 --- /dev/null +++ b/core/testdata/format/companionObjectExtension.md @@ -0,0 +1,23 @@ +[test](test/index) / [Foo](test/-foo/index) + + +# Foo + +`class Foo` + + + +### Constructors + + +| [<init>](test/-foo/-init-) | `Foo()` | + + +### Companion Object Extension Properties + + +| [x](test/x) | `val Foo.Default.x: Int` +The default object property. + + | + diff --git a/core/testdata/format/crossLanguage/kotlinExtendsJava/Bar.html b/core/testdata/format/crossLanguage/kotlinExtendsJava/Bar.html new file mode 100644 index 00000000..e7b139d8 --- /dev/null +++ b/core/testdata/format/crossLanguage/kotlinExtendsJava/Bar.html @@ -0,0 +1,37 @@ +<HTML> +<HEAD> +<title>test / test.Bar</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/test/index">test</a> / <a href="test/test/-bar/index">Bar</a><br/> +<br/> +<h1>Bar</h1> +<code><span class="keyword">class </span><span class="identifier">Bar</span> <span class="symbol">:</span> <a href="test/test/-foo/index"><span class="identifier">Foo</span></a></code><br/> +<p>See <a href="test/test/-foo/xyzzy">xyzzy</a></p> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/test/-bar/-init-"><init></a></td> +<td> +<code><span class="identifier">Bar</span><span class="symbol">(</span><span class="symbol">)</span></code><p>See <a href="test/test/-foo/xyzzy">xyzzy</a></p> +</td> +</tr> +</tbody> +</table> +<h3>Inherited Functions</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/test/-foo/xyzzy">xyzzy</a></td> +<td> +<code><span class="keyword">open</span> <span class="keyword">fun </span><span class="identifier">xyzzy</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/crossLanguage/kotlinExtendsJava/Bar.kt b/core/testdata/format/crossLanguage/kotlinExtendsJava/Bar.kt new file mode 100644 index 00000000..102782f9 --- /dev/null +++ b/core/testdata/format/crossLanguage/kotlinExtendsJava/Bar.kt @@ -0,0 +1,6 @@ +package test + +/** + * See [xyzzy] + */ +class Bar(): Foo() diff --git a/core/testdata/format/crossLanguage/kotlinExtendsJava/test/Foo.java b/core/testdata/format/crossLanguage/kotlinExtendsJava/test/Foo.java new file mode 100644 index 00000000..7c143030 --- /dev/null +++ b/core/testdata/format/crossLanguage/kotlinExtendsJava/test/Foo.java @@ -0,0 +1,6 @@ +package test; + +public class Foo { + public void xyzzy() { + } +} diff --git a/core/testdata/format/deprecated.class.html b/core/testdata/format/deprecated.class.html new file mode 100644 index 00000000..69da63a1 --- /dev/null +++ b/core/testdata/format/deprecated.class.html @@ -0,0 +1,41 @@ +<HTML> +<HEAD> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-c/index">C</a><br/> +<br/> +<h1>C</h1> +<code><span class="keyword">class </span><s><span class="identifier">C</span></s></code><br/> +<strong>Deprecated:</strong> This class sucks<br/> +<br/> +<br/> +<br/> +<a href="test/index">test</a> / <a href="test/f">f</a><br/> +<br/> +<h1>f</h1> +<code><span class="keyword">fun </span><s><span class="identifier">f</span></s><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<strong>Deprecated:</strong> This function sucks<br/> +<br/> +<br/> +<br/> +<a href="test/index">test</a> / <a href="test/p">p</a><br/> +<br/> +<h1>p</h1> +<code><span class="keyword">val </span><s><span class="identifier">p</span></s><span class="symbol">: </span><span class="identifier">Int</span></code><br/> +<strong>Deprecated:</strong> This property sucks<br/> +<br/> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-c/-init-"><init></a></td> +<td> +<code><span class="identifier">C</span><span class="symbol">(</span><span class="symbol">)</span></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/deprecated.kt b/core/testdata/format/deprecated.kt new file mode 100644 index 00000000..4fc568c2 --- /dev/null +++ b/core/testdata/format/deprecated.kt @@ -0,0 +1,5 @@ +@Deprecated("This class sucks") class C() { } + +@Deprecated("This function sucks") fun f() { } + +@Deprecated("This property sucks") val p: Int get() = 0 diff --git a/core/testdata/format/deprecated.package.html b/core/testdata/format/deprecated.package.html new file mode 100644 index 00000000..dd80471c --- /dev/null +++ b/core/testdata/format/deprecated.package.html @@ -0,0 +1,43 @@ +<HTML> +<HEAD> +<title>test / root package</title> +</HEAD> +<BODY> +<a href="test/index">test</a><br/> +<br/> +<h2>Package </h2> +<h3>Types</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-c/index">C</a></td> +<td> +<code><span class="keyword">class </span><s><span class="identifier">C</span></s></code></td> +</tr> +</tbody> +</table> +<h3>Properties</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/p">p</a></td> +<td> +<code><span class="keyword">val </span><s><span class="identifier">p</span></s><span class="symbol">: </span><span class="identifier">Int</span></code></td> +</tr> +</tbody> +</table> +<h3>Functions</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/f">f</a></td> +<td> +<code><span class="keyword">fun </span><s><span class="identifier">f</span></s><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/emptyDescription.kt b/core/testdata/format/emptyDescription.kt new file mode 100644 index 00000000..3ed81dfa --- /dev/null +++ b/core/testdata/format/emptyDescription.kt @@ -0,0 +1,5 @@ +/** + * Function fn + */ +fun fn() { +}
\ No newline at end of file diff --git a/core/testdata/format/emptyDescription.md b/core/testdata/format/emptyDescription.md new file mode 100644 index 00000000..e82e7228 --- /dev/null +++ b/core/testdata/format/emptyDescription.md @@ -0,0 +1,11 @@ +[test](test/index) / [fn](test/fn) + + +# fn + +`fun fn(): Unit` + +Function fn + + + diff --git a/core/testdata/format/entity.html b/core/testdata/format/entity.html new file mode 100644 index 00000000..a939faad --- /dev/null +++ b/core/testdata/format/entity.html @@ -0,0 +1,26 @@ +<HTML> +<HEAD> +<title>test / Bar</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-bar/index">Bar</a><br/> +<br/> +<h1>Bar</h1> +<code><span class="keyword">class </span><span class="identifier">Bar</span></code><br/> +<p>Copyright © JetBrains 2015 "</p> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-bar/-init-"><init></a></td> +<td> +<code><span class="identifier">Bar</span><span class="symbol">(</span><span class="symbol">)</span></code><p>Copyright © JetBrains 2015 "</p> +</td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/entity.kt b/core/testdata/format/entity.kt new file mode 100644 index 00000000..163d2ee6 --- /dev/null +++ b/core/testdata/format/entity.kt @@ -0,0 +1,4 @@ +/** + * Copyright © JetBrains 2015 " + */ +class Bar {} diff --git a/core/testdata/format/enumClass.kt b/core/testdata/format/enumClass.kt new file mode 100644 index 00000000..c1af69d8 --- /dev/null +++ b/core/testdata/format/enumClass.kt @@ -0,0 +1,4 @@ +public enum class InlineOption { + LOCAL_CONTINUE_AND_BREAK, + ONLY_LOCAL_RETURN +} diff --git a/core/testdata/format/enumClass.md b/core/testdata/format/enumClass.md new file mode 100644 index 00000000..7a0e03ff --- /dev/null +++ b/core/testdata/format/enumClass.md @@ -0,0 +1,15 @@ +[test](test/index) / [InlineOption](test/-inline-option/index) + + +# InlineOption + +`enum class InlineOption` + + + +### Enum Values + + +| [LOCAL_CONTINUE_AND_BREAK](test/-inline-option/-l-o-c-a-l_-c-o-n-t-i-n-u-e_-a-n-d_-b-r-e-a-k) | | +| [ONLY_LOCAL_RETURN](test/-inline-option/-o-n-l-y_-l-o-c-a-l_-r-e-t-u-r-n) | | + diff --git a/core/testdata/format/enumClass.value.md b/core/testdata/format/enumClass.value.md new file mode 100644 index 00000000..bcb685d1 --- /dev/null +++ b/core/testdata/format/enumClass.value.md @@ -0,0 +1,8 @@ +[test](test/index) / [InlineOption](test/-inline-option/index) / [LOCAL_CONTINUE_AND_BREAK](test/-inline-option/-l-o-c-a-l_-c-o-n-t-i-n-u-e_-a-n-d_-b-r-e-a-k) + + +# LOCAL_CONTINUE_AND_BREAK + +`LOCAL_CONTINUE_AND_BREAK` + + diff --git a/core/testdata/format/extensionFunctionParameter.kt b/core/testdata/format/extensionFunctionParameter.kt new file mode 100644 index 00000000..bfb344b9 --- /dev/null +++ b/core/testdata/format/extensionFunctionParameter.kt @@ -0,0 +1 @@ +public inline fun <T> T.apply(f: T.() -> Unit): T { f(); return this } diff --git a/core/testdata/format/extensionFunctionParameter.md b/core/testdata/format/extensionFunctionParameter.md new file mode 100644 index 00000000..6980912d --- /dev/null +++ b/core/testdata/format/extensionFunctionParameter.md @@ -0,0 +1,8 @@ +[test](test/index) / [apply](test/apply) + + +# apply + +`inline fun <T> T.apply(f: T.() -> Unit): T` + + diff --git a/core/testdata/format/extensions.class.md b/core/testdata/format/extensions.class.md new file mode 100644 index 00000000..c66cdb01 --- /dev/null +++ b/core/testdata/format/extensions.class.md @@ -0,0 +1,16 @@ +[test](test/index) / [foo](test/foo/index) / [kotlin.String](test/foo/kotlin.-string/index) + + +### Extensions for kotlin.String + + +| [fn](test/foo/kotlin.-string/fn) | `fun String.fn(): Unit` +`fun String.fn(x: Int): Unit` +Function with receiver + + | +| [foobar](test/foo/kotlin.-string/foobar) | `val String.foobar: Int` +Property with receiver. + + | + diff --git a/core/testdata/format/extensions.kt b/core/testdata/format/extensions.kt new file mode 100644 index 00000000..6f2eff9d --- /dev/null +++ b/core/testdata/format/extensions.kt @@ -0,0 +1,19 @@ +package foo + +/** + * Function with receiver + */ +fun String.fn() { +} + +/** + * Function with receiver + */ +fun String.fn(x: Int) { +} + +/** + * Property with receiver. + */ +val String.foobar: Int + get() = size() * 2 diff --git a/core/testdata/format/extensions.package.md b/core/testdata/format/extensions.package.md new file mode 100644 index 00000000..76ff3676 --- /dev/null +++ b/core/testdata/format/extensions.package.md @@ -0,0 +1,11 @@ +[test](test/index) / [foo](test/foo/index) + + +## Package foo + + +### Extensions for External Classes + + +| [kotlin.String](test/foo/kotlin.-string/index) | | + diff --git a/core/testdata/format/functionWithDefaultParameter.kt b/core/testdata/format/functionWithDefaultParameter.kt new file mode 100644 index 00000000..3a3a102f --- /dev/null +++ b/core/testdata/format/functionWithDefaultParameter.kt @@ -0,0 +1 @@ +fun f(x: String = "") {} diff --git a/core/testdata/format/functionWithDefaultParameter.md b/core/testdata/format/functionWithDefaultParameter.md new file mode 100644 index 00000000..21e9eff7 --- /dev/null +++ b/core/testdata/format/functionWithDefaultParameter.md @@ -0,0 +1,8 @@ +[test](test/index) / [f](test/f) + + +# f + +`fun f(x: String = ""): Unit` + + diff --git a/core/testdata/format/htmlEscaping.html b/core/testdata/format/htmlEscaping.html new file mode 100644 index 00000000..a485c08f --- /dev/null +++ b/core/testdata/format/htmlEscaping.html @@ -0,0 +1,14 @@ +<HTML> +<HEAD> +<title>test / x</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/x">x</a><br/> +<br/> +<h1>x</h1> +<code><span class="keyword">fun </span><span class="symbol"><</span><span class="identifier">T</span><span class="symbol">></span> <span class="identifier">x</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">T</span><span class="symbol">?</span></code><br/> +<p>Special characters: < is "less than", > is "greater than", & is "ampersand"</p> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/htmlEscaping.kt b/core/testdata/format/htmlEscaping.kt new file mode 100644 index 00000000..8778d8a5 --- /dev/null +++ b/core/testdata/format/htmlEscaping.kt @@ -0,0 +1,4 @@ +/** + * Special characters: < is "less than", > is "greater than", & is "ampersand" + */ +fun x<T>(): T? = null diff --git a/core/testdata/format/inheritedExtensions.kt b/core/testdata/format/inheritedExtensions.kt new file mode 100644 index 00000000..e38fe00d --- /dev/null +++ b/core/testdata/format/inheritedExtensions.kt @@ -0,0 +1,11 @@ +open class Foo + +class Bar : Foo() + +fun Foo.first() { + +} + +fun Bar.second() { + +} diff --git a/core/testdata/format/inheritedExtensions.md b/core/testdata/format/inheritedExtensions.md new file mode 100644 index 00000000..79137eac --- /dev/null +++ b/core/testdata/format/inheritedExtensions.md @@ -0,0 +1,21 @@ +[test](test/index) / [Bar](test/-bar/index) + + +# Bar + +`class Bar : [Foo](test/-foo/index)` + + + +### Constructors + + +| [<init>](test/-bar/-init-) | `Bar()` | + + +### Extension Functions + + +| [first](test/first) | `fun [Foo](test/-foo/index).first(): Unit` | +| [second](test/second) | `fun [Bar](test/-bar/index).second(): Unit` | + diff --git a/core/testdata/format/inheritedMembers.kt b/core/testdata/format/inheritedMembers.kt new file mode 100644 index 00000000..2d0c4ca1 --- /dev/null +++ b/core/testdata/format/inheritedMembers.kt @@ -0,0 +1,12 @@ +open class Foo { + fun first() { + } + + val firstProp: Int = 0 +} + +class Bar : Foo() { + fun second() + + val secondProp: Int = 1 +} diff --git a/core/testdata/format/inheritedMembers.md b/core/testdata/format/inheritedMembers.md new file mode 100644 index 00000000..d58d3974 --- /dev/null +++ b/core/testdata/format/inheritedMembers.md @@ -0,0 +1,38 @@ +[test](test/index) / [Bar](test/-bar/index) + + +# Bar + +`class Bar : [Foo](test/-foo/index)` + + + +### Constructors + + +| [<init>](test/-bar/-init-) | `Bar()` | + + +### Properties + + +| [secondProp](test/-bar/second-prop) | `val secondProp: Int` | + + +### Inherited Properties + + +| [firstProp](test/-foo/first-prop) | `val firstProp: Int` | + + +### Functions + + +| [second](test/-bar/second) | `fun second(): Unit` | + + +### Inherited Functions + + +| [first](test/-foo/first) | `fun first(): Unit` | + diff --git a/core/testdata/format/javaCodeInParam.java b/core/testdata/format/javaCodeInParam.java new file mode 100644 index 00000000..73025fcc --- /dev/null +++ b/core/testdata/format/javaCodeInParam.java @@ -0,0 +1,5 @@ +/** + * @param T this is {@code some code} and other text + */ +class C<T> { +} diff --git a/core/testdata/format/javaCodeInParam.md b/core/testdata/format/javaCodeInParam.md new file mode 100644 index 00000000..b1d2d871 --- /dev/null +++ b/core/testdata/format/javaCodeInParam.md @@ -0,0 +1,21 @@ +[test](test/index) / [C](test/-c/index) + + +# C + +`protected open class C<T : Any>` + + + + +### Parameters + +`T` - this is `some code` and other text + + + +### Constructors + + +| [<init>](test/-c/-init-) | `C()` | + diff --git a/core/testdata/format/javaCodeLiteralTags.java b/core/testdata/format/javaCodeLiteralTags.java new file mode 100644 index 00000000..e71ddaa7 --- /dev/null +++ b/core/testdata/format/javaCodeLiteralTags.java @@ -0,0 +1,6 @@ +/** + * <p>{@code A<B>C}</p> + * <p>{@literal A<B>C}</p> + */ +class C { +} diff --git a/core/testdata/format/javaCodeLiteralTags.md b/core/testdata/format/javaCodeLiteralTags.md new file mode 100644 index 00000000..83c9cc33 --- /dev/null +++ b/core/testdata/format/javaCodeLiteralTags.md @@ -0,0 +1,23 @@ +[test](test/index) / [C](test/-c/index) + + +# C + +`protected open class C` + + +`A<B>C` + + +A<B>C + + + + + + +### Constructors + + +| [<init>](test/-c/-init-) | `C()` | + diff --git a/core/testdata/format/javaDeprecated.html b/core/testdata/format/javaDeprecated.html new file mode 100644 index 00000000..236c6490 --- /dev/null +++ b/core/testdata/format/javaDeprecated.html @@ -0,0 +1,14 @@ +<HTML> +<HEAD> +<title>test / Foo.foo</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-foo/index">Foo</a> / <a href="test/-foo/foo">foo</a><br/> +<br/> +<h1>foo</h1> +<code><span class="keyword">open</span> <span class="keyword">fun </span><s><span class="identifier">foo</span></s><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<strong>Deprecated:</strong> use <code><a href="test/-foo/bar">#bar</a></code> instead <p></p> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/javaDeprecated.java b/core/testdata/format/javaDeprecated.java new file mode 100644 index 00000000..9694a444 --- /dev/null +++ b/core/testdata/format/javaDeprecated.java @@ -0,0 +1,5 @@ +class Foo { + /** @deprecated use {@link #bar} instead */ + public void foo() {} + public void bar() {} +} diff --git a/core/testdata/format/javaLinkTag.html b/core/testdata/format/javaLinkTag.html new file mode 100644 index 00000000..1b6921ab --- /dev/null +++ b/core/testdata/format/javaLinkTag.html @@ -0,0 +1,36 @@ +<HTML> +<HEAD> +<title>test / Foo</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-foo/index">Foo</a><br/> +<br/> +<h1>Foo</h1> +<code><span class="keyword">protected</span> <span class="keyword">open</span> <span class="keyword">class </span><span class="identifier">Foo</span></code><br/> +<p>Call <code><a href="test/-foo/bar">#bar()</a></code> to do the job. </p> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-foo/-init-"><init></a></td> +<td> +<code><span class="identifier">Foo</span><span class="symbol">(</span><span class="symbol">)</span></code></td> +</tr> +</tbody> +</table> +<h3>Functions</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-foo/bar">bar</a></td> +<td> +<code><span class="keyword">open</span> <span class="keyword">fun </span><span class="identifier">bar</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/javaLinkTag.java b/core/testdata/format/javaLinkTag.java new file mode 100644 index 00000000..84f761c6 --- /dev/null +++ b/core/testdata/format/javaLinkTag.java @@ -0,0 +1,6 @@ +/** + * Call {@link #bar()} to do the job. + */ +class Foo { + public void bar() +} diff --git a/core/testdata/format/javaLinkTagWithLabel.html b/core/testdata/format/javaLinkTagWithLabel.html new file mode 100644 index 00000000..aabfbc58 --- /dev/null +++ b/core/testdata/format/javaLinkTagWithLabel.html @@ -0,0 +1,36 @@ +<HTML> +<HEAD> +<title>test / Foo</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-foo/index">Foo</a><br/> +<br/> +<h1>Foo</h1> +<code><span class="keyword">protected</span> <span class="keyword">open</span> <span class="keyword">class </span><span class="identifier">Foo</span></code><br/> +<p>Call <code><a href="test/-foo/bar">this wonderful method</a></code> to do the job. </p> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-foo/-init-"><init></a></td> +<td> +<code><span class="identifier">Foo</span><span class="symbol">(</span><span class="symbol">)</span></code></td> +</tr> +</tbody> +</table> +<h3>Functions</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-foo/bar">bar</a></td> +<td> +<code><span class="keyword">open</span> <span class="keyword">fun </span><span class="identifier">bar</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/javaLinkTagWithLabel.java b/core/testdata/format/javaLinkTagWithLabel.java new file mode 100644 index 00000000..1db5ad70 --- /dev/null +++ b/core/testdata/format/javaLinkTagWithLabel.java @@ -0,0 +1,6 @@ +/** + * Call {@link #bar() this wonderful method} to do the job. + */ +class Foo { + public void bar() +} diff --git a/core/testdata/format/javaSeeTag.html b/core/testdata/format/javaSeeTag.html new file mode 100644 index 00000000..a6813df3 --- /dev/null +++ b/core/testdata/format/javaSeeTag.html @@ -0,0 +1,38 @@ +<HTML> +<HEAD> +<title>test / Foo</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-foo/index">Foo</a><br/> +<br/> +<h1>Foo</h1> +<code><span class="keyword">open</span> <span class="keyword">class </span><span class="identifier">Foo</span></code><br/> +<p></p> +<strong>See Also</strong><br/> +<a href="test/-foo/bar">#bar</a><br/> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-foo/-init-"><init></a></td> +<td> +<code><span class="identifier">Foo</span><span class="symbol">(</span><span class="symbol">)</span></code></td> +</tr> +</tbody> +</table> +<h3>Functions</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-foo/bar">bar</a></td> +<td> +<code><span class="keyword">open</span> <span class="keyword">fun </span><span class="identifier">bar</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/javaSeeTag.java b/core/testdata/format/javaSeeTag.java new file mode 100644 index 00000000..94a24606 --- /dev/null +++ b/core/testdata/format/javaSeeTag.java @@ -0,0 +1,6 @@ +/** + * @see #bar + */ +public class Foo { + public void bar() {} +}
\ No newline at end of file diff --git a/core/testdata/format/javaSpaceInAuthor.java b/core/testdata/format/javaSpaceInAuthor.java new file mode 100644 index 00000000..f980ae07 --- /dev/null +++ b/core/testdata/format/javaSpaceInAuthor.java @@ -0,0 +1,5 @@ +/** + * @author Dmitry Jemerov + */ +class C { +}
\ No newline at end of file diff --git a/core/testdata/format/javaSpaceInAuthor.md b/core/testdata/format/javaSpaceInAuthor.md new file mode 100644 index 00000000..4d19ed01 --- /dev/null +++ b/core/testdata/format/javaSpaceInAuthor.md @@ -0,0 +1,19 @@ +[test](test/index) / [C](test/-c/index) + + +# C + +`protected open class C` + + + +**Author** +Dmitry Jemerov + + + +### Constructors + + +| [<init>](test/-c/-init-) | `C()` | + diff --git a/core/testdata/format/javaSupertype.html b/core/testdata/format/javaSupertype.html new file mode 100644 index 00000000..4c847281 --- /dev/null +++ b/core/testdata/format/javaSupertype.html @@ -0,0 +1,35 @@ +<HTML> +<HEAD> +<title>test / C.Bar</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-c/index">C</a> / <a href="test/-c/-bar/index">Bar</a><br/> +<br/> +<h1>Bar</h1> +<code><span class="keyword">open</span> <span class="keyword">class </span><span class="identifier">Bar</span> <span class="symbol">:</span> <a href="test/-c/-foo/index"><span class="identifier">Foo</span></a></code><br/> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-c/-bar/-init-"><init></a></td> +<td> +<code><span class="identifier">Bar</span><span class="symbol">(</span><span class="symbol">)</span></code></td> +</tr> +</tbody> +</table> +<h3>Functions</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-c/-bar/return-foo">returnFoo</a></td> +<td> +<code><span class="keyword">open</span> <span class="keyword">fun </span><span class="identifier">returnFoo</span><span class="symbol">(</span><span class="identifier">foo</span><span class="symbol">:</span> <a href="test/-c/-foo/index"><span class="identifier">Foo</span></a><span class="symbol">)</span><span class="symbol">: </span><a href="test/-c/-foo/index"><span class="identifier">Foo</span></a></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/javaSupertype.java b/core/testdata/format/javaSupertype.java new file mode 100644 index 00000000..b3142ff6 --- /dev/null +++ b/core/testdata/format/javaSupertype.java @@ -0,0 +1,8 @@ +class C { + public static class Foo { + } + + public static class Bar extends Foo { + public Foo returnFoo(Foo foo) { return foo; } + } +} diff --git a/core/testdata/format/javadocHtml.java b/core/testdata/format/javadocHtml.java new file mode 100644 index 00000000..622116b2 --- /dev/null +++ b/core/testdata/format/javadocHtml.java @@ -0,0 +1,14 @@ +/** + * <b>Bold</b> + * <strong>Strong</strong> + * <i>Italic</i> + * <em>Emphasized</em> + * <p>Paragraph</p> + * <s>Strikethrough</s> + * <del>Deleted</del> + * <code>Code</code> + * <pre>Block code</pre> + * <ul><li>List Item</li></ul> + */ +public class C { +} diff --git a/core/testdata/format/javadocHtml.md b/core/testdata/format/javadocHtml.md new file mode 100644 index 00000000..db71cc3b --- /dev/null +++ b/core/testdata/format/javadocHtml.md @@ -0,0 +1,27 @@ +[test](test/index) / [C](test/-c/index) + + +# C + +`open class C` + +**Bold** **Strong** *Italic* *Emphasized* +Paragraph + + ~~Strikethrough~~ ~~Deleted~~ `Code` +``` +Block code +``` + + * List Item + + + + + + +### Constructors + + +| [<init>](test/-c/-init-) | `C()` | + diff --git a/core/testdata/format/javadocOrderedList.java b/core/testdata/format/javadocOrderedList.java new file mode 100644 index 00000000..c32d9032 --- /dev/null +++ b/core/testdata/format/javadocOrderedList.java @@ -0,0 +1,8 @@ +/** + * <ol> + * <li>Rinse</li> + * <li>Repeat</li> + * </ol> + */ +public class Bar { +} diff --git a/core/testdata/format/javadocOrderedList.md b/core/testdata/format/javadocOrderedList.md new file mode 100644 index 00000000..02a3c095 --- /dev/null +++ b/core/testdata/format/javadocOrderedList.md @@ -0,0 +1,20 @@ +[test](test/index) / [Bar](test/-bar/index) + + +# Bar + +`open class Bar` + + 1. Rinse + 1. Repeat + + + + + + +### Constructors + + +| [<init>](test/-bar/-init-) | `Bar()` | + diff --git a/core/testdata/format/linkWithLabel.html b/core/testdata/format/linkWithLabel.html new file mode 100644 index 00000000..75769ffd --- /dev/null +++ b/core/testdata/format/linkWithLabel.html @@ -0,0 +1,37 @@ +<HTML> +<HEAD> +<title>test / Bar</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-bar/index">Bar</a><br/> +<br/> +<h1>Bar</h1> +<code><span class="keyword">class </span><span class="identifier">Bar</span></code><br/> +<p>Use <a href="test/-bar/foo">this method</a> for best results.</p> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-bar/-init-"><init></a></td> +<td> +<code><span class="identifier">Bar</span><span class="symbol">(</span><span class="symbol">)</span></code><p>Use <a href="test/-bar/foo">this method</a> for best results.</p> +</td> +</tr> +</tbody> +</table> +<h3>Functions</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-bar/foo">foo</a></td> +<td> +<code><span class="keyword">fun </span><span class="identifier">foo</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/linkWithLabel.kt b/core/testdata/format/linkWithLabel.kt new file mode 100644 index 00000000..4a85c505 --- /dev/null +++ b/core/testdata/format/linkWithLabel.kt @@ -0,0 +1,6 @@ +/** + * Use [this method][Bar.foo] for best results. + */ +class Bar { + fun foo() {} +} diff --git a/core/testdata/format/nullability.kt b/core/testdata/format/nullability.kt new file mode 100644 index 00000000..d1d4545b --- /dev/null +++ b/core/testdata/format/nullability.kt @@ -0,0 +1,5 @@ +class C<T> { + fun foo(): Comparable<T>? { + return null + } +} diff --git a/core/testdata/format/nullability.md b/core/testdata/format/nullability.md new file mode 100644 index 00000000..ee50a0a5 --- /dev/null +++ b/core/testdata/format/nullability.md @@ -0,0 +1,20 @@ +[test](test/index) / [C](test/-c/index) + + +# C + +`class C<T>` + + + +### Constructors + + +| [<init>](test/-c/-init-) | `C()` | + + +### Functions + + +| [foo](test/-c/foo) | `fun foo(): Comparable<T>?` | + diff --git a/core/testdata/format/operatorOverloading.kt b/core/testdata/format/operatorOverloading.kt new file mode 100644 index 00000000..6fe78e45 --- /dev/null +++ b/core/testdata/format/operatorOverloading.kt @@ -0,0 +1,3 @@ +class C { + fun plus(other: C): C +} diff --git a/core/testdata/format/operatorOverloading.md b/core/testdata/format/operatorOverloading.md new file mode 100644 index 00000000..05fe3206 --- /dev/null +++ b/core/testdata/format/operatorOverloading.md @@ -0,0 +1,8 @@ +[test](test/index) / [C](test/-c/index) / [plus](test/-c/plus) + + +# plus + +`fun plus(other: [C](test/-c/index)): [C](test/-c/index)` + + diff --git a/core/testdata/format/orderedList.html b/core/testdata/format/orderedList.html new file mode 100644 index 00000000..9917568f --- /dev/null +++ b/core/testdata/format/orderedList.html @@ -0,0 +1,30 @@ +<HTML> +<HEAD> +<title>test / Bar</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-bar/index">Bar</a><br/> +<br/> +<h1>Bar</h1> +<code><span class="keyword">class </span><span class="identifier">Bar</span></code><br/> +<p>Usage instructions:</p> +<ol><li><p>Rinse</p> +</li><li><p>Repeat</p> +</li></ol><br/> +<br/> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-bar/-init-"><init></a></td> +<td> +<code><span class="identifier">Bar</span><span class="symbol">(</span><span class="symbol">)</span></code><p>Usage instructions:</p> +</td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/orderedList.kt b/core/testdata/format/orderedList.kt new file mode 100644 index 00000000..03681c7a --- /dev/null +++ b/core/testdata/format/orderedList.kt @@ -0,0 +1,8 @@ +/** + * Usage instructions: + * + * 1. Rinse + * 1. Repeat + */ +class Bar { +} diff --git a/core/testdata/format/overloads.html b/core/testdata/format/overloads.html new file mode 100644 index 00000000..752f3989 --- /dev/null +++ b/core/testdata/format/overloads.html @@ -0,0 +1,23 @@ +<HTML> +<HEAD> +<title>test / root package</title> +</HEAD> +<BODY> +<a href="test/index">test</a><br/> +<br/> +<h2>Package </h2> +<h3>Functions</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/f">f</a></td> +<td> +<code><span class="keyword">fun </span><span class="identifier">f</span><span class="symbol">(</span><span class="identifier">x</span><span class="symbol">:</span> <span class="identifier">Int</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<code><span class="keyword">fun </span><span class="identifier">f</span><span class="symbol">(</span><span class="identifier">x</span><span class="symbol">:</span> <span class="identifier">String</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><p>Performs an action on x.</p> +</td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/overloads.kt b/core/testdata/format/overloads.kt new file mode 100644 index 00000000..dcd2d097 --- /dev/null +++ b/core/testdata/format/overloads.kt @@ -0,0 +1,5 @@ +/** Performs an action on x. */ +fun f(x: Int) { } + +/** Performs an action on x. */ +fun f(x: String) { } diff --git a/core/testdata/format/overloadsWithDescription.html b/core/testdata/format/overloadsWithDescription.html new file mode 100644 index 00000000..a0efb472 --- /dev/null +++ b/core/testdata/format/overloadsWithDescription.html @@ -0,0 +1,21 @@ +<HTML> +<HEAD> +<title>test / f</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/f">f</a><br/> +<br/> +<h1>f</h1> +<code><span class="keyword">fun </span><span class="identifier">f</span><span class="symbol">(</span><span class="identifier">x</span><span class="symbol">:</span> <span class="identifier">Int</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<code><span class="keyword">fun </span><span class="identifier">f</span><span class="symbol">(</span><span class="identifier">x</span><span class="symbol">:</span> <span class="identifier">String</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<p>Performs an action on <a href="test/f#x">x</a>.</p> +<p>This is a long description.</p> +<br/> +<br/> +<h3>Parameters</h3> +<a name="x"></a> +<code>x</code> - the value to perform the action on.<br/> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/overloadsWithDescription.kt b/core/testdata/format/overloadsWithDescription.kt new file mode 100644 index 00000000..740e642f --- /dev/null +++ b/core/testdata/format/overloadsWithDescription.kt @@ -0,0 +1,15 @@ +/** + * Performs an action on [x]. + * + * This is a long description. + * @param x the value to perform the action on. + */ +fun f(x: Int) { } + +/** + * Performs an action on [x]. + * + * This is a long description. + * @param x the value to perform the action on. + */ +fun f(x: String) { } diff --git a/core/testdata/format/overloadsWithDifferentDescriptions.html b/core/testdata/format/overloadsWithDifferentDescriptions.html new file mode 100644 index 00000000..30a37e75 --- /dev/null +++ b/core/testdata/format/overloadsWithDifferentDescriptions.html @@ -0,0 +1,30 @@ +<HTML> +<HEAD> +<title>test / f</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/f">f</a><br/> +<br/> +<h1>f</h1> +<code><span class="keyword">fun </span><span class="identifier">f</span><span class="symbol">(</span><span class="identifier">x</span><span class="symbol">:</span> <span class="identifier">Int</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<p>Performs an action on x.</p> +<p>This is a long description.</p> +<br/> +<br/> +<h3>Parameters</h3> +<a name="x"></a> +<code>x</code> - the int value to perform the action on.<br/> +<br/> +<br/> +<code><span class="keyword">fun </span><span class="identifier">f</span><span class="symbol">(</span><span class="identifier">x</span><span class="symbol">:</span> <span class="identifier">String</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<p>Performs an action on x.</p> +<p>This is a long description.</p> +<br/> +<br/> +<h3>Parameters</h3> +<a name="x"></a> +<code>x</code> - the string value to perform the action on.<br/> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/overloadsWithDifferentDescriptions.kt b/core/testdata/format/overloadsWithDifferentDescriptions.kt new file mode 100644 index 00000000..ad3169b0 --- /dev/null +++ b/core/testdata/format/overloadsWithDifferentDescriptions.kt @@ -0,0 +1,15 @@ +/** + * Performs an action on x. + * + * This is a long description. + * @param x the int value to perform the action on. + */ +fun f(x: Int) { } + +/** + * Performs an action on x. + * + * This is a long description. + * @param x the string value to perform the action on. + */ +fun f(x: String) { } diff --git a/core/testdata/format/overridingFunction.kt b/core/testdata/format/overridingFunction.kt new file mode 100644 index 00000000..d7329489 --- /dev/null +++ b/core/testdata/format/overridingFunction.kt @@ -0,0 +1,7 @@ +open class C() { + open fun f() {} +} + +class D(): C() { + override fun f() {} +} diff --git a/core/testdata/format/overridingFunction.md b/core/testdata/format/overridingFunction.md new file mode 100644 index 00000000..2c898b9c --- /dev/null +++ b/core/testdata/format/overridingFunction.md @@ -0,0 +1,9 @@ +[test](test/index) / [D](test/-d/index) / [f](test/-d/f) + + +# f + +`fun f(): Unit` +Overrides [C.f](test/-c/f) + + diff --git a/core/testdata/format/paramTag.kt b/core/testdata/format/paramTag.kt new file mode 100644 index 00000000..47e471f5 --- /dev/null +++ b/core/testdata/format/paramTag.kt @@ -0,0 +1,6 @@ +/** + * @param x A string + * @param y A number with a really long description that spans multiple lines and goes + * on and on and is very interesting to read + */ +fun f(x: String, y: Int) {} diff --git a/core/testdata/format/paramTag.md b/core/testdata/format/paramTag.md new file mode 100644 index 00000000..882083fe --- /dev/null +++ b/core/testdata/format/paramTag.md @@ -0,0 +1,14 @@ +[test](test/index) / [f](test/f) + + +# f + +`fun f(x: String, y: Int): Unit` + +### Parameters + +`x` - A string +`y` - A number with a really long description that spans multiple lines and goes +on and on and is very interesting to read + + diff --git a/core/testdata/format/parameterAnchor.html b/core/testdata/format/parameterAnchor.html new file mode 100644 index 00000000..c5920fb7 --- /dev/null +++ b/core/testdata/format/parameterAnchor.html @@ -0,0 +1,17 @@ +<HTML> +<HEAD> +<title>test / processFiles</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/process-files">processFiles</a><br/> +<br/> +<h1>processFiles</h1> +<code><span class="keyword">fun </span><span class="symbol"><</span><span class="identifier">T</span><span class="symbol">></span> <span class="identifier">processFiles</span><span class="symbol">(</span><span class="identifier">processor</span><span class="symbol">:</span> <span class="symbol">(</span><span class="symbol">)</span> <span class="symbol">-></span> <span class="identifier">T</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">List</span><span class="symbol"><</span><span class="identifier">T</span><span class="symbol">></span></code><br/> +<p>Runs <a href="test/process-files#processor">processor</a> for each file and collects its results into single list</p> +<h3>Parameters</h3> +<a name="processor"></a> +<code>processor</code> - function to receive context for symbol resolution and file for processing<br/> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/parameterAnchor.kt b/core/testdata/format/parameterAnchor.kt new file mode 100644 index 00000000..ae36ee4c --- /dev/null +++ b/core/testdata/format/parameterAnchor.kt @@ -0,0 +1,6 @@ +/** + * Runs [processor] for each file and collects its results into single list + * @param processor function to receive context for symbol resolution and file for processing + */ +public fun processFiles<T>(processor: () -> T): List<T> { +} diff --git a/core/testdata/format/parenthesis.html b/core/testdata/format/parenthesis.html new file mode 100644 index 00000000..c36b311b --- /dev/null +++ b/core/testdata/format/parenthesis.html @@ -0,0 +1,14 @@ +<HTML> +<HEAD> +<title>test / foo</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/foo">foo</a><br/> +<br/> +<h1>foo</h1> +<code><span class="keyword">fun </span><span class="identifier">foo</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<p>foo (bar)</p> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/parenthesis.kt b/core/testdata/format/parenthesis.kt new file mode 100644 index 00000000..b906f64a --- /dev/null +++ b/core/testdata/format/parenthesis.kt @@ -0,0 +1,4 @@ +/** + * foo (bar) + */ +fun foo() {} diff --git a/core/testdata/format/propertyVar.kt b/core/testdata/format/propertyVar.kt new file mode 100644 index 00000000..88be1a7a --- /dev/null +++ b/core/testdata/format/propertyVar.kt @@ -0,0 +1 @@ +var x = 1
\ No newline at end of file diff --git a/core/testdata/format/propertyVar.md b/core/testdata/format/propertyVar.md new file mode 100644 index 00000000..f2110992 --- /dev/null +++ b/core/testdata/format/propertyVar.md @@ -0,0 +1,8 @@ +[test](test/index) / [x](test/x) + + +# x + +`var x: Int` + + diff --git a/core/testdata/format/reifiedTypeParameter.kt b/core/testdata/format/reifiedTypeParameter.kt new file mode 100644 index 00000000..00fa1dc9 --- /dev/null +++ b/core/testdata/format/reifiedTypeParameter.kt @@ -0,0 +1,3 @@ +inline fun f<reified T>() { + +} diff --git a/core/testdata/format/reifiedTypeParameter.md b/core/testdata/format/reifiedTypeParameter.md new file mode 100644 index 00000000..2e96018e --- /dev/null +++ b/core/testdata/format/reifiedTypeParameter.md @@ -0,0 +1,8 @@ +[test](test/index) / [f](test/f) + + +# f + +`inline fun <reified T> f(): Unit` + + diff --git a/core/testdata/format/see.html b/core/testdata/format/see.html new file mode 100644 index 00000000..b3ffb74b --- /dev/null +++ b/core/testdata/format/see.html @@ -0,0 +1,28 @@ +<HTML> +<HEAD> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/quux">quux</a><br/> +<br/> +<h1>quux</h1> +<code><span class="keyword">fun </span><span class="identifier">quux</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<strong>See Also</strong><br/> +<p><a href="test/foo">foo</a></p> +<p><a href="test/bar">bar</a></p> +<br/> +<br/> +<br/> +<a href="test/index">test</a> / <a href="test/foo">foo</a><br/> +<br/> +<h1>foo</h1> +<code><span class="keyword">fun </span><span class="identifier">foo</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<br/> +<br/> +<a href="test/index">test</a> / <a href="test/bar">bar</a><br/> +<br/> +<h1>bar</h1> +<code><span class="keyword">fun </span><span class="identifier">bar</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/see.kt b/core/testdata/format/see.kt new file mode 100644 index 00000000..a0b153b0 --- /dev/null +++ b/core/testdata/format/see.kt @@ -0,0 +1,12 @@ +/** + * @see foo + * @see bar + */ +fun quux() { +} + +fun foo() { +} + +fun bar() { +}
\ No newline at end of file diff --git a/core/testdata/format/starProjection.kt b/core/testdata/format/starProjection.kt new file mode 100644 index 00000000..48d53e47 --- /dev/null +++ b/core/testdata/format/starProjection.kt @@ -0,0 +1,3 @@ +public fun Iterable<*>.containsFoo(element: Any?): Boolean { + return false +} diff --git a/core/testdata/format/starProjection.md b/core/testdata/format/starProjection.md new file mode 100644 index 00000000..c9be2f58 --- /dev/null +++ b/core/testdata/format/starProjection.md @@ -0,0 +1,8 @@ +[test](test/index) / [kotlin.Iterable](test/kotlin.-iterable/index) + + +### Extensions for kotlin.Iterable + + +| [containsFoo](test/kotlin.-iterable/contains-foo) | `fun Iterable<*>.containsFoo(element: Any?): Boolean` | + diff --git a/core/testdata/format/summarizeSignatures.kt b/core/testdata/format/summarizeSignatures.kt new file mode 100644 index 00000000..1d875a50 --- /dev/null +++ b/core/testdata/format/summarizeSignatures.kt @@ -0,0 +1,20 @@ +package kotlin + +class Array<T> +class IntArray +class CharArray + +/** + * Returns true if foo. + */ +fun IntArray.foo(predicate: (Int) -> Boolean): Boolean = false + +/** + * Returns true if foo. + */ +fun CharArray.foo(predicate: (Char) -> Boolean): Boolean = false + +/** + * Returns true if foo. + */ +fun <T> Array<T>.foo(predicate: (T) -> Boolean): Boolean = false diff --git a/core/testdata/format/summarizeSignatures.md b/core/testdata/format/summarizeSignatures.md new file mode 100644 index 00000000..b1707f40 --- /dev/null +++ b/core/testdata/format/summarizeSignatures.md @@ -0,0 +1,23 @@ +[test](test/index) / [kotlin](test/kotlin/index) + + +## Package kotlin + + +### Types + + +| [Array](test/kotlin/-array/index) | `class Array<T>` | +| [CharArray](test/kotlin/-char-array/index) | `class CharArray` | +| [IntArray](test/kotlin/-int-array/index) | `class IntArray` | + + +### Functions + + +| [foo](test/kotlin/foo) | `fun <T> any_array<T>.foo(predicate: (T) -> Boolean): Boolean` + +Returns true if foo. + + | + diff --git a/core/testdata/format/summarizeSignaturesProperty.kt b/core/testdata/format/summarizeSignaturesProperty.kt new file mode 100644 index 00000000..fbbdd328 --- /dev/null +++ b/core/testdata/format/summarizeSignaturesProperty.kt @@ -0,0 +1,20 @@ +package kotlin + +class Array<T> +class IntArray +class CharArray + +/** + * Returns true if foo. + */ +val IntArray.foo: Int = 0 + +/** + * Returns true if foo. + */ +val CharArray.foo: Int = 0 + +/** + * Returns true if foo. + */ +val <T> Array<T>.foo: Int = 0 diff --git a/core/testdata/format/summarizeSignaturesProperty.md b/core/testdata/format/summarizeSignaturesProperty.md new file mode 100644 index 00000000..9646b0f1 --- /dev/null +++ b/core/testdata/format/summarizeSignaturesProperty.md @@ -0,0 +1,23 @@ +[test](test/index) / [kotlin](test/kotlin/index) + + +## Package kotlin + + +### Types + + +| [Array](test/kotlin/-array/index) | `class Array<T>` | +| [CharArray](test/kotlin/-char-array/index) | `class CharArray` | +| [IntArray](test/kotlin/-int-array/index) | `class IntArray` | + + +### Properties + + +| [foo](test/kotlin/foo) | `val <T> any_array<T>.foo: Int` + +Returns true if foo. + + | + diff --git a/core/testdata/format/throwsTag.kt b/core/testdata/format/throwsTag.kt new file mode 100644 index 00000000..29a9c3f7 --- /dev/null +++ b/core/testdata/format/throwsTag.kt @@ -0,0 +1,5 @@ +/** + * @throws IllegalArgumentException on Mondays + * @exception NullPointerException on Tuesdays + */ +fun f() {} diff --git a/core/testdata/format/throwsTag.md b/core/testdata/format/throwsTag.md new file mode 100644 index 00000000..d0a4e610 --- /dev/null +++ b/core/testdata/format/throwsTag.md @@ -0,0 +1,13 @@ +[test](test/index) / [f](test/f) + + +# f + +`fun f(): Unit` + +### Exceptions + +`IllegalArgumentException` - on Mondays +`NullPointerException` - on Tuesdays + + diff --git a/core/testdata/format/tripleBackticks.html b/core/testdata/format/tripleBackticks.html new file mode 100644 index 00000000..13954985 --- /dev/null +++ b/core/testdata/format/tripleBackticks.html @@ -0,0 +1,16 @@ +<HTML> +<HEAD> +<title>test / f</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/f">f</a><br/> +<br/> +<h1>f</h1> +<code><span class="keyword">fun </span><span class="identifier">f</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">: </span><span class="identifier">Unit</span></code><br/> +<p>Description</p> +<pre><code>code sample</code></pre><br/> +<br/> +<br/> +<br/> +</BODY> +</HTML> diff --git a/core/testdata/format/tripleBackticks.kt b/core/testdata/format/tripleBackticks.kt new file mode 100644 index 00000000..54dfa6d5 --- /dev/null +++ b/core/testdata/format/tripleBackticks.kt @@ -0,0 +1,7 @@ +/** + * Description + * ``` + * code sample + * ``` + */ +fun f() {} diff --git a/core/testdata/format/typeLink.html b/core/testdata/format/typeLink.html new file mode 100644 index 00000000..ff99090a --- /dev/null +++ b/core/testdata/format/typeLink.html @@ -0,0 +1,24 @@ +<HTML> +<HEAD> +<title>test / Bar</title> +</HEAD> +<BODY> +<a href="test/index">test</a> / <a href="test/-bar/index">Bar</a><br/> +<br/> +<h1>Bar</h1> +<code><span class="keyword">class </span><span class="identifier">Bar</span> <span class="symbol">:</span> <a href="test/-foo/index"><span class="identifier">Foo</span></a></code><br/> +<br/> +<br/> +<h3>Constructors</h3> +<table> +<tbody> +<tr> +<td> +<a href="test/-bar/-init-"><init></a></td> +<td> +<code><span class="identifier">Bar</span><span class="symbol">(</span><span class="symbol">)</span></code></td> +</tr> +</tbody> +</table> +</BODY> +</HTML> diff --git a/core/testdata/format/typeLink.kt b/core/testdata/format/typeLink.kt new file mode 100644 index 00000000..966e020e --- /dev/null +++ b/core/testdata/format/typeLink.kt @@ -0,0 +1,5 @@ +class Foo() { +} + +class Bar(): Foo { +} diff --git a/core/testdata/format/typeParameterBounds.kt b/core/testdata/format/typeParameterBounds.kt new file mode 100644 index 00000000..5f22f8c5 --- /dev/null +++ b/core/testdata/format/typeParameterBounds.kt @@ -0,0 +1,6 @@ + +/** + * generic function + */ +public fun <T : R, R> generic() { +}
\ No newline at end of file diff --git a/core/testdata/format/typeParameterBounds.md b/core/testdata/format/typeParameterBounds.md new file mode 100644 index 00000000..fe597878 --- /dev/null +++ b/core/testdata/format/typeParameterBounds.md @@ -0,0 +1,11 @@ +[test](test/index) / [generic](test/generic) + + +# generic + +`fun <T : R, R> generic(): Unit` + +generic function + + + diff --git a/core/testdata/format/typeParameterVariance.kt b/core/testdata/format/typeParameterVariance.kt new file mode 100644 index 00000000..28b29f4b --- /dev/null +++ b/core/testdata/format/typeParameterVariance.kt @@ -0,0 +1,2 @@ +class Foo<out T> { +} diff --git a/core/testdata/format/typeParameterVariance.md b/core/testdata/format/typeParameterVariance.md new file mode 100644 index 00000000..c0bfa6de --- /dev/null +++ b/core/testdata/format/typeParameterVariance.md @@ -0,0 +1,14 @@ +[test](test/index) / [Foo](test/-foo/index) + + +# Foo + +`class Foo<out T>` + + + +### Constructors + + +| [<init>](test/-foo/-init-) | `Foo()` | + diff --git a/core/testdata/format/typeProjectionVariance.kt b/core/testdata/format/typeProjectionVariance.kt new file mode 100644 index 00000000..85ee344d --- /dev/null +++ b/core/testdata/format/typeProjectionVariance.kt @@ -0,0 +1 @@ +fun <T> Array<out T>.foo() {} diff --git a/core/testdata/format/typeProjectionVariance.md b/core/testdata/format/typeProjectionVariance.md new file mode 100644 index 00000000..7aa34593 --- /dev/null +++ b/core/testdata/format/typeProjectionVariance.md @@ -0,0 +1,8 @@ +[test](test/index) / [kotlin.Array](test/kotlin.-array/index) + + +### Extensions for kotlin.Array + + +| [foo](test/kotlin.-array/foo) | `fun <T> Array<out T>.foo(): Unit` | + diff --git a/core/testdata/format/varargsFunction.kt b/core/testdata/format/varargsFunction.kt new file mode 100644 index 00000000..deea127b --- /dev/null +++ b/core/testdata/format/varargsFunction.kt @@ -0,0 +1 @@ +fun f(vararg s: String) {} diff --git a/core/testdata/format/varargsFunction.md b/core/testdata/format/varargsFunction.md new file mode 100644 index 00000000..723437aa --- /dev/null +++ b/core/testdata/format/varargsFunction.md @@ -0,0 +1,8 @@ +[test](test/index) / [f](test/f) + + +# f + +`fun f(vararg s: String): Unit` + + diff --git a/core/testdata/functions/annotatedFunction.kt b/core/testdata/functions/annotatedFunction.kt new file mode 100644 index 00000000..f7abbf6c --- /dev/null +++ b/core/testdata/functions/annotatedFunction.kt @@ -0,0 +1,2 @@ +@Strictfp fun f() { +} diff --git a/core/testdata/functions/annotatedFunctionWithAnnotationParameters.kt b/core/testdata/functions/annotatedFunctionWithAnnotationParameters.kt new file mode 100644 index 00000000..e559713a --- /dev/null +++ b/core/testdata/functions/annotatedFunctionWithAnnotationParameters.kt @@ -0,0 +1,7 @@ +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +public annotation class Fancy(val size: Int) + + +@Fancy(1) fun f() {} diff --git a/core/testdata/functions/function.kt b/core/testdata/functions/function.kt new file mode 100644 index 00000000..3ed81dfa --- /dev/null +++ b/core/testdata/functions/function.kt @@ -0,0 +1,5 @@ +/** + * Function fn + */ +fun fn() { +}
\ No newline at end of file diff --git a/core/testdata/functions/functionWithAnnotatedParam.kt b/core/testdata/functions/functionWithAnnotatedParam.kt new file mode 100644 index 00000000..f858e671 --- /dev/null +++ b/core/testdata/functions/functionWithAnnotatedParam.kt @@ -0,0 +1,7 @@ +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +public annotation class Fancy + +fun function(@Fancy notInlined: () -> Unit) { +} diff --git a/core/testdata/functions/functionWithDefaultParameter.kt b/core/testdata/functions/functionWithDefaultParameter.kt new file mode 100644 index 00000000..3a3a102f --- /dev/null +++ b/core/testdata/functions/functionWithDefaultParameter.kt @@ -0,0 +1 @@ +fun f(x: String = "") {} diff --git a/core/testdata/functions/functionWithNoinlineParam.kt b/core/testdata/functions/functionWithNoinlineParam.kt new file mode 100644 index 00000000..640bec83 --- /dev/null +++ b/core/testdata/functions/functionWithNoinlineParam.kt @@ -0,0 +1,2 @@ +fun function(noinline notInlined: () -> Unit) { +} diff --git a/core/testdata/functions/functionWithNotDocumentedAnnotation.kt b/core/testdata/functions/functionWithNotDocumentedAnnotation.kt new file mode 100644 index 00000000..3c7e2ff9 --- /dev/null +++ b/core/testdata/functions/functionWithNotDocumentedAnnotation.kt @@ -0,0 +1,2 @@ +@Suppress("FOO") fun f() { +} diff --git a/core/testdata/functions/functionWithParams.kt b/core/testdata/functions/functionWithParams.kt new file mode 100644 index 00000000..85c49368 --- /dev/null +++ b/core/testdata/functions/functionWithParams.kt @@ -0,0 +1,8 @@ +/** + * Multiline + * + * Function + * Documentation + */ +fun function(/** parameter */ x: Int) { +}
\ No newline at end of file diff --git a/core/testdata/functions/functionWithReceiver.kt b/core/testdata/functions/functionWithReceiver.kt new file mode 100644 index 00000000..c8473251 --- /dev/null +++ b/core/testdata/functions/functionWithReceiver.kt @@ -0,0 +1,11 @@ +/** + * Function with receiver + */ +fun String.fn() { +} + +/** + * Function with receiver + */ +fun String.fn(x: Int) { +} diff --git a/core/testdata/functions/genericFunction.kt b/core/testdata/functions/genericFunction.kt new file mode 100644 index 00000000..05a65070 --- /dev/null +++ b/core/testdata/functions/genericFunction.kt @@ -0,0 +1,5 @@ + +/** + * generic function + */ +private fun <T> generic() {}
\ No newline at end of file diff --git a/core/testdata/functions/genericFunctionWithConstraints.kt b/core/testdata/functions/genericFunctionWithConstraints.kt new file mode 100644 index 00000000..5f22f8c5 --- /dev/null +++ b/core/testdata/functions/genericFunctionWithConstraints.kt @@ -0,0 +1,6 @@ + +/** + * generic function + */ +public fun <T : R, R> generic() { +}
\ No newline at end of file diff --git a/core/testdata/functions/inlineFunction.kt b/core/testdata/functions/inlineFunction.kt new file mode 100644 index 00000000..11c19672 --- /dev/null +++ b/core/testdata/functions/inlineFunction.kt @@ -0,0 +1,2 @@ +inline fun f() { +} diff --git a/core/testdata/java/annotatedAnnotation.java b/core/testdata/java/annotatedAnnotation.java new file mode 100644 index 00000000..6296e400 --- /dev/null +++ b/core/testdata/java/annotatedAnnotation.java @@ -0,0 +1,6 @@ +import java.lang.annotation.*; + +@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD}) +public @interface Attribute { + String value() default ""; +} diff --git a/core/testdata/java/arrayType.java b/core/testdata/java/arrayType.java new file mode 100644 index 00000000..dc42a207 --- /dev/null +++ b/core/testdata/java/arrayType.java @@ -0,0 +1,5 @@ +class Test { + public String[] arrayToString(int[] data) { + return null; + } +} diff --git a/core/testdata/java/constructors.java b/core/testdata/java/constructors.java new file mode 100644 index 00000000..6f899d18 --- /dev/null +++ b/core/testdata/java/constructors.java @@ -0,0 +1,5 @@ +class Test { + public Test() {} + + public Test(String s) {} +} diff --git a/core/testdata/java/deprecation.java b/core/testdata/java/deprecation.java new file mode 100644 index 00000000..be8decd6 --- /dev/null +++ b/core/testdata/java/deprecation.java @@ -0,0 +1,6 @@ +class C { + /** + * @deprecated This should no longer be used + */ + void fn() {} +}
\ No newline at end of file diff --git a/core/testdata/java/enumValues.java b/core/testdata/java/enumValues.java new file mode 100644 index 00000000..d8dda934 --- /dev/null +++ b/core/testdata/java/enumValues.java @@ -0,0 +1,3 @@ +enum E { + Foo +}
\ No newline at end of file diff --git a/core/testdata/java/field.java b/core/testdata/java/field.java new file mode 100644 index 00000000..d9ae75f9 --- /dev/null +++ b/core/testdata/java/field.java @@ -0,0 +1,4 @@ +class Test { + public int i; + public static final String s; +} diff --git a/core/testdata/java/inheritorLinks.java b/core/testdata/java/inheritorLinks.java new file mode 100644 index 00000000..2378f5c6 --- /dev/null +++ b/core/testdata/java/inheritorLinks.java @@ -0,0 +1,7 @@ +class C { + public static class Foo { + } + + public static class Bar extends Foo { + } +} diff --git a/core/testdata/java/innerClass.java b/core/testdata/java/innerClass.java new file mode 100644 index 00000000..0f1be948 --- /dev/null +++ b/core/testdata/java/innerClass.java @@ -0,0 +1,4 @@ +class Test { + public class D { + } +} diff --git a/core/testdata/java/javaLangObject.java b/core/testdata/java/javaLangObject.java new file mode 100644 index 00000000..be3dd570 --- /dev/null +++ b/core/testdata/java/javaLangObject.java @@ -0,0 +1,3 @@ +class Test { + public Object fn() { return null; } +} diff --git a/core/testdata/java/member.java b/core/testdata/java/member.java new file mode 100644 index 00000000..d4f4b8d5 --- /dev/null +++ b/core/testdata/java/member.java @@ -0,0 +1,11 @@ +class Test { + /** + * Summary for Function + * @param name is String parameter + * @param value is int parameter + * @author yole + */ + public void fn(String name, int value) { + + } +}
\ No newline at end of file diff --git a/core/testdata/java/memberWithModifiers.java b/core/testdata/java/memberWithModifiers.java new file mode 100644 index 00000000..ea645c21 --- /dev/null +++ b/core/testdata/java/memberWithModifiers.java @@ -0,0 +1,12 @@ +public abstract class Test { + /** + * Summary for Function + * @param name is String parameter + * @param value is int parameter + */ + protected final void fn(String name, int value) { + + } + + protected void openFn() {} +}
\ No newline at end of file diff --git a/core/testdata/java/staticMethod.java b/core/testdata/java/staticMethod.java new file mode 100644 index 00000000..a2ecd7f1 --- /dev/null +++ b/core/testdata/java/staticMethod.java @@ -0,0 +1,4 @@ +class C { + public static void foo() { + } +} diff --git a/core/testdata/java/superClass.java b/core/testdata/java/superClass.java new file mode 100644 index 00000000..31b5fa96 --- /dev/null +++ b/core/testdata/java/superClass.java @@ -0,0 +1,2 @@ +public class Foo extends Exception implements Cloneable { +} diff --git a/core/testdata/java/typeParameter.java b/core/testdata/java/typeParameter.java new file mode 100644 index 00000000..5a24b30a --- /dev/null +++ b/core/testdata/java/typeParameter.java @@ -0,0 +1,3 @@ +class Foo<T extends Comparable<T>> { + public <E> E foo(); +} diff --git a/core/testdata/java/varargs.java b/core/testdata/java/varargs.java new file mode 100644 index 00000000..d184564e --- /dev/null +++ b/core/testdata/java/varargs.java @@ -0,0 +1,3 @@ +class Foo { + public void bar(String... x); +} diff --git a/core/testdata/javadoc/obj.kt b/core/testdata/javadoc/obj.kt new file mode 100644 index 00000000..1d10a422 --- /dev/null +++ b/core/testdata/javadoc/obj.kt @@ -0,0 +1,7 @@ +package foo + +class O { + companion object { + + } +} diff --git a/core/testdata/javadoc/types.kt b/core/testdata/javadoc/types.kt new file mode 100644 index 00000000..55be6058 --- /dev/null +++ b/core/testdata/javadoc/types.kt @@ -0,0 +1,4 @@ +package foo + +fun foo(x: Int, o: Any): String { +} diff --git a/core/testdata/links/linkToJDK.kt b/core/testdata/links/linkToJDK.kt new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/core/testdata/links/linkToJDK.kt diff --git a/core/testdata/links/linkToMember.kt b/core/testdata/links/linkToMember.kt new file mode 100644 index 00000000..b60eaedb --- /dev/null +++ b/core/testdata/links/linkToMember.kt @@ -0,0 +1,6 @@ +/** + * This is link to [member] + */ +class Foo { + fun member() {} +}
\ No newline at end of file diff --git a/core/testdata/links/linkToParam.kt b/core/testdata/links/linkToParam.kt new file mode 100644 index 00000000..ca42a742 --- /dev/null +++ b/core/testdata/links/linkToParam.kt @@ -0,0 +1,5 @@ +/** + * This is link to [param] + */ +fun Foo(param: String) { +}
\ No newline at end of file diff --git a/core/testdata/links/linkToQualifiedMember.kt b/core/testdata/links/linkToQualifiedMember.kt new file mode 100644 index 00000000..22c154fe --- /dev/null +++ b/core/testdata/links/linkToQualifiedMember.kt @@ -0,0 +1,6 @@ +/** + * This is link to [Foo.member] + */ +class Foo { + fun member() {} +}
\ No newline at end of file diff --git a/core/testdata/links/linkToSelf.kt b/core/testdata/links/linkToSelf.kt new file mode 100644 index 00000000..74395f0f --- /dev/null +++ b/core/testdata/links/linkToSelf.kt @@ -0,0 +1,6 @@ +/** + * This is link to [Foo] + */ +class Foo { + +}
\ No newline at end of file diff --git a/core/testdata/markdown/spec.txt b/core/testdata/markdown/spec.txt new file mode 100644 index 00000000..fce87924 --- /dev/null +++ b/core/testdata/markdown/spec.txt @@ -0,0 +1,6150 @@ +--- +title: CommonMark Spec +author: +- John MacFarlane +version: 2 +date: 2014-09-19 +... + +# Introduction + +## What is Markdown? + +Markdown is a plain text format for writing structured documents, +based on conventions used for indicating formatting in email and +usenet posts. It was developed in 2004 by John Gruber, who wrote +the first Markdown-to-HTML converter in perl, and it soon became +widely used in websites. By 2014 there were dozens of +implementations in many languages. Some of them extended basic +Markdown syntax with conventions for footnotes, definition lists, +tables, and other constructs, and some allowed output not just in +HTML but in LaTeX and many other formats. + +## Why is a spec needed? + +John Gruber's [canonical description of Markdown's +syntax](http://daringfireball.net/projects/markdown/syntax) +does not specify the syntax unambiguously. Here are some examples of +questions it does not answer: + +1. How much indentation is needed for a sublist? The spec says that + continuation paragraphs need to be indented four spaces, but is + not fully explicit about sublists. It is natural to think that + they, too, must be indented four spaces, but `Markdown.pl` does + not require that. This is hardly a "corner case," and divergences + between implementations on this issue often lead to surprises for + users in real documents. (See [this comment by John + Gruber](http://article.gmane.org/gmane.text.markdown.general/1997).) + +2. Is a blank line needed before a block quote or header? + Most implementations do not require the blank line. However, + this can lead to unexpected results in hard-wrapped text, and + also to ambiguities in parsing (note that some implementations + put the header inside the blockquote, while others do not). + (John Gruber has also spoken [in favor of requiring the blank + lines](http://article.gmane.org/gmane.text.markdown.general/2146).) + +3. Is a blank line needed before an indented code block? + (`Markdown.pl` requires it, but this is not mentioned in the + documentation, and some implementations do not require it.) + + ``` markdown + paragraph + code? + ``` + +4. What is the exact rule for determining when list items get + wrapped in `<p>` tags? Can a list be partially "loose" and partially + "tight"? What should we do with a list like this? + + ``` markdown + 1. one + + 2. two + 3. three + ``` + + Or this? + + ``` markdown + 1. one + - a + + - b + 2. two + ``` + + (There are some relevant comments by John Gruber + [here](http://article.gmane.org/gmane.text.markdown.general/2554).) + +5. Can list markers be indented? Can ordered list markers be right-aligned? + + ``` markdown + 8. item 1 + 9. item 2 + 10. item 2a + ``` + +6. Is this one list with a horizontal rule in its second item, + or two lists separated by a horizontal rule? + + ``` markdown + * a + * * * * * + * b + ``` + +7. When list markers change from numbers to bullets, do we have + two lists or one? (The Markdown syntax description suggests two, + but the perl scripts and many other implementations produce one.) + + ``` markdown + 1. fee + 2. fie + - foe + - fum + ``` + +8. What are the precedence rules for the markers of inline structure? + For example, is the following a valid link, or does the code span + take precedence ? + + ``` markdown + [a backtick (`)](/url) and [another backtick (`)](/url). + ``` + +9. What are the precedence rules for markers of emphasis and strong + emphasis? For example, how should the following be parsed? + + ``` markdown + *foo *bar* baz* + ``` + +10. What are the precedence rules between block-level and inline-level + structure? For example, how should the following be parsed? + + ``` markdown + - `a long code span can contain a hyphen like this + - and it can screw things up` + ``` + +11. Can list items include headers? (`Markdown.pl` does not allow this, + but headers can occur in blockquotes.) + + ``` markdown + - # Heading + ``` + +12. Can link references be defined inside block quotes or list items? + + ``` markdown + > Blockquote [foo]. + > + > [foo]: /url + ``` + +13. If there are multiple definitions for the same reference, which takes + precedence? + + ``` markdown + [foo]: /url1 + [foo]: /url2 + + [foo][] + ``` + +In the absence of a spec, early implementers consulted `Markdown.pl` +to resolve these ambiguities. But `Markdown.pl` was quite buggy, and +gave manifestly bad results in many cases, so it was not a +satisfactory replacement for a spec. + +Because there is no unambiguous spec, implementations have diverged +considerably. As a result, users are often surprised to find that +a document that renders one way on one system (say, a github wiki) +renders differently on another (say, converting to docbook using +pandoc). To make matters worse, because nothing in Markdown counts +as a "syntax error," the divergence often isn't discovered right away. + +## About this document + +This document attempts to specify Markdown syntax unambiguously. +It contains many examples with side-by-side Markdown and +HTML. These are intended to double as conformance tests. An +accompanying script `runtests.pl` can be used to run the tests +against any Markdown program: + + perl runtests.pl spec.txt PROGRAM + +Since this document describes how Markdown is to be parsed into +an abstract syntax tree, it would have made sense to use an abstract +representation of the syntax tree instead of HTML. But HTML is capable +of representing the structural distinctions we need to make, and the +choice of HTML for the tests makes it possible to run the tests against +an implementation without writing an abstract syntax tree renderer. + +This document is generated from a text file, `spec.txt`, written +in Markdown with a small extension for the side-by-side tests. +The script `spec2md.pl` can be used to turn `spec.txt` into pandoc +Markdown, which can then be converted into other formats. + +In the examples, the `→` character is used to represent tabs. + +# Preprocessing + +A [line](#line) <a id="line"></a> +is a sequence of zero or more characters followed by a line +ending (CR, LF, or CRLF) or by the end of +file. + +This spec does not specify an encoding; it thinks of lines as composed +of characters rather than bytes. A conforming parser may be limited +to a certain encoding. + +Tabs in lines are expanded to spaces, with a tab stop of 4 characters: + +. +→foo→baz→→bim +. +<pre><code>foo baz bim +</code></pre> +. + +. + a→a + ὐ→a +. +<pre><code>a a +ὐ a +</code></pre> +. + +Line endings are replaced by newline characters (LF). + +A line containing no characters, or a line containing only spaces (after +tab expansion), is called a [blank line](#blank-line). +<a id="blank-line"></a> + +# Blocks and inlines + +We can think of a document as a sequence of [blocks](#block)<a +id="block"></a>---structural elements like paragraphs, block quotations, +lists, headers, rules, and code blocks. Blocks can contain other +blocks, or they can contain [inline](#inline)<a id="inline"></a> content: +words, spaces, links, emphasized text, images, and inline code. + +## Precedence + +Indicators of block structure always take precedence over indicators +of inline structure. So, for example, the following is a list with +two items, not a list with one item containing a code span: + +. +- `one +- two` +. +<ul> +<li>`one</li> +<li>two`</li> +</ul> +. + +This means that parsing can proceed in two steps: first, the block +structure of the document can be discerned; second, text lines inside +paragraphs, headers, and other block constructs can be parsed for inline +structure. The second step requires information about link reference +definitions that will be available only at the end of the first +step. Note that the first step requires processing lines in sequence, +but the second can be parallelized, since the inline parsing of +one block element does not affect the inline parsing of any other. + +## Container blocks and leaf blocks + +We can divide blocks into two types: +[container blocks](#container-block), <a id="container-block"></a> +which can contain other blocks, and [leaf blocks](#leaf-block), +<a id="leaf-block"></a> which cannot. + +# Leaf blocks + +This section describes the different kinds of leaf block that make up a +Markdown document. + +## Horizontal rules + +A line consisting of 0-3 spaces of indentation, followed by a sequence +of three or more matching `-`, `_`, or `*` characters, each followed +optionally by any number of spaces, forms a [horizontal +rule](#horizontal-rule). <a id="horizontal-rule"></a> + +. +*** +--- +___ +. +<hr /> +<hr /> +<hr /> +. + +Wrong characters: + +. ++++ +. +<p>+++</p> +. + +. +=== +. +<p>===</p> +. + +Not enough characters: + +. +-- +** +__ +. +<p>-- +** +__</p> +. + +One to three spaces indent are allowed: + +. + *** + *** + *** +. +<hr /> +<hr /> +<hr /> +. + +Four spaces is too many: + +. + *** +. +<pre><code>*** +</code></pre> +. + +. +Foo + *** +. +<p>Foo +***</p> +. + +More than three characters may be used: + +. +_____________________________________ +. +<hr /> +. + +Spaces are allowed between the characters: + +. + - - - +. +<hr /> +. + +. + ** * ** * ** * ** +. +<hr /> +. + +. +- - - - +. +<hr /> +. + +Spaces are allowed at the end: + +. +- - - - +. +<hr /> +. + +However, no other characters may occur at the end or the +beginning: + +. +_ _ _ _ a + +a------ +. +<p>_ _ _ _ a</p> +<p>a------</p> +. + +It is required that all of the non-space characters be the same. +So, this is not a horizontal rule: + +. + *-* +. +<p><em>-</em></p> +. + +Horizontal rules do not need blank lines before or after: + +. +- foo +*** +- bar +. +<ul> +<li>foo</li> +</ul> +<hr /> +<ul> +<li>bar</li> +</ul> +. + +Horizontal rules can interrupt a paragraph: + +. +Foo +*** +bar +. +<p>Foo</p> +<hr /> +<p>bar</p> +. + +Note, however, that this is a setext header, not a paragraph followed +by a horizontal rule: + +. +Foo +--- +bar +. +<h2>Foo</h2> +<p>bar</p> +. + +When both a horizontal rule and a list item are possible +interpretations of a line, the horizontal rule is preferred: + +. +* Foo +* * * +* Bar +. +<ul> +<li>Foo</li> +</ul> +<hr /> +<ul> +<li>Bar</li> +</ul> +. + +If you want a horizontal rule in a list item, use a different bullet: + +. +- Foo +- * * * +. +<ul> +<li>Foo</li> +<li><hr /></li> +</ul> +. + +## ATX headers + +An [ATX header](#atx-header) <a id="atx-header"></a> +consists of a string of characters, parsed as inline content, between an +opening sequence of 1--6 unescaped `#` characters and an optional +closing sequence of any number of `#` characters. The opening sequence +of `#` characters cannot be followed directly by a nonspace character. +The closing `#` characters may be followed by spaces only. The opening +`#` character may be indented 0-3 spaces. The raw contents of the +header are stripped of leading and trailing spaces before being parsed +as inline content. The header level is equal to the number of `#` +characters in the opening sequence. + +Simple headers: + +. +# foo +## foo +### foo +#### foo +##### foo +###### foo +. +<h1>foo</h1> +<h2>foo</h2> +<h3>foo</h3> +<h4>foo</h4> +<h5>foo</h5> +<h6>foo</h6> +. + +More than six `#` characters is not a header: + +. +####### foo +. +<p>####### foo</p> +. + +A space is required between the `#` characters and the header's +contents. Note that many implementations currently do not require +the space. However, the space was required by the [original ATX +implementation](http://www.aaronsw.com/2002/atx/atx.py), and it helps +prevent things like the following from being parsed as headers: + +. +#5 bolt +. +<p>#5 bolt</p> +. + +This is not a header, because the first `#` is escaped: + +. +\## foo +. +<p>## foo</p> +. + +Contents are parsed as inlines: + +. +# foo *bar* \*baz\* +. +<h1>foo <em>bar</em> *baz*</h1> +. + +Leading and trailing blanks are ignored in parsing inline content: + +. +# foo +. +<h1>foo</h1> +. + +One to three spaces indentation are allowed: + +. + ### foo + ## foo + # foo +. +<h3>foo</h3> +<h2>foo</h2> +<h1>foo</h1> +. + +Four spaces are too much: + +. + # foo +. +<pre><code># foo +</code></pre> +. + +. +foo + # bar +. +<p>foo +# bar</p> +. + +A closing sequence of `#` characters is optional: + +. +## foo ## + ### bar ### +. +<h2>foo</h2> +<h3>bar</h3> +. + +It need not be the same length as the opening sequence: + +. +# foo ################################## +##### foo ## +. +<h1>foo</h1> +<h5>foo</h5> +. + +Spaces are allowed after the closing sequence: + +. +### foo ### +. +<h3>foo</h3> +. + +A sequence of `#` characters with a nonspace character following it +is not a closing sequence, but counts as part of the contents of the +header: + +. +### foo ### b +. +<h3>foo ### b</h3> +. + +Backslash-escaped `#` characters do not count as part +of the closing sequence: + +. +### foo \### +## foo \#\## +# foo \# +. +<h3>foo #</h3> +<h2>foo ##</h2> +<h1>foo #</h1> +. + +ATX headers need not be separated from surrounding content by blank +lines, and they can interrupt paragraphs: + +. +**** +## foo +**** +. +<hr /> +<h2>foo</h2> +<hr /> +. + +. +Foo bar +# baz +Bar foo +. +<p>Foo bar</p> +<h1>baz</h1> +<p>Bar foo</p> +. + +ATX headers can be empty: + +. +## +# +### ### +. +<h2></h2> +<h1></h1> +<h3></h3> +. + +## Setext headers + +A [setext header](#setext-header) <a id="setext-header"></a> +consists of a line of text, containing at least one nonspace character, +with no more than 3 spaces indentation, followed by a [setext header +underline](#setext-header-underline). A [setext header +underline](#setext-header-underline) <a id="setext-header-underline"></a> +is a sequence of `=` characters or a sequence of `-` characters, with no +more than 3 spaces indentation and any number of trailing +spaces. The header is a level 1 header if `=` characters are used, and +a level 2 header if `-` characters are used. The contents of the header +are the result of parsing the first line as Markdown inline content. + +In general, a setext header need not be preceded or followed by a +blank line. However, it cannot interrupt a paragraph, so when a +setext header comes after a paragraph, a blank line is needed between +them. + +Simple examples: + +. +Foo *bar* +========= + +Foo *bar* +--------- +. +<h1>Foo <em>bar</em></h1> +<h2>Foo <em>bar</em></h2> +. + +The underlining can be any length: + +. +Foo +------------------------- + +Foo += +. +<h2>Foo</h2> +<h1>Foo</h1> +. + +The header content can be indented up to three spaces, and need +not line up with the underlining: + +. + Foo +--- + + Foo +----- + + Foo + === +. +<h2>Foo</h2> +<h2>Foo</h2> +<h1>Foo</h1> +. + +Four spaces indent is too much: + +. + Foo + --- + + Foo +--- +. +<pre><code>Foo +--- + +Foo +</code></pre> +<hr /> +. + +The setext header underline can be indented up to three spaces, and +may have trailing spaces: + +. +Foo + ---- +. +<h2>Foo</h2> +. + +Four spaces is too much: + +. +Foo + --- +. +<p>Foo +---</p> +. + +The setext header underline cannot contain internal spaces: + +. +Foo += = + +Foo +--- - +. +<p>Foo += =</p> +<p>Foo</p> +<hr /> +. + +Trailing spaces in the content line do not cause a line break: + +. +Foo +----- +. +<h2>Foo</h2> +. + +Nor does a backslash at the end: + +. +Foo\ +---- +. +<h2>Foo\</h2> +. + +Since indicators of block structure take precedence over +indicators of inline structure, the following are setext headers: + +. +`Foo +---- +` + +<a title="a lot +--- +of dashes"/> +. +<h2>`Foo</h2> +<p>`</p> +<h2><a title="a lot</h2> +<p>of dashes"/></p> +. + +The setext header underline cannot be a lazy line: + +. +> Foo +--- +. +<blockquote> +<p>Foo</p> +</blockquote> +<hr /> +. + +A setext header cannot interrupt a paragraph: + +. +Foo +Bar +--- + +Foo +Bar +=== +. +<p>Foo +Bar</p> +<hr /> +<p>Foo +Bar +===</p> +. + +But in general a blank line is not required before or after: + +. +--- +Foo +--- +Bar +--- +Baz +. +<hr /> +<h2>Foo</h2> +<h2>Bar</h2> +<p>Baz</p> +. + +Setext headers cannot be empty: + +. + +==== +. +<p>====</p> +. + + +## Indented code blocks + +An [indented code block](#indented-code-block) +<a id="indented-code-block"></a> is composed of one or more +[indented chunks](#indented-chunk) separated by blank lines. +An [indented chunk](#indented-chunk) <a id="indented-chunk"></a> +is a sequence of non-blank lines, each indented four or more +spaces. An indented code block cannot interrupt a paragraph, so +if it occurs before or after a paragraph, there must be an +intervening blank line. The contents of the code block are +the literal contents of the lines, including trailing newlines, +minus four spaces of indentation. An indented code block has no +attributes. + +. + a simple + indented code block +. +<pre><code>a simple + indented code block +</code></pre> +. + +The contents are literal text, and do not get parsed as Markdown: + +. + <a/> + *hi* + + - one +. +<pre><code><a/> +*hi* + +- one +</code></pre> +. + +Here we have three chunks separated by blank lines: + +. + chunk1 + + chunk2 + + + + chunk3 +. +<pre><code>chunk1 + +chunk2 + + + +chunk3 +</code></pre> +. + +Any initial spaces beyond four will be included in the content, even +in interior blank lines: + +. + chunk1 + + chunk2 +. +<pre><code>chunk1 + + chunk2 +</code></pre> +. + +An indented code block cannot interrupt a paragraph. (This +allows hanging indents and the like.) + +. +Foo + bar + +. +<p>Foo +bar</p> +. + +However, any non-blank line with fewer than four leading spaces ends +the code block immediately. So a paragraph may occur immediately +after indented code: + +. + foo +bar +. +<pre><code>foo +</code></pre> +<p>bar</p> +. + +And indented code can occur immediately before and after other kinds of +blocks: + +. +# Header + foo +Header +------ + foo +---- +. +<h1>Header</h1> +<pre><code>foo +</code></pre> +<h2>Header</h2> +<pre><code>foo +</code></pre> +<hr /> +. + +The first line can be indented more than four spaces: + +. + foo + bar +. +<pre><code> foo +bar +</code></pre> +. + +Blank lines preceding or following an indented code block +are not included in it: + +. + + + foo + + +. +<pre><code>foo +</code></pre> +. + +Trailing spaces are included in the code block's content: + +. + foo +. +<pre><code>foo +</code></pre> +. + + +## Fenced code blocks + +A [code fence](#code-fence) <a id="code-fence"></a> is a sequence +of at least three consecutive backtick characters (`` ` ``) or +tildes (`~`). (Tildes and backticks cannot be mixed.) +A [fenced code block](#fenced-code-block) <a id="fenced-code-block"></a> +begins with a code fence, indented no more than three spaces. + +The line with the opening code fence may optionally contain some text +following the code fence; this is trimmed of leading and trailing +spaces and called the [info string](#info-string). +<a id="info-string"></a> The info string may not contain any backtick +characters. (The reason for this restriction is that otherwise +some inline code would be incorrectly interpreted as the +beginning of a fenced code block.) + +The content of the code block consists of all subsequent lines, until +a closing [code fence](#code-fence) of the same type as the code block +began with (backticks or tildes), and with at least as many backticks +or tildes as the opening code fence. If the leading code fence is +indented N spaces, then up to N spaces of indentation are removed from +each line of the content (if present). (If a content line is not +indented, it is preserved unchanged. If it is indented less than N +spaces, all of the indentation is removed.) + +The closing code fence may be indented up to three spaces, and may be +followed only by spaces, which are ignored. If the end of the +containing block (or document) is reached and no closing code fence +has been found, the code block contains all of the lines after the +opening code fence until the end of the containing block (or +document). (An alternative spec would require backtracking in the +event that a closing code fence is not found. But this makes parsing +much less efficient, and there seems to be no real down side to the +behavior described here.) + +A fenced code block may interrupt a paragraph, and does not require +a blank line either before or after. + +The content of a code fence is treated as literal text, not parsed +as inlines. The first word of the info string is typically used to +specify the language of the code sample, and rendered in the `class` +attribute of the `code` tag. However, this spec does not mandate any +particular treatment of the info string. + +Here is a simple example with backticks: + +. +``` +< + > +``` +. +<pre><code>< + > +</code></pre> +. + +With tildes: + +. +~~~ +< + > +~~~ +. +<pre><code>< + > +</code></pre> +. + +The closing code fence must use the same character as the opening +fence: + +. +``` +aaa +~~~ +``` +. +<pre><code>aaa +~~~ +</code></pre> +. + +. +~~~ +aaa +``` +~~~ +. +<pre><code>aaa +``` +</code></pre> +. + +The closing code fence must be at least as long as the opening fence: + +. +```` +aaa +``` +`````` +. +<pre><code>aaa +``` +</code></pre> +. + +. +~~~~ +aaa +~~~ +~~~~ +. +<pre><code>aaa +~~~ +</code></pre> +. + +Unclosed code blocks are closed by the end of the document: + +. +``` +. +<pre><code></code></pre> +. + +. +````` + +``` +aaa +. +<pre><code> +``` +aaa +</code></pre> +. + +A code block can have all empty lines as its content: + +. +``` + + +``` +. +<pre><code> + +</code></pre> +. + +A code block can be empty: + +. +``` +``` +. +<pre><code></code></pre> +. + +Fences can be indented. If the opening fence is indented, +content lines will have equivalent opening indentation removed, +if present: + +. + ``` + aaa +aaa +``` +. +<pre><code>aaa +aaa +</code></pre> +. + +. + ``` +aaa + aaa +aaa + ``` +. +<pre><code>aaa +aaa +aaa +</code></pre> +. + +. + ``` + aaa + aaa + aaa + ``` +. +<pre><code>aaa + aaa +aaa +</code></pre> +. + +Four spaces indentation produces an indented code block: + +. + ``` + aaa + ``` +. +<pre><code>``` +aaa +``` +</code></pre> +. + +Code fences (opening and closing) cannot contain internal spaces: + +. +``` ``` +aaa +. +<p><code></code> +aaa</p> +. + +. +~~~~~~ +aaa +~~~ ~~ +. +<pre><code>aaa +~~~ ~~ +</code></pre> +. + +Fenced code blocks can interrupt paragraphs, and can be followed +directly by paragraphs, without a blank line between: + +. +foo +``` +bar +``` +baz +. +<p>foo</p> +<pre><code>bar +</code></pre> +<p>baz</p> +. + +Other blocks can also occur before and after fenced code blocks +without an intervening blank line: + +. +foo +--- +~~~ +bar +~~~ +# baz +. +<h2>foo</h2> +<pre><code>bar +</code></pre> +<h1>baz</h1> +. + +An [info string](#info-string) can be provided after the opening code fence. +Opening and closing spaces will be stripped, and the first word, prefixed +with `language-`, is used as the value for the `class` attribute of the +`code` element within the enclosing `pre` element. + +. +```ruby +def foo(x) + return 3 +end +``` +. +<pre><code class="language-ruby">def foo(x) + return 3 +end +</code></pre> +. + +. +~~~~ ruby startline=3 $%@#$ +def foo(x) + return 3 +end +~~~~~~~ +. +<pre><code class="language-ruby">def foo(x) + return 3 +end +</code></pre> +. + +. +````; +```` +. +<pre><code class="language-;"></code></pre> +. + +Info strings for backtick code blocks cannot contain backticks: + +. +``` aa ``` +foo +. +<p><code>aa</code> +foo</p> +. + +Closing code fences cannot have info strings: + +. +``` +``` aaa +``` +. +<pre><code>``` aaa +</code></pre> +. + + +## HTML blocks + +An [HTML block tag](#html-block-tag) <a id="html-block-tag"></a> is +an [open tag](#open-tag) or [closing tag](#closing-tag) whose tag +name is one of the following (case-insensitive): +`article`, `header`, `aside`, `hgroup`, `blockquote`, `hr`, `iframe`, +`body`, `li`, `map`, `button`, `object`, `canvas`, `ol`, `caption`, +`output`, `col`, `p`, `colgroup`, `pre`, `dd`, `progress`, `div`, +`section`, `dl`, `table`, `td`, `dt`, `tbody`, `embed`, `textarea`, +`fieldset`, `tfoot`, `figcaption`, `th`, `figure`, `thead`, `footer`, +`footer`, `tr`, `form`, `ul`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, +`video`, `script`, `style`. + +An [HTML block](#html-block) <a id="html-block"></a> begins with an +[HTML block tag](#html-block-tag), [HTML comment](#html-comment), +[processing instruction](#processing-instruction), +[declaration](#declaration), or [CDATA section](#cdata-section). +It ends when a [blank line](#blank-line) or the end of the +input is encountered. The initial line may be indented up to three +spaces, and subsequent lines may have any indentation. The contents +of the HTML block are interpreted as raw HTML, and will not be escaped +in HTML output. + +Some simple examples: + +. +<table> + <tr> + <td> + hi + </td> + </tr> +</table> + +okay. +. +<table> + <tr> + <td> + hi + </td> + </tr> +</table> +<p>okay.</p> +. + +. + <div> + *hello* + <foo><a> +. + <div> + *hello* + <foo><a> +. + +Here we have two code blocks with a Markdown paragraph between them: + +. +<DIV CLASS="foo"> + +*Markdown* + +</DIV> +. +<DIV CLASS="foo"> +<p><em>Markdown</em></p> +</DIV> +. + +In the following example, what looks like a Markdown code block +is actually part of the HTML block, which continues until a blank +line or the end of the document is reached: + +. +<div></div> +``` c +int x = 33; +``` +. +<div></div> +``` c +int x = 33; +``` +. + +A comment: + +. +<!-- Foo +bar + baz --> +. +<!-- Foo +bar + baz --> +. + +A processing instruction: + +. +<?php + echo 'foo' +?> +. +<?php + echo 'foo' +?> +. + +CDATA: + +. +<![CDATA[ +function matchwo(a,b) +{ +if (a < b && a < 0) then + { + return 1; + } +else + { + return 0; + } +} +]]> +. +<![CDATA[ +function matchwo(a,b) +{ +if (a < b && a < 0) then + { + return 1; + } +else + { + return 0; + } +} +]]> +. + +The opening tag can be indented 1-3 spaces, but not 4: + +. + <!-- foo --> + + <!-- foo --> +. + <!-- foo --> +<pre><code><!-- foo --> +</code></pre> +. + +An HTML block can interrupt a paragraph, and need not be preceded +by a blank line. + +. +Foo +<div> +bar +</div> +. +<p>Foo</p> +<div> +bar +</div> +. + +However, a following blank line is always needed, except at the end of +a document: + +. +<div> +bar +</div> +*foo* +. +<div> +bar +</div> +*foo* +. + +An incomplete HTML block tag may also start an HTML block: + +. +<div class +foo +. +<div class +foo +. + +This rule differs from John Gruber's original Markdown syntax +specification, which says: + +> The only restrictions are that block-level HTML elements — +> e.g. `<div>`, `<table>`, `<pre>`, `<p>`, etc. — must be separated from +> surrounding content by blank lines, and the start and end tags of the +> block should not be indented with tabs or spaces. + +In some ways Gruber's rule is more restrictive than the one given +here: + +- It requires that an HTML block be preceded by a blank line. +- It does not allow the start tag to be indented. +- It requires a matching end tag, which it also does not allow to + be indented. + +Indeed, most Markdown implementations, including some of Gruber's +own perl implementations, do not impose these restrictions. + +There is one respect, however, in which Gruber's rule is more liberal +than the one given here, since it allows blank lines to occur inside +an HTML block. There are two reasons for disallowing them here. +First, it removes the need to parse balanced tags, which is +expensive and can require backtracking from the end of the document +if no matching end tag is found. Second, it provides a very simple +and flexible way of including Markdown content inside HTML tags: +simply separate the Markdown from the HTML using blank lines: + +. +<div> + +*Emphasized* text. + +</div> +. +<div> +<p><em>Emphasized</em> text.</p> +</div> +. + +Compare: + +. +<div> +*Emphasized* text. +</div> +. +<div> +*Emphasized* text. +</div> +. + +Some Markdown implementations have adopted a convention of +interpreting content inside tags as text if the open tag has +the attribute `markdown=1`. The rule given above seems a simpler and +more elegant way of achieving the same expressive power, which is also +much simpler to parse. + +The main potential drawback is that one can no longer paste HTML +blocks into Markdown documents with 100% reliability. However, +*in most cases* this will work fine, because the blank lines in +HTML are usually followed by HTML block tags. For example: + +. +<table> + +<tr> + +<td> +Hi +</td> + +</tr> + +</table> +. +<table> +<tr> +<td> +Hi +</td> +</tr> +</table> +. + +Moreover, blank lines are usually not necessary and can be +deleted. The exception is inside `<pre>` tags; here, one can +replace the blank lines with ` ` entities. + +So there is no important loss of expressive power with the new rule. + +## Link reference definitions + +A [link reference definition](#link-reference-definition) +<a id="link-reference-definition"></a> consists of a [link +label](#link-label), indented up to three spaces, followed +by a colon (`:`), optional blank space (including up to one +newline), a [link destination](#link-destination), optional +blank space (including up to one newline), and an optional [link +title](#link-title), which if it is present must be separated +from the [link destination](#link-destination) by whitespace. +No further non-space characters may occur on the line. + +A [link reference-definition](#link-reference-definition) +does not correspond to a structural element of a document. Instead, it +defines a label which can be used in [reference links](#reference-link) +and reference-style [images](#image) elsewhere in the document. [Link +reference definitions] can come either before or after the links that use +them. + +. +[foo]: /url "title" + +[foo] +. +<p><a href="/url" title="title">foo</a></p> +. + +. + [foo]: + /url + 'the title' + +[foo] +. +<p><a href="/url" title="the title">foo</a></p> +. + +. +[Foo*bar\]]:my_(url) 'title (with parens)' + +[Foo*bar\]] +. +<p><a href="my_(url)" title="title (with parens)">Foo*bar]</a></p> +. + +. +[Foo bar]: +<my url> +'title' + +[Foo bar] +. +<p><a href="my%20url" title="title">Foo bar</a></p> +. + +The title may be omitted: + +. +[foo]: +/url + +[foo] +. +<p><a href="/url">foo</a></p> +. + +The link destination may not be omitted: + +. +[foo]: + +[foo] +. +<p>[foo]:</p> +<p>[foo]</p> +. + +A link can come before its corresponding definition: + +. +[foo] + +[foo]: url +. +<p><a href="url">foo</a></p> +. + +If there are several matching definitions, the first one takes +precedence: + +. +[foo] + +[foo]: first +[foo]: second +. +<p><a href="first">foo</a></p> +. + +As noted in the section on [Links], matching of labels is +case-insensitive (see [matches](#matches)). + +. +[FOO]: /url + +[Foo] +. +<p><a href="/url">Foo</a></p> +. + +. +[ΑΓΩ]: /φου + +[αγω] +. +<p><a href="/%CF%86%CE%BF%CF%85">αγω</a></p> +. + +Here is a link reference definition with no corresponding link. +It contributes nothing to the document. + +. +[foo]: /url +. +. + +This is not a link reference definition, because there are +non-space characters after the title: + +. +[foo]: /url "title" ok +. +<p>[foo]: /url "title" ok</p> +. + +This is not a link reference definition, because it is indented +four spaces: + +. + [foo]: /url "title" + +[foo] +. +<pre><code>[foo]: /url "title" +</code></pre> +<p>[foo]</p> +. + +This is not a link reference definition, because it occurs inside +a code block: + +. +``` +[foo]: /url +``` + +[foo] +. +<pre><code>[foo]: /url +</code></pre> +<p>[foo]</p> +. + +A [link reference definition](#link-reference-definition) cannot +interrupt a paragraph. + +. +Foo +[bar]: /baz + +[bar] +. +<p>Foo +[bar]: /baz</p> +<p>[bar]</p> +. + +However, it can directly follow other block elements, such as headers +and horizontal rules, and it need not be followed by a blank line. + +. +# [Foo] +[foo]: /url +> bar +. +<h1><a href="/url">Foo</a></h1> +<blockquote> +<p>bar</p> +</blockquote> +. + +Several [link references](#link-reference) can occur one after another, +without intervening blank lines. + +. +[foo]: /foo-url "foo" +[bar]: /bar-url + "bar" +[baz]: /baz-url + +[foo], +[bar], +[baz] +. +<p><a href="/foo-url" title="foo">foo</a>, +<a href="/bar-url" title="bar">bar</a>, +<a href="/baz-url">baz</a></p> +. + +[Link reference definitions](#link-reference-definition) can occur +inside block containers, like lists and block quotations. They +affect the entire document, not just the container in which they +are defined: + +. +[foo] + +> [foo]: /url +. +<p><a href="/url">foo</a></p> +<blockquote> +</blockquote> +. + + +## Paragraphs + +A sequence of non-blank lines that cannot be interpreted as other +kinds of blocks forms a [paragraph](#paragraph).<a id="paragraph"></a> +The contents of the paragraph are the result of parsing the +paragraph's raw content as inlines. The paragraph's raw content +is formed by concatenating the lines and removing initial and final +spaces. + +A simple example with two paragraphs: + +. +aaa + +bbb +. +<p>aaa</p> +<p>bbb</p> +. + +Paragraphs can contain multiple lines, but no blank lines: + +. +aaa +bbb + +ccc +ddd +. +<p>aaa +bbb</p> +<p>ccc +ddd</p> +. + +Multiple blank lines between paragraph have no effect: + +. +aaa + + +bbb +. +<p>aaa</p> +<p>bbb</p> +. + +Leading spaces are skipped: + +. + aaa + bbb +. +<p>aaa +bbb</p> +. + +Lines after the first may be indented any amount, since indented +code blocks cannot interrupt paragraphs. + +. +aaa + bbb + ccc +. +<p>aaa +bbb +ccc</p> +. + +However, the first line may be indented at most three spaces, +or an indented code block will be triggered: + +. + aaa +bbb +. +<p>aaa +bbb</p> +. + +. + aaa +bbb +. +<pre><code>aaa +</code></pre> +<p>bbb</p> +. + +Final spaces are stripped before inline parsing, so a paragraph +that ends with two or more spaces will not end with a hard line +break: + +. +aaa +bbb +. +<p>aaa<br /> +bbb</p> +. + +## Blank lines + +[Blank lines](#blank-line) between block-level elements are ignored, +except for the role they play in determining whether a [list](#list) +is [tight](#tight) or [loose](#loose). + +Blank lines at the beginning and end of the document are also ignored. + +. + + +aaa + + +# aaa + + +. +<p>aaa</p> +<h1>aaa</h1> +. + + +# Container blocks + +A [container block](#container-block) is a block that has other +blocks as its contents. There are two basic kinds of container blocks: +[block quotes](#block-quote) and [list items](#list-item). +[Lists](#list) are meta-containers for [list items](#list-item). + +We define the syntax for container blocks recursively. The general +form of the definition is: + +> If X is a sequence of blocks, then the result of +> transforming X in such-and-such a way is a container of type Y +> with these blocks as its content. + +So, we explain what counts as a block quote or list item by explaining +how these can be *generated* from their contents. This should suffice +to define the syntax, although it does not give a recipe for *parsing* +these constructions. (A recipe is provided below in the section entitled +[A parsing strategy](#appendix-a-a-parsing-strategy).) + +## Block quotes + +A [block quote marker](#block-quote-marker) <a id="block-quote-marker"></a> +consists of 0-3 spaces of initial indent, plus (a) the character `>` together +with a following space, or (b) a single character `>` not followed by a space. + +The following rules define [block quotes](#block-quote): +<a id="block-quote"></a> + +1. **Basic case.** If a string of lines *Ls* constitute a sequence + of blocks *Bs*, then the result of appending a [block quote + marker](#block-quote-marker) to the beginning of each line in *Ls* + is a [block quote](#block-quote) containing *Bs*. + +2. **Laziness.** If a string of lines *Ls* constitute a [block + quote](#block-quote) with contents *Bs*, then the result of deleting + the initial [block quote marker](#block-quote-marker) from one or + more lines in which the next non-space character after the [block + quote marker](#block-quote-marker) is [paragraph continuation + text](#paragraph-continuation-text) is a block quote with *Bs* as + its content. <a id="paragraph-continuation-text"></a> + [Paragraph continuation text](#paragraph-continuation-text) is text + that will be parsed as part of the content of a paragraph, but does + not occur at the beginning of the paragraph. + +3. **Consecutiveness.** A document cannot contain two [block + quotes](#block-quote) in a row unless there is a [blank + line](#blank-line) between them. + +Nothing else counts as a [block quote](#block-quote). + +Here is a simple example: + +. +> # Foo +> bar +> baz +. +<blockquote> +<h1>Foo</h1> +<p>bar +baz</p> +</blockquote> +. + +The spaces after the `>` characters can be omitted: + +. +># Foo +>bar +> baz +. +<blockquote> +<h1>Foo</h1> +<p>bar +baz</p> +</blockquote> +. + +The `>` characters can be indented 1-3 spaces: + +. + > # Foo + > bar + > baz +. +<blockquote> +<h1>Foo</h1> +<p>bar +baz</p> +</blockquote> +. + +Four spaces gives us a code block: + +. + > # Foo + > bar + > baz +. +<pre><code>> # Foo +> bar +> baz +</code></pre> +. + +The Laziness clause allows us to omit the `>` before a +paragraph continuation line: + +. +> # Foo +> bar +baz +. +<blockquote> +<h1>Foo</h1> +<p>bar +baz</p> +</blockquote> +. + +A block quote can contain some lazy and some non-lazy +continuation lines: + +. +> bar +baz +> foo +. +<blockquote> +<p>bar +baz +foo</p> +</blockquote> +. + +Laziness only applies to lines that are continuations of +paragraphs. Lines containing characters or indentation that indicate +block structure cannot be lazy. + +. +> foo +--- +. +<blockquote> +<p>foo</p> +</blockquote> +<hr /> +. + +. +> - foo +- bar +. +<blockquote> +<ul> +<li>foo</li> +</ul> +</blockquote> +<ul> +<li>bar</li> +</ul> +. + +. +> foo + bar +. +<blockquote> +<pre><code>foo +</code></pre> +</blockquote> +<pre><code>bar +</code></pre> +. + +. +> ``` +foo +``` +. +<blockquote> +<pre><code></code></pre> +</blockquote> +<p>foo</p> +<pre><code></code></pre> +. + +A block quote can be empty: + +. +> +. +<blockquote> +</blockquote> +. + +. +> +> +> +. +<blockquote> +</blockquote> +. + +A block quote can have initial or final blank lines: + +. +> +> foo +> +. +<blockquote> +<p>foo</p> +</blockquote> +. + +A blank line always separates block quotes: + +. +> foo + +> bar +. +<blockquote> +<p>foo</p> +</blockquote> +<blockquote> +<p>bar</p> +</blockquote> +. + +(Most current Markdown implementations, including John Gruber's +original `Markdown.pl`, will parse this example as a single block quote +with two paragraphs. But it seems better to allow the author to decide +whether two block quotes or one are wanted.) + +Consecutiveness means that if we put these block quotes together, +we get a single block quote: + +. +> foo +> bar +. +<blockquote> +<p>foo +bar</p> +</blockquote> +. + +To get a block quote with two paragraphs, use: + +. +> foo +> +> bar +. +<blockquote> +<p>foo</p> +<p>bar</p> +</blockquote> +. + +Block quotes can interrupt paragraphs: + +. +foo +> bar +. +<p>foo</p> +<blockquote> +<p>bar</p> +</blockquote> +. + +In general, blank lines are not needed before or after block +quotes: + +. +> aaa +*** +> bbb +. +<blockquote> +<p>aaa</p> +</blockquote> +<hr /> +<blockquote> +<p>bbb</p> +</blockquote> +. + +However, because of laziness, a blank line is needed between +a block quote and a following paragraph: + +. +> bar +baz +. +<blockquote> +<p>bar +baz</p> +</blockquote> +. + +. +> bar + +baz +. +<blockquote> +<p>bar</p> +</blockquote> +<p>baz</p> +. + +. +> bar +> +baz +. +<blockquote> +<p>bar</p> +</blockquote> +<p>baz</p> +. + +It is a consequence of the Laziness rule that any number +of initial `>`s may be omitted on a continuation line of a +nested block quote: + +. +> > > foo +bar +. +<blockquote> +<blockquote> +<blockquote> +<p>foo +bar</p> +</blockquote> +</blockquote> +</blockquote> +. + +. +>>> foo +> bar +>>baz +. +<blockquote> +<blockquote> +<blockquote> +<p>foo +bar +baz</p> +</blockquote> +</blockquote> +</blockquote> +. + +When including an indented code block in a block quote, +remember that the [block quote marker](#block-quote-marker) includes +both the `>` and a following space. So *five spaces* are needed after +the `>`: + +. +> code + +> not code +. +<blockquote> +<pre><code>code +</code></pre> +</blockquote> +<blockquote> +<p>not code</p> +</blockquote> +. + + +## List items + +A [list marker](#list-marker) <a id="list-marker"></a> is a +[bullet list marker](#bullet-list-marker) or an [ordered list +marker](#ordered-list-marker). + +A [bullet list marker](#bullet-list-marker) <a id="bullet-list-marker"></a> +is a `-`, `+`, or `*` character. + +An [ordered list marker](#ordered-list-marker) <a id="ordered-list-marker"></a> +is a sequence of one of more digits (`0-9`), followed by either a +`.` character or a `)` character. + +The following rules define [list items](#list-item): + +1. **Basic case.** If a sequence of lines *Ls* constitute a sequence of + blocks *Bs* starting with a non-space character and not separated + from each other by more than one blank line, and *M* is a list + marker *M* of width *W* followed by 0 < *N* < 5 spaces, then the result + of prepending *M* and the following spaces to the first line of + *Ls*, and indenting subsequent lines of *Ls* by *W + N* spaces, is a + list item with *Bs* as its contents. The type of the list item + (bullet or ordered) is determined by the type of its list marker. + If the list item is ordered, then it is also assigned a start + number, based on the ordered list marker. + +For example, let *Ls* be the lines + +. +A paragraph +with two lines. + + indented code + +> A block quote. +. +<p>A paragraph +with two lines.</p> +<pre><code>indented code +</code></pre> +<blockquote> +<p>A block quote.</p> +</blockquote> +. + +And let *M* be the marker `1.`, and *N* = 2. Then rule #1 says +that the following is an ordered list item with start number 1, +and the same contents as *Ls*: + +. +1. A paragraph + with two lines. + + indented code + + > A block quote. +. +<ol> +<li><p>A paragraph +with two lines.</p> +<pre><code>indented code +</code></pre> +<blockquote> +<p>A block quote.</p> +</blockquote></li> +</ol> +. + +The most important thing to notice is that the position of +the text after the list marker determines how much indentation +is needed in subsequent blocks in the list item. If the list +marker takes up two spaces, and there are three spaces between +the list marker and the next nonspace character, then blocks +must be indented five spaces in order to fall under the list +item. + +Here are some examples showing how far content must be indented to be +put under the list item: + +. +- one + + two +. +<ul> +<li>one</li> +</ul> +<p>two</p> +. + +. +- one + + two +. +<ul> +<li><p>one</p> +<p>two</p></li> +</ul> +. + +. + - one + + two +. +<ul> +<li>one</li> +</ul> +<pre><code> two +</code></pre> +. + +. + - one + + two +. +<ul> +<li><p>one</p> +<p>two</p></li> +</ul> +. + +It is tempting to think of this in terms of columns: the continuation +blocks must be indented at least to the column of the first nonspace +character after the list marker. However, that is not quite right. +The spaces after the list marker determine how much relative indentation +is needed. Which column this indentation reaches will depend on +how the list item is embedded in other constructions, as shown by +this example: + +. + > > 1. one +>> +>> two +. +<blockquote> +<blockquote> +<ol> +<li><p>one</p> +<p>two</p></li> +</ol> +</blockquote> +</blockquote> +. + +Here `two` occurs in the same column as the list marker `1.`, +but is actually contained in the list item, because there is +sufficent indentation after the last containing blockquote marker. + +The converse is also possible. In the following example, the word `two` +occurs far to the right of the initial text of the list item, `one`, but +it is not considered part of the list item, because it is not indented +far enough past the blockquote marker: + +. +>>- one +>> + > > two +. +<blockquote> +<blockquote> +<ul> +<li>one</li> +</ul> +<p>two</p> +</blockquote> +</blockquote> +. + +A list item may not contain blocks that are separated by more than +one blank line. Thus, two blank lines will end a list, unless the +two blanks are contained in a [fenced code block](#fenced-code-block). + +. +- foo + + bar + +- foo + + + bar + +- ``` + foo + + + bar + ``` +. +<ul> +<li><p>foo</p> +<p>bar</p></li> +<li><p>foo</p></li> +</ul> +<p>bar</p> +<ul> +<li><pre><code>foo + + +bar +</code></pre></li> +</ul> +. + +A list item may contain any kind of block: + +. +1. foo + + ``` + bar + ``` + + baz + + > bam +. +<ol> +<li><p>foo</p> +<pre><code>bar +</code></pre> +<p>baz</p> +<blockquote> +<p>bam</p> +</blockquote></li> +</ol> +. + +2. **Item starting with indented code.** If a sequence of lines *Ls* + constitute a sequence of blocks *Bs* starting with an indented code + block and not separated from each other by more than one blank line, + and *M* is a list marker *M* of width *W* followed by + one space, then the result of prepending *M* and the following + space to the first line of *Ls*, and indenting subsequent lines of + *Ls* by *W + 1* spaces, is a list item with *Bs* as its contents. + If a line is empty, then it need not be indented. The type of the + list item (bullet or ordered) is determined by the type of its list + marker. If the list item is ordered, then it is also assigned a + start number, based on the ordered list marker. + +An indented code block will have to be indented four spaces beyond +the edge of the region where text will be included in the list item. +In the following case that is 6 spaces: + +. +- foo + + bar +. +<ul> +<li><p>foo</p> +<pre><code>bar +</code></pre></li> +</ul> +. + +And in this case it is 11 spaces: + +. + 10. foo + + bar +. +<ol start="10"> +<li><p>foo</p> +<pre><code>bar +</code></pre></li> +</ol> +. + +If the *first* block in the list item is an indented code block, +then by rule #2, the contents must be indented *one* space after the +list marker: + +. + indented code + +paragraph + + more code +. +<pre><code>indented code +</code></pre> +<p>paragraph</p> +<pre><code>more code +</code></pre> +. + +. +1. indented code + + paragraph + + more code +. +<ol> +<li><pre><code>indented code +</code></pre> +<p>paragraph</p> +<pre><code>more code +</code></pre></li> +</ol> +. + +Note that an additional space indent is interpreted as space +inside the code block: + +. +1. indented code + + paragraph + + more code +. +<ol> +<li><pre><code> indented code +</code></pre> +<p>paragraph</p> +<pre><code>more code +</code></pre></li> +</ol> +. + +Note that rules #1 and #2 only apply to two cases: (a) cases +in which the lines to be included in a list item begin with a nonspace +character, and (b) cases in which they begin with an indented code +block. In a case like the following, where the first block begins with +a three-space indent, the rules do not allow us to form a list item by +indenting the whole thing and prepending a list marker: + +. + foo + +bar +. +<p>foo</p> +<p>bar</p> +. + +. +- foo + + bar +. +<ul> +<li>foo</li> +</ul> +<p>bar</p> +. + +This is not a significant restriction, because when a block begins +with 1-3 spaces indent, the indentation can always be removed without +a change in interpretation, allowing rule #1 to be applied. So, in +the above case: + +. +- foo + + bar +. +<ul> +<li><p>foo</p> +<p>bar</p></li> +</ul> +. + + +3. **Indentation.** If a sequence of lines *Ls* constitutes a list item + according to rule #1 or #2, then the result of indenting each line + of *L* by 1-3 spaces (the same for each line) also constitutes a + list item with the same contents and attributes. If a line is + empty, then it need not be indented. + +Indented one space: + +. + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +<ol> +<li><p>A paragraph +with two lines.</p> +<pre><code>indented code +</code></pre> +<blockquote> +<p>A block quote.</p> +</blockquote></li> +</ol> +. + +Indented two spaces: + +. + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +<ol> +<li><p>A paragraph +with two lines.</p> +<pre><code>indented code +</code></pre> +<blockquote> +<p>A block quote.</p> +</blockquote></li> +</ol> +. + +Indented three spaces: + +. + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +<ol> +<li><p>A paragraph +with two lines.</p> +<pre><code>indented code +</code></pre> +<blockquote> +<p>A block quote.</p> +</blockquote></li> +</ol> +. + +Four spaces indent gives a code block: + +. + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +<pre><code>1. A paragraph + with two lines. + + indented code + + > A block quote. +</code></pre> +. + + +4. **Laziness.** If a string of lines *Ls* constitute a [list + item](#list-item) with contents *Bs*, then the result of deleting + some or all of the indentation from one or more lines in which the + next non-space character after the indentation is + [paragraph continuation text](#paragraph-continuation-text) is a + list item with the same contents and attributes. + +Here is an example with lazy continuation lines: + +. + 1. A paragraph +with two lines. + + indented code + + > A block quote. +. +<ol> +<li><p>A paragraph +with two lines.</p> +<pre><code>indented code +</code></pre> +<blockquote> +<p>A block quote.</p> +</blockquote></li> +</ol> +. + +Indentation can be partially deleted: + +. + 1. A paragraph + with two lines. +. +<ol> +<li>A paragraph +with two lines.</li> +</ol> +. + +These examples show how laziness can work in nested structures: + +. +> 1. > Blockquote +continued here. +. +<blockquote> +<ol> +<li><blockquote> +<p>Blockquote +continued here.</p> +</blockquote></li> +</ol> +</blockquote> +. + +. +> 1. > Blockquote +> continued here. +. +<blockquote> +<ol> +<li><blockquote> +<p>Blockquote +continued here.</p> +</blockquote></li> +</ol> +</blockquote> +. + + +5. **That's all.** Nothing that is not counted as a list item by rules + #1--4 counts as a [list item](#list-item). + +The rules for sublists follow from the general rules above. A sublist +must be indented the same number of spaces a paragraph would need to be +in order to be included in the list item. + +So, in this case we need two spaces indent: + +. +- foo + - bar + - baz +. +<ul> +<li>foo +<ul> +<li>bar +<ul> +<li>baz</li> +</ul></li> +</ul></li> +</ul> +. + +One is not enough: + +. +- foo + - bar + - baz +. +<ul> +<li>foo</li> +<li>bar</li> +<li>baz</li> +</ul> +. + +Here we need four, because the list marker is wider: + +. +10) foo + - bar +. +<ol start="10"> +<li>foo +<ul> +<li>bar</li> +</ul></li> +</ol> +. + +Three is not enough: + +. +10) foo + - bar +. +<ol start="10"> +<li>foo</li> +</ol> +<ul> +<li>bar</li> +</ul> +. + +A list may be the first block in a list item: + +. +- - foo +. +<ul> +<li><ul> +<li>foo</li> +</ul></li> +</ul> +. + +. +1. - 2. foo +. +<ol> +<li><ul> +<li><ol start="2"> +<li>foo</li> +</ol></li> +</ul></li> +</ol> +. + +A list item may be empty: + +. +- foo +- +- bar +. +<ul> +<li>foo</li> +<li></li> +<li>bar</li> +</ul> +. + +. +- +. +<ul> +<li></li> +</ul> +. + +### Motivation + +John Gruber's Markdown spec says the following about list items: + +1. "List markers typically start at the left margin, but may be indented + by up to three spaces. List markers must be followed by one or more + spaces or a tab." + +2. "To make lists look nice, you can wrap items with hanging indents.... + But if you don't want to, you don't have to." + +3. "List items may consist of multiple paragraphs. Each subsequent + paragraph in a list item must be indented by either 4 spaces or one + tab." + +4. "It looks nice if you indent every line of the subsequent paragraphs, + but here again, Markdown will allow you to be lazy." + +5. "To put a blockquote within a list item, the blockquote's `>` + delimiters need to be indented." + +6. "To put a code block within a list item, the code block needs to be + indented twice — 8 spaces or two tabs." + +These rules specify that a paragraph under a list item must be indented +four spaces (presumably, from the left margin, rather than the start of +the list marker, but this is not said), and that code under a list item +must be indented eight spaces instead of the usual four. They also say +that a block quote must be indented, but not by how much; however, the +example given has four spaces indentation. Although nothing is said +about other kinds of block-level content, it is certainly reasonable to +infer that *all* block elements under a list item, including other +lists, must be indented four spaces. This principle has been called the +*four-space rule*. + +The four-space rule is clear and principled, and if the reference +implementation `Markdown.pl` had followed it, it probably would have +become the standard. However, `Markdown.pl` allowed paragraphs and +sublists to start with only two spaces indentation, at least on the +outer level. Worse, its behavior was inconsistent: a sublist of an +outer-level list needed two spaces indentation, but a sublist of this +sublist needed three spaces. It is not surprising, then, that different +implementations of Markdown have developed very different rules for +determining what comes under a list item. (Pandoc and python-Markdown, +for example, stuck with Gruber's syntax description and the four-space +rule, while discount, redcarpet, marked, PHP Markdown, and others +followed `Markdown.pl`'s behavior more closely.) + +Unfortunately, given the divergences between implementations, there +is no way to give a spec for list items that will be guaranteed not +to break any existing documents. However, the spec given here should +correctly handle lists formatted with either the four-space rule or +the more forgiving `Markdown.pl` behavior, provided they are laid out +in a way that is natural for a human to read. + +The strategy here is to let the width and indentation of the list marker +determine the indentation necessary for blocks to fall under the list +item, rather than having a fixed and arbitrary number. The writer can +think of the body of the list item as a unit which gets indented to the +right enough to fit the list marker (and any indentation on the list +marker). (The laziness rule, #4, then allows continuation lines to be +unindented if needed.) + +This rule is superior, we claim, to any rule requiring a fixed level of +indentation from the margin. The four-space rule is clear but +unnatural. It is quite unintuitive that + +``` markdown +- foo + + bar + + - baz +``` + +should be parsed as two lists with an intervening paragraph, + +``` html +<ul> +<li>foo</li> +</ul> +<p>bar</p> +<ul> +<li>baz</li> +</ul> +``` + +as the four-space rule demands, rather than a single list, + +``` html +<ul> +<li><p>foo</p> +<p>bar</p> +<ul> +<li>baz</li> +</ul></li> +</ul> +``` + +The choice of four spaces is arbitrary. It can be learned, but it is +not likely to be guessed, and it trips up beginners regularly. + +Would it help to adopt a two-space rule? The problem is that such +a rule, together with the rule allowing 1--3 spaces indentation of the +initial list marker, allows text that is indented *less than* the +original list marker to be included in the list item. For example, +`Markdown.pl` parses + +``` markdown + - one + + two +``` + +as a single list item, with `two` a continuation paragraph: + +``` html +<ul> +<li><p>one</p> +<p>two</p></li> +</ul> +``` + +and similarly + +``` markdown +> - one +> +> two +``` + +as + +``` html +<blockquote> +<ul> +<li><p>one</p> +<p>two</p></li> +</ul> +</blockquote> +``` + +This is extremely unintuitive. + +Rather than requiring a fixed indent from the margin, we could require +a fixed indent (say, two spaces, or even one space) from the list marker (which +may itself be indented). This proposal would remove the last anomaly +discussed. Unlike the spec presented above, it would count the following +as a list item with a subparagraph, even though the paragraph `bar` +is not indented as far as the first paragraph `foo`: + +``` markdown + 10. foo + + bar +``` + +Arguably this text does read like a list item with `bar` as a subparagraph, +which may count in favor of the proposal. However, on this proposal indented +code would have to be indented six spaces after the list marker. And this +would break a lot of existing Markdown, which has the pattern: + +``` markdown +1. foo + + indented code +``` + +where the code is indented eight spaces. The spec above, by contrast, will +parse this text as expected, since the code block's indentation is measured +from the beginning of `foo`. + +The one case that needs special treatment is a list item that *starts* +with indented code. How much indentation is required in that case, since +we don't have a "first paragraph" to measure from? Rule #2 simply stipulates +that in such cases, we require one space indentation from the list marker +(and then the normal four spaces for the indented code). This will match the +four-space rule in cases where the list marker plus its initial indentation +takes four spaces (a common case), but diverge in other cases. + +## Lists + +A [list](#list) <a id="list"></a> is a sequence of one or more +list items [of the same type](#of-the-same-type). The list items +may be separated by single [blank lines](#blank-line), but two +blank lines end all containing lists. + +Two list items are [of the same type](#of-the-same-type) +<a id="of-the-same-type"></a> if they begin with a [list +marker](#list-marker) of the same type. Two list markers are of the +same type if (a) they are bullet list markers using the same character +(`-`, `+`, or `*`) or (b) they are ordered list numbers with the same +delimiter (either `.` or `)`). + +A list is an [ordered list](#ordered-list) <a id="ordered-list"></a> +if its constituent list items begin with +[ordered list markers](#ordered-list-marker), and a [bullet +list](#bullet-list) <a id="bullet-list"></a> if its constituent list +items begin with [bullet list markers](#bullet-list-marker). + +The [start number](#start-number) <a id="start-number"></a> +of an [ordered list](#ordered-list) is determined by the list number of +its initial list item. The numbers of subsequent list items are +disregarded. + +A list is [loose](#loose) if it any of its constituent list items are +separated by blank lines, or if any of its constituent list items +directly contain two block-level elements with a blank line between +them. Otherwise a list is [tight](#tight). (The difference in HTML output +is that paragraphs in a loose with are wrapped in `<p>` tags, while +paragraphs in a tight list are not.) + +Changing the bullet or ordered list delimiter starts a new list: + +. +- foo +- bar ++ baz +. +<ul> +<li>foo</li> +<li>bar</li> +</ul> +<ul> +<li>baz</li> +</ul> +. + +. +1. foo +2. bar +3) baz +. +<ol> +<li>foo</li> +<li>bar</li> +</ol> +<ol start="3"> +<li>baz</li> +</ol> +. + +There can be blank lines between items, but two blank lines end +a list: + +. +- foo + +- bar + + +- baz +. +<ul> +<li><p>foo</p></li> +<li><p>bar</p></li> +</ul> +<ul> +<li>baz</li> +</ul> +. + +As illustrated above in the section on [list items](#list-item), +two blank lines between blocks *within* a list item will also end a +list: + +. +- foo + + + bar +- baz +. +<ul> +<li>foo</li> +</ul> +<p>bar</p> +<ul> +<li>baz</li> +</ul> +. + +Indeed, two blank lines will end *all* containing lists: + +. +- foo + - bar + - baz + + + bim +. +<ul> +<li>foo +<ul> +<li>bar +<ul> +<li>baz</li> +</ul></li> +</ul></li> +</ul> +<pre><code> bim +</code></pre> +. + +Thus, two blank lines can be used to separate consecutive lists of +the same type, or to separate a list from an indented code block +that would otherwise be parsed as a subparagraph of the final list +item: + +. +- foo +- bar + + +- baz +- bim +. +<ul> +<li>foo</li> +<li>bar</li> +</ul> +<ul> +<li>baz</li> +<li>bim</li> +</ul> +. + +. +- foo + + notcode + +- foo + + + code +. +<ul> +<li><p>foo</p> +<p>notcode</p></li> +<li><p>foo</p></li> +</ul> +<pre><code>code +</code></pre> +. + +List items need not be indented to the same level. The following +list items will be treated as items at the same list level, +since none is indented enough to belong to the previous list +item: + +. +- a + - b + - c + - d + - e + - f +- g +. +<ul> +<li>a</li> +<li>b</li> +<li>c</li> +<li>d</li> +<li>e</li> +<li>f</li> +<li>g</li> +</ul> +. + +This is a loose list, because there is a blank line between +two of the list items: + +. +- a +- b + +- c +. +<ul> +<li><p>a</p></li> +<li><p>b</p></li> +<li><p>c</p></li> +</ul> +. + +So is this, with a empty second item: + +. +* a +* + +* c +. +<ul> +<li><p>a</p></li> +<li></li> +<li><p>c</p></li> +</ul> +. + +These are loose lists, even though there is no space between the items, +because one of the items directly contains two block-level elements +with a blank line between them: + +. +- a +- b + + c +- d +. +<ul> +<li><p>a</p></li> +<li><p>b</p> +<p>c</p></li> +<li><p>d</p></li> +</ul> +. + +. +- a +- b + + [ref]: /url +- d +. +<ul> +<li><p>a</p></li> +<li><p>b</p></li> +<li><p>d</p></li> +</ul> +. + +This is a tight list, because the blank lines are in a code block: + +. +- a +- ``` + b + + + ``` +- c +. +<ul> +<li>a</li> +<li><pre><code>b + + +</code></pre></li> +<li>c</li> +</ul> +. + +This is a tight list, because the blank line is between two +paragraphs of a sublist. So the inner list is loose while +the other list is tight: + +. +- a + - b + + c +- d +. +<ul> +<li>a +<ul> +<li><p>b</p> +<p>c</p></li> +</ul></li> +<li>d</li> +</ul> +. + +This is a tight list, because the blank line is inside the +block quote: + +. +* a + > b + > +* c +. +<ul> +<li>a +<blockquote> +<p>b</p> +</blockquote></li> +<li>c</li> +</ul> +. + +This list is tight, because the consecutive block elements +are not separated by blank lines: + +. +- a + > b + ``` + c + ``` +- d +. +<ul> +<li>a +<blockquote> +<p>b</p> +</blockquote> +<pre><code>c +</code></pre></li> +<li>d</li> +</ul> +. + +A single-paragraph list is tight: + +. +- a +. +<ul> +<li>a</li> +</ul> +. + +. +- a + - b +. +<ul> +<li>a +<ul> +<li>b</li> +</ul></li> +</ul> +. + +Here the outer list is loose, the inner list tight: + +. +* foo + * bar + + baz +. +<ul> +<li><p>foo</p> +<ul> +<li>bar</li> +</ul> +<p>baz</p></li> +</ul> +. + +. +- a + - b + - c + +- d + - e + - f +. +<ul> +<li><p>a</p> +<ul> +<li>b</li> +<li>c</li> +</ul></li> +<li><p>d</p> +<ul> +<li>e</li> +<li>f</li> +</ul></li> +</ul> +. + +# Inlines + +Inlines are parsed sequentially from the beginning of the character +stream to the end (left to right, in left-to-right languages). +Thus, for example, in + +. +`hi`lo` +. +<p><code>hi</code>lo`</p> +. + +`hi` is parsed as code, leaving the backtick at the end as a literal +backtick. + +## Backslash escapes + +Any ASCII punctuation character may be backslash-escaped: + +. +\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~ +. +<p>!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~</p> +. + +Backslashes before other characters are treated as literal +backslashes: + +. +\→\A\a\ \3\φ\« +. +<p>\ \A\a\ \3\φ\«</p> +. + +Escaped characters are treated as regular characters and do +not have their usual Markdown meanings: + +. +\*not emphasized* +\<br/> not a tag +\[not a link](/foo) +\`not code` +1\. not a list +\* not a list +\# not a header +\[foo]: /url "not a reference" +. +<p>*not emphasized* +<br/> not a tag +[not a link](/foo) +`not code` +1. not a list +* not a list +# not a header +[foo]: /url "not a reference"</p> +. + +If a backslash is itself escaped, the following character is not: + +. +\\*emphasis* +. +<p>\<em>emphasis</em></p> +. + +A backslash at the end of the line is a hard line break: + +. +foo\ +bar +. +<p>foo<br /> +bar</p> +. + +Backslash escapes do not work in code blocks, code spans, autolinks, or +raw HTML: + +. +`` \[\` `` +. +<p><code>\[\`</code></p> +. + +. + \[\] +. +<pre><code>\[\] +</code></pre> +. + +. +~~~ +\[\] +~~~ +. +<pre><code>\[\] +</code></pre> +. + +. +<http://google.com?find=\*> +. +<p><a href="http://google.com?find=%5C*">http://google.com?find=\*</a></p> +. + +. +<a href="/bar\/)"> +. +<p><a href="/bar\/)"></p> +. + +But they work in all other contexts, including URLs and link titles, +link references, and info strings in [fenced code +blocks](#fenced-code-block): + +. +[foo](/bar\* "ti\*tle") +. +<p><a href="/bar*" title="ti*tle">foo</a></p> +. + +. +[foo] + +[foo]: /bar\* "ti\*tle" +. +<p><a href="/bar*" title="ti*tle">foo</a></p> +. + +. +``` foo\+bar +foo +``` +. +<pre><code class="language-foo+bar">foo +</code></pre> +. + + +## Entities + +With the goal of making this standard as HTML-agnostic as possible, all HTML valid HTML Entities in any +context are recognized as such and converted into their actual values (i.e. the UTF8 characters representing +the entity itself) before they are stored in the AST. + +This allows implementations that target HTML output to trivially escape the entities when generating HTML, +and simplifies the job of implementations targetting other languages, as these will only need to handle the +UTF8 chars and need not be HTML-entity aware. + +[Named entities](#name-entities) <a id="named-entities"></a> consist of `&` ++ any of the valid HTML5 entity names + `;`. The [following document](http://www.whatwg.org/specs/web-apps/current-work/multipage/entities.json) +is used as an authoritative source of the valid entity names and their corresponding codepoints. + +Conforming implementations that target Markdown don't need to generate entities for all the valid +named entities that exist, with the exception of `"` (`"`), `&` (`&`), `<` (`<`) and `>` (`>`), +which always need to be written as entities for security reasons. + +. + & © Æ Ď ¾ ℋ ⅆ ∲ +. +<p> & © Æ Ď ¾ ℋ ⅆ ∲</p> +. + +[Decimal entities](#decimal-entities) <a id="decimal-entities"></a> +consist of `&#` + a string of 1--8 arabic digits + `;`. Again, these entities need to be recognised +and tranformed into their corresponding UTF8 codepoints. Invalid Unicode codepoints will be written +as the "unknown codepoint" character (`0xFFFD`) + +. +# Ӓ Ϡ � +. +<p># Ӓ Ϡ �</p> +. + +[Hexadecimal entities](#hexadecimal-entities) <a id="hexadecimal-entities"></a> +consist of `&#` + either `X` or `x` + a string of 1-8 hexadecimal digits ++ `;`. They will also be parsed and turned into their corresponding UTF8 values in the AST. + +. +" ആ ಫ +. +<p>" ആ ಫ</p> +. + +Here are some nonentities: + +. +  &x; &#; &#x; &ThisIsWayTooLongToBeAnEntityIsntIt; &hi?; +. +<p>&nbsp &x; &#; &#x; &ThisIsWayTooLongToBeAnEntityIsntIt; &hi?;</p> +. + +Although HTML5 does accept some entities without a trailing semicolon +(such as `©`), these are not recognized as entities here, because it makes the grammar too ambiguous: + +. +© +. +<p>&copy</p> +. + +Strings that are not on the list of HTML5 named entities are not recognized as entities either: + +. +&MadeUpEntity; +. +<p>&MadeUpEntity;</p> +. + +Entities are recognized in any context besides code spans or +code blocks, including raw HTML, URLs, [link titles](#link-title), and +[fenced code block](#fenced-code-block) info strings: + +. +<a href="öö.html"> +. +<p><a href="öö.html"></p> +. + +. +[foo](/föö "föö") +. +<p><a href="/f%C3%B6%C3%B6" title="föö">foo</a></p> +. + +. +[foo] + +[foo]: /föö "föö" +. +<p><a href="/f%C3%B6%C3%B6" title="föö">foo</a></p> +. + +. +``` föö +foo +``` +. +<pre><code class="language-föö">foo +</code></pre> +. + +Entities are treated as literal text in code spans and code blocks: + +. +`föö` +. +<p><code>f&ouml;&ouml;</code></p> +. + +. + föfö +. +<pre><code>f&ouml;f&ouml; +</code></pre> +. + +## Code span + +A [backtick string](#backtick-string) <a id="backtick-string"></a> +is a string of one or more backtick characters (`` ` ``) that is neither +preceded nor followed by a backtick. + +A code span begins with a backtick string and ends with a backtick +string of equal length. The contents of the code span are the +characters between the two backtick strings, with leading and trailing +spaces and newlines removed, and consecutive spaces and newlines +collapsed to single spaces. + +This is a simple code span: + +. +`foo` +. +<p><code>foo</code></p> +. + +Here two backticks are used, because the code contains a backtick. +This example also illustrates stripping of leading and trailing spaces: + +. +`` foo ` bar `` +. +<p><code>foo ` bar</code></p> +. + +This example shows the motivation for stripping leading and trailing +spaces: + +. +` `` ` +. +<p><code>``</code></p> +. + +Newlines are treated like spaces: + +. +`` +foo +`` +. +<p><code>foo</code></p> +. + +Interior spaces and newlines are collapsed into single spaces, just +as they would be by a browser: + +. +`foo bar + baz` +. +<p><code>foo bar baz</code></p> +. + +Q: Why not just leave the spaces, since browsers will collapse them +anyway? A: Because we might be targeting a non-HTML format, and we +shouldn't rely on HTML-specific rendering assumptions. + +(Existing implementations differ in their treatment of internal +spaces and newlines. Some, including `Markdown.pl` and +`showdown`, convert an internal newline into a `<br />` tag. +But this makes things difficult for those who like to hard-wrap +their paragraphs, since a line break in the midst of a code +span will cause an unintended line break in the output. Others +just leave internal spaces as they are, which is fine if only +HTML is being targeted.) + +. +`foo `` bar` +. +<p><code>foo `` bar</code></p> +. + +Note that backslash escapes do not work in code spans. All backslashes +are treated literally: + +. +`foo\`bar` +. +<p><code>foo\</code>bar`</p> +. + +Backslash escapes are never needed, because one can always choose a +string of *n* backtick characters as delimiters, where the code does +not contain any strings of exactly *n* backtick characters. + +Code span backticks have higher precedence than any other inline +constructs except HTML tags and autolinks. Thus, for example, this is +not parsed as emphasized text, since the second `*` is part of a code +span: + +. +*foo`*` +. +<p>*foo<code>*</code></p> +. + +And this is not parsed as a link: + +. +[not a `link](/foo`) +. +<p>[not a <code>link](/foo</code>)</p> +. + +But this is a link: + +. +<http://foo.bar.`baz>` +. +<p><a href="http://foo.bar.%60baz">http://foo.bar.`baz</a>`</p> +. + +And this is an HTML tag: + +. +<a href="`">` +. +<p><a href="`">`</p> +. + +When a backtick string is not closed by a matching backtick string, +we just have literal backticks: + +. +```foo`` +. +<p>```foo``</p> +. + +. +`foo +. +<p>`foo</p> +. + +## Emphasis and strong emphasis + +John Gruber's original [Markdown syntax +description](http://daringfireball.net/projects/markdown/syntax#em) says: + +> Markdown treats asterisks (`*`) and underscores (`_`) as indicators of +> emphasis. Text wrapped with one `*` or `_` will be wrapped with an HTML +> `<em>` tag; double `*`'s or `_`'s will be wrapped with an HTML `<strong>` +> tag. + +This is enough for most users, but these rules leave much undecided, +especially when it comes to nested emphasis. The original +`Markdown.pl` test suite makes it clear that triple `***` and +`___` delimiters can be used for strong emphasis, and most +implementations have also allowed the following patterns: + +``` markdown +***strong emph*** +***strong** in emph* +***emph* in strong** +**in strong *emph*** +*in emph **strong*** +``` + +The following patterns are less widely supported, but the intent +is clear and they are useful (especially in contexts like bibliography +entries): + +``` markdown +*emph *with emph* in it* +**strong **with strong** in it** +``` + +Many implementations have also restricted intraword emphasis to +the `*` forms, to avoid unwanted emphasis in words containing +internal underscores. (It is best practice to put these in code +spans, but users often do not.) + +``` markdown +internal emphasis: foo*bar*baz +no emphasis: foo_bar_baz +``` + +The following rules capture all of these patterns, while allowing +for efficient parsing strategies that do not backtrack: + +1. A single `*` character [can open emphasis](#can-open-emphasis) + <a id="can-open-emphasis"></a> iff + + (a) it is not part of a sequence of four or more unescaped `*`s, + (b) it is not followed by whitespace, and + (c) either it is not followed by a `*` character or it is + followed immediately by strong emphasis. + +2. A single `_` character [can open emphasis](#can-open-emphasis) iff + + (a) it is not part of a sequence of four or more unescaped `_`s, + (b) it is not followed by whitespace, + (c) it is not preceded by an ASCII alphanumeric character, and + (d) either it is not followed by a `_` character or it is + followed immediately by strong emphasis. + +3. A single `*` character [can close emphasis](#can-close-emphasis) + <a id="can-close-emphasis"></a> iff + + (a) it is not part of a sequence of four or more unescaped `*`s, and + (b) it is not preceded by whitespace. + +4. A single `_` character [can close emphasis](#can-close-emphasis) iff + + (a) it is not part of a sequence of four or more unescaped `_`s, + (b) it is not preceded by whitespace, and + (c) it is not followed by an ASCII alphanumeric character. + +5. A double `**` [can open strong emphasis](#can-open-strong-emphasis) + <a id="can-open-strong-emphasis" ></a> iff + + (a) it is not part of a sequence of four or more unescaped `*`s, + (b) it is not followed by whitespace, and + (c) either it is not followed by a `*` character or it is + followed immediately by emphasis. + +6. A double `__` [can open strong emphasis](#can-open-strong-emphasis) + iff + + (a) it is not part of a sequence of four or more unescaped `_`s, + (b) it is not followed by whitespace, and + (c) it is not preceded by an ASCII alphanumeric character, and + (d) either it is not followed by a `_` character or it is + followed immediately by emphasis. + +7. A double `**` [can close strong emphasis](#can-close-strong-emphasis) + <a id="can-close-strong-emphasis" ></a> iff + + (a) it is not part of a sequence of four or more unescaped `*`s, and + (b) it is not preceded by whitespace. + +8. A double `__` [can close strong emphasis](#can-close-strong-emphasis) + iff + + (a) it is not part of a sequence of four or more unescaped `_`s, + (b) it is not preceded by whitespace, and + (c) it is not followed by an ASCII alphanumeric character. + +9. Emphasis begins with a delimiter that [can open + emphasis](#can-open-emphasis) and includes inlines parsed + sequentially until a delimiter that [can close + emphasis](#can-close-emphasis), and that uses the same + character (`_` or `*`) as the opening delimiter, is reached. + +10. Strong emphasis begins with a delimiter that [can open strong + emphasis](#can-open-strong-emphasis) and includes inlines parsed + sequentially until a delimiter that [can close strong + emphasis](#can-close-strong-emphasis), and that uses the + same character (`_` or `*`) as the opening delimiter, is reached. + +These rules can be illustrated through a series of examples. + +Simple emphasis: + +. +*foo bar* +. +<p><em>foo bar</em></p> +. + +. +_foo bar_ +. +<p><em>foo bar</em></p> +. + +Simple strong emphasis: + +. +**foo bar** +. +<p><strong>foo bar</strong></p> +. + +. +__foo bar__ +. +<p><strong>foo bar</strong></p> +. + +Emphasis can continue over line breaks: + +. +*foo +bar* +. +<p><em>foo +bar</em></p> +. + +. +_foo +bar_ +. +<p><em>foo +bar</em></p> +. + +. +**foo +bar** +. +<p><strong>foo +bar</strong></p> +. + +. +__foo +bar__ +. +<p><strong>foo +bar</strong></p> +. + +Emphasis can contain other inline constructs: + +. +*foo [bar](/url)* +. +<p><em>foo <a href="/url">bar</a></em></p> +. + +. +_foo [bar](/url)_ +. +<p><em>foo <a href="/url">bar</a></em></p> +. + +. +**foo [bar](/url)** +. +<p><strong>foo <a href="/url">bar</a></strong></p> +. + +. +__foo [bar](/url)__ +. +<p><strong>foo <a href="/url">bar</a></strong></p> +. + +Symbols contained in other inline constructs will not +close emphasis: + +. +*foo [bar*](/url) +. +<p>*foo <a href="/url">bar*</a></p> +. + +. +_foo [bar_](/url) +. +<p>_foo <a href="/url">bar_</a></p> +. + +. +**<a href="**"> +. +<p>**<a href="**"></p> +. + +. +__<a href="__"> +. +<p>__<a href="__"></p> +. + +. +*a `*`* +. +<p><em>a <code>*</code></em></p> +. + +. +_a `_`_ +. +<p><em>a <code>_</code></em></p> +. + +. +**a<http://foo.bar?q=**> +. +<p>**a<a href="http://foo.bar?q=**">http://foo.bar?q=**</a></p> +. + +. +__a<http://foo.bar?q=__> +. +<p>__a<a href="http://foo.bar?q=__">http://foo.bar?q=__</a></p> +. + +This is not emphasis, because the opening delimiter is +followed by white space: + +. +and * foo bar* +. +<p>and * foo bar*</p> +. + +. +_ foo bar_ +. +<p>_ foo bar_</p> +. + +. +and ** foo bar** +. +<p>and ** foo bar**</p> +. + +. +__ foo bar__ +. +<p>__ foo bar__</p> +. + +This is not emphasis, because the closing delimiter is +preceded by white space: + +. +and *foo bar * +. +<p>and *foo bar *</p> +. + +. +and _foo bar _ +. +<p>and _foo bar _</p> +. + +. +and **foo bar ** +. +<p>and **foo bar **</p> +. + +. +and __foo bar __ +. +<p>and __foo bar __</p> +. + +The rules imply that a sequence of four or more unescaped `*` or +`_` characters will always be parsed as a literal string: + +. +****hi**** +. +<p>****hi****</p> +. + +. +_____hi_____ +. +<p>_____hi_____</p> +. + +. +Sign here: _________ +. +<p>Sign here: _________</p> +. + +The rules also imply that there can be no empty emphasis or strong +emphasis: + +. +** is not an empty emphasis +. +<p>** is not an empty emphasis</p> +. + +. +**** is not an empty strong emphasis +. +<p>**** is not an empty strong emphasis</p> +. + +To include `*` or `_` in emphasized sections, use backslash escapes +or code spans: + +. +*here is a \** +. +<p><em>here is a *</em></p> +. + +. +__this is a double underscore (`__`)__ +. +<p><strong>this is a double underscore (<code>__</code>)</strong></p> +. + +`*` delimiters allow intra-word emphasis; `_` delimiters do not: + +. +foo*bar*baz +. +<p>foo<em>bar</em>baz</p> +. + +. +foo_bar_baz +. +<p>foo_bar_baz</p> +. + +. +foo__bar__baz +. +<p>foo__bar__baz</p> +. + +. +_foo_bar_baz_ +. +<p><em>foo_bar_baz</em></p> +. + +. +11*15*32 +. +<p>11<em>15</em>32</p> +. + +. +11_15_32 +. +<p>11_15_32</p> +. + +Internal underscores will be ignored in underscore-delimited +emphasis: + +. +_foo_bar_baz_ +. +<p><em>foo_bar_baz</em></p> +. + +. +__foo__bar__baz__ +. +<p><strong>foo__bar__baz</strong></p> +. + +The rules are sufficient for the following nesting patterns: + +. +***foo bar*** +. +<p><strong><em>foo bar</em></strong></p> +. + +. +___foo bar___ +. +<p><strong><em>foo bar</em></strong></p> +. + +. +***foo** bar* +. +<p><em><strong>foo</strong> bar</em></p> +. + +. +___foo__ bar_ +. +<p><em><strong>foo</strong> bar</em></p> +. + +. +***foo* bar** +. +<p><strong><em>foo</em> bar</strong></p> +. + +. +___foo_ bar__ +. +<p><strong><em>foo</em> bar</strong></p> +. + +. +*foo **bar*** +. +<p><em>foo <strong>bar</strong></em></p> +. + +. +_foo __bar___ +. +<p><em>foo <strong>bar</strong></em></p> +. + +. +**foo *bar*** +. +<p><strong>foo <em>bar</em></strong></p> +. + +. +__foo _bar___ +. +<p><strong>foo <em>bar</em></strong></p> +. + +. +*foo **bar*** +. +<p><em>foo <strong>bar</strong></em></p> +. + +. +_foo __bar___ +. +<p><em>foo <strong>bar</strong></em></p> +. + +. +*foo *bar* baz* +. +<p><em>foo <em>bar</em> baz</em></p> +. + +. +_foo _bar_ baz_ +. +<p><em>foo <em>bar</em> baz</em></p> +. + +. +**foo **bar** baz** +. +<p><strong>foo <strong>bar</strong> baz</strong></p> +. + +. +__foo __bar__ baz__ +. +<p><strong>foo <strong>bar</strong> baz</strong></p> +. + +. +*foo **bar** baz* +. +<p><em>foo <strong>bar</strong> baz</em></p> +. + +. +_foo __bar__ baz_ +. +<p><em>foo <strong>bar</strong> baz</em></p> +. + +. +**foo *bar* baz** +. +<p><strong>foo <em>bar</em> baz</strong></p> +. + +. +__foo _bar_ baz__ +. +<p><strong>foo <em>bar</em> baz</strong></p> +. + +Note that you cannot nest emphasis directly inside emphasis +using the same delimeter, or strong emphasis directly inside +strong emphasis: + +. +**foo** +. +<p><strong>foo</strong></p> +. + +. +****foo**** +. +<p>****foo****</p> +. + +For these nestings, you need to switch delimiters: + +. +*_foo_* +. +<p><em><em>foo</em></em></p> +. + +. +**__foo__** +. +<p><strong><strong>foo</strong></strong></p> +. + +Note that a `*` followed by a `*` can close emphasis, and +a `**` followed by a `*` can close strong emphasis (and +similarly for `_` and `__`): + +. +*foo** +. +<p><em>foo</em>*</p> +. + +. +*foo *bar** +. +<p><em>foo <em>bar</em></em></p> +. + +. +**foo*** +. +<p><strong>foo</strong>*</p> +. + +. +***foo* bar*** +. +<p><strong><em>foo</em> bar</strong>*</p> +. + +. +***foo** bar*** +. +<p><em><strong>foo</strong> bar</em>**</p> +. + +The following contains no strong emphasis, because the opening +delimiter is closed by the first `*` before `bar`: + +. +*foo**bar*** +. +<p><em>foo</em><em>bar</em>**</p> +. + +However, a string of four or more `****` can never close emphasis: + +. +*foo**** +. +<p>*foo****</p> +. + +Note that there are some asymmetries here: + +. +*foo** + +**foo* +. +<p><em>foo</em>*</p> +<p>**foo*</p> +. + +. +*foo *bar** + +**foo* bar* +. +<p><em>foo <em>bar</em></em></p> +<p>**foo* bar*</p> +. + +More cases with mismatched delimiters: + +. +**foo* bar* +. +<p>**foo* bar*</p> +. + +. +*bar*** +. +<p><em>bar</em>**</p> +. + +. +***foo* +. +<p>***foo*</p> +. + +. +**bar*** +. +<p><strong>bar</strong>*</p> +. + +. +***foo** +. +<p>***foo**</p> +. + +. +***foo *bar* +. +<p>***foo <em>bar</em></p> +. + +## Links + +A link contains a [link label](#link-label) (the visible text), +a [destination](#destination) (the URI that is the link destination), +and optionally a [link title](#link-title). There are two basic kinds +of links in Markdown. In [inline links](#inline-links) the destination +and title are given immediately after the label. In [reference +links](#reference-links) the destination and title are defined elsewhere +in the document. + +A [link label](#link-label) <a id="link-label"></a> consists of + +- an opening `[`, followed by +- zero or more backtick code spans, autolinks, HTML tags, link labels, + backslash-escaped ASCII punctuation characters, or non-`]` characters, + followed by +- a closing `]`. + +These rules are motivated by the following intuitive ideas: + +- A link label is a container for inline elements. +- The square brackets bind more tightly than emphasis markers, + but less tightly than `<>` or `` ` ``. +- Link labels may contain material in matching square brackets. + +A [link destination](#link-destination) <a id="link-destination"></a> +consists of either + +- a sequence of zero or more characters between an opening `<` and a + closing `>` that contains no line breaks or unescaped `<` or `>` + characters, or + +- a nonempty sequence of characters that does not include + ASCII space or control characters, and includes parentheses + only if (a) they are backslash-escaped or (b) they are part of + a balanced pair of unescaped parentheses that is not itself + inside a balanced pair of unescaped paretheses. + +A [link title](#link-title) <a id="link-title"></a> consists of either + +- a sequence of zero or more characters between straight double-quote + characters (`"`), including a `"` character only if it is + backslash-escaped, or + +- a sequence of zero or more characters between straight single-quote + characters (`'`), including a `'` character only if it is + backslash-escaped, or + +- a sequence of zero or more characters between matching parentheses + (`(...)`), including a `)` character only if it is backslash-escaped. + +An [inline link](#inline-link) <a id="inline-link"></a> +consists of a [link label](#link-label) followed immediately +by a left parenthesis `(`, optional whitespace, +an optional [link destination](#link-destination), +an optional [link title](#link-title) separated from the link +destination by whitespace, optional whitespace, and a right +parenthesis `)`. The link's text consists of the label (excluding +the enclosing square brackets) parsed as inlines. The link's +URI consists of the link destination, excluding enclosing `<...>` if +present, with backslash-escapes in effect as described above. The +link's title consists of the link title, excluding its enclosing +delimiters, with backslash-escapes in effect as described above. + +Here is a simple inline link: + +. +[link](/uri "title") +. +<p><a href="/uri" title="title">link</a></p> +. + +The title may be omitted: + +. +[link](/uri) +. +<p><a href="/uri">link</a></p> +. + +Both the title and the destination may be omitted: + +. +[link]() +. +<p><a href="">link</a></p> +. + +. +[link](<>) +. +<p><a href="">link</a></p> +. + + +If the destination contains spaces, it must be enclosed in pointy +braces: + +. +[link](/my uri) +. +<p>[link](/my uri)</p> +. + +. +[link](</my uri>) +. +<p><a href="/my%20uri">link</a></p> +. + +The destination cannot contain line breaks, even with pointy braces: + +. +[link](foo +bar) +. +<p>[link](foo +bar)</p> +. + +One level of balanced parentheses is allowed without escaping: + +. +[link]((foo)and(bar)) +. +<p><a href="(foo)and(bar)">link</a></p> +. + +However, if you have parentheses within parentheses, you need to escape +or use the `<...>` form: + +. +[link](foo(and(bar))) +. +<p>[link](foo(and(bar)))</p> +. + +. +[link](foo(and\(bar\))) +. +<p><a href="foo(and(bar))">link</a></p> +. + +. +[link](<foo(and(bar))>) +. +<p><a href="foo(and(bar))">link</a></p> +. + +Parentheses and other symbols can also be escaped, as usual +in Markdown: + +. +[link](foo\)\:) +. +<p><a href="foo):">link</a></p> +. + +URL-escaping and should be left alone inside the destination, as all URL-escaped characters +are also valid URL characters. HTML entities in the destination will be parsed into their UTF8 +codepoints, as usual, and optionally URL-escaped when written as HTML. + +. +[link](foo%20bä) +. +<p><a href="foo%20b%C3%A4">link</a></p> +. + +Note that, because titles can often be parsed as destinations, +if you try to omit the destination and keep the title, you'll +get unexpected results: + +. +[link]("title") +. +<p><a href="%22title%22">link</a></p> +. + +Titles may be in single quotes, double quotes, or parentheses: + +. +[link](/url "title") +[link](/url 'title') +[link](/url (title)) +. +<p><a href="/url" title="title">link</a> +<a href="/url" title="title">link</a> +<a href="/url" title="title">link</a></p> +. + +Backslash escapes and entities may be used in titles: + +. +[link](/url "title \""") +. +<p><a href="/url" title="title """>link</a></p> +. + +Nested balanced quotes are not allowed without escaping: + +. +[link](/url "title "and" title") +. +<p>[link](/url "title "and" title")</p> +. + +But it is easy to work around this by using a different quote type: + +. +[link](/url 'title "and" title') +. +<p><a href="/url" title="title "and" title">link</a></p> +. + +(Note: `Markdown.pl` did allow double quotes inside a double-quoted +title, and its test suite included a test demonstrating this. +But it is hard to see a good rationale for the extra complexity this +brings, since there are already many ways---backslash escaping, +entities, or using a different quote type for the enclosing title---to +write titles containing double quotes. `Markdown.pl`'s handling of +titles has a number of other strange features. For example, it allows +single-quoted titles in inline links, but not reference links. And, in +reference links but not inline links, it allows a title to begin with +`"` and end with `)`. `Markdown.pl` 1.0.1 even allows titles with no closing +quotation mark, though 1.0.2b8 does not. It seems preferable to adopt +a simple, rational rule that works the same way in inline links and +link reference definitions.) + +Whitespace is allowed around the destination and title: + +. +[link]( /uri + "title" ) +. +<p><a href="/uri" title="title">link</a></p> +. + +But it is not allowed between the link label and the +following parenthesis: + +. +[link] (/uri) +. +<p>[link] (/uri)</p> +. + +Note that this is not a link, because the closing `]` occurs in +an HTML tag: + +. +[foo <bar attr="](baz)"> +. +<p>[foo <bar attr="](baz)"></p> +. + + +There are three kinds of [reference links](#reference-link): +<a id="reference-link"></a> + +A [full reference link](#full-reference-link) <a id="full-reference-link"></a> +consists of a [link label](#link-label), optional whitespace, and +another [link label](#link-label) that [matches](#matches) a +[link reference definition](#link-reference-definition) elsewhere in the +document. + +One label [matches](#matches) <a id="matches"></a> +another just in case their normalized forms are equal. To normalize a +label, perform the *unicode case fold* and collapse consecutive internal +whitespace to a single space. If there are multiple matching reference +link definitions, the one that comes first in the document is used. (It +is desirable in such cases to emit a warning.) + +The contents of the first link label are parsed as inlines, which are +used as the link's text. The link's URI and title are provided by the +matching [link reference definition](#link-reference-definition). + +Here is a simple example: + +. +[foo][bar] + +[bar]: /url "title" +. +<p><a href="/url" title="title">foo</a></p> +. + +The first label can contain inline content: + +. +[*foo\!*][bar] + +[bar]: /url "title" +. +<p><a href="/url" title="title"><em>foo!</em></a></p> +. + +Matching is case-insensitive: + +. +[foo][BaR] + +[bar]: /url "title" +. +<p><a href="/url" title="title">foo</a></p> +. + +Unicode case fold is used: + +. +[Толпой][Толпой] is a Russian word. + +[ТОЛПОЙ]: /url +. +<p><a href="/url">Толпой</a> is a Russian word.</p> +. + +Consecutive internal whitespace is treated as one space for +purposes of determining matching: + +. +[Foo + bar]: /url + +[Baz][Foo bar] +. +<p><a href="/url">Baz</a></p> +. + +There can be whitespace between the two labels: + +. +[foo] [bar] + +[bar]: /url "title" +. +<p><a href="/url" title="title">foo</a></p> +. + +. +[foo] +[bar] + +[bar]: /url "title" +. +<p><a href="/url" title="title">foo</a></p> +. + +When there are multiple matching [link reference +definitions](#link-reference-definition), the first is used: + +. +[foo]: /url1 + +[foo]: /url2 + +[bar][foo] +. +<p><a href="/url1">bar</a></p> +. + +Note that matching is performed on normalized strings, not parsed +inline content. So the following does not match, even though the +labels define equivalent inline content: + +. +[bar][foo\!] + +[foo!]: /url +. +<p>[bar][foo!]</p> +. + +A [collapsed reference link](#collapsed-reference-link) +<a id="collapsed-reference-link"></a> consists of a [link +label](#link-label) that [matches](#matches) a [link reference +definition](#link-reference-definition) elsewhere in the +document, optional whitespace, and the string `[]`. The contents of the +first link label are parsed as inlines, which are used as the link's +text. The link's URI and title are provided by the matching reference +link definition. Thus, `[foo][]` is equivalent to `[foo][foo]`. + +. +[foo][] + +[foo]: /url "title" +. +<p><a href="/url" title="title">foo</a></p> +. + +. +[*foo* bar][] + +[*foo* bar]: /url "title" +. +<p><a href="/url" title="title"><em>foo</em> bar</a></p> +. + +The link labels are case-insensitive: + +. +[Foo][] + +[foo]: /url "title" +. +<p><a href="/url" title="title">Foo</a></p> +. + + +As with full reference links, whitespace is allowed +between the two sets of brackets: + +. +[foo] +[] + +[foo]: /url "title" +. +<p><a href="/url" title="title">foo</a></p> +. + +A [shortcut reference link](#shortcut-reference-link) +<a id="shortcut-reference-link"></a> consists of a [link +label](#link-label) that [matches](#matches) a [link reference +definition](#link-reference-definition) elsewhere in the +document and is not followed by `[]` or a link label. +The contents of the first link label are parsed as inlines, +which are used as the link's text. the link's URI and title +are provided by the matching link reference definition. +Thus, `[foo]` is equivalent to `[foo][]`. + +. +[foo] + +[foo]: /url "title" +. +<p><a href="/url" title="title">foo</a></p> +. + +. +[*foo* bar] + +[*foo* bar]: /url "title" +. +<p><a href="/url" title="title"><em>foo</em> bar</a></p> +. + +. +[[*foo* bar]] + +[*foo* bar]: /url "title" +. +<p>[<a href="/url" title="title"><em>foo</em> bar</a>]</p> +. + +The link labels are case-insensitive: + +. +[Foo] + +[foo]: /url "title" +. +<p><a href="/url" title="title">Foo</a></p> +. + +If you just want bracketed text, you can backslash-escape the +opening bracket to avoid links: + +. +\[foo] + +[foo]: /url "title" +. +<p>[foo]</p> +. + +Note that this is a link, because link labels bind more tightly +than emphasis: + +. +[foo*]: /url + +*[foo*] +. +<p>*<a href="/url">foo*</a></p> +. + +However, this is not, because link labels bind less +tightly than code backticks: + +. +[foo`]: /url + +[foo`]` +. +<p>[foo<code>]</code></p> +. + +Link labels can contain matched square brackets: + +. +[[[foo]]] + +[[[foo]]]: /url +. +<p><a href="/url">[[foo]]</a></p> +. + +. +[[[foo]]] + +[[[foo]]]: /url1 +[foo]: /url2 +. +<p><a href="/url1">[[foo]]</a></p> +. + +For non-matching brackets, use backslash escapes: + +. +[\[foo] + +[\[foo]: /url +. +<p><a href="/url">[foo</a></p> +. + +Full references take precedence over shortcut references: + +. +[foo][bar] + +[foo]: /url1 +[bar]: /url2 +. +<p><a href="/url2">foo</a></p> +. + +In the following case `[bar][baz]` is parsed as a reference, +`[foo]` as normal text: + +. +[foo][bar][baz] + +[baz]: /url +. +<p>[foo]<a href="/url">bar</a></p> +. + +Here, though, `[foo][bar]` is parsed as a reference, since +`[bar]` is defined: + +. +[foo][bar][baz] + +[baz]: /url1 +[bar]: /url2 +. +<p><a href="/url2">foo</a><a href="/url1">baz</a></p> +. + +Here `[foo]` is not parsed as a shortcut reference, because it +is followed by a link label (even though `[bar]` is not defined): + +. +[foo][bar][baz] + +[baz]: /url1 +[foo]: /url2 +. +<p>[foo]<a href="/url1">bar</a></p> +. + + +## Images + +An (unescaped) exclamation mark (`!`) followed by a reference or +inline link will be parsed as an image. The link label will be +used as the image's alt text, and the link title, if any, will +be used as the image's title. + +. +![foo](/url "title") +. +<p><img src="/url" alt="foo" title="title" /></p> +. + +. +![foo *bar*] + +[foo *bar*]: train.jpg "train & tracks" +. +<p><img src="train.jpg" alt="foo <em>bar</em>" title="train & tracks" /></p> +. + +. +![foo *bar*][] + +[foo *bar*]: train.jpg "train & tracks" +. +<p><img src="train.jpg" alt="foo <em>bar</em>" title="train & tracks" /></p> +. + +. +![foo *bar*][foobar] + +[FOOBAR]: train.jpg "train & tracks" +. +<p><img src="train.jpg" alt="foo <em>bar</em>" title="train & tracks" /></p> +. + +. +![foo](train.jpg) +. +<p><img src="train.jpg" alt="foo" /></p> +. + +. +My ![foo bar](/path/to/train.jpg "title" ) +. +<p>My <img src="/path/to/train.jpg" alt="foo bar" title="title" /></p> +. + +. +![foo](<url>) +. +<p><img src="url" alt="foo" /></p> +. + +. +![](/url) +. +<p><img src="/url" alt="" /></p> +. + +Reference-style: + +. +![foo] [bar] + +[bar]: /url +. +<p><img src="/url" alt="foo" /></p> +. + +. +![foo] [bar] + +[BAR]: /url +. +<p><img src="/url" alt="foo" /></p> +. + +Collapsed: + +. +![foo][] + +[foo]: /url "title" +. +<p><img src="/url" alt="foo" title="title" /></p> +. + +. +![*foo* bar][] + +[*foo* bar]: /url "title" +. +<p><img src="/url" alt="<em>foo</em> bar" title="title" /></p> +. + +The labels are case-insensitive: + +. +![Foo][] + +[foo]: /url "title" +. +<p><img src="/url" alt="Foo" title="title" /></p> +. + +As with full reference links, whitespace is allowed +between the two sets of brackets: + +. +![foo] +[] + +[foo]: /url "title" +. +<p><img src="/url" alt="foo" title="title" /></p> +. + +Shortcut: + +. +![foo] + +[foo]: /url "title" +. +<p><img src="/url" alt="foo" title="title" /></p> +. + +. +![*foo* bar] + +[*foo* bar]: /url "title" +. +<p><img src="/url" alt="<em>foo</em> bar" title="title" /></p> +. + +. +![[foo]] + +[[foo]]: /url "title" +. +<p><img src="/url" alt="[foo]" title="title" /></p> +. + +The link labels are case-insensitive: + +. +![Foo] + +[foo]: /url "title" +. +<p><img src="/url" alt="Foo" title="title" /></p> +. + +If you just want bracketed text, you can backslash-escape the +opening `!` and `[`: + +. +\!\[foo] + +[foo]: /url "title" +. +<p>![foo]</p> +. + +If you want a link after a literal `!`, backslash-escape the +`!`: + +. +\![foo] + +[foo]: /url "title" +. +<p>!<a href="/url" title="title">foo</a></p> +. + +## Autolinks + +Autolinks are absolute URIs and email addresses inside `<` and `>`. +They are parsed as links, with the URL or email address as the link +label. + +A [URI autolink](#uri-autolink) <a id="uri-autolink"></a> +consists of `<`, followed by an [absolute +URI](#absolute-uri) not containing `<`, followed by `>`. It is parsed +as a link to the URI, with the URI as the link's label. + +An [absolute URI](#absolute-uri), <a id="absolute-uri"></a> +for these purposes, consists of a [scheme](#scheme) followed by a colon (`:`) +followed by zero or more characters other than ASCII whitespace and +control characters, `<`, and `>`. If the URI includes these characters, +you must use percent-encoding (e.g. `%20` for a space). + +The following [schemes](#scheme) <a id="scheme"></a> +are recognized (case-insensitive): +`coap`, `doi`, `javascript`, `aaa`, `aaas`, `about`, `acap`, `cap`, +`cid`, `crid`, `data`, `dav`, `dict`, `dns`, `file`, `ftp`, `geo`, `go`, +`gopher`, `h323`, `http`, `https`, `iax`, `icap`, `im`, `imap`, `info`, +`ipp`, `iris`, `iris.beep`, `iris.xpc`, `iris.xpcs`, `iris.lwz`, `ldap`, +`mailto`, `mid`, `msrp`, `msrps`, `mtqp`, `mupdate`, `news`, `nfs`, +`ni`, `nih`, `nntp`, `opaquelocktoken`, `pop`, `pres`, `rtsp`, +`service`, `session`, `shttp`, `sieve`, `sip`, `sips`, `sms`, `snmp`,` +soap.beep`, `soap.beeps`, `tag`, `tel`, `telnet`, `tftp`, `thismessage`, +`tn3270`, `tip`, `tv`, `urn`, `vemmi`, `ws`, `wss`, `xcon`, +`xcon-userid`, `xmlrpc.beep`, `xmlrpc.beeps`, `xmpp`, `z39.50r`, +`z39.50s`, `adiumxtra`, `afp`, `afs`, `aim`, `apt`,` attachment`, `aw`, +`beshare`, `bitcoin`, `bolo`, `callto`, `chrome`,` chrome-extension`, +`com-eventbrite-attendee`, `content`, `cvs`,` dlna-playsingle`, +`dlna-playcontainer`, `dtn`, `dvb`, `ed2k`, `facetime`, `feed`, +`finger`, `fish`, `gg`, `git`, `gizmoproject`, `gtalk`, `hcp`, `icon`, +`ipn`, `irc`, `irc6`, `ircs`, `itms`, `jar`, `jms`, `keyparc`, `lastfm`, +`ldaps`, `magnet`, `maps`, `market`,` message`, `mms`, `ms-help`, +`msnim`, `mumble`, `mvn`, `notes`, `oid`, `palm`, `paparazzi`, +`platform`, `proxy`, `psyc`, `query`, `res`, `resource`, `rmi`, `rsync`, +`rtmp`, `secondlife`, `sftp`, `sgn`, `skype`, `smb`, `soldat`, +`spotify`, `ssh`, `steam`, `svn`, `teamspeak`, `things`, `udp`, +`unreal`, `ut2004`, `ventrilo`, `view-source`, `webcal`, `wtai`, +`wyciwyg`, `xfire`, `xri`, `ymsgr`. + +Here are some valid autolinks: + +. +<http://foo.bar.baz> +. +<p><a href="http://foo.bar.baz">http://foo.bar.baz</a></p> +. + +. +<http://foo.bar.baz?q=hello&id=22&boolean> +. +<p><a href="http://foo.bar.baz?q=hello&id=22&boolean">http://foo.bar.baz?q=hello&id=22&boolean</a></p> +. + +. +<irc://foo.bar:2233/baz> +. +<p><a href="irc://foo.bar:2233/baz">irc://foo.bar:2233/baz</a></p> +. + +Uppercase is also fine: + +. +<MAILTO:FOO@BAR.BAZ> +. +<p><a href="MAILTO:FOO@BAR.BAZ">MAILTO:FOO@BAR.BAZ</a></p> +. + +Spaces are not allowed in autolinks: + +. +<http://foo.bar/baz bim> +. +<p><http://foo.bar/baz bim></p> +. + +An [email autolink](#email-autolink) <a id="email-autolink"></a> +consists of `<`, followed by an [email address](#email-address), +followed by `>`. The link's label is the email address, +and the URL is `mailto:` followed by the email address. + +An [email address](#email-address), <a id="email-address"></a> +for these purposes, is anything that matches +the [non-normative regex from the HTML5 +spec](http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-%28type=email%29): + + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? + (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + +Examples of email autolinks: + +. +<foo@bar.baz.com> +. +<p><a href="mailto:foo@bar.baz.com">foo@bar.baz.com</a></p> +. + +. +<foo+special@Bar.baz-bar0.com> +. +<p><a href="mailto:foo+special@Bar.baz-bar0.com">foo+special@Bar.baz-bar0.com</a></p> +. + +These are not autolinks: + +. +<> +. +<p><></p> +. + +. +<heck://bing.bong> +. +<p><heck://bing.bong></p> +. + +. +< http://foo.bar > +. +<p>< http://foo.bar ></p> +. + +. +<foo.bar.baz> +. +<p><foo.bar.baz></p> +. + +. +<localhost:5001/foo> +. +<p><localhost:5001/foo></p> +. + +. +http://google.com +. +<p>http://google.com</p> +. + +. +foo@bar.baz.com +. +<p>foo@bar.baz.com</p> +. + +## Raw HTML + +Text between `<` and `>` that looks like an HTML tag is parsed as a +raw HTML tag and will be rendered in HTML without escaping. +Tag and attribute names are not limited to current HTML tags, +so custom tags (and even, say, DocBook tags) may be used. + +Here is the grammar for tags: + +A [tag name](#tag-name) <a id="tag-name"></a> consists of an ASCII letter +followed by zero or more ASCII letters or digits. + +An [attribute](#attribute) <a id="attribute"></a> consists of whitespace, +an **attribute name**, and an optional **attribute value +specification**. + +An [attribute name](#attribute-name) <a id="attribute-name"></a> +consists of an ASCII letter, `_`, or `:`, followed by zero or more ASCII +letters, digits, `_`, `.`, `:`, or `-`. (Note: This is the XML +specification restricted to ASCII. HTML5 is laxer.) + +An [attribute value specification](#attribute-value-specification) +<a id="attribute-value-specification"></a> consists of optional whitespace, +a `=` character, optional whitespace, and an [attribute +value](#attribute-value). + +An [attribute value](#attribute-value) <a id="attribute-value"></a> +consists of an [unquoted attribute value](#unquoted-attribute-value), +a [single-quoted attribute value](#single-quoted-attribute-value), +or a [double-quoted attribute value](#double-quoted-attribute-value). + +An [unquoted attribute value](#unquoted-attribute-value) +<a id="unquoted-attribute-value"></a> is a nonempty string of characters not +including spaces, `"`, `'`, `=`, `<`, `>`, or `` ` ``. + +A [single-quoted attribute value](#single-quoted-attribute-value) +<a id="single-quoted-attribute-value"></a> consists of `'`, zero or more +characters not including `'`, and a final `'`. + +A [double-quoted attribute value](#double-quoted-attribute-value) +<a id="double-quoted-attribute-value"></a> consists of `"`, zero or more +characters not including `"`, and a final `"`. + +An [open tag](#open-tag) <a id="open-tag"></a> consists of a `<` character, +a [tag name](#tag-name), zero or more [attributes](#attribute), +optional whitespace, an optional `/` character, and a `>` character. + +A [closing tag](#closing-tag) <a id="closing-tag"></a> consists of the +string `</`, a [tag name](#tag-name), optional whitespace, and the +character `>`. + +An [HTML comment](#html-comment) <a id="html-comment"></a> consists of the +string `<!--`, a string of characters not including the string `--`, and +the string `-->`. + +A [processing instruction](#processing-instruction) +<a id="processing-instruction"></a> consists of the string `<?`, a string +of characters not including the string `?>`, and the string +`?>`. + +A [declaration](#declaration) <a id="declaration"></a> consists of the +string `<!`, a name consisting of one or more uppercase ASCII letters, +whitespace, a string of characters not including the character `>`, and +the character `>`. + +A [CDATA section](#cdata-section) <a id="cdata-section"></a> consists of +the string `<![CDATA[`, a string of characters not including the string +`]]>`, and the string `]]>`. + +An [HTML tag](#html-tag) <a id="html-tag"></a> consists of an [open +tag](#open-tag), a [closing tag](#closing-tag), an [HTML +comment](#html-comment), a [processing +instruction](#processing-instruction), an [element type +declaration](#element-type-declaration), or a [CDATA +section](#cdata-section). + +Here are some simple open tags: + +. +<a><bab><c2c> +. +<p><a><bab><c2c></p> +. + +Empty elements: + +. +<a/><b2/> +. +<p><a/><b2/></p> +. + +Whitespace is allowed: + +. +<a /><b2 +data="foo" > +. +<p><a /><b2 +data="foo" ></p> +. + +With attributes: + +. +<a foo="bar" bam = 'baz <em>"</em>' +_boolean zoop:33=zoop:33 /> +. +<p><a foo="bar" bam = 'baz <em>"</em>' +_boolean zoop:33=zoop:33 /></p> +. + +Illegal tag names, not parsed as HTML: + +. +<33> <__> +. +<p><33> <__></p> +. + +Illegal attribute names: + +. +<a h*#ref="hi"> +. +<p><a h*#ref="hi"></p> +. + +Illegal attribute values: + +. +<a href="hi'> <a href=hi'> +. +<p><a href="hi'> <a href=hi'></p> +. + +Illegal whitespace: + +. +< a>< +foo><bar/ > +. +<p>< a>< +foo><bar/ ></p> +. + +Missing whitespace: + +. +<a href='bar'title=title> +. +<p><a href='bar'title=title></p> +. + +Closing tags: + +. +</a> +</foo > +. +<p></a> +</foo ></p> +. + +Illegal attributes in closing tag: + +. +</a href="foo"> +. +<p></a href="foo"></p> +. + +Comments: + +. +foo <!-- this is a +comment - with hyphen --> +. +<p>foo <!-- this is a +comment - with hyphen --></p> +. + +. +foo <!-- not a comment -- two hyphens --> +. +<p>foo <!-- not a comment -- two hyphens --></p> +. + +Processing instructions: + +. +foo <?php echo $a; ?> +. +<p>foo <?php echo $a; ?></p> +. + +Declarations: + +. +foo <!ELEMENT br EMPTY> +. +<p>foo <!ELEMENT br EMPTY></p> +. + +CDATA sections: + +. +foo <![CDATA[>&<]]> +. +<p>foo <![CDATA[>&<]]></p> +. + +Entities are preserved in HTML attributes: + +. +<a href="ö"> +. +<p><a href="ö"></p> +. + +Backslash escapes do not work in HTML attributes: + +. +<a href="\*"> +. +<p><a href="\*"></p> +. + +. +<a href="\""> +. +<p><a href="""></p> +. + +## Hard line breaks + +A line break (not in a code span or HTML tag) that is preceded +by two or more spaces is parsed as a linebreak (rendered +in HTML as a `<br />` tag): + +. +foo +baz +. +<p>foo<br /> +baz</p> +. + +For a more visible alternative, a backslash before the newline may be +used instead of two spaces: + +. +foo\ +baz +. +<p>foo<br /> +baz</p> +. + +More than two spaces can be used: + +. +foo +baz +. +<p>foo<br /> +baz</p> +. + +Leading spaces at the beginning of the next line are ignored: + +. +foo + bar +. +<p>foo<br /> +bar</p> +. + +. +foo\ + bar +. +<p>foo<br /> +bar</p> +. + +Line breaks can occur inside emphasis, links, and other constructs +that allow inline content: + +. +*foo +bar* +. +<p><em>foo<br /> +bar</em></p> +. + +. +*foo\ +bar* +. +<p><em>foo<br /> +bar</em></p> +. + +Line breaks do not occur inside code spans + +. +`code +span` +. +<p><code>code span</code></p> +. + +. +`code\ +span` +. +<p><code>code\ span</code></p> +. + +or HTML tags: + +. +<a href="foo +bar"> +. +<p><a href="foo +bar"></p> +. + +. +<a href="foo\ +bar"> +. +<p><a href="foo\ +bar"></p> +. + +## Soft line breaks + +A regular line break (not in a code span or HTML tag) that is not +preceded by two or more spaces is parsed as a softbreak. (A +softbreak may be rendered in HTML either as a newline or as a space. +The result will be the same in browsers. In the examples here, a +newline will be used.) + +. +foo +baz +. +<p>foo +baz</p> +. + +Spaces at the end of the line and beginning of the next line are +removed: + +. +foo + baz +. +<p>foo +baz</p> +. + +A conforming parser may render a soft line break in HTML either as a +line break or as a space. + +A renderer may also provide an option to render soft line breaks +as hard line breaks. + +## Strings + +Any characters not given an interpretation by the above rules will +be parsed as string content. + +. +hello $.;'there +. +<p>hello $.;'there</p> +. + +. +Foo χρῆν +. +<p>Foo χρῆν</p> +. + +Internal spaces are preserved verbatim: + +. +Multiple spaces +. +<p>Multiple spaces</p> +. + +<!-- END TESTS --> + +# Appendix A: A parsing strategy {-} + +## Overview {-} + +Parsing has two phases: + +1. In the first phase, lines of input are consumed and the block +structure of the document---its division into paragraphs, block quotes, +list items, and so on---is constructed. Text is assigned to these +blocks but not parsed. Link reference definitions are parsed and a +map of links is constructed. + +2. In the second phase, the raw text contents of paragraphs and headers +are parsed into sequences of Markdown inline elements (strings, +code spans, links, emphasis, and so on), using the map of link +references constructed in phase 1. + +## The document tree {-} + +At each point in processing, the document is represented as a tree of +**blocks**. The root of the tree is a `document` block. The `document` +may have any number of other blocks as **children**. These children +may, in turn, have other blocks as children. The last child of a block +is normally considered **open**, meaning that subsequent lines of input +can alter its contents. (Blocks that are not open are **closed**.) +Here, for example, is a possible document tree, with the open blocks +marked by arrows: + +``` tree +-> document + -> block_quote + paragraph + "Lorem ipsum dolor\nsit amet." + -> list (type=bullet tight=true bullet_char=-) + list_item + paragraph + "Qui *quodsi iracundia*" + -> list_item + -> paragraph + "aliquando id" +``` + +## How source lines alter the document tree {-} + +Each line that is processed has an effect on this tree. The line is +analyzed and, depending on its contents, the document may be altered +in one or more of the following ways: + +1. One or more open blocks may be closed. +2. One or more new blocks may be created as children of the + last open block. +3. Text may be added to the last (deepest) open block remaining + on the tree. + +Once a line has been incorporated into the tree in this way, +it can be discarded, so input can be read in a stream. + +We can see how this works by considering how the tree above is +generated by four lines of Markdown: + +``` markdown +> Lorem ipsum dolor +sit amet. +> - Qui *quodsi iracundia* +> - aliquando id +``` + +At the outset, our document model is just + +``` tree +-> document +``` + +The first line of our text, + +``` markdown +> Lorem ipsum dolor +``` + +causes a `block_quote` block to be created as a child of our +open `document` block, and a `paragraph` block as a child of +the `block_quote`. Then the text is added to the last open +block, the `paragraph`: + +``` tree +-> document + -> block_quote + -> paragraph + "Lorem ipsum dolor" +``` + +The next line, + +``` markdown +sit amet. +``` + +is a "lazy continuation" of the open `paragraph`, so it gets added +to the paragraph's text: + +``` tree +-> document + -> block_quote + -> paragraph + "Lorem ipsum dolor\nsit amet." +``` + +The third line, + +``` markdown +> - Qui *quodsi iracundia* +``` + +causes the `paragraph` block to be closed, and a new `list` block +opened as a child of the `block_quote`. A `list_item` is also +added as a child of the `list`, and a `paragraph` as a child of +the `list_item`. The text is then added to the new `paragraph`: + +``` tree +-> document + -> block_quote + paragraph + "Lorem ipsum dolor\nsit amet." + -> list (type=bullet tight=true bullet_char=-) + -> list_item + -> paragraph + "Qui *quodsi iracundia*" +``` + +The fourth line, + +``` markdown +> - aliquando id +``` + +causes the `list_item` (and its child the `paragraph`) to be closed, +and a new `list_item` opened up as child of the `list`. A `paragraph` +is added as a child of the new `list_item`, to contain the text. +We thus obtain the final tree: + +``` tree +-> document + -> block_quote + paragraph + "Lorem ipsum dolor\nsit amet." + -> list (type=bullet tight=true bullet_char=-) + list_item + paragraph + "Qui *quodsi iracundia*" + -> list_item + -> paragraph + "aliquando id" +``` + +## From block structure to the final document {-} + +Once all of the input has been parsed, all open blocks are closed. + +We then "walk the tree," visiting every node, and parse raw +string contents of paragraphs and headers as inlines. At this +point we have seen all the link reference definitions, so we can +resolve reference links as we go. + +``` tree +document + block_quote + paragraph + str "Lorem ipsum dolor" + softbreak + str "sit amet." + list (type=bullet tight=true bullet_char=-) + list_item + paragraph + str "Qui " + emph + str "quodsi iracundia" + list_item + paragraph + str "aliquando id" +``` + +Notice how the newline in the first paragraph has been parsed as +a `softbreak`, and the asterisks in the first list item have become +an `emph`. + +The document can be rendered as HTML, or in any other format, given +an appropriate renderer. + + diff --git a/core/testdata/packagedocs/stdlib.md b/core/testdata/packagedocs/stdlib.md new file mode 100644 index 00000000..5d7432b5 --- /dev/null +++ b/core/testdata/packagedocs/stdlib.md @@ -0,0 +1,11 @@ +# Module stdlib + +## Kotlin Standard Library + +The Kotlin standard library is a set of functions and types implementing idiomatic patterns when working with collections, +text and files. + +# Package kotlin + +Core functions and types + diff --git a/core/testdata/packages/dottedNamePackage.kt b/core/testdata/packages/dottedNamePackage.kt new file mode 100644 index 00000000..38619310 --- /dev/null +++ b/core/testdata/packages/dottedNamePackage.kt @@ -0,0 +1 @@ +package dot.name
\ No newline at end of file diff --git a/core/testdata/packages/rootPackage.kt b/core/testdata/packages/rootPackage.kt new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/core/testdata/packages/rootPackage.kt diff --git a/core/testdata/packages/simpleNamePackage.kt b/core/testdata/packages/simpleNamePackage.kt new file mode 100644 index 00000000..2c29f4c7 --- /dev/null +++ b/core/testdata/packages/simpleNamePackage.kt @@ -0,0 +1 @@ +package simple diff --git a/core/testdata/packages/simpleNamePackage2.kt b/core/testdata/packages/simpleNamePackage2.kt new file mode 100644 index 00000000..2c29f4c7 --- /dev/null +++ b/core/testdata/packages/simpleNamePackage2.kt @@ -0,0 +1 @@ +package simple diff --git a/core/testdata/properties/annotatedProperty.kt b/core/testdata/properties/annotatedProperty.kt new file mode 100644 index 00000000..8990af29 --- /dev/null +++ b/core/testdata/properties/annotatedProperty.kt @@ -0,0 +1 @@ +@Volatile var property = "test"
\ No newline at end of file diff --git a/core/testdata/properties/propertyOverride.kt b/core/testdata/properties/propertyOverride.kt new file mode 100644 index 00000000..625d1da0 --- /dev/null +++ b/core/testdata/properties/propertyOverride.kt @@ -0,0 +1,7 @@ +open class Foo() { + open val xyzzy: Int get() = 0 +} + +class Bar(): Foo() { + override val xyzzy: Int get() = 1 +} diff --git a/core/testdata/properties/propertyWithReceiver.kt b/core/testdata/properties/propertyWithReceiver.kt new file mode 100644 index 00000000..e282f6bd --- /dev/null +++ b/core/testdata/properties/propertyWithReceiver.kt @@ -0,0 +1,2 @@ +val String.foobar: Int + get() = size() * 2 diff --git a/core/testdata/properties/valueProperty.kt b/core/testdata/properties/valueProperty.kt new file mode 100644 index 00000000..b87cce57 --- /dev/null +++ b/core/testdata/properties/valueProperty.kt @@ -0,0 +1 @@ +val property = "test"
\ No newline at end of file diff --git a/core/testdata/properties/valuePropertyWithGetter.kt b/core/testdata/properties/valuePropertyWithGetter.kt new file mode 100644 index 00000000..64d3848c --- /dev/null +++ b/core/testdata/properties/valuePropertyWithGetter.kt @@ -0,0 +1,2 @@ +val property: String + get() = "test"
\ No newline at end of file diff --git a/core/testdata/properties/variableProperty.kt b/core/testdata/properties/variableProperty.kt new file mode 100644 index 00000000..54ab4595 --- /dev/null +++ b/core/testdata/properties/variableProperty.kt @@ -0,0 +1 @@ +var property = "test"
\ No newline at end of file diff --git a/core/testdata/properties/variablePropertyWithAccessors.kt b/core/testdata/properties/variablePropertyWithAccessors.kt new file mode 100644 index 00000000..152fb7d0 --- /dev/null +++ b/core/testdata/properties/variablePropertyWithAccessors.kt @@ -0,0 +1,4 @@ +var property: String + get() = "test" + set(value) { + }
\ No newline at end of file |