diff options
Diffstat (limited to 'plugins/base/src')
11 files changed, 353 insertions, 10 deletions
diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt index 9b6e9b1a..fd842dc0 100644 --- a/plugins/base/src/main/kotlin/DokkaBase.kt +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -1,6 +1,11 @@ package org.jetbrains.dokka.base import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.base.renderers.FileWriter +import org.jetbrains.dokka.base.renderers.OutputWriter +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jetbrains.dokka.base.resolvers.DefaultLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.LocationProviderFactory import org.jetbrains.dokka.base.transformers.descriptors.DefaultDescriptorToDocumentationTranslator import org.jetbrains.dokka.base.transformers.documentables.DefaultDocumentableMerger import org.jetbrains.dokka.base.transformers.documentables.DefaultDocumentablesToPageTranslator @@ -12,11 +17,12 @@ import org.jetbrains.dokka.base.transformers.pages.merger.PageNodeMerger import org.jetbrains.dokka.base.transformers.pages.merger.SameMethodNamePageMergerStrategy import org.jetbrains.dokka.base.transformers.psi.DefaultPsiToDocumentationTranslator import org.jetbrains.dokka.plugability.DokkaPlugin -import org.jetbrains.dokka.renderers.html.HtmlRenderer class DokkaBase : DokkaPlugin() { val pageMergerStrategy by extensionPoint<PageMergerStrategy>() val commentsToContentConverter by extensionPoint<CommentsToContentConverter>() + val locationproviderFactory by extensionPoint<LocationProviderFactory>() + val outputWriter by extensionPoint<OutputWriter>() val descriptorToDocumentationTranslator by extending(isFallback = true) { CoreExtensions.descriptorToDocumentationTranslator providing ::DefaultDescriptorToDocumentationTranslator @@ -57,4 +63,12 @@ class DokkaBase : DokkaPlugin() { val htmlRenderer by extending { CoreExtensions.renderer providing ::HtmlRenderer applyIf { format == "html" } } + + val locationProvider by extending(isFallback = true) { + locationproviderFactory providing ::DefaultLocationProviderFactory + } + + val fileWriter by extending(isFallback = true) { + outputWriter providing ::FileWriter + } }
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt index c6183cf3..951545d2 100644 --- a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt @@ -1,18 +1,19 @@ package org.jetbrains.dokka.base.renderers -import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.LocationProvider import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.renderers.OutputWriter +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle import org.jetbrains.dokka.renderers.Renderer -import org.jetbrains.dokka.resolvers.LocationProvider import org.jetbrains.dokka.transformers.pages.PageNodeTransformer abstract class DefaultRenderer<T>( protected val context: DokkaContext ) : Renderer { - protected val outputWriter = context.single(CoreExtensions.outputWriter) + protected val outputWriter = context.plugin<DokkaBase>().querySingle { outputWriter } protected lateinit var locationProvider: LocationProvider private set @@ -116,7 +117,7 @@ abstract class DefaultRenderer<T>( val newRoot = preprocessors.fold(root) { acc, t -> t(acc) } locationProvider = - context.single(CoreExtensions.locationProviderFactory).getLocationProvider(newRoot) + context.plugin<DokkaBase>().querySingle { locationproviderFactory }.getLocationProvider(newRoot) root.children<ModulePageNode>().forEach { renderPackageList(it) } diff --git a/plugins/base/src/main/kotlin/renderers/FileWriter.kt b/plugins/base/src/main/kotlin/renderers/FileWriter.kt new file mode 100644 index 00000000..5d3067fc --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/FileWriter.kt @@ -0,0 +1,79 @@ +package org.jetbrains.dokka.base.renderers + +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.renderers.OutputWriter +import java.io.File +import java.io.IOException +import java.net.URI +import java.nio.file.* + +class FileWriter(val context: DokkaContext): OutputWriter { + private val createdFiles: MutableSet<String> = mutableSetOf() + private val jarUriPrefix = "jar:file:" + private val root = context.configuration.outputDir + + override fun write(path: String, text: String, ext: String) { + if (createdFiles.contains(path)) { + context.logger.error("An attempt to write ${root}/$path several times!") + return + } + createdFiles.add(path) + + try { + val dir = Paths.get(root, path.dropLastWhile { it != '/' }).toFile() + dir.mkdirsOrFail() + Files.write(Paths.get(root, "$path$ext"), text.lines()) + } catch (e: Throwable) { + context.logger.error("Failed to write $this. ${e.message}") + e.printStackTrace() + } + } + + override fun writeResources(pathFrom: String, pathTo: String) = + if (javaClass.getResource(pathFrom).toURI().toString().startsWith(jarUriPrefix)) { + copyFromJar(pathFrom, pathTo) + } else { + copyFromDirectory(pathFrom, pathTo) + } + + + private fun copyFromDirectory(pathFrom: String, pathTo: String) { + val dest = Paths.get(root, pathTo).toFile() + val uri = javaClass.getResource(pathFrom).toURI() + File(uri).copyRecursively(dest, true) + } + + private fun copyFromJar(pathFrom: String, pathTo: String) { + val rebase = fun(path: String) = + "$pathTo/${path.removePrefix(pathFrom)}" + val dest = Paths.get(root, pathTo).toFile() + dest.mkdirsOrFail() + val uri = javaClass.getResource(pathFrom).toURI() + val fs = getFileSystemForURI(uri) + val path = fs.getPath(pathFrom) + for (file in Files.walk(path).iterator()) { + if (Files.isDirectory(file)) { + val dirPath = file.toAbsolutePath().toString() + Paths.get(root, rebase(dirPath)).toFile().mkdirsOrFail() + } else { + val filePath = file.toAbsolutePath().toString() + Paths.get(root, rebase(filePath)).toFile().writeBytes( + javaClass.getResourceAsStream(filePath).readBytes() + ) + } + } + } + + private fun File.mkdirsOrFail() { + if (!mkdirs() && !exists()) { + throw IOException("Failed to create directory $this") + } + } + + private fun getFileSystemForURI(uri: URI): FileSystem = + try { + FileSystems.newFileSystem(uri, emptyMap<String, Any>()) + } catch (e: FileSystemAlreadyExistsException) { + FileSystems.getFileSystem(uri) + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/OutputWriter.kt b/plugins/base/src/main/kotlin/renderers/OutputWriter.kt new file mode 100644 index 00000000..a6fda51a --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/OutputWriter.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.base.renderers + +interface OutputWriter { + + fun write(path: String, text: String, ext: String) + fun writeResources(pathFrom: String, pathTo: String) +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt index c9270681..8bf00043 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -1,4 +1,4 @@ -package org.jetbrains.dokka.renderers.html +package org.jetbrains.dokka.base.renderers.html import kotlinx.html.* import kotlinx.html.stream.createHTML @@ -7,7 +7,6 @@ import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.Function import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.renderers.OutputWriter import java.io.File open class HtmlRenderer( diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt index 4a2fb40d..ad574769 100644 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt @@ -1,4 +1,4 @@ -package org.jetbrains.dokka.renderers.html +package org.jetbrains.dokka.base.renderers.html import kotlinx.html.* import kotlinx.html.stream.createHTML diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt index 09164d97..ecd2e89a 100644 --- a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt +++ b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt @@ -1,4 +1,4 @@ -package org.jetbrains.dokka.renderers.html +package org.jetbrains.dokka.base.renderers.html import kotlinx.html.h1 import kotlinx.html.id diff --git a/plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt new file mode 100644 index 00000000..2238b0c3 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt @@ -0,0 +1,116 @@ +package org.jetbrains.dokka.base.resolvers + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.utilities.htmlEscape +import java.util.* + +private const val PAGE_WITH_CHILDREN_SUFFIX = "index" + +open class DefaultLocationProvider( + protected val pageGraphRoot: RootPageNode, + protected val dokkaContext: DokkaContext +) : LocationProvider { + protected val extension = ".html" + + protected val pagesIndex: Map<DRI, ContentPage> = pageGraphRoot.asSequence().filterIsInstance<ContentPage>() + .map { it.dri.map { dri -> dri to it } }.flatten() + .groupingBy { it.first } + .aggregate { dri, _, (_, page), first -> + if (first) page else throw AssertionError("Multiple pages associated with dri: $dri") + } + + protected val pathsIndex: Map<PageNode, List<String>> = IdentityHashMap<PageNode, List<String>>().apply { + fun registerPath(page: PageNode, prefix: List<String>) { + val newPrefix = prefix + page.pathName + put(page, newPrefix) + page.children.forEach { registerPath(it, newPrefix) } + } + put(pageGraphRoot, emptyList()) + pageGraphRoot.children.forEach { registerPath(it, emptyList()) } + } + + override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean): String = + pathTo(node, context) + if (!skipExtension) extension else "" + + override fun resolve(dri: DRI, platforms: List<PlatformData>, context: PageNode?): String = + pagesIndex[dri]?.let { resolve(it, context) } ?: + // Not found in PageGraph, that means it's an external link + ExternalLocationProvider.getLocation(dri, + this.dokkaContext.configuration.passesConfigurations + .filter { passConfig -> + platforms.toSet() + .contains(PlatformData(passConfig.moduleName, passConfig.analysisPlatform, passConfig.targets)) + } // TODO: change targets to something better? + .flatMap { it.externalDocumentationLinks }.distinct() + ) + + override fun resolveRoot(node: PageNode): String = + pathTo(pageGraphRoot, node).removeSuffix(PAGE_WITH_CHILDREN_SUFFIX) + + override fun ancestors(node: PageNode): List<PageNode> = + generateSequence(node) { it.parent() }.toList() + + protected open fun pathTo(node: PageNode, context: PageNode?): String { + fun pathFor(page: PageNode) = pathsIndex[page] ?: throw AssertionError( + "${page::class.simpleName}(${page.name}) does not belong to current page graph so it is impossible to compute its path" + ) + + val contextNode = + if (context?.children?.isEmpty() == true && context.parent() != null) context.parent() else context + val nodePath = pathFor(node) + val contextPath = contextNode?.let { pathFor(it) }.orEmpty() + + val commonPathElements = nodePath.asSequence().zip(contextPath.asSequence()) + .takeWhile { (a, b) -> a == b }.count() + + return (List(contextPath.size - commonPathElements) { ".." } + nodePath.drop(commonPathElements) + + if (node.children.isNotEmpty()) listOf(PAGE_WITH_CHILDREN_SUFFIX) else emptyList()).joinToString("/") + } + + private fun PageNode.parent() = pageGraphRoot.parentMap[this] +} + +fun DRI.toJavadocLocation(jdkVersion: Int): String { // TODO: classes without packages? + val packageLink = packageName?.replace(".", "/") + if (classNames == null) { + return "$packageLink/package-summary.html".htmlEscape() + } + val classLink = if (packageLink == null) "$classNames.html" else "$packageLink/$classNames.html" + val callableChecked = callable ?: return classLink.htmlEscape() + + val callableLink = "$classLink#${callableChecked.name}" + when { + jdkVersion < 8 -> "(${callableChecked.params.joinToString(", ")})" + jdkVersion < 10 -> "-${callableChecked.params.joinToString("-")}-" + else -> "(${callableChecked.params.joinToString(",")})" + } + + return callableLink.htmlEscape() +} + +fun DRI.toDokkaLocation(extension: String): String { // TODO: classes without packages? + val classNamesChecked = classNames ?: return "$packageName/index$extension" + + val classLink = if (packageName == null) { + "" + } else { + "$packageName/" + } + classNamesChecked.split('.').joinToString("/", transform = ::identifierToFilename) + + val callableChecked = callable ?: return "$classLink/index$extension" + + return "$classLink/${identifierToFilename(callableChecked.name)}$extension" +} + +private val reservedFilenames = setOf("index", "con", "aux", "lst", "prn", "nul", "eof", "inp", "out") + +private fun identifierToFilename(name: String): String { + if (name.isEmpty()) return "--root--" + val escaped = name.replace('<', '-').replace('>', '-') + val lowercase = escaped.replace("[A-Z]".toRegex()) { matchResult -> "-" + matchResult.value.toLowerCase() } + return if (lowercase in reservedFilenames) "--$lowercase--" else lowercase +} + +private val PageNode.pathName: String + get() = if (this is PackagePageNode) name else identifierToFilename(name) diff --git a/plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt new file mode 100644 index 00000000..c649e22b --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.base.resolvers + +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext + +class DefaultLocationProviderFactory(private val context: DokkaContext) : LocationProviderFactory { + + override fun getLocationProvider(pageNode: RootPageNode) = DefaultLocationProvider(pageNode, context) +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/ExternalLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/ExternalLocationProvider.kt new file mode 100644 index 00000000..7c0e9952 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/ExternalLocationProvider.kt @@ -0,0 +1,99 @@ +package org.jetbrains.dokka.base.resolvers + +import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink +import org.jetbrains.dokka.links.DRI +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection + +object ExternalLocationProvider { // TODO: Refactor this!!! + private const val DOKKA_PARAM_PREFIX = "\$dokka." + + private val cache: MutableMap<URL, LocationInfo> = mutableMapOf() + + fun getLocation(dri: DRI, externalDocumentationLinks: List<ExternalDocumentationLink>): String { + val toResolve: MutableList<ExternalDocumentationLink> = mutableListOf() + for(link in externalDocumentationLinks){ + val info = cache[link.packageListUrl] + if(info == null) { + toResolve.add(link) + } else if(info.packages.contains(dri.packageName)) { + return link.url.toExternalForm() + getLink(dri, info) + } + } + // Not in cache, resolve packageLists + while (toResolve.isNotEmpty()){ + val link = toResolve.first().also { toResolve.remove(it) } + val locationInfo = loadPackageList(link.packageListUrl) + if(locationInfo.packages.contains(dri.packageName)) { + return link.url.toExternalForm() + getLink(dri, locationInfo) + } + } + return "" + } + + private fun getLink(dri: DRI, locationInfo: LocationInfo): String = when(locationInfo.format) { + "javadoc" -> dri.toJavadocLocation(8) + "kotlin-website-html", "html" -> locationInfo.locations[dri.packageName + "." + dri.classNames] ?: dri.toDokkaLocation(".html") + "markdown" -> locationInfo.locations[dri.packageName + "." + dri.classNames] ?: dri.toDokkaLocation(".md") + // TODO: rework this + else -> throw RuntimeException("Unrecognized format") + } + + + private fun loadPackageList(url: URL): LocationInfo { + val packageListStream = url.doOpenConnectionToReadContent().getInputStream() + val (params, packages) = + packageListStream + .bufferedReader() + .useLines { lines -> lines.partition { it.startsWith(DOKKA_PARAM_PREFIX) } } + + val paramsMap = params.asSequence() + .map { it.removePrefix(DOKKA_PARAM_PREFIX).split(":", limit = 2) } + .groupBy({ (key, _) -> key }, { (_, value) -> value }) + + val format = paramsMap["format"]?.singleOrNull() ?: "javadoc" + + val locations = paramsMap["location"].orEmpty() + .map { it.split("\u001f", limit = 2) } + .map { (key, value) -> key to value } + .toMap() + + val info = LocationInfo(format, packages.toSet(), locations) + cache[url] = info + return info + } + + private fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection { + val connection = this.openConnection().apply { + connectTimeout = timeout + readTimeout = timeout + } + + when (connection) { + is HttpURLConnection -> { + return when (connection.responseCode) { + in 200..299 -> { + connection + } + HttpURLConnection.HTTP_MOVED_PERM, + HttpURLConnection.HTTP_MOVED_TEMP, + HttpURLConnection.HTTP_SEE_OTHER -> { + if (redirectsAllowed > 0) { + val newUrl = connection.getHeaderField("Location") + URL(newUrl).doOpenConnectionToReadContent(timeout, redirectsAllowed - 1) + } else { + throw RuntimeException("Too many redirects") + } + } + else -> { + throw RuntimeException("Unhandled http code: ${connection.responseCode}") + } + } + } + else -> return connection + } + } + data class LocationInfo(val format: String, val packages: Set<String>, val locations: Map<String, String>) + +} diff --git a/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt new file mode 100644 index 00000000..13f4563c --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt @@ -0,0 +1,19 @@ +package org.jetbrains.dokka.base.resolvers + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.PlatformData +import org.jetbrains.dokka.pages.RootPageNode + +interface LocationProvider { + fun resolve(dri: DRI, platforms: List<PlatformData>, context: PageNode? = null): String + fun resolve(node: PageNode, context: PageNode? = null, skipExtension: Boolean = false): String + fun resolveRoot(node: PageNode): String + fun ancestors(node: PageNode): List<PageNode> +} + +interface LocationProviderFactory { + fun getLocationProvider(pageNode: RootPageNode): LocationProvider +} + |