PK N>҃i i build.xml
PK .P"?d README# Play LESS module
This module allows you to use LESS (http://www.lesscss.org) directly in your Play! project. The documentation is in /documentation.
PK V"?V*P5 5 manifestversion=0.3.compatibility
frameworkVersions=1.1, 1.2
PK jT"?0 ! documentation/manual/home.textileh1. Less module compatible with Coffeescript module
*This version of the less module has been adapted to be compatible with the coffeescript module (or other software using Rhino 1.7). If you don't experience issues, use the regular less module versions.*
This module allows you to use "LESS":http://lesscss.org/ stylesheets in your Play application, without having to manually compile them to CSS. It is inspired by the sass plugin that does a similar thing for sass.
h2. Usage
Add the less module to your application.conf:
bc. module.less=${play.path}/modules/less
Now any file in your **public/** directory that ends in **.less** is automatically processed by the plugin, and outputted as CSS.
h2. Example
Create a file called **public/stylesheets/style.less**, with contents:
bc. @color: red;
h1 {
color: @color;
}
Now, add the stylesheet to your main template in **app/views/main.html**:
bc.
h2. Caching
The less module sets and listens to Last-Modified and ETag headers, so most browsers will retrieve the file only once. At the server, a compiled CSS file is stored in the cache, and automatically invalidated when the less file changes, or when one of the imported files change. So you can change your less files even while Play is running in production mode.
h2. Notes
In case of an error in your less file, the error will be shown on your page to help you debug.
PK N>-tha src/play.plugins100:play.modules.less.Plugin
PK N"?δ| ! src/play/modules/less/Plugin.javapackage play.modules.less;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ETAG;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.LAST_MODIFIED;
import java.io.PrintStream;
import java.util.Date;
import play.Play;
import play.PlayPlugin;
import play.mvc.Http;
import play.mvc.Http.Request;
import play.mvc.Http.Response;
import play.utils.Utils;
import play.vfs.VirtualFile;
public class Plugin extends PlayPlugin {
PlayLessEngine playLessEngine;
boolean useEtag = true;
@Override
public void onLoad() {
playLessEngine = new PlayLessEngine(Play.mode == Play.Mode.DEV);
useEtag = Play.configuration.getProperty("http.useETag", "true").equals("true");
}
@Override
public boolean serveStatic(VirtualFile file, Request request, Response response) {
if(file.getName().endsWith(".less")) {
response.contentType = "text/css";
try {
handleResponse(file, request, response);
} catch(Exception e) {
response.status = 500;
response.print("Bugger, the LESS processing failed:,\n");
e.printStackTrace(new PrintStream(response.out));
}
return true;
}
return super.serveStatic(file, request, response);
}
private void handleResponse(VirtualFile file, Request request, Response response) {
long lastModified = playLessEngine.lastModifiedRecursive(file.getRealFile());
final String etag = "\"" + lastModified + "-" + file.hashCode() + "\"";
if(!request.isModified(etag, lastModified)) {
handleNotModified(request, response, etag);
} else {
handleOk(request, response, file, etag, lastModified);
}
}
private void handleNotModified(Request request, Response response, String etag) {
if (request.method.equals("GET")) {
response.status = Http.StatusCode.NOT_MODIFIED;
}
if (useEtag) {
response.setHeader(ETAG, etag);
}
}
private void handleOk(Request request, Response response, VirtualFile file, String etag, long lastModified) {
response.status = 200;
response.print(playLessEngine.get(file.getRealFile()));
response.setHeader(LAST_MODIFIED, Utils.getHttpDateFormatter().format(new Date(lastModified)));
if (useEtag) {
response.setHeader(ETAG, etag);
}
}
}PK N"?ƅ ) src/play/modules/less/PlayLessEngine.javapackage play.modules.less;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import compat16.org.mozilla.javascript.WrappedException;
import com.asual.lesscss.LessEngine;
import com.asual.lesscss.LessException;
import play.Logger;
import play.cache.Cache;
/**
* LessEngine wrapper for Play
*/
public class PlayLessEngine {
LessEngine lessEngine;
Boolean devMode;
Pattern importPattern = Pattern.compile(".*@import\\s*\"(.*?)\".*");
PlayLessEngine(Boolean devMode) {
lessEngine = new LessEngine();
this.devMode = devMode;
}
/**
* Get the CSS for this less file either from the cache, or compile it.
*/
public String get(File lessFile) {
String cacheKey = "less_" + lessFile.getPath() + lastModifiedRecursive(lessFile);
String css = Cache.get(cacheKey, String.class);
if(css == null) {
css = compile(lessFile);
Cache.set(cacheKey, css);
}
return css;
}
// TODO: Maybe prevent infinite looping here, in case of an import loop?
public long lastModifiedRecursive(File lessFile) {
long lastModified = lessFile.lastModified();
for(File imported : getImportsFromCacheOrFile(lessFile)) {
lastModified = Math.max(lastModified, imported.lastModified());
}
return lastModified;
}
protected Set getImportsFromCacheOrFile(File lessFile) {
String cacheKey = "less_imports_" + lessFile.getPath() + lessFile.lastModified();
@SuppressWarnings("unchecked")
Set files = Cache.get(cacheKey, Set.class);
if(files == null) {
try {
files = getImportsFromFile(lessFile);
Cache.set(cacheKey, files);
} catch(IOException e) {
Logger.error(e, "IOException trying to determine imports in LESS file");
files = new HashSet();
}
}
return files;
}
protected Set getImportsFromFile(File lessFile) throws IOException {
BufferedReader r = new BufferedReader(new FileReader(lessFile));
Set files = new HashSet();
String line;
while ((line = r.readLine()) != null) {
Matcher m = importPattern.matcher(line);
while (m.find()) {
File file = new File(lessFile.getParentFile(), m.group(1));
files.add(file);
files.addAll(getImportsFromCacheOrFile(file));
}
}
return files;
}
protected String compile(File lessFile) {
try {
return lessEngine.compile(lessFile);
} catch (LessException e) {
return handleException(lessFile, e);
}
}
public String handleException(File lessFile, LessException e) {
Logger.warn(e, "Less exception");
String filename = e.getFilename();
List extractList = e.getExtract();
String extract = null;
if(extractList != null) {
extract = extractList.toString();
}
// LessEngine reports the file as null when it's not an @imported file
if(filename == null) {
filename = lessFile.getName();
}
// Try to detect missing imports (flaky)
if(extract == null && e.getCause() instanceof WrappedException) {
WrappedException we = (WrappedException) e.getCause();
if(we.getCause() instanceof FileNotFoundException) {
FileNotFoundException fnfe = (FileNotFoundException) we.getCause();
extract = fnfe.getMessage();
}
}
return formatMessage(filename, e.getLine(), e.getColumn(), extract, e.getErrorType());
}
public String formatMessage(String filename, int line, int column, String extract, String errorType) {
return "body:before {display: block; color: #c00; white-space: pre; font-family: monospace; background: #FDD9E1; border-top: 1px solid pink; border-bottom: 1px solid pink; padding: 10px; content: \"[LESS ERROR] " +
String.format("%s:%s: %s (%s)", filename, line, extract, errorType) +
"\"; }";
}
}
PK N>-tha bin/play.plugins100:play.modules.less.Plugin
PK N"?R:O O bin/ApplicationTest.class 2 0 ApplicationTest play/test/FunctionalTest ()V Code
LineNumberTable LocalVariableTable this LApplicationTest; testThatIndexPageWorks RuntimeVisibleAnnotations Lorg/junit/Test; /
GET ,(Ljava/lang/Object;)Lplay/mvc/Http$Response;
assertIsOk (Lplay/mvc/Http$Response;)V text/html
assertContentType -(Ljava/lang/String;Lplay/mvc/Http$Response;)V " utf-8
$ %
assertCharset response Lplay/mvc/Http$Response;
SourceFile ApplicationTest.java InnerClasses , play/mvc/Http$Response .
play/mvc/Http Response ! / *
[ L+ + !+ #
&