<?php
/**
 * PHPTAL templating engine
 *
 * PHP Version 5
 *
 * @category HTML
 * @package  PHPTAL
 * @author   Laurent Bedubourg <lbedubourg@motion-twin.com>
 * @author   Kornel Lesiński <kornel@aardvarkmedia.co.uk>
 * @license  http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
 * @version  SVN: $Id: Transformer.php 605 2009-05-03 02:50:26Z kornel $
 * @link     http://phptal.org/
 */

/**
 * Tranform php: expressions into their php equivalent.
 *
 * This transformer produce php code for expressions like :
 *
 * - a.b["key"].c().someVar[10].foo()
 * - (a or b) and (c or d)
 * - not myBool
 * - ...
 *
 * The $prefix variable may be changed to change the context lookup.
 *
 * example:
 *
 *      $res = PHPTAL_Php_Transformer::transform('a.b.c[x]', '$ctx->');
 *      $res == '$ctx->a->b->c[$ctx->x]';
 *
 * @package PHPTAL
 * @subpackage Php
 * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
 */
class PHPTAL_Php_Transformer
{
    const ST_WHITE  = -1; // start of string or whitespace
    const ST_NONE   = 0;  // pass through (operators, parens, etc.)
    const ST_STR    = 1;  // 'foo'
    const ST_ESTR   = 2;  // "foo ${x} bar"
    const ST_VAR    = 3;  // abcd
    const ST_NUM    = 4;  // 123.02
    const ST_EVAL   = 5;  // $somevar
    const ST_MEMBER = 6;  // abcd.x
    const ST_STATIC = 7;  // class::[$]static|const
    const ST_DEFINE = 8;  // @MY_DEFINE

    /**
     * transform PHPTAL's php-like syntax into real PHP
     */
    public static function transform($str, $prefix='$')
    {
        $len = strlen($str);
        $state = self::ST_WHITE;
        $result = '';
        $i = 0;
        $inString = false;
        $backslashed = false;
        $instanceOf = false;
        $eval = false;


        for ($i = 0; $i <= $len; $i++) {
            if ($i == $len) $c = "\0";
            else $c = $str[$i];

            switch ($state) {

                // after whitespace a variable-variable may start, ${var} → $ctx->{$ctx->var}
                case self::ST_WHITE:
                    if ($c === '$' && $i < $len-2 && $str[$i+1] === '{')
                    {
                        $result .= $prefix;
                        $state = self::ST_NONE;
                        continue;
                    }
                    /* NO BREAK - ST_WHITE is almost the same as ST_NONE */

                // no specific state defined, just eat char and see what to do with it.
                case self::ST_NONE:
                    // begin of eval without {
                    if ($c === '$' && $i < $len && self::isAlpha($str[$i+1])) {
                        $state = self::ST_EVAL;
                        $mark = $i+1;
                        $result .= $prefix.'{';
                    }
                    // that an alphabetic char, then it should be the begining
                    // of a var
                    elseif (self::isAlpha($c) || $c==='_') {
                        $state = self::ST_VAR;
                        $mark = $i;
                    }
                    // begining of double quoted string
                    elseif ($c === '"') {
                        $state = self::ST_ESTR;
                        $mark = $i;
                        $inString = true;
                    }
                    // begining of single quoted string
                    elseif ($c === '\'') {
                        $state = self::ST_STR;
                        $mark = $i;
                        $inString = true;
                    }
                    // closing a method, an array access or an evaluation
                    elseif ($c === ')' || $c === ']' || $c === '}') {
                        $result .= $c;
                        // if next char is dot then an object member must
                        // follow
                        if ($i < $len-1 && $str[$i+1] === '.') {
                            $result .= '->';
                            $state = self::ST_MEMBER;
                            $mark = $i+2;
                            $i+=2;
                        }
                    }
                    // @ is an access to some defined variable
                    elseif ($c === '@') {
                        $state = self::ST_DEFINE;
                        $mark = $i+1;
                    }
                    elseif (ctype_space($c)) {
                        $state = self::ST_WHITE;
                        $result .= $c;
                    }
                    // character we don't mind about
                    else {
                        $result .= $c;
                    }
                    break;

                // $xxx
                case self::ST_EVAL:
                    if (!self::isVarNameChar($c)) {
                        $result .= $prefix . substr($str, $mark, $i-$mark);
                        $result .= '}';
                        $state = self::ST_NONE;
                    }
                    break;

                // single quoted string
                case self::ST_STR:
                    if ($c === '\\') {
                        $backslashed = true;
                    } elseif ($backslashed) {
                        $backslashed = false;
                    }
                    // end of string, back to none state
                    elseif ($c === '\'') {
                        $result .= substr($str, $mark, $i-$mark+1);
                        $inString = false;
                        $state = self::ST_NONE;
                    }
                    break;

                // double quoted string
                case self::ST_ESTR:
                    if ($c === '\\') {
                        $backslashed = true;
                    } elseif ($backslashed) {
                        $backslashed = false;
                    }
                    // end of string, back to none state
                    elseif ($c === '"') {
                        $result .= substr($str, $mark, $i-$mark+1);
                        $inString = false;
                        $state = self::ST_NONE;
                    }
                    // instring interpolation, search } and transform the
                    // interpollation to insert it into the string
                    elseif ($c === '$' && $i < $len && $str[$i+1] === '{') {
                        $result .= substr($str, $mark, $i-$mark) . '{';

                        $sub = 0;
                        for ($j = $i; $j<$len; $j++) {
                            if ($str[$j] === '{') {
                                $sub++;
                            } elseif ($str[$j] === '}' && (--$sub) == 0) {
                                $part = substr($str, $i+2, $j-$i-2);
                                $result .= self::transform($part, $prefix);
                                $i = $j;
                                $mark = $i;
                            }
                        }
                    }
                    break;

                // var state
                case self::ST_VAR:
                    if (self::isVarNameChar($c)) {
                    }
                    // end of var, begin of member (method or var)
                    elseif ($c === '.') {
                        $result .= $prefix . substr($str, $mark, $i-$mark);
                        $result .= '->';
                        $state = self::ST_MEMBER;
                        $mark = $i+1;
                    }
                    // static call, the var is a class name
                    elseif ($c === ':') {
                        $result .= substr($str, $mark, $i-$mark+1);
                        $mark = $i+1;
                        $i++;
                        $state = self::ST_STATIC;
                        break;
                    }
                    // function invocation, the var is a function name
                    elseif ($c === '(') {
                        $result .= substr($str, $mark, $i-$mark+1);
                        $state = self::ST_NONE;
                    }
                    // array index, the var is done
                    elseif ($c === '[') {
                        if ($str[$mark]==='_') { // superglobal?
                            $result .= '$' . substr($str, $mark, $i-$mark+1);
                        } else {
                            $result .= $prefix . substr($str, $mark, $i-$mark+1);
                        }
                        $state = self::ST_NONE;
                    }
                    // end of var with non-var-name character, handle keywords
                    // and populate the var name
                    else {
                        $var = substr($str, $mark, $i-$mark);
                        $low = strtolower($var);
                        // boolean and null
                        if ($low === 'true' || $low === 'false' || $low === 'null') {
                            $result .= $var;
                        }
                        // lt, gt, ge, eq, ...
                        elseif (array_key_exists($low, self::$TranslationTable)) {
                            $result .= self::$TranslationTable[$low];
                        }
                        // instanceof keyword
                        elseif ($low === 'instanceof') {
                            $result .= $var;
                            $instanceOf = true;
                        }
                        // previous was instanceof
                        elseif ($instanceOf) {
                            // last was instanceof, this var is a class name
                            $result .= $var;
                            $instanceOf = false;
                        }
                        // regular variable
                        else {
                            $result .= $prefix . $var;
                        }
                        $i--;
                        $state = self::ST_NONE;
                    }
                    break;

                // object member
                case self::ST_MEMBER:
                    if (self::isVarNameChar($c)) {
                    }
                    // eval mode ${foo}
                    elseif ($c === '$' && ($i >= $len-2 || $str[$i+1] !== '{')) {
                        $result .= '{' . $prefix;
                        $mark++;
                        $eval = true;
                    }
                    // x.${foo} x->{foo}
                    elseif ($c === '$') {
                        $mark++;
                    }
                    // end of var member var, begin of new member
                    elseif ($c === '.') {
                        $result .= substr($str, $mark, $i-$mark);
                        if ($eval) { $result .='}'; $eval = false; }
                        $result .= '->';
                        $mark = $i+1;
                        $state = self::ST_MEMBER;
                    }
                    // begin of static access
                    elseif ($c === ':') {
                        $result .= substr($str, $mark, $i-$mark+1);
                        if ($eval) { $result .='}'; $eval = false; }
                        $state = self::ST_STATIC;
                        break;
                    }
                    // the member is a method or an array
                    elseif ($c === '(' || $c === '[') {
                        $result .= substr($str, $mark, $i-$mark+1);
                        if ($eval) { $result .='}'; $eval = false; }
                        $state = self::ST_NONE;
                    }
                    // regular end of member, it is a var
                    else {
                        $result .= substr($str, $mark, $i-$mark);
                        if ($eval) { $result .='}'; $eval = false; }
                        $state = self::ST_NONE;
                        $i--;
                    }
                    break;

                // wait for separator
                case self::ST_DEFINE:
                    if (self::isVarNameChar($c)) {
                    } else {
                        $state = self::ST_NONE;
                        $result .= substr($str, $mark, $i-$mark);
                        $i--;
                    }
                    break;

                // static call, can be const, static var, static method
                // Klass::$static
                // Klass::const
                // Kclass::staticMethod()
                //
                case self::ST_STATIC:
                    if (self::isVarNameChar($c)) {
                    }
                    // static var
                    elseif ($c === '$') {
                    }
                    // end of static var which is an object and begin of member
                    elseif ($c === '.') {
                        $result .= substr($str, $mark, $i-$mark);
                        $result .= '->';
                        $mark = $i+1;
                        $state = self::ST_MEMBER;
                    }
                    // end of static var which is a class name
                    elseif ($c === ':') {
                        $result .= substr($str, $mark, $i-$mark+1);
                        $state = self::ST_STATIC;
                        break;
                    }
                    // static method or array
                    elseif ($c === '(' || $c === '[') {
                        $result .= substr($str, $mark, $i-$mark+1);
                        $state = self::ST_NONE;
                    }
                    // end of static var or const
                    else {
                        $result .= substr($str, $mark, $i-$mark);
                        $state = self::ST_NONE;
                        $i--;
                    }
                    break;

                // numeric value
                case self::ST_NUM:
                    if (!self::isDigitCompound($c)) {
                        $result .= substr($str, $mark, $i-$mark);
                        $state = self::ST_NONE;
                    }
                    break;
            }
        }

        $result = trim($result);

        // CodeWriter doesn't like expressions that look like blocks
        if ($result[strlen($result)-1] === '}') return '('.$result.')';

        return $result;
    }

    private static function isAlpha($c)
    {
        $c = strtolower($c);
        return $c >= 'a' && $c <= 'z';
    }

    private static function isDigitCompound($c)
    {
        return ($c >= '0' && $c <= '9' || $c === '.');
    }

    private static function isVarNameChar($c)
    {
        return self::isAlpha($c) || ($c >= '0' && $c <= '9') || $c === '_';
    }

    private static $TranslationTable = array(
        'not' => '!',
        'ne'  => '!=',
        'and' => '&&',
        'or'  => '||',
        'lt'  => '<',
        'gt'  => '>',
        'ge'  => '>=',
        'le'  => '<=',
        'eq'  => '==',
    );
}


