From aae594fd3c8397abca4cd4e55f538d41b172b4e3 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 1 Nov 2014 00:56:30 -0700 Subject: added everything --- scalatexApi/src/main/scala/scalatex/Util.scala | 10 + scalatexApi/src/main/scala/scalatex/package.scala | 86 +++ .../src/main/scala/scalatex/stages/Compiler.scala | 161 +++++ .../main/scala/scalatex/stages/IndentHandler.scala | 105 +++ .../src/main/scala/scalatex/stages/Parser.scala | 790 +++++++++++++++++++++ .../src/test/scala/scalatex/AdvancedTests.scala | 120 ++++ .../src/test/scala/scalatex/BasicTests.scala | 418 +++++++++++ .../src/test/scala/scalatex/ErrorTests.scala | 321 +++++++++ .../src/test/scala/scalatex/ParserTests.scala | 50 ++ scalatexApi/src/test/scala/scalatex/TestUtil.scala | 16 + 10 files changed, 2077 insertions(+) create mode 100644 scalatexApi/src/main/scala/scalatex/Util.scala create mode 100644 scalatexApi/src/main/scala/scalatex/package.scala create mode 100644 scalatexApi/src/main/scala/scalatex/stages/Compiler.scala create mode 100644 scalatexApi/src/main/scala/scalatex/stages/IndentHandler.scala create mode 100644 scalatexApi/src/main/scala/scalatex/stages/Parser.scala create mode 100644 scalatexApi/src/test/scala/scalatex/AdvancedTests.scala create mode 100644 scalatexApi/src/test/scala/scalatex/BasicTests.scala create mode 100644 scalatexApi/src/test/scala/scalatex/ErrorTests.scala create mode 100644 scalatexApi/src/test/scala/scalatex/ParserTests.scala create mode 100644 scalatexApi/src/test/scala/scalatex/TestUtil.scala (limited to 'scalatexApi') 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 + """), + "|iamcowhearmemoo|" + ) + } + '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 +

+ 2 + 4 +

+ """ + ) + } + 'parenArgumentLists{ + 'attributes{ + check( + tw(""" + @div(id:="my-id") omg + @div(id:="my-id"){ omg } + @div(id:="my-id") + omg + """), + """ + omg + omg + omg + """ + ) + } + 'multiline{ + + check( + tw(""" + @div( + h1("Hello World"), + p("I am a ", b{"cow"}) + ) + """), + """ +
+

Hello World

+

I am a cow

+
+ """ + ) + } + } + '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 + """), + """ +

HelloWorld

+

helloWorld2

+

Cow

+ """ + ) + } + 'linearNested{ + check( + tw(""" + @h1 @span @a Hello World + @h2 @span @a hello + @b world + @h3 @i + @div Cow + """), + """ +

HelloWorld

+

helloworld

+

Cow

+ """ + ) + } + 'crasher{ + tw(""" +@html + @head + @meta + @div + @a + @span + """) + } + } + 'curlies{ + 'simple{ + val world = "World2" + + check( + tw("""@div{Hello World}"""), + """
HelloWorld
""" + ) + } + 'multiline{ + check( + tw(""" + @div{ + Hello + } + """), + """ +
Hello
+ """ + ) + } + } + 'mixed{ + check( + tw(""" + @div{ + Hello + @div + @h1 WORLD @b{!!!} + lol + @p{ + @h2{Header 2} + } + } + """), + """ +
+ Hello +
+

WORLD!!!lol

+

Header2

+
+
+ """ + ) + } + + 'args{ + val things = Seq(1, 2, 3) + check( + tw(""" + @ul + @things.map { x => + @li @x + } + """), + tw(""" + @ul + @things.map x => + @li @x + + """), + """ +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +
+ """ + ) + } + } + + '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 + } + """), + "

lol00lol01lol10lol11

" + ) + check( + tw(""" + @p + @for(x <- 0 until 2) + @for(y <- 0 until 2) + lol@x@y + """), + "

lol00lol01lol10lol11

" + ) + + * - check( + tw( + """ + @for(x <- 0 until 2; y <- 0 until 2) + @div{@x@y} + + """), + """
00
01
10
11
""" + ) + } + + 'ifElse{ + 'basicExamples{ + * - check( + tw(""" + @if(false) + Hello + @else lols + @p + """), + "lols

" + ) + + * - check( + tw(""" + @div + @if(true) + Hello + @else lols + """), + "
Hello
" + ) + + * - check( + tw(""" + @div + @if(true) Hello + @else lols + """), + "
Hello
" + ) + * - check( + tw(""" + @if(false) Hello + @else lols + """), + "lols" + ) + * - check( + tw(""" + @if(false) + Hello + @else + lols + @img + """), + "lols" + ) + * - check( + tw(""" + @p + @if(true) + Hello + @else + lols + """), + tw(""" + @p + @if(true) { + Hello + } else { + lols + } + """), + "

Hello

" + ) + } + '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 + """), + "

Hello1

" + ) + * - 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 + """), + "

lols2

" + ) + } + } + } +} 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 """, + """ + 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]", "") + } +} -- cgit v1.2.3