text: var ElementType = require("./ElementType.js");
  
  function Parser(cbs, options){
  	this._options = options || defaultOpts;
  	this._cbs = cbs || defaultCbs;
  	this._buffer = "";
  	this._tagSep = ">";
  	this._stack = [];
  	this._wroteSpecial = false;
  	this._contentFlags = 0;
  	this._done = false;
  	this._running = true; //false if paused
  }
  
  //Regular expressions used for cleaning up and parsing (stateless)
  
  /* http://dev.w3.org/html5/html-author/#attributes
   * - Whitespace is permitted after the tag name, but it is not permitted before the tag name.
   * - Attribute names must consist of one or more characters other than the space characters,
   *   control characters, NULL, one of the characters: double quote ("), single quote ('),
   *   greater-than sign (>), solidus (/), equals sign (=), nor any characters that are not defined by Unicode.
   * - An empty attribute is one where the value has been omitted. (<input disabled>...</input>
   * - An unquoted attribute value must not contain any literal space characters, any of the characters:
   *   double quote ("), apostrophe ('), equals sign (=), less-than sign (<), greater-than sign (>),
   *   or grave accent (`), and the value must not be the empty string.
   * - There may be space characters between the attribute name and the equals sign (=),
   *   and between that and the attribute value.
   * - Double-quoted attributes must not contain any double-quote characters or ambiguous ampersands.
   * - Single-quoted attributes must not contain any single-quote characters or ambiguous ampersands.
   */
  // element name:	(<[^<& ]+)
  // attribute name:	( [^"'=>\/]+)
  // attribute value:	(\s*=\s*(?:
  //						"([^"]*)"|
  //						'([^']*)'|
  //						[^\s"'=<>`]+)
  // tag end: (?=\s|\/|$)
  
  var _reAttrib = /\s+([^"'=>\/\s]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))|(?=\s)|\/|$)/g,
      _reTail = /\s|\/|$/;
  
  var defaultOpts = {
  	xmlMode: false, //Special behavior for script/style tags by default
  	lowerCaseAttributeNames: false, //call .toLowerCase for each attribute name
  	lowerCaseTags: false //call .toLowerCase for each tag name
  };
  
  var defaultCbs = {
  	/*
  		This is just a plain object
  		so that the parser doesn't
  		throw if no arguments were
  		provided.
  	*/
  	/*
  		oncdataend,
  		oncdatastart,
  		onclosetag,
  		oncomment,
  		oncommentend,
  		onerror,
  		onopentag,
  		onopentagend,
  		onprocessinginstruction,
  		onreset,
  		ontext
  	*/
  };
  
  var formTags = {
  	input: true,
  	option: true,
  	optgroup: true,
  	select: true,
  	button: true,
  	datalist: true,
  	textarea: true
  };
  var openImpliesClose = {
  	tr      : { tr:true, th:true, td:true },
  	th      : { th:true },
  	td      : { thead:true, td:true },
  	body    : { head:true, link:true, script:true },
  	li      : { li:true },
  	p       : { p:true },
  	select  : formTags,
  	input   : formTags,
  	output  : formTags,
  	button  : formTags,
  	datalist: formTags,
  	textarea: formTags,
  	option  : { option:true },
  	optgroup: { optgroup:true }
  };
  
  //Parses a complete HTML and pushes it to the handler
  Parser.prototype.parseComplete = function(data){
  	this.reset();
  	this.end(data);
  };
  
  //Parses a piece of an HTML document
  Parser.prototype.parseChunk =
  Parser.prototype.write = function(data){
  	if(this._done) this._handleError("Attempted to parse chunk after parsing already done");
  	this._buffer += data; //FIXME: this can be a bottleneck
  	if(this._running) this._parseTags();
  };
  
  //Tells the parser that the HTML being parsed is complete
  Parser.prototype.done =
  Parser.prototype.end = function(chunk){
  	if(this._done) return;
  
  	if(chunk) this.write(chunk);
  	this._done = true;
  	
  	if(this._running) this._finishParsing();
  };
  
  Parser.prototype._finishParsing = function(){
  	//Parse the buffer to its end
  	if(this._buffer) this._parseTags(true);
  	
  	if(this._cbs.onclosetag){
  		while(this._stack.length) this._cbs.onclosetag(this._stack.pop());
  	}
  	
  	if(this._cbs.onend) this._cbs.onend();
  };
  
  Parser.prototype.pause = function(){
  	if(!this._done) this._running = false;
  };
  
  Parser.prototype.resume = function(){
  	if(this._running) return;
  	this._running = true;
  	this._parseTags();
  	if(this._done) this._finishParsing();
  };
  
  //Resets the parser to a blank state, ready to parse a new HTML document
  Parser.prototype.reset = function(){
  	Parser.call(this, this._cbs, this._options);
  	if(this._cbs.onreset) this._cbs.onreset();
  };
  
  //Extracts the base tag name from the data value of an element
  Parser.prototype._parseTagName = function(data){
  	var match = data.substr(0, data.search(_reTail));
  	if(!this._options.lowerCaseTags) return match;
  	return match.toLowerCase();
  };
  
  //Special tags that are treated differently
  var SpecialTags = {};
  //SpecialTags[ElementType.Tag]   = 0x0;
  SpecialTags[ElementType.Style]   = 0x1; //2^0
  SpecialTags[ElementType.Script]  = 0x2; //2^1
  SpecialTags[ElementType.Comment] = 0x4; //2^2
  SpecialTags[ElementType.CDATA]   = 0x8; //2^3
  
  var TagValues = {
  	style: 1,
  	script: 2
  };
  
  //Parses through HTML text and returns an array of found elements
  Parser.prototype._parseTags = function(force){
  	var current = 0,
  	    opening = this._buffer.indexOf("<"),
  	    closing = this._buffer.indexOf(">"),
  	    next, rawData, elementData, lastTagSep;
  
  	//if force is true, parse everything
  	if(force) opening = Infinity;
  
  	//opening !== closing is just false if both are -1
  	while(opening !== closing && this._running){
  		lastTagSep = this._tagSep;
  		
  		if((opening !== -1 && opening < closing) || closing === -1){
  			next = opening;
  			this._tagSep = "<";
  			opening = this._buffer.indexOf("<", next + 1);
  		}
  		else{
  			next = closing;
  			this._tagSep = ">";
  			closing = this._buffer.indexOf(">", next + 1);
  		}
  		rawData = this._buffer.substring(current, next); //The next chunk of data to parse
  		
  		//set elements for next run
  		current = next + 1;
  		
  		if(this._contentFlags >= SpecialTags[ElementType.CDATA]){
  			// We're inside a CDATA section
  			this._writeCDATA(rawData);
  
  		}
  		else if(this._contentFlags >= SpecialTags[ElementType.Comment]){
  			//We're in a comment tag
  			this._writeComment(rawData);
  		}
  		else if(lastTagSep === "<"){
  			elementData = rawData.trimLeft();
  			if(elementData.charAt(0) === "/"){
  				//elementData = elementData.substr(1).trim();
  				elementData = this._parseTagName(elementData.substr(1));
  				if(this._contentFlags !== 0){
  					//if it's a closing tag, remove the flag
  					if(this._contentFlags & TagValues[elementData]){
  						//remove the flag
  						this._contentFlags ^= TagValues[elementData];
  					} else {
  						this._writeSpecial(rawData, lastTagSep);
  						continue;
  					}
  				}
  				this._processCloseTag(elementData);
  			}
  			else if(this._contentFlags !== 0) this._writeSpecial(rawData, lastTagSep);
  			else if(elementData.charAt(0) === "!"){
  				if(elementData.substr(1, 7) === "[CDATA["){
  					this._contentFlags |= SpecialTags[ElementType.CDATA];
  					if(this._cbs.oncdatastart) this._cbs.oncdatastart();
  					this._writeCDATA(elementData.substr(8));
  				}
  				else if(this._contentFlags !== 0) this._writeSpecial(rawData, lastTagSep);
  				else if(elementData.substr(1, 2) === "--"){
  					//This tag is a comment
  					this._contentFlags |= SpecialTags[ElementType.Comment];
  					this._writeComment(rawData.substr(3));
  				}
  				//TODO: This isn't a processing instruction, needs a new name
  				else if(this._cbs.onprocessinginstruction){
  					this._cbs.onprocessinginstruction(
  						"!" + this._parseTagName(elementData.substr(1)),
  						elementData
  					);
  				}
  			}
  			else if(elementData.charAt(0) === "?"){
  				if(this._cbs.onprocessinginstruction){
  					this._cbs.onprocessinginstruction(
  						"?" + this._parseTagName(elementData.substr(1)),
  						elementData
  					);
  				}
  			}
  			else this._processOpenTag(elementData);
  		}
  		else{
  			if(this._contentFlags !== 0){
  				this._writeSpecial(rawData, ">");
  			}
  			else if(this._cbs.ontext){
  				if(this._tagSep === ">") rawData += ">"; //it's the second > in a row
  				if(rawData !== "") this._cbs.ontext(rawData);
  			}
  		}
  	}
  
  	this._buffer = this._buffer.substr(current);
  };
  
  Parser.prototype._writeCDATA = function(data){
  	if(this._tagSep === ">" && data.substr(-2) === "]]"){
  		// CDATA ends
  		if(data.length !== 2 && this._cbs.ontext){
  			this._cbs.ontext(data.slice(0,-2));
  		}
  		this._contentFlags ^= SpecialTags[ElementType.CDATA];
  		if(this._cbs.oncdataend) this._cbs.oncdataend();
  		this._wroteSpecial = false;
      }
      else if(this._cbs.ontext) this._cbs.ontext(data + this._tagSep);
  };
  
  Parser.prototype._writeComment = function(rawData){
  	if(this._tagSep === ">" && rawData.substr(-2) === "--"){ //comment ends
  		//remove the written flag (also removes the comment flag)
  		this._contentFlags ^= SpecialTags[ElementType.Comment];
  		this._wroteSpecial = false;
  		if(this._cbs.oncomment) this._cbs.oncomment(rawData.slice(0, -2));
  		if(this._cbs.oncommentend) this._cbs.oncommentend();
  	}
  	else if(this._cbs.oncomment) this._cbs.oncomment(rawData + this._tagSep);
  };
  
  Parser.prototype._writeSpecial = function(rawData, lastTagSep){
  	//if the previous element is text, append the last tag sep to element
  	if(this._wroteSpecial){
  		if(this._cbs.ontext) this._cbs.ontext(lastTagSep + rawData);
  	}
  	else{ //The previous element was not text
  		this._wroteSpecial = true;
  		if(rawData !== "" && this._cbs.ontext) this._cbs.ontext(rawData);
  	}
  };
  
  var emptyTags = {
  	__proto__: null,
  	area: true,
  	base: true,
  	basefont: true,
  	br: true,
  	col: true,
  	frame: true,
  	hr: true,
  	img: true,
  	input: true,
  	isindex: true,
  	link: true,
  	meta: true,
  	param: true,
  	embed: true
  };
  
  Parser.prototype._processCloseTag = function(name){
  	if(this._stack && (!(name in emptyTags) || this._options.xmlMode)){
  		var pos = this._stack.lastIndexOf(name);
  		if(pos !== -1)
  			if(this._cbs.onclosetag){
  				pos = this._stack.length - pos;
  				while(pos--) this._cbs.onclosetag(this._stack.pop());
  			}
  			else this._stack.splice(pos);
  	}
  	//many browsers (eg. Safari, Chrome) convert </br> to <br>
  	else if(name === "br" && !this._options.xmlMode){
  		this._processOpenTag(name + "/");
  	}
  };
  
  Parser.prototype._parseAttributes = function(data, lcNames){
  	for(var match; match = _reAttrib.exec(data);){
  		this._cbs.onattribute(lcNames ? match[1].toLowerCase() : match[1], match[2] || match[3] || match[4] || "");
  	}
  };
  
  //parses the attribute string
  var parseAttributes = function(data, lcNames){
  	var attrs = {};
  	for(var match; match = _reAttrib.exec(data);){
  		attrs[lcNames ? match[1].toLowerCase() : match[1]] = match[2] || match[3] || match[4] || "";
  	}
  	return attrs;
  };
  
  Parser.prototype._processOpenTag = function(data){
  	var name = this._parseTagName(data),
  		attributes = parseAttributes(data, this._options.lowerCaseAttributeNames),
  		type = ElementType.Tag;
  	
  	if(this._options.xmlMode){ /*do nothing*/ }
  	else if(name === "script") type = ElementType.Script;
  	else if(name === "style")  type = ElementType.Style;
  	if (!this._options.xmlMode && name in openImpliesClose) {
  		var el;
  		while ((el = this._stack[this._stack.length-1]) in openImpliesClose[name]) {
  			this._processCloseTag(el);
  		}
  	}
  	if(this._cbs.onopentagname) this._cbs.onopentagname(name);
  	if(this._cbs.onopentag) this._cbs.onopentag(name, attributes);
  	if(this._cbs.onattribute){
  		this._parseAttributes(data, this._options.lowerCaseAttributeNames);
  	}
  	
  	//If tag self-terminates, add an explicit, separate closing tag
  	/* http://dev.w3.org/html5/html-author/#tags
  	 * In XHTML, self-closing tags are valid but attribute values must be quoted.
  	 * In HTML, self-closing tags must be either void elements or foreign elements.
  	 * Invalid HTML self-closing tag syntax is ignored (treated as an opening tag).
  	 * Foreign elements use XML rules
  	 */
  	if((!this._options.xmlMode && name in emptyTags) || (data.substr(-1) === "/" && data.replace(_reAttrib, "").substr(-1) === "/")){
  		if(this._cbs.onclosetag) this._cbs.onclosetag(name);
  	} else {
  		if(type !== ElementType.Tag){
  			this._contentFlags |= SpecialTags[type];
  			this._wroteSpecial = false;
  		}
  		this._stack.push(name);
  	}
  };
  
  Parser.prototype._handleError = function(error){
  	error = new Error(error);
  	if(this._cbs.onerror) this._cbs.onerror(error);
  	else throw error;
  };
  
  module.exports = Parser;
  