Skip to content

Instantly share code, notes, and snippets.

@BurntPizza
Last active August 29, 2015 14:16
Show Gist options
  • Save BurntPizza/800b6b4322aaa2da1960 to your computer and use it in GitHub Desktop.
Save BurntPizza/800b6b4322aaa2da1960 to your computer and use it in GitHub Desktop.
Java source minifer
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.*;
import java.util.regex.*;
public class CoffeeGrinder {
public static void main(String[] args) throws IOException {
if (args.length == 0) {
System.out.println("CoffeeGrinder: Java source code minifier");
System.out.println("\tby BurntPizza\n");
printUsage();
}
String path = args[args.length - 1];
if (!Files.isReadable(Paths.get(path))) {
System.out.println("Not a readable file: " + Paths.get(path).toAbsolutePath());
printUsage();
}
int lineWrapping = 80;
boolean aggressive = true, printInfo = false;
for (String s : args) {
if (s.equals("-i")) {
printInfo = true;
continue;
}
if (s.equals("-c")) {
aggressive = false;
continue;
}
if (s.matches("-w:\\d+")) {
s = s.substring(s.indexOf(':') + 1);
try {
lineWrapping = Integer.parseInt(s);
} catch (NumberFormatException e) {
System.out.println("\"" + s + "\" is not an integer.");
printUsage();
}
continue;
}
if (s.equals(path))
continue;
System.out.println("Not a valid argument " + s);
printUsage();
}
StringBuilder preprocessed = new StringBuilder();
for (String line : Files.readAllLines(Paths.get(path), Charset.defaultCharset())) {
line = line.trim();
if (!line.isEmpty())
preprocessed.append(line).append('\n');
}
String code = preprocessed.toString();
int originalLength = code.length();
code = minify(code, lineWrapping, aggressive);
System.out.println();
System.out.println(code);
if (printInfo)
System.out.printf("\n%s: %d/%d - %.2f%c\n", path, code.length(), originalLength, code.length() / (double) originalLength * 100, '%');
}
private static void printUsage() {
System.out.println("Usage: [options] file\n");
System.out.println("\t-i\tPrint compression info");
System.out.println("\t-c\tOnly strip comments and annotations");
System.out.println("\t-w:n\tAttempt line wrapping at n columns");
System.out.println("\t\tUse 0 for no wrapping. Default: 80");
System.exit(0);
}
private static String minify(String src, int lineWrap, boolean aggressive) {
PreservationResult pr = preserveStringLiterals(src);
String code = pr.output
// annotations
.replaceAll("@.*?\\s*(\\((.|\\n)*?\\))*\\s+", "")
// comments
.replaceAll("(//.*\\n)|(/\\*(.|\n)*?\\*/)", "");
if (aggressive)
code = code
// easy operators
.replaceAll("\\s*([\\~!&|\\^\\?:;,<>=\\.\\*/%\\{\\}\\(\\)\\[\\]])\\s*", "$1")
// hard operators
.replaceAll("([+-]{2})\\s+([+-][^+-])", "$1$2")
.replaceAll("([^+-]+)\\s+([+-])\\s+([^+-])", "$1$2$3")
.replaceAll("([^\\s+-])\\s*([+-]+)\\s*([^\\s+-])", "$1$2$3")
.replaceAll("([^+-])\\s*([+-]\\s*[+-]+)", "$1$2")
.replaceAll("(\\s[+-]+)\\s+([^\\s+-])", "$1$2")
.replaceAll("([+-])\\s+([^+-])", "$1$2");
// any whitespace leftover, just in case
code = code.replaceAll("\\s+", lineWrap > 0 ? "\n" : " ");
code = pr.revert(code);
// line wrap
if (lineWrap > 0)
code = lineWrap(code, lineWrap);
return code.trim();
}
private static String lineWrap(String text, int width) {
StringBuilder lineWrapped = new StringBuilder();
StringBuilder sb = new StringBuilder();
String[] lines = text.split("\n");
for (int i = 0; i < lines.length;) {
do
sb.append(lines[i++]).append(' ');
while (sb.length() + (i < lines.length ? lines[i].length() + 1 : 0) <= width && i < lines.length);
String line = sb.toString().trim();
if (!line.isEmpty())
lineWrapped.append(line).append(i == lines.length ? "" : "\n");
sb.setLength(0);
}
return lineWrapped.toString();
}
private static PreservationResult preserveStringLiterals(String in) {
Deque<Interval> intervals = new ArrayDeque<>();
Map<String, String> mapping = new HashMap<>();
int seed = 0;
String key;
do
key = "#&" + ++seed + "&\\d+#";
while (Pattern.compile(key).matcher(in).find());
String prefix = key.substring(0, key.length() - 4);
boolean strmode = false, charmode = false, linecomment = false, blockcomment = false, escaped = false;
for (int i = 0, start = i; i < in.length(); i++) {
char c = in.charAt(i);
boolean inComment = (linecomment || blockcomment);
if ((c == '"' && !(charmode || inComment)) || (c == '\'' && !(strmode || inComment)) && !escaped) {
if (c == '"')
strmode ^= true;
else
charmode ^= true;
if (strmode || charmode)
start = i;
else {
mapping.put(prefix + start + '#', in.substring(start, i + 1));
intervals.push(new Interval(start, i + 1));
}
} else if (c == '/' && !(charmode || strmode)) {
if (i > 0 && in.charAt(i - 1) == '/' && !blockcomment)
linecomment = true;
else if (i + 1 < in.length() && in.charAt(i + 1) == '*' && !linecomment)
blockcomment = true;
else if (i - 1 > 0 && in.charAt(i - 1) == '*' && !linecomment)
blockcomment = false;
} else if (c == '\n')
linecomment = false;
escaped = c == '\\' ? !escaped : false;
}
StringBuilder sb = new StringBuilder(in);
while (!intervals.isEmpty()) {
Interval i = intervals.pop();
sb.replace(i.start, i.end, prefix + i.start + '#');
}
PreservationResult pr = new PreservationResult();
pr.output = sb.toString();
pr.key = key;
pr.mapping = mapping;
return pr;
}
private static class PreservationResult {
String output, key;
Map<String, String> mapping;
String revert(String text) {
Matcher m = Pattern.compile(key).matcher(text);
Deque<Interval> intervals = new ArrayDeque<>();
Deque<String> matches = new ArrayDeque<>();
while (m.find()) {
intervals.push(new Interval(m.start(), m.end()));
matches.push(m.group());
}
StringBuilder sb = new StringBuilder(text);
while (!matches.isEmpty()) {
Interval i = intervals.pop();
sb.replace(i.start, i.end, mapping.get(matches.pop()));
}
return sb.toString();
}
}
private static class Interval {
int start, end;
Interval(int x, int y) {
start = x;
end = y;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment