Skip to content

Add DRI-based links between external documentations #10702

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1565,7 +1565,17 @@ object Build {
generateDocumentation(
classDirectory.in(Compile).value.getAbsolutePath,
"scala3doc", "scala3doc/output/self", VersionUtil.gitHash,
"-siteroot scala3doc/documentation -project-logo scala3doc/documentation/logo.svg")
"-siteroot scala3doc/documentation -project-logo scala3doc/documentation/logo.svg " +
"-external-mappings " + raw".*scala\/quoted.*" + "::" +
"scala3doc" + "::" +
"http://dotty.epfl.ch/api/" + ":::" +
raw".*java.*" + "::" +
"javadoc" + "::" +
"https://docs.oracle.com/javase/8/docs/api/" + ":::" +
raw".*scala.*" + "::" +
"scaladoc" + "::" +
"https://www.scala-lang.org/api/current/"
)
}.value,

generateScala3Documentation := Def.inputTaskDyn {
Expand All @@ -1589,7 +1599,14 @@ object Build {
IO.write(dest / "CNAME", "dotty.epfl.ch")
}.dependsOn(generateDocumentation(
roots, "Scala 3", dest.getAbsolutePath, "master",
"-comment-syntax wiki -siteroot scala3doc/scala3-docs -project-logo scala3doc/scala3-docs/logo.svg"))
"-comment-syntax wiki -siteroot scala3doc/scala3-docs -project-logo scala3doc/scala3-docs/logo.svg " +
"-external-mappings " + raw".*java.*" + "::" +
"javadoc" + "::" +
"https://docs.oracle.com/javase/8/docs/api/" + ":::" +
raw".*scala.*" + "::" +
"scaladoc" + "::" +
"https://www.scala-lang.org/api/current/"
))
}.evaluated,

generateTestcasesDocumentation := Def.taskDyn {
Expand Down
48 changes: 48 additions & 0 deletions scala3doc/src/dotty/dokka/DocContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import dotty.tools.dotc.util.Spans
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import scala.io.Codec
import java.net.URL
import scala.util.Try

type CompilerContext = dotty.tools.dotc.core.Contexts.Context

Expand Down Expand Up @@ -93,6 +95,52 @@ case class DocContext(args: Scala3doc.Args, compilerContext: CompilerContext)
sourceLinks
)(using compilerContext))

def parseDocTool(docTool: String) = docTool match {
case "scaladoc" => Some(DocumentationKind.Scaladoc)
case "scala3doc" => Some(DocumentationKind.Scala3doc)
case "javadoc" => Some(DocumentationKind.Javadoc)
case other => None
}
val externalDocumentationLinks: List[Scala3docExternalDocumentationLink] = args.externalMappings.filter(_.size >= 3).flatMap { mapping =>
val regexStr = mapping(0)
val docTool = mapping(1)
val urlStr = mapping(2)
val packageListUrlStr = if mapping.size > 3 then Some(mapping(3)) else None
val regex = Try(regexStr.r).toOption
val url = Try(URL(urlStr)).toOption
val packageListUrl = Try(packageListUrlStr.map(URL(_)))
.fold(
e => {
logger.warn(s"Wrong packageListUrl parameter in external mapping. Found '$packageListUrlStr'. " +
s"Package list url will be omitted")
None},
res => res
)

val parsedDocTool = parseDocTool(docTool)
val res = if regexStr.isEmpty then
logger.warn(s"Wrong regex parameter in external mapping. Found '$regexStr'. Mapping will be omitted")
None
else if url.isEmpty then
logger.warn(s"Wrong url parameter in external mapping. Found '$urlStr'. Mapping will be omitted")
None
else if parsedDocTool.isEmpty then
logger.warn(s"Wrong doc-tool parameter in external mapping. " +
s"Expected one of: 'scaladoc', 'scala3doc', 'javadoc'. Found:'$docTool'. Mapping will be omitted "
)
None
else
Some(
Scala3docExternalDocumentationLink(
List(regexStr.r),
URL(urlStr),
parsedDocTool.get,
packageListUrlStr.map(URL(_))
)
)
res
}

override def getPluginsConfiguration: JList[DokkaConfiguration.PluginConfiguration] =
JList()

Expand Down
21 changes: 21 additions & 0 deletions scala3doc/src/dotty/dokka/DottyDokkaPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.jetbrains.dokka.pages._
import dotty.dokka.model.api._
import org.jetbrains.dokka.CoreExtensions
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.resolvers.shared._

import dotty.dokka.site.NavigationCreator
import dotty.dokka.site.SitePagesCreator
Expand Down Expand Up @@ -71,6 +72,7 @@ class DottyDokkaPlugin extends DokkaJavaPlugin:
val scalaResourceInstaller = extend(
_.extensionPoint(dokkaBase.getHtmlPreprocessors)
.fromRecipe{ case ctx @ given DokkaContext => new ScalaResourceInstaller }
.name("scalaResourceInstaller")
.after(dokkaBase.getCustomResourceInstaller)
)

Expand Down Expand Up @@ -169,6 +171,25 @@ class DottyDokkaPlugin extends DokkaJavaPlugin:
.overrideExtension(dokkaBase.getLocationProvider)
)

val scalaPackageListCreator = extend(
_.extensionPoint(dokkaBase.getHtmlPreprocessors)
.fromRecipe(c => ScalaPackageListCreator(c, RecognizedLinkFormat.DokkaHtml))
.overrideExtension(dokkaBase.getPackageListCreator)
.after(
customDocumentationProvider.getValue
)
)

val scalaExternalLocationProviderFactory = extend(
_.extensionPoint(dokkaBase.getExternalLocationProviderFactory)
.fromRecipe{ case c @ given DokkaContext => new ScalaExternalLocationProviderFactory }
.overrideExtension(dokkaBase.getDokkaLocationProvider)
)

extension (ctx: DokkaContext):
def siteContext: Option[StaticSiteContext] = ctx.getConfiguration.asInstanceOf[DocContext].staticSiteContext
def args: Scala3doc.Args = ctx.getConfiguration.asInstanceOf[DocContext].args

// TODO (https://github.com/lampepfl/scala3doc/issues/232): remove once problem is fixed in Dokka
extension [T] (builder: ExtensionBuilder[T])
def ordered(before: Seq[Extension[_, _, _]], after: Seq[Extension[_, _, _]]): ExtensionBuilder[T] =
Expand Down
3 changes: 2 additions & 1 deletion scala3doc/src/dotty/dokka/Scala3doc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ object Scala3doc:
projectLogo: Option[String] = None,
defaultSyntax: CommentSyntax = CommentSyntax.Markdown,
sourceLinks: List[String] = Nil,
revision: Option[String] = None
revision: Option[String] = None,
externalMappings: List[List[String]] = List.empty
)

def run(args: Array[String], rootContext: CompilerContext): Reporter =
Expand Down
9 changes: 7 additions & 2 deletions scala3doc/src/dotty/dokka/Scala3docArgs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ class Scala3docArgs extends SettingGroup with CommonScalaSettings:
val revision: Setting[String] =
StringSetting("-revision", "revision", "Revision (branch or ref) used to build project project", "")

def scala3docSpecificSettings: Set[Setting[_]] = Set(sourceLinks, syntax, revision)
val externalDocumentationMappings: Setting[String] =
StringSetting("-external-mappings", "external-mappings", "Mapping between regex matching class file and external documentation", "")

def scala3docSpecificSettings: Set[Setting[_]] = Set(sourceLinks, syntax, revision, externalDocumentationMappings)

object Scala3docArgs:
def extract(args: List[String], rootCtx: CompilerContext):(Scala3doc.Args, CompilerContext) =
Expand Down Expand Up @@ -95,6 +98,7 @@ object Scala3docArgs:
CommentSyntax.default
}
}
val externalMappings = externalDocumentationMappings.get.split(":::").map(_.split("::").toList).toList

unsupportedSettings.filter(s => s.get != s.default).foreach { s =>
report.warning(s"Setting ${s.name} is currently not supported.")
Expand All @@ -115,6 +119,7 @@ object Scala3docArgs:
projectLogo.nonDefault,
parseSyntax,
sourceLinks.nonDefault.fold(Nil)(_.split(",").toList),
revision.nonDefault
revision.nonDefault,
externalMappings
)
(docArgs, newContext)
2 changes: 0 additions & 2 deletions scala3doc/src/dotty/dokka/compat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import java.util.stream.Collectors
import org.jetbrains.dokka.plugability._
import kotlin.jvm.JvmClassMappingKt.getKotlinClass

def mkDRI(packageName: String = null, extra: String = null) = new DRI(packageName, null, null, PointingToDeclaration.INSTANCE, extra)

val U: kotlin.Unit = kotlin.Unit.INSTANCE

def JList[T](e: T*): JList[T] = e.asJava
Expand Down
18 changes: 18 additions & 0 deletions scala3doc/src/dotty/dokka/externalDocumentationLinks.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dotty.dokka

import org.jetbrains.dokka._
import java.net.URL
import scala.util.matching._

case class Scala3docExternalDocumentationLink(
originRegexes: List[Regex],
documentationUrl: URL,
kind: DocumentationKind,
packageListUrl: Option[URL] = None
):
def withPackageList(url: URL): Scala3docExternalDocumentationLink = copy(packageListUrl = Some(url))

enum DocumentationKind:
case Javadoc extends DocumentationKind
case Scaladoc extends DocumentationKind
case Scala3doc extends DocumentationKind
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dotty.dokka

import org.jetbrains.dokka.base.resolvers.local._
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.resolvers.external._
import org.jetbrains.dokka.base.resolvers.shared._
import org.jetbrains.dokka.base.resolvers.anchors._
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.DisplaySourceSet
import org.jetbrains.dokka.pages.RootPageNode
import dotty.dokka.model.api._
import org.jetbrains.dokka.plugability._
import collection.JavaConverters._
import java.util.{Set => JSet}


class ScalaExternalLocationProvider(
externalDocumentation: ExternalDocumentation,
extension: String,
kind: DocumentationKind
)(using ctx: DokkaContext) extends DefaultExternalLocationProvider(externalDocumentation, extension, ctx):
override def resolve(dri: DRI): String =
Option(externalDocumentation.getPackageList).map(_.getLocations.asScala.toMap).flatMap(_.get(dri.toString))
.fold(constructPath(dri))( l => {
this.getDocURL + l
}
)

private val originRegex = raw"\[origin:(.*)\]".r

override def constructPath(dri: DRI): String = kind match {
case DocumentationKind.Javadoc => constructPathForJavadoc(dri)
case DocumentationKind.Scaladoc => constructPathForScaladoc(dri)
case DocumentationKind.Scala3doc => constructPathForScala3doc(dri)
}

private def constructPathForJavadoc(dri: DRI): String = {
val location = "\\$+".r.replaceAllIn(dri.location.replace(".","/"), _ => ".")
val origin = originRegex.findFirstIn(dri.extra)
val anchor = dri.anchor
getDocURL + location + extension + anchor.fold("")(a => s"#$a")
}

private def constructPathForScaladoc(dri: DRI): String = {
val location = dri.location.replace(".","/")
val anchor = dri.anchor
getDocURL + location + extension + anchor.fold("")(a => s"#$a")
}

private def constructPathForScala3doc(dri: DRI): String = {
val location = dri.location.replace(".","/")
val anchor = dri.anchor
getDocURL + location + anchor.fold(extension)(a => s"/$a$extension")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dotty.dokka

import org.jetbrains.dokka.base.resolvers.local._
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.resolvers.external._
import org.jetbrains.dokka.base.resolvers.shared._
import org.jetbrains.dokka.base.resolvers.anchors._
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.DisplaySourceSet
import org.jetbrains.dokka.pages.RootPageNode
import org.jetbrains.dokka.plugability._
import collection.JavaConverters._
import java.util.{Set => JSet}

class ScalaExternalLocationProviderFactory(using ctx: DokkaContext) extends ExternalLocationProviderFactory:
override def getExternalLocationProvider(doc: ExternalDocumentation): ExternalLocationProvider =
ScalaExternalLocationProvider(doc, ".html", DocumentationKind.Scala3doc)
46 changes: 46 additions & 0 deletions scala3doc/src/dotty/dokka/location/ScalaPackageListService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dotty.dokka

import org.jetbrains.dokka.base.renderers._
import org.jetbrains.dokka.base.resolvers.local._
import org.jetbrains.dokka.base._
import org.jetbrains.dokka.links._
import org.jetbrains.dokka.pages._
import org.jetbrains.dokka.plugability._
import collection.JavaConverters._
import dotty.dokka.model.api.withNoOrigin

object ScalaPackageListService:
val DOKKA_PARAM_PREFIX = "$dokka"

class ScalaPackageListService(context: DokkaContext, rootPage: RootPageNode):
import ScalaPackageListService._ //Why I need to do this?

val locationProvider = PluginUtils.querySingle[DokkaBase, LocationProviderFactory](context, _.getLocationProviderFactory)
.getLocationProvider(rootPage)

def createPackageList(format: String, linkExtension: String): String = {
val packages = retrievePackageInfo(rootPage)
val relocations = getRelocations(rootPage)
s"$DOKKA_PARAM_PREFIX.format:$format\n" ++
s"$DOKKA_PARAM_PREFIX.linkExtenstion:$linkExtension\n" ++
relocations.map( (dri, link) =>
s"$DOKKA_PARAM_PREFIX.location:${dri.withNoOrigin.toString}\u001f$link.$linkExtension"
).mkString("","\n","\n") ++
packages.mkString("","\n","\n")
}

private def retrievePackageInfo(current: PageNode): Set[String] = current match {
case p: PackagePageNode => p.getChildren.asScala.toSet.flatMap(retrievePackageInfo) ++ Option(p.getDocumentable.getDri.getPackageName)
case other => other.getChildren.asScala.toSet.flatMap(retrievePackageInfo)
}

private def getRelocations(current: PageNode): List[(DRI, String)] = current match {
case c: ContentPage => getRelocation(c.getDri.asScala.toList, c) ++ c.getChildren.asScala.toList.flatMap(getRelocations)
case other => other.getChildren.asScala.toList.flatMap(getRelocations)
}

private def getRelocation(dris: List[DRI], node: ContentPage): List[(DRI, String)] =
val link = locationProvider.resolve(node, rootPage, true)
dris.map( dri =>
if locationProvider.expectedLocationForDri(dri) != link then Some(dri, link) else None
).flatten
29 changes: 29 additions & 0 deletions scala3doc/src/dotty/dokka/model/api/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import collection.JavaConverters._
import org.jetbrains.dokka.model.doc._
import org.jetbrains.dokka.model.properties._
import org.jetbrains.dokka.pages._
import org.jetbrains.dokka.links._

enum Visibility(val name: String):
case Unrestricted extends Visibility("")
Expand Down Expand Up @@ -148,4 +149,32 @@ extension[T] (member: Member)
extension (module: DModule)
def driMap: Map[DRI, Member] = ModuleExtension.getFrom(module).fold(Map.empty)(_.driMap)

extension (dri: DRI):
def withNoOrigin = dri._copy(
extra = Option(dri.getExtra).fold(null)(e => raw"\[origin:(.*)\]".r.replaceAllIn(e, ""))
)

def location: String = dri.getPackageName

def anchor: Option[String] = Option(dri.getClassNames).filterNot(_.isEmpty)

def extra: String = dri.getExtra

def target: DriTarget = dri.getTarget

def _copy(
location: String = dri.location,
anchor: Option[String] = dri.anchor,
target: DriTarget = dri.target,
extra: String = dri.extra
) = new DRI(location, anchor.getOrElse(""), null, target, extra)

object DRI:
def apply(
location: String = "",
anchor: Option[String] = None,
target: DriTarget = PointingToDeclaration.INSTANCE,
extra: String = ""
) = new DRI(location, anchor.getOrElse(""), null, target, extra)

case class TastyDocumentableSource(val path: String, val lineNumber: Int)
Loading