1: <?php
2: /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3: /**
4: * Highlighter base class
5: *
6: * PHP versions 4 and 5
7: *
8: * LICENSE: This source file is subject to version 3.0 of the PHP license
9: * that is available through the world-wide-web at the following URI:
10: * http://www.php.net/license/3_0.txt. If you did not receive a copy of
11: * the PHP License and are unable to obtain it through the web, please
12: * send a note to license@php.net so we can mail you a copy immediately.
13: *
14: * @category Text
15: * @package Text_Highlighter
16: * @author Andrey Demenev <demenev@gmail.com>
17: * @copyright 2004-2006 Andrey Demenev
18: * @license http://www.php.net/license/3_0.txt PHP License
19: * @version CVS: $Id: Highlighter.php,v 1.1 2007/06/03 02:35:28 ssttoo Exp $
20: * @link http://pear.php.net/package/Text_Highlighter
21: */
22:
23: // {{{ BC constants
24:
25: // BC trick : define constants related to default
26: // renderer if needed
27: if (!defined('HL_NUMBERS_LI')) {
28: /**#@+
29: * Constant for use with $options['numbers']
30: * @see Text_Highlighter_Renderer_Html::_init()
31: */
32: /**
33: * use numbered list
34: */
35: define ('HL_NUMBERS_LI' , 1);
36: /**
37: * Use 2-column table with line numbers in left column and code in right column.
38: * Forces $options['tag'] = HL_TAG_PRE
39: */
40: define ('HL_NUMBERS_TABLE' , 2);
41: /**#@-*/
42: }
43:
44: // }}}
45: // {{{ constants
46: /**
47: * for our purpose, it is infinity
48: */
49: define ('HL_INFINITY', 1000000000);
50:
51: // }}}
52:
53: /**
54: * Text highlighter base class
55: *
56: * @author Andrey Demenev <demenev@gmail.com>
57: * @copyright 2004-2006 Andrey Demenev
58: * @license http://www.php.net/license/3_0.txt PHP License
59: * @version Release: 0.7.1
60: * @link http://pear.php.net/package/Text_Highlighter
61: */
62:
63: // {{{ Text_Highlighter
64:
65: /**
66: * Text highlighter base class
67: *
68: * This class implements all functions necessary for highlighting,
69: * but it does not contain highlighting rules. Actual highlighting is
70: * done using a descendent of this class.
71: *
72: * One is not supposed to manually create descendent classes.
73: * Instead, describe highlighting rules in XML format and
74: * use {@link Text_Highlighter_Generator} to create descendent class.
75: * Alternatively, an instance of a descendent class can be created
76: * directly.
77: *
78: * Use {@link Text_Highlighter::factory()} to create an
79: * object for particular language highlighter
80: *
81: * Usage example
82: * <code>
83: *require_once 'Text/Highlighter.php';
84: *$hlSQL =& Text_Highlighter::factory('SQL',array('numbers'=>true));
85: *echo $hlSQL->highlight('SELECT * FROM table a WHERE id = 12');
86: * </code>
87: *
88: * @author Andrey Demenev <demenev@gmail.com>
89: * @package Text_Highlighter
90: * @access public
91: */
92:
93: class Text_Highlighter
94: {
95: // {{{ members
96:
97: /**
98: * Syntax highlighting rules.
99: * Auto-generated classes set this var
100: *
101: * @access protected
102: * @see _init
103: * @var array
104: */
105: var $_syntax;
106:
107: /**
108: * Renderer object.
109: *
110: * @access private
111: * @var array
112: */
113: var $_renderer;
114:
115: /**
116: * Options. Keeped for BC
117: *
118: * @access protected
119: * @var array
120: */
121: var $_options = array();
122:
123: /**
124: * Conditionds
125: *
126: * @access protected
127: * @var array
128: */
129: var $_conditions = array();
130:
131: /**
132: * Disabled keywords
133: *
134: * @access protected
135: * @var array
136: */
137: var $_disabled = array();
138:
139: /**
140: * Language
141: *
142: * @access protected
143: * @var string
144: */
145: var $_language = '';
146:
147: // }}}
148: // {{{ _checkDefines
149:
150: /**
151: * Called by subclssses' constructors to enable/disable
152: * optional highlighter rules
153: *
154: * @param array $defines Conditional defines
155: *
156: * @access protected
157: */
158: function _checkDefines()
159: {
160: if (isset($this->_options['defines'])) {
161: $defines = $this->_options['defines'];
162: } else {
163: $defines = array();
164: }
165: foreach ($this->_conditions as $name => $actions) {
166: foreach($actions as $action) {
167: $present = in_array($name, $defines);
168: if (!$action[1]) {
169: $present = !$present;
170: }
171: if ($present) {
172: unset($this->_disabled[$action[0]]);
173: } else {
174: $this->_disabled[$action[0]] = true;
175: }
176: }
177: }
178: }
179:
180: // }}}
181: // {{{ factory
182:
183: /**
184: * Create a new Highlighter object for specified language
185: *
186: * @param string $lang language, for example "SQL"
187: * @param array $options Rendering options. This
188: * parameter is only keeped for BC reasons, use
189: * {@link Text_Highlighter::setRenderer()} instead
190: *
191: * @return mixed a newly created Highlighter object, or
192: * a PEAR error object on error
193: *
194: * @static
195: * @access public
196: */
197: public static function factory($lang, $options = array())
198: {
199: $lang = strtoupper($lang);
200: $langFile = dirname(__FILE__)."/Highlighter/$lang.php";
201: if (is_file($langFile))
202: include_once $langFile;
203: else
204: return false;
205:
206: $classname = 'Text_Highlighter_' . $lang;
207:
208: if (!class_exists($classname))
209: return false;
210:
211: return new $classname($options);
212: }
213:
214: // }}}
215: // {{{ setRenderer
216:
217: /**
218: * Set renderer object
219: *
220: * @param object $renderer Text_Highlighter_Renderer
221: *
222: * @access public
223: */
224: function setRenderer($renderer)
225: {
226: $this->_renderer = $renderer;
227: }
228:
229: // }}}
230:
231: /**
232: * Helper function to find matching brackets
233: *
234: * @access private
235: */
236: function _matchingBrackets($str)
237: {
238: return strtr($str, '()<>[]{}', ')(><][}{');
239: }
240:
241:
242:
243:
244: function _getToken()
245: {
246: if (!empty($this->_tokenStack)) {
247: return array_pop($this->_tokenStack);
248: }
249: if ($this->_pos >= $this->_len) {
250: return NULL;
251: }
252:
253: if ($this->_state != -1 && preg_match($this->_endpattern, $this->_str, $m, PREG_OFFSET_CAPTURE, $this->_pos)) {
254: $endpos = $m[0][1];
255: $endmatch = $m[0][0];
256: } else {
257: $endpos = -1;
258: }
259: preg_match ($this->_regs[$this->_state], $this->_str, $m, PREG_OFFSET_CAPTURE, $this->_pos);
260: $n = 1;
261:
262:
263: foreach ($this->_counts[$this->_state] as $i=>$count) {
264: if (!isset($m[$n])) {
265: break;
266: }
267: if ($m[$n][1]>-1 && ($endpos == -1 || $m[$n][1] < $endpos)) {
268: if ($this->_states[$this->_state][$i] != -1) {
269: $this->_tokenStack[] = array($this->_delim[$this->_state][$i], $m[$n][0]);
270: } else {
271: $inner = $this->_inner[$this->_state][$i];
272: if (isset($this->_parts[$this->_state][$i])) {
273: $parts = array();
274: $partpos = $m[$n][1];
275: for ($j=1; $j<=$count; $j++) {
276: if ($m[$j+$n][1] < 0) {
277: continue;
278: }
279: if (isset($this->_parts[$this->_state][$i][$j])) {
280: if ($m[$j+$n][1] > $partpos) {
281: array_unshift($parts, array($inner, substr($this->_str, $partpos, $m[$j+$n][1]-$partpos)));
282: }
283: array_unshift($parts, array($this->_parts[$this->_state][$i][$j], $m[$j+$n][0]));
284: }
285: $partpos = $m[$j+$n][1] + strlen($m[$j+$n][0]);
286: }
287: if ($partpos < $m[$n][1] + strlen($m[$n][0])) {
288: array_unshift($parts, array($inner, substr($this->_str, $partpos, $m[$n][1] - $partpos + strlen($m[$n][0]))));
289: }
290: $this->_tokenStack = array_merge($this->_tokenStack, $parts);
291: } else {
292: foreach ($this->_keywords[$this->_state][$i] as $g => $re) {
293: if (isset($this->_disabled[$g])) {
294: continue;
295: }
296: if (preg_match($re, $m[$n][0])) {
297: $inner = $this->_kwmap[$g];
298: break;
299: }
300: }
301: $this->_tokenStack[] = array($inner, $m[$n][0]);
302: }
303: }
304: if ($m[$n][1] > $this->_pos) {
305: $this->_tokenStack[] = array($this->_lastinner, substr($this->_str, $this->_pos, $m[$n][1]-$this->_pos));
306: }
307: $this->_pos = $m[$n][1] + strlen($m[$n][0]);
308: if ($this->_states[$this->_state][$i] != -1) {
309: $this->_stack[] = array($this->_state, $this->_lastdelim, $this->_lastinner, $this->_endpattern);
310: $this->_lastinner = $this->_inner[$this->_state][$i];
311: $this->_lastdelim = $this->_delim[$this->_state][$i];
312: $l = $this->_state;
313: $this->_state = $this->_states[$this->_state][$i];
314: $this->_endpattern = $this->_end[$this->_state];
315: if ($this->_subst[$l][$i]) {
316: for ($k=0; $k<=$this->_counts[$l][$i]; $k++) {
317: if (!isset($m[$i+$k])) {
318: break;
319: }
320: $quoted = preg_quote($m[$n+$k][0], '/');
321: $this->_endpattern = str_replace('%'.$k.'%', $quoted, $this->_endpattern);
322: $this->_endpattern = str_replace('%b'.$k.'%', $this->_matchingBrackets($quoted), $this->_endpattern);
323: }
324: }
325: }
326: return array_pop($this->_tokenStack);
327: }
328: $n += $count + 1;
329: }
330:
331: if ($endpos > -1) {
332: $this->_tokenStack[] = array($this->_lastdelim, $endmatch);
333: if ($endpos > $this->_pos) {
334: $this->_tokenStack[] = array($this->_lastinner, substr($this->_str, $this->_pos, $endpos-$this->_pos));
335: }
336: list($this->_state, $this->_lastdelim, $this->_lastinner, $this->_endpattern) = array_pop($this->_stack);
337: $this->_pos = $endpos + strlen($endmatch);
338: return array_pop($this->_tokenStack);
339: }
340: $p = $this->_pos;
341: $this->_pos = HL_INFINITY;
342: return array($this->_lastinner, substr($this->_str, $p));
343: }
344:
345:
346:
347:
348: // {{{ highlight
349:
350: /**
351: * Highlights code
352: *
353: * @param string $str Code to highlight
354: * @access public
355: * @return string Highlighted text
356: *
357: */
358:
359: function highlight($str)
360: {
361: if (!($this->_renderer)) {
362: include_once(dirname(__FILE__).'/Renderer/Html.php');
363: $this->_renderer = new Text_Highlighter_Renderer_Html($this->_options);
364: }
365: $this->_state = -1;
366: $this->_pos = 0;
367: $this->_stack = array();
368: $this->_tokenStack = array();
369: $this->_lastinner = $this->_defClass;
370: $this->_lastdelim = $this->_defClass;
371: $this->_endpattern = '';
372: $this->_renderer->reset();
373: $this->_renderer->setCurrentLanguage($this->_language);
374: $this->_str = $this->_renderer->preprocess($str);
375: $this->_len = strlen($this->_str);
376: while ($token = $this->_getToken()) {
377: $this->_renderer->acceptToken($token[0], $token[1]);
378: }
379: $this->_renderer->finalize();
380: return $this->_renderer->getOutput();
381: }
382:
383: // }}}
384:
385: }
386:
387: // }}}
388:
389: /*
390: * Local variables:
391: * tab-width: 4
392: * c-basic-offset: 4
393: * c-hanging-comment-ender-p: nil
394: * End:
395: */
396:
397: ?>
398: