import
[web.mtrack] / inc / hyperlight / hyperlight.php
1 <?php
2
3 /*
4  * Copyright 2008 Konrad Rudolph
5  * All rights reserved.
6  * 
7  * Permission is hereby granted, free of charge, to any person obtaining a copy
8  * of this software and associated documentation files (the "Software"), to deal
9  * in the Software without restriction, including without limitation the rights
10  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11  * copies of the Software, and to permit persons to whom the Software is
12  * furnished to do so, subject to the following conditions:
13  * 
14  * The above copyright notice and this permission notice shall be included in
15  * all copies or substantial portions of the Software.
16  * 
17  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23  * THE SOFTWARE.
24  */
25
26 /*
27  * TODO list
28  * =========
29  *
30  * - FIXME Nested syntax elements create redundant nested tags under certain
31  *   circumstances. This can be reproduced by the following PHP snippet:
32  *
33  *      <pre class="<?php echo; ? >">
34  *
35  *   (Remove space between `?` and `>`).
36  *   Although this no longer occurs, it is fixed by checking for `$token === ''`
37  *   in the `emit*` methods. This should never happen anyway. Probably something
38  *   to do with the zero-width lookahead in the PHP syntax definition.
39  *
40  * - `hyperlight_calculate_fold_marks`: refactor, write proper handler
41  *
42  * - Line numbers (on client-side?)
43  *
44  */
45
46 /**
47  * Hyperlight source code highlighter for PHP.
48  * @package hyperlight
49  */
50
51 /** @ignore */
52 require_once dirname(__FILE__) . '/preg_helper.php';
53
54 if (!function_exists('array_peek')) {
55     /**
56      * @internal
57      * This does exactly what you think it does. */
58     function array_peek(array &$array) {
59         $cnt = count($array);
60         return $cnt === 0 ? null : $array[$cnt - 1];
61     }
62 }
63
64 /**
65  * @internal
66  * For internal debugging purposes.
67  */
68 function dump($obj, $descr = null) {
69     if ($descr !== null)
70         echo "<h3>$descr</h3>";
71     ob_start();
72     var_dump($obj);
73     $dump = ob_get_clean();
74     ?><pre><?php echo htmlspecialchars($dump); ?></pre><?php
75     return true;
76 }
77
78 /**
79  * Raised when the grammar offers a rule that has not been defined.
80  */
81 class NoMatchingRuleException extends Exception {
82     /** @internal */
83     public function __construct($states, $position, $code) {
84         $state = array_pop($states);
85         parent::__construct(
86             "State '$state' has no matching rule at position $position:\n" .
87             $this->errorSurrounding($code, $position)
88         );
89     }
90
91     // Try to extract the location of the error more or less precisely.
92     // Only used for a comprehensive display.
93     private function errorSurrounding($code, $pos) {
94         $size = 10;
95         $begin = $pos < $size ? 0 : $pos - $size;
96         $end = $pos + $size > strlen($code) ? strlen($code) : $pos + $size;
97         $offs = $pos - $begin;
98         return substr($code, $begin, $end - $begin) . "\n" . sprintf("%{$offs}s", '^');
99     }
100 }
101
102 /**
103  * Represents a nesting rule in the grammar of a language definition.
104  *
105  * Individual rules can either be represented by raw strings ("simple" rules) or
106  * by a nesting rule. Nesting rules specify where they can start and end. Inside
107  * a nesting rule, other rules may be applied (both simple and nesting).
108  * For example, a nesting rule may define a string literal. Inside that string,
109  * other rules may be applied that recognize escape sequences.
110  *
111  * To use a nesting rule, supply how it may start and end, e.g.:
112  * <code>
113  * $string_rule = array('string' => new Rule('/"/', '/"/'));
114  * </code>
115  * You also need to specify nested states:
116  * <code>
117  * $string_states = array('string' => 'escaped');
118  * <code>
119  * Now you can add another rule for <var>escaped</var>:
120  * <code>
121  * $escaped_rule = array('escaped' => '/\\(x\d{1,4}|.)/');
122  * </code>
123  */
124 class Rule {
125     /**
126      * Common rules.
127      */
128
129     const ALL_WHITESPACE = '/(\s|\r|\n)+/';
130     const C_IDENTIFIER = '/[a-z_][a-z0-9_]*/i';
131     const C_COMMENT = '#//.*?\n|/\*.*?\*/#s';
132     const C_MULTILINECOMMENT = '#/\*.*?\*/#s';
133     const DOUBLEQUOTESTRING = '/"(?:\\\\"|.)*?"/s';
134     const SINGLEQUOTESTRING = "/'(?:\\\\'|.)*?'/s";
135     const C_DOUBLEQUOTESTRING = '/L?"(?:\\\\"|.)*?"/s';
136     const C_SINGLEQUOTESTRING = "/L?'(?:\\\\'|.)*?'/s";
137     const STRING = '/"(?:\\\\"|.)*?"|\'(?:\\\\\'|.)*?\'/s';
138     const C_NUMBER = '/
139         (?: # Integer followed by optional fractional part.
140             (?:
141                 0(?:
142                     x[0-9a-f]+
143                     |
144                     [0-7]*
145                 )
146                 |
147                 \d+
148             )
149             (?:\.\d*)?
150             (?:e[+-]\d+)?
151         )
152         |
153         (?: # Just the fractional part.
154             (?:\.\d+)
155             (?:e[+-]?\d+)?
156         )
157         /ix';
158
159     private $_start;
160     private $_end;
161
162     /** @ignore */
163     public function __construct($start, $end = null) {
164         $this->_start = $start;
165         $this->_end = $end;
166     }
167
168     /**
169      * Returns the pattern with which this rule starts.
170      * @return string
171      */
172     public function start() {
173         return $this->_start;
174     }
175
176     /**
177      * Returns the pattern with which this rule may end.
178      * @return string
179      */
180     public function end() {
181         return $this->_end;
182     }
183 }
184
185 /**
186  * Abstract base class of all Hyperlight language definitions.
187  *
188  * In order to define a new language definition, this class is inherited.
189  * The only function that needs to be overridden is the constructor. Helper
190  * functions from the base class can then be called to construct the grammar
191  * and store additional information.
192  * The name of the subclass must be of the schema <var>{Lang}Language</var>,
193  * where <var>{Lang}</var> is a short, unique name for the language starting
194  * with a capital letter and continuing in lower case. For example,
195  * <var>PhpLanguage</var> is a valid name. The language definition must
196  * reside in a file located at <var>languages/{lang}.php</var>. Here,
197  * <var>{lang}</var> is the all-lowercase spelling of the name, e.g.
198  * <var>languages/php.php</var>.
199  *
200  */
201 abstract class HyperLanguage {
202     private $_states = array();
203     private $_rules = array();
204     private $_mappings = array();
205     private $_info = array();
206     private $_extensions = array();
207     private $_caseInsensitive = false;
208     private $_postProcessors = array();
209
210     private static $_languageCache = array();
211     private static $_compiledLanguageCache = array();
212     private static $_filetypes;
213
214     /**
215      * Indices for information.
216      */
217
218     const NAME = 1;
219     const VERSION = 2;
220     const AUTHOR = 10;
221     const WEBSITE = 5;
222     const EMAIL = 6;
223
224     /**
225      * Retrieves a language definition name based on a file extension.
226      *
227      * Uses the contents of the <var>languages/filetypes</var> file to
228      * guess the language definition name from a file name extension.
229      * This file has to be generated using the
230      * <var>collect-filetypes.php</var> script every time the language
231      * definitions have been changed.
232      *
233      * @param string $ext the file name extension.
234      * @return string The language definition name or <var>NULL</var>.
235      */
236     public static function nameFromExt($ext) {
237         if (self::$_filetypes === null) {
238             $ft_content = file('languages/filetypes', 1);
239
240             foreach ($ft_content as $line) {
241                 list ($name, $extensions) = explode(':', trim($line));
242                 $extensions = explode(',', $extensions);
243                 // Inverse lookup.
244                 foreach ($extensions as $extension)
245                     $ft_data[$extension] = $name;
246             }
247             self::$_filetypes = $ft_data;
248         }
249         $ext = strtolower($ext);
250         return
251             array_key_exists($ext, self::$_filetypes) ?
252             self::$_filetypes[strtolower($ext)] : null;
253     }
254
255     public static function compile(HyperLanguage $lang) {
256         $id = $lang->id();
257         if (!isset(self::$_compiledLanguageCache[$id]))
258             self::$_compiledLanguageCache[$id] = $lang->makeCompiledLanguage();
259         return self::$_compiledLanguageCache[$id];
260     }
261
262     public static function compileFromName($lang) {
263         return self::compile(self::fromName($lang));
264     }
265
266     protected static function exists($lang) {
267         return isset(self::$_languageCache[$lang]) or
268                file_exists("languages/$lang.php");
269     }
270
271     protected static function fromName($lang) {
272         if (!isset(self::$_languageCache[$lang])) {
273                         require_once dirname(__FILE__) . "/$lang.php";
274             $klass = ucfirst("{$lang}Language");
275             self::$_languageCache[$lang] = new $klass();
276         }
277         return self::$_languageCache[$lang];
278     }
279
280     public function id() {
281         $klass = get_class($this);
282         return strtolower(substr($klass, 0, strlen($klass) - strlen('Language')));
283     }
284
285     protected function setCaseInsensitive($value) {
286         $this->_caseInsensitive = $value;
287     }
288
289     protected function addStates(array $states) {
290         $this->_states = self::mergeProperties($this->_states, $states);
291     }
292
293     protected function getState($key) {
294         return $this->_states[$key];
295     }
296
297     protected function removeState($key) {
298         unset($this->_states[$key]);
299     }
300
301     protected function addRules(array $rules) {
302         $this->_rules = self::mergeProperties($this->_rules, $rules);
303     }
304
305     protected function getRule($key) {
306         return $this->_rules[$key];
307     }
308
309     protected function removeRule($key) {
310         unset($this->_rules[$key]);
311     }
312
313     protected function addMappings(array $mappings) {
314         // TODO Implement nested mappings.
315         $this->_mappings = array_merge($this->_mappings, $mappings);
316     }
317
318     protected function getMapping($key) {
319         return $this->_mappings[$key];
320     }
321
322     protected function removeMapping($key) {
323         unset($this->_mappings[$key]);
324     }
325
326     protected function setInfo(array $info) {
327         $this->_info = $info;
328     }
329
330     protected function setExtensions(array $extensions) {
331         $this->_extensions = $extensions;
332     }
333
334     protected function addPostprocessing($rule, HyperLanguage $language) {
335         $this->_postProcessors[$rule] = $language;
336     }
337
338 //    protected function addNestedLanguage(HyperLanguage $language, $hoistBackRules) {
339 //        $prefix = get_class($language);
340 //        if (!is_array($hoistBackRules))
341 //            $hoistBackRules = array($hoistBackRules);
342 //
343 //        $states = array();  // Step 1: states
344 //
345 //        foreach ($language->_states as $stateName => $state) {
346 //            $prefixedRules = array();
347 //
348 //            if (strstr($stateName, ' ')) {
349 //                $parts = explode(' ', $stateName);
350 //                $prefixed = array();
351 //                foreach ($parts as $part)
352 //                    $prefixed[] = "$prefix$part";
353 //                $stateName = implode(' ', $prefixed);
354 //            }
355 //            else
356 //                $stateName = "$prefix$stateName";
357 //
358 //            foreach ($state as $key => $rule) {
359 //                if (is_string($key) and is_array($rule)) {
360 //                    $nestedRules = array();
361 //                    foreach ($rule as $nestedRule)
362 //                        $nestedRules[] = ($nestedRule === '') ? '' :
363 //                                         "$prefix$nestedRule";
364 //
365 //                    $prefixedRules["$prefix$key"] = $nestedRules;
366 //                }
367 //                else
368 //                    $prefixedRules[] = "$prefix$rule";
369 //            }
370 //
371 //            if ($stateName === 'init')
372 //                $prefixedRules = array_merge($hoistBackRules, $prefixedRules);
373 //
374 //            $states[$stateName] = $prefixedRules;
375 //        }
376 //
377 //        $rules = array();   // Step 2: rules
378 //        // Mappings need to set up already!
379 //        $mappings = array();
380 //
381 //        foreach ($language->_rules as $ruleName => $rule) {
382 //            if (is_array($rule)) {
383 //                $nestedRules = array();
384 //                foreach ($rule as $nestedName => $nestedRule) {
385 //                    if (is_string($nestedName)) {
386 //                        $nestedRules["$prefix$nestedName"] = $nestedRule;
387 //                        $mappings["$prefix$nestedName"] = $nestedName;
388 //                    }
389 //                    else
390 //                        $nestedRules[] = $nestedRule;
391 //                }
392 //                $rules["$prefix$ruleName"] = $nestedRules;
393 //            }
394 //            else {
395 //                $rules["$prefix$ruleName"] = $rule;
396 //                $mappings["$prefix$ruleName"] = $ruleName;
397 //            }
398 //        }
399 //
400 //        // Step 3: mappings.
401 //
402 //        foreach ($language->_mappings as $ruleName => $cssClass) {
403 //            if (strstr($ruleName, ' ')) {
404 //                $parts = explode(' ', $ruleName);
405 //                $prefixed = array();
406 //                foreach ($parts as $part)
407 //                    $prefixed[] = "$prefix$part";
408 //                $mappings[implode(' ', $prefixed)] = $cssClass;
409 //            }
410 //            else
411 //                $mappings["$prefix$ruleName"] = $cssClass;
412 //        }
413 //
414 //        $this->addStates($states);
415 //        $this->addRules($rules);
416 //        $this->addMappings($mappings);
417 //
418 //        return $prefix . 'init';
419 //    }
420
421     private function makeCompiledLanguage() {
422         return new HyperlightCompiledLanguage(
423             $this->id(),
424             $this->_info,
425             $this->_extensions,
426             $this->_states,
427             $this->_rules,
428             $this->_mappings,
429             $this->_caseInsensitive,
430             $this->_postProcessors
431         );
432     }
433
434     private static function mergeProperties(array $old, array $new) {
435         foreach ($new as $key => $value) {
436             if (is_string($key)) {
437                 if (isset($old[$key]) and is_array($old[$key]))
438                     $old[$key] = array_merge($old[$key], $new);
439                 else
440                     $old[$key] = $value;
441             }
442             else
443                 $old[] = $value;
444         }
445
446         return $old;
447     }
448 }
449
450 class HyperlightCompiledLanguage {
451     private $_id;
452     private $_info;
453     private $_extensions;
454     private $_states;
455     private $_rules;
456     private $_mappings;
457     private $_caseInsensitive;
458     private $_postProcessors = array();
459
460     public function __construct($id, $info, $extensions, $states, $rules, $mappings, $caseInsensitive, $postProcessors) {
461         $this->_id = $id;
462         $this->_info = $info;
463         $this->_extensions = $extensions;
464         $this->_caseInsensitive = $caseInsensitive;
465         $this->_states = $this->compileStates($states);
466         $this->_rules = $this->compileRules($rules);
467         $this->_mappings = $mappings;
468
469         foreach ($postProcessors as $ppkey => $ppvalue)
470             $this->_postProcessors[$ppkey] = HyperLanguage::compile($ppvalue);
471     }
472
473     public function id() {
474         return $this->_id;
475     }
476
477     public function name() {
478         return $this->_info[HyperLanguage::NAME];
479     }
480
481     public function authorName() {
482         if (!array_key_exists(HyperLanguage::AUTHOR, $this->_info))
483             return null;
484         $author = $this->_info[HyperLanguage::AUTHOR];
485         if (is_string($author))
486             return $author;
487         if (!array_key_exists(HyperLanguage::NAME, $author))
488             return null;
489         return $author[HyperLanguage::NAME];
490     }
491
492     public function authorWebsite() {
493         if (!array_key_exists(HyperLanguage::AUTHOR, $this->_info) or
494             !is_array($this->_info[HyperLanguage::AUTHOR]) or
495             !array_key_exists(HyperLanguage::WEBSITE, $this->_info[HyperLanguage::AUTHOR]))
496             return null;
497         return $this->_info[HyperLanguage::AUTHOR][HyperLanguage::WEBSITE];
498     }
499
500     public function authorEmail() {
501         if (!array_key_exists(HyperLanguage::AUTHOR, $this->_info) or
502             !is_array($this->_info[HyperLanguage::AUTHOR]) or
503             !array_key_exists(HyperLanguage::EMAIL, $this->_info[HyperLanguage::AUTHOR]))
504             return null;
505         return $this->_info[HyperLanguage::AUTHOR][HyperLanguage::EMAIL];
506     }
507
508     public function authorContact() {
509         $email = $this->authorEmail();
510         return $email !== null ? $email : $this->authorWebsite();
511     }
512
513     public function extensions() {
514         return $this->_extensions;
515     }
516
517     public function state($stateName) {
518         return $this->_states[$stateName];
519     }
520
521     public function rule($ruleName) {
522         return $this->_rules[$ruleName];
523     }
524
525     public function className($state) {
526         if (array_key_exists($state, $this->_mappings))
527             return $this->_mappings[$state];
528         else if (strstr($state, ' ') === false)
529             // No mapping for state.
530             return $state;
531         else {
532             // Try mapping parts of nested state name.
533             $parts = explode(' ', $state);
534             $ret = array();
535
536             foreach ($parts as $part) {
537                 if (array_key_exists($part, $this->_mappings))
538                     $ret[] = $this->_mappings[$part];
539                 else
540                     $ret[] = $part;
541             }
542
543             return implode(' ', $ret);
544         }
545     }
546
547     public function postProcessors() {
548         return $this->_postProcessors;
549     }
550
551     private function compileStates($states) {
552         $ret = array();
553
554         foreach ($states as $name => $state) {
555             $newstate = array();
556
557             if (!is_array($state))
558                 $state = array($state);
559
560             foreach ($state as $key => $elem) {
561                 if ($elem === null)
562                     continue;
563                 if (is_string($key)) {
564                     if (!is_array($elem))
565                         $elem = array($elem);
566
567                     foreach ($elem as $el2) {
568                         if ($el2 === '')
569                             $newstate[] = $key;
570                         else
571                             $newstate[] = "$key $el2";
572                     }
573                 }
574                 else
575                     $newstate[] = $elem;
576             }
577
578             $ret[$name] = $newstate;
579         }
580
581         return $ret;
582     }
583
584     private function compileRules($rules) {
585         $tmp = array();
586
587         // Preprocess keyword list and flatten nested lists:
588
589         // End of regular expression matching keywords.
590         $end = $this->_caseInsensitive ? ')\b/i' : ')\b/';
591
592         foreach ($rules as $name => $rule) {
593             if (is_array($rule)) {
594                 if (self::isAssocArray($rule)) {
595                     // Array is a nested list of rules.
596                     foreach ($rule as $key => $value) {
597                         if (is_array($value))
598                             // Array represents a list of keywords.
599                             $value = '/\b(?:' . implode('|', $value) . $end;
600
601                         if (!is_string($key) or strlen($key) === 0)
602                             $tmp[$name] = $value;
603                         else
604                             $tmp["$name $key"] = $value;
605                     }
606                 }
607                 else {
608                     // Array represents a list of keywords.
609                     $rule = '/\b(?:' . implode('|', $rule) . $end;
610                     $tmp[$name] = $rule;
611                 }
612             }
613             else {
614                 $tmp[$name] = $rule;
615             } // if (is_array($rule))
616         } // foreach
617
618         $ret = array();
619
620         foreach ($this->_states as $name => $state) {
621             $regex_rules = array();
622             $regex_names = array();
623             $nesting_rules = array();
624
625             foreach ($state as $rule_name) {
626                 $rule = $tmp[$rule_name];
627                 if ($rule instanceof Rule)
628                     $nesting_rules[$rule_name] = $rule;
629                 else {
630                     $regex_rules[] = $rule;
631                     $regex_names[] = $rule_name;
632                 }
633             }
634
635             $ret[$name] = array_merge(
636                 array(preg_merge('|', $regex_rules, $regex_names)),
637                 $nesting_rules
638             );
639         }
640
641         return $ret;
642     }
643
644     private static function isAssocArray(array $array) {
645         foreach($array as $key => $_)
646             if (is_string($key))
647                 return true;
648         return false;
649     }
650 }
651
652 class Hyperlight {
653     private $_lang;
654     private $_result;
655     private $_states;
656     private $_omitSpans;
657     private $_postProcessors = array();
658
659     public function __construct($lang) {
660         if (is_string($lang))
661             $this->_lang = HyperLanguage::compileFromName(strtolower($lang));
662         else if ($lang instanceof HyperlightCompiledLanguage)
663             $this->_lang = $lang;
664         else if ($lang instanceof HyperLanguage)
665             $this->_lang = HyperLanguage::compile($lang);
666         else
667             trigger_error(
668                 'Invalid argument type for $lang to Hyperlight::__construct',
669                 E_USER_ERROR
670             );
671
672         foreach ($this->_lang->postProcessors() as $ppkey => $ppvalue)
673             $this->_postProcessors[$ppkey] = new Hyperlight($ppvalue);
674
675         $this->reset();
676     }
677
678     public function language() {
679         return $this->_lang;
680     }
681
682     public function reset() {
683         $this->_states = array('init');
684         $this->_omitSpans = array();
685     }
686
687     public function render($code) {
688         // Normalize line breaks.
689         $this->_code = preg_replace('/\r\n?/', "\n", $code);
690         $fm = hyperlight_calculate_fold_marks($this->_code, $this->language()->id());
691         return hyperlight_apply_fold_marks($this->renderCode(), $fm);
692     }
693
694     public function renderAndPrint($code) {
695         echo $this->render($code);
696     }
697
698
699     private function renderCode() {
700         $code = $this->_code;
701         $pos = 0;
702         $len = strlen($code);
703         $this->_result = '';
704         $state = array_peek($this->_states);
705
706         // If there are open states (reentrant parsing), open the corresponding
707         // tags first:
708
709         for ($i = 1; $i < count($this->_states); ++$i)
710             if (!$this->_omitSpans[$i - 1]) {
711                 $class = $this->_lang->className($this->_states[$i]);
712                 $this->write("<span class=\"$class\">");
713             }
714
715         // Emergency break to catch faulty rules.
716         $prev_pos = -1;
717
718         while ($pos < $len) {
719             // The token next to the current position, after the inner loop completes.
720             // i.e. $closest_hit = array($matched_text, $position)
721             $closest_hit = array('', $len);
722             // The rule that found this token.
723             $closest_rule = null;
724             $rules = $this->_lang->rule($state);
725
726             foreach ($rules as $name => $rule) {
727                 if ($rule instanceof Rule)
728                     $this->matchIfCloser(
729                         $rule->start(), $name, $pos, $closest_hit, $closest_rule
730                     );
731                 else if (preg_match($rule, $code, $matches, PREG_OFFSET_CAPTURE, $pos) == 1) {
732                     // Search which of the sub-patterns matched.
733
734                     foreach ($matches as $group => $match) {
735                         if (!is_string($group))
736                             continue;
737                         if ($match[1] !== -1) {
738                             $closest_hit = $match;
739                             $closest_rule = str_replace('_', ' ', $group);
740                             break;
741                         }
742                     }
743                 }
744             } // foreach ($rules)
745
746             // If we're currently inside a rule, check whether we've come to the
747             // end of it, or the end of any other rule we're nested in.
748
749             if (count($this->_states) > 1) {
750                 $n = count($this->_states) - 1;
751                 do {
752                     $rule = $this->_lang->rule($this->_states[$n - 1]);
753                     $rule = $rule[$this->_states[$n]];
754                     --$n;
755                     if ($n < 0)
756                         throw new NoMatchingRuleException($this->_states, $pos, $code);
757                 } while ($rule->end() === null);
758
759                 $this->matchIfCloser($rule->end(), $n + 1, $pos, $closest_hit, $closest_rule);
760             }
761
762             // We take the closest hit:
763
764             if ($closest_hit[1] > $pos)
765                 $this->emit(substr($code, $pos, $closest_hit[1] - $pos));
766
767             $prev_pos = $pos;
768             $pos = $closest_hit[1] + strlen($closest_hit[0]);
769
770             if ($prev_pos === $pos and is_string($closest_rule))
771                 if (array_key_exists($closest_rule, $this->_lang->rule($state))) {
772                     array_push($this->_states, $closest_rule);
773                     $state = $closest_rule;
774                     $this->emitPartial('', $closest_rule);
775                 }
776
777             if ($closest_hit[1] === $len)
778                 break;
779             else if (!is_string($closest_rule)) {
780                 // Pop state.
781                 if (count($this->_states) <= $closest_rule)
782                     throw new NoMatchingRuleException($this->_states, $pos, $code);
783
784                 while (count($this->_states) > $closest_rule + 1) {
785                     $lastState = array_pop($this->_states);
786                     $this->emitPop('', $lastState);
787                 }
788                 $lastState = array_pop($this->_states);
789                 $state = array_peek($this->_states);
790                 $this->emitPop($closest_hit[0], $lastState);
791             }
792             else if (array_key_exists($closest_rule, $this->_lang->rule($state))) {
793                 // Push state.
794                 array_push($this->_states, $closest_rule);
795                 $state = $closest_rule;
796                 $this->emitPartial($closest_hit[0], $closest_rule);
797             }
798             else
799                 $this->emit($closest_hit[0], $closest_rule);
800         } // while ($pos < $len)
801
802         // Close any tags that are still open (can happen in incomplete code
803         // fragments that don't necessarily signify an error (consider PHP
804         // embedded in HTML, or a C++ preprocessor code not ending on newline).
805         
806         $omitSpansBackup = $this->_omitSpans;
807         for ($i = count($this->_states); $i > 1; --$i)
808             $this->emitPop();
809         $this->_omitSpans = $omitSpansBackup;
810
811         return $this->_result;
812     }
813
814     private function matchIfCloser($expr, $next, $pos, &$closest_hit, &$closest_rule) {
815         $matches = array();
816         if (preg_match($expr, $this->_code, $matches, PREG_OFFSET_CAPTURE, $pos) == 1) {
817             if (
818                 (
819                     // Two hits at same position -- compare length
820                     // For equal lengths: first come, first serve.
821                     $matches[0][1] == $closest_hit[1] and
822                     strlen($matches[0][0]) > strlen($closest_hit[0])
823                 ) or
824                 $matches[0][1] < $closest_hit[1]
825             ) {
826                 $closest_hit = $matches[0];
827                 $closest_rule = $next;
828             }
829         }
830     }
831
832     private function processToken($token) {
833         if ($token === '')
834             return '';
835         $nest_lang = array_peek($this->_states);
836         if (array_key_exists($nest_lang, $this->_postProcessors))
837             return $this->_postProcessors[$nest_lang]->render($token);
838         else
839             #return self::htmlentities($token);
840             return htmlspecialchars($token, ENT_NOQUOTES);
841     }
842
843     private function emit($token, $class = '') {
844         $token = $this->processToken($token);
845         if ($token === '')
846             return;
847         $class = $this->_lang->className($class);
848         if ($class === '')
849             $this->write($token);
850         else
851             $this->write("<span class=\"$class\">$token</span>");
852     }
853
854     private function emitPartial($token, $class) {
855         $token = $this->processToken($token);
856         $class = $this->_lang->className($class);
857         if ($class === '') {
858             if ($token !== '')
859                 $this->write($token);
860             array_push($this->_omitSpans, true);
861         }
862         else {
863             $this->write("<span class=\"$class\">$token");
864             array_push($this->_omitSpans, false);
865         }
866     }
867
868     private function emitPop($token = '', $class = '') {
869         $token = $this->processToken($token);
870         if (array_pop($this->_omitSpans))
871             $this->write($token);
872         else
873             $this->write("$token</span>");
874     }
875
876     private function write($text) {
877         $this->_result .= $text;
878     }
879
880 //      // DAMN! What did I need them for? Something to do with encoding …
881 //      // but why not use the `$charset` argument on `htmlspecialchars`?
882 //    private static function htmlentitiesCallback($match) {
883 //        switch ($match[0]) {
884 //            case '<': return '&lt;';
885 //            case '>': return '&gt;';
886 //            case '&': return '&amp;';
887 //        }
888 //    }
889 //
890 //    private static function htmlentities($text) {
891 //        return htmlspecialchars($text, ENT_NOQUOTES);
892 //        return preg_replace_callback(
893 //            '/[<>&]/', array('Hyperlight', 'htmlentitiesCallback'), $text
894 //        );
895 //    }
896 } // class Hyperlight
897
898 /**
899  * <var>echo</var>s a highlighted code.
900  *
901  * For example, the following
902  * <code>
903  * hyperlight('<?php echo \'Hello, world\'; ?>', 'php');
904  * </code>
905  * results in:
906  * <code>
907  * <pre class="source-code php">...</pre>
908  * </code>
909  *
910  * @param string $code The code.
911  * @param string $lang The language of the code.
912  * @param string $tag The surrounding tag to use. Optional.
913  * @param array $attributes Attributes to decorate {@link $tag} with.
914  *          If no tag is given, this argument can be passed in its place. This
915  *          behaviour will be assumed if the third argument is an array.
916  *          Attributes must be given as a hash of key value pairs.
917  */
918 function hyperlight($code, $lang, $tag = 'pre', array $attributes = array()) {
919     if ($code == '')
920         die("`hyperlight` needs a code to work on!");
921     if ($lang == '')
922         die("`hyperlight` needs to know the code's language!");
923     if (is_array($tag) and !empty($attributes))
924         die("Can't pass array arguments for \$tag *and* \$attributes to `hyperlight`!");
925     if ($tag == '')
926         $tag = 'pre';
927     if (is_array($tag)) {
928         $attributes = $tag;
929         $tag = 'pre';
930     }
931     $lang = htmlspecialchars(strtolower($lang));
932     $class = "source-code $lang";
933
934     $attr = array();
935     foreach ($attributes as $key => $value) {
936         if ($key == 'class')
937             $class .= ' ' . htmlspecialchars($value);
938         else
939             $attr[] = htmlspecialchars($key) . '="' .
940                       htmlspecialchars($value) . '"';
941     }
942
943     $attr = empty($attr) ? '' : ' ' . implode(' ', $attr);
944
945     $hl = new Hyperlight($lang);
946     echo "<$tag class=\"$class\"$attr>";
947     $hl->renderAndPrint(trim($code));
948     echo "</$tag>";
949 }
950
951 /**
952  * Is the same as:
953  * <code>
954  * hyperlight(file_get_contents($filename), $lang, $tag, $attributes);
955  * </code>
956  * @see hyperlight()
957  */
958 function hyperlight_file($filename, $lang = null, $tag = 'pre', array $attributes = array()) {
959     if ($lang == '') {
960         // Try to guess it from file extension.
961         $pos = strrpos($filename, '.');
962         if ($pos !== false) {
963             $ext = substr($filename, $pos + 1);
964             $lang = HyperLanguage::nameFromExt($ext);
965         }
966     }
967     hyperlight(file_get_contents($filename), $lang, $tag, $attributes);
968 }
969
970 if (defined('HYPERLIGHT_SHORTCUT')) {
971     function hy() {
972         $args = func_get_args();
973         call_user_func_array('hyperlight', $args);
974     }
975     function hyf() {
976         $args = func_get_args();
977         call_user_func_array('hyperlight_file', $args);
978     }
979 }
980
981 function hyperlight_calculate_fold_marks($code, $lang) {
982     $supporting_languages = array('csharp', 'vb');
983
984     if (!in_array($lang, $supporting_languages))
985         return array();
986
987     $fold_begin_marks = array('/^\s*#Region/', '/^\s*#region/');
988     $fold_end_marks = array('/^\s*#End Region/', '/\s*#endregion/');
989
990     $lines = preg_split('/\r|\n|\r\n/', $code);
991
992     $fold_begin = array();
993     foreach ($fold_begin_marks as $fbm)
994         $fold_begin = $fold_begin + preg_grep($fbm, $lines);
995
996     $fold_end = array();
997     foreach ($fold_end_marks as $fem)
998         $fold_end = $fold_end + preg_grep($fem, $lines);
999
1000     if (count($fold_begin) !== count($fold_end) or count($fold_begin) === 0)
1001         return array();
1002
1003     $fb = array();
1004     $fe = array();
1005     foreach ($fold_begin as $line => $_)
1006         $fb[] = $line;
1007
1008     foreach ($fold_end as $line => $_)
1009         $fe[] = $line;
1010
1011     $ret = array();
1012     for ($i = 0; $i < count($fb); $i++)
1013         $ret[$fb[$i]] = $fe[$i];
1014
1015     return $ret;
1016 }
1017
1018 function hyperlight_apply_fold_marks($code, array $fold_marks) {
1019     if ($fold_marks === null or count($fold_marks) === 0)
1020         return $code;
1021
1022     $lines = explode("\n", $code);
1023
1024     foreach ($fold_marks as $begin => $end) {
1025         $lines[$begin] = '<span class="fold-header">' . $lines[$begin] . '<span class="dots"> </span></span>';
1026         $lines[$begin + 1] = '<span class="fold">' . $lines[$begin + 1];
1027         $lines[$end + 1] = '</span>' . $lines[$end + 1];
1028     }
1029
1030     return implode("\n", $lines);
1031 }
1032
1033 ?>