root/combine.php

User picture

Author: spadgos

Revision: 2 («Previous)


File Size: 10.8 KB

(May 22, 2009 06:59 UTC) About 3 years ago

Initial import.

 
Show/hide line numbers
<?php // $Id$
/**
 *	Javascript Combiner, by Nick Fisher. http://www.spadgos.com/
 *	Uses Nicolas Martin's port of Dean Edward's Javascript Packer
 *	http://dean.edwards.name/download/#packer
 *
 *	Command line tool to combine all the javascript files in a directory into
 *	one file which is then packed to reduce download size and HTTP requests.
 *
 *	Usage:
 *		php c:\path\to\combine.php c:\path\to\JSFolder\
 *	or:
 *		php /path/to/combine.php /path/to/JSFolder/
 *
 *	Relative paths are OK, too.
 *
 *	If there is a file in the given folder with the name "packer.ini", this file
 *	will be read and used to create custom packages. By default, this file is
 *	required. To just pack all the files in the folder using default settings,
 *	then use an extra parameter in the call to this file:
 *		php c:\path\to\combine.php c:\path\to\JSFolder\ 1
 *
 *	If this extra flag is left out, or set to a falsey value (eg: 0), then the
 *	combiner will require a packer.ini file.
 *
 *	Format of the packer.ini file:
 *	[outputFile.js]
 *	input_one.js
 *	input_two.js
 *	input_three.js
 *
 *	[outputFileTwo.js]
 *	input_four.js
 *	input_five.js
 *
 *	Files not mentioned in the packer.ini file won't be packed. If no [headings]
 *	are given (ie: packer.ini is just a list of input filenames), they will all
 *	be packed into "combined.pack.js"
 *
 *	Files from multiple folders can be combined this way. Just specify the path
 *	as relative to the .ini file. eg:
 *	[myFile.js]
 *	src/myFile.js
 *	[packed/myFile.js]		; the output can be in a different folder.
 *	src/myFile.js
 *
 *	Additional options can be specified per output file. Options are written as:
 *	variable = value
 *	With one variable per line, and whitespace on either side of the = being ignored.
 *	A value of "no", "false", "null" or "off" will be converted to false if the
 *	option is a flag (ie: a boolean).
 *
 *	Option name		Type	Default		Description
 *	----------------------------------------------------------------------------
 *	include_list	flag	false		Include a list of the files which were
 *										combined to make this output file.
 *	include_versions flag	true		If include_list is on, this will scan
 *										the first few lines of each input file
 *										for an SVN Id keyword and insert the
 *										revision number after the filename in
 *										the include_list. eg: "myFile.js (r133)"
 *	compress		value	95			0,10,62,95 or 'no', 'None', 'Numeric',
 *										'Normal', 'High ASCII'. This value will
 *										be passed through to the JSPacker class.
 *										See that for more information.
 *	firebug_check	flag	false		Searches JS files for a reference to the
 *										Firebug 'console' object. A warning will
 *										be printed if it is found. This would be
 *										used to make sure you haven't left it in
 *										before deployment to the general public.
 *										How cool are the same size line lengths?
 *
 *	If a file has the string /*nopack* / (but without the space between the last
 *	two chars which was added so this comment wouldn't be broken) then that file
 *	is merely appended to the other, packed files at the end. This is used if a
 *	particular file is already packed and causing the packer to error (usually a
 *	"Array to string" error). Note that nopack files are placed AFTER packed the
 *	packed files - which could lead to strange errors if you were relying on the
 *	order of the included files. Alternatively, change your compression level
 *	down to 62 or even 10.
 *	The best option in these cases though is to not pack any source files in the
 *	first place, and only use the full, non-minified versions.
 */
require 'class.JavaScriptPacker.php';
define("NL", "\r\n");

if ($argc >= 2) {
	$folder = $argv[1];
	if (!is_dir($folder)) die ("You must specify a folder name.");
	if (substr($folder, -1) != "\\" && substr($folder, -1) != "/" ) $folder .= "/";
	$optionalINI = !empty($argv[2]);	// if this is false, then packer.ini is required.
} else {
	echo 'You must specify a folder name.' . "\n";
	die();
}

if (function_exists('mb_detect_encoding')) {
	function is_utf8($str) {
		return mb_detect_encoding($str,"UTF-8, ISO-8859-1, GBK") !== "UTF-8";
	}
} else {
	function is_utf8($str) {
		$c=0; $b=0;
		$bits=0;
		$len=strlen($str);
		for($i=0; $i<$len; $i++){
			$c=ord($str[$i]);
			if($c > 128){
				if(($c >= 254)) return false;
				elseif($c >= 252) $bits=6;
				elseif($c >= 248) $bits=5;
				elseif($c >= 240) $bits=4;
				elseif($c >= 224) $bits=3;
				elseif($c >= 192) $bits=2;
				else return false;
				if(($i+$bits) > $len) return false;
				while($bits > 1){
					$i++;
					$b=ord($str[$i]);
					if($b < 128 || $b > 191) return false;
					$bits--;
				}
			}
		}
		return true;
	}
}



$files = array();
$options = array();
$currFile = "combined.pack.js";
if (file_exists($folder . 'packer.ini')) {
	$lines = file($folder . "packer.ini");
	foreach ($lines as $line) {
		// strip leading/trailing whitespace and everything after a semicolon (the comment character)
		$line = preg_replace("/;.*/", "", $line);
		$line = trim(preg_replace("/^\\s*([^;\\s]*)\\s*(;.*)?$/", "\\1", $line));
		if (!$line) continue;
		if (preg_match("/^\\[([^\\]]+)\\]\$/", $line, $matches)) {
			// headings are defined [like this]
			$currFile = $matches[1];
			$files[$currFile] = array();
			$options[$currFile] = array();
		} else if (preg_match("/^(.*?)=(.*)$/", $line, $matches)) {
			// options are defined like=this
			$var = trim($matches[1]);
			$val = trim($matches[2]);
			$options[$currFile][$var] = $val;
		} else if (strpos($line, "*") !== false || strpos($line, "?") !== false) {
			// a wildcard has been specified
			$matches = glob($line);
			foreach ($matches as $m) {
				if (is_file($m)) {
					$files[$currFile][] = $m;
				}
			}
		} else {
			$files[$currFile][] = $line;
		}
	}
} else if (!$optionalINI) {
	echo "Can't find packer.ini at " . $folder . "packer.ini";
	die();
} else {
	$temp = glob($folder . "*.js");
	$files[$currFile] = $options[$currFile] = array();
	foreach ($temp as $src) {
		if ((strpos($src, ".pack.js") === false) && (substr($src, -3) == ".js")) {
			$files[$currFile][] = $src;
		}
	}
}

if (count($files, COUNT_RECURSIVE) == 0) die("No files found.");

$t1 = microtime(true);


foreach ($files as $outputFile => $inputFiles) {
	echo "Output file: $outputFile\n";

	$myOptions = $options[$outputFile];

	$inputFiles = array_unique($inputFiles);

	$compression = getOptionValue($myOptions, 'compress', 95);
	echo "Compression: " . $compression . "\n";

	$firebugCheck = getOptionFlag($myOptions, 'firebug_check', false);
	$combined = ''; // scripts which are going to be packed.
	$nopack = '';	// scripts which are not to be packed. they get appended **after** the other scripts are packed.
	foreach ($inputFiles as $src) {
		if (!file_exists($src)) {
			echo "Can't find file: $src\n";
		} else {
			if (preg_match("/\\.css\\.php$/i", $src)) {
				// we have a CSS+PHP file.
				$script = parseCSS($src);
			} else {
				$script = file_get_contents($src);
			}
			//$script = convToUtf8($script);
			$addToVar = 'combined';

			echo "Adding " . basename($src);

			if (strpos($script, "/"."*nopack*"."/") !== false) {
				echo " *nopack*";
				$addToVar = 'nopack';
			}

			echo " (" . strlen($script) . " bytes)\n";
			if ($firebugCheck) {
				if (($pos = strpos($script, 'console.')) !== false) {
					echo "WARNING: reference to console found:\n"
						. substr($script, $pos - 20, $pos + 40)
						. "\n\n";
				}
			}
			$$addToVar .= NL . $script;
		}
	}
	$orig = strlen($combined) + strlen($nopack);
	if ($orig == 0) {
		echo "No content.\n";
		continue;
	}

	if ($compression != "0" && !asBool($compression)) {

		$packed = $combined . $nopack;

	} else if (preg_match("/\\.css$/i", $outputFile)) {
		// output file is CSS. we obviously don't want to do the JSPacker.
		$packed = compressCSS($combined) . $nopack;
	} else {
		echo "Compressing at rate: " . $compression;
		$packer = new JavaScriptPacker($combined, $compression, true, false);
		$packed = $packer->pack() . $nopack;
		$packed = str_replace(array("\r\n", "\r", "\n"), NL, $packed);
	}
	if (getOptionFlag($myOptions, 'include_list', false)) {
		$fileList = $inputFiles;
		if (getOptionFlag($myOptions, 'include_versions', false)) {
			$temp = $fileList;
			foreach ($temp as $i => $filename) {
				$version = getSVNVersion($filename);
				if ($version) {
					$temp[$i] = $temp[$i] . " (r$version)";
				}
			}
			$fileList = $temp;
		}
		$packed = "/* Included files:" . NL . implode(NL, $fileList) . NL ."*/" . NL . $packed;
	}

	file_put_contents($outputFile, $packed);

	$packedSize = strlen($packed);

	echo "\n" . basename($outputFile) . ': ' . $orig . " bytes => " . $packedSize . " bytes (" . number_format(100 - ($packedSize / $orig) * 100, 1) . "% saving)\n\n";
}

$t2 = microtime(true);
$time = sprintf('%.4f', ($t2 - $t1));
$numFiles = (count($files, COUNT_RECURSIVE) - count($files));
echo "==========\nCompleted in {$time}s. Packed " . $numFiles . " file" . ($numFiles == 1 ? "" : "s");


//////////////////////////////
//////////////////////////////
//////////////////////////////
function asBool($val) {
	$val = strtolower($val);
	if (in_array($val, array("no", "false", "off", "null"))) return false;
	else return !!$val;
}


function getOptionValue($options, $var, $default = false) {
	if (isset($options[$var])) {
		return $options[$var];
	} else {
		return $default;
	}
}
function getOptionFlag($options, $var, $default = false) {
	return asBool(getOptionValue($options, $var, $default));
}

// search the first 10 lines of a
function getSVNVersion($file) {
	if ($fh = @fopen($file, 'r')) {
		for ($n = 0; !feof($fh) && $n < 10; ++$n) {
			$line = fgets($fh);
			if (preg_match("@\\\$".'I'.'d:' . "\\s\\S+\\s(\\d+)\\s\\S+\\s\\S+\\s\\S+\\s\\\$@", $line, $matches)) {
				fclose($fh);
				return intval($matches[1]);
			}
		}
		fclose($fh);
	} else {
		echo "Can't open file $file.\n";
	}

	return false;
}

function compressCSS($css) {
	return
		preg_replace(
			array('@\s\s+@','@(\w+:)\s*([\w\s,#]+;?)@'),
			array(' ','$1$2'),
			str_replace(
				array("\r","\n","\t",' {','} ',';}'),
				array('','','','{','}','}'),
				preg_replace('@/\*[^*]*\*+([^/][^*]*\*+)*/@', '', $css)
			)
		)
	;
}

// converts all the $variablenames strewn throughout the .css.php file to their equivalent
// this saves you having to type <?php echo $myColour; ? > in your css.
function parseCSS($filename) {
	ob_start();
	include($filename);
	$newVars = get_defined_vars();
	$file = compressCSS(ob_get_clean());
	return eval('return "' . addslashes($file) . '";');
}
function convToUtf8($str) {
	if(!is_utf8($str)) {
		return iconv("gbk","utf-8",$str);
	} else {
		return $str;
	}
}