001: /*
002: * This file is part of the Echo Web Application Framework (hereinafter "Echo").
003: * Copyright (C) 2002-2005 NextApp, Inc.
004: *
005: * Version: MPL 1.1/GPL 2.0/LGPL 2.1
006: *
007: * The contents of this file are subject to the Mozilla Public License Version
008: * 1.1 (the "License"); you may not use this file except in compliance with the
009: * License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
010: *
011: * Software distributed under the License is distributed on an "AS IS" basis,
012: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
013: * the specific language governing rights and limitations under the License.
014: *
015: * Alternatively, the contents of this file may be used under the terms of
016: * either the GNU General Public License Version 2 or later (the "GPL"), or the
017: * GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which
018: * case the provisions of the GPL or the LGPL are applicable instead of those
019: * above. If you wish to allow use of your version of this file only under the
020: * terms of either the GPL or the LGPL, and not to allow others to use your
021: * version of this file under the terms of the MPL, indicate your decision by
022: * deleting the provisions above and replace them with the notice and other
023: * provisions required by the GPL or the LGPL. If you do not delete the
024: * provisions above, a recipient may use your version of this file under the
025: * terms of any one of the MPL, the GPL or the LGPL.
026: */
027:
028: package nextapp.echo2.webrender.util;
029:
030: /**
031: * Compresses a String containing JavaScript by removing comments and
032: * whitespace.
033: */
034: public class JavaScriptCompressor {
035:
036: private static final char LINE_FEED = '\n';
037: private static final char CARRIAGE_RETURN = '\r';
038: private static final char SPACE = ' ';
039: private static final char TAB = '\t';
040:
041: /**
042: * Compresses a String containing JavaScript by removing comments and
043: * whitespace.
044: *
045: * @param script the String to compress
046: * @return a compressed version
047: */
048: public static String compress(String script) {
049: JavaScriptCompressor jsc = new JavaScriptCompressor(script);
050: return jsc.outputBuffer.toString();
051: }
052:
053: /** Original JavaScript text. */
054: private String script;
055:
056: /**
057: * Compressed output buffer.
058: * This buffer may only be modified by invoking the <code>append()</code>
059: * method.
060: */
061: private StringBuffer outputBuffer;
062:
063: /** Current parser cursor position in original text. */
064: private int pos;
065:
066: /** Character at parser cursor position. */
067: private char ch;
068:
069: /** Last character appended to buffer. */
070: private char lastAppend;
071:
072: /** Flag indicating if end-of-buffer has been reached. */
073: private boolean endReached;
074:
075: /** Flag indicating whether content has been appended after last identifier. */
076: private boolean contentAppendedAfterLastIdentifier = true;
077:
078: /**
079: * Creates a new <code>JavaScriptCompressor</code> instance.
080: *
081: * @param script
082: */
083: private JavaScriptCompressor(String script) {
084: this .script = script;
085: outputBuffer = new StringBuffer(script.length());
086: nextChar();
087:
088: while (!endReached) {
089: if (Character.isJavaIdentifierStart(ch)) {
090: renderIdentifier();
091: } else if (ch == ' ') {
092: skipWhiteSpace();
093: } else if (isWhitespace()) {
094: // Compress whitespace
095: skipWhiteSpace();
096: } else if ((ch == '"') || (ch == '\'')) {
097: // Handle strings
098: renderString();
099: } else if (ch == '/') {
100: // Handle comments
101: nextChar();
102: if (ch == '/') {
103: nextChar();
104: skipLineComment();
105: } else if (ch == '*') {
106: nextChar();
107: skipBlockComment();
108: } else {
109: append('/');
110: }
111: } else {
112: append(ch);
113: nextChar();
114: }
115: }
116: }
117:
118: /**
119: * Append character to output.
120: *
121: * @param ch the character to append
122: */
123: private void append(char ch) {
124: lastAppend = ch;
125: outputBuffer.append(ch);
126: contentAppendedAfterLastIdentifier = true;
127: }
128:
129: /**
130: * Determines if current character is whitespace.
131: *
132: * @return true if the character is whitespace
133: */
134: private boolean isWhitespace() {
135: return ch == CARRIAGE_RETURN || ch == SPACE || ch == TAB
136: || ch == LINE_FEED;
137: }
138:
139: /**
140: * Load next character.
141: */
142: private void nextChar() {
143: if (!endReached) {
144: if (pos < script.length()) {
145: ch = script.charAt(pos++);
146: } else {
147: endReached = true;
148: ch = 0;
149: }
150: }
151: }
152:
153: /**
154: * Adds an identifier to output.
155: */
156: private void renderIdentifier() {
157: if (!contentAppendedAfterLastIdentifier)
158: append(SPACE);
159: append(ch);
160: nextChar();
161: while (Character.isJavaIdentifierPart(ch)) {
162: append(ch);
163: nextChar();
164: }
165: contentAppendedAfterLastIdentifier = false;
166: }
167:
168: /**
169: * Adds quoted String starting at current character to output.
170: */
171: private void renderString() {
172: char startCh = ch; // Save quote char
173: append(ch);
174: nextChar();
175: while (true) {
176: if ((ch == LINE_FEED) || (ch == CARRIAGE_RETURN)
177: || (endReached)) {
178: // JavaScript error: string not terminated
179: return;
180: } else {
181: if (ch == '\\') {
182: append(ch);
183: nextChar();
184: if ((ch == LINE_FEED) || (ch == CARRIAGE_RETURN)
185: || (endReached)) {
186: // JavaScript error: string not terminated
187: return;
188: }
189: append(ch);
190: nextChar();
191: } else {
192: append(ch);
193: if (ch == startCh) {
194: nextChar();
195: return;
196: }
197: nextChar();
198: }
199: }
200: }
201: }
202:
203: /**
204: * Moves cursor past a line comment.
205: */
206: private void skipLineComment() {
207: while ((ch != CARRIAGE_RETURN) && (ch != LINE_FEED)) {
208: if (endReached) {
209: return;
210: }
211: nextChar();
212: }
213: }
214:
215: /**
216: * Moves cursor past a block comment.
217: */
218: private void skipBlockComment() {
219: while (true) {
220: if (endReached) {
221: return;
222: }
223: if (ch == '*') {
224: nextChar();
225: if (ch == '/') {
226: nextChar();
227: return;
228: }
229: } else
230: nextChar();
231: }
232: }
233:
234: /**
235: * Renders a new line character, provided previously rendered character
236: * is not a newline.
237: */
238: private void renderNewLine() {
239: if (lastAppend != '\n' && lastAppend != '\r') {
240: append('\n');
241: }
242: }
243:
244: /**
245: * Moves cursor past white space (including newlines).
246: */
247: private void skipWhiteSpace() {
248: if (ch == LINE_FEED || ch == CARRIAGE_RETURN) {
249: renderNewLine();
250: } else {
251: append(ch);
252: }
253: nextChar();
254: while (ch == LINE_FEED || ch == CARRIAGE_RETURN || ch == SPACE
255: || ch == TAB) {
256: if (ch == LINE_FEED || ch == CARRIAGE_RETURN) {
257: renderNewLine();
258: }
259: nextChar();
260: }
261: }
262: }
|