Skip to content

Commit

Permalink
Merge pull request #76 from tml/danbeam-master
Browse files Browse the repository at this point in the history
Fix danbeam's PR
  • Loading branch information
tml committed May 9, 2013
2 parents 65c8e40 + 6ba34ea commit 4018929
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 85 deletions.
44 changes: 35 additions & 9 deletions ports/js/cssmin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* cssmin.js
* Author: Stoyan Stefanov - http://phpied.com/
* Contributor: Dan Beam - http://danbeam.org/
* This is a JavaScript port of the CSS minification tool
* distributed with YUICompressor, itself a port
* of the cssmin utility by Isaac Schlueter - http://foohack.com/
Expand All @@ -12,7 +13,7 @@
* YUI Compressor
* http://developer.yahoo.com/yui/compressor/
* Author: Julien Lecomte - http://www.julienlecomte.net/
* Copyright (c) 2011 Yahoo! Inc. All rights reserved.
* Copyright (c) 2013 Yahoo! Inc. All rights reserved.
* The copyrights embodied in the content of this file are licensed
* by Yahoo! Inc. under the BSD (revised) open source license.
*/
Expand Down Expand Up @@ -43,7 +44,7 @@ YAHOO.compressor._extractDataUrls = function (css, preservedTokens) {
m,
preserver,
token,
pattern = /url\(\s*(["']?)data\:/g;
pattern = /url\(\s*(["']?)data\:/gi;

// Since we need to account for non-base64 data urls, we need to handle
// ' and ) being part of the data string. Hence switching to indexOf,
Expand Down Expand Up @@ -255,21 +256,46 @@ YAHOO.compressor.cssmin = function (css, linebreakpos) {
css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1');
css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":");

// retain space for special IE6 cases
css = css.replace(/:first-(line|letter)(\{|,)/g, ":first-$1 $2");
// retain space for special IE6 cases and lowercase
css = css.replace(/:first-(line|letter)(\{|,)/gi, function(all, $1, $2) {
return ":first-" + $1.toLowerCase() + " " + $2;
});

// no space after the end of a preserved comment
css = css.replace(/\*\/ /g, '*/');


// If there is a @charset, then only allow one, and push to the top of the file.
css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1');
css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1');
// If there is a @charset, then only allow one, and push to the top of the file (and make lowercase).
css = css.replace(/^(.*)(@charset)( "[^"]*";)/gi, function(all, $1, $2, $3) {
return $2.toLowerCase() + $3 + $1;
});
css = css.replace(/^((\s*)(@charset)( [^;]+;\s*))+/gi, function(all, $1, $2, $3, $4) {
return $2 + $3.toLowerCase() + $4;
});

// Put the space back in some cases, to support stuff like
// @media screen and (-webkit-min-device-pixel-ratio:0){
css = css.replace(/\band\(/gi, "and (");

// lowercase some popular @directives (@charset is done right above)
css = css.replace(/@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/gi, function(all, $1) {
return '@' + $1.toLowerCase();
});

// lowercase some more common pseudo-elements
css = css.replace(/:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/gi, function(all, $1) {
return ':' + $1.toLowerCase();
});

// lowercase some more common functions
css = css.replace(/:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\(/gi, function(all, $1) {
return ':' + $1.toLowerCase() + '(';
});

// lower case some common function that can be values
// NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us
css = css.replace(/([:,\( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/gi, function(all, $1, $2) {
return $1 + $2.toLowerCase();
});

// Remove the spaces after the things that should not have spaces after them.
css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1');
Expand All @@ -278,7 +304,7 @@ YAHOO.compressor.cssmin = function (css, linebreakpos) {
css = css.replace(/;+\}/g, "}");

// Replace 0(px,em,%) with 0.
css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2");
css = css.replace(/(^|[^0-9])(?:0?\.)?0(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)/gi, "$10");

// Replace 0 0 0 0; with 0.
css = css.replace(/:0 0 0 0(;|\})/g, ":0$1");
Expand Down
195 changes: 130 additions & 65 deletions src/com/yahoo/platform/yui/compressor/CssCompressor.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* Author: Julien Lecomte - http://www.julienlecomte.net/
* Author: Isaac Schlueter - http://foohack.com/
* Author: Stoyan Stefanov - http://phpied.com/
* Copyright (c) 2011 Yahoo! Inc. All rights reserved.
* Contributor: Dan Beam - http://danbeam.org/
* Copyright (c) 2013 Yahoo! Inc. All rights reserved.
* The copyrights embodied in the content of this file are licensed
* by Yahoo! Inc. under the BSD (revised) open source license.
*/
Expand Down Expand Up @@ -32,61 +33,61 @@ public CssCompressor(Reader in) throws IOException {
// Leave data urls alone to increase parse performance.
protected String extractDataUrls(String css, ArrayList preservedTokens) {

int maxIndex = css.length() - 1;
int maxIndex = css.length() - 1;
int appendIndex = 0;

StringBuffer sb = new StringBuffer();
StringBuffer sb = new StringBuffer();

Pattern p = Pattern.compile("url\\(\\s*([\"']?)data\\:");
Pattern p = Pattern.compile("(?i)url\\(\\s*([\"']?)data\\:");
Matcher m = p.matcher(css);
/*
* Since we need to account for non-base64 data urls, we need to handle

/*
* Since we need to account for non-base64 data urls, we need to handle
* ' and ) being part of the data string. Hence switching to indexOf,
* to determine whether or not we have matching string terminators and
* handling sb appends directly, instead of using matcher.append* methods.
*/

while (m.find()) {

int startIndex = m.start() + 4; // "url(".length()
String terminator = m.group(1); // ', " or empty (not quoted)
if (terminator.length() == 0) {
terminator = ")";
}

boolean foundTerminator = false;

int endIndex = m.end() - 1;
while(foundTerminator == false && endIndex+1 <= maxIndex) {
endIndex = css.indexOf(terminator, endIndex+1);

if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) {
foundTerminator = true;
if (!")".equals(terminator)) {
endIndex = css.indexOf(")", endIndex);
}
}
}

// Enough searching, start moving stuff over to the buffer
sb.append(css.substring(appendIndex, m.start()));

if (foundTerminator) {
String token = css.substring(startIndex, endIndex);
token = token.replaceAll("\\s+", "");
preservedTokens.add(token);

String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)";
sb.append(preserver);

appendIndex = endIndex + 1;
} else {
// No end terminator found, re-add the whole match. Should we throw/warn here?
sb.append(css.substring(m.start(), m.end()));
appendIndex = m.end();
}
int startIndex = m.start() + 4; // "url(".length()
String terminator = m.group(1); // ', " or empty (not quoted)

if (terminator.length() == 0) {
terminator = ")";
}

boolean foundTerminator = false;

int endIndex = m.end() - 1;
while(foundTerminator == false && endIndex+1 <= maxIndex) {
endIndex = css.indexOf(terminator, endIndex+1);

if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) {
foundTerminator = true;
if (!")".equals(terminator)) {
endIndex = css.indexOf(")", endIndex);
}
}
}

// Enough searching, start moving stuff over to the buffer
sb.append(css.substring(appendIndex, m.start()));

if (foundTerminator) {
String token = css.substring(startIndex, endIndex);
token = token.replaceAll("\\s+", "");
preservedTokens.add(token);

String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)";
sb.append(preserver);

appendIndex = endIndex + 1;
} else {
// No end terminator found, re-add the whole match. Should we throw/warn here?
sb.append(css.substring(m.start(), m.end()));
appendIndex = m.end();
}
}

sb.append(css.substring(appendIndex));
Expand Down Expand Up @@ -224,18 +225,82 @@ public void compress(Writer out, int linebreakpos)
css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":");

// retain space for special IE6 cases
css = css.replaceAll(":first\\-(line|letter)(\\{|,)", ":first-$1 $2");
sb = new StringBuffer();
p = Pattern.compile("(?i):first\\-(line|letter)(\\{|,)");
m = p.matcher(css);
while (m.find()) {
m.appendReplacement(sb, ":first-" + m.group(1).toLowerCase() + " " + m.group(2));
}
m.appendTail(sb);
css = sb.toString();

// no space after the end of a preserved comment
css = css.replaceAll("\\*/ ", "*/");

// If there is a @charset, then only allow one, and push to the top of the file.
css = css.replaceAll("^(.*)(@charset \"[^\"]*\";)", "$2$1");
css = css.replaceAll("^(\\s*@charset [^;]+;\\s*)+", "$1");
// If there are multiple @charset directives, push them to the top of the file.
sb = new StringBuffer();
p = Pattern.compile("(?i)^(.*)(@charset)( \"[^\"]*\";)");
m = p.matcher(css);
while (m.find()) {
m.appendReplacement(sb, m.group(2).toLowerCase() + m.group(3) + m.group(1));
}
m.appendTail(sb);
css = sb.toString();

// When all @charset are at the top, remove the second and after (as they are completely ignored).
sb = new StringBuffer();
p = Pattern.compile("(?i)^((\\s*)(@charset)( [^;]+;\\s*))+");
m = p.matcher(css);
while (m.find()) {
m.appendReplacement(sb, m.group(2) + m.group(3).toLowerCase() + m.group(4));
}
m.appendTail(sb);
css = sb.toString();

// lowercase some popular @directives (@charset is done right above)
sb = new StringBuffer();
p = Pattern.compile("(?i)@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)");
m = p.matcher(css);
while (m.find()) {
m.appendReplacement(sb, '@' + m.group(1).toLowerCase());
}
m.appendTail(sb);
css = sb.toString();

// lowercase some more common pseudo-elements
sb = new StringBuffer();
p = Pattern.compile("(?i):(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)");
m = p.matcher(css);
while (m.find()) {
m.appendReplacement(sb, ':' + m.group(1).toLowerCase());
}
m.appendTail(sb);
css = sb.toString();

// lowercase some more common functions
sb = new StringBuffer();
p = Pattern.compile("(?i):(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\\(");
m = p.matcher(css);
while (m.find()) {
m.appendReplacement(sb, ':' + m.group(1).toLowerCase() + '(');
}
m.appendTail(sb);
css = sb.toString();

// lower case some common function that can be values
// NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
sb = new StringBuffer();
p = Pattern.compile("(?i)([:,\\( ]\\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)");
m = p.matcher(css);
while (m.find()) {
m.appendReplacement(sb, m.group(1) + m.group(2).toLowerCase());
}
m.appendTail(sb);
css = sb.toString();

// Put the space back in some cases, to support stuff like
// @media screen and (-webkit-min-device-pixel-ratio:0){
css = css.replaceAll("\\band\\(", "and (");
css = css.replaceAll("(?i)\\band\\(", "and (");

// Remove the spaces after the things that should not have spaces after them.
css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1");
Expand All @@ -244,7 +309,7 @@ public void compress(Writer out, int linebreakpos)
css = css.replaceAll(";+}", "}");

// Replace 0(px,em,%) with 0.
css = css.replaceAll("([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2");
css = css.replaceAll("(?i)(^|[^0-9])(?:0?\\.)?0(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)", "$10");

// Replace 0 0 0 0; with 0.
css = css.replaceAll(":0 0 0 0(;|})", ":0$1");
Expand Down Expand Up @@ -302,29 +367,29 @@ public void compress(Writer out, int linebreakpos)

while (m.find(index)) {

sb.append(css.substring(index, m.start()));
sb.append(css.substring(index, m.start()));

boolean isFilter = (m.group(1) != null && !"".equals(m.group(1)));
boolean isFilter = (m.group(1) != null && !"".equals(m.group(1)));

if (isFilter) {
// Restore, as is. Compression will break filters
sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7));
} else {
if( m.group(2).equalsIgnoreCase(m.group(3)) &&
if (isFilter) {
// Restore, as is. Compression will break filters
sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7));
} else {
if( m.group(2).equalsIgnoreCase(m.group(3)) &&
m.group(4).equalsIgnoreCase(m.group(5)) &&
m.group(6).equalsIgnoreCase(m.group(7))) {

// #AABBCC pattern
sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase());
// #AABBCC pattern
sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase());

} else {
} else {

// Non-compressible color, restore, but lower case.
sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase());
}
// Non-compressible color, restore, but lower case.
sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase());
}
}

index = m.end(7);
index = m.end(7);
}

sb.append(css.substring(index));
Expand Down
1 change: 1 addition & 0 deletions tests/color.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
filter: chroma(color="#FFFFFF");
background: none repeat scroll 0 0 rgb(255, 0,0);
alpha: rgba(1, 2, 3, 4);
border-color: RGBA(1,2,3,4); /* tests uppercase RGBA() */
color:#1122aa
}

Expand Down
2 changes: 1 addition & 1 deletion tests/color.css.min
Original file line number Diff line number Diff line change
@@ -1 +1 @@
.color{me:#7b7b7b;impressed:#fed;again:#abcdef;andagain:#a6c;background-color:#aa66ccc;filter:chroma(color="#FFFFFF");background:none repeat scroll 0 0 red;alpha:rgba(1,2,3,4);color:#12a}#AABBCC{background-color:#fe1;filter:chroma(color = #FFFFFF);color:#412;foo:#0f1 #ABC #abc #123344;border-color:#aa66ccC}.foo #AABBCC{background-color:#fe1;color:#412;border-color:#AbC;filter:chroma(color= #FFFFFF)}.bar,#AABBCC{background-color:#fe1;border-color:#0f1 #abcdef;filter:chroma(color=#11FFFFFF);color:#412}.foo,#AABBCC.foobar{background-color:#fe1;border-color:#0f1 #abcdef #abc;color:#412}@media screen{.bar,#AABBCC{background-color:#fe1;color:#412}}
.color{me:#7b7b7b;impressed:#fed;again:#abcdef;andagain:#a6c;background-color:#aa66ccc;filter:chroma(color="#FFFFFF");background:none repeat scroll 0 0 red;alpha:rgba(1,2,3,4);border-color:rgba(1,2,3,4);color:#12a}#AABBCC{background-color:#fe1;filter:chroma(color = #FFFFFF);color:#412;foo:#0f1 #ABC #abc #123344;border-color:#aa66ccC}.foo #AABBCC{background-color:#fe1;color:#412;border-color:#AbC;filter:chroma(color= #FFFFFF)}.bar,#AABBCC{background-color:#fe1;border-color:#0f1 #abcdef;filter:chroma(color=#11FFFFFF);color:#412}.foo,#AABBCC.foobar{background-color:#fe1;border-color:#0f1 #abcdef #abc;color:#412}@media screen{.bar,#AABBCC{background-color:#fe1;color:#412}}
4 changes: 2 additions & 2 deletions tests/concat-charset.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* This is invalid CSS, but frequently happens as a result of concatenation. */
@charset "utf-8";
@CHARSET "utf-8";
#foo {
border-width:1px;
}
Expand All @@ -12,4 +12,4 @@ The compressor should not get involved.
@charset "another one";
#bar {
border-width:10px;
}
}
2 changes: 1 addition & 1 deletion tests/dataurl-singlequote-font.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Bar

@font-face {
font-family: "Foo Bar";
src: url('data:font/truetype;base64,gRbIUFAIrsQNGditEWbAUKwAA') format("truetype"),
src: URL('data:font/truetype;base64,gRbIUFAIrsQNGditEWbAUKwAA') format("truetype"),
url("http://yuilibrary.com/fonts/foo-bar.svg#webfontse22fewwr") format("svg");
font-weight: normal;
font-style: normal;
Expand Down
Loading

0 comments on commit 4018929

Please sign in to comment.