/**
 * HTML Code Coloring Kit
 *
 * (c) Rob Signorelli 2004
 *     rsignore@indiana.edu
 *
 * This package (javascript files, css stylesheets, examples) 
 * was written to make web pages more friendly when displaying
 * code segments. Using some predefined (and even user-defined)
 * templates, this software will replace the existing html with
 * tagged html (xhtml compliant) that will be redisplayed with
 * colors according to the code-style.css stylesheet.
 *
 * Feel free to use this software for free at your leisure. Edit
 * the source, create templates for new programming languages,
 * use it whatever pages you want. All I ask is that somewhere
 * you give me some credit (like my name in a comment area or
 * something). Also, let me know about it. I'm always looking to
 * improve this so if you have ideas, I'd be tickled silly to know
 * what you've done. Thanks.
 *
 * Browsers Supported (known):
 *   Mozilla 1.5+
 *   IE 6 (Win32)
 *   Safari 1.1
 *
 * Future considerations:
 *     Multiple code types on a single page
 *     Support for multiple types of keywords (painfully easy)
 */


// our css stylesheet utilizes styling <b class="..."> for syntax coloring
var TAG = "b";

// Identifiers so we know which area we are dealing with
var LINE_COMMENT_ID  = 0;
var BLOCK_COMMENT_ID = 1;
var SINGLE_QUOTE_ID  = 2;
var DOUBLE_QUOTE_ID  = 3;
var KEYWORD_ID       = 4;

// These are the css classes that are defined in code-style.css
var cssClasses = new Array(5);
cssClasses[LINE_COMMENT_ID]  = "comment";
cssClasses[BLOCK_COMMENT_ID] = "comment";
cssClasses[SINGLE_QUOTE_ID]  = "quotedCode";
cssClasses[DOUBLE_QUOTE_ID]  = "quotedCode";
cssClasses[KEYWORD_ID]       = "keyword";

// These are the different coloring "modes" we can be in
var inFlag = new Array(5);
inFlag[LINE_COMMENT_ID] = false;
inFlag[BLOCK_COMMENT_ID] = false;
inFlag[SINGLE_QUOTE_ID] = false;
inFlag[DOUBLE_QUOTE_ID] = false;
inFlag[KEYWORD_ID]      = false;       // No need for this (yet?)

// The syntax object that we use for reference
var syntaxModule = null;

/**
 * This is the method you should call from the "onload" event in the
 * body of your HTML page to enable syntax coloring
 */
function runSyntaxColorKit(syntax)
{
	syntaxModule = syntax;

	var elements = getElementsByStyleClass("code");
	for( var i = 0; i < elements.length; i++ )
	{
		var html = "";
		var currentWord = "";

		for( j = LINE_COMMENT_ID; j < KEYWORD_ID; j++ )
			inFlag[j] = false;

		var strTok = new StringTokenizer(elements[i].innerHTML, syntax.DELIMITERS);
		while( strTok.hasMoreTokens() )
		{
			var t = strTok.nextToken();
			currentWord = t.getValue();

			// Handle escape sequence if necessary
			currentWord = applyEscapeSequence(currentWord, strTok);

			// Check to see if we are either starting/ending a comment
			currentWord = tagOpenComment(currentWord, strTok);
			currentWord = tagCloseComment(currentWord, strTok);

			// Check to see if we are in quotes
			currentWord = tagQuotes(currentWord);

			// Check to see if this is a keyword
			currentWord = tagKeyword(currentWord);

			// Add the current built string to the html buffer
			html += currentWord;
		}

		elements[i].innerHTML = html;
	}
}

/**
 * Reset the desired flag and return a string containing
 * the properly tagged text
 */
function toggleMode(currentWord, type)
{
	inFlag[type] = !inFlag[type];
	return inFlag[type] ? openTag(currentWord, type)
						: closeTag(currentWord);
}

/**
 * This method returns the tagged version of a word assuming
 * that it is a keyword, otherwise it returns just the word
 * with no formatting.
 */
function tagKeyword(currentWord)
{
	return (!inComment() && !inQuotes() && isKeyword(currentWord))
				? closeTag(openTag(currentWord, KEYWORD_ID))
				: currentWord;
}

/**
 * This function handles the tagging for syntax coloring when
 * we open a comment. So before the comment, we will tag it
 * with an opening comment tag.
 */
function tagOpenComment(currentWord, strTok)
{
	return !inComment() ? tagComment(currentWord,
									 strTok,
									 syntaxModule.BLOCK_COMMENT_OPEN,
									 syntaxModule.LINE_COMMENT_OPEN)
                        : currentWord;
}

/**
 * This function handles the tagging for syntax coloring when
 * a comment is closed (either type). It appends the closing
 * comment tag to the input text so the text will be the last
 * comment-colored item in this comment.
 */
function tagCloseComment(currentWord, strTok)
{
	return inComment() ? tagComment(currentWord,
								    strTok,
								    syntaxModule.BLOCK_COMMENT_CLOSE,
								    syntaxModule.LINE_COMMENT_CLOSE)
					   : currentWord;
}

/**
 * This is the workhorse function for both of the tag*Comment()
 * functions since their implementations are so similar
 */
function tagComment(currentWord, strTok, blockTest, lineTest)
{
	var sizeLimit = max(blockTest.length, lineTest.length);
	while( (startsWith(blockTest, currentWord) || startsWith(lineTest, currentWord))
		   && currentWord.length <= sizeLimit )
	{
		if( !inFlag[LINE_COMMENT_ID] && currentWord == blockTest )
			return toggleMode(currentWord, BLOCK_COMMENT_ID);

		if( !inFlag[BLOCK_COMMENT_ID] && currentWord == lineTest )
			return toggleMode(currentWord, LINE_COMMENT_ID);

		if( currentWord.length <= sizeLimit )
			currentWord += strTok.nextToken().getValue();
	}
	return currentWord;
}

/**
 * This function checks the global flags to see the state of the
 * "colorizer" and determines if the text passed in needs to be
 * tagged for quote coloring. Don't do this while in a comment.
 */
function tagQuotes(currentWord)
{
	if( inComment() ) 
		return currentWord;

	if( !inFlag[SINGLE_QUOTE_ID] && currentWord == syntaxModule.DOUBLE_QUOTE )
		return toggleMode(currentWord, DOUBLE_QUOTE_ID);

	if( !inFlag[DOUBLE_QUOTE_ID] && currentWord == syntaxModule.SINGLE_QUOTE )
		return toggleMode(currentWord, SINGLE_QUOTE_ID);
	
	return currentWord;
}

/**
 * So we don't trigger any funky tagging on escaped characters,
 * use this to advance the string tokenizer to the appropriate
 * spot. NOTE: This does not validate that it is a valid escape
 * character/word/whatever. It just blindly escapes the next
 * token after the escape character.
 */
function applyEscapeSequence(currentWord, strTok)
{
	return (!inComment() && currentWord == syntaxModule.ESCAPE_CHAR)
		? (currentWord + strTok.nextToken().getValue())
		: currentWord;
}

/**
 * Return true if the text passed in is one of our java keywords
 */
function isKeyword(word)
{
	for( var i = 0; i < syntaxModule.KEYWORDS.length; i++ )
		if( word == syntaxModule.KEYWORDS[i] )
			return true;

	return false;
}

/**
 * Grab all javascript elements of type "div" and return an array
 * of those divs with the appropriate class name (probably "code")
 */
function getElementsByStyleClass(className)
{
	var divs = document.getElementsByTagName('div');
	var elements = new Array();
	for( var e = 0; e < divs.length; e++ )
		if (divs[e].className == className)
			elements[elements.length] = divs[e];

	return elements;
}

/**
 * Given the text and css class, return the open tag form of the string
 */
function openTag(currentWord, type)
{
	return "<" + TAG + " class='" + cssClasses[type] + "'>" + currentWord;
}

/**
 * Given the text, simply close off any of the tags that we open
 */
function closeTag(currentWord)
{
	return currentWord + "</" + TAG + ">";
}

/**
 * Self explanatory helpers
 */
function max(a, b) { return (a > b) ? a : b; }
function startsWith(base, test) { return base.substring(0,test.length) == test; }
function inComment() { return (inFlag[LINE_COMMENT_ID] || inFlag[BLOCK_COMMENT_ID]); }
function inQuotes() { return (inFlag[SINGLE_QUOTE_ID] || inFlag[DOUBLE_QUOTE_ID]); }








///////////////////////////////////////////////////////////////////////////
// The String Tokenizer Class
///////////////////////////////////////////////////////////////////////////



/**
 * This is analogous to the StringTokenizer found in java
 * Simply pass it a source string to tokenize and it will
 * split it into tokens. You have no choice, delimiters are
 * returned regardless
 */
function StringTokenizer(source, delimiters)
{
	// fields
	this.m_source = source;
	this.m_length = source.length;
	this.m_seekPos = 0;
	this.m_delims = new String(delimiters);

	// methods
	this.nextToken = nextToken;
	this.hasMoreTokens = hasMoreTokens;
	this.isDelim = isDelim;

	function hasMoreTokens()
	{
		return (this.m_seekPos < this.m_length);
	}

	function nextToken()
	{
		buffer = "";
		var flag = false;
		var currentChar = this.m_source.charAt(this.m_seekPos);

		if( this.isDelim(currentChar) )
		{
			flag = true;
			buffer += currentChar;
			this.m_seekPos++;
		}
		else
		{
			flag = false;

			while( !this.isDelim(currentChar) )
			{
				buffer += currentChar;
				currentChar = this.m_source.charAt(++this.m_seekPos);
			}
		}

		return new Token(buffer, flag);
	}
	
	function isDelim(ch)
	{
		return (this.m_delims.indexOf(ch) != -1);
	}
}

/**
 * An instance of the Token class is what is returned
 * during a call to StringTokenizer.nextToken()
 */
function Token(value, isDelim)
{
	function getIsDelim() { return this.m_isDelim; }
	function getValue() { return this.m_value; }

	this.m_isDelim = isDelim;
	this.m_value = value;

	this.getIsDelim = getIsDelim;
	this.getValue = getValue;
}
