public static String compress()

in tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CssCompressor.java [176:616]


    public static String compress(String uncompressedCss) throws IOException {

        Pattern p;
        Matcher m;
        String css = uncompressedCss;

        int startIndex = 0;
        int endIndex = 0;
        int i = 0;
        int max = 0;
        List<String> preservedTokens = new ArrayList<>();
        List<String> preservedCssVars = new ArrayList<>();
        List<String> comments = new ArrayList<>();
        String token;
        int totallen = css.length();
        String placeholder;

        StringBuilder sb = new StringBuilder(css);

        // collect all comment blocks...
        while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) {
            endIndex = sb.indexOf("*/", startIndex + 2);
            if (endIndex < 0) {
                endIndex = totallen;
            }

            token = sb.substring(startIndex + 2, endIndex);
            comments.add(token);
            sb.replace(startIndex + 2, endIndex,
                    "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___");
            startIndex += 2;
        }
        css = sb.toString();

        // we need to preserve tokens in two steps, as the "var" is inside of "calc", but
        // thanks to TAP5-2753, the calc tokens needs to be restored, some optimizations
        // around operators, and then css vars would be broken.
        css = preserveToken(css, "var", PRESERVE_CSS_VARS, false, "CSS_VAR", preservedCssVars);
        css = preserveToken(css, "url", PRESERVE_TOKEN_URL, true, "TOKEN", preservedTokens);
        css = preserveToken(css, "calc", PRESERVE_TOKEN_CALC, false, "TOKEN", preservedTokens);
        css = preserveToken(css, "progid:DXImageTransform.Microsoft.Matrix",
                PRESERVE_TOKEN_PROGID_DX_IMAGE_TRANSFORM_MICROSOFT_MATRIX, false, "TOKEN", preservedTokens);

        // preserve strings so their content doesn't get accidentally minified
        StringBuffer sbuffer = new StringBuffer();
        p = PRESERVE_STRINGS;
        m = p.matcher(css);
        while (m.find()) {
            token = m.group();
            char quote = token.charAt(0);
            token = token.substring(1, token.length() - 1);

            // maybe the string contains a comment-like substring?
            // one, maybe more? put'em back then
            if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) {
                for (i = 0, max = comments.size(); i < max; i += 1) {
                    token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___",
                            comments.get(i).toString());
                }
            }

            // minify alpha opacity in filter strings
            token = MINIFY_ALPHA_OPACITY_FILTER_STRINGS.matcher(token).replaceAll("alpha(opacity=");

            preservedTokens.add(token);
            String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote;
            m.appendReplacement(sbuffer, preserver);
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // strings are safe, now wrestle the comments
        for (i = 0, max = comments.size(); i < max; i += 1) {

            token = comments.get(i).toString();
            placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___";

            // ! in the first position of the comment means preserve
            // so push to the preserved tokens while stripping the !
            if (token.startsWith("!")) {
                preservedTokens.add(token);
                css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
                continue;
            }

            // \ in the last position looks like hack for Mac/IE5
            // shorten that to /*\*/ and the next one to /**/
            if (token.endsWith("\\")) {
                preservedTokens.add("\\");
                css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
                i = i + 1; // attn: advancing the loop
                preservedTokens.add("");
                css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___",
                        "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
                continue;
            }

            // keep empty comments after child selectors (IE7 hack)
            // e.g. html >/**/ body
            if (token.length() == 0) {
                startIndex = css.indexOf(placeholder);
                if (startIndex > 2) {
                    if (css.charAt(startIndex - 3) == '>') {
                        preservedTokens.add("");
                        css = css.replace(placeholder,
                                "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
                    }
                }
            }

            // in all other cases kill the comment
            css = css.replace("/*" + placeholder + "*/", "");
        }

        // preserve \9 IE hack
        final String backslash9 = "\\9";
        while (css.indexOf(backslash9) > -1) {
            preservedTokens.add(backslash9);
            css = css.replace(backslash9, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
        }

        // Normalize all whitespace strings to single spaces. Easier to work with that
        // way.
        css = WHITESPACE.matcher(css).replaceAll(" ");

        // Remove the spaces before the things that should not have spaces before them.
        // But, be careful not to turn "p :link {...}" into "p:link{...}"
        // Swap out any pseudo-class colons with the token, and then swap back.
        sbuffer = new StringBuffer();
        p = UNNECESSARY_SPACES1;
        m = p.matcher(css);
        while (m.find()) {
            String s = m.group();
            s = s.replace(":", PSEUDO_CLASS_COLON.pattern());
            s = s.replace("\\", "\\\\");
            s = s.replace("$", "\\$");
            m.appendReplacement(sbuffer, s);
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // Remove spaces before the things that should not have spaces before them.
        css = UNNECESSARY_SPACES2.matcher(css).replaceAll("$1");
        // Restore spaces for !important
        css = IMPORTANT.matcher(css).replaceAll(" !important");
        // bring back the colon
        css = PSEUDO_CLASS_COLON.matcher(css).replaceAll(":");

        // retain space for special IE6 cases
        sbuffer = new StringBuffer();
        p = IE6_SPACE;
        m = p.matcher(css);
        while (m.find()) {
            m.appendReplacement(sbuffer, ":first-" + m.group(1).toLowerCase() + " " + m.group(2));
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

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

        // TODO: Charset handling is broken if more than two charsets

        // If there are multiple @charset directives, push them to the top of the file.
        sbuffer = new StringBuffer();
        p = CHARSET_DIRECTIVE;
        m = p.matcher(css);
        while (m.find()) {
            String s = m.group(1).replace("\\", "\\\\").replace("$", "\\$");
            m.appendReplacement(sbuffer, m.group(2).toLowerCase() + m.group(3) + s);
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // When all @charset are at the top, remove the second and after (as they are
        // completely ignored).
        sbuffer = new StringBuffer();
        p = CHARSET_MULTIPLE;
        m = p.matcher(css);
        while (m.find()) {
            String group2 = m.group(2);
            String group3 = m.group(3);
            String group4 = m.group(4);
            m.appendReplacement(sbuffer, group2 + group3.toLowerCase() + group4);
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // lowercase some popular @directives (@charset is done right above)
        sbuffer = new StringBuffer();
        p = LOWERCASE_DIRECTIVES;
        m = p.matcher(css);
        while (m.find()) {
            m.appendReplacement(sbuffer, '@' + m.group(1).toLowerCase());
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // lowercase some more common pseudo-elements
        sbuffer = new StringBuffer();
        p = LOWERCAUSE_PSEUDO_CLASSES;
        m = p.matcher(css);
        while (m.find()) {
            m.appendReplacement(sbuffer, ':' + m.group(1).toLowerCase());
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // lowercase some more common functions
        sbuffer = new StringBuffer();
        p = LOWERCASE_FUNCTIONS1;
        m = p.matcher(css);
        while (m.find()) {
            m.appendReplacement(sbuffer, ':' + m.group(1).toLowerCase() + '(');
        }
        m.appendTail(sbuffer);
        css = sbuffer.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
        sbuffer = new StringBuffer();
        p = LOWERCASE_FUNCTIONS2;
        m = p.matcher(css);
        while (m.find()) {
            m.appendReplacement(sbuffer, m.group(1) + m.group(2).toLowerCase());
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

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

        // Remove the spaces after the things that should not have spaces after them.
        css = TRAILING_SPACES.matcher(css).replaceAll("$1");

        // remove unnecessary semicolons
        css = UNNECESSARY_SEMICOLON.matcher(css).replaceAll("}");

        // Replace 0(px,em) with 0. (don't replace seconds are they are needed for
        // transitions to be valid)
        String oldCss;
        p = ZERO_UNITS;
        do {
            oldCss = css;
            m = p.matcher(css);
            css = m.replaceAll("$1$20");
        } while (!(css.equals(oldCss)));

        // We do the same with % but don't replace the 0% in keyframes
        p = ZERO_PERCENTAGE;
        do {
            oldCss = css;
            m = p.matcher(css);
            css = m.replaceAll("$1$20");
        } while (!(css.equals(oldCss)));

        // Replace the keyframe 100% step with 'to' which is shorter
        p = KEYFRAME_TO;
        do {
            oldCss = css;
            m = p.matcher(css);
            css = m.replaceAll("$1to{");
        } while (!(css.equals(oldCss)));

        // Replace 0(px,em,%) with 0 inside groups (e.g. -MOZ-RADIAL-GRADIENT(CENTER
        // 45DEG, CIRCLE CLOSEST-SIDE, ORANGE 0%, RED 100%))
        p = ZERO_UNITS_GROUPS;
        do {
            oldCss = css;
            m = p.matcher(css);
            css = m.replaceAll("($10");
        } while (!(css.equals(oldCss)));

        // Replace x.0(px,em,%) with x(px,em,%).
        css = UNNECESSARY_DOT_ZERO1.matcher(css).replaceAll("$1$2");

        // Replace .0(px,em,%) with 0(px,em,%).
        css = UNNECESSARY_DOT_ZERO2.matcher(css).replaceAll("$1\\0$2");

        // Replace 0 0 0 0; with 0.
        css = ZERO_VALUE_1.matcher(css).replaceAll(":0$1");
        css = ZERO_VALUE_2.matcher(css).replaceAll(":0$1");
        css = ZERO_VALUE_3.matcher(css).replaceAll(":0$1");

        // Replace background-position:0; with background-position:0 0;
        // same for transform-origin
        sbuffer = new StringBuffer();
        p = BACKGROUND_POSITION_TRANSFORM_ORIGIN;
        m = p.matcher(css);
        while (m.find()) {
            m.appendReplacement(sbuffer, m.group(1).toLowerCase() + ":0 0" + m.group(2));
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // Replace 0.6 to .6, but only when preceded by : or a white-space
        css = RESTORE_DOT_ZERO.matcher(css).replaceAll("$1.$2");

        // Shorten colors from rgb(51,102,153) to #336699
        // This makes it more likely that it'll get further compressed in the next step.
        p = RGB;
        m = p.matcher(css);
        sbuffer = new StringBuffer();
        while (m.find()) {
            String[] rgbcolors = m.group(1).split(",");
            StringBuffer hexcolor = new StringBuffer("#");
            for (i = 0; i < rgbcolors.length; i++) {
                int val = Integer.parseInt(rgbcolors[i]);
                if (val < 16) {
                    hexcolor.append("0");
                }

                // If someone passes an RGB value that's too big to express in two characters,
                // round down.
                // Probably should throw out a warning here, but generating valid CSS is a
                // bigger concern.
                if (val > 255) {
                    val = 255;
                }
                hexcolor.append(Integer.toHexString(val));
            }
            m.appendReplacement(sbuffer, hexcolor.toString());
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // Shorten colors from #AABBCC to #ABC. Note that we want to make sure
        // the color is not preceded by either ", " or =. Indeed, the property
        // filter: chroma(color="#FFFFFF");
        // would become
        // filter: chroma(color="#FFF");
        // which makes the filter break in IE.
        // We also want to make sure we're only compressing #AABBCC patterns inside { },
        // not id selectors ( #FAABAC {} )
        // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD)
        p = HEX_COLORS;

        m = p.matcher(css);
        sb = new StringBuilder();
        int index = 0;

        while (m.find(index)) {

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

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

            if (isFilter) {
                // Restore, as is. Compression will break filters
                sb.append(m.group(1).concat("#").concat(m.group(2)).concat(m.group(3)).concat(m.group(4))
                        .concat(m.group(5)).concat(m.group(6)).concat(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("#".concat(m.group(3)).concat(m.group(5)).concat(m.group(7)).toLowerCase());

                } else {

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

            index = m.end(7);
        }

        sb.append(css.substring(index));
        css = sb.toString();

        // Replace #f00 -> red
        css = COLOR_RED.matcher(css).replaceAll("$1red$3");
        // Replace other short color keywords
        css = COLOR_NAVY.matcher(css).replaceAll("$1navy$3");
        css = COLOR_GRAY.matcher(css).replaceAll("$1gray$3");
        css = COLOR_OLIVE.matcher(css).replaceAll("$1olive$3");
        css = COLOR_PURPLE.matcher(css).replaceAll("$1purple$3");
        css = COLOR_SILVER.matcher(css).replaceAll("$1silver$3");
        css = COLOR_TEAL.matcher(css).replaceAll("$1teal$3");
        css = COLOR_ORANGE.matcher(css).replaceAll("$1orange$3");
        css = COLOR_MAROON.matcher(css).replaceAll("$1maroon$3");

        // border: none -> border:0
        sbuffer = new StringBuffer();
        p = NONE;
        m = p.matcher(css);
        while (m.find()) {
            m.appendReplacement(sbuffer, m.group(1).toLowerCase().concat(":0").concat(m.group(2)));
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // TODO: Why are we doing this again?
        // shorter opacity IE filter
        css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");

        // Find a fraction that is used for Opera's -o-device-pixel-ratio query
        // Add token to add the "\" back in later
        css = OPERA_DEVICE_PIXEL_RATIO.matcher(css).replaceAll("($1:$2___YUI_QUERY_FRACTION___$3)");

        // Remove empty rules.
        css = EMPTY_RULE.matcher(css).replaceAll("");

        // Add "\" back to fix Opera -o-device-pixel-ratio query
        css = css.replaceAll("___YUI_QUERY_FRACTION___", "/");

        // Replace multiple semi-colons in a row by a single one
        // See SF bug #1980989
        css = MULTI_SEMICOLON.matcher(css).replaceAll(";");

        // restore preserved tokens
        css = restorePreservedTokens(css, "TOKEN", preservedTokens);

        // Add spaces back in between operators for css calc function
        // https://developer.mozilla.org/en-US/docs/Web/CSS/calc
        // Added by Eric Arnol-Martin (earnolmartin@gmail.com)
        sbuffer = new StringBuffer();
        p = CALC;
        m = p.matcher(css);
        while (m.find()) {
            String s = m.group();
            s = CALC_PLUS.matcher(s).replaceAll(" + ");
            s = CALC_MINUS.matcher(s).replaceAll(" - ");
            s = CALC_MULTI.matcher(s).replaceAll(" * ");
            s = CALC_DIV.matcher(s).replaceAll(" / ");

            m.appendReplacement(sbuffer, s);
        }
        m.appendTail(sbuffer);
        css = sbuffer.toString();

        // restore css variables AFTER calc func optimization
        css = restorePreservedTokens(css, "CSS_VAR", preservedCssVars);

        // Trim the final string (for any leading or trailing white spaces)
        return css.trim();
    }