<?php
/*

Copyright 2008 Daniel Lubarov

>> LICENSE <<
This work is licensed under a Creative Commons Attribution 3.0 United States
License <http://creativecommons.org/licenses/by/3.0/us/>. If you use this
code within your own website or application, please provide a link back to
<http://indented.net/>.

>> USAGE EXAMPLE <<
$code_in = $_POST['code'];
$obj_code = new codeObject($code_in);
$obj_code->process();
$code_out = $obj_code->code;

*/

class codeObject {
	var $code;
	var $pad_paren_inside;
	var $pad_paren_after_construct;
	var $pad_paren_after_function;
	var $indentation_unit;
	
	function codeObject( $code_in          = "",
				$pad_paren_inside          = true,
				$pad_paren_after_keyword   = true,
				$pad_paren_after_function  = false,
				$indentation_unit          = "\t") {
		$this->code = $code_in;
		$this->pad_paren_inside = $pad_paren_inside;
		$this->pad_paren_after_keyword = $pad_paren_after_keyword;
		$this->pad_paren_after_function = $pad_paren_after_function;
		$this->indentation_unit = $indentation_unit;
		// Trim unneeded whitespace
		$this->code = trim($this->code);
		// Make new lines uniform so we can work with them easily
		$this->code = str_replace(array("\r\n", "\n", "\r"), "<<NEWLINE>>", $this->code);
		$this->code = str_replace("<<NEWLINE>>", "\n", $this->code);
	}
	
	function process() {
		$i = 0; // position of scanner within code string
		$level = 0; // indentation level
		$keywords = array('if', 'then', 'else', 'for', 'while', 'do', 'until', 'switch', 'and', 'or', 'xor', 'eor', '&&', '||');
		
		while ($i < strlen($this->code)) {
			$first_1 = substr($this->code, $i, 1); // first one character following scanner
			switch ($first_1) {
				case ' ': // whitespace
					$i ++;
					// Delete exess proceeding whitespace
					while (substr($this->code, $i, 1) == ' ') {
						$this->code = $this->str_delete(1, $this->code, $i);
					}
					continue 2;
					break;
				case "\n": // newline
					$i ++;
					// Indent unless this line is just a closing brace
					$first_solid_char = substr($this->code, $i);
					$first_solid_char = trim($first_solid_char);
					$first_solid_char = substr($first_solid_char, 0, 1);
					$this_level = $level;
					if ($first_solid_char == "}") {
						$this_level --;
					}
					// Indent new line
					$i += $this->indent($this_level, $i);
					// Erase preexisting indentation
					while (substr($this->code, $i, 1) == ' ' || substr($this->code, $i, 1) == "\t") {
						$this->code = $this->str_delete(1, $this->code, $i);
					}
					continue 2;
					break;
				case '\'': // single quote
				case '"': // double quote
					do {
						$i ++;
						$localchar = substr($this->code, $i, 1);
						if ($localchar == '\\') { // bcakslash
							// Escape proceeding character
							$i ++;
						}
						elseif ($localchar == $first_1) {
							// Active quote matched >> quote terminated
							$i ++;
							continue 3;
						}
					}
					while ($i < strlen($this->code));
					continue 2;
					break;
				case '(': // opening bracket
					// Left-hand padding
					$j = $i;
					do {
						$j --;
						$localchar = substr($this->code, $j, 1);
					}
					while (($localchar == ' ' || $localchar == "\n") && $j >= 0);
					do {
						$j --;
						$localchar = substr($this->code, $j, 1);
					}
					while ($localchar != ' ' && $localchar != "\n" && $j >= 0);
					$j ++;
					$prev_entity = substr($this->code, $j, $i - $j);
					$prev_entity = trim($prev_entity);
					$following_keyword = in_array($prev_entity, $keywords);
					if ($following_keyword) {
						$req_padding = $this->pad_paren_after_keyword;
					}
					else {
						$req_padding = $this->pad_paren_after_function;
					}
					$has_padding = (substr($this->code, $i - 1, 1) == ' ')? true : false;
					if ($req_padding && !$has_padding) {
						// Add padding
						$this->code = $this->str_insert(' ', $this->code, $i);
						$i ++;
					}
					elseif ($has_padding && !$req_padding) {
						// Get rid of padding
						$this->code = $this->str_delete(1, $this->code, $i - 1);
						$i --;
					}
					// Right-hand padding
					$req_padding = $this->pad_paren_inside;
					$has_padding = (substr($this->code, $i + 1, 1) == ' ')? true : false;
					if ($req_padding && !$has_padding) {
						// Add padding
						$this->code = $this->str_insert(' ', $this->code, $i + 1);
						$i ++;
					}
					elseif ($has_padding && !$req_padding) {
						// Get rid of padding
						$this->code = $this->str_delete(1, $this->code, $i + 1);
					}
					$level += 2;
					$i ++;
					continue 2;
					break;
				case ')': // closing bracket
					// Left-hand padding
					$req_padding = $this->pad_paren_inside;
					$has_padding = (substr($this->code, $i - 1, 1) == ' ')? true : false;
					if ($req_padding && !$has_padding) {
						// Add padding
						$this->code = $this->str_insert(' ', $this->code, $i);
						$i ++;
					}
					elseif ($has_padding && !$req_padding) {
						// Get rid of padding
						$this->code = $this->str_delete(1, $this->code, $i - 1);
						$i --;
					}
					$level -= 2;
					$i ++;
					continue 2;
					break;
				case '{': // opening brace
					$level ++;
					$i ++;
					continue 2;
					break;
				case '}': // closing brace
					$level --;
					$i ++;
					continue 2;
					break;
			}
			
			$first_2 = substr($this->code, $i, 2); // first two characters following scanner
			switch ($first_2) {
				case '/*': // multi-line comment
					// Skip to code after comment
					$end = strpos($this->code, '*/', $i);
					if ($end === false) {
						// Everything remaining is commented out >> we're done
						break 2;
					}
					$j = $i; // hold start posision
					$i = $end + 2; // end position
					while (true) {
						$j = strpos($this->code, "\n", $j);
						$j ++;
						if ($j === false || $j > $i) break;
						$ind = $this->indent($level, $j);
						$i += $ind;
						$j += $ind;
					}
					continue 2;
					break;
				case '//': // single-line comment
					// Catch comment
					$end = strpos($this->code, "\n", $i); // end of line
					if ($end === false) {
						// Everything remaining is commented out >> we're done
						break 2;
					}
					$i += 2; // skip the two slashes
					// Adjust comment spacing
					$comment_old = substr($this->code, $i, $end-$i);
					$comment_new = ' ' . trim($comment_old);
					// Replace old comment with new one
					$this->code = $this->str_delete(strlen($comment_old), $this->code, $i);
					$this->code = $this->str_insert($comment_new, $this->code, $i);
					// Jump to end of line
					$end = strpos($this->code, "\n", $i); // recalculate
					$i = $end;
					continue 2;
					break;
			}
			
			// Default mode: skip to next entity
			$i ++;
			while (ctype_alnum(substr($this->code, $i, 1))) {
				$i ++;
			}
		}
	}
	
	private function str_insert($insertstring, $intostring, $position) {
		$part1 = substr($intostring, 0, $position);
		$part2 = substr($intostring, $position);
		$part1 = $part1 . $insertstring;
		$whole = $part1 . $part2;
		return $whole;
	}
	
	private function str_delete($numchars, $fromstring, $position) {
		$part1 = substr($fromstring, 0, $position);
		$part2 = substr($fromstring, $position + $numchars);
		$whole = $part1 . $part2;
		return $whole;
	}
	
	function indent($level, $pos) {
		if ($level <= 0) return "";
		$indent = str_repeat($this->indentation_unit, $level);
		$this->code = $this->str_insert($indent, $this->code, $pos);
		return strlen($indent);
	}
}
?>