<?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(' ', ' ', $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> ", $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>';
}
}
?>