Skip to content

Commit

Permalink
fixes #264 to get stand alone coffee files compiling to .js files and…
Browse files Browse the repository at this point in the history
… added a few example jade files using embedded or external coffeescript
  • Loading branch information
jstrachan committed Sep 13, 2011
1 parent 71398de commit 96c723c
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 58 deletions.
4 changes: 2 additions & 2 deletions project/build.properties
Expand Up @@ -11,6 +11,6 @@ def.scala.version=2.7.7
#def.scala.version=2.8.1.RC1

project.version=1.6-SNAPSHOT
scala.version=2.9.0
build.scala.versions=2.9.0
scala.version=2.9.1
build.scala.versions=2.9.1
project.initialize=false
8 changes: 8 additions & 0 deletions samples/scalate-example/src/main/webapp/coffee/embedded.jade
@@ -0,0 +1,8 @@
h1 Embedded CoffeeScript demo

p This should demonstrate some CoffeeScript being compiled on the server to JavaScript and included inside a Jade page

:coffeescript
alert "Hello, Coffee embedded!"
p You should get an alert :)
7 changes: 7 additions & 0 deletions samples/scalate-example/src/main/webapp/coffee/external.jade
@@ -0,0 +1,7 @@
h1 External CoffeeScript demo

p This should demonstrate some CoffeeScript being compiled on the server to JavaScript and referenced inside a Jade page

script(src="foo.js" type="text/javascript")

p You should get an alert :)
1 change: 1 addition & 0 deletions samples/scalate-example/src/main/webapp/coffee/foo.coffee
@@ -0,0 +1 @@
alert "Hello, from included Coffee JavaScript file!"
10 changes: 10 additions & 0 deletions samples/scalate-example/src/main/webapp/coffee/index.jade
@@ -0,0 +1,10 @@
h1 CoffeeScript Examples

p Here are a few samples of using CoffeeScript and SASS/SCSS with Scalate using Jade

ul
li
a(href="embedded") Embedded CoffeeScript inside Jade
li
a(href="external") External CoffeeScript referenced in Jade

2 changes: 2 additions & 0 deletions samples/scalate-example/src/main/webapp/index.jade
Expand Up @@ -38,5 +38,7 @@ ul
h2 Miscellaneous Examples

ul
li
a(href="coffee/index") CoffeeScript samples
li
a(href="tableWithLayout") Jade Table With Custom Layout
Expand Up @@ -177,6 +177,19 @@ class TemplateEngine(var sourceDirectories: Traversable[File] = None, var mode:

var pipelines: Map[String, List[Filter]] = Map()

/**
* Maps file extensions to possible template extensions for custom mappins such as for
* Map("js" -> Set("coffee"), "css" => Set("sass", "scss"))
*/
var extensionToTemplateExtension: collection.mutable.Map[String, collection.mutable.Set[String]] = collection.mutable.Map()

/**
* Returns the mutable set of template extensions which are mapped to the given URI extension.
*/
def templateExtensionsFor(extension: String): collection.mutable.Set[String] = {
extensionToTemplateExtension.getOrElseUpdate(extension, collection.mutable.Set())
}

private val attempt = Exception.ignoring(classOf[Throwable])

/**
Expand All @@ -187,12 +200,16 @@ class TemplateEngine(var sourceDirectories: Traversable[File] = None, var mode:

// Attempt to load all the built in filters.. Some may not load do to missing classpath
// dependencies.
attempt( filters += "plain" -> PlainFilter )
attempt( filters += "javascript"-> JavascriptFilter )
attempt( filters += "coffeescript"-> CoffeeScriptFilter )
attempt( filters += "css"-> CssFilter )
attempt( filters += "cdata"-> CdataFilter )
attempt( filters += "escaped"->EscapedFilter )
attempt(filters += "plain" -> PlainFilter)
attempt(filters += "javascript" -> JavascriptFilter)
attempt(filters += "coffeescript" -> CoffeeScriptFilter)
attempt(filters += "css" -> CssFilter)
attempt(filters += "cdata" -> CdataFilter)
attempt(filters += "escaped" -> EscapedFilter)

attempt{
CoffeeScriptPipeline(this)
}

var layoutStrategy: LayoutStrategy = NullLayoutStrategy

Expand Down
Expand Up @@ -26,7 +26,7 @@ import java.util.concurrent.atomic.AtomicBoolean

/**
* Surrounds the filtered text with <script> and CDATA tags.
*
*
* <p>Useful for including inline Javascript.</p>
*
* @author <a href="http://hiramchirino.com">Hiram Chirino</a>
Expand Down Expand Up @@ -62,7 +62,7 @@ object CoffeeScriptFilter extends Filter with Log {

if (serverSideCompile) {
try {
Compiler.compile(content, Some(context.currentTemplate)).fold({
CoffeeScriptCompiler.compile(content, Some(context.currentTemplate)).fold({
error =>
warn("Could not compile coffeescript: " + error, error)
throw new CompilerException(error.message, Nil)
Expand All @@ -83,22 +83,48 @@ object CoffeeScriptFilter extends Filter with Log {
clientSideCompile
}
}
}

/**
* Compiles a .coffee file into JS on the server side
*/
object CoffeeScriptPipeline extends Filter with Log {

/**
* A Scala / Rhino Coffeescript compiler.
* Installs the coffeescript pipeline
*/
object Compiler {

/**
* Compiles a string of Coffeescript code to Javascript.
*
* @param code the Coffeescript code
* @param sourceName a descriptive name for the code unit under compilation (e.g a filename)
* @param bare if true, no function wrapper will be generated
* @return the compiled Javascript code
*/
def compile(code: String, sourceName: Option[String] = None, bare : Boolean = false)
: Either[CompilationError, String] = withContext { ctx =>
def apply(engine: TemplateEngine) {
engine.pipelines += "coffee" -> List(NoLayoutFilter(this, "text/javascript"))
engine.templateExtensionsFor("js") += "coffee"
}

def filter(context: RenderContext, content: String) = {
CoffeeScriptCompiler.compile(content, Some(context.currentTemplate)).fold({
error =>
warn("Could not compile coffeescript: " + error, error)
throw new CompilerException(error.message, Nil)
}, {
coffee => coffee
})
}
}

/**
* A Scala / Rhino Coffeescript compiler.
*/
object CoffeeScriptCompiler {

/**
* Compiles a string of Coffeescript code to Javascript.
*
* @param code the Coffeescript code
* @param sourceName a descriptive name for the code unit under compilation (e.g a filename)
* @param bare if true, no function wrapper will be generated
* @return the compiled Javascript code
*/
def compile(code: String, sourceName: Option[String] = None, bare: Boolean = false)
: Either[CompilationError, String] = withContext {
ctx =>
val scope = ctx.initStandardObjects()
ctx.evaluateReader(
scope,
Expand All @@ -113,22 +139,20 @@ object CoffeeScriptFilter extends Filter with Log {
try {
Right(compileFunc.call(ctx, scope, coffee, Array(code, opts)).asInstanceOf[String])
} catch {
case e : JavaScriptException =>
case e: JavaScriptException =>
Left(CompilationError(sourceName, e.getValue.toString))
}
}
}

def withContext[T](f: Context => T): T = {
val ctx = Context.enter()
try {
ctx.setOptimizationLevel(-1) // Do not compile to byte code (max 64kb methods)
f(ctx)
} finally {
Context.exit()
}
def withContext[T](f: Context => T): T = {
val ctx = Context.enter()
try {
ctx.setOptimizationLevel(-1) // Do not compile to byte code (max 64kb methods)
f(ctx)
} finally {
Context.exit()
}
}

case class CompilationError(sourceName: Option[String], message: String)
}

case class CompilationError(sourceName: Option[String], message: String)
Expand Up @@ -18,6 +18,8 @@
package org.fusesource.scalate
package filter

import servlet.ServletRenderContext

/**
* Represents a request to filter content.
*
Expand All @@ -34,3 +36,16 @@ trait Filter {
def filter(context: RenderContext, content: String): String
}

/**
* A useful filter for wrapping other filters as a Pipeline (a top level processor of stand alone resources)
*/
case class NoLayoutFilter(val next: Filter, contentType: String) extends Filter {
def filter(context: RenderContext, content: String) = {
context.attributes("layout") = "" // disable the layout
context match {
case x: ServletRenderContext => x.response.setContentType(contentType)
case _ =>
}
next.filter(context, content)
}
}
Expand Up @@ -18,6 +18,8 @@
package org.fusesource.scalate.support

import org.fusesource.scalate.TemplateEngine
import org.fusesource.scalate.util.Files

/**
* A helper object to find a template from a URI using a number of possible extensions and directories
*/
Expand All @@ -29,9 +31,9 @@ class TemplateFinder(engine: TemplateEngine) {

def findTemplate(path: String): Option[String] = {
var rc = Option(engine.finderCache.get(path))
if( rc.isEmpty ) {
if (rc.isEmpty) {
rc = search(path)
if( rc.isDefined && !engine.isDevelopmentMode ) {
if (rc.isDefined && !engine.isDevelopmentMode) {
engine.finderCache.put(path, rc.get)
}
}
Expand All @@ -41,7 +43,7 @@ class TemplateFinder(engine: TemplateEngine) {
def search(rootOrPath: String): Option[String] = {
val path = if (rootOrPath == "/") "/index" else rootOrPath

for( p <- hiddenPatterns ; if p.findFirstIn(path).isDefined ) {
for (p <- hiddenPatterns; if p.findFirstIn(path).isDefined) {
return None
}

Expand Down Expand Up @@ -72,16 +74,40 @@ class TemplateFinder(engine: TemplateEngine) {
// Lets try to find the template by replacing the extension
// i.e: /path/page.html -> /path/page.jade
def findReplaced(): Option[String] = {
replacedExtensions.foreach {ext =>
if (path.endsWith(ext)) {
val rc = findAppended(path.stripSuffix(ext))
if (rc != None)
return rc
replacedExtensions.foreach {
ext =>
if (path.endsWith(ext)) {
val rc = findAppended(path.stripSuffix(ext))
if (rc != None)
return rc
}
}
None
}

// Lets try to find the template for well known template extensions
// i.e:
// /path/page.css -> List(/path/page.sass, /path/page.scss)
// /path/page.js -> List(/path/page.coffee)
def findTemplateAlias(uri: String): Option[String] = {
val ext = Files.extension(uri)
lazy val remaining = path.stripSuffix(ext)
if (ext.size > 0) {
engine.extensionToTemplateExtension.get(ext) match {
case Some(set) =>
for (base <- engine.templateDirectories; ext <- set) {
val path = base + remaining + ext
if (engine.resourceLoader.exists(path)) {
return Some(path)
}
}
None
case _ => None
}
}
None
}

findDirect(path).orElse(findAppended(path).orElse(findReplaced()))
findDirect(path).orElse(findAppended(path).orElse(findTemplateAlias(path).orElse(findReplaced())))
}
}
@@ -0,0 +1 @@
alert "Hello, Coffee!"
@@ -0,0 +1,23 @@
package org.fusesource.scalate.filter

import org.fusesource.scalate.TemplateTestSupport
import org.fusesource.scalate.util.ResourceLoader
import org.fusesource.scalate.support.TemplateFinder

class CoffeeScriptPipelineTest extends TemplateTestSupport {

lazy val finder = new TemplateFinder(engine)

test("coffeescript pipeline") {
assertUriOutputContains("/org/fusesource/scalate/filter/sample.js",
"""(function() {
alert("Hello, Coffee!");
}).call(this);
""")
}

override protected def fromUri(uri: String) = {
val newUri = finder.findTemplate(uri).getOrElse(uri)
super.fromUri(newUri)
}
}
Expand Up @@ -4,7 +4,7 @@ import org.fusesource.scalate.util.Log
import org.fusesource.scalate.{TemplateException, RenderContext, TemplateEngine, TemplateEngineAddOn}
import java.io.File
import org.fusesource.scalate.servlet.ServletRenderContext
import org.fusesource.scalate.filter.{CssFilter, Pipeline, Filter}
import org.fusesource.scalate.filter.{NoLayoutFilter, CssFilter, Pipeline, Filter}

/**
* <p>
Expand All @@ -19,28 +19,19 @@ object Sass extends TemplateEngineAddOn with Log {
if (!te.filters.contains("sass")) {
val sass = new Sass(jruby, te)
te.filters += "sass" -> Pipeline(List(sass, CssFilter))
te.pipelines += "sass" -> List(NoLayout(sass))
te.pipelines += "sass" -> List(NoLayoutFilter(sass, "text/css"))
te.templateExtensionsFor("css") += "sass"
}

if (!te.filters.contains("scss")) {
val scss = new Scss(jruby, te)
te.filters += "scss" -> Pipeline(List(scss, CssFilter))
te.pipelines += "scss" -> List(NoLayout(scss))
te.pipelines += "scss" -> List(NoLayoutFilter(scss, "text/css"))
te.templateExtensionsFor("css") += "scss"
}
}
}

case class NoLayout(val next: Sass) extends Filter {
def filter(context: RenderContext, content: String) = {
context.attributes("layout")="" // disable the layout
context match {
case x:ServletRenderContext => x.response.setContentType("text/css")
case _ =>
}
next.filter(context, content)
}
}

class Sass(val jruby:JRuby, val engine: TemplateEngine) extends Filter {

def syntax = "sass"
Expand Down

0 comments on commit 96c723c

Please sign in to comment.