├── .gitignore ├── .travis.yml ├── api └── src │ ├── main │ └── scala │ │ └── scalatex │ │ ├── package.scala │ │ └── stages │ │ ├── Compiler.scala │ │ ├── Parser.scala │ │ └── Trim.scala │ └── test │ ├── resources │ └── scalatex │ │ ├── errors │ │ ├── Nested.scalatex │ │ └── Simple.scalatex │ │ └── success │ │ ├── Nested.scalatex │ │ └── Simple.scalatex │ ├── scala-2.11 │ └── scalatex │ │ └── Scala211ErrorTests.scala │ ├── scala-2.12 │ └── scalatex │ │ └── Scala212ErrorTests.scala │ └── scala │ └── scalatex │ ├── BasicTests.scala │ ├── ErrorTests.scala │ ├── ExampleTests.scala │ ├── Main.scala │ ├── ParseErrors.scala │ ├── ParserTests.scala │ └── TestUtil.scala ├── build.sbt ├── project ├── Constants.scala ├── build.properties └── build.sbt ├── readme.md ├── readme ├── Readme.scalatex ├── advanced │ ├── CustomTags1.scalatex │ └── TableOfContents.scalatex ├── highlighter │ ├── Example1.scalatex │ ├── Example2.scalatex │ ├── Example3.scalatex │ ├── Example4.scalatex │ ├── Example5.scalatex │ ├── Example6.scalatex │ └── hl.scala ├── resources │ ├── Omg.txt │ ├── favicon.png │ └── folder │ │ └── Omg2.txt └── section │ ├── Example1.scalatex │ ├── Example2.scalatex │ └── Example3.scalatex ├── scalatexSbtPlugin └── src │ └── main │ ├── scala-sbt-0.13 │ └── scalatex │ │ └── PluginCompat.scala │ ├── scala-sbt-1.0 │ └── scalatex │ │ └── PluginCompat.scala │ └── scala │ └── scalatex │ └── SbtPlugin.scala ├── scrollspy └── src │ └── main │ └── scala │ └── scalatex │ └── scrollspy │ ├── Controller.scala │ ├── ScrollSpy.scala │ └── Styles.scala ├── site └── src │ ├── main │ ├── resources │ │ └── scalatex │ │ │ └── site │ │ │ └── styles.css │ └── scala │ │ └── scalatex │ │ └── site │ │ ├── Highlighter.scala │ │ ├── Main.scala │ │ ├── Section.scala │ │ ├── Sidebar.scala │ │ ├── Site.scala │ │ ├── Styles.scala │ │ └── Util.scala │ └── test │ ├── scala │ └── scalatex │ │ └── site │ │ └── Tests.scala │ └── scalatex │ └── scalatex │ └── site │ ├── About.scalatex │ └── Hello.scalatex └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | .idea/ 4 | 5 | # Eclipse project files 6 | .classpath 7 | .settings 8 | .project 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | # ignored, because if we do the ++ thing it uses the wrong 4 | # scalaVersion to compile the sbt plugin which blows up 5 | scala: 6 | - 2.12.7 7 | script: 8 | - sbt test "readme/run --validate" 9 | jdk: 10 | - oraclejdk8 11 | - openjdk11 12 | sudo: false -------------------------------------------------------------------------------- /api/src/main/scala/scalatex/package.scala: -------------------------------------------------------------------------------- 1 | 2 | 3 | import scala.reflect.internal.util.{BatchSourceFile, SourceFile, OffsetPosition} 4 | import scala.reflect.io.{PlainFile, AbstractFile} 5 | import scala.reflect.macros._ 6 | import scalatags.Text.all._ 7 | import scalatex.stages.{Ast, Parser, Compiler} 8 | import scala.language.experimental.macros 9 | import acyclic.file 10 | import fastparse._ 11 | 12 | package object scalatex { 13 | /** 14 | * Converts the given string literal into a Scalatex fragment. 15 | */ 16 | def tw(expr: String): Frag = macro Internals.applyMacro 17 | /** 18 | * Converts the given file into Pa Scalatex fragment. 19 | */ 20 | def twf(expr: String): Frag = macro Internals.applyMacroFile 21 | object Internals { 22 | import whitebox._ 23 | 24 | def twRuntimeErrors(expr: String): Frag = macro applyMacroRuntimeErrors 25 | def twfRuntimeErrors(expr: String): Frag = macro applyMacroFileRuntimeErrors 26 | 27 | def applyMacro(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, false, false) 28 | def applyMacroRuntimeErrors(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, true, false) 29 | def applyMacroFile(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFileBase(c)(expr, false) 30 | def applyMacroFileRuntimeErrors(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFileBase(c)(expr, true) 31 | 32 | 33 | def applyMacroFileBase(c: Context)(filename: c.Expr[String], runtimeErrors: Boolean): c.Expr[Frag] = { 34 | import c.universe._ 35 | val fileName = filename.tree 36 | .asInstanceOf[Literal] 37 | .value 38 | .value 39 | .asInstanceOf[String] 40 | val txt = io.Source.fromFile(fileName)(scala.io.Codec.UTF8).mkString 41 | val sourceFile = new BatchSourceFile( 42 | new PlainFile(fileName), 43 | txt.toCharArray 44 | ) 45 | compileThing(c)(txt, sourceFile, 0, runtimeErrors, false) 46 | 47 | } 48 | case class DebugFailure(msg: String, pos: String) extends Exception(msg) 49 | 50 | private[this] def applyMacroFull(c: Context) 51 | (expr: c.Expr[String], 52 | runtimeErrors: Boolean, 53 | debug: Boolean) 54 | : c.Expr[Frag] = { 55 | import c.universe._ 56 | val scalatexFragment = expr.tree 57 | .asInstanceOf[Literal] 58 | .value 59 | .value 60 | .asInstanceOf[String] 61 | val stringStart = 62 | expr.tree 63 | .pos 64 | .lineContent 65 | .drop(expr.tree.pos.column) 66 | .take(2) 67 | compileThing(c)( 68 | scalatexFragment, 69 | expr.tree.pos.source, 70 | expr.tree.pos.point 71 | + (if (stringStart == "\"\"") 3 else 1) // Offset from start of string literal 72 | - 1, // WTF I don't know why we need this 73 | runtimeErrors, 74 | debug 75 | ) 76 | } 77 | def compileThing(c: Context) 78 | (scalatexSource: String, 79 | source: SourceFile, 80 | point: Int, 81 | runtimeErrors: Boolean, 82 | debug: Boolean) = { 83 | import c.universe._ 84 | def compile(s: String): c.Tree = { 85 | val realPos = new OffsetPosition(source, point).asInstanceOf[c.universe.Position] 86 | val input = stages.Trim(s) 87 | Parser.tupled(input) match { 88 | case s: Parsed.Success[Ast.Block] => Compiler(c)(realPos, s.value) 89 | case f: Parsed.Failure => 90 | val lines = Predef.augmentString(input._1.take(f.index)).linesIterator.toVector 91 | throw new TypecheckException( 92 | new OffsetPosition(source, point + f.index).asInstanceOf[c.universe.Position], 93 | "Syntax error, expected (" + f.extra.trace().msg + ")" + 94 | "\n at line " + lines.length + 95 | " column " + lines.last.length + 96 | " index " + f.index 97 | ) 98 | } 99 | 100 | } 101 | 102 | import c.Position 103 | try { 104 | val compiled = compile(scalatexSource) 105 | if (debug) println(compiled) 106 | c.Expr[Frag](c.typecheck(compiled)) 107 | } catch { 108 | case e@TypecheckException(pos: Position, msg) => 109 | if (!runtimeErrors) c.abort(pos, msg) 110 | else { 111 | val posMsg = pos.lineContent + "\n" + (" " * pos.column) + "^" 112 | c.Expr( q"""throw scalatex.Internals.DebugFailure($msg, $posMsg)""") 113 | } 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /api/src/main/scala/scalatex/stages/Compiler.scala: -------------------------------------------------------------------------------- 1 | package scalatex 2 | package stages 3 | 4 | import acyclic.file 5 | 6 | import scala.reflect.macros.whitebox.Context 7 | import scala.reflect.internal.util.{Position, OffsetPosition} 8 | 9 | /** 10 | * Walks the parsed AST, converting it into a structured Scala c.Tree 11 | */ 12 | object Compiler{ 13 | 14 | def apply(c: Context)(fragPos: c.Position, template: Ast.Block): c.Tree = { 15 | 16 | import c.universe._ 17 | def fragType = tq"scalatags.Text.all.Frag" 18 | 19 | def incPosRec(trees: c.Tree, offset: Int): trees.type = { 20 | 21 | trees.foreach(incPos(_, offset)) 22 | trees 23 | } 24 | def incPos(tree: c.Tree, offset: Int): tree.type = { 25 | 26 | val current = if (tree.pos == NoPosition) 0 else tree.pos.point 27 | val finalPos = 28 | current + // start of of tree relative to start of fragment 29 | offset + // start of fragment relative to start of string-literal 30 | fragPos.point // start of string-literal relative to start of file 31 | 32 | // println(s"$finalPos = $current + $offset + ${fragPos.point}\t $tree") 33 | c.internal.setPos(tree, 34 | new OffsetPosition( 35 | fragPos.source, 36 | finalPos 37 | ).asInstanceOf[c.universe.Position] 38 | ) 39 | tree 40 | } 41 | 42 | /** 43 | * We need something to prepend to our `.call` or `()` 44 | * or `[]` strings, so that they'll parse properly 45 | */ 46 | val prefix = "omg" 47 | 48 | def compileChain(code: String, parts: Seq[Ast.Chain.Sub], offset: Int): c.Tree = { 49 | // println("compileChain " + parts + "\t" + offset) 50 | val out = parts.foldLeft(incPosRec(c.parse(code), offset)){ 51 | case (curr, Ast.Chain.Prop(offset2, str)) => 52 | // println(s"Prop $str $offset2") 53 | incPos(q"$curr.${TermName(str)}", offset2) 54 | 55 | case (curr, Ast.Chain.Args(offset2, str)) => 56 | val t @ Apply(fun, args) = c.parse(s"$prefix$str") 57 | // println(s"Args $str $offset2 ${t.pos.point}" ) 58 | val offset3 = offset2 - prefix.length 59 | incPos(Apply(curr, args.map(incPosRec(_, offset3))), offset3 + t.pos.point) 60 | 61 | case (curr, Ast.Chain.TypeArgs(offset2, str)) => 62 | // println(s"TypeArgs $str $offset2") 63 | val t @ TypeApply(fun, args) = c.parse(s"$prefix$str") 64 | val offset3 = offset2 - prefix.length 65 | incPos(TypeApply(curr, args.map(incPosRec(_, offset3))), offset3 + t.pos.point) 66 | 67 | case (curr, Ast.Block(offset2, parts)) => 68 | // println(s"Block $parts $offset2") 69 | // -1 because the offset of a block is currently defined as the 70 | // first character *after* the curly brace, where-as normal Scala 71 | // error messages place the error position for a block-apply on 72 | // the curly brace itself. 73 | incPos(q"$curr(..${compileBlock(parts, offset2-1)})", offset2 - 1) 74 | 75 | case (curr, Ast.Header(offset2, header, block)) => 76 | // println(s"Header $header $offset2") 77 | incPos(q"$curr(${compileHeader(header, block, offset2)})", offset2) 78 | } 79 | 80 | out 81 | } 82 | def compileBlock(parts: Seq[Ast.Block.Sub], offset: Int): Seq[c.Tree] = { 83 | // println("compileBlock " + parts + "\t" + offset) 84 | val res = parts.map{ 85 | case Ast.Block.Text(offset1, str) => 86 | incPos(q"$str", offset1) 87 | 88 | case Ast.Block.Comment(offset1, comment) => 89 | incPos(q"Seq[$fragType]()", offset1) 90 | 91 | case Ast.Chain(offset1, code, parts) => 92 | compileChain(code, parts, offset1) 93 | 94 | case Ast.Header(offset1, header, block) => 95 | compileHeader(header, block, offset1) 96 | 97 | case Ast.Block.IfElse(offset1, condString, Ast.Block(offset2, parts2), elseBlock) => 98 | val If(cond, _, _) = c.parse(condString + "{}") 99 | val elseCompiled = elseBlock match{ 100 | case Some(Ast.Block(offset3, parts3)) => compileBlockWrapped(parts3, offset3) 101 | case None => EmptyTree 102 | } 103 | 104 | val res = If(incPosRec(cond, offset1), compileBlockWrapped(parts2, offset2), elseCompiled) 105 | 106 | incPos(res, offset1) 107 | res 108 | 109 | case Ast.Block.For(offset1, generators, Ast.Block(offset2, parts2)) => 110 | val fresh = c.fresh() 111 | 112 | val tree = incPosRec(c.parse(s"$generators yield $fresh"), offset1) 113 | 114 | def rec(t: Tree): Tree = t match { 115 | case a @ Apply(fun, List(f @ Function(vparams, body))) => 116 | val f2 = Function(vparams, rec(body)) 117 | val a2 = Apply(fun, List(f2)) 118 | a2 119 | case Ident(x: TermName) if x.decoded == fresh => 120 | compileBlockWrapped(parts2, offset2) 121 | } 122 | 123 | rec(tree) 124 | } 125 | res 126 | } 127 | def compileBlockWrapped(parts: Seq[Ast.Block.Sub], offset: Int): c.Tree = { 128 | incPos(q"Seq[$fragType](..${compileBlock(parts, offset)})", offset) 129 | } 130 | def compileHeader(header: String, block: Ast.Block, offset: Int): c.Tree = { 131 | val Block(stmts, expr) = c.parse(s"{$header\n ()}") 132 | Block(stmts.map(incPosRec(_, offset)), compileBlockWrapped(block.parts, block.offset)) 133 | } 134 | 135 | val res = compileBlockWrapped(template.parts, template.offset) 136 | res 137 | } 138 | } -------------------------------------------------------------------------------- /api/src/main/scala/scalatex/stages/Parser.scala: -------------------------------------------------------------------------------- 1 | package scalatex 2 | package stages 3 | 4 | import scalaparse.Scala._ 5 | import scalaparse.syntax._ 6 | import fastparse._, NoWhitespace._ 7 | 8 | /** 9 | * Parses the input text into a roughly-structured AST. This AST 10 | * is much simpler than the real Scala AST, but serves us well 11 | * enough until we stuff the code-strings into the real Scala 12 | * parser later 13 | */ 14 | object Parser extends ((String, Int) => Parsed[Ast.Block]){ 15 | def apply(input: String, offset: Int = 0): Parsed[Ast.Block] = { 16 | parse(input, new Parser(offset).File(_)) 17 | } 18 | } 19 | 20 | class Parser(indent: Int = 0) { 21 | import scalaparse.syntax.{Key => K} 22 | 23 | /** 24 | * This only needs to parse the second `@`l the first one is 25 | * already parsed by [[BodyItem]] 26 | */ 27 | def `@@`[_: P] = P( Index ~ "@" ).map(Ast.Block.Text(_, "@")) 28 | 29 | def TextNot[_: P](chars: String) = { 30 | def AllowedChars = CharsWhile(!(chars + "@\n").contains(_)) 31 | P(Index ~ AllowedChars.!.rep(1)).map { 32 | case (i, x) => Ast.Block.Text(i, x.mkString) 33 | } 34 | } 35 | 36 | def Text[_: P] = TextNot("") 37 | def Code[_: P] = P( (scalaparse.syntax.Identifiers.Id | BlockExpr | ExprCtx.Parened ).! ) 38 | def Header[_: P] = P( (BlockDef | Import).! ) 39 | 40 | def HeaderBlock[_: P] = P( Index ~ Header ~ (WL.! ~ "@" ~ Header).rep ~ Body ).map{ 41 | case (i, start, heads, body) => Ast.Header(i, start + heads.map{case (x, y) => x + y}.mkString, body) 42 | } 43 | 44 | def BlankLine[_: P] = P( "\n" ~ " ".rep ~ &("\n") ) 45 | 46 | def IndentSpaces[_: P] = P( " ".rep(min = indent, sep = Pass) ) 47 | def Indent[_: P] = P( "\n" ~ IndentSpaces ) 48 | def IndentPrefix[_: P] = P( Index ~ (Indent | Start).! ).map(Ast.Block.Text.tupled) 49 | def IndentScalaChain[_: P] = P(ScalaChain ~ (IndentBlock | BraceBlock).?).map{ 50 | case (chain, body) => chain.copy(parts = chain.parts ++ body) 51 | } 52 | 53 | def Deeper[_: P] = P( " ".rep(indent + 1) ) 54 | def IndentBlock[_: P] = { 55 | ("\n" ~ Deeper.!).flatMapX { i => 56 | val size = i.size 57 | val p = implicitly[P[_]] 58 | p.freshSuccessUnit(p.index - (size + 1)) 59 | new Parser(indent = size).Body //actor.rep(1, sep = ("\n" + " " * i)./) 60 | } 61 | } 62 | 63 | def IfHead[_: P] = P( (`if` ~/ "(" ~ ExprCtx.Expr ~ ")").! ) 64 | def IfSuffix[_: P] = P( BraceBlock ~ (K.W("else") ~/ BraceBlock).? ) 65 | def IfElse[_: P] = P( Index ~ IfHead ~ IfSuffix).map { case (w, a, (b, c)) => Ast.Block.IfElse(w, a, b, c) } 66 | def IfBlockSuffix[_: P] = P( IndentBlock ~ (Indent ~ K.W("@else") ~ (BraceBlock | IndentBlock)).? ) 67 | 68 | def IndentIfElse[_: P] = { 69 | P(Index ~ IfHead ~ (IfBlockSuffix | IfSuffix)).map { 70 | case (w, a, (b, c)) => Ast.Block.IfElse(w, a, b, c) 71 | } 72 | } 73 | 74 | def ForHead[_: P] = { 75 | def ForBody = P( "(" ~/ ExprCtx.Enumerators ~ ")" | "{" ~/ StatCtx.Enumerators ~ "}" ) 76 | P( Index ~ (`for` ~/ ForBody).! ) 77 | } 78 | def ForLoop[_: P] = P( ForHead ~ BraceBlock ).map(Ast.Block.For.tupled) 79 | def IndentForLoop[_: P] = P( 80 | (ForHead ~ (IndentBlock | BraceBlock)).map(Ast.Block.For.tupled) 81 | ) 82 | 83 | def ScalaChain[_: P] = P( Index ~ Code ~ TypeAndArgs ~ Extension.rep.map(_.flatten) ).map { 84 | case (x, c, ta, ex) => Ast.Chain(x, c, ta ++ ex) 85 | } 86 | 87 | def TypeArgsVal[_: P] = (Index ~ TypeArgs.!) 88 | def ParenArgListVal[_: P] = (Index ~ ParenArgList.!) 89 | def TypeAndArgs[_: P] = (TypeArgsVal.rep ~ ParenArgListVal.rep).map { 90 | case (oT, oA) => 91 | oT.map { case (iT, t) => Ast.Chain.TypeArgs(iT, t) } ++ 92 | oA.map { case(iA, a) => Ast.Chain.Args(iA, a) } 93 | } 94 | 95 | def Extension[_: P] = { 96 | def InFixCallId = (Index ~ "." ~ Identifiers.Id.! ~ &(".")) 97 | 98 | def InFixCall = 99 | (InFixCallId ~ TypeArgsVal.rep ~ ParenArgListVal.rep).map { 100 | case (iF, f, oT, oA) => 101 | Seq(Ast.Chain.Prop(iF, f)) ++ 102 | oT.map { case (iT, t) => Ast.Chain.TypeArgs(iT, t) } ++ 103 | oA.map { case(iA, a) => Ast.Chain.Args(iA, a) } 104 | } 105 | 106 | def PostFixCallId = (Index ~ "." ~ Identifiers.Id.!) 107 | 108 | def PostFixCall = 109 | (PostFixCallId ~ TypeArgsVal.rep ~ ParenArgListVal.rep).map { 110 | case (iF, f, oT, oA) => 111 | Seq(Ast.Chain.Prop(iF, f)) ++ 112 | oT.map { case (iT, t) => Ast.Chain.TypeArgs(iT, t) } ++ 113 | oA.map { case(iA, a) => Ast.Chain.Args(iA, a) } 114 | } 115 | 116 | P( 117 | // Not cutting after the ".", because full-stops are very common 118 | // in english so this results in lots of spurious failures 119 | InFixCall | PostFixCall | BraceBlock.map(Seq(_)) 120 | ) 121 | } 122 | def BraceBlock[_: P] = P( "{" ~/ BodyNoBrace ~ "}" ) 123 | 124 | def CtrlFlow[_: P] = P( ForLoop | IfElse | ScalaChain | HeaderBlock | `@@` ).map(Seq(_)) 125 | 126 | def CtrlFlowIndented[_: P] = P( IndentForLoop | IndentScalaChain | IndentIfElse | HeaderBlock | `@@` ) 127 | 128 | def IndentedExpr[_: P] = P( 129 | (IndentPrefix ~ "@" ~/ CtrlFlowIndented).map{ case (a, b) => Seq(a, b) } 130 | ) 131 | def BodyText[_: P](exclusions: String) = P( 132 | TextNot(exclusions).map(Seq(_)) | 133 | (Index ~ Indent.!).map(Ast.Block.Text.tupled).map(Seq(_)) | 134 | (Index ~ BlankLine.!).map(Ast.Block.Text.tupled).map(Seq(_)) 135 | ) 136 | def Comment[_: P] = P( 137 | (Index ~ Literals.Comment.!).map { case (i, c) => Seq(Ast.Block.Comment(i, c)) } 138 | ) 139 | 140 | def BodyItem[_: P](exclusions: String) : P[Seq[Ast.Block.Sub]] = P( 141 | Comment | IndentedExpr | "@" ~/ CtrlFlow | BodyText(exclusions) 142 | ) 143 | def Body[_: P] = P( BodyEx("") ) 144 | 145 | // Some day we'll turn this on, but for now it seems to be making things blow up 146 | def File[_: P] = P( Body/* ~ End */) 147 | def BodyNoBrace[_: P] = P( BodyEx("}") ) 148 | def BodyEx[_: P](exclusions: String) = 149 | P( Index ~ BodyItem(exclusions).rep ).map { 150 | case (i, x) => Ast.Block(i, flattenText(x.flatten)) 151 | } 152 | 153 | def flattenText(seq: Seq[Ast.Block.Sub]) = { 154 | seq.foldLeft(Seq.empty[Ast.Block.Sub]){ 155 | case (prev :+ Ast.Block.Text(offset, txt1), Ast.Block.Text(_, txt2)) => 156 | prev :+ Ast.Block.Text(offset, txt1 + txt2) 157 | case (prev, next) => prev :+ next 158 | } 159 | } 160 | } 161 | 162 | trait Ast{ 163 | def offset: Int 164 | } 165 | object Ast{ 166 | 167 | /** 168 | * @param parts The various bits of text and other things which make up this block 169 | * @param offset 170 | */ 171 | case class Block(offset: Int, parts: Seq[Block.Sub]) extends Chain.Sub with Block.Sub 172 | object Block{ 173 | trait Sub extends Ast 174 | case class Text(offset: Int, txt: String) extends Block.Sub 175 | case class For(offset: Int, generators: String, block: Block) extends Block.Sub 176 | case class IfElse(offset: Int, condition: String, block: Block, elseBlock: Option[Block]) extends Block.Sub 177 | case class Comment(offset: Int, str: String) extends Block.Sub 178 | } 179 | case class Header(offset: Int, front: String, block: Block) extends Block.Sub with Chain.Sub 180 | 181 | /** 182 | * @param lhs The first expression in this method-chain 183 | * @param parts A list of follow-on items chained to the first 184 | * @param offset 185 | */ 186 | case class Chain(offset: Int, lhs: String, parts: Seq[Chain.Sub]) extends Block.Sub 187 | object Chain{ 188 | trait Sub extends Ast 189 | case class Prop(offset: Int, str: String) extends Sub 190 | case class TypeArgs(offset: Int, str: String) extends Sub 191 | case class Args(offset: Int, str: String) extends Sub 192 | } 193 | 194 | 195 | } 196 | -------------------------------------------------------------------------------- /api/src/main/scala/scalatex/stages/Trim.scala: -------------------------------------------------------------------------------- 1 | package scalatex.stages 2 | import acyclic.file 3 | 4 | /** 5 | * Preprocesses the input string to normalize things related to whitespace 6 | * 7 | * Find the "first" non-whitespace-line of the text and remove the front 8 | * of every line to align that first line with the left margin. 9 | * 10 | * Remove all trailing whitespace from each line. 11 | */ 12 | object Trim extends (String => (String, Int)){ 13 | def apply(str: String) = { 14 | val lines = str.split("\n", -1) 15 | val nonEmptyLines = lines.iterator.filter(_.trim != "") 16 | val offset = 17 | if (nonEmptyLines.hasNext) 18 | nonEmptyLines.next().takeWhile(_ == ' ').length 19 | else 20 | 0 21 | val res = lines.iterator 22 | .mkString("\n") 23 | (res, offset) 24 | } 25 | def old(str: String) = { 26 | val (res, offset) = this.apply(str) 27 | res.split("\n", -1).map(_.drop(offset)).mkString("\n") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api/src/test/resources/scalatex/errors/Nested.scalatex: -------------------------------------------------------------------------------- 1 | 2 | @p 3 | Hello Everyone! 4 | 5 | @div 6 | @h1 7 | I am a cow 8 | @for(x <- Seq(1, 2, 3)) 9 | @p 10 | And so are you: @x 11 | @y 12 | 13 | -------------------------------------------------------------------------------- /api/src/test/resources/scalatex/errors/Simple.scalatex: -------------------------------------------------------------------------------- 1 | Hello @kk -------------------------------------------------------------------------------- /api/src/test/resources/scalatex/success/Nested.scalatex: -------------------------------------------------------------------------------- 1 | 2 | @p 3 | Hello Everyone! 4 | 5 | @div 6 | @h1 7 | I am a cow 8 | @for(x <- Seq(1, 2, 3)) 9 | @p 10 | And so are you: @x 11 | @img 12 | 13 | -------------------------------------------------------------------------------- /api/src/test/resources/scalatex/success/Simple.scalatex: -------------------------------------------------------------------------------- 1 | Hello @{"world" + "!"} -------------------------------------------------------------------------------- /api/src/test/scala-2.11/scalatex/Scala211ErrorTests.scala: -------------------------------------------------------------------------------- 1 | package scalatex 2 | 3 | import utest._ 4 | import scalatex.stages._ 5 | import scalatags.Text.all._ 6 | import scalatex.Internals.{DebugFailure, twRuntimeErrors, twfRuntimeErrors} 7 | import ErrorTests.check 8 | 9 | object Scala211ErrorTests extends TestSuite { 10 | val tests = TestSuite { 11 | 'chained { 12 | 'curlies { 13 | * - check( 14 | twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), 15 | "missing argument list for method foldLeft", 16 | """ 17 | twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), 18 | ^ 19 | """ 20 | ) 21 | * - check( 22 | twRuntimeErrors("@Nil.map{ @omg}"), 23 | "too many arguments for method map", 24 | """ 25 | twRuntimeErrors("@Nil.map{ @omg}"), 26 | ^ 27 | """ 28 | ) 29 | } 30 | } 31 | 'ifElse { 32 | 'oneLine { 33 | * - check( 34 | twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), 35 | "too many arguments for method sin: (x: Double)Double", 36 | """ 37 | twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), 38 | ^ 39 | """ 40 | ) 41 | } 42 | } 43 | 'forLoop { 44 | 'oneLine { 45 | 'body - check( 46 | twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), 47 | """too many arguments for method +""", 48 | """ 49 | twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), 50 | ^ 51 | """ 52 | ) 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /api/src/test/scala-2.12/scalatex/Scala212ErrorTests.scala: -------------------------------------------------------------------------------- 1 | //package scalatex 2 | // 3 | //import utest._ 4 | //import scalatex.stages._ 5 | //import scalatags.Text.all._ 6 | //import scalatex.Internals.{DebugFailure, twRuntimeErrors, twfRuntimeErrors} 7 | //import ErrorTests.check 8 | // 9 | //object Scala212ErrorTests extends TestSuite { 10 | // val tests = TestSuite { 11 | // 'chained { 12 | // 'curlies { 13 | // * - check( 14 | // twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), 15 | // "type mismatch", 16 | // """ 17 | // twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), 18 | // ^ 19 | // """ 20 | // ) 21 | // * - check( 22 | // twRuntimeErrors("@Nil.map{ @omg}"), 23 | // "too many arguments (2) for method map: ", 24 | // """ 25 | // twRuntimeErrors("@Nil.map{ @omg}"), 26 | // ^ 27 | // """ 28 | // ) 29 | // } 30 | // } 31 | // 'ifElse { 32 | // 'oneLine { 33 | // * - check( 34 | // twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), 35 | // "too many arguments (3) for method sin: (x: Double)Double", 36 | // """ 37 | // twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), 38 | // ^ 39 | // """ 40 | // ) 41 | // } 42 | // } 43 | // 'forLoop { 44 | // 'oneLine { 45 | // 'body - check( 46 | // twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), 47 | // """too many arguments (2) for method +: (other: String)String""", 48 | // """ 49 | // twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), 50 | // ^ 51 | // """ 52 | // ) 53 | // } 54 | // } 55 | // } 56 | //} -------------------------------------------------------------------------------- /api/src/test/scala/scalatex/BasicTests.scala: -------------------------------------------------------------------------------- 1 | package scalatex 2 | import utest._ 3 | import scala.collection.mutable.ArrayBuffer 4 | import scalatex.stages._ 5 | import scalatags.Text.all._ 6 | 7 | 8 | /** 9 | * Created by haoyi on 7/14/14. 10 | */ 11 | object BasicTests extends TestSuite{ 12 | import TestUtil._ 13 | 14 | val tests = Tests { 15 | 16 | 'interpolation{ 17 | 'chained-check( 18 | tw("omg @scala.math.pow(0.5, 3) wtf"), 19 | "omg 0.125 wtf" 20 | ) 21 | 'parens-check( 22 | tw("omg @(1 + 2 + 3 + 4) wtf"), 23 | "omg 10 wtf" 24 | ) 25 | 'block-check( 26 | tw(""" 27 | @{"lol" * 3} 28 | @{ 29 | val omg = "omg" 30 | omg * 2 31 | } 32 | """), 33 | """ 34 | lollollol 35 | omgomg 36 | """ 37 | ) 38 | } 39 | 'definitions{ 40 | 'imports{ 41 | object Whee{ 42 | def func(x: Int) = x * 2 43 | } 44 | check( 45 | tw(""" 46 | @import math._ 47 | @import Whee.func 48 | @abs(-10) 49 | @p 50 | @max(1, 2) 51 | @func(2) 52 | """), 53 | """ 54 | 10 55 |
56 | 2 57 | 4 58 |
59 | """ 60 | ) 61 | } 62 | 'valDefVar{ 63 | check( 64 | tw(""" 65 | Hello 66 | @val x = 1 67 | World @x 68 | @def y = "omg" 69 | mooo 70 | @y 71 | """), 72 | """ 73 | Hello 74 | World 1 75 | mooo 76 | omg 77 | """ 78 | ) 79 | } 80 | 'classObjectTrait{ 81 | check( 82 | tw(""" 83 | @trait Trait{ 84 | def tt = 2 85 | } 86 | Hello 87 | @case object moo extends Trait{ 88 | val omg = "wtf" 89 | } 90 | 91 | @moo.toString 92 | @moo.omg 93 | @case class Foo(i: Int, s: String, b: Boolean) 94 | TT is @moo.tt 95 | @Foo(10, "10", true).toString 96 | """), 97 | """ 98 | Hello 99 | moo 100 | wtf 101 | TT is 2 102 | Foo(10, 10, true) 103 | """ 104 | ) 105 | } 106 | } 107 | 'parenArgumentLists{ 108 | 'attributes{ 109 | check( 110 | tw(""" 111 | @div(id:="my-id"){ omg } 112 | @div(id:="my-id") 113 | omg 114 | """), 115 | """ 116 |I am a cow
134 |lol00lol01lol10lol11
" 325 | ) 326 | check( 327 | tw(""" 328 | @p 329 | @for(x <- 0 until 2) 330 | @for(y <- 0 until 2) 331 | lol@x@y 332 | """), 333 | "lol00lol01lol10lol11
" 334 | ) 335 | 336 | * - check( 337 | tw( 338 | """ 339 | @for(x <- 0 until 2; y <- 0 until 2) 340 | @div{@x@y} 341 | 342 | """), 343 | """Hello
" 417 | ) 418 | } 419 | 'funkyExpressions{ 420 | * - check( 421 | tw(""" 422 | @p 423 | @if(true == false == (true.==(false))) 424 | @if(true == false == (true.==(false))) 425 | Hello1 426 | @else 427 | lols1 428 | @else 429 | @if(true == false == (true.==(false))) 430 | Hello2 431 | @else 432 | lols2 433 | """), 434 | "Hello1
" 435 | ) 436 | * - check( 437 | tw(""" 438 | @p 439 | @if(true == false != (true.==(false))) 440 | @if(true == false != (true.==(false))) 441 | Hello1 442 | @else 443 | lols1 444 | @else 445 | @if(true == false != (true.==(false))) 446 | Hello2 447 | @else 448 | lols2 449 | """), 450 | "lols2
" 451 | ) 452 | } 453 | } 454 | 455 | 'files{ 456 | * - check( 457 | twf("api/src/test/resources/scalatex/success/Simple.scalatex"), 458 | "Hello world!" 459 | ) 460 | * - check( 461 | twf("api/src/test/resources/scalatex/success/Nested.scalatex"), 462 | """ 463 |Hello Everyone!
464 |
467 | And so are you: 1
468 |
469 |
471 | And so are you: 2
472 |
473 |
475 | And so are you: 3
476 |
477 |
26 | I am Cow 27 | Hear me moo 28 | I weigh twice as much as you 29 | And I look good on the barbecue 30 |
31 |63 | Hear me moo 64 | I weigh twice as much as you 65 | And I look good on the barbecue 66 |
67 |214 | The first constant is 1337, the second 215 | is 31337, and wrapping looks like 216 | hello world or 217 | hello world 218 |
219 | """ 220 | ) 221 | } 222 | 'parensNewLine{ 223 | check( 224 | tw(""" 225 | @div( 226 | "hello" 227 | ) 228 | """), 229 | """