summaryrefslogtreecommitdiff
path: root/scalatexApi
diff options
context:
space:
mode:
authorLi Haoyi <haoyi@dropbox.com>2014-11-01 00:56:30 -0700
committerLi Haoyi <haoyi@dropbox.com>2014-11-01 00:56:30 -0700
commitaae594fd3c8397abca4cd4e55f538d41b172b4e3 (patch)
treecef34e2404599a8547d864f3d5c158b5297783f0 /scalatexApi
parent3d73267c4b3ecf3cdca54ded8dfd8a2caeeb3ca9 (diff)
downloadhands-on-scala-js-aae594fd3c8397abca4cd4e55f538d41b172b4e3.tar.gz
hands-on-scala-js-aae594fd3c8397abca4cd4e55f538d41b172b4e3.tar.bz2
hands-on-scala-js-aae594fd3c8397abca4cd4e55f538d41b172b4e3.zip
added everything
Diffstat (limited to 'scalatexApi')
-rw-r--r--scalatexApi/src/main/scala/scalatex/Util.scala10
-rw-r--r--scalatexApi/src/main/scala/scalatex/package.scala86
-rw-r--r--scalatexApi/src/main/scala/scalatex/stages/Compiler.scala161
-rw-r--r--scalatexApi/src/main/scala/scalatex/stages/IndentHandler.scala105
-rw-r--r--scalatexApi/src/main/scala/scalatex/stages/Parser.scala790
-rw-r--r--scalatexApi/src/test/scala/scalatex/AdvancedTests.scala120
-rw-r--r--scalatexApi/src/test/scala/scalatex/BasicTests.scala418
-rw-r--r--scalatexApi/src/test/scala/scalatex/ErrorTests.scala321
-rw-r--r--scalatexApi/src/test/scala/scalatex/ParserTests.scala50
-rw-r--r--scalatexApi/src/test/scala/scalatex/TestUtil.scala16
10 files changed, 2077 insertions, 0 deletions
diff --git a/scalatexApi/src/main/scala/scalatex/Util.scala b/scalatexApi/src/main/scala/scalatex/Util.scala
new file mode 100644
index 0000000..3bf9553
--- /dev/null
+++ b/scalatexApi/src/main/scala/scalatex/Util.scala
@@ -0,0 +1,10 @@
+package scalatex
+import acyclic.file
+
+object Util {
+
+ implicit class Pipeable[T](t: T){
+ def |>[V](f: T => V): V = f(t)
+ }
+}
+
diff --git a/scalatexApi/src/main/scala/scalatex/package.scala b/scalatexApi/src/main/scala/scalatex/package.scala
new file mode 100644
index 0000000..5732411
--- /dev/null
+++ b/scalatexApi/src/main/scala/scalatex/package.scala
@@ -0,0 +1,86 @@
+import scala.reflect.internal.util.{BatchSourceFile, SourceFile, OffsetPosition}
+import scala.reflect.io.{PlainFile, AbstractFile}
+import scala.reflect.macros.{TypecheckException, Context}
+import scalatags.Text.all._
+import scalatex.stages.Compiler
+import scala.language.experimental.macros
+import acyclic.file
+
+package object scalatex {
+ import Util._
+ /**
+ * Wraps the given string as a twist fragment.
+ */
+ def tw(expr: String): Frag = macro Internals.applyMacro
+ def twf(filename: String): Frag = macro Internals.applyMacroFile
+ object Internals {
+
+ def twDebug(expr: String): Frag = macro applyMacroDebug
+
+ def applyMacro(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, false)
+
+ def applyMacroDebug(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, true)
+
+ def applyMacroFile(c: Context)(filename: c.Expr[String]): c.Expr[Frag] = {
+ import c.universe._
+ val s = filename.tree
+ .asInstanceOf[Literal]
+ .value
+ .value
+ .asInstanceOf[String]
+ val txt = io.Source.fromFile(s).mkString |> stages.IndentHandler
+ val sourceFile = new BatchSourceFile(
+ new PlainFile(s),
+ txt.toCharArray
+ )
+
+ compileThing(c)(txt, sourceFile, 0, false)
+ }
+
+ case class DebugFailure(msg: String, pos: String) extends Exception(msg)
+
+ private[this] def applyMacroFull(c: Context)(expr: c.Expr[String], runtimeErrors: Boolean): c.Expr[Frag] = {
+ import c.universe._
+ val s = expr.tree
+ .asInstanceOf[Literal]
+ .value
+ .value
+ .asInstanceOf[String]
+ val stringStart =
+ expr.tree
+ .pos
+ .lineContent
+ .drop(expr.tree.pos.column)
+ .take(2)
+ compileThing(c)(
+ s |> stages.IndentHandler,
+ expr.tree.pos.source,
+ expr.tree.pos.point + (if (stringStart == "\"\"") 1 else -1),
+ runtimeErrors
+ )
+ }
+ }
+
+ def compileThing(c: Context)(s: String, source: SourceFile, point: Int, runtimeErrors: Boolean) = {
+ import c.universe._
+ def compile(s: String): c.Tree = {
+ val realPos = new OffsetPosition(source, point).asInstanceOf[c.universe.Position]
+
+ Compiler(c)(realPos, s |> stages.Parser)
+ }
+
+ import c.Position
+ try {
+ c.Expr(c.typeCheck(compile(s)))
+ } catch {
+ case e@TypecheckException(pos: Position, msg) =>
+ if (!runtimeErrors) c.abort(pos, msg)
+ else {
+ val posMsg = pos.lineContent + "\n" + (" " * pos.column) + "^"
+ c.Expr( q"""throw twist.Internals.DebugFailure($msg, $posMsg)""")
+ }
+ }
+
+ }
+
+}
diff --git a/scalatexApi/src/main/scala/scalatex/stages/Compiler.scala b/scalatexApi/src/main/scala/scalatex/stages/Compiler.scala
new file mode 100644
index 0000000..85bb0f0
--- /dev/null
+++ b/scalatexApi/src/main/scala/scalatex/stages/Compiler.scala
@@ -0,0 +1,161 @@
+package scalatex.stages
+import acyclic.file
+
+import scala.reflect.macros.Context
+import scala.reflect.internal.util.{Position, OffsetPosition}
+
+/**
+ * Walks the parsed AST, converting it into an un-structured Scala source-blob
+ * which when compiled results in a function that can be used to generate the
+ * given Frag at runtime.
+ */
+object Compiler{
+ val WN = TwistNodes
+ def apply(c: Context)(literalPos: c.Position, template: WN.Template): c.Tree = {
+
+ import c.universe._
+ def fragType = tq"scalatags.Text.all.Frag"
+ def posFor(offset: Int) = {
+ new OffsetPosition(
+ literalPos.source,
+ offset
+ ).asInstanceOf[c.universe.Position]
+ }
+ def compileTree(frag: WN.TemplateTree): Tree = {
+
+// println(frag)
+ val fragPos = posFor(literalPos.point + frag.offset)
+
+// println(s"${frag.offset}\n${literalPos.point}\n${pos.point}\n$frag\n")
+
+ val f: Tree = frag match {
+ case WN.Plain(text, offset) => q"$text"
+ case WN.Display(exp, offset) => compileTree(exp)
+ case WN.Comment(msg, offset) => q""
+ case WN.ScalaExp(Seq(WN.Simple(first, _), WN.Block(ws, args, content, _)), offset)
+ if first.startsWith("for(") =>
+ val fresh = c.fresh()
+ val skeleton: Tree = c.parse(first + s"{$fresh}").asInstanceOf[Apply]
+// println("FIRST " + first)
+ skeleton.foreach{x =>
+ x
+ if (x.pos != NoPosition) c.internal.setPos(x, posFor(x.pos.point + fragPos.point + 1))
+ }
+ val b = content.map(compileTree(_))
+ def rec(t: Tree): Tree = t match {
+ case a @ Apply(fun, List(f @ Function(vparams, body))) =>
+ val f2 = Function(vparams, rec(body))
+ val a2 = Apply(fun, List(f2))
+ c.internal.setPos(a2, a.pos)
+ c.internal.setPos(f2, f.pos)
+ a2
+ case Ident(x: TermName) if x.decoded == fresh =>
+ q"Seq[$fragType](..$b)"
+ }
+
+ val out = rec(skeleton)
+
+
+ out
+
+ case WN.ScalaExp(WN.Simple(first, _) +: WN.Block(_, None, content1, _) +: rest, offset)
+ if first.startsWith("if(") =>
+
+ val b1 = content1.map(compileTree(_))
+ val tree = c.parse(first + "{}").asInstanceOf[If]
+ tree.foreach{x =>
+ c.internal.setPos(x, posFor(x.pos.point + fragPos.point + 1))
+ }
+ val If(cond, _, _) = tree
+ val b2 = rest match{
+ case Seq(WN.Simple(next, _), WN.Block(_, None, content2, _)) =>
+ content2.map(compileTree(_))
+ case Seq() => Nil
+ }
+ q"if($cond){ Seq[$fragType](..$b1): $fragType } else { Seq[$fragType](..$b2): $fragType }"
+
+ case xx @ WN.ScalaExp(WN.Simple(first, _) +: rest, offset) =>
+ val firstTree = c.parse(first)
+
+ firstTree.foreach{x =>
+ c.internal.setPos(x, posFor(x.pos.point + fragPos.point))
+ }
+
+ val s = rest.foldLeft[Tree](firstTree) {
+ case (l, WN.Simple(code, _)) =>
+ val fresh = c.fresh()
+
+ val snippet = s"$fresh$code"
+ val skeleton = c.parse(snippet)
+
+ def rec(t: Tree): Tree = {
+ val newPos = posFor(fragPos.point + t.pos.point + first.length - fresh.length)
+ val res = t match {
+ case Apply(fun, args) =>
+ for(arg <- args; tree <- arg if tree.pos != NoPosition){
+ c.internal.setPos(tree, posFor(tree.pos.point + newPos.point - t.pos.point))
+ }
+
+ Apply(rec(fun), args)
+ case Select(qualifier, name) => Select(rec(qualifier), name)
+ case Ident(x: TermName) if x.decoded == fresh => l
+ }
+ c.internal.setPos(res, newPos)
+
+// println(Position.formatMessage(newPos.asInstanceOf[scala.reflect.internal.util.Position], "", true))
+ res
+ }
+
+
+ rec(skeleton)
+
+ case (l, WN.Block(ws, None, content, _)) =>
+ q"$l(..${content.map(compileTree(_))})"
+
+ case (l, WN.Block(ws, Some(args), content, _)) =>
+ val snippet = s"{$args ()}"
+ val skeleton = c.parse(snippet)
+ val Function(vparamss, body) = skeleton
+
+ val func = Function(vparamss, q"Seq[$fragType](..${content.map(compileTree(_))})")
+ c.internal.setPos(func, posFor(fragPos.point + skeleton.pos.point))
+ q"$l($func)"
+ }
+
+ s
+ }
+
+// f.foreach(_.pos = pos)
+
+// println("XXX " + f.pos)
+// println("F " + f)
+ f
+ }
+
+ def compileTemplate(tmpl: WN.Template): Tree = {
+ val WN.Template(name, comment, params, topImports, imports, subs, content, offset) = tmpl
+ val fullName = if (name.toString == "") c.fresh("outer") else name
+
+ val DefDef(mods, realName, tparams, vparamss, tpt, rhs) = {
+ val snippet = s"def $fullName$params = {}"
+ val z = c.parse(snippet).asInstanceOf[DefDef]
+ z
+ }
+
+ val innerDefs = subs.map(compileTemplate(_))
+
+ val body = atPos(literalPos)(q"""{
+ import scalatags.Text.all._
+ ..${topImports.map(i => c.parse(i.code))}
+ ..$innerDefs
+
+ Seq[scalatags.Text.all.Frag](..${content.map(compileTree(_))})
+ }""")
+
+ if (name.toString == "") body
+ else DefDef(mods, realName, tparams, vparamss, tpt, body)
+ }
+
+ atPos(literalPos)(q"${compileTemplate(template)}")
+ }
+} \ No newline at end of file
diff --git a/scalatexApi/src/main/scala/scalatex/stages/IndentHandler.scala b/scalatexApi/src/main/scala/scalatex/stages/IndentHandler.scala
new file mode 100644
index 0000000..180d134
--- /dev/null
+++ b/scalatexApi/src/main/scala/scalatex/stages/IndentHandler.scala
@@ -0,0 +1,105 @@
+package scalatex.stages
+
+import acyclic.file
+/**
+ * Implements the offside-rule for Twirlite fragments.
+ *
+ * This stage walks over the whitespace before each line of the Twirlite,
+ * augmenting it with the corresponding open-{ and close-} such that an
+ * un-modified Twirl parser can then parse it.
+ *
+ * The rule is simple: any line which starts with an expression potentially
+ * opens a new block. Any text after the end of the expression is inside the
+ * block, as well as any text on subsequent lines which are indented more
+ * deeply.
+ */
+object IndentHandler extends (String => String){
+ def noBraceLine(remainder: String) = {
+ !remainder.trim.headOption.exists("{(".toSeq.contains)
+ }
+
+ def successRemainder(r: Parser.ParseResult[_]) = r match {
+ case Parser.Error(_, _) => None
+ case Parser.Success(_, input) => Some(input.source().take(input.offset()))
+ }
+
+ def apply(input: String): String = {
+
+ val lines = "" :: input.lines.toList ::: List("")
+ val tails = lines.tails.takeWhile(_ != Nil)
+
+ def getIndent(rest: List[String]): (Int, String) = rest match {
+ case Nil => (0, "")
+ case x if x.head.trim() != "" => (x.head.takeWhile(_ == ' ').length, x.head)
+ case _ => getIndent(rest.tail)
+ }
+ val min = input.lines
+ .map(_.indexWhere(_ != ' '))
+ .filter(_ != -1)
+ .min
+
+ val linesOut = tails.foldLeft((min :: Nil, Seq[String]())){ (x, tail) =>
+ val (stack, text) = x
+ val spacedCurrent :: next = tail
+ val (spaces, current) = spacedCurrent.splitAt(
+ math.max(spacedCurrent.indexWhere(_ != ' '), 0)
+ )
+
+// println("index " + math.max(spacedCurrent.indexWhere(_ != ' '), 0))
+// println(spaces, current)
+
+ val declRemainder = successRemainder(Parser.parse(current.trim, _.templateDeclaration()))
+
+ val exprRemainder = successRemainder(Parser.parse(current.trim, _.expression())).filter(_ == current.trim)
+
+
+ /**
+ * Whether or not the line starts with some sort of indent-start-marker, and
+ * if it does what the remainder of the line contains
+ */
+ val headerSplit: Option[(String, String)] = {
+ if (current.startsWith("@import ")) None
+ else
+ declRemainder.orElse(exprRemainder)
+ .map(r => (r, current.trim.drop(r.length)))
+ }
+
+ val (nextIndent, nextLine) = getIndent(next)
+ val indent = spaces.length
+ val nextIsElse = nextLine.drop(nextIndent).take("@else ".length).trim.startsWith("@else")
+
+ val elseCorrection = if (nextIsElse) 1 else 0
+
+ val delta =
+ if (nextIndent > indent && headerSplit.map(_._2).exists(noBraceLine)) 1
+ else -stack.takeWhile(_ > nextIndent).length
+
+ val baseLine: String = headerSplit match {
+ case None => current
+ case Some((before, after)) =>
+
+ val newFirst =
+ if (!before.startsWith("@else")) before
+ else before.dropRight("@else".length)+ "} else"
+
+ if (delta > 0 && noBraceLine(after)) newFirst + "{" + after
+ else if (delta <= 0 && noBraceLine(after) && after.trim != "") newFirst + "{" + after + "}" * (1 - elseCorrection)
+ else current
+
+
+ }
+
+ val closing = "}" * (-delta - elseCorrection)
+
+ val newStack =
+ if (delta > 0) nextIndent :: stack
+ else stack.drop(-delta)
+// println(stack.toString.padTo(15, ' ') + current.padTo(15, ' ') + indent + "\t" + nextIndent + "\t" + delta + "\t" + headerSplit.toString.padTo(15, ' ') + (baseLine + closing))
+ (newStack, text :+ (spaces + baseLine + closing))
+ }
+
+ val res = linesOut._2.mkString("\n")
+// println(res)
+ res
+ }
+}
diff --git a/scalatexApi/src/main/scala/scalatex/stages/Parser.scala b/scalatexApi/src/main/scala/scalatex/stages/Parser.scala
new file mode 100644
index 0000000..c83197d
--- /dev/null
+++ b/scalatexApi/src/main/scala/scalatex/stages/Parser.scala
@@ -0,0 +1,790 @@
+package scalatex.stages
+
+import acyclic.file
+import scala.annotation.elidable
+import scala.annotation.elidable._
+import scala.Some
+
+import scala.collection.mutable.{ArrayBuffer, Buffer, ListBuffer}
+
+object TwistNodes{
+ trait Positioned{
+ def offset: Int
+ }
+ abstract class TemplateTree extends Positioned
+ abstract class ScalaExpPart extends Positioned
+
+ case class Params(code: String, offset: Int) extends Positioned
+ case class Template(name: PosString, comment: Option[Comment], params: PosString, topImports: Seq[Simple], imports: Seq[Simple], sub: Seq[Template], content: Seq[TemplateTree], offset: Int) extends Positioned
+ case class PosString(str: String, offset: Int) extends Positioned {
+ override def toString = str
+ }
+ case class Plain(text: String, offset: Int) extends TemplateTree with Positioned
+ case class Display(exp: ScalaExp, offset: Int) extends TemplateTree with Positioned
+ case class Comment(msg: String, offset: Int) extends TemplateTree with Positioned
+ case class ScalaExp(parts: Seq[ScalaExpPart], offset: Int) extends TemplateTree with Positioned
+ case class Simple(code: String, offset: Int) extends ScalaExpPart with Positioned
+ case class Block(whitespace: String, args: Option[PosString], content: Seq[TemplateTree], offset: Int) extends ScalaExpPart with Positioned
+}
+
+import TwistNodes._
+
+object Parser extends (String => Template){
+ def apply(source: String) = {
+ Parser.parse(source, _.template()) match{
+ case Parser.Success(tmpl: Template, input) => tmpl
+ case Parser.Error(input, errors) => throw new Exception("Parsing Failed " + errors.mkString("\n"))
+ }
+ }
+
+ def parse[T](source: String, f: Parser => T): ParseResult[T] = {
+ val p = new Parser
+
+ // Initialize mutable state
+
+ p.input.reset(source)
+ p.errorStack.clear()
+ val res = f(p)
+
+ if (p.errorStack.length == 0 && res != null) Success(res, p.input)
+ else Error(p.input, p.errorStack.toList)
+ }
+
+ sealed abstract class ParseResult[+T]{
+ def toOption: Option[T]
+ }
+ case class Success[+T](t: T, input: Input) extends ParseResult[T]{
+ def toOption = Some(t)
+ }
+ case class Error(input: Input, errors: List[PosString]) extends ParseResult[Nothing]{
+ def toOption = None
+ }
+
+ case class Input() {
+ private var offset_ = 0
+ private var source_ = ""
+ private var length_ = 1
+ val regressionStatistics = new collection.mutable.HashMap[String, (Int, Int)]
+
+ /** Peek at the current input. Does not check for EOF. */
+ def apply(): Char = source_.charAt(offset_)
+
+ /**
+ * Peek `length` characters ahead. Does not check for EOF.
+ * @return string from current offset upto current offset + `length`
+ */
+ def apply(length: Int): String = source_.substring(offset_, (offset_ + length))
+
+ /** Equivalent to `input(str.length) == str`. Does not check for EOF. */
+ def matches(str: String): Boolean = {
+ var i = 0;
+ val l = str.length
+ while (i < l) {
+ if (source_.charAt(offset_ + i) != str.charAt(i))
+ return false
+ i += 1
+ }
+ true
+ }
+
+ /** Advance input by one character */
+ def advance(): Unit = offset_ += 1
+
+ /** Advance input by `increment` number of characters */
+ def advance(increment: Int): Unit = offset_ += increment
+
+ /** Backtrack by `decrement` numner of characters */
+ def regress(decrement: Int): Unit = offset_ -= decrement
+
+ /** Backtrack to a known offset */
+ def regressTo(offset: Int): Unit = {
+ @noinline @elidable(INFO)
+ def updateRegressionStatistics() = {
+ val distance = offset_ - offset
+ val methodName = Thread.currentThread().getStackTrace()(2).getMethodName()
+ val (count, charAccum) = regressionStatistics.get(methodName) getOrElse ((0, 0))
+ regressionStatistics(methodName) = (count + 1, charAccum + distance)
+ }
+
+ offset_ = offset
+ }
+
+ def isPastEOF(len: Int): Boolean = (offset_ + len-1) >= length_
+
+ def isEOF() = isPastEOF(1)
+
+ def atEnd() = isEOF()
+
+ def offset() = offset_
+
+ def source() = source_
+
+ /** Reset the input to have the given contents */
+ def reset(source: String) {
+ offset_ = 0
+ source_ = source
+ length_ = source.length()
+ regressionStatistics.clear()
+ }
+ }
+}
+class Parser{
+ import Parser._
+
+ val input: Input = new Input
+ val errorStack: ListBuffer[PosString] = ListBuffer()
+
+ /**
+ * Try to match `str` and advance `str.length` characters.
+ *
+ * Reports an error if the input does not match `str` or if `str.length` goes past the EOF.
+ */
+ def accept(str: String): Unit = {
+ val len = str.length
+ if (!input.isPastEOF(len) && input.matches(str))
+ input.advance(len)
+ else
+ error("Expected '" + str + "' but found '" + (if (input.isPastEOF(len)) "EOF" else input(len)) + "'")
+ }
+
+ /**
+ * Does `f` applied to the current peek return true or false? If true, advance one character.
+ *
+ * Will not advance if at EOF.
+ *
+ * @return true if advancing, false otherwise.
+ */
+ def check(f: Char => Boolean): Boolean = {
+ if (!input.isEOF() && f(input())) {
+ input.advance()
+ true
+ } else false
+ }
+
+ /**
+ * Does the current input match `str`? If so, advance `str.length`.
+ *
+ * Will not advance if `str.length` surpasses EOF
+ *
+ * @return true if advancing, false otherwise.
+ */
+ def check(str: String): Boolean = {
+ val len = str.length
+ if (!input.isPastEOF(len) && input.matches(str)){
+ input.advance(len)
+ true
+ } else false
+ }
+
+ def error(message: String, offset: Int = input.offset): Unit = {
+ errorStack += PosString(message, offset)
+ }
+
+ /** Consume/Advance `length` characters, and return the consumed characters. Returns "" if at EOF. */
+ def any(length: Int = 1): String = {
+ if (input.isEOF()) {
+ error("Expected more input but found 'EOF'")
+ ""
+ } else {
+ val s = input(length)
+ input.advance(length)
+ s
+ }
+ }
+
+ /**
+ * Consume characters until input matches `stop`
+ *
+ * @param inclusive - should stop be included in the consumed characters?
+ * @return the consumed characters
+ */
+ def anyUntil(stop: String, inclusive: Boolean): String = {
+ var sb = new StringBuilder
+ while (!input.isPastEOF(stop.length) && !input.matches(stop))
+ sb.append(any())
+ if (inclusive && !input.isPastEOF(stop.length))
+ sb.append(any(stop.length))
+ sb.toString()
+ }
+
+ /**
+ * Consume characters until `f` returns false on the peek of input.
+ *
+ * @param inclusive - should the stopped character be included in the consumed characters?
+ * @return the consumed characters
+ */
+ def anyUntil(f: Char => Boolean, inclusive: Boolean): String = {
+ var sb = new StringBuilder
+ while (!input.isEOF() && f(input()) == false)
+ sb.append(any())
+ if (inclusive && !input.isEOF())
+ sb.append(any())
+ sb.toString
+ }
+
+ /** Recursively match pairs of prefixes and suffixes and return the consumed characters
+ *
+ * Terminates at EOF.
+ */
+ def recursiveTag(prefix: String, suffix: String, allowStringLiterals: Boolean = false): String = {
+ if (check(prefix)) {
+ var stack = 1
+ val sb = new StringBuffer
+ sb.append(prefix)
+ while (stack > 0) {
+ if (check(prefix)) {
+ stack += 1
+ sb.append(prefix)
+ } else if (check(suffix)) {
+ stack -= 1
+ sb.append(suffix)
+ } else if (input.isEOF()) {
+ error("Expected '" + suffix + "' but found 'EOF'")
+ stack = 0
+ } else if (allowStringLiterals) {
+ stringLiteral("\"", "\\") match {
+ case null => sb.append(any())
+ case s => sb.append(s)
+ }
+ } else {
+ sb.append(any())
+ }
+ }
+ sb.toString()
+ } else null
+ }
+
+ /**
+ * Match a string literal, allowing for escaped quotes.
+ * Terminates at EOF.
+ */
+ def stringLiteral(quote: String, escape: String): String = {
+ if (check(quote)) {
+ var within = true
+ val sb = new StringBuffer
+ sb.append(quote)
+ while (within) {
+ if (check(quote)) { // end of string literal
+ sb.append(quote)
+ within = false
+ } else if (check(escape)) {
+ sb.append(escape)
+ if (check(quote)) { // escaped quote
+ sb.append(quote)
+ } else if (check(escape)) { // escaped escape
+ sb.append(escape)
+ }
+ } else if (input.isEOF()) {
+ error("Expected '" + quote + "' but found 'EOF'")
+ within = false
+ } else {
+ sb.append(any())
+ }
+ }
+ sb.toString()
+ } else null
+ }
+
+ /** Match zero or more `parser` */
+ def several[T, BufferType <: Buffer[T]](parser: () => T, provided: BufferType = null)(implicit manifest: Manifest[BufferType]): BufferType = {
+
+ val ab = if (provided != null) provided else manifest.runtimeClass.newInstance().asInstanceOf[BufferType]
+ var parsed = parser()
+ while (parsed != null) {
+ ab += parsed
+ parsed = parser()
+ }
+ ab
+ }
+
+ def parentheses(): String = recursiveTag("(", ")", allowStringLiterals = true)
+
+ def squareBrackets(): String = recursiveTag("[", "]")
+
+ def whitespace(): String = anyUntil(_ > '\u0020', inclusive = false)
+
+ // not completely faithful to original because it allows for zero whitespace
+ def whitespaceNoBreak(): String = anyUntil(c => c != ' ' && c != '\t', inclusive = false)
+
+ def identifier(): String = {
+ var result: String = null
+ // TODO: should I be checking for EOF here?
+ if (!input.isEOF() && Character.isJavaIdentifierStart(input())) {
+ result = anyUntil(Character.isJavaIdentifierPart(_) == false, false)
+ }
+ result
+ }
+
+ def comment(): Comment = {
+ val pos = input.offset
+ if (check("@*")) {
+ val text = anyUntil("*@", inclusive = false)
+ accept("*@")
+ Comment(text, pos)
+ } else null
+ }
+
+ def startArgs(): String = {
+ val result = several[String, ArrayBuffer[String]](parentheses)
+ if (result.length > 0)
+ result.mkString
+ else
+ null
+ }
+
+ def importExpression(): Simple = {
+ val p = input.offset
+ if (check("@import "))
+ Simple("import " + anyUntil("\n", inclusive = true).trim, p+1) // don't include position of @
+ else null
+ }
+
+ def scalaBlock(): Simple = {
+ if (check("@{")) {
+ input.regress(1); // we need to parse the '{' via 'brackets()'
+ val p = input.offset
+ brackets() match {
+ case null => null
+ case b => Simple(b, p)
+ }
+ } else null
+ }
+
+ def brackets(): String = {
+ var result = recursiveTag("{", "}")
+ // mimicking how the original parser handled EOF for this rule
+ if (result != null && result.last != '}')
+ result = null
+ result
+ }
+
+ def mixed(): ListBuffer[TemplateTree] = {
+ // parses: comment | scalaBlockDisplayed | forExpression | matchExpOrSafeExprOrExpr | caseExpression | plain
+ def opt1(): ListBuffer[TemplateTree] = {
+ val t =
+ comment() match {
+ case null => scalaBlockDisplayed() match {
+ case null => forExpression match {
+ case null => matchExpOrSafeExpOrExpr() match {
+ case null => caseExpression() match {
+ case null => plain()
+ case x => x
+ }
+ case x => x
+ }
+ case x => x
+ }
+ case x => x
+ }
+ case x => x
+ }
+ if (t != null) ListBuffer(t)
+ else null
+ }
+
+ // parses: '{' mixed* '}'
+ def opt2(): ListBuffer[TemplateTree] = {
+ val lbracepos = input.offset()
+ if (check("{")) {
+ var buffer = new ListBuffer[TemplateTree]
+ buffer += Plain("{", lbracepos)
+ for (m <- several[ListBuffer[TemplateTree], ListBuffer[ListBuffer[TemplateTree]]](mixed))
+ buffer = buffer ++ m // creates a new object, but is constant in time, as opposed to buffer ++= m which is linear (proportional to size of m)
+ val rbracepos = input.offset
+ if (check("}"))
+ buffer += Plain("}", rbracepos)
+ else
+ error("Expected ending '}'")
+ buffer
+ } else null
+ }
+
+ opt1() match {
+ case null => opt2()
+ case x => x
+ }
+ }
+
+ def scalaBlockDisplayed(): Display = {
+ val sb = scalaBlock()
+
+ if (sb != null)
+ Display(ScalaExp(sb :: Nil, sb.offset), sb.offset)
+ else
+ null
+ }
+
+ def blockArgs(): PosString = {
+ val p = input.offset
+ val result = anyUntil("=>", true)
+ if (result.endsWith("=>") && !result.contains("\n"))
+ PosString(result, p)
+ else {
+ input.regress(result.length())
+ null
+ }
+ }
+
+ def block(): Block = {
+ var result: Block = null
+ val p = input.offset
+ val ws = whitespaceNoBreak()
+ if (check("{")) {
+ val blkArgs = Option(blockArgs())
+ val mixeds = several[ListBuffer[TemplateTree], ListBuffer[ListBuffer[TemplateTree]]](mixed)
+ accept("}")
+ // TODO - not use flatten here (if it's a performance problem)
+ result = Block(ws, blkArgs, mixeds.flatten, p)
+ } else {
+ input.regressTo(p)
+ }
+
+ result
+ }
+
+ def caseExpression(): TemplateTree = {
+ var result: TemplateTree = null
+
+ val wspos = input.offset
+ val ws = whitespace()
+ val p = input.offset()
+ if (check("case ")) {
+ val pattern = Simple("case " + anyUntil("=>", inclusive = true), p)
+ val blk = block()
+ if (blk != null) {
+ result = ScalaExp(ListBuffer(pattern, blk), blk.offset)
+ whitespace()
+ } else {
+ //error("Expected block after 'case'")
+ input.regressTo(wspos)
+ }
+ } else if (ws.length > 0) {
+ // We could regress here and not return something for the ws, because the plain production rule
+ // would parse this, but this would actually be a hotspot for backtracking, so let's return it
+ // here seeing as it's been parsed all ready.
+ result = Plain(ws, wspos)
+ }
+
+ result
+ }
+
+ def matchExpOrSafeExpOrExpr(): Display = {
+ val resetPosition = input.offset
+ val result =
+ expression() match {
+ case null => safeExpression()
+ case x => x
+ }
+
+ if (result != null) {
+ val exprs = result.exp.parts.asInstanceOf[ListBuffer[ScalaExpPart]]
+ val mpos = input.offset
+ val ws = whitespaceNoBreak()
+ if (check("match")) {
+ val m = Simple(ws + "match", mpos)
+ val blk = block()
+ if (blk != null) {
+ exprs.append(m)
+ exprs.append(blk)
+ } else {
+ // error("expected block after match")
+ input.regressTo(mpos)
+ }
+ } else {
+ input.regressTo(mpos)
+ }
+ }
+
+ result
+ }
+
+ def forExpression(): Display = {
+ var result: Display = null
+ val p = input.offset
+ if (check("@for")) {
+ val parens = parentheses()
+ if (parens != null) {
+ val blk = block()
+ if (blk != null) {
+ result = Display(ScalaExp(ListBuffer(Simple("for" + parens + " yield ", p+1), blk), p+1), p+1) // don't include pos of @
+ }
+ }
+ }
+
+ if (result == null)
+ input.regressTo(p)
+
+ result
+ }
+
+ def safeExpression(): Display = {
+ if (check("@(")) {
+ input.regress(1)
+ val p = input.offset
+ Display(ScalaExp(ListBuffer(Simple(parentheses(), p)), p), p)
+ } else null
+ }
+
+ def plain(): Plain = {
+ def single(): String = {
+ if (check("@@")) "@"
+ else if (!input.isEOF() && input() != '@' && input() != '}' && input() != '{') any()
+ else null
+ }
+ val p = input.offset
+ var result: Plain = null
+ var part = single()
+ if (part != null) {
+ val sb = new StringBuffer
+ while (part != null) {
+ sb.append(part)
+ part = single()
+ }
+ result = Plain(sb.toString(), p)
+ }
+
+ result
+ }
+
+ def expression(): Display = {
+ var result: Display = null
+ if (check("@")) {
+ val pos = input.offset
+ val code = methodCall()
+ if (code != null) {
+ val parts = several[ScalaExpPart, ListBuffer[ScalaExpPart]](expressionPart)
+ parts.prepend(Simple(code, pos))
+ result = Display(ScalaExp(parts, pos-1), pos-1)
+ } else {
+ input.regressTo(pos - 1) // don't consume the @ if we fail
+ }
+ }
+
+ result
+ }
+
+ def methodCall(): String = {
+ val name = identifier()
+ if (name != null) {
+ val sb = new StringBuffer(name)
+ sb.append(Option(squareBrackets) getOrElse "")
+ sb.append(Option(parentheses) getOrElse "")
+ sb.toString()
+ } else null
+ }
+
+ def expressionPart(): ScalaExpPart = {
+ def simpleParens() = {
+ val p = input.offset
+ val parens = parentheses()
+ if (parens != null) Simple(parens, p)
+ else null
+ }
+
+ def wsThenScalaBlockChained() = {
+ val reset = input.offset
+ val ws = whitespaceNoBreak()
+ val chained = scalaBlockChained()
+ if (chained eq null) input.regressTo(reset)
+ chained
+ }
+
+ chainedMethods() match {
+ case null => block() match {
+ case null => wsThenScalaBlockChained() match {
+ case null => elseCall() match {
+ case null => simpleParens()
+ case x => x
+ }
+ case x => x
+ }
+ case x => x
+ }
+ case x => x
+ }
+ }
+
+ def scalaBlockChained(): Block = {
+ val blk = scalaBlock()
+ if (blk != null)
+ Block("", None, ListBuffer(ScalaExp(ListBuffer(blk), blk.offset)), blk.offset)
+ else null
+ }
+
+ def chainedMethods(): Simple = {
+ val p = input.offset
+ var result: Simple = null
+ if (check(".")) {
+ val firstMethodCall = methodCall()
+ if (firstMethodCall != null) {
+ val sb = new StringBuffer("." + firstMethodCall)
+ var done = false
+ while (!done) {
+ val reset = input.offset
+ var nextLink: String = null
+ if (check(".")) {
+ methodCall() match {
+ case m: String => nextLink = m
+ case _ =>
+ }
+ }
+
+ nextLink match {
+ case null => {
+ done = true
+ input.regressTo(reset)
+ }
+ case _ => {
+ sb.append(".")
+ sb.append(nextLink)
+ }
+ }
+ }
+
+ result = Simple(sb.toString(), p)
+ } else input.regressTo(p)
+ }
+
+ result
+ }
+
+ def elseCall(): Simple = {
+ val reset = input.offset
+ whitespaceNoBreak()
+ val p = input.offset
+ if (check("else")) {
+ whitespaceNoBreak()
+ Simple("else", p)
+ } else {
+ input.regressTo(reset)
+ null
+ }
+ }
+
+ def template(): Template = {
+ val topImports = extraImports()
+ whitespace()
+ val commentpos = input.offset
+ val cm = Option(comment()).map(_.copy(offset = commentpos))
+ whitespace()
+ val args =
+ if (check("@(")) {
+ input.regress(1)
+ val p = input.offset
+ val args = startArgs()
+ if (args != null) Some(PosString(args, p))
+ else None
+ } else None
+ val (imports, templates, mixeds) = templateContent()
+
+ Template(PosString("", 0), cm, args.getOrElse(PosString("()", 0)), topImports, imports, templates, mixeds, 0)
+ }
+ def subTemplate(): Template = {
+ var result: Template = null
+ val resetPosition = input.offset
+ val templDecl = templateDeclaration()
+ if (templDecl != null) {
+ anyUntil(c => c != ' ' && c != '\t', inclusive = false)
+ if (check("{")) {
+ val (imports, templates, mixeds) = templateContent()
+ if (check("}"))
+ result = Template(templDecl._1, None, templDecl._2, Nil, imports, templates, mixeds, templDecl._1.offset)
+ }
+ }
+
+ if (result == null)
+ input.regressTo(resetPosition)
+ result
+ }
+
+ def templateDeclaration(): (PosString, PosString) = {
+ if (check("@")) {
+ val namepos = input.offset
+ val name = identifier() match {
+ case null => null
+ case id => PosString(id, namepos)
+ }
+
+ if (name != null) {
+ val paramspos = input.offset
+ val types = Option(squareBrackets) getOrElse ""
+ val args = several[String, ArrayBuffer[String]](parentheses)
+ val params = PosString(types + args.mkString, paramspos)
+ if (params != null)
+
+ anyUntil(c => c != ' ' && c != '\t', inclusive = false)
+ if (check("=")) {
+ return (name, params)
+ }
+ } else input.regress(1) // don't consume @
+ }
+
+ null
+ }
+
+ def templateContent(): (Seq[Simple], Seq[Template], Seq[TemplateTree]) = {
+ val imports = new ArrayBuffer[Simple]
+
+ val templates = new ArrayBuffer[Template]
+ val mixeds = new ArrayBuffer[TemplateTree]
+
+ var done = false
+ while (!done) {
+ val impExp = importExpression()
+ if (impExp != null) imports += impExp
+ else {
+
+ val templ = subTemplate()
+ if (templ != null) templates += templ
+ else {
+ val mix = mixed()
+ if (mix != null) mixeds ++= mix
+ else {
+ // check for an invalid '@' symbol, and just skip it so we can continue the parse
+ val pos = input.offset
+ if (check("@")) error("Invalid '@' symbol", pos)
+ else done = true
+ }
+ }
+ }
+ }
+
+ (imports, templates, mixeds)
+ }
+
+ def extraImports(): Seq[Simple] = {
+ val resetPosition = input.offset
+ val imports = new ArrayBuffer[Simple]
+
+ while (whitespace().nonEmpty || (comment() ne null)) {} // ignore
+
+ var done = false
+ while (!done) {
+ val importExp = importExpression()
+ if (importExp ne null) {
+ imports += importExp
+ whitespace()
+ } else {
+ done = true
+ }
+ }
+
+ if (imports.isEmpty) {
+ input.regressTo(resetPosition)
+ }
+
+ imports
+ }
+
+
+
+ def mkRegressionStatisticsString() {
+ val a = input.regressionStatistics.toArray.sortBy { case (m, (c, a)) => c }
+ a.mkString("\n")
+ }
+
+ // TODO - only for debugging purposes, remove before release
+ def setSource(source: String) {
+ input.reset(source)
+ }
+} \ No newline at end of file
diff --git a/scalatexApi/src/test/scala/scalatex/AdvancedTests.scala b/scalatexApi/src/test/scala/scalatex/AdvancedTests.scala
new file mode 100644
index 0000000..4315735
--- /dev/null
+++ b/scalatexApi/src/test/scala/scalatex/AdvancedTests.scala
@@ -0,0 +1,120 @@
+package scalatex
+
+import utest._
+import scalatex.stages._
+import scalatags.Text.all._
+
+
+/**
+* Created by haoyi on 7/14/14.
+*/
+object AdvancedTests extends TestSuite{
+ import TestUtil._
+
+ val tests = TestSuite{
+ 'localDef{
+ check(
+ tw("""
+ @lol(n: Int) = @{
+ "omg" * n
+ }
+
+ @lol(2)
+ """),
+ "omgomg"
+ )
+ }
+ 'innerTemplate{
+ check(
+ tw("""
+ @lol(f: Int) =
+ omg @f
+
+ @lol(1)
+ @lol(2: Int)
+ @lol(3 + 1)
+ """),
+ tw("""
+ @lol(f: Int) ={
+ omg @f
+ }
+ @lol(1)
+ @lol(2: Int)
+ @lol(3 + 1)
+ """),
+ tw("""
+ @lol(f: Int) = {
+ omg @f
+ }
+ @lol(1)
+ @lol(2: Int)
+ @lol(3 + 1)
+ """),
+ """
+ omg1omg2omg4
+ """
+ )
+ }
+ 'innerInnerTemplate{
+ check(
+ tw("""
+ @lol(f: Int) =
+ @wtf(g: Int) =
+ wtf @g
+
+ @wtf(1 + 2 + 3)
+ @wtf(f)
+
+ @lol(1)
+ @lol(2: Int)
+ @lol(3 + 1)
+ """),
+ tw("""
+ @lol(f: Int) = {
+ @wtf(g: Int) = {
+ wtf @g
+ }
+ @wtf(1 + 2 + 3)
+ @wtf(f)
+ }
+ @lol(1)
+ @lol(2: Int)
+ @lol(3 + 1)
+ """),
+ tw("""
+ @lol(f: Int) = {
+ @wtf(g: Int) =
+ wtf @g
+
+ @wtf(1 + 2 + 3)
+ @wtf(f)
+ }
+ @lol(1)
+ @lol(2: Int)
+ @lol(3 + 1)
+ """),
+ tw("""
+ @lol(f: Int) =
+ @wtf(g: Int) = {
+ wtf @g
+ }
+ @wtf(1 + 2 + 3)
+ @wtf(f)
+
+ @lol(1)
+ @lol(2: Int)
+ @lol(3 + 1)
+ """),
+ """
+ wtf6
+ wtf1
+ wtf6
+ wtf2
+ wtf6
+ wtf4
+ """
+ )
+ }
+
+ }
+}
diff --git a/scalatexApi/src/test/scala/scalatex/BasicTests.scala b/scalatexApi/src/test/scala/scalatex/BasicTests.scala
new file mode 100644
index 0000000..0488ef6
--- /dev/null
+++ b/scalatexApi/src/test/scala/scalatex/BasicTests.scala
@@ -0,0 +1,418 @@
+package scalatex
+import utest._
+import scalatex.stages._
+import scalatags.Text.all._
+
+
+/**
+* Created by haoyi on 7/14/14.
+*/
+object BasicTests extends TestSuite{
+ import TestUtil._
+
+ val tests = TestSuite{
+
+ 'helloWorld{
+ object omg {
+ def wtf(s: Frag*): Frag = Seq[Frag]("|", s, "|")
+ }
+ def str = "hear me moo"
+ check(
+ tw("""
+ @omg.wtf
+ i @b{am} cow @str
+ """),
+ "|i<b>am</b>cowhearmemoo|"
+ )
+ }
+ 'interpolation{
+ 'chained-check(
+ tw("omg @scala.math.pow(0.5, 3) wtf"),
+ "omg 0.125 wtf"
+ )
+ 'parens-check(
+ tw("omg @(1 + 2 + 3 + 4) wtf"),
+ "omg 10 wtf"
+ )
+ 'block-check(
+ tw("""
+ @{"lol" * 3}
+ @{
+ val omg = "omg"
+ omg * 2
+ }
+ """),
+ """
+ lollollol
+ omgomg
+ """
+ )
+ }
+ 'imports{
+ object Whee{
+ def func(x: Int) = x * 2
+ }
+ check(
+ tw("""
+ @import math._
+ @import Whee.func
+ @abs(-10)
+ @p
+ @max(1, 2)
+ @func(2)
+ """),
+ """
+ 10
+ <p>
+ 2
+ 4
+ </p>
+ """
+ )
+ }
+ 'parenArgumentLists{
+ 'attributes{
+ check(
+ tw("""
+ @div(id:="my-id") omg
+ @div(id:="my-id"){ omg }
+ @div(id:="my-id")
+ omg
+ """),
+ """
+ <divid="my-id">omg</div>
+ <divid="my-id">omg</div>
+ <divid="my-id">omg</div>
+ """
+ )
+ }
+ 'multiline{
+
+ check(
+ tw("""
+ @div(
+ h1("Hello World"),
+ p("I am a ", b{"cow"})
+ )
+ """),
+ """
+ <div>
+ <h1>Hello World</h1>
+ <p>I am a <b>cow</b></p>
+ </div>
+ """
+ )
+ }
+ }
+ 'grouping{
+ 'negative{
+ // The indentation for "normal" text is ignored; we only
+ // create blocks from the indentation following a scala
+ // @xxx expression
+ check(
+ tw("""
+ I am cow hear me moo
+ I weigh twice as much as you
+ And I look good on the barbecue
+ Yoghurt curds cream cheese and butter
+ Comes from liquids from my udder
+ I am cow I am cow hear me moooooo
+ """),
+ """
+ I am cow hear me moo
+ I weigh twice as much as you
+ And I look good on the barbecue
+ Yoghurt curds cream cheese and butter
+ Comes from liquids from my udder
+ I am cow I am cow hear me moooooo
+ """
+ )
+ }
+ 'indentation{
+ 'simple{
+ val world = "World2"
+
+ check(
+ tw("""
+ @h1 Hello World
+ @h2 hello
+ @world
+ @h3
+ Cow
+ """),
+ """
+ <h1>HelloWorld</h1>
+ <h2>helloWorld2</h2>
+ <h3>Cow</h3>
+ """
+ )
+ }
+ 'linearNested{
+ check(
+ tw("""
+ @h1 @span @a Hello World
+ @h2 @span @a hello
+ @b world
+ @h3 @i
+ @div Cow
+ """),
+ """
+ <h1><span></span><a></a>HelloWorld</h1>
+ <h2><span></span><a></a>hello<b>world</b></h2>
+ <h3><i></i><div>Cow</div></h3>
+ """
+ )
+ }
+ 'crasher{
+ tw("""
+@html
+ @head
+ @meta
+ @div
+ @a
+ @span
+ """)
+ }
+ }
+ 'curlies{
+ 'simple{
+ val world = "World2"
+
+ check(
+ tw("""@div{Hello World}"""),
+ """<div>HelloWorld</div>"""
+ )
+ }
+ 'multiline{
+ check(
+ tw("""
+ @div{
+ Hello
+ }
+ """),
+ """
+ <div>Hello</div>
+ """
+ )
+ }
+ }
+ 'mixed{
+ check(
+ tw("""
+ @div{
+ Hello
+ @div
+ @h1 WORLD @b{!!!}
+ lol
+ @p{
+ @h2{Header 2}
+ }
+ }
+ """),
+ """
+ <div>
+ Hello
+ <div>
+ <h1>WORLD<b>!!!</b>lol</h1>
+ <p><h2>Header2</h2></p>
+ </div>
+ </div>
+ """
+ )
+ }
+
+ 'args{
+ val things = Seq(1, 2, 3)
+ check(
+ tw("""
+ @ul
+ @things.map { x =>
+ @li @x
+ }
+ """),
+ tw("""
+ @ul
+ @things.map x =>
+ @li @x
+
+ """),
+ """
+ <ul>
+ <li>1</li>
+ <li>2</li>
+ <li>3</li>
+ </ul>
+ """
+ )
+ }
+ }
+
+ 'loops {
+
+ * - check(
+ tw("""
+ @for(x <- 0 until 3)
+ lol
+ """),
+ tw("""
+ @for(x <- 0 until 3){
+ lol
+ }
+ """),
+ tw(
+ """
+ @for(x <- 0 until 3) {
+ lol
+ }
+ """),
+ "lollollol"
+ )
+
+
+ * - check(
+ tw("""
+ @p
+ @for(x <- 0 until 2)
+ @for(y <- 0 until 2)
+ lol@x@y
+ """),
+ tw( """
+ @p
+ @for(x <- 0 until 2) {
+ @for(y <- 0 until 2)
+ lol@x@y
+ }
+ """),
+ tw("""
+ @p
+ @for(x <- 0 until 2)
+ @for(y <- 0 until 2){
+ lol@x@y
+ }
+ """),
+ "<p>lol00lol01lol10lol11</p>"
+ )
+ check(
+ tw("""
+ @p
+ @for(x <- 0 until 2)
+ @for(y <- 0 until 2)
+ lol@x@y
+ """),
+ "<p>lol00lol01lol10lol11</p>"
+ )
+
+ * - check(
+ tw(
+ """
+ @for(x <- 0 until 2; y <- 0 until 2)
+ @div{@x@y}
+
+ """),
+ """<div>00</div><div>01</div><div>10</div><div>11</div>"""
+ )
+ }
+
+ 'ifElse{
+ 'basicExamples{
+ * - check(
+ tw("""
+ @if(false)
+ Hello
+ @else lols
+ @p
+ """),
+ "lols<p></p>"
+ )
+
+ * - check(
+ tw("""
+ @div
+ @if(true)
+ Hello
+ @else lols
+ """),
+ "<div>Hello</div>"
+ )
+
+ * - check(
+ tw("""
+ @div
+ @if(true) Hello
+ @else lols
+ """),
+ "<div>Hello</div>"
+ )
+ * - check(
+ tw("""
+ @if(false) Hello
+ @else lols
+ """),
+ "lols"
+ )
+ * - check(
+ tw("""
+ @if(false)
+ Hello
+ @else
+ lols
+ @img
+ """),
+ "lols<img/>"
+ )
+ * - check(
+ tw("""
+ @p
+ @if(true)
+ Hello
+ @else
+ lols
+ """),
+ tw("""
+ @p
+ @if(true) {
+ Hello
+ } else {
+ lols
+ }
+ """),
+ "<p>Hello</p>"
+ )
+ }
+ 'funkyExpressions{
+ * - check(
+ tw("""
+ @p
+ @if(true == false == (true.==(false)))
+ @if(true == false == (true.==(false)))
+ Hello1
+ @else
+ lols1
+ @else
+ @if(true == false == (true.==(false)))
+ Hello2
+ @else
+ lols2
+ """),
+ "<p>Hello1</p>"
+ )
+ * - check(
+ tw("""
+ @p
+ @if(true == false != (true.==(false)))
+ @if(true == false != (true.==(false)))
+ Hello1
+ @else
+ lols1
+ @else
+ @if(true == false != (true.==(false)))
+ Hello2
+ @else
+ lols2
+ """),
+ "<p>lols2</p>"
+ )
+ }
+ }
+ }
+}
diff --git a/scalatexApi/src/test/scala/scalatex/ErrorTests.scala b/scalatexApi/src/test/scala/scalatex/ErrorTests.scala
new file mode 100644
index 0000000..a122dd2
--- /dev/null
+++ b/scalatexApi/src/test/scala/scalatex/ErrorTests.scala
@@ -0,0 +1,321 @@
+package scalatex
+
+import utest._
+import scalatex.stages._
+import scalatags.Text.all._
+import scalatex.Internals.{DebugFailure, twDebug}
+
+/**
+* Created by haoyi on 7/14/14.
+*/
+object ErrorTests extends TestSuite{
+ def check(x: => Unit, expectedMsg: String, expectedError: String) = {
+ val DebugFailure(msg, pos) = intercept[DebugFailure](x)
+ def format(str: String) = {
+ val whitespace = " \t\n".toSet
+ "\n" + str.dropWhile(_ == '\n')
+ .reverse
+ .dropWhile(whitespace.contains)
+ .reverse
+ }
+ // Format these guys nicely to normalize them and make them
+ // display nicely in the assert error message if it blows up
+ val formattedPos = format(pos)
+ val formattedExpectedPos = format(expectedError)
+
+ assert(msg.contains(expectedMsg))
+ assert(formattedPos == formattedExpectedPos)
+
+ }
+ val tests = TestSuite{
+
+
+ 'simple - check(
+ twDebug("omg @notInScope lol"),
+ """not found: value notInScope""",
+ """
+ twDebug("omg @notInScope lol"),
+ ^
+ """
+ )
+
+ 'chained{
+ 'properties {
+ * - check(
+ twDebug("omg @math.lol lol"),
+ """object lol is not a member of package math""",
+ """
+ twDebug("omg @math.lol lol"),
+ ^
+ """
+ )
+
+ * - check(
+ twDebug("omg @math.E.lol lol"),
+ """value lol is not a member of Double""",
+ """
+ twDebug("omg @math.E.lol lol"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("omg @_root_.scala.math.lol lol"),
+ """object lol is not a member of package math""",
+ """
+ twDebug("omg @_root_.scala.math.lol lol"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("omg @_root_.scala.gg.lol lol"),
+ """object gg is not a member of package scala""",
+ """
+ twDebug("omg @_root_.scala.gg.lol lol"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("omg @_root_.ggnore.math.lol lol"),
+ """object ggnore is not a member of package <root>""",
+ """
+ twDebug("omg @_root_.ggnore.math.lol lol"),
+ ^
+ """
+ )
+ }
+ 'calls{
+ * - check(
+ twDebug("@scala.QQ.abs(-10).tdo(10).sum.z"),
+ """object QQ is not a member of package scala""",
+ """
+ twDebug("@scala.QQ.abs(-10).tdo(10).sum.z"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("@scala.math.abs(-10).tdo(10).sum.z"),
+ "value tdo is not a member of Int",
+ """
+ twDebug("@scala.math.abs(-10).tdo(10).sum.z"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("@scala.math.abs(-10).to(10).sum.z"),
+ "value z is not a member of Int",
+ """
+ twDebug("@scala.math.abs(-10).to(10).sum.z"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("@scala.math.abs(-10).to(10).sum.z()"),
+ "value z is not a member of Int",
+ """
+ twDebug("@scala.math.abs(-10).to(10).sum.z()"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("@scala.math.abs(-10).cow.sum.z"),
+ "value cow is not a member of Int",
+ """
+ twDebug("@scala.math.abs(-10).cow.sum.z"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("@scala.smath.abs.cow.sum.z"),
+ "object smath is not a member of package scala",
+ """
+ twDebug("@scala.smath.abs.cow.sum.z"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("""
+ I am cow hear me moo
+ @scala.math.abs(-10).tdo(10).sum.z
+ I weigh twice as much as you
+ """),
+ "value tdo is not a member of Int",
+ """
+ @scala.math.abs(-10).tdo(10).sum.z
+ ^
+ """
+ )
+ }
+ 'callContents{
+ * - check(
+ twDebug("@scala.math.abs((1, 2).wtf)"),
+ "value wtf is not a member of (Int, Int)",
+ """
+ twDebug("@scala.math.abs((1, 2).wtf)"),
+ ^
+ """
+ )
+
+ * - check(
+ twDebug("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"),
+ "value wtf is not a member of String",
+ """
+ twDebug("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"),
+ ^
+ """
+ )
+ }
+ }
+ 'ifElse{
+ 'oneLine {
+ * - check(
+ twDebug("@if(math > 10){ 1 }else{ 2 }"),
+ "object > is not a member of package math",
+ """
+ twDebug("@if(math > 10){ 1 }else{ 2 }"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"),
+ "Unspecified value parameter y",
+ """
+ twDebug("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"),
+ ^
+ """
+ )
+ * - check(
+ twDebug("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"),
+ "too many arguments for method sin: (x: Double)Double",
+ """
+ twDebug("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"),
+ ^
+ """
+ )
+ }
+ 'multiLine{
+ * - check(
+ twDebug("""
+ Ho Ho Ho
+
+ @if(math != 10)
+ I am a cow
+ @else
+ You are a cow
+ GG
+ """),
+ "object != is not a member of package math",
+ """
+ @if(math != 10)
+ ^
+ """
+ )
+ * - check(
+ twDebug("""
+ Ho Ho Ho
+
+ @if(4 != 10)
+ I am a cow @math.lols
+ @else
+ You are a cow
+ GG
+ """),
+ "object lols is not a member of package math",
+ """
+ I am a cow @math.lols
+ ^
+ """
+ )
+ * - check(
+ twDebug("""
+ Ho Ho Ho
+
+ @if(12 != 10)
+ I am a cow
+ @else
+ @math.E.toString.gog(1)
+ GG
+ """),
+ "value gog is not a member of String",
+ """
+ @math.E.toString.gog(1)
+ ^
+ """
+ )
+ }
+ }
+ 'forLoop{
+ 'oneLine{
+ 'header - check(
+ twDebug("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"),
+ """value omglolol is not a member of Int""",
+ """
+ twDebug("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"),
+ ^
+ """
+ )
+
+ 'body - check(
+ twDebug("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"),
+ """too many arguments for method +""",
+ """
+ twDebug("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"),
+ ^
+ """
+ )
+ }
+ 'multiLine{
+ 'body - check(
+ twDebug("""
+ omg
+ @for(x <- 0 until 10)
+ I am cow hear me moo
+ I weigh twice as much as @x.kkk
+ """),
+ """value kkk is not a member of Int""",
+ """
+ I weigh twice as much as @x.kkk
+ ^
+ """
+ )
+ }
+ }
+ 'multiLine{
+ 'missingVar - check(
+ twDebug("""
+ omg @notInScope lol
+ """),
+ """not found: value notInScope""",
+ """
+ omg @notInScope lol
+ ^
+ """
+ )
+ 'wrongType - check(
+ twDebug("""
+ omg @{() => ()} lol
+ """),
+ """type mismatch""",
+ """
+ omg @{() => ()} lol
+ ^
+ """
+ )
+
+ 'bigExpression - check(
+ twDebug("""
+ @{
+ val x = 1 + 2
+ val y = new Object()
+ val z = y * x
+ x
+ }
+ """),
+ "value * is not a member of Object",
+ """
+ val z = y * x
+ ^
+ """
+ )
+ }
+ }
+}
diff --git a/scalatexApi/src/test/scala/scalatex/ParserTests.scala b/scalatexApi/src/test/scala/scalatex/ParserTests.scala
new file mode 100644
index 0000000..619bb3a
--- /dev/null
+++ b/scalatexApi/src/test/scala/scalatex/ParserTests.scala
@@ -0,0 +1,50 @@
+package scalatex
+import utest._
+import scalatex.stages.{TwistNodes, Parser}
+
+/**
+ * Created by haoyi on 8/2/14.
+ */
+object ParserTests extends TestSuite{
+ val WN = TwistNodes
+ val WP = Parser
+ def check[T](s: String, f: Parser => T, expected: Option[T]){
+ val parsed = WP.parse(s, f).toOption
+ assert(parsed == expected)
+ }
+ val tests = TestSuite{
+// 'chainedExpressions {
+// check("", _.expression(), None)
+// check("asd", _.expression(), None)
+// check("@asd", _.expression(), Some(
+// WN.Display(WN.ScalaExp(Seq(WN.Simple("asd"))))
+// ))
+//
+// check("@asd{", _.expression(), None)
+// check("@asd(", _.expression(), None)
+// check("@asd()", _.expression(), Some(
+// WN.Display(WN.ScalaExp(Seq(WN.Simple("asd()"))))
+// ))
+// check("@asd(ggnore)", _.expression(), Some(
+// WN.Display(WN.ScalaExp(Seq(WN.Simple("asd(ggnore)"))))
+// ))
+// check("@asd.wtf(ggnore).bbq.lol", _.expression(), Some(
+// WN.Display(WN.ScalaExp(Seq(WN.Simple("asd"), WN.Simple(".wtf(ggnore).bbq.lol"))))
+// ))
+// check("@asd{}", _.expression(), Some(
+// WN.Display(WN.ScalaExp(Seq(WN.Simple("asd"), WN.Block("", None, Seq()))))
+// ))
+// check("@asd{lol}", _.expression(), Some(
+// WN.Display(WN.ScalaExp(Seq(WN.Simple("asd"), WN.Block("", None, Seq(WN.Plain("lol"))))))
+// ))
+// check("@asd{lol}.wtf('l'){gg}", _.expression(), Some(
+// WN.Display(WN.ScalaExp(Seq(
+// WN.Simple("asd"),
+// WN.Block("", None, Seq(WN.Plain("lol"))),
+// WN.Simple(".wtf('l')"),
+// WN.Block("", None, Seq(WN.Plain("gg")))
+// )))
+// ))
+// }
+ }
+}
diff --git a/scalatexApi/src/test/scala/scalatex/TestUtil.scala b/scalatexApi/src/test/scala/scalatex/TestUtil.scala
new file mode 100644
index 0000000..5a72677
--- /dev/null
+++ b/scalatexApi/src/test/scala/scalatex/TestUtil.scala
@@ -0,0 +1,16 @@
+package scalatex
+
+import utest._
+
+
+object TestUtil {
+ implicit def stringify(f: scalatags.Text.all.Frag) = f.render
+ def check(rendered: String*) = {
+ val collapsed = rendered.map(collapse)
+ val first = collapsed(0)
+ assert(collapsed.forall(_ == first))
+ }
+ def collapse(s: String): String = {
+ s.replaceAll("[ \n]", "")
+ }
+}