< ?php

    /**
	 * Documentar esto
	 *
	 * autor: gerardooscarjt@gmail.com
	 * fecha: 13/04/2011
	 * Uso típico:
	 * 		$parse = TreeScript::getParse($codigo);
	 * 		print_r($parse->getTokens());
	*/
	
	class TreeScript {
		
		private $encoding;
		private $pos;
		private $len;
		private $code;
		private $tokens = array();
		private $token = null;
		private $last_key = null;
		private $equal = false;
		private $errors = array();
		private $line = 1;
		
		/**
		 * Documentar esto
		*/
		private function __construct($code, $encoding='UTF-8') {
			Profiling::start("TreeScript __construct");
			$this->encoding = $encoding;
			$this->pos = 0;
			$this->code = $code;
			$this->len = mb_strlen($code, $this->encoding);
			$this->parse();
			Profiling::end();
		}
		
		/**
		 * Documentar esto
		*/
		public function getParse($code) {
			return new TreeScript($code);
		}
		
		/**
		 * Documentar esto
		*/
		
		private function parse() {
			$asignacion = '( *[:|=] *)';
		
			$sin_comillas = '[^ ]*';
			$comillas_simples = "'[^']*'";
			$comillas_dobles  = '"[^"]*"';
			$atributos = "(([^ |^=|^:]*)$asignacion($comillas_dobles|$comillas_simples|$sin_comillas))";  // FUNCIONA DE PUTA MADREEEERL
		
			$info1 = array();
			$pattern1 = '((([^\[]|\[(?!\[))*)(\[\[([^ |^\]\]]*)([^\]\]]*)\]\])*(([^\[]|\[(?!\[))*))';  // Obtengo los tokens de texto, los tags y cadenas de atributos

			preg_match_all($pattern1, $this->code, $info1, PREG_SET_ORDER );
		
			foreach($info1 as $i) {
				
				if (strlen($i[1])) {
				    $this->tokens[] = array(
				        'type'=>'text',
				        'data'=>$i[1]
				    );
				}
				
				if (strlen($i[4])) {
				    
				    $info2 = array();
				    preg_match_all($atributos, $i[5], $info2, PREG_SET_ORDER);
				    $data = array();
				    foreach ($info2 as $i2) {
				        $key = $i2[1];
				        $value = $i2[3];
				        if ($value[0]=='"') {
				            $value = trim($value, '"');
				        } else if ($value[0]=="'") {
				            $value = trim($value, "'");
				        }
				        
				        $data[$key] = $value;
				    }
				    
				    $this->tokens[] = array(
				        'type'=>'token',
				        'data'=>$data,
				        'name'=>$i[4]
				    );
				}
				
				if (strlen($i[6])) {
				    $this->tokens[] = array(
				        'type'=>'text',
				        'data'=>$i[6]
				    );
				}
			}
			return true;
		}
		
		private function parse_old() {
			$state = 0;
			$this->tokens = array();
			
			
			$this->token = array('type'=>'text', 'data'=>'');
			$key = null;
			
			while (null !== $c = $this->getc()) {
				if ($c == "\n") $this->line++;
				switch ($state) {
					case 0:
						if ($c == '[') {
							$state = 1;
						} else if ($c == '{') {
							$state = 9;
						} else {
							$this->token['data'] .= $c;
						}
						break;
					case 1:
						if ($c == '[') {
							$this->tokens[] = $this->token;
							$this->token = array('type'=>'token', 'name'=>'', 'data'=>array());
							$state = 2;
						} else {
							$this->token['data'] .= '['.$c;
							$state = 0;
						}
						break;
					case 2:
						if ($this->is_blank($c) || $c == ']') {
							$this->error('Se esperaba un identificador justo después de \'[[\'');
						} else {
							$this->token['name'] = $c;
							$state = 20;
						}
						break;
					case 20:
						if ($this->is_blank($c)) {
							$state = 5;
						} else if ($c == ']') {
							$state = 4;
						} else {
							$this->token['name'] .= $c;
						}
						break;
					case 4:
						if ($c == ']') {
							$this->tokens[] = $this->token;
							$this->token = array('type'=>'text', 'data'=>'');
							$state = 0;
						} else {
							$this->error('Se esperaba \']\'');
							$state = 0;
						}
						break;
					case 5:
						if ($this->is_blank($c)) {
							// No hago nada
						} else if ($c==']') {
							$state = 4;
						} else if ($c=="'") {
							$state = 7;
						} else if ($c=='"') {
							$state = 8;
						} else if ($c=='=' || $c==':') {
							if ($this->equal || $this->last_key == null) {
								$this->error('No se permiten dos asignaciones seguidas (\'=\' o \':\')');
							} else {
								$this->equal = true;
							}
						} else {
							$key = $c;
							$state = 6;
						}
						break;
					case 6:
						if ($this->is_blank($c)) {
							$this->addKey($key);
							$state = 5;
						} else if ($c == ']') {
							$this->addKey($key);
							$state = 4;
						} else if ($c == '=' || $c == ':') {
							$this->addKey($key);
							$state = 5;
							if ($this->equal || $this->last_key == null) {
								$this->error('No se permiten dos asignaciones seguidas (\'=\' o \':\')');
							} else {
								$this->equal = true;
							}
						}else {
							$key .= $c;
						}
						break;
					case 7:
						if ($c=="'") {
							$this->addKey($key);
							$state = 5;
						} else {
							$key .= $c;
						}
						break;
					case 8:
						if ($c=='"') {
							$this->addKey($key);
							$state = 5;
						} else {
							$key .= $c;
						}
						break;
					case 11:
						if ($c =='}') {
							$this->tokens[] = $this->token;
							$this->token = array('type'=>'text', 'data'=>'');
						} else {
							$this->error('Se esperaba una llave de cierre de comentario adicional \'}\'');
						}
						$state = 0;
						break;
					case 10:
						if ($c == '}') {
							$state = 11;
						} else {
							$this->token['data'] .= $c;
						}
						break;
					case 9:
						if ($c == '{') {
							$this->tokens[] = $this->token;
							$this->token = array('type'=>'comment', 'data'=>'');
							$state = 10;
						} else {
							$this->token['data'] .= '{'.$c;
							$state = 0;
						}
						break;
					
				}
				
			}
			
			if ($this->token['type'] == 'text') {
				$this->tokens[] = $this->token;
			} else {
				//ERROR, has dejado un token abierto !!!
				$this->error('Hay una etiqueta abierta, falta de cerrar con \']]\'');
				return false;
			}
			return true;
		}
		
		/**
		 * Documentar esto
		*/
		public function &getErrors() {
			if (count($this->errors))
				return $this->errors;
			return false;
		}
		
		/**
		 * Documentar esto
		*/
		private function error($error) {
			$this->errors[] = 'ERROR (line '.$this->line.'): '.$error;
		}
		
		/**
		 * Documentar esto
		*/
		private function addKey(&$key) {
			if ($this->equal) {
				$this->token['data'][$this->last_key] = $key;
				$this->last_key = null;
			} else {
				$this->token['data'][$key] = null;
				$this->last_key = $key;
			}
			$this->equal = false;
			$key = '';
		}
		
		/**
		 * Documentar esto
		*/
		private function is_letterordigit($c) {
			return in_array($c, self::$letterordigit);
		}
		
		/**
		 * Documentar esto
		*/
		private function is_blank($c) {
			return $c == "\n" || $c == "\c" || $c == "\t" || $c == " ";
		}
		
		/**
		 * Documentar esto
		*/
		private function getc() {
			if ($this->pos < $this->len) {
				$c = mb_substr($this->code, $this->pos, 1, $this->encoding);
				//$c = $this->code[$this->pos];
				$this->pos++;
				return $c;
			} else {
				return null;
			}
		}
		
		/**
		 * Documentar esto
		*/
		public function &getTokens() {
			return $this->tokens;
		}
	}


?>