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;
}
}