diff options
Diffstat (limited to 'dottydoc/jvm')
11 files changed, 1281 insertions, 0 deletions
diff --git a/dottydoc/jvm/src/dotty/tools/dottydoc/DottyDoc.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/DottyDoc.scala new file mode 100644 index 000000000..1c24b67a7 --- /dev/null +++ b/dottydoc/jvm/src/dotty/tools/dottydoc/DottyDoc.scala @@ -0,0 +1,40 @@ +package dotty.tools +package dottydoc + +import core.Phases.DocPhase +import dotc.config.CompilerCommand +import dotc.config.Printers.dottydoc +import dotc.core.Contexts.Context +import dotc.core.Phases.Phase +import dotc.typer.FrontEnd +import dotc.{Compiler, Driver} + +/** 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 JSON from the results of the FrontEnd phase + */ +case object DottyDocCompiler extends Compiler { + override def phases: List[List[Phase]] = + List(new FrontEnd) :: + List(new DocPhase) :: + Nil +} + +object DottyDoc extends Driver { + 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) + val fileNames = CompilerCommand.checkUsage(summary, sourcesRequired)(ctx) + (fileNames, ctx) + } + + override def newCompiler(implicit ctx: Context): Compiler = DottyDocCompiler +} diff --git a/dottydoc/jvm/src/dotty/tools/dottydoc/core/Phases.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/core/Phases.scala new file mode 100644 index 000000000..a52572363 --- /dev/null +++ b/dottydoc/jvm/src/dotty/tools/dottydoc/core/Phases.scala @@ -0,0 +1,104 @@ +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.Phases.Phase +import dotc.core.Symbols.Symbol + +object Phases { + + class DocPhase extends Phase { + import model.comment.Comment + import model.CommentParsers.wikiParser + import model.Entities._ + import model.EntityFactories._ + import dotty.tools.dotc.core.Flags + import dotty.tools.dotc.ast.tpd._ + + def phaseName = "docphase" + + /** Build documentation hierarchy from existing tree */ + def collect(tree: Tree)(implicit ctx: Context): Entity = { + + def collectList(xs: List[Tree])(implicit ctx: Context): List[Entity] = + xs.map(collect).filter(_ != NonEntity) + + def collectPackageMembers(xs: List[Tree])(implicit ctx: Context): List[PackageMember] = + collectList(xs).asInstanceOf[List[PackageMember]] + + def collectMembers(tree: Tree)(implicit ctx: Context): List[Entity] = tree match { + case t: Template => collectList(t.body) + case _ => Nil + } + + val comment = + ctx.base.docstring(tree.symbol).map(c => Comment(wikiParser.parseHtml(c))) + + tree match { + /** package */ + case p @ PackageDef(pid, st) => + val name = pid.name.toString + Package(name, collectPackageMembers(st), comment, path(p, name)) + + /** trait */ + case t @ TypeDef(n, rhs) if t.symbol.is(Flags.Trait) => + val name = n.toString + Trait(name, collectMembers(rhs), comment, flags(t), path(t, name)) + + /** objects, on the format "Object$" so drop the last letter */ + case o @ TypeDef(n, rhs) if o.symbol.is(Flags.Module) => + val name = n.toString.dropRight(1) + Object(name, collectMembers(rhs), comment, flags(o), path(o, name)) + + /** class / case class */ + case c @ TypeDef(name, rhs) if c.symbol.isClass => + (name.toString, collectMembers(rhs), comment, flags(c), path(c, name.toString)) match { + case x if c.symbol.is(Flags.CaseClass) => CaseClass.tupled(x) + case x => Class.tupled(x) + } + + /** def */ + case d: DefDef => + Def(d.name.toString, comment, flags(d), path(d, d.name.toString)) + + /** val */ + case v: ValDef if !v.symbol.is(Flags.ModuleVal) => + Val(v.name.toString, comment, flags(v), path(v, v.name.toString)) + + case x => { + dottydoc.println(s"Found unwanted entity: $x (${x.pos}, ${comment})\n${x.show}") + NonEntity + } + } + } + + var packages: Map[String, Package] = Map.empty + + def addEntity(p: Package): Unit = { + val path = p.path.mkString(".") + packages = packages + (path -> packages.get(path).map { ex => + val children = (ex.children ::: p.children).distinct.sortBy(_.name) + val comment = ex.comment.orElse(p.comment) + Package(p.name, children, comment, p.path) + }.getOrElse(p)) + } + + override def run(implicit ctx: Context): Unit = + collect(ctx.compilationUnit.tpdTree) match { + case p: Package => addEntity(p) + case _ => () + } + + override def runOn(units: List[CompilationUnit])(implicit ctx: Context): List[CompilationUnit] = { + val compUnits = super.runOn(units) + util.IndexWriters.writeJs(packages, "../js/out") + compUnits + } + } + +} diff --git a/dottydoc/jvm/src/dotty/tools/dottydoc/model/CommentParsers.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/model/CommentParsers.scala new file mode 100644 index 000000000..dce8f4a69 --- /dev/null +++ b/dottydoc/jvm/src/dotty/tools/dottydoc/model/CommentParsers.scala @@ -0,0 +1,16 @@ +package dotty.tools +package dottydoc +package model + +object CommentParsers { + import comment._ + import BodyParsers._ + + sealed class WikiParser + extends CommentCleaner with CommentParser with CommentCooker { + def parseHtml(str: String): String = + parse(clean(str), str).toHtml + } + + val wikiParser = new WikiParser +} diff --git a/dottydoc/jvm/src/dotty/tools/dottydoc/model/Html.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/model/Html.scala new file mode 100644 index 000000000..4fb9168b7 --- /dev/null +++ b/dottydoc/jvm/src/dotty/tools/dottydoc/model/Html.scala @@ -0,0 +1,37 @@ +package dotty.tools.dottydoc +package model + +import scalatags.Text.all._ + +object Html { + import prickle._ + import Entities._ + + private def relPath(to: String, from: Entity) = + "../" * from.path.length + to + + def entityHtml(ent: Entity) = "<!DOCTYPE html>" + html( + head( + meta(charset := "utf-8"), + meta(name := "viewport", + content := "width=device-width, initial-scale=1, shrink-to-fit=no"), + meta("http-equiv".attr := "x-ua-compatible", content := "ie=edge"), + + script(`type` := "text/javascript", src := relPath("static/material.min.js", ent)), + script(`type` := "text/javascript", src := relPath("static/highlight.pack.js", ent)), + script(`type` := "text/javascript", src := relPath("index.js", ent)), + script(`type` := "text/javascript", src := relPath("target/scala-2.11/dottydoc-fastopt.js", ent)), + link(rel := "stylesheet", href := relPath("static/material.min.css", ent)), + link(rel := "stylesheet", href := relPath("static/github.css", ent)), + link(rel := "stylesheet", href := relPath("static/index.css", ent)), + link(rel := "stylesheet", href := "https://fonts.googleapis.com/icon?family=Material+Icons") + + ), + body(div(id := "main-container")), + script(raw( + s"""|Index.currentEntity = ${Pickle.intoString(ent)}; + |dotty.tools.dottydoc.js.DottyDocJS() + | .main(document.getElementById("main-container")); + """.stripMargin)) + ) +} diff --git a/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/BodyEntities.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/BodyEntities.scala new file mode 100644 index 000000000..e06789e98 --- /dev/null +++ b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/BodyEntities.scala @@ -0,0 +1,88 @@ +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[Inline] = { + 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(inline) + case inlines => Some(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 +//TODO: this should be used +//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 diff --git a/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/BodyParsers.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/BodyParsers.scala new file mode 100644 index 000000000..9b6ec600d --- /dev/null +++ b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/BodyParsers.scala @@ -0,0 +1,57 @@ +package dotty.tools.dottydoc +package model +package comment + +object BodyParsers { + implicit class BodyToHtml(val body: Body) extends AnyVal { + def toHtml: String = bodyToHtml(body) + + private def inlineToHtml(inl: Inline): String = inl match { + case Chain(items) => (items map inlineToHtml).mkString + case Italic(in) => s"<i>${inlineToHtml(in)}</i>" + case Bold(in) => s"<b>${inlineToHtml(in)}</b>" + case Underline(in) => s"<u>${inlineToHtml(in)}</u>" + case Superscript(in) => s"<sup>${inlineToHtml(in)}</sup>" + case Subscript(in) => s"<sub>${inlineToHtml(in) }</sub>" + case Link(raw, title) => s"""<a href=$raw target="_blank">${inlineToHtml(title)}</a>""" + case Monospace(in) => s"<code>${inlineToHtml(in)}</code>" + case Text(text) => text + case Summary(in) => inlineToHtml(in) + case HtmlTag(tag) => tag + //TODO: when we have EntityLinks, they should be enabled here too + //case EntityLink(target, link) => linkToHtml(target, link, hasLinks = true) + } + + private def bodyToHtml(body: Body): String = + (body.blocks map blockToHtml).mkString + + private 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/>" + } + + private 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>" + } + } + } +} diff --git a/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentCleaner.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentCleaner.scala new file mode 100644 index 000000000..5eb64c612 --- /dev/null +++ b/dottydoc/jvm/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 => + java.util.regex.Matcher.quoteReplacement(safeTagMarker + mtch.matched + safeTagMarker) + }) + markedTagComment.lines.toList map (cleanLine(_)) + } +} diff --git a/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentCooker.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentCooker.scala new file mode 100644 index 000000000..4d4474d52 --- /dev/null +++ b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentCooker.scala @@ -0,0 +1,8 @@ +package dotty.tools.dottydoc +package model +package comment + +trait CommentCooker { + trait Context + def cook(comment: String)(implicit ctx: Context): String = "" +} diff --git a/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentParser.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentParser.scala new file mode 100644 index 000000000..5ba05fbfe --- /dev/null +++ b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentParser.scala @@ -0,0 +1,779 @@ +package dotty.tools.dottydoc +package model +package comment + +import dotty.tools.dotc.util.Positions._ +import dotty.tools.dotc.core.Symbols._ +import scala.collection.mutable +import dotty.tools.dotc.config.Printers.dottydoc +import scala.util.matching.Regex + +//TODO: re-enable pos? +trait CommentParser { + import Regexes._ + + /** Parses a raw comment string into a `Comment` object. + * @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(comment: List[String], src: String, /*pos: Position,*/ site: Symbol = NoSymbol): Body = { + + /** 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 + ): Body = 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(_, /*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(key: SimpleTagKey): List[Body] = + (bodyTags remove key).getOrElse(Nil).filterNot(_.blocks.isEmpty) + + 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 (name,body) => + //val link = memberLookup(pos, name, site) + val newBody = body match { + case Body(List(Paragraph(Chain(content)))) => + val descr = Text(" ") +: content + //val entityLink = EntityLink(Monospace(Text(name)), link) + Body(List(Paragraph(Chain(/*entityLink +: */descr)))) + case _ => body + } + (name, newBody) + } + } + + // TODO: this method should return a parsed comment with the members below + //val com = createComment ( + // body0 = Some(parseWikiAtSymbol(docBody.toString, pos, site)), + // authors0 = allTags(SimpleTagKey("author")), + // see0 = allTags(SimpleTagKey("see")), + // result0 = oneTag(SimpleTagKey("return")), + // throws0 = linkedExceptions, + // valueParams0 = allSymsOneTag(SimpleTagKey("param")), + // typeParams0 = allSymsOneTag(SimpleTagKey("tparam")), + // version0 = oneTag(SimpleTagKey("version")), + // since0 = oneTag(SimpleTagKey("since")), + // todo0 = allTags(SimpleTagKey("todo")), + // deprecated0 = oneTag(SimpleTagKey("deprecated"), filterEmpty = false), + // note0 = allTags(SimpleTagKey("note")), + // example0 = allTags(SimpleTagKey("example")), + // constructor0 = oneTag(SimpleTagKey("constructor")), + // source0 = Some(clean(src).mkString("\n")), + // inheritDiagram0 = inheritDiagramText, + // contentDiagram0 = contentDiagramText, + // group0 = oneTag(SimpleTagKey("group")), + // groupDesc0 = allSymsOneTag(SimpleTagKey("groupdesc")), + // groupNames0 = allSymsOneTag(SimpleTagKey("groupname")), + // groupPrio0 = allSymsOneTag(SimpleTagKey("groupprio")), + // hideImplicitConversions0 = allTags(SimpleTagKey("hideImplicitConversion")), + // shortDescription0 = allTags(SimpleTagKey("shortDescription")) + //) + // + //for ((key, _) <- bodyTags) + // dottydoc.println(s"$pos: Tag '@${key.name}' is not recognised") + + parseWikiAtSymbol(docBody.toString, /*pos,*/ site) + } + } + + 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(string: String, /*pos: Position,*/ site: Symbol): Body = new WikiParser(string, /*pos,*/ site).document() + + /** TODO + * + * @author Ingo Maier + * @author Manohar Jonnalagedda + * @author Gilles Dubochet */ + protected final class WikiParser(val buffer: String, /*pos: Position,*/ site: Symbol) 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)]( // TODO Should this be defined at some list companion? + "- " -> ( 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) => + //TODO: this should be enabled + //makeEntityLink(optTitle getOrElse Text(target), pos, target, site) + title.getOrElse(Text("broken link")) + } + } + + /* 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") + dottydoc.println(s"$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/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentRegex.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/model/comment/CommentRegex.scala new file mode 100644 index 000000000..2d75b0c66 --- /dev/null +++ b/dottydoc/jvm/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/dottydoc/jvm/src/dotty/tools/dottydoc/util/IndexWriters.scala b/dottydoc/jvm/src/dotty/tools/dottydoc/util/IndexWriters.scala new file mode 100644 index 000000000..4f9a87949 --- /dev/null +++ b/dottydoc/jvm/src/dotty/tools/dottydoc/util/IndexWriters.scala @@ -0,0 +1,43 @@ +package dotty.tools.dottydoc +package util + +object IndexWriters { + import prickle._ + import model.Entities._ + import model.Html._ + + def writeJs(packs: Map[String, Package], outPath: String): Unit = { + for ((_, pack) <- packs) { + println(s"""Writing '${pack.path.mkString(".")}'""") + writeFile( + entityHtml(pack), + outPath + pack.path.tail.mkString("/", "/", "/"), + "index.html") + + for (child <- pack.children) { + println(s"""Writing '${child.path.mkString(".")}'""") + writeFile( + entityHtml(child), + outPath + child.path.dropRight(1).mkString("/", "/", "/"), + child.path.last + ".html") + } + } + + val pickled = Pickle.intoString(packs) + val js = "Index = {}; Index.packages = " + pickled + ";" + println("Writing index.js...") + writeFile(js, outPath + "/../", "index.js") + println("Done writing static material, building js-app") + + } + + def writeFile(str: String, path: String, file: String): Unit = { + def printToFile(f: java.io.File)(op: java.io.PrintWriter => Unit) { + val p = new java.io.PrintWriter(f) + try { op(p) } finally { p.close() } + } + + new java.io.File(path).mkdirs() + printToFile(new java.io.File(path + file))(_.println(str)) + } +} |