-
- namespace Ice\Filter;
-
- use Ice\Exception;
-
- /**
- * Minify js string.
- *
- * @package Ice/Filter
- * @category Minification
- * @author Ice Team
- * @copyright (c) 2014-2025 Ice Team
- * @license http://iceframework.org/license
- * @uses jsmin.c www.crockford.com
- */
- class Js
- {
- const ORD_LF = 10;
- const ORD_SPACE = 32;
- const ACTION_KEEP_A = 1;
- const ACTION_DELETE_A = 2;
- const ACTION_DELETE_A_B = 3;
- protected a = "";
- protected b = "";
- protected input = "";
- protected inputIndex = 0;
- protected inputLength = 0;
- protected lookAhead = null;
- protected output = "" { get };
-
- /**
- * Minify the js.
- *
- * @param string js JS code to minify
- * @return string
- */
- public function sanitize(string js)
- {
- let this->a = "",
- this->b = "",
- this->input = str_replace("\r\n", "\n", js),
- this->inputLength = strlen(this->input),
- this->inputIndex = 0,
- this->lookAhead = null,
- this->output = "";
-
- return this->min();
- }
-
- /**
- * Action -- do something! What to do is determined by the $command argument.
- *
- * action treats a string as a single character. Wow!
- * action recognizes a regular expression if it is preceded by ( or , or =.
- *
- * @throws Exception If parser errors are found:
- * - Unterminated string literal
- * - Unterminated regular expression set in regex literal
- * - Unterminated regular expression literal
- *
- * @param int $command One of class constants:
- * ACTION_KEEP_A Output A. Copy B to A. Get the next B.
- * ACTION_DELETE_A Copy B to A. Get the next B. (Delete A).
- * ACTION_DELETE_A_B Get the next B. (Delete B).
- */
- protected function action(int command)
- {
- //switch command {
- //case self::ACTION_KEEP_A: //1
- if command == self::ACTION_KEEP_A {
- let this->output = this->output . this->a;
- }
-
- //case self::ACTION_DELETE_A: //1, 2
- if command == self::ACTION_KEEP_A || command == self::ACTION_DELETE_A {
- let this->a = this->b;
-
- if this->a === "'" || this->a === "\"" {
- while 1 {
- let this->output = this->output . this->a,
- this->a = this->get();
-
- if this->a === this->b {
- break;
- }
-
- if ord(this->a) <= self::ORD_LF {
- throw new Exception("Unterminated string literal.");
- }
-
- if this->a === "\\" {
- let this->output = this->output . this->a,
- this->a = this->get();
- }
- }
- }
- }
-
- //case self::ACTION_DELETE_A_B: //1, 2, 3
- if command == self::ACTION_KEEP_A || command == self::ACTION_DELETE_A || command == self::ACTION_DELETE_A_B {
- let this->b = this->next();
-
- if this->b === "/" && (
- this->a === "(" || this->a === "," || this->a === "=" ||
- this->a === ":" || this->a === "[" || this->a === "!" ||
- this->a === "&" || this->a === "|" || this->a === "?" ||
- this->a === "{" || this->a === "}" || this->a === ";" ||
- this->a === "\n" ) {
- let this->output = this->output . this->a . this->b;
-
- while 1 {
- let this->a = this->get();
- if this->a === "[" {
- /*
- inside a regex [...] set, which MAY contain a "/" itself. Example: mootools Form.Validator near line 460:
- 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"));
- */
- while 1 {
- let this->output = this->output . this->a,
- this->a = this->get();
-
- if this->a === "]" {
- break;
- } elseif this->a === "\\" {
- let this->output = this->output . this->a,
- this->a = this->get();
- } elseif ord(this->a) <= self::ORD_LF {
- throw new Exception("Unterminated regular expression set in regex literal.");
- }
- }
- } elseif this->a === "/" {
- break;
- } elseif this->a === "\\" {
- let this->output = this->output . this->a,
- this->a = this->get();
- } elseif ord(this->a) <= self::ORD_LF {
- throw new Exception("Unterminated regular expression literal.");
- }
-
- let this->output = this->output . this->a;
- }
-
- let this->b = this->next();
- }
- }
- //}
- }
-
- /**
- * Get next char. Convert ctrl char to space.
- *
- * @return string|null
- */
- protected function get()
- {
- var c, i;
-
- let c = this->lookAhead,
- this->lookAhead = null;
-
- if c === null {
- if this->inputIndex < this->inputLength {
- let c = substr(this->input, this->inputIndex, 1),
- i = this->inputIndex,
- this->inputIndex = i + 1;
- } else {
- let c = null;
- }
- }
-
- if c === "\r" {
- return "\n";
- }
-
- if c === null || c === "\n" || ord(c) >= self::ORD_SPACE {
- return c;
- }
-
- return " ";
- }
-
- /**
- * Is $c a letter, digit, underscore, dollar sign, or non-ASCII character.
- *
- * @return bool
- */
- protected function isAlphaNum(c)
- {
- return ord(c) > 126 || c === "\\" || preg_match("/^[\\w\\$]$/", c) === 1;
- }
-
- /**
- * Perform minification, return result
- *
- * @return string
- */
- protected function min()
- {
- if 0 == strncmp(this->peek(), "\xef", 1) {
- this->get();
- this->get();
- this->get();
- }
-
- let this->a = "\n";
- this->action(self::ACTION_DELETE_A_B);
-
- while this->a !== null {
- switch this->a {
- case " ":
- if this->isAlphaNum(this->b) {
- this->action(self::ACTION_KEEP_A);
- } else {
- this->action(self::ACTION_DELETE_A);
- }
- break;
- case "\n":
- switch this->b {
- case "{":
- case "[":
- case "(":
- case "+":
- case "-":
- case "!":
- case "~":
- this->action(self::ACTION_KEEP_A);
- break;
- case " ":
- this->action(self::ACTION_DELETE_A_B);
- break;
- default:
- if this->isAlphaNum(this->b) {
- this->action(self::ACTION_KEEP_A);
- } else {
- this->action(self::ACTION_DELETE_A);
- }
- }
- break;
- default:
- switch this->b {
- case " ":
- if this->isAlphaNum(this->a) {
- this->action(self::ACTION_KEEP_A);
- break;
- }
- this->action(self::ACTION_DELETE_A_B);
- break;
- case "\n":
- switch this->a {
- case "}":
- case "]":
- case ")":
- case "+":
- case "-":
- case "\"":
- case "'":
- this->action(self::ACTION_KEEP_A);
- break;
- default:
- if this->isAlphaNum(this->a) {
- this->action(self::ACTION_KEEP_A);
- } else {
- this->action(self::ACTION_DELETE_A_B);
- }
- }
- break;
- default:
- this->action(self::ACTION_KEEP_A);
- break;
- }
- }
- }
-
- return this->output;
- }
-
- /**
- * Get the next character, skipping over comments. peek() is used to see
- * if a "/" is followed by a "/" or "*".
- *
- * @throws Exception On unterminated comment.
- * @return string
- */
- protected function next()
- {
- var c;
-
- let c = this->get();
-
- if c === "/" {
- switch this->peek() {
- case "/":
- while 1 {
- let c = this->get();
- if ord((string) c) <= self::ORD_LF {
- return c;
- }
- }
- case "*":
- this->get();
-
- while 1 {
- switch this->get() {
- case "*":
- if this->peek() === "/" {
- this->get();
- return " ";
- }
- break;
- case null:
- throw new Exception("Unterminated comment.");
- }
- }
- default:
- return c;
- }
- }
- return c;
- }
-
- /**
- * Get next char. If is ctrl character, translate to a space or newline.
- *
- * @return string|null
- */
- protected function peek()
- {
- let this->lookAhead = this->get();
- return this->lookAhead;
- }
- }