diff options
Diffstat (limited to 'scalatex/api')
9 files changed, 1689 insertions, 0 deletions
diff --git a/scalatex/api/src/main/scala/scalatex/package.scala b/scalatex/api/src/main/scala/scalatex/package.scala new file mode 100644 index 0000000..1f13e63 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/package.scala @@ -0,0 +1,98 @@ +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.{Parser, Compiler} +import scala.language.experimental.macros +import acyclic.file + +package object scalatex { + /** + * 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 twRuntimeErrors(expr: String): Frag = macro applyMacroRuntimeErrors + def twDebug(expr: String): Frag = macro applyMacroDebug + + def applyMacro(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, false, false) + def applyMacroDebug(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, false, true) + + def applyMacroRuntimeErrors(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, true, false) + + def applyMacroFile(c: Context)(filename: c.Expr[String]): c.Expr[Frag] = { + import c.universe._ + val fileName = filename.tree + .asInstanceOf[Literal] + .value + .value + .asInstanceOf[String] + val txt = io.Source.fromFile(fileName).mkString + val sourceFile = new BatchSourceFile( + new PlainFile(fileName), + txt.toCharArray + ) + + compileThing(c)(txt, sourceFile, 0, false, false) + } + + case class DebugFailure(msg: String, pos: String) extends Exception(msg) + + private[this] def applyMacroFull(c: Context) + (expr: c.Expr[String], + runtimeErrors: Boolean, + debug: Boolean) + : c.Expr[Frag] = { + import c.universe._ + val scalatexFragment = expr.tree + .asInstanceOf[Literal] + .value + .value + .asInstanceOf[String] + val stringStart = + expr.tree + .pos + .lineContent + .drop(expr.tree.pos.column) + .take(2) + compileThing(c)( + scalatexFragment, + expr.tree.pos.source, + expr.tree.pos.point + (if (stringStart == "\"\"") 1 else -1), + runtimeErrors, + debug + ) + } + } + + def compileThing(c: Context) + (scalatexSource: String, + source: SourceFile, + point: Int, + runtimeErrors: Boolean, + debug: Boolean) = { + import c.universe._ + def compile(s: String): c.Tree = { + val realPos = new OffsetPosition(source, point).asInstanceOf[c.universe.Position] + + Compiler(c)(realPos, Parser.tupled(stages.Trim(s))) + } + + + import c.Position + try { + val compiled = compile(scalatexSource) + if (debug) println(compiled) + c.Expr[Frag](c.typeCheck(compiled)) + } 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 scalatex.Internals.DebugFailure($msg, $posMsg)""") + } + } + } +} diff --git a/scalatex/api/src/main/scala/scalatex/stages/Compiler.scala b/scalatex/api/src/main/scala/scalatex/stages/Compiler.scala new file mode 100644 index 0000000..3df8da7 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/stages/Compiler.scala @@ -0,0 +1,103 @@ +package scalatex +package stages + +import acyclic.file + +import scala.reflect.macros.whitebox.Context +import scala.reflect.internal.util.{Position, OffsetPosition} + +/** + * Walks the parsed AST, converting it into a structured Scala c.Tree + */ +object Compiler{ + + def apply(c: Context)(fragPos: c.Position, template: Ast.Block): c.Tree = { + + import c.universe._ + def fragType = tq"scalatags.Text.all.Frag" + + def incPosRec(trees: c.Tree, offset: Int): trees.type = { + + trees.foreach(incPos(_, offset)) + trees + } + def incPos(tree: c.Tree, offset: Int): tree.type = { + + val current = if (tree.pos == NoPosition) 0 else tree.pos.point + c.internal.setPos(tree, + new OffsetPosition( + fragPos.source, + offset + current + fragPos.point + ).asInstanceOf[c.universe.Position] + ) + tree + } + + def compileChain(code: String, parts: Seq[Ast.Chain.Sub], offset: Int): c.Tree = { + + val out = parts.foldLeft(incPosRec(c.parse(code), offset + 1)){ + case (curr, Ast.Chain.Prop(str, offset2)) => + incPos(q"$curr.${TermName(str)}", offset2 + 1) + case (curr, Ast.Chain.Args(str, offset2)) => + val Apply(fun, args) = c.parse(s"omg$str") + incPos(Apply(curr, args.map(incPosRec(_, offset2 - 2))), offset2) + case (curr, Ast.Chain.TypeArgs(str, offset2)) => + val TypeApply(fun, args) = c.parse(s"omg$str") + incPos(TypeApply(curr, args.map(incPosRec(_, offset2 - 2))), offset2) + case (curr, Ast.Block(parts, offset1)) => + incPos(q"$curr(..${compileBlock(parts, offset1)})", offset1) + case (curr, Ast.Header(header, block, offset1)) => + incPos(q"$curr(${compileHeader(header, block, offset1)})", offset1) + } + + out + } + def compileBlock(parts: Seq[Ast.Block.Sub], offset: Int): Seq[c.Tree] = { + val res = parts.map{ + case Ast.Block.Text(str, offset1) => + incPos(q"$str", offset1) + case Ast.Chain(code, parts, offset1) => + compileChain(code, parts, offset1) + case Ast.Header(header, block, offset1) => + compileHeader(header, block, offset1) + case Ast.Block.IfElse(condString, Ast.Block(parts2, offset2), elseBlock, offset1) => + val If(cond, _, _) = c.parse(condString + "{}") + val elseCompiled = elseBlock match{ + case Some(Ast.Block(parts3, offset3)) => compileBlockWrapped(parts3, offset3) + case None => EmptyTree + } + + val res = If(incPosRec(cond, offset1 + 2), compileBlockWrapped(parts2, offset2), elseCompiled) + + incPos(res, offset1) + res + case Ast.Block.For(generators, Ast.Block(parts2, offset2), offset1) => + val fresh = c.fresh() + + val tree = incPosRec(c.parse(s"$generators yield $fresh"), offset1 + 2) + + 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)) + a2 + case Ident(x: TermName) if x.decoded == fresh => + compileBlockWrapped(parts2, offset2) + } + + rec(tree) + } + res + } + def compileBlockWrapped(parts: Seq[Ast.Block.Sub], offset: Int): c.Tree = { + incPos(q"Seq[$fragType](..${compileBlock(parts, offset)})", offset) + } + def compileHeader(header: String, block: Ast.Block, offset: Int): c.Tree = { + val Block(stmts, expr) = c.parse(s"{$header\n ()}") + Block(stmts, compileBlockWrapped(block.parts, block.offset)) + } + + val res = compileBlockWrapped(template.parts, template.offset) + res + } +}
\ No newline at end of file diff --git a/scalatex/api/src/main/scala/scalatex/stages/Omg/scala.scala b/scalatex/api/src/main/scala/scalatex/stages/Omg/scala.scala new file mode 100644 index 0000000..81fa1f9 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/stages/Omg/scala.scala @@ -0,0 +1,8 @@ +package scalatex.stages.Omg + +/** + * Created by haoyi on 12/3/14. + */ +class scala { + +} diff --git a/scalatex/api/src/main/scala/scalatex/stages/Parser.scala b/scalatex/api/src/main/scala/scalatex/stages/Parser.scala new file mode 100644 index 0000000..0b87d97 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/stages/Parser.scala @@ -0,0 +1,170 @@ +package scalatex +package stages +import acyclic.file +import org.parboiled2._ +import scalaParser.ScalaSyntax + +/** + * Parses the input text into a roughly-structured AST. This AST + * is much simpler than the real Scala AST, but serves us well + * enough until we stuff the code-strings into the real Scala + * parser later + */ +object Parser extends ((String, Int) => Ast.Block){ + def apply(input: String, offset: Int = 0): Ast.Block = { + new Parser(input, offset).Body.run().get + } +} +class Parser(input: ParserInput, indent: Int = 0, offset: Int = 0) extends scalaParser.ScalaSyntax(input) { + def offsetCursor = offset + cursor + val txt = input.sliceString(0, input.length) + val indentTable = txt.split('\n').map{ s => + if (s.trim == "") -1 + else s.takeWhile(_ == ' ').length + } + val nextIndentTable = (0 until indentTable.length).map { i => + val index = indentTable.indexWhere(_ != -1, i + 1) + if (index == -1) 100000 + else indentTable(index) + } + def cursorNextIndent() = { + nextIndentTable(txt.take(cursor).count(_ == '\n')) + } + + def TextNot(chars: String) = rule { + push(offsetCursor) ~ capture(oneOrMore(noneOf(chars + "\n") | "@@")) ~> { + (i, x) => Ast.Block.Text(x.replace("@@", "@"), i) + } + } + def Text = TextNot("@") + def Code = rule { + "@" ~ capture(Identifiers.Id | BlockExpr2 | ('(' ~ optional(Exprs) ~ ')')) + } + def Header = rule { + "@" ~ capture(Def | Import) + } + + def HeaderBlock: Rule1[Ast.Header] = rule{ + Header ~ zeroOrMore(capture(WL) ~ Header ~> (_ + _)) ~ runSubParser{new Parser(_, indent, cursor).Body0} ~> { + (start: String, heads: Seq[String], body: Ast.Block) => Ast.Header(start + heads.mkString, body) + } + } + + def BlankLine = rule{ '\n' ~ zeroOrMore(' ') ~ &('\n') } + def IndentSpaces = rule{ indent.times(' ') ~ zeroOrMore(' ') } + def Indent = rule{ '\n' ~ IndentSpaces } + def LoneScalaChain: Rule2[Ast.Block.Text, Ast.Chain] = rule { + (push(offsetCursor) ~ capture(Indent) ~> ((i, x) => Ast.Block.Text(x, i))) ~ + ScalaChain ~ + IndentBlock ~> { + (chain: Ast.Chain, body: Ast.Block) => chain.copy(parts = chain.parts :+ body) + } + } + def IndentBlock = rule{ + &("\n") ~ + test(cursorNextIndent() > indent) ~ + runSubParser(new Parser(_, cursorNextIndent(), cursor).Body) + } + def IfHead = rule{ "@" ~ capture("if" ~ "(" ~ Expr ~ ")") } + def IfElse1 = rule{ + push(offsetCursor) ~ IfHead ~ BraceBlock ~ optional("else" ~ (BraceBlock | IndentBlock)) + } + def IfElse2 = rule{ + Indent ~ push(offsetCursor) ~ IfHead ~ IndentBlock ~ optional(Indent ~ "@else" ~ (BraceBlock | IndentBlock)) + } + def IfElse = rule{ + (IfElse1 | IfElse2) ~> ((a, b, c, d) => Ast.Block.IfElse(b, c, d, a)) + } + + def ForHead = rule{ + push(offsetCursor) ~ "@" ~ capture("for" ~ '(' ~ Enumerators ~ ')') + } + def ForLoop = rule{ + ForHead ~ + BraceBlock ~> ((a, b, c) => Ast.Block.For(b, c, a)) + } + def LoneForLoop = rule{ + (push(offsetCursor) ~ capture(Indent) ~> ((i, t) => Ast.Block.Text(t, i))) ~ + ForHead ~ + IndentBlock ~> + ((a, b, c) => Ast.Block.For(b, c, a)) + } + + def ScalaChain = rule { + push(offsetCursor) ~ Code ~ zeroOrMore(Extension) ~> { (a, b, c) => Ast.Chain(b, c, a)} + } + def Extension: Rule1[Ast.Chain.Sub] = rule { + (push(offsetCursor) ~ '.' ~ capture(Identifiers.Id) ~> ((x, y) => Ast.Chain.Prop(y, x))) | + (push(offsetCursor) ~ capture(TypeArgs2) ~> ((x, y) => Ast.Chain.TypeArgs(y, x))) | + (push(offsetCursor) ~ capture(ArgumentExprs2) ~> ((x, y) => Ast.Chain.Args(y, x))) | + BraceBlock + } + def Ws = WL + // clones of the version in ScalaSyntax, but without tailing whitespace or newlines + def TypeArgs2 = rule { '[' ~ Ws ~ Types ~ ']' } + def ArgumentExprs2 = rule { + '(' ~ Ws ~ + (optional(Exprs ~ ',' ~ Ws) ~ PostfixExpr ~ ':' ~ Ws ~ '_' ~ Ws ~ '*' ~ Ws | optional(Exprs) ) ~ + ')' + } + def BlockExpr2: Rule0 = rule { '{' ~ Ws ~ (CaseClauses | Block) ~ Ws ~ '}' } + def BraceBlock: Rule1[Ast.Block] = rule{ '{' ~ BodyNoBrace ~ '}' } + + def BodyItem(exclusions: String): Rule1[Seq[Ast.Block.Sub]] = rule{ + ForLoop ~> (Seq(_)) | + LoneForLoop ~> (Seq(_, _)) | + IfElse ~> (Seq(_)) | + LoneScalaChain ~> (Seq(_, _)) | + HeaderBlock ~> (Seq(_)) | + TextNot("@" + exclusions) ~> (Seq(_)) | + (push(offsetCursor) ~ capture(Indent) ~> ((i, x) => Seq(Ast.Block.Text(x, i)))) | + (push(offsetCursor) ~ capture(BlankLine) ~> ((i, x) => Seq(Ast.Block.Text(x, i)))) | + ScalaChain ~> (Seq(_: Ast.Block.Sub)) + } + def Body = rule{ BodyEx() } + def BodyNoBrace = rule{ BodyEx("}") } + def BodyEx(exclusions: String = "") = rule{ + push(offsetCursor) ~ oneOrMore(BodyItem(exclusions)) ~> {(i, x) => + Ast.Block(x.flatten, i) + } + } + def Body0 = rule{ + push(offsetCursor) ~ zeroOrMore(BodyItem("")) ~> {(i, x) => + Ast.Block(x.flatten, i) + } + } +} + +trait Ast{ + def offset: Int +} +object Ast{ + + /** + * @param parts The various bits of text and other things which make up this block + * @param offset + */ + case class Block(parts: Seq[Block.Sub], + offset: Int = 0) + extends Chain.Sub with Block.Sub + object Block{ + trait Sub extends Ast + case class Text(txt: String, offset: Int = 0) extends Block.Sub + case class For(generators: String, block: Block, offset: Int = 0) extends Block.Sub + case class IfElse(condition: String, block: Block, elseBlock: Option[Block], offset: Int = 0) extends Block.Sub + } + case class Header(front: String, block: Block, offset: Int = 0) extends Block.Sub with Chain.Sub + + /** + * @param lhs The first expression in this method-chain + * @param parts A list of follow-on items chained to the first + * @param offset + */ + case class Chain(lhs: String, parts: Seq[Chain.Sub], offset: Int = 0) extends Block.Sub + object Chain{ + trait Sub extends Ast + case class Prop(str: String, offset: Int = 0) extends Sub + case class TypeArgs(str: String, offset: Int = 0) extends Sub + case class Args(str: String, offset: Int = 0) extends Sub + } +} diff --git a/scalatex/api/src/main/scala/scalatex/stages/Trim.scala b/scalatex/api/src/main/scala/scalatex/stages/Trim.scala new file mode 100644 index 0000000..8993734 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/stages/Trim.scala @@ -0,0 +1,29 @@ +package scalatex.stages +import acyclic.file + +/** + * Preprocesses the input string to normalize things related to whitespace + * + * Find the "first" non-whitespace-line of the text and remove the front + * of every line to align that first line with the left margin. + * + * Remove all trailing whitespace from each line. + */ +object Trim extends (String => (String, Int)){ + def apply(str: String) = { + val lines = str.split("\n", -1) + val offset = lines.iterator + .filter(_.length > 0) + .next() + .takeWhile(_ == ' ') + .length + val res = lines.iterator + .map(_.replaceFirst("\\s+$", "")) + .mkString("\n") + (res, offset) + } + def old(str: String) = { + val (res, offset) = this.apply(str) + res.split("\n", -1).map(_.drop(offset)).mkString("\n") + } +} diff --git a/scalatex/api/src/test/scala/scalatex/BasicTests.scala b/scalatex/api/src/test/scala/scalatex/BasicTests.scala new file mode 100644 index 0000000..4bc362c --- /dev/null +++ b/scalatex/api/src/test/scala/scalatex/BasicTests.scala @@ -0,0 +1,468 @@ +package scalatex +import utest._ +import scala.collection.mutable.ArrayBuffer +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 + """ + ) + } + 'definitions{ + '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> + """ + ) + } + 'valDefVar{ + check( + tw(""" + Hello + @val x = 1 + World @x + @def y = "omg" + mooo + @y + """), + """ + Hello + World 1 + mooo + omg + """ + ) + } + 'classObjectTrait{ + check( + tw(""" + @trait Trait{ + def tt = 2 + } + Hello + @case object moo extends Trait{ + val omg = "wtf" + } + + @moo.toString + @moo.omg + @case class Foo(i: Int, s: String, b: Boolean) + TT is @moo.tt + @Foo(10, "10", true).toString + """), + """ + Hello + moo + wtf + TT is 2 + Foo(10, 10, true) + """ + ) + } + } + 'parenArgumentLists{ + 'attributes{ + check( + tw(""" + @div(id:="my-id"){ omg } + @div(id:="my-id") + omg + """), + """ + <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></h1><span></span><a></a>HelloWorld + <h2></h2><span></span><a></a>hello<b></b>world + <h3></h3><i></i><div></div>Cow + """ + ) + } + '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 + } + """), + "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/scalatex/api/src/test/scala/scalatex/ErrorTests.scala b/scalatex/api/src/test/scala/scalatex/ErrorTests.scala new file mode 100644 index 0000000..d8cd4f5 --- /dev/null +++ b/scalatex/api/src/test/scala/scalatex/ErrorTests.scala @@ -0,0 +1,373 @@ +package scalatex + +import utest._ +import scalatex.stages._ +import scalatags.Text.all._ +import scalatex.Internals.{DebugFailure, twRuntimeErrors} + +/** +* 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( + twRuntimeErrors("omg @notInScope lol"), + """not found: value notInScope""", + """ + twRuntimeErrors("omg @notInScope lol"), + ^ + """ + ) + + 'chained{ + 'properties { + * - check( + twRuntimeErrors("omg @math.lol lol"), + """object lol is not a member of package math""", + """ + twRuntimeErrors("omg @math.lol lol"), + ^ + """ + ) + + * - check( + twRuntimeErrors("omg @math.E.lol lol"), + """value lol is not a member of Double""", + """ + twRuntimeErrors("omg @math.E.lol lol"), + ^ + """ + ) + * - check( + twRuntimeErrors("omg @_root_.scala.math.lol lol"), + """object lol is not a member of package math""", + """ + twRuntimeErrors("omg @_root_.scala.math.lol lol"), + ^ + """ + ) + * - check( + twRuntimeErrors("omg @_root_.scala.gg.lol lol"), + """object gg is not a member of package scala""", + """ + twRuntimeErrors("omg @_root_.scala.gg.lol lol"), + ^ + """ + ) + * - check( + twRuntimeErrors("omg @_root_.ggnore.math.lol lol"), + """object ggnore is not a member of package <root>""", + """ + twRuntimeErrors("omg @_root_.ggnore.math.lol lol"), + ^ + """ + ) + } + 'calls{ + * - check( + twRuntimeErrors("@scala.QQ.abs(-10).tdo(10).sum.z"), + """object QQ is not a member of package scala""", + """ + twRuntimeErrors("@scala.QQ.abs(-10).tdo(10).sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.abs(-10).tdo(10).sum.z"), + "value tdo is not a member of Int", + """ + twRuntimeErrors("@scala.math.abs(-10).tdo(10).sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z"), + "value z is not a member of Int", + """ + twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z()"), + "value z is not a member of Int", + """ + twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z()"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.abs(-10).cow.sum.z"), + "value cow is not a member of Int", + """ + twRuntimeErrors("@scala.math.abs(-10).cow.sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.smath.abs.cow.sum.z"), + "object smath is not a member of package scala", + """ + twRuntimeErrors("@scala.smath.abs.cow.sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.cos('omg)"), + "type mismatch", + """ + twRuntimeErrors("@scala.math.cos('omg)"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.cos[omg]('omg)"), + "not found: type omg", + """ + twRuntimeErrors("@scala.math.cos[omg]('omg)"), + ^ + """ + ) + * - check( + twRuntimeErrors(""" + 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 + ^ + """ + ) + } + 'curlies{ + * - check( + twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), + "missing arguments for method foldLeft", + """ + twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), + ^ + """ + ) + + * - check( + twRuntimeErrors("@Nil.foldLeft{XY}"), + "missing arguments for method foldLeft", + """ + twRuntimeErrors("@Nil.foldLeft{XY}"), + ^ + """ + ) + +// * - check( +// twRuntimeErrors("@Seq(1).map{(y: String) => omg}"), +// "type mismatch", +// """ +// twRuntimeErrors("@Seq(1).map{(y: String) => omg}"), +// ^ +// """ +// ) +// * - check( +// twRuntimeErrors("@Nil.map{ omg}"), +// "too many arguments for method map", +// """ +// twRuntimeErrors("@Nil.map{ omg}"), +// ^ +// """ +// ) + } + 'callContents{ + * - check( + twRuntimeErrors("@scala.math.abs((1, 2).wtf)"), + "value wtf is not a member of (Int, Int)", + """ + twRuntimeErrors("@scala.math.abs((1, 2).wtf)"), + ^ + """ + ) + + * - check( + twRuntimeErrors("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"), + "value wtf is not a member of String", + """ + twRuntimeErrors("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"), + ^ + """ + ) + } + } + 'ifElse{ + 'oneLine { + * - check( + twRuntimeErrors("@if(math > 10){ 1 }else{ 2 }"), + "object > is not a member of package math", + """ + twRuntimeErrors("@if(math > 10){ 1 }else{ 2 }"), + ^ + """ + ) + * - check( + twRuntimeErrors("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"), + "Unspecified value parameter y", + """ + twRuntimeErrors("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"), + ^ + """ + ) + * - check( + twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), + "too many arguments for method sin: (x: Double)Double", + """ + twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), + ^ + """ + ) + } + 'multiLine{ + * - check( + twRuntimeErrors(""" + 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( + twRuntimeErrors(""" + 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( + twRuntimeErrors(""" + 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( + twRuntimeErrors("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"), + """value omglolol is not a member of Int""", + """ + twRuntimeErrors("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"), + ^ + """ + ) + + 'body - check( + twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), + """too many arguments for method +""", + """ + twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), + ^ + """ + ) + } + 'multiLine{ + 'body - check( + twRuntimeErrors(""" + 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( + twRuntimeErrors(""" + omg @notInScope lol + """), + """not found: value notInScope""", + """ + omg @notInScope lol + ^ + """ + ) +// 'wrongType - check( +// twRuntimeErrors(""" +// omg @{() => ()} lol +// """), +// """type mismatch""", +// """ +// omg @{() => ()} lol +// ^ +// """ +// ) + + 'bigExpression - check( + twRuntimeErrors(""" + @{ + 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/scalatex/api/src/test/scala/scalatex/ParserTests.scala b/scalatex/api/src/test/scala/scalatex/ParserTests.scala new file mode 100644 index 0000000..9a4ee63 --- /dev/null +++ b/scalatex/api/src/test/scala/scalatex/ParserTests.scala @@ -0,0 +1,424 @@ +package scalatex + + +import org.parboiled2._ +import scalaParser.ScalaSyntax + +import scalatex.stages.{Trim, Parser, Ast} +import scalatex.stages.Ast.Block.{IfElse, For, Text} +import Ast.Chain.Args + +object ParserTests extends utest.TestSuite{ + import Ast._ + import utest._ + def check[T](input: String, parse: Parser => scala.util.Try[T], expected: T) = { + val parsed = parse(new Parser(input)).get + assert(parsed == expected) + } + def tests = TestSuite{ + 'Trim{ + def wrap(s: String) = "|" + s + "|" + * - { + val trimmed = wrap(stages.Trim.old(""" + i am cow + hear me moo + i weigh twice as much as you + """)) + val expected = wrap(""" + |i am cow + | hear me moo + | i weigh twice as much as you + |""".stripMargin) + assert(trimmed == expected) + + } + * - { + val trimmed = wrap(stages.Trim.old( + """ + @{"lol" * 3} + @{ + val omg = "omg" + omg * 2 + } + """ + )) + val expected = wrap( + """ + |@{"lol" * 3} + |@{ + | val omg = "omg" + | omg * 2 + |} + |""".stripMargin + ) + assert(trimmed == expected) + } + 'dropTrailingWhitespace - { + + val trimmed = wrap(stages.Trim.old( + Seq( + " i am a cow ", + " hear me moo ", + " i weigh twice as much as you" + ).mkString("\n") + )) + val expected = wrap( + Seq( + "i am a cow", + " hear me moo", + " i weigh twice as much as you" + ).mkString("\n") + ) + assert(trimmed == expected) + } + } + 'Text { + * - check("i am a cow", _.Text.run(), Block.Text("i am a cow")) + * - check("i am a @cow", _.Text.run(), Block.Text("i am a ")) + * - check("i am a @@cow", _.Text.run(), Block.Text("i am a @cow")) + * - check("i am a @@@cow", _.Text.run(), Block.Text("i am a @")) + * - check("i am a @@@@cow", _.Text.run(), Block.Text("i am a @@cow")) + + } + 'Code{ + 'identifier - check("@gggg ", _.Code.run(), "gggg") + 'parens - check("@(1 + 1)lolsss\n", _.Code.run(), "(1 + 1)") + 'curlies - check("@{{1} + (1)} ", _.Code.run(), "{{1} + (1)}") + 'blocks - check("@{val x = 1; 1} ", _.Code.run(), "{val x = 1; 1}") + 'weirdBackticks - check("@{`{}}{()@`}\n", _.Code.run(), "{`{}}{()@`}") + } + 'MiscCode{ + 'imports{ + * - check("@import math.abs", _.Header.run(), "import math.abs") + * - check("@import math.{abs, sin}", _.Header.run(), "import math.{abs, sin}") + } + 'headerblocks{ + check( + """@import math.abs + |@import math.sin + | + |hello world + |""".stripMargin, + _.HeaderBlock.run(), + Ast.Header( + "import math.abs\nimport math.sin", + Ast.Block( + Seq(Text("\n", 33), Text("\n", 34), Text("hello world", 35), Text("\n", 46)), + 33 + ) + ) + ) + } + 'caseclass{ + check( + """@case class Foo(i: Int, s: String) + """.stripMargin, + _.Header.run(), + "case class Foo(i: Int, s: String)" + ) + } + + } + 'Block{ + * - check("{i am a cow}", _.BraceBlock.run(), Block(Seq(Block.Text("i am a cow", 1)), 1)) + * - check("{i @am a @cow}", _.BraceBlock.run(), + Block(Seq( + Block.Text("i ", 1), + Chain("am",Seq(), 3), + Block.Text(" a ", 6), + Chain("cow",Seq(), 9) + ), 1) + ) + } + 'Chain{ + * - check("@omg.bbq[omg].fff[fff](123) ", _.ScalaChain.run(), + Chain("omg",Seq( + Chain.Prop("bbq", 4), + Chain.TypeArgs("[omg]", 8), + Chain.Prop("fff", 13), + Chain.TypeArgs("[fff]", 17), + Chain.Args("(123)", 22) + )) + ) + * - check("@omg{bbq}.cow(moo){a @b}\n", _.ScalaChain.run(), + Chain("omg",Seq( + Block(Seq(Text("bbq", 5)), 5), + Chain.Prop("cow", 9), + Chain.Args("(moo)", 13), + Block(Seq(Text("a ", 19), Chain("b", Nil, 21)), 19) + )) + ) + } + 'ControlFlow{ + 'for { + 'for - check( + "@for(x <- 0 until 3){lol}", + _.ForLoop.run(), + For("for(x <- 0 until 3)", Block(Seq(Text("lol", 21)), 21)) + ) + 'forBlock - check( + """ + |@for(x <- 0 until 3) + | lol""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + For( + "for(x <- 0 until 3)", + Block(Seq(Text("\n ", 21), Text("lol", 24)), 21), + 1 + ) + )) + ) + 'forBlockBraces - check( + """ + |@for(x <- 0 until 3){ + | lol + |}""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + For( + "for(x <- 0 until 3)", + Block(Seq(Text("\n ", 22), Text("lol", 25), Text("\n", 28)), 22), + 1 + ) + )) + ) + } + 'ifElse { + 'if - check( + "@if(true){lol}", + _.IfElse.run(), + IfElse("if(true)", Block(Seq(Text("lol", 10)), 10), None) + ) + 'ifElse - check( + "@if(true){lol}else{ omg }", + _.IfElse.run(), + IfElse("if(true)", Block(Seq(Text("lol", 10)), 10), Some(Block(Seq(Text(" omg ", 19)), 19))) + ) + 'ifBlock - check( + """ + |@if(true) + | omg""".stripMargin, + _.IfElse.run(), + IfElse("if(true)", Block(Seq(Text("\n ", 10), Text("omg", 13)), 10), None, 1) + ) + 'ifBlockElseBlock - check( + """ + |@if(true) + | omg + |@else + | wtf""".stripMargin, + _.IfElse.run(), + IfElse( + "if(true)", + Block(Seq(Text("\n ", 10), Text("omg", 13)), 10), + Some(Block(Seq(Text("\n ", 22), Text("wtf", 25)), 22)), + 1 + ) + ) + 'ifBlockElseBraceBlock - check( + """@if(true){ + | omg + |}else{ + | wtf + |}""".stripMargin, + _.IfElse.run(), + IfElse( + "if(true)", + Block(Seq(Text("\n ", 10), Text("omg", 13), Text("\n", 16)), 10), + Some(Block(Seq(Text("\n ", 23), Text("wtf", 26), Text("\n", 29)), 23)), + 0 + ) + ) + 'ifBlockElseBraceBlockNested - { + val res = Parser(Trim.old( + """ + @p + @if(true){ + Hello + }else{ + lols + } + """)) + val expected = + Block(Vector( + Text("\n"), + Chain("p",Vector(Block(Vector( + Text("\n ", 3), + IfElse("if(true)", + Block(Vector( + Text("\n ", 16), Text("Hello", 21), Text("\n ", 26) + ), 16), + Some(Block(Vector( + Text("\n ", 35), Text("lols", 40), Text("\n ", 44) + ), 35)), + 6 + )), 3)), 1), + Text("\n", 48) + )) + assert(res == expected) + } + 'ifElseBlock - check( + """@if(true){ + | omg + |}else + | wtf""".stripMargin, + _.IfElse.run(), + IfElse( + "if(true)", + Block(Seq(Text("\n ", 10), Text("omg", 13), Text("\n", 16)), 10), + Some(Block(Seq(Text("\n ", 22), Text("wtf", 25)), 22)) + ) + ) + } + + } + 'Body{ + 'indents - check( + """ + |@omg + | @wtf + | @bbq + | @lol""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("omg",Seq(Block(Seq( + Text("\n ", 5), + Chain("wtf",Seq(Block(Seq( + Text("\n ", 7), + Chain("bbq",Seq(Block(Seq( + Text("\n ", 9), + Chain("lol",Seq(), 16) + ), 9)), 12) + ), 7)), 8) + ), 5)), 1) + )) + ) + 'dedents - check( + """ + |@omg + | @wtf + |@bbq""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("omg",Seq(Block( + Seq( + Text("\n ", 5), + Chain("wtf",Seq(), 8) + ), + 5 + )), 1), + Text("\n", 12), + Chain("bbq", Seq(), 13) + )) + ) + 'braces - check( + """ + |@omg{ + | @wtf + |} + |@bbq""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("omg",Seq(Block( + Seq( + Text("\n ", 6), + Chain("wtf",Seq(), 9), + Text("\n", 13) + ), + 6 + )), 1), + Text("\n", 15), + Chain("bbq", Seq(), 16) + )) + ) + 'dedentText - check( + """ + |@omg("lol", 1, 2) + | @wtf + |bbq""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("omg",Seq( + Args("""("lol", 1, 2)""", 5), + Block(Seq( + Text("\n ", 18), + Chain("wtf",Seq(), 21) + ), 18) + ), 1), + Text("\n", 25), + Text("bbq", 26) + )) + ) + * - check( + """ + |@omg("lol", + |1, + | 2 + | ) + | wtf + |bbq""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n", 0), + Chain("omg",Seq( + Args("(\"lol\",\n1,\n 2\n )", 5), + Block(Seq( + Text("\n ", 30), Text("wtf", 33) + ), 30) + ), 1), + Text("\n", 36), + Text("bbq", 37) + ), 0) + ) + 'codeBlock - check( + """@{ + | val omg = "omg" + | omg * 2 + |}""".stripMargin, + _.Code.run(), + """{ + | val omg = "omg" + | omg * 2 + |}""".stripMargin + ) + 'codeBlocks - check( + """ + |@{"lol" * 3} + |@{ + | val omg = "omg" + | omg * 2 + |}""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("{\"lol\" * 3}", Seq(), 1), + Text("\n", 13), + Chain("""{ + | val omg = "omg" + | omg * 2 + |}""".stripMargin, + Seq(), + 14 + ) + )) + ) + } +// 'Test{ +// check( +// "@{() => ()}", +// _.Code.run(), +// "" +// ) +// } + } +} + + + diff --git a/scalatex/api/src/test/scala/scalatex/TestUtil.scala b/scalatex/api/src/test/scala/scalatex/TestUtil.scala new file mode 100644 index 0000000..5a72677 --- /dev/null +++ b/scalatex/api/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]", "") + } +} |