<?php
/*
 * FastSHL                              | Universal Syntax HighLighter |
 * ---------------------------------------------------------------------

   Copyright (C) 2002-2005  Juraj 'hvge' Durech

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
   
 * ---------------------------------------------------------------------
 * fshl.php
 *
 * main fshl parser core
 * ---------------------------------------------------------------------
 */

define ('FSHL_PARSER_VERSION', '0.4.13');

if(!defined('FSHL_PATH')) {
	define('FSHL_PATH', dirname(__FILE__).'/');		// thanx to martin*cohen & johno for this great hack:)
}
require_once(FSHL_PATH.'fshl-config.php');

class fshlParser
{
	// class variables
	var $text, $textlen, $textpos;
	var $options, $output;
	var $text_position,  $tab_indent, $tabs;
	var $line_counter;

	var $out, $outf;			// final output and output fragment
	
	var $_trans, $_flags, $_data, $_delim, $_class, $_keywords;
	var $_ret,$_quit;
	
	var $lexers, $lang;			// loaded lexers and current language
	var $stack;					// parser context stack
	
	var $timestamp, $last_time;	// timer


	// -----------------------------------------------------------------------
	// USER LEVEL functions
	//

	// class constructor
	function fshlParser($output_mode, $options = 0, $tab_indent_value = 4)
	{
		$this->options  = $options;
		// initialize output module
		$out  = $output_mode.'_output';
		require_once (FSHL_OUTMODULE.$out.'.php');
		$this->output = new $out;
		// initialize tab emulation and line counter
		if($options & P_TAB_INDENT) {
			if($tab_indent_value > 0) {
				// precalculate table for tab indent emulation
				$tab = $options & P_USE_NBSP ? ' ' : ' ';		// P_USE_NBSP flag is deprecated
				$t = $tab; $ti = 0;
				for($i = $tab_indent_value; $i; $i--) {
					$this->tabs[($i) % $tab_indent_value] = array($t, $ti++);
					$t .= $tab;
				}
				$this->tab_indent = $tab_indent_value;
			} else {
				$this->tabs = null;		// disable tab indent
				$this->options &= ~P_TAB_INDENT;
			}
		} else {
			$this->tabs = null;
		}
		$this->line_counter = $options & P_LINE_COUNTER;
		// init defaults
		$this->text_position = array(1,0,0);		// 1 = line, 0 = char in line
		$this->lexers = array();
		$this->lang = NULL;
		$this->text = NULL;
		$this->textpos = 0;
		$this->textlen = 0;
		$this->last_time = -1;
	}
	
	// highlight string
	//
	function & highlightString($language, $text, $offset = 0, $total_line_count = 0) {
		assert(is_string($text));
		$this->startTimer();
		// parser initialization
		$this->initText($text, $offset);
		$this->text_position = array(1,0,0);
		if($this->line_counter) {
			$this->text_position[2] = $total_line_count ? $total_line_count : $this->calcCounterPadding($text);
			$this->outf .= $this->output->template(str_pad('1', $this->text_position[2], ' ', STR_PAD_LEFT).': ', 'count');
		}			
		// start parser
		$this->resetStack();
		$this->setLanguage($language);
		$this->parseString($language, $this->lang->initial_state);
		$this->stopTimer();
		return $this->getOut();
	}
	
	// highlight next string
	//	
	function & highlightNextString($text, $offset = 0) {
		assert(is_string($text));
		$language = null;
		$state = null;
		if($this->popState($language, $state)) {
			// continue in parsing
			$this->startTimer();
			$this->initText($text, $offset);
			$this->setLanguage($language);
			$this->parseString($language, $state);
			$this->stopTimer();
			return $this->getOut();
		}
		return null;
	}
	
	function calcCounterPadding(&$text) {
		assert(is_string($text));
		$data = count_chars($text, 0);
		return isset($data[ord("\n")]) ? strlen($data[ord("\n")]+1) : 2;		
	}
	
	function getPosition()	{ return $this->textpos; }
	
	function getParseTime() { return $this->last_time; }
	
	function isError() { return false;	}	//for shlParser class compatibility

	function & getOut() {
		if(is_array($this->out)) {
			$this->out = implode('', $this->out);
		}
		return $this->out;
	}

	// ---------------------------------------------------------------------------------
	// LOW LEVEL functions
	//
	
	// set current language
	function setLanguage($language) {
		if(!isset($this->lexers[$language])) {
			// load new language
			$lang = $language."_lang";
			require_once (FSHL_CACHE."$lang.php");
			$this->lexers[$language] = new $lang;
			$this->loadStatisticsIntoLexer($language);
		}
		$this->lang		= &$this->lexers[$language];
		$this->_trans 	= &$this->lang->trans;
		$this->_flags 	= &$this->lang->flags;
		$this->_data 	= &$this->lang->data;
		//$this->_delim 	= &$this->lang->delim;		// deprecated in FSHL (will be used in debug mode?)
		$this->_class 	= &$this->lang->class;
		$this->_ret		= &$this->lang->ret;
		$this->_quit	= &$this->lang->quit;
		$this->_keywords= &$this->lang->keywords;
		// FSHL text pointers init
		$this->lang->pt		= &$this->text;
		$this->lang->pti	= &$this->textpos;
	}
	
	function initText(&$text, $offset) {
		$this->text = &$text;
		$this->textlen = strlen($text);
		$this->text .= 'MULHOLLANDDRIVE';	// bugfixed on 1st live designia session.
		$this->textpos = $offset;
		$this->outf = null;
		$this->out = array();
	}	
	
	// main parser function
	//
	function parseString($language, $state)
	{
		$new_language = $language;
		$delimiter = null;
		while(($word = $this->getword($state)) != null)
		{
			if(count($word) != 2) {
				// Sem sa dostaneme, ak nacitane slovo nebolo delimiter.
				$this->text_position[1] += strlen($word[2]);
				$this->template($word[2], $state);
				if($word[0] < 0) {
					break;
				}
			}
			$show_number = FALSE;
			$this->text_position[1] += strlen($word[1]);
			// TAB INDENT and LINE COUNTER
			if($word[1]=="\n") {
				$this->text_position[0]++;
				$this->text_position[1] = 0;
				if($this->line_counter) {
					$show_number = str_pad($this->text_position[0], $this->text_position[2], ' ', STR_PAD_LEFT).': ';
				}
			} else {
				if($this->tabs && $word[1]=="\t")
				{
					$i = $this->text_position[1] % $this->tab_indent;
					$this->text_position[1] += $this->tabs[$i][1];
					$word[1] = $this->tabs[$i][0];
				}
			}
			$newstate = $this->_trans[$state][$word[0]][XL_DSTATE];	//nacitaj novy stav
			if($newstate == $this->_ret) 
			{
				// rozhodujeme, ci sa slovo spracuje uz teraz, alebo az po navrate z rekurzie
				// =0 - aplikuje sa stary stav na toto slovo,
				// =1 - slovo sa vrati spet do streamu.
				if($this->_trans[$state][$word[0]][XL_DTYPE]) {
					$lng = strlen($word[1]);
					$this->textpos -= $lng;
					$this->text_position[1] -= $lng;
				} else  {
					$this->template($word[1], $state);
					if($show_number !== FALSE) {
						$this->outf .= $this->output->template($show_number, 'count');
					}
				}
				if($this->popState($new_language, $state)) {
					if($new_language != $language) {
						$this->setLanguage($new_language);
						$language = $new_language;
					}
				} else {
					assert(0);		// error in grammar - bad escape from recursion
					$state = $this->lang->initial_state;	// set safety state
				}
				continue;
			}
			// rozhodujeme, ci sa zmena stavu aplikuje na toto, alebo az dalsie slovo
			// = 0 - aplikuje sa na toto slovo, 
			// = 1 - aplikuje sa dalsie slovo.
			// =-1 - znak sa vrati do streamu
			if(($type = $this->_trans[$state][$word[0]][XL_DTYPE]) > 0) {			//if XL_DTYPE == 1
				$this->template($word[1], $state);
				if($show_number !== FALSE) {
					$this->outf .= $this->output->template($show_number, 'count');
				}
			} else {
				if($type < 0) {
					// $type == -1 -> char back to stream
					$lng = strlen($word[1]);
					$this->textpos -= $lng;
					$this->text_position[1] -= $lng;					
				} else {
					$this->template($word[1], $newstate);
					if($show_number !== FALSE) {
						$this->outf .= $this->output->template($show_number, 'count');
					}
				}
			}
			
			if($this->_flags[$newstate] & PF_NEWLANG)
			{
				if($newstate == $this->_quit) {
					if($this->popState($new_language, $state)) {
						if($new_language != $language) {
							$this->setLanguage($new_language);
							$language = $new_language;
						}
					} else {
						$state = $this->lang->initial_state;	// fixed _QUIT in non-embeded context
					}
					continue;
				}
				$new_language = $this->_data[$newstate];
				$this->pushState($language, $this->_trans[$newstate] ? $newstate: $state);
				$this->setLanguage($new_language);
				$language = $new_language;
				$state = $this->lang->initial_state;
				continue;
			}
			if($this->_flags[$newstate] & PF_RECURSION)	//ak je flag rekurzia, volaj stav rekurentne
			{
				if($state != $newstate) {		//zabranenie vnoreniu rekurzie do rekurzie
					$this->pushState($language, $state);
				}
			}
			$state = $newstate;		//inak len zmen stav
		} //END while()
		
		// PUSH CURRENT STATE
		$this->pushState($language, $state);
		$this->outf .= $this->output->template_end();
		$this->appendFragment();
	}
	
	function resetStack() {
		$this->stack = array();
	}
	
	function pushState($lang, $state) {
		array_unshift($this->stack, array($lang, $state));
	}
	
	function popState(&$lang, &$state) {
		$item = array_shift($this->stack);
		if($item) {
			$lang = $item[0];
			$state = $item[1];
			return true;
		}
		return false;
	}
	
	// text fragmentation reduces frequency of big reallocations inside PHP
	function appendFragment() {
		$this->out[] = $this->outf;
		$this->outf = null;
	}
	
	//
	// template with keywords
	//
	function template($word, $state) {
		if($this->_flags[$state] & PF_KEYWORD) {
			$word_lc = strtolower($word);
			if(isset($this->_keywords[1][$word_lc])) {
				$this->outf .= $this->output->keyword($word, $this->_keywords[0].$this->_keywords[1][$word_lc]);
			} else {
				$this->outf .= $this->output->template($word, $this->_class[$state]);
			}
		} else {
			$this->outf .= $this->output->template($word, $this->_class[$state]);
		}
		// TODO: optimize block sizes for different string lenghts...
		if(strlen($this->outf) > 8100) {
			$this->appendFragment();
		}
	}
	
	// get word from string
	//
	function getword($state) {
		$state = 'isd'.$state;
		$result = null;
		if($this->textpos < $this->textlen) {
			$del = $this->lang->$state();
			if($del != false) {
				$this->textpos += strlen($del[1]);
				return $del;										// <del>xxxx => array(ID,'<del>');
			} else {
				while(($this->textpos < $this->textlen) && !($del = $this->lang->$state())) {
					$result .= $this->text[$this->textpos++];
				}
				if($this->textpos < $this->textlen) {
					$this->textpos += strlen($del[1]);				// xxxx<del> => array(ID,'<del>','xxxx');
					$del[2] = $result;
					return $del;
				}
				return array(-1, 0, $result);						// -1 end of string - no delimiter
			}
		}
		return $result;
	}
	
	// SelfTimer functions
	
	function getMicroTime()	{
		list($usec, $sec) = explode(" ", microtime());
		return ((float)$usec + (float)$sec);
	}
	
	function startTimer() {
		$this->timestamp = $this->getMicroTime();
	}
	
	function stopTimer() {
		$this->last_time = $this->getMicroTime() - $this->timestamp;
	}
	
	// --------------------------------------------------------------------------
	// debug statistics
	//
	// WARNING: statistics are experimental feature, don't use lexers with this
	//          feature on live webs !!
	// --------------------------------------------------------------------------
	function getInternalStatistics($html_safe_view = true) {
	
		$out = null;
		foreach($this->lexers as $lang => $lexer) {
			$substat = $this->getInternalStatisticsFromLexer($lang);
			$out .= $substat;
			$this->saveStatisticsFromLexer($lang);
			
			$filename = FSHL_CACHE.'stats/'.$lang.'_lang.stat.txt';
			@$filehandle = fopen ($filename, 'w');
			if($filehandle) {
				fwrite($filehandle, $substat);
				fclose($filehandle);
			}

		}
		if($out && $html_safe_view) {
			$out .= "\n\n\n";
			$out = htmlentities($out);
		}
		return $out;
	}

	function getInternalStatisticsFromLexer($lang) {
	
		$out = null;
		if(isset($this->lexers[$lang]->statistic)) {
			$lexer = & $this->lexers[$lang];
			$out .= "\n\n----------------------------------------------------\n";
			$out .= "Statistics for language ".$lang."\n";
			$out .= "----------------------------------------------------\n\n";
			
			foreach($lexer->statistic as $state => $stat) {
				$state_name = $lexer->names[$state];
				$total = $stat[-1];
				
				$out .= "\n  STATE '$state_name',\t\tTOTAL HITS: $total\n\n";
				
				arsort($stat);
				
				foreach($stat as $trans_id => $trans_count) {
					if($trans_id < 0) continue;
					if(isset($lexer->delim[$state][$trans_id])) {
					
						$trans_string = '['.$lexer->delim[$state][$trans_id].'] -> ';
						$new_state = $lexer->trans[$state][$trans_id][XL_DSTATE];
						$trans_string .= $lexer->names[$new_state];
						
						$trans_string = str_replace("\n", "\\n", $trans_string);
						$trans_string = str_replace("\t", "\\t", $trans_string);
					} else {
						$trans_string = 'return false';
					}
					$percent = sprintf("%0.3f",($trans_count / $total) * 100);
					$out .= "            $trans_id: $percent%\t\t$trans_string\t\n";
				}
			}
		}
		return $out;
	}

	function loadStatisticsIntoLexer($lang) {
	
		if(isset($this->lexers[$lang]->statistic)) {
			$filename = FSHL_CACHE.'stats/'.$lang.'_lang.stat';
			@$filehandle = fopen ($filename, 'r');
			if($filehandle) {
				$data = fread($filehandle, filesize($filename));
				fclose($filehandle);
				$this->lexers[$lang]->statistic = unserialize($data);
				return true;
			}
		}
		return false;
	}
	
	function saveStatisticsFromLexer($lang) {
	
		if(isset($this->lexers[$lang]->statistic)) {
			$filename = FSHL_CACHE.'stats/'.$lang.'_lang.stat';
			@$filehandle = fopen ($filename, 'w');
			if($filehandle) {
				$data = serialize($this->lexers[$lang]->statistic);
				fwrite($filehandle, $data);
				fclose($filehandle);
				return true;
			}
		}
		return false;		
	}	
	
} // END class fshlParser
?>