1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50:
51: class Horde_Text_Filter_JavascriptMinify_JsMin
52: {
53:
54: const ORD_LF = 10;
55: const ORD_SPACE = 32;
56: const ACTION_KEEP_A = 1;
57: const ACTION_DELETE_A = 2;
58: const ACTION_DELETE_A_B = 3;
59:
60:
61: protected $_a = "\n";
62: protected $_b = '';
63: protected $_input;
64: protected $_inputIndex = 0;
65: protected $_inputLength;
66: protected $_lookAhead = null;
67: protected $_output = '';
68: protected $_x = null;
69: protected $_y = null;
70:
71: public function __construct($input)
72: {
73: $this->_input = str_replace("\r\n", "\n", $input);
74: $this->_inputLength = strlen($this->_input);
75: }
76:
77: public function minify()
78: {
79: $this->_action(self::ACTION_DELETE_A_B);
80:
81: while (!is_null($this->_a)) {
82: $cmd = self::ACTION_KEEP_A;
83: switch ($this->_a) {
84: case ' ':
85: if (!$this->_isAlphaNum($this->_b)) {
86: $cmd = self::ACTION_DELETE_A;
87: }
88: break;
89:
90: case "\n":
91: if ($this->_b === ' ') {
92: $cmd = self::ACTION_DELETE_A_B;
93: } elseif (!strspn($this->_b, '{[(+-') &&
94: !$this->_isAlphaNum($this->_b)) {
95: $cmd = self::ACTION_DELETE_A;
96: }
97: break;
98:
99: default:
100: if (!$this->_isAlphaNum($this->_a) &&
101: (($this->_b === ' ') ||
102: (($this->_b === "\n" && !strspn($this->_a, '}])+-"\''))))) {
103: $cmd = self::ACTION_DELETE_A_B;
104: }
105: break;
106: }
107: $this->_action($cmd);
108: }
109:
110: return trim($this->_output);
111: }
112:
113: protected function _action($d)
114: {
115: switch($d) {
116: case self::ACTION_KEEP_A:
117: $this->_output .= $this->_a;
118: if (($this->_a == $this->_b) &&
119: ($this->_y != $this->_a) &&
120: strspn($this->_a, '+-')) {
121: $this->_output .= ' ';
122: }
123:
124: case self::ACTION_DELETE_A:
125: $this->_a = $this->_b;
126:
127: if ($this->_a === '\'' || $this->_a === '"') {
128: while (true) {
129: $this->_output .= $this->_a;
130: $this->_a = $this->_get();
131:
132: if ($this->_a === $this->_b) {
133: break;
134: }
135:
136: if (ord($this->_a) <= self::ORD_LF) {
137: throw new Exception('Unterminated string literal.');
138: }
139:
140: if ($this->_a === '\\') {
141: $this->_output .= $this->_a;
142: $this->_a = $this->_get();
143: }
144: }
145: }
146:
147: case self::ACTION_DELETE_A_B:
148: $this->_b = $this->_next();
149:
150: if ($this->_b === '/' && strspn($this->_a, '(,=:[!&|?{};\n')) {
151: $this->_output .= $this->_a . $this->_b;
152:
153: while (true) {
154: $this->_a = $this->_get();
155:
156: if ($this->_a === '[') {
157: 158:
159: while (true) {
160: $this->_output .= $this->_a;
161: $this->_a = $this->_get();
162:
163: if ($this->_a === ']') {
164: break;
165: } elseif ($this->_a === '\\') {
166: $this->_output .= $this->_a;
167: $this->_a = $this->_get();
168: } elseif (ord($this->_a) <= self::ORD_LF) {
169: throw new Exception('Unterminated regular expression set in regex literal.');
170: }
171: }
172: } elseif ($this->_a === '/') {
173: break;
174: } elseif ($this->_a === '\\') {
175: $this->_output .= $this->_a;
176: $this->_a = $this->_get();
177: } elseif (ord($this->_a) <= self::ORD_LF) {
178: throw new Exception('Unterminated regular expression literal.');
179: }
180:
181: $this->_output .= $this->_a;
182: }
183:
184: $this->_b = $this->_next();
185: }
186: }
187: }
188:
189: protected function _get()
190: {
191: $c = $this->_lookAhead;
192: $this->_lookAhead = null;
193:
194: if (is_null($c) &&
195: ($this->_inputIndex < $this->_inputLength)) {
196: $c = $this->_input[$this->_inputIndex];
197: $this->_inputIndex += 1;
198: $this->_y = $this->_x;
199: $this->_x = $c;
200: }
201:
202: if ($c === "\r") {
203: return "\n";
204: }
205:
206: if (is_null($c) || ($c === "\n") || (ord($c) >= self::ORD_SPACE)) {
207: return $c;
208: }
209:
210: return ' ';
211: }
212:
213: protected function _isAlphaNum($c)
214: {
215: return (ord($c) > 126 || preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c));
216: }
217:
218: protected function _next()
219: {
220:
221: $c = $this->_get();
222:
223: if ($c !== '/') {
224: return $c;
225: }
226:
227: switch ($this->_peek()) {
228: case '/':
229: $comment = '';
230:
231: while (true) {
232: $c = $this->_get();
233: $comment .= $c;
234: if (ord($c) <= self::ORD_LF) {
235:
236: if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
237: return '/' . $comment;
238: }
239:
240: return $c;
241: }
242: }
243:
244: case '*':
245: $comment = '';
246: $this->_get();
247:
248: while (true) {
249: $get = $this->_get();
250: switch ($get) {
251: case '*':
252: if ($this->_peek() === '/') {
253: $this->_get();
254:
255:
256: if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
257: return '/*' . $comment . '*/';
258: }
259:
260: return ' ';
261: }
262: break;
263:
264: case null:
265: throw new Exception('Unterminated comment.');
266: }
267:
268: $comment .= $get;
269: }
270: }
271:
272: return $c;
273: }
274:
275: protected function _peek()
276: {
277: $this->_lookAhead = $this->_get();
278: return $this->_lookAhead;
279: }
280:
281: }
282: