Skip to content

Commit 96c723c

Browse files
committed
fixes scalate#264 to get stand alone coffee files compiling to .js files and added a few example jade files using embedded or external coffeescript
1 parent 71398de commit 96c723c

File tree

13 files changed

+183
-58
lines changed

13 files changed

+183
-58
lines changed

project/build.properties

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ def.scala.version=2.7.7
1111
#def.scala.version=2.8.1.RC1
1212

1313
project.version=1.6-SNAPSHOT
14-
scala.version=2.9.0
15-
build.scala.versions=2.9.0
14+
scala.version=2.9.1
15+
build.scala.versions=2.9.1
1616
project.initialize=false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
h1 Embedded CoffeeScript demo
2+
3+
p This should demonstrate some CoffeeScript being compiled on the server to JavaScript and included inside a Jade page
4+
5+
:coffeescript
6+
alert "Hello, Coffee embedded!"
7+
8+
p You should get an alert :)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
h1 External CoffeeScript demo
2+
3+
p This should demonstrate some CoffeeScript being compiled on the server to JavaScript and referenced inside a Jade page
4+
5+
script(src="foo.js" type="text/javascript")
6+
7+
p You should get an alert :)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
alert "Hello, from included Coffee JavaScript file!"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
h1 CoffeeScript Examples
2+
3+
p Here are a few samples of using CoffeeScript and SASS/SCSS with Scalate using Jade
4+
5+
ul
6+
li
7+
a(href="embedded") Embedded CoffeeScript inside Jade
8+
li
9+
a(href="external") External CoffeeScript referenced in Jade
10+

samples/scalate-example/src/main/webapp/index.jade

+2
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,7 @@ ul
3838
h2 Miscellaneous Examples
3939

4040
ul
41+
li
42+
a(href="coffee/index") CoffeeScript samples
4143
li
4244
a(href="tableWithLayout") Jade Table With Custom Layout

scalate-core/src/main/scala/org/fusesource/scalate/TemplateEngine.scala

+23-6
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,19 @@ class TemplateEngine(var sourceDirectories: Traversable[File] = None, var mode:
177177

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

180+
/**
181+
* Maps file extensions to possible template extensions for custom mappins such as for
182+
* Map("js" -> Set("coffee"), "css" => Set("sass", "scss"))
183+
*/
184+
var extensionToTemplateExtension: collection.mutable.Map[String, collection.mutable.Set[String]] = collection.mutable.Map()
185+
186+
/**
187+
* Returns the mutable set of template extensions which are mapped to the given URI extension.
188+
*/
189+
def templateExtensionsFor(extension: String): collection.mutable.Set[String] = {
190+
extensionToTemplateExtension.getOrElseUpdate(extension, collection.mutable.Set())
191+
}
192+
180193
private val attempt = Exception.ignoring(classOf[Throwable])
181194

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

188201
// Attempt to load all the built in filters.. Some may not load do to missing classpath
189202
// dependencies.
190-
attempt( filters += "plain" -> PlainFilter )
191-
attempt( filters += "javascript"-> JavascriptFilter )
192-
attempt( filters += "coffeescript"-> CoffeeScriptFilter )
193-
attempt( filters += "css"-> CssFilter )
194-
attempt( filters += "cdata"-> CdataFilter )
195-
attempt( filters += "escaped"->EscapedFilter )
203+
attempt(filters += "plain" -> PlainFilter)
204+
attempt(filters += "javascript" -> JavascriptFilter)
205+
attempt(filters += "coffeescript" -> CoffeeScriptFilter)
206+
attempt(filters += "css" -> CssFilter)
207+
attempt(filters += "cdata" -> CdataFilter)
208+
attempt(filters += "escaped" -> EscapedFilter)
209+
210+
attempt{
211+
CoffeeScriptPipeline(this)
212+
}
196213

197214
var layoutStrategy: LayoutStrategy = NullLayoutStrategy
198215

scalate-core/src/main/scala/org/fusesource/scalate/filter/CoffeeScriptFilter.scala

+51-27
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import java.util.concurrent.atomic.AtomicBoolean
2626

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

6363
if (serverSideCompile) {
6464
try {
65-
Compiler.compile(content, Some(context.currentTemplate)).fold({
65+
CoffeeScriptCompiler.compile(content, Some(context.currentTemplate)).fold({
6666
error =>
6767
warn("Could not compile coffeescript: " + error, error)
6868
throw new CompilerException(error.message, Nil)
@@ -83,22 +83,48 @@ object CoffeeScriptFilter extends Filter with Log {
8383
clientSideCompile
8484
}
8585
}
86+
}
87+
88+
/**
89+
* Compiles a .coffee file into JS on the server side
90+
*/
91+
object CoffeeScriptPipeline extends Filter with Log {
8692

8793
/**
88-
* A Scala / Rhino Coffeescript compiler.
94+
* Installs the coffeescript pipeline
8995
*/
90-
object Compiler {
91-
92-
/**
93-
* Compiles a string of Coffeescript code to Javascript.
94-
*
95-
* @param code the Coffeescript code
96-
* @param sourceName a descriptive name for the code unit under compilation (e.g a filename)
97-
* @param bare if true, no function wrapper will be generated
98-
* @return the compiled Javascript code
99-
*/
100-
def compile(code: String, sourceName: Option[String] = None, bare : Boolean = false)
101-
: Either[CompilationError, String] = withContext { ctx =>
96+
def apply(engine: TemplateEngine) {
97+
engine.pipelines += "coffee" -> List(NoLayoutFilter(this, "text/javascript"))
98+
engine.templateExtensionsFor("js") += "coffee"
99+
}
100+
101+
def filter(context: RenderContext, content: String) = {
102+
CoffeeScriptCompiler.compile(content, Some(context.currentTemplate)).fold({
103+
error =>
104+
warn("Could not compile coffeescript: " + error, error)
105+
throw new CompilerException(error.message, Nil)
106+
}, {
107+
coffee => coffee
108+
})
109+
}
110+
}
111+
112+
/**
113+
* A Scala / Rhino Coffeescript compiler.
114+
*/
115+
object CoffeeScriptCompiler {
116+
117+
/**
118+
* Compiles a string of Coffeescript code to Javascript.
119+
*
120+
* @param code the Coffeescript code
121+
* @param sourceName a descriptive name for the code unit under compilation (e.g a filename)
122+
* @param bare if true, no function wrapper will be generated
123+
* @return the compiled Javascript code
124+
*/
125+
def compile(code: String, sourceName: Option[String] = None, bare: Boolean = false)
126+
: Either[CompilationError, String] = withContext {
127+
ctx =>
102128
val scope = ctx.initStandardObjects()
103129
ctx.evaluateReader(
104130
scope,
@@ -113,22 +139,20 @@ object CoffeeScriptFilter extends Filter with Log {
113139
try {
114140
Right(compileFunc.call(ctx, scope, coffee, Array(code, opts)).asInstanceOf[String])
115141
} catch {
116-
case e : JavaScriptException =>
142+
case e: JavaScriptException =>
117143
Left(CompilationError(sourceName, e.getValue.toString))
118144
}
119-
}
145+
}
120146

121-
def withContext[T](f: Context => T): T = {
122-
val ctx = Context.enter()
123-
try {
124-
ctx.setOptimizationLevel(-1) // Do not compile to byte code (max 64kb methods)
125-
f(ctx)
126-
} finally {
127-
Context.exit()
128-
}
147+
def withContext[T](f: Context => T): T = {
148+
val ctx = Context.enter()
149+
try {
150+
ctx.setOptimizationLevel(-1) // Do not compile to byte code (max 64kb methods)
151+
f(ctx)
152+
} finally {
153+
Context.exit()
129154
}
130155
}
131-
132-
case class CompilationError(sourceName: Option[String], message: String)
133156
}
134157

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

scalate-core/src/main/scala/org/fusesource/scalate/filter/Filter.scala

+15
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
package org.fusesource.scalate
1919
package filter
2020

21+
import servlet.ServletRenderContext
22+
2123
/**
2224
* Represents a request to filter content.
2325
*
@@ -34,3 +36,16 @@ trait Filter {
3436
def filter(context: RenderContext, content: String): String
3537
}
3638

39+
/**
40+
* A useful filter for wrapping other filters as a Pipeline (a top level processor of stand alone resources)
41+
*/
42+
case class NoLayoutFilter(val next: Filter, contentType: String) extends Filter {
43+
def filter(context: RenderContext, content: String) = {
44+
context.attributes("layout") = "" // disable the layout
45+
context match {
46+
case x: ServletRenderContext => x.response.setContentType(contentType)
47+
case _ =>
48+
}
49+
next.filter(context, content)
50+
}
51+
}

scalate-core/src/main/scala/org/fusesource/scalate/support/TemplateFinder.scala

+35-9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
package org.fusesource.scalate.support
1919

2020
import org.fusesource.scalate.TemplateEngine
21+
import org.fusesource.scalate.util.Files
22+
2123
/**
2224
* A helper object to find a template from a URI using a number of possible extensions and directories
2325
*/
@@ -29,9 +31,9 @@ class TemplateFinder(engine: TemplateEngine) {
2931

3032
def findTemplate(path: String): Option[String] = {
3133
var rc = Option(engine.finderCache.get(path))
32-
if( rc.isEmpty ) {
34+
if (rc.isEmpty) {
3335
rc = search(path)
34-
if( rc.isDefined && !engine.isDevelopmentMode ) {
36+
if (rc.isDefined && !engine.isDevelopmentMode) {
3537
engine.finderCache.put(path, rc.get)
3638
}
3739
}
@@ -41,7 +43,7 @@ class TemplateFinder(engine: TemplateEngine) {
4143
def search(rootOrPath: String): Option[String] = {
4244
val path = if (rootOrPath == "/") "/index" else rootOrPath
4345

44-
for( p <- hiddenPatterns ; if p.findFirstIn(path).isDefined ) {
46+
for (p <- hiddenPatterns; if p.findFirstIn(path).isDefined) {
4547
return None
4648
}
4749

@@ -72,16 +74,40 @@ class TemplateFinder(engine: TemplateEngine) {
7274
// Lets try to find the template by replacing the extension
7375
// i.e: /path/page.html -> /path/page.jade
7476
def findReplaced(): Option[String] = {
75-
replacedExtensions.foreach {ext =>
76-
if (path.endsWith(ext)) {
77-
val rc = findAppended(path.stripSuffix(ext))
78-
if (rc != None)
79-
return rc
77+
replacedExtensions.foreach {
78+
ext =>
79+
if (path.endsWith(ext)) {
80+
val rc = findAppended(path.stripSuffix(ext))
81+
if (rc != None)
82+
return rc
83+
}
84+
}
85+
None
86+
}
87+
88+
// Lets try to find the template for well known template extensions
89+
// i.e:
90+
// /path/page.css -> List(/path/page.sass, /path/page.scss)
91+
// /path/page.js -> List(/path/page.coffee)
92+
def findTemplateAlias(uri: String): Option[String] = {
93+
val ext = Files.extension(uri)
94+
lazy val remaining = path.stripSuffix(ext)
95+
if (ext.size > 0) {
96+
engine.extensionToTemplateExtension.get(ext) match {
97+
case Some(set) =>
98+
for (base <- engine.templateDirectories; ext <- set) {
99+
val path = base + remaining + ext
100+
if (engine.resourceLoader.exists(path)) {
101+
return Some(path)
102+
}
103+
}
104+
None
105+
case _ => None
80106
}
81107
}
82108
None
83109
}
84110

85-
findDirect(path).orElse(findAppended(path).orElse(findReplaced()))
111+
findDirect(path).orElse(findAppended(path).orElse(findTemplateAlias(path).orElse(findReplaced())))
86112
}
87113
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
alert "Hello, Coffee!"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.fusesource.scalate.filter
2+
3+
import org.fusesource.scalate.TemplateTestSupport
4+
import org.fusesource.scalate.util.ResourceLoader
5+
import org.fusesource.scalate.support.TemplateFinder
6+
7+
class CoffeeScriptPipelineTest extends TemplateTestSupport {
8+
9+
lazy val finder = new TemplateFinder(engine)
10+
11+
test("coffeescript pipeline") {
12+
assertUriOutputContains("/org/fusesource/scalate/filter/sample.js",
13+
"""(function() {
14+
alert("Hello, Coffee!");
15+
}).call(this);
16+
""")
17+
}
18+
19+
override protected def fromUri(uri: String) = {
20+
val newUri = finder.findTemplate(uri).getOrElse(uri)
21+
super.fromUri(newUri)
22+
}
23+
}

scalate-jruby/src/main/scala/org/fusesource/scalate/jruby/Sass.scala

+5-14
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import org.fusesource.scalate.util.Log
44
import org.fusesource.scalate.{TemplateException, RenderContext, TemplateEngine, TemplateEngineAddOn}
55
import java.io.File
66
import org.fusesource.scalate.servlet.ServletRenderContext
7-
import org.fusesource.scalate.filter.{CssFilter, Pipeline, Filter}
7+
import org.fusesource.scalate.filter.{NoLayoutFilter, CssFilter, Pipeline, Filter}
88

99
/**
1010
* <p>
@@ -19,28 +19,19 @@ object Sass extends TemplateEngineAddOn with Log {
1919
if (!te.filters.contains("sass")) {
2020
val sass = new Sass(jruby, te)
2121
te.filters += "sass" -> Pipeline(List(sass, CssFilter))
22-
te.pipelines += "sass" -> List(NoLayout(sass))
22+
te.pipelines += "sass" -> List(NoLayoutFilter(sass, "text/css"))
23+
te.templateExtensionsFor("css") += "sass"
2324
}
2425

2526
if (!te.filters.contains("scss")) {
2627
val scss = new Scss(jruby, te)
2728
te.filters += "scss" -> Pipeline(List(scss, CssFilter))
28-
te.pipelines += "scss" -> List(NoLayout(scss))
29+
te.pipelines += "scss" -> List(NoLayoutFilter(scss, "text/css"))
30+
te.templateExtensionsFor("css") += "scss"
2931
}
3032
}
3133
}
3234

33-
case class NoLayout(val next: Sass) extends Filter {
34-
def filter(context: RenderContext, content: String) = {
35-
context.attributes("layout")="" // disable the layout
36-
context match {
37-
case x:ServletRenderContext => x.response.setContentType("text/css")
38-
case _ =>
39-
}
40-
next.filter(context, content)
41-
}
42-
}
43-
4435
class Sass(val jruby:JRuby, val engine: TemplateEngine) extends Filter {
4536

4637
def syntax = "sass"

0 commit comments

Comments
 (0)