<?php
/**
 * Source Code Highlighting
 *
 * The classes in this file are responsible for the dynamic @example, and
 * <programlisting role="tutorial"> tags output.  Using the
 * WordParser, the phpDocumentor_TutorialHighlightParser
 * retrieves PHP tokens one by one from the array generated by
 * {@link WordParser} source retrieval functions
 * and then highlights them individually.
 *
 * It accomplishes this highlighting through the assistance of methods in
 * the output Converter passed to its parse() method, and then returns the
 * fully highlighted source as a string
 * 
 * phpDocumentor :: automatic documentation generator
 * 
 * PHP versions 4 and 5
 *
 * Copyright (c) 2003-2007 Gregory Beaver
 * 
 * LICENSE:
 * 
 * This library is free software; you can redistribute it
 * and/or modify it under the terms of the GNU Lesser General
 * Public License as published by the Free Software Foundation;
 * either version 2.1 of the License, or (at your option) any
 * later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 * @category   ToolsAndUtilities
 * @package    phpDocumentor
 * @subpackage Parsers
 * @author     Gregory Beaver <cellog@php.net>
 * @copyright  2003-2007 Gregory Beaver
 * @license    http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @version    CVS: $Id: TutorialHighlightParser.inc 246148 2007-11-14 01:57:04Z ashnazg $
 * @tutorial   tags.example.pkg, tags.filesource.pkg
 * @link       http://www.phpdoc.org
 * @link       http://pear.php.net/PhpDocumentor
 * @since      1.3.0
 * @todo       CS cleanup - change package to PhpDocumentor
 * @todo       CS cleanup - PHPCS needs to ignore CVS Id length
 */

/**
 * Highlights source code using {@link parse()}
 *
 * @category   ToolsAndUtilities
 * @package    phpDocumentor
 * @subpackage Parsers
 * @author     Gregory Beaver <cellog@php.net>
 * @copyright  2003-2007 Gregory Beaver
 * @license    http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @version    Release: 1.4.3
 * @link       http://www.phpdoc.org
 * @link       http://pear.php.net/PhpDocumentor
 * @todo       CS cleanup - change package to PhpDocumentor
 * @todo       CS cleanup - change classname to PhpDocumentor_*
 */
class phpDocumentor_TutorialHighlightParser extends Parser
{
    /**#@+ @access private */
    /**
     * Highlighted source is built up in this string
     * @var string
     */
    var $_output;
    /**
     * contents of the current source code line as it is parsed
     * @var string
     */
    var $_line;
    /**
     * Used to retrieve highlighted tokens
     * @var Converter a descendant of Converter
     */
    var $_converter;
    /**
     * Path to file being highlighted, if this is from a @filesource tag
     * @var false|string full path
     */
    var $_filesourcepath;
    /**
     * @var array
     */
    var $eventHandlers = array(
        TUTORIAL_EVENT_NOEVENTS    => 'defaultHandler',
        TUTORIAL_EVENT_ITAG        => 'defaultHandler',
        TUTORIAL_EVENT_ATTRIBUTE   => 'attrHandler',
        TUTORIAL_EVENT_OPENTAG     => 'defaultHandler',
        TUTORIAL_EVENT_CLOSETAG    => 'defaultHandler',
        TUTORIAL_EVENT_ENTITY      => 'defaultHandler',
        TUTORIAL_EVENT_COMMENT     => 'defaultHandler',
        TUTORIAL_EVENT_SINGLEQUOTE => 'defaultHandler',
        TUTORIAL_EVENT_DOUBLEQUOTE => 'defaultHandler',
    );
    /**#@-*/
    
    /**
     * advances output to a new line
     *
     * @return void
     * @uses Converter::SourceLine() encloses {@link $_line} in a
     *                               converter-specific format
     */
    function newLineNum()
    {
        $this->_line   .= $this->_converter->flushHighlightCache();
        $this->_output .= $this->_converter->SourceLine($this->_pv_curline + 1, 
            $this->_line, $this->_path);
        $this->_line    = '';
    }
    
    /**
     * Start the parsing at a certain line number
     *
     * @param int $num the line number
     *
     * @return void
     */
    function setLineNum($num)
    {
        $this->_wp->linenum = $num;
    }
    
    /**
     * Parse a new file
     *
     * The parse() method is a do...while() loop that retrieves tokens one by
     * one from the {@link $_event_stack}, and uses the token event array set up
     * by the class constructor to call event handlers.
     *
     * The event handlers each process the tokens passed to them, and use the
     * {@link _addoutput()} method to append the processed tokens to the
     * {@link $_line} variable.  The word parser calls {@link newLineNum()}
     * every time a line is reached.
     *
     * In addition, the event handlers use special linking functions
     * {@link _link()} and its cousins (_classlink(), etc.) to create in-code
     * hyperlinks to the documentation for source code elements that are in the
     * source code.
     *
     * @param string        $parse_data     blah
     * @param Converter     &$converter     blah
     * @param false|string  $filesourcepath full path to file with @filesource tag,
     *                                      if this is a @filesource parse
     * @param false|integer $linenum        starting line number from
     *                                      {@}source linenum}
     *
     * @staticvar integer used for recursion limiting if a handler for
     *                    an event is not found
     * @return    bool
     * @uses setupStates() initialize parser state variables
     * @uses configWordParser() pass $parse_data to prepare retrieval of tokens
     * @todo CS cleanup - unable to get function signature below 85char wide
     */
    function parse($parse_data, &$converter, $filesourcepath = false, $linenum = false)
    {
        static $endrecur  = 0;
        $parse_data       = 
            str_replace(array("\r\n", "\t"), array("\n", '    '), $parse_data);
        $this->_converter = &$converter;
        $converter->startHighlight();
        $this->_path = $filesourcepath;
        $this->setupStates($parse_data);

        $this->configWordParser(TUTORIAL_EVENT_NOEVENTS);
        if ($linenum !== false) {
            $this->setLineNum($linenum);
        }
        // initialize variables so E_ALL error_reporting doesn't complain
        $pevent = 0;
        $word   = 0;

        do {
            $lpevent = $pevent;
            $pevent  = $this->_event_stack->getEvent();
            if ($lpevent != $pevent) {
                $this->_last_pevent = $lpevent;
                $this->configWordParser($pevent);
            }
            $this->_wp->setWhitespace(true);

            $dbg_linenum         = $this->_wp->linenum;
            $dbg_pos             = $this->_wp->getPos();
            $this->_pv_last_word = $word;
            $this->_pv_curline   = $this->_wp->linenum;
            $word                = $this->_wp->getWord();

            if (PHPDOCUMENTOR_DEBUG == true) {
                echo "LAST: ";
                echo "|" . $this->_pv_last_word;
                echo "|\n";
                echo "PEVENT: " . $this->getParserEventName($pevent) . "\n";
                echo "LASTPEVENT: " . 
                    $this->getParserEventName($this->_last_pevent) . "\n";
                //DEBUG echo "LINE: " . $this->_line . "\n";
                //DEBUG echo "OUTPUT: " . $this->_output . "\n";
                echo $dbg_linenum.'-'.$dbg_pos . ": ";
                echo '|'.htmlspecialchars($word);
                echo "|\n";
                echo "-------------------\n\n\n";
                flush();
            }
            if (isset($this->eventHandlers[$pevent])) {
                $handle = $this->eventHandlers[$pevent];
                $this->$handle($word, $pevent);
            } else {
                debug('WARNING: possible error, no handler for event number '
                    . $pevent);
                if ($endrecur++ == 25) {
                    die("FATAL ERROR, recursion limit reached");
                }
            }
        } while (!($word === false));
        if (strlen($this->_line)) {
            $this->newLineNum();
        }
        return $this->_output;
    }
    
    /**#@+
     * Event Handlers
     *
     * All Event Handlers use {@link checkEventPush()} and
     * {@link checkEventPop()} to set up the event stack and parser state.
     *
     * @param string|array $word   token value
     * @param integer      $pevent parser event from {@link Parser.inc}
     *
     * @return void
     * @access private
     */
    /**
     * Most tokens only need highlighting, and this method handles them
     *
     * @todo CS cleanup - PHPCS needs to recognize docblock template tags
     */
    function defaultHandler($word, $pevent)
    {
        if ($word == "\n") {
            $this->newLineNum();
            return;
        }
        if ($this->checkEventPush($word, $pevent)) {
            $this->_wp->backupPos($word);
            return;
        }
        $this->_addoutput($word);
        $this->checkEventPop($word, $pevent);
    }

    /**
     * Most tokens only need highlighting, and this method handles them
     *
     * @todo CS cleanup - PHPCS needs to recognize docblock template tags
     */
    function attrHandler($word, $pevent)
    {
        if ($word == "\n") {
            $this->newLineNum();
            return;
        }
        if ($e = $this->checkEventPush($word, $pevent)) {
            if ($e == TUTORIAL_EVENT_SINGLEQUOTE 
                || $e == TUTORIAL_EVENT_DOUBLEQUOTE
            ) {
                $this->_addoutput($word);
            }
            return;
        }
        if ($this->checkEventPop($word, $pevent)) {
            $this->_wp->backupPos($word);
            return;
        }
        $this->_addoutput($word);
    }
    /**#@-*/
    
    /**#@+
     * Output Methods
     * @access private
     */
    /**
     * This method adds output to {@link $_line}
     *
     * If a string with variables like "$test this" is present, then special
     * handling is used to allow processing of the variable in context.
     *
     * @param string $word         the output to add
     * @param bool   $preformatted whether or not its preformatted
     *
     * @return void
     * @see _flush_save()
     */
    function _addoutput($word, $preformatted = false)
    {
        $type = array(
            TUTORIAL_EVENT_ATTRIBUTE   => 'attribute',
            TUTORIAL_EVENT_SINGLEQUOTE => 'attributevalue',
            TUTORIAL_EVENT_DOUBLEQUOTE => 'attributevalue',
            TUTORIAL_EVENT_CLOSETAG    => 'closetag',
            TUTORIAL_EVENT_ENTITY      => 'entity',
            TUTORIAL_EVENT_ITAG        => 'itag',
            TUTORIAL_EVENT_OPENTAG     => 'opentag',
            TUTORIAL_EVENT_COMMENT     => 'comment',
        );

        $a = $this->_event_stack->getEvent();
        if (in_array($a, array_keys($type))) {
            $this->_line .= 
                $this->_converter->highlightTutorialSource($type[$a], $word);
        } else {
            $this->_line .= $this->_converter->flushHighlightCache();
            $this->_line .= $this->_converter->postProcess($word);
        }
    }
    /**#@-*/

    /**
     * Tell the parser's WordParser {@link $wp} to set up tokens to parse words by.
     *
     * Tokens are word separators.  In English, a space or punctuation are
     * examples of tokens.  In PHP, a token can be a ;, a parenthesis, or 
     * even the word "function"
     *
     * @param integer $e an event number
     *
     * @return void
     * @see WordParser
     */
    function configWordParser($e)
    {
        $this->_wp->setSeperator($this->tokens[($e + 100)]);
    }

    /**#@+
     * @param string|array $word   token value
     * @param integer      $pevent parser event from {@link Parser.inc}
     *
     * @return mixed returns false, or the event number
     */
    /**
     * This function checks whether parameter $word is a token 
     * for pushing a new event onto the Event Stack.
     *
     * @todo CS cleanup - PHPCS needs to recognize docblock template tags
     */
    function checkEventPush($word, $pevent)
    {
        $e = false;
        if (isset($this->pushEvent[$pevent])) {
            if (isset($this->pushEvent[$pevent][strtolower($word)])) {
                $e = $this->pushEvent[$pevent][strtolower($word)];
            }
        }
        if ($e) {
            $this->_event_stack->pushEvent($e);
            return $e;
        } else {
            return false;
        }
    }

    /**
     * This function checks whether parameter $word is a token 
     * for popping the current event off of the Event Stack.
     *
     * @todo CS cleanup - PHPCS needs to recognize docblock template tags
     */
    function checkEventPop($word, $pevent)
    {
        if (!isset($this->popEvent[$pevent])) {
            return false;
        }
        if (in_array(strtolower($word), $this->popEvent[$pevent])) {
            return $this->_event_stack->popEvent();
        } else {
            return false;
        }
    }
    /**#@-*/

    /**
     * Initialize all parser state variables
     *
     * @param bool|string $parsedata true if we are highlighting an inline {@}source}
     *                               tag's output, or the name of class we are going 
     *                               to start from
     *
     * @return void
     * @uses $_wp sets to a new {@link phpDocumentor_HighlightWordParser}
     */
    function setupStates($parsedata)
    {
        $this->_output = '';
        $this->_line   = '';

        unset($this->_wp);
        $this->_wp = new WordParser;
        $this->_wp->setup($parsedata);

        $this->_event_stack = new EventStack;
        $this->_event_stack->popEvent();
        $this->_event_stack->pushEvent(TUTORIAL_EVENT_NOEVENTS);

        $this->_pv_linenum   = null;
        $this->_pv_next_word = false;
    }

    /**
     * Initialize the {@link $tokenpushEvent, $wordpushEvent} arrays
     */
    function phpDocumentor_TutorialHighlightParser()
    {
        $this->allowableInlineTags                = 
            $GLOBALS['_phpDocumentor_inline_tutorial_tags_allowed']
        ;
        $this->inlineTagHandlers                  = 
            array('*' => 'handleDefaultInlineTag')
        ;
        $this->tokens[STATE_TUTORIAL_NOEVENTS]    = 
            array("\n",'{@', '<!--', '</', '<', '&');
        $this->tokens[STATE_TUTORIAL_ITAG]        = array("\n","}");
        $this->tokens[STATE_TUTORIAL_OPENTAG]     = array("\n","\t"," ", '>', '/>');
        $this->tokens[STATE_TUTORIAL_CLOSETAG]    = array("\n",'>');
        $this->tokens[STATE_TUTORIAL_COMMENT]     = array("\n",'-->');
        $this->tokens[STATE_TUTORIAL_ENTITY]      = array("\n",';');
        $this->tokens[STATE_TUTORIAL_ATTRIBUTE]   = array("\n",'"',"'",'>','/>');
        $this->tokens[STATE_TUTORIAL_DOUBLEQUOTE] = array("\n",'"','&','{@');
        $this->tokens[STATE_TUTORIAL_SINGLEQUOTE] = array("\n","'",'&','{@');
        /**************************************************************/

        $this->pushEvent[TUTORIAL_EVENT_NOEVENTS] = array(
            "{@"   => TUTORIAL_EVENT_ITAG,
            '<'    => TUTORIAL_EVENT_OPENTAG,
            '</'   => TUTORIAL_EVENT_CLOSETAG,
            '&'    => TUTORIAL_EVENT_ENTITY,
            '<!--' => TUTORIAL_EVENT_COMMENT,
        );
        /**************************************************************/

        $this->pushEvent[TUTORIAL_EVENT_OPENTAG] = array(
            " "  => TUTORIAL_EVENT_ATTRIBUTE,
            "\n" => TUTORIAL_EVENT_ATTRIBUTE,
        );
        /**************************************************************/

        $this->pushEvent[TUTORIAL_EVENT_ATTRIBUTE] = array(
            "'" => TUTORIAL_EVENT_SINGLEQUOTE,
            '"' => TUTORIAL_EVENT_DOUBLEQUOTE,
        );
        /**************************************************************/

        $this->pushEvent[TUTORIAL_EVENT_SINGLEQUOTE] = array(
            '&'  => TUTORIAL_EVENT_ENTITY,
            '{@' => TUTORIAL_EVENT_ITAG,
        );
        /**************************************************************/

        $this->pushEvent[TUTORIAL_EVENT_DOUBLEQUOTE] = array(
            '&'  => TUTORIAL_EVENT_ENTITY,
            '{@' => TUTORIAL_EVENT_ITAG,
        );
        /**************************************************************/

        $this->popEvent[TUTORIAL_EVENT_ENTITY] = array(';');
        /**************************************************************/

        $this->popEvent[TUTORIAL_EVENT_SINGLEQUOTE] = array("'");
        /**************************************************************/

        $this->popEvent[TUTORIAL_EVENT_DOUBLEQUOTE] = array('"');
        /**************************************************************/

        $this->popEvent[TUTORIAL_EVENT_OPENTAG] = array('>', '/>');
        /**************************************************************/

        $this->popEvent[TUTORIAL_EVENT_CLOSETAG] = array('>');
        /**************************************************************/

        $this->popEvent[TUTORIAL_EVENT_COMMENT] = array('-->');
        /**************************************************************/

        $this->popEvent[TUTORIAL_EVENT_ATTRIBUTE] = array('>','/>');
        /**************************************************************/

        $this->popEvent[TUTORIAL_EVENT_ITAG] = array('}');
        /**************************************************************/
    }

    /**
     * searches for a parser event name based on its number
     *
     * @param int $value the event number
     *
     * @return string|int the event name, or the original value
     */
    function getParserEventName ($value)
    {    
        $lookup = array(
            TUTORIAL_EVENT_NOEVENTS    => "TUTORIAL_EVENT_NOEVENTS",
            TUTORIAL_EVENT_ITAG        => "TUTORIAL_EVENT_ITAG",
            TUTORIAL_EVENT_OPENTAG     => "TUTORIAL_EVENT_OPENTAG",
            TUTORIAL_EVENT_ATTRIBUTE   => "TUTORIAL_EVENT_ATTRIBUTE",
            TUTORIAL_EVENT_CLOSETAG    => "TUTORIAL_EVENT_CLOSETAG",
            TUTORIAL_EVENT_ENTITY      => "TUTORIAL_EVENT_ENTITY",
            TUTORIAL_EVENT_COMMENT     => "TUTORIAL_EVENT_COMMENT",
            TUTORIAL_EVENT_SINGLEQUOTE => "TUTORIAL_EVENT_SINGLEQUOTE",
            TUTORIAL_EVENT_DOUBLEQUOTE => "TUTORIAL_EVENT_DOUBLEQUOTE",
        );
        if (isset($lookup[$value])) {
            return $lookup[$value];
        } else {
            return $value;
        }
    }
}


/**
 * starting state
 */
define("TUTORIAL_EVENT_NOEVENTS", 1);

/**
 * currently in starting state
 */
define("STATE_TUTORIAL_NOEVENTS", 101);

/**
 * used when an {@}inline tag} is found
 */
define("TUTORIAL_EVENT_ITAG", 2);

/**
 * currently parsing an {@}inline tag}
 */
define("STATE_TUTORIAL_ITAG", 102);

/**
 * used when an open <tag> is found
 */
define("TUTORIAL_EVENT_OPENTAG", 3);

/**
 * currently parsing an open <tag>
 */
define("STATE_TUTORIAL_OPENTAG", 103);

/**
 * used when a <tag attr="attribute"> is found
 */
define("TUTORIAL_EVENT_ATTRIBUTE", 4);

/**
 * currently parsing an open <tag>
 */
define("STATE_TUTORIAL_ATTRIBUTE", 104);

/**
 * used when a close </tag> is found
 */
define("TUTORIAL_EVENT_CLOSETAG", 5);

/**
 * currently parsing a close </tag>
 */
define("STATE_TUTORIAL_CLOSETAG", 105);

/**
 * used when an &entity; is found
 */
define("TUTORIAL_EVENT_ENTITY", 6);

/**
 * currently parsing an &entity;
 */
define("STATE_TUTORIAL_ENTITY", 106);

/**
 * used when a <!-- comment --> is found
 */
define("TUTORIAL_EVENT_COMMENT", 7);

/**
 * currently parsing a <!-- comment -->
 */
define("STATE_TUTORIAL_COMMENT", 107);

/**
 * used when a <!-- comment --> is found
 */
define("TUTORIAL_EVENT_SINGLEQUOTE", 8);

/**
 * currently parsing a <!-- comment -->
 */
define("STATE_TUTORIAL_SINGLEQUOTE", 108);

/**
 * used when a <!-- comment --> is found
 */
define("TUTORIAL_EVENT_DOUBLEQUOTE", 9);

/**
 * currently parsing a <!-- comment -->
 */
define("STATE_TUTORIAL_DOUBLEQUOTE", 109);

?>