includes/js-compressor.php
author Dan
Wed, 26 Mar 2008 02:56:23 -0400
changeset 509 175df10e0b56
parent 507 586fd7d3202d
child 536 218a627eb53e
permissions -rw-r--r--
Added a copy of Firebug Lite for debugging purposes. License is uncertain but being treated as MPL. (If is is not MPL then it is under something more permissive that permits relicensing anyway)

<?php

/*
 * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
 * Version 1.1.3 (Caoineag alpha 3)
 * Copyright (C) 2006-2007 Dan Fuhry
 * Javascript compression library - used to compact the client-side Javascript code (all 72KB of it!) to save some bandwidth
 *
 * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
 *
 * This class was written by Andrea Giammarchi and was downloaded from PHPClasses.org. The information page stated that
 * this class is licensed under the GNU General Public License, the terms of which can be found by reading the file
 * "GPL" included with the Enano package.
 */

/**
 * JavaScriptCompressor class,
 *	removes comments or pack JavaScript source[s] code.
 * ______________________________________________________________
 * JavaScriptCompressor (just 2 public methods)
 *    |
 *    |________ getClean(jsSource:mixed):string
 *    |         	returns one or more JavaScript code without comments,
 *    |         	by default removes some spaces too
 *    |
 *    |________ getPacked(jsSource:mixed):string
 *              	returns one or more JavaScript code packed,
 *	        	using getClean and obfuscating output
 * --------------------------------------------------------------
 * Note about $jsSource input varible:
 * 	this var should be a string (i.e. $jsSource = file_get_contents("myFile.js");)
 *      should be an array of strings (i.e. array(file_get_contents("1.js"), file_get_contents("2.js"), ... ))
 *      should be an array with 1 or 2 keys:
 *      	(i.e. array('code'=>file_get_contents("mySource.js")))
 *              (i.e. array('code'=>file_get_contents("mySource.js"), 'name'=>'mySource'))
 *      ... and should be an array of arrays created with theese rules
 *      array(
 *		file_get_contents("secret.js"),
 *              array('code'=>$anotherJS),
 *              array('code'=>$myJSapplication, 'name'=>'JSApplication V 1.0')
 *      )
 *
 *      The name used on dedicated key, will be write on parsed source header
 * --------------------------------------------------------------
 * Note about returned strings:
 * 	Your browser should wrap very long strings, then don't use
 *      cut and paste from your browser, save output into your database or directly
 *      in a file or print them only inside <script> and </script> tags
 * --------------------------------------------------------------
 * Note about parser performance:
 * 	With pure PHP embed code this class should be slow and not really safe
 *      for your server performance then don't parse JavaScript runtime for each
 *      file you need and create some "parsed" caching system
 *      (at least while i've not created a compiled version of theese class functions).
 *      Here there's a caching system example: http://www.phpclasses.org/browse/package/3158.html
 * --------------------------------------------------------------
 * Note about JavaScript packed compatibility:
 * 	To be sure about compatibility include before every script JSL Library:
 *      http://www.devpro.it/JSL/
 * JSL library add some features for old or buggy browsers, one of
 * those functions is String.replace with function as second argument,
 * used by JavaScript generated packed code to rebuild original code.
 *
 * Remember that KDE 3.5, Safari and IE5 will not work correctly with packed version
 * if you'll not include JSL.
 * --------------------------------------------------------------
 * @Compatibility	>= PHP 4
 * @Author		Andrea Giammarchi
 * @see       http://www.devpro.it/
 * @since	    2006/05/31
 * @since		  2006/08/01 [requires SourceMap.class.php to parse source faster and better (dojo.js.uncompressed.js file (211Kb) successfull cleaned or packed)]
 * @version		0.8
 * Dependencies:
 *      Server: BaseConvert.class.php
 *			Server: SourceMap.class.php
 *			Client: JSL.js (http://www.devpro.it/JSL/)
 * Convertion is supported by every browser with JSL Library (FF 1+ Opera 8+ and IE5.5+ are supported without JSL too)
 * @copyright Dean Edwards for his originally idea [dean.edwards.name] and his JavaScript packer
 */
class JavaScriptCompressor {

	/**
	 * public variables
         * 	stats:string		after every compression has some informations
         *      version:string		version of this class
	 */
	var	$stats = '',
		$version = '0.8';

	/** 'private' variables, any comment sorry */
	var	$__startTime = 0,
		$__sourceLength = 0,
		$__sourceNewLength = 0,
		$__totalSources = 0,
		$__sources = array(),
		$__delimeter = array(),
		$__cleanFinder = array("/(\n|\r)+/", "/( |\t)+/", "/(\n )|( \n)|( \n )/", "/[[:space:]]+(\)|})/", "/(\(|{)[[:space:]]+/", "/[[:space:]]*(;|,|:|<|>|\&|\||\=|\?|\+|\-|\%)[[:space:]]*/", "/\)[[:space:]]+{/", "/}[[:space:]]+\(/"),
		$__cleanReplacer = array("\n", " ", "\n", "\\1", "\\1", "\\1", "){", "}("),
		$__BC = null,
		$__SourceMap = null;

	/**
	 * public constructor
         * 	creates a new BaseConvert class variable (base 36)
	 */
	function __construct() {
		$this->__SourceMap = new SourceMap();
		$this->__BC = new BaseConvert('0123456789abcdefghijklmnopqrstuvwxyz');
		$this->__delimeter = array(
			array('name'=>'doublequote', 'start'=>'"', 'end'=>'"', 'noslash'=>true),
			array('name'=>'singlequote', 'start'=>"'", 'end'=>"'", 'noslash'=>true),
			array('name'=>'singlelinecomment', 'start'=>'//', 'end'=>array("\n", "\r")),
			array('name'=>'multilinecomment', 'start'=>'/*', 'end'=>'*/'),
			array('name'=>'regexp', 'start'=>'/', 'end'=>'/', 'match'=>"/^\/[^\n\r]+\/$/", 'noslash'=>true)
		);
	}

	/**
	 * public method
         * 	getClean(mixed [, bool]):string
         *      compress JavaScript removing comments and somespaces (on by default)
         * @param	mixed		view example and notes on class comments
	 */
	function getClean($jsSource) {
		return $this->__commonInitMethods($jsSource, false);
	}
	
	/**
	 * public method
         * 	getPacked(mixed):string
         *      compress JavaScript replaceing words and removing comments and some spaces
         * @param	mixed		view example and notes on class comments
	 */
	function getPacked($jsSource) {
		return $this->__commonInitMethods($jsSource, true);
	}
	
	/** 'private' methods, any comment sorry */
	function __addCleanCode($str) {
		return preg_replace($this->__cleanFinder, $this->__cleanReplacer, trim($str));
	}
	function __addClean(&$arr, &$str, &$start, &$end, $clean) {
		if($clean)
			array_push($arr, $this->__addCleanCode(substr($str, $start, $end - $start)));
		else
			array_push($arr, substr($str, $start, $end - $start));
	}
	function __clean(&$str) {
		$len = strlen($str);
		$type = '';
		$clean = array();
		$map = $this->__SourceMap->getMap($str, $this->__delimeter);
		for($a = 0, $b = 0, $c = count($map); $a < $c; $a++) {
			$type = &$map[$a]['name'];
			switch($type) {
				case 'code':
				case 'regexp':
				case 'doublequote':
				case 'singlequote':
					$this->__addClean($clean, $str, $map[$a]['start'], $map[$a]['end'], ($type === 'code'));
					if($type !== 'regexp')
						array_push($clean, "\n");	
					break;
			}
 		}
		return preg_replace("/(\n)+/", "\n", trim(implode('', $clean)));
	}
	function __commonInitMethods(&$jsSource, $packed) { 
		$header = '';
		$this->__startTime = $this->__getTime();
		$this->__sourceLength = 0;
		$this->__sourceManager($jsSource);
		for($a = 0, $b = $this->__totalSources; $a < $b; $a++)
			$this->__sources[$a]['code'] = $this->__clean($this->__sources[$a]['code']);
		$header = $this->__getHeader();
		for($a = 0, $b = $this->__totalSources; $a < $b; $a++)
			$this->__sources[$a] = &$this->__sources[$a]['code'];
		$this->__sources = implode(';', $this->__sources);
		if($packed)
			$this->__sources = $this->__pack($this->__sources);
		$this->__sourceNewLength = strlen($this->__sources);
		$this->__setStats();
		return $header.$this->__sources;
	}
	function __getHeader() {
		return implode('', array(
			'/* ',$this->__getScriptNames(),'JavaScriptCompressor ',$this->version,' [www.devpro.it], ',
			'thanks to Dean Edwards for idea [dean.edwards.name]',
			" */\r\n"		
		));
	}
	function __getScriptNames() {
		$a = 0;
		$result = array();
		for($b = $this->__totalSources; $a < $b; $a++) {
			if($this->__sources[$a]['name'] !== '')
				array_push($result, $this->__sources[$a]['name']);
		}
		$a = count($result);
		if($a-- > 0)
			$result[$a] .= ' with ';
		return $a < 0 ? '' : implode(', ', $result);
	}
	function __getSize($size, $dec = 2) {
		$toEval = '';
		$type = array('bytes', 'Kb', 'Mb', 'Gb');
		$nsize = $size;
		$times = 0;
		while($nsize > 1024) {
			$nsize = $nsize / 1024;
			$toEval .= '/1024';
			$times++;
		}
		if($times === 0)
			$fSize = $size.' '.$type[$times];
		else {
			eval('$size=($size'.$toEval.');');
			$fSize =  number_format($size, $dec, '.', '').' '.$type[$times];
		}
		return $fSize;
	}
	function __getTime($startTime = null) {
		list($usec, $sec) = explode(' ', microtime());
		$newtime = (float)$usec + (float)$sec;
		if($startTime !== null)
			$newtime = number_format(($newtime - $startTime), 3);
		return $newtime;
	}
	function __pack(&$str) {
		$container = array();
		$str = preg_replace("/(\w+)/e", '$this->__BC->toBase($this->__wordsParser("\\1",$container));', $this->__clean($str));
		$str = str_replace("\n", '\n', addslashes($str));
		return 'eval(function(A,G){return A.replace(/(\\w+)/g,function(a,b){return G[parseInt(b,36)]})}("'.$str.'","'.implode(',', $container).'".split(",")));';
	}
	function __setStats() {
		$this->stats = implode(' ', array(
			$this->__getSize($this->__sourceLength),
			'to',
			$this->__getSize($this->__sourceNewLength),
			'in',
			$this->__getTime($this->__startTime),
			'seconds'
		));
	}
	function __sourceManager(&$jsSource) {
		$b = count($jsSource);
		$this->__sources = array();
		if(is_string($jsSource))
			$this->__sourcePusher($jsSource, '');
		elseif(is_array($jsSource) && $b > 0) {
			if(isset($jsSource['code']))
				$this->__sourcePusher($jsSource['code'], (isset($jsSource['name']) ? $jsSource['name'] : ''));
			else {
				for($a = 0; $a < $b; $a++) {
					if(is_array($jsSource[$a]) && isset($jsSource[$a]['code'], $jsSource[$a]['name']))
						$this->__sourcePusher($jsSource[$a]['code'], trim($jsSource[$a]['name']));
					elseif(is_string($jsSource[$a]))
						$this->__sourcePusher($jsSource[$a], '');
				}
			}
		}
		$this->__totalSources = count($this->__sources);
	}
	function __sourcePusher(&$code, $name) {
		$this->__sourceLength += strlen($code);
		array_push($this->__sources, array('code'=>$code, 'name'=>$name));
	}
	function __wordsParser($str, &$d) {
		if(is_null($key = array_shift($key = array_keys($d,$str))))
			$key = array_push($d, $str) - 1;
		return $key;
	}
}

/**
 * BaseConvert class,
 *	converts an unsigned base 10 integer to a different base and vice versa.
 * ______________________________________________________________
 * BaseConvert
 *    |
 *    |________ constructor(newBase:string)
 *    |         	uses newBase string var for convertion
 *    |                 [i.e. "0123456789abcdef" for an hex convertion]
 *    |
 *    |________ toBase(unsignedInteger:uint):string
 *    |         	return base value of input
 *    |
 *    |________ fromBase(baseString:string):uint
 *              	return base 10 integer value of base input
 * --------------------------------------------------------------
 * REMEMBER: PHP < 6 doesn't work correctly with integer greater than 2147483647 (2^31 - 1)
 * --------------------------------------------------------------
 * @Compatibility	>= PHP 4
 * @Author		Andrea Giammarchi
 * @Site		http://www.devpro.it/
 * @Date		2006/06/05
 * @Version		1.0
 */

class BaseConvert {
	
	var	$base, $baseLength;
	
	function BaseConvert($base) {
		$this->base = &$base;
		$this->baseLength = strlen($base);
	}
	
	function toBase($num) {
		$module = 0; $result = '';
		while($num) {
			$result = $this->base{($module = $num % $this->baseLength)}.$result;
			$num = (int)(($num - $module) / $this->baseLength);
		}
		return $result !== '' ? $result : $this->base{0};
	}
	
	function fromBase($str) {
		$pos = 0; $len = strlen($str) - 1; $result = 0;
		while($pos < $len)
			$result += pow($this->baseLength, ($len - $pos)) * strpos($this->base, $str{($pos++)});
		return $len >= 0 ? $result + strpos($this->base, $str{($pos)}) : null;
	}
}

/**
* SourceMap class,
*    reads a generic language source code and returns its map.
* ______________________________________________________________
* The SourceMap goals is to create a map of a generic script/program language.
* The getMap method returns an array/list of arrays/dictionary/objects
* of source map using delimeters variable to map correctly:
*  - multi line comments
*  - single line comments
*  - double quoted strings
*  - single quoted strings
*  - pure code
*  - everything else (for example regexp [/re/] with javascript), just adding a correct delimeter
* --------------------------------------------------------------
* What about the delimeter
*     It's an array/list of arrays/dictionary/obects with some properties to find what you're looking for.
*
* parameters are:
*  - name, the name of the delimeter (i.e. "doublequote")
*  - start, one or mode chars to find as start delimeter (i.e. " for double quoted string)
*  - end, one or mode chars to find as end delimeter (i.e. " for double quoted string) [end should be an array/list too]
*
* optional parameters are:
*  - noslash, if true find the end of the delimeter only if last char is not slashed (i.e. "string\"test" find " after test)
*  - match, if choosed language has regexp, verify if string from start to end matches used regexp (i.e. /^\/[^\n\r]+\/$/ for JavaScript regexp)
*
* If end parameter is an array, match and noslash are not supported (i.e. ["\n", "\r"] for end delimeter of a single line comment)
* --------------------------------------------------------------
* What about SourceMap usage
*     It should be a good solution to create sintax highlighter, parser,
*     verifier or some other source code parsing procedure
* --------------------------------------------------------------
* What about SourceMap performance script/languages
*     I've created different version of this class to test each script/program language performance too.
* Python with or without Psyco is actually the faster parser.
* However with this PHP version this class has mapped "dojo.js.uncompressed.js" file (about 211Kb) in less than 0.5 second.
* Test has been done with embed class and PHP as module, any accelerator was used for this PHP test.
* --------------------------------------------------------------
* @Compatibility    >= PHP 4
* @Author        Andrea Giammarchi
* @Site        http://www.devpro.it/
* @Date        2006/08/01
* @LastMOd        2006/08/01
* @Version        0.1
* @Application        Last version of JavaScriptCompressor class use this one to map source code.
*/
class SourceMap {
    
    /**
     * public method
         *     getMap(&$source:string, &$delimeters:array):array
     * Maps the source code using $delimeters rules and returns map as an array
         * NOTE: read comments to know more about map and delimeter
         *
         * @param    string        generic source code
         * @param    array        array with nested array with code rules
     */
    function getMap(&$source, &$delimeters) {
        
        # "unsigned" integer variables
        $sourcePosition = 0;
        $delimetersPosition = 0;
        $findLength = 0;
        $len = 0;
        $tempIndex = 0;
        $sourceLength = strlen($source);
        $delimetersLength = count($delimeters);
        
        # integer variables
        $tempPosition = -1;
        $endPosition = -1;
        
        # array variables
        $map = array();
        $tempMap = array();
        $tempDelimeter = array();
        
        while($sourcePosition < $sourceLength) {
            $endPosition = -1;
            for($delimetersPosition = 0; $delimetersPosition < $delimetersLength; $delimetersPosition++) {
                $tempPosition = strpos($source, $delimeters[$delimetersPosition]['start'], $sourcePosition);
                if($tempPosition !== false && ($tempPosition < $endPosition || $endPosition === -1)) {
                    $endPosition = $tempPosition;
                    $tempIndex = $delimetersPosition;
                }
            }
            if($endPosition !== -1) {
                $sourcePosition = $endPosition;
                $tempDelimeter = &$delimeters[$tempIndex];
                $findLength = strlen($tempDelimeter['start']);
                if(is_array($tempDelimeter['end'])) {
                    $delimetersPosition = 0;
                    $endPosition = -1;
                    for($len = count($tempDelimeter['end']); $delimetersPosition < $len; $delimetersPosition++) {
                        $tempPosition = strpos($source, $tempDelimeter['end'][$delimetersPosition], $sourcePosition + $findLength);
                        if($tempPosition !== false && ($tempPosition < $endPosition || $endPosition === -1)) {
                            $endPosition = $tempPosition;
                            $tempIndex = $delimetersPosition;
                        }    
                    }
                    if($endPosition !== -1)
                        $endPosition = $endPosition + strlen($tempDelimeter['end'][$tempIndex]);
                    else
                        $endPosition = $sourceLength;
                    array_push($map, array('name'=>$tempDelimeter['name'], 'start'=>$sourcePosition, 'end'=>$endPosition));
                    $sourcePosition = $endPosition - 1;
                }
                elseif(isset($tempDelimeter['match'])) {
                    $tempPosition = strpos($source, $tempDelimeter['end'], $sourcePosition + $findLength);
                    $len = strlen($tempDelimeter['end']);
                    if($tempPosition !== false && preg_match($tempDelimeter['match'], substr($source, $sourcePosition, $tempPosition - $sourcePosition + $len))) {
                        $endPosition = isset($tempDelimeter['noslash']) ? $this->__endCharNoSlash($source, $sourcePosition, $tempDelimeter['end'], $sourceLength) : $tempPosition + $len;
                        array_push($map, array('name'=>$tempDelimeter['name'], 'start'=>$sourcePosition, 'end'=>$endPosition));
                        $sourcePosition = $endPosition - 1;
                    }
                }
                else {
                    if(isset($tempDelimeter['noslash']))
                        $endPosition = $this->__endCharNoSlash($source, $sourcePosition, $tempDelimeter['end'], $sourceLength);
                    else {
                        $tempPosition = strpos($source, $tempDelimeter['end'], $sourcePosition + $findLength);
                        if($tempPosition !== false)
                            $endPosition = $tempPosition + strlen($tempDelimeter['end']);
                        else
                            $endPosition = $sourceLength;
                    }
                    array_push($map, array('name'=>$tempDelimeter['name'], 'start'=>$sourcePosition, 'end'=>$endPosition));
                    $sourcePosition = $endPosition - 1;
                }
            }
            else
                $sourcePosition = $sourceLength - 1;
            ++$sourcePosition;
        }
        $len = count($map);
        if($len === 0)
            array_push($tempMap, array('name'=>'code', 'start'=>0, 'end'=>$sourceLength));
        else {
            for($tempIndex = 0; $tempIndex < $len; $tempIndex++) {
                if($tempIndex === 0 && $map[$tempIndex]['start'] > 0)
                    array_push($tempMap, array('name'=>'code', 'start'=>0, 'end'=>$map[$tempIndex]['start']));
                elseif($tempIndex > 0 && $map[$tempIndex]['start'] > $map[$tempIndex-1]['end'])
                    array_push($tempMap, array('name'=>'code', 'start'=>$map[$tempIndex-1]['end'], 'end'=>$map[$tempIndex]['start']));
                array_push($tempMap, array('name'=>$map[$tempIndex]['name'], 'start'=>$map[$tempIndex]['start'], 'end'=>$map[$tempIndex]['end']));
                if($tempIndex + 1 === $len && $map[$tempIndex]['end'] < $sourceLength)
                    array_push($tempMap, array('name'=>'code', 'start'=>$map[$tempIndex]['end'], 'end'=>$sourceLength));
            }
        }
        return $tempMap;
    }
    
    function __endCharNoSlash(&$source, $position, &$find, &$len) {
        $temp = strlen($find);
        do {
            $position = strpos($source, $find, $position + 1);
        }while($position !== false && !$this->__charNoSlash($source, $position));
        if($position === false) $position = $len - $temp;
        return $position + $temp;
    }
    
    function __charNoSlash(&$source, &$position) {
        $next = 1; $len = $position - $next;
        while($len > 0 && $source{$len} === '\\') $len = $position - (++$next);
        return (($next - 1) % 2 === 0);
    }
}

/**
 * Wrapper for the JavaScriptCompressor class.
 * @param string Javascript code to compact. If this doesn't contain any newline characters, will be treated as a filename.
 * @param bool If true, aggressively compresses the code. Otherwise, just strips comments and some whitespace.
 * @return string Compressed JS
 */

function perform_js_compress($text_or_file, $aggressive = false)
{
  static $compressor = false;
  
  if ( !is_object($compressor) )
    $compressor = new JavaScriptCompressor();
  
  if ( strpos($text_or_file, "\n") )
  {
    $text =& $text_or_file;
  }
  else if ( file_exists($text_or_file) )
  {
    $text = file_get_contents($text_or_file);
  }
  else
  {
    $text =& $text_or_file;
  }
  
  return ( $aggressive ) ? $compressor->getPacked($text) : $compressor->getClean($text);
}

?>