diff --git a/HISTORY b/HISTORY index c69fc8d..a1f3f94 100644 --- a/HISTORY +++ b/HISTORY @@ -1,5 +1,10 @@ Minify Release History +Version 1.9.0 (2008-02-28) + * Complete overhaul! Minify is now a PEAR-style class and toolkit for building + customized minifying file servers. + * Utility classes HTTP_Encoder and HTTP_ConditionalGet + Version 1.0.1 (2007-05-05) * Fixed various problems resolving pathnames when hosted on an NFS mount. * Fixed 'undefined constant' notice. diff --git a/README b/README index 5757ee4..bc906b8 100644 --- a/README +++ b/README @@ -1 +1,5 @@ -Please see http://code.google.com/p/minify/ for documentation. +Note: Current trunk is progress on V2 and should be considered "alpha". + +Documentation at http://code.google.com/p/minify/ needs updating. + +For example usage, see files in /examples/1/ \ No newline at end of file diff --git a/examples/1/index.php b/examples/1/index.php new file mode 100644 index 0000000..60e3e1c --- /dev/null +++ b/examples/1/index.php @@ -0,0 +1,66 @@ + + + + + Minify Example 1 + + + + + + +

Note: You should always enable caching using +Minify::useServerCache(). For the examples this can be set in +config.php. Notice that minifying jquery.js takes several seconds!.

+ + +

Minify Example 1

+ +

This is an example of Minify serving a directory of single css/js files. +Each file is minified and sent with HTTP encoding (browser-permitting).

+ + + +

Link to this page (F5 can trigger no-cache headers)

+ + + + + + + $content + ,'id' => __FILE__ + ,'lastModifiedTime' => filemtime(__FILE__) + + // also minify the CSS/JS inside the HTML + ,'minifyAll' => true +)); \ No newline at end of file diff --git a/examples/1/jquery-1.2.3.js b/examples/1/jquery-1.2.3.js new file mode 100644 index 0000000..2e43a82 --- /dev/null +++ b/examples/1/jquery-1.2.3.js @@ -0,0 +1,3408 @@ +(function(){ +/* + * jQuery 1.2.3 - New Wave Javascript + * + * Copyright (c) 2008 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2008-02-06 00:21:25 -0500 (Wed, 06 Feb 2008) $ + * $Rev: 4663 $ + */ + +// Map over jQuery in case of overwrite +if ( window.jQuery ) + var _jQuery = window.jQuery; + +var jQuery = window.jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.prototype.init( selector, context ); +}; + +// Map over the $ in case of overwrite +if ( window.$ ) + var _$ = window.$; + +// Map the jQuery namespace to the '$' one +window.$ = jQuery; + +// A simple way to check for HTML strings or ID strings +// (both of which we optimize for) +var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/; + +// Is it a simple selector +var isSimple = /^.[^:#\[\.]*$/; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this[0] = selector; + this.length = 1; + return this; + + // Handle HTML strings + } else if ( typeof selector == "string" ) { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) + selector = jQuery.clean( [ match[1] ], context ); + + // HANDLE: $("#id") + else { + var elem = document.getElementById( match[3] ); + + // Make sure an element was located + if ( elem ) + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id != match[3] ) + return jQuery().find( selector ); + + // Otherwise, we inject the element directly into the jQuery object + else { + this[0] = elem; + this.length = 1; + return this; + } + + else + selector = []; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return new jQuery( context ).find( selector ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) + return new jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector ); + + return this.setArray( + // HANDLE: $(array) + selector.constructor == Array && selector || + + // HANDLE: $(arraylike) + // Watch for when an array-like object, contains DOM nodes, is passed in as the selector + (selector.jquery || selector.length && selector != window && !selector.nodeType && selector[0] != undefined && selector[0].nodeType) && jQuery.makeArray( selector ) || + + // HANDLE: $(*) + [ selector ] ); + }, + + // The current version of jQuery being used + jquery: "1.2.3", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // The number of elements contained in the matched element set + length: 0, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == undefined ? + + // Return a 'clean' array + jQuery.makeArray( this ) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + // Build a new jQuery matched element set + var ret = jQuery( elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function( elems ) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply( this, elems ); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + var ret = -1; + + // Locate the position of the desired element + this.each(function(i){ + if ( this == elem ) + ret = i; + }); + + return ret; + }, + + attr: function( name, value, type ) { + var options = name; + + // Look for the case where we're accessing a style value + if ( name.constructor == String ) + if ( value == undefined ) + return this.length && jQuery[ type || "attr" ]( this[0], name ) || undefined; + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i){ + // Set all the styles + for ( name in options ) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop( this, options[ name ], type, i, name ) + ); + }); + }, + + css: function( key, value ) { + // ignore negative width and height values + if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) + value = undefined; + return this.attr( key, value, "curCSS" ); + }, + + text: function( text ) { + if ( typeof text != "object" && text != null ) + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + + var ret = ""; + + jQuery.each( text || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text( [ this ] ); + }); + }); + + return ret; + }, + + wrapAll: function( html ) { + if ( this[0] ) + // The elements to wrap the target around + jQuery( html, this[0].ownerDocument ) + .clone() + .insertBefore( this[0] ) + .map(function(){ + var elem = this; + + while ( elem.firstChild ) + elem = elem.firstChild; + + return elem; + }) + .append(this); + + return this; + }, + + wrapInner: function( html ) { + return this.each(function(){ + jQuery( this ).contents().wrapAll( html ); + }); + }, + + wrap: function( html ) { + return this.each(function(){ + jQuery( this ).wrapAll( html ); + }); + }, + + append: function() { + return this.domManip(arguments, true, false, function(elem){ + if (this.nodeType == 1) + this.appendChild( elem ); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, true, function(elem){ + if (this.nodeType == 1) + this.insertBefore( elem, this.firstChild ); + }); + }, + + before: function() { + return this.domManip(arguments, false, false, function(elem){ + this.parentNode.insertBefore( elem, this ); + }); + }, + + after: function() { + return this.domManip(arguments, false, true, function(elem){ + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + }, + + end: function() { + return this.prevObject || jQuery( [] ); + }, + + find: function( selector ) { + var elems = jQuery.map(this, function(elem){ + return jQuery.find( selector, elem ); + }); + + return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ? + jQuery.unique( elems ) : + elems ); + }, + + clone: function( events ) { + // Do the clone + var ret = this.map(function(){ + if ( jQuery.browser.msie && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var clone = this.cloneNode(true), + container = document.createElement("div"); + container.appendChild(clone); + return jQuery.clean([container.innerHTML])[0]; + } else + return this.cloneNode(true); + }); + + // Need to set the expando to null on the cloned set if it exists + // removeData doesn't work here, IE removes it from the original as well + // this is primarily for IE but the data expando shouldn't be copied over in any browser + var clone = ret.find("*").andSelf().each(function(){ + if ( this[ expando ] != undefined ) + this[ expando ] = null; + }); + + // Copy the events from the original to the clone + if ( events === true ) + this.find("*").andSelf().each(function(i){ + if (this.nodeType == 3) + return; + var events = jQuery.data( this, "events" ); + + for ( var type in events ) + for ( var handler in events[ type ] ) + jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data ); + }); + + // Return the cloned set + return ret; + }, + + filter: function( selector ) { + return this.pushStack( + jQuery.isFunction( selector ) && + jQuery.grep(this, function(elem, i){ + return selector.call( elem, i ); + }) || + + jQuery.multiFilter( selector, this ) ); + }, + + not: function( selector ) { + if ( selector.constructor == String ) + // test special case where just one selector is passed in + if ( isSimple.test( selector ) ) + return this.pushStack( jQuery.multiFilter( selector, this, true ) ); + else + selector = jQuery.multiFilter( selector, this ); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; + }); + }, + + add: function( selector ) { + return !selector ? this : this.pushStack( jQuery.merge( + this.get(), + selector.constructor == String ? + jQuery( selector ).get() : + selector.length != undefined && (!selector.nodeName || jQuery.nodeName(selector, "form")) ? + selector : [selector] ) ); + }, + + is: function( selector ) { + return selector ? + jQuery.multiFilter( selector, this ).length > 0 : + false; + }, + + hasClass: function( selector ) { + return this.is( "." + selector ); + }, + + val: function( value ) { + if ( value == undefined ) { + + if ( this.length ) { + var elem = this[0]; + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if ( index < 0 ) + return null; + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery.browser.msie && !option.attributes.value.specified ? option.text : option.value; + + // We don't need an array for one selects + if ( one ) + return value; + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + + // Everything else, we just grab the value + } else + return (this[0].value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + return this.each(function(){ + if ( this.nodeType != 1 ) + return; + + if ( value.constructor == Array && /radio|checkbox/.test( this.type ) ) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if ( jQuery.nodeName( this, "select" ) ) { + var values = value.constructor == Array ? + value : + [ value ]; + + jQuery( "option", this ).each(function(){ + this.selected = (jQuery.inArray( this.value, values ) >= 0 || + jQuery.inArray( this.text, values ) >= 0); + }); + + if ( !values.length ) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function( value ) { + return value == undefined ? + (this.length ? + this[0].innerHTML : + null) : + this.empty().append( value ); + }, + + replaceWith: function( value ) { + return this.after( value ).remove(); + }, + + eq: function( i ) { + return this.slice( i, i + 1 ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ) ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function(elem, i){ + return callback.call( elem, i, elem ); + })); + }, + + andSelf: function() { + return this.add( this.prevObject ); + }, + + data: function( key, value ){ + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value == null ) { + var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + if ( data == undefined && this.length ) + data = jQuery.data( this[0], key ); + + return data == null && parts[1] ? + this.data( parts[0] ) : + data; + } else + return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){ + jQuery.data( this, key, value ); + }); + }, + + removeData: function( key ){ + return this.each(function(){ + jQuery.removeData( this, key ); + }); + }, + + domManip: function( args, table, reverse, callback ) { + var clone = this.length > 1, elems; + + return this.each(function(){ + if ( !elems ) { + elems = jQuery.clean( args, this.ownerDocument ); + + if ( reverse ) + elems.reverse(); + } + + var obj = this; + + if ( table && jQuery.nodeName( this, "table" ) && jQuery.nodeName( elems[0], "tr" ) ) + obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") ); + + var scripts = jQuery( [] ); + + jQuery.each(elems, function(){ + var elem = clone ? + jQuery( this ).clone( true )[0] : + this; + + // execute all scripts after the elements have been injected + if ( jQuery.nodeName( elem, "script" ) ) { + scripts = scripts.add( elem ); + } else { + // Remove any inner scripts for later evaluation + if ( elem.nodeType == 1 ) + scripts = scripts.add( jQuery( "script", elem ).remove() ); + + // Inject the elements into the document + callback.call( obj, elem ); + } + }); + + scripts.each( evalScript ); + }); + } +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.prototype.init.prototype = jQuery.prototype; + +function evalScript( i, elem ) { + if ( elem.src ) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + + if ( elem.parentNode ) + elem.parentNode.removeChild( elem ); +} + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if ( target.constructor == Boolean ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target != "object" && typeof target != "function" ) + target = {}; + + // extend jQuery itself if only one argument is passed + if ( length == 1 ) { + target = this; + i = 0; + } + + for ( ; i < length; i++ ) + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) + // Extend the base object + for ( var name in options ) { + // Prevent never-ending loop + if ( target === options[ name ] ) + continue; + + // Recurse if we're merging object values + if ( deep && options[ name ] && typeof options[ name ] == "object" && target[ name ] && !options[ name ].nodeType ) + target[ name ] = jQuery.extend( target[ name ], options[ name ] ); + + // Don't bring in undefined values + else if ( options[ name ] != undefined ) + target[ name ] = options[ name ]; + + } + + // Return the modified object + return target; +}; + +var expando = "jQuery" + (new Date()).getTime(), uuid = 0, windowData = {}; + +// exclude the following css properties to add px +var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning this function. + isFunction: function( fn ) { + return !!fn && typeof fn != "string" && !fn.nodeName && + fn.constructor != Array && /function/i.test( fn + "" ); + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function( elem ) { + return elem.documentElement && !elem.body || + elem.tagName && elem.ownerDocument && !elem.ownerDocument.body; + }, + + // Evalulates a script in a global context + globalEval: function( data ) { + data = jQuery.trim( data ); + + if ( data ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if ( jQuery.browser.msie ) + script.text = data; + else + script.appendChild( document.createTextNode( data ) ); + + head.appendChild( script ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + cache: {}, + + data: function( elem, name, data ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ]; + + // Compute a unique ID for the element + if ( !id ) + id = elem[ expando ] = ++uuid; + + // Only generate the data cache if we're + // trying to access or manipulate it + if ( name && !jQuery.cache[ id ] ) + jQuery.cache[ id ] = {}; + + // Prevent overriding the named cache with undefined values + if ( data != undefined ) + jQuery.cache[ id ][ name ] = data; + + // Return the named cache data, or the ID for the element + return name ? + jQuery.cache[ id ][ name ] : + id; + }, + + removeData: function( elem, name ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ]; + + // If we want to remove a specific section of the element's data + if ( name ) { + if ( jQuery.cache[ id ] ) { + // Remove the section of cache data + delete jQuery.cache[ id ][ name ]; + + // If we've removed all the data, remove the element's cache + name = ""; + + for ( name in jQuery.cache[ id ] ) + break; + + if ( !name ) + jQuery.removeData( elem ); + } + + // Otherwise, we want to remove all of the element's data + } else { + // Clean up the element expando + try { + delete elem[ expando ]; + } catch(e){ + // IE has trouble directly removing the expando + // but it's ok with using removeAttribute + if ( elem.removeAttribute ) + elem.removeAttribute( expando ); + } + + // Completely remove the data cache + delete jQuery.cache[ id ]; + } + }, + + // args is for internal usage only + each: function( object, callback, args ) { + if ( args ) { + if ( object.length == undefined ) { + for ( var name in object ) + if ( callback.apply( object[ name ], args ) === false ) + break; + } else + for ( var i = 0, length = object.length; i < length; i++ ) + if ( callback.apply( object[ i ], args ) === false ) + break; + + // A special, fast, case for the most common use of each + } else { + if ( object.length == undefined ) { + for ( var name in object ) + if ( callback.call( object[ name ], name, object[ name ] ) === false ) + break; + } else + for ( var i = 0, length = object.length, value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + } + + return object; + }, + + prop: function( elem, value, type, i, name ) { + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, i ); + + // Handle passing in a number to a CSS property + return value && value.constructor == Number && type == "curCSS" && !exclude.test( name ) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, classNames ) { + jQuery.each((classNames || "").split(/\s+/), function(i, className){ + if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, classNames ) { + if (elem.nodeType == 1) + elem.className = classNames != undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className){ + return !jQuery.className.has( classNames, className ); + }).join(" ") : + ""; + }, + + // internal only, use is(".class") + has: function( elem, className ) { + return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) + elem.style[ name ] = old[ name ]; + }, + + css: function( elem, name, force ) { + if ( name == "width" || name == "height" ) { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + var padding = 0, border = 0; + jQuery.each( which, function() { + padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + }); + val -= Math.round(padding + border); + } + + if ( jQuery(elem).is(":visible") ) + getWH(); + else + jQuery.swap( elem, props, getWH ); + + return Math.max(0, val); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret; + + // A helper method for determining if an element's values are broken + function color( elem ) { + if ( !jQuery.browser.safari ) + return false; + + var ret = document.defaultView.getComputedStyle( elem, null ); + return !ret || ret.getPropertyValue("color") == ""; + } + + // We need to handle opacity special in IE + if ( name == "opacity" && jQuery.browser.msie ) { + ret = jQuery.attr( elem.style, "opacity" ); + + return ret == "" ? + "1" : + ret; + } + // Opera sometimes will give the wrong display answer, this fixes it, see #2037 + if ( jQuery.browser.opera && name == "display" ) { + var save = elem.style.outline; + elem.style.outline = "0 solid black"; + elem.style.outline = save; + } + + // Make sure we're using the right name for getting the float value + if ( name.match( /float/i ) ) + name = styleFloat; + + if ( !force && elem.style && elem.style[ name ] ) + ret = elem.style[ name ]; + + else if ( document.defaultView && document.defaultView.getComputedStyle ) { + + // Only "float" is needed here + if ( name.match( /float/i ) ) + name = "float"; + + name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + + var getComputedStyle = document.defaultView.getComputedStyle( elem, null ); + + if ( getComputedStyle && !color( elem ) ) + ret = getComputedStyle.getPropertyValue( name ); + + // If the element isn't reporting its values properly in Safari + // then some display: none elements are involved + else { + var swap = [], stack = []; + + // Locate all of the parent display: none elements + for ( var a = elem; a && color(a); a = a.parentNode ) + stack.unshift(a); + + // Go through and make them visible, but in reverse + // (It would be better if we knew the exact display type that they had) + for ( var i = 0; i < stack.length; i++ ) + if ( color( stack[ i ] ) ) { + swap[ i ] = stack[ i ].style.display; + stack[ i ].style.display = "block"; + } + + // Since we flip the display style, we have to handle that + // one special, otherwise get the value + ret = name == "display" && swap[ stack.length - 1 ] != null ? + "none" : + ( getComputedStyle && getComputedStyle.getPropertyValue( name ) ) || ""; + + // Finally, revert the display styles back + for ( var i = 0; i < swap.length; i++ ) + if ( swap[ i ] != null ) + stack[ i ].style.display = swap[ i ]; + } + + // We should always get a number back from opacity + if ( name == "opacity" && ret == "" ) + ret = "1"; + + } else if ( elem.currentStyle ) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter){ + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var style = elem.style.left, runtimeStyle = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + elem.style.left = ret || 0; + ret = elem.style.pixelLeft + "px"; + + // Revert the changed values + elem.style.left = style; + elem.runtimeStyle.left = runtimeStyle; + } + } + + return ret; + }, + + clean: function( elems, context ) { + var ret = []; + context = context || document; + // !context.createElement fails in IE with an error but returns typeof 'object' + if (typeof context.createElement == 'undefined') + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + jQuery.each(elems, function(i, elem){ + if ( !elem ) + return; + + if ( elem.constructor == Number ) + elem = elem.toString(); + + // Convert html string into DOM nodes + if ( typeof elem == "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = jQuery.trim( elem ).toLowerCase(), div = context.createElement("div"); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
" ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and + + + $content + ,'type' => $type +)); +$he->encode(); +$he->sendAll(); + +?> \ No newline at end of file diff --git a/lib/Minify.php b/lib/Minify.php new file mode 100644 index 0000000..55059ec --- /dev/null +++ b/lib/Minify.php @@ -0,0 +1,364 @@ + + * and by the article "Supercharged JavaScript" by Patrick Hunlock + * . + * + * JSMin was originally written by Douglas Crockford . + * + * Requires PHP 5.2.1+. + * + * @package Minify + * @author Ryan Grove + * @author Stephen Clay + * @copyright 2007 Ryan Grove. All rights reserved. + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version 1.9.0 + * @link http://code.google.com/p/minify/ + */ + +require_once 'Minify/Source.php'; + +class Minify { + + /** + * @var bool Should the un-encoded version be cached? + * + * True results in more cache files, but lower PHP load if different + * encodings are commonly requested. + */ + public static $cacheUnencodedVersion = true; + + /** + * Specify a writeable directory for cache files. If not called, Minify + * will not use a disk cache and, for each 200 response, will need to + * recombine files, minify and encode the output. + * + * @param string $path Full directory path for cache files (should not end + * in directory separator character). If not provided, Minify will attempt to + * write to the path returned by sys_get_temp_dir(). + * + * @return null + */ + public static function useServerCache($path = null) { + self::$_cachePath = (null === $path) + ? sys_get_temp_dir() + : $path; + } + + /** + * Create a controller instance and handle the request + * + * @param string type This should be the filename of the controller without + * extension. e.g. 'Group' + * + * @param array $spec options for the controller's constructor + * + * @return mixed a Minify controller object + */ + public static function serve($type, $spec = array(), $options = array()) { + $class = 'Minify_Controller_' . $type; + if (! class_exists($class, false)) { + require_once "Minify/Controller/{$type}.php"; + } + $ctrl = new $class($spec, $options); + if (! self::handleRequest($ctrl)) { + header("HTTP/1.0 400 Bad Request"); + exit('400 Bad Request'); + } + } + + /** + * Handle a request for a minified file. + * + * You must supply a controller object which has the same public API + * as Minify_Controller. + * + * @param Minify_Controller $controller + * + * @return bool successfully sent a 304 or 200 with content + */ + public static function handleRequest($controller) { + if (! $controller->requestIsValid) { + return false; + } + self::$_controller = $controller; + self::_setOptions(); + + $cgOptions = array( + 'lastModifiedTime' => self::$_options['lastModifiedTime'] + ,'isPublic' => self::$_options['isPublic'] + ); + if (null !== self::$_options['cacheUntil']) { + $cgOptions['cacheUntil'] = self::$_options['cacheUntil']; + } + + // check client cache + require_once 'HTTP/ConditionalGet.php'; + $cg = new HTTP_ConditionalGet($cgOptions); + if ($cg->cacheIsValid) { + // client's cache is valid + $cg->sendHeaders(); + return true; + } + // client will need output + $headers = $cg->getHeaders(); + unset($cg); + + // determine encoding + if (self::$_options['encodeOutput']) { + if (self::$_options['encodeMethod'] !== null) { + // controller specifically requested this + $contentEncoding = self::$_options['encodeMethod']; + } else { + // sniff request header + require_once 'HTTP/Encoder.php'; + // depending on what the client accepts, $contentEncoding may be + // 'x-gzip' while our internal encodeMethod is 'gzip' + list(self::$_options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding(); + } + } else { + self::$_options['encodeMethod'] = ''; // identity (no encoding) + } + + if (null !== self::$_cachePath) { + self::_setupCache(); + // fetch content from cache file(s). + $content = self::_fetchContent(self::$_options['encodeMethod']); + self::$_cache = null; + } else { + // no cache, just combine, minify, encode + $content = self::_combineMinify(); + $content = self::_encode($content); + } + + // add headers to those from ConditionalGet + //$headers['Content-Length'] = strlen($content); + $headers['Content-Type'] = (null !== self::$_options['contentTypeCharset']) + ? self::$_options['contentType'] . ';charset=' . self::$_options['contentTypeCharset'] + : self::$_options['contentType']; + if (self::$_options['encodeMethod'] !== '') { + $headers['Content-Encoding'] = $contentEncoding; + $headers['Vary'] = 'Accept-Encoding'; + } + + // output headers & content + foreach ($headers as $name => $val) { + header($name . ': ' . $val); + } + echo $content; + return true; + } + + /** + * @var mixed null if disk cache is not to be used + */ + private static $_cachePath = null; + + /** + * @var Minify_Controller active controller for current request + */ + private static $_controller = null; + + /** + * @var array options for current request + */ + private static $_options = null; + + /** + * @var Cache_Lite_File cache obj for current request + */ + private static $_cache = null; + + /** + * Set class options based on controller's options and defaults + * + * @return null + */ + private static function _setOptions() + { + $given = self::$_controller->options; + self::$_options = array_merge(array( + // default options + 'isPublic' => true + ,'encodeOutput' => true + ,'encodeMethod' => null // determine later + ,'encodeLevel' => 9 + ,'perType' => array() // per-type minifier options + ,'contentTypeCharset' => null // leave out of Content-Type header + ,'cacheUntil' => null + ), $given); + $defaultMinifiers = array( + 'text/css' => array('Minify_CSS', 'minify') + ,'application/x-javascript' => array('Minify_Javascript', 'minify') + ,'text/html' => array('Minify_HTML', 'minify') + ); + if (! isset($given['minifiers'])) { + $given['minifiers'] = array(); + } + self::$_options['minifiers'] = array_merge($defaultMinifiers, $given['minifiers']); + } + + /** + * Fetch encoded content from cache (or generate and store it). + * + * If self::$cacheUnencodedVersion is true and encoded content must be + * generated, this function will call itself recursively to fetch (or + * generate) the minified content. Otherwise, it will always recombine + * and reminify files to generate different encodings. + * + * @param string $encodeMethod + * + * @return string minified, encoded content + */ + private static function _fetchContent($encodeMethod) + { + $cacheId = self::_getCacheId(self::$_controller->sources, self::$_options) + . $encodeMethod; + $content = self::$_cache->get($cacheId, 'Minify'); + if (false === $content) { + // must generate + if ($encodeMethod === '') { + // generate identity cache to store + $content = self::_combineMinify(); + } else { + // fetch identity cache & encode it to store + if (self::$cacheUnencodedVersion) { + // double layer cache + $content = self::_fetchContent(''); + } else { + // recombine + $content = self::_combineMinify(); + } + $content = self::_encode($content); + } + self::$_cache->save($content, $cacheId, 'Minify'); + } + return $content; + } + + /** + * Set self::$_cache to a new instance of Cache_Lite_File (patched 2007-10-03) + * + * @return null + */ + private static function _setupCache() { + // until the patch is rolled into PEAR, we'll provide the + // class in our package + require_once dirname(__FILE__) . '/Cache/Lite/File.php'; + + self::$_cache = new Cache_Lite_File(array( + 'cacheDir' => self::$_cachePath . '/' + ,'fileNameProtection' => false + + // currently only available in patched Cache_Lite_File + ,'masterTime' => self::$_options['lastModifiedTime'] + )); + } + + /** + * Combines sources and minifies the result. + * + * @return string + */ + private static function _combineMinify() { + $type = self::$_options['contentType']; // ease readability + + // when combining scripts, make sure all statements separated + $implodeSeparator = ($type === 'application/x-javascript') + ? ';' + : ''; + + // default options and minifier function for all sources + $defaultOptions = isset(self::$_options['perType'][$type]) + ? self::$_options['perType'][$type] + : array(); + $defaultMinifier = isset(self::$_options['minifiers'][$type]) + ? self::$_options['minifiers'][$type] + : array('Minify', '_trim'); + + if (Minify_Source::haveNoMinifyPrefs(self::$_controller->sources)) { + // all source have same options/minifier, better performance + foreach (self::$_controller->sources as $source) { + $pieces[] = $source->getContent(); + } + $content = implode($implodeSeparator, $pieces); + self::$_controller->loadMinifier($defaultMinifier); + $content = call_user_func($defaultMinifier, $content, $defaultOptions); + } else { + // minify each source with its own options and minifier + foreach (self::$_controller->sources as $source) { + // allow the source to override our minifier and options + $minifier = (null !== $source->minifier) + ? $source->minifier + : $defaultMinifier; + $options = (null !== $source->minifyOptions) + ? array_merge($defaultOptions, $source->minifyOptions) + : $defaultOptions; + self::$_controller->loadMinifier($minifier); + // get source content and minify it + $pieces[] = call_user_func($minifier, $source->getContent(), $options); + } + $content = implode($implodeSeparator, $pieces); + } + return $content; + } + + /** + * Applies HTTP encoding + * + * @param string $content + * + * @return string + */ + private static function _encode($content) + { + if (self::$_options['encodeMethod'] === '' + || ! self::$_options['encodeOutput']) { + // "identity" encoding + return $content; + } + require_once 'HTTP/Encoder.php'; + $encoder = new HTTP_Encoder(array( + 'content' => $content + ,'method' => self::$_options['encodeMethod'] + )); + $encoder->encode(self::$_options['encodeLevel']); + return $encoder->getContent(); + } + + /** + * Make a unique cache id for for this request. + * + * Any settings that could affect output are taken into consideration + * + * @return string + */ + private static function _getCacheId() { + return md5(serialize(array( + Minify_Source::getDigest(self::$_controller->sources) + ,self::$_options['minifiers'] + ,self::$_options['perType'] + ))); + } + + /** + * The default minifier if content-type has no minifier + * + * This is necessary because trim() throws notices when you send in options + * as a 2nd arg. + * + * @param string $content + * + * @return string + */ + private static function _trim($content, $options) + { + return trim($content); + } +} diff --git a/lib/Minify/CSS.php b/lib/Minify/CSS.php new file mode 100644 index 0000000..a000cfc --- /dev/null +++ b/lib/Minify/CSS.php @@ -0,0 +1,180 @@ +' + // http://www.webdevout.net/css-hacks#in_css-selectors + $css = preg_replace('/>\\/\\*\\s*\\*\\//', '>/*keep*/', $css); + + // preserve empty comment between property and value + // http://css-discuss.incutio.com/?page=BoxModelHack + $css = preg_replace('/\\/\\*\\s*\\*\\/\\s*:/', '/*keep*/:', $css); + $css = preg_replace('/:\\s*\\/\\*\\s*\\*\\//', ':/*keep*/', $css); + + // apply callback to all valid comments (and strip out surrounding ws + self::$_inHack = false; + $css = preg_replace_callback('/\\s*\\/\\*([\\s\\S]*?)\\*\\/\\s*/' + ,array('Minify_CSS', '_commentCB'), $css); + + // compress whitespace. Yes, this will affect "copyright" comments. + $css = preg_replace('/\s+/', ' ', $css); + + // leave needed comments + $css = str_replace('/*keep*/', '/**/', $css); + + // remove ws around { } + $css = preg_replace('/\\s*{\\s*/', '{', $css); + $css = preg_replace('/;?\\s*}\\s*/', '}', $css); + + // remove ws between rules + $css = preg_replace('/\\s*;\\s*/', ';', $css); + + // remove ws around urls + $css = preg_replace('/url\\([\\s]*([^\\)]+?)[\\s]*\\)/', 'url($1)', $css); + + // remove ws between rules and colons + $css = preg_replace('/\\s*([{;])\\s*([\\w\\-]+)\\s*:\\s*\\b/', '$1$2:', $css); + + // remove ws in selectors + $css = preg_replace_callback('/(?:\\s*[^~>+,\\s]+\\s*[,>+~])+\\s*[^~>+,\\s]+{/' + ,array('Minify_CSS', '_selectorsCB'), $css); + + // minimize hex colors + $css = preg_replace('/#([a-f\\d])\\1([a-f\\d])\\2([a-f\\d])\\3([\\s;\\}])/i' + , '#$1$2$3$4', $css); + + if (isset($options['prependRelativePath'])) { + self::$_tempPrepend = $options['prependRelativePath']; + $css = preg_replace_callback('/@import ([\'"])(.*?)[\'"]\\s*;/' + ,array('Minify_CSS', '_urlCB'), $css); + + $css = preg_replace_callback('/url\\(([^\\)]+)\\)/' + ,array('Minify_CSS', '_urlCB'), $css); + } + + return trim($css); + } + + /** + * @var bool Are we "in" a hack? + * + * I.e. are some browsers targetted until the next comment? + */ + private static $_inHack = false; + + /** + * @var string string to be prepended to relative URIs + */ + private static $_tempPrepend = ''; + + /** + * Process what looks like a comment and return a replacement + * + * @param array $m regex matches + * + * @return string + */ + private static function _commentCB($m) + { + $m = $m[1]; + // $m is everything after the opening tokens and before the closing tokens + // but return will replace the entire comment. + if ($m === 'keep') { + return '/*keep*/'; + } + if (false !== strpos($m, 'copyright')) { + // contains copyright, preserve + self::$_inHack = false; + return "/*{$m}*/"; + } + if (self::$_inHack) { + // inversion: feeding only to one browser + if (preg_match('/^\\/\\s*(\\S[\\s\\S]+?)\\s*\\/\\*/', $m, $n)) { + self::$_inHack = false; + return "/*/{$n[1]}/*keep*/"; + } + } + if (substr($m, -1) === '\\') { + self::$_inHack = true; + return '/*\\*/'; + } + if (substr($m, 0, 1) === '/') { + self::$_inHack = true; + return '/*/*/'; + } + if (self::$_inHack) { + self::$_inHack = false; + return '/*keep*/'; + } + return ''; + } + + /** + * Replace what looks like a set of selectors + * + * @param array $m regex matches + * + * @return string + */ + private static function _selectorsCB($m) + { + return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]); + } + + private static function _urlCB($m) + { + $isImport = (0 === strpos($m[0], '@import')); + if ($isImport) { + $quote = $m[1]; + $url = $m[2]; + } else { + // $m[1] is surrounded by quotes or not + $quote = ($m[1][0] === '\'' || $m[1][0] === '"') + ? $m[1][0] + : ''; + $url = ($quote === '') + ? $m[1] + : substr($m[1], 1, strlen($m[1]) - 2); + } + if ('/' === $url[0]) { + if ('/' === $url[1]) { + // protocol relative URI! + $url = '//' . self::$_tempPrepend . substr($url, 2); + } + } else { + if (strpos($url, '//') > 0) { + // probably starts with protocol, do not alter + } else { + // relative URI + $url = self::$_tempPrepend . $url; + } + } + if ($isImport) { + return "@import {$quote}{$url}{$quote};"; + } else { + return "url({$quote}{$url}{$quote})"; + } + } +} + diff --git a/lib/Minify/Controller/Base.php b/lib/Minify/Controller/Base.php new file mode 100644 index 0000000..58dff4f --- /dev/null +++ b/lib/Minify/Controller/Base.php @@ -0,0 +1,129 @@ +options['minifiers']['application/x-javascript'] = 'myJsPacker'; + * Note that, when providing your own minifier, the controller must be able + * to load its code on demand. @see loadMinifier() + * + * 'perType' : this is an array of options to send to a particular content + * type minifier by using the content-type as key. E.g. To send the CSS + * minifier an option: $options['perType']['text/css']['foo'] = 'bar'; + * When the CSS minifier is called, the 2nd argument will be + * array('foo' => 'bar'). + * + * 'isPublic' : send "public" instead of "private" in Cache-Control headers, + * allowing shared caches to cache the output. (default true) + * + * 'encodeOutput' : to disable content encoding, set this to false + * + * 'encodeMethod' : generally you should let this be determined by + * HTTP_Encoder (the default null), but you can force a particular encoding + * to be returned, by setting this to 'gzip', 'deflate', 'compress', or '' + * (no encoding) + * + * 'encodeLevel' : level of encoding compression (0 to 9, default 9) + * + * 'contentTypeCharset' : if given, this will be appended to the Content-Type + * header sent, useful mainly for HTML docs. + * + * 'cacheUntil' : set this to a timestamp or GMT date to have Minify send + * an HTTP Expires header instead of checking for conditional GET. + * E.g. (time() + 86400 * 365) for 1yr (default null) + * This has nothing to do with server-side caching. + * + */ + public $options = array(); + + /** + * @var bool was the user request valid + * + * This must be explicity be set to true to process the request. This should + * be done by the child class constructor. + */ + public $requestIsValid = false; + + /** + * Parent constructor for a controller class + * + * Generally you'll call this at the end of your child class constructor: + * + * parent::__construct($sources, $options); + * + * + * This function sets the sources and determines the 'contentType' and + * 'lastModifiedTime', if not given. + * + * If no sources are provided, $this->requestIsValid will be set to false. + * + * @param array $sources array of instances of Minify_Source + * + * @param array $options + * + * @return null + */ + public function __construct($sources, $options = array()) { + if (empty($sources)) { + $this->requestIsValid = false; + } + $this->sources = $sources; + if (! isset($options['contentType'])) { + $options['contentType'] = Minify_Source::getContentType($this->sources); + } + // last modified is needed for caching, even if cacheUntil is set + if (! isset($options['lastModifiedTime'])) { + $max = 0; + foreach ($sources as $source) { + $max = max($source->lastModified, $max); + } + $options['lastModifiedTime'] = $max; + } + $this->options = $options; + } + + /** + * Load any code necessary to execute the given minifier callback. + * + * The controller is responsible for loading minification code on demand + * via this method. This built-in function will only load classes for + * static method callbacks where the class isn't already defined. It uses + * the PEAR convention, so, given array('Jimmy_Minifier', 'minCss'), this + * function will include 'Jimmy/Minifier.php' + * + * If you need code loaded on demand and this doesn't suit you, you'll need + * to override this function by extending the class. + * + * @return null + */ + public function loadMinifier($minifierCallback) + { + if (is_array($minifierCallback) + && is_string($minifierCallback[0]) + && !class_exists($minifierCallback[0], false)) { + + require str_replace('_', '/', $minifierCallback[0]) . '.php'; + } + } +} diff --git a/lib/Minify/Controller/Files.php b/lib/Minify/Controller/Files.php new file mode 100644 index 0000000..e2d7700 --- /dev/null +++ b/lib/Minify/Controller/Files.php @@ -0,0 +1,46 @@ + + * $dr = $_SERVER['DOCUMENT_ROOT']; + * Minify::minify('Files', array( + * $dr . '/js/jquery.js' + * ,$dr . '/js/plugins.js' + * ,$dr . '/js/site.js' + * )); + * + * + */ +class Minify_Controller_Files extends Minify_Controller_Base { + + /** + * @param array $spec array or full paths of files to be minified + * + * @param array $options optional options to pass to Minify + * + * @return null + */ + public function __construct($spec, $options = array()) { + $sources = array(); + foreach ($spec as $file) { + $file = realpath($file); + if (file_exists($file)) { + $sources[] = new Minify_Source(array( + 'filepath' => $file + )); + } else { + return; + } + } + if ($sources) { + $this->requestIsValid = true; + } + parent::__construct($sources, $options); + } +} + diff --git a/lib/Minify/Controller/Groups.php b/lib/Minify/Controller/Groups.php new file mode 100644 index 0000000..5e3d741 --- /dev/null +++ b/lib/Minify/Controller/Groups.php @@ -0,0 +1,59 @@ + + * $dr = $_SERVER['DOCUMENT_ROOT']; + * Minify::minify('Groups', array( + * 'css' => array( + * $dr . '/css/type.css' + * ,$dr . '/css/layout.css' + * ) + * ,'js' => array( + * $dr . '/js/jquery.js' + * ,$dr . '/js/plugins.js' + * ,$dr . '/js/site.js' + * ) + * )); + * + * + * If the above code were placed in /serve.php, it would enable the URLs + * /serve.php/js and /serve.php/css + */ +class Minify_Controller_Groups extends Minify_Controller_Base { + + /** + * @param array $spec associative array of keys to arrays of file paths. + * + * @param array $options optional options to pass to Minify + * + * @return null + */ + public function __construct($spec, $options = array()) { + $pi = substr($_SERVER['PATH_INFO'], 1); + if (! isset($spec[$pi])) { + // not a valid group + return; + } + $sources = array(); + foreach ($spec[$pi] as $file) { + $file = realpath($file); + if (file_exists($file)) { + $sources[] = new Minify_Source(array( + 'filepath' => $file + )); + } else { + return; + } + } + if ($sources) { + $this->requestIsValid = true; + } + parent::__construct($sources, $options); + } +} + diff --git a/lib/Minify/Controller/Page.php b/lib/Minify/Controller/Page.php new file mode 100644 index 0000000..cf0b758 --- /dev/null +++ b/lib/Minify/Controller/Page.php @@ -0,0 +1,61 @@ + + * $dr = $_SERVER['DOCUMENT_ROOT']; + * Minify::minify('Files', array( + * $dr . '/js/jquery.js' + * ,$dr . '/js/plugins.js' + * ,$dr . '/js/site.js' + * )); + * + * + */ +class Minify_Controller_Page extends Minify_Controller_Base { + + /** + * + * + * @param array $options optional options to pass to Minify + * + * @return null + */ + public function __construct($spec, $options = array()) { + $sourceSpec = array( + 'content' => $spec['content'] + ,'id' => $spec['id'] + ,'minifier' => array('Minify_HTML', 'minify') + ); + if (isset($spec['minifyAll'])) { + $sourceSpec['minifyOptions'] = array( + 'cssMinifier' => array('Minify_CSS', 'minify') + ,'jsMinifier' => array('Minify_Javascript', 'minify') + ); + $this->_loadCssJsMinifiers = true; + } + $sources[] = new Minify_Source($sourceSpec); + if (isset($spec['lastModifiedTime'])) { + $options['lastModifiedTime'] = $spec['lastModifiedTime']; + } + $options['contentType'] = 'text/html'; + $this->requestIsValid = true; + parent::__construct($sources, $options); + } + + private $_loadCssJsMinifiers = false; + + public function loadMinifier($minifierCallback) + { + if ($this->_loadCssJsMinifiers) { + require 'Minify/CSS.php'; + require 'Minify/Javascript.php'; + } + parent::loadMinifier($minifierCallback); + } +} + diff --git a/lib/Minify/HTML.php b/lib/Minify/HTML.php new file mode 100644 index 0000000..defb88a --- /dev/null +++ b/lib/Minify/HTML.php @@ -0,0 +1,162 @@ +]*?>)([\\s\\S]*?)<\\/script>\\s*/i', + array('Minify_HTML', '_removeScriptCB'), $html); + + // remove STYLEs (and minify) + $html = preg_replace_callback('/\\s*(]*?>)([\\s\\S]*?)<\\/style>\\s*/i', + array('Minify_HTML', '_removeStyleCB'), $html); + + // remove HTML comments (but not IE conditional comments). + $html = preg_replace('//', '', $html); + + // replace PREs with token text + self::$_pres = array(); + $html = preg_replace_callback('/\\s*(]*?>[\\s\\S]*?<\\/pre>)\\s*/i' + ,array('Minify_HTML', '_removePreCB') + , $html); + + // remove leading and trailing ws from each line. + // @todo take into account attribute values that span multiple lines. + $html = preg_replace('/^\\s*(.*?)\\s*$/m', "$1", $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)|title' + .'|ul)\\b[^>]*>)/i', '$1', $html); + + // remove ws between and inside elements. + $html = preg_replace('/>\\s+(\\S[\\s\\S]*?)? $1<", $html); + $html = preg_replace('/>(\\S[\\s\\S]*?)?\\s+$1 <", $html); + $html = preg_replace('/>\\s+ <", $html); + + // replace PREs + $i = count(self::$_pres); + while ($i > 0) { + $rep = array_pop(self::$_pres); + $html = str_replace(self::$_replacementHash . 'PRE' . $i, $rep, $html); + $i--; + } + + // replace SCRIPTs + $i = count(self::$_scripts); + while ($i > 0) { + $rep = array_pop(self::$_scripts); + $html = str_replace(self::$_replacementHash . 'SCRIPT' . $i, $rep, $html); + $i--; + } + + // replace STYLEs + $i = count(self::$_styles); + while ($i > 0) { + $rep = array_pop(self::$_styles); + $html = str_replace(self::$_replacementHash . 'STYLE' . $i, $rep, $html); + $i--; + } + + self::$_cssMinifier = self::$_jsMinifier = null; + return $html; + } + + private static $_isXhtml = false; + private static $_replacementHash = null; + private static $_pres = array(); + private static $_scripts = array(); + private static $_styles = array(); + private static $_cssMinifier = null; + private static $_jsMinifier = null; + + private static function _removePreCB($m) + { + self::$_pres[] = $m[1]; + return self::$_replacementHash . 'PRE' . count(self::$_pres); + } + + private 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); + + // store + self::$_styles[] = self::_needsCdata($css) + ? "{$openStyle}/**/" + : "{$openStyle}{$css}"; + + + return self::$_replacementHash . 'STYLE' . count(self::$_styles); + } + + private static function _removeScriptCB($m) + { + $openScript = $m[1]; + $js = $m[2]; + + // remove HTML comments (and ending "//" if present) + $js = preg_replace('/(?:^\\s*\\s*$)/', '', $js); + + // remove CDATA section markers + $js = self::_removeCdata($js); + + // minify + $minifier = self::$_jsMinifier + ? self::$_jsMinifier + : 'trim'; + $js = call_user_func($minifier, $js); + + // store + self::$_scripts[] = self::_needsCdata($js) + ? "{$openScript}/**/" + : "{$openScript}{$js}"; + return self::$_replacementHash . 'SCRIPT' . count(self::$_scripts); + } + + private static function _removeCdata($str) + { + return (false !== strpos($str, ''), '', $str) + : $str; + } + + private static function _needsCdata($str) + { + return (self::$_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str)); + } +} + diff --git a/lib/jsmin.php b/lib/Minify/Javascript.php similarity index 77% rename from lib/jsmin.php rename to lib/Minify/Javascript.php index 050b7a4..047b9d6 100644 --- a/lib/jsmin.php +++ b/lib/Minify/Javascript.php @@ -1,16 +1,16 @@ - * @copyright 2002 Douglas Crockford (jsmin.c) + * @copyright 2002 Douglas Crockford (JSMin.c) * @copyright 2007 Ryan Grove (PHP port) * @license http://opensource.org/licenses/mit-license.php MIT License * @version 1.1.0 (2007-06-01) * @link http://code.google.com/p/jsmin-php/ */ -class JSMin { +class Minify_Javascript { const ORD_LF = 10; const ORD_SPACE = 32; - protected $a = ''; - protected $b = ''; - protected $input = ''; - protected $inputIndex = 0; - protected $inputLength = 0; - protected $lookAhead = null; - protected $output = array(); + private $a = ''; + private $b = ''; + private $input = ''; + private $inputIndex = 0; + private $inputLength = 0; + private $lookAhead = null; + private $output = array(); // -- Public Static Methods -------------------------------------------------- - public static function minify($js) { - $jsmin = new JSMin($js); - return $jsmin->min(); + public static function minify($js, $options = array()) { + $js = new Minify_Javascript($js); + return trim($js->min()); } - // -- Public Instance Methods ------------------------------------------------ + // -- Private Instance Methods --------------------------------------------- - public function __construct($input) { + private function __construct($input) { $this->input = str_replace("\r\n", "\n", $input); $this->inputLength = strlen($this->input); } - // -- Protected Instance Methods --------------------------------------------- - - protected function action($d) { + private function action($d) { switch($d) { case 1: $this->output[] = $this->a; @@ -91,7 +89,7 @@ class JSMin { } if (ord($this->a) <= self::ORD_LF) { - throw new JSMinException('Unterminated string literal.'); + throw new Minify_JavascriptException('Unterminated string literal.'); } if ($this->a === '\\') { @@ -123,7 +121,7 @@ class JSMin { $this->a = $this->get(); } elseif (ord($this->a) <= self::ORD_LF) { - throw new JSMinException('Unterminated regular expression '. + throw new Minify_JavascriptException('Unterminated regular expression '. 'literal.'); } @@ -135,7 +133,7 @@ class JSMin { } } - protected function get() { + private function get() { $c = $this->lookAhead; $this->lookAhead = null; @@ -160,18 +158,14 @@ class JSMin { return ' '; } - protected function isAlphaNum($c) { - return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1; - } - - protected function min() { + private function min() { $this->a = "\n"; $this->action(3); while ($this->a !== null) { switch ($this->a) { case ' ': - if ($this->isAlphaNum($this->b)) { + if (self::isAlphaNum($this->b)) { $this->action(1); } else { @@ -194,7 +188,7 @@ class JSMin { break; default: - if ($this->isAlphaNum($this->b)) { + if (self::isAlphaNum($this->b)) { $this->action(1); } else { @@ -206,7 +200,7 @@ class JSMin { default: switch ($this->b) { case ' ': - if ($this->isAlphaNum($this->a)) { + if (self::isAlphaNum($this->a)) { $this->action(1); break; } @@ -227,7 +221,7 @@ class JSMin { break; default: - if ($this->isAlphaNum($this->a)) { + if (self::isAlphaNum($this->a)) { $this->action(1); } else { @@ -246,7 +240,7 @@ class JSMin { return implode('', $this->output); } - protected function next() { + private function next() { $c = $this->get(); if ($c === '/') { @@ -273,7 +267,7 @@ class JSMin { break; case null: - throw new JSMinException('Unterminated comment.'); + throw new Minify_JavascriptException('Unterminated comment.'); } } @@ -285,12 +279,18 @@ class JSMin { return $c; } - protected function peek() { + private function peek() { $this->lookAhead = $this->get(); return $this->lookAhead; } + + // Private static functions -------------------------------------------------- + + private static function isAlphaNum($c) { + return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1; + } } // -- Exceptions --------------------------------------------------------------- -class JSMinException extends Exception {} -?> \ No newline at end of file +class Minify_JavascriptException extends Exception {} + diff --git a/lib/Minify/Packer.php b/lib/Minify/Packer.php new file mode 100644 index 0000000..defc61e --- /dev/null +++ b/lib/Minify/Packer.php @@ -0,0 +1,745 @@ +pack(); + * + * or + * + * $myPacker = new JavaScriptPacker($script, 'Normal', true, false); + * $packed = $myPacker->pack(); + * + * or (default values) + * + * $myPacker = new JavaScriptPacker($script); + * $packed = $myPacker->pack(); + * + * + * params of the constructor : + * $script: the JavaScript to pack, string. + * $encoding: level of encoding, int or string : + * 0,10,62,95 or 'None', 'Numeric', 'Normal', 'High ASCII'. + * default: 62. + * $fastDecode: include the fast decoder in the packed result, boolean. + * default : true. + * $specialChars: if you are flagged your private and local variables + * in the script, boolean. + * default: false. + * + * The pack() method return the compressed JavasScript, as a string. + * + * see http://dean.edwards.name/packer/usage/ for more information. + * + * Notes : + * # need PHP 5 . Tested with PHP 5.1.2 + * + * # The packed result may be different than with the Dean Edwards + * version, but with the same length. The reason is that the PHP + * function usort to sort array don't necessarily preserve the + * original order of two equal member. The Javascript sort function + * in fact preserve this order (but that's not require by the + * ECMAScript standard). So the encoded keywords order can be + * different in the two results. + * + * # Be careful with the 'High ASCII' Level encoding if you use + * UTF-8 in your files... + */ + + +class JavaScriptPacker { + // constants + const IGNORE = '$1'; + + // validate parameters + private $_script = ''; + private $_encoding = 62; + private $_fastDecode = true; + private $_specialChars = false; + + private $LITERAL_ENCODING = array( + 'None' => 0, + 'Numeric' => 10, + 'Normal' => 62, + 'High ASCII' => 95 + ); + + public function __construct($_script, $_encoding = 62, $_fastDecode = true, $_specialChars = false) + { + $this->_script = $_script . "\n"; + if (array_key_exists($_encoding, $this->LITERAL_ENCODING)) + $_encoding = $this->LITERAL_ENCODING[$_encoding]; + $this->_encoding = min((int)$_encoding, 95); + $this->_fastDecode = $_fastDecode; + $this->_specialChars = $_specialChars; + } + + public function pack() { + $this->_addParser('_basicCompression'); + if ($this->_specialChars) + $this->_addParser('_encodeSpecialChars'); + if ($this->_encoding) + $this->_addParser('_encodeKeywords'); + + // go! + return $this->_pack($this->_script); + } + + // apply all parsing routines + private function _pack($script) { + for ($i = 0; isset($this->_parsers[$i]); $i++) { + $script = call_user_func(array(&$this,$this->_parsers[$i]), $script); + } + return $script; + } + + // keep a list of parsing functions, they'll be executed all at once + private $_parsers = array(); + private function _addParser($parser) { + $this->_parsers[] = $parser; + } + + // zero encoding - just removal of white space and comments + private function _basicCompression($script) { + $parser = new ParseMaster(); + // make safe + $parser->escapeChar = '\\'; + // protect strings + $parser->add('/\'[^\'\\n\\r]*\'/', self::IGNORE); + $parser->add('/"[^"\\n\\r]*"/', self::IGNORE); + // remove comments + $parser->add('/\\/\\/[^\\n\\r]*[\\n\\r]/', ' '); + $parser->add('/\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\//', ' '); + // protect regular expressions + $parser->add('/\\s+(\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?)/', '$2'); // IGNORE + $parser->add('/[^\\w\\x24\\/\'"*)\\?:]\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?/', self::IGNORE); + // remove: ;;; doSomething(); + if ($this->_specialChars) $parser->add('/;;;[^\\n\\r]+[\\n\\r]/'); + // remove redundant semi-colons + $parser->add('/\\(;;\\)/', self::IGNORE); // protect for (;;) loops + $parser->add('/;+\\s*([};])/', '$2'); + // apply the above + $script = $parser->exec($script); + + // remove white-space + $parser->add('/(\\b|\\x24)\\s+(\\b|\\x24)/', '$2 $3'); + $parser->add('/([+\\-])\\s+([+\\-])/', '$2 $3'); + $parser->add('/\\s+/', ''); + // done + return $parser->exec($script); + } + + private function _encodeSpecialChars($script) { + $parser = new ParseMaster(); + // replace: $name -> n, $$name -> na + $parser->add('/((\\x24+)([a-zA-Z$_]+))(\\d*)/', + array('fn' => '_replace_name') + ); + // replace: _name -> _0, double-underscore (__name) is ignored + $regexp = '/\\b_[A-Za-z\\d]\\w*/'; + // build the word list + $keywords = $this->_analyze($script, $regexp, '_encodePrivate'); + // quick ref + $encoded = $keywords['encoded']; + + $parser->add($regexp, + array( + 'fn' => '_replace_encoded', + 'data' => $encoded + ) + ); + return $parser->exec($script); + } + + private function _encodeKeywords($script) { + // escape high-ascii values already in the script (i.e. in strings) + if ($this->_encoding > 62) + $script = $this->_escape95($script); + // create the parser + $parser = new ParseMaster(); + $encode = $this->_getEncoder($this->_encoding); + // for high-ascii, don't encode single character low-ascii + $regexp = ($this->_encoding > 62) ? '/\\w\\w+/' : '/\\w+/'; + // build the word list + $keywords = $this->_analyze($script, $regexp, $encode); + $encoded = $keywords['encoded']; + + // encode + $parser->add($regexp, + array( + 'fn' => '_replace_encoded', + 'data' => $encoded + ) + ); + if (empty($script)) return $script; + else { + //$res = $parser->exec($script); + //$res = $this->_bootStrap($res, $keywords); + //return $res; + return $this->_bootStrap($parser->exec($script), $keywords); + } + } + + private function _analyze($script, $regexp, $encode) { + // analyse + // retreive all words in the script + $all = array(); + preg_match_all($regexp, $script, $all); + $_sorted = array(); // list of words sorted by frequency + $_encoded = array(); // dictionary of word->encoding + $_protected = array(); // instances of "protected" words + $all = $all[0]; // simulate the javascript comportement of global match + if (!empty($all)) { + $unsorted = array(); // same list, not sorted + $protected = array(); // "protected" words (dictionary of word->"word") + $value = array(); // dictionary of charCode->encoding (eg. 256->ff) + $this->_count = array(); // word->count + $i = count($all); $j = 0; //$word = null; + // count the occurrences - used for sorting later + do { + --$i; + $word = '$' . $all[$i]; + if (!isset($this->_count[$word])) { + $this->_count[$word] = 0; + $unsorted[$j] = $word; + // make a dictionary of all of the protected words in this script + // these are words that might be mistaken for encoding + //if (is_string($encode) && method_exists($this, $encode)) + $values[$j] = call_user_func(array(&$this, $encode), $j); + $protected['$' . $values[$j]] = $j++; + } + // increment the word counter + $this->_count[$word]++; + } while ($i > 0); + // prepare to sort the word list, first we must protect + // words that are also used as codes. we assign them a code + // equivalent to the word itself. + // e.g. if "do" falls within our encoding range + // then we store keywords["do"] = "do"; + // this avoids problems when decoding + $i = count($unsorted); + do { + $word = $unsorted[--$i]; + if (isset($protected[$word]) /*!= null*/) { + $_sorted[$protected[$word]] = substr($word, 1); + $_protected[$protected[$word]] = true; + $this->_count[$word] = 0; + } + } while ($i); + + // sort the words by frequency + // Note: the javascript and php version of sort can be different : + // in php manual, usort : + // " If two members compare as equal, + // their order in the sorted array is undefined." + // so the final packed script is different of the Dean's javascript version + // but equivalent. + // the ECMAscript standard does not guarantee this behaviour, + // and thus not all browsers (e.g. Mozilla versions dating back to at + // least 2003) respect this. + usort($unsorted, array(&$this, '_sortWords')); + $j = 0; + // because there are "protected" words in the list + // we must add the sorted words around them + do { + if (!isset($_sorted[$i])) + $_sorted[$i] = substr($unsorted[$j++], 1); + $_encoded[$_sorted[$i]] = $values[$i]; + } while (++$i < count($unsorted)); + } + return array( + 'sorted' => $_sorted, + 'encoded' => $_encoded, + 'protected' => $_protected); + } + + private $_count = array(); + private function _sortWords($match1, $match2) { + return $this->_count[$match2] - $this->_count[$match1]; + } + + // build the boot function used for loading and decoding + private function _bootStrap($packed, $keywords) { + $ENCODE = $this->_safeRegExp('$encode\\($count\\)'); + + // $packed: the packed script + $packed = "'" . $this->_escape($packed) . "'"; + + // $ascii: base for encoding + $ascii = min(count($keywords['sorted']), $this->_encoding); + if ($ascii == 0) $ascii = 1; + + // $count: number of words contained in the script + $count = count($keywords['sorted']); + + // $keywords: list of words contained in the script + foreach ($keywords['protected'] as $i=>$value) { + $keywords['sorted'][$i] = ''; + } + // convert from a string to an array + ksort($keywords['sorted']); + $keywords = "'" . implode('|',$keywords['sorted']) . "'.split('|')"; + + $encode = ($this->_encoding > 62) ? '_encode95' : $this->_getEncoder($ascii); + $encode = $this->_getJSFunction($encode); + $encode = preg_replace('/_encoding/','$ascii', $encode); + $encode = preg_replace('/arguments\\.callee/','$encode', $encode); + $inline = '\\$count' . ($ascii > 10 ? '.toString(\\$ascii)' : ''); + + // $decode: code snippet to speed up decoding + if ($this->_fastDecode) { + // create the decoder + $decode = $this->_getJSFunction('_decodeBody'); + if ($this->_encoding > 62) + $decode = preg_replace('/\\\\w/', '[\\xa1-\\xff]', $decode); + // perform the encoding inline for lower ascii values + elseif ($ascii < 36) + $decode = preg_replace($ENCODE, $inline, $decode); + // special case: when $count==0 there are no keywords. I want to keep + // the basic shape of the unpacking funcion so i'll frig the code... + if ($count == 0) + $decode = preg_replace($this->_safeRegExp('($count)\\s*=\\s*1'), '$1=0', $decode, 1); + } + + // boot function + $unpack = $this->_getJSFunction('_unpack'); + if ($this->_fastDecode) { + // insert the decoder + $this->buffer = $decode; + $unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastDecode'), $unpack, 1); + } + $unpack = preg_replace('/"/', "'", $unpack); + if ($this->_encoding > 62) { // high-ascii + // get rid of the word-boundaries for regexp matches + $unpack = preg_replace('/\'\\\\\\\\b\'\s*\\+|\\+\s*\'\\\\\\\\b\'/', '', $unpack); + } + if ($ascii > 36 || $this->_encoding > 62 || $this->_fastDecode) { + // insert the encode function + $this->buffer = $encode; + $unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastEncode'), $unpack, 1); + } else { + // perform the encoding inline + $unpack = preg_replace($ENCODE, $inline, $unpack); + } + // pack the boot function too + $unpackPacker = new JavaScriptPacker($unpack, 0, false, true); + $unpack = $unpackPacker->pack(); + + // arguments + $params = array($packed, $ascii, $count, $keywords); + if ($this->_fastDecode) { + $params[] = 0; + $params[] = '{}'; + } + $params = implode(',', $params); + + // the whole thing + return 'eval(' . $unpack . '(' . $params . "))\n"; + } + + private $buffer; + private function _insertFastDecode($match) { + return '{' . $this->buffer . ';'; + } + private function _insertFastEncode($match) { + return '{$encode=' . $this->buffer . ';'; + } + + // mmm.. ..which one do i need ?? + private function _getEncoder($ascii) { + return $ascii > 10 ? $ascii > 36 ? $ascii > 62 ? + '_encode95' : '_encode62' : '_encode36' : '_encode10'; + } + + // zero encoding + // characters: 0123456789 + private function _encode10($charCode) { + return $charCode; + } + + // inherent base36 support + // characters: 0123456789abcdefghijklmnopqrstuvwxyz + private function _encode36($charCode) { + return base_convert($charCode, 10, 36); + } + + // hitch a ride on base36 and add the upper case alpha characters + // characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ + private function _encode62($charCode) { + $res = ''; + if ($charCode >= $this->_encoding) { + $res = $this->_encode62((int)($charCode / $this->_encoding)); + } + $charCode = $charCode % $this->_encoding; + + if ($charCode > 35) + return $res . chr($charCode + 29); + else + return $res . base_convert($charCode, 10, 36); + } + + // use high-ascii values + // characters: ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ + private function _encode95($charCode) { + $res = ''; + if ($charCode >= $this->_encoding) + $res = $this->_encode95($charCode / $this->_encoding); + + return $res . chr(($charCode % $this->_encoding) + 161); + } + + private function _safeRegExp($string) { + return '/'.preg_replace('/\$/', '\\\$', $string).'/'; + } + + private function _encodePrivate($charCode) { + return "_" . $charCode; + } + + // protect characters used by the parser + private function _escape($script) { + return preg_replace('/([\\\\\'])/', '\\\$1', $script); + } + + // protect high-ascii characters already in the script + private function _escape95($script) { + return preg_replace_callback( + '/[\\xa1-\\xff]/', + array(&$this, '_escape95Bis'), + $script + ); + } + private function _escape95Bis($match) { + return '\x'.((string)dechex(ord($match))); + } + + + private function _getJSFunction($aName) { + if (defined('self::JSFUNCTION'.$aName)) + return constant('self::JSFUNCTION'.$aName); + else + return ''; + } + + // JavaScript Functions used. + // Note : In Dean's version, these functions are converted + // with 'String(aFunctionName);'. + // This internal conversion complete the original code, ex : + // 'while (aBool) anAction();' is converted to + // 'while (aBool) { anAction(); }'. + // The JavaScript functions below are corrected. + + // unpacking function - this is the boot strap function + // data extracted from this packing routine is passed to + // this function when decoded in the target + // NOTE ! : without the ';' final. + const JSFUNCTION_unpack = + +'function($packed, $ascii, $count, $keywords, $encode, $decode) { + while ($count--) { + if ($keywords[$count]) { + $packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]); + } + } + return $packed; +}'; +/* +'function($packed, $ascii, $count, $keywords, $encode, $decode) { + while ($count--) + if ($keywords[$count]) + $packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]); + return $packed; +}'; +*/ + + // code-snippet inserted into the unpacker to speed up decoding + const JSFUNCTION_decodeBody = +//_decode = function() { +// does the browser support String.replace where the +// replacement value is a function? + +' if (!\'\'.replace(/^/, String)) { + // decode all the values we need + while ($count--) { + $decode[$encode($count)] = $keywords[$count] || $encode($count); + } + // global replacement function + $keywords = [function ($encoded) {return $decode[$encoded]}]; + // generic match + $encode = function () {return \'\\\\w+\'}; + // reset the loop counter - we are now doing a global replace + $count = 1; + } +'; +//}; +/* +' if (!\'\'.replace(/^/, String)) { + // decode all the values we need + while ($count--) $decode[$encode($count)] = $keywords[$count] || $encode($count); + // global replacement function + $keywords = [function ($encoded) {return $decode[$encoded]}]; + // generic match + $encode = function () {return\'\\\\w+\'}; + // reset the loop counter - we are now doing a global replace + $count = 1; + }'; +*/ + + // zero encoding + // characters: 0123456789 + const JSFUNCTION_encode10 = +'function($charCode) { + return $charCode; +}';//;'; + + // inherent base36 support + // characters: 0123456789abcdefghijklmnopqrstuvwxyz + const JSFUNCTION_encode36 = +'function($charCode) { + return $charCode.toString(36); +}';//;'; + + // hitch a ride on base36 and add the upper case alpha characters + // characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ + const JSFUNCTION_encode62 = +'function($charCode) { + return ($charCode < _encoding ? \'\' : arguments.callee(parseInt($charCode / _encoding))) + + (($charCode = $charCode % _encoding) > 35 ? String.fromCharCode($charCode + 29) : $charCode.toString(36)); +}'; + + // use high-ascii values + // characters: ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ + const JSFUNCTION_encode95 = +'function($charCode) { + return ($charCode < _encoding ? \'\' : arguments.callee($charCode / _encoding)) + + String.fromCharCode($charCode % _encoding + 161); +}'; + +} + + +class ParseMaster { + public $ignoreCase = false; + public $escapeChar = ''; + + // constants + const EXPRESSION = 0; + const REPLACEMENT = 1; + const LENGTH = 2; + + // used to determine nesting levels + private $GROUPS = '/\\(/';//g + private $SUB_REPLACE = '/\\$\\d/'; + private $INDEXED = '/^\\$\\d+$/'; + private $TRIM = '/([\'"])\\1\\.(.*)\\.\\1\\1$/'; + private $ESCAPE = '/\\\./';//g + private $QUOTE = '/\'/'; + private $DELETED = '/\\x01[^\\x01]*\\x01/';//g + + public function add($expression, $replacement = '') { + // count the number of sub-expressions + // - add one because each pattern is itself a sub-expression + $length = 1 + preg_match_all($this->GROUPS, $this->_internalEscape((string)$expression), $out); + + // treat only strings $replacement + if (is_string($replacement)) { + // does the pattern deal with sub-expressions? + if (preg_match($this->SUB_REPLACE, $replacement)) { + // a simple lookup? (e.g. "$2") + if (preg_match($this->INDEXED, $replacement)) { + // store the index (used for fast retrieval of matched strings) + $replacement = (int)(substr($replacement, 1)) - 1; + } else { // a complicated lookup (e.g. "Hello $2 $1") + // build a function to do the lookup + $quote = preg_match($this->QUOTE, $this->_internalEscape($replacement)) + ? '"' : "'"; + $replacement = array( + 'fn' => '_backReferences', + 'data' => array( + 'replacement' => $replacement, + 'length' => $length, + 'quote' => $quote + ) + ); + } + } + } + // pass the modified arguments + if (!empty($expression)) $this->_add($expression, $replacement, $length); + else $this->_add('/^$/', $replacement, $length); + } + + public function exec($string) { + // execute the global replacement + $this->_escaped = array(); + + // simulate the _patterns.toSTring of Dean + $regexp = '/'; + foreach ($this->_patterns as $reg) { + $regexp .= '(' . substr($reg[self::EXPRESSION], 1, -1) . ')|'; + } + $regexp = substr($regexp, 0, -1) . '/'; + $regexp .= ($this->ignoreCase) ? 'i' : ''; + + $string = $this->_escape($string, $this->escapeChar); + $string = preg_replace_callback( + $regexp, + array( + &$this, + '_replacement' + ), + $string + ); + $string = $this->_unescape($string, $this->escapeChar); + + return preg_replace($this->DELETED, '', $string); + } + + public function reset() { + // clear the patterns collection so that this object may be re-used + $this->_patterns = array(); + } + + // private + private $_escaped = array(); // escaped characters + private $_patterns = array(); // patterns stored by index + + // create and add a new pattern to the patterns collection + private function _add() { + $arguments = func_get_args(); + $this->_patterns[] = $arguments; + } + + // this is the global replace function (it's quite complicated) + private function _replacement($arguments) { + if (empty($arguments)) return ''; + + $i = 1; $j = 0; + // loop through the patterns + while (isset($this->_patterns[$j])) { + $pattern = $this->_patterns[$j++]; + // do we have a result? + if (isset($arguments[$i]) && ($arguments[$i] != '')) { + $replacement = $pattern[self::REPLACEMENT]; + + if (is_array($replacement) && isset($replacement['fn'])) { + + if (isset($replacement['data'])) $this->buffer = $replacement['data']; + return call_user_func(array(&$this, $replacement['fn']), $arguments, $i); + + } elseif (is_int($replacement)) { + return $arguments[$replacement + $i]; + + } + $delete = ($this->escapeChar == '' || + strpos($arguments[$i], $this->escapeChar) === false) + ? '' : "\x01" . $arguments[$i] . "\x01"; + return $delete . $replacement; + + // skip over references to sub-expressions + } else { + $i += $pattern[self::LENGTH]; + } + } + } + + private function _backReferences($match, $offset) { + $replacement = $this->buffer['replacement']; + $quote = $this->buffer['quote']; + $i = $this->buffer['length']; + while ($i) { + $replacement = str_replace('$'.$i--, $match[$offset + $i], $replacement); + } + return $replacement; + } + + private function _replace_name($match, $offset){ + $length = strlen($match[$offset + 2]); + $start = $length - max($length - strlen($match[$offset + 3]), 0); + return substr($match[$offset + 1], $start, $length) . $match[$offset + 4]; + } + + private function _replace_encoded($match, $offset) { + return $this->buffer[$match[$offset]]; + } + + + // php : we cannot pass additional data to preg_replace_callback, + // and we cannot use &$this in create_function, so let's go to lower level + private $buffer; + + // encode escaped characters + private function _escape($string, $escapeChar) { + if ($escapeChar) { + $this->buffer = $escapeChar; + return preg_replace_callback( + '/\\' . $escapeChar . '(.)' .'/', + array(&$this, '_escapeBis'), + $string + ); + + } else { + return $string; + } + } + private function _escapeBis($match) { + $this->_escaped[] = $match[1]; + return $this->buffer; + } + + // decode escaped characters + private function _unescape($string, $escapeChar) { + if ($escapeChar) { + $regexp = '/'.'\\'.$escapeChar.'/'; + $this->buffer = array('escapeChar'=> $escapeChar, 'i' => 0); + return preg_replace_callback + ( + $regexp, + array(&$this, '_unescapeBis'), + $string + ); + + } else { + return $string; + } + } + private function _unescapeBis() { + if (!empty($this->_escaped[$this->buffer['i']])) { + $temp = $this->_escaped[$this->buffer['i']]; + } else { + $temp = ''; + } + $this->buffer['i']++; + return $this->buffer['escapeChar'] . $temp; + } + + private function _internalEscape($string) { + return preg_replace($this->ESCAPE, '', $string); + } +} + +// trivial wrapper for Minify +class Minify_Packer { + public static function minify($code, $options = array()) + { + // @todo: set encoding options based on $options :) + $packer = new JavascriptPacker($code, 'Normal', true, false); + return trim($packer->pack()); + } +} diff --git a/lib/Minify/Source.php b/lib/Minify/Source.php new file mode 100644 index 0000000..9d4f24a --- /dev/null +++ b/lib/Minify/Source.php @@ -0,0 +1,136 @@ +_filepath = $spec['filepath']; + $this->_id = $spec['filepath']; + $this->lastModified = filemtime($spec['filepath']); + } elseif (isset($spec['id'])) { + $this->_id = 'id::' . $spec['id']; + $this->_content = $spec['content']; + $this->lastModified = isset($spec['lastModified']) + ? $spec['lastModified'] + : time(); + } + if (isset($spec['minifier'])) { + $this->minifier = $spec['minifier']; + } + if (isset($spec['minifyOptions'])) { + $this->minifyOptions = $spec['minifyOptions']; + } + } + + /** + * Get content + * + * @return string + */ + public function getContent() + { + return (null !== $this->_content) + ? $this->_content + : file_get_contents($this->_filepath); + } + + /** + * Verifies a single minification call can handle all sources + * + * @param array $sources Minify_Source instances + * + * @return bool true iff there no sources with specific minifier preferences. + */ + public static function haveNoMinifyPrefs($sources) + { + foreach ($sources as $source) { + if (null !== $source->minifier + || null !== $source->minifyOptions) { + return false; + } + } + return true; + } + + /** + * Get unique string for a set of sources + * + * @param array $sources Minify_Source instances + * + * @return string + */ + public static function getDigest($sources) + { + foreach ($sources as $source) { + $info[] = array( + $source->_id, $source->minifier, $source->minifyOptions + ); + } + return md5(serialize($info)); + } + + /** + * Guess content type from the first filename extension available + * + * This is called if the user doesn't pass in a 'contentType' options + * + * @param array $sources Minify_Source instances + * + * @return string content type. e.g. 'text/css' + */ + public static function getContentType($sources) + { + $exts = array( + 'css' => 'text/css' + ,'js' => 'application/x-javascript' + ,'html' => 'text/html' + ); + foreach ($sources as $source) { + if (null !== $source->_filepath) { + $segments = explode('.', $source->_filepath); + $ext = array_pop($segments); + if (isset($exts[$ext])) { + return $exts[$ext]; + } + } + } + return 'text/plain'; + } + + private $_content = null; + private $_filepath = null; + private $_id = null; +} + diff --git a/lib/htmlmin.php b/lib/htmlmin.php deleted file mode 100644 index 5ac2bb3..0000000 --- a/lib/htmlmin.php +++ /dev/null @@ -1,69 +0,0 @@ -getMinifiedHtml(); - } - - // -- Private Instance Variables --------------------------------------------- - private $input; - - // -- Private Instance Methods ----------------------------------------------- - private function replaceCSS($matches) { - // Remove HTML comment markers from the CSS (they shouldn't be there - // anyway). - $css = preg_replace('//', "$1", $matches[2]); - - return ''.trim(Minify::min($css, Minify::TYPE_CSS)). - ''; - } - - private function replaceJavaScript($matches) { - // Remove HTML comment markers from the JS (they shouldn't be there anyway). - $js = preg_replace('//', "$1", $matches[2]); - - return ''.trim(Minify::min($js, Minify::TYPE_JS)). - ''; - } - - // -- Public Instance Methods ------------------------------------------------ - public function __construct($input = '') { - $this->setInput($input); - } - - public function getInput() { - return $this->input; - } - - public function getMinifiedHtml() { - $html = trim($this->input); - - // Run JavaScript blocks through JSMin. - $html = preg_replace_callback('/([\s\S]*?)<\/script>/i', - array($this, 'replaceJavaScript'), $html); - - // Run CSS blocks through Minify's CSS minifier. - $html = preg_replace_callback('/([\s\S]*?)<\/style>/i', - array($this, 'replaceCSS'), $html); - - // Remove HTML comments (but not IE conditional comments). - $html = preg_replace('//', '', $html); - - // Remove leading and trailing whitespace from each line. - // FIXME: This needs to take into account attribute values that span multiple lines. - $html = preg_replace('/^\s*(.*?)\s*$/m', "$1", $html); - - // Remove unnecessary whitespace between and inside elements. - $html = preg_replace('/>\s+(\S[\s\S]*?)? $1<", $html); - $html = preg_replace('/>(\S[\s\S]*?)?\s+$1 <", $html); - $html = preg_replace('/>\s+ <", $html); - - return $html; - } - - public function setInput($input) { - $this->input = $input; - } -} -?> \ No newline at end of file diff --git a/minify.php b/minify.php deleted file mode 100644 index 78ca2d0..0000000 --- a/minify.php +++ /dev/null @@ -1,490 +0,0 @@ - - * and by the article "Supercharged JavaScript" by Patrick Hunlock - * . - * - * JSMin was originally written by Douglas Crockford . - * - * Requires PHP 5.2.1+. - * - * @package Minify - * @author Ryan Grove - * @copyright 2007 Ryan Grove. All rights reserved. - * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version 1.1.0 (?) - * @link http://code.google.com/p/minify/ - */ - -if (!defined('MINIFY_BASE_DIR')) { - /** - * Base path from which all relative file paths should be resolved. By default - * this is set to the document root. - */ - define('MINIFY_BASE_DIR', realpath($_SERVER['DOCUMENT_ROOT'])); -} - -if (!defined('MINIFY_CACHE_DIR')) { - /** Directory where compressed files will be cached. */ - define('MINIFY_CACHE_DIR', sys_get_temp_dir()); -} - -if (!defined('MINIFY_ENCODING')) { - /** Character set to use when outputting the minified files. */ - define('MINIFY_ENCODING', 'utf-8'); -} - -if (!defined('MINIFY_MAX_FILES')) { - /** Maximum number of files to combine in one request. */ - define('MINIFY_MAX_FILES', 16); -} - -if (!defined('MINIFY_REWRITE_CSS_URLS')) { - /** - * Whether or not Minify should attempt to rewrite relative URLs used in CSS - * files so that they continue to point to the correct location after the file - * is combined and minified. - * - * Minify is pretty good at getting this right, but occasionally it can make - * mistakes. If you find that URL rewriting results in problems, you should - * disable it. - */ - define('MINIFY_REWRITE_CSS_URLS', true); -} - -if (!defined('MINIFY_USE_CACHE')) { - /** - * Whether or not Minify should use a disk-based cache to increase - * performance. - */ - define('MINIFY_USE_CACHE', true); -} - -/** - * Minify is a library for combining, minifying, and caching JavaScript and CSS - * files on demand before sending them to a web browser. - * - * @package Minify - * @author Ryan Grove - * @copyright 2007 Ryan Grove. All rights reserved. - * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version 1.1.0 (?) - * @link http://code.google.com/p/minify/ - */ -class Minify { - const TYPE_CSS = 'text/css'; - const TYPE_HTML = 'text/html'; - const TYPE_JS = 'text/javascript'; - - protected $files = array(); - protected $type = self::TYPE_JS; - - // -- Public Static Methods -------------------------------------------------- - - /** - * Combines, minifies, and outputs the requested files. - * - * Inspects the $_GET array for a 'files' entry containing a comma-separated - * list and uses this as the set of files to be combined and minified. - */ - public static function handleRequest() { - // 404 if no files were requested. - if (!isset($_GET['files'])) { - header('HTTP/1.0 404 Not Found'); - exit; - } - - $files = array_map('trim', explode(',', $_GET['files'], MINIFY_MAX_FILES)); - - // 404 if the $files array is empty for some weird reason. - if (!count($files)) { - header('HTTP/1.0 404 Not Found'); - exit; - } - - // Determine the content type based on the extension of the first file - // requested. - if (preg_match('/\.js$/iD', $files[0])) { - $type = self::TYPE_JS; - } else if (preg_match('/\.css$/iD', $files[0])) { - $type = self::TYPE_CSS; - } else { - $type = self::TYPE_HTML; - } - - // Minify and spit out the result. - try { - $minify = new Minify($type, $files); - - header("Content-Type: $type;charset=".MINIFY_ENCODING); - - $minify->browserCache(); - echo $minify->combine(); - exit; - } - catch (MinifyException $e) { - header('HTTP/1.0 404 Not Found'); - echo htmlentities($e->getMessage()); - exit; - } - } - - /** - * Minifies the specified string and returns it. - * - * @param string $string JavaScript, CSS, or HTML string to minify - * @param string $type content type of the string (Minify::TYPE_CSS, - * Minify::TYPE_HTML, or Minify::TYPE_JS) - * @return string minified string - */ - public static function min($string, $type = self::TYPE_JS) { - switch ($type) { - case self::TYPE_CSS: - return self::minifyCSS($string); - break; - - case self::TYPE_HTML: - return self::minifyHTML($string); - break; - - case self::TYPE_JS: - return self::minifyJS($string); - break; - } - - return $string; - } - - // -- Protected Static Methods ----------------------------------------------- - - /** - * Minifies the specified CSS string and returns it. - * - * @param string $css CSS string - * @return string minified string - * @see minify() - * @see minifyJS() - */ - protected static function minifyCSS($css) { - // Compress whitespace. - $css = preg_replace('/\s+/', ' ', $css); - - // Remove comments. - $css = preg_replace('/\/\*.*?\*\//', '', $css); - - return trim($css); - } - - protected static function minifyHTML($html) { - require_once dirname(__FILE__).'/lib/htmlmin.php'; - return HTMLMin::minify($html); - } - - /** - * Minifies the specified JavaScript string and returns it. - * - * @param string $js JavaScript string - * @return string minified string - * @see minify() - * @see minifyCSS() - */ - protected static function minifyJS($js) { - require_once dirname(__FILE__).'/lib/jsmin.php'; - return JSMin::minify($js); - } - - /** - * Rewrites relative URLs in the specified CSS string to point to the correct - * location. URLs are assumed to be relative to the absolute path specified in - * the $path parameter. - * - * @param string $css CSS string - * @param string $path absolute path to which URLs are relative (should be a - * directory, not a file) - * @return string CSS string with rewritten URLs - */ - protected static function rewriteCSSUrls($css, $path) { - /* - Parentheses, commas, whitespace chars, single quotes, and double quotes are - escaped with a backslash as described in the CSS spec: - http://www.w3.org/TR/REC-CSS1#url - */ - $relativePath = preg_replace('/([\(\),\s\'"])/', '\\\$1', - str_replace(MINIFY_BASE_DIR, '', $path)); - - return preg_replace('/url\(\s*[\'"]?\/?(.+?)[\'"]?\s*\)/i', 'url('. - $relativePath.'/$1)', $css); - } - - // -- Public Instance Methods ------------------------------------------------ - - /** - * Instantiates a new Minify object. A filename can be in the form of a - * relative path or a URL that resolves to the same site that hosts Minify. - * - * @param string $type content type of the specified files (either - * Minify::TYPE_CSS or Minify::TYPE_JS) - * @param array|string $files filename or array of filenames to be minified - */ - public function __construct($type = self::TYPE_JS, $files = array()) { - if ($type !== self::TYPE_JS && $type !== self::TYPE_CSS) { - throw new MinifyInvalidArgumentException('Invalid argument ($type): '. - $type); - } - - $this->type = $type; - - if (count((array) $files)) { - $this->addFile($files); - } - } - - /** - * Adds the specified filename or array of filenames to the list of files to - * be minified. A filename can be in the form of a relative path or a URL - * that resolves to the same site that hosts Minify. - * - * @param array|string $files filename or array of filenames - * @see getFiles() - * @see removeFile() - */ - public function addFile($files) { - $files = @array_map(array($this, 'resolveFilePath'), (array) $files); - $this->files = array_unique(array_merge($this->files, $files)); - } - - /** - * Attempts to serve the combined, minified files from the cache if possible. - * - * This method first checks the ETag value and If-Modified-Since timestamp - * sent by the browser and exits with an HTTP "304 Not Modified" response if - * the requested files haven't changed since they were last sent to the - * client. - * - * If the browser hasn't cached the content, we check to see if it's been - * cached on the server and, if so, we send the cached content and exit. - * - * If neither the client nor the server has the content in its cache, we don't - * do anything. - * - * @return bool - */ - public function browserCache() { - $hash = $this->getHash(); - $lastModified = $this->getLastModified(); - - $lastModifiedGMT = gmdate('D, d M Y H:i:s', $lastModified).' GMT'; - - // Check/set the ETag. - $etag = $hash.'_'.$lastModified; - - if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) { - if (strpos($_SERVER['HTTP_IF_NONE_MATCH'], $etag) !== false) { - header("Last-Modified: $lastModifiedGMT", true, 304); - exit; - } - } - - header('ETag: "'.$etag.'"'); - - // Check If-Modified-Since. - if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { - if ($lastModified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { - header("Last-Modified: $lastModifiedGMT", true, 304); - exit; - } - } - - header("Last-Modified: $lastModifiedGMT"); - - return false; - } - - /** - * Combines and returns the contents of all files that have been added with - * addFile() or via this class's constructor. - * - * If MINIFY_USE_CACHE is true, the content will be returned from the server's - * cache if the cache is up to date; otherwise the new content will be saved - * to the cache for future use. - * - * @param bool $minify minify the combined contents before returning them - * @return string combined file contents - */ - public function combine($minify = true) { - // Return contents from server cache if possible. - if (MINIFY_USE_CACHE) { - if ($cacheResult = $this->serverCache(true)) { - return $cacheResult; - } - } - - // Combine contents. - $combined = array(); - - foreach($this->files as $file) { - if ($this->type === self::TYPE_CSS && MINIFY_REWRITE_CSS_URLS) { - // Rewrite relative CSS URLs. - $combined[] = self::rewriteCSSUrls(file_get_contents($file), - dirname($file)); - } - else { - $combined[] = file_get_contents($file); - } - } - - $combined = $minify ? self::minify(implode("\n", $combined), $this->type) : - implode("\n", $combined); - - // Save combined contents to the cache. - if (MINIFY_USE_CACHE) { - $cacheFile = MINIFY_CACHE_DIR.'/minify_'.$this->getHash(); - @file_put_contents($cacheFile, $combined, LOCK_EX); - } - - return $combined; - } - - /** - * Gets an array of absolute pathnames of all files that have been added with - * addFile() or via this class's constructor. - * - * @return array array of absolute pathnames - * @see addFile() - * @see removeFile() - */ - public function getFiles() { - return $this->files; - } - - /** - * Gets the MD5 hash of the concatenated filenames from the list of files to - * be minified. - */ - public function getHash() { - return hash('md5', implode('', $this->files)); - } - - /** - * Gets the timestamp of the most recently modified file. - * - * @return int timestamp - */ - public function getLastModified() { - $lastModified = 0; - - // Get the timestamp of the most recently modified file. - foreach($this->files as $file) { - $modified = filemtime($file); - - if ($modified !== false && $modified > $lastModified) { - $lastModified = $modified; - } - } - - return $lastModified; - } - - /** - * Removes the specified filename or array of filenames from the list of files - * to be minified. - * - * @param array|string $files filename or array of filenames - * @see addFile() - * @see getFiles() - */ - public function removeFile($files) { - $files = @array_map(array($this, 'resolveFilePath'), (array) $files); - $this->files = array_diff($this->files, $files); - } - - /** - * Attempts to serve the combined, minified files from the server's disk-based - * cache if possible. - * - * @param bool $return return cached content as a string instead of outputting - * it to the client - * @return bool|string - */ - public function serverCache($return = false) { - $cacheFile = MINIFY_CACHE_DIR.'/minify_'.$this->getHash(); - $lastModified = $this->getLastModified(); - - if (is_file($cacheFile) && $lastModified <= filemtime($cacheFile)) { - if ($return) { - return file_get_contents($cacheFile); - } - else { - echo file_get_contents($cacheFile); - exit; - } - } - - return false; - } - - // -- Protected Instance Methods --------------------------------------------- - - /** - * Returns the canonicalized absolute pathname to the specified file or local - * URL. - * - * @param string $file relative file path - * @return string canonicalized absolute pathname - */ - protected function resolveFilePath($file) { - // Is this a URL? - if (preg_match('/^https?:\/\//i', $file)) { - if (!$parsedUrl = parse_url($file)) { - throw new MinifyInvalidUrlException("Invalid URL: $file"); - } - - // Does the server name match the local server name? - if (!isset($parsedUrl['host']) || - $parsedUrl['host'] != $_SERVER['SERVER_NAME']) { - throw new MinifyInvalidUrlException('Non-local URL not supported: '. - $file); - } - - // Get the file's absolute path. - $filepath = realpath(MINIFY_BASE_DIR.$parsedUrl['path']); - } - else { - // Get the file's absolute path. - $filepath = realpath(MINIFY_BASE_DIR.'/'.$file); - } - - // Ensure that the file exists, that the path is under the base directory, - // that the file's extension is either '.css' or '.js', and that the file is - // actually readable. - if (!$filepath || - !is_file($filepath) || - !is_readable($filepath) || - !preg_match('/^'.preg_quote(MINIFY_BASE_DIR, '/').'/', $filepath) || - !preg_match('/\.(?:css|js)$/iD', $filepath)) { - - // Even when the file exists, we still throw a - // MinifyFileNotFoundException in order to try to prevent an information - // disclosure vulnerability. - throw new MinifyFileNotFoundException("File not found: $file"); - } - - return $filepath; - } -} - -// -- Exception Classes -------------------------------------------------------- -class MinifyException extends Exception {} -class MinifyFileNotFoundException extends MinifyException {} -class MinifyInvalidArgumentException extends MinifyException {} -class MinifyInvalidUrlException extends MinifyException {} - -// -- Global Scope ------------------------------------------------------------- -if (realpath(__FILE__) == realpath($_SERVER['SCRIPT_FILENAME'])) { - Minify::handleRequest(); -} -?> \ No newline at end of file diff --git a/test/_inc.php b/test/_inc.php new file mode 100644 index 0000000..2fb2bb0 --- /dev/null +++ b/test/_inc.php @@ -0,0 +1,27 @@ +0, 'fail'=>0, 'total'=>0); + + $mode = $test ? 'pass' : 'fail'; + printf("%s: %s (%d of %d tests run so far have %sed)\n", + strtoupper($mode), $message, ++$count[$mode], ++$count['total'], $mode); + + return (bool)$test; +} + +?> \ No newline at end of file diff --git a/test/css/caio.css b/test/css/caio.css new file mode 100644 index 0000000..8946bdb --- /dev/null +++ b/test/css/caio.css @@ -0,0 +1,3 @@ +/*/*/ a{} +.foo {color:red} +/* blah */ \ No newline at end of file diff --git a/test/css/caio.min.css b/test/css/caio.min.css new file mode 100644 index 0000000..7562f41 --- /dev/null +++ b/test/css/caio.min.css @@ -0,0 +1 @@ +/*/*/a{}.foo{color:red}/**/ \ No newline at end of file diff --git a/test/css/comments.css b/test/css/comments.css new file mode 100644 index 0000000..fa2930a --- /dev/null +++ b/test/css/comments.css @@ -0,0 +1,6 @@ + +/* block comments get removed */ + +/* comments containing the word "copyright" are left in, though */ + +/* but all other comments are removed */ diff --git a/test/css/comments.min b/test/css/comments.min new file mode 100644 index 0000000..03b4e48 --- /dev/null +++ b/test/css/comments.min @@ -0,0 +1 @@ +/* comments containing the word "copyright" are left in, though */ \ No newline at end of file diff --git a/test/css/comments.min.css b/test/css/comments.min.css new file mode 100644 index 0000000..03b4e48 --- /dev/null +++ b/test/css/comments.min.css @@ -0,0 +1 @@ +/* comments containing the word "copyright" are left in, though */ \ No newline at end of file diff --git a/test/css/hacks.css b/test/css/hacks.css new file mode 100644 index 0000000..20eb006 --- /dev/null +++ b/test/css/hacks.css @@ -0,0 +1,31 @@ +/* hide from ie5/mac \*/ a{} +.foo {color:red} +/* necessary comment */ + +/* comment */ + +/* feed to ie5/mac \*//*/ +@import "ie5mac.css"; +/* necessary comment */ + +/* comment */ + +/*/ hide from nav4 */ +.foo {display:block;} +/* necessary comment */ + +/* comment */ + +/*/ feed to nav *//*/ +.foo {display:crazy;} +/* necessary comment */ + +/* comment */ + +div { + width: 140px; + width/* */:/**/100px; + width: /**/100px; +} + +html>/**/body {} \ No newline at end of file diff --git a/test/css/hacks.min b/test/css/hacks.min new file mode 100644 index 0000000..3f96ed0 --- /dev/null +++ b/test/css/hacks.min @@ -0,0 +1 @@ +/*\*/a{}.foo{color:red}/**//*\*//*/@import "ie5mac.css";/**//*/*/.foo{display:block}/**//*/*//*/.foo{display:crazy}/**/div{width:140px;width/**/:/**/100px;width:/**/100px}html>/**/body{} \ No newline at end of file diff --git a/test/css/hacks.min.css b/test/css/hacks.min.css new file mode 100644 index 0000000..3f96ed0 --- /dev/null +++ b/test/css/hacks.min.css @@ -0,0 +1 @@ +/*\*/a{}.foo{color:red}/**//*\*//*/@import "ie5mac.css";/**//*/*/.foo{display:block}/**//*/*//*/.foo{display:crazy}/**/div{width:140px;width/**/:/**/100px;width:/**/100px}html>/**/body{} \ No newline at end of file diff --git a/test/css/paths.css b/test/css/paths.css new file mode 100644 index 0000000..19d248e --- /dev/null +++ b/test/css/paths.css @@ -0,0 +1,9 @@ +@import "foo.css"; +@import 'bar/foo.css'; +@import '/css/foo.css'; +@import 'http://foo.com/css/foo.css'; +@import url(./foo.css); +@import url("/css/foo.css"); +@import url(/css2/foo.css); +foo {background:url('bar/foo.png')} +foo {background:url('http://foo.com/css/foo.css');} \ No newline at end of file diff --git a/test/css/paths.min b/test/css/paths.min new file mode 100644 index 0000000..b887891 --- /dev/null +++ b/test/css/paths.min @@ -0,0 +1 @@ +@import "../foo.css";@import '../bar/foo.css';@import '/css/foo.css';@import 'http://foo.com/css/foo.css';@import url(.././foo.css);@import url("/css/foo.css");@import url(/css2/foo.css);foo{background:url('../bar/foo.png')}foo{background:url('http://foo.com/css/foo.css')} \ No newline at end of file diff --git a/test/css/paths.min.css b/test/css/paths.min.css new file mode 100644 index 0000000..b887891 --- /dev/null +++ b/test/css/paths.min.css @@ -0,0 +1 @@ +@import "../foo.css";@import '../bar/foo.css';@import '/css/foo.css';@import 'http://foo.com/css/foo.css';@import url(.././foo.css);@import url("/css/foo.css");@import url(/css2/foo.css);foo{background:url('../bar/foo.png')}foo{background:url('http://foo.com/css/foo.css')} \ No newline at end of file diff --git a/test/css/readme.txt b/test/css/readme.txt new file mode 100644 index 0000000..0181755 --- /dev/null +++ b/test/css/readme.txt @@ -0,0 +1 @@ +Test suite from http://search.cpan.org/~gtermars/CSS-Minifier-XS/ \ No newline at end of file diff --git a/test/css/styles.css b/test/css/styles.css new file mode 100644 index 0000000..d3daf36 --- /dev/null +++ b/test/css/styles.css @@ -0,0 +1,21 @@ +/* some CSS to try to exercise things in general */ + +@import url( more.css ); + + body, td, th { + font-family: Verdana, "Bitstream Vera Sans", sans-serif; + + font-size : 12px; +} + +.nav { + margin-left: 20%; +} +#main-nav { + background-color: red; + border: 1px solid #00ff77; +} +div#content h1 + p { + padding-top: 0; + margin-top: 0; +} diff --git a/test/css/styles.min b/test/css/styles.min new file mode 100644 index 0000000..fe75287 --- /dev/null +++ b/test/css/styles.min @@ -0,0 +1 @@ +@import url(more.css);body,td,th{font-family:Verdana, "Bitstream Vera Sans",sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px solid #0f7}div#content h1+p{padding-top:0;margin-top:0} \ No newline at end of file diff --git a/test/css/styles.min.css b/test/css/styles.min.css new file mode 100644 index 0000000..fe75287 --- /dev/null +++ b/test/css/styles.min.css @@ -0,0 +1 @@ +@import url(more.css);body,td,th{font-family:Verdana, "Bitstream Vera Sans",sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px solid #0f7}div#content h1+p{padding-top:0;margin-top:0} \ No newline at end of file diff --git a/test/css/subsilver.css b/test/css/subsilver.css new file mode 100644 index 0000000..79d34ec --- /dev/null +++ b/test/css/subsilver.css @@ -0,0 +1,434 @@ +/* Based on the original Style Sheet for the fisubsilver v2 Theme for phpBB version 2+ +Edited by Daz - http://www.forumimages.com - last updated 26-06-03 */ +/* The content of the posts (body of text) */ +/* General page style */ + + + + /* begin suggest post */ + .float-l{ + float: left; + } + + .form-suggest{ + height:200px; + background:#DEE2D0; + vertical-align: top; + } + + .form-input input{ + font-size: 10px; + } + + .hide{ + display:none; + } + + .form-input textarea{ + font-size: 11px; + width: 350px; + } + + .form-label{ + font-size: 10px; + font-weight: bold; + line-height: 25px; + padding-right: 10px; + text-align: right; + width: 100px; + color: #39738F; + } + + .font-9{ + font-size: 9px; + } + + .form-topic{ + font-weight:bold; + + } + + .form-error{ + color:red; + } + + .inline{ + display: inline; + } + + .space-10{ + clear: both; + font-size: 10px; + height: 10px; + line-height: 10px; + } + + .suggest-success{ + color:green; + padding-left:10px; + font-size:11px; + font-weight:bold; + } + + .top{ + vertical-align: top; + } + /* end suggest post */ + +table td{ + padding:3px; +} + +a:link,a:active,a:visited,a.postlink{ + color: #006699; + text-decoration: none; +} + +a:hover{ + color: #DD6900; +} + +a.admin:hover,a.mod:hover{ + color: #DD6900; +} + +a.but,a.but:hover,a.but:visited{ + color: #000000; + text-decoration: none; +} + +a.topictitle:visited{ + color: #5493B4; +} + +a.topictitle:hover{ + color: #DD6900; +} + + + +body{ + color: #000000; + font: 11px Verdana,Arial,Helvetica,sans-serif; + margin: 0 10px 10px 10px; + padding: 0; + overflow:auto; +} + +/* General font families for common tags */ +font,th,td,p{ + font: 12px Verdana,Arial,Helvetica,sans-serif; +} + +/* Form elements */ +form{ + display: inline; +} + +hr{ + border: 0px solid #FFFFFF; + border-top-width: 1px; + height: 0px; +} + +/* Gets rid of the need for border="0" on hyperlinked images */ +img{ + border: 0 solid; +} + +input{ + font: 11px Verdana,Arial,Helvetica,sans-serif; +} + +input.button,input.liteoption,.fakebut{ + background: #FAFAFA; + border: 1px solid #000000; + font-size: 11px; +} + +input.catbutton{ + background: #FAFAFA; + border: 1px solid #000000; + font-size: 10px; +} + +input.mainoption{ + background: #FAFAFA; + border: 1px solid #000000; + font-size: 11px; + font-weight: bold; +} + +input.post,textarea.post{ + background: #FFFFFF; + border: 1px solid #000000; + font: 11px Verdana,Arial,Helvetica,sans-serif; + padding-bottom: 2px; + padding-left: 2px; +} + +select{ + background: #FFFFFF; + font: 11px Verdana,Arial,Helvetica,sans-serif; +} + +table{ + text-align: left; +} + +td{ + vertical-align: middle; +} + +/* Category gradients*/ +td.cat{ + background-color: #C2C6BA; + font-weight: bold; + height: 20px; + letter-spacing: 1px; + text-indent: 4px; +} + +td.genmed,.genmed{ + font-size: 11px; +} + +/* This is for the table cell above the Topics,Post & Last posts on the index.php */ +td.rowpic{ + background: #C2C6BA; +} + +td.spacerow{ + background: #E5E6E2; +} + +/* Table Header cells */ +th{ + background-color: #FADD31; + background-image: url(images/cellpic3.gif); + background-repeat: repeat-x; + color: #68685E; + font-size: 11px; + font-weight: bold; + line-height:16px; + height: 16px; + padding-left: 8px; + padding-right: 8px; + text-align: center; + white-space: nowrap; +} + +/* Admin & Moderator Colours MODification */ +.admin,.mod{ + font-size: 11px; + font-weight: bold; +} + +.admin,a.admin,a.admin:visited{ + color: #FFA34F; +} + +/* This is the border line & background colour round the entire page */ +.bodyline{ + background: #FFFFFF; + border: 1px solid #98AAB1; +} + +.center{ + text-align: center; +} + +/* Code blocks */ +.code{ + background: #FAFAFA; + border: 1px solid #D1D7DC; + color: #006600; + font: 12px Courier,"Courier New",sans-serif; + padding: 5px; +} + +/* This is for the error messages that pop up */ +.errorline{ + background: #E5E6E2; + border: 1px solid #8F8B8B; + color:#D92A2A; +} + +.explaintitle{ + color: #5C81B1; + font-size: 11px; + font-weight: bold; +} + +/* This is the outline round the main forum tables */ +.forumline{ + background: #FFFFFF; +} + +/* General text */ +.gensmall{ + font-size: 10px; +} + +.h1-font{ + color: #006699; + display: inline; + font: bold 13px Verdana, Arial, Helvetica, sans-serif; + margin: 0; + text-decoration: none; +} + +.h2-font{ + display: inline; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 11px; +} + +.height1{ + height: 1px; +} + +.height22{ + height: 22px; +} + +.height25{ + height: 25px; +} + +.height28{ + height: 28px; +} + +.height30{ + height: 30px; +} + +.height40{ + height: 40px; +} + +/* This is the line in the posting page which shows the rollover +help line. Colour value in row2 */ +.helpline{ + border: 0 solid; + font-size: 10px; +} + +.imgfolder{ + margin: 1px 4px 1px 4px; +} + +.imgspace{ + margin-left: 1px; + margin-right: 2px; +} + +/* Specify the space around images */ +.imgtopic,.imgicon{ + margin-left: 3px; +} + +.left{ + text-align: left; +} + +/* The largest text used in the index page title and toptic title etc. */ +.maintitle,h1,h2{ + color: #5C81B1; + font: bold 20px/120% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif; + text-decoration: none; +} + +.maxwidth{ + width: 100%; +} + +.mod,a.mod,a.mod:visited{ + color: #006600; +} + +/* Name of poster in viewmsg.php and viewtopic.php and other places */ +.name{ + font-size: 11px; + font-weight: bold; +} + +/* Used for the navigation text,(Page 1,2,3 etc) and the navigation bar when in a forum */ +.nav{ + font-size: 11px; + font-weight: bold; +} + +.nowrap{ + white-space: nowrap; +} + +.postbody{ + font-size: 12px; + line-height: 125%; +} + +.postbody a{ + text-decoration: underline; +} + +/* Location,number of posts,post date etc */ +.postdetails{ + color: #00396A; + font-size: 10px; +} + +/* Quote blocks */ +.quote{ + background: #F3F3EF; + border: 1px solid #C2C6BA; + color: #006699; + font-size: 11px; + line-height: 125%; +} + +.right{ + text-align: right; +} + +/* Main table cell colours and backgrounds */ +.row1{ + background: #F0F0EB; +} + +.row2,.helpline{ + background: #E5E6E2; +} + +.row3{ + background: #DBDBD4; +} + +.subtitle,h2{ + font: bold 18px/180% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif; + text-decoration: none; +} + +/* titles for the topics:could specify viewed link colour too */ +.topictitle { + color: #000000; + font-size: 11px; + font-weight: bold; +} + +.underline{ + text-decoration: underline; +} + +.top{ +vertical-align:top; +} + +.image-hspace{ +margin-right:3px; +} + +.clear{ +clear:both; +} \ No newline at end of file diff --git a/test/css/subsilver.min b/test/css/subsilver.min new file mode 100644 index 0000000..dfcc476 --- /dev/null +++ b/test/css/subsilver.min @@ -0,0 +1 @@ +.float-l{float:left}.form-suggest{height:200px;background:#DEE2D0;vertical-align:top}.form-input input{font-size:10px}.hide{display:none}.form-input textarea{font-size:11px;width:350px}.form-label{font-size:10px;font-weight:bold;line-height:25px;padding-right:10px;text-align:right;width:100px;color: #39738F}.font-9{font-size:9px}.form-topic{font-weight:bold}.form-error{color:red}.inline{display:inline}.space-10{clear:both;font-size:10px;height:10px;line-height:10px}.suggest-success{color:green;padding-left:10px;font-size:11px;font-weight:bold}.top{vertical-align:top}table td{padding:3px}a:link,a:active,a:visited,a.postlink{color: #069;text-decoration:none}a:hover{color: #DD6900}a.admin:hover,a.mod:hover{color: #DD6900}a.but,a.but:hover,a.but:visited{color: #000;text-decoration:none}a.topictitle:visited{color: #5493B4}a.topictitle:hover{color: #DD6900}body{color: #000;font:11px Verdana,Arial,Helvetica,sans-serif;margin:0 10px 10px 10px;padding:0;overflow:auto}font,th,td,p{font:12px Verdana,Arial,Helvetica,sans-serif}form{display:inline}hr{border:0px solid #FFF;border-top-width:1px;height:0px}img{border:0 solid}input{font:11px Verdana,Arial,Helvetica,sans-serif}input.button,input.liteoption,.fakebut{background: #FAFAFA;border:1px solid #000;font-size:11px}input.catbutton{background: #FAFAFA;border:1px solid #000;font-size:10px}input.mainoption{background: #FAFAFA;border:1px solid #000;font-size:11px;font-weight:bold}input.post,textarea.post{background: #FFF;border:1px solid #000;font:11px Verdana,Arial,Helvetica,sans-serif;padding-bottom:2px;padding-left:2px}select{background: #FFF;font:11px Verdana,Arial,Helvetica,sans-serif}table{text-align:left}td{vertical-align:middle}td.cat{background-color: #C2C6BA;font-weight:bold;height:20px;letter-spacing:1px;text-indent:4px}td.genmed,.genmed{font-size:11px}td.rowpic{background: #C2C6BA}td.spacerow{background: #E5E6E2}th{background-color: #FADD31;background-image:url(images/cellpic3.gif);background-repeat:repeat-x;color: #68685E;font-size:11px;font-weight:bold;line-height:16px;height:16px;padding-left:8px;padding-right:8px;text-align:center;white-space:nowrap}.admin,.mod{font-size:11px;font-weight:bold}.admin,a.admin,a.admin:visited{color: #FFA34F}.bodyline{background: #FFF;border:1px solid #98AAB1}.center{text-align:center}.code{background: #FAFAFA;border:1px solid #D1D7DC;color: #060;font:12px Courier,"Courier New",sans-serif;padding:5px}.errorline{background: #E5E6E2;border:1px solid #8F8B8B;color:#D92A2A}.explaintitle{color: #5C81B1;font-size:11px;font-weight:bold}.forumline{background: #FFF}.gensmall{font-size:10px}.h1-font{color: #069;display:inline;font:bold 13px Verdana,Arial,Helvetica,sans-serif;margin:0;text-decoration:none}.h2-font{display:inline;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px}.height1{height:1px}.height22{height:22px}.height25{height:25px}.height28{height:28px}.height30{height:30px}.height40{height:40px}.helpline{border:0 solid;font-size:10px}.imgfolder{margin:1px 4px 1px 4px}.imgspace{margin-left:1px;margin-right:2px}.imgtopic,.imgicon{margin-left:3px}.left{text-align:left}.maintitle,h1,h2{color: #5C81B1;font:bold 20px/120% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;text-decoration:none}.maxwidth{width:100%}.mod,a.mod,a.mod:visited{color: #060}.name{font-size:11px;font-weight:bold}.nav{font-size:11px;font-weight:bold}.nowrap{white-space:nowrap}.postbody{font-size:12px;line-height:125%}.postbody a{text-decoration:underline}.postdetails{color: #00396A;font-size:10px}.quote{background: #F3F3EF;border:1px solid #C2C6BA;color: #069;font-size:11px;line-height:125%}.right{text-align:right}.row1{background: #F0F0EB}.row2,.helpline{background: #E5E6E2}.row3{background: #DBDBD4}.subtitle,h2{font:bold 18px/180% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;text-decoration:none}.topictitle{color: #000;font-size:11px;font-weight:bold}.underline{text-decoration:underline}.top{vertical-align:top}.image-hspace{margin-right:3px}.clear{clear:both} \ No newline at end of file diff --git a/test/css/subsilver.min.css b/test/css/subsilver.min.css new file mode 100644 index 0000000..dfcc476 --- /dev/null +++ b/test/css/subsilver.min.css @@ -0,0 +1 @@ +.float-l{float:left}.form-suggest{height:200px;background:#DEE2D0;vertical-align:top}.form-input input{font-size:10px}.hide{display:none}.form-input textarea{font-size:11px;width:350px}.form-label{font-size:10px;font-weight:bold;line-height:25px;padding-right:10px;text-align:right;width:100px;color: #39738F}.font-9{font-size:9px}.form-topic{font-weight:bold}.form-error{color:red}.inline{display:inline}.space-10{clear:both;font-size:10px;height:10px;line-height:10px}.suggest-success{color:green;padding-left:10px;font-size:11px;font-weight:bold}.top{vertical-align:top}table td{padding:3px}a:link,a:active,a:visited,a.postlink{color: #069;text-decoration:none}a:hover{color: #DD6900}a.admin:hover,a.mod:hover{color: #DD6900}a.but,a.but:hover,a.but:visited{color: #000;text-decoration:none}a.topictitle:visited{color: #5493B4}a.topictitle:hover{color: #DD6900}body{color: #000;font:11px Verdana,Arial,Helvetica,sans-serif;margin:0 10px 10px 10px;padding:0;overflow:auto}font,th,td,p{font:12px Verdana,Arial,Helvetica,sans-serif}form{display:inline}hr{border:0px solid #FFF;border-top-width:1px;height:0px}img{border:0 solid}input{font:11px Verdana,Arial,Helvetica,sans-serif}input.button,input.liteoption,.fakebut{background: #FAFAFA;border:1px solid #000;font-size:11px}input.catbutton{background: #FAFAFA;border:1px solid #000;font-size:10px}input.mainoption{background: #FAFAFA;border:1px solid #000;font-size:11px;font-weight:bold}input.post,textarea.post{background: #FFF;border:1px solid #000;font:11px Verdana,Arial,Helvetica,sans-serif;padding-bottom:2px;padding-left:2px}select{background: #FFF;font:11px Verdana,Arial,Helvetica,sans-serif}table{text-align:left}td{vertical-align:middle}td.cat{background-color: #C2C6BA;font-weight:bold;height:20px;letter-spacing:1px;text-indent:4px}td.genmed,.genmed{font-size:11px}td.rowpic{background: #C2C6BA}td.spacerow{background: #E5E6E2}th{background-color: #FADD31;background-image:url(images/cellpic3.gif);background-repeat:repeat-x;color: #68685E;font-size:11px;font-weight:bold;line-height:16px;height:16px;padding-left:8px;padding-right:8px;text-align:center;white-space:nowrap}.admin,.mod{font-size:11px;font-weight:bold}.admin,a.admin,a.admin:visited{color: #FFA34F}.bodyline{background: #FFF;border:1px solid #98AAB1}.center{text-align:center}.code{background: #FAFAFA;border:1px solid #D1D7DC;color: #060;font:12px Courier,"Courier New",sans-serif;padding:5px}.errorline{background: #E5E6E2;border:1px solid #8F8B8B;color:#D92A2A}.explaintitle{color: #5C81B1;font-size:11px;font-weight:bold}.forumline{background: #FFF}.gensmall{font-size:10px}.h1-font{color: #069;display:inline;font:bold 13px Verdana,Arial,Helvetica,sans-serif;margin:0;text-decoration:none}.h2-font{display:inline;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px}.height1{height:1px}.height22{height:22px}.height25{height:25px}.height28{height:28px}.height30{height:30px}.height40{height:40px}.helpline{border:0 solid;font-size:10px}.imgfolder{margin:1px 4px 1px 4px}.imgspace{margin-left:1px;margin-right:2px}.imgtopic,.imgicon{margin-left:3px}.left{text-align:left}.maintitle,h1,h2{color: #5C81B1;font:bold 20px/120% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;text-decoration:none}.maxwidth{width:100%}.mod,a.mod,a.mod:visited{color: #060}.name{font-size:11px;font-weight:bold}.nav{font-size:11px;font-weight:bold}.nowrap{white-space:nowrap}.postbody{font-size:12px;line-height:125%}.postbody a{text-decoration:underline}.postdetails{color: #00396A;font-size:10px}.quote{background: #F3F3EF;border:1px solid #C2C6BA;color: #069;font-size:11px;line-height:125%}.right{text-align:right}.row1{background: #F0F0EB}.row2,.helpline{background: #E5E6E2}.row3{background: #DBDBD4}.subtitle,h2{font:bold 18px/180% "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;text-decoration:none}.topictitle{color: #000;font-size:11px;font-weight:bold}.underline{text-decoration:underline}.top{vertical-align:top}.image-hspace{margin-right:3px}.clear{clear:both} \ No newline at end of file diff --git a/test/html/before.html b/test/html/before.html new file mode 100644 index 0000000..a50d5aa --- /dev/null +++ b/test/html/before.html @@ -0,0 +1,91 @@ + + + + + + + + + + + css Zen Garden: The Beauty in CSS Design + + + + + + + + + + + + +
+ +
+	White  space  is  important   here!
+		
+
+

A demonstration of what can be accomplished visually through CSS-based design. Select any style sheet from the list to load it into this page.

+

Download the sample html file and css file

+
+
+ + \ No newline at end of file diff --git a/test/html/before.min.html b/test/html/before.min.html new file mode 100644 index 0000000..454e1c6 --- /dev/null +++ b/test/html/before.min.html @@ -0,0 +1,13 @@ +css Zen Garden: The Beauty in CSS Design
+	White  space  is  important   here!
+		

A demonstration of what can be accomplished visually through CSS-based design. Select any style sheet from the list to load it into this page.

Download the sample html file and css file

\ No newline at end of file diff --git a/test/js/before.js b/test/js/before.js new file mode 100644 index 0000000..4c0b090 --- /dev/null +++ b/test/js/before.js @@ -0,0 +1,32 @@ +// is.js + +// (c) 2001 Douglas Crockford +// 2001 June 3 + + +// is + +// The -is- object is used to identify the browser. Every browser edition +// identifies itself, but there is no standard way of doing it, and some of +// the identification is deceptive. This is because the authors of web +// browsers are liars. For example, Microsoft's IE browsers claim to be +// Mozilla 4. Netscape 6 claims to be version 5. + +var is = { + ie: navigator.appName == 'Microsoft Internet Explorer', + java: navigator.javaEnabled(), + ns: navigator.appName == 'Netscape', + ua: navigator.userAgent.toLowerCase(), + version: parseFloat(navigator.appVersion.substr(21)) || + parseFloat(navigator.appVersion), + win: navigator.platform == 'Win32' +} +is.mac = is.ua.indexOf('mac') >= 0; +if (is.ua.indexOf('opera') >= 0) { + is.ie = is.ns = false; + is.opera = true; +} +if (is.ua.indexOf('gecko') >= 0) { + is.ie = is.ns = false; + is.gecko = true; +} \ No newline at end of file diff --git a/test/js/before.min.js b/test/js/before.min.js new file mode 100644 index 0000000..1b4c3d9 --- /dev/null +++ b/test/js/before.min.js @@ -0,0 +1,3 @@ +var is={ie:navigator.appName=='Microsoft Internet Explorer',java:navigator.javaEnabled(),ns:navigator.appName=='Netscape',ua:navigator.userAgent.toLowerCase(),version:parseFloat(navigator.appVersion.substr(21))||parseFloat(navigator.appVersion),win:navigator.platform=='Win32'} +is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){is.ie=is.ns=false;is.opera=true;} +if(is.ua.indexOf('gecko')>=0){is.ie=is.ns=false;is.gecko=true;} \ No newline at end of file diff --git a/test/minify/QueryString.js b/test/minify/QueryString.js new file mode 100644 index 0000000..d926d64 --- /dev/null +++ b/test/minify/QueryString.js @@ -0,0 +1,168 @@ +var MrClay = window.MrClay || {}; + +/** + * Simplified access to/manipulation of the query string + * + * Based on: http://adamv.com/dev/javascript/files/querystring.js + * Design pattern: http://www.litotes.demon.co.uk/js_info/private_static.html#wConst + */ +MrClay.QueryString = function(){ + /** + * @static + * @private + */ + var parse = function(str) { + var assignments = str.split('&') + ,obj = {} + ,propValue; + for (var i = 0, l = assignments.length; i < l; ++i) { + propValue = assignments[i].split('='); + if (propValue.length > 2 + || -1 != propValue[0].indexOf('+') + || propValue[0] == '' + ) { + continue; + } + if (propValue.length == 1) { + propValue[1] = propValue[0]; + } + obj[unescape(propValue[0])] = unescape(propValue[1].replace(/\+/g, ' ')); + } + return obj; + }; + + /** + * Constructor (MrClay.QueryString becomes this) + * + * @param mixed A window object, a query string, or empty (default current window) + */ + function construct_(spec) { + spec = spec || window; + if (typeof spec == 'object') { + // get querystring from window + this.window = spec; + spec = spec.location.search.substr(1); + } else { + this.window = window; + } + this.vars = parse(spec); + } + + /** + * Reload the window + * + * @static + * @public + * @param object vars Specify querystring vars only if you wish to replace them + * @param object window_ window to be reloaded (current window by default) + */ + construct_.reload = function(vars, window_) { + window_ = window_ || window; + vars = vars || (new MrClay.QueryString(window_)).vars; + var l = window_.location + ,currUrl = l.href + ,s = MrClay.QueryString.toString(vars) + ,newUrl = l.protocol + '//' + l.hostname + l.pathname + + (s ? '?' + s : '') + l.hash; + if (currUrl == newUrl) { + l.reload(); + } else { + l.assign(newUrl); + } + }; + + /** + * Get the value of a querystring var + * + * @static + * @public + * @param string key + * @param mixed default_ value to return if key not found + * @param object window_ window to check (current window by default) + * @return mixed + */ + construct_.get = function(key, default_, window_) { + window_ = window_ || window; + return (new MrClay.QueryString(window_)).get(key, default_); + }; + + /** + * Reload the page setting one or multiple querystring vars + * + * @static + * @public + * @param mixed key object of query vars/values, or a string key for a single + * assignment + * @param mixed null for multiple settings, the value to assign for single + * @param object window_ window to reload (current window by default) + */ + construct_.set = function(key, value, window_) { + window_ = window_ || window; + (new MrClay.QueryString(window_)).set(key, value).reload(); + }; + + /** + * Convert an object of query vars/values to a querystring + * + * @static + * @public + * @param object query vars/values + * @return string + */ + construct_.toString = function(vars) { + var pieces = []; + for (var prop in vars) { + pieces.push(escape(prop) + '=' + escape(vars[prop])); + } + return pieces.join('&'); + }; + + /** + * @public + */ + construct_.prototype.reload = function() { + MrClay.QueryString.reload(this.vars, this.window); + return this; + }; + + /** + * @public + */ + construct_.prototype.get = function(key, default_) { + if (typeof default_ == 'undefined') { + default_ = null; + } + return (this.vars[key] == null) + ? default_ + : this.vars[key]; + }; + + /** + * @public + */ + construct_.prototype.set = function(key, value) { + var obj = {}; + if (typeof key == 'string') { + obj[key] = value; + } else { + obj = key; + } + for (var prop in obj) { + if (obj[prop] == null) { + delete this.vars[prop]; + } else { + this.vars[prop] = obj[prop]; + } + } + return this; + }; + + /** + * @public + */ + construct_.prototype.toString = function() { + return QueryString.toString(this.vars); + }; + + return construct_; +}(); // define and execute \ No newline at end of file diff --git a/test/minify/email.js b/test/minify/email.js new file mode 100644 index 0000000..b725379 --- /dev/null +++ b/test/minify/email.js @@ -0,0 +1,24 @@ +// http://mrclay.org/ +(function(){ + var + reMailto = /^mailto:my_name_is_(\S+)_and_the_domain_is_(\S+)$/, + reRemoveTitleIf = /^my name is/, + oo = window.onload, + fixHrefs = function() { + var i = 0, l, m; + while (l = document.links[i++]) { + // require phrase in href property + if (m = l.href.match(reMailto)) { + l.href = 'mailto:' + m[1] + '@' + m[2]; + if (reRemoveTitleIf.test(l.title)) { + l.title = ''; + } + } + } + }; + // end var + window.onload = function() { + oo && oo(); + fixHrefs(); + }; +})(); \ No newline at end of file diff --git a/test/packer/before.js b/test/packer/before.js new file mode 100644 index 0000000..4c0b090 --- /dev/null +++ b/test/packer/before.js @@ -0,0 +1,32 @@ +// is.js + +// (c) 2001 Douglas Crockford +// 2001 June 3 + + +// is + +// The -is- object is used to identify the browser. Every browser edition +// identifies itself, but there is no standard way of doing it, and some of +// the identification is deceptive. This is because the authors of web +// browsers are liars. For example, Microsoft's IE browsers claim to be +// Mozilla 4. Netscape 6 claims to be version 5. + +var is = { + ie: navigator.appName == 'Microsoft Internet Explorer', + java: navigator.javaEnabled(), + ns: navigator.appName == 'Netscape', + ua: navigator.userAgent.toLowerCase(), + version: parseFloat(navigator.appVersion.substr(21)) || + parseFloat(navigator.appVersion), + win: navigator.platform == 'Win32' +} +is.mac = is.ua.indexOf('mac') >= 0; +if (is.ua.indexOf('opera') >= 0) { + is.ie = is.ns = false; + is.opera = true; +} +if (is.ua.indexOf('gecko') >= 0) { + is.ie = is.ns = false; + is.gecko = true; +} \ No newline at end of file diff --git a/test/packer/before.min.js b/test/packer/before.min.js new file mode 100644 index 0000000..ee2234a --- /dev/null +++ b/test/packer/before.min.js @@ -0,0 +1 @@ +eval(function(p,a,c,k,e,d){e=function(c){return c.toString(36)};if(!''.replace(/^/,String)){while(c--){d[c.toString(a)]=k[c]||c.toString(a)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('j 1={5:2.7==\'m k i\',g:2.h(),6:2.7==\'l\',3:2.u.s(),t:9(2.b.r(q))||9(2.b),n:2.o==\'p\'}1.a=1.3.4(\'a\')>=0;d(1.3.4(\'c\')>=0){1.5=1.6=e;1.c=8}d(1.3.4(\'f\')>=0){1.5=1.6=e;1.f=8}',31,31,'|is|navigator|ua|indexOf|ie|ns|appName|true|parseFloat|mac|appVersion|opera|if|false|gecko|java|javaEnabled|Explorer|var|Internet|Netscape|Microsoft|win|platform|Win32|21|substr|toLowerCase|version|userAgent'.split('|'),0,{})) \ No newline at end of file diff --git a/test/test.php b/test/test.php deleted file mode 100644 index 9cc505e..0000000 --- a/test/test.php +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/test/test_CSS.php b/test/test_CSS.php new file mode 100644 index 0000000..5b895f6 --- /dev/null +++ b/test/test_CSS.php @@ -0,0 +1,32 @@ +read())) { + if (preg_match('/^([\w\\-]+)\.css$/', $entry, $m)) { + $list[] = $m[1]; + } +} +$d->close(); + +foreach ($list as $item) { + + $options = ($item === 'paths') + ? array('prependRelativePath' => '../') + : array(); + + $src = file_get_contents($thisDir . '/css/' . $item . '.css'); + $minExpected = file_get_contents($thisDir . '/css/' . $item . '.min.css'); + $minOutput = Minify_CSS::minify($src, $options); + assertTrue($minExpected === $minOutput, 'Minify_CSS : ' . $item); + + if ($minExpected !== $minOutput) { + echo "\n---Source\n\n{$src}"; + echo "\n---Expected\n\n{$minExpected}"; + echo "\n---Output\n\n{$minOutput}\n\n\n\n"; + } +} + diff --git a/test/test_HTML.php b/test/test_HTML.php new file mode 100644 index 0000000..c734858 --- /dev/null +++ b/test/test_HTML.php @@ -0,0 +1,22 @@ + array('Minify_CSS', 'minify') + ,'jsMinifier' => array('Minify_Javascript', 'minify') +)); + +$passed = assertTrue($minExpected === $minOutput, 'Minify_HTML'); + +echo "\n---Output: " .strlen($minOutput). " bytes\n\n{$minOutput}"; +if (! $passed) { + echo "\n\n\n\n---Expected: " .strlen($minExpected). " bytes\n\n{$minExpected}"; +} +echo "\n\n---Source: " .strlen($src). " bytes\n\n{$src}"; diff --git a/test/test_Javascript.php b/test/test_Javascript.php new file mode 100644 index 0000000..d473234 --- /dev/null +++ b/test/test_Javascript.php @@ -0,0 +1,16 @@ +