Skip to content

Commit

Permalink
Adding CssTransformer - a metaphor to transform input to output css w…
Browse files Browse the repository at this point in the history
…ith the help of a Transform

a) CssTransformer parses the input css and generates CssToken's. BackgroundImage is one such token
b) These tokens are then sent to a Transform implementation.
c) Adding several unit tests to verify css parsing

Yet to add concrete implementations of Transform
  • Loading branch information
sripathikrishnan committed Aug 22, 2011
1 parent 0aa75e0 commit 58fa43d
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 0 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@
<version>1.8.2</version> <version>1.8.2</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.0.1</version>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/net/nczonline/web/cssembed/BackgroundImage.java
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,40 @@
package net.nczonline.web.cssembed;

public class BackgroundImage implements CssToken {
private final String url;

BackgroundImage(String url) {
this.url = url;
}
public String getUrl() {
return url;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((url == null) ? 0 : url.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BackgroundImage other = (BackgroundImage) obj;
if (url == null) {
if (other.url != null)
return false;
} else if (!url.equals(other.url))
return false;
return true;
}

public String toCss() {
return " background-image:url(\"" + url + "\"); ";
}
}
5 changes: 5 additions & 0 deletions src/main/java/net/nczonline/web/cssembed/CssToken.java
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.nczonline.web.cssembed;

public interface CssToken {
public String toCss();
}
83 changes: 83 additions & 0 deletions src/main/java/net/nczonline/web/cssembed/CssTransformer.java
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,83 @@
package net.nczonline.web.cssembed;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;

public class CssTransformer {

private static final String URL_PATTERN_REGEX = "([\\\\/\\.a-zA-Z0-9%\\?\\-_:=&]+)";
private static final String WHITE_SPACE = "\\s*";
private static final String OPTIONAL_QUOTE = "(['\"]{0,1})";
private static final String MATCH_OPENING_QUOTE = "\\1";
private static final String CASE_INSENSITIVE = "(?i)";
private static final String OPTIONAL_SEMI_COLON = "[;]{0,1}";

static final int URL_GROUP = 2;
static final Pattern BACKGROUND_IMAGE = getBackgroundImagePattern();


static final Pattern getBackgroundImagePattern() {
StringBuilder regex = new StringBuilder();
regex
.append(CASE_INSENSITIVE)
.append(rightWhiteSpace("background-image"))
.append(rightWhiteSpace(":"))
.append(rightWhiteSpace("url"))
.append(rightWhiteSpace("\\("))
.append(OPTIONAL_QUOTE)
.append(URL_PATTERN_REGEX)
.append(MATCH_OPENING_QUOTE)
.append(leftWhiteSpace("\\)"))
.append(leftWhiteSpace(OPTIONAL_SEMI_COLON));

return Pattern.compile(regex.toString());
}

private static String leftWhiteSpace(String literal) {
return WHITE_SPACE + literal;
}

private static String rightWhiteSpace(String literal) {
return literal + WHITE_SPACE;
}

/**
* Reads a CSS file from input, transforms it according to transformation, and then writes the transformed css to output
*
* This method parses the input files into 'Tokens'. For each token, the appropriate transform() method
* is called on the transformation. The output of the transformation is written to the output <em>instead of</em> the token
*
*
* @param input the source css file
* @param t the transformation to apply on the CSS
* @param output the output css file
* @throws IOException If the input can't be read, or the output can't be written for some reason
*/
public void transform(Reader input, Transform t, Writer output) throws IOException {
String source = IOUtils.toString(input);

source = t.preTransform(source);

/*
* At present, we only generate background image tokens
*/
Matcher m = BACKGROUND_IMAGE.matcher(source);
StringBuffer sb = new StringBuffer();
while(m.find()) {

String url = m.group(URL_GROUP);
BackgroundImage image = new BackgroundImage(url);
String replacement = t.transform(image);
m.appendReplacement(sb, replacement);
}
m.appendTail(sb);

String finalOutput = t.postTransform(sb.toString());
IOUtils.write(finalOutput, output);
}
}
23 changes: 23 additions & 0 deletions src/main/java/net/nczonline/web/cssembed/DefaultTransform.java
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,23 @@
package net.nczonline.web.cssembed;

public class DefaultTransform implements Transform {

public String preTransform(String source) {
return source;
}

public String transform(CssToken token) {
if(token instanceof BackgroundImage) {
transform((BackgroundImage)token);
}
throw new IllegalArgumentException("Unknown token type - " + token.getClass());
}

protected String transform(BackgroundImage image) {
return image.toCss();
}

public String postTransform(String source) {
return source;
}
}
11 changes: 11 additions & 0 deletions src/main/java/net/nczonline/web/cssembed/Transform.java
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.nczonline.web.cssembed;

public interface Transform {

String preTransform(String source);

String transform(CssToken token);

String postTransform(String source);

}
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,78 @@
package net.nczonline.web.cssembed;

import java.util.regex.Matcher;

import org.junit.Test;
import static junit.framework.Assert.*;

public class BackgroundImageRegexTest {

@Test
public void testRegex() {
RegexTest[] positive_matches = {
new RegexTest("No quotes", "background-image:url(image.png)", "image.png"),
new RegexTest("Single quotes", "background-image:url('image.png')", "image.png"),
new RegexTest("Double quotes", "background-image:url(\"image.png\")", "image.png"),

new RegexTest("With semi-colon at end", "background-image:url(\"image.png\") ;", "image.png"),

new RegexTest("With spaces", "background-image: url( \"image.png\" )", "image.png"),
new RegexTest("With tabs", "background-image\t:\turl(\"image.png\")", "image.png"),
new RegexTest("Multiple lines", "background-image:\nurl(\n\t'image.png')", "image.png"),

new RegexTest("Mixed case", "Background-Image:Url('image.png')", "image.png"),
new RegexTest("Case preserved in url", "Background-Image:Url('ImagE.png')", "ImagE.png"),

new RegexTest("Forward slashes", "background-image:url(/path/to/image.png)", "/path/to/image.png"),
new RegexTest("Back slashes", "background-image:url(\\path\\to\\image.png)", "\\path\\to\\image.png"),

new RegexTest("Absolute URL", "background-image:url(http://some.server.com/path/to/image.png)",
"http://some.server.com/path/to/image.png"),

new RegexTest("URL with port number", "background-image:url(http://some.server.com:8080/path/to/image.png)",
"http://some.server.com:8080/path/to/image.png"),

new RegexTest("URL with query parameters", "background-image:url(http://some.server.com/path/to/image.png?key=value&key2=value2)",
"http://some.server.com/path/to/image.png?key=value&key2=value2"),

new RegexTest("Absolute URL in double quotes", "background-image:url(\"http://some.server.com/path/to/image.png?key=value&key2=value2\")",
"http://some.server.com/path/to/image.png?key=value&key2=value2"),

new RegexTest("Absolute URL in single quotes", "background-image:url('http://some.server.com/path/to/image.png?key=value&key2=value2')",
"http://some.server.com/path/to/image.png?key=value&key2=value2"),

new RegexTest("Absolute URL with % encoding", "background-image:url('http://some.server.com/image.png?key=value%20with%20space')",
"http://some.server.com/image.png?key=value%20with%20space"),

};

String negative_matches[][] = {
{"Mismatched quotes", "background-image:url(\"image.png')"},
{"Path with spaces", "background-image:url(\"my images/image.png')"},
};

for(RegexTest test : positive_matches) {
Matcher matcher = CssTransformer.BACKGROUND_IMAGE.matcher(test.css);
assertTrue(test.description, matcher.matches());
assertEquals(test.description + " - URL does not match", test.expectedUrl, matcher.group(CssTransformer.URL_GROUP));
}

for(int i=0; i<negative_matches.length; i++) {
String message = negative_matches[i][0];
String css = negative_matches[i][1];
assertFalse(message, CssTransformer.BACKGROUND_IMAGE.matcher(css).matches());
}
}

static class RegexTest {
String description;
String css;
String expectedUrl;

RegexTest(String description, String css, String expectedUrl) {
this.description = description;
this.css = css;
this.expectedUrl = expectedUrl;
}
}
}
66 changes: 66 additions & 0 deletions src/test/java/net/nczonline/web/cssembed/CssTransformerTest.java
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,66 @@
package net.nczonline.web.cssembed;

import static junit.framework.Assert.*;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;

import org.junit.Test;

public class CssTransformerTest {

private CssTransformer transformer = new CssTransformer();

@Test
public void testTransformationCallbacksWithoutBackgroundImages() throws IOException {
final String css = ".someclass { font-size:small;}";
final String expectedOutput = ".someotherclass { font-size:large;}";

Transform t = new Transform() {

public String transform(CssToken token) {
throw new IllegalStateException("transform should not be called");
}

public String preTransform(String source) {
return source.replaceAll("someclass", "someotherclass");
}

public String postTransform(String source) {
return source.replaceAll("small", "large");
}
};

String output = transform(css, t);
assertEquals("Pre and Post transformation not called", expectedOutput, output);
}

@Test
public void testMultipleBackgroundImages() throws IOException {
final String css = ".firstclass {background-image:url('first.png')} .secondclass {background-image:url('second.png'); .thirdclass{}}";
final String expectedOutput = ".firstclass {background-image:url('first.png?version=12');} " +
".secondclass {background-image:url('second.png?version=12'); .thirdclass{}}";

Transform t = new DefaultTransform() {
@Override
public String transform(CssToken token) {
if(token instanceof BackgroundImage) {
BackgroundImage image = (BackgroundImage)token;
return "background-image:url('" + image.getUrl() + "?version=12');";
}
throw new IllegalStateException("Unknown token " + token);
}
};

String output = transform(css, t);
assertEquals(expectedOutput, output);
}

private String transform(String css, Transform t) throws IOException {
StringWriter writer = new StringWriter();
transformer.transform(new StringReader(css), t, writer);
return writer.toString();
}

}

0 comments on commit 58fa43d

Please sign in to comment.