1157 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			1157 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | /** | ||
|  |  * Sunlight | ||
|  |  *    Intelligent syntax highlighting | ||
|  |  * | ||
|  |  * http://sunlightjs.com/
 | ||
|  |  * | ||
|  |  * by Tommy Montgomery <http://tmont.com>
 | ||
|  |  * Licensed under WTFPL <http://sam.zoy.org/wtfpl/>
 | ||
|  |  */ | ||
|  | (function(window, document, undefined){ | ||
|  | 
 | ||
|  | 	var  | ||
|  | 		//http://webreflection.blogspot.com/2009/01/32-bytes-to-know-if-your-browser-is-ie.html
 | ||
|  | 		//we have to sniff this because IE requires \r
 | ||
|  | 		isIe = !+"\v1",  | ||
|  | 		EOL = isIe ? "\r" : "\n", | ||
|  | 		EMPTY = function() { return null; }, | ||
|  | 		HIGHLIGHTED_NODE_COUNT = 0, | ||
|  | 		DEFAULT_LANGUAGE = "plaintext", | ||
|  | 		DEFAULT_CLASS_PREFIX = "sunlight-", | ||
|  | 		 | ||
|  | 		//global sunlight variables
 | ||
|  | 		defaultAnalyzer, | ||
|  | 		getComputedStyle, | ||
|  | 		globalOptions = { | ||
|  | 			tabWidth: 4, | ||
|  | 			classPrefix: DEFAULT_CLASS_PREFIX, | ||
|  | 			showWhitespace: false, | ||
|  | 			maxHeight: false | ||
|  | 		}, | ||
|  | 		languages = {}, | ||
|  | 		languageDefaults = {}, | ||
|  | 		events = { | ||
|  | 			beforeHighlightNode: [], | ||
|  | 			beforeHighlight: [], | ||
|  | 			beforeTokenize: [], | ||
|  | 			afterTokenize: [], | ||
|  | 			beforeAnalyze: [], | ||
|  | 			afterAnalyze: [], | ||
|  | 			afterHighlight: [], | ||
|  | 			afterHighlightNode: [] | ||
|  | 		}; | ||
|  | 
 | ||
|  | 	defaultAnalyzer = (function() { | ||
|  | 		function defaultHandleToken(suffix) { | ||
|  | 			return function(context) { | ||
|  | 				var element = document.createElement("span"); | ||
|  | 				element.className = context.options.classPrefix + suffix; | ||
|  | 				element.appendChild(context.createTextNode(context.tokens[context.index])); | ||
|  | 				return context.addNode(element) || true; | ||
|  | 			}; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return { | ||
|  | 			handleToken: function(context) {  | ||
|  | 				return defaultHandleToken(context.tokens[context.index].name)(context);  | ||
|  | 			}, | ||
|  | 
 | ||
|  | 			//just append default content as a text node
 | ||
|  | 			handle_default: function(context) {  | ||
|  | 				return context.addNode(context.createTextNode(context.tokens[context.index]));  | ||
|  | 			}, | ||
|  | 
 | ||
|  | 			//this handles the named ident mayhem
 | ||
|  | 			handle_ident: function(context) { | ||
|  | 				var evaluate = function(rules, createRule) { | ||
|  | 					var i; | ||
|  | 					rules = rules || []; | ||
|  | 					for (i = 0; i < rules.length; i++) { | ||
|  | 						if (typeof(rules[i]) === "function") { | ||
|  | 							if (rules[i](context)) { | ||
|  | 								return defaultHandleToken("named-ident")(context); | ||
|  | 							} | ||
|  | 						} else if (createRule && createRule(rules[i])(context.tokens)) { | ||
|  | 							return defaultHandleToken("named-ident")(context); | ||
|  | 						} | ||
|  | 					} | ||
|  | 
 | ||
|  | 					return false; | ||
|  | 				}; | ||
|  | 
 | ||
|  | 				return evaluate(context.language.namedIdentRules.custom) | ||
|  | 					|| evaluate(context.language.namedIdentRules.follows, function(ruleData) { return createProceduralRule(context.index - 1, -1, ruleData, context.language.caseInsensitive); }) | ||
|  | 					|| evaluate(context.language.namedIdentRules.precedes, function(ruleData) { return createProceduralRule(context.index + 1, 1, ruleData, context.language.caseInsensitive); }) | ||
|  | 					|| evaluate(context.language.namedIdentRules.between, function(ruleData) { return createBetweenRule(context.index, ruleData.opener, ruleData.closer, context.language.caseInsensitive); }) | ||
|  | 					|| defaultHandleToken("ident")(context); | ||
|  | 			} | ||
|  | 		}; | ||
|  | 	}()); | ||
|  | 
 | ||
|  | 	languageDefaults = { | ||
|  | 		analyzer: create(defaultAnalyzer), | ||
|  | 		customTokens: [], | ||
|  | 		namedIdentRules: {}, | ||
|  | 		punctuation: /[^\w\s]/, | ||
|  | 		numberParser: defaultNumberParser, | ||
|  | 		caseInsensitive: false, | ||
|  | 		doNotParse: /\s/, | ||
|  | 		contextItems: {}, | ||
|  | 		embeddedLanguages: {} | ||
|  | 	}; | ||
|  | 	 | ||
|  | 	//adapted from http://blargh.tommymontgomery.com/2010/04/get-computed-style-in-javascript/
 | ||
|  | 	getComputedStyle = (function() { | ||
|  | 		var func = null; | ||
|  | 		if (document.defaultView && document.defaultView.getComputedStyle) { | ||
|  | 			func = document.defaultView.getComputedStyle; | ||
|  | 		} else { | ||
|  | 			func = function(element, anything) { | ||
|  | 				return element["currentStyle"] || {}; | ||
|  | 			}; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return function(element, style) { | ||
|  | 			return func(element, null)[style]; | ||
|  | 		} | ||
|  | 	}()); | ||
|  | 	 | ||
|  | 	//-----------
 | ||
|  | 	//FUNCTIONS
 | ||
|  | 	//-----------
 | ||
|  | 
 | ||
|  | 	function createCodeReader(text) { | ||
|  | 		var index = 0, | ||
|  | 			line = 1, | ||
|  | 			column = 1, | ||
|  | 			length, | ||
|  | 			EOF = undefined, | ||
|  | 			currentChar, | ||
|  | 			nextReadBeginsLine; | ||
|  | 
 | ||
|  | 		text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); //normalize line endings to unix
 | ||
|  | 
 | ||
|  | 		length = text.length; | ||
|  | 		currentChar = length > 0 ? text.charAt(0) : EOF; | ||
|  | 
 | ||
|  | 		function getCharacters(count) { | ||
|  | 			var value; | ||
|  | 			if (count === 0) { | ||
|  | 				return ""; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			count = count || 1; | ||
|  | 
 | ||
|  | 			value = text.substring(index + 1, index + count + 1); | ||
|  | 			return value === "" ? EOF : value; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return { | ||
|  | 			toString: function() { | ||
|  | 				return "length: " + length + ", index: " + index + ", line: " + line + ", column: " + column + ", current: [" + currentChar + "]"; | ||
|  | 			}, | ||
|  | 
 | ||
|  | 			peek: function(count) { | ||
|  | 				return getCharacters(count); | ||
|  | 			}, | ||
|  | 
 | ||
|  | 			substring: function() { | ||
|  | 				return text.substring(index); | ||
|  | 			}, | ||
|  | 
 | ||
|  | 			peekSubstring: function() { | ||
|  | 				return text.substring(index + 1); | ||
|  | 			}, | ||
|  | 
 | ||
|  | 			read: function(count) { | ||
|  | 				var value = getCharacters(count), | ||
|  | 					newlineCount, | ||
|  | 					lastChar; | ||
|  | 
 | ||
|  | 				if (value === "") { | ||
|  | 					//this is a result of reading/peeking/doing nothing
 | ||
|  | 					return value; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				if (value !== EOF) { | ||
|  | 					//advance index
 | ||
|  | 					index += value.length; | ||
|  | 					column += value.length; | ||
|  | 
 | ||
|  | 					//update line count
 | ||
|  | 					if (nextReadBeginsLine) { | ||
|  | 						line++; | ||
|  | 						column = 1; | ||
|  | 						nextReadBeginsLine = false; | ||
|  | 					} | ||
|  | 
 | ||
|  | 					newlineCount = value.substring(0, value.length - 1).replace(/[^\n]/g, "").length; | ||
|  | 					if (newlineCount > 0) { | ||
|  | 						line += newlineCount; | ||
|  | 						column = 1; | ||
|  | 					} | ||
|  | 
 | ||
|  | 					lastChar = last(value); | ||
|  | 					if (lastChar === "\n") { | ||
|  | 						nextReadBeginsLine = true; | ||
|  | 					} | ||
|  | 
 | ||
|  | 					currentChar = lastChar; | ||
|  | 				} else { | ||
|  | 					index = length; | ||
|  | 					currentChar = EOF; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return value; | ||
|  | 			}, | ||
|  | 
 | ||
|  | 			text: function() { return text; }, | ||
|  | 
 | ||
|  | 			getLine: function() { return line; }, | ||
|  | 			getColumn: function() { return column; }, | ||
|  | 			isEof: function() { return index >= length; }, | ||
|  | 			isSol: function() { return column === 1; }, | ||
|  | 			isSolWs: function() { | ||
|  | 				var temp = index, | ||
|  | 					c; | ||
|  | 				if (column === 1) { | ||
|  | 					return true; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				//look backward until we find a newline or a non-whitespace character
 | ||
|  | 				while ((c = text.charAt(--temp)) !== "") { | ||
|  | 					if (c === "\n") { | ||
|  | 						return true; | ||
|  | 					} | ||
|  | 					if (!/\s/.test(c)) { | ||
|  | 						return false; | ||
|  | 					} | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return true; | ||
|  | 			}, | ||
|  | 			isEol: function() { return nextReadBeginsLine; }, | ||
|  | 			EOF: EOF, | ||
|  | 			current: function() { return currentChar; } | ||
|  | 		}; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	//http://javascript.crockford.com/prototypal.html
 | ||
|  | 	function create(o) { | ||
|  | 		function F() {} | ||
|  | 		F.prototype = o; | ||
|  | 		return new F(); | ||
|  | 	} | ||
|  | 	 | ||
|  | 	function appendAll(parent, children) { | ||
|  | 		var i; | ||
|  | 		for (i = 0; i < children.length; i++) { | ||
|  | 			parent.appendChild(children[i]); | ||
|  | 		} | ||
|  | 	} | ||
|  | 	 | ||
|  | 	//gets the last character in a string or the last element in an array
 | ||
|  | 	function last(thing) { | ||
|  | 		return thing.charAt ? thing.charAt(thing.length - 1) : thing[thing.length - 1]; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	//array.contains()
 | ||
|  | 	function contains(arr, value, caseInsensitive) { | ||
|  | 		var i; | ||
|  | 		if (arr.indexOf && !caseInsensitive) { | ||
|  | 			return arr.indexOf(value) >= 0; | ||
|  | 		} | ||
|  | 		 | ||
|  | 		for (i = 0; i < arr.length; i++) { | ||
|  | 			if (arr[i] === value) { | ||
|  | 				return true; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if (caseInsensitive && typeof(arr[i]) === "string" && typeof(value) === "string" && arr[i].toUpperCase() === value.toUpperCase()) { | ||
|  | 				return true; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return false; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	//non-recursively merges one object into the other
 | ||
|  | 	function merge(defaultObject, objectToMerge) { | ||
|  | 		var key; | ||
|  | 		if (!objectToMerge) { | ||
|  | 			return defaultObject; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		for (key in objectToMerge) { | ||
|  | 			defaultObject[key] = objectToMerge[key]; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return defaultObject; | ||
|  | 	} | ||
|  | 	 | ||
|  | 	function clone(object) { | ||
|  | 		return merge({}, object); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	//http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript/3561711#3561711
 | ||
|  | 	function regexEscape(s) { | ||
|  | 		return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	function createProceduralRule(startIndex, direction, tokenRequirements, caseInsensitive) { | ||
|  | 		tokenRequirements = tokenRequirements.slice(0); | ||
|  | 		return function(tokens) { | ||
|  | 			var tokenIndexStart = startIndex, | ||
|  | 				j, | ||
|  | 				expected, | ||
|  | 				actual; | ||
|  | 				 | ||
|  | 			if (direction === 1) { | ||
|  | 				tokenRequirements.reverse(); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			for (j = 0; j < tokenRequirements.length; j++) { | ||
|  | 				actual = tokens[tokenIndexStart + (j * direction)]; | ||
|  | 				expected = tokenRequirements[tokenRequirements.length - 1 - j]; | ||
|  | 
 | ||
|  | 				if (actual === undefined) { | ||
|  | 					if (expected["optional"] !== undefined && expected.optional) { | ||
|  | 						tokenIndexStart -= direction; | ||
|  | 					} else { | ||
|  | 						return false; | ||
|  | 					} | ||
|  | 				} else if (actual.name === expected.token && (expected["values"] === undefined || contains(expected.values, actual.value, caseInsensitive))) { | ||
|  | 					//derp
 | ||
|  | 					continue; | ||
|  | 				} else if (expected["optional"] !== undefined && expected.optional) { | ||
|  | 					tokenIndexStart -= direction; //we need to reevaluate against this token again
 | ||
|  | 				} else { | ||
|  | 					return false; | ||
|  | 				} | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return true; | ||
|  | 		}; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	function createBetweenRule(startIndex, opener, closer, caseInsensitive) { | ||
|  | 		return function(tokens) { | ||
|  | 			var index = startIndex, | ||
|  | 				token, | ||
|  | 				success = false; | ||
|  | 
 | ||
|  | 			//check to the left: if we run into a closer or never run into an opener, fail
 | ||
|  | 			while ((token = tokens[--index]) !== undefined) { | ||
|  | 				if (token.name === closer.token && contains(closer.values, token.value)) { | ||
|  | 					if (token.name === opener.token && contains(opener.values, token.value, caseInsensitive)) { | ||
|  | 						//if the closer is the same as the opener that's okay
 | ||
|  | 						success = true; | ||
|  | 						break; | ||
|  | 					} | ||
|  | 
 | ||
|  | 					return false; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				if (token.name === opener.token && contains(opener.values, token.value, caseInsensitive)) { | ||
|  | 					success = true; | ||
|  | 					break; | ||
|  | 				} | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if (!success) { | ||
|  | 				return false; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			//check to the right for the closer
 | ||
|  | 			index = startIndex; | ||
|  | 			while ((token = tokens[++index]) !== undefined) { | ||
|  | 				if (token.name === opener.token && contains(opener.values, token.value, caseInsensitive)) { | ||
|  | 					if (token.name === closer.token && contains(closer.values, token.value, caseInsensitive)) { | ||
|  | 						//if the closer is the same as the opener that's okay
 | ||
|  | 						success = true; | ||
|  | 						break; | ||
|  | 					} | ||
|  | 
 | ||
|  | 					return false; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				if (token.name === closer.token && contains(closer.values, token.value, caseInsensitive)) { | ||
|  | 					success = true; | ||
|  | 					break; | ||
|  | 				} | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return success; | ||
|  | 		}; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	function matchWord(context, wordMap, tokenName, doNotRead) { | ||
|  | 		var current = context.reader.current(), | ||
|  | 			i, | ||
|  | 			word, | ||
|  | 			peek, | ||
|  | 			line = context.reader.getLine(), | ||
|  | 			column = context.reader.getColumn(); | ||
|  | 			 | ||
|  | 		wordMap = wordMap || []; | ||
|  | 		if (context.language.caseInsensitive) { | ||
|  | 			current = current.toUpperCase(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if (!wordMap[current]) { | ||
|  | 			return null; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		wordMap = wordMap[current]; | ||
|  | 		for (i = 0; i < wordMap.length; i++) { | ||
|  | 			word = wordMap[i].value; | ||
|  | 
 | ||
|  | 			peek = current + context.reader.peek(word.length); | ||
|  | 			if (word === peek || wordMap[i].regex.test(peek)) { | ||
|  | 				return context.createToken( | ||
|  | 					tokenName, | ||
|  | 					context.reader.current() + context.reader[doNotRead ? "peek" : "read"](word.length - 1), | ||
|  | 					line, | ||
|  | 					column | ||
|  | 				); | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return null; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	//gets the next token in the specified direction while matcher matches the current token
 | ||
|  | 	function getNextWhile(tokens, index, direction, matcher) { | ||
|  | 		var count = 1,  | ||
|  | 			token; | ||
|  | 		 | ||
|  | 		direction = direction || 1; | ||
|  | 		while (token = tokens[index + (direction * count++)]) { | ||
|  | 			if (!matcher(token)) { | ||
|  | 				return token; | ||
|  | 			} | ||
|  | 		} | ||
|  | 		 | ||
|  | 		return undefined; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	//this is crucial for performance
 | ||
|  | 	function createHashMap(wordMap, boundary, caseInsensitive) { | ||
|  | 		//creates a hash table where the hash is the first character of the word
 | ||
|  | 		var newMap = { }, | ||
|  | 			i, | ||
|  | 			word, | ||
|  | 			firstChar; | ||
|  | 		 | ||
|  | 		for (i = 0; i < wordMap.length; i++) { | ||
|  | 			word = caseInsensitive ? wordMap[i].toUpperCase() : wordMap[i]; | ||
|  | 			firstChar = word.charAt(0); | ||
|  | 			if (!newMap[firstChar]) { | ||
|  | 				newMap[firstChar] = []; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			newMap[firstChar].push({ value: word, regex: new RegExp("^" + regexEscape(word) + boundary, caseInsensitive ? "i" : "") }); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return newMap; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	function defaultNumberParser(context) { | ||
|  | 		var current = context.reader.current(),  | ||
|  | 			number,  | ||
|  | 			line = context.reader.getLine(),  | ||
|  | 			column = context.reader.getColumn(), | ||
|  | 			allowDecimal = true, | ||
|  | 			peek; | ||
|  | 
 | ||
|  | 		if (!/\d/.test(current)) { | ||
|  | 			//is it a decimal followed by a number?
 | ||
|  | 			if (current !== "." || !/\d/.test(context.reader.peek())) { | ||
|  | 				return null; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			//decimal without leading zero
 | ||
|  | 			number = current + context.reader.read(); | ||
|  | 			allowDecimal = false; | ||
|  | 		} else { | ||
|  | 			number = current; | ||
|  | 			if (current === "0" && context.reader.peek() !== ".") { | ||
|  | 				//hex or octal
 | ||
|  | 				allowDecimal = false; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		//easy way out: read until it's not a number or letter
 | ||
|  | 		//this will work for hex (0xef), octal (012), decimal and scientific notation (1e3)
 | ||
|  | 		//anything else and you're on your own
 | ||
|  | 
 | ||
|  | 		while ((peek = context.reader.peek()) !== context.reader.EOF) { | ||
|  | 			if (!/[A-Za-z0-9]/.test(peek)) { | ||
|  | 				if (peek === "." && allowDecimal && /\d$/.test(context.reader.peek(2))) { | ||
|  | 					number += context.reader.read(); | ||
|  | 					allowDecimal = false; | ||
|  | 					continue; | ||
|  | 				} | ||
|  | 				 | ||
|  | 				break; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			number += context.reader.read(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return context.createToken("number", number, line, column); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	function fireEvent(eventName, highlighter, eventContext) { | ||
|  | 		var delegates = events[eventName] || [], | ||
|  | 			i; | ||
|  | 		 | ||
|  | 		for (i = 0; i < delegates.length; i++) { | ||
|  | 			delegates[i].call(highlighter, eventContext); | ||
|  | 		} | ||
|  | 	} | ||
|  | 	 | ||
|  | 	function Highlighter(options) { | ||
|  | 		this.options = merge(clone(globalOptions), options); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	Highlighter.prototype = (function() { | ||
|  | 		var parseNextToken = (function() { | ||
|  | 			function isIdentMatch(context) { | ||
|  | 				return context.language.identFirstLetter && context.language.identFirstLetter.test(context.reader.current()); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			//token parsing functions
 | ||
|  | 			function parseKeyword(context) { | ||
|  | 				return matchWord(context, context.language.keywords, "keyword"); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			function parseCustomTokens(context) { | ||
|  | 				var tokenName, | ||
|  | 					token; | ||
|  | 				if (context.language.customTokens === undefined) { | ||
|  | 					return null; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				for (tokenName in context.language.customTokens) { | ||
|  | 					token = matchWord(context, context.language.customTokens[tokenName], tokenName); | ||
|  | 					if (token !== null) { | ||
|  | 						return token; | ||
|  | 					} | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return null; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			function parseOperator(context) { | ||
|  | 				return matchWord(context, context.language.operators, "operator"); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			function parsePunctuation(context) { | ||
|  | 				var current = context.reader.current(); | ||
|  | 				if (context.language.punctuation.test(regexEscape(current))) { | ||
|  | 					return context.createToken("punctuation", current, context.reader.getLine(), context.reader.getColumn()); | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return null; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			function parseIdent(context) { | ||
|  | 				var ident, | ||
|  | 					peek, | ||
|  | 					line = context.reader.getLine(), | ||
|  | 					column = context.reader.getColumn(); | ||
|  | 
 | ||
|  | 				if (!isIdentMatch(context)) { | ||
|  | 					return null; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				ident = context.reader.current(); | ||
|  | 				while ((peek = context.reader.peek()) !== context.reader.EOF) { | ||
|  | 					if (!context.language.identAfterFirstLetter.test(peek)) { | ||
|  | 						break; | ||
|  | 					} | ||
|  | 
 | ||
|  | 					ident += context.reader.read(); | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return context.createToken("ident", ident, line, column); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			function parseDefault(context) { | ||
|  | 				if (context.defaultData.text === "") { | ||
|  | 					//new default token
 | ||
|  | 					context.defaultData.line = context.reader.getLine(); | ||
|  | 					context.defaultData.column = context.reader.getColumn(); | ||
|  | 				} | ||
|  | 
 | ||
|  | 				context.defaultData.text += context.reader.current(); | ||
|  | 				return null; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			function parseScopes(context) { | ||
|  | 				var current = context.reader.current(), | ||
|  | 					tokenName, | ||
|  | 					specificScopes, | ||
|  | 					j, | ||
|  | 					opener, | ||
|  | 					line, | ||
|  | 					column, | ||
|  | 					continuation, | ||
|  | 					value; | ||
|  | 
 | ||
|  | 				for (tokenName in context.language.scopes) { | ||
|  | 					specificScopes = context.language.scopes[tokenName]; | ||
|  | 					for (j = 0; j < specificScopes.length; j++) { | ||
|  | 						opener = specificScopes[j][0]; | ||
|  | 
 | ||
|  | 						value = current + context.reader.peek(opener.length - 1); | ||
|  | 
 | ||
|  | 						if (opener !== value && (!context.language.caseInsensitive || value.toUpperCase() !== opener.toUpperCase())) { | ||
|  | 							continue; | ||
|  | 						} | ||
|  | 
 | ||
|  | 						line = context.reader.getLine(), column = context.reader.getColumn(); | ||
|  | 						context.reader.read(opener.length - 1); | ||
|  | 						continuation = getScopeReaderFunction(specificScopes[j], tokenName); | ||
|  | 						return continuation(context, continuation, value, line, column); | ||
|  | 					} | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return null; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			function parseNumber(context) { | ||
|  | 				return context.language.numberParser(context); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			function parseCustomRules(context) { | ||
|  | 				var customRules = context.language.customParseRules, | ||
|  | 					i, | ||
|  | 					token; | ||
|  | 
 | ||
|  | 				if (customRules === undefined) { | ||
|  | 					return null; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				for (i = 0; i < customRules.length; i++) { | ||
|  | 					token = customRules[i](context); | ||
|  | 					if (token) { | ||
|  | 						return token; | ||
|  | 					} | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return null; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return function(context) { | ||
|  | 				if (context.language.doNotParse.test(context.reader.current())) { | ||
|  | 					return parseDefault(context); | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return parseCustomRules(context) | ||
|  | 					|| parseCustomTokens(context) | ||
|  | 					|| parseKeyword(context) | ||
|  | 					|| parseScopes(context) | ||
|  | 					|| parseIdent(context) | ||
|  | 					|| parseNumber(context) | ||
|  | 					|| parseOperator(context) | ||
|  | 					|| parsePunctuation(context) | ||
|  | 					|| parseDefault(context); | ||
|  | 			} | ||
|  | 		}()); | ||
|  | 		 | ||
|  | 		function getScopeReaderFunction(scope, tokenName) { | ||
|  | 			var escapeSequences = scope[2] || [], | ||
|  | 				closerLength = scope[1].length, | ||
|  | 				closer = typeof(scope[1]) === "string" ? new RegExp(regexEscape(scope[1])) : scope[1].regex, | ||
|  | 				zeroWidth = scope[3] || false; | ||
|  | 
 | ||
|  | 			//processCurrent indicates that this is being called from a continuation
 | ||
|  | 			//which means that we need to process the current char, rather than peeking at the next
 | ||
|  | 			return function(context, continuation, buffer, line, column, processCurrent) { | ||
|  | 				var foundCloser = false; | ||
|  | 				buffer = buffer || ""; | ||
|  | 					 | ||
|  | 				processCurrent = processCurrent ? 1 : 0; | ||
|  | 
 | ||
|  | 				function process(processCurrent) { | ||
|  | 					//check for escape sequences
 | ||
|  | 					var peekValue, | ||
|  | 						current = context.reader.current(), | ||
|  | 						i; | ||
|  | 					 | ||
|  | 					for (i = 0; i < escapeSequences.length; i++) { | ||
|  | 						peekValue = (processCurrent ? current : "") + context.reader.peek(escapeSequences[i].length - processCurrent); | ||
|  | 						if (peekValue === escapeSequences[i]) { | ||
|  | 							buffer += context.reader.read(peekValue.length - processCurrent); | ||
|  | 							return true; | ||
|  | 						} | ||
|  | 					} | ||
|  | 
 | ||
|  | 					peekValue = (processCurrent ? current : "") + context.reader.peek(closerLength - processCurrent); | ||
|  | 					if (closer.test(peekValue)) { | ||
|  | 						foundCloser = true; | ||
|  | 						return false; | ||
|  | 					} | ||
|  | 
 | ||
|  | 					buffer += processCurrent ? current : context.reader.read(); | ||
|  | 					return true; | ||
|  | 				}; | ||
|  | 
 | ||
|  | 				if (!processCurrent || process(true)) { | ||
|  | 					while (context.reader.peek() !== context.reader.EOF && process(false)) { } | ||
|  | 				} | ||
|  | 
 | ||
|  | 				if (processCurrent) { | ||
|  | 					buffer += context.reader.current(); | ||
|  | 					context.reader.read(); | ||
|  | 				} else { | ||
|  | 					buffer += zeroWidth || context.reader.peek() === context.reader.EOF ? "" : context.reader.read(closerLength); | ||
|  | 				} | ||
|  | 
 | ||
|  | 				if (!foundCloser) { | ||
|  | 					//we need to signal to the context that this scope was never properly closed
 | ||
|  | 					//this has significance for partial parses (e.g. for nested languages)
 | ||
|  | 					context.continuation = continuation; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return context.createToken(tokenName, buffer, line, column); | ||
|  | 			}; | ||
|  | 		} | ||
|  | 		 | ||
|  | 		//called before processing the current
 | ||
|  | 		function switchToEmbeddedLanguageIfNecessary(context) { | ||
|  | 			var i, | ||
|  | 				embeddedLanguage; | ||
|  | 			 | ||
|  | 			for (i = 0; i < context.language.embeddedLanguages.length; i++) { | ||
|  | 				if (!languages[context.language.embeddedLanguages[i].language]) { | ||
|  | 					//unregistered language
 | ||
|  | 					continue; | ||
|  | 				} | ||
|  | 				 | ||
|  | 				embeddedLanguage = clone(context.language.embeddedLanguages[i]); | ||
|  | 				 | ||
|  | 				if (embeddedLanguage.switchTo(context)) { | ||
|  | 					embeddedLanguage.oldItems = clone(context.items); | ||
|  | 					context.embeddedLanguageStack.push(embeddedLanguage); | ||
|  | 					context.language = languages[embeddedLanguage.language]; | ||
|  | 					context.items = merge(context.items, clone(context.language.contextItems)); | ||
|  | 					break; | ||
|  | 				} | ||
|  | 			} | ||
|  | 		} | ||
|  | 		 | ||
|  | 		//called after processing the current
 | ||
|  | 		function switchBackFromEmbeddedLanguageIfNecessary(context) { | ||
|  | 			var current = last(context.embeddedLanguageStack), | ||
|  | 				lang; | ||
|  | 			 | ||
|  | 			if (current && current.switchBack(context)) { | ||
|  | 				context.language = languages[current.parentLanguage]; | ||
|  | 				lang = context.embeddedLanguageStack.pop(); | ||
|  | 				 | ||
|  | 				//restore old items
 | ||
|  | 				context.items = clone(lang.oldItems); | ||
|  | 				lang.oldItems = {}; | ||
|  | 			} | ||
|  | 		} | ||
|  | 		 | ||
|  | 		function tokenize(unhighlightedCode, language, partialContext, options) { | ||
|  | 			var tokens = [], | ||
|  | 				context, | ||
|  | 				continuation, | ||
|  | 				token; | ||
|  | 				 | ||
|  | 			fireEvent("beforeTokenize", this, { code: unhighlightedCode, language: language }); | ||
|  | 			context = { | ||
|  | 				reader: createCodeReader(unhighlightedCode), | ||
|  | 				language: language, | ||
|  | 				items: clone(language.contextItems), | ||
|  | 				token: function(index) { return tokens[index]; }, | ||
|  | 				getAllTokens: function() { return tokens.slice(0); }, | ||
|  | 				count: function() { return tokens.length; }, | ||
|  | 				options: options, | ||
|  | 				embeddedLanguageStack: [], | ||
|  | 				 | ||
|  | 				defaultData: { | ||
|  | 					text: "", | ||
|  | 					line: 1, | ||
|  | 					column: 1 | ||
|  | 				}, | ||
|  | 				createToken: function(name, value, line, column) { | ||
|  | 					return { | ||
|  | 						name: name, | ||
|  | 						line: line, | ||
|  | 						value: isIe ? value.replace(/\n/g, "\r") : value, | ||
|  | 						column: column, | ||
|  | 						language: this.language.name | ||
|  | 					}; | ||
|  | 				} | ||
|  | 			}; | ||
|  | 
 | ||
|  | 			//if continuation is given, then we need to pick up where we left off from a previous parse
 | ||
|  | 			//basically it indicates that a scope was never closed, so we need to continue that scope
 | ||
|  | 			if (partialContext.continuation) { | ||
|  | 				continuation = partialContext.continuation; | ||
|  | 				partialContext.continuation = null; | ||
|  | 				tokens.push(continuation(context, continuation, "", context.reader.getLine(), context.reader.getColumn(), true)); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			while (!context.reader.isEof()) { | ||
|  | 				switchToEmbeddedLanguageIfNecessary(context); | ||
|  | 				token = parseNextToken(context); | ||
|  | 
 | ||
|  | 				//flush default data if needed (in pretty much all languages this is just whitespace)
 | ||
|  | 				if (token !== null) { | ||
|  | 					if (context.defaultData.text !== "") { | ||
|  | 						tokens.push(context.createToken("default", context.defaultData.text, context.defaultData.line, context.defaultData.column)); | ||
|  | 						context.defaultData.text = ""; | ||
|  | 					} | ||
|  | 
 | ||
|  | 					if (token[0] !== undefined) { | ||
|  | 						//multiple tokens
 | ||
|  | 						tokens = tokens.concat(token); | ||
|  | 					} else { | ||
|  | 						//single token
 | ||
|  | 						tokens.push(token); | ||
|  | 					} | ||
|  | 				} | ||
|  | 
 | ||
|  | 				switchBackFromEmbeddedLanguageIfNecessary(context); | ||
|  | 				context.reader.read(); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			//append the last default token, if necessary
 | ||
|  | 			if (context.defaultData.text !== "") { | ||
|  | 				tokens.push(context.createToken("default", context.defaultData.text, context.defaultData.line, context.defaultData.column)); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			fireEvent("afterTokenize", this, { code: unhighlightedCode, parserContext: context }); | ||
|  | 			return context; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		function createAnalyzerContext(parserContext, partialContext, options) { | ||
|  | 			var nodes = [], | ||
|  | 				prepareText = function() { | ||
|  | 					var nbsp, tab; | ||
|  | 					if (options.showWhitespace) { | ||
|  | 						nbsp = String.fromCharCode(0xB7); | ||
|  | 						tab = new Array(options.tabWidth).join(String.fromCharCode(0x2014)) + String.fromCharCode(0x2192); | ||
|  | 					} else { | ||
|  | 						nbsp = String.fromCharCode(0xA0); | ||
|  | 						tab = new Array(options.tabWidth + 1).join(nbsp); | ||
|  | 					} | ||
|  | 					 | ||
|  | 					return function(token) { | ||
|  | 						var value = token.value.split(" ").join(nbsp), | ||
|  | 							tabIndex, | ||
|  | 							lastNewlineColumn, | ||
|  | 							actualColumn, | ||
|  | 							tabLength; | ||
|  | 						 | ||
|  | 						//tabstop madness: replace \t with the appropriate number of characters, depending on the tabWidth option and its relative position in the line
 | ||
|  | 						while ((tabIndex = value.indexOf("\t")) >= 0) { | ||
|  | 							lastNewlineColumn = value.lastIndexOf(EOL, tabIndex); | ||
|  | 							actualColumn = lastNewlineColumn === -1 ? tabIndex : tabIndex - lastNewlineColumn - 1; | ||
|  | 							tabLength = options.tabWidth - (actualColumn % options.tabWidth); //actual length of the TAB character
 | ||
|  | 							 | ||
|  | 							value = value.substring(0, tabIndex) + tab.substring(options.tabWidth - tabLength) + value.substring(tabIndex + 1); | ||
|  | 						} | ||
|  | 						 | ||
|  | 						return value; | ||
|  | 					}; | ||
|  | 				}(); | ||
|  | 
 | ||
|  | 			return { | ||
|  | 				tokens: (partialContext.tokens || []).concat(parserContext.getAllTokens()), | ||
|  | 				index: partialContext.index ? partialContext.index + 1 : 0, | ||
|  | 				language: null, | ||
|  | 				getAnalyzer: EMPTY, | ||
|  | 				options: options, | ||
|  | 				continuation: parserContext.continuation, | ||
|  | 				addNode: function(node) { nodes.push(node); }, | ||
|  | 				createTextNode: function(token) { return document.createTextNode(prepareText(token)); }, | ||
|  | 				getNodes: function() { return nodes; }, | ||
|  | 				resetNodes: function() { nodes = []; }, | ||
|  | 				items: parserContext.items | ||
|  | 			}; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		//partialContext allows us to perform a partial parse, and then pick up where we left off at a later time
 | ||
|  | 		//this functionality enables nested highlights (language within a language, e.g. PHP within HTML followed by more PHP)
 | ||
|  | 		function highlightText(unhighlightedCode, languageId, partialContext) { | ||
|  | 			var language = languages[languageId], | ||
|  | 				analyzerContext; | ||
|  | 			 | ||
|  | 			partialContext = partialContext || { }; | ||
|  | 			if (language === undefined) { | ||
|  | 				//use default language if one wasn't specified or hasn't been registered
 | ||
|  | 				language = languages[DEFAULT_LANGUAGE]; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			fireEvent("beforeHighlight", this, { code: unhighlightedCode, language: language, previousContext: partialContext }); | ||
|  | 			 | ||
|  | 			analyzerContext = createAnalyzerContext( | ||
|  | 				tokenize.call(this, unhighlightedCode, language, partialContext, this.options), | ||
|  | 				partialContext, | ||
|  | 				this.options | ||
|  | 			); | ||
|  | 			 | ||
|  | 			analyze.call(this, analyzerContext, partialContext.index ? partialContext.index + 1 : 0); | ||
|  | 			 | ||
|  | 			fireEvent("afterHighlight", this, { analyzerContext: analyzerContext }); | ||
|  | 
 | ||
|  | 			return analyzerContext; | ||
|  | 		} | ||
|  | 		 | ||
|  | 		function createContainer(ctx) { | ||
|  | 			var container = document.createElement("span"); | ||
|  | 			container.className = ctx.options.classPrefix + ctx.language.name; | ||
|  | 			return container; | ||
|  | 		} | ||
|  | 		 | ||
|  | 		function analyze(analyzerContext, startIndex) { | ||
|  | 			var nodes, | ||
|  | 				lastIndex, | ||
|  | 				container, | ||
|  | 				i, | ||
|  | 				tokenName, | ||
|  | 				func, | ||
|  | 				language, | ||
|  | 				analyzer; | ||
|  | 			 | ||
|  | 			fireEvent("beforeAnalyze", this, { analyzerContext: analyzerContext }); | ||
|  | 			 | ||
|  | 			if (analyzerContext.tokens.length > 0) { | ||
|  | 				analyzerContext.language = languages[analyzerContext.tokens[0].language] || languages[DEFAULT_LANGUAGE];; | ||
|  | 				nodes = []; | ||
|  | 				lastIndex = 0; | ||
|  | 				container = createContainer(analyzerContext); | ||
|  | 				 | ||
|  | 				for (i = startIndex; i < analyzerContext.tokens.length; i++) { | ||
|  | 					language = languages[analyzerContext.tokens[i].language] || languages[DEFAULT_LANGUAGE]; | ||
|  | 					if (language.name !== analyzerContext.language.name) { | ||
|  | 						appendAll(container, analyzerContext.getNodes()); | ||
|  | 						analyzerContext.resetNodes(); | ||
|  | 						 | ||
|  | 						nodes.push(container); | ||
|  | 						analyzerContext.language = language; | ||
|  | 						container = createContainer(analyzerContext); | ||
|  | 					} | ||
|  | 					 | ||
|  | 					analyzerContext.index = i; | ||
|  | 					tokenName = analyzerContext.tokens[i].name; | ||
|  | 					func = "handle_" + tokenName; | ||
|  | 
 | ||
|  | 					analyzer = analyzerContext.getAnalyzer.call(analyzerContext) || analyzerContext.language.analyzer; | ||
|  | 					analyzer[func] ? analyzer[func](analyzerContext) : analyzer.handleToken(analyzerContext); | ||
|  | 				} | ||
|  | 				 | ||
|  | 				//append the last nodes, and add the final nodes to the context
 | ||
|  | 				appendAll(container, analyzerContext.getNodes()); | ||
|  | 				nodes.push(container); | ||
|  | 				analyzerContext.resetNodes(); | ||
|  | 				for (i = 0; i < nodes.length; i++) { | ||
|  | 					analyzerContext.addNode(nodes[i]); | ||
|  | 				} | ||
|  | 			} | ||
|  | 			 | ||
|  | 			fireEvent("afterAnalyze", this, { analyzerContext: analyzerContext }); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return { | ||
|  | 			//matches the language of the node to highlight
 | ||
|  | 			matchSunlightNode: function() { | ||
|  | 				var regex; | ||
|  | 				 | ||
|  | 				return function(node) { | ||
|  | 					if (!regex) { | ||
|  | 						regex = new RegExp("(?:\\s|^)" + this.options.classPrefix + "highlight-(\\S+)(?:\\s|$)"); | ||
|  | 					} | ||
|  | 					 | ||
|  | 					return regex.exec(node.className); | ||
|  | 				}; | ||
|  | 			}(), | ||
|  | 			 | ||
|  | 			//determines if the node has already been highlighted
 | ||
|  | 			isAlreadyHighlighted: function() { | ||
|  | 				var regex; | ||
|  | 				return function(node) { | ||
|  | 					if (!regex) { | ||
|  | 						regex = new RegExp("(?:\\s|^)" + this.options.classPrefix + "highlighted(?:\\s|$)"); | ||
|  | 					} | ||
|  | 					 | ||
|  | 					return regex.test(node.className); | ||
|  | 				}; | ||
|  | 			}(), | ||
|  | 			 | ||
|  | 			//highlights a block of text
 | ||
|  | 			highlight: function(code, languageId) { return highlightText.call(this, code, languageId); }, | ||
|  | 
 | ||
|  | 			//recursively highlights a DOM node
 | ||
|  | 			highlightNode: function highlightRecursive(node) { | ||
|  | 				var match, | ||
|  | 					languageId, | ||
|  | 					currentNodeCount, | ||
|  | 					j, | ||
|  | 					nodes, | ||
|  | 					k, | ||
|  | 					partialContext, | ||
|  | 					container, | ||
|  | 					codeContainer; | ||
|  | 				 | ||
|  | 				if (this.isAlreadyHighlighted(node) || (match = this.matchSunlightNode(node)) === null) { | ||
|  | 					return; | ||
|  | 				} | ||
|  | 
 | ||
|  | 				languageId = match[1]; | ||
|  | 				currentNodeCount = 0; | ||
|  | 				fireEvent("beforeHighlightNode", this, { node: node }); | ||
|  | 				for (j = 0; j < node.childNodes.length; j++) { | ||
|  | 					if (node.childNodes[j].nodeType === 3) { | ||
|  | 						//text nodes
 | ||
|  | 						partialContext = highlightText.call(this, node.childNodes[j].nodeValue, languageId, partialContext); | ||
|  | 						HIGHLIGHTED_NODE_COUNT++; | ||
|  | 						currentNodeCount = currentNodeCount || HIGHLIGHTED_NODE_COUNT; | ||
|  | 						nodes = partialContext.getNodes(); | ||
|  | 
 | ||
|  | 						node.replaceChild(nodes[0], node.childNodes[j]); | ||
|  | 						for (k = 1; k < nodes.length; k++) { | ||
|  | 							node.insertBefore(nodes[k], nodes[k - 1].nextSibling); | ||
|  | 						} | ||
|  | 					} else if (node.childNodes[j].nodeType === 1) { | ||
|  | 						//element nodes
 | ||
|  | 						highlightRecursive.call(this, node.childNodes[j]); | ||
|  | 					} | ||
|  | 				} | ||
|  | 
 | ||
|  | 				//indicate that this node has been highlighted
 | ||
|  | 				node.className += " " + this.options.classPrefix + "highlighted"; | ||
|  | 				 | ||
|  | 				//if the node is block level, we put it in a container, otherwise we just leave it alone
 | ||
|  | 				if (getComputedStyle(node, "display") === "block") { | ||
|  | 					container = document.createElement("div"); | ||
|  | 					container.className = this.options.classPrefix + "container"; | ||
|  | 					 | ||
|  | 					codeContainer = document.createElement("div"); | ||
|  | 					codeContainer.className = this.options.classPrefix + "code-container"; | ||
|  | 
 | ||
|  | 					//apply max height if specified in options
 | ||
|  | 					if (this.options.maxHeight !== false) { | ||
|  | 						codeContainer.style.overflowY = "auto"; | ||
|  | 						codeContainer.style.maxHeight = this.options.maxHeight + (/^\d+$/.test(this.options.maxHeight) ? "px" : ""); | ||
|  | 					} | ||
|  | 					 | ||
|  | 					container.appendChild(codeContainer); | ||
|  | 					 | ||
|  | 					node.parentNode.insertBefore(codeContainer, node); | ||
|  | 					node.parentNode.removeChild(node); | ||
|  | 					codeContainer.appendChild(node); | ||
|  | 					 | ||
|  | 					codeContainer.parentNode.insertBefore(container, codeContainer); | ||
|  | 					codeContainer.parentNode.removeChild(codeContainer); | ||
|  | 					container.appendChild(codeContainer); | ||
|  | 				} | ||
|  | 				 | ||
|  | 				fireEvent("afterHighlightNode", this, {  | ||
|  | 					container: container, | ||
|  | 					codeContainer: codeContainer, | ||
|  | 					node: node,  | ||
|  | 					count: currentNodeCount | ||
|  | 				}); | ||
|  | 			} | ||
|  | 		}; | ||
|  | 	}()); | ||
|  | 
 | ||
|  | 	//public facing object
 | ||
|  | 	window.Sunlight = { | ||
|  | 		version: "1.18", | ||
|  | 		Highlighter: Highlighter, | ||
|  | 		createAnalyzer: function() { return create(defaultAnalyzer); }, | ||
|  | 		globalOptions: globalOptions, | ||
|  | 
 | ||
|  | 		highlightAll: function(options) { | ||
|  | 			var highlighter = new Highlighter(options), | ||
|  | 				tags = document.getElementsByTagName("*"), | ||
|  | 				i; | ||
|  | 			 | ||
|  | 			for (i = 0; i < tags.length; i++) { | ||
|  | 				highlighter.highlightNode(tags[i]); | ||
|  | 			} | ||
|  | 		}, | ||
|  | 
 | ||
|  | 		registerLanguage: function(languageId, languageData) { | ||
|  | 			var tokenName, | ||
|  | 				embeddedLanguages, | ||
|  | 				languageName; | ||
|  | 			 | ||
|  | 			if (!languageId) { | ||
|  | 				throw "Languages must be registered with an identifier, e.g. \"php\" for PHP"; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			languageData = merge(merge({}, languageDefaults), languageData); | ||
|  | 			languageData.name = languageId; | ||
|  | 
 | ||
|  | 			//transform keywords, operators and custom tokens into a hash map
 | ||
|  | 			languageData.keywords = createHashMap(languageData.keywords || [], "\\b", languageData.caseInsensitive); | ||
|  | 			languageData.operators = createHashMap(languageData.operators || [], "", languageData.caseInsensitive); | ||
|  | 			for (tokenName in languageData.customTokens) { | ||
|  | 				languageData.customTokens[tokenName] = createHashMap( | ||
|  | 					languageData.customTokens[tokenName].values, | ||
|  | 					languageData.customTokens[tokenName].boundary, | ||
|  | 					languageData.caseInsensitive | ||
|  | 				); | ||
|  | 			} | ||
|  | 			 | ||
|  | 			//convert the embedded language object to an easier-to-use array
 | ||
|  | 			embeddedLanguages = []; | ||
|  | 			for (languageName in languageData.embeddedLanguages) { | ||
|  | 				embeddedLanguages.push({ | ||
|  | 					parentLanguage: languageData.name, | ||
|  | 					language: languageName, | ||
|  | 					switchTo: languageData.embeddedLanguages[languageName].switchTo, | ||
|  | 					switchBack: languageData.embeddedLanguages[languageName].switchBack | ||
|  | 				}); | ||
|  | 			} | ||
|  | 			 | ||
|  | 			languageData.embeddedLanguages = embeddedLanguages; | ||
|  | 
 | ||
|  | 			languages[languageData.name] = languageData; | ||
|  | 		}, | ||
|  | 		 | ||
|  | 		isRegistered: function(languageId) { return languages[languageId] !== undefined; }, | ||
|  | 		 | ||
|  | 		bind: function(event, callback) { | ||
|  | 			if (!events[event]) { | ||
|  | 				throw "Unknown event \"" + event + "\""; | ||
|  | 			} | ||
|  | 			 | ||
|  | 			events[event].push(callback); | ||
|  | 		}, | ||
|  | 
 | ||
|  | 		util: { | ||
|  | 			last: last, | ||
|  | 			regexEscape: regexEscape, | ||
|  | 			eol: EOL, | ||
|  | 			clone: clone, | ||
|  | 			escapeSequences: ["\\n", "\\t", "\\r", "\\\\", "\\v", "\\f"], | ||
|  | 			contains: contains, | ||
|  | 			matchWord: matchWord, | ||
|  | 			createHashMap: createHashMap, | ||
|  | 			createBetweenRule: createBetweenRule, | ||
|  | 			createProceduralRule: createProceduralRule, | ||
|  | 			getNextNonWsToken: function(tokens, index) { return getNextWhile(tokens, index, 1, function(token) { return token.name === "default"; }); }, | ||
|  | 			getPreviousNonWsToken: function(tokens, index) { return getNextWhile(tokens, index, -1, function(token) { return token.name === "default"; }); }, | ||
|  | 			getNextWhile: function(tokens, index, matcher) { return getNextWhile(tokens, index, 1, matcher); }, | ||
|  | 			getPreviousWhile: function(tokens, index, matcher) { return getNextWhile(tokens, index, -1, matcher); }, | ||
|  | 			whitespace: { token: "default", optional: true }, | ||
|  | 			getComputedStyle: getComputedStyle | ||
|  | 		} | ||
|  | 	}; | ||
|  | 
 | ||
|  | 	//register the default language
 | ||
|  | 	window.Sunlight.registerLanguage(DEFAULT_LANGUAGE, { punctuation: /(?!x)x/, numberParser: EMPTY }); | ||
|  | 
 | ||
|  | }(this, document)); |