summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi@dropbox.com>2014-10-26 12:28:32 -0700
committerLi Haoyi <haoyi@dropbox.com>2014-10-26 12:28:32 -0700
commitab668dead5c3123eb9fb26b9e94c6eccabaf6ab7 (patch)
treef6f538f84db0cb10076c26d9ae6ed16a5de436ba
downloadhands-on-scala-js-ab668dead5c3123eb9fb26b9e94c6eccabaf6ab7.tar.gz
hands-on-scala-js-ab668dead5c3123eb9fb26b9e94c6eccabaf6ab7.tar.bz2
hands-on-scala-js-ab668dead5c3123eb9fb26b9e94c6eccabaf6ab7.zip
first commit
-rw-r--r--.gitignore5
-rw-r--r--api/src/main/scala/twist/Util.scala10
-rw-r--r--api/src/main/scala/twist/package.scala88
-rw-r--r--api/src/main/scala/twist/stages/Compiler.scala160
-rw-r--r--api/src/main/scala/twist/stages/IndentHandler.scala105
-rw-r--r--api/src/main/scala/twist/stages/Parser.scala790
-rw-r--r--api/src/test/scala/twist/AdvancedTests.scala120
-rw-r--r--api/src/test/scala/twist/BasicTests.scala418
-rw-r--r--api/src/test/scala/twist/ErrorTests.scala321
-rw-r--r--api/src/test/scala/twist/ParserTests.scala50
-rw-r--r--api/src/test/scala/twist/TestUtil.scala16
-rw-r--r--book/index.tw31
-rw-r--r--book/intro.tw97
-rwxr-xr-xbook/src/main/resources/css/layouts/side-menu.css238
-rwxr-xr-xbook/src/main/resources/css/pure-min.css11
-rw-r--r--book/src/main/resources/images/javascript-the-good-parts-the-definitive-guide.jpgbin0 -> 53631 bytes
-rwxr-xr-xbook/src/main/resources/js/ui.js35
-rw-r--r--book/src/main/scala/book/Book.scala43
-rw-r--r--book/src/main/scala/book/Main.scala36
-rw-r--r--book/src/main/scala/book/Utils.scala57
-rw-r--r--build.sbt38
-rw-r--r--examples/src/main/scala/Example.scala33
-rw-r--r--intro.md76
-rw-r--r--project/build.properties1
-rw-r--r--project/build.sbt2
-rw-r--r--readme.md64
26 files changed, 2845 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c70f9c2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/*/target/
+target/
+output/
+.DS_STORE
+*.iml
diff --git a/api/src/main/scala/twist/Util.scala b/api/src/main/scala/twist/Util.scala
new file mode 100644
index 0000000..c7b3598
--- /dev/null
+++ b/api/src/main/scala/twist/Util.scala
@@ -0,0 +1,10 @@
+package twist
+import acyclic.file
+
+object Util {
+
+ implicit class Pipeable[T](t: T){
+ def |>[V](f: T => V): V = f(t)
+ }
+}
+
diff --git a/api/src/main/scala/twist/package.scala b/api/src/main/scala/twist/package.scala
new file mode 100644
index 0000000..a70212a
--- /dev/null
+++ b/api/src/main/scala/twist/package.scala
@@ -0,0 +1,88 @@
+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 twist.stages.Compiler
+import scala.language.experimental.macros
+import acyclic.file
+
+package object twist {
+ 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/api/src/main/scala/twist/stages/Compiler.scala b/api/src/main/scala/twist/stages/Compiler.scala
new file mode 100644
index 0000000..fdfc68e
--- /dev/null
+++ b/api/src/main/scala/twist/stages/Compiler.scala
@@ -0,0 +1,160 @@
+package twist.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 =>
+ if (x.pos != NoPosition) x.pos = 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))
+ a2.pos = a.pos
+ f2.pos = 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 =>
+ x.pos = 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 =>
+ x.pos = 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){
+ tree.pos = 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
+ }
+ res.pos = 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(_))})")
+ func.pos = 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/api/src/main/scala/twist/stages/IndentHandler.scala b/api/src/main/scala/twist/stages/IndentHandler.scala
new file mode 100644
index 0000000..ea396e7
--- /dev/null
+++ b/api/src/main/scala/twist/stages/IndentHandler.scala
@@ -0,0 +1,105 @@
+package twist.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()))
+
+
+ /**
+ * 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/api/src/main/scala/twist/stages/Parser.scala b/api/src/main/scala/twist/stages/Parser.scala
new file mode 100644
index 0000000..925cbcc
--- /dev/null
+++ b/api/src/main/scala/twist/stages/Parser.scala
@@ -0,0 +1,790 @@
+package twist.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/api/src/test/scala/twist/AdvancedTests.scala b/api/src/test/scala/twist/AdvancedTests.scala
new file mode 100644
index 0000000..eb9e69f
--- /dev/null
+++ b/api/src/test/scala/twist/AdvancedTests.scala
@@ -0,0 +1,120 @@
+package twist
+
+import utest._
+import twist.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/api/src/test/scala/twist/BasicTests.scala b/api/src/test/scala/twist/BasicTests.scala
new file mode 100644
index 0000000..0151ba3
--- /dev/null
+++ b/api/src/test/scala/twist/BasicTests.scala
@@ -0,0 +1,418 @@
+package twist
+import utest._
+import twist.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/api/src/test/scala/twist/ErrorTests.scala b/api/src/test/scala/twist/ErrorTests.scala
new file mode 100644
index 0000000..fec3fc3
--- /dev/null
+++ b/api/src/test/scala/twist/ErrorTests.scala
@@ -0,0 +1,321 @@
+package twist
+
+import utest._
+import twist.stages._
+import scalatags.Text.all._
+import twist.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/api/src/test/scala/twist/ParserTests.scala b/api/src/test/scala/twist/ParserTests.scala
new file mode 100644
index 0000000..721fb29
--- /dev/null
+++ b/api/src/test/scala/twist/ParserTests.scala
@@ -0,0 +1,50 @@
+package twist
+import utest._
+import twist.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/api/src/test/scala/twist/TestUtil.scala b/api/src/test/scala/twist/TestUtil.scala
new file mode 100644
index 0000000..33af527
--- /dev/null
+++ b/api/src/test/scala/twist/TestUtil.scala
@@ -0,0 +1,16 @@
+package twist
+
+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]", "")
+ }
+}
diff --git a/book/index.tw b/book/index.tw
new file mode 100644
index 0000000..b55eb9b
--- /dev/null
+++ b/book/index.tw
@@ -0,0 +1,31 @@
+
+@raw("<!doctype html>")
+@html
+ @head
+ @meta(charset:="utf-8")
+ @meta(name:="viewport", content:="width=device-width, initial-scale=1.0")
+ @tags2.title("Hands-on Scala.js")
+ @Utils.includes
+ @script("hljs.initHighlightingOnLoad();")
+
+ @div(id:="layout")
+ @a(href:="#menu", id:="menuLink", cls:="menu-link")
+ @span
+
+ @div(id:="menu")
+ @div(cls:="pure-menu pure-menu-open")
+ @a(cls:="pure-menu-heading", href:="#") Contents
+ @Book.contentBar
+
+ @div(id:="main")
+ @div(cls:="header")
+ @h1 Hands-on Scala.js
+ @h2 Writing client-side web applications in Scala
+
+ @div(cls:="content")
+ @Book.intro
+
+
+
+
+
diff --git a/book/intro.tw b/book/intro.tw
new file mode 100644
index 0000000..a3b5b09
--- /dev/null
+++ b/book/intro.tw
@@ -0,0 +1,97 @@
+
+@sect{Intro to Scala.js}
+ @highlight.scala
+ val graphs = Seq[(String, Double => Double)](
+ ("red", sin),
+ ("green", x => 2 - abs(x % 8 - 4)),
+ ("blue", x => 3 * pow(sin(x / 12), 2) * sin(x))
+ ).zipWithIndex
+ dom.setInterval(() => {
+ x = (x + 1) % w
+ if (x == 0) Page.renderer.clearRect(0, 0, w, h)
+ else for (((color, func), i) <- graphs) {
+ val y = func(x/w * 75) * h/40 + h/3 * (i+0.5)
+ Page.renderer.fillStyle = color
+ Page.renderer.fillRect(x, y, 3, 3)
+ }
+ }, 10)
+ @canvas(width:=100, height:=100, id:="example-canvas")
+ @script(src:="example-fastopt.js")
+ @script("Example().main('example-canvas')")
+
+ @p
+ @a("Scala.js", href:="http://www.scala-js.org/") is a compiler that compiles Scala source code to equivalent Javascript code. That lets you write Scala code that you can run in a web browser, or other environments (Chrome plugins, Node.js, etc.) where Javascript is supported.
+
+ @p
+ This lets you to develop web applications with the safety and toolability that comes with a statically typed language:
+
+ @ul
+ @li Typo-safety due to its compiler which catches many silly errors before the code is run
+ @li In-editor support for autocomplete, error-highlighting, refactors, and intelligent navigation
+ @li Very small compiled executables, in the 100-400kb range
+ @li Source-maps for ease of debugging
+
+ @p
+ The value proposition is that due to the superior language and tooling, writing a web application in Scala.js will result in a codebase that is more flexible and robust than an equivalent application written in Javascript.
+
+ @sect{Who this book is for}
+ @p
+ This book is targeted at people who have some experience in both Scala and Javascript. You do not need to be an expert in both, but I will skim over basic concepts in both languages to cut to the Scala.js specific points.
+ @p
+ Furthermore, this book aims to only give an overview of the main steps you need to perform (and ideas you need to understand) to get started using Scala.js. It isn't a comprehensive guide, so if you want to know something this book doesn't cover, feel free to ask on the @a("mailing list", href:="https://groups.google.com/forum/#!forum/scala-js").
+
+ @sect{Why Scala.js}
+ @p
+ Javascript is the language supported by web browsers, and is the only language available if you wish to write interactive web applications. As more and more activity moves online, the importance of web apps will only increase over time.
+
+ @sect{Javascript-the-language}
+ @p
+ However, Javascript is not an easy language to work in. Apart from being untyped (which some people don't mind) Javascript is also extremely verbose, has a lot of surprising behavior, and has a culture that make even the simplest of patterns (e.g. instantiating an object) a debate between a dozen different and equally-bad options.
+ @p
+ To work in Javascript, you need the discipline to limit yourself to the sane subset of the language, avoiding all the pitfalls along the way:
+
+ @img(src:="images/javascript-the-good-parts-the-definitive-guide.jpg")
+
+ @sect{Javascript-the-platform}
+ @p
+ However, even as Javascript-the-language sucks, Javascript-the-platform has some very nice properties that make it a good target for application developers:
+
+ @ul
+ @li Zero-install distribution: just go to a URL and have the application downloaded and ready to use.
+ @li Hyperlinks: being able to link to a particular page or item within a web app is a feature other platforms lack, and makes it much easier to cross-reference between different systems
+ @li Sandboxed security: before the advent of mobile apps, web apps were the most secure runtime available, offering none of the risk or worry that comes with installing desktop software
+
+ @p
+ These features are all very nice to have, and together have made the web platform the success it is today.
+
+ @hr
+
+ @p
+ This is where Scala.js comes in. As developers we want Javascript-the-platform, with its ease-of-distribution, hyperlinks and security characteristics. We do not want Javascript-the-language, with its propensity for bugs, verbosity, and fragility. With Scala.js, you can cross compile your Scala code to a Javascript executable that can run on all major web browsers, thus saving you from the endless stream of gotcha's like the one below:
+
+ @highlight.javascript
+ javascript> ["10", "10", "10", "10"].map(parseInt)
+ [10, NaN, 2, 3] // WTF
+
+ @highlight.scala
+ scala> List("10", "10", "10", "10").map(parseInt)
+ List(10, 10, 10, 10) // Yay!
+
+ @p
+ Scala.js allows you to take advantage of the Javascript platform while still enjoying all the benefits of a concise, safe, modern language. The benefits of Scala are well documented, and I will not make a case here for Scala vs. some other language. Suffice to say, I believe it's a considerable improvement over programming in Javascript, and with Scala.js we can bring this improvement in development speed and happiness from the backend systems (where Scala has traditionally been used) to the front-end web application.
+
+@sect{Getting Started}
+ @p
+ The quickest way to get started with Scala.js is to clone a("workbench-example-app", href:="https://github.com/lihaoyi/workbench-example-app"), go into the repository root, and run @code{~fastOptJS}
+
+ @highlight.bash
+ git clone https://github.com/lihaoyi/workbench-example-app
+ cd workbench-example-app
+ sbt ~fastOptJS
+
+
+
+
+
+
+
diff --git a/book/src/main/resources/css/layouts/side-menu.css b/book/src/main/resources/css/layouts/side-menu.css
new file mode 100755
index 0000000..e04fe81
--- /dev/null
+++ b/book/src/main/resources/css/layouts/side-menu.css
@@ -0,0 +1,238 @@
+body {
+ color: #777;
+}
+
+.pure-img-responsive {
+ max-width: 100%;
+ height: auto;
+}
+
+/*
+Add transition to containers so they can push in and out.
+*/
+#layout,
+#menu,
+.menu-link {
+ -webkit-transition: all 0.2s ease-out;
+ -moz-transition: all 0.2s ease-out;
+ -ms-transition: all 0.2s ease-out;
+ -o-transition: all 0.2s ease-out;
+ transition: all 0.2s ease-out;
+}
+
+/*
+This is the parent `<div>` that contains the menu and the content area.
+*/
+#layout {
+ position: relative;
+ padding-left: 0;
+}
+ #layout.active {
+ position: relative;
+ left: 250px;
+ }
+ #layout.active #menu {
+ left: 250px;
+ width: 250px;
+ }
+
+ #layout.active .menu-link {
+ left: 250px;
+ }
+/*
+The content `<div>` is where all your content goes.
+*/
+.content {
+ margin: 0 auto;
+ padding: 0 2em;
+ max-width: 800px;
+ margin-bottom: 50px;
+ line-height: 1.6em;
+}
+
+.header {
+ margin: 0;
+ color: #333;
+ text-align: center;
+ padding: 2.5em 2em 0;
+ border-bottom: 1px solid #eee;
+ }
+ .header h1 {
+ margin: 0.2em 0;
+ font-size: 3em;
+ font-weight: 300;
+ }
+ .header h2 {
+ font-weight: 300;
+ color: #ccc;
+ padding: 0;
+ margin-top: 0;
+ }
+
+.content-subhead {
+ margin: 50px 0 20px 0;
+ font-weight: 300;
+ color: #888;
+}
+
+
+
+/*
+The `#menu` `<div>` is the parent `<div>` that contains the `.pure-menu` that
+appears on the left side of the page.
+*/
+
+#menu {
+ margin-left: -250px; /* "#menu" width */
+ width: 250px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ z-index: 1000; /* so the menu or its navicon stays above all content */
+ background: #191818;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+}
+ /*
+ All anchors inside the menu should be styled like this.
+ */
+ #menu a {
+ color: #999;
+ border: none;
+ padding: 0.6em 0 0.6em 0.6em;
+ }
+
+ /*
+ Remove all background/borders, since we are applying them to #menu.
+ */
+ #menu .pure-menu,
+ #menu .pure-menu ul {
+ border: none;
+ background: transparent;
+ }
+
+ /*
+ Add that light border to separate items into groups.
+ */
+ #menu .pure-menu ul,
+ #menu .pure-menu .menu-item-divided {
+ border-top: 1px solid #333;
+ }
+ /*
+ Change color of the anchor links on hover/focus.
+ */
+ #menu .pure-menu li a:hover,
+ #menu .pure-menu li a:focus {
+ background: #333;
+ }
+
+ /*
+ This styles the selected menu item `<li>`.
+ */
+ #menu .pure-menu-selected,
+ #menu .pure-menu-heading {
+ background: #1f8dd6;
+ }
+ /*
+ This styles a link within a selected menu item `<li>`.
+ */
+ #menu .pure-menu-selected a {
+ color: #fff;
+ }
+
+ /*
+ This styles the menu heading.
+ */
+ #menu .pure-menu-heading {
+ font-size: 110%;
+ color: #fff;
+ margin: 0;
+ }
+
+/* -- Dynamic Button For Responsive Menu -------------------------------------*/
+
+/*
+The button to open/close the Menu is custom-made and not part of Pure. Here's
+how it works:
+*/
+
+/*
+`.menu-link` represents the responsive menu toggle that shows/hides on
+small screens.
+*/
+.menu-link {
+ position: fixed;
+ display: block; /* show this only on small screens */
+ top: 0;
+ left: 0; /* "#menu width" */
+ background: #000;
+ background: rgba(0,0,0,0.7);
+ font-size: 10px; /* change this value to increase/decrease button size */
+ z-index: 10;
+ width: 2em;
+ height: auto;
+ padding: 2.1em 1.6em;
+}
+
+ .menu-link:hover,
+ .menu-link:focus {
+ background: #000;
+ }
+
+ .menu-link span {
+ position: relative;
+ display: block;
+ }
+
+ .menu-link span,
+ .menu-link span:before,
+ .menu-link span:after {
+ background-color: #fff;
+ width: 100%;
+ height: 0.2em;
+ }
+
+ .menu-link span:before,
+ .menu-link span:after {
+ position: absolute;
+ margin-top: -0.6em;
+ content: " ";
+ }
+
+ .menu-link span:after {
+ margin-top: 0.6em;
+ }
+
+
+/* -- Responsive Styles (Media Queries) ------------------------------------- */
+
+/*
+Hides the menu at `48em`, but modify this based on your app's needs.
+*/
+@media (min-width: 48em) {
+
+ .header,
+ .content {
+ padding-left: 2em;
+ padding-right: 2em;
+ }
+
+ #layout {
+ padding-left: 250px; /* left col width "#menu" */
+ left: 0;
+ }
+ #menu {
+ left: 250px;
+ }
+
+ .menu-link {
+ position: fixed;
+ left: 250px;
+ display: none;
+ }
+
+ #layout.active .menu-link {
+ left: 250px;
+ }
+}
diff --git a/book/src/main/resources/css/pure-min.css b/book/src/main/resources/css/pure-min.css
new file mode 100755
index 0000000..14497d9
--- /dev/null
+++ b/book/src/main/resources/css/pure-min.css
@@ -0,0 +1,11 @@
+/*!
+Pure v0.5.0
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/
+/*!
+normalize.css v1.1.3 | MIT License | git.io/normalize
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*/
+/*! normalize.css v1.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}h2{font-size:1.5em;margin:.83em 0}h3{font-size:1.17em;margin:1em 0}h4{font-size:1em;margin:1.33em 0}h5{font-size:.83em;margin:1.67em 0}h6{font-size:.67em;margin:2.33em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:1em 40px}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],html input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{display:inline-block;*display:inline;zoom:1;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;*font-size:90%;*overflow:visible;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);*color:#444;border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;outline:thin dotted \9;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;outline:thin dotted \9;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin dotted #333;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#ee5f5b}.pure-form input:focus:invalid:focus,.pure-form textarea:focus:invalid:focus,.pure-form select:focus:invalid:focus{border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 10em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input{display:block;padding:10px;margin:0;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus{z-index:2}.pure-form .pure-group input:first-child{top:1px;border-radius:4px 4px 0 0}.pure-form .pure-group input:last-child{top:-2px;border-radius:0 0 4px 4px}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu ul{position:absolute;visibility:hidden}.pure-menu.pure-menu-open{visibility:visible;z-index:2;width:100%}.pure-menu ul{left:-10000px;list-style:none;margin:0;padding:0;top:-10000px;z-index:1}.pure-menu>ul{position:relative}.pure-menu-open>ul{left:0;top:0;visibility:visible}.pure-menu-open>ul:focus{outline:0}.pure-menu li{position:relative}.pure-menu a,.pure-menu .pure-menu-heading{display:block;color:inherit;line-height:1.5em;padding:5px 20px;text-decoration:none;white-space:nowrap}.pure-menu.pure-menu-horizontal>.pure-menu-heading{display:inline-block;*display:inline;zoom:1;margin:0;vertical-align:middle}.pure-menu.pure-menu-horizontal>ul{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu li a{padding:5px 20px}.pure-menu-can-have-children>.pure-menu-label:after{content:'\25B8';float:right;font-family:'Lucida Grande','Lucida Sans Unicode','DejaVu Sans',sans-serif;margin-right:-20px;margin-top:-1px}.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-separator{background-color:#dfdfdf;display:block;height:1px;font-size:0;margin:7px 2px;overflow:hidden}.pure-menu-hidden{display:none}.pure-menu-fixed{position:fixed;top:0;left:0;width:100%}.pure-menu-horizontal li{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-horizontal li li{display:block}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label:after{content:"\25BE"}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-horizontal li.pure-menu-separator{height:50%;width:1px;margin:0 7px}.pure-menu-horizontal li li.pure-menu-separator{height:1px;width:auto;margin:7px 2px}.pure-menu.pure-menu-open,.pure-menu.pure-menu-horizontal li .pure-menu-children{background:#fff;border:1px solid #b7b7b7}.pure-menu.pure-menu-horizontal,.pure-menu.pure-menu-horizontal .pure-menu-heading{border:0}.pure-menu a{border:1px solid transparent;border-left:0;border-right:0}.pure-menu a,.pure-menu .pure-menu-can-have-children>li:after{color:#777}.pure-menu .pure-menu-can-have-children>li:hover:after{color:#fff}.pure-menu .pure-menu-open{background:#dedede}.pure-menu li a:hover,.pure-menu li a:focus{background:#eee}.pure-menu li.pure-menu-disabled a:hover,.pure-menu li.pure-menu-disabled a:focus{background:#fff;color:#bfbfbf}.pure-menu .pure-menu-disabled>a{background-image:none;border-color:transparent;cursor:default}.pure-menu .pure-menu-disabled>a,.pure-menu .pure-menu-can-have-children.pure-menu-disabled>a:after{color:#bfbfbf}.pure-menu .pure-menu-heading{color:#565d64;text-transform:uppercase;font-size:90%;margin-top:.5em;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#dfdfdf}.pure-menu .pure-menu-selected a{color:#000}.pure-menu.pure-menu-open.pure-menu-fixed{border:0;border-bottom:1px solid #b7b7b7}.pure-paginator{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;list-style:none;margin:0;padding:0}.opera-only :-o-prefocus,.pure-paginator{word-spacing:-.43em}.pure-paginator li{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-paginator .pure-button{border-radius:0;padding:.8em 1.4em;vertical-align:top;height:1.1em}.pure-paginator .pure-button:focus,.pure-paginator .pure-button:active{outline-style:none}.pure-paginator .prev,.pure-paginator .next{color:#C0C1C3;text-shadow:0 -1px 0 rgba(0,0,0,.45)}.pure-paginator .prev{border-radius:2px 0 0 2px}.pure-paginator .next{border-radius:0 2px 2px 0}@media (max-width:480px){.pure-menu-horizontal{width:100%}.pure-menu-children li{display:block;border-bottom:1px solid #000}}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child td,.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0} \ No newline at end of file
diff --git a/book/src/main/resources/images/javascript-the-good-parts-the-definitive-guide.jpg b/book/src/main/resources/images/javascript-the-good-parts-the-definitive-guide.jpg
new file mode 100644
index 0000000..3c1a1c5
--- /dev/null
+++ b/book/src/main/resources/images/javascript-the-good-parts-the-definitive-guide.jpg
Binary files differ
diff --git a/book/src/main/resources/js/ui.js b/book/src/main/resources/js/ui.js
new file mode 100755
index 0000000..acc38a0
--- /dev/null
+++ b/book/src/main/resources/js/ui.js
@@ -0,0 +1,35 @@
+(function (window, document) {
+
+ var layout = document.getElementById('layout'),
+ menu = document.getElementById('menu'),
+ menuLink = document.getElementById('menuLink');
+
+ function toggleClass(element, className) {
+ var classes = element.className.split(/\s+/),
+ length = classes.length,
+ i = 0;
+
+ for(; i < length; i++) {
+ if (classes[i] === className) {
+ classes.splice(i, 1);
+ break;
+ }
+ }
+ // The className is not found
+ if (length === classes.length) {
+ classes.push(className);
+ }
+
+ element.className = classes.join(' ');
+ }
+
+ menuLink.onclick = function (e) {
+ var active = 'active';
+
+ e.preventDefault();
+ toggleClass(layout, active);
+ toggleClass(menu, active);
+ toggleClass(menuLink, active);
+ };
+
+}(this, this.document));
diff --git a/book/src/main/scala/book/Book.scala b/book/src/main/scala/book/Book.scala
new file mode 100644
index 0000000..e509716
--- /dev/null
+++ b/book/src/main/scala/book/Book.scala
@@ -0,0 +1,43 @@
+package book
+
+import twist._
+
+import scalatags.Text.tags2
+import scala.collection.mutable
+import scalatags.Text.all._
+
+/**
+ * Created by haoyi on 10/26/14.
+ */
+object Book {
+
+ import Utils.sect
+
+ val intro = twf("book/intro.tw")
+ val contentBar = {
+ def rec(current: Node, depth: Int): Frag = {
+ div(
+ marginLeft := s"${depth * 5}px",
+ a(current.name, href:="#"+Utils.munge(current.name)),
+ current.children.map(
+ rec(_, depth + 1)
+ )
+ )
+ }
+ // @li(cls:="menu-item-divided pure-menu-selected")
+ ul(rec(Utils.structure, 0))
+ }
+ println(contentBar)
+
+ val txt = twf("book/index.tw").render
+
+ object highlight{
+ def highlight(snippet: Seq[String], lang: String) = {
+ pre(code(cls:=lang, snippet.mkString))
+ }
+
+ def javascript(code: String*) = highlight(code, "javascript")
+ def scala(code: String*) = highlight(code, "scala")
+ def bash(code: String*) = highlight(code, "bash")
+ }
+}
diff --git a/book/src/main/scala/book/Main.scala b/book/src/main/scala/book/Main.scala
new file mode 100644
index 0000000..9b06d29
--- /dev/null
+++ b/book/src/main/scala/book/Main.scala
@@ -0,0 +1,36 @@
+package book
+
+import java.io.InputStream
+import java.nio.charset.StandardCharsets
+import java.nio.file.{Paths, Files}
+
+import scala.collection.mutable
+import scalatags.Text.all._
+import scalatags.Text.tags2
+
+
+object Main {
+ def write(txt: String, dest: String) = {
+ Paths.get(dest).toFile.getParentFile.mkdirs()
+ Files.deleteIfExists(Paths.get(dest))
+ Files.write(Paths.get(dest), txt.getBytes)
+ }
+ def copy(src: InputStream, dest: String) = {
+ Paths.get(dest).toFile.getParentFile.mkdirs()
+ Files.deleteIfExists(Paths.get(dest))
+ Files.copy(src, Paths.get(dest))
+ }
+
+ def main(args: Array[String]): Unit = {
+ println("Writing Book")
+
+ write(Book.txt, "output/index.html")
+
+ for(res <- Utils.autoResources ++ Utils.manualResources) {
+ copy(getClass.getResourceAsStream("/" + res), "output/" + res)
+ }
+ println("Writing Done")
+ }
+
+
+}
diff --git a/book/src/main/scala/book/Utils.scala b/book/src/main/scala/book/Utils.scala
new file mode 100644
index 0000000..0b435a3
--- /dev/null
+++ b/book/src/main/scala/book/Utils.scala
@@ -0,0 +1,57 @@
+package book
+
+
+import scala.collection.mutable
+import scalatags.Text.all._
+
+case class Node(name: String, children: mutable.Buffer[Node])
+object Utils{
+ val autoResources = Seq(
+ "META-INF/resources/webjars/highlightjs/8.2-1/highlight.min.js",
+ "META-INF/resources/webjars/highlightjs/8.2-1/styles/idea.min.css",
+ "META-INF/resources/webjars/highlightjs/8.2-1/languages/scala.min.js",
+ "META-INF/resources/webjars/highlightjs/8.2-1/languages/javascript.min.js",
+ "META-INF/resources/webjars/highlightjs/8.2-1/languages/bash.min.js",
+ "css/pure-min.css",
+ "css/layouts/side-menu.css",
+ "js/ui.js"
+ )
+
+ val manualResources = Seq(
+ "images/javascript-the-good-parts-the-definitive-guide.jpg",
+ "example-fastopt.js"
+ )
+
+ val includes = for(res <- Utils.autoResources) yield {
+ if (res.endsWith(".js"))
+ script(src:=res)
+ else if (res.endsWith(".css"))
+ link(rel:="stylesheet", href:=res)
+ else
+ raw("")
+ }
+ println(includes)
+ var indent = 1
+ val headers = Seq(h1, h2, h3, h4, h5, h6)
+ val structure = Node("Hands-on Scala.js", mutable.Buffer.empty)
+ var current = structure
+ case class sect(name: String){
+ indent += 1
+ val newNode = Node(name, mutable.Buffer.empty)
+ current.children.append(newNode)
+ val prev = current
+ current = newNode
+ def apply(args: Frag*) = {
+ val res = Seq(
+ headers(indent-1)(cls:="content-subhead", id:=munge(name), name) +: args:_*
+ )
+ indent -= 1
+ current = prev
+ res
+ }
+ }
+ def munge(name: String) = {
+ name.replace(" ", "")
+ }
+
+} \ No newline at end of file
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..1f7e228
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,38 @@
+import scala.scalajs.sbtplugin.ScalaJSPlugin._
+import ScalaJSKeys._
+
+lazy val api = project.in(file("api"))
+ .settings(
+ libraryDependencies ++= Seq(
+ "com.lihaoyi" %% "utest" % "0.2.4",
+ "com.scalatags" %% "scalatags" % "0.4.2",
+ "org.scala-lang" % "scala-reflect" % scalaVersion.value,
+ "com.lihaoyi" %% "acyclic" % "0.1.2" % "provided",
+ compilerPlugin("org.scalamacros" % s"paradise" % "2.0.0" cross CrossVersion.full)
+ ) ++ (
+ if (scalaVersion.value startsWith "2.11.") Nil
+ else Seq("org.scalamacros" %% s"quasiquotes" % "2.0.0")
+ ),
+ addCompilerPlugin("com.lihaoyi" %% "acyclic" % "0.1.2"),
+ testFrameworks += new TestFramework("utest.runner.JvmFramework")
+ )
+
+lazy val book = Project(
+ id = "book",
+ base = file("book"),
+ dependencies = Seq(api)
+).settings(
+ libraryDependencies += "org.webjars" % "highlightjs" % "8.2-1",
+ (resources in Compile) += {
+ (fastOptJS in (examples, Compile)).value
+ (artifactPath in (examples, Compile, fastOptJS)).value
+ }
+)
+lazy val examples = project.in(file("examples")).settings(scalaJSSettings:_*).settings(
+ name := "Example",
+ version := "0.1-SNAPSHOT",
+ scalaVersion := "2.11.1",
+ libraryDependencies ++= Seq(
+ "org.scala-lang.modules.scalajs" %%% "scalajs-dom" % "0.6"
+ )
+) \ No newline at end of file
diff --git a/examples/src/main/scala/Example.scala b/examples/src/main/scala/Example.scala
new file mode 100644
index 0000000..5d315b2
--- /dev/null
+++ b/examples/src/main/scala/Example.scala
@@ -0,0 +1,33 @@
+import Math._
+import org.scalajs.dom
+
+object Example extends scalajs.js.JSApp{
+ def main() = {
+ val canvas =
+ dom.document
+ .getElementById("example-canvas")
+ .asInstanceOf[dom.HTMLCanvasElement]
+
+ val renderer =
+ canvas.getContext("2d")
+ .asInstanceOf[dom.CanvasRenderingContext2D]
+
+ val (h, w) = (canvas.height, canvas.width)
+ var x = 0.0
+ val graphs = Seq[(String, Double => Double)](
+ ("red", sin),
+ ("green", x => 2 - abs(x % 8 - 4)),
+ ("blue", x => 3 * pow(sin(x / 12), 2) * sin(x))
+ ).zipWithIndex
+ dom.setInterval(() => {
+ x = (x + 1) % w
+ if (x == 0) renderer.clearRect(0, 0, w, h)
+ else for (((color, func), i) <- graphs) {
+ val y = func(x/w * 75) * h/40 + h/3 * (i+0.5)
+ renderer.fillStyle = color
+ renderer.fillRect(x, y, 3, 3)
+ }
+ }, 10)
+
+ }
+} \ No newline at end of file
diff --git a/intro.md b/intro.md
new file mode 100644
index 0000000..ada6fb7
--- /dev/null
+++ b/intro.md
@@ -0,0 +1,76 @@
+Hands-On Scala.js
+
+ - Some Scala experience
+ - Some Javascript/Web experience
+
+ Intro to Scala.js
+ What
+ Why
+ Where
+
+ Tutorial
+ A HTML5 Canvas Application
+ Accessing DOM APIs
+ Using js.Dynamic
+ Using scala-js-dom for type-safety
+
+ Input, Output
+ Publishing
+ Looking through the generated code
+
+ Interactive Web Pages
+ Managing HTML using the DOM
+ Managing HTML using Scalatags
+ Wiring up DOM events and interactions
+ Publishing
+
+ Building Cross-platform Libraries
+ Shared code organization
+ Shared tests using uTest
+ Publishing
+
+ Client-Server Integration
+ Hello World client/server project
+ Serving the client javascript from the server
+
+ Sharing code
+ Using shared libraries
+ Writing shared application logic
+
+ Ajax calls
+ Using the DOM APIs
+ Using uPickle for serialization
+ Using Autowire for routing
+
+ Deployment
+
+ Reference
+ Javascript Interop
+ Calling Javascript from Scala.js
+ Writing your own FFI facades
+ Calling Scala.js from Javascript
+ Mapping of Types
+
+ Library Dependencies
+
+ Differences from Scala/JVM
+
+ Compilation Pipeline
+ Optimization Phases
+
+
+
+Intro to Intro to Scala.js
+
+ Scala.js is a compiler that compiles Scala source code to equivalent Javascript code. That lets you write Scala code that you can run in a web browser, or other environments (Chrome plugins, Node.js, etc.) where Javascript is supported.
+
+ This book is targeted at people who have some experience in both Scala and Javascript. You do not need to be an expert in both, but I will skim over basic concepts in both languages to cut to the Scala.js specific points.
+
+ Scala.js on its own allows you to develop web applications with the safety and toolability that comes with a statically typed language.
+
+ - Typo-safety due to its compiler which catches many silly errors before the code is run
+ - In-editor support for autocomplete, error-highlighting, refactors, and intelligent navigation
+ - Very small compiled executables, in the 170-400kb range
+ - Source-maps for ease of debugging
+
+ In general, the development experience is on \ No newline at end of file
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000..be6c454
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version=0.13.5
diff --git a/project/build.sbt b/project/build.sbt
new file mode 100644
index 0000000..86ce45d
--- /dev/null
+++ b/project/build.sbt
@@ -0,0 +1,2 @@
+
+addSbtPlugin("org.scala-lang.modules.scalajs" % "scalajs-sbt-plugin" % "0.5.5") \ No newline at end of file
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..db717a0
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,64 @@
+Twist
+=====
+
+Twist is a lightweight, whitespace-delimited, markdown-like relative of the [Twirl](https://github.com/playframework/twirl) templating engine. It allows you to write
+
+```
+@(titleString: String)(sidebar: Html)(content: Html)
+
+@html
+ @head
+ @title @titleString
+ @body
+ @section(cls:="sidebar") @sidebar
+ @section(cls:="content") @content
+```
+
+Instead of the more verbose Twirl-template
+
+```html
+@(title: String)(sidebar: Html)(content: Html)
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>@title</title>
+ </head>
+ <body>
+ <section class="sidebar">@sidebar</section>
+ <section class="content">@content</section>
+ </body>
+</html>
+```
+
+Apart from the syntactic difference, Twirlite templates are just as type-safe as Twirl templates. Furthermore, they use the [Scalatags](https://github.com/lihaoyi/scalatags) HTML-generation library under the hood, making them render around [4x faster](https://github.com/lihaoyi/scalatags#performance) than the equivalent Twirl template. Like Twirl templates, you can use arbitrary function calls, control-flow structures like `if`/`for`, and other Scala expressions within your templates.
+
+Why Twirlite?
+-------------
+
+Twirlite emerged out of the deficiencies of other languages used for marking up text.
+
+###[Markdown](http://en.wikipedia.org/wiki/Markdown)
+
+is nice to use but too inflexible: it is impossible to define abstractions in the language, and if you want any sort of automation, e.g. substituting in sections from somewhere else, or re-usable blocks of title+paragraph, you're left with performing hacky string-replacements on the markdown source. With Twirlite, tags are simply functions, and you can define additional tags yourself to abstract away arbitrary patterns within your markup.
+
+###[Scalatags](https://github.com/lihaoyi/scalatags)
+
+is perfect for forming heavily structured markup: since in Scalatags the tags/structure is "raw" while the text is quoted, it facilitates structure-heavy markup at the cost of making text-heavy markup very cumbersome to write. With Twirlite, the situation is reversed: text is left raw, while the tags are quoted (using `@`), lending itself to easily marking-up text-heavy documents. As Twirlite is built on Scalatags, we still get all the other advantages of speed and composability.
+
+###[Twirl](https://github.com/playframework/twirl)
+
+is almost what I want for a markup language, as it doesn't suffer from the same problems Scalatags or Markdown does. However, it has a rather noisy syntax for a langauge meant for marking up text: you need to surround blocks of text in curly braces `{...}` to pass them into functions/tags. Furthermore, Twirl by default uses Scala.XML both in syntax and in implementation, resulting in an odd mix of <XML>-tags and @twirl-tags. Twirlite solves the first by using whitespace as a delimiter, and solves the second by using Scalatags to provide the HTML structure, making all tags uniformly @twirl-tags.
+
+----------
+
+Twirlite also has some other design decisions which are unique, for better or for worse:
+
+###Twirlite as a Macro
+
+Twirlite is usable as a macro right inside your code; that means if you have a text-heavy section of your Scalatags code, you can simply drop into a `twl(...)` macro and start writing long-form text, while still using `@` to interpolate tags as necessary, or splicing in variables in the enclosing scope.
+
+###Direct Text Generation
+
+Twirlite works as a direct-text-generator rather than as a markup-language that is interpreted to generate text. This has advantages in speed and simplicity-of-abstraction, since creating re-usable tags is as simple as defining a function `f(inner: Frag*): Frag` and making it available in scope.
+
+However, it makes it more difficult to do certain kinds of whole-program-analysis, since there never really is a syntax-tree available that you can analyze. Twirlite compiles to Scala source code, which when evaluated spits out HTML `String`s, with nothing in between. Furthermore, since Twirlite can run arbitrary Scala code, it makes it much more difficult to sandbox than Markdown or similar languages. \ No newline at end of file