JFinal模板引擎指令扩展实现压缩成一行

使用Enjoy的指令实现将html代码压缩为一行:

正则表达式版:

import com.jfinal.template.Directive;
import com.jfinal.template.Env;
import com.jfinal.template.TemplateException;
import com.jfinal.template.expr.ast.ExprList;
import com.jfinal.template.io.CharWriter;
import com.jfinal.template.io.FastStringWriter;
import com.jfinal.template.io.Writer;
import com.jfinal.template.stat.Scope;

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

public class CompressDirective extends Directive {

    // 不压缩pre/script/style标签
    private static final Pattern ignoredPattern = Pattern.compile("(<pre>(.|\n)*?</pre>)|(<script>(.|\n)*?</script>)|(<style>(.|\n)*?</style>)");
    private static final Pattern matchedPattern = Pattern.compile("\\s+");

    @Override
    public void setExprList(ExprList exprList) {
        if (exprList.length() != 0) {
            throw new RuntimeException("#compress directive support no parameters only");
        }
        super.setExprList(exprList);
    }

    @Override
    public void exec(Env env, Scope scope, Writer writer) {
        CharWriter charWriter = new CharWriter(2048);
        FastStringWriter fsw = new FastStringWriter();
        charWriter.init(fsw);
        stat.exec(env, scope, charWriter);

        try {

            StringBuilder temp = fsw.getBuffer();
            Matcher ignoredMatcher = ignoredPattern.matcher(temp);
            int lastIndex = 0;

            while (ignoredMatcher.find()) {
                int end = ignoredMatcher.start();
                writer.write(compress(temp.substring(lastIndex, end)));
                writer.write(ignoredMatcher.group());
                lastIndex = ignoredMatcher.end() + 1;
            }

            // 将最后一个标签后的内容压缩并写入
            writer.write(compress(temp.substring(lastIndex, temp.length())));

        } catch (IOException e) {
            throw new TemplateException(e.getMessage(), location, e);
        }

    }

    private String compress(String temp) {
        Matcher matchedMatcher = matchedPattern.matcher(temp.trim());
        // 多个空白字符变为1个
        temp = matchedMatcher.replaceAll(" ");
        return temp;
    }

    @Override
    public boolean hasEnd() {
        return true;
    }

}

手动分析版:

import com.jfinal.template.Directive;
import com.jfinal.template.Env;
import com.jfinal.template.TemplateException;
import com.jfinal.template.expr.ast.ExprList;
import com.jfinal.template.io.CharWriter;
import com.jfinal.template.io.FastStringWriter;
import com.jfinal.template.io.Writer;
import com.jfinal.template.stat.Scope;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class CompressDirective extends Directive {

    // 不压缩的标签
    private static final List<char[]> REQUIRED_TAG_NAMES = buildRequiredTagNames();
    private final StringBuilder newString = new StringBuilder();
    private int cur = 0;
    private char[] buf;
    private char[] currentTagName = null;

    private static List<char[]> buildRequiredTagNames() {
        List<char[]> requiredTagName = new ArrayList<>();
        requiredTagName.add("pre".toCharArray());
        requiredTagName.add("script".toCharArray());
        requiredTagName.add("style".toCharArray());
        return requiredTagName;
    }

    @Override
    public void setExprList(ExprList exprList) {
        if (exprList.length() != 0) {
            throw new RuntimeException("#compress directive support no parameters only");
        }
        super.setExprList(exprList);
    }

    @Override
    public void exec(Env env, Scope scope, Writer writer) {
        CharWriter charWriter = new CharWriter(2048);
        FastStringWriter fsw = new FastStringWriter();
        charWriter.init(fsw);
        stat.exec(env, scope, charWriter);
        buf = fsw.getBuffer().toString().toCharArray();
        compress();
        try {
            writer.write(newString.toString());
        } catch (IOException e) {
            throw new TemplateException(e.getMessage(), location, e);
        }
    }

    private void compress() {
        boolean isLastSpace = false;
        while (cur < buf.length) {
            char c = buf[cur];
            boolean isSpace = Character.isWhitespace(c);
            // 多个空白字符只保留一个
            if (isLastSpace) {
                if (!isSpace) {
                    newString.append(c);
                    isLastSpace = false;
                }
            } else if (isSpace) {
                newString.append(' ');
                isLastSpace = true;
            } else {
                newString.append(c);
            }
            cur++;
            if (c == '<') {
                tryStartTag();
            }
        }
    }

    private void tryStartTag() {
        if (cur == buf.length) {
            return;
        }
        if (!isRequiredStartTag()) {
            return;
        }
        startTagEnd();
        while (cur < buf.length) {
            char c = buf[cur];
            newString.append(c);
            cur++;
            if (c == '<') {
                tryEndTag();
                if (currentTagName == null) {
                    break;
                }
            }
        }
    }

    private void startTagEnd() {
        while (cur < buf.length) {
            char c = buf[cur];
            newString.append(c);
            cur++;
            if (c == '>') {
                break;
            }
        }
    }

    private void tryEndTag() {
        if (cur == buf.length) {
            return;
        }
        if (!isRequiredEndTag()) {
            return;
        }
        startTagEnd();
        currentTagName = null;
    }

    private boolean isRequiredEndTag() {
        char c = buf[cur];
        if (c != '/') {
            return false;
        }
        cur++;
        newString.append(c);
        int length = currentTagName.length;
        int i = 0;
        while (i < length) {
            if (cur == buf.length) {
                return false;
            }
            c = buf[cur];
            if (currentTagName[i] != c) {
                return false;
            }
            i++;
            cur++;
            newString.append(c);
        }
        if (cur == buf.length) {
            return false;
        }
        c = buf[cur];
        while (Character.isWhitespace(c)) {
            newString.append(c);
            cur++;
            if (cur == buf.length) {
                return false;
            }
            c = buf[cur];
        }
        return c == '>';
    }

    private boolean isRequiredStartTag() {
        for (char[] tagName : REQUIRED_TAG_NAMES) {
            boolean isNeeded = true;
            int length = tagName.length;
            int i = 0;
            while (i < length) {
                if (cur == buf.length) {
                    isNeeded = false;
                    break;
                }
                char c = buf[cur];
                if (c != tagName[i]) {
                    isNeeded = false;
                    break;
                }
                newString.append(c);
                cur++;
                i++;
            }
            if (isNeeded && cur < buf.length) {
                if (Character.isWhitespace(buf[cur]) || buf[cur] == '>') {
                    currentTagName = tagName;
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public boolean hasEnd() {
        return true;
    }

}

在配置中添加该指令:

engine.addDirective("compress", CompressDirective.class);

即可使用指令:

#compress()
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>压缩测试</title>
    <style>
        body {
            margin: 0;
        }
    </style>
</head>
<body>
压
缩
<pre>
    原样输出
</pre>
<script>
    //原样输出
</script>
</body>
</html>
#end


评论区

lyh061619

2018-01-17 19:48

这个实用,感谢分享。

lyh061619

2018-01-17 23:51

不过这个规则有问题。哈哈

fmpoffice

2018-01-18 08:34

@lyh061619 请问有啥问题哈?方便指点吗?

jerryhw

2018-01-18 09:43

@lyh061619 确实有问题,标签上带一些其他属性会出现问题,或者嵌套也会有问题,嵌套问题没想到好办法,不过带属性的话只要把ignoredPattern那个变量的正则表达式扩展一下应该就好了。

lyh061619

2018-01-18 09:54

@jerryhw 你这说对了一半问题,另外还会报一个异常中: java.lang.StackOverflowError
详细堆栈:
java.lang.StackOverflowError
at java.lang.Character.codePointAt(Character.java:4866)
at java.util.regex.Pattern$CharProperty.match(Pattern.java:3777)

jerryhw

2018-01-18 10:14

@lyh061619 这个问题看样子是结构复杂情况下正则表达式转换成NFA后解析的时候递归次数过多引起的栈溢出,感觉正则表达式没办法避免这个问题,也许需要一个简易的词法分析来取代正则表达式匹配来解决。

lyh061619

2018-01-18 10:18

@jerryhw 也就是当标签有大量的js的时候,递归太深,其次js长度太长了就会报以上异常,最后从性能上来分析估计是有不少的损耗,但这个损耗一般模板引擎基本上有缓存存储,如果模板没有缓存,就得考虑缓存将压缩过的页面进行缓存,下次访问直接从缓存读取,当界面发生变化再次压缩并更新缓存。

lyh061619

2018-01-18 10:20

瞎吹啊,你们自己琢磨琢磨啊,哈哈!!

jerryhw

2018-01-18 14:10

@lyh061619 如果可以试一试我新添加的这个,应该没有刚才的那个栈溢出问题了,但是有可能会有其他问题。

lyh061619

2018-01-18 15:15

@jerryhw 哈哈,感谢分享,我就不玩了,还是建议你自己测试一遍。^_^!!