<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  ocProducts Ltd
 * @package    core_webstandards
 */

/*
Unsupported for good reasons...

(foo).bar;                                                                          [we can't take references from expressions]

instance: new _shim[comp]()                                                         [we don't allow dynamic classnames

do_cors: Test(window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()),  [we don't allow 'in' with complex expressions]

droppablesLoop: for ( i = 0; i < m.length; i++ ) { }                                [we don't allow commands as expressions]

{ of: true }                                                                        [we don't allow properties that are keywords, except for 'undefined' as jQuery uses that]
*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__webstandards_js_parse()
{
    // In precedence order. Note REFERENCE==BW_AND (it gets converted, for clarity). Ditto QUESTION==TERNARY_IF
    global $JS_OPS;
    $JS_OPS = array('QUESTION', 'TERNARY_IF', 'BOOLEAN_OR', 'BOOLEAN_AND', 'BW_OR', 'BW_XOR', 'OBJECT_OPERATOR', 'BW_AND', 'IS_EQUAL', 'IS_NOT_EQUAL', 'IS_IDENTICAL', 'IS_NOT_IDENTICAL', 'IS_SMALLER', 'IS_SMALLER_OR_EQUAL', 'IS_GREATER', 'IS_GREATER_OR_EQUAL', 'SL', 'SR', 'ZSR', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'REMAINDER', 'INSTANCEOF', 'IN');
}

/**
 * Return parse info for parse type.
 *
 * @return ?map Parse info (null: error)
 */
function webstandards_js_parse()
{
    global $JS_PARSE_POSITION;
    $JS_PARSE_POSITION = 0;

    $structure = _webstandards_js_parse_js();

    return $structure;
}

/**
 * Return parse info for parse type.
 *
 * @return ?map Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_js()
{
    // Choice{"FUNCTION" "IDENTIFIER "BRACKET_OPEN" comma_parameters "BRACKET_CLOSE" command | command}*

    $next = parser_peek();
    $program = array();
    $program['functions'] = array();
    $program['main'] = array();
    while (!is_null($next)) {
        switch ($next) {
            case 'FUNCTION':
                $_function = _webstandards_js_parse_function_dec();
                if (is_null($_function)) {
                    return null;
                }
                foreach ($program['functions'] as $_) {
                    if ($_['name'] == $_function['name']) {
                        js_log_warning('PARSER', 'Duplicated function \'' . $_function['name'] . '\'');
                    }
                }
                //log_special('defined', $_function['name']);
                $program['functions'][] = $_function;

                // Sometimes happens when people get confused by =function() and function blah() {};
                $next_2 = parser_peek();
                if ($next_2 == 'COMMAND_TERMINATE') {
                    parser_next();
                }
                break;

            default:
                $command = _webstandards_js_parse_command();
                if (is_null($command)) {
                    return null;
                }
                $program['main'] = array_merge($program['main'], $command);
                break;
        }

        $next = parser_peek();
    }
    return $program;
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_command()
{
    // Choice{"CURLY_OPEN" command* "CURLY_CLOSE" | command_actual "COMMAND_TERMINATE"*}

    if (parser_peek() == 'COMMAND_TERMINATE') {
        parser_next();
        return array(array('CONTINUE', array('SOLO', array('LITERAL', array('NUMBER', 1)), $GLOBALS['JS_PARSE_POSITION']), $GLOBALS['JS_PARSE_POSITION']));
    }

    $next = parser_peek();
    $command = array();
    switch ($next) {
        case 'CURLY_OPEN':
            parser_next();
            $next_2 = parser_peek();
            while (true) {
                switch ($next_2) {
                    case 'CURLY_CLOSE':
                        parser_next();
                        break 2;

                    default:
                        $command2 = _webstandards_js_parse_command();
                        if (is_null($command2)) {
                            return null;
                        }
                        $command = array_merge($command, $command2);
                        break;
                }
                $next_2 = parser_peek();
            }
            break;

        default:
            $new_command = _webstandards_js_parse_command_actual();
            if (is_null($new_command)) {
                return null;
            }

            // This is now a bit weird. Not all commands end with a COMMAND_TERMINATE, and those are actually for the commands to know their finished (and the ones requiring would have complained if they were missing). Therefore we now just skip any semicolons. There can be more than one, it's valid, albeit crazy.
            $command[] = $new_command;
            $next_2 = parser_peek();
            while ($next_2 == 'COMMAND_TERMINATE') {
                parser_next();
                $next_2 = parser_peek();
            }

            break;
    }
    return $command;
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_command_actual()
{
    // Choice{"VAR" comma_variables | "FUNCTION" | variable "DEC" | variable "INC" | variable assignment_operator expression | function "BRACKET_OPEN" comma_expressions "BRACKET_CLOSE" | "IF" expression command if_rest? | "FINALLY" identifier "CURLY_OPEN" command "CURLY_CLOSE" | "CATCH" identifier "CURLY_OPEN" command "CURLY_CLOSE" | "TRY" "CURLY_OPEN" command "CURLY_CLOSE" | "SWITCH" expression "CURLY_OPEN" cases "CURLY_CLOSE" | "FOR" "BRACKET_OPEN" identifier "OF" expression "BRACKET_CLOSE" command | "FOR" "BRACKET_OPEN" command expression command "BRACKET_CLOSE" command | "DO" command "WHILE" "BRACKET_OPEN" expression "BRACKET_CLOSE" | "WHILE" "BRACKET_OPEN" expression "BRACKET_CLOSE" command | "RETURN" | "CONTINUE" | "BREAK" | "RETURN" expression | "THROW" expression | "DELETE" identifier}

    $next = parser_peek(true);
    switch ($next[0]) {
        case 'VAR':
            parser_next();
            $t = _webstandards_js_parse_comma_parameters(true, 'COMMAND_TERMINATE');
            if (is_null($t)) {
                return null;
            }
            $command = array('VAR', $t, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'DELETE':
            parser_next();
            $variable = _webstandards_js_parse_variable();
            if (is_null($variable)) {
                return null;
            }
            $command = array('DELETE', $variable, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'FUNCTION':
            $command = array('INNER_FUNCTION', _webstandards_js_parse_function_dec(), $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'IF':
            parser_next();
            $c_pos = $GLOBALS['JS_PARSE_POSITION'];
            if (is_null(parser_expect('BRACKET_OPEN'))) {
                return null;
            }
            $expression = _webstandards_js_parse_expression();
            if (is_null($expression)) {
                return null;
            }
            if (is_null(parser_expect('BRACKET_CLOSE'))) {
                return null;
            }
            $command = _webstandards_js_parse_command();
            if (is_null($command)) {
                return null;
            }

            $next_2 = parser_peek();
            if ($next_2 == 'ELSE') {
                $if_rest = _webstandards_js_parse_if_rest();
                if (is_null($if_rest)) {
                    return null;
                }
                $command = array('IF_ELSE', $expression, $command, $if_rest, $c_pos);
            } else {
                $command = array('IF', $expression, $command, $c_pos);
            }
            break;

        case 'TRY':
            parser_next();
            $c_pos = $GLOBALS['JS_PARSE_POSITION'];
            $command = _webstandards_js_parse_command();
            if (is_null($command)) {
                return null;
            }
            $command = array('TRY', $command, $c_pos);
            break;

        case 'WITH':
            parser_next();
            $c_pos = $GLOBALS['JS_PARSE_POSITION'];
            if (is_null(parser_expect('BRACKET_OPEN'))) {
                return null;
            }
            $var = _webstandards_js_parse_variable();
            if (is_null($var)) {
                $exp = null;
            }
            if (is_null(parser_expect('BRACKET_CLOSE'))) {
                return null;
            }
            $command = _webstandards_js_parse_command();
            if (is_null($command)) {
                return null;
            }
            $command = array('WITH', $var, $command, $c_pos);
            break;

        case 'CATCH':
            parser_next();
            $c_pos = $GLOBALS['JS_PARSE_POSITION'];
            if (is_null(parser_expect('BRACKET_OPEN'))) {
                return null;
            }
            $target = parser_expect('IDENTIFIER');
            if (is_null($target)) {
                return null;
            }
            if (is_null(parser_expect('BRACKET_CLOSE'))) {
                return null;
            }
            $command = _webstandards_js_parse_command();
            if (is_null($command)) {
                return null;
            }
            $command = array('CATCH', $target, $command, $c_pos);
            break;

        case 'FINALLY':
            parser_next();
            $c_pos = $GLOBALS['JS_PARSE_POSITION'];
            $command = _webstandards_js_parse_command();
            if (is_null($command)) {
                return null;
            }
            $command = array('FINALLY', $command, $c_pos);
            break;

        case 'SWITCH':
            parser_next();
            $c_pos = $GLOBALS['JS_PARSE_POSITION'];
            $expression = _webstandards_js_parse_expression();
            if (is_null($expression)) {
                return null;
            }
            if (is_null(parser_expect('CURLY_OPEN'))) {
                return null;
            }
            $cases = _webstandards_js_parse_cases();
            if (is_null($cases)) {
                return null;
            }
            if (is_null(parser_expect('CURLY_CLOSE'))) {
                return null;
            }
            $command = array('SWITCH', $expression, $cases, $c_pos);
            break;

        case 'FOR':
            parser_next();
            $c_pos = $GLOBALS['JS_PARSE_POSITION'];
            if (is_null(parser_expect('BRACKET_OPEN'))) {
                return null;
            }
            $_test = parser_peek_dist(1);
            $_test2 = parser_peek_dist(2);
            if (($_test2 == 'OF') || ($_test2 == 'IN') || ($_test == 'OF') || ($_test == 'IN')) {
                $test = parser_peek();
                if ($test == 'VAR') {
                    parser_next();
                }

                $c_pos = $GLOBALS['JS_PARSE_POSITION'];
                $variable = _webstandards_js_parse_variable();
                if (is_null($variable)) {
                    return null;
                }
                if (is_null(parser_expect((($_test == 'OF') || ($_test == 'IN')) ? $_test : $_test2))) {
                    return null;
                }
                $expression = _webstandards_js_parse_expression();
                if (is_null($expression)) {
                    return null;
                }
                if (is_null(parser_expect('BRACKET_CLOSE'))) {
                    return null;
                }
                $loop_command = _webstandards_js_parse_command();
                if (is_null($loop_command)) {
                    return null;
                }
                $command = array('FOREACH_list', $expression, $variable, $loop_command, $c_pos);
            } else {
                $next_2 = parser_peek();
                if ($next_2 == 'COMMAND_TERMINATE') {
                    $init_commands = null;
                } else {
                    $init_commands = _webstandards_js_parse_comma_commands();
                    if (is_null($init_commands)) {
                        return null;
                    }
                }
                if (is_null(parser_expect('COMMAND_TERMINATE'))) {
                    return null;
                }
                $control_expression = _webstandards_js_parse_expression();
                if (is_null($control_expression)) {
                    return null;
                }
                if (is_null(parser_expect('COMMAND_TERMINATE'))) {
                    return null;
                }
                $next_2 = parser_peek();
                if ($next_2 == 'BRACKET_CLOSE') {
                    $control_commands = null;
                } else {
                    $control_commands = _webstandards_js_parse_comma_commands();
                    if (is_null($control_commands)) {
                        return null;
                    }
                }
                if (is_null(parser_expect('BRACKET_CLOSE'))) {
                    return null;
                }
                $next_2 = parser_peek();
                if ($next_2 == 'COMMAND_TERMINATE') {
                    $loop_command = null;
                } else {
                    $loop_command = _webstandards_js_parse_command();
                    if (is_null($loop_command)) {
                        return null;
                    }
                }
                $command = array('FOR', $init_commands, $control_expression, $control_commands, $loop_command, $c_pos);
            }
            break;

        case 'DO':
            parser_next();
            $c_pos = $GLOBALS['JS_PARSE_POSITION'];
            $loop_command = _webstandards_js_parse_command();
            if (is_null($loop_command)) {
                return null;
            }
            if (is_null(parser_expect('WHILE'))) {
                return null;
            }
            if (is_null(parser_expect('BRACKET_OPEN'))) {
                return null;
            }
            $control_expression = _webstandards_js_parse_expression();
            if (is_null($control_expression)) {
                return null;
            }
            if (is_null(parser_expect('BRACKET_CLOSE'))) {
                return null;
            }
            $command = array('DO', $control_expression, $loop_command, $c_pos);
            break;

        case 'WHILE':
            parser_next();
            $c_pos = $GLOBALS['JS_PARSE_POSITION'];
            if (is_null(parser_expect('BRACKET_OPEN'))) {
                return null;
            }
            $control_expression = _webstandards_js_parse_expression();
            if (is_null(parser_expect('BRACKET_CLOSE'))) {
                return null;
            }
            if (is_null($control_expression)) {
                return null;
            }
            $loop_command = _webstandards_js_parse_command();
            if (is_null($loop_command)) {
                return null;
            }
            $command = array('WHILE', $control_expression, $loop_command, $c_pos);
            break;

        case 'RETURN':
            parser_next();
            $next_2 = parser_peek();
            switch ($next_2) {
                case 'COMMAND_TERMINATE':
                    $command = array('RETURN', array('SOLO', array('LITERAL', array('Null')), $GLOBALS['JS_PARSE_POSITION']), $GLOBALS['JS_PARSE_POSITION']);
                    break;

                default:
                    $expression = _webstandards_js_parse_expression();
                    if (is_null($expression)) {
                        return null;
                    }
                    $command = array('RETURN', $expression, $GLOBALS['JS_PARSE_POSITION']);
            }
            break;

        case 'THROW':
            parser_next();
            $expression = _webstandards_js_parse_expression();
            if (is_null($expression)) {
                return null;
            }
            $command = array('THROW', $expression, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'CONTINUE':
            parser_next();
            $command = array('CONTINUE', array('SOLO', array('LITERAL', array('NUMBER', 1)), $GLOBALS['JS_PARSE_POSITION']), $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'BREAK':
            parser_next();
            $command = array('BREAK', array('SOLO', array('LITERAL', array('NUMBER', 1)), $GLOBALS['JS_PARSE_POSITION']), $GLOBALS['JS_PARSE_POSITION']);
            break;

        default:
            $command = _webstandards_js_parse_expression();
    }
    return $command;
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_if_rest()
{
    // Choice{else command | elseif expression command if_rest?}

    $next = parser_peek();
    switch ($next) {
        case 'ELSE':
            parser_next();
            $command = _webstandards_js_parse_command();
            if (is_null($command)) {
                return null;
            }
            $if_rest = $command;
            break;

        default:
            webstandards_js_parser_error('Expected <if_rest> but got ' . $next);
            return null;
    }
    return $if_rest;
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_cases()
{
    // Choice{"CASE" expression "COLON" command* | "DEFAULT" "COLON" command*}*

    $next = parser_peek();
    $cases = array();
    while (($next == 'CASE') || ($next == 'DEFAULT')) {
        switch ($next) {
            case 'CASE':
                parser_next();
                $expression = _webstandards_js_parse_expression();
                if (is_null($expression)) {
                    return null;
                }
                if (is_null(parser_expect('COLON'))) {
                    return null;
                }
                $next_2 = parser_peek();
                $commands = array();
                while (($next_2 != 'CURLY_CLOSE') && ($next_2 != 'CASE') && ($next_2 != 'DEFAULT')) {
                    $command = _webstandards_js_parse_command();
                    if (is_null($command)) {
                        return null;
                    }
                    $commands = array_merge($commands, $command);
                    $next_2 = parser_peek();
                }
                $cases[] = array($expression, $commands);
                break;

            case 'DEFAULT':
                parser_next();
                if (is_null(parser_expect('COLON'))) {
                    return null;
                }
                $next_2 = parser_peek();
                $commands = array();
                while (($next_2 != 'CURLY_CLOSE') && ($next_2 != 'CASE')) {
                    $command = _webstandards_js_parse_command();
                    if (is_null($command)) {
                        return null;
                    }
                    $commands += $command;
                    $next_2 = parser_peek();
                }
                $cases[] = array(null, $commands);
                break;

            default:
                webstandards_js_parser_error('Expected <cases> but got ' . $next);
                return null;
        }

        $next = parser_peek();
    }

    return $cases;
}

/**
 * Return parse info for parse type.
 *
 * @param  boolean $anonymous Whether this is an anonymous function
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_function_dec($anonymous = false)
{
    $function = array();
    $function['offset'] = $GLOBALS['JS_PARSE_POSITION'];
    if (is_null(parser_expect('FUNCTION'))) {
        return null;
    }
    if ((!$anonymous) || (parser_peek() == 'IDENTIFIER')/*Actually we do see a combination of declarations and inline declarations*/) {
        $function['name'] = parser_expect('IDENTIFIER');
        if (is_null($function['name'])) {
            return null;
        }
    }
    if (is_null(parser_expect('BRACKET_OPEN'))) {
        return null;
    }
    $function['parameters'] = _webstandards_js_parse_comma_parameters(false);
    if (is_null($function['parameters'])) {
        return null;
    }
    if (is_null(parser_expect('BRACKET_CLOSE'))) {
        return null;
    }
    $function['code'] = _webstandards_js_parse_command();
    if (is_null($function['code'])) {
        return null;
    }

    return $function;
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_expression()
{
    // Choice{expression_inner | expression_inner binary_operation expression_inner | expression_inner QUESTION expression_inner COLON expression_inner}

    global $JS_OPS;

    $expression = _webstandards_js_parse_expression_inner();
    if (is_null($expression)) {
        return null;
    }
    $op_list = array($expression);

    $next = parser_peek();
    while (in_array($next, $JS_OPS)) {
        parser_next();
        if ($next == 'QUESTION') {
            $expression_2 = _webstandards_js_parse_expression();
            if (is_null($expression_2)) {
                return null;
            }
            if (is_null(parser_expect('COLON'))) {
                return null;
            }
            $expression_3 = _webstandards_js_parse_expression();
            if (is_null($expression_3)) {
                return null;
            }
            $op_list[] = 'TERNARY_IF';
            $op_list[] = array($expression_2, $expression_3);
        } else {
            $expression_2 = _webstandards_js_parse_expression_inner();
            if (is_null($expression_2)) {
                return null;
            }
            $op_list[] = $next;
            $op_list[] = $expression_2;
        }
        $next = parser_peek();
    }

    $op_tree = precedence_sort($op_list);
    return $op_tree;
}

/**
 * Sort an unordered structure of operations into a precedence tree.
 *
 * @param  array $op_list Ops in
 * @return array Ops out
 */
function precedence_sort($op_list)
{
    if (count($op_list) == 1) {
        return $op_list[0];
    }

    if (count($op_list) == 2) {
        $_e_pos = $op_list[0][count($op_list[0]) - 1];
        $new = array($op_list[1], $op_list[0], $op_list[2], $_e_pos);
        return $new;
    }

    global $JS_OPS;

    foreach ($JS_OPS as $op_try) {
        foreach ($op_list as $JS_PARSE_POSITION => $op) {
            if ($JS_PARSE_POSITION % 2 == 0) {
                continue;
            }
            if ($op == $op_try) {
                $left = array_slice($op_list, 0, $JS_PARSE_POSITION);
                $right = array();
                foreach ($op_list as $i => $bit) {
                    if ($i > $JS_PARSE_POSITION) {
                        $right[] = $bit;
                    }
                }
                $_e_pos = $left[count($left) - 1][count($left[count($left) - 1]) - 1];
                $_left = precedence_sort($left);
                $_right = precedence_sort($right);
                return array($op, $_left, $_right, $_e_pos);
            }
        }
    }

    // Should never get here
    echo '!';
    print_r($op_list);
    return array();
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_expression_inner()
{
    // Choice{"BW_NOT" expression | "BOOLEAN_NOT" expression | "TYPEOF" "IDENTIFIER" | "IDENTIFIER" "IN" "IDENTIFIER" | "IDENTIFIER" "INSTANCEOF" "IDENTIFIER" | SUBTRACT expression | literal | variable | variable "BRACKET_OPEN" comma_parameters "BRACKET_CLOSE" | "IDENTIFIER" | "IDENTIFIER" "BRACKET_OPEN" comma_parameters "BRACKET_CLOSE" | "NEW" "IDENTIFIER" "BRACKET_OPEN" comma_expressions "BRACKET_CLOSE" | "NEW" "IDENTIFIER" | "ARRAY" "BRACKET_OPEN" create_array "BRACKET_CLOSE" | "BRACKET_OPEN" expression "BRACKET_CLOSE" | "BRACKET_OPEN" assignment "BRACKET_CLOSE"}

    $next = parser_peek();

    if (($next == 'OBJECT_OPERATOR') && (parser_peek_dist(1) == 'number_literal')) {
        // Actually it's a float start
        parser_next();
        $next = parser_peek();
    }

    if (in_array($next, array('number_literal', 'string_literal', 'true', 'false', 'null', 'undefined', 'NaN', 'Infinity'))) { // little trick
        $next = '*literal';
    }
    switch ($next) {
        case 'DEC':
            parser_next();
            $variable = _webstandards_js_parse_variable();
            if (is_null($variable)) {
                return null;
            }
            $expression = array('PRE_DEC', $variable, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'INC':
            parser_next();
            $variable = _webstandards_js_parse_variable();
            if (is_null($variable)) {
                return null;
            }
            $expression = array('PRE_INC', $variable, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'BOOLEAN_NOT':
            parser_next();
            $_expression = _webstandards_js_parse_expression_inner();
            if (is_null($_expression)) {
                return null;
            }
            $expression = array('BOOLEAN_NOT', $_expression, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'BW_NOT':
            parser_next();
            $_expression = _webstandards_js_parse_expression_inner();
            if (is_null($_expression)) {
                return null;
            }
            $expression = array('BW_NOT', $_expression, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'TYPEOF':
            parser_next();
            $_expression = _webstandards_js_parse_expression_inner();
            if (is_null($_expression)) {
                return null;
            }
            $expression = array('TYPEOF', $_expression, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'ADD': // When used like a cast
            parser_next();
            $_expression = _webstandards_js_parse_expression_inner();
            if (is_null($_expression)) {
                return null;
            }
            $expression = $_expression;
            break;

        case 'SUBTRACT': // When used to negate
            parser_next();
            $_expression = _webstandards_js_parse_expression_inner();
            if (is_null($_expression)) {
                return null;
            }
            $expression = array('NEGATE', $_expression, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case '*literal':
            $literal = _webstandards_js_parse_literal();
            if (is_null($literal)) {
                return null;
            }
            $expression = array('LITERAL', $literal, $GLOBALS['JS_PARSE_POSITION']);

            $test = parser_peek();
            if ($test == 'IN') {
                $expression = _webstandards_js_parse_identify_chain($expression);
            }

            break;

        case 'CATCH': // May be a promises 'catch' method
            parser_next();
            $expression = array('VARIABLE', 'catch', array(), $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'IDENTIFIER':
            $variable = _webstandards_js_parse_variable();
            if (is_null($variable)) {
                return null;
            }
            $expression = _webstandards_js_parse_identify_chain($variable);
            break;

        case 'FUNCTION':
            $function = _webstandards_js_parse_function_dec(true);
            if (is_null($function)) {
                return null;
            }
            $expression = array('NEW_OBJECT_FUNCTION', $function, $GLOBALS['JS_PARSE_POSITION']);

            $test = parser_peek();
            if ($test == 'BRACKET_OPEN') {
                parser_next();

                $parameters = _webstandards_js_parse_comma_expressions();
                if (is_null($parameters)) {
                    return null;
                }
                if (is_null(parser_expect('BRACKET_CLOSE'))) {
                    return null;
                }
                $expression = array('CALL', array('VARIABLE', '_', array(), $GLOBALS['JS_PARSE_POSITION']), $parameters, $GLOBALS['JS_PARSE_POSITION']);
            }

            break;

        case 'NEW':
            parser_next();
            $identifier = parser_next(true);
            if ($identifier[0] != 'IDENTIFIER') {
                webstandards_js_parser_error('Expected IDENTIFIER but got ' . $identifier[0]);
                return null;
            }
            $next_2 = parser_peek();
            if ($next_2 == 'BRACKET_OPEN') {
                parser_next();
                $expressions = _webstandards_js_parse_comma_expressions();
                if (is_null($expressions)) {
                    return null;
                }
                if (is_null(parser_expect('BRACKET_CLOSE'))) {
                    return null;
                }
                $expression = array('NEW_OBJECT', $identifier[1], $expressions, $GLOBALS['JS_PARSE_POSITION']);
            } else {
                $expression = array('NEW_OBJECT', $identifier[1], array(), $GLOBALS['JS_PARSE_POSITION']);
            }
            break;

        case 'EXTRACT_OPEN':
            parser_next();
            $expressions = _webstandards_js_parse_comma_expressions(true, 'EXTRACT_CLOSE');
            if (is_null($expressions)) {
                return null;
            }
            $expression = array('NEW_OBJECT', 'Array', $expressions, $GLOBALS['JS_PARSE_POSITION']);
            if (is_null(parser_expect('EXTRACT_CLOSE'))) {
                return null;
            }
            if (parser_peek() == 'EXTRACT_OPEN') {
                $extra = _webstandards_js_parse_variable_actual();
                $expression = array('VARIABLE', $expression, $extra, $GLOBALS['JS_PARSE_POSITION']);
            }
            break;

        case 'CURLY_OPEN':
            parser_next();
            $expressions = _webstandards_js_parse_comma_parameters(true, 'CURLY_CLOSE', 'COLON');
            if (is_null($expressions)) {
                return null;
            }
            $expression = array('NEW_OBJECT', '!Object', $expressions, $GLOBALS['JS_PARSE_POSITION']); // This is a hack. There could be no general-purpose expressions constructor for an object. The member names aren't even stored. But as it's dynamic, we couldn't check this anyway. So we just store a new "unknown" object and check the expressions as isolated (which shoving them in a constructor function will do).
            if (is_null(parser_expect('CURLY_CLOSE'))) {
                return null;
            }
            if ((parser_peek() == 'EXTRACT_OPEN') || (parser_peek() == 'OBJECT_OPERATOR')) {
                $extra = _webstandards_js_parse_variable_actual();
                $expression = array('VARIABLE', $expression, $extra, $GLOBALS['JS_PARSE_POSITION']);
            }
            break;

        case 'BRACKET_OPEN':
            parser_next();

            // Look ahead to see if this is an embedded assignment or a cast
            $next_2 = parser_peek_dist(0);
            $next_3 = parser_peek_dist(1);
            if ($next_3 == 'EQUAL') { // Not really necessary, but makes a bit cleaner
                $target = _webstandards_js_parse_variable();
                if (is_null($target)) {
                    return null;
                }
                if (is_null(parser_expect('EQUAL'))) {
                    return null;
                }
                $_expression = _webstandards_js_parse_expression();
                if (is_null($_expression)) {
                    return null;
                }
                $expression = array('ASSIGNMENT', 'EQUAL', $target, $_expression, $GLOBALS['JS_PARSE_POSITION']);
                if (is_null(parser_expect('BRACKET_CLOSE'))) {
                    return null;
                }
            } else {
                $_expression = _webstandards_js_parse_expression();
                if (is_null($_expression)) {
                    return null;
                }
                if (is_null(parser_expect('BRACKET_CLOSE'))) {
                    return null;
                }
                $test = parser_peek();
                if (($test == 'EXTRACT_OPEN') || ($test == 'OBJECT_OPERATOR')) {
                    $extra = _webstandards_js_parse_variable_actual();
                    $expression = array('VARIABLE', $_expression, $extra, $GLOBALS['JS_PARSE_POSITION']);
                } elseif ($test == 'BRACKET_OPEN') {
                    $variable = array('VARIABLE', $_expression, array(), $GLOBALS['JS_PARSE_POSITION']);
                    parser_next();
                    $parameters = _webstandards_js_parse_comma_expressions();
                    if (is_null($parameters)) {
                        return null;
                    }
                    if (is_null(parser_expect('BRACKET_CLOSE'))) {
                        return null;
                    }
                    $expression = array('CALL', $variable, $parameters, $GLOBALS['JS_PARSE_POSITION']);
                } else {
                    $expression = array('BRACKETED', $_expression, $GLOBALS['JS_PARSE_POSITION']);
                }
            }
            break;

        default:
            webstandards_js_parser_error('Invalid expression token ' . $next);
            return null;
    }
    return $expression;
}

/**
 * Return parse info for parse type.
 *
 * @param  array $variable The variable
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_identify_chain($variable)
{
    $next_2 = parser_peek();
    switch ($next_2) {
        case 'DEC':
            parser_next();
            $expression = array('DEC', $variable, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'INC':
            parser_next();
            $expression = array('INC', $variable, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'BRACKET_OPEN': // Is it an inline direct function call, or possibly a longer chain
            parser_next();
            $parameters = _webstandards_js_parse_comma_expressions();
            if (is_null($parameters)) {
                return null;
            }
            if (is_null(parser_expect('BRACKET_CLOSE'))) {
                return null;
            }
            $expression = array('CALL', $variable, $parameters, $GLOBALS['JS_PARSE_POSITION']);
            //log_special('functions', $next[1] . '/' . count($parameters)); Useful for debugging

            // Now, it is possible we are actually part of a larger variable
            $next_2 = parser_peek();
            if ($next_2 == 'EXTRACT_OPEN') { // If this is a "." then we will handle that as an operator
                $extra = _webstandards_js_parse_variable_actual();
                $expression = array('VARIABLE', $expression, $extra, $GLOBALS['JS_PARSE_POSITION']);
            }

            // Extension?
            $next_2 = parser_peek();
            if (in_array($next_2, array('BRACKET_OPEN', 'DEC', 'INC', 'IN', 'INSTANCEOF', 'EQUAL', 'CONCAT_EQUAL', 'DIV_EQUAL', 'MUL_EQUAL', 'SUBTRACT_EQUAL', 'PLUS_EQUAL', 'SL_EQUAL', 'SR_EQUAL', 'ZSR_EQUAL', 'BW_AND_EQUAL', 'BW_OR_EQUAL'))) {
                return _webstandards_js_parse_identify_chain(array('VARIABLE', $expression, array(), $GLOBALS['JS_PARSE_POSITION']));
            }
            break;

        case 'IN':
            parser_next();
            $identifier = parser_expect('IDENTIFIER');
            if (is_null($identifier)) {
                return null;
            }
            $expression = array('IN', $variable, $identifier, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'INSTANCEOF':
            parser_next();
            $identifier = parser_expect('IDENTIFIER');
            if (is_null($identifier)) {
                return null;
            }
            $expression = array('INSTANCEOF', $variable, $identifier, $GLOBALS['JS_PARSE_POSITION']);
            break;

        default:
            if (in_array($next_2, array('EQUAL', 'CONCAT_EQUAL', 'DIV_EQUAL', 'MUL_EQUAL', 'SUBTRACT_EQUAL', 'PLUS_EQUAL', 'SL_EQUAL', 'SR_EQUAL', 'ZSR_EQUAL', 'BW_AND_EQUAL', 'BW_OR_EQUAL'))) {
                $assignment = _webstandards_js_parse_assignment_operator();
                if (is_null($assignment)) {
                    return null;
                }
                $_expression = _webstandards_js_parse_expression();
                if (is_null($_expression)) {
                    return null;
                }
                $expression = array('ASSIGNMENT', $assignment, $variable, $_expression, $GLOBALS['JS_PARSE_POSITION']);
            } else {
                $expression = $variable;
            }
    }

    return $expression;
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_variable()
{
    // Choice{"IDENTIFIER" | "IDENTIFIER" "OBJECT_OPERATOR" "IDENTIFIER" | "IDENTIFIER" "EXTRACT_OPEN" expression "EXTRACT_CLOSE"}

    $variable = parser_next(true);
    if ($variable[0] != 'IDENTIFIER') {
        webstandards_js_parser_error('Expected IDENTIFIER but got ' . $variable[0]);
        return null;
    }
    $variable_actual = _webstandards_js_parse_variable_actual();
    if (is_null($variable_actual)) {
        return null;
    }

    return array('VARIABLE', $variable[1], $variable_actual, $GLOBALS['JS_PARSE_POSITION']);
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_variable_actual()
{
    $next = parser_peek();
    switch ($next) {
        case 'OBJECT_OPERATOR':
            parser_next();
            $next_2 = parser_peek(true);
            if ($next_2[0] != 'IDENTIFIER') {
                $next_2 = array('IDENTIFIER', strtolower($next_2[0]));
                parser_next();
            } else {
                if (is_null(parser_expect('IDENTIFIER'))) {
                    return null;
                }
            }
            $dereference = array('VARIABLE', $next_2[1], array(), $GLOBALS['JS_PARSE_POSITION']);
            $tunnel = array();
            $next_3 = parser_peek();
            $next_4 = parser_peek_dist(1);
            if ((($next_3 == 'EXTRACT_OPEN') && ($next_4 != 'EXTRACT_CLOSE')) || ($next_3 == 'CURLY_OPEN') || ($next_3 == 'OBJECT_OPERATOR')) {
                $tunnel = _webstandards_js_parse_variable_actual();
                if (is_null($tunnel)) {
                    return null;
                }
            }

            $variable = array('OBJECT_OPERATOR', $dereference, $tunnel, $GLOBALS['JS_PARSE_POSITION']);

            if ($next_3 == 'BRACKET_OPEN') { // Function call actually
                $variable = _webstandards_js_parse_identify_chain($variable);
            }

            break;

        case 'EXTRACT_OPEN':
            $next_t = parser_peek_dist(1);
            if ($next_t == 'EXTRACT_CLOSE') {
                $variable = array();
                break;
            }
            parser_next();
            $next_2 = parser_peek(true);
            $expression = _webstandards_js_parse_expression();
            if (is_null($expression)) {
                return null;
            }
            if (is_null(parser_expect('EXTRACT_CLOSE'))) {
                return null;
            }
            $tunnel = array();
            $next_3 = parser_peek();
            $next_4 = parser_peek_dist(1);
            if ((($next_3 == 'EXTRACT_OPEN') && ($next_4 != 'EXTRACT_CLOSE')) || ($next_3 == 'OBJECT_OPERATOR')) {
                $tunnel = _webstandards_js_parse_variable_actual();
                if (is_null($tunnel)) {
                    return null;
                }
            }
            $variable = array('ARRAY_AT', $expression, $tunnel, $GLOBALS['JS_PARSE_POSITION']);
            break;

        default:
            $variable = array();
            break;
    }

    return $variable;
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_assignment_operator()
{
    // Choice{"EQUAL" | "CONCAT_EQUAL" | "DIV_EQUAL" | "MUL_EQUAL" | "SUBTRACT_EQUAL" | "PLUS_EQUAL" | "SL_EQUAL" | "SR_EQUAL" | "ZSR_EQUAL" | "BW_AND_EQUAL" | "BW_OR_EQUAL"}

    $next = parser_next();
    if (!in_array($next, array('EQUAL', 'CONCAT_EQUAL', 'DIV_EQUAL', 'MUL_EQUAL', 'SUBTRACT_EQUAL', 'PLUS_EQUAL', 'SL_EQUAL', 'SR_EQUAL', 'ZSR_EQUAL', 'BW_AND_EQUAL', 'BW_OR_EQUAL'))) {
        webstandards_js_parser_error('Expected assignment operator but got ' . $next);
        return null;
    }
    return $next;
}

/**
 * Return parse info for parse type.
 *
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_literal()
{
    // Choice{"SUBTRACT" literal | "number_literal" | "string_literal" | "true" | "false" | "null" | "undefined" | "Infinity" | "NaN" | "IDENTIFIER"}

    $next = parser_peek();
    switch ($next) {
        case 'SUBTRACT': // As negation
            parser_next();
            $_literal = _webstandards_js_parse_literal();
            if (is_null($_literal)) {
                return null;
            }
            $literal = array('NEGATE', $_literal, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'number_literal':
            $_literal = parser_next(true);
            $literal = array('Number', $_literal[1], $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'string_literal':
            $_literal = parser_next(true);
            $literal = array('String', $_literal[1], $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'true':
            parser_next();
            $literal = array('Boolean', true, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'false':
            parser_next();
            $literal = array('Boolean', false, $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'null':
            parser_next();
            $literal = array('Null', $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'undefined':
            parser_next();
            $literal = array('Undefined', $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'NaN':
            parser_next();
            $literal = array('Number', $GLOBALS['JS_PARSE_POSITION']);
            break;

        case 'Infinity':
            parser_next();
            $literal = array('Number', $GLOBALS['JS_PARSE_POSITION']);
            break;

        default:
            webstandards_js_parser_error('Expected <literal> but got ' . $next);
            return null;
    }
    return $literal;
}

/**
 * Return parse info for parse type.
 *
 * @param  boolean $allow_blanks Whether to allow blanks in the list
 * @param  string $closer The token to close the list
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_comma_expressions($allow_blanks = false, $closer = 'BRACKET_CLOSE')
{
    // Choice{expression "COMMA" comma_expressions | expression}

    $expressions = array();

    $next = parser_peek();
    if ($next == $closer) {
        return array();
    }

    $next_2 = '';
    do {
        $next_2 = parser_peek();
        if (($next_2 == 'COMMA') || ($next_2 == $closer)) {
            $expression = array('LITERAL', array('Undefined', $GLOBALS['JS_PARSE_POSITION']), $GLOBALS['JS_PARSE_POSITION']);
        } else {
            $expression = _webstandards_js_parse_expression();
            if (is_null($expression)) {
                return null;
            }
        }
        $expressions[] = $expression;

        $next_2 = parser_peek();
        if ($next_2 == 'COMMA') {
            parser_next();
        }
    } while ($next_2 == 'COMMA');

    return $expressions;
}

/**
 * Return parse info for parse type.
 *
 * @param  boolean $allow_blanks Whether to allow blanks in the list
 * @param  string $closer The token to close the list
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_comma_variables($allow_blanks = false, $closer = 'BRACKET_CLOSE')
{
    // Choice{"variable" "COMMA" comma_variables | "variable"}

    $variables = array();

    $next = parser_peek();
    if (($next == $closer)) {
        return $variables;
    }

    $next_2 = '';
    do {
        $next_2 = parser_peek();
        while (($allow_blanks) && (($next_2 == 'COMMA') || ($next_2 == $closer))) {
            if ($next_2 == 'COMMA') { // ,,
                parser_next();
                $variables[] = array('VARIABLE', '_', array());
            } elseif ($next_2 == $closer) { // ,,
                $variables[] = array('VARIABLE', '_', array());
                return $variables;
            }

            $next_2 = parser_peek();
        }

        $variable = _webstandards_js_parse_variable();
        if (is_null($variable)) {
            return null;
        }
        $variables[] = $variable;

        $next_2 = parser_peek();
        if ($next_2 == 'COMMA') {
            parser_next();
        }
    } while ($next_2 == 'COMMA');

    return $variables;
}

/**
 * Return parse info for parse type.
 *
 * @param  boolean $allow_expressions Whether to allow expressions in this
 * @param  string $closer The token to close the list
 * @param  string $separator The token that sits as the 'separator' between name and value
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_comma_parameters($allow_expressions = true, $closer = 'BRACKET_CLOSE', $separator = 'EQUAL')
{
    // Choice{parameter | parameter "COMMA" comma_parameters}?

    $parameters = array();

    $next = parser_peek();
    if ($next == $closer) {
        return $parameters;
    }

    $next_2 = '';
    do {
        $parameter = _webstandards_js_parse_parameter($allow_expressions, $separator);
        if (is_null($parameter)) {
            return null;
        }
        $parameters[] = $parameter;

        $next_2 = parser_peek();
        if ($next_2 == 'COMMA') {
            parser_next();

            if (($closer == 'CURLY_CLOSE') && (parser_peek() == 'CURLY_CLOSE')) {
                return $parameters;
            }
        }
    } while ($next_2 == 'COMMA');

    return $parameters;
}

/**
 * Return parse info for parse type.
 *
 * @param  string $closer The token to close the list
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_comma_commands($closer = 'COMMAND_TERMINATE')
{
    // Choice{parameter | parameter "COMMA" comma_commands}?

    $commands = array();

    $next = parser_peek();
    if ($next == $closer) {
        return $commands;
    }

    $next_2 = '';
    do {
        $command = _webstandards_js_parse_command_actual();
        if ($command === null) {
            return null;
        }
        $commands[] = $command;

        $next_2 = parser_peek();
        if ($next_2 == 'COMMA') {
            parser_next();
        }
    } while ($next_2 == 'COMMA');

    return $commands;
}

/**
 * Return parse info for parse type.
 *
 * @param  boolean $allow_expressions Whether to allow expressions in this
 * @param  string $separator The token that sits as the 'separator' between name and value
 * @return ?list Parse info (null: error)
 * @ignore
 */
function _webstandards_js_parse_parameter($allow_expressions = true, $separator = 'EQUAL')
{
    // Choice{"REFERENCE" "variable" | "variable" | "variable" "EQUAL" literal}

    $next = parser_next(true);
    if ($next[0] == 'undefined') {
        $parameter = array('CALL_BY_VALUE', $next[0], null, $GLOBALS['JS_PARSE_POSITION']);
    } elseif (($next[0] == 'IDENTIFIER') || ((substr($next[0], -8) == '_literal') && ($separator == 'COLON'))) {
        $parameter = array('CALL_BY_VALUE', $next[1], null, $GLOBALS['JS_PARSE_POSITION']);
        if ($allow_expressions) {
            $next_2 = parser_peek();
            if ($next_2 == $separator) {
                parser_next();
                $value = _webstandards_js_parse_expression();
                if (is_null($value)) {
                    return null;
                }
                $parameter[2] = $value;
            }
        }
    } else {
        webstandards_js_parser_error('Expected (parameter) but got ' . $next[0]);
        return null;
    }
    return $parameter;
}

/**
 * Expect a token during parsing. Give error if not found. Else give token parameters.
 *
 * @param  string $token The token we want
 * @return ?mixed The token parameters (null: error)
 */
function parser_expect($token)
{
    global $JS_LEX_TOKENS, $JS_PARSE_POSITION;
    if (!isset($JS_LEX_TOKENS[$JS_PARSE_POSITION])) {
        webstandards_js_parser_error('Ran out of input when expecting ' . $token);
        return null;
    }
    $next = $JS_LEX_TOKENS[$JS_PARSE_POSITION];
    if ($next[0] == 'comment') {
        return parser_expect($token);
    }
    $JS_PARSE_POSITION++;
    if ($next[0] != $token) {
        webstandards_js_parser_error('Expected ' . $token . ' but got ' . $next[0]);
        return null;
    }
    return $next[1];
}

/**
 * Peek to find the next token.
 *
 * @param  boolean $all Whether we want all the token parameters (as opposed to just the first)
 * @return ?mixed All the token parameters, or just the first (null: error)
 */
function parser_peek($all = false)
{
    global $JS_LEX_TOKENS, $JS_PARSE_POSITION;
    if (!isset($JS_LEX_TOKENS[$JS_PARSE_POSITION])) {
        return null;
    }
    if ($JS_LEX_TOKENS[$JS_PARSE_POSITION][0] == 'comment') {
        $JS_PARSE_POSITION++;
        return parser_peek($all);
    }
    if ($all) {
        return $JS_LEX_TOKENS[$JS_PARSE_POSITION];
    }
    return $JS_LEX_TOKENS[$JS_PARSE_POSITION][0];
}

/**
 * Peek to find the next token after a distance.
 *
 * @param  integer $d The distance
 * @param  ?integer $p Whether to start looking from (null: current position in parse)
 * @return ?mixed The first token parameter (null: error)
 */
function parser_peek_dist($d, $p = null)
{
    global $JS_LEX_TOKENS, $JS_PARSE_POSITION;
    if (is_null($p)) {
        $p = $JS_PARSE_POSITION;
    }
    while ($d != 0) {
        if (!isset($JS_LEX_TOKENS[$p])) {
            return null;
        }
        if ($JS_LEX_TOKENS[$p][0] == 'comment') {
            return parser_peek_dist($d, $p + 1);
        }
        $p++;
        $d--;
    }
    if (!isset($JS_LEX_TOKENS[$p])) {
        return null;
    }
    return $JS_LEX_TOKENS[$p][0];
}

/**
 * Find the next token and move on.
 *
 * @param  boolean $all Whether we want all the token parameters (as opposed to just the first)
 * @return ?mixed All the token parameters, or just the first (null: error)
 */
function parser_next($all = false)
{
    global $JS_LEX_TOKENS, $JS_PARSE_POSITION;
    if (!isset($JS_LEX_TOKENS[$JS_PARSE_POSITION])) {
        return null;
    }
    $next = $JS_LEX_TOKENS[$JS_PARSE_POSITION];
    $JS_PARSE_POSITION++;
    if ($next[0] == 'comment') {
        return parser_next($all);
    }
    if ($all) {
        return $next;
    }
    return $next[0];
}

/**
 * Give a parse error.
 *
 * @param  string $message The error
 * @return ?boolean Always null (null: exit)
 */
function webstandards_js_parser_error($message)
{
    global $JS_LEX_TOKENS, $JS_PARSE_POSITION;
    list($pos, $line, $_, $i) = js_pos_to_line_details($JS_PARSE_POSITION);
    return js_die_error('PARSER', $pos, $line, $message, $i);
}
