├── .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 |
omg
117 |
omg
118 | """ 119 | ) 120 | } 121 | 'multiline{ 122 | 123 | check( 124 | tw(""" 125 | @div( 126 | h1("Hello World"), 127 | p("I am a ", b{"cow"}) 128 | ) 129 | """), 130 | """ 131 |
132 |

Hello World

133 |

I am a cow

134 |
135 | """ 136 | ) 137 | } 138 | } 139 | 'grouping{ 140 | 'negative{ 141 | // The indentation for "normal" text is ignored; we only 142 | // create blocks from the indentation following a scala 143 | // @xxx expression 144 | check( 145 | tw(""" 146 | I am cow hear me moo 147 | I weigh twice as much as you 148 | And I look good on the barbecue 149 | Yoghurt curds cream cheese and butter 150 | Comes from liquids from my udder 151 | I am cow I am cow hear me moooooo 152 | """), 153 | """ 154 | I am cow hear me moo 155 | I weigh twice as much as you 156 | And I look good on the barbecue 157 | Yoghurt curds cream cheese and butter 158 | Comes from liquids from my udder 159 | I am cow I am cow hear me moooooo 160 | """ 161 | ) 162 | } 163 | 'indentation{ 164 | 'simple{ 165 | val world = "World2" 166 | 167 | check( 168 | tw(""" 169 | @h1 170 | Hello World 171 | @h2 172 | hello @world 173 | @h3 174 | Cow 175 | """), 176 | """ 177 |

HelloWorld

178 |

helloWorld2

179 |

Cow

180 | """ 181 | ) 182 | } 183 | 'linearNested{ 184 | check( 185 | tw(""" 186 | @h1 @span @a Hello World 187 | @h2 @span @a hello 188 | @b world 189 | @h3 @i 190 | @div Cow 191 | """), 192 | """ 193 |

HelloWorld 194 |

helloworld 195 |

Cow 196 | """ 197 | ) 198 | } 199 | 'crasher{ 200 | tw(""" 201 | @html 202 | @head 203 | @meta 204 | @div 205 | @a 206 | @span 207 | """) 208 | } 209 | } 210 | 'curlies{ 211 | 'simple{ 212 | val world = "World2" 213 | 214 | check( 215 | tw("""@div{Hello World}"""), 216 | """
HelloWorld
""" 217 | ) 218 | } 219 | 'multiline{ 220 | check( 221 | tw(""" 222 | @div{ 223 | Hello 224 | } 225 | """), 226 | """ 227 |
Hello
228 | """ 229 | ) 230 | } 231 | } 232 | 'mixed{ 233 | check( 234 | tw(""" 235 | @div{ 236 | Hello 237 | @div 238 | @h1 239 | WORLD @b{!!!} 240 | lol 241 | @p{ 242 | @h2{Header 2} 243 | } 244 | } 245 | """), 246 | """ 247 |
248 | Hello 249 |
250 |

WORLD!!!lol

251 |

Header2

252 |
253 |
254 | """ 255 | ) 256 | } 257 | // This would be cool butis currently unsupported 258 | // 259 | // 'args{ 260 | // val things = Seq(1, 2, 3) 261 | // check( 262 | // tw(""" 263 | // @ul 264 | // @things.map { x => 265 | // @li 266 | // @x 267 | // } 268 | // """), 269 | // tw(""" 270 | // @ul 271 | // @things.map x => 272 | // @li 273 | // @x 274 | // 275 | // """), 276 | // """ 277 | // 282 | // """ 283 | // ) 284 | // } 285 | } 286 | // 287 | 'loops { 288 | // 289 | * - check( 290 | tw(""" 291 | @for(x <- 0 until 3) 292 | lol 293 | """), 294 | tw(""" 295 | @for(x <- 0 until 3){ 296 | lol 297 | } 298 | """), 299 | "lollollol" 300 | ) 301 | 302 | 303 | * - check( 304 | tw(""" 305 | @p 306 | @for(x <- 0 until 2) 307 | @for(y <- 0 until 2) 308 | lol@x@y 309 | """), 310 | tw( """ 311 | @p 312 | @for(x <- 0 until 2){ 313 | @for(y <- 0 until 2) 314 | lol@x@y 315 | } 316 | """), 317 | tw(""" 318 | @p 319 | @for(x <- 0 until 2) 320 | @for(y <- 0 until 2){ 321 | lol@x@y 322 | } 323 | """), 324 | "

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 | """
00
01
10
11
""" 344 | ) 345 | } 346 | 347 | 'ifElse{ 348 | 'basicExamples{ 349 | * - check( 350 | tw(""" 351 | @if(false) 352 | Hello 353 | @else 354 | lols 355 | @p 356 | """), 357 | "lols

" 358 | ) 359 | 360 | 361 | * - check( 362 | tw(""" 363 | @div 364 | @if(true) 365 | Hello 366 | @else 367 | lols 368 | """), 369 | "
Hello
" 370 | ) 371 | 372 | * - check( 373 | tw("""@div 374 | @if(true) 375 | Hello 376 | @else 377 | lols 378 | """), 379 | "
Hello
" 380 | ) 381 | * - check( 382 | tw(""" 383 | @if(false) 384 | Hello 385 | @else 386 | lols 387 | """), 388 | "lols" 389 | ) 390 | * - check( 391 | tw(""" 392 | @if(false) 393 | Hello 394 | @else 395 | lols 396 | @img 397 | """), 398 | "lols" 399 | ) 400 | * - check( 401 | tw(""" 402 | @p 403 | @if(true) 404 | Hello 405 | @else 406 | lols 407 | """), 408 | tw(""" 409 | @p 410 | @if(true){ 411 | Hello 412 | }else{ 413 | lols 414 | } 415 | """), 416 | "

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 |
465 |

I am a cow

466 |

467 | And so are you: 1 468 | 469 |

470 |

471 | And so are you: 2 472 | 473 |

474 |

475 | And so are you: 3 476 | 477 |

478 |
""" 479 | ) 480 | } 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /api/src/test/scala/scalatex/ErrorTests.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 | 8 | /** 9 | * Created by haoyi on 7/14/14. 10 | */ 11 | object ErrorTests extends TestSuite { 12 | def check(x: => Unit, expectedMsg: String, expectedError: String) = { 13 | val DebugFailure(msg, pos) = intercept[DebugFailure](x) 14 | def format(str: String) = { 15 | val whitespace = " \t\n".toSet 16 | "\n" + str.dropWhile(_ == '\n') 17 | .reverse 18 | .dropWhile(whitespace.contains) 19 | .reverse 20 | } 21 | // Format these guys nicely to normalize them and make them 22 | // display nicely in the assert error message if it blows up 23 | val formattedPos = format(pos) 24 | val formattedExpectedPos = format(expectedError) 25 | 26 | assert(msg.contains(expectedMsg)) 27 | assert(formattedPos == formattedExpectedPos) 28 | } 29 | 30 | val tests = TestSuite{ 31 | 'syntax{ 32 | 'simple - check( 33 | twRuntimeErrors(""" 34 | @p 35 | ff @ omg 36 | """), 37 | """Syntax error, expected""", 38 | """ 39 | ff @ omg 40 | ^ 41 | """ 42 | ) 43 | 'trailingWhitespace - check( 44 | twRuntimeErrors("@p \n ff @ omg"), 45 | """Syntax error, expected""", 46 | """ 47 | twRuntimeErrors("@p \n ff @ omg"), 48 | ^ 49 | """ 50 | ) 51 | } 52 | 'simple { 53 | * - check( 54 | twRuntimeErrors("@o"), 55 | """not found: value o""", 56 | """ 57 | twRuntimeErrors("@o"), 58 | ^ 59 | """ 60 | ) 61 | val tq = "\"\"\"" 62 | * - check( 63 | 64 | twRuntimeErrors("""@o"""), 65 | """not found: value o""", 66 | s""" 67 | twRuntimeErrors($tq@o$tq), 68 | ^ 69 | """ 70 | ) 71 | 72 | * - check( 73 | twRuntimeErrors("omg @notInScope lol"), 74 | """not found: value notInScope""", 75 | """ 76 | twRuntimeErrors("omg @notInScope lol"), 77 | ^ 78 | """ 79 | ) 80 | } 81 | 'chained{ 82 | 'properties { 83 | * - check( 84 | twRuntimeErrors("omg @math.lol lol"), 85 | """object lol is not a member of package math""", 86 | """ 87 | twRuntimeErrors("omg @math.lol lol"), 88 | ^ 89 | """ 90 | ) 91 | 92 | * - check( 93 | twRuntimeErrors("omg @math.E.lol lol"), 94 | """value lol is not a member of Double""", 95 | """ 96 | twRuntimeErrors("omg @math.E.lol lol"), 97 | ^ 98 | """ 99 | ) 100 | * - check( 101 | twRuntimeErrors("omg @_root_.scala.math.lol lol"), 102 | """object lol is not a member of package math""", 103 | """ 104 | twRuntimeErrors("omg @_root_.scala.math.lol lol"), 105 | ^ 106 | """ 107 | ) 108 | * - check( 109 | twRuntimeErrors("omg @_root_.scala.gg.lol lol"), 110 | """object gg is not a member of package scala""", 111 | """ 112 | twRuntimeErrors("omg @_root_.scala.gg.lol lol"), 113 | ^ 114 | """ 115 | ) 116 | * - check( 117 | twRuntimeErrors("omg @_root_.ggnore.math.lol lol"), 118 | """object ggnore is not a member of package """, 119 | """ 120 | twRuntimeErrors("omg @_root_.ggnore.math.lol lol"), 121 | ^ 122 | """ 123 | ) 124 | } 125 | 'calls{ 126 | * - check( 127 | twRuntimeErrors("@scala.QQ.abs(-10).tdo(10).sum.z"), 128 | """object QQ is not a member of package scala""", 129 | """ 130 | twRuntimeErrors("@scala.QQ.abs(-10).tdo(10).sum.z"), 131 | ^ 132 | """ 133 | ) 134 | * - check( 135 | twRuntimeErrors("@scala.math.abs(-10).tdo(10).sum.z"), 136 | "value tdo is not a member of Int", 137 | """ 138 | twRuntimeErrors("@scala.math.abs(-10).tdo(10).sum.z"), 139 | ^ 140 | """ 141 | ) 142 | * - check( 143 | twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z"), 144 | "value z is not a member of Int", 145 | """ 146 | twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z"), 147 | ^ 148 | """ 149 | ) 150 | * - check( 151 | twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z()"), 152 | "value z is not a member of Int", 153 | """ 154 | twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z()"), 155 | ^ 156 | """ 157 | ) 158 | * - check( 159 | twRuntimeErrors("@scala.math.abs(-10).cow.sum.z"), 160 | "value cow is not a member of Int", 161 | """ 162 | twRuntimeErrors("@scala.math.abs(-10).cow.sum.z"), 163 | ^ 164 | """ 165 | ) 166 | * - check( 167 | twRuntimeErrors("@scala.smath.abs.cow.sum.z"), 168 | "object smath is not a member of package scala", 169 | """ 170 | twRuntimeErrors("@scala.smath.abs.cow.sum.z"), 171 | ^ 172 | """ 173 | ) 174 | * - check( 175 | twRuntimeErrors("@scala.math.cos('omg)"), 176 | "type mismatch", 177 | """ 178 | twRuntimeErrors("@scala.math.cos('omg)"), 179 | ^ 180 | """ 181 | ) 182 | 183 | * - check( 184 | twRuntimeErrors("@scala.math.cos[omg]('omg)"), 185 | "not found: type omg", 186 | """ 187 | twRuntimeErrors("@scala.math.cos[omg]('omg)"), 188 | ^ 189 | """ 190 | ) 191 | * - check( 192 | twRuntimeErrors(""" 193 | I am cow hear me moo 194 | @scala.math.abs(-10).tdo(10).sum.z 195 | I weigh twice as much as you 196 | """), 197 | "value tdo is not a member of Int", 198 | """ 199 | @scala.math.abs(-10).tdo(10).sum.z 200 | ^ 201 | """ 202 | ) 203 | } 204 | 'curlies{ 205 | * - check( 206 | twRuntimeErrors("@Nil.foldLeft{XY}"), 207 | "missing argument list for method foldLeft", 208 | """ 209 | twRuntimeErrors("@Nil.foldLeft{XY}"), 210 | ^ 211 | """ 212 | ) 213 | * - check( 214 | twRuntimeErrors("@Seq(1).map{(y: String) => omg}"), 215 | "type mismatch", 216 | """ 217 | twRuntimeErrors("@Seq(1).map{(y: String) => omg}"), 218 | ^ 219 | """ 220 | ) 221 | } 222 | 223 | 'callContents{ 224 | * - check( 225 | twRuntimeErrors("@scala.math.abs((1, 2).wtf)"), 226 | "value wtf is not a member of (Int, Int)", 227 | """ 228 | twRuntimeErrors("@scala.math.abs((1, 2).wtf)"), 229 | ^ 230 | """ 231 | ) 232 | 233 | * - check( 234 | twRuntimeErrors("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"), 235 | "value wtf is not a member of String", 236 | """ 237 | twRuntimeErrors("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"), 238 | ^ 239 | """ 240 | ) 241 | } 242 | } 243 | 'ifElse{ 244 | 'oneLine { 245 | * - check( 246 | twRuntimeErrors("@if(math > 10){ 1 }else{ 2 }"), 247 | "object > is not a member of package math", 248 | """ 249 | twRuntimeErrors("@if(math > 10){ 1 }else{ 2 }"), 250 | ^ 251 | """ 252 | ) 253 | * - check( 254 | twRuntimeErrors("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"), 255 | "Unspecified value parameter y", 256 | """ 257 | twRuntimeErrors("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"), 258 | ^ 259 | """ 260 | ) 261 | } 262 | 'multiLine{ 263 | * - check( 264 | twRuntimeErrors(""" 265 | Ho Ho Ho 266 | 267 | @if(math != 10) 268 | I am a cow 269 | @else 270 | You are a cow 271 | GG 272 | """), 273 | "object != is not a member of package math", 274 | """ 275 | @if(math != 10) 276 | ^ 277 | """ 278 | ) 279 | * - check( 280 | twRuntimeErrors(""" 281 | Ho Ho Ho 282 | 283 | @if(4 != 10) 284 | I am a cow @math.lols 285 | @else 286 | You are a cow 287 | GG 288 | """), 289 | "object lols is not a member of package math", 290 | """ 291 | I am a cow @math.lols 292 | ^ 293 | """ 294 | ) 295 | * - check( 296 | twRuntimeErrors(""" 297 | Ho Ho Ho 298 | 299 | @if(12 != 10) 300 | I am a cow 301 | @else 302 | @math.E.toString.gog(1) 303 | GG 304 | """), 305 | "value gog is not a member of String", 306 | """ 307 | @math.E.toString.gog(1) 308 | ^ 309 | """ 310 | ) 311 | } 312 | } 313 | 'forLoop{ 314 | 'oneLine{ 315 | 'header - check( 316 | twRuntimeErrors("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"), 317 | """value omglolol is not a member of Int""", 318 | """ 319 | twRuntimeErrors("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"), 320 | ^ 321 | """ 322 | ) 323 | } 324 | 'multiLine{ 325 | 'body - check( 326 | twRuntimeErrors(""" 327 | omg 328 | @for(x <- 0 until 10) 329 | I am cow hear me moo 330 | I weigh twice as much as @x.kkk 331 | """), 332 | """value kkk is not a member of Int""", 333 | """ 334 | I weigh twice as much as @x.kkk 335 | ^ 336 | """ 337 | ) 338 | } 339 | } 340 | 341 | 'multiLine{ 342 | 'missingVar - check( 343 | twRuntimeErrors(""" 344 | omg @notInScope lol 345 | """), 346 | """not found: value notInScope""", 347 | """ 348 | omg @notInScope lol 349 | ^ 350 | """ 351 | ) 352 | 'wrongType - check( 353 | twRuntimeErrors(""" 354 | omg @{() => ()} lol 355 | """), 356 | """type mismatch""", 357 | """ 358 | omg @{() => ()} lol 359 | ^ 360 | """ 361 | ) 362 | 363 | 'bigExpression - check( 364 | twRuntimeErrors(""" 365 | @{ 366 | val x = 1 + 2 367 | val y = new Object() 368 | val z = y * x 369 | x 370 | } 371 | """), 372 | "value * is not a member of Object", 373 | """ 374 | val z = y * x 375 | ^ 376 | """ 377 | ) 378 | 379 | 'header - check( 380 | twRuntimeErrors(""" 381 | @val x = 1 382 | @val y = x map 0 383 | lol lol 384 | """), 385 | "value map is not a member of Int", 386 | """ 387 | @val y = x map 0 388 | ^ 389 | """ 390 | ) 391 | 392 | 'blocks - check( 393 | twRuntimeErrors(""" 394 | lol 395 | omg 396 | wtf 397 | bbq 398 | @body 399 | @div 400 | @span 401 | @lol 402 | """), 403 | "not found: value lol", 404 | """ 405 | @lol 406 | ^ 407 | """ 408 | ) 409 | } 410 | 'files{ 411 | * - check( 412 | twfRuntimeErrors("api/src/test/resources/scalatex/errors/Simple.scalatex"), 413 | "not found: value kk", 414 | """ 415 | |Hello @kk 416 | | ^ 417 | """.stripMargin 418 | ) 419 | 420 | * - check( 421 | twfRuntimeErrors("api/src/test/resources/scalatex/errors/Nested.scalatex"), 422 | "not found: value y", 423 | """ 424 | | @y 425 | | ^ 426 | """.stripMargin 427 | ) 428 | } 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /api/src/test/scala/scalatex/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package scalatex 2 | 3 | import utest._ 4 | 5 | import scalatags.Text.all._ 6 | 7 | 8 | object ExampleTests extends TestSuite{ 9 | import scalatex.TestUtil._ 10 | val tests = TestSuite{ 11 | 'helloWorld - check( 12 | tw(""" 13 | @div(id:="my-div") 14 | @h1 15 | Hello World! 16 | @p 17 | I am @b{Cow} 18 | Hear me moo 19 | I weigh twice as much as you 20 | And I look good on the barbecue 21 | """), 22 | """ 23 |
24 |

Hello World!

25 |

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 |
32 | """ 33 | ) 34 | 'empty1 - check( 35 | tw( 36 | """ 37 | """), 38 | "" 39 | ) 40 | 'empty2 - check( 41 | tw(""""""), 42 | "" 43 | ) 44 | 'escaping - check( 45 | tw("haoyi@@gmail.com"), 46 | "haoyi@gmail.com" 47 | ) 48 | 'variousSyntaxes - check( 49 | tw(""" 50 | @div 51 | @h1("Hello World") 52 | @h2{I am Cow} 53 | @p 54 | Hear me @b{moo} 55 | I weigh twice as much as you 56 | And I look good on the barbecue 57 | """), 58 | """ 59 |
60 |

Hello World

61 |

I am Cow

62 |

63 | Hear me moo 64 | I weigh twice as much as you 65 | And I look good on the barbecue 66 |

67 |
68 | """ 69 | ) 70 | 71 | 'controlFlow{ 72 | 'loops - check( 73 | tw(""" 74 | @ul 75 | @for(x <- 0 until 3) 76 | @li 77 | lol @x 78 | """), 79 | tw(""" 80 | @ul 81 | @for(x <- 0 until 3){@li{lol @x}} 82 | """), 83 | """ 84 | 89 | """ 90 | ) 91 | 92 | 'conditionals - check( 93 | tw(""" 94 | @ul 95 | @for(x <- 0 until 5) 96 | @li 97 | @if(x % 2 == 0) 98 | @b{lol} @x 99 | @else 100 | @i{lol} @x 101 | """), 102 | """ 103 | 110 | """ 111 | ) 112 | 113 | 'functions - check( 114 | tw(""" 115 | @span 116 | The square root of 9.0 117 | is @math.sqrt(9.0) 118 | """), 119 | """ 120 | 121 | The square root of 9.0 is 3.0 122 | 123 | """ 124 | ) 125 | 126 | } 127 | 'interpolation{ 128 | 'simple - check( 129 | tw(""" 130 | @span 131 | 1 + 2 is @(1 + 2) 132 | """), 133 | """ 134 | 1 + 2 is 3 135 | """ 136 | ) 137 | 138 | 'multiline - check( 139 | tw(""" 140 | @div 141 | 1 + 2 is @{ 142 | val x = "1" 143 | val y = "2" 144 | println(s"Adding $x and $y") 145 | x.toInt + y.toInt 146 | } 147 | """), 148 | 149 | """ 150 |
151 | 1 + 2 is 3 152 |
153 | """ 154 | ) 155 | } 156 | 'definitions{ 157 | 'imports - check( 158 | tw(""" 159 | @import scala.math._ 160 | @ul 161 | @for(i <- -2 to 2) 162 | @li 163 | @abs(i) 164 | """), 165 | """ 166 | 173 | """ 174 | ) 175 | 176 | 'externalDefinitions{ 177 | object Stuff { 178 | object omg { 179 | def wrap(s: Frag*): Frag = Seq[Frag]("...", s, "...") 180 | } 181 | def str = "hear me moo" 182 | } 183 | 'externalDefinitions2 184 | check( 185 | tw(""" 186 | @import Stuff._ 187 | @omg.wrap 188 | i @b{am} cow @str 189 | """), 190 | """ 191 | ...i am cow hear me moo... 192 | """ 193 | ) 194 | } 195 | 'internalDefinitions{ 196 | 197 | check( 198 | tw(""" 199 | @object Foo{ 200 | val const1 = 1337 201 | } 202 | @val const2 = 30000 + 1337 203 | 204 | @p 205 | @def wrap(s: Frag*) = span(u(s)) 206 | 207 | The first constant is @Foo.const1, 208 | the second is @const2, and wrapping 209 | looks like @wrap{hello @b{world}} 210 | or @wrap("hello ", b("world")) 211 | """), 212 | """ 213 |

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 | """
hello
""" 230 | ) 231 | } 232 | 'thingy { 233 | check( 234 | tw(""" 235 | @val x = {1} 236 | @val y = {2} 237 | 238 | @x + @y is @(x + y) 239 | """), 240 | """ 241 | 1 + 2 is 3 242 | """ 243 | ) 244 | } 245 | 'comment { 246 | check( 247 | tw(""" 248 | @val x = {1} 249 | @val y = {2} 250 | 251 | /* Comment */ 252 | 253 | @x + @y is @(x + y) 254 | """), 255 | """ 256 | 1 + 2 is 3 257 | """ 258 | ) 259 | } 260 | } 261 | 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /api/src/test/scala/scalatex/Main.scala: -------------------------------------------------------------------------------- 1 | //package scalatex 2 | //import scalatags.Text.all._ 3 | //import scalatex.Internals._ 4 | // 5 | //object Main { 6 | // def main(args: Array[String]): Unit = { 7 | // 8 | //// twRuntimeErrors(""" 9 | //// @val x = 1 10 | //// @val y = x map 0 11 | //// lol lol 12 | //// """) 13 | // } 14 | //} 15 | // 16 | -------------------------------------------------------------------------------- /api/src/test/scala/scalatex/ParseErrors.scala: -------------------------------------------------------------------------------- 1 | package scalatex 2 | 3 | import utest._ 4 | import fastparse._ 5 | import scalatex.Internals.{DebugFailure, twRuntimeErrors, twfRuntimeErrors} 6 | 7 | /** 8 | * Created by haoyi on 7/14/14. 9 | */ 10 | object ParseErrors extends TestSuite { 11 | 12 | def check(input: String, expectedTrace: String) = { 13 | val failure = parse(input, new stages.Parser().File(_)).asInstanceOf[fastparse.Parsed.Failure] 14 | val parsedTrace = failure.trace().msg //trace(false) 15 | assert(parsedTrace.trim == expectedTrace.trim) 16 | } 17 | 18 | val tests = Tests { 19 | * - check( 20 | """@{) 21 | |""".stripMargin, 22 | """ Expected "}":1:3, found ")\n" """ 23 | ) 24 | * - check( 25 | """@({ 26 | |""".stripMargin, 27 | """ Expected "}":2:1, found "" """ 28 | ) 29 | * - check( 30 | """@for{; 31 | |""".stripMargin, 32 | """ Expected (TypePattern | BindPattern):1:6, found ";\n" """ 33 | ) 34 | * - check( 35 | """@{ => x 36 | |""".stripMargin, 37 | """ Expected "}":1:4, found "=> x\n" """ 38 | ) 39 | 40 | * - check( 41 | """@( => x 42 | |""".stripMargin, 43 | """ Expected ")":1:4, found "=> x\n" """ 44 | ) 45 | * - check( 46 | """@x{ 47 | |""".stripMargin, 48 | """ Expected "}":2:1, found "" """ 49 | ) 50 | * - check( 51 | """@ """.stripMargin, 52 | """ Expected (IndentForLoop | IndentScalaChain | IndentIfElse | HeaderBlock | @@):1:2, found " """" 53 | ) 54 | 55 | * - check( 56 | """@p 57 | | @if( 58 | |""".stripMargin, 59 | """ Expected (If | While | Try | DoWhile | For | Throw | Return | ImplicitLambda | SmallerExprOrLambda):2:7, found "\n" """ 60 | ) 61 | * - check( 62 | """@if(true){ 123 }else lol 63 | |""".stripMargin, 64 | """ Expected "{":1:21, found " lol\n" """ 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /api/src/test/scala/scalatex/ParserTests.scala: -------------------------------------------------------------------------------- 1 | package scalatex 2 | 3 | 4 | 5 | import scalatex.stages.{Trim, Parser, Ast} 6 | import Ast.Chain.Args 7 | import Ast._ 8 | import utest._ 9 | import fastparse._ 10 | 11 | object ParserTests extends utest.TestSuite{ 12 | 13 | def check[T](input: String, parser: P[_] => P[T], expected: T) = { 14 | parse(input, parser) match{ 15 | case s: fastparse.Parsed.Success[T] => 16 | val result = s.value 17 | assert(result == expected) 18 | case f: fastparse.Parsed.Failure => throw new Exception(f.extra.trace(true).longAggregateMsg) 19 | } 20 | } 21 | 22 | def tests = Tests { 23 | val p = new Parser() 24 | import p._ 25 | 26 | 'Trim{ 27 | def wrap(s: String) = "|" + s + "|" 28 | * - { 29 | val trimmed = wrap(stages.Trim.old(""" 30 | i am cow 31 | hear me moo 32 | i weigh twice as much as you 33 | """)) 34 | val expected = wrap(""" 35 | |i am cow 36 | | hear me moo 37 | | i weigh twice as much as you 38 | |""".stripMargin) 39 | assert(trimmed == expected) 40 | } 41 | 42 | * - { 43 | val trimmed = wrap(stages.Trim.old( 44 | """ 45 | @{"lol" * 3} 46 | @{ 47 | val omg = "omg" 48 | omg * 2 49 | } 50 | """ 51 | )) 52 | val expected = wrap( 53 | """ 54 | |@{"lol" * 3} 55 | |@{ 56 | | val omg = "omg" 57 | | omg * 2 58 | |} 59 | |""".stripMargin 60 | ) 61 | assert(trimmed == expected) 62 | } 63 | 'dontDropTrailingWhitespace - { 64 | 65 | val trimmed = wrap(stages.Trim.old( 66 | Seq( 67 | " i am a cow ", 68 | " hear me moo ", 69 | " i weigh twice as much as you" 70 | ).mkString("\n") 71 | )) 72 | val expected = wrap( 73 | Seq( 74 | "i am a cow ", 75 | " hear me moo ", 76 | " i weigh twice as much as you" 77 | ).mkString("\n") 78 | ) 79 | assert(trimmed == expected) 80 | } 81 | } 82 | 'Text { 83 | println("Checking...") 84 | * - check("i am a cow", Text(_), Block.Text(0, "i am a cow")) 85 | * - check("i am a @cow", Text(_), Block.Text(0, "i am a ")) 86 | * - check("i am a @@cow", Text(_), Block.Text(0, "i am a ")) 87 | * - check("i am a @@@cow", Text(_), Block.Text(0, "i am a ")) 88 | * - check("i am a @@@@cow", Text(_), Block.Text(0, "i am a ")) 89 | } 90 | 'Code{ 91 | 'identifier - check("gggg ", Code(_), "gggg") 92 | 'parens - check("(1 + 1)lolsss\n", Code(_), "(1 + 1)") 93 | 'curlies - check("{{1} + (1)} ", Code(_), "{{1} + (1)}") 94 | 'blocks - check("{val x = 1; 1} ", Code(_), "{val x = 1; 1}") 95 | 'weirdBackticks - check("{`{}}{()@`}\n", Code(_), "{`{}}{()@`}") 96 | } 97 | 'MiscCode{ 98 | 'imports{ 99 | * - check("import math.abs", Header(_), "import math.abs") 100 | * - check("import math.{abs, sin}", Header(_), "import math.{abs, sin}") 101 | } 102 | 'headerblocks{ 103 | check( 104 | """import math.abs 105 | |@import math.sin 106 | | 107 | |hello world 108 | |""".stripMargin, 109 | HeaderBlock(_), 110 | Ast.Header( 111 | 0, 112 | "import math.abs\nimport math.sin", 113 | Block(32, 114 | Seq(Block.Text(32, "\n\nhello world\n")) 115 | ) 116 | ) 117 | ) 118 | } 119 | 'caseclass{ 120 | check( 121 | """case class Foo(i: Int, s: String)""".stripMargin, 122 | Header(_), 123 | "case class Foo(i: Int, s: String)" 124 | ) 125 | } 126 | 127 | } 128 | 'Block{ 129 | * - check("{i am a cow}", BraceBlock(_), Block(1, Seq(Block.Text(1, "i am a cow")))) 130 | * - check("{i @am a @cow}", BraceBlock(_), 131 | Block(1, Seq( 132 | Block.Text(1, "i "), 133 | Chain(4, "am",Seq()), 134 | Block.Text(6, " a "), 135 | Chain(10, "cow",Seq()) 136 | )) 137 | ) 138 | } 139 | 'Chain{ 140 | * - check("omg.bbq[omg].fff[fff](123) ", ScalaChain(_), 141 | Chain(0, "omg",Seq( 142 | Chain.Prop(3, "bbq"), 143 | Chain.TypeArgs(7, "[omg]"), 144 | Chain.Prop(12, "fff"), 145 | Chain.TypeArgs(16, "[fff]"), 146 | Chain.Args(21, "(123)") 147 | )) 148 | ) 149 | * - check("omg{bbq}.cow(moo){a @b}\n", ScalaChain(_), 150 | Chain(0, "omg",Seq( 151 | Block(4, Seq(Block.Text(4, "bbq"))), 152 | Chain.Prop(8, "cow"), 153 | Chain.Args(12, "(moo)"), 154 | Block(18, Seq(Block.Text(18, "a "), Chain(21, "b", Nil))) 155 | )) 156 | ) 157 | } 158 | 'Escaping{ 159 | check( 160 | """ 161 | |haoyi@@gmail.com""".stripMargin, 162 | File(_), 163 | Block(0, Seq( 164 | Block.Text(0, "\nhaoyi@gmail.com") 165 | )) 166 | ) 167 | } 168 | 'ControlFlow{ 169 | 'for { 170 | 'for - check( 171 | "for(x <- 0 until 3){lol}", 172 | ForLoop(_), 173 | Block.For(0, "for(x <- 0 until 3)", Block(20, Seq(Block.Text(20, "lol")))) 174 | ) 175 | 'forBlock - check( 176 | """ 177 | |@for(x <- 0 until 3) 178 | | lol""".stripMargin, 179 | File(_), 180 | Block(0, Seq( 181 | Block.Text(0, "\n"), 182 | Block.For( 183 | 2, 184 | "for(x <- 0 until 3)", 185 | Block(21, Seq(Block.Text(21, "\n lol"))) 186 | ) 187 | )) 188 | ) 189 | 'forBlockBraces - check( 190 | """ 191 | |@for(x <- 0 until 3){ 192 | | lol 193 | |}""".stripMargin, 194 | File(_), 195 | Block(0, Seq( 196 | Block.Text(0, "\n"), 197 | Block.For( 198 | 2, 199 | "for(x <- 0 until 3)", 200 | Block(22, Seq(Block.Text(22, "\n lol\n"))) 201 | ) 202 | )) 203 | ) 204 | } 205 | 'ifElse { 206 | 'if - check( 207 | "if(true){lol}", 208 | IfElse(_), 209 | Block.IfElse(0, "if(true)", Block(9, Seq(Block.Text(9, "lol"))), None) 210 | ) 211 | 'ifElse - check( 212 | "if(true){lol}else{ omg }", 213 | IfElse(_), 214 | Block.IfElse(0, "if(true)", Block(9, Seq(Block.Text(9, "lol"))), Some(Block(18, Seq(Block.Text(18, " omg "))))) 215 | ) 216 | 'ifBlock - check( 217 | """if(true) 218 | | omg""".stripMargin, 219 | IndentIfElse(_), 220 | Block.IfElse(0, "if(true)", Block(8, Seq(Block.Text(8, "\n omg"))), None) 221 | ) 222 | 223 | 'ifBlockElseBlock - check( 224 | """if(true) 225 | | omg 226 | |@else 227 | | wtf""".stripMargin, 228 | IndentIfElse(_), 229 | Block.IfElse( 230 | 0, 231 | "if(true)", 232 | Block(8, Seq(Block.Text(8, "\n omg"))), 233 | Some(Block(20, Seq(Block.Text(20, "\n wtf")))) 234 | ) 235 | ) 236 | 'ifBlockElseBraceBlock - check( 237 | """if(true){ 238 | | omg 239 | |}else{ 240 | | wtf 241 | |}""".stripMargin, 242 | IfElse(_), 243 | Block.IfElse( 244 | 0, 245 | "if(true)", 246 | Block(9, Seq(Block.Text(9, "\n omg\n"))), 247 | Some(Block(22, Seq(Block.Text(22, "\n wtf\n")))) 248 | ) 249 | ) 250 | 'ifBlockElseBraceBlockNested - { 251 | val res = Parser(Trim.old( 252 | """ 253 | @p 254 | @if(true){ 255 | Hello 256 | }else{ 257 | lols 258 | } 259 | """)).get.value 260 | val expected = 261 | Block(0, Vector( 262 | Block.Text(0, "\n"), 263 | Chain(2, "p",Vector(Block(3, Vector( 264 | Block.Text(3, "\n "), 265 | Block.IfElse(7, "if(true)", 266 | Block(16, Vector( 267 | Block.Text(16, "\n Hello\n ") 268 | )), 269 | Some(Block(35, Vector( 270 | Block.Text(35, "\n lols\n ") 271 | ))) 272 | ))))), 273 | Block.Text(48, "\n") 274 | )) 275 | assert(res == expected) 276 | } 277 | } 278 | 279 | } 280 | 'Body{ 281 | 'indents - check( 282 | """ 283 | |@omg 284 | | @wtf 285 | | @bbq 286 | | @lol""".stripMargin, 287 | File(_), 288 | Block(0, Seq( 289 | Block.Text(0, "\n"), 290 | Chain(2, "omg",Seq(Block(5, Seq( 291 | Block.Text(5, "\n "), 292 | Chain(9, "wtf",Seq(Block(12, Seq( 293 | Block.Text(12, "\n "), 294 | Chain(18, "bbq",Seq(Block(21, Seq( 295 | Block.Text(21, "\n "), 296 | Chain(29, "lol",Seq()) 297 | )))) 298 | )))) 299 | )))) 300 | )) 301 | ) 302 | 'dedents - check( 303 | """ 304 | |@omg 305 | | @wtf 306 | |@bbq""".stripMargin, 307 | File(_), 308 | Block(0, 309 | Seq( 310 | Block.Text(0, "\n"), 311 | Chain(2, "omg",Seq(Block(5, 312 | Seq( 313 | Block.Text(5, "\n "), 314 | Chain(9, "wtf",Seq()) 315 | ) 316 | ))), 317 | Block.Text(12, "\n"), 318 | Chain(14, "bbq", Seq()) 319 | )) 320 | ) 321 | 'braces - check( 322 | """ 323 | |@omg{ 324 | | @wtf 325 | |} 326 | |@bbq""".stripMargin, 327 | File(_), 328 | Block(0, Seq( 329 | Block.Text(0, "\n"), 330 | Chain(2, "omg",Seq(Block(6, 331 | Seq( 332 | Block.Text(6, "\n "), 333 | Chain(10, "wtf",Seq()), 334 | Block.Text(13, "\n") 335 | ) 336 | ))), 337 | Block.Text(15, "\n"), 338 | Chain(17, "bbq", Seq()) 339 | )) 340 | ) 341 | 'dedentText - check( 342 | """ 343 | |@omg("lol", 1, 2) 344 | | @wtf 345 | |bbq""".stripMargin, 346 | File(_), 347 | Block(0, Seq( 348 | Block.Text(0, "\n"), 349 | Chain(2, "omg",Seq( 350 | Args(5, """("lol", 1, 2)"""), 351 | Block(18, Seq( 352 | Block.Text(18, "\n "), 353 | Chain(22, "wtf",Seq()) 354 | )) 355 | )), 356 | Block.Text(25, "\nbbq") 357 | )) 358 | ) 359 | // 'weird - check( 360 | // """ 361 | // |@omg("lol", 362 | // |1, 363 | // | 2 364 | // | ) 365 | // | wtf 366 | // |bbq""".stripMargin, 367 | // _.File, 368 | // Block(0, Seq( 369 | // Text(0, "\n"), 370 | // Chain(1, "omg",Seq( 371 | // Args(5, "(\"lol\",\n1,\n 2\n )"), 372 | // Block(30, Seq( 373 | // Text(30, "\n "), Text(33, "wtf") 374 | // )) 375 | // )), 376 | // Text(36, "\n"), 377 | // Text(37, "bbq") 378 | // )) 379 | // ) 380 | 'codeBlock - check( 381 | """{ 382 | | val omg = "omg" 383 | | omg * 2 384 | |}""".stripMargin, 385 | Code(_), 386 | """{ 387 | | val omg = "omg" 388 | | omg * 2 389 | |}""".stripMargin 390 | ) 391 | 'codeBlocks - check( 392 | """ 393 | |@{"lol" * 3} 394 | |@{ 395 | | val omg = "omg" 396 | | omg * 2 397 | |}""".stripMargin, 398 | File(_), 399 | Block(0, Seq( 400 | Block.Text(0, "\n"), 401 | Chain(2, "{\"lol\" * 3}", Seq()), 402 | Block.Text(13, "\n"), 403 | Chain(15, """{ 404 | | val omg = "omg" 405 | | omg * 2 406 | |}""".stripMargin, 407 | Seq() 408 | ) 409 | )) 410 | ) 411 | 'nesting - check( 412 | """ 413 | |lol 414 | |omg 415 | |wtf 416 | |bbq 417 | |@body 418 | | @div 419 | | @span 420 | | @lol""".stripMargin, 421 | File(_), 422 | Block(0,List( 423 | Block.Text(0,"\nlol\nomg\nwtf\nbbq\n"), 424 | Chain(18,"body",Vector( 425 | Block(22,List(Block.Text(22,"\n "), 426 | Chain(26,"div",Vector(Block(29,List(Block.Text(29, "\n "), 427 | Chain(35,"span",Vector(Block(39,List(Block.Text(39, "\n "), 428 | Chain(47,"lol",Vector()))) 429 | )) 430 | )) 431 | )))))))) 432 | ) 433 | } 434 | 'Coment { 435 | 'comentBlock - check( 436 | """ 437 | |/* This is a comment */ 438 | |This isn't""".stripMargin, 439 | File(_), 440 | Block(0,List(Block.Text(0, "\n"), Block.Comment(1,"/* This is a comment */"), Block.Text(24,"\nThis isn't"))) 441 | ) 442 | 'nestedComent - check( 443 | """ 444 | |@b{Comment using /*Test*/} 445 | |""".stripMargin, 446 | File(_), 447 | Block(0,List(Block.Text(0,"\n"), Chain(2,"b",List(Block(4,List(Block.Text(4,"Comment using /*Test*/"))))), Block.Text(27,"\n"))) 448 | ) 449 | } 450 | //// 'Test{ 451 | //// check( 452 | //// "@{() => ()}", 453 | //// _.Code, 454 | //// "" 455 | //// ) 456 | //// } 457 | } 458 | } 459 | 460 | 461 | 462 | -------------------------------------------------------------------------------- /api/src/test/scala/scalatex/TestUtil.scala: -------------------------------------------------------------------------------- 1 | package scalatex 2 | 3 | import utest._ 4 | 5 | 6 | object TestUtil { 7 | implicit def stringify(f: scalatags.Text.all.Frag) = f.render 8 | def check(rendered: String*) = { 9 | val collapsed = rendered.map(collapse) 10 | val first = collapsed(0) 11 | assert(collapsed.forall(_ == first)) 12 | } 13 | def collapse(s: String): String = { 14 | s.replaceAll("[ \n]", "") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val Constants = _root_.scalatex.Constants 2 | sharedSettings 3 | noPublish 4 | 5 | version in ThisBuild := Constants.version 6 | releasePublishArtifactsAction := PgpKeys.publishSigned.value 7 | releaseVersionBump := sbtrelease.Version.Bump.Minor 8 | releaseTagComment := s"Releasing ${(version in ThisBuild).value}" 9 | releaseCommitMessage := s"Bump version to ${(version in ThisBuild).value}" 10 | sonatypeProfileName := Constants.organization 11 | releaseCrossBuild := true 12 | publishConfiguration := publishConfiguration.value.withOverwrite(true) 13 | 14 | import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations._ 15 | 16 | releaseProcess := Seq[ReleaseStep]( 17 | checkSnapshotDependencies, 18 | //inquireVersions, 19 | runClean, 20 | runTest, 21 | //setReleaseVersion, 22 | //commitReleaseVersion, 23 | tagRelease, 24 | //publishArtifacts, 25 | //setNextVersion, 26 | //commitNextVersion, 27 | releaseStepCommandAndRemaining("+publishSigned"), 28 | releaseStepCommand("sonatypeBundleRelease"), 29 | //releaseStepCommand("+publishSigned"), 30 | //releaseStepCommand("sonatypeReleaseAll"), 31 | //pushChanges 32 | ) 33 | 34 | def supportedScalaVersion = Seq(Constants.scala212, Constants.scala213) 35 | 36 | lazy val sharedSettings = Seq( 37 | version := Constants.version, 38 | organization := Constants.organization, 39 | scalaVersion := Constants.scala213, 40 | crossScalaVersions := supportedScalaVersion, 41 | libraryDependencies += "com.lihaoyi" %% "acyclic" % "0.2.0" % "provided", 42 | addCompilerPlugin("com.lihaoyi" %% "acyclic" % "0.2.0"), 43 | autoCompilerPlugins := true, 44 | publishMavenStyle := true, 45 | publishTo := sonatypePublishToBundle.value, 46 | //publishTo := Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2"), 47 | pomExtra := 48 | https://github.com/lihaoyi/Scalatex 49 | 50 | 51 | MIT license 52 | http://www.opensource.org/licenses/mit-license.php 53 | 54 | 55 | 56 | 57 | lihaoyi 58 | Li Haoyi 59 | https://github.com/lihaoyi 60 | 61 | 62 | reuillon 63 | Romain Reuillon 64 | https://github.com/reuillon 65 | 66 | 67 | ) 68 | 69 | lazy val noPublish = Seq( 70 | publishArtifact := false, 71 | publishLocal := {}, 72 | publish := {} 73 | ) 74 | 75 | lazy val circe = 76 | Seq( 77 | "io.circe" %% "circe-core", 78 | "io.circe" %% "circe-generic", 79 | "io.circe" %% "circe-parser" 80 | ).map(_ % Constants.circe) 81 | 82 | lazy val api = project.settings(sharedSettings:_*) 83 | .settings( 84 | name := "scalatex-api", 85 | libraryDependencies ++= Seq( 86 | "com.lihaoyi" %% "utest" % Constants.uTest % "test", 87 | "com.lihaoyi" %% "scalaparse" % "2.2.4", 88 | "com.lihaoyi" %% "scalatags" % Constants.scalaTags, 89 | "org.scala-lang" % "scala-reflect" % scalaVersion.value 90 | ), 91 | testFrameworks += new TestFramework("utest.runner.Framework") 92 | ) 93 | 94 | def isScala12(v: String) = 95 | v match { 96 | case "2.12" => false 97 | case _ => true 98 | } 99 | 100 | lazy val scalatexSbtPlugin = project.settings(sharedSettings:_*) 101 | .settings( 102 | name := "scalatex-sbt-plugin", 103 | scalaVersion := Constants.scala212, 104 | skip in Compile := isScala12(scalaBinaryVersion.value), 105 | sources in (Compile, doc) := Nil, 106 | publishArtifact := !isScala12(scalaBinaryVersion.value), 107 | publishArtifact in (Compile, packageDoc) := !isScala12(scalaBinaryVersion.value), 108 | crossSbtVersions := List("1.3.7"), 109 | sbtPlugin := true, 110 | (unmanagedSources in Compile) += baseDirectory.value/".."/"project"/"Constants.scala" 111 | ) 112 | 113 | lazy val site = 114 | project 115 | .dependsOn(api) 116 | .settings(scalatex.SbtPlugin.projectSettings:_*) 117 | .settings(sharedSettings:_*) 118 | .settings( 119 | libraryDependencies := libraryDependencies.value.filter(!_.toString.contains("scalatex-api")), 120 | name := "scalatex-site", 121 | crossScalaVersions := supportedScalaVersion, 122 | libraryDependencies ++= Seq( 123 | "com.lihaoyi" %% "utest" % Constants.uTest % "test", 124 | "com.lihaoyi" %% "ammonite-ops" % "2.0.4", 125 | "org.webjars.bower" % "highlightjs" % "9.12.0", 126 | "org.webjars.bowergithub.highlightjs" % "highlight.js" % "9.12.0", 127 | "org.webjars" % "font-awesome" % "4.7.0", 128 | "com.lihaoyi" %% "scalatags" % Constants.scalaTags, 129 | "org.webjars" % "pure" % "0.6.2", 130 | "org.scalaj" %% "scalaj-http" % "2.4.2" 131 | ) ++ circe, 132 | testFrameworks += new TestFramework("utest.runner.Framework"), 133 | (managedResources in Compile) += { 134 | val file = (resourceManaged in Compile).value/"scalatex"/"scrollspy"/"scrollspy.js" 135 | val js = (fullOptJS in (scrollspy, Compile)).value.data 136 | sbt.IO.copyFile(js, file) 137 | file 138 | } 139 | ) 140 | 141 | lazy val scrollspy = project 142 | .enablePlugins(ScalaJSPlugin) 143 | .settings( 144 | sharedSettings, 145 | scalaVersion := Constants.scala213, 146 | crossScalaVersions := supportedScalaVersion, 147 | //scalacOptions += "-P:scalajs:suppressExportDeprecations", // see https://github.com/scala-js/scala-js/issues/3092 148 | libraryDependencies ++= Seq( 149 | "org.scala-js" %%% "scalajs-dom" % "1.0.0", 150 | "com.lihaoyi" %%% "scalatags" % Constants.scalaTags, 151 | "io.circe" %%% "circe-core" % Constants.circe, 152 | "io.circe" %%% "circe-generic" % Constants.circe, 153 | "io.circe" %%% "circe-parser" % Constants.circe 154 | ), 155 | test := {}, 156 | //emitSourceMaps := false, 157 | noPublish 158 | ) 159 | 160 | lazy val readme = scalatex.ScalatexReadme( 161 | projectId = "readme", 162 | wd = file(""), 163 | url = "https://github.com/lihaoyi/scalatex/tree/master", 164 | source = "Readme" 165 | ) 166 | .settings( 167 | sharedSettings, 168 | siteSourceDirectory := target.value / "scalatex", 169 | git.remoteRepo := "git@github.com:lihaoyi/scalatex.git", 170 | libraryDependencies := libraryDependencies.value.filter(_.name == "scalatex-site"), 171 | (unmanagedSources in Compile) += baseDirectory.value/".."/"project"/"Constants.scala", 172 | noPublish 173 | ) 174 | .dependsOn( 175 | site 176 | ) 177 | .enablePlugins(GhpagesPlugin) 178 | -------------------------------------------------------------------------------- /project/Constants.scala: -------------------------------------------------------------------------------- 1 | package scalatex 2 | object Constants{ 3 | val version = "0.4.6" 4 | val scalaTags = "0.9.1" 5 | val scala212 = "2.12.10" 6 | val scala213 = "2.13.2" 7 | val circe = "0.13.0" 8 | val uTest = "0.7.3" 9 | 10 | val organization = "org.openmole" 11 | } 12 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.7 2 | -------------------------------------------------------------------------------- /project/build.sbt: -------------------------------------------------------------------------------- 1 | unmanagedSources in Compile ++= { 2 | val root = baseDirectory.value.getParentFile 3 | val exclude = Map( 4 | "0.13" -> "1.0", 5 | "1.0" -> "0.13" 6 | ).apply(sbtBinaryVersion.value) 7 | (root / "scalatexSbtPlugin") 8 | .descendantsExcept("*.scala", s"*sbt-$exclude*") 9 | .get 10 | } 11 | 12 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") 13 | 14 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.1.0") 15 | 16 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.2") 17 | 18 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.9.3") 19 | 20 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.12") 21 | 22 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8.1") 23 | 24 | 25 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Scalatex [![Build Status](https://travis-ci.org/lihaoyi/Scalatex.svg?branch=master)](https://travis-ci.org/lihaoyi/Scalatex) [![Join the chat at https://gitter.im/lihaoyi/Scalatex](https://badges.gitter.im/lihaoyi/Scalatex.svg)](https://gitter.im/lihaoyi/Scalatex?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | [Documentation](http://lihaoyi.github.io/Scalatex) 3 | 4 | -------------------------------------------------------------------------------- /readme/Readme.scalatex: -------------------------------------------------------------------------------- 1 | @import Main._ 2 | @import scalatex.site._ 3 | 4 | 5 | @def exampleWrapper(f: Frag*) = Seq( 6 | hr, 7 | div( 8 | opacity:="0.6", 9 | fontStyle.oblique, 10 | f 11 | ), 12 | hr 13 | ) 14 | @def pairs(frags: Frag*) = div(frags, div(clear:="both")) 15 | @def half(frags: Frag*) = div(frags, width:="50%", float.left) 16 | @def exampleRef(start: String, classes: String) = { 17 | val path = wd/'api/'src/'test/'scala/'scalatex/"ExampleTests.scala" 18 | val tq = "\"\"\"" 19 | val classList = classes.split(" ") 20 | val chunks = for(i <- 0 until classList.length) yield half{ 21 | hl.ref(path, Seq("'" + start, tq) ++ Seq.fill(i*2)(tq) ++ Seq(""), Seq(tq), classList(i)) 22 | } 23 | pairs(chunks) 24 | } 25 | @a( 26 | href:="https://github.com/lihaoyi/scalatex", 27 | position.absolute, 28 | top:=0,right:=0,border:=0, 29 | img( 30 | src:="https://camo.githubusercontent.com/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67", 31 | alt:="Fork me on GitHub" 32 | ) 33 | ) 34 | @sect("Scalateχ ", scalatex.Constants.version) 35 | @exampleRef("helloWorld", "scala html") 36 | 37 | @p 38 | @lnk("Scalatex", "https://github.com/lihaoyi/Scalatex") is a language for generating rich HTML documents in Scala. It lets you DRY up your documents the same way you DRY your code. Unlike most templating languages, Scalatex is a thin shim on top of the Scala programming language. This means that some things which require built-in support or are otherwise impossible in other document/static-site-generators are completely trivial with Scalatex, for example: 39 | @ul 40 | @li 41 | Extracting the structure of the document as a @sect.ref{Section}, which you can use to generate a navigation bar or @sect.ref{Generating a Table of Contents} 42 | @li 43 | Enforcing that @sect.ref("Section", "all internal links point to a valid document header"), and @sect.ref("lnk", "all external links point to a valid URL") 44 | @li 45 | @sect.ref("Highlighter", "Fetching snippets of text from files on disk") to e.g. use unit-test-code as examples in the docs 46 | @li 47 | Easily defining @sect.ref{Custom Tags} to encapsulate repetitive fragments of your document, e.g. an image-with-caption or code-snippet-with-link. 48 | 49 | @p 50 | All these things are trivial and elegant to perform with Scalatex, and difficult or messy to do with other static-site-generators. Although Scalatex generates HTML, you can easily build up whatever tags best describe what @i{you} want to do, and use those to build your document, rather than relying on the ad-hoc and messy set provided by browsers. 51 | 52 | @p 53 | Scalatex lets you write your HTML in a hierarchical structure which then gets compiled to HTML. In Scalatex, everything is part of the output document, except for tokens marked with an @hl.scala{@@} sign. These correspond to HTML tags (provided by @lnk("Scalatags", "https://github.com/lihaoyi/scalatags")), values you want to interpolate, control flow statements, function calls (or definitions!), basically anything that isn't plain text. 54 | @p 55 | Scalatex is currently used to generate its own readme (@lnk("here", "https://github.com/lihaoyi/scalatex/blob/master/readme/Readme.scalatex")) as well as the e-book @lnk("Hands-on Scala.js", "http://lihaoyi.github.io/hands-on-scala-js"). It is only published for Scala 2.11.x for the time being. 56 | 57 | @sect{Getting Started} 58 | @p 59 | To get started with Scalatex, add the following to your @code{project/build.sbt}: 60 | 61 | @hl.scala 62 | addSbtPlugin("org.openmole" % "scalatex-sbt-plugin" % "@scalatex.Constants.version") 63 | 64 | @p 65 | And the following to your project in your @code{build.sbt}: 66 | 67 | @hl.scala 68 | scalatex.SbtPlugin.projectSettings 69 | 70 | scalaVersion := "2.11.4" 71 | 72 | @p 73 | To begin with, let's create a file in @code{src/main/scalatex/Hello.scalatex} 74 | 75 | @hl.ref(wd/'site/'src/'test/'scalatex/'scalatex/'site/"Hello.scalatex") 76 | 77 | @p 78 | Next, you can simply call 79 | 80 | @hl.ref(wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", Seq("Hello().render"), Seq(""), "scala") 81 | 82 | @p 83 | And it'll print out 84 | 85 | @hl.ref(wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", Seq("
"), Seq("\"\"\""), "html") 86 | 87 | 88 | @p 89 | There we have it, your first Scalatex document! You can put this on gh-pages, use it on a website, or where-ever you want. 90 | 91 | @sect{What Scalatex Does} 92 | @p 93 | Scalatex converts every @code{.scalatex} file in its source folder into a corresponding Scala @code{object}. These objects have an @hl.scala{apply} method which returns a Scalatags @hl.scala{Frag}, which you can then call @hl.scala{.render} on to give you a HTML string. You can also do other things with the Scalatags @hl.scala{Frag}, but to learn more about it and Scalatags in general take a look at the Scalatags documentation. 94 | 95 | @p 96 | Inside each Scalatex file, @hl.scala{@@}-escaped things correspond to Scala keywords or names that are currently in scope. Apart from keywords, only @hl.scala{scalatags.Text.all._} is imported by default. This brings in all the HTML tags that we used above to build those HTML fragments. However, you can also @hl.scala{@@import} whatever other names you want, or refer to them fully qualified. 97 | 98 | @sect{Exploring Scalatex} 99 | @exampleRef("variousSyntaxes", "scala html") 100 | 101 | @p 102 | Superficially, Scalatex does a small number of translations to convert the @code{.scalatex} document into Scala code: 103 | 104 | @ul 105 | @li 106 | @hl.scala{@@}-escapes followed by indentation (like @hl.scala{@@div} above) are passed all the subsequently-indented lines as children. 107 | @li 108 | @hl.scala{@@}-escapes followed by curly braces (like @hl.scala{@@h2} above) are passed everything inside the curly braces as children 109 | @li 110 | @hl.scala{@@}-escapes followed by parentheses (like @hl.scala{@@h1} above) are passed the contents of the parentheses as a Scala expression. 111 | @li 112 | Everything outside of a set of parentheses, that isn't an @hl.scala{@@}-escape, is treated as a string. 113 | 114 | @p 115 | This accounts for the variation in syntax that you see above. In general, you almost always want to use indentation-based blocks to delimit large sections of the document, only falling back to curly braces for one-line or one-word tags like @hl.scala{@@h2} or @hl.scala{@@b} above. 116 | 117 | @sect{Loops} 118 | @exampleRef("loops", "scala scala html") 119 | 120 | @p 121 | Scalatex supports for-loops, as shown above. Again, everything inside the parentheses are an arbitrary Scala expression. Here we can see it binding the @hl.scala{x} value, which is then used in the body of the loop as @hl.scala{@@x} to splice it into the document. 122 | @p 123 | In general, there are always two contexts to keep track of: 124 | 125 | @ul 126 | @li 127 | Scalatex: strings are raw @code{text}, but variables are escaped as @hl.scala{@@x} 128 | @li 129 | Scala: strings are enclosed e.g. @code{"text"}, but variables are raw @hl.scala{x} 130 | 131 | @p 132 | The value of strings or variables is completely identical in both contexts; it's only the syntax that differs. 133 | 134 | @sect{Conditionals} 135 | @exampleRef("conditionals", "scala html") 136 | 137 | @p 138 | Scalatex supports @hl.scala{if}-@hl.scala{else} statements, that behave as you'd expect. Here we're using one in conjunction with a loop to alternate the formatting of different items in a list. 139 | 140 | @sect{Functions} 141 | @exampleRef("functions", "scala html") 142 | 143 | @p 144 | Apart from splicing values into the document, you can also call functions, such as @hl.scala{math.sqrt} here. 145 | 146 | @sect{Interpolations} 147 | @exampleRef("interpolation", "scala html") 148 | 149 | @p 150 | You can also splice the result of arbitrary chunks of code within a Scalatex document. Using parens @hl.scala{()} lets you splice a single expression, whereas using curlies @hl.scala{{}} lets you splice a block which can contain multiple statements. 151 | 152 | @p 153 | Blocks can span many lines: 154 | 155 | @exampleRef("multiline", "scala html") 156 | 157 | @sect{External Definitions} 158 | @p 159 | You can use imports to bring things into scope, so you don't need to keep referring to them by their full name: 160 | 161 | @exampleRef("imports", "scala html") 162 | @p 163 | Since you can splice the value of any Scala expressions, of course you can splice the values that you defined yourself somewhere else: 164 | 165 | @hl.ref(wd/'api/'src/'test/'scala/'scalatex/"ExampleTests.scala", "object Stuff", "'externalDefinitions", "scala") 166 | 167 | @exampleRef("externalDefinitions2", "scala html") 168 | 169 | @sect{Internal Definitions} 170 | @p 171 | You can also define things anywhere @i{inside} your Scalatex document. These definitions are only visible within the same document, and are scoped to any blocks they're defined within: 172 | 173 | @exampleRef("internalDefinitions", "scala html") 174 | @p 175 | This is very useful for defining constants or functions which aren't needed anywhere else, but can help reduce repetition within the current document 176 | 177 | 178 | @sect{Scalatex Site} 179 | @p 180 | The core of Scalatex is a very small, superficial translation from the @hl.scala{.scalatex} syntax to a @lnk("Scalatags", "https://github.com/lihaoyi/scalatags") fragment, letting you spit out HTML strings. Nevertheless, there are other concerns common to most (all?) documents, and Scalatags provides a number of common utilities that can quickly turn your structured document into a pretty, browse-able web page. 181 | 182 | @sect{Quick Start} 183 | @p 184 | If you want to get started quickly without fuss, given you've already added the SBT plugin to your @code{build.sbt} file, you should use @hl.scala{scalatex.ScalatexReadme} to get off the ground quickly: 185 | 186 | @hl.ref(wd/"build.sbt", "scalatex.ScalatexReadme", ".settings") 187 | 188 | @p 189 | This example usage would quickly create a project in the @code{readme/} folder, that you can immediately work with. Simple place a @code{Readme.scalatex} file in @code{readme/} and you can start writing Scalatex that will get turned into your documentation page. This default setup sets up all the commonly used functionality into the @hl.scala{Main} object, so make sure you start off with 190 | 191 | @hl.scala(""" 192 | @import Main._ 193 | """) 194 | @p 195 | to get access to functionality such as: 196 | 197 | @ul 198 | @li 199 | @sect.ref("Section", "sect"): a mechanism for defining sections in your document that will automatically get the correct-sized heading, and turn up in the table of contents 200 | @li 201 | @sect.ref("Highlighter", "hl"): a way to highlight strings in various languages or grab snippets from the filesystem to highlight 202 | 203 | @p 204 | When all is said and done, run: 205 | 206 | @hl.scala 207 | readme/run 208 | 209 | @p 210 | To generate the Scalatex site inside the @code{target/site} folder. 211 | 212 | 213 | @hr 214 | 215 | @p 216 | The changes are, this will create a reasonable looking document with an interactive navigation bar, nice looking headers, etc.. If you wish to really customize Scalatex, look at the following sections to see how these various components of a Scalatex site actually work. 217 | 218 | @sect{Section} 219 | @p 220 | One concern common to almost all documents is the idea of a section hierarchy: a tree of document sections where each section has a header and some contents, potentially containing more (smaller) sections. Scalatex provides the @hl.scala{scalatex.site.Section} helper that lets you do this easily: 221 | 222 | @pairs 223 | @half 224 | @hl.ref(wd/'readme/'section/"Example1.scalatex", "@sect") 225 | @half 226 | @exampleWrapper 227 | @section.Example1() 228 | 229 | @p 230 | As you can see, all the indented sections are automatically set to smaller headings. This is of course doable manually with @hl.scala{@@h1} @hl.scala{@@h2} @hl.scala{@@h3} tags etc., but then you have to manually keep track of how deep something is. With @hl.scala{Section}, this is kept track of for you. 231 | 232 | @p 233 | You can override the default rendering of @hl.scala{Section}s to e.g. use different headers: 234 | 235 | @pairs 236 | @half 237 | @hl.ref(wd/'readme/'section/"Example2.scalatex", Nil, "@sect") 238 | @half 239 | @exampleWrapper 240 | @section.Example2() 241 | 242 | @p 243 | Above, you see it using bold, underlines and strike-throughs to delimit the boundaries between headers. You can also override things in more detail using the @hl.scala{Section.Header} class instead of simple tags. 244 | 245 | @p 246 | You can use @hl.scala{@@sect.ref} to link to another part of the document: 247 | 248 | @pairs 249 | @half 250 | @hl.ref(wd/'readme/'section/"Example3.scalatex", "@sect") 251 | @half 252 | @exampleWrapper 253 | @section.Example3() 254 | 255 | @p 256 | Any links created with @hl.scala{@@sect.ref} are stored in an @hl.scala{@@sect.usedRefs} property as a list. We also expose a @hl.scala{@@sect.structure} property which allows you to inspect the tree of sections. If any of the sections referred to by @hl.scala{sect.ref} are not found, site-generation will fail with a helpful error telling you which one so you can fix it. 257 | 258 | @p 259 | Here we're just stringifying the @hl.scala{Tree} structure and placing it as text on the page, but you can easily recurse over the @hl.scala{Tree} structure and use it to e.g. build a table of contents, or a navigation bar. 260 | 261 | @sect{Highlighter} 262 | @p 263 | Scalatex Site provides a @hl.scala{Highlighter} class, which lets you easily highlight snippets of code pulled from files on disk. First, you need to instantiate a @hl.scala{Highlighter} object: 264 | 265 | @hl.ref(wd/'readme/'highlighter/"hl.scala") 266 | 267 | @p 268 | Once you have done so, you can highlight individual code snippets: 269 | @pairs 270 | @half 271 | @hl.ref(wd/'readme/'highlighter/"Example5.scalatex") 272 | @half 273 | @exampleWrapper 274 | @highlighter.Example5() 275 | 276 | @p 277 | Or you can use it to reference files in your code base in whole; e.g. here is Scalatex's plugins file: 278 | 279 | @pairs 280 | @half 281 | @hl.ref(wd/'readme/'highlighter/"Example1.scalatex") 282 | @half 283 | @exampleWrapper 284 | @highlighter.Example1() 285 | 286 | @p 287 | Note here we are using @lnk("Ammonite", "https://github.com/lihaoyi/Ammonite") to construct a path to pass to @hl.scala{@@hl.ref}; this path takes a working directory which you'll need to define yourself (here imported from @hl.scala{Main}) via @hl.scala{val wd = ammonite.ops.cwd}. 288 | 289 | @p 290 | Apart than referencing the whole file, you can also grab text from the line containing a key-string to the line containing another; here is parts of Scalatex's build file, from @hl.scala{lazy val api} to the next @hl.scala{lazy val}: 291 | 292 | @pairs 293 | @half 294 | @hl.ref(wd/'readme/'highlighter/"Example2.scalatex") 295 | @half 296 | @exampleWrapper 297 | @highlighter.Example2() 298 | 299 | @p 300 | Using multiple key-strings to navigate to a particular start- or end- line; here is the body of the unit test @hl.scala{scalatex.BasicTests.parenArgumentLists.attributes}, only from the @hl.scala{check} to the @hl.scala("}") closing that test. 301 | 302 | @pairs 303 | @half 304 | @hl.ref(wd/'readme/'highlighter/"Example3.scalatex") 305 | @half 306 | @exampleWrapper 307 | @highlighter.Example3() 308 | 309 | @p 310 | Note the spaces before the @hl.scala("}"), which ensures that it doesn't grab the earlier @hl.scala("}") in the string-literal which is part of the test body. 311 | @p 312 | Normally, the language used to highlight is inferred from the @hl.scala{suffixMappings} you defined when instantiating the @hl.scala{Highlighter}. You can override it manually by passing it in: 313 | 314 | @pairs 315 | @half 316 | @hl.ref(wd/'readme/'highlighter/"Example6.scalatex") 317 | @half 318 | @exampleWrapper 319 | @highlighter.Example6() 320 | @p 321 | As you can see, this is especially useful from grabbing examples from unit tests. You can encapsulate the boilerplate in a function, and easily grab both the code and the expected output from the unit tests highlighted in their own particular way to include in the document. 322 | 323 | @p 324 | Lastly, you can define a @hl.scala{pathMappings} property, which is a sequence of prefixes which the highlighter tries to match against the files you're referencing. If any of them match, a link is generated (in the top right corner of the code snippet) containing the mapped path prefix combined with the remainder of the file path. Here's one that links everything to github: 325 | 326 | @pairs 327 | @half 328 | @hl.ref(wd/'readme/'highlighter/"Example4.scalatex") 329 | @half 330 | @exampleWrapper 331 | @highlighter.Example4() 332 | 333 | 334 | @p 335 | You can customize the link to link to other places (bitbucket, local files, etc.) by changing the mapped URL. You can also pass in multiple @hl.scala{pathMappings} have links to files in different folders map to different base URLs, e.g. if you are referencing files in sub-repositories that have different public code 336 | 337 | @hr 338 | @p 339 | In general, @hl.scala{Highlighter.ref} is very helpful to quickly grab snippets of code from your unit tests, examples, or other places to include in your document. Using @hl.scala{Highlighter.ref}, you should never need to copy & paste code from unit tests to your documentation: simply refer to it directly, and you are guaranteed that your documentation will always be compile-able and in sync with what your code actually does in its unit tests. 340 | @sect{lnk} 341 | @p 342 | Scalatex provides the @hl.scala{lnk} function. This can be called with a single URL: 343 | 344 | @hl.scala 345 | @@lnk("http://www.google.com") 346 | 347 | @p 348 | Or with a custom name 349 | 350 | @hl.scala 351 | @@lnk("Google", "http://www.google.com") 352 | @p 353 | And generates a link to that URL. @hl.scala{@@lnk} also keeps track of all the links you create, and you can validate them later by running @hl.scala{sbt "readme/run --validate"} on your Scalatex readme project. This will reach out to every link you use and fail with an error if any of them are broken. 354 | 355 | @sect{Site} 356 | @p 357 | Finally, if all you want is to produce a working, pretty website without having to set up CSS and hosting and all that yourself, Scalatex provides a @hl.scala{Site} trait which lets you do this easily: 358 | 359 | @hl.ref(wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", Seq("'Site", "'simple", "val site"), "def check") 360 | 361 | @p 362 | This will result in a single HTML file @code{index.html} in the @code{site/target/output/} folder, as well as the JavaScript for @lnk("Highlight.js", "https://highlightjs.org/") and CSS for @lnk("Font Awesome", "http://fortawesome.github.io/Font-Awesome/") and @lnk("Less CSS", "http://purecss.io/") bundled together into @code{scripts.js} and @code{styles.css}. From there, you can simply copy the whole folder and serve it on whatever static file hosting (Github Pages, S3, etc.). 363 | 364 | @p 365 | Although @hl.scala{Site} provides reasonable defaults for all these things they are entirely customizable, and you just need to override various definitions when instantiating the @hl.scala{Site}. For example, here's a slightly more complex @hl.scala{Site} that generates multiple HTML pages (@code{page1.html} and @code{page2.html}) and generates them with custom names for the CSS and JavaScript bundles. 366 | 367 | @hl.ref(wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", Seq("'Site", "'overriding", "val site"), "val page1") 368 | 369 | @p 370 | Similarly, you can choose to supplement the default settings with your own customization. The following example 371 | specifies a different title for each of the two pages. 372 | 373 | @hl.ref(wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", Seq("'CustomHead", "'differentTitles", "val site"), "val page1") 374 | 375 | @p 376 | There are several other things you to can override, for a full list, look at the source code of @hl.scala{Site} to see what's available. 377 | 378 | @sect{GitHub pages} 379 | @p 380 | To publish a Scalateχ site to GitHub pages, it's possible to use 381 | @lnk("sbt-ghpages", "https://github.com/sbt/sbt-ghpages"). 382 | 383 | @hl.scala 384 | // project/plugins.sbt 385 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.0") 386 | // build.sbt 387 | lazy val readme = scalatex 388 | .ScalatexReadme(/* ... */) 389 | .settings( 390 | // ... 391 | siteSourceDirectory := target.value / "scalatex", 392 | git.remoteRepo := "git@@github.com:org/repo.git" 393 | ) 394 | .enablePlugins(GhpagesPlugin) 395 | Once setup, publish your site with @code{sbt readme/ghpagesPushSite}. 396 | 397 | @sect{Advanced Techniques} 398 | @p 399 | This section goes through a number of things which are not part of Scalatex by default, but are easily doable by extending Scalatex with snippets of Scala code. In general, all of these are probably sufficiently ad-hoc that the exact implementation would vary from project to project and so a built-in implementation wouldn't make sense. 400 | @sect{Custom Tags} 401 | @p 402 | Since tags are just functions, it is easy to define custom tags just by defining your own function that returns a Scalatags fragment. For example, auto-linking URL-looking-text isn't built in as part of Scalatex, but here's a custom tag that adds the capability: 403 | 404 | 405 | @pairs 406 | @half 407 | @hl.ref(wd/'readme/'advanced/"CustomTags1.scalatex") 408 | @half 409 | @exampleWrapper 410 | @advanced.CustomTags1() 411 | 412 | 413 | @p 414 | Again, if you are using the @hl.scala{@@def} in multiple files you can define it in Scala code and import it, but if you're only using it in one @code{.scalatex} file you can simply define it locally. 415 | 416 | @p 417 | Note how we check for the @code{://} inside the URL and default it to @code{http://} if it's missing; this is just plain-old-Scala, since the tag is just a Scala function. 418 | 419 | 420 | @sect{Generating a Table of Contents} 421 | @p 422 | Generating a TOC has also traditionally been a pain point for documents. There are two common scenarios: 423 | 424 | @ul 425 | @li 426 | The TOC is kept and maintained manually, separate from the document. This gives you full flexibility to customize it, at the cost of it easily falling out of date if the document changes. 427 | @li 428 | The TOC is auto-generated from the document with a tool that makes it difficult to customize. This often results in a TOC with is hard to customize, which you can only do using some clunky configuration language. 429 | @p 430 | With Scalatex, generating a TOC from the document is both easy and customizable. Here is a trivial example: 431 | 432 | @pairs 433 | @half 434 | @hl.ref(wd/'readme/'advanced/"TableOfContents.scalatex") 435 | @half 436 | @exampleWrapper 437 | @advanced.TableOfContents() 438 | 439 | @p 440 | As you can see, one short recursive function and is all that is necessary to transform the captured structure into a @hl.scala{ul} tree, with links to all the section headers! If you wish you can trivially modify this function to e.g. generate different HTML/CSS, only show top-level or top-and-second-level headers, or all manner of other things. 441 | 442 | @p 443 | Note the use of the @hl.scala{sect.munge} function. This is the function that converts the headings (with their spaces and other special characters) into a HTML element ID that can be used for the anchor link. By default, it simply removes spaces, but you can override it in the @hl.scala{Highlighter} object if you want some custom behavior. 444 | 445 | @sect{Why Scalatex} 446 | @p 447 | Scalatex requires that you learn a new syntax for doing things, which is incompatible with @lnk("Markdown", "http://en.wikipedia.org/wiki/Markdown"), @lnk("Textile", "http://en.wikipedia.org/wiki/Textile_(markup_language)"), and any other markup language you previously learned. Apart from that, Scalatex itself was the culmination of quite a lot of work to implement. Why did I do this? 448 | @p 449 | In general, Scalatex aims to make eliminating repetition and C&P sufficiently easy and elegant. Most template processors (with the exception of Twirl) start from text, and apply a series of transformations that turn the document into the HTML that you want. This ends up implementing a custom programming language with a custom syntax right inside your templates, often with 450 | 451 | @ul 452 | @li 453 | Unclear semantics and interactions 454 | @li 455 | A buggy parser 456 | @li 457 | No spec 458 | @li 459 | Lots of space for XSS vulnerabilities 460 | 461 | @p 462 | This is not great, but it always happens given a sufficiently complex document. For example, here's a sample of "real world" markdown taken from the Scala.js website, the render a relatively simple page: 463 | 464 | @pre 465 | @code 466 | --- 467 | layout: page 468 | title: Passing basic types to JavaScript 469 | --- 470 | {% include JB/setup %} 471 | 472 | ## Problem 473 | 474 | You need to pass simple types (String, Int, Boolean) to 475 | JavaScript functions. 476 | 477 | ## Solution 478 | 479 | Let us illustrate the case by writing some functions on the host-page. 480 | 481 | Edit `index-dev.html` and add the following function: 482 | 483 | {% highlight scala %} 484 | function happyNew(name, year) { 485 | alert("Hey " + name + ", happy new " + year + "!"); 486 | } 487 | {% endhighlight %} 488 | 489 | As we saw in the recipe of [accessing the global scope][1], 490 | we need to make the following import: 491 | 492 | {% highlight scala %} 493 | import scala.scalajs.js 494 | import js.Dynamic.{ global => g } 495 | {% endhighlight %} 496 | 497 | Now on the Scala side you can call it like this: 498 | 499 | {% highlight scala %} 500 | def main(): Unit = { 501 | g.happyNew("Martin", 2014) 502 | } 503 | {% endhighlight %} 504 | 505 | Which will result in the values being used and the corresponding 506 | alert being shown. 507 | 508 | [1]: ./accessing-global-scope.html 509 | @p 510 | Note how we have: 511 | @ul 512 | @li 513 | @lnk("YAML", "http://en.wikipedia.org/wiki/YAML") mixed in at the top of the file, ad-hoc separated from the rest via @code{---}s. 514 | @li 515 | A mix of @lnk("Markdown", "http://commonmark.org") @code{##}s and @code{``}s and @lnk("Jekyll", "http://en.wikipedia.org/wiki/Jekyll_(software)") @code("{% %}")s to actually mark things up 516 | @li 517 | Custom imports at the top of the page @code("{% include JB/setup %}") 518 | @p 519 | This is a markdown document, but the same fate ends up befalling any complex document written in any existing document generation system: you end up with a chimera of 3 different languages spliced together using regexes. Scalatex fixes this not by making custom-logic unnecessary, but by making custom-logic so @sect.ref("Custom Tags", "easy and standardized") such that you never need to fall back to hacky regex manipulation. 520 | @p 521 | Furthermore, by leveraging the Scala compiler, Scalatex also provides many things that other markup languages do note: 522 | @ul 523 | @li 524 | @b{Precise error messages}: if you make a typo in a tag name, you get a compilation error telling you exactly where it went wrong 525 | @li 526 | @b{Type-safety}: this is what turns those typos into compile errors (instead of runtime mis-formats) in the first place 527 | @li 528 | @b{Incremental compilation}: Only recompile what you need to, so in a large (@lnk("100s of pages", "http://lihaoyi.github.io/hands-on-scala-js/")) document, you don't end up recompiling the world every change. 529 | 530 | @p 531 | These are things that virtually no markup generators provide, but Scalatex does. Here are a few more detailed comparisons between Scalatex and alternatives. 532 | 533 | @sect{Why Not: Markdown} 534 | @p 535 | @lnk("Markdown", "http://en.wikipedia.org/wiki/Markdown") is a lightweight format for writing rich text that has gained popularity recently. It has advantages of being extremely readable in plain-text: Markdown documents are formatted often exactly as someone would format emails or chat. 536 | @p 537 | Markdown has the advantages of being: 538 | 539 | @ul 540 | @li 541 | @b{Much more lightweight than Scalatex}: @code{`hello`} rather than @code{@@code{hello}}, @code{[link](url)} rather than @code{@@lnk("link", "url")} 542 | @li 543 | @b{Much more popularly supported}: @lnk("Github", "https://help.github.com/articles/github-flavored-markdown/"), @lnk("Stackoverflow", "http://stackoverflow.com/editing-help") and many others already use semi-compatible implementations of it 544 | 545 | @p 546 | However, it also has some downsides: 547 | @ul 548 | @li 549 | @b{Completely non-customizable by default}: in Markdown, you have @code{`code`} for specifying code. But what if you want to specify the language which you want your code to be highlighted? Impossible, without special extensions. Similarly, you have @code{[link](url)} for specifying links. But what if you want to treat external links differently from internal links? Again, impossible. With Scalatex, you can easily define different tags for each one: @hl.scala("@hl.scala{code}") to highlight Scala code, @hl.scala("@hl.javascript{code}") for JavaScript, @hl.scala("@hl.html{code}") to highlight HTML. Similarly, you can easily define @sect.ref{Custom Tags} that distinguish internal links from external links, giving them different CSS, markup of assertions. 550 | @li 551 | @b{Messy, ad-hoc extensions}: @lnk("Python Markdown", "http://pythonhosted.org/Markdown/extensions/") provides a list of extensions. @lnk("CommonMark.org", "http://commonmark.org") does too. How do these extensions interact with other extensions? How do you implement your own? The answer to the first question is "Who knows?" and to the second "Regexes". Naturally, this makes people hesitant to implement extensions: they'd rather C&P markup than deal with crazy regex transforms. Is highlight code done using @code{```scala} or @code("{% highlight scala %}")? With Scalatex, defining @sect.ref{Custom Tags} is so easy that it's the natural thing to do, so you never need to worry about weird interactions between weird custom syntaxes. 552 | @li 553 | @b{Unclear semantics}. For example, for rendering a link with parentheses in the URL, there are a @lnk("dozen different work-arounds", "http://meta.stackexchange.com/questions/13501/links-to-urls-containing-parentheses") that make it work in increasingly roundabout ways, all stemming from the fact that Markdown doesn't have a good, standard way of expressing escape-characters. With Scalatex, the semantics are much more regular: you can put anything in a @hl.scala{@@("string")} to escape it, and always fall back to Scala-strings with Scala-string-escapes. And their semantics behave exactly as you'd expect. 554 | @li 555 | @b{Unsafe}: What happens if you fat-finger some special characters in Markdown? You get another, perfectly valid markdown document, that you did not want. If you fat-finger a tag in Scalatex, you get a compilation error telling you where you made a mistake. 556 | @p 557 | In general, Markdown is far more concise than Scalatex, and probably better for writing small documents like comments or chat boxes. For anything larger, you probably do want to define larger, re-usable components to deal with, rather than working directly with the fixed set of primitives markdown provides. Instead of working with fenced-code-snippets, links, and headers, you're working with references to your unit test code, or document sections that automatically provide the correct sized headers. 558 | 559 | @p 560 | This makes large-scale documents much more pleasant, to the extent that you can perform large-scale refactorings of your Scalatex document without worrying about broken intra-document links or a stale table-of-contents. 561 | 562 | @sect{Why Not: Twirl} 563 | @p 564 | @lnk("Twirl", "https://github.com/playframework/twirl#twirl") is the templating engine used by the @lnk("Play framework", "https://www.playframework.com/documentation/2.3.x/ScalaTemplates"). It provides a Scala-based syntax (and implementation!) very similar to Scalatex. 565 | @p 566 | In general, Scalatex is heavily inspired by Twirl: the original implementation started off as a fork of the Twirl source code, and though it has now diverged, the similarities are obvious. Scalatex has attempted to clean up a lot of unnecessary or ad-hoc cruft in the Twirl language, in an attempt to make something cleaner and nicer to use. For example: 567 | 568 | @ul 569 | @li 570 | @b{Scalatex has dropped Twirl's XML syntax}: using tags like @hl.scala{
} are redundant when you can use tags like @hl.scala("@div"), and so the latter is now the only way to do things. 571 | @li 572 | @b{Scalatex has regularized the Twirl grammar substantially}: Twirl only allows function definitions to occur in templates at the top-level, which Scalatex allows @hl.scala{object}s, @hl.scala{class}es, @hl.scala{def}s, @hl.scala{val}s anywhere with your document with proper lexical scoping. This makes working with large documents much easier, as you aren't restricted by the ad-hoc "only-top-level" constraint when trying to remove duplication within your document. 573 | @li 574 | @b{Scalatex has made indentation significant}, thus greatly reducing the need for curly brackets in large documents. 575 | @li 576 | @b{Scalatex has a greatly simplified implementation} that gives you incremental compilation, proper error messages, etc. all for free without the crazy contortions that the Play team have gone through to get them working with their source-generation model. 577 | @p 578 | Scalatex is still missing a few features: 579 | @ul 580 | @li 581 | Being able to parametrize an entire template as a function 582 | @li 583 | Being able to define @hl.scala{def}s using Scalatex syntax; Scalatex @hl.scala{def}s have to be defined using Scala syntax 584 | @p 585 | In general, Scalatex aims to be much more regular and lighter weight than Twirl. You probably wouldn't want to use Twirl to write a book due to its excess of curlies and XML, whereas with Scalatex you can (and @lnk("I have", "http://lihaoyi.github.io/hands-on-scala-js/#Hands-onScala.js")). At the same time, Scalatex keeps the best part of Twirl - the abilities to eliminate repetition using plain Scala - and extends to to work more regularly. 586 | @p 587 | The limitations are not fundamental, but purely a matter of how Scalatex is being used now: static documents, rather than dynamic pages. If there's interest removing them would not be hard. 588 | 589 | @sect{Changelog} 590 | @sect{0.3.11} 591 | @ul 592 | @li 593 | sbt 1.0 support for scalatex-sbt-plugin. 594 | @sect{0.3.9} 595 | @ul 596 | @li 597 | Upgrade fastparse dependency to 0.4.3. 598 | @li 599 | Note. This release was signed with @lnk("@olafurpg", "https://github.com/olafurpg")'s 600 | @lnk("PGP key", "https://gist.github.com/olafurpg/078d1f2a58490ccaeebd7d3f6cde00fc"). 601 | 602 | @sect{0.3.7} 603 | @ul 604 | @li 605 | Update to Scala 2.12.x 606 | 607 | @sect{0.3.6} 608 | @ul 609 | @li 610 | Bump Scalatags version to 0.6.0 611 | 612 | @sect{0.3.5} 613 | @ul 614 | @li 615 | Scalatex sites now support favicons! Place @code{favicon.png} in the @code{resources/} folder of your Scalatex subproject and it should get picked up automatically. 616 | @li 617 | Scalatex sites now set the HTML page-title using the first header on the page. 618 | 619 | @sect{0.3.4} 620 | @ul 621 | @li 622 | Upgrade ScalaParse to 0.3.1 623 | @sect{0.3.3} 624 | @ul 625 | @li 626 | Fix botched release 627 | @sect{0.3.2} 628 | @ul 629 | @li 630 | Better error reporting for syntax errors; line-contents, caret and line/col number rather than just character-offset 631 | @li 632 | Trailing whitespace on a line no longer botches error messages after it 633 | @li 634 | Added the @sect.ref{lnk} function to link to external URLs, and the @code{--validate} flag passed to @code{run} that makes sure none of them are broken 635 | -------------------------------------------------------------------------------- /readme/advanced/CustomTags1.scalatex: -------------------------------------------------------------------------------- 1 | @def autoLink(url: String) = { 2 | if(url.contains("://")) 3 | a(url, href:=url) 4 | else 5 | a(url, href:=s"http://$url") 6 | } 7 | 8 | @p 9 | I always like going to either 10 | @autoLink{www.google.com} or 11 | @autoLink{https://www.facebook.com} 12 | in order to look for things. -------------------------------------------------------------------------------- /readme/advanced/TableOfContents.scalatex: -------------------------------------------------------------------------------- 1 | @import scalatex.site.Section 2 | @import scalatex.site.Tree 3 | @object sect extends Section() 4 | 5 | @sect{TOC Main Section} 6 | @p 7 | Hello World 8 | 9 | @sect{TOC Subsection A} 10 | @p 11 | I am Cow 12 | @sect{TOC Subsection B} 13 | @p 14 | Hear me moo 15 | @sect{TOC Subsection C} 16 | @sect{TOC Sub-sub-section C1} 17 | @p 18 | I weigh twice as much as you 19 | @sect{TOC Sub-sub-section C2} 20 | @p 21 | And I look good on 22 | the barbecue 23 | 24 | @hr 25 | @b{Table of Contents} 26 | @{ 27 | def rec(t: Tree[String]): Frag = { 28 | val id = 29 | "#" + sect.munge(t.value) 30 | div( 31 | a(t.value, href:=id), 32 | ul(t.children.toSeq.map( 33 | c => li(rec(c)) 34 | )) 35 | ) 36 | } 37 | sect.structure.children.toSeq.map(rec) 38 | } 39 | -------------------------------------------------------------------------------- /readme/highlighter/Example1.scalatex: -------------------------------------------------------------------------------- 1 | @import Main._ 2 | @hl.ref(wd/'project/"build.sbt") 3 | -------------------------------------------------------------------------------- /readme/highlighter/Example2.scalatex: -------------------------------------------------------------------------------- 1 | @import Main._ 2 | 3 | @hl.ref( 4 | wd/"build.sbt", 5 | start = "lazy val api", 6 | end = "lazy val" 7 | ) 8 | -------------------------------------------------------------------------------- /readme/highlighter/Example3.scalatex: -------------------------------------------------------------------------------- 1 | @import Main._ 2 | @import ammonite.ops._ 3 | @hl.ref( 4 | 'api/'src/'test/'scala/ 5 | 'scalatex/"BasicTests.scala", 6 | start = Seq( 7 | "parenArgumentLists", 8 | "attributes", "check" 9 | ), 10 | end = Seq(" }") 11 | ) 12 | -------------------------------------------------------------------------------- /readme/highlighter/Example4.scalatex: -------------------------------------------------------------------------------- 1 | @import Main._ 2 | @object hl extends scalatex.site.Highlighter{ 3 | override def pathMappings = Seq( 4 | wd -> ( 5 | "https://github.com/lihaoyi/" + 6 | "Scalatex/blob/master/" 7 | ) 8 | ) 9 | } 10 | 11 | @hl.ref( 12 | wd/'api/'src/'test/'scala/ 13 | 'scalatex/"BasicTests.scala", 14 | start = Seq( 15 | "parenArgumentLists", 16 | "attributes", "check" 17 | ), 18 | end = Seq(" }") 19 | ) 20 | -------------------------------------------------------------------------------- /readme/highlighter/Example5.scalatex: -------------------------------------------------------------------------------- 1 | @import Main._ 2 | @div 3 | @hl.xml{
Some HTML
} 4 | @div 5 | @hl.js("f = function(x){return x+1}") 6 | @div 7 | @hl.scala 8 | val f = (x) => { x + 1 } 9 | 10 | -------------------------------------------------------------------------------- /readme/highlighter/Example6.scalatex: -------------------------------------------------------------------------------- 1 | @import Main._ 2 | 3 | @def example(s: Seq[String], 4 | cls: String) = { 5 | val path = 6 | wd/'api/'src/'test/'scala/ 7 | 'scalatex/"BasicTests.scala" 8 | hl.ref( 9 | path, s, Seq("\"\"\""), cls 10 | ) 11 | } 12 | 13 | @p 14 | Compiling: 15 | 16 | @example(Seq("parenArgumentLists", "attributes", "@div"), "scala") 17 | 18 | @p 19 | Gives you: 20 | 21 | @example(Seq("parenArgumentLists", "attributes", " s"package $s") 34 | .mkString("\n") 35 | IO.write( 36 | outFile, 37 | s""" 38 | |package scalatex 39 | |$pkgName 40 | |import scalatags.Text.all._ 41 | | 42 | |object $objectName{ 43 | | def apply(): Frag = _root_.scalatex.twf("${fixPath(inFile)}") 44 | | def sourcePath = "$relativeInputFile" 45 | |} 46 | | 47 | |${IO.readLines(inFile).map("//"+_).mkString("\n")} 48 | """.stripMargin 49 | ) 50 | outFile 51 | } 52 | outputFiles 53 | } 54 | ) 55 | override val projectSettings = inConfig(Test)(mySeq) ++ inConfig(Compile)(mySeq) ++ Seq( 56 | libraryDependencies ++= Seq( 57 | "org.openmole" %% "scalatex-api" % scalatexVersion 58 | ), 59 | watchSources ++= { 60 | val compileTarget = (target in Compile).value 61 | for{ 62 | f <- (scalatexDirectory in Compile).value.get 63 | if f.relativeTo(compileTarget).isEmpty 64 | } yield f 65 | } 66 | ) 67 | // fix backslash path separator 68 | def fixPath(file:String) = file.replaceAll("\\\\","/") 69 | def fixPath(file:sbt.File):String = fixPath(file.getAbsolutePath) 70 | } 71 | object ScalatexReadme{ 72 | import SbtPlugin.fixPath 73 | /** 74 | * 75 | * @param projectId The name this readme project will take, 76 | * and the folder it will live in 77 | * @param wd The working directory of this readme project will 78 | * use as `wd` inside the code 79 | * @param source The name of the scalatex file which will be bundled into 80 | * `index.html`, without the `.scalatex` suffix 81 | * @param url Where this project lives on the internet, so `hl.ref` can 82 | * properly insert links 83 | * @param autoResources Any other CSS or JS files you want to bundle into 84 | * index.html 85 | */ 86 | def apply(projectId: String = "scalatex", 87 | wd: java.io.File, 88 | source: String, 89 | url: String, 90 | autoResources: Seq[String] = Nil) = 91 | Project(id = projectId, base = file(projectId)).settings( 92 | resourceDirectory in Compile := file(projectId) / "resources", 93 | sourceGenerators in Compile += task{ 94 | val dir = (sourceManaged in Compile).value 95 | val manualResources: Seq[String] = for{ 96 | f <- (file(projectId) / "resources" ** "*").get 97 | if f.isFile 98 | rel <- f.relativeTo(file(projectId) / "resources") 99 | } yield fixPath(rel.getPath) 100 | 101 | val generated = dir / "scalatex" / "Main.scala" 102 | 103 | val autoResourcesStrings = autoResources.map('"' + _ + '"').mkString(",") 104 | 105 | val manualResourceStrings = manualResources.map('"' + _ + '"').mkString(",") 106 | IO.write(generated, s""" 107 | package scalatex 108 | object Main extends scalatex.site.Main( 109 | url = "$url", 110 | wd = ammonite.ops.Path("${fixPath(wd)}"), 111 | output = ammonite.ops.Path("${fixPath((target in Compile).value / "scalatex")}"), 112 | extraAutoResources = Seq[String]($autoResourcesStrings).map(ammonite.ops.resource/ammonite.ops.RelPath(_)), 113 | extraManualResources = Seq[String]($manualResourceStrings).map(ammonite.ops.resource/ammonite.ops.RelPath(_)), 114 | scalatex.$source() 115 | ) 116 | """) 117 | Seq(generated) 118 | }, 119 | (SbtPlugin.scalatexDirectory in Compile) := file(projectId), 120 | libraryDependencies += "org.openmole" %% "scalatex-site" % SbtPlugin.scalatexVersion, 121 | scalaVersion := _root_.scalatex.Constants.scala212 122 | ).enablePlugins(SbtPlugin) 123 | } 124 | -------------------------------------------------------------------------------- /scrollspy/src/main/scala/scalatex/scrollspy/Controller.scala: -------------------------------------------------------------------------------- 1 | package scalatex.scrollspy 2 | 3 | import org.scalajs.dom 4 | import org.scalajs.dom.ext._ 5 | import org.scalajs.dom.html 6 | 7 | import scalajs.js 8 | import scalajs.js.annotation._ 9 | import scalatags.JsDom.all._ 10 | import scalatags.JsDom.tags2 11 | import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._ 12 | 13 | class Toggler(var open: Boolean, 14 | menu: => html.Element, 15 | body: => html.Element, 16 | all: => html.Element){ 17 | 18 | def toggle() = open = !open 19 | 20 | def apply() = { 21 | body.style.transition = "margin-left 0.2s ease-out" 22 | val width = if (open) "250px" else s"${Styles.itemHeight}px" 23 | all.style.opacity = if (open) "1.0" else "0.0" 24 | menu.style.width = width 25 | body.style.marginLeft = width 26 | } 27 | } 28 | 29 | object Controller{ 30 | lazy val styleTag = tags2.style.render 31 | dom.document.head.appendChild(styleTag) 32 | styleTag.textContent += Styles.styleSheetText 33 | 34 | def munge(name: String) = { 35 | name.replace(" ", "") 36 | } 37 | 38 | @JSExportTopLevel("scalatexScrollspyController") 39 | def main(data: js.Any) = { 40 | (new Controller(data)).init() 41 | } 42 | } 43 | 44 | class Controller(data: js.Any){ 45 | 46 | 47 | val scrollSpy = new ScrollSpy( 48 | // upickle.default.readJs[Seq[Tree[String]]](upickle.json.readJs(data)) 49 | decode[Seq[Tree[String]]](js.JSON.stringify(data)).right.get 50 | ) 51 | 52 | val list = ul( 53 | cls := "menu-item-list", 54 | margin := 0, 55 | padding := 0, 56 | flex := 10000, 57 | scrollSpy.domTrees.map(_.value.frag) 58 | ).render 59 | 60 | def updateScroll() = scrollSpy() 61 | 62 | def toggleButton(clsA: String, clsB: String, action: () => Unit, mods: Modifier*) = { 63 | val icon = i( 64 | cls := "fa " + clsA, 65 | color := "white" 66 | ).render 67 | val link = a( 68 | icon, 69 | href := "javascript:", 70 | Styles.menuLink, 71 | onclick := { (e: dom.Event) => 72 | icon.classList.toggle(clsA) 73 | icon.classList.toggle(clsB) 74 | action() 75 | }, 76 | mods 77 | ) 78 | link 79 | } 80 | 81 | val expandLink = toggleButton( 82 | "fa-caret-down", "fa-caret-up", 83 | () => scrollSpy.toggleOpen(), 84 | right := Styles.itemHeight 85 | ).render 86 | 87 | val initiallyOpen = dom.window.innerWidth > 800 88 | val toggler = new Toggler(initiallyOpen, menu, dom.document.body, all) 89 | 90 | val openLink = { 91 | val (startCls, altCls) = 92 | if (initiallyOpen) ("fa-caret-left", "fa-caret-right") 93 | else ("fa-caret-right", "fa-caret-left") 94 | 95 | toggleButton( 96 | startCls, altCls, 97 | () => {toggler.toggle(); toggler.apply()} 98 | )(right := "0px").render 99 | } 100 | 101 | def init() = { 102 | 103 | updateScroll() 104 | 105 | toggler.apply() 106 | dom.document.body.appendChild(menu) 107 | 108 | dom.window.addEventListener("scroll", (e: dom.UIEvent) => updateScroll()) 109 | } 110 | 111 | val footer = div( 112 | Styles.noteBox, 113 | a( 114 | "Published using Scalatex", 115 | href:="https://lihaoyi.github.io/Scalatex", 116 | Styles.note 117 | ) 118 | ) 119 | val all = div( 120 | display := "flex", 121 | flexDirection := "column", 122 | minHeight := "100%", 123 | transition := "opacity 0.2s ease-out", 124 | list, 125 | expandLink, 126 | footer 127 | ).render 128 | 129 | val menu: html.Element = div( 130 | Styles.menu, 131 | all, 132 | openLink 133 | ).render 134 | } -------------------------------------------------------------------------------- /scrollspy/src/main/scala/scalatex/scrollspy/ScrollSpy.scala: -------------------------------------------------------------------------------- 1 | package scalatex.scrollspy 2 | 3 | import org.scalajs.dom 4 | import org.scalajs.dom.ext._ 5 | import org.scalajs.dom.html 6 | import scalatags.JsDom.all._ 7 | 8 | case class Tree[T](value: T, children: Vector[Tree[T]]) 9 | 10 | case class MenuNode(frag: html.Element, 11 | link: html.Element, 12 | list: html.Element, 13 | header: html.Element, 14 | id: String, 15 | start: Int, 16 | end: Int) 17 | 18 | /** 19 | * High performance scalatex.scrollspy to work keep the left menu bar in sync. 20 | * Lots of sketchy imperative code in order to maximize performance. 21 | */ 22 | class ScrollSpy(structure: Seq[Tree[String]]){ 23 | 24 | lazy val domTrees = { 25 | var i = -1 26 | def recurse(t: Tree[String], depth: Int): Tree[MenuNode] = { 27 | val link = a( 28 | t.value, 29 | href:="#"+Controller.munge(t.value), 30 | cls:="menu-item", 31 | Styles.menuItem 32 | ).render 33 | val originalI = i 34 | val children = t.children.map(recurse(_, depth + 1)) 35 | 36 | val list = ul( 37 | Styles.menuList, 38 | children.map(_.value.frag) 39 | ).render 40 | 41 | val curr = li( 42 | display.block, 43 | link, 44 | list 45 | ).render 46 | 47 | i += 1 48 | 49 | Tree( 50 | MenuNode( 51 | curr, 52 | link, 53 | list, 54 | dom.document.getElementById(Controller.munge(t.value)).asInstanceOf[html.Element], 55 | Controller.munge(t.value), 56 | originalI, 57 | if (children.length > 0) children.map(_.value.end).max else originalI + 1 58 | ), 59 | children 60 | ) 61 | } 62 | 63 | val domTrees = structure.map(recurse(_, 0)) 64 | domTrees 65 | } 66 | def offset(el: html.Element): Double = { 67 | val parent = dom.document.body 68 | if (el == parent) 0 69 | else el.offsetTop + offset(el.offsetParent.asInstanceOf[html.Element]) 70 | } 71 | 72 | var open = false 73 | def toggleOpen() = { 74 | open = !open 75 | if (open){ 76 | def rec(tree: Tree[MenuNode])(f: MenuNode => Unit): Unit = { 77 | f(tree.value) 78 | tree.children.foreach(rec(_)(f)) 79 | } 80 | domTrees.map(rec(_)(setFullHeight)) 81 | }else{ 82 | start(force = true) 83 | } 84 | } 85 | 86 | def setFullHeight(mn: MenuNode) = { 87 | // height + 1 to account for border 88 | mn.list.style.maxHeight = (mn.end - mn.start + 1) * (Styles.itemHeight + 1) + "px" 89 | } 90 | 91 | def apply(): Unit = { 92 | start() 93 | } 94 | 95 | /** 96 | * Recurse over the navbar tree, opening and closing things as necessary 97 | */ 98 | private[this] def start(force: Boolean = false) = { 99 | val scrollTop = { 100 | // Fix #21: 101 | // document.documentElement.scrollTop is the correct cross-browser way to get main window scroll offset 102 | // see discussion: https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/75hVThfrJ08/YD9kSgQljFEJ 103 | // however it is broken in current Chrome: https://code.google.com/p/chromium/issues/detail?id=157855 104 | // so the document.body workaround must be used for now 105 | val docScrollTop = dom.document.documentElement.scrollTop 106 | if (docScrollTop > 0) docScrollTop else dom.document.body.scrollTop 107 | } 108 | 109 | def close(tree: Tree[MenuNode]): Unit = { 110 | if (!open) tree.value.list.style.maxHeight = "0px" 111 | else setFullHeight(tree.value) 112 | tree.value.frag.classList.remove(Styles.pathed.name) 113 | 114 | tree.children.foreach(close) 115 | tree.value.link.classList.add(Styles.closed.name) 116 | tree.value.link.classList.remove(Styles.selected.name) 117 | } 118 | def walk(tree: Tree[MenuNode]): Unit = { 119 | setFullHeight(tree.value) 120 | recChildren(tree.children) 121 | } 122 | def recChildren(children: Seq[Tree[MenuNode]]) = { 123 | val epsilon = 10 124 | 125 | for((child, idx) <- children.zipWithIndex) { 126 | if(offset(child.value.header) <= scrollTop + epsilon) { 127 | if (idx+1 >= children.length || offset(children(idx+1).value.header) > scrollTop + epsilon) { 128 | child.value.link.classList.remove(Styles.closed.name) 129 | child.value.link.classList.add(Styles.selected.name) 130 | walk(child) 131 | child.value.frag.classList.remove(Styles.pathed.name) 132 | }else { 133 | 134 | close(child) 135 | child.value.frag.classList.add(Styles.pathed.name) 136 | } 137 | }else{ 138 | child.value.frag.classList.remove(Styles.pathed.name) 139 | close(child) 140 | } 141 | } 142 | } 143 | 144 | recChildren(domTrees) 145 | for(t <- domTrees){ 146 | val cl = t.value.link.classList 147 | setFullHeight(t.value) 148 | cl.remove(Styles.closed.name) 149 | t.value.frag.classList.remove(Styles.pathed.name) 150 | cl.add(Styles.selected.name) 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /scrollspy/src/main/scala/scalatex/scrollspy/Styles.scala: -------------------------------------------------------------------------------- 1 | package scalatex.scrollspy 2 | 3 | import scalatags.JsDom.all._ 4 | import scalatags.stylesheet.{SourceClasses, StyleSheet} 5 | 6 | object Styles extends StyleSheet{ 7 | val itemHeight = 44 8 | val selectedColor = "#1f8dd6" 9 | val menuBackground = "#191818" 10 | 11 | def noteBox = cls( 12 | height := "14px", 13 | textAlign.right, 14 | bottom := 0, 15 | paddingTop := 5, 16 | paddingRight := 5, 17 | paddingBottom := 2, 18 | backgroundColor := Styles.menuBackground 19 | ) 20 | 21 | def note = cls( 22 | fontSize := "12px", 23 | color := "#555", 24 | textDecoration.none, 25 | fontStyle.italic, 26 | &hover( 27 | color := "#777" 28 | ), 29 | &active( 30 | color := "#999" 31 | ) 32 | ) 33 | def menu = cls( 34 | position.fixed, 35 | overflow.scroll, 36 | whiteSpace.nowrap, 37 | backgroundColor := Styles.menuBackground, 38 | transition := "width 0.2s ease-out", 39 | height := "100%", 40 | left := 0, 41 | top := 0 42 | ) 43 | def menuLink = cls( 44 | position.absolute, 45 | top := "0px", 46 | height := Styles.itemHeight, 47 | width := Styles.itemHeight, 48 | display := "flex", 49 | alignItems := "center", 50 | justifyContent := "center", 51 | textDecoration.none, 52 | selected.splice 53 | ) 54 | def menuItem = cls( 55 | &hover( 56 | color := "#bbb", 57 | backgroundColor := "#292828" 58 | ), 59 | &active( 60 | color := "#ddd", 61 | backgroundColor := "#393838" 62 | ), 63 | display.block, 64 | textDecoration.none, 65 | paddingLeft := 15, 66 | height := Styles.itemHeight, 67 | lineHeight := "44px", 68 | borderBottom := "1px solid #444" 69 | ) 70 | def menuList = cls( 71 | paddingLeft := "15px", 72 | margin := 0, 73 | overflow.hidden, 74 | position.relative, 75 | display.block, 76 | left := 0, 77 | top := 0, 78 | transition := "max-height 0.2s ease-out" 79 | ) 80 | 81 | def selected = cls( 82 | backgroundColor := Styles.selectedColor, 83 | &hover( 84 | backgroundColor := "#369FE2", 85 | color := "white" 86 | ), 87 | &active( 88 | backgroundColor := "#62b4e8", 89 | color := "white" 90 | ), 91 | color := "white" 92 | ) 93 | 94 | def closed = cls(color := "#999") 95 | 96 | def pathed = cls(borderLeft := "2px solid white") 97 | 98 | def exact = cls( 99 | fontStyle.italic 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /site/src/main/resources/scalatex/site/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmole/scalatex/8c90e13c6424706e1e5515fdaf5ae362b02505b1/site/src/main/resources/scalatex/site/styles.css -------------------------------------------------------------------------------- /site/src/main/scala/scalatex/site/Highlighter.scala: -------------------------------------------------------------------------------- 1 | package scalatex.site 2 | import collection.mutable 3 | import ammonite.ops.{RelPath, Path} 4 | 5 | import scalatags.Text.all._ 6 | import ammonite.ops._ 7 | /** 8 | * Lets you instantiate a Highlighter object. This can be used to reference 9 | * snippets of code from files within your project via the `.ref` method, often 10 | * used via `hl.ref` where `hl` is a previously-instantiated Highlighter. 11 | */ 12 | trait Highlighter{ hl => 13 | val languages = mutable.Set.empty[String] 14 | def webjars = resource/"META-INF"/'resources/'webjars 15 | def highlightJs = webjars/'highlightjs/"9.12.0" 16 | def highlightJsSource = webjars/"highlight.js" 17 | def style: String = "idea" 18 | 19 | case class lang(name: String){ 20 | def apply(s: Any*) = hl.highlight(s.mkString, name) 21 | } 22 | def as = lang("actionscript") 23 | def scala = lang("scala") 24 | def asciidoc = lang("asciidoc") 25 | def ahk = lang("autohotkey") 26 | def sh = lang("bash") 27 | def clj = lang("clojure") 28 | def coffee = lang("coffeescript") 29 | def ex = lang("elixir") 30 | def erl = lang("erlang") 31 | def fs = lang("fsharp") 32 | def hs = lang("haskell") 33 | def hx = lang("haxe") 34 | def js = lang("javascript") 35 | def nim = lang("nimrod") 36 | def rb = lang("ruby") 37 | def ts = lang("typescript") 38 | def vb = lang("vbnet") 39 | def xml = lang("xml") 40 | def diff = lang("diff") 41 | def autoResources = { 42 | Seq(highlightJs/"highlight.pack.min.js") ++ 43 | Seq(highlightJs/'styles/s"$style.css") ++ 44 | languages.map(x => highlightJsSource/'src/'languages/s"$x.js") 45 | } 46 | /** 47 | * A mapping of file-path-prefixes to URLs where the source 48 | * can be accessed. e.g. 49 | * 50 | * Seq( 51 | * "clones/scala-js" -> "https://github.com/scala-js/scala-js/blob/master", 52 | * "" -> "https://github.com/lihaoyi/scalatex/blob/master" 53 | * ) 54 | * 55 | * Will link any code reference from clones/scala-js to the scala-js 56 | * github repo, while all other paths will default to the scalatex 57 | * github repo. 58 | * 59 | * If a path is not covered by any of these rules, no link is rendered 60 | */ 61 | def pathMappings: Seq[(Path, String)] = Nil 62 | 63 | /** 64 | * A mapping of file name suffixes to highlight.js classes. 65 | * Usually something like: 66 | * 67 | * Map( 68 | * "scala" -> "scala", 69 | * "js" -> "javascript" 70 | * ) 71 | */ 72 | def suffixMappings: Map[String, String] = Map( 73 | "scala" -> "scala", 74 | "sbt" -> "scala", 75 | "scalatex" -> "scala", 76 | "as" -> "actionscript", 77 | "ahk" -> "autohotkey", 78 | "coffee" -> "coffeescript", 79 | "clj" -> "clojure", 80 | "cljs" -> "clojure", 81 | "sh" -> "bash", 82 | "ex" -> "elixir", 83 | "erl" -> "erlang", 84 | "fs" -> "fsharp", 85 | "hs" -> "haskell", 86 | "hx" -> "haxe", 87 | "js" -> "javascript", 88 | "nim" -> "nimrod", 89 | "rkt" -> "lisp", 90 | "scm" -> "lisp", 91 | "sch" -> "lisp", 92 | "rb" -> "ruby", 93 | "ts" -> "typescript", 94 | "vb" -> "vbnet" 95 | ) 96 | 97 | /** 98 | * Highlight a short code snippet with the specified language 99 | */ 100 | def highlight(string: String, lang: String) = { 101 | languages.add(lang) 102 | val lines = string.split("\n", -1) 103 | if (lines.length == 1){ 104 | code( 105 | cls:=lang + " " + Styles.highlightMe.name, 106 | display:="inline", 107 | padding:=0, 108 | margin:=0, 109 | lines(0) 110 | ) 111 | 112 | }else{ 113 | val minIndent = lines.filter(_.trim != "").map(_.takeWhile(_ == ' ').length).min 114 | val stripped = lines.map(_.drop(minIndent)) 115 | .dropWhile(_ == "") 116 | .mkString("\n") 117 | 118 | pre(code(cls:=lang + " " + Styles.highlightMe.name, stripped)) 119 | } 120 | } 121 | import Highlighter._ 122 | /** 123 | * Grab a snippet of code from the given filepath, and highlight it. 124 | * 125 | * @param filePath The file containing the code in question 126 | * @param start Snippets used to navigate to the start of the snippet 127 | * you want, from the beginning of the file 128 | * @param end Snippets used to navigate to the end of the snippet 129 | * you want, from the start of start of the snippet 130 | * @param className An optional css class set on the rendered snippet 131 | * to determine what language it gets highlighted as. 132 | * If not given, it defaults to the class given in 133 | * [[suffixMappings]] 134 | */ 135 | def ref[S: RefPath, V: RefPath] 136 | (filePath: ammonite.ops.BasePath, 137 | start: S = Nil, 138 | end: V = Nil, 139 | className: String = null) = { 140 | val absPath = filePath match{ 141 | case p: Path => p 142 | case p: RelPath => pwd/p 143 | } 144 | 145 | val ext = filePath.last.split('.').last 146 | val lang = Option(className) 147 | .getOrElse(suffixMappings.getOrElse(ext, ext)) 148 | 149 | 150 | val linkData = 151 | pathMappings.iterator 152 | .find{case (prefix, path) => absPath startsWith prefix} 153 | val (startLine, endLine, blob) = referenceText(absPath, start, end) 154 | val link = linkData.map{ case (prefix, url) => 155 | val hash = 156 | if (endLine == -1) "" 157 | else s"#L$startLine-L$endLine" 158 | 159 | val linkUrl = s"$url/${absPath relativeTo prefix}$hash" 160 | a( 161 | Styles.headerLink, 162 | i(cls:="fa fa-link "), 163 | position.absolute, 164 | right:="0.5em", 165 | top:="0.5em", 166 | display.block, 167 | fontSize:="24px", 168 | href:=linkUrl, 169 | target:="_blank" 170 | ) 171 | } 172 | 173 | pre( 174 | Styles.hoverContainer, 175 | code(cls:=lang + " " + Styles.highlightMe.name, blob), 176 | link 177 | ) 178 | } 179 | 180 | def referenceText[S: RefPath, V: RefPath](filepath: Path, start: S, end: V) = { 181 | val fileLines = read.lines! filepath 182 | // Start from -1 so that searching for things on the first line of the file (-1 + 1 = 0) 183 | 184 | 185 | def walk(query: Seq[String], start: Int) = { 186 | var startIndex = start 187 | for(str <- query){ 188 | startIndex = fileLines.indexWhere(_.contains(str), startIndex + 1) 189 | if (startIndex == -1) throw new RefError( 190 | s"Highlighter unable to resolve reference $str in selector $query" 191 | ) 192 | } 193 | startIndex 194 | } 195 | // But if there are no selectors, start from 0 and not -1 196 | val startQuery = implicitly[RefPath[S]].apply(start) 197 | val startIndex = if (startQuery == Nil) 0 else walk(startQuery, -1) 198 | val startIndent = fileLines(startIndex).takeWhile(_.isWhitespace).length 199 | val endQuery = implicitly[RefPath[V]].apply(end) 200 | val endIndex = if (endQuery == Nil) { 201 | val next = fileLines.drop(startIndex).takeWhile{ line => 202 | line.trim == "" || line.takeWhile(_.isWhitespace).length >= startIndent 203 | } 204 | startIndex + next.length 205 | } else { 206 | 207 | walk(endQuery, startIndex) 208 | } 209 | val margin = fileLines(startIndex).takeWhile(_.isWhitespace).length 210 | val lines = fileLines.slice(startIndex, endIndex) 211 | .map(_.drop(margin)) 212 | .reverse 213 | .dropWhile(_.trim == "") 214 | .reverse 215 | 216 | (startIndex, endIndex, lines.mkString("\n")) 217 | 218 | } 219 | } 220 | 221 | object Highlighter{ 222 | class RefError(msg: String) extends Exception(msg) 223 | def snippet = script(raw(s""" 224 | ['DOMContentLoaded', 'load'].forEach(function(ev){ 225 | addEventListener(ev, function(){ 226 | Array.prototype.forEach.call( 227 | document.getElementsByClassName('${Styles.highlightMe.name}'), 228 | hljs.highlightBlock 229 | ); 230 | }) 231 | }) 232 | """)) 233 | /** 234 | * A context bound used to ensure you pass a `String` 235 | * or `Seq[String]` to the `@hl.ref` function 236 | */ 237 | trait RefPath[T]{ 238 | def apply(t: T): Seq[String] 239 | } 240 | object RefPath{ 241 | implicit object StringRefPath extends RefPath[String]{ 242 | def apply(t: String) = Seq(t) 243 | } 244 | implicit object SeqRefPath extends RefPath[Seq[String]]{ 245 | def apply(t: Seq[String]) = t 246 | } 247 | implicit object NilRefPath extends RefPath[Nil.type]{ 248 | def apply(t: Nil.type) = t 249 | } 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /site/src/main/scala/scalatex/site/Main.scala: -------------------------------------------------------------------------------- 1 | package scalatex.site 2 | 3 | import java.util.concurrent.Executors 4 | 5 | import ammonite.ops.{Path } 6 | import os.ResourcePath 7 | 8 | import scala.concurrent.duration.Duration 9 | import scala.concurrent.{Await, ExecutionContext, Future} 10 | import scala.util.{Failure, Success, Try} 11 | import scalaj.http._ 12 | import scalatags.Text.all._ 13 | import scalatex.site 14 | 15 | 16 | /** 17 | * A default `Main` implementation you can subclass if you do not need 18 | * the flexibility given by constructing your [[Site]] manually. 19 | * 20 | * Hooks up all the common components (Highlighter, Section, etc) in a 21 | * common configuration. 22 | */ 23 | class Main(url: String, 24 | val wd: Path, 25 | output: Path, 26 | extraAutoResources: Seq[ResourcePath], 27 | extraManualResources: Seq[ResourcePath], 28 | frag: => Frag) extends scalatex.site.Site{ 29 | 30 | lazy val hl = new Highlighter { 31 | override def pathMappings = Seq( 32 | wd -> url 33 | ) 34 | } 35 | 36 | def main(args: Array[String]): Unit = { 37 | renderTo(output) 38 | val unknownRefs = sect.usedRefs.filterNot(sect.headerSeq.contains) 39 | assert( 40 | unknownRefs.isEmpty, 41 | s"Unknown sections referred to by your `sect.ref` calls: $unknownRefs" 42 | ) 43 | if (args.contains("--validate")){ 44 | val tp = Executors.newFixedThreadPool(100) 45 | try{ 46 | implicit val ec = ExecutionContext.fromExecutorService( 47 | tp 48 | ) 49 | import concurrent.duration._ 50 | println("Validating links") 51 | 52 | val codes = for(link <- usedLinks) yield ( 53 | link, 54 | Future{ 55 | Http(link).timeout(connTimeoutMs = 5000, readTimeoutMs = 5000).asBytes.code 56 | } 57 | ) 58 | 59 | val results = codes.map{ case (link, f) => (link, Try(Await.result(f, 10.seconds)))} 60 | val failures = results.collect{ 61 | case (link, Failure(exc)) => (link, exc) 62 | case (link, Success(x)) if x >= 400 => (link, new Exception("Return code " + x)) 63 | } 64 | if (failures.length > 0){ 65 | val failureText = 66 | failures.map{case (link, exc) => link + "\n\t" + exc}.mkString("\n") 67 | throw new Exception("Invalid links found in site\n" + failureText) 68 | } 69 | println("Links OK") 70 | }finally{ 71 | tp.shutdown() 72 | } 73 | } 74 | } 75 | 76 | override def manualResources = super.manualResources ++ extraManualResources 77 | override def autoResources = 78 | super.autoResources ++ 79 | hl.autoResources ++ 80 | site.Sidebar.autoResources ++ 81 | extraAutoResources 82 | 83 | 84 | val sect = new site.Section{} 85 | 86 | /** 87 | * Default 88 | */ 89 | override def pageTitle = { 90 | println(sect.headerSeq.lift(1)) 91 | sect.headerSeq.lift(1) 92 | } 93 | override def bodyFrag(frag: Frag) = { 94 | Seq( 95 | super.bodyFrag(frag), 96 | site.Sidebar.snippet(sect.structure.children.toSeq), 97 | Highlighter.snippet 98 | ) 99 | } 100 | def content = { 101 | /** 102 | * Precompute so we have the set of headers ready, since we use the first 103 | * header to use as the title of the page 104 | */ 105 | val precalcFrag = frag 106 | Map("index.html" -> (defaultHeader, precalcFrag)) 107 | } 108 | 109 | val usedLinks = collection.mutable.Buffer.empty[String] 110 | def lnk(name: String, customUrl: String = "") = { 111 | val usedUrl = if (customUrl == "") name else customUrl 112 | usedLinks.append(usedUrl) 113 | a(name, href := usedUrl) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /site/src/main/scala/scalatex/site/Section.scala: -------------------------------------------------------------------------------- 1 | package scalatex.site 2 | 3 | import scala.collection.mutable 4 | import scalatags.Text.all 5 | import scalatags.Text.all._ 6 | import scalatags.text.Builder 7 | 8 | object Section{ 9 | case class Proxy(func: Seq[Frag] => Frag){ 10 | def apply(body: Frag*) = func(body) 11 | } 12 | trait Header{ 13 | def header(anchor: Frag, name: String, subname: String): ConcreteHtmlTag[String] 14 | def content(frag: Frag): Frag 15 | } 16 | object Header{ 17 | def apply(h: (Frag, String, String) => ConcreteHtmlTag[String], c: Frag => Frag = f => f) = { 18 | new Header { 19 | def header(anchor: Frag, name: String, subname: String) = h(anchor, name, subname) 20 | def content(frag: all.Frag) = c(frag) 21 | } 22 | } 23 | implicit def TagToHeaderStrategy(t: ConcreteHtmlTag[String]): Header = 24 | Header((frag, name, subname) => t(frag, name)) 25 | } 26 | 27 | 28 | } 29 | 30 | /** 31 | * Lets you instantiate an object used to delimit sections of your document. 32 | * 33 | * This lets you determine a sequence of headers used 34 | */ 35 | trait Section{ 36 | 37 | import Section._ 38 | type Header = Section.Header 39 | val Header = Section.Header 40 | var structure = Tree[String]("root", mutable.Buffer.empty) 41 | var depth = 0 42 | /** 43 | .header { 44 | margin: 0; 45 | color: #333; 46 | text-align: center; 47 | padding: 2.5em 2em 0; 48 | border-bottom: 1px solid #eee; 49 | } 50 | .header h1 { 51 | margin: 0.2em 0; 52 | font-size: 3em; 53 | font-weight: 300; 54 | } 55 | .header h2 { 56 | font-weight: 300; 57 | color: #ccc; 58 | padding: 0; 59 | margin-top: 0; 60 | } 61 | */ 62 | val header = Seq( 63 | margin := 0, 64 | color := "#333", 65 | textAlign.center, 66 | padding := "2.5em 2em 0", 67 | borderBottom := "1px solid #eee" 68 | ) 69 | val headerH1 = Seq( 70 | margin := "0.2em 0", 71 | fontSize := "3em", 72 | fontWeight := 300 73 | ) 74 | val headerH2 = Seq( 75 | fontWeight := 300, 76 | color := "#ccc", 77 | padding := 0, 78 | marginTop := 0 79 | ) 80 | val headers: Seq[Header] = Seq( 81 | Header( 82 | (l, h, s) => div(header, h1(headerH1, h, l), br, if(s != "") h2(headerH2, s) else ()), 83 | f => div(Styles.content, f) 84 | ), 85 | Header( 86 | (l, h, s) => div(header, h1(headerH1, h, l), br, if(s != "") h2(headerH2, s) else ())), 87 | h1, h2, h3, h4, h5, h6 88 | ) 89 | 90 | val usedRefs = mutable.Set.empty[String] 91 | 92 | def ref(s: String, txt: String = "") = { 93 | usedRefs += s 94 | a(if (txt == "") s else txt, href:=s"#${munge(s)}") 95 | } 96 | 97 | def headerSeq = { 98 | def rec(t: Tree[String]): Iterator[String] = { 99 | Iterator(t.value) ++ t.children.flatMap(rec) 100 | } 101 | rec(structure).toVector 102 | } 103 | 104 | def munge(name: String): String = name.replace(" ", "") 105 | 106 | def headingAnchor(name: String) = a( 107 | Styles.headerLink, 108 | href:=s"#${munge(name)}", 109 | i(cls:="fa fa-link"), 110 | position.absolute, 111 | right:=0 112 | ) 113 | 114 | 115 | def apply(header: String, subHeader: String = "") = { 116 | depth += 1 117 | val newNode = Tree[String](header, mutable.Buffer.empty) 118 | structure.children.append(newNode) 119 | val prev = structure 120 | structure = newNode 121 | Proxy{body => 122 | val hs = headers(depth - 1) 123 | val munged = munge(header) 124 | 125 | val res = Seq[Frag]( 126 | hs.header(headingAnchor(munged), header, subHeader)( 127 | id:=munged, 128 | display.block, 129 | Styles.hoverContainer, 130 | Styles.headerTag 131 | ), 132 | hs.content(body) 133 | ) 134 | depth -= 1 135 | structure = prev 136 | res 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /site/src/main/scala/scalatex/site/Sidebar.scala: -------------------------------------------------------------------------------- 1 | package scalatex.site 2 | 3 | import ammonite.ops._ 4 | 5 | import scalatags.Text.all._ 6 | import io.circe._, io.circe.generic.auto._, io.circe.syntax._ 7 | 8 | object Sidebar { 9 | def snippet(tree: Seq[Tree[String]]) = script(raw(s""" 10 | scalatexScrollspyController( 11 | ${tree.asJson} 12 | )""")) 13 | def autoResources = Seq(resource/'scalatex/'scrollspy/"scrollspy.js") 14 | } -------------------------------------------------------------------------------- /site/src/main/scala/scalatex/site/Site.scala: -------------------------------------------------------------------------------- 1 | package scalatex.site 2 | 3 | import java.io.FileOutputStream 4 | import java.nio.CharBuffer 5 | import java.nio.file.{Files, StandardCopyOption, StandardOpenOption} 6 | 7 | import ammonite.ops.{Path, _} 8 | import os.SubProcess.{InputStream, OutputStream} 9 | import scalatags.Text.all._ 10 | import scalatags.Text.{attrs, tags2} 11 | 12 | /** 13 | * A semi-abstract trait that encapsulates everything necessary to generate 14 | * a Scalatex site. Only `content` is left abstract (and needs to be filled 15 | * in) but the rest of the properties and definitions are all override-able 16 | * if you wish to customize things. 17 | */ 18 | trait Site{ 19 | 20 | def webjars = resource/"META-INF"/'resources/'webjars 21 | 22 | def fontAwesome = webjars/"font-awesome"/"4.7.0" 23 | 24 | /** 25 | * Resources related to the pure-css library 26 | */ 27 | def pureCss = Seq( 28 | webjars/'pure/"0.6.2"/"pure-min.css" 29 | ) 30 | /** 31 | * Resources related to the font awesome library 32 | */ 33 | def fontAwesomeResources = Seq( 34 | fontAwesome/'fonts/"FontAwesome.otf", 35 | fontAwesome/'fonts/"fontawesome-webfont.eot", 36 | fontAwesome/'fonts/"fontawesome-webfont.svg", 37 | fontAwesome/'fonts/"fontawesome-webfont.ttf", 38 | fontAwesome/'fonts/"fontawesome-webfont.woff", 39 | fontAwesome/'css/"font-awesome.min.css" 40 | ) 41 | /** 42 | * Resources custom-provided for this particular site 43 | */ 44 | def siteCss = Set( 45 | resource/'scalatex/'site/"styles.css" 46 | ) 47 | 48 | /** 49 | * Resources that get automatically included in the bundled js or css file 50 | */ 51 | def autoResources = pureCss ++ siteCss 52 | 53 | /** 54 | * Resources copied to the output folder but not included on the page by default 55 | */ 56 | def manualResources = fontAwesomeResources 57 | /** 58 | * The name of the javascript file that all javascript resources get bundled into 59 | */ 60 | def scriptName = "scripts.js" 61 | /** 62 | * The name of the css file that all css resources get bundled into 63 | */ 64 | def stylesName = "styles.css" 65 | 66 | def pageTitle: Option[String] = None 67 | /** 68 | * The header of this site's HTML page 69 | */ 70 | def defaultHeader: Seq[Frag] = Seq( 71 | link(href:="META-INF/resources/webjars/font-awesome/4.7.0/css/font-awesome.min.css", rel:="stylesheet"), 72 | link(href:=stylesName, rel:="stylesheet"), 73 | link(rel:="shortcut icon", `type`:="image/png", href:="favicon.png"), 74 | meta(httpEquiv:="Content-Type", attrs.content:="text/html; charset=UTF-8"), 75 | tags2.style(raw(Styles.styleSheetText)), 76 | pageTitle.map(tags2.title(_)), 77 | script(src:=scriptName) 78 | ) 79 | 80 | /** 81 | * The body of this site's HTML page 82 | */ 83 | def bodyFrag(frag: Frag): Frag = div( 84 | frag 85 | ) 86 | 87 | 88 | type Body = Frag 89 | /** Enable pages to specify multiple header entries */ 90 | type Header = Seq[Frag] 91 | type Page = (Header, Body) 92 | /** 93 | * The contents of the site. 94 | * Maps String paths to the pages, to their actual content. 95 | */ 96 | def content: Map[String, Page] 97 | 98 | def bundleResources(outputRoot: Path) = { 99 | for { 100 | dest <- Seq(scriptName, stylesName) 101 | path = outputRoot / dest 102 | } new FileOutputStream(path.toIO, true).close() 103 | 104 | for { 105 | (ext, dest) <- Seq("js" -> scriptName, "css" -> stylesName) 106 | path = outputRoot / dest 107 | res <- autoResources |? (_.ext == ext) 108 | } { 109 | val content = read(res) 110 | 111 | path.toIO.getParentFile.mkdirs() 112 | Files.writeString(path.wrapped, content, StandardOpenOption.CREATE) 113 | } 114 | 115 | for { 116 | res <- manualResources 117 | } { 118 | val content = read(res) 119 | val path = outputRoot/(res relativeTo resource) 120 | path.toIO.getParentFile.mkdirs() 121 | Files.writeString(path.wrapped, content, StandardOpenOption.CREATE) 122 | } 123 | } 124 | 125 | def generateHtml(outputRoot: Path) = { 126 | for((path, (pageHeaders, pageBody)) <- content){ 127 | val txt = html( 128 | head(pageHeaders), 129 | body(bodyFrag(pageBody)) 130 | ).render 131 | val cb = CharBuffer.wrap("" + txt) 132 | val bytes = scala.io.Codec.UTF8.encoder.encode(cb) 133 | 134 | write.over(outputRoot/path, bytes.array().slice(bytes.position(), bytes.limit()), createFolders = true) 135 | } 136 | } 137 | def renderTo(outputRoot: Path) = { 138 | generateHtml(outputRoot) 139 | bundleResources(outputRoot) 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /site/src/main/scala/scalatex/site/Styles.scala: -------------------------------------------------------------------------------- 1 | package scalatex.site 2 | import scalatags.Text.all._ 3 | import scalatags.stylesheet.{CascadingStyleSheet, SourceClasses} 4 | 5 | object Styles extends CascadingStyleSheet { 6 | 7 | initStyleSheet() 8 | 9 | def highlightMe = cls() 10 | 11 | def headerLink = cls( 12 | color := "#777", 13 | opacity := 0.05, 14 | textDecoration := "none" 15 | ) 16 | 17 | def headerTag = cls() 18 | 19 | def hoverContainer = cls.hover( 20 | headerLink( 21 | headerLink.splice, 22 | &.hover( 23 | opacity := 1.0 24 | ), 25 | &.active(opacity := 0.75), 26 | opacity := 0.5 27 | ) 28 | ) 29 | 30 | def content = cls( 31 | *( 32 | position.relative 33 | ), 34 | marginLeft:="auto", 35 | marginRight:="auto", 36 | margin := "0 auto", 37 | padding := "0 1em", 38 | maxWidth := 800, 39 | paddingBottom := 50, 40 | lineHeight := "1.6em", 41 | color := "#777", 42 | p( 43 | textAlign.justify 44 | ), 45 | a( 46 | &link( 47 | color := "#37a", 48 | textDecoration := "none" 49 | ), 50 | &visited( 51 | color := "#949", 52 | textDecoration := "none" 53 | ), 54 | &hover( 55 | textDecoration := "underline" 56 | ), 57 | &active( 58 | color := "#000", 59 | textDecoration := "underline" 60 | ) 61 | ), 62 | code( 63 | color := "#000" 64 | ) 65 | 66 | ) 67 | override def styleSheetText = super.styleSheetText + 68 | """ 69 | |/*Workaround for bug in highlight.js IDEA theme*/ 70 | |span.hljs-tag, span.hljs-symbol{ 71 | | background: none; 72 | |} 73 | """.stripMargin 74 | } 75 | -------------------------------------------------------------------------------- /site/src/main/scala/scalatex/site/Util.scala: -------------------------------------------------------------------------------- 1 | package scalatex.site 2 | 3 | import scala.collection.mutable 4 | 5 | case class Tree[T](value: T, children: mutable.Buffer[Tree[T]]) -------------------------------------------------------------------------------- /site/src/test/scala/scalatex/site/Tests.scala: -------------------------------------------------------------------------------- 1 | package scalatex.site 2 | 3 | import ammonite.ops._ 4 | import utest._ 5 | 6 | import scala.collection.mutable 7 | import scalatags.Text.all._ 8 | import scalatex.scalatex.site._ 9 | object Tests extends TestSuite{ 10 | 11 | def cmp(s1: String, s2: String) = { 12 | val f1 = s1.filter(!_.isWhitespace).mkString 13 | val f2 = s2.filter(!_.isWhitespace) 14 | assert(f1 == f2) 15 | } 16 | val wd = ammonite.ops.pwd 17 | val tests = Tests { 18 | 'Hello{ 19 | cmp( 20 | Hello().render 21 | , 22 | """ 23 |
24 | Hello World 25 | 26 |

I am a cow!

27 |
28 | """ 29 | ) 30 | } 31 | 32 | 'Section{ 33 | 'Basic { 34 | import scalatex.site.Section 35 | object sect extends Section 36 | val txt = sect("Main")( 37 | sect("SectionA")( 38 | "Hello World" 39 | ), 40 | sect("SectionB0")( 41 | sect("SectionB1")( 42 | "Lols", sect.ref("Main") 43 | ), 44 | sect("SectionB2")( 45 | "Hai", sect.ref("SectionA") 46 | ) 47 | ) 48 | ).render 49 | assert(sect.usedRefs.forall(sect.headerSeq.contains)) 50 | // 1s are raw-text, 3s are titles, and 5s are 51 | // titles which are linked to somewhere 52 | val expectedTokens = Seq( 53 | "Main" -> 5, 54 | "SectionA" -> 5, 55 | "Hello World" -> 1, 56 | "SectionB0" -> 3, 57 | "SectionB1" -> 3, 58 | "SectionB2" -> 3, 59 | "Lols" -> 1, 60 | "Hai" -> 1 61 | ) 62 | for ((token, count) <- expectedTokens) { 63 | val found = token.r.findAllIn(txt).toVector 64 | assert({txt; found.length} == count) 65 | } 66 | val expectedTree = Tree[String]("root", mutable.Buffer( 67 | Tree[String]("Main", mutable.Buffer( 68 | Tree[String]("SectionA", mutable.Buffer()), 69 | Tree[String]("SectionB0", mutable.Buffer( 70 | Tree[String]("SectionB1", mutable.Buffer()), 71 | Tree[String]("SectionB2", mutable.Buffer()) 72 | )) 73 | )) 74 | )) 75 | val struct = sect.structure 76 | assert(struct == expectedTree) 77 | assert(sect.usedRefs == Set("Main", "SectionA")) 78 | } 79 | 'Failure{ 80 | import scalatex.site.Section 81 | object sect extends Section 82 | val txt = sect("Main")( 83 | sect("SectionA")( 84 | "Hello World" 85 | ), 86 | sect("SectionB0")( 87 | sect("SectionB1")( 88 | "Lols", sect.ref("Mani") 89 | ), 90 | sect("SectionB2")( 91 | "Hai", sect.ref("SectionA") 92 | ) 93 | ) 94 | ).render 95 | intercept[AssertionError]( 96 | assert(sect.usedRefs.forall(sect.headerSeq.contains)) 97 | ) 98 | } 99 | } 100 | 'Highlighter{ 101 | object hl extends Highlighter { 102 | } 103 | 'wholeFile { 104 | val (start, end, txt) = hl.referenceText( 105 | wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", 106 | Nil, 107 | Nil 108 | ) 109 | val expected = read! wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala" 110 | cmp(txt, expected) 111 | } 112 | 'selectors { 113 | /** 114 | * Comment I'm trawling for 115 | */ 116 | val (start, end, txt) = hl.referenceText( 117 | wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", 118 | Seq("Highlighter", "selectors", "/**"), 119 | Seq("*/", "val") 120 | ) 121 | 122 | val expected = 123 | """ 124 | /** 125 | * Comment I'm trawling for 126 | */ 127 | """ 128 | cmp(txt, expected) 129 | } 130 | 'trimBlanks{ 131 | // Make sure that indentaton and trailing blank lines get removed 132 | 133 | // trimBlanks start 134 | 135 | // trimBlanks content 136 | 137 | // trimBlanks end 138 | val (start, end, txt) = hl.referenceText( 139 | wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", 140 | "trimBlanks start", 141 | "trimBlanks end" 142 | ) 143 | val expected = 144 | """// trimBlanks start 145 | | 146 | |// trimBlanks content""".stripMargin 147 | assert(txt == expected) 148 | } 149 | 'dedentEnd{ 150 | // Make sure any dedentation from the starting line ends 151 | // the snippet, even if no explicit ending is specified. 152 | val test = { 153 | Seq("Hello! ", 154 | "I am a cow" 155 | ) 156 | } 157 | val (start, end, txt) = hl.referenceText( 158 | wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", 159 | Seq("dedentEnd", "val test", "Hello!"), 160 | Nil 161 | ) 162 | val expected = 163 | """Seq("Hello! ", 164 | | "I am a cow" 165 | |)""".stripMargin 166 | assert(txt == expected) 167 | } 168 | 'indentation{ 169 | val lang = "js" 170 | 'noIndent{ 171 | // Shouldn't crash when no lines are indented 172 | val text = "{\ntest\n}" 173 | val expectedLines = text 174 | val expected = pre(code(cls:=lang + " " + Styles.highlightMe.name, expectedLines)) 175 | val actual = hl.highlight(text, lang) 176 | assert(actual == expected) 177 | } 178 | 'zeroMinIndent{ 179 | // Shouldn't delete text if minimum indent is 0 180 | val lang = "js" 181 | val text = "{\n test\n}" 182 | val expectedLines = text 183 | val expected = pre(code(cls:=lang + " " + Styles.highlightMe.name, expectedLines)) 184 | val actual = hl.highlight(text, lang) 185 | assert(actual == expected) 186 | } 187 | } 188 | 'notFound - intercept[Highlighter.RefError]{ 189 | val (start, end, txt) = hl.referenceText( 190 | wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", 191 | Seq("this string " + "doesn't exist"), 192 | Nil 193 | ) 194 | 195 | } 196 | 'notFound2 - intercept[Highlighter.RefError]{ 197 | val (start, end, txt) = hl.referenceText( 198 | wd/'site/'src/'test/'scala/'scalatex/'site/"Tests.scala", 199 | Nil, 200 | "this string " + "doesn't exist either" 201 | ) 202 | 203 | } 204 | } 205 | 'Site{ 206 | 'simple { 207 | rm! wd / "site" / "target"/ "output" 208 | val site = new scalatex.site.Site { 209 | def content = Map("index.html" -> (defaultHeader, Hello())) 210 | } 211 | site.renderTo(wd/ "site" / "target" / "output") 212 | 213 | def check() = { 214 | val readText = read! wd / "site" / "target" / "output" /"index.html" 215 | assert( 216 | readText.contains("Hello World"), 217 | readText.contains("I am a cow!"), 218 | readText.contains("
"), 219 | readText.contains("

"), 220 | exists(wd / "site" / "target" / "output" / "scripts.js"), 221 | exists(wd/ "site" / "target" / "output" / "styles.css") 222 | ) 223 | } 224 | check() 225 | // re-rendering works 226 | site.renderTo(wd/'site/'target/'output) 227 | 228 | // deleting and re-rendering works too 229 | 230 | rm! wd/'site/'target/'output 231 | assert(!exists(wd/'site/'target/'output)) 232 | site.renderTo(wd/'site/'target/'output) 233 | check() 234 | } 235 | 'overriding{ 236 | rm! wd/'site/'target/'output2 237 | assert(!exists(wd/'site/'target/'output2)) 238 | val site = new scalatex.site.Site { 239 | override def scriptName = "custom.js" 240 | override def stylesName = "custom.css" 241 | override def defaultHeader = super.defaultHeader ++ Seq( 242 | script("console.log('Hi!')") 243 | ) 244 | 245 | def content = Map( 246 | "page1.html" -> (defaultHeader, Hello()), 247 | "page2.html" -> (defaultHeader, About()) 248 | ) 249 | } 250 | site.renderTo(wd/'site/'target/'output2) 251 | val page1 = read! wd/'site/'target/'output2/"page1.html" 252 | val page2 = read! wd/'site/'target/'output2/"page2.html" 253 | 254 | assert( 255 | page1.contains("Hello World"), 256 | page1.contains("I am a cow!"), 257 | page1.contains("
"), 258 | page1.contains("

"), 259 | page2.contains("Hear me moo"), 260 | exists(wd/'site/'target/'output2/"custom.js"), 261 | exists(wd/'site/'target/'output2/"custom.css") 262 | ) 263 | } 264 | } 265 | 'CustomHead { 266 | // check that head remains unchanged 267 | 'default { 268 | rm ! wd / 'site / 'target / 'output 269 | val site = new scalatex.site.Site { 270 | def content = Map("index.html" ->(defaultHeader, Hello())) 271 | } 272 | site.renderTo(wd / 'site / 'target / 'output) 273 | 274 | def check() = { 275 | val expected = site.defaultHeader map (_.render) 276 | // return empty head if not found => problem 277 | val results = site.content.getOrElse("index.html", (Seq(), Hello()))._1 map (_.render) 278 | val ta = expected zip results map (t => t._1.equals(t._2)) 279 | 280 | assert( ta.reduce(_ && _) ) 281 | } 282 | check() 283 | } 284 | 'customTitle { 285 | rm ! wd / 'site / 'target / 'output 286 | val site = new scalatex.site.Site { 287 | val mooMarker = "Moooo" 288 | 289 | def customHead = defaultHeader ++ Seq(scalatags.Text.tags2.title(mooMarker)) 290 | def content = Map("index.html" ->(customHead, Hello())) 291 | } 292 | site.renderTo(wd / 'site / 'target / 'output) 293 | val page1 = read! wd/'site/'target/'output/"index.html" 294 | 295 | assert( page1.contains(s"${site.mooMarker}") ) 296 | } 297 | 'differentTitles { 298 | rm ! wd / 'site / 'target / 'output 299 | val site = new scalatex.site.Site { 300 | val mooMarker = "Moooo" 301 | val bbqMarker = "Barbecue!" 302 | 303 | def content = Map( 304 | "page1.html" -> (defaultHeader ++ Seq(scalatags.Text.tags2.title(mooMarker)), Hello()), 305 | "page2.html" -> (defaultHeader ++ Seq(scalatags.Text.tags2.title(bbqMarker)), About()) 306 | ) 307 | } 308 | site.renderTo(wd / 'site / 'target / 'output) 309 | val page1 = read! wd/'site/'target/'output/"page1.html" 310 | val page2 = read! wd/'site/'target/'output/"page2.html" 311 | 312 | assert( 313 | page1.contains(s"${site.mooMarker}"), 314 | !page1.contains(s"${site.bbqMarker}"), 315 | page2.contains(s"${site.bbqMarker}"), 316 | !page2.contains(s"${site.mooMarker}") 317 | ) 318 | } 319 | 'onlyOneCustom { 320 | rm ! wd / 'site / 'target / 'output 321 | val site = new scalatex.site.Site { 322 | val mooMarker = "Moooo" 323 | 324 | def content = Map( 325 | "page1.html" -> (defaultHeader ++ Seq(scalatags.Text.tags2.title(mooMarker)), Hello()), 326 | "page2.html" -> (defaultHeader, About()) 327 | ) 328 | } 329 | site.renderTo(wd / 'site / 'target / 'output) 330 | val page1 = read! wd/'site/'target/'output/"page1.html" 331 | val page2 = read! wd/'site/'target/'output/"page2.html" 332 | 333 | assert( 334 | page1.contains(s"${site.mooMarker}"), 335 | !page2.contains(s"") 336 | ) 337 | } 338 | } 339 | } 340 | } 341 | 342 | -------------------------------------------------------------------------------- /site/src/test/scalatex/scalatex/site/About.scalatex: -------------------------------------------------------------------------------- 1 | 2 | 3 | @p 4 | Hear me moo 5 | 6 | @p 7 | I weigh twice as much as you 8 | 9 | @p 10 | And I look good on the barbecue -------------------------------------------------------------------------------- /site/src/test/scalatex/scalatex/site/Hello.scalatex: -------------------------------------------------------------------------------- 1 | @div 2 | Hello World 3 | 4 | @h1 5 | I am a cow! -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | lazy val Constants = _root_.scalatex.Constants 2 | 3 | version in ThisBuild := Constants.version 4 | --------------------------------------------------------------------------------