aboutsummaryrefslogtreecommitdiff
path: root/doc-tool/src/dotty/tools/dottydoc/model
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/dotty/tools/dottydoc/model
parentfcdb1c984c4b32c0c1b13337d9bd50a3883cb3b3 (diff)
downloaddotty-94a8177f208418682b86934eb3f0b8edab99b1f0.tar.gz
dotty-94a8177f208418682b86934eb3f0b8edab99b1f0.tar.bz2
dotty-94a8177f208418682b86934eb3f0b8edab99b1f0.zip
Move `dottydoc` -> `doc-tool`
Diffstat (limited to 'doc-tool/src/dotty/tools/dottydoc/model')
-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
12 files changed, 1894 insertions, 0 deletions
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
+}