﻿/*
	This work is licensed under a Creative Commons License.

	License: http://creativecommons.org/licenses/by/1.0/

	You are free:

	to copy, distribute, display, and perform the work
	to make derivative works
	to make commercial use of the work

	Under the following conditions:

	Attribution. You must give the original author credit

	Author:  Dean Edwards/2004
	Web:     http://dean.edwards.name/
*/

/* keeping code tidy! */

/* extendible css query function for common platforms

   tested on IE5.0/5.5/6.0, Mozilla 1.6/Firefox 0.8, Opera 7.23/7.5
   (all windows platforms - somebody buy me a mac!)
*/

// -----------------------------------------------------------------------
//  css query engine
// -----------------------------------------------------------------------

var 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 = 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";
		return true;
	} : 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) {
					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;
}();

