Forráskód Böngészése

Added URL and Cache plugins,

Taddeus Kroes 14 éve
szülő
commit
b2bb93a6e1
7 módosított fájl, 1246 hozzáadás és 0 törlés
  1. 196 0
      pquery.cache.php
  2. 81 0
      pquery.css.php
  3. 73 0
      pquery.js.php
  4. 88 0
      pquery.url.php
  5. 257 0
      utils/CssParser.php
  6. 333 0
      utils/jshrink.php
  7. 218 0
      utils/minify_html.php

+ 196 - 0
pquery.cache.php

@@ -0,0 +1,196 @@
+<?php
+/**
+ * pQuery plugin for parsing templates.
+ * 
+ * @package pQuery
+ */
+
+/**
+ * @todo Documentation
+ * @property string $files 
+ */
+class pQueryCache extends pQuery implements pQueryExtension {
+	const CACHE_FOLDER = 'cache/';
+	const ADMINISTRATION_FILE = 'administration.php';
+	
+	static $accepts = array('array' => 'get_modification_dates', 'string' => 'make_array');
+	
+	/**
+	 * @see pQuery::$variable_alias
+	 * @var string|array
+	 */
+	static $variable_alias = 'files';
+	
+	/**
+	 * A list of latest known modification timestamps of all files currently in the cache.
+	 * 
+	 * @var array
+	 */
+	static $admin;
+	
+	/**
+	 * A list of actual modification timestamps of the current file list.
+	 * 
+	 * @var array
+	 */
+	var $modification_dates;
+	
+	/**
+	 * Reduced script content.
+	 * 
+	 * @var string
+	 */
+	var $content = '';
+	
+	/**
+	 * Make a single file into an array.
+	 * 
+	 * @param string $file The file to put in an array.
+	 */
+	function make_array($file) {
+		return $this->get_modification_dates(array($file));
+	}
+	
+	/**
+	 * 
+	 */
+	function get_modification_dates($files) {
+		// Assert existence of all files
+		foreach( $files as $file )
+			file_exists($file) || self::error('File "%s" does not exist.', $file);
+		
+		$timestamps = array_map('filemtime', $files);
+		$this->modification_dates = array_combine($files, $timestamps);
+		
+		return $files;
+	}
+	
+	/**
+	 * 
+	 * 
+	 * @returns bool Whether the file list is in the cache and not updated.
+	 */
+	function admin_updated() {
+		self::assert_admin_exists();
+		
+		foreach( $this->modification_dates as $file => $timestamp )
+			if( !isset(self::$admin[$file]) || self::$admin[$file] !== $timestamp )
+				return true;
+		
+		return false;
+	}
+	
+	/**
+	 * 
+	 */
+	function concatenate() {
+		$this->content = implode("\n", array_map('file_get_contents', $this->files));
+		
+		return $this;
+	}
+	
+	/**
+	 * 
+	 */
+	function filename() {
+		return str_replace('/', '-', implode('-', $this->files));
+	}
+	
+	/**
+	 * 
+	 */
+	function output() {
+		$last_modified = max($this->modification_dates);
+		header('Last-Modified: '.date('r', $last_modified));
+		header('Expires: '.date('r', $last_modified + 60 * 60 * 24 * 365));
+		header('Cache-Control: private');
+		method_exists($this, 'set_headers') && $this->set_headers();
+		
+		if( $admin_updated = $this->admin_updated() || !isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ) {
+			//echo 'admin updated';
+			$this->save();
+		} elseif( !isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ) {
+			//echo 'hard refresh';
+			//$this->concatenate();
+		} elseif( isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ) {
+			$if_modified_since = strtotime(preg_replace('/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE']));
+			
+			if( $if_modified_since >= $last_modified ) {
+				//echo 'not modified';
+				// Not modified
+				header((php_sapi_name() == 'CGI' ? 'Status:' : 'HTTP/1.0').' 304 Not Modified');
+				exit;
+			} else {
+				//echo 'modified';
+			}
+		}
+		
+		die($this->content);
+	}
+	
+	/**
+	 * 
+	 */
+	function save() {
+		$this->concatenate();
+		self::assert_cache_folder_exists();
+		method_exists($this, 'minify') && $this->minify();
+		file_put_contents(self::CACHE_FOLDER.$this->filename(), $this->content);
+		self::$admin = array_merge(self::$admin, $this->modification_dates);
+		self::save_administration();
+	}
+	
+	/**
+	 * 
+	 */
+	static function save_administration() {
+		$handle = fopen(self::CACHE_FOLDER.self::ADMINISTRATION_FILE, 'w');
+		fwrite($handle, "<?php\n\npQueryCache::\$admin = array(\n");
+		
+		foreach( self::$admin as $file => $timestamp )
+			fwrite($handle, "\t'$file' => $timestamp,\n");
+		
+		fwrite($handle, ");\n\n?>");
+		fclose($handle);
+	}
+	
+	/**
+	 * Assert existence of the administration list by including the administration
+	 * file if it exists, and assigning an empty array otherwise.
+	 */
+	static function assert_admin_exists() {
+		if( self::$admin !== null )
+			return;
+		
+		$path = self::CACHE_FOLDER.self::ADMINISTRATION_FILE;
+		
+		if( file_exists($path) )
+			include_once $path;
+		else
+			self::$admin = array();
+	}
+	
+	/**
+	 * Assert existence of the cache folder.
+	 */
+	static function assert_cache_folder_exists() {
+		is_dir(self::CACHE_FOLDER) || mkdir(self::CACHE_FOLDER, 0777, true);
+	}
+}
+
+/**
+ * Shortcut constructor for {@link pQueryCache}.
+ * 
+ * @param array|string $files 
+ * @returns pQueryCache A new cache instance.
+ */
+function _cache($scripts) {
+	return pQuery::create('cache', $scripts);
+}
+
+/*
+ * Add plugin to pQuery
+ */
+__p::extend('pQueryCache', 'cache');
+
+?>

+ 81 - 0
pquery.css.php

@@ -0,0 +1,81 @@
+<?php
+/**
+ * pQuery plugin for parsing templates.
+ * 
+ * @package pQuery
+ */
+
+__p::require_plugins('cache');
+__p::load_util('CssParser');
+
+/**
+ * @todo Documentation
+ */
+class pQueryCss extends pQueryCache implements pQueryExtension {
+	static $accepts = array('array' => 'add_extensions', 'string' => 'make_array');
+	
+	var $minify_config = array(
+			'replace_shorthands' => true,
+			'sort_rules' => true,
+			'minify' => true,
+			'compress_measurements' => true,
+			'compress_colors' => true
+		);
+	
+	/**
+	 * Make a single file into an array.
+	 * 
+	 * @param string $file The file to put in an array.
+	 */
+	function make_array($file) {
+		return $this->add_extensions(array($file));
+	}
+	
+	/**
+	 * 
+	 * 
+	 * @param array $files 
+	 */
+	function add_extensions($files) {
+		foreach( $files as $i => $file )
+			if( !preg_match('/\.css$/', $file) )
+				$files[$i] = $file.'.css';
+		
+		return $this->get_modification_dates($files);
+	}
+	
+	/**
+	 * 
+	 */
+	function minify() {
+		$this->content = CssParser::minify($this->content, $this->minify_config);
+		
+		return $this;
+	}
+	
+	/**
+	 * 
+	 */
+	function set_headers() {
+		header('Content-Type: text/css');
+		
+		return $this;
+	}
+}
+
+/**
+ * Shortcut constructor for {@link pQueryCss}.
+ * 
+ * @param array|string $stylesheets 
+ * @returns pQueryCss A new stylesheet cache instance.
+ */
+function _css($stylesheets) {
+	return pQuery::create('css', $stylesheets);
+}
+
+/*
+ * Add plugin to pQuery
+ */
+__p::extend('pQueryCss', 'css');
+
+?>

+ 73 - 0
pquery.js.php

@@ -0,0 +1,73 @@
+<?php
+/**
+ * pQuery plugin for parsing templates.
+ * 
+ * @package pQuery
+ */
+
+__p::require_plugins('cache');
+__p::load_util('jshrink');
+
+/**
+ * @todo Documentation
+ */
+class pQueryJs extends pQueryCache implements pQueryExtension {
+	static $accepts = array('array' => 'add_extensions', 'string' => 'make_array');
+	
+	/**
+	 * Make a single file into an array.
+	 * 
+	 * @param string $file The file to put in an array.
+	 */
+	function make_array($file) {
+		return $this->add_extensions(array($file));
+	}
+	
+	/**
+	 * 
+	 * 
+	 * @param array $files 
+	 */
+	function add_extensions($files) {
+		foreach( $files as $i => $file )
+			if( !preg_match('/\.js$/', $file) )
+				$files[$i] = $file.'.js';
+		
+		return $this->get_modification_dates($files);
+	}
+	
+	/**
+	 * 
+	 */
+	function minify() {
+		$this->content = JShrink::minify($this->content, array('flaggedComments' => false));
+		
+		return $this;
+	}
+	
+	/**
+	 * 
+	 */
+	function set_headers() {
+		header('Content-Type: application/javascript');
+		
+		return $this;
+	}
+}
+
+/**
+ * Shortcut constructor for {@link pQueryJs}.
+ * 
+ * @param array|string $scripts 
+ * @returns pQueryJs A new script cache instance.
+ */
+function _js($scripts) {
+	return pQuery::create('js', $scripts);
+}
+
+/*
+ * Add plugin to pQuery
+ */
+__p::extend('pQueryJs', 'js');
+
+?>

+ 88 - 0
pquery.url.php

@@ -0,0 +1,88 @@
+<?php
+/**
+ * pQuery plugin for parsing templates.
+ * 
+ * @package pQuery
+ */
+
+/**
+ * @todo Documentation
+ * @property string $content The template's content.
+ */
+class pQueryUrl extends pQuery implements pQueryExtension {
+	static $accepts = array('string' => 'parse_url');
+	
+	/**
+	 * @see pQuery::$variable_alias
+	 * @var string|array
+	 */
+	static $variable_alias = 'url';
+	
+	/**
+	 * 
+	 * 
+	 * @var string
+	 */
+	static $handlers = array();
+	
+	/**
+	 * Remove slashes at the begin and end of the URL.
+	 * 
+	 * @param string $url The URL to parse.
+	 */
+	function parse_url($url) {
+		return preg_replace('%(^/|/$)%', '', $url);
+	}
+	
+	/**
+	 * Execute the handler of the first matching URL regex.
+	 * 
+	 * @param string $path The path to add.
+	 * @param bool $relative Indicates whether the path is relative to the document root.
+	 */
+	function handler() {
+		foreach( self::$handlers as $pattern => $handler )
+			if( preg_match($pattern, $this->url, $matches) )
+				return call_user_func_array($handler, array_slice($matches, 1));
+		
+		//self::error('URL has no handler.', $this->url);
+	}
+	
+	/**
+	 * Add a handler function to a URL match.
+	 * 
+	 * @param string $pattern The URL pattern to match.
+	 * @param callback $handler The handler to execute when the pattern is matched.
+	 */
+	static function add_handler($pattern, $handler) {
+		is_callable($handler) || self::error('Handler "%s" is not callable.', $handler);
+		self::$handlers["%$pattern%"] = $handler;
+	}
+	
+	/**
+	 * Add a list of handler functions to regexes.
+	 * 
+	 * @param array $handlers The list of handlers to add, with regexes as keys.
+	 */
+	static function add_handlers($handlers) {
+		foreach( $handlers as $pattern => $handler )
+			self::add_handler($pattern, $handler);
+	}
+}
+
+/**
+ * Shortcut constructor for {@link pQueryUrl}.
+ * 
+ * @param string $url 
+ * @returns pQueryUrl A new URL instance.
+ */
+function _url($url) {
+	return pQuery::create('url', $url);
+}
+
+/*
+ * Add plugin to pQuery
+ */
+__p::extend('pQueryUrl', 'url');
+
+?>

+ 257 - 0
utils/CssParser.php

@@ -0,0 +1,257 @@
+<?php
+
+class CssNode {
+	static $shorthands = array(
+		'margin' => 'top right bottom left',
+		'padding' => 'top right bottom left',
+		'font' => 'weight [style] size [/line-height] family',
+		'border' => 'width style color',
+		'background' => '[color] image repeat [attachment] [position]',
+		'list-style' => '[type] [position] [image]',
+		'outline' => '[color] [style] [width]'
+	);
+	static $colors = array(
+		'#f0f' => 'aqua', 'black' => '#000', 'fuchsia' => '#f0f', '#808080' => 'grey',
+		'#008000' => 'green', '#800000' => 'maroon', '#000080' => 'navy', '#808000' => 'olive',
+		'#800080' => 'purple', '#f00' => 'red', '#c0c0c0' => 'silver', '#008080' => 'teal',
+		'white' => '#fff', 'yellow' => '#ff0'
+	);
+	static $config;
+	public $selector;
+	public $css;
+	public $parent_node;
+	public $rules = array();
+	public $children = array();
+	
+	function __construct($selector, $css='', $parent_node=null) {
+		$this->selector = trim($selector);
+		$this->css = trim($css);
+		$this->parent_node = $parent_node;
+	}
+	
+	static function extract_importance($values) {
+		$important = '';
+		
+		foreach( $values as $rule => $value ) {
+			if( preg_match('/^(."+?) !important$/', $value, $m) ) {
+				$important = ' !important';
+				$values[$rule] = $m[1];
+			}
+		}
+		
+		return array($values, $important);
+	}
+	
+	function replace_shorthands() {
+		$rules = array();
+		
+		// Put sub-selectors in arrays
+		$pattern = '/^('.implode('|', array_keys(self::$shorthands)).')-([\w-]+)/';
+		
+		foreach( $this->rules as $rule => $value ) {
+			if( preg_match($pattern, $rule, $m) ) {
+				$base_rule = $m[1];
+				
+				if( isset($rules[$base_rule]) ) {
+					if( !is_array($rules[$base_rule]) )
+						$rules[$base_rule] = array('__main__' => $rules[$base_rule]);
+				} else {
+					$rules[$base_rule] = array();
+				}
+				
+				$rules[$base_rule][$m[2]] = $value;
+			} else {
+				$rules[$rule] = $value;
+			}
+		}
+		
+		// Filter out base rules with one property value
+		foreach( $rules as $rule => $values ) {
+			if( is_array($values) && count($values) == 1 ) {
+				$rules[$rule.'-'.key($values)] = reset($values);
+				unset($rules[$rule]);
+			}
+		}
+		
+		foreach( $rules as $rule => $values ) {
+			if( is_array($values) ) {
+				list($values, $important) = self::extract_importance($values);
+				
+				if( $rule == 'font' && isset($rules['line-height']) ) {
+					$values['line-height'] = $rules['line-height'];
+					unset($rules['line-height']);
+				}
+				
+				if( isset(self::$shorthands[$rule]) ) {
+					$replace = true;
+					$replacement = '';
+					$parts = explode(' ', self::$shorthands[$rule]);
+					
+					foreach( array_keys($parts) as $i ) {
+						$part = $parts[$i];
+						
+						if( $part == '[' ) {
+							$parts[$i + 1] = '[ '.$parts[$i + 1];
+							continue;
+						}
+						
+						if( preg_match('%^\[(/)?([^]]+)\]$%', $part, $m) ) {
+							$part = $m[2];
+							
+							if( isset($values[$part]) ) {
+								$value = $values[$part];
+								
+								if( self::$config['compress_colors'] && strpos($part, 'color') !== false )
+									$value = self::compress_color($value);
+								
+								$replacement .= (empty($m[1]) ? ' ' : $m[1]).$value;
+							}
+						} elseif( isset($values[$part]) ) {
+							$value = $values[$part];
+							
+							if( self::$config['compress_colors'] && strpos($part, 'color') !== false )
+								$value = self::compress_color($value);
+							
+							$i && $replacement .= ' ';
+							$replacement .= $value;
+						} else {
+							$replace = false;
+							break;
+						}
+					}
+					
+					$replace && $values['__main__'] = $replacement;
+				}
+				
+				if( isset($values['__main__']) ) {
+					$rules[$rule] = $values['__main__'];
+				} else {
+					foreach( $values as $sub_rule => $value )
+						$rules[$rule.'-'.$sub_rule] = $value;
+					
+					unset($rules[$rule]);
+				}
+			}
+		}
+		
+		return $rules;
+	}
+	
+	function compress_color($color) {
+		$color = preg_replace('/#(\w)\1(\w)\2(\w)\3/', '#\1\2\3', strtolower($color));
+		
+		if( isset(self::$colors[$color]) )
+			$color = self::$colors[$color];
+		
+		return $color;
+	}
+	
+	function compress($value, $rule) {
+		if( self::$config['compress_colors'] && preg_match('/color$/', $rule) )
+			$value = self::compress_color($value);
+		
+		// Replace any redundant margins and paddings
+		if( self::$config['compress_measurements']
+				&& ($rule == 'margin' || $rule == 'padding')
+				&& preg_match('/^(\w+) (\w+) (\w+)(?: (\w+))?$/', $value, $m) ) {
+			$value = $m[1].' '.$m[2];
+			$left_needed = isset($m[4]) && $m[4] != $m[2];
+			$bottom_needed = $left_needed || $m[3] != $m[1];
+			
+			if( $bottom_needed ) {
+				$value .= ' '.$m[3];
+				$left_needed && $value .= ' '.$m[4];
+			}
+		}
+		
+		return $rule.(self::$config['minify'] ? ':' : ': ').trim($value);
+	}
+	
+	function parse_rules($rules) {
+		foreach( preg_split('/\s*;\s*/', trim($rules)) as $rule ) {
+			$split = preg_split('/\s*:\s*/', $rule, 2);
+			
+			if( count($split) == 2 && !empty($split[0]) && !empty($split[1]) )
+				$this->rules[$split[0]] = $split[1];
+		}
+	}
+	
+	function parse() {
+		$minify = self::$config['minify'];
+		$current_node = $this;
+		
+		// Remove comments and redundant whitespaces
+		$css = preg_replace(array('/\s+/', '%/\*.*?\*/%'), array(' ', ''), $this->css);
+		
+		foreach( array_map('trim', preg_split('/;|\}/', $css)) as $line ) {
+			if( preg_match('/^ ?([^{]+) ?\{ ?(.*)$/', $line, $m) ) {
+				// Start tag
+				$self = preg_match('/^self(.*)/', $m[1], $selector_match);
+				$child_selectors = $self ? $selector_match[1] : $m[1];
+				
+				if( strpos($child_selectors, ',') !== false ) {
+					$selectors = array();
+					
+					foreach( preg_split('/ ?, ?/', trim($child_selectors)) as $child_selector )
+						$selectors[] = $current_node->selector.' '.$child_selector;
+					
+					$selector = implode(',', $selectors);
+				} else {
+					$selector = $current_node->selector.($self ? '' : ' ').$child_selectors;
+				}
+				
+				$current_node = $current_node->children[] = new CssNode($selector, $m[2], $current_node);
+				$line = $m[2];
+			}
+			
+			if( empty($line) ) {
+				// End tag
+				$current_node = $current_node->parent_node;
+			} else {
+				// Normal rule
+				$current_node->parse_rules($line);
+			}
+		}
+		
+		// Build new CSS string according to config
+		$css = '';
+		
+		if( strlen($this->selector) ) {
+			$rules = self::$config['replace_shorthands'] ? $this->replace_shorthands() : $this->rules;
+			self::$config['sort_rules'] && ksort($rules);
+			$rules = array_map('self::compress', $rules, array_keys($rules));
+			
+			if( $minify )
+				$css .= $this->selector.'{'.implode(';', $rules).'}';
+			else
+				$css .= preg_replace('/, ?/', ",\n", $this->selector)." {\n\t".implode(";\n\t", $rules).";\n}";
+		}
+		
+		// Parse children recursively
+		foreach( $this->children as $child ) {
+			$minify || $css .= "\n\n";
+			$css .= $child->parse();
+		}
+		
+		return trim($css);
+	}
+}
+
+class CssParser {
+	static $default_config = array(
+		'replace_shorthands' => true,
+		'sort_rules' => true,
+		'minify' => true,
+		'compress_measurements' => true,
+		'compress_colors' => true
+	);
+	
+	static function minify($css, $config=array()) {
+		CssNode::$config = array_merge(self::$default_config, $config);
+		$node = new CssNode('', $css);
+		
+		return $node->parse();
+	}
+}
+
+?>

+ 333 - 0
utils/jshrink.php

@@ -0,0 +1,333 @@
+<?php
+/*
+  JShrink
+
+  Copyright (c) 2009, Robert Hafner
+  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+		disclaimer.
+	* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
+		following disclaimer in the documentation and/or other materials provided with the distribution.
+	* Neither the name of the <ORGANIZATION> nor the names of its contributors may be used to endorse or promote
+		products derived from this software without specific prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+/**
+ * JShrink
+ *
+ * Usage - JShrink::minify($js);
+ * Usage - JShrink::minify($js, $options);
+ * Usage - JShrink::minify($js, array('flaggedComments' => false));
+ *
+ * @version 0.2
+ * @package JShrink
+ * @author Robert Hafner <tedivm@tedivm.com>
+ * @license http://www.opensource.org/licenses/bsd-license.php
+ */
+class JShrink
+{
+	protected $input;
+	protected $index = 0;
+
+	protected $a = '';
+	protected $b = '';
+	protected $c;
+
+	protected $options;
+
+	static protected $defaultOptions = array('flaggedComments' => true);
+
+	static public function minify($js, $options = array())
+	{
+		try{
+			$currentOptions = array_merge(self::$defaultOptions, $options);
+
+			ob_start();
+			$currentOptions = array_merge(self::$defaultOptions, $options);
+			$me = new JShrink();
+			$me->breakdownScript($js, $currentOptions);
+			$output = ob_get_clean();
+			return $output;
+
+		}catch(Exception $e){
+			ob_end_clean();
+			throw $e;
+		}
+	}
+
+	protected function breakdownScript($js, $currentOptions)
+	{
+		$this->options = $currentOptions;
+
+		$js = str_replace("\r\n", "\n", $js);
+		$this->input = str_replace("\r", "\n", $js);
+
+		$this->a = $this->getReal();
+
+		// the only time the length can be higher than 1 is if a conditional comment needs to be displayed
+		// and the only time that can happen for $a is on the very first run
+		while(strlen($this->a) > 1)
+		{
+			echo $this->a;
+			$this->a = $this->getReal();
+		}
+
+		$this->b = $this->getReal();
+
+		while($this->a !== false && !is_null($this->a) && $this->a !== '')
+		{
+
+			// now we give $b the same check for conditional comments we gave $a before we began looping
+			if(strlen($this->b) > 1)
+			{
+				echo $this->a . $this->b;
+				$this->a = $this->getReal();
+				$this->b = $this->getReal();
+				continue;
+			}
+
+			switch($this->a)
+			{
+				// new lines
+				case "\n":
+					// if the next line is something that can't stand alone preserver the newline
+					if(strpos('(-+{[@', $this->b) !== false)
+					{
+						echo $this->a;
+						$this->saveString();
+						break;
+					}
+
+					// if its a space we move down to the string test below
+					if($this->b === ' ')
+						break;
+
+					// otherwise we treat the newline like a space
+
+				case ' ':
+					if(self::isAlphaNumeric($this->b))
+						echo $this->a;
+
+					$this->saveString();
+					break;
+
+				default:
+					switch($this->b)
+					{
+						case "\n":
+							if(strpos('}])+-"\'', $this->a) !== false)
+							{
+								echo $this->a;
+								$this->saveString();
+								break;
+							}else{
+								if(self::isAlphaNumeric($this->a))
+								{
+									echo $this->a;
+									$this->saveString();
+								}
+							}
+							break;
+
+						case ' ':
+							if(!self::isAlphaNumeric($this->a))
+								break;
+
+						default:
+							// check for some regex that breaks stuff
+							if($this->a == '/' && ($this->b == '\'' || $this->b == '"'))
+							{
+								$this->saveRegex();
+								continue;
+							}
+
+							echo $this->a;
+							$this->saveString();
+							break;
+					}
+			}
+
+			// do reg check of doom
+			$this->b = $this->getReal();
+
+			if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false))
+				$this->saveRegex();
+		}
+	}
+
+	protected function getChar()
+	{
+		if(isset($this->c))
+		{
+			$char = $this->c;
+			unset($this->c);
+		}else{
+			if(isset($this->input[$this->index]))
+			{
+				$char = $this->input[$this->index];
+				$this->index++;
+			}else{
+				return false;
+			}
+		}
+
+		if($char === "\n" || ord($char) >= 32)
+			return $char;
+
+		return ' ';
+	}
+
+	protected function getReal()
+	{
+		$startIndex = $this->index;
+		$char = $this->getChar();
+
+		if($char == '/')
+		{
+			$this->c = $this->getChar();
+
+			if($this->c == '/')
+			{
+				$thirdCommentString = $this->input[$this->index];
+
+				// kill rest of line
+				$char = $this->getNext("\n");
+
+				if($thirdCommentString == '@')
+				{
+					$endPoint = ($this->index) - $startIndex;
+					unset($this->c);
+					$char = "\n" . substr($this->input, $startIndex, $endPoint);// . "\n";
+				}else{
+					$char = $this->getChar();
+					$char = $this->getChar();
+				}
+
+			}elseif($this->c == '*'){
+
+				$this->getChar(); // current C
+				$thirdCommentString = $this->getChar();
+
+				if($thirdCommentString == '@')
+				{
+					// we're gonna back up a bit and and send the comment back, where the first
+					// char will be echoed and the rest will be treated like a string
+					$this->index = $this->index-2;
+					return '/';
+
+				}elseif($this->getNext('*/')){
+				// kill everything up to the next */
+
+					$this->getChar(); // get *
+					$this->getChar(); // get /
+
+					$char = $this->getChar(); // get next real charactor
+
+					// if YUI-style comments are enabled we reinsert it into the stream
+					if($this->options['flaggedComments'] && $thirdCommentString == '!')
+					{
+						$endPoint = ($this->index - 1) - $startIndex;
+						echo "\n" . substr($this->input, $startIndex, $endPoint) . "\n";
+					}
+
+				}else{
+					$char = false;
+				}
+
+				if($char === false)
+					throw new JShrinkException('Stray comment. ' . $this->index);
+
+				// if we're here c is part of the comment and therefore tossed
+				if(isset($this->c))
+					unset($this->c);
+			}
+		}
+		return $char;
+	}
+
+	protected function getNext($string)
+	{
+		$pos = strpos($this->input, $string, $this->index);
+
+		if($pos === false)
+			return false;
+
+		$this->index = $pos ;
+		return $this->input[$this->index];
+	}
+
+	protected function saveString()
+	{
+		$this->a = $this->b;
+		if($this->a == '\'' || $this->a == '"')
+		{
+			// save literal string
+			$stringType = $this->a;
+
+			while(1)
+			{
+				echo $this->a;
+				$this->a = $this->getChar();
+
+				switch($this->a)
+				{
+					case $stringType:
+						break 2;
+
+					case "\n":
+						throw new JShrinkException('Unclosed string. ' . $this->index);
+						break;
+
+					case '\\':
+						echo $this->a;
+						$this->a = $this->getChar();
+				}
+			}
+		}
+	}
+
+	protected function saveRegex()
+	{
+		echo $this->a . $this->b;
+
+		while(($this->a = $this->getChar()) !== false)
+		{
+			if($this->a == '/')
+				break;
+
+			if($this->a == '\\')
+			{
+				echo $this->a;
+				$this->a = $this->getChar();
+			}
+
+			if($this->a == "\n")
+				throw new JShrinkException('Stray regex pattern. ' . $this->index);
+
+			echo $this->a;
+		}
+		$this->b = $this->getReal();
+	}
+
+	static protected function isAlphaNumeric($char)
+	{
+		return preg_match('/^[\w\$]$/', $char) === 1 || $char == '/';
+	}
+
+}
+
+// Adding a custom exception handler for your own projects just means changing this line
+class JShrinkException extends Exception {}
+?>

+ 218 - 0
utils/minify_html.php

@@ -0,0 +1,218 @@
+<?php
+/**
+ * Class Minify_HTML  
+ * @package Minify
+ */
+
+/**
+ * Compress HTML
+ *
+ * This is a heavy regex-based removal of whitespace, unnecessary comments and 
+ * tokens. IE conditional comments are preserved. There are also options to have
+ * STYLE and SCRIPT blocks compressed by callback functions. 
+ * 
+ * A test suite is available.
+ * 
+ * @package Minify
+ * @author Stephen Clay <steve@mrclay.org>
+ */
+class Minify_HTML {
+
+    /**
+     * Defines which class to call as part of callbacks, change this
+     * if you extend Minify_HTML
+     * @var string
+     */
+    protected static $className = 'Minify_HTML';
+
+    /**
+     * "Minify" an HTML page
+     *
+     * @param string $html
+     *
+     * @param array $options
+     *
+     * 'cssMinifier' : (optional) callback function to process content of STYLE
+     * elements.
+     * 
+     * 'jsMinifier' : (optional) callback function to process content of SCRIPT
+     * elements. Note: the type attribute is ignored.
+     * 
+     * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If
+     * unset, minify will sniff for an XHTML doctype.
+     * 
+     * @return string
+     */
+    public static function minify($html, $options = array()) {
+        
+        if (isset($options['cssMinifier'])) {
+            self::$_cssMinifier = $options['cssMinifier'];
+        }
+        if (isset($options['jsMinifier'])) {
+            self::$_jsMinifier = $options['jsMinifier'];
+        }
+        
+        $html = str_replace("\r\n", "\n", trim($html));
+        
+        self::$_isXhtml = (
+            isset($options['xhtml'])
+                ? (bool)$options['xhtml']
+                : (false !== strpos($html, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML'))
+        );
+        
+        self::$_replacementHash = 'MINIFYHTML' . md5(time());
+        self::$_placeholders = array();
+        
+        // replace SCRIPTs (and minify) with placeholders
+        $html = preg_replace_callback(
+            '/\\s*(<script\\b[^>]*?>)([\\s\\S]*?)<\\/script>\\s*/i'
+            ,array(self::$className, '_removeScriptCB')
+            ,$html);
+        
+        // replace STYLEs (and minify) with placeholders
+        $html = preg_replace_callback(
+            '/\\s*(<style\\b[^>]*?>)([\\s\\S]*?)<\\/style>\\s*/i'
+            ,array(self::$className, '_removeStyleCB')
+            ,$html);
+        
+        // remove HTML comments (not containing IE conditional comments).
+        $html = preg_replace_callback(
+            '/<!--([\\s\\S]*?)-->/'
+            ,array(self::$className, '_commentCB')
+            ,$html);
+        
+        // replace PREs with placeholders
+        $html = preg_replace_callback('/\\s*(<pre\\b[^>]*?>[\\s\\S]*?<\\/pre>)\\s*/i'
+            ,array(self::$className, '_removePreCB')
+            , $html);
+        
+        // replace TEXTAREAs with placeholders
+        $html = preg_replace_callback(
+            '/\\s*(<textarea\\b[^>]*?>[\\s\\S]*?<\\/textarea>)\\s*/i'
+            ,array(self::$className, '_removeTaCB')
+            , $html);
+        
+        // trim each line.
+        // @todo take into account attribute values that span multiple lines.
+        $html = preg_replace('/^\\s+|\\s+$/m', '', $html);
+        
+        // remove ws around block/undisplayed elements
+        $html = preg_replace('/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body'
+            .'|caption|center|cite|col(?:group)?|dd|dir|div|dl|dt|fieldset|form'
+            .'|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta'
+            .'|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)'
+            .'|ul)\\b[^>]*>)/i', '$1', $html);
+        
+        // remove ws outside of all elements
+        $html = preg_replace_callback(
+            '/>([^<]+)</'
+            ,array(self::$className, '_outsideTagCB')
+            ,$html);
+        
+        // use newlines before 1st attribute in open tags (to limit line lengths)
+        $html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $html);
+        
+        // fill placeholders
+        $html = str_replace(
+            array_keys(self::$_placeholders)
+            ,array_values(self::$_placeholders)
+            ,$html
+        );
+        self::$_placeholders = array();
+        
+        self::$_cssMinifier = self::$_jsMinifier = null;
+        return $html;
+    }
+    
+    protected static function _commentCB($m)
+    {
+        return (0 === strpos($m[1], '[') || false !== strpos($m[1], '<!['))
+            ? $m[0]
+            : '';
+    }
+    
+    protected static function _reservePlace($content)
+    {
+        $placeholder = '%' . self::$_replacementHash . count(self::$_placeholders) . '%';
+        self::$_placeholders[$placeholder] = $content;
+        return $placeholder;
+    }
+
+    protected static $_isXhtml = false;
+    protected static $_replacementHash = null;
+    protected static $_placeholders = array();
+    protected static $_cssMinifier = null;
+    protected static $_jsMinifier = null;
+
+    protected static function _outsideTagCB($m)
+    {
+        return '>' . preg_replace('/^\\s+|\\s+$/', ' ', $m[1]) . '<';
+    }
+    
+    protected static function _removePreCB($m)
+    {
+        return self::_reservePlace($m[1]);
+    }
+    
+    protected static function _removeTaCB($m)
+    {
+        return self::_reservePlace($m[1]);
+    }
+
+    protected static function _removeStyleCB($m)
+    {
+        $openStyle = $m[1];
+        $css = $m[2];
+        // remove HTML comments
+        $css = preg_replace('/(?:^\\s*<!--|-->\\s*$)/', '', $css);
+        
+        // remove CDATA section markers
+        $css = self::_removeCdata($css);
+        
+        // minify
+        $minifier = self::$_cssMinifier
+            ? self::$_cssMinifier
+            : 'trim';
+        $css = call_user_func($minifier, $css);
+        
+        return self::_reservePlace(self::_needsCdata($css)
+            ? "{$openStyle}/*<![CDATA[*/{$css}/*]]>*/</style>"
+            : "{$openStyle}{$css}</style>"
+        );
+    }
+
+    protected static function _removeScriptCB($m)
+    {
+        $openScript = $m[1];
+        $js = $m[2];
+        
+        // remove HTML comments (and ending "//" if present)
+        $js = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $js);
+            
+        // remove CDATA section markers
+        $js = self::_removeCdata($js);
+        
+        // minify
+        $minifier = self::$_jsMinifier
+            ? self::$_jsMinifier
+            : 'trim'; 
+        $js = call_user_func($minifier, $js);
+        
+        return self::_reservePlace(self::_needsCdata($js)
+            ? "{$openScript}/*<![CDATA[*/{$js}/*]]>*/</script>"
+            : "{$openScript}{$js}</script>"
+        );
+    }
+
+    protected static function _removeCdata($str)
+    {
+        return (false !== strpos($str, '<![CDATA['))
+            ? str_replace(array('<![CDATA[', ']]>'), '', $str)
+            : $str;
+    }
+    
+    protected static function _needsCdata($str)
+    {
+        return (self::$_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str));
+    }
+}