diff options
Diffstat (limited to 'doc-tool')
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"> <: </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) } |