scanners = new LuminousScanners(); $this->register_default_scanners(); } /// registers builtin scanners private function register_default_scanners() { $this->settings = new LuminousOptions(); require dirname(__FILE__) . '/load_scanners.php'; } /** * Returns an instance of the current formatter */ function get_formatter() { $fmt_path = dirname(__FILE__) . '/formatters/'; $fmt = $this->settings->format; $formatter = null; if (!is_string($fmt) && is_subclass_of($fmt, 'LuminousFormatter')) { $formatter = clone $fmt; } elseif ($fmt === 'html') { require_once($fmt_path . 'htmlformatter.class.php'); $formatter = new LuminousFormatterHTML(); } elseif ($fmt === 'html-inline') { require_once($fmt_path . 'htmlformatter.class.php'); $formatter = new LuminousFormatterHTMLInline(); } elseif ($fmt === 'html-full') { require_once($fmt_path . 'htmlformatter.class.php'); $formatter = new LuminousFormatterHTMLFullPage(); } elseif($fmt === 'latex') { require_once($fmt_path . 'latexformatter.class.php'); $formatter = new LuminousFormatterLatex(); } elseif($fmt === null || $fmt === 'none') { require_once($fmt_path . 'identityformatter.class.php'); $formatter = new LuminousIdentityFormatter(); } if ($formatter === null) { throw new Exception('Unknown formatter: ' . $this->settings->format); return null; } $this->set_formatter_options($formatter); return $formatter; } /** * Sets up a formatter instance according to our current options/settings */ private function set_formatter_options(&$formatter) { $formatter->wrap_length = $this->settings->wrap_width; $formatter->line_numbers = $this->settings->line_numbers; $formatter->start_line = $this->settings->start_line; $formatter->link = $this->settings->auto_link; $formatter->height = $this->settings->max_height; $formatter->strict_standards = $this->settings->html_strict; $formatter->set_theme(luminous::theme($this->settings->theme)); $formatter->highlight_lines = $this->settings->highlight_lines; $formatter->language = $this->language; } /** * calculates a 'cache_id' for the input. This is dependent upon the * source code and the settings. This should be (near-as-feasible) unique * for any cobmination of source, language and settings */ private function cache_id($scanner, $source) { // to figure out the cache id, we mash a load of stuff together and // md5 it. This gives us a unique (assuming no collisions) handle to // a cache file, which depends on the input source, the relevant formatter // settings, the version, and scanner. $settings = array($this->settings->wrap_width, $this->settings->line_numbers, $this->settings->start_line, $this->settings->auto_link, $this->settings->max_height, $this->settings->format, $this->settings->theme, $this->settings->html_strict, LUMINOUS_VERSION, ); $id = md5($source); $id = md5($id . serialize($scanner)); $id = md5($id . serialize($settings)); return $id; } /** * The real highlighting function * @throw InvalidArgumentException if $scanner is not either a string or a * LuminousScanner instance, or if $source is not a string. */ function highlight($scanner, $source, $settings = null) { $old_settings = null; if ($settings !== null) { if (!is_array($settings)) { throw new Exception('Luminous internal error: Settings is not an array'); } $old_settings = clone $this->settings; foreach($settings as $k=>$v) { $this->settings->set($k, $v); } } $should_reset_language = false; $this->cache = null; if (!is_string($source)) throw new InvalidArgumentException('Non-string ' . 'supplied for $source'); if (!($scanner instanceof LuminousScanner)) { if (!is_string($scanner)) throw new InvalidArgumentException('Non-string or LuminousScanner instance supplied for $scanner'); $code = $scanner; $scanner = $this->scanners->GetScanner($code); if ($scanner === null) throw new Exception("No known scanner for '$code' and no default set"); $should_reset_language = true; $this->language = $this->scanners->GetDescription($code); } $cache_hit = true; $out = null; if ($this->settings->cache) { $cache_id = $this->cache_id($scanner, $source); if ($this->settings->sql_function !== null) { $this->cache = new LuminousSQLCache($cache_id); $this->cache->set_sql_function($this->settings->sql_function); } else { $this->cache = new LuminousFileSystemCache($cache_id); } $this->cache->set_purge_time($this->settings->cache_age); $out = $this->cache->read(); } if ($out === null) { $cache_hit = false; $out_raw = $scanner->highlight($source); $formatter = $this->get_formatter(); $out = $formatter->format($out_raw); } if ($this->settings->cache && !$cache_hit) { $this->cache->write($out); } if ($should_reset_language) $this->language = null; if ($old_settings !== null) { $this->settings = $old_settings; } return $out; } } /// @endcond // ends ALL // Here's our singleton. global $luminous_; // sometimes need this or the object seems to disappear $luminous_ = new _Luminous(); /// @cond USER // here's our 'real' UI class, which uses the above singleton. This is all // static because these are actually procudural functions, we're using the // class as a namespace. /** * @brief Users' API */ abstract class luminous { /** * @brief Highlights a string according to the current settings * * @param $scanner The scanner to use, this can either be a langauge code, * or it can be an instance of LuminousScanner. * @param $source The source string * @param $cache Whether or not to use the cache * @return the highlighted source code. * * To specify different output formats or other options, see set(). */ static function highlight($scanner, $source, $cache_or_settings=null) { global $luminous_; try { $settings = null; if (is_bool($cache_or_settings)) { $settings = array('cache' => $cache_or_settings); } else if (is_array($cache_or_settings)) { $settings = $cache_or_settings; } $h = $luminous_->highlight($scanner, $source, $settings); if ($luminous_->settings->verbose) { $errs = self::cache_errors(); if (!empty($errs)) { trigger_error("Le cache de Luminous a été desactivé en raison du manque de permission d'écriture. \n" . 'See luminous::cache_errors() for details.'); } } return $h; } catch (InvalidArgumentException $e) { // this is a user error, let it bubble //FIXME how do we let it bubble without throwing? the stack trace will // be wrong. throw $e; } catch (Exception $e) { // this is an internal error or a scanner error, or something // it might not technically be Luminous that caused it, but let's not // make it kill the whole page in production code if (LUMINOUS_DEBUG) throw $e; else { $return = $source; if (($t = self::setting('failure-tag'))) $return = "<$t>$return"; return $return; } } } /** * @brief Highlights a file according to the current setings. * * @param $scanner The scanner to use, this can either be a langauge code, * or it can be an instance of LuminousScanner. * @param $file the source string * @param $cache Whether or not to use the cache * @return the highlighted source code. * * To specify different output formats or other options, see set(). */ static function highlight_file($scanner, $file, $cache_or_settings=null) { return self::highlight($scanner, file_get_contents($file), $cache_or_settings); } /** * @brief Returns a list of cache errors encountered during the most recent highlight * * @return An array of errors the cache encountered (which may be empty), * or @c FALSE if the cache is not enabled */ static function cache_errors() { global $luminous_; $c = $luminous_->cache; if ($c === null) return FALSE; return $c->errors(); } /** * @brief Registers a scanner * * Registers a scanner with Luminous's scanner table. Utilising this * function means that Luminous will handle instantiation and inclusion of * the scanner's source file in a lazy-manner. * * @param $language_code A string or array of strings designating the * aliases by which this scanner may be accessed * @param $classname The class name of the scanner, as string. If you * leave this as 'null', it will be treated as a dummy file (you can use * this to handle a set of non-circular include rules, if you run into * problems). * @param $readable_language A human readable language name * @param $path The path to the source file containing your scanner * @param dependencies An array of other scanners which this scanner * depends on (as sub-scanners, or superclasses). Each item in the * array should be a $language_code for another scanner. */ static function register_scanner($language_code, $classname, $readable_language, $path, $dependencies=null) { global $luminous_; $luminous_->scanners->AddScanner($language_code, $classname, $readable_language, $path, $dependencies); } /** * @brief Get the full filesystem path to Luminous * @return what Luminous thinks its location is on the filesystem * @internal */ static function root() { return realpath(dirname(__FILE__) . '/../'); } /** * @brief Gets a list of installed themes * * @return the list of theme files present in style/. * Each theme will simply be a filename, and will end in .css, and will not * have any directory prefix. */ static function themes() { $themes_uri = self::root() . '/style/'; $themes = array(); foreach(glob($themes_uri . '/*.css') as $css) { $fn = trim(preg_replace("%.*/%", '', $css)); switch($fn) { // these are special, exclude these case 'luminous.css': case 'luminous_print.css': case 'luminous.min.css': continue; default: $themes[] = $fn; } } return $themes; } /** * @brief Checks whether a theme exists * @param $theme the name of a theme, which should be suffixed with .css * @return @c TRUE if a theme exists in style/, else @c FALSE */ static function theme_exists($theme) { return in_array($theme, self::themes()); } /** * @brief Reads a CSS theme file * Gets the CSS-string content of a theme file. * Use this function for reading themes as it involves security * checks against reading arbitrary files * * @param $theme The name of the theme to retrieve, which may or may not * include the .css suffix. * @return the content of a theme; this is the actual CSS text. * @internal */ static function theme($theme) { if (!preg_match('/\.css$/i', $theme)) $theme .= '.css'; if (self::theme_exists($theme)) return file_get_contents(self::root() . "/style/" . $theme); else throw new Exception('No such theme file: ' . $theme); } /** * @brief Gets a setting's value * @param $option The name of the setting (corresponds to an attribute name * in LuminousOptions) * @return The value of the given setting * @throws Exception if the option is unrecognised * * Options are stored in LuminousOptions, which provides documentation of * each option. * @see LuminousOptions */ static function setting($option) { global $luminous_; $option = str_replace('-', '_', $option); return $luminous_->settings->$option; } /** * @brief Sets the given option to the given value * @param $option The name of the setting (corresponds to an attribute name * in LuminousOptions) * @param $value The new value of the setting * @throws Exception if the option is unrecognised (and in various other * validation failures), * @throws InvalidArgumentException if the argument fails the type-validation * check * * @note This function can also accept multiple settings if $option is a * map of option_name=>value * * Options are stored in LuminousOptions, which provides documentation of * each option. * * @note as of 0.7 this is a thin wrapper around LuminousOptions::set() * * @see LuminousOptions::set */ static function set($option, $value=null) { global $luminous_; $luminous_->settings->set($option, $value); } /** * @brief Gets a list of registered scanners * * @return a list of scanners currently registered. The list is in the * format: * * language_name => codes, * * where language_name is a string, and codes is an array of strings. * * The array is sorted alphabetically by key. */ static function scanners() { global $luminous_; $scanners = $luminous_->scanners->ListScanners(); ksort($scanners); return $scanners; } /** * @brief Gets a formatter instance * * @return an instance of a LuminousFormatter according to the current * format setting * * This shouldn't be necessary for general usage, it is only implemented * for testing. * @internal */ static function formatter() { global $luminous_; return $luminous_->get_formatter(); } /** * @internal * Comparison function for guess_language()'s sorting */ static function __guess_language_cmp($a, $b) { $c = $a['p'] - $b['p']; if ($c === 0) return 0; elseif ($c < 0) return -1; else return 1; } /** * @brief Attempts to guess the language of a piece of source code * @param $src The source code whose language is to be guessed * @param $confidence The desired confidence level: if this is 0.05 but the * best guess has a confidence of 0.04, then $default is returned. Note * that the confidence level returned by scanners is quite arbitrary, so * don't set this to '1' thinking that'll give you better results. * A realistic confidence is likely to be quite low, because a scanner will * only return 1 if it's able to pick out a shebang (#!) line or something * else definitive. If there exists no such identifier, a 'strong' * confidence which is right most of the time might be as low as 0.1. * Therefore it is recommended to keep this between 0.01 and 0.10. * @param $default The default name to return in the event that no scanner * thinks this source belongs to them (at the desired confidence). * * @return A valid code for the best scanner, or $default. * * This is a wrapper around luminous::guess_language_full */ static function guess_language($src, $confidence=0.05, $default = 'plain') { $guess = self::guess_language_full($src); if ($guess[0]['p'] >= $confidence) return $guess[0]['codes'][0]; else return $default; } /** * @brief Attempts to guess the language of a piece of source code * @param $src The source code whose language is to be guessed * @return An array - the array is ordered by probability, with the most * probable language coming first in the array. * Each array element is an array which represents a language (scanner), * and has the keys: * \li \c 'language' => Human-readable language description, * \li \c 'codes' => valid codes for the language (array), * \li \c 'p' => the probability (between 0.0 and 1.0 inclusive), * * note that \c 'language' and \c 'codes' are the key => value pair from * luminous::scanners() * * @warning Language guessing is inherently unreliable but should be right * about 80% of the time on common languages. Bear in mind that guessing is * going to be severely hampered in the case that one language is used to * generate code in another language. * * Usage for this function will be something like this: * @code * $guesses = luminous::guess_language($src); * $output = luminous::highlight($guesses[0]['codes'][0], $src); * @endcode * * @see luminous::guess_language */ static function guess_language_full($src) { global $luminous_; // first we're going to make an 'info' array for the source, which // precomputes some frequently useful things, like how many lines it // has, etc. It prevents scanners from redundantly figuring these things // out themselves $lines = preg_split("/\r\n|[\r\n]/", $src); $shebang = ''; if (preg_match('/^#!.*/', $src, $m)) $shebang = $m[0]; $info = array( 'lines' => $lines, 'num_lines' => count($lines), 'trimmed' => trim($src), 'shebang' => $shebang ); $return = array(); foreach(self::scanners() as $lang=>$codes) { $scanner_name = $luminous_->scanners->GetScanner($codes[0], false, false); assert($scanner_name !== null); $return[] = array( 'language' => $lang, 'codes' => $codes, 'p' => call_user_func(array($scanner_name, 'guess_language'), $src, $info) ); } uasort($return, array('luminous', '__guess_language_cmp')); $return = array_reverse($return); return $return; } /** * @brief Gets the markup you need to include in your web page * @return a string representing everything that needs to be printed in * the \ section of a website. * * This is influenced by the following settings: * relative-root, * include-javascript, * include-jquery * theme */ static function head_html() { global $luminous_; $theme = self::setting('theme'); $relative_root = self::setting('relative-root'); $js = self::setting('include-javascript'); $jquery = self::setting('include-jquery'); if (!preg_match('/\.css$/i', $theme)) $theme .= '.css'; if (!self::theme_exists($theme)) $theme = 'luminous_light.css'; if ($relative_root === null) { $relative_root = str_replace($_SERVER['DOCUMENT_ROOT'], '/', dirname(__FILE__)); $relative_root = str_replace('\\', '/', $relative_root); // bah windows $relative_root = rtrim($relative_root, '/'); // go up one level. $relative_root = preg_replace('%/[^/]*$%', '', $relative_root); } // if we ended up with any double slashes, let's zap them, and also // trim any trailing ones $relative_root = preg_replace('%(?\n"; $script_template = "\n"; $out .= sprintf($link_template, 'luminous.css', 'luminous-style'); $out .= sprintf($link_template, $theme, 'luminous-theme'); if ($js) { if ($jquery) $out .= sprintf($script_template, 'jquery-1.6.4.min.js'); $out .= sprintf($script_template, 'luminous.js'); } return $out; } } /// @endcond // ends user