本文是基于 Java 的 CSS 压缩和格式化的算法分析和实现文章。
最近因为我个人开发的软件的很多用户对自定义 CSS 要求的呼声太高,所以,我决定在应用内增加一个自定义 CSS 的功能。因为要允许用户自己对 CSS 进行编辑,所以我需要对 CSS 进行格式化的美化以提升用户体验。此外,在编辑完成之后为了减少存储空间的占用,通常需要对 CSS 进行压缩。所以,我这里写了一个压缩和一个格式化的算法,基于 Java 实现。
1、CSS 压缩
1.1 算法实现分析
对 CSS 进行压缩并不复杂,而且还有比较多的线上工具可以使用。首先,经过对 CSS 语法的分析,可知,要对 CSS 进行压缩,我们可以从下面几种字符着手:
- 空格
- 格式控制符,比如 '\n'、'\r' 和 '\t' 等
- 注释
对于注释和格式控制符,我们只需要在遇到的时候将其移除即可。而对于空格,需要注意并非所有的空格都是可以移除的,比如在 margin: 10px 10px 10px 10px
这样的语法中,空格分隔的四个属性分别对应于上下左右四个方向,如果移除空格可能会导致该属性失效。所以,经过分析,可以被移除空格的情形有如下几种,
- 连续存在的空格可以移除
- 对于用来指定 CSS 格式的符号,比如 '{'、'}' 和 ':' 等附近存在的空格可以移除
1.2 算法的实现和分析
经过上述分析,于是,具体的实现代码如下,
private static String compressCSS(String from) {
int validCharLength = 0;
char[] fromChars = from.toCharArray();
boolean inComment = false;
for (int i=0, len=fromChars.length; i<len; i++) {
char c = fromChars[i];
if (c == ' ' && (i == len-1 || i == 0 || isNormalSpace(fromChars[i+1]) || isNormalSpace(fromChars[i-1]))) {
continue;
}
if (c == '\n' || c == '\t' || c == '\r') {
continue;
}
if (i < len-1 && c == '/' && fromChars[i+1] == '*') { // begin of /*
inComment = true;
continue;
}
if (i > 0 && c == '/' && fromChars[i-1] == '*') { // end of */
inComment = false;
continue;
}
if (inComment) { // in /* and */
continue;
}
validCharLength++;
}
char[] chars = new char[validCharLength];
int index = 0;
inComment = false;
for (int i=0, len=fromChars.length; i<len; i++) {
char c = fromChars[i];
if (c == ' ' && (i == len-1 || i == 0 || isNormalSpace(fromChars[i+1]) || isNormalSpace(fromChars[i-1]))) {
continue;
}
if (c == '\n' || c == '\t' || c == '\r') {
continue;
}
if (i < len-1 && c == '/' && fromChars[i+1] == '*') { // begin of /*
inComment = true;
continue;
}
if (i > 0 && c == '/' && fromChars[i-1] == '*') { // end of */
inComment = false;
continue;
}
if (inComment) { // in /* and */
continue;
}
chars[index++] = c;
}
return new String(chars);
}
private static boolean isNormalSpace(char c) {
return c == '}' || c == ':' || c == ';' || c == ' ' || c == '{';
}
复制代码
下面是对算法的详细说明,
对于数据结构的选择:因为压缩之前和压缩之后的文本长度不同,我们需要在计算的过程中动态调整数组的长度。一种方式是基于现有的集合数据结构,比如 ArrayList 和 LinkedList 等,但是因为 ArrayList 和 LinkedList 仅适用于包装类,而我们知道,包装类占用的内存存储空间比基本数据类型要大得多。所以,从内存占用角度,我们应该使用基本数据类型。对于基本数据类型,我们也可以使用 System.arrayCopy()
方法,在数组容量不足的时候进行动态扩容,但是这种方式实现起来难度要大得多。所以,这里采用了先计算需要的数组长度,得到长度之后直接申请需要的长度的数组,然后再对内容进行填充的方式实现。这样,从时间复杂度上将,两个遍历的复杂度都是 O(n)
,整体的复杂度是 O(n) + O(n) = O(n)
. 也就是,最终的时间复杂度是 O(n)
.
对注释的处理:这里在处理注释的时候的逻辑是,遇到了 /
的时候判断下一个字符是否为 *
或者上一个字符是否为 *
的方式来判断是否为注释的开始和结束。然后使用布尔类型变量记录当前遍历的内容是否为注释的内容,若为注释的内容则直接忽略,不加入到最终的数组中。
对控制符的处理:遇到控制符的时候只需要直接绕过即可。
对空格的处理:如之前的分析所示,在遇到空格的时候,我们可以通过判断它的上一个或者下一个字符来决定该空格是否可以被移除。也就是,比如 '{' 前后的字符是可以被移除的,因为它们只起到了格式美化,并没有实际作用。
2、CSS 格式化
2.1 算法实现分析
相比于压缩算法,格式化算法则复杂得多。这主要体现在,
- 需要自己判断一行是否结束,并且要在代码块
{}
内进行缩进处理。 - 一行结束不是非得有 ';' 或者 '}' 这样的明显的比较,也可能不加分号
- 此外,我们还需要考虑代码块嵌套的情形,需要进行多次缩进
这里对缩进的处理是,在遇到一行结束的时候主动给下一行增加缩进。这里用变量 regionCount
表示当前代码块的层次,比如一层的时候值是 1,两层的时候值是 2,以此类推。所以,下一行需要追加的缩进的空格的数量就是(这里默认使用两个空格进行缩进):
int spaceCount = regionCount * 2;
while (spaceCount-- > 0) {
chars[index++] = ' ';
}
复制代码
一行结束的标识符是 ;
、}
或者 {
,但也有可能不使用分号 ;
标识最后一行的结尾。所以,此时,我们需要根据 }
符号进行判断。因为不以分号结尾发生在所有样式语句的最后一条。此时,我们根据 }
的上一个字符进行判断,如果上一个字符是 ;
则表明之前已经针对 ;
处理过缩进了,如果不是,则表明上一行并非以分号结尾,所以没有处理过缩进,因此需要在当前行自己给自己追加缩进。不过,此时需要考虑一个特殊情形,就是上一行不是 CSS 样式,而是跟当前行一样是一个 }
,所以需要过滤这种情形,因此代码如下,
if (i > 0 && fromChars[i-1] != ';' && fromChars[i-1] != '}') {
chars[index++] = '\n';
}
// 加缩进
regionCount --;
int spaceCount = regionCount * 2;
while (spaceCount-- > 0) {
chars[index++] = ' ';
}
复制代码
2.2 算法的实现和分析
private static String formatCSS(String from) {
char[] fromChars = from.toCharArray();
int extendLength = 0;
int regionCount = 0;
for (int i=0, len=fromChars.length; i<len; i++) {
char c = fromChars[i];
if (c == '{') {
// '{' 前增加一个空格,后面增加一个 '\n'
extendLength += 2;
regionCount ++;
extendLength += (2 * regionCount);
} else if (c == '}') {
// 上一行结束,但是没有使用 ';'
if (i > 0 && fromChars[i-1] != ';'&& fromChars[i-1] != '}') {
extendLength += 1;
}
// '}' 后增加一个 '\n'
extendLength += 1;
regionCount --;
extendLength += (2 * regionCount);
if (regionCount > 0 && i < len-1 && fromChars[i+1] != '}') {
extendLength += (2 * regionCount);
}
}else if (c == ':' && regionCount > 0) {
extendLength += 1;
} else if (c == ';') {
extendLength += 1;
// 下一行仍有格式,追加两个空格
if (regionCount > 0 && i < len-1 && fromChars[i+1] != '}') {
extendLength += (2 * regionCount);
}
}
}
char[] chars = new char[extendLength + fromChars.length];
int index = 0;
regionCount = 0;
for (int i=0, len=fromChars.length; i<len; i++) {
char c = fromChars[i];
if (c == '{') {
// '{' 前增加一个空格,后面增加一个 '\n'
chars[index++] = ' ';
chars[index++] = c;
chars[index++] = '\n';
regionCount ++;
// 为下一行加缩进
int spaceCount = regionCount * 2;
while (spaceCount-- > 0) {
chars[index++] = ' ';
}
} else if (c == '}') {
// 上一行结束,但是没有使用 ';' 或者 '}',因为它们会自己追加 '\n'
if (i > 0 && fromChars[i-1] != ';' && fromChars[i-1] != '}') {
chars[index++] = '\n';
}
// 加缩进
regionCount --;
int spaceCount = regionCount * 2;
while (spaceCount-- > 0) {
chars[index++] = ' ';
}
chars[index++] = c;
// '}' 后增加一个 '\n'
chars[index++] = '\n';
// 如果下一行有样式,为下一行加缩进
if (regionCount > 0 && i < len-1 && fromChars[i+1] != '}') {
spaceCount = regionCount * 2;
while (spaceCount-- > 0) {
chars[index++] = ' ';
}
}
} else if (c == ';') {
chars[index++] = c;
chars[index++] = '\n';
// 如果下一行有样式,为下一行加缩进
if (regionCount > 0 && i < len-1 && fromChars[i+1] != '}') {
int spaceCount = regionCount * 2;
while (spaceCount-- > 0) {
chars[index++] = ' ';
}
}
} else if (c == ':' && regionCount > 0) {
// TODO rare cases 也有可能存在嵌套的 a:hover 的情形
chars[index++] = c;
chars[index++] = ' ';
} else {
chars[index++] = c;
}
}
return new String(chars);
}
复制代码
算法的思路分析的时候已经分析了部分主要的实现,上述代码应该不难看懂。除了上面分析的之外,还要注意,对于 :
,它可能用来标识标签的行为比如 a:hover
,因此不能总是在后面追加空格。
总结
以上是对 Java 实现的 CSS 压缩和格式化算法的分析。对于我在应用中的使用场景,对算法的精度要求并不高,以上算法可以满足我的要求。其他的应用场景,建议酌情进行大量的样本测试之后再使用。