4 * Copyright 2008 Konrad Rudolph
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:
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
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
30 * - FIXME Nested syntax elements create redundant nested tags under certain
31 * circumstances. This can be reproduced by the following PHP snippet:
33 * <pre class="<?php echo; ? >">
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.
40 * - `hyperlight_calculate_fold_marks`: refactor, write proper handler
42 * - Line numbers (on client-side?)
47 * Hyperlight source code highlighter for PHP.
52 require_once dirname(__FILE__) . '/preg_helper.php';
54 if (!function_exists('array_peek')) {
57 * This does exactly what you think it does. */
58 function array_peek(array &$array) {
60 return $cnt === 0 ? null : $array[$cnt - 1];
66 * For internal debugging purposes.
68 function dump($obj, $descr = null) {
70 echo "<h3>$descr</h3>";
73 $dump = ob_get_clean();
74 ?><pre><?php echo htmlspecialchars($dump); ?></pre><?php
79 * Raised when the grammar offers a rule that has not been defined.
81 class NoMatchingRuleException extends Exception {
83 public function __construct($states, $position, $code) {
84 $state = array_pop($states);
86 "State '$state' has no matching rule at position $position:\n" .
87 $this->errorSurrounding($code, $position)
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) {
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", '^');
103 * Represents a nesting rule in the grammar of a language definition.
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.
111 * To use a nesting rule, supply how it may start and end, e.g.:
113 * $string_rule = array('string' => new Rule('/"/', '/"/'));
115 * You also need to specify nested states:
117 * $string_states = array('string' => 'escaped');
119 * Now you can add another rule for <var>escaped</var>:
121 * $escaped_rule = array('escaped' => '/\\(x\d{1,4}|.)/');
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';
139 (?: # Integer followed by optional fractional part.
153 (?: # Just the fractional part.
163 public function __construct($start, $end = null) {
164 $this->_start = $start;
169 * Returns the pattern with which this rule starts.
172 public function start() {
173 return $this->_start;
177 * Returns the pattern with which this rule may end.
180 public function end() {
186 * Abstract base class of all Hyperlight language definitions.
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>.
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();
210 private static $_languageCache = array();
211 private static $_compiledLanguageCache = array();
212 private static $_filetypes;
215 * Indices for information.
225 * Retrieves a language definition name based on a file extension.
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.
233 * @param string $ext the file name extension.
234 * @return string The language definition name or <var>NULL</var>.
236 public static function nameFromExt($ext) {
237 if (self::$_filetypes === null) {
238 $ft_content = file('languages/filetypes', 1);
240 foreach ($ft_content as $line) {
241 list ($name, $extensions) = explode(':', trim($line));
242 $extensions = explode(',', $extensions);
244 foreach ($extensions as $extension)
245 $ft_data[$extension] = $name;
247 self::$_filetypes = $ft_data;
249 $ext = strtolower($ext);
251 array_key_exists($ext, self::$_filetypes) ?
252 self::$_filetypes[strtolower($ext)] : null;
255 public static function compile(HyperLanguage $lang) {
257 if (!isset(self::$_compiledLanguageCache[$id]))
258 self::$_compiledLanguageCache[$id] = $lang->makeCompiledLanguage();
259 return self::$_compiledLanguageCache[$id];
262 public static function compileFromName($lang) {
263 return self::compile(self::fromName($lang));
266 protected static function exists($lang) {
267 return isset(self::$_languageCache[$lang]) or
268 file_exists("languages/$lang.php");
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();
277 return self::$_languageCache[$lang];
280 public function id() {
281 $klass = get_class($this);
282 return strtolower(substr($klass, 0, strlen($klass) - strlen('Language')));
285 protected function setCaseInsensitive($value) {
286 $this->_caseInsensitive = $value;
289 protected function addStates(array $states) {
290 $this->_states = self::mergeProperties($this->_states, $states);
293 protected function getState($key) {
294 return $this->_states[$key];
297 protected function removeState($key) {
298 unset($this->_states[$key]);
301 protected function addRules(array $rules) {
302 $this->_rules = self::mergeProperties($this->_rules, $rules);
305 protected function getRule($key) {
306 return $this->_rules[$key];
309 protected function removeRule($key) {
310 unset($this->_rules[$key]);
313 protected function addMappings(array $mappings) {
314 // TODO Implement nested mappings.
315 $this->_mappings = array_merge($this->_mappings, $mappings);
318 protected function getMapping($key) {
319 return $this->_mappings[$key];
322 protected function removeMapping($key) {
323 unset($this->_mappings[$key]);
326 protected function setInfo(array $info) {
327 $this->_info = $info;
330 protected function setExtensions(array $extensions) {
331 $this->_extensions = $extensions;
334 protected function addPostprocessing($rule, HyperLanguage $language) {
335 $this->_postProcessors[$rule] = $language;
338 // protected function addNestedLanguage(HyperLanguage $language, $hoistBackRules) {
339 // $prefix = get_class($language);
340 // if (!is_array($hoistBackRules))
341 // $hoistBackRules = array($hoistBackRules);
343 // $states = array(); // Step 1: states
345 // foreach ($language->_states as $stateName => $state) {
346 // $prefixedRules = array();
348 // if (strstr($stateName, ' ')) {
349 // $parts = explode(' ', $stateName);
350 // $prefixed = array();
351 // foreach ($parts as $part)
352 // $prefixed[] = "$prefix$part";
353 // $stateName = implode(' ', $prefixed);
356 // $stateName = "$prefix$stateName";
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";
365 // $prefixedRules["$prefix$key"] = $nestedRules;
368 // $prefixedRules[] = "$prefix$rule";
371 // if ($stateName === 'init')
372 // $prefixedRules = array_merge($hoistBackRules, $prefixedRules);
374 // $states[$stateName] = $prefixedRules;
377 // $rules = array(); // Step 2: rules
378 // // Mappings need to set up already!
379 // $mappings = array();
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;
390 // $nestedRules[] = $nestedRule;
392 // $rules["$prefix$ruleName"] = $nestedRules;
395 // $rules["$prefix$ruleName"] = $rule;
396 // $mappings["$prefix$ruleName"] = $ruleName;
400 // // Step 3: mappings.
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;
411 // $mappings["$prefix$ruleName"] = $cssClass;
414 // $this->addStates($states);
415 // $this->addRules($rules);
416 // $this->addMappings($mappings);
418 // return $prefix . 'init';
421 private function makeCompiledLanguage() {
422 return new HyperlightCompiledLanguage(
429 $this->_caseInsensitive,
430 $this->_postProcessors
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);
450 class HyperlightCompiledLanguage {
453 private $_extensions;
457 private $_caseInsensitive;
458 private $_postProcessors = array();
460 public function __construct($id, $info, $extensions, $states, $rules, $mappings, $caseInsensitive, $postProcessors) {
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;
469 foreach ($postProcessors as $ppkey => $ppvalue)
470 $this->_postProcessors[$ppkey] = HyperLanguage::compile($ppvalue);
473 public function id() {
477 public function name() {
478 return $this->_info[HyperLanguage::NAME];
481 public function authorName() {
482 if (!array_key_exists(HyperLanguage::AUTHOR, $this->_info))
484 $author = $this->_info[HyperLanguage::AUTHOR];
485 if (is_string($author))
487 if (!array_key_exists(HyperLanguage::NAME, $author))
489 return $author[HyperLanguage::NAME];
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]))
497 return $this->_info[HyperLanguage::AUTHOR][HyperLanguage::WEBSITE];
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]))
505 return $this->_info[HyperLanguage::AUTHOR][HyperLanguage::EMAIL];
508 public function authorContact() {
509 $email = $this->authorEmail();
510 return $email !== null ? $email : $this->authorWebsite();
513 public function extensions() {
514 return $this->_extensions;
517 public function state($stateName) {
518 return $this->_states[$stateName];
521 public function rule($ruleName) {
522 return $this->_rules[$ruleName];
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.
532 // Try mapping parts of nested state name.
533 $parts = explode(' ', $state);
536 foreach ($parts as $part) {
537 if (array_key_exists($part, $this->_mappings))
538 $ret[] = $this->_mappings[$part];
543 return implode(' ', $ret);
547 public function postProcessors() {
548 return $this->_postProcessors;
551 private function compileStates($states) {
554 foreach ($states as $name => $state) {
557 if (!is_array($state))
558 $state = array($state);
560 foreach ($state as $key => $elem) {
563 if (is_string($key)) {
564 if (!is_array($elem))
565 $elem = array($elem);
567 foreach ($elem as $el2) {
571 $newstate[] = "$key $el2";
578 $ret[$name] = $newstate;
584 private function compileRules($rules) {
587 // Preprocess keyword list and flatten nested lists:
589 // End of regular expression matching keywords.
590 $end = $this->_caseInsensitive ? ')\b/i' : ')\b/';
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;
601 if (!is_string($key) or strlen($key) === 0)
602 $tmp[$name] = $value;
604 $tmp["$name $key"] = $value;
608 // Array represents a list of keywords.
609 $rule = '/\b(?:' . implode('|', $rule) . $end;
615 } // if (is_array($rule))
620 foreach ($this->_states as $name => $state) {
621 $regex_rules = array();
622 $regex_names = array();
623 $nesting_rules = array();
625 foreach ($state as $rule_name) {
626 $rule = $tmp[$rule_name];
627 if ($rule instanceof Rule)
628 $nesting_rules[$rule_name] = $rule;
630 $regex_rules[] = $rule;
631 $regex_names[] = $rule_name;
635 $ret[$name] = array_merge(
636 array(preg_merge('|', $regex_rules, $regex_names)),
644 private static function isAssocArray(array $array) {
645 foreach($array as $key => $_)
657 private $_postProcessors = array();
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);
668 'Invalid argument type for $lang to Hyperlight::__construct',
672 foreach ($this->_lang->postProcessors() as $ppkey => $ppvalue)
673 $this->_postProcessors[$ppkey] = new Hyperlight($ppvalue);
678 public function language() {
682 public function reset() {
683 $this->_states = array('init');
684 $this->_omitSpans = array();
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);
694 public function renderAndPrint($code) {
695 echo $this->render($code);
699 private function renderCode() {
700 $code = $this->_code;
702 $len = strlen($code);
704 $state = array_peek($this->_states);
706 // If there are open states (reentrant parsing), open the corresponding
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\">");
715 // Emergency break to catch faulty rules.
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);
726 foreach ($rules as $name => $rule) {
727 if ($rule instanceof Rule)
728 $this->matchIfCloser(
729 $rule->start(), $name, $pos, $closest_hit, $closest_rule
731 else if (preg_match($rule, $code, $matches, PREG_OFFSET_CAPTURE, $pos) == 1) {
732 // Search which of the sub-patterns matched.
734 foreach ($matches as $group => $match) {
735 if (!is_string($group))
737 if ($match[1] !== -1) {
738 $closest_hit = $match;
739 $closest_rule = str_replace('_', ' ', $group);
744 } // foreach ($rules)
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.
749 if (count($this->_states) > 1) {
750 $n = count($this->_states) - 1;
752 $rule = $this->_lang->rule($this->_states[$n - 1]);
753 $rule = $rule[$this->_states[$n]];
756 throw new NoMatchingRuleException($this->_states, $pos, $code);
757 } while ($rule->end() === null);
759 $this->matchIfCloser($rule->end(), $n + 1, $pos, $closest_hit, $closest_rule);
762 // We take the closest hit:
764 if ($closest_hit[1] > $pos)
765 $this->emit(substr($code, $pos, $closest_hit[1] - $pos));
768 $pos = $closest_hit[1] + strlen($closest_hit[0]);
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);
777 if ($closest_hit[1] === $len)
779 else if (!is_string($closest_rule)) {
781 if (count($this->_states) <= $closest_rule)
782 throw new NoMatchingRuleException($this->_states, $pos, $code);
784 while (count($this->_states) > $closest_rule + 1) {
785 $lastState = array_pop($this->_states);
786 $this->emitPop('', $lastState);
788 $lastState = array_pop($this->_states);
789 $state = array_peek($this->_states);
790 $this->emitPop($closest_hit[0], $lastState);
792 else if (array_key_exists($closest_rule, $this->_lang->rule($state))) {
794 array_push($this->_states, $closest_rule);
795 $state = $closest_rule;
796 $this->emitPartial($closest_hit[0], $closest_rule);
799 $this->emit($closest_hit[0], $closest_rule);
800 } // while ($pos < $len)
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).
806 $omitSpansBackup = $this->_omitSpans;
807 for ($i = count($this->_states); $i > 1; --$i)
809 $this->_omitSpans = $omitSpansBackup;
811 return $this->_result;
814 private function matchIfCloser($expr, $next, $pos, &$closest_hit, &$closest_rule) {
816 if (preg_match($expr, $this->_code, $matches, PREG_OFFSET_CAPTURE, $pos) == 1) {
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])
824 $matches[0][1] < $closest_hit[1]
826 $closest_hit = $matches[0];
827 $closest_rule = $next;
832 private function processToken($token) {
835 $nest_lang = array_peek($this->_states);
836 if (array_key_exists($nest_lang, $this->_postProcessors))
837 return $this->_postProcessors[$nest_lang]->render($token);
839 #return self::htmlentities($token);
840 return htmlspecialchars($token, ENT_NOQUOTES);
843 private function emit($token, $class = '') {
844 $token = $this->processToken($token);
847 $class = $this->_lang->className($class);
849 $this->write($token);
851 $this->write("<span class=\"$class\">$token</span>");
854 private function emitPartial($token, $class) {
855 $token = $this->processToken($token);
856 $class = $this->_lang->className($class);
859 $this->write($token);
860 array_push($this->_omitSpans, true);
863 $this->write("<span class=\"$class\">$token");
864 array_push($this->_omitSpans, false);
868 private function emitPop($token = '', $class = '') {
869 $token = $this->processToken($token);
870 if (array_pop($this->_omitSpans))
871 $this->write($token);
873 $this->write("$token</span>");
876 private function write($text) {
877 $this->_result .= $text;
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 '<';
885 // case '>': return '>';
886 // case '&': return '&';
890 // private static function htmlentities($text) {
891 // return htmlspecialchars($text, ENT_NOQUOTES);
892 // return preg_replace_callback(
893 // '/[<>&]/', array('Hyperlight', 'htmlentitiesCallback'), $text
896 } // class Hyperlight
899 * <var>echo</var>s a highlighted code.
901 * For example, the following
903 * hyperlight('<?php echo \'Hello, world\'; ?>', 'php');
907 * <pre class="source-code php">...</pre>
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.
918 function hyperlight($code, $lang, $tag = 'pre', array $attributes = array()) {
920 die("`hyperlight` needs a code to work on!");
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`!");
927 if (is_array($tag)) {
931 $lang = htmlspecialchars(strtolower($lang));
932 $class = "source-code $lang";
935 foreach ($attributes as $key => $value) {
937 $class .= ' ' . htmlspecialchars($value);
939 $attr[] = htmlspecialchars($key) . '="' .
940 htmlspecialchars($value) . '"';
943 $attr = empty($attr) ? '' : ' ' . implode(' ', $attr);
945 $hl = new Hyperlight($lang);
946 echo "<$tag class=\"$class\"$attr>";
947 $hl->renderAndPrint(trim($code));
954 * hyperlight(file_get_contents($filename), $lang, $tag, $attributes);
958 function hyperlight_file($filename, $lang = null, $tag = 'pre', array $attributes = array()) {
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);
967 hyperlight(file_get_contents($filename), $lang, $tag, $attributes);
970 if (defined('HYPERLIGHT_SHORTCUT')) {
972 $args = func_get_args();
973 call_user_func_array('hyperlight', $args);
976 $args = func_get_args();
977 call_user_func_array('hyperlight_file', $args);
981 function hyperlight_calculate_fold_marks($code, $lang) {
982 $supporting_languages = array('csharp', 'vb');
984 if (!in_array($lang, $supporting_languages))
987 $fold_begin_marks = array('/^\s*#Region/', '/^\s*#region/');
988 $fold_end_marks = array('/^\s*#End Region/', '/\s*#endregion/');
990 $lines = preg_split('/\r|\n|\r\n/', $code);
992 $fold_begin = array();
993 foreach ($fold_begin_marks as $fbm)
994 $fold_begin = $fold_begin + preg_grep($fbm, $lines);
997 foreach ($fold_end_marks as $fem)
998 $fold_end = $fold_end + preg_grep($fem, $lines);
1000 if (count($fold_begin) !== count($fold_end) or count($fold_begin) === 0)
1005 foreach ($fold_begin as $line => $_)
1008 foreach ($fold_end as $line => $_)
1012 for ($i = 0; $i < count($fb); $i++)
1013 $ret[$fb[$i]] = $fe[$i];
1018 function hyperlight_apply_fold_marks($code, array $fold_marks) {
1019 if ($fold_marks === null or count($fold_marks) === 0)
1022 $lines = explode("\n", $code);
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];
1030 return implode("\n", $lines);