/** * ---------------------- * Minify.java 2015-10-04 * ---------------------- * * Copyright (c) 2015 Charles Bihis (www.whoischarles.com) * * This work is an adaptation of JSMin.java published by John Reilly which is a translation from C to Java of jsmin.c * published by Douglas Crockford. Permission is hereby granted to use this Java version under the same conditions as * the original jsmin.c on which all of these derivatives are based. * * * * --------------------- * JSMin.java 2006-02-13 * --------------------- * * Copyright (c) 2006 John Reilly (www.inconspicuous.org) * * This work is a translation from C to Java of jsmin.c published by Douglas Crockford. Permission is hereby granted to * use the Java version under the same conditions as the jsmin.c on which it is based. * * * * ------------------ * jsmin.c 2003-04-21 * ------------------ * * Copyright (c) 2002 Douglas Crockford (www.crockford.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the * Software. * * The Software shall be used for Good, not Evil. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.whoischarles.util.json; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PushbackInputStream; import java.nio.charset.StandardCharsets; /** * Minify.java is written by Charles Bihis (www.whoischarles.com) and is adapted from JSMin.java written by John Reilly * (www.inconspicuous.org) which is itself a translation of jsmin.c written by Douglas Crockford (www.crockford.com). * * @see http://www.unl.edu/ucomm/templatedependents/JSMin.java * @see http://www.crockford.com/javascript/jsmin.c */ public class Minify { private static final int EOF = -1; private PushbackInputStream in; private OutputStream out; private int currChar; private int nextChar; private int line; private int column; public static enum Action { OUTPUT_CURR, DELETE_CURR, DELETE_NEXT } public Minify() { this.in = null; this.out = null; } /** * Minifies the input JSON string. * * Takes the input JSON string and deletes the characters which are insignificant to JavaScipt. Comments will be * removed, tabs will be replaced with spaces, carriage returns will be replaced with line feeds, and most spaces * and line feeds will be removed. The result will be returned. * * @param json The JSON string for which to minify * @return A minified, yet functionally identical, version of the input JSON string */ public String minify(String json) { InputStream in = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { minify(in, out); } catch (Exception e) { e.printStackTrace(); return null; } return out.toString().trim(); } /** * Takes an input stream to a JSON string and outputs minified JSON to the output stream. * * Takes the input JSON via the input stream and deletes the characters which are insignificant to JavaScript. * Comments will be removed, tabs will be replaced with spaaces, carriage returns will be replaced with line feeds, * and most spaces and line feeds will be removed. The result is streamed to the output stream. * * @param in The InputStream from which to get the un-minified JSON * @param out The OutputStream where the resulting minified JSON will be streamed to * @throws IOException * @throws UnterminatedRegExpLiteralException * @throws UnterminatedCommentException * @throws UnterminatedStringLiteralException */ public void minify(InputStream in, OutputStream out) throws IOException, UnterminatedRegExpLiteralException, UnterminatedCommentException, UnterminatedStringLiteralException { // Initialize this.in = new PushbackInputStream(in); this.out = out; this.line = 0; this.column = 0; // currChar = '\n'; // action(Action.DELETE_NEXT); currChar = get(); nextChar = peek(); // Process input while (currChar != EOF) { switch (currChar) { case ' ': if (isAlphanum(nextChar)) { action(Action.OUTPUT_CURR); } else { action(Action.DELETE_CURR); } break; case '\n': switch (nextChar) { case '{': case '[': case '(': case '+': case '-': action(Action.OUTPUT_CURR); break; case ' ': action(Action.DELETE_NEXT); break; default: if (isAlphanum(nextChar)) { action(Action.OUTPUT_CURR); } else { action(Action.DELETE_CURR); } } break; default: switch (nextChar) { case ' ': if (isAlphanum(currChar)) { action(Action.OUTPUT_CURR); break; } action(Action.DELETE_NEXT); break; case '\n': switch (currChar) { case '}': case ']': case ')': case '+': case '-': case '"': case '\'': action(Action.OUTPUT_CURR); break; default: if (isAlphanum(currChar)) { action(Action.OUTPUT_CURR); } else { action(Action.DELETE_NEXT); } } break; default: action(Action.OUTPUT_CURR); break; } } } out.flush(); } /** * Process the current character with an appropriate action. * * The action that occurs is determined by the current character. The options are: * * 1. Output currChar: output currChar, copy nextChar to currChar, get the next character and save it to nextChar * 2. Delete currChar: copy nextChar to currChar, get the next character and save it to nextChar * 3. Delete nextChar: get the next character and save it to nextChar * * This method essentially treats a string as a single character. Also recognizes regular expressions if they are * preceded by '(', ',', or '='. * * @param action The action to perform * @throws IOException * @throws UnterminatedRegExpLiteralException * @throws UnterminatedCommentException * @throws UnterminatedStringLiteralException */ private void action(Action action) throws IOException, UnterminatedRegExpLiteralException, UnterminatedCommentException, UnterminatedStringLiteralException { // Process action switch (action) { case OUTPUT_CURR: out.write(currChar); case DELETE_CURR: currChar = nextChar; if (currChar == '\'' || currChar == '"') { for ( ; ; ) { out.write(currChar); currChar = get(); if (currChar == nextChar) { break; } if (currChar <= '\n') { throw new UnterminatedStringLiteralException(line, column); } if (currChar == '\\') { out.write(currChar); currChar = get(); } } } case DELETE_NEXT: nextChar = next(); if (nextChar == '/' && (currChar == '(' || currChar == ',' || currChar == '=' || currChar == ':')) { out.write(currChar); out.write(nextChar); for ( ; ; ) { currChar = get(); if (currChar == '/') { break; } else if (currChar == '\\') { out.write(currChar); currChar = get(); } else if (currChar <= '\n') { throw new UnterminatedRegExpLiteralException(line, column); } out.write(currChar); } nextChar = next(); } } } /** * Determines whether a given character is a letter, digit, underscore, dollar sign, or non-ASCII character. * * @param c The character to compare * @return True if the character is a letter, digit, underscore, dollar sign, or non-ASCII character. False otherwise. */ private boolean isAlphanum(int c) { return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$' || c == '\\' || c > 126); } /** * Returns the next character from the input stream. * * Will pop the next character from the input stack. If the character is a control character, translate it to a space * or line feed. * * @return The next character from the input stream * @throws IOException */ private int get() throws IOException { int c = in.read(); if (c == '\n') { line++; column = 0; } else { column++; } if (c >= ' ' || c == '\n' || c == EOF) { return c; } if (c == '\r') { column = 0; return '\n'; } return ' '; } /** * Returns the next character from the input stream without popping it from the stack. * * @return The next character from the input stream * @throws IOException */ private int peek() throws IOException { int lookaheadChar = in.read(); in.unread(lookaheadChar); return lookaheadChar; } /** * Get the next character from the input stream, excluding comments. * * Will read from the input stream via the get() method. Will exclude characters that are part of * comments. peek() is used to se if a '/' is followed by a '/' or a '*' for the purpose of identifying * comments. * * @return The next character from the input stream, excluding characters from comments * @throws IOException * @throws UnterminatedCommentException */ private int next() throws IOException, UnterminatedCommentException { int c = get(); if (c == '/') { switch (peek()) { case '/': for ( ; ; ) { c = get(); if (c <= '\n') { return c; } } case '*': get(); for ( ; ; ) { switch (get()) { case '*': if (peek() == '/') { get(); return ' '; } break; case EOF: throw new UnterminatedCommentException(line, column); } } default: return c; } } return c; } /** * Exception to be thrown when an unterminated comment appears in the input. */ public static class UnterminatedCommentException extends Exception { public UnterminatedCommentException(int line, int column) { super("Unterminated comment at line " + line + " and column " + column); } } /** * Exception to be thrown when an unterminated string literal appears in the input. */ public static class UnterminatedStringLiteralException extends Exception { public UnterminatedStringLiteralException(int line, int column) { super("Unterminated string literal at line " + line + " and column " + column); } } /** * Exception to be thrown when an unterminated regular expression literal appears in the input. */ public static class UnterminatedRegExpLiteralException extends Exception { public UnterminatedRegExpLiteralException(int line, int column) { super("Unterminated regular expression at line " + line + " and column " + column); } } }