ice framework documentation ice doc v1.11.0
  1. namespace Ice\Filter;
  2.  
  3. use Ice\Exception;
  4.  
  5. /**
  6. * Minify js string.
  7. *
  8. * @package Ice/Filter
  9. * @category Minification
  10. * @author Ice Team
  11. * @copyright (c) 2014-2025 Ice Team
  12. * @license http://iceframework.org/license
  13. * @uses jsmin.c www.crockford.com
  14. */
  15. class Js
  16. {
  17. const ORD_LF = 10;
  18. const ORD_SPACE = 32;
  19. const ACTION_KEEP_A = 1;
  20. const ACTION_DELETE_A = 2;
  21. const ACTION_DELETE_A_B = 3;
  22. protected a = "";
  23. protected b = "";
  24. protected input = "";
  25. protected inputIndex = 0;
  26. protected inputLength = 0;
  27. protected lookAhead = null;
  28. protected output = "" { get };
  29.  
  30. /**
  31. * Minify the js.
  32. *
  33. * @param string js JS code to minify
  34. * @return string
  35. */
  36. public function sanitize(string js)
  37. {
  38. let this->a = "",
  39. this->b = "",
  40. this->input = str_replace("\r\n", "\n", js),
  41. this->inputLength = strlen(this->input),
  42. this->inputIndex = 0,
  43. this->lookAhead = null,
  44. this->output = "";
  45.  
  46. return this->min();
  47. }
  48.  
  49. /**
  50. * Action -- do something! What to do is determined by the $command argument.
  51. *
  52. * action treats a string as a single character. Wow!
  53. * action recognizes a regular expression if it is preceded by ( or , or =.
  54. *
  55. * @throws Exception If parser errors are found:
  56. * - Unterminated string literal
  57. * - Unterminated regular expression set in regex literal
  58. * - Unterminated regular expression literal
  59. *
  60. * @param int $command One of class constants:
  61. * ACTION_KEEP_A Output A. Copy B to A. Get the next B.
  62. * ACTION_DELETE_A Copy B to A. Get the next B. (Delete A).
  63. * ACTION_DELETE_A_B Get the next B. (Delete B).
  64. */
  65. protected function action(int command)
  66. {
  67. //switch command {
  68. //case self::ACTION_KEEP_A: //1
  69. if command == self::ACTION_KEEP_A {
  70. let this->output = this->output . this->a;
  71. }
  72.  
  73. //case self::ACTION_DELETE_A: //1, 2
  74. if command == self::ACTION_KEEP_A || command == self::ACTION_DELETE_A {
  75. let this->a = this->b;
  76.  
  77. if this->a === "'" || this->a === "\"" {
  78. while 1 {
  79. let this->output = this->output . this->a,
  80. this->a = this->get();
  81.  
  82. if this->a === this->b {
  83. break;
  84. }
  85.  
  86. if ord(this->a) <= self::ORD_LF {
  87. throw new Exception("Unterminated string literal.");
  88. }
  89.  
  90. if this->a === "\\" {
  91. let this->output = this->output . this->a,
  92. this->a = this->get();
  93. }
  94. }
  95. }
  96. }
  97.  
  98. //case self::ACTION_DELETE_A_B: //1, 2, 3
  99. if command == self::ACTION_KEEP_A || command == self::ACTION_DELETE_A || command == self::ACTION_DELETE_A_B {
  100. let this->b = this->next();
  101.  
  102. if this->b === "/" && (
  103. this->a === "(" || this->a === "," || this->a === "=" ||
  104. this->a === ":" || this->a === "[" || this->a === "!" ||
  105. this->a === "&" || this->a === "|" || this->a === "?" ||
  106. this->a === "{" || this->a === "}" || this->a === ";" ||
  107. this->a === "\n" ) {
  108. let this->output = this->output . this->a . this->b;
  109.  
  110. while 1 {
  111. let this->a = this->get();
  112. if this->a === "[" {
  113. /*
  114. inside a regex [...] set, which MAY contain a "/" itself. Example: mootools Form.Validator near line 460:
  115. return Form.Validator.getValidator("IsEmpty").test(element) || (/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]\.?){0,63}[a-z0-9!#$%&'*+/=?^_`{|}~-]@(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])$/i).test(element.get("value"));
  116. */
  117. while 1 {
  118. let this->output = this->output . this->a,
  119. this->a = this->get();
  120.  
  121. if this->a === "]" {
  122. break;
  123. } elseif this->a === "\\" {
  124. let this->output = this->output . this->a,
  125. this->a = this->get();
  126. } elseif ord(this->a) <= self::ORD_LF {
  127. throw new Exception("Unterminated regular expression set in regex literal.");
  128. }
  129. }
  130. } elseif this->a === "/" {
  131. break;
  132. } elseif this->a === "\\" {
  133. let this->output = this->output . this->a,
  134. this->a = this->get();
  135. } elseif ord(this->a) <= self::ORD_LF {
  136. throw new Exception("Unterminated regular expression literal.");
  137. }
  138.  
  139. let this->output = this->output . this->a;
  140. }
  141.  
  142. let this->b = this->next();
  143. }
  144. }
  145. //}
  146. }
  147.  
  148. /**
  149. * Get next char. Convert ctrl char to space.
  150. *
  151. * @return string|null
  152. */
  153. protected function get()
  154. {
  155. var c, i;
  156.  
  157. let c = this->lookAhead,
  158. this->lookAhead = null;
  159.  
  160. if c === null {
  161. if this->inputIndex < this->inputLength {
  162. let c = substr(this->input, this->inputIndex, 1),
  163. i = this->inputIndex,
  164. this->inputIndex = i + 1;
  165. } else {
  166. let c = null;
  167. }
  168. }
  169.  
  170. if c === "\r" {
  171. return "\n";
  172. }
  173.  
  174. if c === null || c === "\n" || ord(c) >= self::ORD_SPACE {
  175. return c;
  176. }
  177.  
  178. return " ";
  179. }
  180.  
  181. /**
  182. * Is $c a letter, digit, underscore, dollar sign, or non-ASCII character.
  183. *
  184. * @return bool
  185. */
  186. protected function isAlphaNum(c)
  187. {
  188. return ord(c) > 126 || c === "\\" || preg_match("/^[\\w\\$]$/", c) === 1;
  189. }
  190.  
  191. /**
  192. * Perform minification, return result
  193. *
  194. * @return string
  195. */
  196. protected function min()
  197. {
  198. if 0 == strncmp(this->peek(), "\xef", 1) {
  199. this->get();
  200. this->get();
  201. this->get();
  202. }
  203.  
  204. let this->a = "\n";
  205. this->action(self::ACTION_DELETE_A_B);
  206.  
  207. while this->a !== null {
  208. switch this->a {
  209. case " ":
  210. if this->isAlphaNum(this->b) {
  211. this->action(self::ACTION_KEEP_A);
  212. } else {
  213. this->action(self::ACTION_DELETE_A);
  214. }
  215. break;
  216. case "\n":
  217. switch this->b {
  218. case "{":
  219. case "[":
  220. case "(":
  221. case "+":
  222. case "-":
  223. case "!":
  224. case "~":
  225. this->action(self::ACTION_KEEP_A);
  226. break;
  227. case " ":
  228. this->action(self::ACTION_DELETE_A_B);
  229. break;
  230. default:
  231. if this->isAlphaNum(this->b) {
  232. this->action(self::ACTION_KEEP_A);
  233. } else {
  234. this->action(self::ACTION_DELETE_A);
  235. }
  236. }
  237. break;
  238. default:
  239. switch this->b {
  240. case " ":
  241. if this->isAlphaNum(this->a) {
  242. this->action(self::ACTION_KEEP_A);
  243. break;
  244. }
  245. this->action(self::ACTION_DELETE_A_B);
  246. break;
  247. case "\n":
  248. switch this->a {
  249. case "}":
  250. case "]":
  251. case ")":
  252. case "+":
  253. case "-":
  254. case "\"":
  255. case "'":
  256. this->action(self::ACTION_KEEP_A);
  257. break;
  258. default:
  259. if this->isAlphaNum(this->a) {
  260. this->action(self::ACTION_KEEP_A);
  261. } else {
  262. this->action(self::ACTION_DELETE_A_B);
  263. }
  264. }
  265. break;
  266. default:
  267. this->action(self::ACTION_KEEP_A);
  268. break;
  269. }
  270. }
  271. }
  272.  
  273. return this->output;
  274. }
  275.  
  276. /**
  277. * Get the next character, skipping over comments. peek() is used to see
  278. * if a "/" is followed by a "/" or "*".
  279. *
  280. * @throws Exception On unterminated comment.
  281. * @return string
  282. */
  283. protected function next()
  284. {
  285. var c;
  286.  
  287. let c = this->get();
  288.  
  289. if c === "/" {
  290. switch this->peek() {
  291. case "/":
  292. while 1 {
  293. let c = this->get();
  294. if ord((string) c) <= self::ORD_LF {
  295. return c;
  296. }
  297. }
  298. case "*":
  299. this->get();
  300.  
  301. while 1 {
  302. switch this->get() {
  303. case "*":
  304. if this->peek() === "/" {
  305. this->get();
  306. return " ";
  307. }
  308. break;
  309. case null:
  310. throw new Exception("Unterminated comment.");
  311. }
  312. }
  313. default:
  314. return c;
  315. }
  316. }
  317. return c;
  318. }
  319.  
  320. /**
  321. * Get next char. If is ctrl character, translate to a space or newline.
  322. *
  323. * @return string|null
  324. */
  325. protected function peek()
  326. {
  327. let this->lookAhead = this->get();
  328. return this->lookAhead;
  329. }
  330. }