aboutsummaryrefslogtreecommitdiff
path: root/doc-tool
diff options
context:
space:
mode:
authorFelix Mulder <felix.mulder@gmail.com>2017-02-01 19:37:39 +0100
committerFelix Mulder <felix.mulder@gmail.com>2017-02-01 19:37:39 +0100
commite64c2e2d01cf30fe843fa3d4eff977c8b1ecccc4 (patch)
tree666cddc172ff213008e5841ace6dc62acf7e26b0 /doc-tool
parentdbbb7a3d9a668bbb8b62bec38f065f2444dacb91 (diff)
downloaddotty-e64c2e2d01cf30fe843fa3d4eff977c8b1ecccc4.tar.gz
dotty-e64c2e2d01cf30fe843fa3d4eff977c8b1ecccc4.tar.bz2
dotty-e64c2e2d01cf30fe843fa3d4eff977c8b1ecccc4.zip
Add position based error reporting to dottydoc
Diffstat (limited to 'doc-tool')
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/ContextDottydoc.scala29
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/core/DocASTPhase.scala4
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/comment/Comment.scala7
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/model/comment/CommentParser.scala10
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/staticsite/BlogPost.scala11
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/staticsite/Page.scala101
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/staticsite/Site.scala116
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/staticsite/Template.scala72
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/staticsite/tags.scala74
-rw-r--r--doc-tool/src/dotty/tools/dottydoc/util/syntax.scala11
-rw-r--r--doc-tool/test/SourceFileOps.scala32
-rw-r--r--doc-tool/test/TemplateErrorTests.scala32
-rw-r--r--doc-tool/test/dotty/tools/dottydoc/staticsite/PageTests.scala45
-rw-r--r--doc-tool/test/dotty/tools/dottydoc/staticsite/SiteTests.scala26
14 files changed, 374 insertions, 196 deletions
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/ContextDottydoc.scala b/doc-tool/src/dotty/tools/dottydoc/core/ContextDottydoc.scala
index c60038836..16f0776fa 100644
--- a/doc-tool/src/dotty/tools/dottydoc/core/ContextDottydoc.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/core/ContextDottydoc.scala
@@ -6,6 +6,10 @@ import dotc.core.Symbols.Symbol
import dotc.core.Comments.ContextDocstrings
import model.Package
+import dotc.core.Contexts.Context
+import dotc.printing.Highlighting._
+import dotc.util.{ SourcePosition, NoSourcePosition }
+
class ContextDottydoc extends ContextDocstrings {
import scala.collection.mutable
@@ -20,4 +24,29 @@ class ContextDottydoc extends ContextDocstrings {
def addDef(s: Symbol, d: Symbol): Unit = _defs = (_defs + {
s -> _defs.get(s).map(xs => xs + d).getOrElse(Set(d))
})
+
+ def error(msg: String, pos: SourcePosition)(implicit ctx: Context): Unit = ctx.error({
+ NoColor("[") + Red("doc error") + "] " + msg
+ }.toString, pos)
+
+ def error(msg: String)(implicit ctx: Context): Unit = error(msg, NoSourcePosition)
+
+ def warn(msg: String, pos: SourcePosition)(implicit ctx: Context): Unit = ctx.warning({
+ NoColor("[") + Yellow("doc warn") + "] " + msg
+ }.toString, pos)
+
+ def warn(msg: String)(implicit ctx: Context): Unit = warn(msg, NoSourcePosition)
+
+ def echo(msg: String, pos: SourcePosition)(implicit ctx: Context): Unit = ctx.echo({
+ "[doc info] " + msg
+ }.toString, pos)
+
+ def echo(msg: String)(implicit ctx: Context): Unit = echo(msg, NoSourcePosition)
+
+ def debug(msg: String, pos: SourcePosition)(implicit ctx: Context): Unit =
+ if (ctx.settings.debug.value) ctx.inform({
+ "[doc debug] " + msg
+ }.toString, pos)
+
+ def debug(msg: String)(implicit ctx: Context): Unit = debug(msg, NoSourcePosition)
}
diff --git a/doc-tool/src/dotty/tools/dottydoc/core/DocASTPhase.scala b/doc-tool/src/dotty/tools/dottydoc/core/DocASTPhase.scala
index 36b9db93c..460566838 100644
--- a/doc-tool/src/dotty/tools/dottydoc/core/DocASTPhase.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/core/DocASTPhase.scala
@@ -124,7 +124,7 @@ class DocASTPhase extends Phase {
ValImpl(v.symbol, annotations(v.symbol), v.name.decode.toString, flags(v), path(v.symbol), returnType(v.tpt.tpe), kind)
case x => {
- //dottydoc.println(s"Found unwanted entity: $x (${x.pos},\n${x.show}")
+ ctx.docbase.debug(s"Found unwanted entity: $x (${x.pos},\n${x.show}")
NonEntity
}
}
@@ -226,7 +226,7 @@ class DocASTPhase extends Phase {
override def run(implicit ctx: Context): Unit = {
currentRun += 1
- println(s"Compiling ($currentRun/$totalRuns): ${ctx.compilationUnit.source.file.name}")
+ ctx.docbase.echo(s"Compiling ($currentRun/$totalRuns): ${ctx.compilationUnit.source.file.name}")
collect(ctx.compilationUnit.tpdTree) // Will put packages in `packages` var
}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/comment/Comment.scala b/doc-tool/src/dotty/tools/dottydoc/model/comment/Comment.scala
index 1d98ff3f6..5b6c34de8 100644
--- a/doc-tool/src/dotty/tools/dottydoc/model/comment/Comment.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/model/comment/Comment.scala
@@ -11,6 +11,8 @@ import com.vladsch.flexmark.ast.{ Node => MarkdownNode }
import HtmlParsers._
import util.MemberLookup
+import dotc.util.SourceFile
+
case class Comment (
body: String,
short: String,
@@ -73,7 +75,10 @@ trait MarkupConversion[T] extends MemberLookup {
private def single(annot: String, xs: List[String], filter: Boolean = true)(implicit ctx: Context): Option[T] =
(if (filter) filterEmpty(xs) else xs.map(stringToMarkup)) match {
case x :: xs =>
- if (xs.nonEmpty) dottydoc.println(s"Only allowed to have a single annotation for $annot")
+ if (xs.nonEmpty) ctx.docbase.warn(
+ s"Only allowed to have a single annotation for $annot",
+ ent.symbol.sourcePosition(pos)
+ )
Some(x)
case _ => None
}
diff --git a/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentParser.scala b/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentParser.scala
index 3c389af1e..b7a33a7ef 100644
--- a/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentParser.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/model/comment/CommentParser.scala
@@ -195,8 +195,14 @@ trait CommentParser extends util.MemberLookup {
shortDescription = allTags(SimpleTagKey("shortDescription"))
)
- for ((key, _) <- bodyTags)
- dottydoc.println(s"$pos: Tag '@${key.name}' is not recognised")
+ for ((key, _) <- bodyTags) ctx.docbase.warn(
+ s"Tag '@${key.name}' is not recognised",
+ // FIXME: here the position is stretched out over the entire comment,
+ // with the point being at the very end. This ensures that the entire
+ // comment will be visible in error reporting. A more fine-grained
+ // reporting would be amazing here.
+ entity.symbol.sourcePosition(Position(pos.start, pos.end, pos.end))
+ )
cmt
}
diff --git a/doc-tool/src/dotty/tools/dottydoc/staticsite/BlogPost.scala b/doc-tool/src/dotty/tools/dottydoc/staticsite/BlogPost.scala
index f68157e40..9268199ca 100644
--- a/doc-tool/src/dotty/tools/dottydoc/staticsite/BlogPost.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/staticsite/BlogPost.scala
@@ -5,7 +5,8 @@ package staticsite
import java.io.{ File => JFile }
import java.util.{ List => JList, Map => JMap }
-import dotc.config.Printers.dottydoc
+import dotc.core.Contexts.Context
+import util.syntax._
import MapOperations._
@@ -42,9 +43,9 @@ class BlogPost(
object BlogPost {
val extract = """(\d\d\d\d)-(\d\d)-(\d\d)-(.*)\.(md|html)""".r
- def apply(file: JFile, page: Page): BlogPost = {
+ def apply(file: JFile, page: Page)(implicit ctx: Context): Option[BlogPost] = {
def report(key: String, fallback: String = "") = {
- /*dottydoc.*/println(s"couldn't find page.$key in ${file.getName}")
+ ctx.docbase.error(s"couldn't find page.$key in ${file.getName}")
fallback
}
@@ -56,6 +57,8 @@ object BlogPost {
val excerptSep = page.yaml.getString("excerpt_separator")
val categories = page.yaml.list("categories")
- new BlogPost(title, url, date, page.html, page.firstParagraph, excerptSep, categories)
+ page.html.map { html =>
+ new BlogPost(title, url, date, html, page.firstParagraph, excerptSep, categories)
+ }
}
}
diff --git a/doc-tool/src/dotty/tools/dottydoc/staticsite/Page.scala b/doc-tool/src/dotty/tools/dottydoc/staticsite/Page.scala
index fda41a234..4cbb57705 100644
--- a/doc-tool/src/dotty/tools/dottydoc/staticsite/Page.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/staticsite/Page.scala
@@ -8,9 +8,12 @@ import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.ext.front.matter.AbstractYamlFrontMatterVisitor
import java.util.{ Map => JMap, List => JList }
+import java.io.{ OutputStreamWriter, BufferedWriter }
-import dotc.config.Printers.dottydoc
+import io.VirtualFile
+import dotc.core.Contexts.Context
import model.Package
+import scala.io.Codec
/** When the YAML front matter cannot be parsed, this exception is thrown */
case class IllegalFrontMatter(message: String) extends Exception(message)
@@ -34,49 +37,62 @@ trait Page {
def path: String
/** YAML front matter from the top of the file */
- def yaml: Map[String, AnyRef] = {
- if (_yaml eq null) initFields()
+ def yaml(implicit ctx: Context): Map[String, AnyRef] = {
+ if (_yaml eq null) initFields
_yaml
}
/** HTML generated from page */
- def html: String = {
- if (_html eq null) initFields()
+ def html(implicit ctx: Context): Option[String] = {
+ if (_html eq null) initFields
_html
}
/** First paragraph of page extracted from rendered HTML */
- def firstParagraph: String = {
- if (_html eq null) initFields()
-
- val sb = new StringBuilder
- var pos = 0
- // to handle nested paragraphs in non markdown code
- var open = 0
-
- while (pos < _html.length - 4) {
- val str = _html.substring(pos, pos + 4)
- val lstr = str.toLowerCase
- sb append str.head
-
- pos += 1
- if (lstr.contains("<p>"))
- open += 1
- else if (lstr == "</p>") {
- open -= 1
- if (open == 0) {
- pos = Int.MaxValue
- sb append "/p>"
+ def firstParagraph(implicit ctx: Context): String = {
+ if (_html eq null) initFields
+
+ _html.map { _html =>
+ val sb = new StringBuilder
+ var pos = 0
+ // to handle nested paragraphs in non markdown code
+ var open = 0
+
+ while (pos < _html.length - 4) {
+ val str = _html.substring(pos, pos + 4)
+ val lstr = str.toLowerCase
+ sb append str.head
+
+ pos += 1
+ if (lstr.contains("<p>"))
+ open += 1
+ else if (lstr == "</p>") {
+ open -= 1
+ if (open == 0) {
+ pos = Int.MaxValue
+ sb append "/p>"
+ }
}
}
+
+ sb.toString
}
+ .getOrElse("")
+ }
+
+ protected def virtualFile(subSource: String): SourceFile = {
+ val virtualFile = new VirtualFile(path, path)
+ val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8"))
+ writer.write(subSource)
+ writer.close()
- sb.toString
+ new SourceFile(virtualFile, Codec.UTF8)
}
+
protected[this] var _yaml: Map[String, AnyRef /* String | JList[String] */] = _
- protected[this] var _html: String = _
- protected[this] def initFields() = {
+ protected[this] var _html: Option[String] = _
+ protected[this] def initFields(implicit ctx: Context) = {
val md = Parser.builder(Site.markdownOptions).build.parse(content)
val yamlCollector = new AbstractYamlFrontMatterVisitor()
yamlCollector.visit(md)
@@ -96,7 +112,7 @@ trait Page {
}
// YAML must start with "---" and end in either "---" or "..."
- val withoutYaml =
+ val withoutYaml = virtualFile(
if (content.startsWith("---\n")) {
val str =
content.lines
@@ -108,6 +124,7 @@ trait Page {
else str
}
else content
+ )
// make accessible via "{{ page.title }}" in templates
val page = Map("page" -> _yaml.asJava)
@@ -144,16 +161,18 @@ class MarkdownPage(
docs: Map[String, Package]
) extends Page {
- override protected[this] def initFields() = {
- super.initFields()
- val md = Parser.builder(Site.markdownOptions).build.parse(_html)
- // fix markdown linking
- MarkdownLinkVisitor(md, docs, params)
- MarkdownCodeBlockVisitor(md)
- _html = HtmlRenderer
- .builder(Site.markdownOptions)
- .escapeHtml(false)
- .build()
- .render(md)
+ override protected[this] def initFields(implicit ctx: Context) = {
+ super.initFields
+ _html = _html.map { _html =>
+ val md = Parser.builder(Site.markdownOptions).build.parse(_html)
+ // fix markdown linking
+ MarkdownLinkVisitor(md, docs, params)
+ MarkdownCodeBlockVisitor(md)
+ HtmlRenderer
+ .builder(Site.markdownOptions)
+ .escapeHtml(false)
+ .build()
+ .render(md)
+ }
}
}
diff --git a/doc-tool/src/dotty/tools/dottydoc/staticsite/Site.scala b/doc-tool/src/dotty/tools/dottydoc/staticsite/Site.scala
index 05ec113e0..6f1681a0a 100644
--- a/doc-tool/src/dotty/tools/dottydoc/staticsite/Site.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/staticsite/Site.scala
@@ -4,10 +4,9 @@ package staticsite
import java.nio.file.{ Files, FileSystems }
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
-import java.io.{ File => JFile, OutputStreamWriter, BufferedWriter }
+import java.io.{ File => JFile, OutputStreamWriter, BufferedWriter, ByteArrayInputStream }
import java.util.{ List => JList, Map => JMap, Arrays }
import java.nio.file.Path
-import java.io.ByteArrayInputStream
import java.nio.charset.StandardCharsets
import com.vladsch.flexmark.parser.ParserEmulationProfile
@@ -21,13 +20,13 @@ import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension
import com.vladsch.flexmark.ext.front.matter.YamlFrontMatterExtension
import com.vladsch.flexmark.util.options.{ DataHolder, MutableDataSet }
-import dotc.config.Printers.dottydoc
import dotc.core.Contexts.Context
import dotc.util.SourceFile
import model.Package
import scala.io.{ Codec, Source }
import io.{ AbstractFile, VirtualFile, File }
import scala.collection.mutable.ArrayBuffer
+import util.syntax._
case class Site(val root: JFile, val projectTitle: String, val documentation: Map[String, Package]) extends ResourceFinder {
/** Documentation serialized to java maps */
@@ -42,8 +41,8 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
* @note files that are *not* considered static are files ending in a compilable
* extension.
*/
- def staticAssets: Array[JFile] = {
- if (_staticAssets eq null) initFiles()
+ def staticAssets(implicit ctx: Context): Array[JFile] = {
+ if (_staticAssets eq null) initFiles
_staticAssets
}
@@ -53,8 +52,8 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
*
* @note files that are considered compilable end in `.md` or `.html`
*/
- def compilableFiles: Array[JFile] = {
- if (_compilableFiles eq null) initFiles()
+ def compilableFiles(implicit ctx: Context): Array[JFile] = {
+ if (_compilableFiles eq null) initFiles
_compilableFiles
}
@@ -66,12 +65,12 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
*
* where `ext` is either markdown or html.
*/
- def blogposts: Array[JFile] = {
- if (_blogposts eq null) initFiles()
+ def blogposts(implicit ctx: Context): Array[JFile] = {
+ if (_blogposts eq null) initFiles
_blogposts
}
- /** TODO */
+ /** Sidebar created from `sidebar.yml` file in site root */
val sidebar: Sidebar =
root
.listFiles
@@ -81,24 +80,32 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
.flatMap(Sidebar.apply)
.getOrElse(Sidebar.empty)
- protected lazy val blogInfo: Array[BlogPost] =
- blogposts
- .map { file =>
- val BlogPost.extract(year, month, day, name, ext) = file.getName
- val sourceFile = toSourceFile(file)
- val params = defaultParams(file, 2).withUrl(s"/blog/$year/$month/$day/$name.html").toMap
- val page =
- if (ext == "md")
- new MarkdownPage(file.getPath, sourceFile, params, includes, documentation)
- else new HtmlPage(file.getPath, sourceFile, params, includes)
- BlogPost(file, page)
+ private[this] var _blogInfo: Array[BlogPost] = _
+ protected def blogInfo(implicit ctx: Context): Array[BlogPost] = {
+ if (_blogInfo eq null) {
+ _blogInfo =
+ blogposts
+ .flatMap { file =>
+ val BlogPost.extract(year, month, day, name, ext) = file.getName
+ val sourceFile = toSourceFile(file)
+ val params = defaultParams(file, 2).withUrl(s"/blog/$year/$month/$day/$name.html").toMap
+ val page =
+ if (ext == "md")
+ new MarkdownPage(file.getPath, sourceFile, params, includes, documentation)
+ else new HtmlPage(file.getPath, sourceFile, params, includes)
+ BlogPost(file, page)
+ }
+ .sortBy(_.date)
+ .reverse
}
- .sortBy(_.date)
- .reverse
+
+ _blogInfo
+ }
// FileSystem getter
private[this] val fs = FileSystems.getDefault
+ /** Create virtual file from string `sourceCode` */
private def stringToSourceFile(name: String, path: String, sourceCode: String): SourceFile = {
val virtualFile = new VirtualFile(name, path)
val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8"))
@@ -108,10 +115,9 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
new SourceFile(virtualFile, Codec.UTF8)
}
- def copyStaticFiles(outDir: JFile = new JFile(root.getAbsolutePath + "/_site")): this.type = {
- if (!outDir.isDirectory) outDir.mkdirs()
- if (!outDir.isDirectory) /*dottydoc.*/println(s"couldn't create output folder: $outDir")
- else {
+ /** Copy static files to `outDir` */
+ def copyStaticFiles(outDir: JFile = new JFile(root.getAbsolutePath + "/_site"))(implicit ctx: Context): this.type =
+ createOutput (outDir) {
// Copy user-defined static assets
staticAssets.foreach { asset =>
val target = mkdirs(fs.getPath(outDir.getAbsolutePath, stripRoot(asset)))
@@ -133,9 +139,8 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
Files.copy(source, target, REPLACE_EXISTING)
}
}
- this
- }
+ /** Generate default params included in each page */
private def defaultParams(pageLocation: JFile, additionalDepth: Int = 0): DefaultParams = {
import scala.collection.JavaConverters._
val pathFromRoot = stripRoot(pageLocation)
@@ -148,9 +153,10 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
DefaultParams(docs, documentation, PageInfo(pathFromRoot), SiteInfo(baseUrl, projectTitle, Array()), sidebar)
}
- private def createOutput(outDir: JFile)(op: => Unit): this.type = {
+ /* Creates output directories if allowed */
+ private def createOutput(outDir: JFile)(op: => Unit)(implicit ctx: Context): this.type = {
if (!outDir.isDirectory) outDir.mkdirs()
- if (!outDir.isDirectory) /*dottydoc.*/println(s"couldn't create output folder: $outDir")
+ if (!outDir.isDirectory) ctx.docbase.error(s"couldn't create output folder: $outDir")
else op
this
}
@@ -159,7 +165,7 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
def generateApiDocs(outDir: JFile = new JFile(root.getAbsolutePath + "/_site"))(implicit ctx: Context): this.type =
createOutput(outDir) {
def genDoc(e: model.Entity): Unit = {
- /*dottydoc.*/println(s"Generating doc page for: ${e.path.mkString(".")}")
+ ctx.docbase.echo(s"Generating doc page for: ${e.path.mkString(".")}")
// Suffix is index.html for packages and therefore the additional depth
// is increased by 1
val (suffix, offset) =
@@ -170,10 +176,10 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
val params = defaultParams(target.toFile, -1).withPosts(blogInfo).withEntity(e).toMap
val page = new HtmlPage("_layouts/api-page.html", layouts("api-page").content, params, includes)
- val rendered = render(page)
- val source = new ByteArrayInputStream(rendered.getBytes(StandardCharsets.UTF_8))
-
- Files.copy(source, target, REPLACE_EXISTING)
+ render(page).foreach { rendered =>
+ val source = new ByteArrayInputStream(rendered.getBytes(StandardCharsets.UTF_8))
+ Files.copy(source, target, REPLACE_EXISTING)
+ }
// Generate docs for nested objects/classes:
e.children.foreach(genDoc)
@@ -196,14 +202,16 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
if (asset.getName.endsWith(".md")) new MarkdownPage(pathFromRoot, sourceFile, params, includes, documentation)
else new HtmlPage(pathFromRoot, sourceFile, params, includes)
- val renderedPage = render(page)
- val source = new ByteArrayInputStream(renderedPage.getBytes(StandardCharsets.UTF_8))
- val target = pathFromRoot.splitAt(pathFromRoot.lastIndexOf('.'))._1 + ".html"
- val htmlTarget = mkdirs(fs.getPath(outDir.getAbsolutePath, target))
- Files.copy(source, htmlTarget, REPLACE_EXISTING)
+ render(page).foreach { renderedPage =>
+ val source = new ByteArrayInputStream(renderedPage.getBytes(StandardCharsets.UTF_8))
+ val target = pathFromRoot.splitAt(pathFromRoot.lastIndexOf('.'))._1 + ".html"
+ val htmlTarget = mkdirs(fs.getPath(outDir.getAbsolutePath, target))
+ Files.copy(source, htmlTarget, REPLACE_EXISTING)
+ }
}
}
+ /** Generate blog from files in `blog/_posts` and output in `outDir` */
def generateBlog(outDir: JFile = new JFile(root.getAbsolutePath + "/_site"))(implicit ctx: Context): this.type =
createOutput(outDir) {
blogposts.foreach { file =>
@@ -221,17 +229,19 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
else
new HtmlPage(target.toString, sourceFile, params, includes)
-
- val source = new ByteArrayInputStream(render(page).getBytes(StandardCharsets.UTF_8))
- Files.copy(source, target, REPLACE_EXISTING)
+ render(page).map { rendered =>
+ val source = new ByteArrayInputStream(rendered.getBytes(StandardCharsets.UTF_8))
+ Files.copy(source, target, REPLACE_EXISTING)
+ }
}
}
- private def mkdirs(path: Path): path.type = {
+ /** Create directories and issue an error if could not */
+ private def mkdirs(path: Path)(implicit ctx: Context): path.type = {
val parent = path.getParent.toFile
if (!parent.isDirectory && !parent.mkdirs())
- dottydoc.println(s"couldn't create directory: $parent")
+ ctx.docbase.error(s"couldn't create directory: $parent")
path
}
@@ -254,14 +264,14 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
private[this] var _compilableFiles: Array[JFile] = _
private[this] var _blogposts: Array[JFile] = _
- private[this] def initFiles() = {
+ private[this] def initFiles(implicit ctx: Context) = {
// Split files between compilable and static assets
def splitFiles(f: JFile, assets: ArrayBuffer[JFile], comp: ArrayBuffer[JFile]): Unit = {
val name = f.getName
if (f.isDirectory) {
val name = f.getName
if (!name.startsWith("_") && name != "api") f.listFiles.foreach(splitFiles(_, assets, comp))
- if (f.getName == "api") dottydoc.println {
+ if (f.getName == "api") ctx.docbase.warn {
"the specified `/api` directory will not be used since it is needed for the api documentation"
}
}
@@ -369,15 +379,15 @@ case class Site(val root: JFile, val projectTitle: String, val documentation: Ma
/** Render a page to html, the resulting string is the result of the complete
* expansion of the template with all its layouts and includes.
*/
- def render(page: Page, params: Map[String, AnyRef] = Map.empty)(implicit ctx: Context): String =
+ def render(page: Page, params: Map[String, AnyRef] = Map.empty)(implicit ctx: Context): Option[String] =
page.yaml.get("layout").flatMap(xs => layouts.get(xs.toString)) match {
- case None =>
- page.html
- case Some(layout) =>
+ case Some(layout) if page.html.isDefined =>
import scala.collection.JavaConverters._
- val newParams = page.params ++ params ++ Map("page" -> page.yaml) ++ Map("content" -> page.html)
+ val newParams = page.params ++ params ++ Map("page" -> page.yaml) ++ Map("content" -> page.html.get)
val expandedTemplate = new HtmlPage(layout.path, layout.content, newParams, includes)
render(expandedTemplate, params)
+ case _ =>
+ page.html
}
}
diff --git a/doc-tool/src/dotty/tools/dottydoc/staticsite/Template.scala b/doc-tool/src/dotty/tools/dottydoc/staticsite/Template.scala
index 00d51f083..9720084f3 100644
--- a/doc-tool/src/dotty/tools/dottydoc/staticsite/Template.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/staticsite/Template.scala
@@ -3,7 +3,11 @@ package dottydoc
package staticsite
import scala.util.control.NonFatal
+
import dotc.util.SourceFile
+import dotc.core.Contexts.Context
+import dotc.util.Positions.{ Position, NoPosition }
+import util.syntax._
trait Template {
def path: String
@@ -18,8 +22,9 @@ case class Layout(path: String, content: SourceFile) extends Template
case class Include(path: String, content: SourceFile) extends Template
-case class LiquidTemplate(path: String, content: String) extends ResourceFinder {
+case class LiquidTemplate(path: String, content: SourceFile) extends Template with ResourceFinder {
import scala.collection.JavaConverters._
+ import dotc.printing.Highlighting._
import liqp.Template
import liqp.filters.Filter
import liqp.parser.Flavor.JEKYLL
@@ -38,16 +43,69 @@ case class LiquidTemplate(path: String, content: String) extends ResourceFinder
map
}
- def render(params: Map[String, AnyRef], includes: Map[String, Include]): String =
- try {
- Template.parse(content, JEKYLL)
+ private def protectedRender(op: => String)(implicit ctx: Context) = try {
+ Some(op)
+ } catch {
+ case NonFatal(ex) => {
+ // TODO: when we reimplement the liquid parser, this can go away. For now
+ // this is an OK approximation of what went wrong.
+ if ((ex.getCause eq null) || ex.getMessage.contains("exceeded the max amount of time")) {
+ ctx.docbase.error(
+ "unknown error occurred in " +
+ Blue(path).toString +
+ ", most likely incorrect usage of tag"
+ )
+ None
+ }
+ else ex.getCause match {
+ case mm: org.antlr.runtime.MismatchedTokenException => {
+ val unexpected = LiquidTemplate.token(mm.getUnexpectedType)
+ val expected = LiquidTemplate.token(mm.expecting)
+
+ ctx.error(
+ if (unexpected == "EOF")
+ s"unexpected end of file, expected: '$expected'"
+ else
+ s"unexpected token '$unexpected', expected: '$expected'",
+ content atPos Position(mm.index)
+ )
+
+ None
+ }
+ case ex => {
+ if (true || ctx.settings.debug.value)
+ throw ex
+
+ None
+ }
+ }
+ }
+ }
+
+ def render(params: Map[String, AnyRef], includes: Map[String, Include])(implicit ctx: Context): Option[String] =
+ protectedRender {
+ Template.parse(show, JEKYLL)
.`with`(ResourceInclude(params, includes))
.`with`(RenderReference(params))
.`with`(RenderTitle(params))
.`with`(Docstring(params))
.render(toJavaMap(params))
- } catch {
- case NonFatal(ex) =>
- throw TemplateRenderingError(path, ex)
}
}
+
+object LiquidTemplate {
+ import liqp.parser.LiquidParser
+
+ private val _tokens: Map[String, String] = Map(
+ "TagStart" -> "{%",
+ "TagEnd" -> "%}"
+ )
+
+ def token(i: Int): String =
+ if (i == -1) "EOF"
+ else if (i >= LiquidParser.tokenNames.length)
+ "non-existing token"
+ else _tokens
+ .get(LiquidParser.tokenNames(i))
+ .getOrElse(s"token $i")
+}
diff --git a/doc-tool/src/dotty/tools/dottydoc/staticsite/tags.scala b/doc-tool/src/dotty/tools/dottydoc/staticsite/tags.scala
index 51b3b760f..cd2ea587d 100644
--- a/doc-tool/src/dotty/tools/dottydoc/staticsite/tags.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/staticsite/tags.scala
@@ -3,6 +3,7 @@ package dottydoc
package staticsite
import model.references._
+import dotc.core.Contexts.Context
import liqp.tags.Tag
import liqp.TemplateContext
@@ -10,30 +11,38 @@ import liqp.nodes.LNode
import java.util.{ Map => JMap }
import model._
+import util.syntax._
object tags {
sealed trait ParamConverter {
def params: Map[String, AnyRef]
- val baseurl: String =
- params.get("site").flatMap {
- case map: JMap[String, String] @unchecked =>
- Some(map.get("baseurl"))
- case _ =>
- None
- }
- .getOrElse {
- /*dottydoc.*/println(s"missing `baseurl` in: $params")
- ""
+ private[this] var _baseurl: String = _
+ def baseurl(implicit ctx: Context): String = {
+ if (_baseurl eq null) {
+ _baseurl =
+ params.get("site").flatMap {
+ case map: JMap[String, String] @unchecked =>
+ Some(map.get("baseurl"))
+ case _ =>
+ None
+ }
+ .getOrElse {
+ ctx.docbase.warn(s"missing `baseurl` in: $params")
+ ""
+ }
}
+ _baseurl
+ }
}
/** Renders a `MaterializableLink` into a HTML anchor tag. If the link is
* `NoLink` it will just return a string with the link's title.
*/
- final case class RenderLink(params: Map[String, AnyRef]) extends Tag("renderLink") with ParamConverter {
- override def render(ctx: TemplateContext, nodes: LNode*): AnyRef = nodes(0).render(ctx) match {
+ final case class RenderLink(params: Map[String, AnyRef])(implicit ctx: Context)
+ extends Tag("renderLink") with ParamConverter {
+ override def render(tctx: TemplateContext, nodes: LNode*): AnyRef = nodes(0).render(tctx) match {
case map: JMap[String, AnyRef] @unchecked =>
val link = map.get("scala")
if (link.isInstanceOf[MaterializableLink] && (link ne null))
@@ -41,7 +50,7 @@ object tags {
else if (link eq null)
null // Option[Reference] was None
else {
- /*dottydoc.*/println(s"illegal argument: $link, to `renderLink` function")
+ ctx.docbase.error(s"illegal argument: $link, to `renderLink` function")
null
}
case _ => null
@@ -49,13 +58,14 @@ object tags {
}
- private[this] def renderLink(baseurl: String, link: MaterializableLink): String = link match {
- case MaterializedLink(title, target) =>
- s"""<a href="$baseurl/api/$target">$title</a>"""
- case _ => link.title
- }
+ private[this] def renderLink(baseurl: String, link: MaterializableLink)(implicit ctx: Context): String =
+ link match {
+ case MaterializedLink(title, target) =>
+ s"""<a href="$baseurl/api/$target">$title</a>"""
+ case _ => link.title
+ }
- final case class RenderReference(params: Map[String, AnyRef])
+ final case class RenderReference(params: Map[String, AnyRef])(implicit ctx: Context)
extends Tag("renderRef") with ParamConverter {
private def renderReference(ref: Reference): String = ref match {
@@ -90,31 +100,31 @@ object tags {
s"""${ renderReference(low) }<span class="bounds"> &lt;: </span>${ renderReference(high) }"""
case NamedReference(title, _, _, _) =>
- /*dottydoc.*/println(s"received illegal named reference in rendering: $ref")
+ ctx.docbase.error(s"received illegal named reference in rendering: $ref")
title
case ConstantReference(title) => title
}
- override def render(ctx: TemplateContext, nodes: LNode*): AnyRef = nodes(0).render(ctx) match {
+ override def render(tctx: TemplateContext, nodes: LNode*): AnyRef = nodes(0).render(tctx) match {
case map: JMap[String, AnyRef] @unchecked =>
val ref = map.get("scala")
if (ref.isInstanceOf[Reference] && (ref ne null)) renderReference(ref.asInstanceOf[Reference])
else if (ref eq null) null // Option[Reference] was None
else {
- /*dottydoc.*/println(s"illegal argument: $ref, to `renderRef` function")
+ ctx.docbase.error(s"illegal argument: $ref, to `renderRef` function")
null
}
case _ => null
}
}
- case class ResourceInclude(params: Map[String, AnyRef], includes: Map[String, Include])
+ case class ResourceInclude(params: Map[String, AnyRef], includes: Map[String, Include])(implicit ctx: Context)
extends Tag("include") {
import scala.collection.JavaConverters._
val DefaultExtension = ".html"
- override def render(ctx: TemplateContext, nodes: LNode*): AnyRef = {
- val origInclude = asString(nodes(0).render(ctx))
+ override def render(tctx: TemplateContext, nodes: LNode*): AnyRef = {
+ val origInclude = asString(nodes(0).render(tctx))
val incResource = origInclude match {
case fileWithExt if fileWithExt.indexOf('.') > 0 => fileWithExt
case file => file + DefaultExtension
@@ -123,13 +133,14 @@ object tags {
includes
.get(incResource)
.map { template =>
- if (nodes.length > 1) ctx.put(origInclude, nodes(1).render(ctx))
+ if (nodes.length > 1) tctx.put(origInclude, nodes(1).render(tctx))
- LiquidTemplate(template.path, template.show)
- .render(ctx.getVariables.asScala.toMap, includes)
+ LiquidTemplate(template.path, template.content)
+ .render(tctx.getVariables.asScala.toMap, includes)
+ .getOrElse("")
}
.getOrElse {
- /*dottydoc.*/println(s"couldn't find include file '$origInclude'")
+ ctx.docbase.error(s"couldn't find include file '$origInclude'")
""
}
}
@@ -145,7 +156,8 @@ object tags {
* The rendering currently works on depths up to 2. This means that each
* title can have a subsection with its own titles.
*/
- case class RenderTitle(params: Map[String, AnyRef]) extends Tag("renderTitle") with ParamConverter {
+ case class RenderTitle(params: Map[String, AnyRef])(implicit ctx: Context)
+ extends Tag("renderTitle") with ParamConverter {
private def renderTitle(t: Title, parent: String): String = {
if (!t.url.isDefined && t.subsection.nonEmpty) {
val onclickFunction =
@@ -160,7 +172,7 @@ object tags {
s"""<a href="$baseurl/$url">${t.title}</a>"""
}
else /*if (t.subsection.nonEmpty)*/ {
- /*dottydoc.*/println(s"url was defined for subsection with title: ${t.title}, remove url to get toggleable entries")
+ ctx.docbase.error(s"url was defined for subsection with title: ${t.title}, remove url to get toggleable entries")
t.title
}
}
diff --git a/doc-tool/src/dotty/tools/dottydoc/util/syntax.scala b/doc-tool/src/dotty/tools/dottydoc/util/syntax.scala
index dd3d21f8d..005545d67 100644
--- a/doc-tool/src/dotty/tools/dottydoc/util/syntax.scala
+++ b/doc-tool/src/dotty/tools/dottydoc/util/syntax.scala
@@ -6,6 +6,11 @@ import dotc.core.Contexts.Context
import dotc.core.Comments._
import model.Package
import core.ContextDottydoc
+import dotc.core.Symbols._
+
+import dotc.util.{ SourcePosition, SourceFile }
+import dotc.util.Positions.Position
+import scala.io.Codec
object syntax {
implicit class ContextWithContextDottydoc(val ctx: Context) extends AnyVal {
@@ -13,4 +18,10 @@ object syntax {
throw new IllegalStateException("DocBase must be set before running dottydoc phases")
}.asInstanceOf[ContextDottydoc]
}
+
+ implicit class SymbolExtensions(val sym: Symbol) extends AnyVal {
+ def sourcePosition(pos: Position)(implicit ctx: Context): SourcePosition =
+ new SourceFile(sym.sourceFile, Codec(ctx.settings.encoding.value)) atPos pos
+
+ }
}
diff --git a/doc-tool/test/SourceFileOps.scala b/doc-tool/test/SourceFileOps.scala
index 7b0c2e807..37520921d 100644
--- a/doc-tool/test/SourceFileOps.scala
+++ b/doc-tool/test/SourceFileOps.scala
@@ -7,7 +7,12 @@ import java.io.{ BufferedWriter, OutputStreamWriter }
import io.VirtualFile
import scala.io.Codec
+import model.Package
+
trait SourceFileOps {
+ import scala.collection.JavaConverters._
+ val site = new Site(new java.io.File("../doc-tool/resources/"), "test-site", Map.empty)
+
def stringToSource(path: String, sourceCode: String): SourceFile = {
val virtualFile = new VirtualFile(path, path)
val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8"))
@@ -16,4 +21,31 @@ trait SourceFileOps {
new SourceFile(virtualFile, Codec.UTF8)
}
+
+ def markdownPage(
+ sourceCode: String,
+ path: String = "test-page",
+ params: Map[String, AnyRef] = Map.empty,
+ includes: Map[String, Include] = Map.empty,
+ docs: Map[String, Package] = Map.empty
+ ) = new MarkdownPage(
+ path,
+ stringToSource(path, sourceCode),
+ params,
+ includes,
+ docs
+ )
+
+ def htmlPage(
+ sourceCode: String,
+ path: String = "test-page",
+ params: Map[String, AnyRef] = Map.empty,
+ includes: Map[String, Include] = Map.empty,
+ docs: Map[String, Package] = Map.empty
+ ) = new HtmlPage(
+ path,
+ stringToSource(path, sourceCode),
+ params,
+ includes
+ )
}
diff --git a/doc-tool/test/TemplateErrorTests.scala b/doc-tool/test/TemplateErrorTests.scala
new file mode 100644
index 000000000..3359c7791
--- /dev/null
+++ b/doc-tool/test/TemplateErrorTests.scala
@@ -0,0 +1,32 @@
+package dotty.tools
+package dottydoc
+package staticsite
+
+import org.junit.Test
+import org.junit.Assert._
+
+class TemplateErrorTests extends DottyDocTest with SourceFileOps {
+ @Test def unclosedTag: Unit = {
+ htmlPage(
+ """|Yo dawg:
+ |{% include "stuff"
+ |I heard you like to include stuff""".stripMargin
+ ).html
+ }
+
+ @Test def missingEndif: Unit = {
+ htmlPage(
+ """|{% if someStuff %}
+ |Dude
+ |""".stripMargin
+ ).html
+ }
+
+ @Test def nonExistingTag: Unit = {
+ htmlPage(
+ """|{% someStuff 'ofDude' %}
+ |Dude
+ |""".stripMargin
+ ).html
+ }
+}
diff --git a/doc-tool/test/dotty/tools/dottydoc/staticsite/PageTests.scala b/doc-tool/test/dotty/tools/dottydoc/staticsite/PageTests.scala
index 20a41e70b..7febe7fe5 100644
--- a/doc-tool/test/dotty/tools/dottydoc/staticsite/PageTests.scala
+++ b/doc-tool/test/dotty/tools/dottydoc/staticsite/PageTests.scala
@@ -5,38 +5,9 @@ package staticsite
import org.junit.Test
import org.junit.Assert._
-import model.Package
-
class PageTests extends DottyDocTest with SourceFileOps {
import scala.collection.JavaConverters._
- private def markdownPage(
- sourceCode: String,
- path: String = "test-page",
- params: Map[String, AnyRef] = Map.empty,
- includes: Map[String, Include] = Map.empty,
- docs: Map[String, Package] = Map.empty
- ) = new MarkdownPage(
- path,
- stringToSource(path, sourceCode),
- params,
- includes,
- docs
- )
-
- private def htmlPage(
- sourceCode: String,
- path: String = "test-page",
- params: Map[String, AnyRef] = Map.empty,
- includes: Map[String, Include] = Map.empty,
- docs: Map[String, Package] = Map.empty
- ) = new HtmlPage(
- path,
- stringToSource(path, sourceCode),
- params,
- includes
- )
-
@Test def mdHas1Key = {
val page = markdownPage(
"""|---
@@ -51,7 +22,7 @@ class PageTests extends DottyDocTest with SourceFileOps {
s"""incorrect yaml, expected "key:" without key in: ${page.yaml}"""
)
- assertEquals("<p>great</p>\n", page.html)
+ assertEquals("<p>great</p>\n", page.html.get)
}
@Test def yamlPreservesLiquidTags = {
@@ -69,7 +40,7 @@ class PageTests extends DottyDocTest with SourceFileOps {
s"""incorrect yaml, expected "key:" without key in: ${page1.yaml}"""
)
- assertEquals("<p>Hello, world!</p>\n", page1.html)
+ assertEquals("<p>Hello, world!</p>\n", page1.html.get)
val page2 = markdownPage(
"""|{{ content }}""".stripMargin,
@@ -79,7 +50,7 @@ class PageTests extends DottyDocTest with SourceFileOps {
page2.yaml == Map(),
s"""incorrect yaml, expected "key:" without key in: ${page2.yaml}"""
)
- assertEquals("<p>hello</p>\n", page2.html)
+ assertEquals("<p>hello</p>\n", page2.html.get)
val page3 = markdownPage(
"""|{% if product.title == "Awesome Shoes" %}
@@ -90,14 +61,14 @@ class PageTests extends DottyDocTest with SourceFileOps {
assertEquals(
"<p>These shoes are awesome!</p>\n",
- page3.html
+ page3.html.get
)
}
@Test def simpleHtmlPage = {
val p1 = htmlPage("""<h1>{{ "hello, world!" }}</h1>""")
assert(p1.yaml == Map(), "non-empty yaml found")
- assertEquals("<h1>hello, world!</h1>", p1.html)
+ assertEquals("<h1>hello, world!</h1>", p1.html.get)
}
@Test def htmlPageHasNoYaml = {
@@ -109,8 +80,8 @@ class PageTests extends DottyDocTest with SourceFileOps {
|Hello, world!""".stripMargin
)
- assert(!page.html.contains("---\nlayout: main\n---"),
- s"page still contains yaml:\n${page.html}")
+ assert(!page.html.get.contains("---\nlayout: main\n---"),
+ s"page still contains yaml:\n${page.html.get}")
}
@Test def illegalYamlFrontMatter = try {
@@ -122,7 +93,7 @@ class PageTests extends DottyDocTest with SourceFileOps {
|Hello, world!""".stripMargin
)
- page.html
+ page.html.get
fail("illegal front matter didn't throw exception")
} catch {
case IllegalFrontMatter(x) => // success!
diff --git a/doc-tool/test/dotty/tools/dottydoc/staticsite/SiteTests.scala b/doc-tool/test/dotty/tools/dottydoc/staticsite/SiteTests.scala
index 77b49700c..a4279e18c 100644
--- a/doc-tool/test/dotty/tools/dottydoc/staticsite/SiteTests.scala
+++ b/doc-tool/test/dotty/tools/dottydoc/staticsite/SiteTests.scala
@@ -6,16 +6,6 @@ import org.junit.Test
import org.junit.Assert._
class SiteTests extends DottyDocTest with SourceFileOps {
- import scala.collection.JavaConverters._
- val site = new Site(new java.io.File("../doc-tool/resources/"), "test-site", Map.empty)
-
- private def html(
- str: String,
- path: String = "test-page",
- params: Map[String, AnyRef] = Map("docs" -> List.empty.asJava),
- includes: Map[String, Include] = Map.empty
- ) = new HtmlPage(path, stringToSource(path, str), params, includes)
-
@Test def hasCorrectLayoutFiles = {
assert(site.root.exists && site.root.isDirectory,
s"'${site.root.getName}' is not a directory")
@@ -26,13 +16,13 @@ class SiteTests extends DottyDocTest with SourceFileOps {
}
@Test def renderHelloInMainLayout = {
- val renderedPage = site.render(html(
+ val renderedPage = site.render(htmlPage(
"""|---
|layout: main
|---
|
|Hello, world!""".stripMargin
- ), Map.empty)
+ ), Map.empty).get
assert(
renderedPage.contains("Hello, world!") &&
@@ -43,12 +33,12 @@ class SiteTests extends DottyDocTest with SourceFileOps {
}
@Test def renderMultipleTemplates = {
- val renderedPage = site.render(html(
+ val renderedPage = site.render(htmlPage(
"""|---
|layout: index
|---
|Hello, world!""".stripMargin
- ), Map.empty)
+ ), Map.empty).get
assert(
renderedPage.contains("<h1>Hello, world!</h1>") &&
@@ -60,13 +50,13 @@ class SiteTests extends DottyDocTest with SourceFileOps {
}
@Test def preservesPageYaml = {
- val renderedPage = site.render(html(
+ val renderedPage = site.render(htmlPage(
"""|---
|title: Hello, world
|layout: index
|---
|Hello, world!""".stripMargin
- ), Map.empty)
+ ), Map.empty).get
assert(
renderedPage.contains("<h1>Hello, world!</h1>") &&
@@ -80,9 +70,9 @@ class SiteTests extends DottyDocTest with SourceFileOps {
@Test def include = {
val renderedInclude = site.render(
- html("""{% include "header.html" %}""", includes = site.includes),
+ htmlPage("""{% include "header.html" %}""", includes = site.includes),
Map.empty
- )
+ ).get
assertEquals("<h1>Some header</h1>\n", renderedInclude)
}