<?php
/*
	DevSaver Web Framework
	Copyright (c) 2013-2016 DevSaver. 
	All rights reserved.
		web:  www.devsaver.com
		mail: support@devsaver.com				
*/

/**
* description
*
* @library	
* @author	
* @since	
*/
class Shortcodes {
	var $shortcodes;
	var $content;

	/**
	* description
	*
	* @param
	*
	* @return
	*
	* @access
	*/
	function setContent($content) {
		$this->content = $content;
	}

	/**
	* description
	*
	* @param
	*
	* @return
	*
	* @access
	*/
	function getContent() {
		return $this->content;
	}
	
	

	/**
	* description
	*
	* @param
	*
	* @return
	*
	* @access
	*/
	function Add($code , $type = "func", $extra = null , $nested = false) {
		$this->shortcodes[$code] = array(
			"code"		=> $code,
			"type"		=> $type , 
			"extra"		=> $extra,
			"nested"	=> $nested
		);
	}

	private function hasShortcodes($content) {

		if (count($this->shortcodes) == 0) {
			return false;
		}
		
		if (strpos($content , "[") === false) {
			return false;
		} else {
			return true;
		}		
	}
	

	/**
	* description
	*
	* @param
	*
	* @return
	*
	* @access
	*/
	function Parse($process = "process") {
		
			if (!$this->hasShortcodes($this->content)) {
				return false;
			}
			

			// Find all registered tag names in $content.
			if ( !is_array( $tags = $this->extractTags()) ) {
				return true;
			}

			switch ($process) {
				case "strip":
					$func_name = "stripShortcode";
				break;

				default:
					$func_name = "parseShortcode";
				break;
			}
						

			//add strip certain codes in future eg: [CDATA[

			$pattern = $this->generateRegex( $tags );

			$this->content = preg_replace_callback( 
				"/$pattern/", 
				array(
					$this,
					$func_name
				), 
				$this->content 
			);


			//process nested shortcodes

			foreach ($this->shortcodes as $key => $val) {
				if ($val["nested"]) {
					$nested[$key] = $val;
				}				
			}

			//run the nested shortcodes parser until i dont find any shortcodees to replace
			if (is_array($nested)) {

				while ( is_array($tags = $this->extractTags()) && count($tags)) {

					$pattern = $this->generateRegex( $tags);

					$this->content = preg_replace_callback( 
						"/$pattern/", 
						array(
							$this,
							$func_name
						), 
						$this->content 
					);
				}

			}
						

			//revert back the encoded stripped codes eg: [CDATA[
	}

	function stripAll( ) {
		$this->Parse("strip");
	}

	/**
	* description
	*
	* @param
	*
	* @return
	*
	* @access
	*/
	private function parseShortcode($params) {

		// [[ xx ]] shortcode escaping
		if ( $params[1] == '[' && $params[6] == ']' ) {
			return substr($params[0], 1, -1);
		}


		$code = $params[2];
		$attr = $this->parseAttributes( $params[3] );


		switch ($this->shortcodes[$code]["type"]) {
			case "html":
				return $this->processSimpleShortcode($params , $code , $attr);
			break;

			case "func":
				return $this->processFunctionShortcode($params , $code , $attr);
			break;	
		}			

	}

	private function stripShortcode($params) {

		// [[ xx ]] shortcode escaping
		if ( $params[1] == '[' && $params[6] == ']' ) {
			return substr($params[0], 1, -1);
		}

		return
			$params[1] . 
			$params[5] .	//if text enclosed in shortcodes then remove the shortcode but keep the text need to think if to keep it or not
			$params[6];

	}

	
	private function processFunctionShortcode($params , $code , $attr) {

		$vars = array();

		$vars["content"] = $params[5];
	

		if ($attr[0]) {
			$vars["attr"] = substr($attr[0] , 1 );
		} elseif ( is_array($attr)) {				
			$vars = array_merge($vars , $attr);
		}

		return $m[1] . call_user_func( $this->shortcodes[$code]["extra"], array("vars" => $vars , "shortcode" => $code) ) . $m[6];

	}
	
	/**
	* description
	*
	* @param
	*
	* @return
	*
	* @access
	*/
	private function processSimpleShortcode($params , $code , $attr) {
				
		$content = $this->shortcodes[$code]["extra"];

		$vars["content"] = $params[5];

		if (is_array($attr) && count($attr)) {

			$_attr = array();

			foreach ($attr as $key => $val) {
				if (is_numeric($key)) {
					$_attr[] = str_replace(array( "=" , "\"") , array( "" , "") , $val);
				} else {
					$vars[$key] = str_replace(array( "=" , "\"") , array( "" , "") , $val);
				}
				
			}
			$vars["attr"] = implode(" " , $_attr);
/*
			if ($attr[0]) {
				$vars["attr"] = substr($attr[0] , 1 );
		
				//strip quotes
			} elseif (is_array($attr)) {				
				$vars = array_merge($vars , $attr);
			}
			*/

			//debug($vars);
		}	
		

		if (is_array($content)) {
			if (empty($attr) || $attr[0]) {
				$content = $content["single"];
			} else {
				$content = $content["multi"];
			}
		}

		$this->cleanupVars($vars);

		return $params[1] . CTemplateStatic::EmptyVars($content , $vars) .$params[6];
	}
	

	private function cleanupVars(&$vars) {

		if (is_array($vars) && count($vars)) {
			foreach ($vars as $key => &$val) {
				if ($key != "content") {

					$val = html_entity_decode($val);

					$val = str_replace('"' , '' , $val);
				}				
			}			
		}
	}


	private function generateRegex($tagnames = null ) {
		global $shortcode_tags;

		if ( empty( $tagnames ) ) {
			$tagnames = array_keys( $this->shortcodes);
		}
		$tagregexp = join( '|', array_map('preg_quote', $tagnames) );

		//using the following regex code from wordpress engine.
		return
			  '\\['                              // Opening bracket
			. '(\\[?)'                           // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
			. "($tagregexp)"                     // 2: Shortcode name
			. '(?![\\w-])'                       // Not followed by word character or hyphen
			. '('                                // 3: Unroll the loop: Inside the opening shortcode tag
			.     '[^\\]\\/]*'                   // Not a closing bracket or forward slash
			.     '(?:'
			.         '\\/(?!\\])'               // A forward slash not followed by a closing bracket
			.         '[^\\]\\/]*'               // Not a closing bracket or forward slash
			.     ')*?'
			. ')'
			. '(?:'
			.     '(\\/)'                        // 4: Self closing tag ...
			.     '\\]'                          // ... and closing bracket
			. '|'
			.     '\\]'                          // Closing bracket
			.     '(?:'
			.         '('                        // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags
			.             '[^\\[]*+'             // Not an opening bracket
			.             '(?:'
			.                 '\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag
			.                 '[^\\[]*+'         // Not an opening bracket
			.             ')*+'
			.         ')'
			.         '\\[\\/\\2\\]'             // Closing shortcode tag
			.     ')?'
			. ')'
			. '(\\]?)';                          // 6: Optional second closing brocket for escaping shortcodes: [[tag]]

	}



	private function parseAttributes($text) {
		$atts = array();
		//atrribute parser from wordpress engine
		$pattern = '/([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/';
		$text = preg_replace("/[\x{00a0}\x{200b}]+/u", " ", $text);
		if ( preg_match_all($pattern, $text, $match, PREG_SET_ORDER) ) {
			foreach ($match as $m) {
				if (!empty($m[1]))
					$atts[strtolower($m[1])] = stripcslashes($m[2]);
				elseif (!empty($m[3]))
					$atts[strtolower($m[3])] = stripcslashes($m[4]);
				elseif (!empty($m[5]))
					$atts[strtolower($m[5])] = stripcslashes($m[6]);
				elseif (isset($m[7]) && strlen($m[7]))
					$atts[] = stripcslashes($m[7]);
				elseif (isset($m[8]))
					$atts[] = stripcslashes($m[8]);
			}

			// Reject any unclosed HTML elements
			foreach( $atts as &$value ) {
				if ( false !== strpos( $value, '<' ) ) {
					if ( 1 !== preg_match( '/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $value ) ) {
						$value = '';
					}
				}
			}
		} else {
			$atts = ltrim($text);
		}
		return $atts;
	}

	private function extractTags() {
		preg_match_all( '@\[([^<>&/\[\]\x00-\x20=]++)@', $this->content, $matches );
		$tags = array_intersect( array_keys( $this->shortcodes ), $matches[1] );

		return $tags;


	}
}

?>
