Permalink
Browse files

Adding CssTransformer - a metaphor to transform input to output css w…

…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...
1 parent 0aa75e0 commit 58fa43de0c3a8cc3e6fabb59e31c69769e194f46 @sripathikrishnan committed Aug 22, 2011
View
@@ -54,6 +54,11 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.0.1</version>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
@@ -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 + "\"); ";
+ }
+}
@@ -0,0 +1,5 @@
+package net.nczonline.web.cssembed;
+
+public interface CssToken {
+ public String toCss();
+}
@@ -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);
+ }
+}
@@ -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;
+ }
+}
@@ -0,0 +1,11 @@
+package net.nczonline.web.cssembed;
+
+public interface Transform {
+
+ String preTransform(String source);
+
+ String transform(CssToken token);
+
+ String postTransform(String source);
+
+}
@@ -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;
+ }
+ }
+}
@@ -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.