// CSS3MultiColumn - a javascript implementation of the CSS3 multi-column module
// v1.02 beta - Jan 08 2008
// Copyright (c) 2005 Cdric Savarese <pro@4213miles.com>
// This software is licensed under the CC-GNU LGPL <http://creativecommons.org/licenses/LGPL/2.1/>

// For additional information, see : http://www.csscripting.com/

// Supported Properties:
// column-count
// column-width
// column-gap
// column-rule

// Unsupported Properties:
// column-rule-width (use column-rule instead)
// column-rule-style (use column-rule instead)
// column-rule-color (use column-rule instead)
// column-span
// column-width-policy
// column-space-distribution


function CSS3MultiColumn() {
    //alert('Development Version');
    var cssCache = new Object();
    var splitableTags = new Array('P','DIV', 'SPAN', 'BLOCKQUOTE','ADDRESS','PRE', 'A', 'EM', 'I', 'STRONG', 'B', 'CITE', 'OL', 'UL', 'LI' );
    var pseudoCSSRules = new Object();
    var ut = new CSS3Utility();

    var debug = ut.debug;
    if(document.location.search.match('mode=debug')) var isDebug = true;
    else var isDebug = false;

    var bestSplitPoint = null;
    var secondSplitPoint = null;
    var secondSplitBottom = 0;
    var documentReady = false;

    // INITIALIZATION
    ut.XBrowserAddEventHandler(window,'load',function() { documentReady = true;  processElements(); } );
    loadStylesheets();

    // CSS PARSING
    // --------------------------------------------------------------------------------------
    // loadStylesheets:
    // Loop through the stylesheets collection and load the css text into the cssCache object
    function loadStylesheets() {
        if(document.styleSheets) {  // Firefox & IE
            // initialize cache
            for(var i=0;i < document.styleSheets.length;i++) {
                cssCache[document.styleSheets[i].href] = false;
            }
            // load css in the cache
            for(var i=0;i < document.styleSheets.length;i++) {
                loadCssCache(document.styleSheets[i], 'parseStylesheets');
            }
        } else if (document.getElementsByTagName) { // OPERA
            var Lt = document.getElementsByTagName('link');
            // initialize cache
            for(var i= 0; i<Lt.length; i++) {
                cssCache[Lt[i].href] = false;
            }
            // load css in the cache
            for(var i= 0; i<Lt.length; i++) {
                loadCssCache(Lt[i], 'parseStylesheets');
            }
            //var St = document.getElementsByTagName('style');
        }
    }

    // loadCssCache
    // Asynchronous function. Call the 'callback' function when done.
    function loadCssCache(s, callback) {
        if (s.href && s.cssText) {
            cssCache[s.href] = s.cssText;
            eval(callback)();
        }
        if (s.href && typeof XMLHttpRequest!='undefined') {
            var xmlhttp = new XMLHttpRequest();
            //if(xmlhttp.abort) xmlhttp.abort();
            xmlhttp.onreadystatechange = function() {
                if(xmlhttp.readyState == 4) {
                    if(typeof xmlhttp.status == 'undefined' || xmlhttp.status == 200 || xmlhttp.status == 304 ) {
                        cssCache[s.href] = xmlhttp.responseText;
                        eval(callback)();
                    }
                }
            }
            xmlhttp.open("GET", s.href, true); //synchrone transaction crashes Opera 8.01
            xmlhttp.send(null);
        }
    }

    // parseStylesheets:
    // Iterates the cssCache object and send the serialized css to the mini-parser.
    function parseStylesheets() {
        var allDone = true;
        for(var i in cssCache) {
            if(cssCache[i]!=false) parseStylesheet(cssCache[i]);
            else allDone = false;
        }
        if(allDone) {
            processElements();
        }
    }

    // parseStylesheet:
    // Loads the pseudoCSSRules object with the values for column-count, column-width, column-gap...
    function parseStylesheet(cssText) {

        // Retrieving column-count property
        var cc = new ut.getPseudoCssRules('column-count',cssText);
        for(var i=0; cc && i<cc.cssRules.length;i++) {
            if(!pseudoCSSRules[cc.cssRules[i].selectorText])
                pseudoCSSRules[cc.cssRules[i].selectorText] = new Object();
            pseudoCSSRules[cc.cssRules[i].selectorText]['column-count'] = cc.cssRules[i].value;
        }
        // Retrieving column-width property
        cc = new ut.getPseudoCssRules('column-width',cssText);
        for(var i=0; cc && i<cc.cssRules.length;i++) {
            if(!pseudoCSSRules[cc.cssRules[i].selectorText])
                pseudoCSSRules[cc.cssRules[i].selectorText] = new Object();
            pseudoCSSRules[cc.cssRules[i].selectorText]['column-width'] = cc.cssRules[i].value;
        }
        // Retrieving column-gap property
        cc = new ut.getPseudoCssRules('column-gap',cssText);
        for(var i=0; cc && i<cc.cssRules.length;i++) {
            if(!pseudoCSSRules[cc.cssRules[i].selectorText])
                pseudoCSSRules[cc.cssRules[i].selectorText] = new Object();
            pseudoCSSRules[cc.cssRules[i].selectorText]['column-gap'] = cc.cssRules[i].value;
        }
        // Retrieving column-rule property
        cc = new ut.getPseudoCssRules('column-rule',cssText);
        for(var i=0; cc && i<cc.cssRules.length;i++) {
            if(!pseudoCSSRules[cc.cssRules[i].selectorText])
                pseudoCSSRules[cc.cssRules[i].selectorText] = new Object();
            pseudoCSSRules[cc.cssRules[i].selectorText]['column-rule'] = cc.cssRules[i].value;
        }
    }

    // COLUMN PROCESSING
    function processElements() {
        // wait for page to finish loading
        if(!documentReady) return;

        for(var i in pseudoCSSRules) {
            debug(i + ' cc:' + pseudoCSSRules[i]['column-count'] + ' cw:' + pseudoCSSRules[i]['column-width'] + ' cr:' + pseudoCSSRules[i]['column-rule'] + ' cg:' + pseudoCSSRules[i]['column-gap']);
            var affectedElements = ut.cssQuery(i);
            for(var j=0;j<affectedElements.length;j++) {
                //debug("affected element: " + affectedElements[j].tagName + ' [' + affectedElements[j].id + ' / ' + affectedElements[j].className + ']');
                processElement(affectedElements[j], pseudoCSSRules[i]['column-count'], pseudoCSSRules[i]['column-width'], pseudoCSSRules[i]['column-gap'], pseudoCSSRules[i]['column-rule']);
            }
        }
    }

    function processElement(affectedElement, column_count, column_width, column_gap, column_rule ) {
        //affectedElement.style.visibility = 'hidden';
        var widthUnit;
        var width;
        var column_rule_width = 0;

        // Get available width
        // see http://www.csscripting.com/css-multi-column/dom-width-height.php
        // offsetWidth & scrollWidth are the only consistent values across browsers.
        // offsetWidth includes border, padding and scroll bars
        // scrollWidth includes border and padding
        // clientWidth when available includes padding only.
        // see http://msdn.microsoft.com/workshop/author/om/measuring.asp

        if(affectedElement.clientWidth && affectedElement.clientWidth != 0) {
            var padding;
            if(affectedElement.currentStyle) {
                padding = parseInt(affectedElement.currentStyle.paddingLeft.replace(/[\D]*/gi,"")) + parseInt(affectedElement.currentStyle.paddingRight.replace(/[\D]*/gi,""))
            } else if (document.defaultView && document.defaultView.getComputedStyle) {
                padding = parseInt(document.defaultView.getComputedStyle(affectedElement,"").getPropertyValue("padding-left").replace(/[\D]*/gi,"")) + parseInt(document.defaultView.getComputedStyle(affectedElement,"").getPropertyValue("padding-left").replace(/[\D]*/gi,""))
                //padding = parseInt(window.getComputedStyle(affectedElement,"").getPropertyValue("padding-left").replace(/[\D]*/gi,"")) + parseInt(window.getComputedStyle(affectedElement,"").getPropertyValue("padding-left").replace(/[\D]*/gi,""))
            }

            if (isNaN(padding)) padding = 0;
            width = (affectedElement.clientWidth - padding).toString() + "px";
        }
        else if(affectedElement.scrollWidth) {
            var borderWidth;
            var padding;

            if(affectedElement.currentStyle) {
                padding = parseInt(affectedElement.currentStyle.paddingLeft.replace(/[\D]*/gi,"")) + parseInt(affectedElement.currentStyle.paddingRight.replace(/[\D]*/gi,""))
            } else if (document.defaultView && document.defaultView.getComputedStyle) {
                padding = parseInt(document.defaultView.getComputedStyle(affectedElement,"").getPropertyValue("padding-left").replace(/[\D]*/gi,"")) + parseInt(document.defaultView.getComputedStyle(affectedElement,"").getPropertyValue("padding-left").replace(/[\D]*/gi,""))
            }

            if (isNaN(padding)) padding = 0;

            if(affectedElement.currentStyle) {
                borderWidth = parseInt(affectedElement.currentStyle.borderLeftWidth.replace(/[\D]*/gi,"")) + parseInt(affectedElement.currentStyle.borderRightWidth.replace(/[\D]*/gi,""))
            } else if (document.defaultView && document.defaultView.getComputedStyle) {
                borderWidth = parseInt(document.defaultView.getComputedStyle(affectedElement,"").getPropertyValue("border-left-width").replace(/[\D]*/gi,"")) + parseInt(document.defaultView.getComputedStyle(affectedElement,"").getPropertyValue("border-right-width").replace(/[\D]*/gi,""))
            }
            if (isNaN(borderWidth)) borderWidth = 0;

            width = (affectedElement.scrollWidth - padding - borderWidth).toString() + "px";
        }
        else width = "99%"; // ever used?

        var availableWidth = parseInt(width.replace(/[\D]*/gi,""));

        // Get width unit
        if(!column_width || column_width == 'auto')
            widthUnit = width.replace(/[\d]*/gi,"");
        else
            widthUnit = column_width.replace(/[\d]*/gi,"");
        if(!widthUnit)
            widthUnit = "px";

        if(!column_gap) { // Compute column spacing (column_gap)
            if(widthUnit=="%")
                column_gap = 1; //%;
            else
                column_gap = 15; //px;
        } else {
            column_gap = parseInt(column_gap.replace(/[\D]*/gi,""));
        }
        if(column_rule && column_rule != 'none') {
            column_gap = Math.floor(column_gap/2);
            // we add half the original column_gap to the column_rule_width to fix the column_width count below.
            column_rule_width = column_gap + parseInt(column_rule.substring(column_rule.search(/\d/),column_rule.search(/\D/)));
        }
        if(!column_width || column_width == 'auto') {// Compute columns' width
            column_width = (availableWidth-((column_gap+column_rule_width)*(column_count-1))) / column_count;
        } else {
            column_width = parseInt(column_width.replace(/[\D]*/gi,""))
            if(!column_count || column_count == 'auto') {// Compute column count
                column_count = Math.floor(availableWidth / (column_width + column_gap));
            }
        }

        column_width -= 1;

        // Create a wrapper
        var wrapper = document.createElement('div'); //affectedElement.tagName
        var pn = affectedElement.parentNode;
        wrapper = pn.insertBefore(wrapper, affectedElement);
        var elem =  pn.removeChild(affectedElement);
        elem = wrapper.appendChild(elem);
        //wrapper.style.border = "1px solid #F00";
        wrapper.className = elem.className;
        elem.className = "";
        // since all columns will be left-floating we need to clear the floats after them.
        //wrapper.style.overflow = 'auto';

        // Assign the content element a random Id ?
        elem.id = ut.randomId();

        // Adjust content's width and float the element
        elem.style.width = column_width.toString() + widthUnit;
        //elem.style.padding = "0";
        //elem.style.margin = "0";

        if(typeof elem.style.styleFloat != 'undefined')
            elem.style.styleFloat  = "left";
        if(typeof elem.style.cssFloat != 'undefined')
            elem.style.cssFloat  = "left";

        // Compute Desired Height
        var newHeight = Math.floor(elem.offsetHeight / column_count)+14;
        if(!wrapper.id) wrapper.id = ut.randomId();

        // Find split points (j is the max # of attempts to find a good height with no unsplittable element on the split point.
        var j=1;
        for(var i=1; i < column_count && elem && j < (column_count + 5) ; i++) {
            bestSplitPoint = null;
            secondSplitPoint = null;
            secondSplitBottom = 0;
            findSplitPoint(elem, newHeight*i, wrapper);

            if(isDebug) bestSplitPoint.style.border = "1px solid #00FF00";

            if(bestSplitPoint && !isElementSplitable(bestSplitPoint)) {

                    newHeight = getElementRelativeTop(bestSplitPoint, wrapper) + bestSplitPoint.offsetHeight + 10;
                    i=1; // reset the height. Try again.
                    debug('reset new Height = '+newHeight + ' relativetop=' + getElementRelativeTop(bestSplitPoint, wrapper) + ' offsetHeight= ' + bestSplitPoint.offsetHeight );
            }
            else if (!bestSplitPoint) {
                debug("No split point found with " + newHeight);
            }

            j++;
        }

        //wrapper.style.minHeight = newHeight + 'px';
        //if(document.all && !window.opera)
            //wrapper.style.height = newHeight + 'px';
        debug('<table><tr><td>Avail. Width</td><td>'+availableWidth+'</td><td>Units</td><td>'+widthUnit+'</td></tr><tr><td>column_width</td><td>'+column_width+'</td><td>column_count</td><td>'+column_count+'</td></tr><tr><td>column_gap</td><td>'+column_gap+'</td><td>column_rule</td><td>'+column_rule+'</td></tr><tr><td>New Height</td><td>' + newHeight + '</td><td></td><td></td></tr></table>'  );

        for(var i=1; i < column_count && elem; i++) {
            // Find the split point (a child element, sitting on the column split point)
            bestSplitPoint = null;
            secondSplitPoint = null;
            secondSplitBottom = 0;

            findSplitPoint(elem, newHeight, wrapper);
            if(bestSplitPoint && isElementSplitable(bestSplitPoint) && elem.id != bestSplitPoint.id) {
                var splitE = bestSplitPoint;
                if(isDebug) secondSplitPoint.style.border = "1px dotted #00F";
            }
            else {
                var splitE = secondSplitPoint;
            }
            if(!splitE) {
                debug("<hr />No split point found for " + elem.tagName + ' ' + newHeight);
                return;
            }

            // DEBUG ONLY: SHOW SPLIT ELEMENT
            //debug("split top=" + getElementRelativeTop(splitE, wrapper));
            if(isDebug) splitE.style.border = "1px solid #F00";
            // END DEBUG ONLY: SHOW SPLIT ELEMENT

            // Create New Column
            var newCol = elem.cloneNode(false);
            newCol.id = ut.randomId();

            // Insert new column in the document
            elem.parentNode.insertBefore(newCol, elem.nextSibling);

            // Add the column_gap
            newCol.style.paddingLeft = column_gap + widthUnit;

            // Add the column_rule
            if(column_rule && column_rule != 'none') {
                newCol.style.borderLeft = column_rule;
                elem.style.paddingRight = column_gap + widthUnit;
            }
            if(document.all && !window.opera)
                elem.style.height = newHeight+'px';
            elem.style.minHeight = newHeight+'px';

            // Move all elements after the element to be splitted (splitE) to the new column
            var insertPoint = createNodeAncestors(splitE,elem, newCol, 'append');

            var refElement = splitE;
            while(refElement && refElement.id != elem.id ) {
                var littleSib = refElement.nextSibling;
                while(littleSib) {
                    moveNode(littleSib, elem, newCol);
                    littleSib = refElement.nextSibling;
                }
                refElement = refElement.parentNode;
            }

            var strippedLine = splitElement(splitE, newHeight - getElementRelativeTop(splitE, wrapper), elem, newCol);

            // cleaning emptied elements
            var pn = splitE.parentNode;
            while(pn && pn.id != elem.id) {
                var n = pn.firstChild;
                while(n) {
                    if((n.nodeType==1 && n.childNodes.length == 0) ||
                        (n.nodeType==3 && n.nodeValue.replace(/[\u0020\u0009\u000A]*/,'') == "")) {
                        pn.removeChild(n);
                        n = pn.firstChild;
                    } else {
                        n = n.nextSibling;
                    }
                }
                pn = pn.parentNode;
            }

            // if text-align is justified, insert &nbsp; to force the justify
            if(strippedLine) {
                splitE = elem.lastChild;
                if(splitE && (document.defaultView  && document.defaultView.getComputedStyle(splitE,'').getPropertyValue('text-align')=='justify') ||
                   (splitE.currentStyle && splitE.currentStyle.textAlign == 'justify')) {
                      var txtFiller = document.createTextNode(' ' + strippedLine.replace(/./g,"\u00a0")); // &nbsp;
                      var filler = document.createElement('span');
                      splitE.appendChild(filler);
                      filler.style.lineHeight="1px";
                      filler.appendChild(txtFiller);
                }
            }
            // move on to split the newly added column
            elem = newCol;
        }
        if(elem) {//mainly to set the column rule at the right height.
            if(document.all && !window.opera)
                elem.style.height = newHeight+'px';
            elem.style.minHeight = newHeight+'px';
        }

        var clearFloatDiv = document.createElement('div');
        clearFloatDiv.style.clear = "left";  // < bug in Safari 1.3 ? (duplicates content)
        clearFloatDiv.appendChild(document.createTextNode(' '));
        wrapper.appendChild(clearFloatDiv);
        if(navigator.userAgent.toLowerCase().indexOf('safari') + 1)
            wrapper.innerHTML+=' '; // forces redraw in safari and fixes bug above.

        //wrapper.style.visibility = 'visible';
    }

    // Find the deepest splitable element that sits on the split point.
    function findSplitPoint(n, newHeight, wrapper) {
        if (n.nodeType==1) {
            var top = getElementRelativeTop(n, wrapper);
            var bot = top+n.offsetHeight;
            if(top < newHeight && bot > newHeight) {
                bestSplitPoint = n;
                if(isElementSplitable(n)) {
                    for(var i=0;i<n.childNodes.length;i++) {
                        findSplitPoint(n.childNodes[i], newHeight, wrapper);
                    }
                }
                return;
            }
            if(bot <= newHeight && bot >= secondSplitBottom) {
                secondSplitBottom = bot;
                secondSplitPoint = n;
            }
        }
        return;
    }

    function isElementSplitable(n) {
        if(n.tagName) {
            var tagName = n.tagName.toUpperCase();
            for(var i=0;i<splitableTags.length;i++)
                if(tagName==splitableTags[i]) return true;
        }
        return false;
    }

    function splitElement(n, targetHeight, col1, col2) {

        var cn = n.lastChild;
        while(cn) {
            // if the child node is a text node
            if(cn.nodeType==3) {
                var strippedText = "dummmy";
                var allStrippedText = "";
                // the +2 is for tweaking.. allowing lines to fit more easily
                while(n.offsetHeight > targetHeight+2 && strippedText!="") {
                    // remove lines of text until the splittable element reaches the targeted height or we run out of text.
                    strippedText = stripOneLine(cn);
                    allStrippedText = strippedText + allStrippedText;
                }
                if(allStrippedText!="") {
                    var insertPoint = createNodeAncestors(cn,col1,col2,'insertBefore');
                    insertPoint.insertBefore(document.createTextNode(allStrippedText), insertPoint.firstChild);
                }
                if(cn.nodeValue=="") {
                    cn.parentNode.removeChild(cn);
                }
                else
                    break;
            }
            else {
                // move element
                var insertPoint = createNodeAncestors(cn,col1,col2,'insertBefore');
                insertPoint.insertBefore(cn.parentNode.removeChild(cn), insertPoint.firstChild);
            }
            cn = n.lastChild;
        }
        return strippedText; // returns the last line of text removed (used later for forcing the justification)
    }


    // stripOneLine()
    // This function removes exactly one line to
    // any element containing text
    // and returns the removed text as a string.
    function stripOneLine (n) {
        // get the text node
        while(n && n.nodeType != 3)
            n = n.firstChild;
        if(!n) return;

        // get the height of the element
        var e = n.parentNode;
        var h = e.offsetHeight;

        if(!h) {
            //debug('no height for: ' + e.tagName);
            return "";
        }

        // get the text as a string
        var str = n.nodeValue;

        // remove a word from the end of the string
        // until the height of the element changes
        // (ie. a line has been removed)
        var wIdx= n.nodeValue.lastIndexOf(' ');
        while(wIdx!=-1 && e.offsetHeight == h) {
            n.nodeValue = n.nodeValue.substr(0, wIdx);
            wIdx = n.nodeValue.lastIndexOf(' ');
            if(wIdx==-1) wIdx = n.nodeValue.lastIndexOf('\n');
            //debug(e.offsetHeight + ' ' + h + ' text=' + n.nodeValue + ' wIdx= ' + wIdx);
        }

        if(e.offsetHeight == h)
            n.nodeValue = "";
        // returns the removed text

        return str.substr(n.nodeValue.length);
    }

    // method= 'append'/'insertBefore', relative to col2
    function createNodeAncestors(n,col1,col2,method) {
        var ancestors = new Array;
        var insertNode = col2;
        var pn = n.parentNode;
        while(pn && pn.id != col1.id) {
            ancestors[ancestors.length] = pn;
            if(!pn.id) pn.id = ut.randomId();
            pn = pn.parentNode;
        }

        for (var i=ancestors.length-1; i >= 0; i--) {

            for(var j=0; j < insertNode.childNodes.length && (insertNode.childNodes[j].nodeType==3 || !insertNode.childNodes[j].className.match(ancestors[i].id+'-css3mc')); j++);

            if(j==insertNode.childNodes.length) {
                // Ancestor node not found, needs to be created.
                if(method=='append')
                    insertNode = insertNode.appendChild(document.createElement(ancestors[i].tagName));
                else
                    insertNode = insertNode.insertBefore(document.createElement(ancestors[i].tagName),insertNode.firstChild);
                insertNode.className = ancestors[i].className+ ' ' + ancestors[i].id + '-css3mc';
                insertNode.style.marginTop = "0";
                insertNode.style.paddingTop = "0";
                if(insertNode.tagName.toUpperCase() == 'OL' && n.nodeType == 1 && n.tagName.toUpperCase() =='LI') {
                    var prevsib = n.previousSibling;
                    var count=0;
                    while(prevsib) {
                        if(prevsib.nodeType==1 && prevsib.tagName.toUpperCase() == 'LI')
                            count++;
                        prevsib = prevsib.previousSibling;
                    }
                    insertNode.setAttribute('start', count);
                }
            } else {
                insertNode = insertNode.childNodes[j];
                if(insertNode.tagName.toUpperCase() == 'OL' && (insertNode.start==-1 || insertNode.start==1) && n.nodeType == 1 && n.tagName.toUpperCase() =='LI') {
                    // happens if the tag was created while processing a text node.
                    var prevsib = n.previousSibling;
                    var count=0;
                    while(prevsib) {
                        if(prevsib.nodeType==1 && prevsib.tagName.toUpperCase() == 'LI')
                            count++;
                        prevsib = prevsib.previousSibling;
                    }
                    insertNode.setAttribute('start', count);
                }
            }
        }
        return insertNode;
    }

    function moveNode(n,col1,col2) {
        var insertNode=createNodeAncestors(n,col1,col2, 'append');
        var movedNode = insertNode.appendChild(n.parentNode.removeChild(n));
        if(insertNode.id == col2.id && movedNode.nodeType ==1 ) {
            movedNode.style.paddingTop = "0px";
            movedNode.style.marginTop = "0px";
        }
        return movedNode;
    }


    function getElementRelativeTop(obj, refObj) {
        var cur = 0;
        if(obj.offsetParent) {
            while(obj.offsetParent) {
                cur+=obj.offsetTop;
                obj = obj.offsetParent;
            }
        }
        var cur2 = 0;
        if(refObj.offsetParent) {
            while(refObj.offsetParent) {
                cur2+=refObj.offsetTop;
                refObj = refObj.offsetParent;
            }
        }
        return cur-cur2; // + document.body.offsetTop;
    }

}

// =====================================================================================
// Utility Class Constructor skeleton
function CSS3Utility() {
    // Event Handler utility list
    this.handlerList = new Array();
}


// Public Methods
// ==============

// querying of a DOM document using CSS selectors (a getElementsByTagName on steroids)
// see http://dean.edwards.name/my/cssQuery.js.html
/*
    License: http://creativecommons.org/licenses/by/1.0/
    Author:  Dean Edwards/2004
    Web:     http://dean.edwards.name/
*/
CSS3Utility.prototype.cssQuery = function() {

    var version = "1.0.1"; // timestamp: 2004/05/25

    // constants
    var STANDARD_SELECT = /^[^>\+~\s]/;
    var STREAM = /[\s>\+~:@#\.]|[^\s>\+~:@#\.]+/g;
    var NAMESPACE = /\|/;
    var IMPLIED_SELECTOR = /([\s>\+~\,]|^)([\.:#@])/g;
    var ASTERISK ="$1*$2";
    var WHITESPACE = /^\s+|\s*([\+\,>\s;:])\s*|\s+$/g;
    var TRIM = "$1";
    var NODE_ELEMENT = 1;
    var NODE_TEXT = 3;
    var NODE_DOCUMENT = 9;

    // sniff for explorer (cos of one little bug)
    var isMSIE = /MSIE/.test(navigator.appVersion), isXML;

    // cache results for faster processing
    var cssCache = {};

    // this is the query function
    function cssQuery(selector, from) {
        if (!selector) return [];
        var useCache = arguments.callee.caching && !from;
        from = (from) ? (from.constructor == Array) ? from : [from] : [document];
        isXML = false;//checkXML(from[0]);
        // process comma separated selectors
        var selectors = parseSelector(selector).split(",");
        var match = [];
        for (var i in selectors) {
            // convert the selector to a stream
            selector = toStream(selectors[i]);
            // process the stream
            var j = 0, token, filter, cacheSelector = "", filtered = from;
            while (j < selector.length) {
                token = selector[j++];
                filter = selector[j++];
                cacheSelector += token + filter;
                // process a token/filter pair
                filtered = (useCache && cssCache[cacheSelector]) ? cssCache[cacheSelector] : select(filtered, token, filter);
                if (useCache) cssCache[cacheSelector] = filtered;
            }
            match = match.concat(filtered);
        }
        // return the filtered selection
        return match;
    };
    cssQuery.caching = false;
    cssQuery.reset = function() {
        cssCache = {};
    };
    cssQuery.toString = function () {
        return "function cssQuery() {\n  [version " + version + "]\n}";
    };

    var checkXML = (isMSIE) ? function(node) {
        if (node.nodeType != NODE_DOCUMENT) node = node.document;
        return node.mimeType == "XML Document";
    } : function(node) {
        if (node.nodeType == NODE_DOCUMENT) node = node.documentElement;
        return node.localName != "HTML";
    };

    function parseSelector(selector) {
        return selector
        // trim whitespace
        .replace(WHITESPACE, TRIM)
        // encode attribute selectors
        .replace(attributeSelector.ALL, attributeSelector.ID)
        // e.g. ".class1" --> "*.class1"
        .replace(IMPLIED_SELECTOR, ASTERISK);
    };

    // convert css selectors to a stream of tokens and filters
    //  it's not a real stream. it's just an array of strings.
    function toStream(selector) {
        if (STANDARD_SELECT.test(selector)) selector = " " + selector;
        return selector.match(STREAM) || [];
    };

    var pseudoClasses = { // static
        // CSS1
        "link": function(element) {
            for (var i = 0; i < document.links; i++) {
                if (document.links[i] == element) return true;
            }
        },
        "visited": function(element) {
            // can't do this without jiggery-pokery
        },
        // CSS2
        "first-child": function(element) {
            return !previousElement(element);
        },
        // CSS3
        "last-child": function(element) {
            return !nextElement(element);
        },
        "root": function(element) {
            var document = element.ownerDocument || element.document;
            return Boolean(element == document.documentElement);
        },
        "empty": function(element) {
            for (var i = 0; i < element.childNodes.length; i++) {
                if (isElement(element.childNodes[i]) || element.childNodes[i].nodeType == NODE_TEXT) return false;
            }
            return true;
        }
        // add your own...
    };

    var QUOTED = /([\'\"])[^\1]*\1/;
    function quote(value) {return (QUOTED.test(value)) ? value : "'" + value + "'"};
    function unquote(value) {return (QUOTED.test(value)) ? value.slice(1, -1) : value};

    var attributeSelectors = [];

    function attributeSelector(attribute, compare, value) {
        // properties
        this.id = attributeSelectors.length;
        // build the test expression
        var test = "element.";
        switch (attribute.toLowerCase()) {
            case "id":
                test += "id";
                break;
            case "class":
                test += "className";
                break;
            default:
                test += "getAttribute('" + attribute + "')";
        }
        // continue building the test expression
        switch (compare) {
            case "=":
                test += "==" + quote(value);
                break;
            case "~=":
                test = "/(^|\\s)" + unquote(value) + "(\\s|$)/.test(" + test + ")";
                break;
            case "|=":
                test = "/(^|-)" + unquote(value) + "(-|$)/.test(" + test + ")";
                break;
        }
        push(attributeSelectors, new Function("element", "return " + test));
    };
    attributeSelector.prototype.toString = function() {
        return attributeSelector.PREFIX + this.id;
    };
    // constants
    attributeSelector.PREFIX = "@";
    attributeSelector.ALL = /\[([^~|=\]]+)([~|]?=?)([^\]]+)?\]/g;
    // class methods
    attributeSelector.ID = function(match, attribute, compare, value) {
        return new attributeSelector(attribute, compare, value);
    };

    // select a set of matching elements.
    // "from" is an array of elements.
    // "token" is a character representing the type of filter
    //  e.g. ">" means child selector
    // "filter" represents the tag name, id or class name that is being selected
    // the function returns an array of matching elements
    function select(from, token, filter) {
        //alert("token="+token+",filter="+filter);
        var namespace = "";
        if (NAMESPACE.test(filter)) {
            filter = filter.split("|");
            namespace = filter[0];
            filter = filter[1];
        }
        var filtered = [], i;
        switch (token) {
            case " ": // descendant
                for (i in from) {
                    if(typeof from[i]=='function') continue;
                    var subset = getElementsByTagNameNS(from[i], filter, namespace);
                    for (var j = 0; j < subset.length; j++) {
                        if (isElement(subset[j]) && (!namespace || compareNamespace(subset[j], namespace)))
                            push(filtered, subset[j]);
                    }
                }
                break;
            case ">": // child
                for (i in from) {
                    var subset = from[i].childNodes;
                    for (var j = 0; j < subset.length; j++)
                        if (compareTagName(subset[j], filter, namespace)) push(filtered, subset[j]);
                }
                break;
            case "+": // adjacent (direct)
                for (i in from) {
                    var adjacent = nextElement(from[i]);
                    if (adjacent && compareTagName(adjacent, filter, namespace)) push(filtered, adjacent);
                }
                break;
            case "~": // adjacent (indirect)
                for (i in from) {
                    var adjacent = from[i];
                    while (adjacent = nextElement(adjacent)) {
                        if (adjacent && compareTagName(adjacent, filter, namespace)) push(filtered, adjacent);
                    }
                }
                break;
            case ".": // class
                filter = new RegExp("(^|\\s)" + filter + "(\\s|$)");
                for (i in from) if (filter.test(from[i].className)) push(filtered, from[i]);
                break;
            case "#": // id
                for (i in from) if (from[i].id == filter) push(filtered, from[i]);
                break;
            case "@": // attribute selector
                filter = attributeSelectors[filter];
                for (i in from) if (filter(from[i])) push(filtered, from[i]);
                break;
            case ":": // pseudo-class (static)
                filter = pseudoClasses[filter];
                for (i in from) if (filter(from[i])) push(filtered, from[i]);
                break;
        }
        return filtered;
    };

    var getElementsByTagNameNS = (isMSIE) ? function(from, tagName) {
        return (tagName == "*" && from.all) ? from.all : from.getElementsByTagName(tagName);
    } : function(from, tagName, namespace) {
        return (namespace) ? from.getElementsByTagNameNS("*", tagName) : from.getElementsByTagName(tagName);
    };

    function compareTagName(element, tagName, namespace) {
        if (namespace && !compareNamespace(element, namespace)) return false;
        return (tagName == "*") ? isElement(element) : (isXML) ? (element.tagName == tagName) : (element.tagName == tagName.toUpperCase());
    };

    var PREFIX = (isMSIE) ? "scopeName" : "prefix";
    function compareNamespace(element, namespace) {
        return element[PREFIX] == namespace;
    };

    // return the previous element to the supplied element
    //  previousSibling is not good enough as it might return a text or comment node
    function previousElement(element) {
        while ((element = element.previousSibling) && !isElement(element)) continue;
        return element;
    };

    // return the next element to the supplied element
    function nextElement(element) {
        while ((element = element.nextSibling) && !isElement(element)) continue;
        return element;
    };

    function isElement(node) {
        return Boolean(node.nodeType == NODE_ELEMENT && node.tagName != "!");
    };

    // use a baby push function because IE5.0 doesn't support Array.push
    function push(array, item) {
        array[array.length] = item;
    };

    // fix IE5.0 String.replace
    if ("i".replace(/i/,function(){return""})) {
        // preserve String.replace
        var string_replace = String.prototype.replace;
        // create String.replace for handling functions
        var function_replace = function(regexp, replacement) {
            var match, newString = "", string = this;
            while ((match = regexp.exec(string))) {
                // five string replacement arguments is sufficent for cssQuery
                newString += string.slice(0, match.index) + replacement(match[0], match[1], match[2], match[3], match[4]);
                string = string.slice(match.lastIndex);
            }
            return newString + string;
        };
        // replace String.replace
        String.prototype.replace = function (regexp, replacement) {
            this.replace = (typeof replacement == "function") ? function_replace : string_replace;
            return this.replace(regexp, replacement);
        };
    }

    return cssQuery;
}();

// Cross-Browser event handler.
CSS3Utility.prototype.XBrowserAddEventHandler = function(target,eventName,handlerName) {
    if(!target) return;
    if (target.addEventListener) {
        target.addEventListener(eventName, function(e){eval(handlerName)(e);}, false);
    } else if (target.attachEvent) {
        target.attachEvent("on" + eventName, function(e){eval(handlerName)(e);});
        } else {
        // THIS CODE NOT TESTED
        var originalHandler = target["on" + eventName];
        if (originalHandler) {
          target["on" + eventName] = function(e){originalHandler(e);eval(handlerName)(e);};
        } else {
          target["on" + eventName] = eval(handlerName);
        }
    }
    // Keep track of added handlers.
    var l = this.handlerList.length;
    this.handlerList[l] = new Array(2);
    this.handlerList[l][0] = target.id;
    this.handlerList[l][1] = eventName;
    // see http://weblogs.asp.net/asmith/archive/2003/10/06/30744.aspx
    // for a complete XBrowserAddEventHandler
}



// getPseudoCssRules()
// Constructor for a pseudo-css rule object
// (an unsupported property, thus not present in the DOM rules collection)

// Constructor parameters
// ----------------------
// the css property name
// the stylesheet (as a text stream)

// Object properties:
// ------------------
// selector (string)
// property (string)
// value (string)
CSS3Utility.prototype.getPseudoCssRules = function(propertyName, serializedStylesheet) {
    this.cssRules = new Array();
    var valuePattern = propertyName.replace("-","\-")+"[\\s]*:[\\s]*([^;}]*)[;}]";
    var selectorPattern = "$";
    var regx = new RegExp(valuePattern,"g");
    var regxMatch = regx.exec(serializedStylesheet);
    var j=0;

    while(regxMatch){
        var str = serializedStylesheet.substr(0,serializedStylesheet.substr(0,serializedStylesheet.indexOf(regxMatch[0])).lastIndexOf('{'));
        var selectorText = str.substr(str.lastIndexOf('}')+1).replace(/^\s*|\s*$/g,"");
        // ignore commented rule !!
        this.cssRules[j] = new Object();
        this.cssRules[j].selectorText = selectorText;
        this.cssRules[j].property = propertyName;
        this.cssRules[j].value = regxMatch[1].replace(/(\r?\n)*/g,"");  // suppress line breaks
        j++;
        regxMatch = regx.exec(serializedStylesheet);
    }
}


// Generates a random ID
CSS3Utility.prototype.randomId = function () {
    var rId = "";
    for (var i=0; i<6;i++)
        rId += String.fromCharCode(97 + Math.floor((Math.random()*24)))
    return rId;
}

CSS3Utility.prototype.debug = function(text) {
    var debugOutput = document.getElementById('debugOutput'); // Debug Output
    if(typeof debugOutput != "undefined" && debugOutput) {
        //debugOutput.appendChild(document.createElement('hr'));
        //debugOutput.appendChild(document.createTextNode(text));
        debugOutput.innerHTML+= text;
    }
}

// Object Instance
var css3MC = new CSS3MultiColumn();
