Location: PHPKode > projects > Content*Builder > cb_pear/Text/Highlighter.php
<?php
//
// +----------------------------------------------------------------------+
// | Copyright (c) 2004 The PHP Group                                     |
// +----------------------------------------------------------------------+
// | This source file is subject to version 3.0 of the PHP license,       |
// | that is bundled with this package in the file LICENSE, and is        |
// | available at through the world-wide-web at                           |
// | http://www.php.net/license/3_0.txt.                                  |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | hide@address.com so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Author: Andrey Demenev <hide@address.com>                      |
// +----------------------------------------------------------------------+
// $Id: Highlighter.php,v 1.1 2004/05/21 10:48:19 cb_fog Exp $

/**
 * Text highlighter base class
 * 
 * @package Text_Highlighter
 */


require_once 'PEAR.php';

/**#@+
 * Constant for use with $options['tag']
 * @see Text_Highlighter::_init()
 */
/**
 * use CODE as top-level tag
 */
define ('HL_TAG_CODE'    , 'code');
/**
 * use PRE as top-level tag
 */
define ('HL_TAG_PRE',      'pre');
/**#@-*/

/**#@+
 * Constant for use with $options['numbers']
 * @see Text_Highlighter::_init()
 */
/**
 * use numbered list
 */
define ('HL_NUMBERS_LI'    ,    1);
/**
 * Use 2-column table with line numbers in left column and code in  right column.
 * Forces $options['tag'] = HL_TAG_PRE
 */
define ('HL_NUMBERS_TABLE'    , 2);
/**#@-*/

/**
 * just a big number, bigger than any string's length
 */
define ('HL_BIG_NUM',      1000000000);

 

/**
 * Text highlighter base class
 *
 * This class implements all functions necessary for highlighting,
 * but it does not contain highlighting rules. Actual highlighting is
 * done using a descendent of this class.
 *
 * One is not supposed to manually create descendent classes.
 * Instead, describe highlighting  rules in XML format and
 * use {@link Text_Highlighter_Generator} to create descendent class.
 * Alternatively, an instance of a descendent class can be created
 * directly.
 *
 * Use {@link Text_Highlighter::factory()} to create an
 * object for particular language highlighter
 *
 * Usage example
 * <code>
 *require_once 'Text/Highlighter.php';
 *$hlSQL =& Text_Highlighter::factory('SQL',array('numbers'=>true));
 *echo $hlSQL->highlight('SELECT * FROM table a WHERE id = 12');
 * </code>
 *
 * @author Andrey Demenev <hide@address.com>
 * @package Text_Highlighter
 * @access public
 */
class Text_Highlighter
{

    /**#@+
     * @access private
     * @see _init
     */

    /**
     * Syntax highlighting rules.
     * Auto-generated classes set this var
     *
     * @var array
     */
    var $_syntax;

    /**
     * Color group of last outputted text chunk
     *
     * @var string
     */
    var $_lastcolor;

    /**
     * HTML tag surrounding the code
     *
     * @var string
     */
    var $_tag = HL_TAG_PRE;

    /**
     * Line numbering style
     *
     * @var integer
     */
    var $_numbers = 0;

    /**
     * Tab size
     *
     * @var integer
     */
    var $_tabsize = 4;
    /**#@-*/

    /**
     * Highlighted text
     *
     * @var string
     * @access private
     */
    var $_output = '';



    /**
     * Create a new Highlighter object for specified language
     *
     * @param string $lang    language, for example "SQL"
     * @param array  $options Highlighting options. This array is passed
     * to new object's constructor, and the constructor, in turn,
     * passes it to {@link Text_Highlighter::_init()}
     *
     * @return mixed a newly created Highlighter object, or a PEAR error object on
     * error
     *
     * @static
     * @see Text_Highlighter::_init()
     * @access public
     */
    function &factory($lang, $options = array())
    {
        $lang = strtoupper($lang);
        @include_once("Text/Highlighter/${lang}.php");

        $classname = "Text_Highlighter_${lang}";

        if (!class_exists($classname)) {
            return PEAR::raiseError('Highlighter for ${lang} not found');
        }

        $obj =& new $classname($options);

        return $obj;
    }

    /**
     * Initilizes highlighting options
     *
     * @access private
     *
     * @param  array $options  Output options.
     * Assocative array with following elements (each being optional):
     *
     * - 'colors'  - array with color mappings
     * - 'tag'     - {@link HL_TAG_PRE} or {@link HL_TAG_CODE}
     * - 'numbers' - Whether to add line numbering
     * - 'tabsize' - Tab size
     * - 'style'   - CSS style of top-level tag
     *
     * Passing any of these elements overwrites the default value of 
     * corresponding variable. $options['colors'] is appended to $_colors
     *
     * Descendents of Text_Highlighter call this method from the constructor,
     * passing $options they get as parameter.
     */
    function _init($options = array())
    {
        $this->_lastcolor = 'default';
        if (isset($options['colors'])) {
            $this->_colors = array_merge($this->_colors, $options['colors']);
        }
        if (isset($options['tag']))     $this->_tag     = $options['tag'];
        if (isset($options['numbers'])) $this->_numbers = $options['numbers'];
        if (isset($options['tabsize'])) $this->_tabsize = $options['tabsize'];
        if ($this->_numbers == HL_NUMBERS_TABLE) {
            $this->_tag = HL_TAG_PRE;
        }
    }

    /**
     * Highlights code
     *
     * @param  string $str Code to highlight
     * @access public
     * @return string Highlighted text
     *
     */
    function highlight($str)
    {
        $this->_output = '';
        // normalize whitespace and tabs
        $str = str_replace("\r\n","\n", $str);
        $str = str_replace("\t",str_repeat(' ', $this->_tabsize), $str);

        // current position in string
        $pos = 0;
        // nested regions stack
        $stack = array();

        // current region
        $current = NULL;
        
        // what to seek first
        $lookForBlocks = isset($this->_syntax['toplevel']['blocks']) ? 
                          $this->_syntax['toplevel']['blocks'] :
                          null;
        $lookForRegions = isset($this->_syntax['toplevel']['regions']) ? 
                          $this->_syntax['toplevel']['regions'] :
                          null;

        $defcolor=$this->_syntax['defcolor'];
        while (true) {
            // init loop vars
            $matchpos = HL_BIG_NUM;
            $matchlen = 0;
            $what = -1;
            $thematch = null;
            $theregion = null;
            
            // get rid of the chars already processed
            $substr = substr($str, $pos);
            if ($substr === false) break;

            // trick for speeding up blocks lookups
            $firstline = $substr;
            
            // look for blocks, either top-level or
            // allowed within current region
            if ($matchpos && $lookForBlocks) {
                foreach ($lookForBlocks as $region) {
                    $region = $this->_syntax['blocks'][$region];
                    if ($current) {
                        $defcolor = $current['innerColor'];
                    }
                    if (preg_match($region['match'], $firstline, $matches, PREG_OFFSET_CAPTURE) &&
                            $matchpos > $matches[0][1]) {
                        $matchlen = strlen($matches[0][0]);
                        $matchpos = $matches[0][1];
                        $thematch = $matches[0][0];
                        $thematches = $matches;
                        $what     = 1;
                        $theregion = $region;
                        if (!$region['multiline']) {
                            // if found a block, and it is not multi-line
                            // then remove all after that line from the subject string
                            $newlinePos = strpos($firstline, "\n", $matchpos);
                            if ($newlinePos) {
                                $firstline = substr($firstline, 0, $newlinePos);
                            }
                        }
                    }
                    if (!$matchpos) {
                        break;
                    }
                }
            }

            
            // look for start of region, either top-level or
            // allowed within current region
            if ($matchpos && $lookForRegions) {
                foreach ($lookForRegions as $region) {
                    $region = $this->_syntax['regions'][$region];
                    if ($current) {
                        $defcolor=$current['innerColor'];
                    }
                    if (preg_match($region['start'], $substr, $matches, PREG_OFFSET_CAPTURE) &&
                            $matchpos > $matches[0][1]) {
                        $matchlen  = strlen($matches[0][0]);
                        $matchpos  = $matches[0][1];
                        $thematch  = $matches[0][0];
                        $what      = 0;
                        if ($region['remember']) {
                            foreach ($matches as $i => $amatch) {
                                $quoted = preg_quote($amatch[0]);
                                $region['end'] = str_replace('%'.$i.'%', $quoted, $region['end']);
                            }
                        }
                        $theregion = $region;
                    }
                    if (!$matchpos) {
                        break;
                    }
                }
            }


            // look for end of region
            if ($matchpos &&
                    $current && 
                    preg_match($current['end'], $substr, $matches, PREG_OFFSET_CAPTURE) &&
                    $matchpos > $matches[0][1]) {
                $matchlen  = strlen($matches[0][0]);
                $matchpos  = $matches[0][1];
                $thematch  = $matches[0][0];
                $what      = 2;
                $theregion = $region;
            }

            switch ($what) {
                // found start of region
                case 0:
                    if ($matchpos) {
                        $this->_chunk(substr($substr, 0, $matchpos), $defcolor);
                    }
                    $this->_chunk($thematch, $theregion['delimColor']);
                    if ($current) {
                        array_push($stack, $current);
                    }
                    $current = $theregion;
                    $lookForBlocks = isset($current['lookfor']['blocks']) ? 
                                       $current['lookfor']['blocks'] :
                                       null;
                    $lookForRegions = isset($current['lookfor']['regions']) ? 
                                       $current['lookfor']['regions'] :
                                       null;
                    $pos += $matchpos + $matchlen;
                    break;

                // found a block
                case 1:
                    if ($matchpos) {
                        $this->_chunk(substr($substr, 0, $matchpos), $defcolor);
                    }
                    $color = $theregion['innerColor'];
                    if (isset($theregion['partcolor'])) {
                        $partpos = $matchpos;
                        $nparts=count($thematches);
                        for ($i=1; $i<$nparts; $i++) {
                            if (isset($theregion['partcolor'][$i])) {
                                $this->_chunk(substr($substr, $partpos, $thematches[$i][1]-$partpos), $color);
                                $this->_chunk($thematches[$i][0], $theregion['partcolor'][$i]);
                            }
                            $partpos = $thematches[$i][1] + strlen($thematches[$i][0]);
                        }
                        if ($partpos < $matchpos + $matchlen) {
                            $this->_chunk(substr($substr, $partpos, $matchlen - $partpos + $matchpos), $color);
                        }
                    } else {
                        while (true) {
                            $newregion = null;
                            foreach ((array)$this->_syntax['keywords'] as $kwgroup) {
                                if ($kwgroup['inherits'] == $theregion['name']) {
                                    $csmatch = $kwgroup['case'] ? $thematch : strtolower($thematch);
                                    if (isset($kwgroup['match'][$csmatch])) {
                                        $color = $kwgroup['innerColor'];
                                        $newregion = null;
                                        break;
                                    }
                                    if (isset($kwgroup['otherwise'])) {
                                        $newregion = $this->_syntax['blocks'][$kwgroup['otherwise']];
                                    }
                                }
                            }
                            if ($newregion) {
                                $theregion = $newregion;
                                continue;
                            }
                            break;
                        }
                        $this->_chunk($thematch, $color);
                    }
                    $pos += $matchpos + $matchlen;
                    break;

                // found end of region
                case 2:
                    if ($matchpos) {
                        $this->_chunk(substr($substr, 0, $matchpos), $current['innerColor']);
                    }
                    $pos += $matchpos + $matchlen;
                    $this->_chunk($thematch, $current['delimColor']);
                    $current = array_pop($stack);
                    if ($current) {
                        $lookForBlocks = isset($current['lookfor']['blocks']) ? 
                                       $current['lookfor']['blocks'] :
                                       null;
                        $lookForRegions = isset($current['lookfor']['regions']) ? 
                                       $current['lookfor']['regions'] :
                                       null;
                    } else {
                         $lookForBlocks = isset($this->_syntax['toplevel']['blocks']) ? 
                                           $this->_syntax['toplevel']['blocks'] :
                                           null;
                         $lookForRegions = isset($this->_syntax['toplevel']['regions']) ? 
                                           $this->_syntax['toplevel']['regions'] :
                                           null;
                         $defcolor=$this->_syntax['defcolor'];
                    }
                    break;
                default:
                    $this->_chunk($substr, $defcolor);
                    $pos = HL_BIG_NUM;
            }

        }
        $this->_finish();
        if ($this->_tag != HL_TAG_PRE) {
            $this->_output = nl2br($this->_output);
            $this->_output = str_replace('  ', ' &nbsp;', $this->_output);
        }
        if ($this->_numbers == HL_NUMBERS_LI) {
            /* additional whitespace for browsers that do not display
            empty list items correctly */
            $this->_output = preg_replace('~^|\n~', "\n<li>&nbsp;", $this->_output);
            $this->_output = '<ol class="hl-main"><'.$this->_tag.'>' .
                             $this->_output . '</' . $this->_tag . '></ol>';
        } else {
            $this->_output = '<'.$this->_tag . ' class="hl-main">' .
                              $this->_output . '</'.$this->_tag .'>';
        }
        if ($this->_numbers == HL_NUMBERS_TABLE) {
            $numbers = '';
            $nlines = substr_count($this->_output,"\n")+1;
            for ($i=1; $i<=$nlines; $i++) {
                $numbers .= $i . "\n";
            }
            $this->_output = '<table class="hl-table" width="100%"><tr><td class="hl-gutter" align="right" valign="top" style="width:4ex;">' . 
                             '<pre>' . $numbers . '</pre></td><td class="hl-main" valign="top">' .
                             $this->_output . '</td></tr></table>';
        }
        return $this->_output;
    }

    
    /**
     * Adds next chunk to output
     *
     * @param  string $text  Text to output
     * @param  string $color Color group
     * @access private
     *
     */
    function _chunk($text,$color)
    {
        $text = htmlspecialchars($text);

        if ($color != $this->_lastcolor) {
            $colortag = '';
            if ($this->_output) {
                $colortag .= '</span>';
            }
            $colortag .= '<span class="hl-' . $color . '">';
            $this->_output .= $colortag;
        }

        // make coloring tags not cross the list item tags
        if ($this->_numbers == HL_NUMBERS_LI) {
            $colortag = "</span>\n<span class=\"hl-" . $color . '">';
            $text = str_replace("\n", $colortag, $text);
        }

        $this->_output .= $text;
        $this->_lastcolor = $color;
    }

    
    /**
     * Closes tags
     *
     * @access private
     *
     */
    function _finish()
    {
        if (!$this->_output) {
            return;
        }
        $this->_output .= '</span>';
    }

}
?>
Return current item: Content*Builder