From bac53bef685f4e00e7061923dcd57dea4f9e229b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 3 Dec 2014 22:46:04 -0800 Subject: scalatex broken out into separate project which needs to be published separately --- book/src/main/scala/book/Book.scala | 6 +- .../scalatex/book/handson/ClientServer.scalatex | 15 +- build.sbt | 55 +-- examples/demos/build.sbt | 4 +- .../src/main/scala/scrollmenu/ScrollMenu.scala | 170 -------- .../src/main/scala/scrollmenu/ScrollSpy.scala | 170 ++++++++ project/build.sbt | 4 +- .../src/main/scala/scalaParser/ScalaSyntax.scala | 416 ------------------ .../src/main/scala/scalaParser/syntax/Basic.scala | 51 --- .../scala/scalaParser/syntax/Identifiers.scala | 35 -- .../main/scala/scalaParser/syntax/Literals.scala | 58 --- .../src/test/scala/scalaParser/SyntaxTest.scala | 448 -------------------- scalatex/api/src/main/scala/scalatex/package.scala | 98 +++++ .../src/main/scala/scalatex/stages/Compiler.scala | 103 +++++ .../src/main/scala/scalatex/stages/Omg/scala.scala | 8 + .../src/main/scala/scalatex/stages/Parser.scala | 170 ++++++++ .../api/src/main/scala/scalatex/stages/Trim.scala | 29 ++ .../api/src/test/scala/scalatex/BasicTests.scala | 468 +++++++++++++++++++++ .../api/src/test/scala/scalatex/ErrorTests.scala | 373 ++++++++++++++++ .../api/src/test/scala/scalatex/ParserTests.scala | 424 +++++++++++++++++++ .../api/src/test/scala/scalatex/TestUtil.scala | 16 + scalatex/build.sbt | 44 ++ .../src/main/resources/scalac-plugin.xml | 4 + .../src/main/scala/scalatex/CompilerPlugin.scala | 79 ++++ scalatex/project/build.properties | 1 + scalatex/project/build.sbt | 1 + .../src/main/scala/scalaParser/ScalaSyntax.scala | 416 ++++++++++++++++++ .../src/main/scala/scalaParser/syntax/Basic.scala | 51 +++ .../scala/scalaParser/syntax/Identifiers.scala | 35 ++ .../main/scala/scalaParser/syntax/Literals.scala | 58 +++ .../src/test/scala/scalaParser/SyntaxTest.scala | 448 ++++++++++++++++++++ .../src/main/scala/SbtPlugin.scala | 16 + scalatexApi/src/main/scala/scalatex/package.scala | 98 ----- .../src/main/scala/scalatex/stages/Compiler.scala | 103 ----- .../src/main/scala/scalatex/stages/Parser.scala | 170 -------- .../src/main/scala/scalatex/stages/Trim.scala | 29 -- .../src/test/scala/scalatex/AdvancedTests.scala | 120 ------ .../src/test/scala/scalatex/BasicTests.scala | 468 --------------------- .../src/test/scala/scalatex/ErrorTests.scala | 373 ---------------- .../src/test/scala/scalatex/ParserTests.scala | 424 ------------------- scalatexApi/src/test/scala/scalatex/TestUtil.scala | 16 - .../src/main/resources/scalac-plugin.xml | 4 - .../src/main/scala/scalatex/ScalaTexPlugin.scala | 66 --- 43 files changed, 3041 insertions(+), 3104 deletions(-) delete mode 100644 examples/demos/src/main/scala/scrollmenu/ScrollMenu.scala create mode 100644 examples/demos/src/main/scala/scrollmenu/ScrollSpy.scala delete mode 100644 scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala delete mode 100644 scalaParser/src/main/scala/scalaParser/syntax/Basic.scala delete mode 100644 scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala delete mode 100644 scalaParser/src/main/scala/scalaParser/syntax/Literals.scala delete mode 100644 scalaParser/src/test/scala/scalaParser/SyntaxTest.scala create mode 100644 scalatex/api/src/main/scala/scalatex/package.scala create mode 100644 scalatex/api/src/main/scala/scalatex/stages/Compiler.scala create mode 100644 scalatex/api/src/main/scala/scalatex/stages/Omg/scala.scala create mode 100644 scalatex/api/src/main/scala/scalatex/stages/Parser.scala create mode 100644 scalatex/api/src/main/scala/scalatex/stages/Trim.scala create mode 100644 scalatex/api/src/test/scala/scalatex/BasicTests.scala create mode 100644 scalatex/api/src/test/scala/scalatex/ErrorTests.scala create mode 100644 scalatex/api/src/test/scala/scalatex/ParserTests.scala create mode 100644 scalatex/api/src/test/scala/scalatex/TestUtil.scala create mode 100644 scalatex/build.sbt create mode 100755 scalatex/compilerPlugin/src/main/resources/scalac-plugin.xml create mode 100755 scalatex/compilerPlugin/src/main/scala/scalatex/CompilerPlugin.scala create mode 100644 scalatex/project/build.properties create mode 100644 scalatex/project/build.sbt create mode 100644 scalatex/scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala create mode 100644 scalatex/scalaParser/src/main/scala/scalaParser/syntax/Basic.scala create mode 100644 scalatex/scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala create mode 100644 scalatex/scalaParser/src/main/scala/scalaParser/syntax/Literals.scala create mode 100644 scalatex/scalaParser/src/test/scala/scalaParser/SyntaxTest.scala create mode 100644 scalatex/scalatexSbtPlugin/src/main/scala/SbtPlugin.scala delete mode 100644 scalatexApi/src/main/scala/scalatex/package.scala delete mode 100644 scalatexApi/src/main/scala/scalatex/stages/Compiler.scala delete mode 100644 scalatexApi/src/main/scala/scalatex/stages/Parser.scala delete mode 100644 scalatexApi/src/main/scala/scalatex/stages/Trim.scala delete mode 100644 scalatexApi/src/test/scala/scalatex/AdvancedTests.scala delete mode 100644 scalatexApi/src/test/scala/scalatex/BasicTests.scala delete mode 100644 scalatexApi/src/test/scala/scalatex/ErrorTests.scala delete mode 100644 scalatexApi/src/test/scala/scalatex/ParserTests.scala delete mode 100644 scalatexApi/src/test/scala/scalatex/TestUtil.scala delete mode 100755 scalatexPlugin/src/main/resources/scalac-plugin.xml delete mode 100755 scalatexPlugin/src/main/scala/scalatex/ScalaTexPlugin.scala diff --git a/book/src/main/scala/book/Book.scala b/book/src/main/scala/book/Book.scala index d023d56..71c0d65 100644 --- a/book/src/main/scala/book/Book.scala +++ b/book/src/main/scala/book/Book.scala @@ -25,7 +25,7 @@ object Book { "META-INF/resources/webjars/font-awesome/4.2.0/fonts/fontawesome-webfont.woff", "css/side-menu.css", "example-opt.js", - "example-opt.js.map", +// "example-opt.js.map", "webpage/weather.js", "favicon.svg", "favicon.png" @@ -72,6 +72,7 @@ object Book { script(raw(googleAnalytics)) ), body( + onload:=s"Controller().main($data)", div(id:="layout")( a(href:="#menu", id:="menuLink", cls:="menu-link")( span @@ -83,8 +84,7 @@ object Book { div(id:="main-box")( txt ) - ), - script(raw(s"Controller().main($data)")) + ) ) ) ).render diff --git a/book/src/main/scalatex/book/handson/ClientServer.scalatex b/book/src/main/scalatex/book/handson/ClientServer.scalatex index 48d35f2..50afe76 100644 --- a/book/src/main/scalatex/book/handson/ClientServer.scalatex +++ b/book/src/main/scalatex/book/handson/ClientServer.scalatex @@ -1,5 +1,12 @@ @import BookData._ +@def lazyload(id: String) = script(raw(""" + document.addEventListener("load", function(){ + document.getElementById(id).src = 'https://hands-on-scala-js.herokuapp.com/' + }) +""")) + + @p Historically, sharing code across client & server has been a holy-grail for web development. There are many things which have made it hard in the past: @@ -75,7 +82,9 @@ @p Now, if we go to the browser at @code{localhost:8080}, we should see our web-page! - @iframe(src:="https://hands-on-scala-js.herokuapp.com/", width:="100%", height:="350px", "frameBorder".attr:="0") + @iframe(id:="heroku1", width:="100%", height:="350px", "frameBorder".attr:="0") + @lazyload("heroku1") + @p This is a real, live example running on a @lnk("Heroku server", "https://hands-on-scala-js.herokuapp.com/"). Feel free to poke around and explore the filesystem on the server, just to convince yourself that this actually works and is not just a mock up. @@ -215,7 +224,9 @@ @p Other than that, nothing much has changed. If you've done this correctly, the web application will look and behave exactly as it did earlier! - @iframe(src:="https://hands-on-scala-js.herokuapp.com/", width:="100%", height:="350px", "frameBorder".attr:="0") + @iframe(id:="heroku2", width:="100%", height:="350px", "frameBorder".attr:="0") + @lazyload("heroku2") + @p So why did we do this in the first place? diff --git a/build.sbt b/build.sbt index 4b1d7dd..43d679c 100644 --- a/build.sbt +++ b/build.sbt @@ -16,67 +16,26 @@ val sharedSettings = Seq( autoCompilerPlugins := true ) -lazy val scalaParser = project.settings(sharedSettings:_*) - .settings( - libraryDependencies ++= Seq( - "com.lihaoyi" %% "utest" % "0.2.4", - "org.parboiled" %% "parboiled" % "2.0.1" - ), - testFrameworks += new TestFramework("utest.runner.JvmFramework") -) -lazy val scalatexApi = project.settings(sharedSettings:_*) - .dependsOn(scalaParser) - .settings( - libraryDependencies ++= Seq( - "com.lihaoyi" %% "utest" % "0.2.4", - "com.scalatags" %% "scalatags" % "0.4.2", - "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "org.parboiled" %% "parboiled" % "2.0.1" - ), - testFrameworks += new TestFramework("utest.runner.JvmFramework") -) - -lazy val scalatexPlugin = Project( - id = "scalatexPlugin", - base = file("scalatexPlugin"), - dependencies = Seq(scalatexApi) -).settings(sharedSettings:_*).settings( - libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value, - publishArtifact in Compile := false -) lazy val book = Project( id = "book", - base = file("book"), - dependencies = Seq(scalatexApi) -).settings(sharedSettings:_*).settings( + base = file("book") +).settings(sharedSettings ++ inConfig(Compile)(scalatex.SbtPlugin.settings):_*).settings( libraryDependencies ++= Seq( "org.webjars" % "highlightjs" % "8.2-1", "org.webjars" % "pure" % "0.5.0", "org.webjars" % "font-awesome" % "4.2.0", + "com.scalatags" %% "scalatags" % "0.4.2", "com.lihaoyi" %%% "upickle" % "0.2.5" ), - (resources in Compile) ++= { + (resources in Compile) += { (fullOptJS in (demos, Compile)).value - Seq( - (artifactPath in (demos, Compile, fullOptJS)).value, - new java.io.File((artifactPath in (demos, Compile, fullOptJS)).value.getPath + ".map") - ) + (artifactPath in (demos, Compile, fullOptJS)).value }, + (unmanagedResourceDirectories in Compile) ++= (unmanagedResourceDirectories in (demos, Compile)).value, - scalacOptions in Compile ++= { - val jar = (Keys.`package` in (scalatexPlugin, Compile)).value - val addPlugin = "-Xplugin:" + jar.getAbsolutePath - // add plugin timestamp to compiler options to trigger recompile of - // main after editing the plugin. (Otherwise a 'clean' is needed.) - val dummy = "-Jdummy=" + jar.lastModified - val options = "-P:scalatex-options:" + sourceDirectory.value / "scalatex" - Seq(addPlugin, dummy) - }, - watchSources ++= { - ((sourceDirectory in Compile).value / "scalatex" ** "*.scalatex").get - }, + cloneRepos := { val localPath = target.value / "clones" if (!localPath.isDirectory){ diff --git a/examples/demos/build.sbt b/examples/demos/build.sbt index 5b55829..aa801b0 100644 --- a/examples/demos/build.sbt +++ b/examples/demos/build.sbt @@ -1,4 +1,6 @@ -import scala.scalajs.sbtplugin.ScalaJSPlugin.ScalaJSKeys.jsDependencies +import scala.scalajs.sbtplugin.ScalaJSPlugin.ScalaJSKeys._ + +(emitSourceMaps in fullOptJS) := false scalaJSSettings diff --git a/examples/demos/src/main/scala/scrollmenu/ScrollMenu.scala b/examples/demos/src/main/scala/scrollmenu/ScrollMenu.scala deleted file mode 100644 index ce3f07f..0000000 --- a/examples/demos/src/main/scala/scrollmenu/ScrollMenu.scala +++ /dev/null @@ -1,170 +0,0 @@ -package scrollmenu - -import org.scalajs.dom -import org.scalajs.dom.extensions._ -import scala.scalajs.js -import scalatags.JsDom.all._ - -case class Tree[T](value: T, children: Vector[Tree[T]]) - -case class MenuNode(frag: dom.HTMLElement, id: String, start: Int, end: Int) - -/** - * High performance scrollspy to work keep the left menu bar in sync. - * Lots of sketchy imperative code in order to maximize performance. - */ -class ScrollSpy(structure: Tree[String], - main: dom.HTMLElement){ - val (headers, domTrees) = { - var i = -1 - def recurse(t: Tree[String], depth: Int): Tree[MenuNode] = { - val curr = - li( - a( - t.value, - display := (if (i == -1) "none" else "block"), - href:="#"+Controller.munge(t.value), - cls:="menu-item" - ) - ) - val originalI = i - i += 1 - val children = t.children.map(recurse(_, depth + 1)) - Tree( - MenuNode( - curr(ul(paddingLeft := "15px",children.map(_.value.frag))).render, - Controller.munge(t.value), - originalI, - if (children.length > 0) children.map(_.value.end).max else originalI + 1 - ), - children - ) - } - def offset(el: dom.HTMLElement, parent: dom.HTMLElement): Double = { - if (el == parent) 0 - else el.offsetTop + offset(el.offsetParent.asInstanceOf[dom.HTMLElement], parent) - } - val headers = { - val menuItems = { - def rec(current: Tree[String]): Seq[String] = { - current.value +: current.children.flatMap(rec) - } - rec(structure).tail - } - - js.Array( - menuItems.map(Controller.munge) - .map(dom.document.getElementById) - .map(offset(_, main)):_* - ) - } - val domTrees = recurse(structure, 0) - (headers, domTrees) - } - - var open = false - def toggleOpen() = { - open = !open - if (open){ - def rec(tree: Tree[MenuNode])(f: MenuNode => Unit): Unit = { - f(tree.value) - tree.children.foreach(rec(_)(f)) - } - rec(domTrees)(setFullHeight) - }else{ - start(force = true) - } - } - - def setFullHeight(mn: MenuNode) = { - mn.frag - .children(1) - .asInstanceOf[dom.HTMLElement] - .style - .maxHeight = (mn.end - mn.start + 1) * 44 + "px" - } - private[this] var scrolling = false - private[this] var scrollTop = -1 - def apply(): Unit = { - if (!scrolling) { - scrolling = true - scrollTop = main.scrollTop - dom.setTimeout({() => - scrolling = false - if (scrollTop == main.scrollTop) start() - else apply() - }, - 75 - ) - } - } - private[this] var previousWin: MenuNode = null - private[this] def start(force: Boolean = false) = { - - def scroll(el: dom.Element) = { - val rect = el.getBoundingClientRect() - if (rect.top <= 0) - el.scrollIntoView(true) - else if (rect.top > dom.innerHeight) - el.scrollIntoView(false) - } - val scrollTop = main.scrollTop - def walkIndex(tree: Tree[MenuNode]): List[Tree[MenuNode]] = { - val t @ Tree(m, children) = tree - val win = if(m.start == -1) true - else { - val before = headers(m.start) <= scrollTop - val after = (m.end >= headers.length) || headers(m.end) > scrollTop - before && after - } - val childIndexes = children.map(walkIndex) - val childWin = childIndexes.indexWhere(_ != null) - if (childWin != -1) t :: childIndexes(childWin) - else if (win) List(t) - else null - } - - val winPath = walkIndex(domTrees) - val winItem = winPath.last.value - def walkTree(indices: List[Tree[MenuNode]]): Int = indices match { - case Nil => 0 - case (Tree(mn, children) :: rest) => - - mn.frag.classList.remove("hide") - mn.frag.classList.remove("selected") - - mn.frag.children(0).classList.add("pure-menu-selected") - for { - child <- children - if !indices.headOption.exists(_.value.frag == child.value.frag) - } walkHide(child) - - val size = walkTree(rest) + children.length - mn.frag.children(1).asInstanceOf[dom.HTMLElement].style.maxHeight = - if (!open) size * 44 + "px" else "none" - size - } - - def walkHide(tree: Tree[MenuNode]): Unit = { - val frag = tree.value.frag - - frag.children(0).classList.remove("pure-menu-selected") - frag.classList.add("hide") - - frag.children(1).asInstanceOf[dom.HTMLElement].style.maxHeight = - if (!open) "0px" else "none" - - if (tree.value.start < winItem.start) frag.classList.add("selected") - else frag.classList.remove("selected") - tree.children.foreach(walkHide) - } - - if (winItem != previousWin || force){ - scroll(winItem.frag.children(0)) - dom.history.replaceState(null, null, "#" + winItem.id) - previousWin = winItem -// println(winPath.map(_.value.id)) - walkTree(winPath) - } - } -} \ No newline at end of file diff --git a/examples/demos/src/main/scala/scrollmenu/ScrollSpy.scala b/examples/demos/src/main/scala/scrollmenu/ScrollSpy.scala new file mode 100644 index 0000000..736f9b3 --- /dev/null +++ b/examples/demos/src/main/scala/scrollmenu/ScrollSpy.scala @@ -0,0 +1,170 @@ +package scrollmenu + +import org.scalajs.dom +import org.scalajs.dom.extensions._ +import scala.scalajs.js +import scalatags.JsDom.all._ + +case class Tree[T](value: T, children: Vector[Tree[T]]) + +case class MenuNode(frag: dom.HTMLElement, id: String, start: Int, end: Int) + +/** + * High performance scrollspy to work keep the left menu bar in sync. + * Lots of sketchy imperative code in order to maximize performance. + */ +class ScrollSpy(structure: Tree[String], + main: dom.HTMLElement){ + lazy val domTrees = { + var i = -1 + def recurse(t: Tree[String], depth: Int): Tree[MenuNode] = { + val curr = + li( + a( + t.value, + display := (if (i == -1) "none" else "block"), + href:="#"+Controller.munge(t.value), + cls:="menu-item" + ) + ) + val originalI = i + i += 1 + val children = t.children.map(recurse(_, depth + 1)) + Tree( + MenuNode( + curr(ul(paddingLeft := "15px",children.map(_.value.frag))).render, + Controller.munge(t.value), + originalI, + if (children.length > 0) children.map(_.value.end).max else originalI + 1 + ), + children + ) + } + + val domTrees = recurse(structure, 0) + domTrees + } + def offset(el: dom.HTMLElement, parent: dom.HTMLElement): Double = { + if (el == parent) 0 + else el.offsetTop + offset(el.offsetParent.asInstanceOf[dom.HTMLElement], parent) + } + lazy val headers = { + val menuItems = { + def rec(current: Tree[String]): Seq[String] = { + current.value +: current.children.flatMap(rec) + } + rec(structure).tail + } + + js.Array( + menuItems.map(name => dom.document.getElementById(Controller.munge(name))) + .map((el) => () => offset(el, main)):_* + ) + } + + var open = false + def toggleOpen() = { + open = !open + if (open){ + def rec(tree: Tree[MenuNode])(f: MenuNode => Unit): Unit = { + f(tree.value) + tree.children.foreach(rec(_)(f)) + } + rec(domTrees)(setFullHeight) + }else{ + start(force = true) + } + } + + def setFullHeight(mn: MenuNode) = { + mn.frag + .children(1) + .asInstanceOf[dom.HTMLElement] + .style + .maxHeight = (mn.end - mn.start + 1) * 44 + "px" + } + private[this] var scrolling = false + private[this] var scrollTop = -1 + def apply(): Unit = { + if (!scrolling) { + scrolling = true + scrollTop = main.scrollTop + dom.setTimeout({() => + scrolling = false + if (scrollTop == main.scrollTop) start() + else apply() + }, + 75 + ) + } + } + private[this] var previousWin: MenuNode = null + private[this] def start(force: Boolean = false) = { + + def scroll(el: dom.Element) = { + val rect = el.getBoundingClientRect() + if (rect.top <= 0) + el.scrollIntoView(true) + else if (rect.top > dom.innerHeight) + el.scrollIntoView(false) + } + val scrollTop = main.scrollTop + def walkIndex(tree: Tree[MenuNode]): List[Tree[MenuNode]] = { + val t @ Tree(m, children) = tree + val win = if(m.start == -1) true + else { + val before = headers(m.start)() <= scrollTop + val after = (m.end >= headers.length) || headers(m.end)() > scrollTop + before && after + } + val childIndexes = children.map(walkIndex) + val childWin = childIndexes.indexWhere(_ != null) + if (childWin != -1) t :: childIndexes(childWin) + else if (win) List(t) + else null + } + + val winPath = walkIndex(domTrees) + val winItem = winPath.last.value + def walkTree(indices: List[Tree[MenuNode]]): Int = indices match { + case Nil => 0 + case (Tree(mn, children) :: rest) => + + mn.frag.classList.remove("hide") + mn.frag.classList.remove("selected") + + mn.frag.children(0).classList.add("pure-menu-selected") + for { + child <- children + if !indices.headOption.exists(_.value.frag == child.value.frag) + } walkHide(child) + + val size = walkTree(rest) + children.length + mn.frag.children(1).asInstanceOf[dom.HTMLElement].style.maxHeight = + if (!open) size * 44 + "px" else "none" + size + } + + def walkHide(tree: Tree[MenuNode]): Unit = { + val frag = tree.value.frag + + frag.children(0).classList.remove("pure-menu-selected") + frag.classList.add("hide") + + frag.children(1).asInstanceOf[dom.HTMLElement].style.maxHeight = + if (!open) "0px" else "none" + + if (tree.value.start < winItem.start) frag.classList.add("selected") + else frag.classList.remove("selected") + tree.children.foreach(walkHide) + } + + if (winItem != previousWin || force){ + scroll(winItem.frag.children(0)) + dom.history.replaceState(null, null, "#" + winItem.id) + previousWin = winItem +// println(winPath.map(_.value.id)) + walkTree(winPath) + } + } +} \ No newline at end of file diff --git a/project/build.sbt b/project/build.sbt index e04b021..489e892 100644 --- a/project/build.sbt +++ b/project/build.sbt @@ -4,4 +4,6 @@ addSbtPlugin("com.lihaoyi" % "utest-js-plugin" % "0.2.4") addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "0.7.4") -libraryDependencies += "org.eclipse.jgit" % "org.eclipse.jgit" % "3.5.1.201410131835-r" \ No newline at end of file +libraryDependencies += "org.eclipse.jgit" % "org.eclipse.jgit" % "3.5.1.201410131835-r" + +addSbtPlugin("com.lihaoyi" % "scalatex-sbt-plugin" % "0.1.0") diff --git a/scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala b/scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala deleted file mode 100644 index de9f039..0000000 --- a/scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala +++ /dev/null @@ -1,416 +0,0 @@ -package scalaParser -import acyclic.file -import language.implicitConversions -import syntax._ -import org.parboiled2._ - -/** - * Parser for Scala syntax. - * - * The `G` parameter that gets passed in to each rule stands for - * "Greedy", and determines whether or not that rule is to consume - * newlines after the last terminal in that rule. We need to pass it - * everywhere so it can go all the way to the last terminal deep - * inside the parse tree, which can then decide whether or not to - * consume whitespace. - * - * The vast majority of terminals will consume newlines; only rules - * which occur in {} blocks won't have their terminals consume newlines, - * and only the *last* terminal in the rule will be affected. - * That's why the parser does terminals-consume-newlines-by-default, - * and leaves it up to the dev to thread the `G` variable where-ever - * we want the opposite behavior. - */ -class ScalaSyntax(val input: ParserInput) extends Parser with Basic with Identifiers with Literals { - // Aliases for common things. These things are used in almost every parser - // in the file, so it makes sense to keep them short. - type B = Boolean - val t = true - type R0 = Rule0 - /** - * Parses all whitespace, excluding newlines. This is only - * really useful in e.g. {} blocks, where we want to avoid - * capturing newlines so semicolon-inference would work - */ - def WS = rule { zeroOrMore(Basic.WhitespaceChar | Literals.Comment) } - - /** - * Parses whitespace, including newlines. - * This is the default for most things - */ - def WL = rule{ zeroOrMore(Basic.WhitespaceChar | Literals.Comment | Basic.Newline) } - - - - /** - * By default, all strings and characters greedily - * capture all whitespace immediately after the token. - */ - implicit private[this] def wspStr(s: String): R0 = rule { WL ~ str(s) } - implicit private[this] def wspChar(s: Char): R0 = rule { WL ~ ch(s) } - - /** - * Most keywords don't just require the correct characters to match, - * they have to ensure that subsequent characters *don't* match in - * order for it to be a keyword. This enforces that rule for key-words - * (W) and key-operators (O) which have different non-match criteria. - */ - object K { - def W(s: String) = rule { - WL ~ Key.W(s) - } - - def O(s: String) = rule { - WL ~ Key.O(s) - } - } - - - def pos = cursor -> cursorChar - - /** - * helper printing function - */ - def pr(s: String) = rule { run(println(s"LOGGING $cursor: $s")) } - - def Id = rule { WL ~ Identifiers.Id } - def VarId = rule { WL ~ Identifiers.VarId } - def Literal = rule { WL ~ Literals.Literal } - def Semi = rule { WS ~ Basic.Semi } - def Semis = rule { oneOrMore(Semi) } - def Newline = rule { WL ~ Basic.Newline } - - def QualId = rule { WL ~ oneOrMore(Id).separatedBy('.') } - def Ids = rule { oneOrMore(Id) separatedBy ',' } - - def Path: R0 = rule { - zeroOrMore(Id ~ '.') ~ K.W("this") ~ zeroOrMore(Id).separatedBy('.') | - StableId - } - def StableId: R0 = rule { - zeroOrMore(Id ~ '.') ~ (K.W("this") | K.W("super") ~ optional(ClassQualifier)) ~ '.' ~ oneOrMore(Id).separatedBy('.') | - Id ~ zeroOrMore(WL ~ '.' ~ WL ~ Id) - } - - def ClassQualifier = rule { '[' ~ Id ~ ']' } - - def Type: R0 = rule { - FunctionArgTypes ~ K.O("=>") ~ Type | InfixType ~ optional(WL ~ ExistentialClause) - } - def FunctionArgTypes = rule { - InfixType | '(' ~ optional(oneOrMore(ParamType) separatedBy ',') ~ ')' - } - - def ExistentialClause = rule { "forSome" ~ '{' ~ oneOrMore(ExistentialDcl).separatedBy(Semi) } - def ExistentialDcl = rule { K.W("type") ~ TypeDcl | K.W("val") ~ ValDcl } - - def InfixType = rule { - CompoundType ~ zeroOrMore(WL ~ Id ~ optional(Newline) ~ CompoundType) - } - def CompoundType = rule { - oneOrMore(AnnotType).separatedBy(WL ~ K.W("with")) ~ optional(Refinement) - } - def AnnotType = rule { - SimpleType ~ zeroOrMore(WL ~ Annotation) - } - def SimpleType: R0 = rule { - BasicType ~ - optional(WL ~ '#' ~ Id) ~ - optional(WL ~ TypeArgs) - } - def BasicType: R0 = rule { - '(' ~ Types ~ ')' | - Path ~ '.' ~ K.W("type") | - StableId - } - def TypeArgs = rule { '[' ~ Types ~ "]" } - def Types = rule { oneOrMore(Type).separatedBy(',') } - def Refinement = rule { - optional(Newline) ~ '{' ~ oneOrMore(RefineStat).separatedBy(Semi) ~ "}" - } - def RefineStat = rule { "type" ~ TypeDef | Dcl | MATCH } - def TypePat = rule { CompoundType } - def Ascription = rule { - ":" ~ ("_" ~ "*" | InfixType | oneOrMore(Annotation)) - } - - def ParamType = rule { K.O("=>") ~ Type | Type ~ "*" | Type } - - def Expr: R0 = rule { - (Bindings | optional(K.W("implicit")) ~ Id | "_") ~ K.O("=>") ~ Expr | - Expr1 - } - def Expr1: R0 = rule { - IfCFlow | - WhileCFlow | - TryCFlow | - DoWhileCFlow | - ForCFlow | - K.W("throw") ~ Expr | - K.W("return") ~ optional(Expr) | - SimpleExpr ~ K.O("=") ~ Expr | - PostfixExpr ~ optional("match" ~ '{' ~ CaseClauses ~ "}" | Ascription) - - } - def IfCFlow = rule { "if" ~ '(' ~ Expr ~ ')' ~ zeroOrMore(Newline) ~ Expr ~ optional(optional(Semi) ~ K.W("else") ~ Expr) } - def WhileCFlow = rule { "while" ~ '(' ~ Expr ~ ')' ~ zeroOrMore(Newline) ~ Expr } - def TryCFlow = rule { - K.W("try") ~ Expr ~ - optional(WL ~ K.W("catch") ~ Expr) ~ - optional(WL ~ K.W("finally") ~ Expr) - } - - def DoWhileCFlow = rule { K.W("do") ~ Expr ~ optional(Semi) ~ "while" ~ '(' ~ Expr ~ ")" } - def ForCFlow = rule { - "for" ~ - ('(' ~ Enumerators ~ ')' | '{' ~ Enumerators ~ '}') ~ - zeroOrMore(Newline) ~ - optional(K.W("yield")) ~ - Expr } - def NotNewline: R0 = rule{ &( WS ~ noneOf("\n") )} - def PostfixExpr: R0 = rule { InfixExpr ~ optional(NotNewline ~ Id ~ optional(Newline)) } - def InfixExpr: R0 = rule { - PrefixExpr ~ - zeroOrMore( - NotNewline ~ - Id ~ - optional(Newline) ~ - PrefixExpr - ) - } - def PrefixExpr = rule { optional(WL ~ anyOf("-+~!")) ~ SimpleExpr } - - def SimpleExpr: R0 = rule { - SimpleExpr1 ~ - zeroOrMore(WL ~ ('.' ~ Id | TypeArgs | ArgumentExprs)) ~ - optional(WL ~ "_") - } - - def SimpleExpr1 = rule{ - K.W("new") ~ (ClassTemplate | TemplateBody) | - BlockExpr | - Literal | - Path | - K.W("_") | - '(' ~ optional(Exprs) ~ ")" - } - - - - def Exprs: R0 = rule { oneOrMore(Expr).separatedBy(',') } - def ArgumentExprs: R0 = rule { - '(' ~ optional(Exprs ~ optional(K.O(":") ~ K.W("_") ~ '*')) ~ ")" | - optional(Newline) ~ BlockExpr - } - - def BlockExpr: R0 = rule { '{' ~ (CaseClauses | Block) ~ "}" } - def BlockEnd: R0 = rule{ optional(Semis) ~ &("}" | "case") } - def Block: R0 = rule { - optional(Semis) ~ - ( - BlockStats ~ optional(Semis ~ ResultExpr) ~ BlockEnd | - ResultExpr ~ BlockEnd | - MATCH ~ BlockEnd - ) - } - def BlockStats: R0 = rule{ - oneOrMore(BlockStat).separatedBy(Semis) - } - def BlockStat: R0 = rule { - Import | - zeroOrMore(Annotation) ~ (optional(K.W("implicit") | K.W("lazy")) ~ Def | zeroOrMore(LocalModifier) ~ TmplDef) | - Expr1 - } - def ResultExpr: R0 = rule { - (Bindings | optional(K.W("implicit")) ~ Id | "_") ~ K.W("=>") ~ Block | Expr1 - } - def Enumerators: R0 = rule { Generator ~ zeroOrMore(Semi ~ Enumerator) ~ WL } - def Enumerator: R0 = rule { Generator | Guard | Pattern1 ~ K.O("=") ~ Expr } - def Generator: R0 = rule { Pattern1 ~ K.O("<-") ~ Expr ~ optional(WL ~ Guard) } - def CaseClauses: R0 = rule { oneOrMore(CaseClause) } - def CaseClause: R0 = rule { K.W("case") ~ Pattern ~ optional(Guard) ~ K.O("=>") ~ Block } - def Guard: R0 = rule { K.W("if") ~ PostfixExpr } - def Pattern: R0 = rule { - oneOrMore(Pattern1).separatedBy('|') - } - def Pattern1: R0 = rule { - K.W("_") ~ K.O(":") ~ TypePat | VarId ~ K.O(":") ~ TypePat | Pattern2 - } - def Pattern2: R0 = rule { - VarId ~ "@" ~ Pattern3 | Pattern3 | VarId - } - def Pattern3: R0 = rule { - SimplePattern ~ zeroOrMore(Id ~ SimplePattern) - } - def SimplePattern: R0 = rule { - K.W("_") | - Literal | - '(' ~ optional(Patterns) ~ ')' | - ( - StableId ~ - optional( - '(' ~ - (optional(Patterns ~ ',') ~ optional(VarId ~ '@') ~ K.W("_") ~ '*' | optional(Patterns)) ~ - ')' - ) - ) | - VarId - } - def Patterns: R0 = rule { K.W("_") ~ '*' | oneOrMore(Pattern).separatedBy(',') } - - def TypeParamClause: R0 = rule { '[' ~ oneOrMore(VariantTypeParam).separatedBy(',') ~ ']' } - def FunTypeParamClause: R0 = rule { '[' ~ oneOrMore(TypeParam).separatedBy(',') ~ ']' } - def VariantTypeParam: R0 = rule { zeroOrMore(Annotation) ~ optional(anyOf("+-")) ~ TypeParam } - def TypeParam: R0 = rule { - (Id | K.W("_")) ~ - optional(TypeParamClause) ~ - optional(K.O(">:") ~ Type) ~ - optional(K.O("<:") ~ Type) ~ - zeroOrMore(K.O("<%") ~ Type) ~ - zeroOrMore(K.O(":") ~ Type) - } - def ParamClauses: R0 = rule { zeroOrMore(ParamClause) ~ optional(optional(Newline) ~ '(' ~ K.W("implicit") ~ Params ~ ')') } - def ParamClause: R0 = rule { optional(Newline) ~ '(' ~ optional(Params) ~ ')' } - def Params: R0 = rule { zeroOrMore(Param).separatedBy(',') } - def Param: R0 = rule { zeroOrMore(Annotation) ~ Id ~ optional(K.O(":") ~ ParamType) ~ optional(K.O("=") ~ Expr) } - def ClassParamClauses: R0 = rule { zeroOrMore(ClassParamClause) ~ optional(optional(Newline) ~ '(' ~ K.W("implicit") ~ ClassParam ~ ")") } - def ClassParamClause: R0 = rule { optional(Newline) ~ '(' ~ optional(ClassParams) ~ ")" } - def ClassParams: R0 = rule { oneOrMore(ClassParam).separatedBy(',') } - def ClassParam: R0 = rule { zeroOrMore(Annotation) ~ optional(zeroOrMore(Modifier) ~ (K.W("val") | K.W("var"))) ~ Id ~ K.O(":") ~ ParamType ~ optional(K.O("=") ~ Expr) } - - def Bindings: R0 = rule { '(' ~ zeroOrMore(Binding).separatedBy(',') ~ ')' } - def Binding: R0 = rule { (Id | K.W("_")) ~ optional(K.O(":") ~ Type) } - - def Modifier: R0 = rule { LocalModifier | AccessModifier | K.W("override") } - def LocalModifier: R0 = rule { K.W("abstract") | K.W("final") | K.W("sealed") | K.W("implicit") | K.W("lazy") } - def AccessModifier: R0 = rule { (K.W("private") | K.W("protected")) ~ optional(AccessQualifier) } - def AccessQualifier: R0 = rule { '[' ~ (K.W("this") | Id) ~ ']' } - - def Annotation: R0 = rule { '@' ~ SimpleType ~ zeroOrMore(WL ~ ArgumentExprs) } - def ConstrAnnotation: R0 = rule { '@' ~ SimpleType ~ ArgumentExprs } - - def TemplateBody: R0 = rule { - '{' ~ - optional(SelfType) ~ - zeroOrMore(TemplateStat).separatedBy(Semis) ~ - '}' - } - def TemplateStat: R0 = rule { - Import | - zeroOrMore(Annotation ~ optional(Newline)) ~ zeroOrMore(Modifier) ~ (Def | Dcl) | - Expr - } - - def SelfType: R0 = rule { K.W("this") ~ K.O(":") ~ Type ~ K.O("=>") | Id ~ optional(K.O(":") ~ Type) ~ K.O("=>") } - - def Import: R0 = rule { K.W("import") ~ oneOrMore(ImportExpr).separatedBy(',') } - - def ImportExpr: R0 = rule { - StableId ~ optional('.' ~ ("_" | ImportSelectors)) - } - def ImportSelectors: R0 = rule { '{' ~ zeroOrMore(ImportSelector ~ ',') ~ (ImportSelector | K.W("_")) ~ "}" } - def ImportSelector: R0 = rule { Id ~ optional(K.O("=>") ~ (Id | K.W("_"))) } - - def Dcl: R0 = rule { - K.W("val") ~ ValDcl | - K.W("var") ~ VarDcl | - K.W("def") ~ FunDcl | - K.W("type") ~ zeroOrMore(Newline) ~ TypeDcl - } - def ValDcl: R0 = rule { Ids ~ K.O(":") ~ Type } - def VarDcl: R0 = rule { Ids ~ K.O(":") ~ Type } - def FunDcl: R0 = rule { FunSig ~ optional(WL ~ K.O(":") ~ Type) } - def FunSig: R0 = rule { Id ~ optional(FunTypeParamClause) ~ ParamClauses } - def TypeDcl: R0 = rule { - Id ~ - optional(WL ~ TypeParamClause) ~ - optional(WL ~ K.O(">:") ~ Type) ~ - optional(WL ~ K.O("<:") ~ Type) - } - - def PatVarDef: R0 = rule { K.W("val") ~ PatDef | K.W("var") ~ VarDef } - def Def: R0 = rule { K.W("def") ~ FunDef | K.W("type") ~ zeroOrMore(Newline) ~ TypeDef | PatVarDef | TmplDef } - def PatDef: R0 = rule { oneOrMore(Pattern2).separatedBy(',') ~ optional(K.O(":") ~ Type) ~ K.O("=") ~ Expr } - def VarDef: R0 = rule { Ids ~ K.O(":") ~ Type ~ K.O("=") ~ K.W("_") | PatDef } - def FunDef: R0 = rule { - K.W("this") ~ ParamClause ~ ParamClauses ~ (K.O("=") ~ ConstrExpr | optional(Newline) ~ ConstrBlock) | - FunSig ~ - ( - optional(K.O(":") ~ Type) ~ K.O("=") ~ optional(K.W("macro")) ~ Expr | - optional(Newline) ~ '{' ~ Block ~ "}" - ) - } - def TypeDef: R0 = rule { Id ~ optional(TypeParamClause) ~ K.O("=") ~ Type } - - def TmplDef: R0 = rule { - K.W("trait") ~ TraitDef | - optional(K.W("case")) ~ (K.W("class") ~ ClassDef | - K.W("object") ~ ObjectDef) - } - def ClassDef: R0 = rule { - Id ~ - optional(TypeParamClause) ~ - zeroOrMore(ConstrAnnotation) ~ - optional(AccessModifier) ~ - ClassParamClauses ~ - ClassTemplateOpt - } - def TraitDef: R0 = rule { Id ~ optional(TypeParamClause) ~ TraitTemplateOpt } - def ObjectDef: R0 = rule { Id ~ ClassTemplateOpt } - def ClassTemplateOpt: R0 = rule { - WL ~ K.W("extends") ~ ClassTemplate | - optional(WL ~ optional(K.W("extends")) ~ TemplateBody) - } - def TraitTemplateOpt: R0 = rule { K.W("extends") ~ TraitTemplate | optional(optional(K.W("extends")) ~ TemplateBody) } - def ClassTemplate: R0 = rule { - optional(EarlyDefs) ~ - ClassParents ~ - optional(WL ~ TemplateBody) - } - - def TraitTemplate: R0 = rule { - optional(EarlyDefs) ~ TraitParents ~ optional(TemplateBody) - } - def ClassParents: R0 = rule { - Constr ~ zeroOrMore(WL ~ K.W("with") ~ AnnotType) - } - def TraitParents: R0 = rule { - AnnotType ~ zeroOrMore(WL ~ K.W("with") ~ AnnotType) - } - def Constr: R0 = rule { - AnnotType ~ zeroOrMore(WL ~ ArgumentExprs) - } - def EarlyDefs: R0 = rule { - '{' ~ optional(oneOrMore(EarlyDef).separatedBy(Semis)) ~ '}' ~ K.W("with") - } - def EarlyDef: R0 = rule { - zeroOrMore(Annotation ~ optional(Newline)) ~ zeroOrMore(Modifier) ~ PatVarDef - } - def ConstrExpr: R0 = rule { ConstrBlock | SelfInvocation } - def ConstrBlock: R0 = rule { '{' ~ SelfInvocation ~ zeroOrMore(Semis ~ BlockStat) ~ '}' } - def SelfInvocation: R0 = rule { K.W("this") ~ oneOrMore(ArgumentExprs) } - - def TopStatSeq: R0 = rule { oneOrMore(TopStat).separatedBy(Semis) } - def TopStat: R0 = rule { - Packaging | - PackageObject | - Import | - zeroOrMore(Annotation ~ optional(Newline)) ~ zeroOrMore(Modifier) ~ TmplDef - } - def Packaging: R0 = rule { K.W("package") ~ QualId ~ '{' ~ TopStatSeq ~ '}' } - def PackageObject: R0 = rule { K.W("package") ~ K.W("object") ~ ObjectDef } - def TopPackageSeq: R0 = rule{ - oneOrMore(K.W("package") ~ QualId).separatedBy(Semis) - } - def CompilationUnit: Rule1[String] = rule { - capture( - pr("CompulationUnit 0") ~ - optional(Semis) ~ - pr("CompulationUnit 1") ~ - (TopPackageSeq ~ optional(Semis ~ TopStatSeq) | TopStatSeq) ~ - optional(Semis) ~ - WL - - ) - } -} diff --git a/scalaParser/src/main/scala/scalaParser/syntax/Basic.scala b/scalaParser/src/main/scala/scalaParser/syntax/Basic.scala deleted file mode 100644 index 8d3232a..0000000 --- a/scalaParser/src/main/scala/scalaParser/syntax/Basic.scala +++ /dev/null @@ -1,51 +0,0 @@ -package scalaParser -package syntax -import acyclic.file -import org.parboiled2._ - -trait Basic { self: Parser => - object Basic{ - def UnicodeExcape = rule { "\\u" ~ 4.times(HexDigit) } - - - //Numbers and digits - def HexDigit = rule { Digit | "a" - "f" | "A" - "Z" } - def Digit = rule { "0" | NonZeroDigit } - def NonZeroDigit = rule { "1" - "9" } - def HexNumeral = rule { "0x" ~ oneOrMore(HexDigit) } - def DecimalNumeral = rule(oneOrMore(Digit)) - def ExponentPart = rule { anyOf("Ee") ~ optional(anyOf("+-")) ~ oneOrMore(Digit) } - def FloatType = rule { anyOf("FfDd") } - - def Parentheses = rule { "(" | ")" | "[" | "]" | "{" | "}" } - def DelimiterChar = rule { "'" | "\"" | "." | ";" | "," } - - def WhitespaceChar = rule { "\u0020" | "\u0009" } - def Newline = rule { "\r\n" | "\n" } - def Semi = rule { ';' | oneOrMore(Newline) } - def OperatorChar = rule { - anyOf("""!#$%&*+-/:<=>?@\^|~""") | - CharPredicate.from(_.getType match { - case Character.OTHER_SYMBOL | Character.MATH_SYMBOL => true; case _ => false - }) - } - def Letter = rule { Upper | Lower | CharPredicate.from(c => c.isLetter | c.isDigit) } - def Lower = rule { "a" - "z" | "$" | "_" | CharPredicate.from(_.isLower) } - def Upper = rule { "A" - "Z" | CharPredicate.from(_.isUpper) } - } - /** - * Most keywords don't just require the correct characters to match, - * they have to ensure that subsequent characters *don't* match in - * order for it to be a keyword. This enforces that rule for key-words - * (W) and key-operators (O) which have different non-match criteria. - */ - object Key { - def W(s: String) = rule { - str(s) ~ !(Basic.Letter | Basic.Digit) - } - - def O(s: String) = rule { - str(s) ~ !Basic.OperatorChar - } - } -} diff --git a/scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala b/scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala deleted file mode 100644 index 4bc972f..0000000 --- a/scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala +++ /dev/null @@ -1,35 +0,0 @@ -package scalaParser -package syntax -import acyclic.file -import org.parboiled2._ - -trait Identifiers { self: Parser with Basic => - object Identifiers{ - import Basic._ - def Operator = rule(oneOrMore(OperatorChar)) - - def VarId = rule { - !(Keywords ~ (WhitespaceChar | Newline | "//" | "/*")) ~ Lower ~ IdRest - } - def PlainId = rule { Upper ~ IdRest | VarId | !(Keywords ~ (WhitespaceChar | Newline | "//" | "/*")) ~ Operator } - def Id = rule { PlainId | ("`" ~ oneOrMore(noneOf("`")) ~ "`") } - def IdRest = rule { - zeroOrMore(zeroOrMore("_") ~ oneOrMore(!"_" ~ Letter | Digit)) ~ - optional(oneOrMore("_") ~ optional(Operator)) - } - - - def AlphabetKeywords = rule { - "abstract" | "case" | "catch" | "class" | "def" | "do" | "else" | "extends" | "false" | "finally" | "final" | "finally" | "forSome" | "for" | "if" | - "implicit" | "import" | "lazy" | "match" | "new" | "null" | "object" | "override" | "package" | "private" | "protected" | "return" | - "sealed" | "super" | "this" | "throw" | "trait" | "try" | "true" | "type" | "val" | "var" | "while" | "with" | "yield" | "_" - } - def SymbolicKeywords = rule{ - ":" | ";" | "=>" | "=" | "<-" | "<:" | "<%" | ">:" | "#" | "@" | "\u21d2" | "\u2190" - } - def Keywords = rule { - AlphabetKeywords ~ !Letter | SymbolicKeywords ~ !OperatorChar - - } - } -} diff --git a/scalaParser/src/main/scala/scalaParser/syntax/Literals.scala b/scalaParser/src/main/scala/scalaParser/syntax/Literals.scala deleted file mode 100644 index 9fd9d5b..0000000 --- a/scalaParser/src/main/scala/scalaParser/syntax/Literals.scala +++ /dev/null @@ -1,58 +0,0 @@ -package scalaParser -package syntax -import acyclic.file -import org.parboiled2._ - -trait Literals { self: Parser with Basic with Identifiers => - object Literals{ - import Basic._ - def FloatingPointLiteral = rule { - - "." ~ oneOrMore(Digit) ~ optional(ExponentPart) ~ optional(FloatType) | - oneOrMore(Digit) ~ ( - "." ~ oneOrMore(Digit) ~ optional(ExponentPart) ~ optional(FloatType) | - ExponentPart ~ optional(FloatType) | - optional(ExponentPart) ~ FloatType - ) - } - - def IntegerLiteral = rule { (DecimalNumeral | HexNumeral) ~ optional(anyOf("Ll")) } - - def BooleanLiteral = rule { Key.W("true") | Key.W("false") } - - def MultilineComment: Rule0 = rule { "/*" ~ zeroOrMore(MultilineComment | !"*/" ~ ANY) ~ "*/" } - def Comment: Rule0 = rule { - MultilineComment | - "//" ~ zeroOrMore(!Basic.Newline ~ ANY) ~ &(Basic.Newline | EOI) - } - - def Literal = rule { - (optional("-") ~ (FloatingPointLiteral | IntegerLiteral)) | - BooleanLiteral | - CharacterLiteral | - StringLiteral | - SymbolLiteral | - (Key.W("null") ~ !(Basic.Letter | Basic.Digit)) - } - - - def EscapedChars = rule { '\\' ~ anyOf("rnt\\\"") } - - // Note that symbols can take on the same values as keywords! - def SymbolLiteral = rule { ''' ~ (Identifiers.PlainId | Identifiers.Keywords) } - - def CharacterLiteral = rule { ''' ~ (UnicodeExcape | EscapedChars | !'\\' ~ CharPredicate.from(isPrintableChar)) ~ ''' } - - def MultiLineChars = rule { zeroOrMore(optional('"') ~ optional('"') ~ noneOf("\"")) } - def StringLiteral = rule { - (optional(Identifiers.Id) ~ "\"\"\"" ~ MultiLineChars ~ ("\"\"\"" ~ zeroOrMore('"'))) | - (optional(Identifiers.Id) ~ '"' ~ zeroOrMore("\\\"" | noneOf("\n\"")) ~ '"') - } - - def isPrintableChar(c: Char): Boolean = { - val block = Character.UnicodeBlock.of(c) - !Character.isISOControl(c) && !Character.isSurrogate(c) && block != null && block != Character.UnicodeBlock.SPECIALS - } - } -} - diff --git a/scalaParser/src/test/scala/scalaParser/SyntaxTest.scala b/scalaParser/src/test/scala/scalaParser/SyntaxTest.scala deleted file mode 100644 index fe5fc2c..0000000 --- a/scalaParser/src/test/scala/scalaParser/SyntaxTest.scala +++ /dev/null @@ -1,448 +0,0 @@ -package scalaParser - -import org.parboiled2.ParseError -import utest._ -import utest.framework.Test -import utest.util.Tree - -import scala.util.{Failure, Success} - -object SyntaxTest extends TestSuite{ - def check[T](input: String) = { - println("Checking...") - new ScalaSyntax(input).CompilationUnit.run() match{ - case Failure(f: ParseError) => - println(f.position) - println(f.formatExpectedAsString) - println(f.formatTraces) - throw new Exception(f.position + "\t" + f.formatTraces) - case Success(parsed) => - assert(parsed == input) - } - } - println("running") - def tests = TestSuite{ - 'unit { - * - check( - "package torimatomeru" - - ) - * - check( - """package torimatomeru - | - |package lols - """.stripMargin - ) - * - check( - """package torimatomeru - |import a - |import b - """.stripMargin - ) - * - check( - """ - |package torimatomeru - | - |import org.parboiled2.ParseError - |import utest._ - |import utest.framework.Test - |import utest.util.Tree - | - |import scala.util.{Failure, Success} - | - |object SyntaxTest extends TestSuite - """.stripMargin - ) - * - check( - """ - |object SyntaxTest extends TestSuite{ - | def check[T](input: String) = { - | - | } - |} - """.stripMargin - ) - * - check( - """ - |object SyntaxTest{ - | a() - | throw 1 - |} - """.stripMargin - ) - * - check( - """ - |object SyntaxTest extends TestSuite{ - | { - | println - | throw 1 - | } - |} - """.stripMargin - ) - * - check( - """package scalatex - | - | - |import org.parboiled2._ - |import torimatomeru.ScalaSyntax - | - |import scalatex.stages.{Trim, Parser, Ast} - |import scalatex.stages.Ast.Block.{IfElse, For, Text} - |import Ast.Chain.Args - | - |object ParserTests extends utest.TestSuite{ - | import Ast._ - | import utest._ - | def check[T](input: String, parse: Parser => scala.util.Try[T], expected: T) = { - | val parsed = parse(new Parser(input)).get - | assert(parsed == expected) - | } - | def tests = TestSuite{} - |} - """.stripMargin - ) - * - check( - """ - |object Moo{ - | a - | .b - | - | c - |} - """.stripMargin - ) - * - check( - """ - |object Moo{ - | filename - | .asInstanceOf[Literal] - |10 - |} - """.stripMargin - ) - * - check( - """ - |object Cow{ - | ().mkString - | - | 1 - |} - """.stripMargin - ) - * - check( - """ - |object O{ - | private[this] val applyMacroFull = 1 - |} - """.stripMargin - ) - * - check( - """ - |object O{ - | private[this] def applyMacroFull(c: Context) - | (expr: c.Expr[String], - | runtimeErrors: Boolean, - | debug: Boolean) - | : c.Expr[Frag] = { - | } - |} - """.stripMargin - ) - * - check( - """ - |object O{ - | class DebugFailure extends Exception - | - | 1 - |} - """.stripMargin - ) - * - check( - """ - |package torimatomeru - | - |package syntax - | - |import org.parboiled2._ - | - """.stripMargin - ) - * - check( - """ - |object Foo{ - | 0 match { - | case A | B => 0 - | } - |} - """.stripMargin - ) - * - check( - """ - |object Compiler{ - | - | def apply = { - | def rec = t match { - | case 0 => 0 - | } - | - | rec(tree) - | } - |} - | - """.stripMargin - ) - * - check( - """ - |object O { - | A(A(A(A(A(A(A(A()))))))) - |} - | - """.stripMargin - ) - * - check( - """ - |object O{ - | A(A(A(A(A(A(A(A(A(A(A(A(A(A(A(A()))))))))))))))) - |} - """.stripMargin - ) - * - check( - """ - |object L{ - | a.b = c - | a().b = c - |} - """.stripMargin - ) - * - check( - """ - |object L{ - | a b c - | d = 1 - |} - """.stripMargin - ) - - * - check( - """/* __ *\ - |** ________ ___ / / ___ __ ____ Scala.js CLI ** - |** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** - |** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** - |** /____/\___/_/ |_/____/_/ | |__/ /____/ ** - |** |/____/ ** - |\* */ - | - |package scala.scalajs.cli - | - """.stripMargin - ) - * - check( - """ - |object O{ - | for { - | a <- b - | c <- d - | } { - | 1 - | } - |} - """.stripMargin - ) - * - check( - """ - |object O{ - | val jarFile = - | try { 1 } - | catch { case _: F => G } - |} - """.stripMargin - ) - * - check( - """ - |object F{ - | func{ case _: F => fail } - |} - """.stripMargin - ) - * - check( - """ - |object Foo{ - | val a = d // g - | val b = e // h - | val c = f - |} - """.stripMargin - ) - * - check( - """ - |object L{ - | x match{ - | case y.Y(z) => z - | } - |} - """.stripMargin - ) - * - check( - """object K{ - | val a: B { - | val c: D - | } - | - | 1 - |} - """.stripMargin - ) - * - check( - """ - |object LOLS{ - | def run() {} - | - | def apply() {} - |} - """.stripMargin - ) - * - check( - """ - |object O{ - | a =:= b.c - |} - """.stripMargin - ) - * - check( - """ - |object K{ - | a( - | 1: _* - | ) - |} - """.stripMargin - ) - * - check( - """ - |object P{ - | tree match { - | case stats :+ expr => 1 - | } - |} - """.stripMargin - ) - * - check( - """ - |object K{ - | val trueA = 1 - |} - """.stripMargin - ) - * - check( - """ - |object K{ - | val nullo :: cow = 1 - |} - """.stripMargin - ) - * - check( - """ - |object K{ - | val omg_+ = 1 - |} - """.stripMargin - ) - * - check( - """ - |object K{ - | val + = 1 - | var * = 2 - |} - """.stripMargin - ) - * - check( - """ - |object O{ - | c match { - | case b_ => 1 - | } - |} - """.stripMargin - ) - * - check( - """ - |trait Basic { - | b match { - | case C => true; case _ => false - | } - |} - """.stripMargin - ) - * - check( - """trait Basic { - | !a.b - |} - """.stripMargin - ) - * - check( - """ - |class Parser { - | {() => } - |} - | - """.stripMargin - ) - * - check( - """ - | - | - | - |package omg - |; - | - |; - | - |; - |class Parser - |; - | - |; - | - |; - """.stripMargin - ) - } - def checkFile(path: String) = check(io.Source.fromFile(path).mkString) - 'file{ - - * - checkFile("test.txt") - * - checkFile("scalaParser/src/main/scala/scalaParser/syntax/Basic.scala") - * - checkFile("scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala") - * - checkFile("scalaParser/src/main/scala/scalaParser/syntax/Literals.scala") - * - checkFile("scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala") - - * - checkFile("scalaParser/src/test/scala/scalaParser/SyntaxTest.scala") - - - * - checkFile("scalatexApi/src/main/scala/scalatex/stages/Compiler.scala") - * - checkFile("scalatexApi/src/main/scala/scalatex/stages/Parser.scala") - * - checkFile("scalatexApi/src/main/scala/scalatex/stages/Trim.scala") - * - checkFile("scalatexApi/src/main/scala/scalatex/package.scala") - - * - checkFile("scalatexApi/src/test/scala/scalatex/ParserTests.scala") - * - checkFile("scalatexApi/src/test/scala/scalatex/BasicTests.scala") - * - checkFile("scalatexApi/src/test/scala/scalatex/ErrorTests.scala") - * - checkFile("scalatexApi/src/test/scala/scalatex/TestUtil.scala") - - * - checkFile("scalatexPlugin/src/main/scala/scalatex/ScalaTexPlugin.scala") - } - -// 'omg{ -// val root = new java.io.File("../scala-js/") -// def listFiles(s: java.io.File): Iterator[String] = { -// val (dirs, files) = s.listFiles().toIterator.partition(_.isDirectory) -// files.map(_.getPath) ++ dirs.flatMap(listFiles) -// } -// for(f <- listFiles(root).filter(_.endsWith(".scala"))){ -// println("CHECKING " + f) -// checkFile(f) -// } -// } - } -} \ No newline at end of file diff --git a/scalatex/api/src/main/scala/scalatex/package.scala b/scalatex/api/src/main/scala/scalatex/package.scala new file mode 100644 index 0000000..1f13e63 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/package.scala @@ -0,0 +1,98 @@ +import scala.reflect.internal.util.{BatchSourceFile, SourceFile, OffsetPosition} +import scala.reflect.io.{PlainFile, AbstractFile} +import scala.reflect.macros.{TypecheckException, Context} +import scalatags.Text.all._ +import scalatex.stages.{Parser, Compiler} +import scala.language.experimental.macros +import acyclic.file + +package object scalatex { + /** + * Wraps the given string as a twist fragment. + */ + def tw(expr: String): Frag = macro Internals.applyMacro + def twf(filename: String): Frag = macro Internals.applyMacroFile + object Internals { + + def twRuntimeErrors(expr: String): Frag = macro applyMacroRuntimeErrors + def twDebug(expr: String): Frag = macro applyMacroDebug + + def applyMacro(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, false, false) + def applyMacroDebug(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, false, true) + + def applyMacroRuntimeErrors(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, true, false) + + def applyMacroFile(c: Context)(filename: c.Expr[String]): c.Expr[Frag] = { + import c.universe._ + val fileName = filename.tree + .asInstanceOf[Literal] + .value + .value + .asInstanceOf[String] + val txt = io.Source.fromFile(fileName).mkString + val sourceFile = new BatchSourceFile( + new PlainFile(fileName), + txt.toCharArray + ) + + compileThing(c)(txt, sourceFile, 0, false, false) + } + + case class DebugFailure(msg: String, pos: String) extends Exception(msg) + + private[this] def applyMacroFull(c: Context) + (expr: c.Expr[String], + runtimeErrors: Boolean, + debug: Boolean) + : c.Expr[Frag] = { + import c.universe._ + val scalatexFragment = expr.tree + .asInstanceOf[Literal] + .value + .value + .asInstanceOf[String] + val stringStart = + expr.tree + .pos + .lineContent + .drop(expr.tree.pos.column) + .take(2) + compileThing(c)( + scalatexFragment, + expr.tree.pos.source, + expr.tree.pos.point + (if (stringStart == "\"\"") 1 else -1), + runtimeErrors, + debug + ) + } + } + + def compileThing(c: Context) + (scalatexSource: String, + source: SourceFile, + point: Int, + runtimeErrors: Boolean, + debug: Boolean) = { + import c.universe._ + def compile(s: String): c.Tree = { + val realPos = new OffsetPosition(source, point).asInstanceOf[c.universe.Position] + + Compiler(c)(realPos, Parser.tupled(stages.Trim(s))) + } + + + import c.Position + try { + val compiled = compile(scalatexSource) + if (debug) println(compiled) + c.Expr[Frag](c.typeCheck(compiled)) + } catch { + case e@TypecheckException(pos: Position, msg) => + if (!runtimeErrors) c.abort(pos, msg) + else { + val posMsg = pos.lineContent + "\n" + (" " * pos.column) + "^" + c.Expr( q"""throw scalatex.Internals.DebugFailure($msg, $posMsg)""") + } + } + } +} diff --git a/scalatex/api/src/main/scala/scalatex/stages/Compiler.scala b/scalatex/api/src/main/scala/scalatex/stages/Compiler.scala new file mode 100644 index 0000000..3df8da7 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/stages/Compiler.scala @@ -0,0 +1,103 @@ +package scalatex +package stages + +import acyclic.file + +import scala.reflect.macros.whitebox.Context +import scala.reflect.internal.util.{Position, OffsetPosition} + +/** + * Walks the parsed AST, converting it into a structured Scala c.Tree + */ +object Compiler{ + + def apply(c: Context)(fragPos: c.Position, template: Ast.Block): c.Tree = { + + import c.universe._ + def fragType = tq"scalatags.Text.all.Frag" + + def incPosRec(trees: c.Tree, offset: Int): trees.type = { + + trees.foreach(incPos(_, offset)) + trees + } + def incPos(tree: c.Tree, offset: Int): tree.type = { + + val current = if (tree.pos == NoPosition) 0 else tree.pos.point + c.internal.setPos(tree, + new OffsetPosition( + fragPos.source, + offset + current + fragPos.point + ).asInstanceOf[c.universe.Position] + ) + tree + } + + def compileChain(code: String, parts: Seq[Ast.Chain.Sub], offset: Int): c.Tree = { + + val out = parts.foldLeft(incPosRec(c.parse(code), offset + 1)){ + case (curr, Ast.Chain.Prop(str, offset2)) => + incPos(q"$curr.${TermName(str)}", offset2 + 1) + case (curr, Ast.Chain.Args(str, offset2)) => + val Apply(fun, args) = c.parse(s"omg$str") + incPos(Apply(curr, args.map(incPosRec(_, offset2 - 2))), offset2) + case (curr, Ast.Chain.TypeArgs(str, offset2)) => + val TypeApply(fun, args) = c.parse(s"omg$str") + incPos(TypeApply(curr, args.map(incPosRec(_, offset2 - 2))), offset2) + case (curr, Ast.Block(parts, offset1)) => + incPos(q"$curr(..${compileBlock(parts, offset1)})", offset1) + case (curr, Ast.Header(header, block, offset1)) => + incPos(q"$curr(${compileHeader(header, block, offset1)})", offset1) + } + + out + } + def compileBlock(parts: Seq[Ast.Block.Sub], offset: Int): Seq[c.Tree] = { + val res = parts.map{ + case Ast.Block.Text(str, offset1) => + incPos(q"$str", offset1) + case Ast.Chain(code, parts, offset1) => + compileChain(code, parts, offset1) + case Ast.Header(header, block, offset1) => + compileHeader(header, block, offset1) + case Ast.Block.IfElse(condString, Ast.Block(parts2, offset2), elseBlock, offset1) => + val If(cond, _, _) = c.parse(condString + "{}") + val elseCompiled = elseBlock match{ + case Some(Ast.Block(parts3, offset3)) => compileBlockWrapped(parts3, offset3) + case None => EmptyTree + } + + val res = If(incPosRec(cond, offset1 + 2), compileBlockWrapped(parts2, offset2), elseCompiled) + + incPos(res, offset1) + res + case Ast.Block.For(generators, Ast.Block(parts2, offset2), offset1) => + val fresh = c.fresh() + + val tree = incPosRec(c.parse(s"$generators yield $fresh"), offset1 + 2) + + def rec(t: Tree): Tree = t match { + case a @ Apply(fun, List(f @ Function(vparams, body))) => + val f2 = Function(vparams, rec(body)) + val a2 = Apply(fun, List(f2)) + a2 + case Ident(x: TermName) if x.decoded == fresh => + compileBlockWrapped(parts2, offset2) + } + + rec(tree) + } + res + } + def compileBlockWrapped(parts: Seq[Ast.Block.Sub], offset: Int): c.Tree = { + incPos(q"Seq[$fragType](..${compileBlock(parts, offset)})", offset) + } + def compileHeader(header: String, block: Ast.Block, offset: Int): c.Tree = { + val Block(stmts, expr) = c.parse(s"{$header\n ()}") + Block(stmts, compileBlockWrapped(block.parts, block.offset)) + } + + val res = compileBlockWrapped(template.parts, template.offset) + res + } +} \ No newline at end of file diff --git a/scalatex/api/src/main/scala/scalatex/stages/Omg/scala.scala b/scalatex/api/src/main/scala/scalatex/stages/Omg/scala.scala new file mode 100644 index 0000000..81fa1f9 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/stages/Omg/scala.scala @@ -0,0 +1,8 @@ +package scalatex.stages.Omg + +/** + * Created by haoyi on 12/3/14. + */ +class scala { + +} diff --git a/scalatex/api/src/main/scala/scalatex/stages/Parser.scala b/scalatex/api/src/main/scala/scalatex/stages/Parser.scala new file mode 100644 index 0000000..0b87d97 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/stages/Parser.scala @@ -0,0 +1,170 @@ +package scalatex +package stages +import acyclic.file +import org.parboiled2._ +import scalaParser.ScalaSyntax + +/** + * Parses the input text into a roughly-structured AST. This AST + * is much simpler than the real Scala AST, but serves us well + * enough until we stuff the code-strings into the real Scala + * parser later + */ +object Parser extends ((String, Int) => Ast.Block){ + def apply(input: String, offset: Int = 0): Ast.Block = { + new Parser(input, offset).Body.run().get + } +} +class Parser(input: ParserInput, indent: Int = 0, offset: Int = 0) extends scalaParser.ScalaSyntax(input) { + def offsetCursor = offset + cursor + val txt = input.sliceString(0, input.length) + val indentTable = txt.split('\n').map{ s => + if (s.trim == "") -1 + else s.takeWhile(_ == ' ').length + } + val nextIndentTable = (0 until indentTable.length).map { i => + val index = indentTable.indexWhere(_ != -1, i + 1) + if (index == -1) 100000 + else indentTable(index) + } + def cursorNextIndent() = { + nextIndentTable(txt.take(cursor).count(_ == '\n')) + } + + def TextNot(chars: String) = rule { + push(offsetCursor) ~ capture(oneOrMore(noneOf(chars + "\n") | "@@")) ~> { + (i, x) => Ast.Block.Text(x.replace("@@", "@"), i) + } + } + def Text = TextNot("@") + def Code = rule { + "@" ~ capture(Identifiers.Id | BlockExpr2 | ('(' ~ optional(Exprs) ~ ')')) + } + def Header = rule { + "@" ~ capture(Def | Import) + } + + def HeaderBlock: Rule1[Ast.Header] = rule{ + Header ~ zeroOrMore(capture(WL) ~ Header ~> (_ + _)) ~ runSubParser{new Parser(_, indent, cursor).Body0} ~> { + (start: String, heads: Seq[String], body: Ast.Block) => Ast.Header(start + heads.mkString, body) + } + } + + def BlankLine = rule{ '\n' ~ zeroOrMore(' ') ~ &('\n') } + def IndentSpaces = rule{ indent.times(' ') ~ zeroOrMore(' ') } + def Indent = rule{ '\n' ~ IndentSpaces } + def LoneScalaChain: Rule2[Ast.Block.Text, Ast.Chain] = rule { + (push(offsetCursor) ~ capture(Indent) ~> ((i, x) => Ast.Block.Text(x, i))) ~ + ScalaChain ~ + IndentBlock ~> { + (chain: Ast.Chain, body: Ast.Block) => chain.copy(parts = chain.parts :+ body) + } + } + def IndentBlock = rule{ + &("\n") ~ + test(cursorNextIndent() > indent) ~ + runSubParser(new Parser(_, cursorNextIndent(), cursor).Body) + } + def IfHead = rule{ "@" ~ capture("if" ~ "(" ~ Expr ~ ")") } + def IfElse1 = rule{ + push(offsetCursor) ~ IfHead ~ BraceBlock ~ optional("else" ~ (BraceBlock | IndentBlock)) + } + def IfElse2 = rule{ + Indent ~ push(offsetCursor) ~ IfHead ~ IndentBlock ~ optional(Indent ~ "@else" ~ (BraceBlock | IndentBlock)) + } + def IfElse = rule{ + (IfElse1 | IfElse2) ~> ((a, b, c, d) => Ast.Block.IfElse(b, c, d, a)) + } + + def ForHead = rule{ + push(offsetCursor) ~ "@" ~ capture("for" ~ '(' ~ Enumerators ~ ')') + } + def ForLoop = rule{ + ForHead ~ + BraceBlock ~> ((a, b, c) => Ast.Block.For(b, c, a)) + } + def LoneForLoop = rule{ + (push(offsetCursor) ~ capture(Indent) ~> ((i, t) => Ast.Block.Text(t, i))) ~ + ForHead ~ + IndentBlock ~> + ((a, b, c) => Ast.Block.For(b, c, a)) + } + + def ScalaChain = rule { + push(offsetCursor) ~ Code ~ zeroOrMore(Extension) ~> { (a, b, c) => Ast.Chain(b, c, a)} + } + def Extension: Rule1[Ast.Chain.Sub] = rule { + (push(offsetCursor) ~ '.' ~ capture(Identifiers.Id) ~> ((x, y) => Ast.Chain.Prop(y, x))) | + (push(offsetCursor) ~ capture(TypeArgs2) ~> ((x, y) => Ast.Chain.TypeArgs(y, x))) | + (push(offsetCursor) ~ capture(ArgumentExprs2) ~> ((x, y) => Ast.Chain.Args(y, x))) | + BraceBlock + } + def Ws = WL + // clones of the version in ScalaSyntax, but without tailing whitespace or newlines + def TypeArgs2 = rule { '[' ~ Ws ~ Types ~ ']' } + def ArgumentExprs2 = rule { + '(' ~ Ws ~ + (optional(Exprs ~ ',' ~ Ws) ~ PostfixExpr ~ ':' ~ Ws ~ '_' ~ Ws ~ '*' ~ Ws | optional(Exprs) ) ~ + ')' + } + def BlockExpr2: Rule0 = rule { '{' ~ Ws ~ (CaseClauses | Block) ~ Ws ~ '}' } + def BraceBlock: Rule1[Ast.Block] = rule{ '{' ~ BodyNoBrace ~ '}' } + + def BodyItem(exclusions: String): Rule1[Seq[Ast.Block.Sub]] = rule{ + ForLoop ~> (Seq(_)) | + LoneForLoop ~> (Seq(_, _)) | + IfElse ~> (Seq(_)) | + LoneScalaChain ~> (Seq(_, _)) | + HeaderBlock ~> (Seq(_)) | + TextNot("@" + exclusions) ~> (Seq(_)) | + (push(offsetCursor) ~ capture(Indent) ~> ((i, x) => Seq(Ast.Block.Text(x, i)))) | + (push(offsetCursor) ~ capture(BlankLine) ~> ((i, x) => Seq(Ast.Block.Text(x, i)))) | + ScalaChain ~> (Seq(_: Ast.Block.Sub)) + } + def Body = rule{ BodyEx() } + def BodyNoBrace = rule{ BodyEx("}") } + def BodyEx(exclusions: String = "") = rule{ + push(offsetCursor) ~ oneOrMore(BodyItem(exclusions)) ~> {(i, x) => + Ast.Block(x.flatten, i) + } + } + def Body0 = rule{ + push(offsetCursor) ~ zeroOrMore(BodyItem("")) ~> {(i, x) => + Ast.Block(x.flatten, i) + } + } +} + +trait Ast{ + def offset: Int +} +object Ast{ + + /** + * @param parts The various bits of text and other things which make up this block + * @param offset + */ + case class Block(parts: Seq[Block.Sub], + offset: Int = 0) + extends Chain.Sub with Block.Sub + object Block{ + trait Sub extends Ast + case class Text(txt: String, offset: Int = 0) extends Block.Sub + case class For(generators: String, block: Block, offset: Int = 0) extends Block.Sub + case class IfElse(condition: String, block: Block, elseBlock: Option[Block], offset: Int = 0) extends Block.Sub + } + case class Header(front: String, block: Block, offset: Int = 0) extends Block.Sub with Chain.Sub + + /** + * @param lhs The first expression in this method-chain + * @param parts A list of follow-on items chained to the first + * @param offset + */ + case class Chain(lhs: String, parts: Seq[Chain.Sub], offset: Int = 0) extends Block.Sub + object Chain{ + trait Sub extends Ast + case class Prop(str: String, offset: Int = 0) extends Sub + case class TypeArgs(str: String, offset: Int = 0) extends Sub + case class Args(str: String, offset: Int = 0) extends Sub + } +} diff --git a/scalatex/api/src/main/scala/scalatex/stages/Trim.scala b/scalatex/api/src/main/scala/scalatex/stages/Trim.scala new file mode 100644 index 0000000..8993734 --- /dev/null +++ b/scalatex/api/src/main/scala/scalatex/stages/Trim.scala @@ -0,0 +1,29 @@ +package scalatex.stages +import acyclic.file + +/** + * Preprocesses the input string to normalize things related to whitespace + * + * Find the "first" non-whitespace-line of the text and remove the front + * of every line to align that first line with the left margin. + * + * Remove all trailing whitespace from each line. + */ +object Trim extends (String => (String, Int)){ + def apply(str: String) = { + val lines = str.split("\n", -1) + val offset = lines.iterator + .filter(_.length > 0) + .next() + .takeWhile(_ == ' ') + .length + val res = lines.iterator + .map(_.replaceFirst("\\s+$", "")) + .mkString("\n") + (res, offset) + } + def old(str: String) = { + val (res, offset) = this.apply(str) + res.split("\n", -1).map(_.drop(offset)).mkString("\n") + } +} diff --git a/scalatex/api/src/test/scala/scalatex/BasicTests.scala b/scalatex/api/src/test/scala/scalatex/BasicTests.scala new file mode 100644 index 0000000..4bc362c --- /dev/null +++ b/scalatex/api/src/test/scala/scalatex/BasicTests.scala @@ -0,0 +1,468 @@ +package scalatex +import utest._ +import scala.collection.mutable.ArrayBuffer +import scalatex.stages._ +import scalatags.Text.all._ + + +/** +* Created by haoyi on 7/14/14. +*/ +object BasicTests extends TestSuite{ + import TestUtil._ + + val tests = TestSuite{ + + 'helloWorld{ + object omg { + def wtf(s: Frag*): Frag = Seq[Frag]("|", s, "|") + } + def str = "hear me moo" + check( + tw(""" + @omg.wtf + i @b{am} cow @str + """), + "|iamcowhearmemoo|" + ) + } + 'interpolation{ + 'chained-check( + tw("omg @scala.math.pow(0.5, 3) wtf"), + "omg 0.125 wtf" + ) + 'parens-check( + tw("omg @(1 + 2 + 3 + 4) wtf"), + "omg 10 wtf" + ) + 'block-check( + tw(""" + @{"lol" * 3} + @{ + val omg = "omg" + omg * 2 + } + """), + """ + lollollol + omgomg + """ + ) + } + 'definitions{ + 'imports{ + object Whee{ + def func(x: Int) = x * 2 + } + check( + tw(""" + @import math._ + @import Whee.func + @abs(-10) + @p + @max(1, 2) + @func(2) + """), + """ + 10 +

+ 2 + 4 +

+ """ + ) + } + 'valDefVar{ + check( + tw(""" + Hello + @val x = 1 + World @x + @def y = "omg" + mooo + @y + """), + """ + Hello + World 1 + mooo + omg + """ + ) + } + 'classObjectTrait{ + check( + tw(""" + @trait Trait{ + def tt = 2 + } + Hello + @case object moo extends Trait{ + val omg = "wtf" + } + + @moo.toString + @moo.omg + @case class Foo(i: Int, s: String, b: Boolean) + TT is @moo.tt + @Foo(10, "10", true).toString + """), + """ + Hello + moo + wtf + TT is 2 + Foo(10, 10, true) + """ + ) + } + } + 'parenArgumentLists{ + 'attributes{ + check( + tw(""" + @div(id:="my-id"){ omg } + @div(id:="my-id") + omg + """), + """ + omg + omg + """ + ) + } +// 'multiline{ +// +// check( +// tw(""" +// @div( +// h1("Hello World"), +// p("I am a ", b{"cow"}) +// ) +// """), +// """ +//
+//

Hello World

+//

I am a cow

+//
+// """ +// ) +// } + } + 'grouping{ + 'negative{ + // The indentation for "normal" text is ignored; we only + // create blocks from the indentation following a scala + // @xxx expression + check( + tw(""" + I am cow hear me moo + I weigh twice as much as you + And I look good on the barbecue + Yoghurt curds cream cheese and butter + Comes from liquids from my udder + I am cow I am cow hear me moooooo + """), + """ + I am cow hear me moo + I weigh twice as much as you + And I look good on the barbecue + Yoghurt curds cream cheese and butter + Comes from liquids from my udder + I am cow I am cow hear me moooooo + """ + ) + } + 'indentation{ + 'simple{ + val world = "World2" + + check( + tw(""" + @h1 + Hello World + @h2 + hello @world + @h3 + Cow + """), + """ +

HelloWorld

+

helloWorld2

+

Cow

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

HelloWorld +

helloworld +

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

WORLD!!!lol

+

Header2

+
+
+ """ + ) + } +// +// 'args{ +// val things = Seq(1, 2, 3) +// check( +// tw(""" +// @ul +// @things.map { x => +// @li +// @x +// } +// """), +// tw(""" +// @ul +// @things.map x => +// @li +// @x +// +// """), +// """ +//
    +//
  • 1
  • +//
  • 2
  • +//
  • 3
  • +//
+// """ +// ) +// } + } +// + 'loops { +// + * - check( + tw(""" + @for(x <- 0 until 3) + lol + """), + tw(""" + @for(x <- 0 until 3){ + lol + } + """), + "lollollol" + ) + + + * - check( + tw(""" + @p + @for(x <- 0 until 2) + @for(y <- 0 until 2) + lol@x@y + """), + tw( """ + @p + @for(x <- 0 until 2){ + @for(y <- 0 until 2) + lol@x@y + } + """), + tw(""" + @p + @for(x <- 0 until 2) + @for(y <- 0 until 2){ + lol@x@y + } + """), + "

lol00lol01lol10lol11

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

lol00lol01lol10lol11

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

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

Hello

" + ) + } +// 'funkyExpressions{ +// * - check( +// tw(""" +// @p +// @if(true == false == (true.==(false))) +// @if(true == false == (true.==(false))) +// Hello1 +// @else +// lols1 +// @else +// @if(true == false == (true.==(false))) +// Hello2 +// @else +// lols2 +// """), +// "

Hello1

" +// ) +// * - check( +// tw(""" +// @p +// @if(true == false != (true.==(false))) +// @if(true == false != (true.==(false))) +// Hello1 +// @else +// lols1 +// @else +// @if(true == false != (true.==(false))) +// Hello2 +// @else +// lols2 +// """), +// "

lols2

" +// ) +// } + } + } + +} diff --git a/scalatex/api/src/test/scala/scalatex/ErrorTests.scala b/scalatex/api/src/test/scala/scalatex/ErrorTests.scala new file mode 100644 index 0000000..d8cd4f5 --- /dev/null +++ b/scalatex/api/src/test/scala/scalatex/ErrorTests.scala @@ -0,0 +1,373 @@ +package scalatex + +import utest._ +import scalatex.stages._ +import scalatags.Text.all._ +import scalatex.Internals.{DebugFailure, twRuntimeErrors} + +/** +* Created by haoyi on 7/14/14. +*/ +object ErrorTests extends TestSuite{ + def check(x: => Unit, expectedMsg: String, expectedError: String) = { + val DebugFailure(msg, pos) = intercept[DebugFailure](x) + def format(str: String) = { + val whitespace = " \t\n".toSet + "\n" + str.dropWhile(_ == '\n') + .reverse + .dropWhile(whitespace.contains) + .reverse + } + // Format these guys nicely to normalize them and make them + // display nicely in the assert error message if it blows up + val formattedPos = format(pos) + val formattedExpectedPos = format(expectedError) + + assert(msg.contains(expectedMsg)) + assert(formattedPos == formattedExpectedPos) + + } + val tests = TestSuite{ + + + 'simple - check( + twRuntimeErrors("omg @notInScope lol"), + """not found: value notInScope""", + """ + twRuntimeErrors("omg @notInScope lol"), + ^ + """ + ) + + 'chained{ + 'properties { + * - check( + twRuntimeErrors("omg @math.lol lol"), + """object lol is not a member of package math""", + """ + twRuntimeErrors("omg @math.lol lol"), + ^ + """ + ) + + * - check( + twRuntimeErrors("omg @math.E.lol lol"), + """value lol is not a member of Double""", + """ + twRuntimeErrors("omg @math.E.lol lol"), + ^ + """ + ) + * - check( + twRuntimeErrors("omg @_root_.scala.math.lol lol"), + """object lol is not a member of package math""", + """ + twRuntimeErrors("omg @_root_.scala.math.lol lol"), + ^ + """ + ) + * - check( + twRuntimeErrors("omg @_root_.scala.gg.lol lol"), + """object gg is not a member of package scala""", + """ + twRuntimeErrors("omg @_root_.scala.gg.lol lol"), + ^ + """ + ) + * - check( + twRuntimeErrors("omg @_root_.ggnore.math.lol lol"), + """object ggnore is not a member of package """, + """ + twRuntimeErrors("omg @_root_.ggnore.math.lol lol"), + ^ + """ + ) + } + 'calls{ + * - check( + twRuntimeErrors("@scala.QQ.abs(-10).tdo(10).sum.z"), + """object QQ is not a member of package scala""", + """ + twRuntimeErrors("@scala.QQ.abs(-10).tdo(10).sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.abs(-10).tdo(10).sum.z"), + "value tdo is not a member of Int", + """ + twRuntimeErrors("@scala.math.abs(-10).tdo(10).sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z"), + "value z is not a member of Int", + """ + twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z()"), + "value z is not a member of Int", + """ + twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z()"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.abs(-10).cow.sum.z"), + "value cow is not a member of Int", + """ + twRuntimeErrors("@scala.math.abs(-10).cow.sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.smath.abs.cow.sum.z"), + "object smath is not a member of package scala", + """ + twRuntimeErrors("@scala.smath.abs.cow.sum.z"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.cos('omg)"), + "type mismatch", + """ + twRuntimeErrors("@scala.math.cos('omg)"), + ^ + """ + ) + * - check( + twRuntimeErrors("@scala.math.cos[omg]('omg)"), + "not found: type omg", + """ + twRuntimeErrors("@scala.math.cos[omg]('omg)"), + ^ + """ + ) + * - check( + twRuntimeErrors(""" + I am cow hear me moo + @scala.math.abs(-10).tdo(10).sum.z + I weigh twice as much as you + """), + "value tdo is not a member of Int", + """ + @scala.math.abs(-10).tdo(10).sum.z + ^ + """ + ) + } + 'curlies{ + * - check( + twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), + "missing arguments for method foldLeft", + """ + twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), + ^ + """ + ) + + * - check( + twRuntimeErrors("@Nil.foldLeft{XY}"), + "missing arguments for method foldLeft", + """ + twRuntimeErrors("@Nil.foldLeft{XY}"), + ^ + """ + ) + +// * - check( +// twRuntimeErrors("@Seq(1).map{(y: String) => omg}"), +// "type mismatch", +// """ +// twRuntimeErrors("@Seq(1).map{(y: String) => omg}"), +// ^ +// """ +// ) +// * - check( +// twRuntimeErrors("@Nil.map{ omg}"), +// "too many arguments for method map", +// """ +// twRuntimeErrors("@Nil.map{ omg}"), +// ^ +// """ +// ) + } + 'callContents{ + * - check( + twRuntimeErrors("@scala.math.abs((1, 2).wtf)"), + "value wtf is not a member of (Int, Int)", + """ + twRuntimeErrors("@scala.math.abs((1, 2).wtf)"), + ^ + """ + ) + + * - check( + twRuntimeErrors("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"), + "value wtf is not a member of String", + """ + twRuntimeErrors("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"), + ^ + """ + ) + } + } + 'ifElse{ + 'oneLine { + * - check( + twRuntimeErrors("@if(math > 10){ 1 }else{ 2 }"), + "object > is not a member of package math", + """ + twRuntimeErrors("@if(math > 10){ 1 }else{ 2 }"), + ^ + """ + ) + * - check( + twRuntimeErrors("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"), + "Unspecified value parameter y", + """ + twRuntimeErrors("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"), + ^ + """ + ) + * - check( + twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), + "too many arguments for method sin: (x: Double)Double", + """ + twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), + ^ + """ + ) + } + 'multiLine{ + * - check( + twRuntimeErrors(""" + Ho Ho Ho + + @if(math != 10) + I am a cow + @else + You are a cow + GG + """), + "object != is not a member of package math", + """ + @if(math != 10) + ^ + """ + ) + * - check( + twRuntimeErrors(""" + Ho Ho Ho + + @if(4 != 10) + I am a cow @math.lols + @else + You are a cow + GG + """), + "object lols is not a member of package math", + """ + I am a cow @math.lols + ^ + """ + ) + * - check( + twRuntimeErrors(""" + Ho Ho Ho + + @if(12 != 10) + I am a cow + @else + @math.E.toString.gog(1) + GG + """), + "value gog is not a member of String", + """ + @math.E.toString.gog(1) + ^ + """ + ) + } + } + 'forLoop{ + 'oneLine{ + 'header - check( + twRuntimeErrors("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"), + """value omglolol is not a member of Int""", + """ + twRuntimeErrors("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"), + ^ + """ + ) + + 'body - check( + twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), + """too many arguments for method +""", + """ + twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), + ^ + """ + ) + } + 'multiLine{ + 'body - check( + twRuntimeErrors(""" + omg + @for(x <- 0 until 10) + I am cow hear me moo + I weigh twice as much as @x.kkk + """), + """value kkk is not a member of Int""", + """ + I weigh twice as much as @x.kkk + ^ + """ + ) + } + } + 'multiLine{ + 'missingVar - check( + twRuntimeErrors(""" + omg @notInScope lol + """), + """not found: value notInScope""", + """ + omg @notInScope lol + ^ + """ + ) +// 'wrongType - check( +// twRuntimeErrors(""" +// omg @{() => ()} lol +// """), +// """type mismatch""", +// """ +// omg @{() => ()} lol +// ^ +// """ +// ) + + 'bigExpression - check( + twRuntimeErrors(""" + @{ + val x = 1 + 2 + val y = new Object() + val z = y * x + x + } + """), + "value * is not a member of Object", + """ + val z = y * x + ^ + """ + ) + } + } +} diff --git a/scalatex/api/src/test/scala/scalatex/ParserTests.scala b/scalatex/api/src/test/scala/scalatex/ParserTests.scala new file mode 100644 index 0000000..9a4ee63 --- /dev/null +++ b/scalatex/api/src/test/scala/scalatex/ParserTests.scala @@ -0,0 +1,424 @@ +package scalatex + + +import org.parboiled2._ +import scalaParser.ScalaSyntax + +import scalatex.stages.{Trim, Parser, Ast} +import scalatex.stages.Ast.Block.{IfElse, For, Text} +import Ast.Chain.Args + +object ParserTests extends utest.TestSuite{ + import Ast._ + import utest._ + def check[T](input: String, parse: Parser => scala.util.Try[T], expected: T) = { + val parsed = parse(new Parser(input)).get + assert(parsed == expected) + } + def tests = TestSuite{ + 'Trim{ + def wrap(s: String) = "|" + s + "|" + * - { + val trimmed = wrap(stages.Trim.old(""" + i am cow + hear me moo + i weigh twice as much as you + """)) + val expected = wrap(""" + |i am cow + | hear me moo + | i weigh twice as much as you + |""".stripMargin) + assert(trimmed == expected) + + } + * - { + val trimmed = wrap(stages.Trim.old( + """ + @{"lol" * 3} + @{ + val omg = "omg" + omg * 2 + } + """ + )) + val expected = wrap( + """ + |@{"lol" * 3} + |@{ + | val omg = "omg" + | omg * 2 + |} + |""".stripMargin + ) + assert(trimmed == expected) + } + 'dropTrailingWhitespace - { + + val trimmed = wrap(stages.Trim.old( + Seq( + " i am a cow ", + " hear me moo ", + " i weigh twice as much as you" + ).mkString("\n") + )) + val expected = wrap( + Seq( + "i am a cow", + " hear me moo", + " i weigh twice as much as you" + ).mkString("\n") + ) + assert(trimmed == expected) + } + } + 'Text { + * - check("i am a cow", _.Text.run(), Block.Text("i am a cow")) + * - check("i am a @cow", _.Text.run(), Block.Text("i am a ")) + * - check("i am a @@cow", _.Text.run(), Block.Text("i am a @cow")) + * - check("i am a @@@cow", _.Text.run(), Block.Text("i am a @")) + * - check("i am a @@@@cow", _.Text.run(), Block.Text("i am a @@cow")) + + } + 'Code{ + 'identifier - check("@gggg ", _.Code.run(), "gggg") + 'parens - check("@(1 + 1)lolsss\n", _.Code.run(), "(1 + 1)") + 'curlies - check("@{{1} + (1)} ", _.Code.run(), "{{1} + (1)}") + 'blocks - check("@{val x = 1; 1} ", _.Code.run(), "{val x = 1; 1}") + 'weirdBackticks - check("@{`{}}{()@`}\n", _.Code.run(), "{`{}}{()@`}") + } + 'MiscCode{ + 'imports{ + * - check("@import math.abs", _.Header.run(), "import math.abs") + * - check("@import math.{abs, sin}", _.Header.run(), "import math.{abs, sin}") + } + 'headerblocks{ + check( + """@import math.abs + |@import math.sin + | + |hello world + |""".stripMargin, + _.HeaderBlock.run(), + Ast.Header( + "import math.abs\nimport math.sin", + Ast.Block( + Seq(Text("\n", 33), Text("\n", 34), Text("hello world", 35), Text("\n", 46)), + 33 + ) + ) + ) + } + 'caseclass{ + check( + """@case class Foo(i: Int, s: String) + """.stripMargin, + _.Header.run(), + "case class Foo(i: Int, s: String)" + ) + } + + } + 'Block{ + * - check("{i am a cow}", _.BraceBlock.run(), Block(Seq(Block.Text("i am a cow", 1)), 1)) + * - check("{i @am a @cow}", _.BraceBlock.run(), + Block(Seq( + Block.Text("i ", 1), + Chain("am",Seq(), 3), + Block.Text(" a ", 6), + Chain("cow",Seq(), 9) + ), 1) + ) + } + 'Chain{ + * - check("@omg.bbq[omg].fff[fff](123) ", _.ScalaChain.run(), + Chain("omg",Seq( + Chain.Prop("bbq", 4), + Chain.TypeArgs("[omg]", 8), + Chain.Prop("fff", 13), + Chain.TypeArgs("[fff]", 17), + Chain.Args("(123)", 22) + )) + ) + * - check("@omg{bbq}.cow(moo){a @b}\n", _.ScalaChain.run(), + Chain("omg",Seq( + Block(Seq(Text("bbq", 5)), 5), + Chain.Prop("cow", 9), + Chain.Args("(moo)", 13), + Block(Seq(Text("a ", 19), Chain("b", Nil, 21)), 19) + )) + ) + } + 'ControlFlow{ + 'for { + 'for - check( + "@for(x <- 0 until 3){lol}", + _.ForLoop.run(), + For("for(x <- 0 until 3)", Block(Seq(Text("lol", 21)), 21)) + ) + 'forBlock - check( + """ + |@for(x <- 0 until 3) + | lol""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + For( + "for(x <- 0 until 3)", + Block(Seq(Text("\n ", 21), Text("lol", 24)), 21), + 1 + ) + )) + ) + 'forBlockBraces - check( + """ + |@for(x <- 0 until 3){ + | lol + |}""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + For( + "for(x <- 0 until 3)", + Block(Seq(Text("\n ", 22), Text("lol", 25), Text("\n", 28)), 22), + 1 + ) + )) + ) + } + 'ifElse { + 'if - check( + "@if(true){lol}", + _.IfElse.run(), + IfElse("if(true)", Block(Seq(Text("lol", 10)), 10), None) + ) + 'ifElse - check( + "@if(true){lol}else{ omg }", + _.IfElse.run(), + IfElse("if(true)", Block(Seq(Text("lol", 10)), 10), Some(Block(Seq(Text(" omg ", 19)), 19))) + ) + 'ifBlock - check( + """ + |@if(true) + | omg""".stripMargin, + _.IfElse.run(), + IfElse("if(true)", Block(Seq(Text("\n ", 10), Text("omg", 13)), 10), None, 1) + ) + 'ifBlockElseBlock - check( + """ + |@if(true) + | omg + |@else + | wtf""".stripMargin, + _.IfElse.run(), + IfElse( + "if(true)", + Block(Seq(Text("\n ", 10), Text("omg", 13)), 10), + Some(Block(Seq(Text("\n ", 22), Text("wtf", 25)), 22)), + 1 + ) + ) + 'ifBlockElseBraceBlock - check( + """@if(true){ + | omg + |}else{ + | wtf + |}""".stripMargin, + _.IfElse.run(), + IfElse( + "if(true)", + Block(Seq(Text("\n ", 10), Text("omg", 13), Text("\n", 16)), 10), + Some(Block(Seq(Text("\n ", 23), Text("wtf", 26), Text("\n", 29)), 23)), + 0 + ) + ) + 'ifBlockElseBraceBlockNested - { + val res = Parser(Trim.old( + """ + @p + @if(true){ + Hello + }else{ + lols + } + """)) + val expected = + Block(Vector( + Text("\n"), + Chain("p",Vector(Block(Vector( + Text("\n ", 3), + IfElse("if(true)", + Block(Vector( + Text("\n ", 16), Text("Hello", 21), Text("\n ", 26) + ), 16), + Some(Block(Vector( + Text("\n ", 35), Text("lols", 40), Text("\n ", 44) + ), 35)), + 6 + )), 3)), 1), + Text("\n", 48) + )) + assert(res == expected) + } + 'ifElseBlock - check( + """@if(true){ + | omg + |}else + | wtf""".stripMargin, + _.IfElse.run(), + IfElse( + "if(true)", + Block(Seq(Text("\n ", 10), Text("omg", 13), Text("\n", 16)), 10), + Some(Block(Seq(Text("\n ", 22), Text("wtf", 25)), 22)) + ) + ) + } + + } + 'Body{ + 'indents - check( + """ + |@omg + | @wtf + | @bbq + | @lol""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("omg",Seq(Block(Seq( + Text("\n ", 5), + Chain("wtf",Seq(Block(Seq( + Text("\n ", 7), + Chain("bbq",Seq(Block(Seq( + Text("\n ", 9), + Chain("lol",Seq(), 16) + ), 9)), 12) + ), 7)), 8) + ), 5)), 1) + )) + ) + 'dedents - check( + """ + |@omg + | @wtf + |@bbq""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("omg",Seq(Block( + Seq( + Text("\n ", 5), + Chain("wtf",Seq(), 8) + ), + 5 + )), 1), + Text("\n", 12), + Chain("bbq", Seq(), 13) + )) + ) + 'braces - check( + """ + |@omg{ + | @wtf + |} + |@bbq""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("omg",Seq(Block( + Seq( + Text("\n ", 6), + Chain("wtf",Seq(), 9), + Text("\n", 13) + ), + 6 + )), 1), + Text("\n", 15), + Chain("bbq", Seq(), 16) + )) + ) + 'dedentText - check( + """ + |@omg("lol", 1, 2) + | @wtf + |bbq""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("omg",Seq( + Args("""("lol", 1, 2)""", 5), + Block(Seq( + Text("\n ", 18), + Chain("wtf",Seq(), 21) + ), 18) + ), 1), + Text("\n", 25), + Text("bbq", 26) + )) + ) + * - check( + """ + |@omg("lol", + |1, + | 2 + | ) + | wtf + |bbq""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n", 0), + Chain("omg",Seq( + Args("(\"lol\",\n1,\n 2\n )", 5), + Block(Seq( + Text("\n ", 30), Text("wtf", 33) + ), 30) + ), 1), + Text("\n", 36), + Text("bbq", 37) + ), 0) + ) + 'codeBlock - check( + """@{ + | val omg = "omg" + | omg * 2 + |}""".stripMargin, + _.Code.run(), + """{ + | val omg = "omg" + | omg * 2 + |}""".stripMargin + ) + 'codeBlocks - check( + """ + |@{"lol" * 3} + |@{ + | val omg = "omg" + | omg * 2 + |}""".stripMargin, + _.Body.run(), + Block(Seq( + Text("\n"), + Chain("{\"lol\" * 3}", Seq(), 1), + Text("\n", 13), + Chain("""{ + | val omg = "omg" + | omg * 2 + |}""".stripMargin, + Seq(), + 14 + ) + )) + ) + } +// 'Test{ +// check( +// "@{() => ()}", +// _.Code.run(), +// "" +// ) +// } + } +} + + + diff --git a/scalatex/api/src/test/scala/scalatex/TestUtil.scala b/scalatex/api/src/test/scala/scalatex/TestUtil.scala new file mode 100644 index 0000000..5a72677 --- /dev/null +++ b/scalatex/api/src/test/scala/scalatex/TestUtil.scala @@ -0,0 +1,16 @@ +package scalatex + +import utest._ + + +object TestUtil { + implicit def stringify(f: scalatags.Text.all.Frag) = f.render + def check(rendered: String*) = { + val collapsed = rendered.map(collapse) + val first = collapsed(0) + assert(collapsed.forall(_ == first)) + } + def collapse(s: String): String = { + s.replaceAll("[ \n]", "") + } +} diff --git a/scalatex/build.sbt b/scalatex/build.sbt new file mode 100644 index 0000000..4769bad --- /dev/null +++ b/scalatex/build.sbt @@ -0,0 +1,44 @@ +val sharedSettings = Seq( + version := "0.1.0", + organization := "com.lihaoyi", + crossScalaVersions:= Seq("2.10.4", "2.11.2"), + scalaVersion := "2.11.4", + libraryDependencies += "com.lihaoyi" %% "acyclic" % "0.1.2" % "provided", + addCompilerPlugin("com.lihaoyi" %% "acyclic" % "0.1.2"), + autoCompilerPlugins := true +) + +lazy val scalaParser = project.settings(sharedSettings:_*) + .settings( + name := "scala-parser-lite", + libraryDependencies ++= Seq( + "com.lihaoyi" %% "utest" % "0.2.4", + "org.parboiled" %% "parboiled" % "2.0.1" + ), + testFrameworks += new TestFramework("utest.runner.JvmFramework") + ) +lazy val api = project.settings(sharedSettings:_*) + .dependsOn(scalaParser) + .settings( + name := "scalatex-api", + libraryDependencies ++= Seq( + "com.lihaoyi" %% "utest" % "0.2.4", + "com.scalatags" %% "scalatags" % "0.4.2", + "org.scala-lang" % "scala-reflect" % scalaVersion.value, + "org.parboiled" %% "parboiled" % "2.0.1" + ), + testFrameworks += new TestFramework("utest.runner.JvmFramework") + ) + +lazy val scalatexSbtPlugin = project.settings(sharedSettings:_*) + .settings( + name := "scalatex-sbt-plugin", + scalaVersion := "2.10.4", + sbtPlugin := true +) +lazy val compilerPlugin = project.settings(sharedSettings:_*) + .dependsOn(api) + .settings( + name := "scalatex-compiler-plugin", + libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value +) \ No newline at end of file diff --git a/scalatex/compilerPlugin/src/main/resources/scalac-plugin.xml b/scalatex/compilerPlugin/src/main/resources/scalac-plugin.xml new file mode 100755 index 0000000..a446f13 --- /dev/null +++ b/scalatex/compilerPlugin/src/main/resources/scalac-plugin.xml @@ -0,0 +1,4 @@ + + demo-plugin + scalatex.CompilerPlugin + \ No newline at end of file diff --git a/scalatex/compilerPlugin/src/main/scala/scalatex/CompilerPlugin.scala b/scalatex/compilerPlugin/src/main/scala/scalatex/CompilerPlugin.scala new file mode 100755 index 0000000..e122de5 --- /dev/null +++ b/scalatex/compilerPlugin/src/main/scala/scalatex/CompilerPlugin.scala @@ -0,0 +1,79 @@ +package scalatex + +import java.nio.file.Paths + +import scala.reflect.internal.util.BatchSourceFile +import scala.reflect.io.VirtualFile +import scala.tools.nsc.{ Global, Phase } +import scala.tools.nsc.plugins.{ Plugin, PluginComponent } + +class CompilerPlugin(val global: Global) extends Plugin { + import global._ + + override def init(options: List[String], error: String => Unit): Boolean = true + + val name = "scalatex" + val description = "Compiles scalatex files into Scala compilation units" + val components = List[PluginComponent](DemoComponent) + private object DemoComponent extends PluginComponent { + + val global = CompilerPlugin.this.global + import global._ + + override val runsAfter = List("parser") + override val runsBefore = List("namer") + + val phaseName = "Demo" + + override def newPhase(prev: Phase) = new GlobalPhase(prev) { + val splitOptions = options.map(o => o.splitAt(o.indexOf(":")+1)) + val scalatexRoots = splitOptions.collect{case ("root:", p) => p} + override def run() = { + def recursiveListFiles(f: java.io.File): Iterator[java.io.File] = { + val (dirs, files) = + Option(f.listFiles()) + .toSeq + .flatten + .partition(_.isDirectory) + files.iterator ++ dirs.iterator.flatMap(recursiveListFiles) + } + for { + scalatexRoot <- scalatexRoots + file <- recursiveListFiles(new java.io.File(scalatexRoot)) + } { + val name = file.getCanonicalPath + val fakeJfile = new java.io.File(name) + val txt = io.Source.fromFile(name).mkString + val virtualFile = new VirtualFile(name) { + override def file = fakeJfile + } + val sourceFile = new BatchSourceFile(virtualFile, txt) + val unit = new CompilationUnit(sourceFile) + val objectName = name.slice(name.lastIndexOf('/')+1, name.lastIndexOf('.')) + val pkgName = + Paths.get(scalatexRoot) + .relativize(fakeJfile.getParentFile.toPath) + .toString + .split("/") + .map(s => s"package $s") + .mkString("\n") + + val shim = s""" + $pkgName + import scalatags.Text.all._ + + object $objectName{ + def apply() = scalatex.twf("${name}") + } + """ + unit.body = global.newUnitParser(shim).parse() + global.currentRun.compileLate(unit) + } + } + + def name: String = phaseName + + def apply(unit: global.CompilationUnit): Unit = {} + } + } +} diff --git a/scalatex/project/build.properties b/scalatex/project/build.properties new file mode 100644 index 0000000..748703f --- /dev/null +++ b/scalatex/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.7 diff --git a/scalatex/project/build.sbt b/scalatex/project/build.sbt new file mode 100644 index 0000000..f1db6ad --- /dev/null +++ b/scalatex/project/build.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.lihaoyi" % "utest-js-plugin" % "0.2.4") diff --git a/scalatex/scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala b/scalatex/scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala new file mode 100644 index 0000000..de9f039 --- /dev/null +++ b/scalatex/scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala @@ -0,0 +1,416 @@ +package scalaParser +import acyclic.file +import language.implicitConversions +import syntax._ +import org.parboiled2._ + +/** + * Parser for Scala syntax. + * + * The `G` parameter that gets passed in to each rule stands for + * "Greedy", and determines whether or not that rule is to consume + * newlines after the last terminal in that rule. We need to pass it + * everywhere so it can go all the way to the last terminal deep + * inside the parse tree, which can then decide whether or not to + * consume whitespace. + * + * The vast majority of terminals will consume newlines; only rules + * which occur in {} blocks won't have their terminals consume newlines, + * and only the *last* terminal in the rule will be affected. + * That's why the parser does terminals-consume-newlines-by-default, + * and leaves it up to the dev to thread the `G` variable where-ever + * we want the opposite behavior. + */ +class ScalaSyntax(val input: ParserInput) extends Parser with Basic with Identifiers with Literals { + // Aliases for common things. These things are used in almost every parser + // in the file, so it makes sense to keep them short. + type B = Boolean + val t = true + type R0 = Rule0 + /** + * Parses all whitespace, excluding newlines. This is only + * really useful in e.g. {} blocks, where we want to avoid + * capturing newlines so semicolon-inference would work + */ + def WS = rule { zeroOrMore(Basic.WhitespaceChar | Literals.Comment) } + + /** + * Parses whitespace, including newlines. + * This is the default for most things + */ + def WL = rule{ zeroOrMore(Basic.WhitespaceChar | Literals.Comment | Basic.Newline) } + + + + /** + * By default, all strings and characters greedily + * capture all whitespace immediately after the token. + */ + implicit private[this] def wspStr(s: String): R0 = rule { WL ~ str(s) } + implicit private[this] def wspChar(s: Char): R0 = rule { WL ~ ch(s) } + + /** + * Most keywords don't just require the correct characters to match, + * they have to ensure that subsequent characters *don't* match in + * order for it to be a keyword. This enforces that rule for key-words + * (W) and key-operators (O) which have different non-match criteria. + */ + object K { + def W(s: String) = rule { + WL ~ Key.W(s) + } + + def O(s: String) = rule { + WL ~ Key.O(s) + } + } + + + def pos = cursor -> cursorChar + + /** + * helper printing function + */ + def pr(s: String) = rule { run(println(s"LOGGING $cursor: $s")) } + + def Id = rule { WL ~ Identifiers.Id } + def VarId = rule { WL ~ Identifiers.VarId } + def Literal = rule { WL ~ Literals.Literal } + def Semi = rule { WS ~ Basic.Semi } + def Semis = rule { oneOrMore(Semi) } + def Newline = rule { WL ~ Basic.Newline } + + def QualId = rule { WL ~ oneOrMore(Id).separatedBy('.') } + def Ids = rule { oneOrMore(Id) separatedBy ',' } + + def Path: R0 = rule { + zeroOrMore(Id ~ '.') ~ K.W("this") ~ zeroOrMore(Id).separatedBy('.') | + StableId + } + def StableId: R0 = rule { + zeroOrMore(Id ~ '.') ~ (K.W("this") | K.W("super") ~ optional(ClassQualifier)) ~ '.' ~ oneOrMore(Id).separatedBy('.') | + Id ~ zeroOrMore(WL ~ '.' ~ WL ~ Id) + } + + def ClassQualifier = rule { '[' ~ Id ~ ']' } + + def Type: R0 = rule { + FunctionArgTypes ~ K.O("=>") ~ Type | InfixType ~ optional(WL ~ ExistentialClause) + } + def FunctionArgTypes = rule { + InfixType | '(' ~ optional(oneOrMore(ParamType) separatedBy ',') ~ ')' + } + + def ExistentialClause = rule { "forSome" ~ '{' ~ oneOrMore(ExistentialDcl).separatedBy(Semi) } + def ExistentialDcl = rule { K.W("type") ~ TypeDcl | K.W("val") ~ ValDcl } + + def InfixType = rule { + CompoundType ~ zeroOrMore(WL ~ Id ~ optional(Newline) ~ CompoundType) + } + def CompoundType = rule { + oneOrMore(AnnotType).separatedBy(WL ~ K.W("with")) ~ optional(Refinement) + } + def AnnotType = rule { + SimpleType ~ zeroOrMore(WL ~ Annotation) + } + def SimpleType: R0 = rule { + BasicType ~ + optional(WL ~ '#' ~ Id) ~ + optional(WL ~ TypeArgs) + } + def BasicType: R0 = rule { + '(' ~ Types ~ ')' | + Path ~ '.' ~ K.W("type") | + StableId + } + def TypeArgs = rule { '[' ~ Types ~ "]" } + def Types = rule { oneOrMore(Type).separatedBy(',') } + def Refinement = rule { + optional(Newline) ~ '{' ~ oneOrMore(RefineStat).separatedBy(Semi) ~ "}" + } + def RefineStat = rule { "type" ~ TypeDef | Dcl | MATCH } + def TypePat = rule { CompoundType } + def Ascription = rule { + ":" ~ ("_" ~ "*" | InfixType | oneOrMore(Annotation)) + } + + def ParamType = rule { K.O("=>") ~ Type | Type ~ "*" | Type } + + def Expr: R0 = rule { + (Bindings | optional(K.W("implicit")) ~ Id | "_") ~ K.O("=>") ~ Expr | + Expr1 + } + def Expr1: R0 = rule { + IfCFlow | + WhileCFlow | + TryCFlow | + DoWhileCFlow | + ForCFlow | + K.W("throw") ~ Expr | + K.W("return") ~ optional(Expr) | + SimpleExpr ~ K.O("=") ~ Expr | + PostfixExpr ~ optional("match" ~ '{' ~ CaseClauses ~ "}" | Ascription) + + } + def IfCFlow = rule { "if" ~ '(' ~ Expr ~ ')' ~ zeroOrMore(Newline) ~ Expr ~ optional(optional(Semi) ~ K.W("else") ~ Expr) } + def WhileCFlow = rule { "while" ~ '(' ~ Expr ~ ')' ~ zeroOrMore(Newline) ~ Expr } + def TryCFlow = rule { + K.W("try") ~ Expr ~ + optional(WL ~ K.W("catch") ~ Expr) ~ + optional(WL ~ K.W("finally") ~ Expr) + } + + def DoWhileCFlow = rule { K.W("do") ~ Expr ~ optional(Semi) ~ "while" ~ '(' ~ Expr ~ ")" } + def ForCFlow = rule { + "for" ~ + ('(' ~ Enumerators ~ ')' | '{' ~ Enumerators ~ '}') ~ + zeroOrMore(Newline) ~ + optional(K.W("yield")) ~ + Expr } + def NotNewline: R0 = rule{ &( WS ~ noneOf("\n") )} + def PostfixExpr: R0 = rule { InfixExpr ~ optional(NotNewline ~ Id ~ optional(Newline)) } + def InfixExpr: R0 = rule { + PrefixExpr ~ + zeroOrMore( + NotNewline ~ + Id ~ + optional(Newline) ~ + PrefixExpr + ) + } + def PrefixExpr = rule { optional(WL ~ anyOf("-+~!")) ~ SimpleExpr } + + def SimpleExpr: R0 = rule { + SimpleExpr1 ~ + zeroOrMore(WL ~ ('.' ~ Id | TypeArgs | ArgumentExprs)) ~ + optional(WL ~ "_") + } + + def SimpleExpr1 = rule{ + K.W("new") ~ (ClassTemplate | TemplateBody) | + BlockExpr | + Literal | + Path | + K.W("_") | + '(' ~ optional(Exprs) ~ ")" + } + + + + def Exprs: R0 = rule { oneOrMore(Expr).separatedBy(',') } + def ArgumentExprs: R0 = rule { + '(' ~ optional(Exprs ~ optional(K.O(":") ~ K.W("_") ~ '*')) ~ ")" | + optional(Newline) ~ BlockExpr + } + + def BlockExpr: R0 = rule { '{' ~ (CaseClauses | Block) ~ "}" } + def BlockEnd: R0 = rule{ optional(Semis) ~ &("}" | "case") } + def Block: R0 = rule { + optional(Semis) ~ + ( + BlockStats ~ optional(Semis ~ ResultExpr) ~ BlockEnd | + ResultExpr ~ BlockEnd | + MATCH ~ BlockEnd + ) + } + def BlockStats: R0 = rule{ + oneOrMore(BlockStat).separatedBy(Semis) + } + def BlockStat: R0 = rule { + Import | + zeroOrMore(Annotation) ~ (optional(K.W("implicit") | K.W("lazy")) ~ Def | zeroOrMore(LocalModifier) ~ TmplDef) | + Expr1 + } + def ResultExpr: R0 = rule { + (Bindings | optional(K.W("implicit")) ~ Id | "_") ~ K.W("=>") ~ Block | Expr1 + } + def Enumerators: R0 = rule { Generator ~ zeroOrMore(Semi ~ Enumerator) ~ WL } + def Enumerator: R0 = rule { Generator | Guard | Pattern1 ~ K.O("=") ~ Expr } + def Generator: R0 = rule { Pattern1 ~ K.O("<-") ~ Expr ~ optional(WL ~ Guard) } + def CaseClauses: R0 = rule { oneOrMore(CaseClause) } + def CaseClause: R0 = rule { K.W("case") ~ Pattern ~ optional(Guard) ~ K.O("=>") ~ Block } + def Guard: R0 = rule { K.W("if") ~ PostfixExpr } + def Pattern: R0 = rule { + oneOrMore(Pattern1).separatedBy('|') + } + def Pattern1: R0 = rule { + K.W("_") ~ K.O(":") ~ TypePat | VarId ~ K.O(":") ~ TypePat | Pattern2 + } + def Pattern2: R0 = rule { + VarId ~ "@" ~ Pattern3 | Pattern3 | VarId + } + def Pattern3: R0 = rule { + SimplePattern ~ zeroOrMore(Id ~ SimplePattern) + } + def SimplePattern: R0 = rule { + K.W("_") | + Literal | + '(' ~ optional(Patterns) ~ ')' | + ( + StableId ~ + optional( + '(' ~ + (optional(Patterns ~ ',') ~ optional(VarId ~ '@') ~ K.W("_") ~ '*' | optional(Patterns)) ~ + ')' + ) + ) | + VarId + } + def Patterns: R0 = rule { K.W("_") ~ '*' | oneOrMore(Pattern).separatedBy(',') } + + def TypeParamClause: R0 = rule { '[' ~ oneOrMore(VariantTypeParam).separatedBy(',') ~ ']' } + def FunTypeParamClause: R0 = rule { '[' ~ oneOrMore(TypeParam).separatedBy(',') ~ ']' } + def VariantTypeParam: R0 = rule { zeroOrMore(Annotation) ~ optional(anyOf("+-")) ~ TypeParam } + def TypeParam: R0 = rule { + (Id | K.W("_")) ~ + optional(TypeParamClause) ~ + optional(K.O(">:") ~ Type) ~ + optional(K.O("<:") ~ Type) ~ + zeroOrMore(K.O("<%") ~ Type) ~ + zeroOrMore(K.O(":") ~ Type) + } + def ParamClauses: R0 = rule { zeroOrMore(ParamClause) ~ optional(optional(Newline) ~ '(' ~ K.W("implicit") ~ Params ~ ')') } + def ParamClause: R0 = rule { optional(Newline) ~ '(' ~ optional(Params) ~ ')' } + def Params: R0 = rule { zeroOrMore(Param).separatedBy(',') } + def Param: R0 = rule { zeroOrMore(Annotation) ~ Id ~ optional(K.O(":") ~ ParamType) ~ optional(K.O("=") ~ Expr) } + def ClassParamClauses: R0 = rule { zeroOrMore(ClassParamClause) ~ optional(optional(Newline) ~ '(' ~ K.W("implicit") ~ ClassParam ~ ")") } + def ClassParamClause: R0 = rule { optional(Newline) ~ '(' ~ optional(ClassParams) ~ ")" } + def ClassParams: R0 = rule { oneOrMore(ClassParam).separatedBy(',') } + def ClassParam: R0 = rule { zeroOrMore(Annotation) ~ optional(zeroOrMore(Modifier) ~ (K.W("val") | K.W("var"))) ~ Id ~ K.O(":") ~ ParamType ~ optional(K.O("=") ~ Expr) } + + def Bindings: R0 = rule { '(' ~ zeroOrMore(Binding).separatedBy(',') ~ ')' } + def Binding: R0 = rule { (Id | K.W("_")) ~ optional(K.O(":") ~ Type) } + + def Modifier: R0 = rule { LocalModifier | AccessModifier | K.W("override") } + def LocalModifier: R0 = rule { K.W("abstract") | K.W("final") | K.W("sealed") | K.W("implicit") | K.W("lazy") } + def AccessModifier: R0 = rule { (K.W("private") | K.W("protected")) ~ optional(AccessQualifier) } + def AccessQualifier: R0 = rule { '[' ~ (K.W("this") | Id) ~ ']' } + + def Annotation: R0 = rule { '@' ~ SimpleType ~ zeroOrMore(WL ~ ArgumentExprs) } + def ConstrAnnotation: R0 = rule { '@' ~ SimpleType ~ ArgumentExprs } + + def TemplateBody: R0 = rule { + '{' ~ + optional(SelfType) ~ + zeroOrMore(TemplateStat).separatedBy(Semis) ~ + '}' + } + def TemplateStat: R0 = rule { + Import | + zeroOrMore(Annotation ~ optional(Newline)) ~ zeroOrMore(Modifier) ~ (Def | Dcl) | + Expr + } + + def SelfType: R0 = rule { K.W("this") ~ K.O(":") ~ Type ~ K.O("=>") | Id ~ optional(K.O(":") ~ Type) ~ K.O("=>") } + + def Import: R0 = rule { K.W("import") ~ oneOrMore(ImportExpr).separatedBy(',') } + + def ImportExpr: R0 = rule { + StableId ~ optional('.' ~ ("_" | ImportSelectors)) + } + def ImportSelectors: R0 = rule { '{' ~ zeroOrMore(ImportSelector ~ ',') ~ (ImportSelector | K.W("_")) ~ "}" } + def ImportSelector: R0 = rule { Id ~ optional(K.O("=>") ~ (Id | K.W("_"))) } + + def Dcl: R0 = rule { + K.W("val") ~ ValDcl | + K.W("var") ~ VarDcl | + K.W("def") ~ FunDcl | + K.W("type") ~ zeroOrMore(Newline) ~ TypeDcl + } + def ValDcl: R0 = rule { Ids ~ K.O(":") ~ Type } + def VarDcl: R0 = rule { Ids ~ K.O(":") ~ Type } + def FunDcl: R0 = rule { FunSig ~ optional(WL ~ K.O(":") ~ Type) } + def FunSig: R0 = rule { Id ~ optional(FunTypeParamClause) ~ ParamClauses } + def TypeDcl: R0 = rule { + Id ~ + optional(WL ~ TypeParamClause) ~ + optional(WL ~ K.O(">:") ~ Type) ~ + optional(WL ~ K.O("<:") ~ Type) + } + + def PatVarDef: R0 = rule { K.W("val") ~ PatDef | K.W("var") ~ VarDef } + def Def: R0 = rule { K.W("def") ~ FunDef | K.W("type") ~ zeroOrMore(Newline) ~ TypeDef | PatVarDef | TmplDef } + def PatDef: R0 = rule { oneOrMore(Pattern2).separatedBy(',') ~ optional(K.O(":") ~ Type) ~ K.O("=") ~ Expr } + def VarDef: R0 = rule { Ids ~ K.O(":") ~ Type ~ K.O("=") ~ K.W("_") | PatDef } + def FunDef: R0 = rule { + K.W("this") ~ ParamClause ~ ParamClauses ~ (K.O("=") ~ ConstrExpr | optional(Newline) ~ ConstrBlock) | + FunSig ~ + ( + optional(K.O(":") ~ Type) ~ K.O("=") ~ optional(K.W("macro")) ~ Expr | + optional(Newline) ~ '{' ~ Block ~ "}" + ) + } + def TypeDef: R0 = rule { Id ~ optional(TypeParamClause) ~ K.O("=") ~ Type } + + def TmplDef: R0 = rule { + K.W("trait") ~ TraitDef | + optional(K.W("case")) ~ (K.W("class") ~ ClassDef | + K.W("object") ~ ObjectDef) + } + def ClassDef: R0 = rule { + Id ~ + optional(TypeParamClause) ~ + zeroOrMore(ConstrAnnotation) ~ + optional(AccessModifier) ~ + ClassParamClauses ~ + ClassTemplateOpt + } + def TraitDef: R0 = rule { Id ~ optional(TypeParamClause) ~ TraitTemplateOpt } + def ObjectDef: R0 = rule { Id ~ ClassTemplateOpt } + def ClassTemplateOpt: R0 = rule { + WL ~ K.W("extends") ~ ClassTemplate | + optional(WL ~ optional(K.W("extends")) ~ TemplateBody) + } + def TraitTemplateOpt: R0 = rule { K.W("extends") ~ TraitTemplate | optional(optional(K.W("extends")) ~ TemplateBody) } + def ClassTemplate: R0 = rule { + optional(EarlyDefs) ~ + ClassParents ~ + optional(WL ~ TemplateBody) + } + + def TraitTemplate: R0 = rule { + optional(EarlyDefs) ~ TraitParents ~ optional(TemplateBody) + } + def ClassParents: R0 = rule { + Constr ~ zeroOrMore(WL ~ K.W("with") ~ AnnotType) + } + def TraitParents: R0 = rule { + AnnotType ~ zeroOrMore(WL ~ K.W("with") ~ AnnotType) + } + def Constr: R0 = rule { + AnnotType ~ zeroOrMore(WL ~ ArgumentExprs) + } + def EarlyDefs: R0 = rule { + '{' ~ optional(oneOrMore(EarlyDef).separatedBy(Semis)) ~ '}' ~ K.W("with") + } + def EarlyDef: R0 = rule { + zeroOrMore(Annotation ~ optional(Newline)) ~ zeroOrMore(Modifier) ~ PatVarDef + } + def ConstrExpr: R0 = rule { ConstrBlock | SelfInvocation } + def ConstrBlock: R0 = rule { '{' ~ SelfInvocation ~ zeroOrMore(Semis ~ BlockStat) ~ '}' } + def SelfInvocation: R0 = rule { K.W("this") ~ oneOrMore(ArgumentExprs) } + + def TopStatSeq: R0 = rule { oneOrMore(TopStat).separatedBy(Semis) } + def TopStat: R0 = rule { + Packaging | + PackageObject | + Import | + zeroOrMore(Annotation ~ optional(Newline)) ~ zeroOrMore(Modifier) ~ TmplDef + } + def Packaging: R0 = rule { K.W("package") ~ QualId ~ '{' ~ TopStatSeq ~ '}' } + def PackageObject: R0 = rule { K.W("package") ~ K.W("object") ~ ObjectDef } + def TopPackageSeq: R0 = rule{ + oneOrMore(K.W("package") ~ QualId).separatedBy(Semis) + } + def CompilationUnit: Rule1[String] = rule { + capture( + pr("CompulationUnit 0") ~ + optional(Semis) ~ + pr("CompulationUnit 1") ~ + (TopPackageSeq ~ optional(Semis ~ TopStatSeq) | TopStatSeq) ~ + optional(Semis) ~ + WL + + ) + } +} diff --git a/scalatex/scalaParser/src/main/scala/scalaParser/syntax/Basic.scala b/scalatex/scalaParser/src/main/scala/scalaParser/syntax/Basic.scala new file mode 100644 index 0000000..8d3232a --- /dev/null +++ b/scalatex/scalaParser/src/main/scala/scalaParser/syntax/Basic.scala @@ -0,0 +1,51 @@ +package scalaParser +package syntax +import acyclic.file +import org.parboiled2._ + +trait Basic { self: Parser => + object Basic{ + def UnicodeExcape = rule { "\\u" ~ 4.times(HexDigit) } + + + //Numbers and digits + def HexDigit = rule { Digit | "a" - "f" | "A" - "Z" } + def Digit = rule { "0" | NonZeroDigit } + def NonZeroDigit = rule { "1" - "9" } + def HexNumeral = rule { "0x" ~ oneOrMore(HexDigit) } + def DecimalNumeral = rule(oneOrMore(Digit)) + def ExponentPart = rule { anyOf("Ee") ~ optional(anyOf("+-")) ~ oneOrMore(Digit) } + def FloatType = rule { anyOf("FfDd") } + + def Parentheses = rule { "(" | ")" | "[" | "]" | "{" | "}" } + def DelimiterChar = rule { "'" | "\"" | "." | ";" | "," } + + def WhitespaceChar = rule { "\u0020" | "\u0009" } + def Newline = rule { "\r\n" | "\n" } + def Semi = rule { ';' | oneOrMore(Newline) } + def OperatorChar = rule { + anyOf("""!#$%&*+-/:<=>?@\^|~""") | + CharPredicate.from(_.getType match { + case Character.OTHER_SYMBOL | Character.MATH_SYMBOL => true; case _ => false + }) + } + def Letter = rule { Upper | Lower | CharPredicate.from(c => c.isLetter | c.isDigit) } + def Lower = rule { "a" - "z" | "$" | "_" | CharPredicate.from(_.isLower) } + def Upper = rule { "A" - "Z" | CharPredicate.from(_.isUpper) } + } + /** + * Most keywords don't just require the correct characters to match, + * they have to ensure that subsequent characters *don't* match in + * order for it to be a keyword. This enforces that rule for key-words + * (W) and key-operators (O) which have different non-match criteria. + */ + object Key { + def W(s: String) = rule { + str(s) ~ !(Basic.Letter | Basic.Digit) + } + + def O(s: String) = rule { + str(s) ~ !Basic.OperatorChar + } + } +} diff --git a/scalatex/scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala b/scalatex/scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala new file mode 100644 index 0000000..4bc972f --- /dev/null +++ b/scalatex/scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala @@ -0,0 +1,35 @@ +package scalaParser +package syntax +import acyclic.file +import org.parboiled2._ + +trait Identifiers { self: Parser with Basic => + object Identifiers{ + import Basic._ + def Operator = rule(oneOrMore(OperatorChar)) + + def VarId = rule { + !(Keywords ~ (WhitespaceChar | Newline | "//" | "/*")) ~ Lower ~ IdRest + } + def PlainId = rule { Upper ~ IdRest | VarId | !(Keywords ~ (WhitespaceChar | Newline | "//" | "/*")) ~ Operator } + def Id = rule { PlainId | ("`" ~ oneOrMore(noneOf("`")) ~ "`") } + def IdRest = rule { + zeroOrMore(zeroOrMore("_") ~ oneOrMore(!"_" ~ Letter | Digit)) ~ + optional(oneOrMore("_") ~ optional(Operator)) + } + + + def AlphabetKeywords = rule { + "abstract" | "case" | "catch" | "class" | "def" | "do" | "else" | "extends" | "false" | "finally" | "final" | "finally" | "forSome" | "for" | "if" | + "implicit" | "import" | "lazy" | "match" | "new" | "null" | "object" | "override" | "package" | "private" | "protected" | "return" | + "sealed" | "super" | "this" | "throw" | "trait" | "try" | "true" | "type" | "val" | "var" | "while" | "with" | "yield" | "_" + } + def SymbolicKeywords = rule{ + ":" | ";" | "=>" | "=" | "<-" | "<:" | "<%" | ">:" | "#" | "@" | "\u21d2" | "\u2190" + } + def Keywords = rule { + AlphabetKeywords ~ !Letter | SymbolicKeywords ~ !OperatorChar + + } + } +} diff --git a/scalatex/scalaParser/src/main/scala/scalaParser/syntax/Literals.scala b/scalatex/scalaParser/src/main/scala/scalaParser/syntax/Literals.scala new file mode 100644 index 0000000..9fd9d5b --- /dev/null +++ b/scalatex/scalaParser/src/main/scala/scalaParser/syntax/Literals.scala @@ -0,0 +1,58 @@ +package scalaParser +package syntax +import acyclic.file +import org.parboiled2._ + +trait Literals { self: Parser with Basic with Identifiers => + object Literals{ + import Basic._ + def FloatingPointLiteral = rule { + + "." ~ oneOrMore(Digit) ~ optional(ExponentPart) ~ optional(FloatType) | + oneOrMore(Digit) ~ ( + "." ~ oneOrMore(Digit) ~ optional(ExponentPart) ~ optional(FloatType) | + ExponentPart ~ optional(FloatType) | + optional(ExponentPart) ~ FloatType + ) + } + + def IntegerLiteral = rule { (DecimalNumeral | HexNumeral) ~ optional(anyOf("Ll")) } + + def BooleanLiteral = rule { Key.W("true") | Key.W("false") } + + def MultilineComment: Rule0 = rule { "/*" ~ zeroOrMore(MultilineComment | !"*/" ~ ANY) ~ "*/" } + def Comment: Rule0 = rule { + MultilineComment | + "//" ~ zeroOrMore(!Basic.Newline ~ ANY) ~ &(Basic.Newline | EOI) + } + + def Literal = rule { + (optional("-") ~ (FloatingPointLiteral | IntegerLiteral)) | + BooleanLiteral | + CharacterLiteral | + StringLiteral | + SymbolLiteral | + (Key.W("null") ~ !(Basic.Letter | Basic.Digit)) + } + + + def EscapedChars = rule { '\\' ~ anyOf("rnt\\\"") } + + // Note that symbols can take on the same values as keywords! + def SymbolLiteral = rule { ''' ~ (Identifiers.PlainId | Identifiers.Keywords) } + + def CharacterLiteral = rule { ''' ~ (UnicodeExcape | EscapedChars | !'\\' ~ CharPredicate.from(isPrintableChar)) ~ ''' } + + def MultiLineChars = rule { zeroOrMore(optional('"') ~ optional('"') ~ noneOf("\"")) } + def StringLiteral = rule { + (optional(Identifiers.Id) ~ "\"\"\"" ~ MultiLineChars ~ ("\"\"\"" ~ zeroOrMore('"'))) | + (optional(Identifiers.Id) ~ '"' ~ zeroOrMore("\\\"" | noneOf("\n\"")) ~ '"') + } + + def isPrintableChar(c: Char): Boolean = { + val block = Character.UnicodeBlock.of(c) + !Character.isISOControl(c) && !Character.isSurrogate(c) && block != null && block != Character.UnicodeBlock.SPECIALS + } + } +} + diff --git a/scalatex/scalaParser/src/test/scala/scalaParser/SyntaxTest.scala b/scalatex/scalaParser/src/test/scala/scalaParser/SyntaxTest.scala new file mode 100644 index 0000000..fe5fc2c --- /dev/null +++ b/scalatex/scalaParser/src/test/scala/scalaParser/SyntaxTest.scala @@ -0,0 +1,448 @@ +package scalaParser + +import org.parboiled2.ParseError +import utest._ +import utest.framework.Test +import utest.util.Tree + +import scala.util.{Failure, Success} + +object SyntaxTest extends TestSuite{ + def check[T](input: String) = { + println("Checking...") + new ScalaSyntax(input).CompilationUnit.run() match{ + case Failure(f: ParseError) => + println(f.position) + println(f.formatExpectedAsString) + println(f.formatTraces) + throw new Exception(f.position + "\t" + f.formatTraces) + case Success(parsed) => + assert(parsed == input) + } + } + println("running") + def tests = TestSuite{ + 'unit { + * - check( + "package torimatomeru" + + ) + * - check( + """package torimatomeru + | + |package lols + """.stripMargin + ) + * - check( + """package torimatomeru + |import a + |import b + """.stripMargin + ) + * - check( + """ + |package torimatomeru + | + |import org.parboiled2.ParseError + |import utest._ + |import utest.framework.Test + |import utest.util.Tree + | + |import scala.util.{Failure, Success} + | + |object SyntaxTest extends TestSuite + """.stripMargin + ) + * - check( + """ + |object SyntaxTest extends TestSuite{ + | def check[T](input: String) = { + | + | } + |} + """.stripMargin + ) + * - check( + """ + |object SyntaxTest{ + | a() + | throw 1 + |} + """.stripMargin + ) + * - check( + """ + |object SyntaxTest extends TestSuite{ + | { + | println + | throw 1 + | } + |} + """.stripMargin + ) + * - check( + """package scalatex + | + | + |import org.parboiled2._ + |import torimatomeru.ScalaSyntax + | + |import scalatex.stages.{Trim, Parser, Ast} + |import scalatex.stages.Ast.Block.{IfElse, For, Text} + |import Ast.Chain.Args + | + |object ParserTests extends utest.TestSuite{ + | import Ast._ + | import utest._ + | def check[T](input: String, parse: Parser => scala.util.Try[T], expected: T) = { + | val parsed = parse(new Parser(input)).get + | assert(parsed == expected) + | } + | def tests = TestSuite{} + |} + """.stripMargin + ) + * - check( + """ + |object Moo{ + | a + | .b + | + | c + |} + """.stripMargin + ) + * - check( + """ + |object Moo{ + | filename + | .asInstanceOf[Literal] + |10 + |} + """.stripMargin + ) + * - check( + """ + |object Cow{ + | ().mkString + | + | 1 + |} + """.stripMargin + ) + * - check( + """ + |object O{ + | private[this] val applyMacroFull = 1 + |} + """.stripMargin + ) + * - check( + """ + |object O{ + | private[this] def applyMacroFull(c: Context) + | (expr: c.Expr[String], + | runtimeErrors: Boolean, + | debug: Boolean) + | : c.Expr[Frag] = { + | } + |} + """.stripMargin + ) + * - check( + """ + |object O{ + | class DebugFailure extends Exception + | + | 1 + |} + """.stripMargin + ) + * - check( + """ + |package torimatomeru + | + |package syntax + | + |import org.parboiled2._ + | + """.stripMargin + ) + * - check( + """ + |object Foo{ + | 0 match { + | case A | B => 0 + | } + |} + """.stripMargin + ) + * - check( + """ + |object Compiler{ + | + | def apply = { + | def rec = t match { + | case 0 => 0 + | } + | + | rec(tree) + | } + |} + | + """.stripMargin + ) + * - check( + """ + |object O { + | A(A(A(A(A(A(A(A()))))))) + |} + | + """.stripMargin + ) + * - check( + """ + |object O{ + | A(A(A(A(A(A(A(A(A(A(A(A(A(A(A(A()))))))))))))))) + |} + """.stripMargin + ) + * - check( + """ + |object L{ + | a.b = c + | a().b = c + |} + """.stripMargin + ) + * - check( + """ + |object L{ + | a b c + | d = 1 + |} + """.stripMargin + ) + + * - check( + """/* __ *\ + |** ________ ___ / / ___ __ ____ Scala.js CLI ** + |** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** + |** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** + |** /____/\___/_/ |_/____/_/ | |__/ /____/ ** + |** |/____/ ** + |\* */ + | + |package scala.scalajs.cli + | + """.stripMargin + ) + * - check( + """ + |object O{ + | for { + | a <- b + | c <- d + | } { + | 1 + | } + |} + """.stripMargin + ) + * - check( + """ + |object O{ + | val jarFile = + | try { 1 } + | catch { case _: F => G } + |} + """.stripMargin + ) + * - check( + """ + |object F{ + | func{ case _: F => fail } + |} + """.stripMargin + ) + * - check( + """ + |object Foo{ + | val a = d // g + | val b = e // h + | val c = f + |} + """.stripMargin + ) + * - check( + """ + |object L{ + | x match{ + | case y.Y(z) => z + | } + |} + """.stripMargin + ) + * - check( + """object K{ + | val a: B { + | val c: D + | } + | + | 1 + |} + """.stripMargin + ) + * - check( + """ + |object LOLS{ + | def run() {} + | + | def apply() {} + |} + """.stripMargin + ) + * - check( + """ + |object O{ + | a =:= b.c + |} + """.stripMargin + ) + * - check( + """ + |object K{ + | a( + | 1: _* + | ) + |} + """.stripMargin + ) + * - check( + """ + |object P{ + | tree match { + | case stats :+ expr => 1 + | } + |} + """.stripMargin + ) + * - check( + """ + |object K{ + | val trueA = 1 + |} + """.stripMargin + ) + * - check( + """ + |object K{ + | val nullo :: cow = 1 + |} + """.stripMargin + ) + * - check( + """ + |object K{ + | val omg_+ = 1 + |} + """.stripMargin + ) + * - check( + """ + |object K{ + | val + = 1 + | var * = 2 + |} + """.stripMargin + ) + * - check( + """ + |object O{ + | c match { + | case b_ => 1 + | } + |} + """.stripMargin + ) + * - check( + """ + |trait Basic { + | b match { + | case C => true; case _ => false + | } + |} + """.stripMargin + ) + * - check( + """trait Basic { + | !a.b + |} + """.stripMargin + ) + * - check( + """ + |class Parser { + | {() => } + |} + | + """.stripMargin + ) + * - check( + """ + | + | + | + |package omg + |; + | + |; + | + |; + |class Parser + |; + | + |; + | + |; + """.stripMargin + ) + } + def checkFile(path: String) = check(io.Source.fromFile(path).mkString) + 'file{ + + * - checkFile("test.txt") + * - checkFile("scalaParser/src/main/scala/scalaParser/syntax/Basic.scala") + * - checkFile("scalaParser/src/main/scala/scalaParser/syntax/Identifiers.scala") + * - checkFile("scalaParser/src/main/scala/scalaParser/syntax/Literals.scala") + * - checkFile("scalaParser/src/main/scala/scalaParser/ScalaSyntax.scala") + + * - checkFile("scalaParser/src/test/scala/scalaParser/SyntaxTest.scala") + + + * - checkFile("scalatexApi/src/main/scala/scalatex/stages/Compiler.scala") + * - checkFile("scalatexApi/src/main/scala/scalatex/stages/Parser.scala") + * - checkFile("scalatexApi/src/main/scala/scalatex/stages/Trim.scala") + * - checkFile("scalatexApi/src/main/scala/scalatex/package.scala") + + * - checkFile("scalatexApi/src/test/scala/scalatex/ParserTests.scala") + * - checkFile("scalatexApi/src/test/scala/scalatex/BasicTests.scala") + * - checkFile("scalatexApi/src/test/scala/scalatex/ErrorTests.scala") + * - checkFile("scalatexApi/src/test/scala/scalatex/TestUtil.scala") + + * - checkFile("scalatexPlugin/src/main/scala/scalatex/ScalaTexPlugin.scala") + } + +// 'omg{ +// val root = new java.io.File("../scala-js/") +// def listFiles(s: java.io.File): Iterator[String] = { +// val (dirs, files) = s.listFiles().toIterator.partition(_.isDirectory) +// files.map(_.getPath) ++ dirs.flatMap(listFiles) +// } +// for(f <- listFiles(root).filter(_.endsWith(".scala"))){ +// println("CHECKING " + f) +// checkFile(f) +// } +// } + } +} \ No newline at end of file diff --git a/scalatex/scalatexSbtPlugin/src/main/scala/SbtPlugin.scala b/scalatex/scalatexSbtPlugin/src/main/scala/SbtPlugin.scala new file mode 100644 index 0000000..f09dc17 --- /dev/null +++ b/scalatex/scalatexSbtPlugin/src/main/scala/SbtPlugin.scala @@ -0,0 +1,16 @@ +package scalatex + +import sbt.Keys._ +import sbt._ +object SbtPlugin extends sbt.Plugin{ + val scalatexDirectory = taskKey[sbt.File]("Clone stuff from github") + override val settings = Seq( + scalatexDirectory := sourceDirectory.value / "scalatex", + scalacOptions += { + "-P:scalatex:root:" + scalatexDirectory.value.getCanonicalPath + }, + watchSources += scalatexDirectory.value, + addCompilerPlugin("com.lihaoyi" %% "scalatex-compiler-plugin" % "0.1.0"), + libraryDependencies += "com.lihaoyi" %% "scalatex-api" % "0.1.0" + ) +} diff --git a/scalatexApi/src/main/scala/scalatex/package.scala b/scalatexApi/src/main/scala/scalatex/package.scala deleted file mode 100644 index 1f13e63..0000000 --- a/scalatexApi/src/main/scala/scalatex/package.scala +++ /dev/null @@ -1,98 +0,0 @@ -import scala.reflect.internal.util.{BatchSourceFile, SourceFile, OffsetPosition} -import scala.reflect.io.{PlainFile, AbstractFile} -import scala.reflect.macros.{TypecheckException, Context} -import scalatags.Text.all._ -import scalatex.stages.{Parser, Compiler} -import scala.language.experimental.macros -import acyclic.file - -package object scalatex { - /** - * Wraps the given string as a twist fragment. - */ - def tw(expr: String): Frag = macro Internals.applyMacro - def twf(filename: String): Frag = macro Internals.applyMacroFile - object Internals { - - def twRuntimeErrors(expr: String): Frag = macro applyMacroRuntimeErrors - def twDebug(expr: String): Frag = macro applyMacroDebug - - def applyMacro(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, false, false) - def applyMacroDebug(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, false, true) - - def applyMacroRuntimeErrors(c: Context)(expr: c.Expr[String]): c.Expr[Frag] = applyMacroFull(c)(expr, true, false) - - def applyMacroFile(c: Context)(filename: c.Expr[String]): c.Expr[Frag] = { - import c.universe._ - val fileName = filename.tree - .asInstanceOf[Literal] - .value - .value - .asInstanceOf[String] - val txt = io.Source.fromFile(fileName).mkString - val sourceFile = new BatchSourceFile( - new PlainFile(fileName), - txt.toCharArray - ) - - compileThing(c)(txt, sourceFile, 0, false, false) - } - - case class DebugFailure(msg: String, pos: String) extends Exception(msg) - - private[this] def applyMacroFull(c: Context) - (expr: c.Expr[String], - runtimeErrors: Boolean, - debug: Boolean) - : c.Expr[Frag] = { - import c.universe._ - val scalatexFragment = expr.tree - .asInstanceOf[Literal] - .value - .value - .asInstanceOf[String] - val stringStart = - expr.tree - .pos - .lineContent - .drop(expr.tree.pos.column) - .take(2) - compileThing(c)( - scalatexFragment, - expr.tree.pos.source, - expr.tree.pos.point + (if (stringStart == "\"\"") 1 else -1), - runtimeErrors, - debug - ) - } - } - - def compileThing(c: Context) - (scalatexSource: String, - source: SourceFile, - point: Int, - runtimeErrors: Boolean, - debug: Boolean) = { - import c.universe._ - def compile(s: String): c.Tree = { - val realPos = new OffsetPosition(source, point).asInstanceOf[c.universe.Position] - - Compiler(c)(realPos, Parser.tupled(stages.Trim(s))) - } - - - import c.Position - try { - val compiled = compile(scalatexSource) - if (debug) println(compiled) - c.Expr[Frag](c.typeCheck(compiled)) - } catch { - case e@TypecheckException(pos: Position, msg) => - if (!runtimeErrors) c.abort(pos, msg) - else { - val posMsg = pos.lineContent + "\n" + (" " * pos.column) + "^" - c.Expr( q"""throw scalatex.Internals.DebugFailure($msg, $posMsg)""") - } - } - } -} diff --git a/scalatexApi/src/main/scala/scalatex/stages/Compiler.scala b/scalatexApi/src/main/scala/scalatex/stages/Compiler.scala deleted file mode 100644 index 3df8da7..0000000 --- a/scalatexApi/src/main/scala/scalatex/stages/Compiler.scala +++ /dev/null @@ -1,103 +0,0 @@ -package scalatex -package stages - -import acyclic.file - -import scala.reflect.macros.whitebox.Context -import scala.reflect.internal.util.{Position, OffsetPosition} - -/** - * Walks the parsed AST, converting it into a structured Scala c.Tree - */ -object Compiler{ - - def apply(c: Context)(fragPos: c.Position, template: Ast.Block): c.Tree = { - - import c.universe._ - def fragType = tq"scalatags.Text.all.Frag" - - def incPosRec(trees: c.Tree, offset: Int): trees.type = { - - trees.foreach(incPos(_, offset)) - trees - } - def incPos(tree: c.Tree, offset: Int): tree.type = { - - val current = if (tree.pos == NoPosition) 0 else tree.pos.point - c.internal.setPos(tree, - new OffsetPosition( - fragPos.source, - offset + current + fragPos.point - ).asInstanceOf[c.universe.Position] - ) - tree - } - - def compileChain(code: String, parts: Seq[Ast.Chain.Sub], offset: Int): c.Tree = { - - val out = parts.foldLeft(incPosRec(c.parse(code), offset + 1)){ - case (curr, Ast.Chain.Prop(str, offset2)) => - incPos(q"$curr.${TermName(str)}", offset2 + 1) - case (curr, Ast.Chain.Args(str, offset2)) => - val Apply(fun, args) = c.parse(s"omg$str") - incPos(Apply(curr, args.map(incPosRec(_, offset2 - 2))), offset2) - case (curr, Ast.Chain.TypeArgs(str, offset2)) => - val TypeApply(fun, args) = c.parse(s"omg$str") - incPos(TypeApply(curr, args.map(incPosRec(_, offset2 - 2))), offset2) - case (curr, Ast.Block(parts, offset1)) => - incPos(q"$curr(..${compileBlock(parts, offset1)})", offset1) - case (curr, Ast.Header(header, block, offset1)) => - incPos(q"$curr(${compileHeader(header, block, offset1)})", offset1) - } - - out - } - def compileBlock(parts: Seq[Ast.Block.Sub], offset: Int): Seq[c.Tree] = { - val res = parts.map{ - case Ast.Block.Text(str, offset1) => - incPos(q"$str", offset1) - case Ast.Chain(code, parts, offset1) => - compileChain(code, parts, offset1) - case Ast.Header(header, block, offset1) => - compileHeader(header, block, offset1) - case Ast.Block.IfElse(condString, Ast.Block(parts2, offset2), elseBlock, offset1) => - val If(cond, _, _) = c.parse(condString + "{}") - val elseCompiled = elseBlock match{ - case Some(Ast.Block(parts3, offset3)) => compileBlockWrapped(parts3, offset3) - case None => EmptyTree - } - - val res = If(incPosRec(cond, offset1 + 2), compileBlockWrapped(parts2, offset2), elseCompiled) - - incPos(res, offset1) - res - case Ast.Block.For(generators, Ast.Block(parts2, offset2), offset1) => - val fresh = c.fresh() - - val tree = incPosRec(c.parse(s"$generators yield $fresh"), offset1 + 2) - - def rec(t: Tree): Tree = t match { - case a @ Apply(fun, List(f @ Function(vparams, body))) => - val f2 = Function(vparams, rec(body)) - val a2 = Apply(fun, List(f2)) - a2 - case Ident(x: TermName) if x.decoded == fresh => - compileBlockWrapped(parts2, offset2) - } - - rec(tree) - } - res - } - def compileBlockWrapped(parts: Seq[Ast.Block.Sub], offset: Int): c.Tree = { - incPos(q"Seq[$fragType](..${compileBlock(parts, offset)})", offset) - } - def compileHeader(header: String, block: Ast.Block, offset: Int): c.Tree = { - val Block(stmts, expr) = c.parse(s"{$header\n ()}") - Block(stmts, compileBlockWrapped(block.parts, block.offset)) - } - - val res = compileBlockWrapped(template.parts, template.offset) - res - } -} \ No newline at end of file diff --git a/scalatexApi/src/main/scala/scalatex/stages/Parser.scala b/scalatexApi/src/main/scala/scalatex/stages/Parser.scala deleted file mode 100644 index 0b87d97..0000000 --- a/scalatexApi/src/main/scala/scalatex/stages/Parser.scala +++ /dev/null @@ -1,170 +0,0 @@ -package scalatex -package stages -import acyclic.file -import org.parboiled2._ -import scalaParser.ScalaSyntax - -/** - * Parses the input text into a roughly-structured AST. This AST - * is much simpler than the real Scala AST, but serves us well - * enough until we stuff the code-strings into the real Scala - * parser later - */ -object Parser extends ((String, Int) => Ast.Block){ - def apply(input: String, offset: Int = 0): Ast.Block = { - new Parser(input, offset).Body.run().get - } -} -class Parser(input: ParserInput, indent: Int = 0, offset: Int = 0) extends scalaParser.ScalaSyntax(input) { - def offsetCursor = offset + cursor - val txt = input.sliceString(0, input.length) - val indentTable = txt.split('\n').map{ s => - if (s.trim == "") -1 - else s.takeWhile(_ == ' ').length - } - val nextIndentTable = (0 until indentTable.length).map { i => - val index = indentTable.indexWhere(_ != -1, i + 1) - if (index == -1) 100000 - else indentTable(index) - } - def cursorNextIndent() = { - nextIndentTable(txt.take(cursor).count(_ == '\n')) - } - - def TextNot(chars: String) = rule { - push(offsetCursor) ~ capture(oneOrMore(noneOf(chars + "\n") | "@@")) ~> { - (i, x) => Ast.Block.Text(x.replace("@@", "@"), i) - } - } - def Text = TextNot("@") - def Code = rule { - "@" ~ capture(Identifiers.Id | BlockExpr2 | ('(' ~ optional(Exprs) ~ ')')) - } - def Header = rule { - "@" ~ capture(Def | Import) - } - - def HeaderBlock: Rule1[Ast.Header] = rule{ - Header ~ zeroOrMore(capture(WL) ~ Header ~> (_ + _)) ~ runSubParser{new Parser(_, indent, cursor).Body0} ~> { - (start: String, heads: Seq[String], body: Ast.Block) => Ast.Header(start + heads.mkString, body) - } - } - - def BlankLine = rule{ '\n' ~ zeroOrMore(' ') ~ &('\n') } - def IndentSpaces = rule{ indent.times(' ') ~ zeroOrMore(' ') } - def Indent = rule{ '\n' ~ IndentSpaces } - def LoneScalaChain: Rule2[Ast.Block.Text, Ast.Chain] = rule { - (push(offsetCursor) ~ capture(Indent) ~> ((i, x) => Ast.Block.Text(x, i))) ~ - ScalaChain ~ - IndentBlock ~> { - (chain: Ast.Chain, body: Ast.Block) => chain.copy(parts = chain.parts :+ body) - } - } - def IndentBlock = rule{ - &("\n") ~ - test(cursorNextIndent() > indent) ~ - runSubParser(new Parser(_, cursorNextIndent(), cursor).Body) - } - def IfHead = rule{ "@" ~ capture("if" ~ "(" ~ Expr ~ ")") } - def IfElse1 = rule{ - push(offsetCursor) ~ IfHead ~ BraceBlock ~ optional("else" ~ (BraceBlock | IndentBlock)) - } - def IfElse2 = rule{ - Indent ~ push(offsetCursor) ~ IfHead ~ IndentBlock ~ optional(Indent ~ "@else" ~ (BraceBlock | IndentBlock)) - } - def IfElse = rule{ - (IfElse1 | IfElse2) ~> ((a, b, c, d) => Ast.Block.IfElse(b, c, d, a)) - } - - def ForHead = rule{ - push(offsetCursor) ~ "@" ~ capture("for" ~ '(' ~ Enumerators ~ ')') - } - def ForLoop = rule{ - ForHead ~ - BraceBlock ~> ((a, b, c) => Ast.Block.For(b, c, a)) - } - def LoneForLoop = rule{ - (push(offsetCursor) ~ capture(Indent) ~> ((i, t) => Ast.Block.Text(t, i))) ~ - ForHead ~ - IndentBlock ~> - ((a, b, c) => Ast.Block.For(b, c, a)) - } - - def ScalaChain = rule { - push(offsetCursor) ~ Code ~ zeroOrMore(Extension) ~> { (a, b, c) => Ast.Chain(b, c, a)} - } - def Extension: Rule1[Ast.Chain.Sub] = rule { - (push(offsetCursor) ~ '.' ~ capture(Identifiers.Id) ~> ((x, y) => Ast.Chain.Prop(y, x))) | - (push(offsetCursor) ~ capture(TypeArgs2) ~> ((x, y) => Ast.Chain.TypeArgs(y, x))) | - (push(offsetCursor) ~ capture(ArgumentExprs2) ~> ((x, y) => Ast.Chain.Args(y, x))) | - BraceBlock - } - def Ws = WL - // clones of the version in ScalaSyntax, but without tailing whitespace or newlines - def TypeArgs2 = rule { '[' ~ Ws ~ Types ~ ']' } - def ArgumentExprs2 = rule { - '(' ~ Ws ~ - (optional(Exprs ~ ',' ~ Ws) ~ PostfixExpr ~ ':' ~ Ws ~ '_' ~ Ws ~ '*' ~ Ws | optional(Exprs) ) ~ - ')' - } - def BlockExpr2: Rule0 = rule { '{' ~ Ws ~ (CaseClauses | Block) ~ Ws ~ '}' } - def BraceBlock: Rule1[Ast.Block] = rule{ '{' ~ BodyNoBrace ~ '}' } - - def BodyItem(exclusions: String): Rule1[Seq[Ast.Block.Sub]] = rule{ - ForLoop ~> (Seq(_)) | - LoneForLoop ~> (Seq(_, _)) | - IfElse ~> (Seq(_)) | - LoneScalaChain ~> (Seq(_, _)) | - HeaderBlock ~> (Seq(_)) | - TextNot("@" + exclusions) ~> (Seq(_)) | - (push(offsetCursor) ~ capture(Indent) ~> ((i, x) => Seq(Ast.Block.Text(x, i)))) | - (push(offsetCursor) ~ capture(BlankLine) ~> ((i, x) => Seq(Ast.Block.Text(x, i)))) | - ScalaChain ~> (Seq(_: Ast.Block.Sub)) - } - def Body = rule{ BodyEx() } - def BodyNoBrace = rule{ BodyEx("}") } - def BodyEx(exclusions: String = "") = rule{ - push(offsetCursor) ~ oneOrMore(BodyItem(exclusions)) ~> {(i, x) => - Ast.Block(x.flatten, i) - } - } - def Body0 = rule{ - push(offsetCursor) ~ zeroOrMore(BodyItem("")) ~> {(i, x) => - Ast.Block(x.flatten, i) - } - } -} - -trait Ast{ - def offset: Int -} -object Ast{ - - /** - * @param parts The various bits of text and other things which make up this block - * @param offset - */ - case class Block(parts: Seq[Block.Sub], - offset: Int = 0) - extends Chain.Sub with Block.Sub - object Block{ - trait Sub extends Ast - case class Text(txt: String, offset: Int = 0) extends Block.Sub - case class For(generators: String, block: Block, offset: Int = 0) extends Block.Sub - case class IfElse(condition: String, block: Block, elseBlock: Option[Block], offset: Int = 0) extends Block.Sub - } - case class Header(front: String, block: Block, offset: Int = 0) extends Block.Sub with Chain.Sub - - /** - * @param lhs The first expression in this method-chain - * @param parts A list of follow-on items chained to the first - * @param offset - */ - case class Chain(lhs: String, parts: Seq[Chain.Sub], offset: Int = 0) extends Block.Sub - object Chain{ - trait Sub extends Ast - case class Prop(str: String, offset: Int = 0) extends Sub - case class TypeArgs(str: String, offset: Int = 0) extends Sub - case class Args(str: String, offset: Int = 0) extends Sub - } -} diff --git a/scalatexApi/src/main/scala/scalatex/stages/Trim.scala b/scalatexApi/src/main/scala/scalatex/stages/Trim.scala deleted file mode 100644 index 8993734..0000000 --- a/scalatexApi/src/main/scala/scalatex/stages/Trim.scala +++ /dev/null @@ -1,29 +0,0 @@ -package scalatex.stages -import acyclic.file - -/** - * Preprocesses the input string to normalize things related to whitespace - * - * Find the "first" non-whitespace-line of the text and remove the front - * of every line to align that first line with the left margin. - * - * Remove all trailing whitespace from each line. - */ -object Trim extends (String => (String, Int)){ - def apply(str: String) = { - val lines = str.split("\n", -1) - val offset = lines.iterator - .filter(_.length > 0) - .next() - .takeWhile(_ == ' ') - .length - val res = lines.iterator - .map(_.replaceFirst("\\s+$", "")) - .mkString("\n") - (res, offset) - } - def old(str: String) = { - val (res, offset) = this.apply(str) - res.split("\n", -1).map(_.drop(offset)).mkString("\n") - } -} diff --git a/scalatexApi/src/test/scala/scalatex/AdvancedTests.scala b/scalatexApi/src/test/scala/scalatex/AdvancedTests.scala deleted file mode 100644 index cbca40a..0000000 --- a/scalatexApi/src/test/scala/scalatex/AdvancedTests.scala +++ /dev/null @@ -1,120 +0,0 @@ -//package scalatex -// -//import utest._ -//import scalatex.stages._ -//import scalatags.Text.all._ -// -// -///** -//* Created by haoyi on 7/14/14. -//*/ -//object AdvancedTests extends TestSuite{ -// import TestUtil._ -// -// val tests = TestSuite{ -// 'localDef{ -// check( -// tw(""" -// @lol(n: Int) = @{ -// "omg" * n -// } -// -// @lol(2) -// """), -// "omgomg" -// ) -// } -// 'innerTemplate{ -// check( -// tw(""" -// @lol(f: Int) = -// omg @f -// -// @lol(1) -// @lol(2: Int) -// @lol(3 + 1) -// """), -// tw(""" -// @lol(f: Int) ={ -// omg @f -// } -// @lol(1) -// @lol(2: Int) -// @lol(3 + 1) -// """), -// tw(""" -// @lol(f: Int) = { -// omg @f -// } -// @lol(1) -// @lol(2: Int) -// @lol(3 + 1) -// """), -// """ -// omg1omg2omg4 -// """ -// ) -// } -// 'innerInnerTemplate{ -// check( -// tw(""" -// @lol(f: Int) = -// @wtf(g: Int) = -// wtf @g -// -// @wtf(1 + 2 + 3) -// @wtf(f) -// -// @lol(1) -// @lol(2: Int) -// @lol(3 + 1) -// """), -// tw(""" -// @lol(f: Int) = { -// @wtf(g: Int) = { -// wtf @g -// } -// @wtf(1 + 2 + 3) -// @wtf(f) -// } -// @lol(1) -// @lol(2: Int) -// @lol(3 + 1) -// """), -// tw(""" -// @lol(f: Int) = { -// @wtf(g: Int) = -// wtf @g -// -// @wtf(1 + 2 + 3) -// @wtf(f) -// } -// @lol(1) -// @lol(2: Int) -// @lol(3 + 1) -// """), -// tw(""" -// @lol(f: Int) = -// @wtf(g: Int) = { -// wtf @g -// } -// @wtf(1 + 2 + 3) -// @wtf(f) -// -// @lol(1) -// @lol(2: Int) -// @lol(3 + 1) -// """), -// """ -// wtf6 -// wtf1 -// wtf6 -// wtf2 -// wtf6 -// wtf4 -// """ -// ) -// } -// -// } -//} diff --git a/scalatexApi/src/test/scala/scalatex/BasicTests.scala b/scalatexApi/src/test/scala/scalatex/BasicTests.scala deleted file mode 100644 index 4bc362c..0000000 --- a/scalatexApi/src/test/scala/scalatex/BasicTests.scala +++ /dev/null @@ -1,468 +0,0 @@ -package scalatex -import utest._ -import scala.collection.mutable.ArrayBuffer -import scalatex.stages._ -import scalatags.Text.all._ - - -/** -* Created by haoyi on 7/14/14. -*/ -object BasicTests extends TestSuite{ - import TestUtil._ - - val tests = TestSuite{ - - 'helloWorld{ - object omg { - def wtf(s: Frag*): Frag = Seq[Frag]("|", s, "|") - } - def str = "hear me moo" - check( - tw(""" - @omg.wtf - i @b{am} cow @str - """), - "|iamcowhearmemoo|" - ) - } - 'interpolation{ - 'chained-check( - tw("omg @scala.math.pow(0.5, 3) wtf"), - "omg 0.125 wtf" - ) - 'parens-check( - tw("omg @(1 + 2 + 3 + 4) wtf"), - "omg 10 wtf" - ) - 'block-check( - tw(""" - @{"lol" * 3} - @{ - val omg = "omg" - omg * 2 - } - """), - """ - lollollol - omgomg - """ - ) - } - 'definitions{ - 'imports{ - object Whee{ - def func(x: Int) = x * 2 - } - check( - tw(""" - @import math._ - @import Whee.func - @abs(-10) - @p - @max(1, 2) - @func(2) - """), - """ - 10 -

- 2 - 4 -

- """ - ) - } - 'valDefVar{ - check( - tw(""" - Hello - @val x = 1 - World @x - @def y = "omg" - mooo - @y - """), - """ - Hello - World 1 - mooo - omg - """ - ) - } - 'classObjectTrait{ - check( - tw(""" - @trait Trait{ - def tt = 2 - } - Hello - @case object moo extends Trait{ - val omg = "wtf" - } - - @moo.toString - @moo.omg - @case class Foo(i: Int, s: String, b: Boolean) - TT is @moo.tt - @Foo(10, "10", true).toString - """), - """ - Hello - moo - wtf - TT is 2 - Foo(10, 10, true) - """ - ) - } - } - 'parenArgumentLists{ - 'attributes{ - check( - tw(""" - @div(id:="my-id"){ omg } - @div(id:="my-id") - omg - """), - """ - omg - omg - """ - ) - } -// 'multiline{ -// -// check( -// tw(""" -// @div( -// h1("Hello World"), -// p("I am a ", b{"cow"}) -// ) -// """), -// """ -//
-//

Hello World

-//

I am a cow

-//
-// """ -// ) -// } - } - 'grouping{ - 'negative{ - // The indentation for "normal" text is ignored; we only - // create blocks from the indentation following a scala - // @xxx expression - check( - tw(""" - I am cow hear me moo - I weigh twice as much as you - And I look good on the barbecue - Yoghurt curds cream cheese and butter - Comes from liquids from my udder - I am cow I am cow hear me moooooo - """), - """ - I am cow hear me moo - I weigh twice as much as you - And I look good on the barbecue - Yoghurt curds cream cheese and butter - Comes from liquids from my udder - I am cow I am cow hear me moooooo - """ - ) - } - 'indentation{ - 'simple{ - val world = "World2" - - check( - tw(""" - @h1 - Hello World - @h2 - hello @world - @h3 - Cow - """), - """ -

HelloWorld

-

helloWorld2

-

Cow

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

HelloWorld -

helloworld -

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

WORLD!!!lol

-

Header2

-
-
- """ - ) - } -// -// 'args{ -// val things = Seq(1, 2, 3) -// check( -// tw(""" -// @ul -// @things.map { x => -// @li -// @x -// } -// """), -// tw(""" -// @ul -// @things.map x => -// @li -// @x -// -// """), -// """ -//
    -//
  • 1
  • -//
  • 2
  • -//
  • 3
  • -//
-// """ -// ) -// } - } -// - 'loops { -// - * - check( - tw(""" - @for(x <- 0 until 3) - lol - """), - tw(""" - @for(x <- 0 until 3){ - lol - } - """), - "lollollol" - ) - - - * - check( - tw(""" - @p - @for(x <- 0 until 2) - @for(y <- 0 until 2) - lol@x@y - """), - tw( """ - @p - @for(x <- 0 until 2){ - @for(y <- 0 until 2) - lol@x@y - } - """), - tw(""" - @p - @for(x <- 0 until 2) - @for(y <- 0 until 2){ - lol@x@y - } - """), - "

lol00lol01lol10lol11

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

lol00lol01lol10lol11

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

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

Hello

" - ) - } -// 'funkyExpressions{ -// * - check( -// tw(""" -// @p -// @if(true == false == (true.==(false))) -// @if(true == false == (true.==(false))) -// Hello1 -// @else -// lols1 -// @else -// @if(true == false == (true.==(false))) -// Hello2 -// @else -// lols2 -// """), -// "

Hello1

" -// ) -// * - check( -// tw(""" -// @p -// @if(true == false != (true.==(false))) -// @if(true == false != (true.==(false))) -// Hello1 -// @else -// lols1 -// @else -// @if(true == false != (true.==(false))) -// Hello2 -// @else -// lols2 -// """), -// "

lols2

" -// ) -// } - } - } - -} diff --git a/scalatexApi/src/test/scala/scalatex/ErrorTests.scala b/scalatexApi/src/test/scala/scalatex/ErrorTests.scala deleted file mode 100644 index d8cd4f5..0000000 --- a/scalatexApi/src/test/scala/scalatex/ErrorTests.scala +++ /dev/null @@ -1,373 +0,0 @@ -package scalatex - -import utest._ -import scalatex.stages._ -import scalatags.Text.all._ -import scalatex.Internals.{DebugFailure, twRuntimeErrors} - -/** -* Created by haoyi on 7/14/14. -*/ -object ErrorTests extends TestSuite{ - def check(x: => Unit, expectedMsg: String, expectedError: String) = { - val DebugFailure(msg, pos) = intercept[DebugFailure](x) - def format(str: String) = { - val whitespace = " \t\n".toSet - "\n" + str.dropWhile(_ == '\n') - .reverse - .dropWhile(whitespace.contains) - .reverse - } - // Format these guys nicely to normalize them and make them - // display nicely in the assert error message if it blows up - val formattedPos = format(pos) - val formattedExpectedPos = format(expectedError) - - assert(msg.contains(expectedMsg)) - assert(formattedPos == formattedExpectedPos) - - } - val tests = TestSuite{ - - - 'simple - check( - twRuntimeErrors("omg @notInScope lol"), - """not found: value notInScope""", - """ - twRuntimeErrors("omg @notInScope lol"), - ^ - """ - ) - - 'chained{ - 'properties { - * - check( - twRuntimeErrors("omg @math.lol lol"), - """object lol is not a member of package math""", - """ - twRuntimeErrors("omg @math.lol lol"), - ^ - """ - ) - - * - check( - twRuntimeErrors("omg @math.E.lol lol"), - """value lol is not a member of Double""", - """ - twRuntimeErrors("omg @math.E.lol lol"), - ^ - """ - ) - * - check( - twRuntimeErrors("omg @_root_.scala.math.lol lol"), - """object lol is not a member of package math""", - """ - twRuntimeErrors("omg @_root_.scala.math.lol lol"), - ^ - """ - ) - * - check( - twRuntimeErrors("omg @_root_.scala.gg.lol lol"), - """object gg is not a member of package scala""", - """ - twRuntimeErrors("omg @_root_.scala.gg.lol lol"), - ^ - """ - ) - * - check( - twRuntimeErrors("omg @_root_.ggnore.math.lol lol"), - """object ggnore is not a member of package """, - """ - twRuntimeErrors("omg @_root_.ggnore.math.lol lol"), - ^ - """ - ) - } - 'calls{ - * - check( - twRuntimeErrors("@scala.QQ.abs(-10).tdo(10).sum.z"), - """object QQ is not a member of package scala""", - """ - twRuntimeErrors("@scala.QQ.abs(-10).tdo(10).sum.z"), - ^ - """ - ) - * - check( - twRuntimeErrors("@scala.math.abs(-10).tdo(10).sum.z"), - "value tdo is not a member of Int", - """ - twRuntimeErrors("@scala.math.abs(-10).tdo(10).sum.z"), - ^ - """ - ) - * - check( - twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z"), - "value z is not a member of Int", - """ - twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z"), - ^ - """ - ) - * - check( - twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z()"), - "value z is not a member of Int", - """ - twRuntimeErrors("@scala.math.abs(-10).to(10).sum.z()"), - ^ - """ - ) - * - check( - twRuntimeErrors("@scala.math.abs(-10).cow.sum.z"), - "value cow is not a member of Int", - """ - twRuntimeErrors("@scala.math.abs(-10).cow.sum.z"), - ^ - """ - ) - * - check( - twRuntimeErrors("@scala.smath.abs.cow.sum.z"), - "object smath is not a member of package scala", - """ - twRuntimeErrors("@scala.smath.abs.cow.sum.z"), - ^ - """ - ) - * - check( - twRuntimeErrors("@scala.math.cos('omg)"), - "type mismatch", - """ - twRuntimeErrors("@scala.math.cos('omg)"), - ^ - """ - ) - * - check( - twRuntimeErrors("@scala.math.cos[omg]('omg)"), - "not found: type omg", - """ - twRuntimeErrors("@scala.math.cos[omg]('omg)"), - ^ - """ - ) - * - check( - twRuntimeErrors(""" - I am cow hear me moo - @scala.math.abs(-10).tdo(10).sum.z - I weigh twice as much as you - """), - "value tdo is not a member of Int", - """ - @scala.math.abs(-10).tdo(10).sum.z - ^ - """ - ) - } - 'curlies{ - * - check( - twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), - "missing arguments for method foldLeft", - """ - twRuntimeErrors("@p{@Seq(1, 2, 3).foldLeft(0)}"), - ^ - """ - ) - - * - check( - twRuntimeErrors("@Nil.foldLeft{XY}"), - "missing arguments for method foldLeft", - """ - twRuntimeErrors("@Nil.foldLeft{XY}"), - ^ - """ - ) - -// * - check( -// twRuntimeErrors("@Seq(1).map{(y: String) => omg}"), -// "type mismatch", -// """ -// twRuntimeErrors("@Seq(1).map{(y: String) => omg}"), -// ^ -// """ -// ) -// * - check( -// twRuntimeErrors("@Nil.map{ omg}"), -// "too many arguments for method map", -// """ -// twRuntimeErrors("@Nil.map{ omg}"), -// ^ -// """ -// ) - } - 'callContents{ - * - check( - twRuntimeErrors("@scala.math.abs((1, 2).wtf)"), - "value wtf is not a member of (Int, Int)", - """ - twRuntimeErrors("@scala.math.abs((1, 2).wtf)"), - ^ - """ - ) - - * - check( - twRuntimeErrors("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"), - "value wtf is not a member of String", - """ - twRuntimeErrors("@scala.math.abs((1, 2).swap._1.toString().map(_.toString.wtf))"), - ^ - """ - ) - } - } - 'ifElse{ - 'oneLine { - * - check( - twRuntimeErrors("@if(math > 10){ 1 }else{ 2 }"), - "object > is not a member of package math", - """ - twRuntimeErrors("@if(math > 10){ 1 }else{ 2 }"), - ^ - """ - ) - * - check( - twRuntimeErrors("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"), - "Unspecified value parameter y", - """ - twRuntimeErrors("@if(true){ (@math.pow(10)) * 10 }else{ 2 }"), - ^ - """ - ) - * - check( - twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), - "too many arguments for method sin: (x: Double)Double", - """ - twRuntimeErrors("@if(true){ * 10 }else{ @math.sin(3, 4, 5) }"), - ^ - """ - ) - } - 'multiLine{ - * - check( - twRuntimeErrors(""" - Ho Ho Ho - - @if(math != 10) - I am a cow - @else - You are a cow - GG - """), - "object != is not a member of package math", - """ - @if(math != 10) - ^ - """ - ) - * - check( - twRuntimeErrors(""" - Ho Ho Ho - - @if(4 != 10) - I am a cow @math.lols - @else - You are a cow - GG - """), - "object lols is not a member of package math", - """ - I am a cow @math.lols - ^ - """ - ) - * - check( - twRuntimeErrors(""" - Ho Ho Ho - - @if(12 != 10) - I am a cow - @else - @math.E.toString.gog(1) - GG - """), - "value gog is not a member of String", - """ - @math.E.toString.gog(1) - ^ - """ - ) - } - } - 'forLoop{ - 'oneLine{ - 'header - check( - twRuntimeErrors("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"), - """value omglolol is not a member of Int""", - """ - twRuntimeErrors("omg @for(x <- (0 + 1 + 2) omglolol (10 + 11 + 2)){ hello }"), - ^ - """ - ) - - 'body - check( - twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), - """too many arguments for method +""", - """ - twRuntimeErrors("omg @for(x <- 0 until 10){ @((x, 2) + (1, 2)) }"), - ^ - """ - ) - } - 'multiLine{ - 'body - check( - twRuntimeErrors(""" - omg - @for(x <- 0 until 10) - I am cow hear me moo - I weigh twice as much as @x.kkk - """), - """value kkk is not a member of Int""", - """ - I weigh twice as much as @x.kkk - ^ - """ - ) - } - } - 'multiLine{ - 'missingVar - check( - twRuntimeErrors(""" - omg @notInScope lol - """), - """not found: value notInScope""", - """ - omg @notInScope lol - ^ - """ - ) -// 'wrongType - check( -// twRuntimeErrors(""" -// omg @{() => ()} lol -// """), -// """type mismatch""", -// """ -// omg @{() => ()} lol -// ^ -// """ -// ) - - 'bigExpression - check( - twRuntimeErrors(""" - @{ - val x = 1 + 2 - val y = new Object() - val z = y * x - x - } - """), - "value * is not a member of Object", - """ - val z = y * x - ^ - """ - ) - } - } -} diff --git a/scalatexApi/src/test/scala/scalatex/ParserTests.scala b/scalatexApi/src/test/scala/scalatex/ParserTests.scala deleted file mode 100644 index 9a4ee63..0000000 --- a/scalatexApi/src/test/scala/scalatex/ParserTests.scala +++ /dev/null @@ -1,424 +0,0 @@ -package scalatex - - -import org.parboiled2._ -import scalaParser.ScalaSyntax - -import scalatex.stages.{Trim, Parser, Ast} -import scalatex.stages.Ast.Block.{IfElse, For, Text} -import Ast.Chain.Args - -object ParserTests extends utest.TestSuite{ - import Ast._ - import utest._ - def check[T](input: String, parse: Parser => scala.util.Try[T], expected: T) = { - val parsed = parse(new Parser(input)).get - assert(parsed == expected) - } - def tests = TestSuite{ - 'Trim{ - def wrap(s: String) = "|" + s + "|" - * - { - val trimmed = wrap(stages.Trim.old(""" - i am cow - hear me moo - i weigh twice as much as you - """)) - val expected = wrap(""" - |i am cow - | hear me moo - | i weigh twice as much as you - |""".stripMargin) - assert(trimmed == expected) - - } - * - { - val trimmed = wrap(stages.Trim.old( - """ - @{"lol" * 3} - @{ - val omg = "omg" - omg * 2 - } - """ - )) - val expected = wrap( - """ - |@{"lol" * 3} - |@{ - | val omg = "omg" - | omg * 2 - |} - |""".stripMargin - ) - assert(trimmed == expected) - } - 'dropTrailingWhitespace - { - - val trimmed = wrap(stages.Trim.old( - Seq( - " i am a cow ", - " hear me moo ", - " i weigh twice as much as you" - ).mkString("\n") - )) - val expected = wrap( - Seq( - "i am a cow", - " hear me moo", - " i weigh twice as much as you" - ).mkString("\n") - ) - assert(trimmed == expected) - } - } - 'Text { - * - check("i am a cow", _.Text.run(), Block.Text("i am a cow")) - * - check("i am a @cow", _.Text.run(), Block.Text("i am a ")) - * - check("i am a @@cow", _.Text.run(), Block.Text("i am a @cow")) - * - check("i am a @@@cow", _.Text.run(), Block.Text("i am a @")) - * - check("i am a @@@@cow", _.Text.run(), Block.Text("i am a @@cow")) - - } - 'Code{ - 'identifier - check("@gggg ", _.Code.run(), "gggg") - 'parens - check("@(1 + 1)lolsss\n", _.Code.run(), "(1 + 1)") - 'curlies - check("@{{1} + (1)} ", _.Code.run(), "{{1} + (1)}") - 'blocks - check("@{val x = 1; 1} ", _.Code.run(), "{val x = 1; 1}") - 'weirdBackticks - check("@{`{}}{()@`}\n", _.Code.run(), "{`{}}{()@`}") - } - 'MiscCode{ - 'imports{ - * - check("@import math.abs", _.Header.run(), "import math.abs") - * - check("@import math.{abs, sin}", _.Header.run(), "import math.{abs, sin}") - } - 'headerblocks{ - check( - """@import math.abs - |@import math.sin - | - |hello world - |""".stripMargin, - _.HeaderBlock.run(), - Ast.Header( - "import math.abs\nimport math.sin", - Ast.Block( - Seq(Text("\n", 33), Text("\n", 34), Text("hello world", 35), Text("\n", 46)), - 33 - ) - ) - ) - } - 'caseclass{ - check( - """@case class Foo(i: Int, s: String) - """.stripMargin, - _.Header.run(), - "case class Foo(i: Int, s: String)" - ) - } - - } - 'Block{ - * - check("{i am a cow}", _.BraceBlock.run(), Block(Seq(Block.Text("i am a cow", 1)), 1)) - * - check("{i @am a @cow}", _.BraceBlock.run(), - Block(Seq( - Block.Text("i ", 1), - Chain("am",Seq(), 3), - Block.Text(" a ", 6), - Chain("cow",Seq(), 9) - ), 1) - ) - } - 'Chain{ - * - check("@omg.bbq[omg].fff[fff](123) ", _.ScalaChain.run(), - Chain("omg",Seq( - Chain.Prop("bbq", 4), - Chain.TypeArgs("[omg]", 8), - Chain.Prop("fff", 13), - Chain.TypeArgs("[fff]", 17), - Chain.Args("(123)", 22) - )) - ) - * - check("@omg{bbq}.cow(moo){a @b}\n", _.ScalaChain.run(), - Chain("omg",Seq( - Block(Seq(Text("bbq", 5)), 5), - Chain.Prop("cow", 9), - Chain.Args("(moo)", 13), - Block(Seq(Text("a ", 19), Chain("b", Nil, 21)), 19) - )) - ) - } - 'ControlFlow{ - 'for { - 'for - check( - "@for(x <- 0 until 3){lol}", - _.ForLoop.run(), - For("for(x <- 0 until 3)", Block(Seq(Text("lol", 21)), 21)) - ) - 'forBlock - check( - """ - |@for(x <- 0 until 3) - | lol""".stripMargin, - _.Body.run(), - Block(Seq( - Text("\n"), - For( - "for(x <- 0 until 3)", - Block(Seq(Text("\n ", 21), Text("lol", 24)), 21), - 1 - ) - )) - ) - 'forBlockBraces - check( - """ - |@for(x <- 0 until 3){ - | lol - |}""".stripMargin, - _.Body.run(), - Block(Seq( - Text("\n"), - For( - "for(x <- 0 until 3)", - Block(Seq(Text("\n ", 22), Text("lol", 25), Text("\n", 28)), 22), - 1 - ) - )) - ) - } - 'ifElse { - 'if - check( - "@if(true){lol}", - _.IfElse.run(), - IfElse("if(true)", Block(Seq(Text("lol", 10)), 10), None) - ) - 'ifElse - check( - "@if(true){lol}else{ omg }", - _.IfElse.run(), - IfElse("if(true)", Block(Seq(Text("lol", 10)), 10), Some(Block(Seq(Text(" omg ", 19)), 19))) - ) - 'ifBlock - check( - """ - |@if(true) - | omg""".stripMargin, - _.IfElse.run(), - IfElse("if(true)", Block(Seq(Text("\n ", 10), Text("omg", 13)), 10), None, 1) - ) - 'ifBlockElseBlock - check( - """ - |@if(true) - | omg - |@else - | wtf""".stripMargin, - _.IfElse.run(), - IfElse( - "if(true)", - Block(Seq(Text("\n ", 10), Text("omg", 13)), 10), - Some(Block(Seq(Text("\n ", 22), Text("wtf", 25)), 22)), - 1 - ) - ) - 'ifBlockElseBraceBlock - check( - """@if(true){ - | omg - |}else{ - | wtf - |}""".stripMargin, - _.IfElse.run(), - IfElse( - "if(true)", - Block(Seq(Text("\n ", 10), Text("omg", 13), Text("\n", 16)), 10), - Some(Block(Seq(Text("\n ", 23), Text("wtf", 26), Text("\n", 29)), 23)), - 0 - ) - ) - 'ifBlockElseBraceBlockNested - { - val res = Parser(Trim.old( - """ - @p - @if(true){ - Hello - }else{ - lols - } - """)) - val expected = - Block(Vector( - Text("\n"), - Chain("p",Vector(Block(Vector( - Text("\n ", 3), - IfElse("if(true)", - Block(Vector( - Text("\n ", 16), Text("Hello", 21), Text("\n ", 26) - ), 16), - Some(Block(Vector( - Text("\n ", 35), Text("lols", 40), Text("\n ", 44) - ), 35)), - 6 - )), 3)), 1), - Text("\n", 48) - )) - assert(res == expected) - } - 'ifElseBlock - check( - """@if(true){ - | omg - |}else - | wtf""".stripMargin, - _.IfElse.run(), - IfElse( - "if(true)", - Block(Seq(Text("\n ", 10), Text("omg", 13), Text("\n", 16)), 10), - Some(Block(Seq(Text("\n ", 22), Text("wtf", 25)), 22)) - ) - ) - } - - } - 'Body{ - 'indents - check( - """ - |@omg - | @wtf - | @bbq - | @lol""".stripMargin, - _.Body.run(), - Block(Seq( - Text("\n"), - Chain("omg",Seq(Block(Seq( - Text("\n ", 5), - Chain("wtf",Seq(Block(Seq( - Text("\n ", 7), - Chain("bbq",Seq(Block(Seq( - Text("\n ", 9), - Chain("lol",Seq(), 16) - ), 9)), 12) - ), 7)), 8) - ), 5)), 1) - )) - ) - 'dedents - check( - """ - |@omg - | @wtf - |@bbq""".stripMargin, - _.Body.run(), - Block(Seq( - Text("\n"), - Chain("omg",Seq(Block( - Seq( - Text("\n ", 5), - Chain("wtf",Seq(), 8) - ), - 5 - )), 1), - Text("\n", 12), - Chain("bbq", Seq(), 13) - )) - ) - 'braces - check( - """ - |@omg{ - | @wtf - |} - |@bbq""".stripMargin, - _.Body.run(), - Block(Seq( - Text("\n"), - Chain("omg",Seq(Block( - Seq( - Text("\n ", 6), - Chain("wtf",Seq(), 9), - Text("\n", 13) - ), - 6 - )), 1), - Text("\n", 15), - Chain("bbq", Seq(), 16) - )) - ) - 'dedentText - check( - """ - |@omg("lol", 1, 2) - | @wtf - |bbq""".stripMargin, - _.Body.run(), - Block(Seq( - Text("\n"), - Chain("omg",Seq( - Args("""("lol", 1, 2)""", 5), - Block(Seq( - Text("\n ", 18), - Chain("wtf",Seq(), 21) - ), 18) - ), 1), - Text("\n", 25), - Text("bbq", 26) - )) - ) - * - check( - """ - |@omg("lol", - |1, - | 2 - | ) - | wtf - |bbq""".stripMargin, - _.Body.run(), - Block(Seq( - Text("\n", 0), - Chain("omg",Seq( - Args("(\"lol\",\n1,\n 2\n )", 5), - Block(Seq( - Text("\n ", 30), Text("wtf", 33) - ), 30) - ), 1), - Text("\n", 36), - Text("bbq", 37) - ), 0) - ) - 'codeBlock - check( - """@{ - | val omg = "omg" - | omg * 2 - |}""".stripMargin, - _.Code.run(), - """{ - | val omg = "omg" - | omg * 2 - |}""".stripMargin - ) - 'codeBlocks - check( - """ - |@{"lol" * 3} - |@{ - | val omg = "omg" - | omg * 2 - |}""".stripMargin, - _.Body.run(), - Block(Seq( - Text("\n"), - Chain("{\"lol\" * 3}", Seq(), 1), - Text("\n", 13), - Chain("""{ - | val omg = "omg" - | omg * 2 - |}""".stripMargin, - Seq(), - 14 - ) - )) - ) - } -// 'Test{ -// check( -// "@{() => ()}", -// _.Code.run(), -// "" -// ) -// } - } -} - - - diff --git a/scalatexApi/src/test/scala/scalatex/TestUtil.scala b/scalatexApi/src/test/scala/scalatex/TestUtil.scala deleted file mode 100644 index 5a72677..0000000 --- a/scalatexApi/src/test/scala/scalatex/TestUtil.scala +++ /dev/null @@ -1,16 +0,0 @@ -package scalatex - -import utest._ - - -object TestUtil { - implicit def stringify(f: scalatags.Text.all.Frag) = f.render - def check(rendered: String*) = { - val collapsed = rendered.map(collapse) - val first = collapsed(0) - assert(collapsed.forall(_ == first)) - } - def collapse(s: String): String = { - s.replaceAll("[ \n]", "") - } -} diff --git a/scalatexPlugin/src/main/resources/scalac-plugin.xml b/scalatexPlugin/src/main/resources/scalac-plugin.xml deleted file mode 100755 index d4062fa..0000000 --- a/scalatexPlugin/src/main/resources/scalac-plugin.xml +++ /dev/null @@ -1,4 +0,0 @@ - - demo-plugin - scalatex.ScalaTexPlugin - \ No newline at end of file diff --git a/scalatexPlugin/src/main/scala/scalatex/ScalaTexPlugin.scala b/scalatexPlugin/src/main/scala/scalatex/ScalaTexPlugin.scala deleted file mode 100755 index 6686258..0000000 --- a/scalatexPlugin/src/main/scala/scalatex/ScalaTexPlugin.scala +++ /dev/null @@ -1,66 +0,0 @@ -package scalatex - -import java.nio.file.Paths - -import scala.reflect.internal.util.BatchSourceFile -import scala.reflect.io.VirtualFile -import scala.tools.nsc.{ Global, Phase } -import scala.tools.nsc.plugins.{ Plugin, PluginComponent } - -class ScalaTexPlugin(val global: Global) extends Plugin { - import global._ - println(options) - val name = "scalatex-plugin" - val description = "Enforces coding standards" - val components = List[PluginComponent](DemoComponent) - private object DemoComponent extends PluginComponent { - val global = ScalaTexPlugin.this.global - import global._ - - override val runsAfter = List("parser") - override val runsBefore = List("namer") - - val phaseName = "Demo" - val scalatexRoot = "book/src/main/scalatex" - override def newPhase(prev: Phase) = new GlobalPhase(prev) { - override def run() = { - def recursiveListFiles(f: java.io.File): Array[java.io.File] = { - val (dirs, files) = f.listFiles().partition(_.isDirectory) - files ++ dirs.flatMap(recursiveListFiles) - } - for (file <- recursiveListFiles(new java.io.File(scalatexRoot))) { - val txt = io.Source.fromFile(file).mkString - val fakeJfile = new java.io.File(file.getName) - val virtualFile = new VirtualFile(file.getName) { - override def file = fakeJfile - } - val sourceFile = new BatchSourceFile(virtualFile, txt) - val unit = new CompilationUnit(sourceFile) - val name = file.getName - val objectName = name.slice(name.lastIndexOf('/'), name.lastIndexOf('.')) - val pkgName = - Paths.get(scalatexRoot) - .relativize(file.getParentFile.toPath) - .toString - .split("/") - .map(s => s"package $s") - .mkString("\n") - - val shim = s""" - $pkgName - import scalatags.Text.all._ - object $objectName{ - def apply() = scalatex.twf("${file.getPath}") - } - """ - unit.body = global.newUnitParser(shim).parse() - global.currentRun.compileLate(unit) - } - } - - def name: String = phaseName - - def apply(unit: global.CompilationUnit): Unit = {} - } - } -} -- cgit v1.2.3