Commit b2bb93a6 authored by Taddeus Kroes's avatar Taddeus Kroes

Added URL and Cache plugins,

parent 6315d2e7
<?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');
?>
\ No newline at end of file
<?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');
?>
\ No newline at end of file
<?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');
?>
\ No newline at end of file
<?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');
?>
\ No newline at end of file
<?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();
}
}
?>
\ No newline at end of file
<?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 {}
?>
\ No newline at end of file
<?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));
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment