aboutsummaryrefslogtreecommitdiff
path: root/doc-tool/src
diff options
context:
space:
mode:
authorFelix Mulder <felix.mulder@gmail.com>2016-11-03 16:20:28 +0100
committerGuillaume Martres <smarter@ubuntu.com>2016-11-22 01:35:07 +0100
commit94a8177f208418682b86934eb3f0b8edab99b1f0 (patch)
treeef2018006aee9af3f3afb9a92037ec605d835cd1 /doc-tool/src
parentfcdb1c984c4b32c0c1b13337d9bd50a3883cb3b3 (diff)
downloaddotty-94a8177f208418682b86934eb3f0b8edab99b1f0.tar.gz
dotty-94a8177f208418682b86934eb3f0b8edab99b1f0.tar.bz2
dotty-94a8177f208418682b86934eb3f0b8edab99b1f0.zip
Move `dottydoc` -> `doc-tool`
Diffstat (limited to 'doc-tool/src')
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/DottyDoc.scala84
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/api/java/Dottydoc.java63
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/api/scala/Dottydoc.scala49
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/AlternateConstructorsPhase.scala34
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/ContextDottydoc.scala23
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/DocASTPhase.scala171
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/DocImplicitsPhase.scala28
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/DocstringPhase.scala47
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/MiniPhaseTransform.scala208
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/SortMembersPhase.scala32
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/TypeLinkingPhases.scala116
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/UsecasePhase.scala31
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/comment/BodyEntities.scala94
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/comment/BodyParsers.scala82
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/comment/Comment.scala28
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/comment/CommentCleaner.scala25
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/comment/CommentParser.scala846
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/comment/CommentRegex.scala84
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/entities.scala119
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/factories.scala183
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/internal.scala97
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/java.scala223
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/json.scala93
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/references.scala20
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/util/MemberLookup.scala92
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/util/OutputWriter.scala125
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/util/Traversing.scala25
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/util/mutate.scala65
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/util/syntax.scala16
29 files changed, 3103 insertions, 0 deletions
diff --git a/doc-tool/src/dotty/tools/dottydoc/DottyDoc.scala b/doc-tool/src/dotty/tools/dottydoc/DottyDoc.scala
new file mode 100644
index 000000000..0dea96134
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/DottyDoc.scala
@@ -0,0 +1,84 @@
+package dotty.tools
+package dottydoc
+
+import dotty.tools.dottydoc.util.syntax._
+import core._
+import core.transform._
+import dotc.config.CompilerCommand
+import dotc.config.Printers.dottydoc
+import dotc.core.Contexts._
+import dotc.core.Comments.ContextDoc
+import dotc.core.Phases.Phase
+import dotc.typer.FrontEnd
+import dotc.{ CompilationUnit, Compiler, Driver, Run }
+import io.PlainFile
+import model.Package
+import model.json._
+
+import _root_.java.util.{ Map => JMap }
+
+/** Custom Compiler with phases for the documentation tool
+ *
+ * The idea here is to structure `dottydoc` around the new infrastructure. As
+ * such, dottydoc will itself be a compiler. It will, however, produce a format
+ * that can be used by other tools or web-browsers.
+ *
+ * Example:
+ * 1. Use the existing FrontEnd to typecheck the code being fed to dottydoc
+ * 2. Create an AST that is serializable
+ * 3. Serialize to JS object
+ */
+class DocCompiler extends Compiler {
+ override def phases: List[List[Phase]] = List(
+ List(new DocFrontEnd),
+ List(new DocImplicitsPhase),
+ List(new DocASTPhase),
+ List(DocMiniTransformations(new UsecasePhase,
+ new DocstringPhase,
+ new LinkReturnTypes,
+ new LinkParamListTypes,
+ new LinkImplicitlyAddedTypes,
+ new LinkSuperTypes,
+ new AlternateConstructors,
+ new SortMembers))
+ )
+}
+
+class DocFrontEnd extends FrontEnd {
+ override protected def discardAfterTyper(unit: CompilationUnit)(implicit ctx: Context) =
+ unit.isJava
+}
+
+abstract class DocDriver extends Driver {
+ import scala.collection.JavaConverters._
+
+ override def setup(args: Array[String], rootCtx: Context): (List[String], Context) = {
+ val ctx = rootCtx.fresh
+ val summary = CompilerCommand.distill(args)(ctx)
+
+ ctx.setSettings(summary.sstate)
+ ctx.setSetting(ctx.settings.YkeepComments, true)
+ ctx.setProperty(ContextDoc, new ContextDottydoc)
+
+ val fileNames = CompilerCommand.checkUsage(summary, sourcesRequired)(ctx)
+ (fileNames, ctx)
+ }
+
+ override def newCompiler(implicit ctx: Context): Compiler = new DocCompiler
+
+ def compiledDocs(args: Array[String]): collection.Map[String, Package] = {
+ val (fileNames, ctx) = setup(args, initCtx.fresh)
+ doCompile(newCompiler(ctx), fileNames)(ctx)
+
+ ctx.docbase.packages
+ }
+
+ def compiledDocsJava(args: Array[String]): JMap[String, Package] =
+ compiledDocs(args).asJava
+
+ def indexToJson(index: collection.Map[String, Package]): String =
+ index.json
+
+ def indexToJsonJava(index: JMap[String, Package]): String =
+ indexToJson(index.asScala)
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/api/java/Dottydoc.java b/doc-tool/src/dotty/tools/dottydoc/api/java/Dottydoc.java
new file mode 100644
index 000000000..1bdfe0488
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/api/java/Dottydoc.java
@@ -0,0 +1,63 @@
+package dotty.tools.dottydoc.api.java;
+
+import dotty.tools.dottydoc.DocDriver;
+import dotty.tools.dottydoc.model.Package;
+import dotty.tools.dottydoc.util.OutputWriter;
+import java.util.Map;
+import java.util.List;
+import java.net.URL;
+
+/**
+ * The Dottydoc API is fairly simple. The tool creates an index by calling:
+ * "createIndex" with the same argument list as you would the compiler - e.g:
+ *
+ * {{{
+ * String[] array = {
+ * "-language:Scala2"
+ * };
+ *
+ * Map<String, Package> index = createIndex(array);
+ * }}}
+ *
+ * Once the index has been generated, the tool can also build a documentation
+ * API given a Mustache template and a flat resources structure (i.e. absolute
+ * paths to each resource, which will be put in the same directory).
+ *
+ * {{{
+ * buildDocs("path/to/output/dir", templateURL, resources, index);
+ * }}}
+ *
+ * The tool can also generate JSON from the created index using "toJson(index)"
+ * or directly using "createJsonIndex"
+ */
+public class Dottydoc extends DocDriver {
+
+ /** Creates index from compiler arguments */
+ public Map<String, Package> createIndex(String[] args) {
+ return compiledDocsJava(args);
+ }
+
+ /** Creates JSON from compiler arguments */
+ public String createJsonIndex(String[] args) {
+ return indexToJsonJava(createIndex(args));
+ }
+
+ public String toJson(Map<String, Package> index) {
+ return indexToJsonJava(index);
+ }
+
+ /** Creates a documentation from the given parameters */
+ public void buildDocs(
+ String outputDir,
+ URL template,
+ List<URL> resources,
+ Map<String, Package> index
+ ) {
+ new OutputWriter().writeJava(index, outputDir, template, resources);
+ }
+
+ /** Writes JSON to an output directory as "index.json" */
+ public void writeJson(Map<String, Package> index, String outputDir) {
+ new OutputWriter().writeJsonJava(index, outputDir);
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/api/scala/Dottydoc.scala b/doc-tool/src/dotty/tools/dottydoc/api/scala/Dottydoc.scala
new file mode 100644
index 000000000..15db81a95
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/api/scala/Dottydoc.scala
@@ -0,0 +1,49 @@
+package dotty.tools.dottydoc.api.scala
+
+import dotty.tools.dottydoc.DocDriver
+import dotty.tools.dottydoc.model.Package
+import dotty.tools.dottydoc.util.OutputWriter
+
+import scala.collection.Map
+import java.net.URL
+
+/**
+ * The Dottydoc API is fairly simple. The tool creates an index by calling:
+ * "createIndex" with the same argument list as you would the compiler - e.g:
+ *
+ * {{{
+ * val array: Array[String] = Array(
+ * "-language:Scala2"
+ * )
+ *
+ * val index: Map[String, Package] = createIndex(array)
+ * }}}
+ *
+ * Once the index has been generated, the tool can also build a documentation
+ * API given a Mustache template and a flat resources structure (i.e. absolute
+ * paths to each resource, which will be put in the same directory).
+ *
+ * {{{
+ * buildDocs("path/to/output/dir", templateURL, resources, index)
+ * }}}
+ *
+ * The tool can also generate JSON from the created index using "indexToJson"
+ * or directly using "createJsonIndex"
+ */
+trait Dottydoc extends DocDriver {
+ /** Creates index from compiler arguments */
+ def createIndex(args: Array[String]): Map[String, Package] =
+ compiledDocs(args)
+
+ /** Creates JSON from compiler arguments */
+ def createJsonIndex(args: Array[String]): String =
+ indexToJson(compiledDocs(args))
+
+ /** Creates a documentation from the given parameters */
+ def buildDocs(outDir: String, template: URL, resources: List[URL], index: Map[String, Package]) =
+ new OutputWriter().write(index, outDir, template, resources)
+
+ /** Writes JSON to an output directory as "index.json" */
+ def writeJson(index: Map[String, Package], outputDir: String) =
+ new OutputWriter().writeJson(index, outputDir)
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/AlternateConstructorsPhase.scala b/doc-tool/src/dotty/tools/dottydoc/core/AlternateConstructorsPhase.scala
new file mode 100644
index 000000000..53c96fc87
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/core/AlternateConstructorsPhase.scala
@@ -0,0 +1,34 @@
+package dotty.tools
+package dottydoc
+package core
+
+import dotc.core.Contexts.Context
+
+import transform.DocMiniPhase
+import model._
+import model.internal._
+
+/** This DocMiniPhase adds the alternate constructors, currently defined as
+ * methods with the name `<init>`, to the Entity#constructors list
+ */
+class AlternateConstructors extends DocMiniPhase {
+ def partitionMembers(ent: Entity with Constructors with Members): (List[List[ParamList]], List[Entity]) = {
+ val (constructors, members) = ent.members.partition(x => x.name == "<init>")
+
+ val paramLists: List[List[ParamList]] = constructors.collect {
+ case df: Def => df.paramLists
+ }
+
+ (ent.constructors ++ paramLists, members)
+ }
+
+ override def transformClass(implicit ctx: Context) = { case cls: ClassImpl =>
+ val (constructors, members) = partitionMembers(cls)
+ cls.copy(members = members, constructors = constructors)
+ }
+
+ override def transformCaseClass(implicit ctx: Context) = { case cc: CaseClassImpl =>
+ val (constructors, members) = partitionMembers(cc)
+ cc.copy(members = members, constructors = constructors)
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/ContextDottydoc.scala b/doc-tool/src/dotty/tools/dottydoc/core/ContextDottydoc.scala
new file mode 100644
index 000000000..c60038836
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/core/ContextDottydoc.scala
@@ -0,0 +1,23 @@
+package dotty.tools
+package dottydoc
+package core
+
+import dotc.core.Symbols.Symbol
+import dotc.core.Comments.ContextDocstrings
+import model.Package
+
+class ContextDottydoc extends ContextDocstrings {
+ import scala.collection.mutable
+
+ private[this] val _packages: mutable.Map[String, Package] = mutable.Map.empty
+ def packages: Map[String, Package] = _packages.toMap
+ def packagesMutable: mutable.Map[String, Package] = _packages
+
+ /** Should perhaps factorize this into caches that get flushed */
+ private var _defs: Map[Symbol, Set[Symbol]] = Map.empty
+ def defs(sym: Symbol): Set[Symbol] = _defs.get(sym).getOrElse(Set.empty)
+
+ def addDef(s: Symbol, d: Symbol): Unit = _defs = (_defs + {
+ s -> _defs.get(s).map(xs => xs + d).getOrElse(Set(d))
+ })
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/DocASTPhase.scala b/doc-tool/src/dotty/tools/dottydoc/core/DocASTPhase.scala
new file mode 100644
index 000000000..806d9d0ae
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/core/DocASTPhase.scala
@@ -0,0 +1,171 @@
+package dotty.tools
+package dottydoc
+package core
+
+/** Dotty and Dottydoc imports */
+import dotc.ast.Trees._
+import dotc.CompilationUnit
+import dotc.config.Printers.dottydoc
+import dotc.core.Contexts.Context
+import dotc.core.Comments.ContextDocstrings
+import dotc.core.Phases.Phase
+import dotc.core.Symbols.{ Symbol, NoSymbol }
+
+class DocASTPhase extends Phase {
+ import model._
+ import model.factories._
+ import model.internal._
+ import model.comment.Comment
+ import dotty.tools.dotc.core.Flags
+ import dotty.tools.dotc.ast.tpd._
+ import dotty.tools.dottydoc.util.syntax._
+ import util.traversing._
+ import util.internal.setters._
+
+ def phaseName = "docphase"
+
+ /** Build documentation hierarchy from existing tree */
+ def collect(tree: Tree, prev: List[String] = Nil)(implicit ctx: Context): Entity = {
+ val implicitConversions = ctx.docbase.defs(tree.symbol)
+
+ def collectList(xs: List[Tree], ps: List[String]): List[Entity] =
+ xs.map(collect(_, ps)).filter(_ != NonEntity)
+
+ def collectEntityMembers(xs: List[Tree], ps: List[String]) =
+ collectList(xs, ps).asInstanceOf[List[Entity with Members]]
+
+ def collectMembers(tree: Tree, ps: List[String] = prev)(implicit ctx: Context): List[Entity] = {
+ val defs = (tree match {
+ case t: Template => collectList(t.body, ps)
+ case _ => Nil
+ })
+
+ defs ++ implicitConversions.flatMap(membersFromSymbol)
+ }
+
+ def membersFromSymbol(sym: Symbol): List[Entity] = {
+ val defs = sym.info.bounds.hi.membersBasedOnFlags(Flags.Method, Flags.Synthetic | Flags.Private)
+ .filterNot(_.symbol.owner.name.show == "Any")
+ .map { meth =>
+ DefImpl(
+ meth.symbol,
+ meth.symbol.name.show,
+ Nil,
+ path(meth.symbol),
+ returnType(meth.info),
+ typeParams(meth.symbol),
+ paramLists(meth.info),
+ implicitlyAddedFrom = Some(returnType(meth.symbol.owner.info))
+ )
+ }.toList
+
+ val vals = sym.info.fields.filterNot(_.symbol.is(Flags.Private | Flags.Synthetic)).map { value =>
+ ValImpl(
+ value.symbol,
+ value.symbol.name.show,
+ Nil, path(value.symbol),
+ returnType(value.info),
+ implicitlyAddedFrom = Some(returnType(value.symbol.owner.info))
+ )
+ }
+
+ defs ++ vals
+ }
+
+
+ tree match {
+ /** package */
+ case pd @ PackageDef(pid, st) =>
+ val newPath = prev :+ pid.name.toString
+ addEntity(PackageImpl(pd.symbol, newPath.mkString("."), collectEntityMembers(st, newPath), newPath))
+
+ /** trait */
+ case t @ TypeDef(n, rhs) if t.symbol.is(Flags.Trait) =>
+ val name = n.decode.toString
+ val newPath = prev :+ name
+ //TODO: should not `collectMember` from `rhs` - instead: get from symbol, will get inherited members as well
+ TraitImpl(t.symbol, name, collectMembers(rhs), flags(t), newPath, typeParams(t.symbol), traitParameters(t.symbol), superTypes(t))
+
+ /** objects, on the format "Object$" so drop the last letter */
+ case o @ TypeDef(n, rhs) if o.symbol.is(Flags.Module) =>
+ val name = n.decode.toString.dropRight(1)
+ //TODO: should not `collectMember` from `rhs` - instead: get from symbol, will get inherited members as well
+ ObjectImpl(o.symbol, name, collectMembers(rhs, prev :+ name), flags(o), prev :+ (name + "$"), superTypes(o))
+
+ /** class / case class */
+ case c @ TypeDef(n, rhs) if c.symbol.isClass =>
+ val name = n.decode.toString
+ val newPath = prev :+ name
+ //TODO: should not `collectMember` from `rhs` - instead: get from symbol, will get inherited members as well
+ (c.symbol, name, collectMembers(rhs), flags(c), newPath, typeParams(c.symbol), constructors(c.symbol), superTypes(c), None) match {
+ case x if c.symbol.is(Flags.CaseClass) => CaseClassImpl.tupled(x)
+ case x => ClassImpl.tupled(x)
+ }
+
+ /** def */
+ case d: DefDef =>
+ DefImpl(d.symbol, d.name.decode.toString, flags(d), path(d.symbol), returnType(d.tpt.tpe), typeParams(d.symbol), paramLists(d.symbol.info))
+
+ /** val */
+ case v: ValDef if !v.symbol.is(Flags.ModuleVal) =>
+ ValImpl(v.symbol, v.name.decode.toString, flags(v), path(v.symbol), returnType(v.tpt.tpe))
+
+ case x => {
+ //dottydoc.println(s"Found unwanted entity: $x (${x.pos},\n${x.show}")
+ NonEntity
+ }
+ }
+ }
+
+ var packages: Map[String, Package] = Map.empty
+
+ def addEntity(p: Package): Package = {
+ def mergedChildren(x1s: List[Entity], x2s: List[Entity]): List[Entity] = {
+ val (packs1, others1) = x1s.partition(_.kind == "package")
+ val (packs2, others2) = x2s.partition(_.kind == "package")
+
+ val others = others1 ::: others2
+ val packs = (packs1 ::: packs2).groupBy(_.path).map(_._2.head)
+
+ (others ++ packs).sortBy(_.name)
+ }
+
+ val path = p.path.mkString(".")
+ val newPack = packages.get(path).map {
+ case ex: PackageImpl =>
+ if (!ex.comment.isDefined) ex.comment = p.comment
+ ex.members = mergedChildren(ex.members, p.members)
+ ex
+ }.getOrElse(p)
+
+ packages = packages + (path -> newPack)
+ newPack
+ }
+
+ private[this] var totalRuns = 0
+ private[this] var currentRun = 0
+
+ override def run(implicit ctx: Context): Unit = {
+ currentRun += 1
+ println(s"Compiling ($currentRun/$totalRuns): ${ctx.compilationUnit.source.file.name}")
+ collect(ctx.compilationUnit.tpdTree) // Will put packages in `packages` var
+ }
+
+ override def runOn(units: List[CompilationUnit])(implicit ctx: Context): List[CompilationUnit] = {
+ // (1) Create package structure for all `units`, this will give us a complete structure
+ totalRuns = units.length
+ val compUnits = super.runOn(units)
+
+ // (2) Set parents of entities, needed for linking
+ for {
+ parent <- packages.values
+ child <- parent.children
+ } setParent(child, to = parent)
+
+ // (3) Update Doc AST in ctx.base
+ for (kv <- packages) ctx.docbase.packagesMutable += kv
+
+ // Return super's result
+ compUnits
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/DocImplicitsPhase.scala b/doc-tool/src/dotty/tools/dottydoc/core/DocImplicitsPhase.scala
new file mode 100644
index 000000000..6577f0cb7
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/core/DocImplicitsPhase.scala
@@ -0,0 +1,28 @@
+package dotty.tools
+package dottydoc
+package core
+
+import dotty.tools.dotc.transform.TreeTransforms.{ MiniPhaseTransform, TransformerInfo }
+import dotty.tools.dotc.core.Flags
+import dotc.core.Contexts.Context
+import util.syntax._
+
+class DocImplicitsPhase extends MiniPhaseTransform { thisTransformer =>
+ import dotty.tools.dotc.ast.tpd._
+
+ def phaseName = "addImplicitsPhase"
+
+ override def transformDefDef(tree: DefDef)(implicit ctx: Context, info: TransformerInfo): Tree = {
+ if (
+ tree.symbol.is(Flags.Implicit) && // has to have an implicit flag
+ tree.symbol.owner.isStaticOwner && // owner has to be static (e.g. top-level `object`)
+ tree.vparamss.length > 0 &&
+ tree.vparamss(0).length == 1 // should only take one arg, since it has to be a transformation
+ ) {
+ val convertee = tree.vparamss(0)(0).symbol.info.widenDealias.finalResultType.typeSymbol // the pimped type (i.e. `class`)
+ ctx.docbase.addDef(convertee, tree.symbol.info.widenDealias.finalResultType.typeSymbol)
+ }
+
+ tree
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/DocstringPhase.scala b/doc-tool/src/dotty/tools/dottydoc/core/DocstringPhase.scala
new file mode 100644
index 000000000..cff614528
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/core/DocstringPhase.scala
@@ -0,0 +1,47 @@
+package dotty.tools
+package dottydoc
+package core
+
+import dotc.core.Contexts.Context
+import transform.DocMiniPhase
+import model._
+import model.internal._
+import model.comment._
+import BodyParsers._
+import util.syntax._
+
+class DocstringPhase extends DocMiniPhase with CommentParser with CommentCleaner {
+ private def parsedComment[E <: Entity](ent: E)(implicit ctx: Context): Option[Comment] =
+ ctx.docbase.docstring(ent.symbol).map { cmt =>
+ parse(ent, ctx.docbase.packages, clean(cmt.raw), cmt.raw, cmt.pos)
+ .toComment(_.toHtml(ent))
+ }
+
+ override def transformPackage(implicit ctx: Context) = { case ent: PackageImpl =>
+ ent.copy(comment = parsedComment(ent))
+ }
+
+ override def transformClass(implicit ctx: Context) = { case ent: ClassImpl =>
+ ent.copy(comment = parsedComment(ent))
+ }
+
+ override def transformCaseClass(implicit ctx: Context) = { case ent: CaseClassImpl =>
+ ent.copy(comment = parsedComment(ent))
+ }
+
+ override def transformTrait(implicit ctx: Context) = { case ent: TraitImpl =>
+ ent.copy(comment = parsedComment(ent))
+ }
+
+ override def transformObject(implicit ctx: Context) = { case ent: ObjectImpl =>
+ ent.copy(comment = parsedComment(ent))
+ }
+
+ override def transformDef(implicit ctx: Context) = { case ent: DefImpl =>
+ ent.copy(comment = parsedComment(ent))
+ }
+
+ override def transformVal(implicit ctx: Context) = { case ent: ValImpl =>
+ ent.copy(comment = parsedComment(ent))
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/MiniPhaseTransform.scala b/doc-tool/src/dotty/tools/dottydoc/core/MiniPhaseTransform.scala
new file mode 100644
index 000000000..150a4e08f
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/core/MiniPhaseTransform.scala
@@ -0,0 +1,208 @@
+package dotty.tools
+package dottydoc
+package core
+
+import dotc.CompilationUnit
+import dotc.core.Contexts.Context
+import dotc.core.Comments.ContextDocstrings
+import dotc.core.Phases.Phase
+import model._
+import model.internal._
+import util.syntax._
+
+object transform {
+ /**
+ * The idea behind DocMiniTransformations is to fuse transformations to the
+ * doc AST, much like `MiniPhaseTransform` in dotty core - but in a much more
+ * simple implementation
+ *
+ * Usage
+ * -----
+ *
+ * Create a `DocMiniPhase` which overrides the relevant method:
+ *
+ * {{{
+ * override def transformDef(implicit ctx: Context) = {
+ * case x if shouldTransform(x) => x.copy(newValue = ...)
+ * }
+ * }}}
+ *
+ * On each node in the AST, the appropriate method in `DocMiniPhase` will be
+ * called in the order that they are supplied in
+ * `DocMiniphaseTransformations`.
+ *
+ * There won't be a match-error as `transformX` is composed with an
+ * `identity` function.
+ *
+ * The transformations in `DocMiniTransformations` will apply transformations
+ * to all nodes - this means that you do _not_ need to transform children in
+ * `transformPackage`, because `transformX` will be called for the relevant
+ * children. If you want to add children to `Package` you need to do that in
+ * `transformPackage`, these additions will be persisted.
+ */
+ abstract class DocMiniTransformations(transformations: List[DocMiniPhase]) extends Phase {
+
+ override def runOn(units: List[CompilationUnit])(implicit ctx: Context): List[CompilationUnit] = {
+ for {
+ rootName <- rootPackages
+ pack = ctx.docbase.packages(rootName)
+ transformed = performPackageTransform(pack)
+ } yield ctx.docbase.packagesMutable(rootName) = transformed
+ super.runOn(units)
+ }
+
+ private def rootPackages(implicit ctx: Context): List[String] = {
+ var currentDepth = Int.MaxValue
+ var packs = List.empty[String]
+
+ for (key <- ctx.docbase.packages.keys) {
+ val keyDepth = key.split("\\.").length
+ packs =
+ if (keyDepth < currentDepth) {
+ currentDepth = keyDepth
+ key :: Nil
+ } else if (keyDepth == currentDepth) {
+ key :: packs
+ } else packs
+ }
+ packs
+ }
+
+ private def performPackageTransform(pack: Package)(implicit ctx: Context): Package = {
+ def transformEntity[E <: Entity](e: E, f: DocMiniPhase => E => E)(createNew: E => E): E = {
+ val transformedEntity = transformations.foldLeft(e) { case (oldE, transf) =>
+ f(transf)(oldE)
+ }
+ createNew(transformedEntity)
+ }
+
+ def traverse(ent: Entity): Entity = ent match {
+ case p: Package => transformEntity(p, _.packageTransformation) { p =>
+ val newPackage = PackageImpl(
+ p.symbol,
+ p.name,
+ p.members.map(traverse),
+ p.path,
+ p.comment
+ )
+
+ // Update reference in context to newPackage
+ ctx.docbase.packagesMutable += (newPackage.path.mkString(".") -> newPackage)
+
+ newPackage
+ }
+ case c: Class => transformEntity(c, _.classTransformation) { cls =>
+ ClassImpl(
+ cls.symbol,
+ cls.name,
+ cls.members.map(traverse),
+ cls.modifiers,
+ cls.path,
+ cls.typeParams,
+ cls.constructors,
+ cls.superTypes,
+ cls.comment
+ )
+ }
+ case cc: CaseClass => transformEntity(cc, _.caseClassTransformation) { cc =>
+ CaseClassImpl(
+ cc.symbol,
+ cc.name,
+ cc.members.map(traverse),
+ cc.modifiers,
+ cc.path,
+ cc.typeParams,
+ cc.constructors,
+ cc.superTypes,
+ cc.comment
+ )
+ }
+ case trt: Trait => transformEntity(trt, _.traitTransformation) { trt =>
+ TraitImpl(
+ trt.symbol,
+ trt.name,
+ trt.members.map(traverse),
+ trt.modifiers,
+ trt.path,
+ trt.typeParams,
+ trt.traitParams,
+ trt.superTypes,
+ trt.comment
+ )
+ }
+ case obj: Object => transformEntity(obj, _.objectTransformation) { obj =>
+ ObjectImpl(
+ obj.symbol,
+ obj.name,
+ obj.members.map(traverse),
+ obj.modifiers,
+ obj.path,
+ obj.superTypes,
+ obj.comment
+ )
+ }
+ case df: Def => transformEntity(df, _.defTransformation) { df =>
+ DefImpl(
+ df.symbol,
+ df.name,
+ df.modifiers,
+ df.path,
+ df.returnValue,
+ df.typeParams,
+ df.paramLists,
+ df.comment,
+ df.implicitlyAddedFrom
+ )
+ }
+ case vl: Val => transformEntity(vl, _.valTransformation) { vl =>
+ ValImpl(
+ vl.symbol,
+ vl.name,
+ vl.modifiers,
+ vl.path,
+ vl.returnValue,
+ vl.comment,
+ vl.implicitlyAddedFrom
+ )
+ }
+ }
+
+ traverse(pack).asInstanceOf[Package]
+ }
+
+ override def run(implicit ctx: Context): Unit = ()
+ }
+
+ object DocMiniTransformations {
+ private var previousPhase = 0
+ def apply(transformations: DocMiniPhase*) =
+ new DocMiniTransformations(transformations.toList) {
+ val packages = Map.empty[String, Package]
+
+ def phaseName = s"MiniTransformation${ previousPhase += 1 }"
+ }
+ }
+
+ trait DocMiniPhase { phase =>
+ private def identity[E]: PartialFunction[E, E] = {
+ case id => id
+ }
+
+ // Partial functions instead????
+ def transformPackage(implicit ctx: Context): PartialFunction[Package, Package] = identity
+ def transformClass(implicit ctx: Context): PartialFunction[Class, Class] = identity
+ def transformCaseClass(implicit ctx: Context): PartialFunction[CaseClass, CaseClass] = identity
+ def transformTrait(implicit ctx: Context): PartialFunction[Trait, Trait] = identity
+ def transformObject(implicit ctx: Context): PartialFunction[Object, Object] = identity
+ def transformDef(implicit ctx: Context): PartialFunction[Def, Def] = identity
+ def transformVal(implicit ctx: Context): PartialFunction[Val, Val] = identity
+
+ private[transform] def packageTransformation(p: Package)(implicit ctx: Context) = (transformPackage orElse identity)(p)
+ private[transform] def classTransformation(cls: Class)(implicit ctx: Context) = (transformClass orElse identity)(cls)
+ private[transform] def caseClassTransformation(cc: CaseClass)(implicit ctx: Context) = (transformCaseClass orElse identity)(cc)
+ private[transform] def traitTransformation(trt: Trait)(implicit ctx: Context) = (transformTrait orElse identity)(trt)
+ private[transform] def objectTransformation(obj: Object)(implicit ctx: Context) = (transformObject orElse identity)(obj)
+ private[transform] def defTransformation(df: Def)(implicit ctx: Context) = (transformDef orElse identity)(df)
+ private[transform] def valTransformation(vl: Val)(implicit ctx: Context) = (transformVal orElse identity)(vl)
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/SortMembersPhase.scala b/doc-tool/src/dotty/tools/dottydoc/core/SortMembersPhase.scala
new file mode 100644
index 000000000..c8de532bb
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/core/SortMembersPhase.scala
@@ -0,0 +1,32 @@
+package dotty.tools
+package dottydoc
+package core
+
+import dotc.core.Contexts.Context
+
+import transform.DocMiniPhase
+import model._
+import model.internal._
+
+/** This DocMiniPhase sorts the members of all classes, traits, objects and packages */
+class SortMembers extends DocMiniPhase {
+ override def transformPackage(implicit ctx: Context) = { case p: PackageImpl =>
+ p.copy(members = p.members.sortBy(_.name))
+ }
+
+ override def transformClass(implicit ctx: Context) = { case c: ClassImpl =>
+ c.copy(members = c.members.sortBy(_.name))
+ }
+
+ override def transformCaseClass(implicit ctx: Context) = { case cc: CaseClassImpl =>
+ cc.copy(members = cc.members.sortBy(_.name))
+ }
+
+ override def transformTrait(implicit ctx: Context) = { case t: TraitImpl =>
+ t.copy(members = t.members.sortBy(_.name))
+ }
+
+ override def transformObject(implicit ctx: Context) = { case o: ObjectImpl =>
+ o.copy(members = o.members.sortBy(_.name))
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/TypeLinkingPhases.scala b/doc-tool/src/dotty/tools/dottydoc/core/TypeLinkingPhases.scala
new file mode 100644
index 000000000..1aecca9e1
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/core/TypeLinkingPhases.scala
@@ -0,0 +1,116 @@
+package dotty.tools
+package dottydoc
+package core
+
+import dotc.core.Contexts.Context
+import dotc.util.Positions.NoPosition
+
+import transform.DocMiniPhase
+import model._
+import model.internal._
+import model.comment._
+import model.references._
+import BodyParsers._
+import util.MemberLookup
+import util.traversing._
+import util.internal.setters._
+import util.syntax._
+
+class LinkReturnTypes extends DocMiniPhase with TypeLinker {
+ override def transformDef(implicit ctx: Context) = { case df: DefImpl =>
+ val returnValue = linkReference(df, df.returnValue, ctx.docbase.packages)
+ df.copy(returnValue = returnValue)
+ }
+
+ override def transformVal(implicit ctx: Context) = { case vl: ValImpl =>
+ val returnValue = linkReference(vl, vl.returnValue, ctx.docbase.packages)
+ vl.copy(returnValue = returnValue)
+ }
+}
+
+class LinkParamListTypes extends DocMiniPhase with TypeLinker {
+ override def transformDef(implicit ctx: Context) = { case df: DefImpl =>
+ val newParamLists = for {
+ ParamListImpl(list, isImplicit) <- df.paramLists
+ newList = list.map(linkReference(df, _, ctx.docbase.packages))
+ } yield ParamListImpl(newList.asInstanceOf[List[NamedReference]], isImplicit)
+
+ df.copy(paramLists = newParamLists)
+ }
+}
+
+class LinkSuperTypes extends DocMiniPhase with TypeLinker {
+ def linkSuperTypes(ent: Entity with SuperTypes)(implicit ctx: Context): List[MaterializableLink] =
+ ent.superTypes.collect {
+ case UnsetLink(title, query) =>
+ val packages = ctx.docbase.packages
+ val entityLink = makeEntityLink(ent, packages, Text(title), NoPosition, query).link
+ handleEntityLink(title, entityLink, ent)
+ }
+
+ override def transformClass(implicit ctx: Context) = { case cls: ClassImpl =>
+ cls.copy(superTypes = linkSuperTypes(cls))
+ }
+
+ override def transformCaseClass(implicit ctx: Context) = { case cc: CaseClassImpl =>
+ cc.copy(superTypes = linkSuperTypes(cc))
+ }
+
+ override def transformTrait(implicit ctx: Context) = { case trt: TraitImpl =>
+ trt.copy(superTypes = linkSuperTypes(trt))
+ }
+
+ override def transformObject(implicit ctx: Context) = { case obj: ObjectImpl =>
+ obj.copy(superTypes = linkSuperTypes(obj))
+ }
+}
+
+class LinkImplicitlyAddedTypes extends DocMiniPhase with TypeLinker {
+ override def transformDef(implicit ctx: Context) = {
+ case df: DefImpl if df.implicitlyAddedFrom.isDefined =>
+ val implicitlyAddedFrom = linkReference(df, df.implicitlyAddedFrom.get, ctx.docbase.packages)
+ df.copy(implicitlyAddedFrom = Some(implicitlyAddedFrom))
+ }
+
+ override def transformVal(implicit ctx: Context) = {
+ case vl: ValImpl if vl.implicitlyAddedFrom.isDefined =>
+ val implicitlyAddedFrom = linkReference(vl, vl.implicitlyAddedFrom.get, ctx.docbase.packages)
+ vl.copy(implicitlyAddedFrom = Some(implicitlyAddedFrom))
+ }
+}
+
+trait TypeLinker extends MemberLookup {
+ def handleEntityLink(title: String, lt: LinkTo, ent: Entity): MaterializableLink = lt match {
+ case Tooltip(str) => NoLink(title, str)
+ case LinkToExternal(_, url) => MaterializedLink(title, url)
+ case LinkToEntity(target) => MaterializedLink(title, util.traversing.relativePath(ent, target))
+ }
+
+ def linkReference(ent: Entity, ref: Reference, packs: Map[String, Package]): Reference = {
+ def linkRef(ref: Reference) = linkReference(ent, ref, packs)
+
+ ref match {
+ case ref @ TypeReference(_, UnsetLink(t, query), tps) =>
+ val inlineToHtml = InlineToHtml(ent)
+ val title = t
+
+ val target = handleEntityLink(title, makeEntityLink(ent, packs, Text(t), NoPosition, query).link, ent)
+ val tpTargets = tps.map(linkReference(ent, _, packs))
+ ref.copy(tpeLink = target, paramLinks = tpTargets)
+ case ref @ OrTypeReference(left, right) =>
+ ref.copy(left = linkReference(ent, left, packs), right = linkReference(ent, right, packs))
+ case ref @ AndTypeReference(left, right) =>
+ ref.copy(left = linkReference(ent, left, packs), right = linkReference(ent, right, packs))
+ case ref @ NamedReference(_, rf, _, _) =>
+ ref.copy(ref = linkRef(rf))
+ case ref @ FunctionReference(args, rv) =>
+ ref.copy(args = args.map(linkReference(ent, _, packs)), returnValue = linkReference(ent, rv, packs))
+ case ref @ TupleReference(args) =>
+ ref.copy(args = args.map(linkRef))
+ case ref @ BoundsReference(low, high) =>
+ ref.copy(low = linkRef(low), high = linkRef(high))
+ case _ =>
+ ref
+ }
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/UsecasePhase.scala b/doc-tool/src/dotty/tools/dottydoc/core/UsecasePhase.scala
new file mode 100644
index 000000000..47376eb26
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/core/UsecasePhase.scala
@@ -0,0 +1,31 @@
+package dotty.tools
+package dottydoc
+package core
+
+import dotc.core.Contexts.Context
+import dotc.ast.tpd
+
+import transform.DocMiniPhase
+import model.internal._
+import model.factories._
+import dotty.tools.dotc.core.Symbols.Symbol
+import util.syntax._
+
+class UsecasePhase extends DocMiniPhase {
+ private def defdefToDef(d: tpd.DefDef, sym: Symbol)(implicit ctx: Context) = {
+ val name = d.name.show.split("\\$").head // UseCase defs get $pos appended to their names
+ DefImpl(
+ sym,
+ name,
+ flags(d),
+ path(d.symbol).init :+ name,
+ returnType(d.tpt.tpe),
+ typeParams(d.symbol),
+ paramLists(d.symbol.info)
+ )
+ }
+
+ override def transformDef(implicit ctx: Context) = { case df: DefImpl =>
+ ctx.docbase.docstring(df.symbol).flatMap(_.usecases.headOption.map(_.tpdCode)).map(defdefToDef(_, df.symbol)).getOrElse(df)
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/comment/BodyEntities.scala b/doc-tool/src/dotty/tools/dottydoc/model/comment/BodyEntities.scala
new file mode 100644
index 000000000..29fe48de3
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/comment/BodyEntities.scala
@@ -0,0 +1,94 @@
+package dotty.tools.dottydoc
+package model
+package comment
+
+import scala.collection._
+
+/** A body of text. A comment has a single body, which is composed of
+ * at least one block. Inside every body is exactly one summary (see
+ * [[scala.tools.nsc.doc.model.comment.Summary]]). */
+final case class Body(blocks: Seq[Block]) {
+
+ /** The summary text of the comment body. */
+ lazy val summary: Option[Body] = {
+ def summaryInBlock(block: Block): Seq[Inline] = block match {
+ case Title(text, _) => summaryInInline(text)
+ case Paragraph(text) => summaryInInline(text)
+ case UnorderedList(items) => items flatMap summaryInBlock
+ case OrderedList(items, _) => items flatMap summaryInBlock
+ case DefinitionList(items) => items.values.toSeq flatMap summaryInBlock
+ case _ => Nil
+ }
+ def summaryInInline(text: Inline): Seq[Inline] = text match {
+ case Summary(text) => List(text)
+ case Chain(items) => items flatMap summaryInInline
+ case Italic(text) => summaryInInline(text)
+ case Bold(text) => summaryInInline(text)
+ case Underline(text) => summaryInInline(text)
+ case Superscript(text) => summaryInInline(text)
+ case Subscript(text) => summaryInInline(text)
+ case Link(_, title) => summaryInInline(title)
+ case _ => Nil
+ }
+ (blocks flatMap summaryInBlock).toList match {
+ case Nil => None
+ case inline :: Nil => Some(Body(Seq(Paragraph(inline))))
+ case inlines => Some(Body(Seq(Paragraph(Chain(inlines)))))
+ }
+ }
+}
+
+/** A block-level element of text, such as a paragraph or code block. */
+sealed abstract class Block
+
+final case class Title(text: Inline, level: Int) extends Block
+final case class Paragraph(text: Inline) extends Block
+final case class Code(data: String) extends Block
+final case class UnorderedList(items: Seq[Block]) extends Block
+final case class OrderedList(items: Seq[Block], style: String) extends Block
+final case class DefinitionList(items: SortedMap[Inline, Block]) extends Block
+final case class HorizontalRule() extends Block
+
+/** An section of text inside a block, possibly with formatting. */
+sealed abstract class Inline
+
+final case class Chain(items: Seq[Inline]) extends Inline
+final case class Italic(text: Inline) extends Inline
+final case class Bold(text: Inline) extends Inline
+final case class Underline(text: Inline) extends Inline
+final case class Superscript(text: Inline) extends Inline
+final case class Subscript(text: Inline) extends Inline
+final case class Link(target: String, title: Inline) extends Inline
+final case class Monospace(text: Inline) extends Inline
+final case class Text(text: String) extends Inline
+abstract class EntityLink(val title: Inline) extends Inline { def link: LinkTo }
+object EntityLink {
+ def apply(title: Inline, linkTo: LinkTo) = new EntityLink(title) { def link: LinkTo = linkTo }
+ def unapply(el: EntityLink): Option[(Inline, LinkTo)] = Some((el.title, el.link))
+}
+final case class HtmlTag(data: String) extends Inline {
+ private val Pattern = """(?ms)\A<(/?)(.*?)[\s>].*\z""".r
+ private val (isEnd, tagName) = data match {
+ case Pattern(s1, s2) =>
+ (! s1.isEmpty, Some(s2.toLowerCase))
+ case _ =>
+ (false, None)
+ }
+
+ def canClose(open: HtmlTag) = {
+ isEnd && tagName == open.tagName
+ }
+
+ private val TagsNotToClose = Set("br", "img")
+ def close = tagName collect { case name if !TagsNotToClose(name) => HtmlTag(s"</$name>") }
+}
+
+/** The summary of a comment, usually its first sentence. There must be exactly one summary per body. */
+final case class Summary(text: Inline) extends Inline
+
+sealed trait LinkTo
+final case class LinkToExternal(name: String, url: String) extends LinkTo
+final case class Tooltip(name: String) extends LinkTo
+
+/** Linking directly to entities is not picklable because of cyclic references */
+final case class LinkToEntity(entity: Entity) extends LinkTo
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/comment/BodyParsers.scala b/doc-tool/src/dotty/tools/dottydoc/model/comment/BodyParsers.scala
new file mode 100644
index 000000000..8c1fa8d49
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/comment/BodyParsers.scala
@@ -0,0 +1,82 @@
+package dotty.tools.dottydoc
+package model
+package comment
+
+object BodyParsers {
+
+ implicit class BodyToHtml(val body: Body) extends AnyVal {
+ def toHtml(origin: Entity): String = {
+ val inlineToHtml = InlineToHtml(origin)
+
+ def bodyToHtml(body: Body): String =
+ (body.blocks map blockToHtml).mkString
+
+ def blockToHtml(block: Block): String = block match {
+ case Title(in, 1) => s"<h1>${inlineToHtml(in)}</h1>"
+ case Title(in, 2) => s"<h2>${inlineToHtml(in)}</h2>"
+ case Title(in, 3) => s"<h3>${inlineToHtml(in)}</h3>"
+ case Title(in, _) => s"<h4>${inlineToHtml(in)}</h4>"
+ case Paragraph(in) => s"<p>${inlineToHtml(in)}</p>"
+ case Code(data) => s"""<pre><code class="scala">$data</code></pre>"""
+ case UnorderedList(items) =>
+ s"<ul>${listItemsToHtml(items)}</ul>"
+ case OrderedList(items, listStyle) =>
+ s"<ol class=${listStyle}>${listItemsToHtml(items)}</ol>"
+ case DefinitionList(items) =>
+ s"<dl>${items map { case (t, d) => s"<dt>${inlineToHtml(t)}</dt><dd>${blockToHtml(d)}</dd>" } }</dl>"
+ case HorizontalRule() =>
+ "<hr/>"
+ }
+
+ def listItemsToHtml(items: Seq[Block]) =
+ items.foldLeft(""){ (list, item) =>
+ item match {
+ case OrderedList(_, _) | UnorderedList(_) => // html requires sub ULs to be put into the last LI
+ list + s"<li>${blockToHtml(item)}</li>"
+ case Paragraph(inline) =>
+ list + s"<li>${inlineToHtml(inline)}</li>" // LIs are blocks, no need to use Ps
+ case block =>
+ list + s"<li>${blockToHtml(block)}</li>"
+ }
+ }
+
+ bodyToHtml(body)
+ }
+ }
+
+ case class InlineToHtml(origin: Entity) {
+ def apply(inline: Inline) = toHtml(inline)
+
+ def relativePath(target: Entity) =
+ util.traversing.relativePath(origin, target)
+
+ def toHtml(inline: Inline): String = inline match {
+ case Chain(items) => (items map toHtml).mkString
+ case Italic(in) => s"<i>${toHtml(in)}</i>"
+ case Bold(in) => s"<b>${toHtml(in)}</b>"
+ case Underline(in) => s"<u>${toHtml(in)}</u>"
+ case Superscript(in) => s"<sup>${toHtml(in)}</sup>"
+ case Subscript(in) => s"<sub>${toHtml(in) }</sub>"
+ case Link(raw, title) => s"""<a href=$raw target="_blank">${toHtml(title)}</a>"""
+ case Monospace(in) => s"<code>${toHtml(in)}</code>"
+ case Text(text) => text
+ case Summary(in) => toHtml(in)
+ case HtmlTag(tag) => tag
+ case EntityLink(target, link) => enityLinkToHtml(target, link)
+ }
+
+ def enityLinkToHtml(target: Inline, link: LinkTo) = link match {
+ case Tooltip(_) => toHtml(target)
+ case LinkToExternal(n, url) => s"""<a href="$url">$n</a>"""
+ case LinkToEntity(t: Entity) => t match {
+ // Entity is a package member
+ case e: Entity with Members =>
+ s"""<a href="${relativePath(t)}">${toHtml(target)}</a>"""
+ // Entity is a Val / Def
+ case x => x.parent.fold(toHtml(target)) { xpar =>
+ s"""<a href="${relativePath(xpar)}#${x.name}">${toHtml(target)}</a>"""
+ }
+ }
+ }
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/comment/Comment.scala b/doc-tool/src/dotty/tools/dottydoc/model/comment/Comment.scala
new file mode 100644
index 000000000..c4f6ccf5d
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/comment/Comment.scala
@@ -0,0 +1,28 @@
+package dotty.tools
+package dottydoc
+package model
+package comment
+
+case class Comment (
+ body: String,
+ short: String,
+ authors: List[String],
+ see: List[String],
+ result: Option[String],
+ throws: Map[String, String],
+ valueParams: Map[String, String],
+ typeParams: Map[String, String],
+ version: Option[String],
+ since: Option[String],
+ todo: List[String],
+ deprecated: Option[String],
+ note: List[String],
+ example: List[String],
+ constructor: Option[String],
+ group: Option[String],
+ groupDesc: Map[String, String],
+ groupNames: Map[String, String],
+ groupPrio: Map[String, String],
+ /** List of conversions to hide - containing e.g: `scala.Predef.FloatArrayOps` */
+ hideImplicitConversions: List[String]
+)
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentCleaner.scala b/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentCleaner.scala
new file mode 100644
index 000000000..27b0ff977
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentCleaner.scala
@@ -0,0 +1,25 @@
+package dotty.tools.dottydoc
+package model
+package comment
+
+trait CommentCleaner {
+ import Regexes._
+
+ def clean(comment: String): List[String] = {
+ def cleanLine(line: String): String = {
+ // Remove trailing whitespaces
+ TrailingWhitespace.replaceAllIn(line, "") match {
+ case CleanCommentLine(ctl) => ctl
+ case tl => tl
+ }
+ }
+ val strippedComment = comment.trim.stripPrefix("/*").stripSuffix("*/")
+ val safeComment = DangerousTags.replaceAllIn(strippedComment, { htmlReplacement(_) })
+ val javadoclessComment = JavadocTags.replaceAllIn(safeComment, { javadocReplacement(_) })
+ val markedTagComment =
+ SafeTags.replaceAllIn(javadoclessComment, { mtch =>
+ _root_.java.util.regex.Matcher.quoteReplacement(safeTagMarker + mtch.matched + safeTagMarker)
+ })
+ markedTagComment.lines.toList map (cleanLine(_))
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentParser.scala b/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentParser.scala
new file mode 100644
index 000000000..9685b6934
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentParser.scala
@@ -0,0 +1,846 @@
+package dotty.tools.dottydoc
+package model
+package comment
+
+import dotty.tools.dotc.util.Positions._
+import dotty.tools.dotc.core.Symbols._
+import dotty.tools.dotc.core.Contexts.Context
+import scala.collection.mutable
+import dotty.tools.dotc.config.Printers.dottydoc
+import scala.util.matching.Regex
+
+trait CommentParser extends util.MemberLookup {
+ import Regexes._
+ import model.internal._
+
+ case class FullComment (
+ body: Body,
+ authors: List[Body],
+ see: List[Body],
+ result: Option[Body],
+ throws: Map[String, Body],
+ valueParams: Map[String, Body],
+ typeParams: Map[String, Body],
+ version: Option[Body],
+ since: Option[Body],
+ todo: List[Body],
+ deprecated: Option[Body],
+ note: List[Body],
+ example: List[Body],
+ constructor: Option[Body],
+ group: Option[Body],
+ groupDesc: Map[String, Body],
+ groupNames: Map[String, Body],
+ groupPrio: Map[String, Body],
+ hideImplicitConversions: List[Body],
+ shortDescription: List[Body]
+ ) {
+
+ /**
+ * Transform this CommentParser.FullComment to a Comment using the supplied
+ * Body transformer
+ */
+ def toComment(transform: Body => String) = Comment(
+ transform(body),
+ short =
+ if (shortDescription.nonEmpty) shortDescription.map(transform).mkString
+ else body.summary.map(transform).getOrElse(""),
+ authors.map(transform),
+ see.map(transform),
+ result.map(transform),
+ throws.map { case (k, v) => (k, transform(v)) },
+ valueParams.map { case (k, v) => (k, transform(v)) },
+ typeParams.map { case (k, v) => (k, transform(v)) },
+ version.map(transform),
+ since.map(transform),
+ todo.map(transform),
+ deprecated.map(transform),
+ note.map(transform),
+ example.map(transform),
+ constructor.map(transform),
+ group.map(transform),
+ groupDesc.map { case (k, v) => (k, transform(v)) },
+ groupNames.map { case (k, v) => (k, transform(v)) },
+ groupPrio.map { case (k, v) => (k, transform(v)) },
+ hideImplicitConversions.map(transform)
+ )
+ }
+
+ /** Parses a raw comment string into a `Comment` object.
+ * @param packages all packages parsed by Scaladoc tool, used for lookup
+ * @param cleanComment a cleaned comment to be parsed
+ * @param src the raw comment source string.
+ * @param pos the position of the comment in source.
+ */
+ def parse(
+ entity: Entity,
+ packages: Map[String, Package],
+ comment: List[String],
+ src: String,
+ pos: Position,
+ site: Symbol = NoSymbol
+ )(implicit ctx: Context): FullComment = {
+
+ /** Parses a comment (in the form of a list of lines) to a `Comment`
+ * instance, recursively on lines. To do so, it splits the whole comment
+ * into main body and tag bodies, then runs the `WikiParser` on each body
+ * before creating the comment instance.
+ *
+ * @param docBody The body of the comment parsed until now.
+ * @param tags All tags parsed until now.
+ * @param lastTagKey The last parsed tag, or `None` if the tag section
+ * hasn't started. Lines that are not tagged are part
+ * of the previous tag or, if none exists, of the body.
+ * @param remaining The lines that must still recursively be parsed.
+ * @param inCodeBlock Whether the next line is part of a code block (in
+ * which no tags must be read).
+ */
+ def parseComment (
+ docBody: StringBuilder,
+ tags: Map[TagKey, List[String]],
+ lastTagKey: Option[TagKey],
+ remaining: List[String],
+ inCodeBlock: Boolean
+ ): FullComment = remaining match {
+
+ case CodeBlockStartRegex(before, marker, after) :: ls if (!inCodeBlock) =>
+ if (!before.trim.isEmpty && !after.trim.isEmpty)
+ parseComment(docBody, tags, lastTagKey, before :: marker :: after :: ls, inCodeBlock = false)
+ else if (!before.trim.isEmpty)
+ parseComment(docBody, tags, lastTagKey, before :: marker :: ls, inCodeBlock = false)
+ else if (!after.trim.isEmpty)
+ parseComment(docBody, tags, lastTagKey, marker :: after :: ls, inCodeBlock = true)
+ else lastTagKey match {
+ case Some(key) =>
+ val value =
+ ((tags get key): @unchecked) match {
+ case Some(b :: bs) => (b + endOfLine + marker) :: bs
+ case None => oops("lastTagKey set when no tag exists for key")
+ }
+ parseComment(docBody, tags + (key -> value), lastTagKey, ls, inCodeBlock = true)
+ case None =>
+ parseComment(docBody append endOfLine append marker, tags, lastTagKey, ls, inCodeBlock = true)
+ }
+
+ case CodeBlockEndRegex(before, marker, after) :: ls => {
+ if (!before.trim.isEmpty && !after.trim.isEmpty)
+ parseComment(docBody, tags, lastTagKey, before :: marker :: after :: ls, inCodeBlock = true)
+ if (!before.trim.isEmpty)
+ parseComment(docBody, tags, lastTagKey, before :: marker :: ls, inCodeBlock = true)
+ else if (!after.trim.isEmpty)
+ parseComment(docBody, tags, lastTagKey, marker :: after :: ls, inCodeBlock = false)
+ else lastTagKey match {
+ case Some(key) =>
+ val value =
+ ((tags get key): @unchecked) match {
+ case Some(b :: bs) => (b + endOfLine + marker) :: bs
+ case None => oops("lastTagKey set when no tag exists for key")
+ }
+ parseComment(docBody, tags + (key -> value), lastTagKey, ls, inCodeBlock = false)
+ case None =>
+ parseComment(docBody append endOfLine append marker, tags, lastTagKey, ls, inCodeBlock = false)
+ }
+ }
+
+ case SymbolTagRegex(name, sym, body) :: ls if (!inCodeBlock) => {
+ val key = SymbolTagKey(name, sym)
+ val value = body :: tags.getOrElse(key, Nil)
+ parseComment(docBody, tags + (key -> value), Some(key), ls, inCodeBlock)
+ }
+
+ case SimpleTagRegex(name, body) :: ls if (!inCodeBlock) => {
+ val key = SimpleTagKey(name)
+ val value = body :: tags.getOrElse(key, Nil)
+ parseComment(docBody, tags + (key -> value), Some(key), ls, inCodeBlock)
+ }
+
+ case SingleTagRegex(name) :: ls if (!inCodeBlock) => {
+ val key = SimpleTagKey(name)
+ val value = "" :: tags.getOrElse(key, Nil)
+ parseComment(docBody, tags + (key -> value), Some(key), ls, inCodeBlock)
+ }
+
+ case line :: ls if (lastTagKey.isDefined) => {
+ val newtags = if (!line.isEmpty) {
+ val key = lastTagKey.get
+ val value =
+ ((tags get key): @unchecked) match {
+ case Some(b :: bs) => (b + endOfLine + line) :: bs
+ case None => oops("lastTagKey set when no tag exists for key")
+ }
+ tags + (key -> value)
+ } else tags
+ parseComment(docBody, newtags, lastTagKey, ls, inCodeBlock)
+ }
+
+ case line :: ls => {
+ if (docBody.length > 0) docBody append endOfLine
+ docBody append line
+ parseComment(docBody, tags, lastTagKey, ls, inCodeBlock)
+ }
+
+ case Nil => {
+ // Take the {inheritance, content} diagram keys aside, as it doesn't need any parsing
+ val inheritDiagramTag = SimpleTagKey("inheritanceDiagram")
+ val contentDiagramTag = SimpleTagKey("contentDiagram")
+
+ val inheritDiagramText: List[String] = tags.get(inheritDiagramTag) match {
+ case Some(list) => list
+ case None => List.empty
+ }
+
+ val contentDiagramText: List[String] = tags.get(contentDiagramTag) match {
+ case Some(list) => list
+ case None => List.empty
+ }
+
+ val stripTags=List(inheritDiagramTag, contentDiagramTag, SimpleTagKey("template"), SimpleTagKey("documentable"))
+ val tagsWithoutDiagram = tags.filterNot(pair => stripTags.contains(pair._1))
+
+ val bodyTags: mutable.Map[TagKey, List[Body]] =
+ mutable.Map((tagsWithoutDiagram mapValues {tag => tag map (parseWikiAtSymbol(entity, packages, _, pos, site))}).toSeq: _*)
+
+ def oneTag(key: SimpleTagKey, filterEmpty: Boolean = true): Option[Body] =
+ ((bodyTags remove key): @unchecked) match {
+ case Some(r :: rs) if !(filterEmpty && r.blocks.isEmpty) =>
+ if (!rs.isEmpty) dottydoc.println(s"$pos: only one '@${key.name}' tag is allowed")
+ Some(r)
+ case _ => None
+ }
+
+ def allTags[B](key: SimpleTagKey): List[Body] =
+ (bodyTags remove key).getOrElse(Nil).filterNot(_.blocks.isEmpty).reverse
+
+ def allSymsOneTag(key: TagKey, filterEmpty: Boolean = true): Map[String, Body] = {
+ val keys: Seq[SymbolTagKey] =
+ bodyTags.keys.toSeq flatMap {
+ case stk: SymbolTagKey if (stk.name == key.name) => Some(stk)
+ case stk: SimpleTagKey if (stk.name == key.name) =>
+ dottydoc.println(s"$pos: tag '@${stk.name}' must be followed by a symbol name")
+ None
+ case _ => None
+ }
+ val pairs: Seq[(String, Body)] =
+ for (key <- keys) yield {
+ val bs = (bodyTags remove key).get
+ if (bs.length > 1)
+ dottydoc.println(s"$pos: only one '@${key.name}' tag for symbol ${key.symbol} is allowed")
+ (key.symbol, bs.head)
+ }
+ Map.empty[String, Body] ++ (if (filterEmpty) pairs.filterNot(_._2.blocks.isEmpty) else pairs)
+ }
+
+ def linkedExceptions: Map[String, Body] = {
+ val m = allSymsOneTag(SimpleTagKey("throws"), filterEmpty = false)
+
+ m.map { case (targetStr,body) =>
+ val link = lookup(entity, packages, targetStr, pos)
+ val newBody = body match {
+ case Body(List(Paragraph(Chain(content)))) =>
+ val descr = Text(" ") +: content
+ val entityLink = EntityLink(Monospace(Text(targetStr)), link)
+ Body(List(Paragraph(Chain(entityLink +: descr))))
+ case _ => body
+ }
+ (targetStr, newBody)
+ }
+ }
+
+ val cmt = FullComment(
+ body = parseWikiAtSymbol(entity, packages, docBody.toString, pos, site),
+ authors = allTags(SimpleTagKey("author")),
+ see = allTags(SimpleTagKey("see")),
+ result = oneTag(SimpleTagKey("return")),
+ throws = linkedExceptions,
+ valueParams = allSymsOneTag(SimpleTagKey("param")),
+ typeParams = allSymsOneTag(SimpleTagKey("tparam")),
+ version = oneTag(SimpleTagKey("version")),
+ since = oneTag(SimpleTagKey("since")),
+ todo = allTags(SimpleTagKey("todo")),
+ deprecated = oneTag(SimpleTagKey("deprecated"), filterEmpty = false),
+ note = allTags(SimpleTagKey("note")),
+ example = allTags(SimpleTagKey("example")),
+ constructor = oneTag(SimpleTagKey("constructor")),
+ group = oneTag(SimpleTagKey("group")),
+ groupDesc = allSymsOneTag(SimpleTagKey("groupdesc")),
+ groupNames = allSymsOneTag(SimpleTagKey("groupname")),
+ groupPrio = allSymsOneTag(SimpleTagKey("groupprio")),
+ hideImplicitConversions = allTags(SimpleTagKey("hideImplicitConversion")),
+ shortDescription = allTags(SimpleTagKey("shortDescription"))
+ )
+
+ for ((key, _) <- bodyTags)
+ dottydoc.println(s"$pos: Tag '@${key.name}' is not recognised")
+
+ cmt
+ }
+ }
+
+ parseComment(new StringBuilder(comment.size), Map.empty, None, comment, inCodeBlock = false)
+ }
+
+ /** A key used for a tag map. The key is built from the name of the tag and
+ * from the linked symbol if the tag has one.
+ * Equality on tag keys is structural. */
+ private sealed abstract class TagKey {
+ def name: String
+ }
+
+ private final case class SimpleTagKey(name: String) extends TagKey
+ private final case class SymbolTagKey(name: String, symbol: String) extends TagKey
+
+ /** Something that should not have happened, happened, and Scaladoc should exit. */
+ private def oops(msg: String): Nothing =
+ throw new IllegalArgumentException("program logic: " + msg)
+
+ /** Parses a string containing wiki syntax into a `Comment` object.
+ * Note that the string is assumed to be clean:
+ * - Removed Scaladoc start and end markers.
+ * - Removed start-of-line star and one whitespace afterwards (if present).
+ * - Removed all end-of-line whitespace.
+ * - Only `endOfLine` is used to mark line endings. */
+ def parseWikiAtSymbol(
+ entity: Entity,
+ packages: Map[String, Package],
+ string: String,
+ pos: Position,
+ site: Symbol
+ )(implicit ctx: Context): Body = new WikiParser(entity, packages, string, pos, site).document()
+
+ /** Original wikiparser from NSC
+ * @author Ingo Maier
+ * @author Manohar Jonnalagedda
+ * @author Gilles Dubochet
+ */
+ protected final class WikiParser(
+ entity: Entity,
+ packages: Map[String, Package],
+ val buffer: String,
+ pos: Position,
+ site: Symbol
+ )(implicit ctx: Context) extends CharReader(buffer) { wiki =>
+ var summaryParsed = false
+
+ def document(): Body = {
+ val blocks = new mutable.ListBuffer[Block]
+ while (char != endOfText)
+ blocks += block()
+ Body(blocks.toList)
+ }
+
+ /* BLOCKS */
+
+ /** {{{ block ::= code | title | hrule | listBlock | para }}} */
+ def block(): Block = {
+ if (checkSkipInitWhitespace("{{{"))
+ code()
+ else if (checkSkipInitWhitespace('='))
+ title()
+ else if (checkSkipInitWhitespace("----"))
+ hrule()
+ else if (checkList)
+ listBlock
+ else {
+ para()
+ }
+ }
+
+ /** listStyle ::= '-' spc | '1.' spc | 'I.' spc | 'i.' spc | 'A.' spc | 'a.' spc
+ * Characters used to build lists and their constructors */
+ protected val listStyles = Map[String, (Seq[Block] => Block)](
+ "- " -> ( UnorderedList(_) ),
+ "1. " -> ( OrderedList(_,"decimal") ),
+ "I. " -> ( OrderedList(_,"upperRoman") ),
+ "i. " -> ( OrderedList(_,"lowerRoman") ),
+ "A. " -> ( OrderedList(_,"upperAlpha") ),
+ "a. " -> ( OrderedList(_,"lowerAlpha") )
+ )
+
+ /** Checks if the current line is formed with more than one space and one the listStyles */
+ def checkList =
+ (countWhitespace > 0) && (listStyles.keys exists { checkSkipInitWhitespace(_) })
+
+ /** {{{
+ * nListBlock ::= nLine { mListBlock }
+ * nLine ::= nSpc listStyle para '\n'
+ * }}}
+ * Where n and m stand for the number of spaces. When `m > n`, a new list is nested. */
+ def listBlock(): Block = {
+
+ /** Consumes one list item block and returns it, or None if the block is
+ * not a list or a different list. */
+ def listLine(indent: Int, style: String): Option[Block] =
+ if (countWhitespace > indent && checkList)
+ Some(listBlock)
+ else if (countWhitespace != indent || !checkSkipInitWhitespace(style))
+ None
+ else {
+ jumpWhitespace()
+ jump(style)
+ val p = Paragraph(inline(isInlineEnd = false))
+ blockEnded("end of list line ")
+ Some(p)
+ }
+
+ /** Consumes all list item blocks (possibly with nested lists) of the
+ * same list and returns the list block. */
+ def listLevel(indent: Int, style: String): Block = {
+ val lines = mutable.ListBuffer.empty[Block]
+ var line: Option[Block] = listLine(indent, style)
+ while (line.isDefined) {
+ lines += line.get
+ line = listLine(indent, style)
+ }
+ val constructor = listStyles(style)
+ constructor(lines)
+ }
+
+ val indent = countWhitespace
+ val style = (listStyles.keys find { checkSkipInitWhitespace(_) }).getOrElse(listStyles.keys.head)
+ listLevel(indent, style)
+ }
+
+ def code(): Block = {
+ jumpWhitespace()
+ jump("{{{")
+ val str = readUntil("}}}")
+ if (char == endOfText)
+ reportError(pos, "unclosed code block")
+ else
+ jump("}}}")
+ blockEnded("code block")
+ Code(normalizeIndentation(str))
+ }
+
+ /** {{{ title ::= ('=' inline '=' | "==" inline "==" | ...) '\n' }}} */
+ def title(): Block = {
+ jumpWhitespace()
+ val inLevel = repeatJump('=')
+ val text = inline(check("=" * inLevel))
+ val outLevel = repeatJump('=', inLevel)
+ if (inLevel != outLevel)
+ reportError(pos, "unbalanced or unclosed heading")
+ blockEnded("heading")
+ Title(text, inLevel)
+ }
+
+ /** {{{ hrule ::= "----" { '-' } '\n' }}} */
+ def hrule(): Block = {
+ jumpWhitespace()
+ repeatJump('-')
+ blockEnded("horizontal rule")
+ HorizontalRule()
+ }
+
+ /** {{{ para ::= inline '\n' }}} */
+ def para(): Block = {
+ val p =
+ if (summaryParsed)
+ Paragraph(inline(isInlineEnd = false))
+ else {
+ val s = summary()
+ val r =
+ if (checkParaEnded()) List(s) else List(s, inline(isInlineEnd = false))
+ summaryParsed = true
+ Paragraph(Chain(r))
+ }
+ while (char == endOfLine && char != endOfText)
+ nextChar()
+ p
+ }
+
+ /* INLINES */
+
+ val OPEN_TAG = "^<([A-Za-z]+)( [^>]*)?(/?)>$".r
+ val CLOSE_TAG = "^</([A-Za-z]+)>$".r
+ private def readHTMLFrom(begin: HtmlTag): String = {
+ val list = mutable.ListBuffer.empty[String]
+ val stack = mutable.ListBuffer.empty[String]
+
+ begin.close match {
+ case Some(HtmlTag(CLOSE_TAG(s))) =>
+ stack += s
+ case _ =>
+ return ""
+ }
+
+ do {
+ val str = readUntil { char == safeTagMarker || char == endOfText }
+ nextChar()
+
+ list += str
+
+ str match {
+ case OPEN_TAG(s, _, standalone) => {
+ if (standalone != "/") {
+ stack += s
+ }
+ }
+ case CLOSE_TAG(s) => {
+ if (s == stack.last) {
+ stack.remove(stack.length-1)
+ }
+ }
+ case _ => ;
+ }
+ } while (stack.length > 0 && char != endOfText)
+
+ list mkString ""
+ }
+
+ def inline(isInlineEnd: => Boolean): Inline = {
+
+ def inline0(): Inline = {
+ if (char == safeTagMarker) {
+ val tag = htmlTag()
+ HtmlTag(tag.data + readHTMLFrom(tag))
+ }
+ else if (check("'''")) bold()
+ else if (check("''")) italic()
+ else if (check("`")) monospace()
+ else if (check("__")) underline()
+ else if (check("^")) superscript()
+ else if (check(",,")) subscript()
+ else if (check("[[")) link()
+ else {
+ val str = readUntil {
+ char == safeTagMarker ||
+ check("''") ||
+ char == '`' ||
+ check("__") ||
+ char == '^' ||
+ check(",,") ||
+ check("[[") ||
+ isInlineEnd ||
+ checkParaEnded ||
+ char == endOfLine
+ }
+ Text(str)
+ }
+ }
+
+ val inlines: List[Inline] = {
+ val iss = mutable.ListBuffer.empty[Inline]
+ iss += inline0()
+ while (!isInlineEnd && !checkParaEnded) {
+ val skipEndOfLine = if (char == endOfLine) {
+ nextChar()
+ true
+ } else {
+ false
+ }
+
+ val current = inline0()
+ (iss.last, current) match {
+ case (Text(t1), Text(t2)) if skipEndOfLine =>
+ iss.update(iss.length - 1, Text(t1 + endOfLine + t2))
+ case (i1, i2) if skipEndOfLine =>
+ iss ++= List(Text(endOfLine.toString), i2)
+ case _ => iss += current
+ }
+ }
+ iss.toList
+ }
+
+ inlines match {
+ case Nil => Text("")
+ case i :: Nil => i
+ case is => Chain(is)
+ }
+
+ }
+
+ def htmlTag(): HtmlTag = {
+ jump(safeTagMarker)
+ val read = readUntil(safeTagMarker)
+ if (char != endOfText) jump(safeTagMarker)
+ HtmlTag(read)
+ }
+
+ def bold(): Inline = {
+ jump("'''")
+ val i = inline(check("'''"))
+ jump("'''")
+ Bold(i)
+ }
+
+ def italic(): Inline = {
+ jump("''")
+ val i = inline(check("''"))
+ jump("''")
+ Italic(i)
+ }
+
+ def monospace(): Inline = {
+ jump("`")
+ val i = inline(check("`"))
+ jump("`")
+ Monospace(i)
+ }
+
+ def underline(): Inline = {
+ jump("__")
+ val i = inline(check("__"))
+ jump("__")
+ Underline(i)
+ }
+
+ def superscript(): Inline = {
+ jump("^")
+ val i = inline(check("^"))
+ if (jump("^")) {
+ Superscript(i)
+ } else {
+ Chain(Seq(Text("^"), i))
+ }
+ }
+
+ def subscript(): Inline = {
+ jump(",,")
+ val i = inline(check(",,"))
+ jump(",,")
+ Subscript(i)
+ }
+
+ def summary(): Inline = {
+ val i = inline(checkSentenceEnded())
+ Summary(
+ if (jump("."))
+ Chain(List(i, Text(".")))
+ else
+ i
+ )
+ }
+
+ def link(): Inline = {
+ val SchemeUri = """([a-z]+:.*)""".r
+ jump("[[")
+ val parens = 2 + repeatJump('[')
+ val stop = "]" * parens
+ val target = readUntil { check(stop) || isWhitespaceOrNewLine(char) }
+ val title =
+ if (!check(stop)) Some({
+ jumpWhitespaceOrNewLine()
+ inline(check(stop))
+ })
+ else None
+ jump(stop)
+
+ (target, title) match {
+ case (SchemeUri(uri), optTitle) =>
+ Link(uri, optTitle getOrElse Text(uri))
+ case (qualName, optTitle) =>
+ makeEntityLink(entity, packages, optTitle getOrElse Text(target), pos, target)
+ }
+ }
+
+ /* UTILITY */
+
+ /** {{{ eol ::= { whitespace } '\n' }}} */
+ def blockEnded(blockType: String): Unit = {
+ if (char != endOfLine && char != endOfText) {
+ reportError(pos, "no additional content on same line after " + blockType)
+ jumpUntil(endOfLine)
+ }
+ while (char == endOfLine)
+ nextChar()
+ }
+
+ /**
+ * Eliminates the (common) leading spaces in all lines, based on the first line
+ * For indented pieces of code, it reduces the indent to the least whitespace prefix:
+ * {{{
+ * indented example
+ * another indented line
+ * if (condition)
+ * then do something;
+ * ^ this is the least whitespace prefix
+ * }}}
+ */
+ def normalizeIndentation(_code: String): String = {
+
+ val code = _code.replaceAll("\\s+$", "").dropWhile(_ == '\n') // right-trim + remove all leading '\n'
+ val lines = code.split("\n")
+
+ // maxSkip - size of the longest common whitespace prefix of non-empty lines
+ val nonEmptyLines = lines.filter(_.trim.nonEmpty)
+ val maxSkip = if (nonEmptyLines.isEmpty) 0 else nonEmptyLines.map(line => line.prefixLength(_ == ' ')).min
+
+ // remove common whitespace prefix
+ lines.map(line => if (line.trim.nonEmpty) line.substring(maxSkip) else line).mkString("\n")
+ }
+
+ def checkParaEnded(): Boolean = {
+ (char == endOfText) ||
+ ((char == endOfLine) && {
+ val poff = offset
+ nextChar() // read EOL
+ val ok = {
+ checkSkipInitWhitespace(endOfLine) ||
+ checkSkipInitWhitespace('=') ||
+ checkSkipInitWhitespace("{{{") ||
+ checkList ||
+ checkSkipInitWhitespace('\u003D')
+ }
+ offset = poff
+ ok
+ })
+ }
+
+ def checkSentenceEnded(): Boolean = {
+ (char == '.') && {
+ val poff = offset
+ nextChar() // read '.'
+ val ok = char == endOfText || char == endOfLine || isWhitespace(char)
+ offset = poff
+ ok
+ }
+ }
+
+ def reportError(pos: Position, message: String) =
+ dottydoc.println(s"$pos: $message")
+ }
+
+ protected sealed class CharReader(buffer: String) { reader =>
+
+ var offset: Int = 0
+ def char: Char =
+ if (offset >= buffer.length) endOfText else buffer charAt offset
+
+ final def nextChar() =
+ offset += 1
+
+ final def check(chars: String): Boolean = {
+ val poff = offset
+ val ok = jump(chars)
+ offset = poff
+ ok
+ }
+
+ def checkSkipInitWhitespace(c: Char): Boolean = {
+ val poff = offset
+ jumpWhitespace()
+ val ok = jump(c)
+ offset = poff
+ ok
+ }
+
+ def checkSkipInitWhitespace(chars: String): Boolean = {
+ val poff = offset
+ jumpWhitespace()
+ val (ok0, chars0) =
+ if (chars.charAt(0) == ' ')
+ (offset > poff, chars substring 1)
+ else
+ (true, chars)
+ val ok = ok0 && jump(chars0)
+ offset = poff
+ ok
+ }
+
+ def countWhitespace: Int = {
+ var count = 0
+ val poff = offset
+ while (isWhitespace(char) && char != endOfText) {
+ nextChar()
+ count += 1
+ }
+ offset = poff
+ count
+ }
+
+ /* Jumpers */
+
+ /** Jumps a character and consumes it
+ * @return true only if the correct character has been jumped */
+ final def jump(ch: Char): Boolean = {
+ if (char == ch) {
+ nextChar()
+ true
+ }
+ else false
+ }
+
+ /** Jumps all the characters in chars, consuming them in the process.
+ * @return true only if the correct characters have been jumped
+ */
+ final def jump(chars: String): Boolean = {
+ var index = 0
+ while (index < chars.length && char == chars.charAt(index) && char != endOfText) {
+ nextChar()
+ index += 1
+ }
+ index == chars.length
+ }
+
+ final def repeatJump(c: Char, max: Int = Int.MaxValue): Int = {
+ var count = 0
+ while (jump(c) && count < max)
+ count += 1
+ count
+ }
+
+ final def jumpUntil(ch: Char): Int = {
+ var count = 0
+ while (char != ch && char != endOfText) {
+ nextChar()
+ count += 1
+ }
+ count
+ }
+
+ final def jumpUntil(pred: => Boolean): Int = {
+ var count = 0
+ while (!pred && char != endOfText) {
+ nextChar()
+ count += 1
+ }
+ count
+ }
+
+ def jumpWhitespace() = jumpUntil(!isWhitespace(char))
+
+ def jumpWhitespaceOrNewLine() = jumpUntil(!isWhitespaceOrNewLine(char))
+
+
+ /* Readers */
+ final def readUntil(c: Char): String = {
+ withRead {
+ while (char != c && char != endOfText) {
+ nextChar()
+ }
+ }
+ }
+
+ final def readUntil(chars: String): String = {
+ assert(chars.length > 0)
+ withRead {
+ val c = chars.charAt(0)
+ while (!check(chars) && char != endOfText) {
+ nextChar()
+ while (char != c && char != endOfText)
+ nextChar()
+ }
+ }
+ }
+
+ final def readUntil(pred: => Boolean): String = {
+ withRead {
+ while (char != endOfText && !pred) {
+ nextChar()
+ }
+ }
+ }
+
+ private def withRead(read: => Unit): String = {
+ val start = offset
+ read
+ buffer.substring(start, offset)
+ }
+
+ /* Chars classes */
+ def isWhitespace(c: Char) = c == ' ' || c == '\t'
+
+ def isWhitespaceOrNewLine(c: Char) = isWhitespace(c) || c == '\n'
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentRegex.scala b/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentRegex.scala
new file mode 100644
index 000000000..2d75b0c66
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentRegex.scala
@@ -0,0 +1,84 @@
+package dotty.tools.dottydoc
+package model
+package comment
+
+import scala.util.matching.Regex
+
+object Regexes {
+ val TrailingWhitespace = """\s+$""".r
+
+ /** The body of a line, dropping the (optional) start star-marker,
+ * one leading whitespace and all trailing whitespace
+ */
+ val CleanCommentLine =
+ new Regex("""(?:\s*\*\s?)?(.*)""")
+
+ /** Dangerous HTML tags that should be replaced by something safer,
+ * such as wiki syntax, or that should be dropped
+ */
+ val DangerousTags =
+ new Regex("""<(/?(div|ol|ul|li|h[1-6]|p))( [^>]*)?/?>|<!--.*-->""")
+
+ /** Javadoc tags that should be replaced by something useful, such as wiki
+ * syntax, or that should be dropped. */
+ val JavadocTags =
+ new Regex("""\{\@(code|docRoot|linkplain|link|literal|value)\p{Zs}*([^}]*)\}""")
+
+ /** Maps a javadoc tag to a useful wiki replacement, or an empty string if it cannot be salvaged. */
+ def javadocReplacement(mtch: Regex.Match): String = {
+ mtch.group(1) match {
+ case "code" => "<code>" + mtch.group(2) + "</code>"
+ case "docRoot" => ""
+ case "link" => "`[[" + mtch.group(2) + "]]`"
+ case "linkplain" => "[[" + mtch.group(2) + "]]"
+ case "literal" => "`" + mtch.group(2) + "`"
+ case "value" => "`" + mtch.group(2) + "`"
+ case _ => ""
+ }
+ }
+
+ /** Maps a dangerous HTML tag to a safe wiki replacement, or an empty string
+ * if it cannot be salvaged. */
+ def htmlReplacement(mtch: Regex.Match): String = mtch.group(1) match {
+ case "p" | "div" => "\n\n"
+ case "h1" => "\n= "
+ case "/h1" => " =\n"
+ case "h2" => "\n== "
+ case "/h2" => " ==\n"
+ case "h3" => "\n=== "
+ case "/h3" => " ===\n"
+ case "h4" | "h5" | "h6" => "\n==== "
+ case "/h4" | "/h5" | "/h6" => " ====\n"
+ case "li" => "\n * - "
+ case _ => ""
+ }
+
+ /** Safe HTML tags that can be kept. */
+ val SafeTags =
+ new Regex("""((&\w+;)|(&#\d+;)|(</?(abbr|acronym|address|area|a|bdo|big|blockquote|br|button|b|caption|cite|code|col|colgroup|dd|del|dfn|em|fieldset|form|hr|img|input|ins|i|kbd|label|legend|link|map|object|optgroup|option|param|pre|q|samp|select|small|span|strong|sub|sup|table|tbody|td|textarea|tfoot|th|thead|tr|tt|var)( [^>]*)?/?>))""")
+
+ val safeTagMarker = '\u000E'
+ val endOfLine = '\u000A'
+ val endOfText = '\u0003'
+
+ /** A Scaladoc tag not linked to a symbol and not followed by text */
+ val SingleTagRegex =
+ new Regex("""\s*@(\S+)\s*""")
+
+ /** A Scaladoc tag not linked to a symbol. Returns the name of the tag, and the rest of the line. */
+ val SimpleTagRegex =
+ new Regex("""\s*@(\S+)\s+(.*)""")
+
+ /** A Scaladoc tag linked to a symbol. Returns the name of the tag, the name
+ * of the symbol, and the rest of the line. */
+ val SymbolTagRegex =
+ new Regex("""\s*@(param|tparam|throws|groupdesc|groupname|groupprio)\s+(\S*)\s*(.*)""")
+
+ /** The start of a Scaladoc code block */
+ val CodeBlockStartRegex =
+ new Regex("""(.*?)((?:\{\{\{)|(?:\u000E<pre(?: [^>]*)?>\u000E))(.*)""")
+
+ /** The end of a Scaladoc code block */
+ val CodeBlockEndRegex =
+ new Regex("""(.*?)((?:\}\}\})|(?:\u000E</pre>\u000E))(.*)""")
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/entities.scala b/doc-tool/src/dotty/tools/dottydoc/model/entities.scala
new file mode 100644
index 000000000..52379f9ad
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/entities.scala
@@ -0,0 +1,119 @@
+package dotty.tools.dottydoc
+package model
+
+import comment._
+import references._
+import dotty.tools.dotc.core.Symbols.{ Symbol, NoSymbol }
+
+trait Entity {
+ def symbol: Symbol
+
+ def name: String
+
+ /** Path from root, i.e. `scala.Option$` */
+ def path: List[String]
+
+ def comment: Option[Comment]
+
+ def kind: String
+
+ def parent: Entity
+
+ /** All parents from package level i.e. Package to Object to Member etc */
+ def parents: List[Entity] = parent match {
+ case NonEntity => Nil
+ case e => e :: e.parents
+ }
+
+ /** Applies `f` to entity if != `NonEntity` */
+ def fold[A](nonEntity: A)(f: Entity => A) = this match {
+ case NonEntity => nonEntity
+ case x => f(x)
+ }
+}
+
+trait SuperTypes {
+ def superTypes: List[MaterializableLink]
+}
+
+trait Members {
+ def members: List[Entity]
+}
+
+trait Modifiers {
+ def modifiers: List[String]
+
+ val isPrivate: Boolean =
+ modifiers.contains("private")
+}
+
+trait TypeParams {
+ def typeParams: List[String]
+}
+
+trait ReturnValue {
+ def returnValue: Reference
+}
+
+trait ParamList {
+ def list: List[NamedReference]
+ def isImplicit: Boolean
+}
+
+trait Constructors {
+ def constructors: List[List[ParamList]]
+}
+
+trait ImplicitlyAddedEntity extends Entity {
+ def implicitlyAddedFrom: Option[Reference]
+}
+
+trait Package extends Entity with Members {
+ val kind = "package"
+
+ def children: List[Entity with Members]
+}
+
+trait Class extends Entity with Modifiers with TypeParams with Constructors with SuperTypes with Members {
+ val kind = "class"
+}
+
+trait CaseClass extends Entity with Modifiers with TypeParams with Constructors with SuperTypes with Members {
+ override val kind = "case class"
+}
+
+trait Trait extends Entity with Modifiers with TypeParams with SuperTypes with Members {
+ def traitParams: List[ParamList]
+ override val kind = "trait"
+}
+
+trait Object extends Entity with Modifiers with SuperTypes with Members {
+ override val kind = "object"
+}
+
+trait Def extends Entity with Modifiers with TypeParams with ReturnValue with ImplicitlyAddedEntity {
+ val kind = "def"
+ def paramLists: List[ParamList]
+}
+
+trait Val extends Entity with Modifiers with ReturnValue with ImplicitlyAddedEntity {
+ val kind = "val"
+}
+
+trait Var extends Entity with Modifiers with ReturnValue {
+ val kind = "var"
+}
+
+trait NonEntity extends Entity {
+ val name = ""
+ val symbol = NoSymbol
+ val comment = None
+ val path = Nil
+ val kind = ""
+ val parent = NonEntity
+}
+
+final case object NonEntity extends NonEntity
+final case object RootEntity extends NonEntity {
+ override val name = "root"
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/factories.scala b/doc-tool/src/dotty/tools/dottydoc/model/factories.scala
new file mode 100644
index 000000000..f95474ef1
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/factories.scala
@@ -0,0 +1,183 @@
+package dotty.tools.dottydoc
+package model
+
+import comment._
+import references._
+import dotty.tools.dotc
+import dotc.core.Types._
+import dotc.core.TypeApplications._
+import dotc.core.Contexts.Context
+import dotc.core.Symbols.{ Symbol, ClassSymbol }
+import dotty.tools.dotc.core.SymDenotations._
+import dotty.tools.dotc.core.Names.TypeName
+import dotc.ast.Trees._
+
+
+object factories {
+ import dotty.tools.dotc.ast.tpd._
+ import dotty.tools.dottydoc.model.internal.ParamListImpl
+ import dotc.core.Flags._
+
+ type TypeTree = dotty.tools.dotc.ast.Trees.Tree[Type]
+
+ def flags(t: Tree)(implicit ctx: Context): List[String] =
+ (t.symbol.flags & SourceModifierFlags)
+ .flagStrings.toList
+ .filter(_ != "<trait>")
+ .filter(_ != "interface")
+
+ def path(sym: Symbol)(implicit ctx: Context): List[String] = sym match {
+ case sym if sym.name.decode.toString == "<root>" => Nil
+ case sym => path(sym.owner) :+ sym.name.show
+ }
+
+
+ private val product = """Product[1-9][0-9]*""".r
+
+ def returnType(t: Type)(implicit ctx: Context): Reference = {
+ val defn = ctx.definitions
+
+ def typeRef(name: String, query: String = "", params: List[Reference] = Nil) = {
+ val realQuery = if (query != "") query else name
+ TypeReference(name, UnsetLink(name, realQuery), params)
+ }
+
+ def expandTpe(t: Type, params: List[Reference] = Nil): Reference = t match {
+ case tl: PolyType =>
+ //FIXME: should be handled correctly
+ // example, in `Option`:
+ //
+ // {{{
+ // def companion: GenericCompanion[collection.Iterable]
+ // }}}
+ //
+ // Becomes: def companion: [+X0] -> collection.Iterable[X0]
+ typeRef(tl.show + " (not handled)")
+ case AppliedType(tycon, args) =>
+ val cls = tycon.typeSymbol
+ if (tycon.isRepeatedParam)
+ expandTpe(args.head)
+ else if (defn.isFunctionClass(cls))
+ FunctionReference(args.init.map(expandTpe(_, Nil)), expandTpe(args.last))
+ else if (defn.isTupleClass(cls))
+ TupleReference(args.map(expandTpe(_, Nil)))
+ else {
+ val query = tycon.show
+ val name = query.split("\\.").last
+ typeRef(name, query, params = args.map(expandTpe(_, Nil)))
+ }
+
+ case ref @ RefinedType(parent, rn, info) =>
+ expandTpe(parent) //FIXME: will be a refined HK, aka class Foo[X] { def bar: List[X] } or similar
+ case ref @ HKApply(tycon, args) =>
+ expandTpe(tycon, args.map(expandTpe(_, params)))
+ case TypeRef(_, n) =>
+ val name = n.decode.toString.split("\\$").last
+ typeRef(name, params = params)
+ case ta: TypeAlias =>
+ expandTpe(ta.alias.widenDealias)
+ case OrType(left, right) =>
+ OrTypeReference(expandTpe(left), expandTpe(right))
+ case AndType(left, right) =>
+ AndTypeReference(expandTpe(left), expandTpe(right))
+ case tb @ TypeBounds(lo, hi) =>
+ BoundsReference(expandTpe(lo), expandTpe(hi))
+ case AnnotatedType(tpe, _) =>
+ expandTpe(tpe)
+ case ExprType(tpe) =>
+ expandTpe(tpe)
+ case c: ConstantType =>
+ ConstantReference(c.show)
+ case tt: ThisType =>
+ expandTpe(tt.underlying)
+ case ci: ClassInfo =>
+ val query = path(ci.typeSymbol).mkString(".")
+ typeRef(ci.cls.name.show, query = query)
+ case mt: MethodType =>
+ expandTpe(mt.resultType)
+ case pt: PolyType =>
+ expandTpe(pt.resultType)
+ case pp: PolyParam =>
+ val paramName = pp.paramName.show
+ val name =
+ if (paramName.contains('$'))
+ paramName.split("\\$\\$").last
+ else paramName
+
+ typeRef(name)
+ }
+
+ expandTpe(t)
+ }
+
+ def typeParams(sym: Symbol)(implicit ctx: Context): List[String] =
+ sym.info match {
+ case pt: PolyType => // TODO: not sure if this case is needed anymore
+ pt.paramNames.map(_.show.split("\\$").last)
+ case ClassInfo(_, _, _, decls, _) =>
+ decls.iterator
+ .filter(_.flags is TypeParam)
+ .map { tp =>
+ val prefix =
+ if (tp.flags is Covariant) "+"
+ else if (tp.flags is Contravariant) "-"
+ else ""
+ prefix + tp.name.show.split("\\$").last
+ }
+ .toList
+ case _ =>
+ Nil
+ }
+
+ def constructors(sym: Symbol)(implicit ctx: Context): List[List[ParamList]] = sym match {
+ case sym: ClassSymbol =>
+ paramLists(sym.primaryConstructor.info) :: Nil
+ case _ => Nil
+ }
+
+ def traitParameters(sym: Symbol)(implicit ctx: Context): List[ParamList] =
+ constructors(sym).head
+
+ def paramLists(tpe: Type)(implicit ctx: Context): List[ParamList] = tpe match {
+ case pt: PolyType =>
+ paramLists(pt.resultType)
+
+ case mt: MethodType =>
+ ParamListImpl(mt.paramNames.zip(mt.paramTypes).map { case (name, tpe) =>
+ NamedReference(
+ name.decode.toString,
+ returnType(tpe),
+ isByName = tpe.isInstanceOf[ExprType],
+ isRepeated = tpe.isRepeatedParam
+ )
+ }, mt.isImplicit) :: paramLists(mt.resultType)
+
+ case annot: AnnotatedType => paramLists(annot.tpe)
+ case (_: PolyParam | _: RefinedType | _: TypeRef | _: ThisType |
+ _: ExprType | _: OrType | _: AndType | _: HKApply) => Nil // return types should not be in the paramlist
+ }
+
+ def superTypes(t: Tree)(implicit ctx: Context): List[MaterializableLink] = t.symbol.denot match {
+ case cd: ClassDenotation =>
+ def isJavaLangObject(prefix: Type): Boolean =
+ prefix match {
+ case TypeRef(ThisType(TypeRef(NoPrefix, outerName)), innerName) =>
+ outerName.toString == "lang" && innerName.toString == "Object"
+ case _ => false
+ }
+
+ def isProductWithArity(prefix: Type): Boolean = prefix match {
+ case TypeRef(TermRef(TermRef(NoPrefix, root), scala), prod) =>
+ root.toString == "_root_" &&
+ scala.toString == "scala" &&
+ product.findFirstIn(prod.toString).isDefined
+ case _ => false
+ }
+
+ cd.classParents.collect {
+ case t: TypeRef if !isJavaLangObject(t) && !isProductWithArity(t) =>
+ UnsetLink(t.name.toString, path(t.symbol).mkString("."))
+ }
+ case _ => Nil
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/internal.scala b/doc-tool/src/dotty/tools/dottydoc/model/internal.scala
new file mode 100644
index 000000000..09f642d0b
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/internal.scala
@@ -0,0 +1,97 @@
+package dotty.tools.dottydoc
+package model
+
+import comment.Comment
+import references._
+import dotty.tools.dotc.core.Symbols.Symbol
+
+object internal {
+
+ trait Impl {
+ var parent: Entity = NonEntity
+ }
+
+ final case class PackageImpl(
+ symbol: Symbol,
+ name: String,
+ var members: List[Entity],
+ path: List[String],
+ var comment: Option[Comment] = None
+ ) extends Package with Impl {
+ def children: List[Entity with Members] =
+ members.collect { case x: Entity with Members => x }
+ }
+
+ final case class ClassImpl(
+ symbol: Symbol,
+ name: String,
+ members: List[Entity],
+ modifiers: List[String],
+ path: List[String],
+ typeParams: List[String] = Nil,
+ constructors: List[List[ParamList]] = Nil,
+ superTypes: List[MaterializableLink] = Nil,
+ var comment: Option[Comment] = None
+ ) extends Class with Impl
+
+ final case class CaseClassImpl(
+ symbol: Symbol,
+ name: String,
+ members: List[Entity],
+ modifiers: List[String],
+ path: List[String],
+ typeParams: List[String] = Nil,
+ constructors: List[List[ParamList]] = Nil,
+ superTypes: List[MaterializableLink] = Nil,
+ var comment: Option[Comment] = None
+ ) extends CaseClass with Impl
+
+ final case class TraitImpl(
+ symbol: Symbol,
+ name: String,
+ members: List[Entity],
+ modifiers: List[String],
+ path: List[String],
+ typeParams: List[String] = Nil,
+ traitParams: List[ParamList] = Nil,
+ superTypes: List[MaterializableLink] = Nil,
+ var comment: Option[Comment] = None
+ ) extends Trait with Impl
+
+ final case class ObjectImpl(
+ symbol: Symbol,
+ name: String,
+ members: List[Entity],
+ modifiers: List[String],
+ path: List[String],
+ superTypes: List[MaterializableLink] = Nil,
+ var comment: Option[Comment] = None
+ ) extends Object with Impl
+
+ final case class DefImpl(
+ symbol: Symbol,
+ name: String,
+ modifiers: List[String],
+ path: List[String],
+ returnValue: Reference,
+ typeParams: List[String] = Nil,
+ paramLists: List[ParamList] = Nil,
+ var comment: Option[Comment] = None,
+ implicitlyAddedFrom: Option[Reference] = None
+ ) extends Def with Impl
+
+ final case class ValImpl(
+ symbol: Symbol,
+ name: String,
+ modifiers: List[String],
+ path: List[String],
+ returnValue: Reference,
+ var comment: Option[Comment] = None,
+ implicitlyAddedFrom: Option[Reference] = None
+ ) extends Val with Impl
+
+ final case class ParamListImpl(
+ list: List[NamedReference],
+ isImplicit: Boolean
+ ) extends ParamList
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/java.scala b/doc-tool/src/dotty/tools/dottydoc/model/java.scala
new file mode 100644
index 000000000..410085061
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/java.scala
@@ -0,0 +1,223 @@
+package dotty.tools.dottydoc
+package model
+
+import comment._
+import references._
+
+object java {
+ import scala.collection.JavaConverters._
+ import _root_.java.util.{ Optional => JOptional, Map => JMap }
+
+ implicit class JavaOption[A](val opt: Option[A]) extends AnyVal {
+ def asJava: JOptional[A] =
+ opt.map(a => JOptional.of(a)).getOrElse(JOptional.empty[A])
+ }
+
+ implicit class JavaComment(val cmt: Comment) extends AnyVal {
+ def asJava: JMap[String, _] = Map(
+ "body" -> cmt.body,
+ "short" -> cmt.short,
+ "authors" -> cmt.authors.asJava,
+ "see" -> cmt.see.asJava,
+ "result" -> cmt.result.asJava,
+ "throws" -> cmt.throws.asJava,
+ "valueParams" -> cmt.valueParams.asJava,
+ "typeParams" -> cmt.typeParams.asJava,
+ "version" -> cmt.version.asJava,
+ "since" -> cmt.since.asJava,
+ "todo" -> cmt.todo.asJava,
+ "deprecated" -> cmt.deprecated.asJava,
+ "note" -> cmt.note.asJava,
+ "example" -> cmt.example.asJava,
+ "constructor" -> cmt.constructor.asJava,
+ "group" -> cmt.group.asJava,
+ "groupDesc" -> cmt.groupDesc.asJava,
+ "groupNames" -> cmt.groupNames.asJava,
+ "groupPrio" -> cmt.groupPrio.asJava,
+ "hideImplicitConversions" -> cmt.hideImplicitConversions.asJava
+ ).asJava
+ }
+
+ implicit class JavaPackage(val ent: Package) extends AnyVal {
+ def asJava(extras: Map[String, _] = Map.empty): JMap[String, _] = (Map(
+ "kind" -> ent.kind,
+ "name" -> ent.name,
+ "path" -> ent.path.asJava,
+ "members" -> ent.members.map(_.asJava()).asJava,
+ "comment" -> ent.comment.map(_.asJava).asJava
+ ) ++ extras).asJava
+ }
+
+ implicit class JavaCaseClass(val ent: CaseClass) extends AnyVal {
+ def asJava(extras: Map[String, _] = Map.empty): JMap[String, _] = (Map(
+ "kind" -> ent.kind,
+ "name" -> ent.name,
+ "members" -> ent.members.map(_.asJava()).asJava,
+ "modifiers" -> ent.modifiers.asJava,
+ "path" -> ent.path.asJava,
+ "typeParams" -> ent.typeParams.asJava,
+ "superTypes" -> ent.superTypes.map(_.asJava).asJava,
+ "comment" -> ent.comment.map(_.asJava).asJava
+ ) ++ extras).asJava
+ }
+
+ implicit class JavaClass(val ent: Class) extends AnyVal {
+ def asJava(extras: Map[String, _] = Map.empty): JMap[String, _] = (Map(
+ "kind" -> ent.kind,
+ "name" -> ent.name,
+ "members" -> ent.members.map(_.asJava()).asJava,
+ "modifiers" -> ent.modifiers.asJava,
+ "path" -> ent.path.asJava,
+ "typeParams" -> ent.typeParams.asJava,
+ "superTypes" -> ent.superTypes.map(_.asJava).asJava,
+ "comment" -> ent.comment.map(_.asJava).asJava
+ ) ++ extras).asJava
+ }
+
+ implicit class JavaTrait(val ent: Trait) extends AnyVal {
+ def asJava(extras: Map[String, _] = Map.empty): JMap[String, _] = (Map(
+ "kind" -> ent.kind,
+ "name" -> ent.name,
+ "members" -> ent.members.map(_.asJava()).asJava,
+ "modifiers" -> ent.modifiers.asJava,
+ "path" -> ent.path.asJava,
+ "typeParams" -> ent.typeParams.asJava,
+ "superTypes" -> ent.superTypes.map(_.asJava).asJava,
+ "comment" -> ent.comment.map(_.asJava).asJava
+ ) ++ extras).asJava
+ }
+
+ implicit class JavaObject(val ent: Object) extends AnyVal {
+ def asJava(extras: Map[String, _] = Map.empty): JMap[String, _] = (Map(
+ "kind" -> ent.kind,
+ "name" -> ent.name,
+ "members" -> ent.members.map(_.asJava()).asJava,
+ "modifiers" -> ent.modifiers.asJava,
+ "path" -> ent.path.asJava,
+ "superTypes" -> ent.superTypes.map(_.asJava).asJava,
+ "comment" -> ent.comment.map(_.asJava).asJava
+ ) ++ extras).asJava
+ }
+
+ implicit class JavaDef(val ent: Def) extends AnyVal {
+ def asJava: JMap[String, _] = Map(
+ "kind" -> ent.kind,
+ "name" -> ent.name,
+ "modifiers" -> ent.modifiers.asJava,
+ "path" -> ent.path.asJava,
+ "returnValue" -> ent.returnValue.asJava,
+ "typeParams" -> ent.typeParams.asJava,
+ "paramLists" -> ent.paramLists.map(_.asJava).asJava,
+ "comment" -> ent.comment.map(_.asJava).asJava,
+ "implicitlyAddedFrom" -> ent.implicitlyAddedFrom.map(_.asJava).asJava
+ ).asJava
+ }
+
+ implicit class JavaVal(val ent: Val) extends AnyVal {
+ def asJava: JMap[String, _] = Map(
+ "kind" -> ent.kind,
+ "name" -> ent.name,
+ "modifiers" -> ent.modifiers.asJava,
+ "path" -> ent.path.asJava,
+ "returnValue" -> ent.returnValue.asJava,
+ "comment" -> ent.comment.map(_.asJava).asJava,
+ "implicitlyAddedFrom" -> ent.implicitlyAddedFrom.map(_.asJava).asJava
+ ).asJava
+ }
+
+ implicit class JavaParamList(val pl: ParamList) extends AnyVal {
+ def asJava: JMap[String, _] = Map(
+ "list" -> pl.list.map(_.asJava).asJava,
+ "isImplicit" -> pl.isImplicit
+ ).asJava
+ }
+
+ implicit class JavaReference(val ref: Reference) extends AnyVal {
+ def asJava: JMap[String, _] = ref match {
+ case TypeReference(title, tpeLink, paramLinks) => Map(
+ "kind" -> "TypeReference",
+ "title" -> title,
+ "tpeLink" -> tpeLink.asJava,
+ "paramLinks" -> paramLinks.map(_.asJava).asJava
+ ).asJava
+
+ case OrTypeReference(left, right) => Map(
+ "kind" -> "OrTypeReference",
+ "left" -> left.asJava,
+ "right" -> right.asJava
+ ).asJava
+
+ case AndTypeReference(left, right) => Map(
+ "kind" -> "AndTypeReference",
+ "left" -> left.asJava,
+ "right" -> right.asJava
+ ).asJava
+
+ case FunctionReference(args, returnValue) => Map(
+ "kind" -> "FunctionReference",
+ "args" -> args.map(_.asJava).asJava,
+ "returnValue" -> returnValue
+ ).asJava
+
+ case TupleReference(args) => Map(
+ "kind" -> "TupleReference",
+ "args" -> args.map(_.asJava).asJava
+ ).asJava
+
+ case BoundsReference(low, high) => Map(
+ "kind" -> "BoundsReference",
+ "low" -> low.asJava,
+ "hight" -> high.asJava
+ ).asJava
+
+ case NamedReference(title, ref, isByName, isRepeated) => Map(
+ "kind" -> "NamedReference",
+ "title" -> title,
+ "ref" -> ref.asJava,
+ "isByName" -> isByName,
+ "isRepeated" -> isRepeated
+ ).asJava
+
+ case ConstantReference(title) => Map(
+ "kind" -> "ConstantReference",
+ "title" -> title
+ ).asJava
+ }
+ }
+
+ implicit class JavaMaterializableLink(val link: MaterializableLink) extends AnyVal {
+ def asJava: JMap[String, _] = link match {
+ case UnsetLink(title, query) => Map(
+ "kind" -> "UnsetLink",
+ "title" -> title,
+ "query" -> query
+ ).asJava
+
+ case MaterializedLink(title, target) => Map(
+ "kind" -> "MaterializedLink",
+ "title" -> title,
+ "target" -> target
+ ).asJava
+
+ case NoLink(title, target) => Map(
+ "kind" -> "NoLink",
+ "title" -> title,
+ "target" -> target
+ ).asJava
+ }
+ }
+
+ implicit class JavaEntity(val ent: Entity) extends AnyVal {
+ def asJava(extras: Map[String, _] = Map.empty): JMap[String, _] = parseEntity(ent, extras)
+ }
+
+ private def parseEntity(ent: Entity, extras: Map[String, _]): JMap[String, _] = ent match {
+ case ent: Package => ent.asJava(extras)
+ case ent: CaseClass => ent.asJava(extras)
+ case ent: Class => ent.asJava(extras)
+ case ent: Trait => ent.asJava(extras)
+ case ent: Object => ent.asJava(extras)
+ case ent: Def => ent.asJava
+ case ent: Val => ent.asJava
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/json.scala b/doc-tool/src/dotty/tools/dottydoc/model/json.scala
new file mode 100644
index 000000000..145728f8a
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/json.scala
@@ -0,0 +1,93 @@
+package dotty.tools.dottydoc
+package model
+
+import comment._
+import references._
+
+/** This object provides a protocol for serializing the package AST to JSON
+ *
+ * TODO: It might be a good ideat to represent the JSON better than just
+ * serializing a big string-blob in the future.
+ */
+object json {
+ implicit class JsonString(val str: String) extends AnyVal {
+ def json: String = {
+ val cleanedString = str
+ .replaceAll("\\\\","\\\\\\\\")
+ .replaceAll("\\\"", "\\\\\"")
+ .replaceAll("\n", "\\\\n")
+
+ s""""$cleanedString""""
+ }
+ }
+
+ implicit class JsonBoolean(val boo: Boolean) extends AnyVal {
+ def json: String = if (boo) "true" else "false"
+ }
+
+ implicit class JsonComment(val cmt: Comment) extends AnyVal {
+ def json: String =
+ s"""{"body":${cmt.body.json},"short":${cmt.short.json},"authors":${cmt.authors.map(_.json).mkString("[",",","]")},"see":${cmt.see.map(_.json).mkString("[",",","]")},${cmt.result.map(res => s""""result":${res.json},""").getOrElse("")}"throws":${cmt.throws.map { case (k, v) => s"${k.json}:${v.json}" }.mkString("{",",","}")},"valueParams":${cmt.valueParams.map { case (k, v) => s"${k.json}:${v.json}"}.mkString("{",",","}")},"typeParams":${cmt.typeParams.map { case (k, v) => s"${k.json}:${v.json}"}.mkString("{",",","}")},${cmt.version.map(x => s""""version":${x.json},""").getOrElse("")}${cmt.since.map(x => s""""since":${x.json},""").getOrElse("")}"todo":${cmt.todo.map(_.json).mkString("[",",","]")},${cmt.deprecated.map(x => s""""deprecated":${x.json},""").getOrElse("")}"note":${cmt.note.map(_.json).mkString("[",",","]")},"example":${cmt.example.map(_.json).mkString("[",",","]")},${cmt.constructor.map(x => s""""constructor":${x.json},""").getOrElse("")}${cmt.group.map(x => s""""group":${x.json},""").getOrElse("")}"groupDesc":${cmt.groupDesc.map { case (k, v) => s"${k.json}:${v.json}"}.mkString("{",",","}")},"groupNames":${cmt.groupNames.map { case (k, v) => s"${k.json}:${v.json}"}.mkString("{",",","}")},"groupPrio":${cmt.groupPrio.map { case (k, v) => s"${k.json}:${v.json}"}.mkString("{",",","}")},"hideImplicitConversions":${cmt.hideImplicitConversions.map(_.json).mkString("[",",","]")}}"""
+ }
+
+ implicit class LinkJson(val link: MaterializableLink) extends AnyVal {
+ def json: String = {
+ val (secondTitle, secondValue, kind) = link match {
+ case ul: UnsetLink => ("query".json, ul.query.json, "UnsetLink".json)
+ case ml: MaterializedLink => ("target".json, ml.target.json, "MaterializedLink".json)
+ case nl: NoLink => ("target".json, nl.target.json, "NoLink".json)
+ }
+ s"""{"title":${link.title.json},$secondTitle:${secondValue},"kind":$kind}"""
+ }
+ }
+
+ implicit class ParamListJson(val plist: ParamList) extends AnyVal {
+ def json: String =
+ s"""{"list":${plist.list.map(_.json).mkString("[",",","]")},"isImplicit":${plist.isImplicit.json}}"""
+ }
+
+ private def refToJson(ref: Reference): String = ref match {
+ case ref: TypeReference =>
+ s"""{"title":${ref.title.json},"tpeLink":${ref.tpeLink.json},"paramLinks":${ref.paramLinks.map(_.json).mkString("[",",","]")},"kind":"TypeReference"}"""
+ case ref: AndTypeReference =>
+ s"""{"left":${refToJson(ref.left)},"right":${refToJson(ref.right)},"kind":"AndTypeReference"}"""
+ case ref: OrTypeReference =>
+ s"""{"left":${refToJson(ref.left)},"right":${refToJson(ref.right)},"kind":"OrTypeReference"}"""
+ case ref: BoundsReference =>
+ s"""{"low":${refToJson(ref.low)},"high":${refToJson(ref.high)},"kind":"BoundsReference"}"""
+ case ref: NamedReference =>
+ s"""{"title":${ref.title.json},"ref":${refToJson(ref.ref)},"isByName":${ref.isByName.json},"isRepeated":${ref.isRepeated.json},"kind":"NamedReference"}"""
+ case ref: ConstantReference =>
+ s"""{"title":${ref.title.json},"kind": "ConstantReference"}"""
+ case ref: FunctionReference =>
+ s"""{"args":${ref.args.map(refToJson).mkString("[",",","]")},"returnValue":${refToJson(ref.returnValue)},"kind": "FunctionReference"}"""
+ case ref: TupleReference =>
+ s"""{"args":${ref.args.map(refToJson).mkString("[",",","]")},"kind": "TupleReference"}"""
+ }
+ implicit class ReferenceJson(val ref: Reference) extends AnyVal { def json: String = refToJson(ref) }
+
+ private def entToJson(ent: Entity): String = ent match {
+ case ent: Package =>
+ s"""{"name":${ent.name.json},"members":${ent.members.map(_.json).mkString("[",",","]")},"path":${ent.path.map(_.json).mkString("[",",","]")},${ent.comment.map(_.json).fold("")(cmt => s""""comment":$cmt,""")}"kind":"package"}"""
+ case ent: Class =>
+ s"""{"name":${ent.name.json},"members":${ent.members.map(_.json).mkString("[",",","]")},"modifiers":${ent.modifiers.map(_.json).mkString("[",",","]")},"path":${ent.path.map(_.json).mkString("[",",","]")},"typeParams":${ent.typeParams.map(_.json).mkString("[",",","]")},"constructors":${ent.constructors.map(xs => xs.map(_.json).mkString("[",",","]")).mkString("[",",","]")},"superTypes":${ent.superTypes.map(_.json).mkString("[",",","]")},${ent.comment.map(_.json).fold("")(cmt => s""""comment":$cmt,""")}"kind":"class"}"""
+ case ent: CaseClass =>
+ s"""{"name":${ent.name.json},"members":${ent.members.map(_.json).mkString("[",",","]")},"modifiers":${ent.modifiers.map(_.json).mkString("[",",","]")},"path":${ent.path.map(_.json).mkString("[",",","]")},"typeParams":${ent.typeParams.map(_.json).mkString("[",",","]")},"constructors":${ent.constructors.map(xs => xs.map(_.json).mkString("[",",","]")).mkString("[",",","]")},"superTypes":${ent.superTypes.map(_.json).mkString("[",",","]")},${ent.comment.map(_.json).fold("")(cmt => s""""comment":$cmt,""")}"kind":"case class"}"""
+ case ent: Trait =>
+ s"""{"name":${ent.name.json},"members":${ent.members.map(_.json).mkString("[",",","]")},"modifiers":${ent.modifiers.map(_.json).mkString("[",",","]")},"path":${ent.path.map(_.json).mkString("[",",","]")},"typeParams":${ent.typeParams.map(_.json).mkString("[",",","]")},"traitParams":${ent.traitParams.map(_.json).mkString("[",",","]")},"superTypes":${ent.superTypes.map(_.json).mkString("[",",","]")},${ent.comment.map(_.json).fold("")(cmt => s""""comment":$cmt,""")}"kind":"trait"}"""
+ case ent: Object =>
+ s"""{"name":${ent.name.json},"members":${ent.members.map(_.json).mkString("[",",","]")},"modifiers":${ent.modifiers.map(_.json).mkString("[",",","]")},"path":${ent.path.map(_.json).mkString("[",",","]")},"superTypes":${ent.superTypes.map(_.json).mkString("[",",","]")},${ent.comment.map(_.json).fold("")(cmt => s""""comment":$cmt,""")}"kind":"object"}"""
+ case ent: Def =>
+ s"""{"name":${ent.name.json},"modifiers":${ent.modifiers.map(_.json).mkString("[",",","]")},"path":${ent.path.map(_.json).mkString("[",",","]")},"returnValue":${ent.returnValue.json},"typeParams":${ent.typeParams.map(_.json).mkString("[",",","]")},"paramLists":${ent.paramLists.map(_.json).mkString("[",",","]")},${ent.comment.map(_.json).fold("")(cmt => s""""comment":$cmt,""")}${ent.implicitlyAddedFrom.fold("")(ref => s""""implicitlyAddedFrom":${ref.json},""")}"kind":"def"}"""
+ case ent: Val =>
+ s"""{"name":${ent.name.json},"modifiers":${ent.modifiers.map(_.json).mkString("[",",","]")},"path":${ent.path.map(_.json).mkString("[",",","]")},"returnValue":${ent.returnValue.json},${ent.comment.map(_.json).fold("")(cmt => s""""comment":$cmt,""")}${ent.implicitlyAddedFrom.fold("")(ref => s""""implicitlyAddedFrom":${ref.json},""")}"kind":"val"}"""
+ }
+ implicit class EntityJson(val ent: Entity) extends AnyVal { def json: String = entToJson(ent) }
+ implicit class PackageJson(val pack: Package) extends AnyVal { def json: String = (pack: Entity).json }
+
+ implicit class PackMapJson(val packs: collection.Map[String, Package]) extends AnyVal {
+ def json: String = packs
+ .map { case (k, v) => s"${k.json}: ${v.json}" }
+ .mkString("{",",","}")
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/references.scala b/doc-tool/src/dotty/tools/dottydoc/model/references.scala
new file mode 100644
index 000000000..a28148fa7
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/model/references.scala
@@ -0,0 +1,20 @@
+package dotty.tools.dottydoc
+package model
+
+object references {
+ sealed trait Reference
+ final case class TypeReference(title: String, tpeLink: MaterializableLink, paramLinks: List[Reference]) extends Reference
+ final case class OrTypeReference(left: Reference, right: Reference) extends Reference
+ final case class AndTypeReference(left: Reference, right: Reference) extends Reference
+ final case class FunctionReference(args: List[Reference], returnValue: Reference) extends Reference
+ final case class TupleReference(args: List[Reference]) extends Reference
+ final case class BoundsReference(low: Reference, high: Reference) extends Reference
+ final case class NamedReference(title: String, ref: Reference, isByName: Boolean = false, isRepeated: Boolean = false) extends Reference
+ final case class ConstantReference(title: String) extends Reference
+
+ /** Use MaterializableLink for entities that need be picklable */
+ sealed trait MaterializableLink { def title: String }
+ final case class UnsetLink(title: String, query: String) extends MaterializableLink
+ final case class MaterializedLink(title: String, target: String) extends MaterializableLink
+ final case class NoLink(title: String, target: String) extends MaterializableLink
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/util/MemberLookup.scala b/doc-tool/src/dotty/tools/dottydoc/util/MemberLookup.scala
new file mode 100644
index 000000000..40c775428
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/util/MemberLookup.scala
@@ -0,0 +1,92 @@
+package dotty.tools
+package dottydoc
+package util
+
+import dotc.config.Printers.dottydoc
+import dotc.core.Contexts.Context
+import dotc.core.Flags
+import dotc.core.Names._
+import dotc.core.Symbols._
+import dotc.core.Types._
+import dotc.core.Names._
+import dotc.util.Positions._
+import model.internal._
+import model.comment._
+import model._
+
+trait MemberLookup {
+ /** Performs a lookup based on the provided (pruned) query string
+ *
+ * Will return a `Tooltip` if unsucessfull, otherwise a LinkToEntity or LinkToExternal
+ */
+ def lookup(
+ entity: Entity,
+ packages: Map[String, Package],
+ query: String,
+ pos: Position
+ ): LinkTo = {
+ val notFound: LinkTo = Tooltip(query)
+ val querys = query.split("\\.").toList
+
+ /** Looks for the specified entity among `ent`'s members */
+ def localLookup(ent: Entity with Members, searchStr: String): LinkTo =
+ ent
+ .members
+ .collect { case x if x.name == searchStr => x }
+ .sortBy(_.path.last)
+ .headOption
+ .fold(notFound)(e => LinkToEntity(e))
+
+ /** Looks for an entity down in the structure, if the search list is Nil,
+ * the search stops
+ */
+ def downwardLookup(ent: Entity with Members, search: List[String]): LinkTo =
+ search match {
+ case Nil => notFound
+ case x :: Nil =>
+ localLookup(ent, x)
+ case x :: xs =>
+ ent
+ .members
+ .collect { case e: Entity with Members if e.name == x => e }
+ .headOption
+ .fold(notFound)(e => downwardLookup(e, xs))
+ }
+
+ /** Finds package with longest matching name, then does downwardLookup in
+ * the package
+ */
+ def globalLookup: LinkTo = {
+ def longestMatch(list: List[String]): List[String] =
+ if (list == Nil) Nil
+ else
+ packages
+ .get(list.mkString("."))
+ .map(_ => list)
+ .getOrElse(longestMatch(list.dropRight(1)))
+
+ longestMatch(querys) match {
+ case Nil => notFound
+ case xs => downwardLookup(packages(xs.mkString(".")), querys diff xs)
+ }
+ }
+
+ (querys, entity) match {
+ case (x :: Nil, e: Entity with Members) =>
+ localLookup(e, x)
+ case (x :: _, e: Entity with Members) if x == entity.name =>
+ downwardLookup(e, querys)
+ case (x :: xs, _) =>
+ if (xs.nonEmpty) globalLookup
+ else lookup(entity, packages, "scala." + query, pos)
+ }
+ }
+
+ def makeEntityLink(
+ entity: Entity,
+ packages: Map[String, Package],
+ title: Inline,
+ pos: Position,
+ query: String
+ ): EntityLink = EntityLink(title, lookup(entity, packages, query, pos))
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/util/OutputWriter.scala b/doc-tool/src/dotty/tools/dottydoc/util/OutputWriter.scala
new file mode 100644
index 000000000..2084e0a97
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/util/OutputWriter.scala
@@ -0,0 +1,125 @@
+package dotty.tools.dottydoc
+package util
+
+import dotty.tools.dotc.config.Printers.dottydoc
+
+import _root_.java.io.{
+ File => JFile,
+ PrintWriter => JPrintWriter,
+ FileReader => JFileReader,
+ BufferedInputStream,
+ InputStream,
+ InputStreamReader,
+ FileOutputStream,
+ BufferedOutputStream,
+ FileNotFoundException
+}
+import _root_.java.net.URL
+import _root_.java.util.{ Map => JMap, List => JList }
+import model.{ Entity, Package }
+import model.json._
+import com.github.mustachejava.DefaultMustacheFactory
+import scala.collection.JavaConverters._
+
+class OutputWriter {
+
+ def writeJava(packs: JMap[String, Package], outPath: String, template: URL, resources: JList[URL]): Unit = {
+ write(packs.asScala, outPath, template, resources.asScala)
+ }
+
+ def write(packs: collection.Map[String, Package], outPath: String, template: URL, resources: Traversable[URL]): Unit = {
+ // Write all packages to `outPath`
+ for (pack <- packs.values) {
+ println(s"""Writing '${pack.path.mkString(".")}'""")
+ writeFile(
+ expandTemplate(template, pack, outPath),
+ outPath + pack.path.mkString("/", "/", "/"),
+ "index.html")
+
+ // Write all package children to outPath
+ for {
+ child <- pack.children
+ if child.kind != "package"
+ } {
+ println(s"""Writing '${child.path.mkString(".")}'""")
+ writeFile(
+ expandTemplate(template, child, outPath),
+ outPath + child.path.dropRight(1).mkString("/", "/", "/"),
+ child.path.last + ".html")
+ }
+ }
+
+ // Write full index to outPath
+ val js = "Index = {}; Index.packages = " + packs.json + ";"
+ println("Writing index.js...")
+ writeFile(js, outPath + "/docassets/", "index.js")
+
+ // Write resources to outPath
+ println("Copying CSS/JS resources to destination...")
+ assert(resources.nonEmpty)
+
+ // TODO: splitting the URL by '/' and taking the last means that we don't
+ // allow folders among the resources
+ resources.foreach(url => copy(url.openStream, outPath, url.getFile.split("/").last))
+
+ println("Done writing static material, building js-app")
+ }
+
+ def writeJsonJava(index: JMap[String, Package], outputDir: String): Unit =
+ writeJson(index.asScala, outputDir)
+
+ def writeJson(index: collection.Map[String, Package], outputDir: String): Unit =
+ writeFile(index.json, outputDir + "/", "index.json")
+
+ def expandTemplate(template: URL, entity: Entity, outPath: String): String = try {
+ import model.json._
+ import model.java._
+
+ val inputStream = template.openStream
+ val writer = new _root_.java.io.StringWriter()
+ val mf = new DefaultMustacheFactory()
+
+ def toRoot = "../" * (entity.path.length - { if (entity.isInstanceOf[Package]) 0 else 1 })
+
+ val entityWithExtras = entity.asJava(Map(
+ "assets" -> s"${toRoot}docassets",
+ "index" -> s"${toRoot}docassets/index.js",
+ "currentEntity" -> entity.json
+ ))
+
+ mf.compile(new InputStreamReader(inputStream), "template")
+ .execute(writer, entityWithExtras)
+
+ inputStream.close()
+ writer.flush()
+ writer.toString
+ } catch {
+ case fnf: FileNotFoundException =>
+ dottydoc.println(s"""Couldn't find the template: "${template.getFile}"...exiting""")
+ System.exit(1); ""
+ }
+
+ def writeFile(str: String, path: String, file: String): Unit =
+ writeFile(str.map(_.toByte).toArray, path, file)
+
+ def writeFile(bytes: Array[Byte], path: String, file: String): Unit = {
+ def printToFile(f: JFile)(op: JPrintWriter => Unit) = {
+ val bos = new BufferedOutputStream(new FileOutputStream(f))
+ try {
+ Stream.continually(bos.write(bytes))
+ } finally bos.close()
+ }
+
+ new JFile(path).mkdirs()
+ printToFile(new JFile(path + file))(printer => bytes.foreach(printer.print))
+ }
+
+ def copy(src: InputStream, path: String, name: String): Unit = {
+ val reader = new BufferedInputStream(src)
+ try {
+ val bytes = Stream.continually(reader.read).takeWhile(-1 != _).map(_.toByte)
+ writeFile(bytes.toArray, path + "/docassets/", name)
+ src.close()
+ } finally reader.close()
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/util/Traversing.scala b/doc-tool/src/dotty/tools/dottydoc/util/Traversing.scala
new file mode 100644
index 000000000..a3b60fa44
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/util/Traversing.scala
@@ -0,0 +1,25 @@
+package dotty.tools.dottydoc
+package util
+
+object traversing {
+ import model._
+
+ def mutateEntities(e: Entity)(trans: Entity => Unit): Unit = e match {
+ case e: Entity with Members =>
+ trans(e)
+ e.members.map(mutateEntities(_)(trans))
+ case e: Entity => trans(e)
+ }
+
+ def relativePath(from: Entity, to: Entity) = {
+ val offset = from match {
+ case v: Val if v.implicitlyAddedFrom.isDefined => 3
+ case d: Def if d.implicitlyAddedFrom.isDefined => 3
+ case _: Val | _: Def => 2
+ case _ => 1
+ }
+
+ "../" * (from.path.length - offset) +
+ to.path.mkString("", "/", ".html")
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/util/mutate.scala b/doc-tool/src/dotty/tools/dottydoc/util/mutate.scala
new file mode 100644
index 000000000..a5a4dfec6
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/util/mutate.scala
@@ -0,0 +1,65 @@
+package dotty.tools.dottydoc
+package util
+package internal
+
+object setters {
+ import model._
+ import comment.Comment
+ import model.references._
+ import internal._
+
+ def setComment(ent: Entity, to: Option[Comment]) = ent match {
+ case x: PackageImpl => x.comment = to
+ case x: ClassImpl => x.comment = to
+ case x: CaseClassImpl => x.comment = to
+ case x: TraitImpl => x.comment = to
+ case x: ObjectImpl => x.comment = to
+ case x: DefImpl => x.comment = to
+ case x: ValImpl => x.comment = to
+ }
+
+ def setParent(ent: Entity, to: Entity): Unit = ent match {
+ case e: ClassImpl =>
+ e.parent = to
+ e.members.foreach(setParent(_, e))
+ case e: CaseClassImpl =>
+ e.parent = to
+ e.members.foreach(setParent(_, e))
+ case e: ObjectImpl =>
+ e.parent = to
+ e.members.foreach(setParent(_, e))
+ case e: TraitImpl =>
+ e.parent = to
+ e.members.foreach(setParent(_, e))
+ case e: ValImpl =>
+ e.parent = to
+ case e: DefImpl =>
+ e.parent = to
+ case _ => ()
+ }
+
+ implicit class FlattenedEntity(val ent: Entity) extends AnyVal {
+ /** Returns a flat copy if anything was changed (Entity with Members) else
+ * the identity
+ */
+ def flat: Entity = {
+ def flattenMember: Entity => Entity = {
+ case e: PackageImpl => e.copy(members = Nil)
+ case e: ObjectImpl => e.copy(members = Nil)
+ case e: CaseClassImpl => e.copy(members = Nil)
+ case e: ClassImpl => e.copy(members = Nil)
+ case e: TraitImpl => e.copy(members = Nil)
+ case other => other
+ }
+
+ ent match {
+ case e: PackageImpl => e.copy(members = e.members.map(flattenMember))
+ case e: ObjectImpl => e.copy(members = e.members.map(flattenMember))
+ case e: CaseClassImpl => e.copy(members = e.members.map(flattenMember))
+ case e: ClassImpl => e.copy(members = e.members.map(flattenMember))
+ case e: TraitImpl => e.copy(members = e.members.map(flattenMember))
+ case other => other
+ }
+ }
+ }
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/util/syntax.scala b/doc-tool/src/dotty/tools/dottydoc/util/syntax.scala
new file mode 100644
index 000000000..dd3d21f8d
--- /dev/null
+++ b/doc-tool/src/dotty/tools/dottydoc/util/syntax.scala
@@ -0,0 +1,16 @@
+package dotty.tools
+package dottydoc
+package util
+
+import dotc.core.Contexts.Context
+import dotc.core.Comments._
+import model.Package
+import core.ContextDottydoc
+
+object syntax {
+ implicit class ContextWithContextDottydoc(val ctx: Context) extends AnyVal {
+ def docbase: ContextDottydoc = ctx.docCtx.getOrElse {
+ throw new IllegalStateException("DocBase must be set before running dottydoc phases")
+ }.asInstanceOf[ContextDottydoc]
+ }
+}