1<?php 2/// @cond ALL 3 4/** 5 * Collection of templates and templating utilities 6 */ 7class LuminousHTMLTemplates { 8 9 // NOTE Don't worry about whitespace in the templates - it gets stripped from the innerHTML, 10 // so the <pre>s aren't affected. Make it readable :) 11 12 /// Normal container 13 const container_template = ' 14 <div 15 class="luminous" 16 data-language="{language}" 17 style="{height_css}" 18 > 19 {subelement} 20 </div>'; 21 22 /// Inline code container 23 const inline_template = ' 24 <div 25 class="luminous inline" 26 data-language="{language}" 27 > 28 {subelement} 29 </div>'; 30 31 /// line number-less 32 const numberless_template = ' 33 <pre 34 class="code" 35 > 36 {code} 37 </pre>'; 38 39 /// line numbered 40 // NOTE: there's a good reason we use tables here and that's because 41 // nothing else works reliably. 42 const numbered_template = ' 43 <table> 44 <tbody> 45 <tr> 46 <td> 47 <pre class="line-numbers"> 48 {line_numbers} 49 </pre> 50 </td> 51 52 <td class="code-container"> 53 <pre class="code numbered" 54 data-startline="{start_line}" 55 data-highlightlines="{highlight_lines}" 56 > 57 {code} 58 </pre> 59 </td> 60 </tr> 61 </tbody> 62 </table>'; 63 64 65 66 67 private static function _strip_template_whitespace_cb($matches) { 68 return ($matches[0][0] === '<')? $matches[0] : ''; 69 } 70 private static function _strip_template_whitespace($string) { 71 return preg_replace_callback('/\s+|<[^>]++>/', 72 array('self', '_strip_template_whitespace_cb'), 73 $string); 74 } 75 76 /** 77 * Formats a string with a given set of values 78 * The format syntax uses {xyz} as a placeholder, which will be 79 * substituted from the 'xyz' key from $variables 80 * 81 * @param $template The template string 82 * @param $variables An associative (keyed) array of values to be substituted 83 * @param $strip_whitespace_from_template If @c TRUE, the template's whitespace is removed. 84 * This allows templates to be written to be easeier to read, without having to worry about 85 * the pre element inherting any unintended whitespace 86 */ 87 public static function format($template, $variables, $strip_whitespace_from_template = true) { 88 89 if ($strip_whitespace_from_template) { 90 $template = self::_strip_template_whitespace($template); 91 } 92 93 foreach($variables as $search => $replace) { 94 $template = str_replace("{" . $search . "}", $replace, $template); 95 } 96 return $template; 97 } 98} 99 100class LuminousFormatterHTML extends LuminousFormatter { 101 102 // overridden by inline formatter 103 protected $inline = false; 104 public $height = 0; 105 /** 106 * strict HTML standards: the target attribute won't be used in links 107 * \since 0.5.7 108 */ 109 public $strict_standards = false; 110 111 private function height_css() { 112 $height = trim('' . $this->height); 113 $css = ''; 114 if (!empty($height) && (int)$height > 0) { 115 // look for units, use px is there are none 116 if (!preg_match('/\D$/', $height)) $height .= 'px'; 117 $css = "max-height: {$height};"; 118 } 119 else 120 $css = ''; 121 return $css; 122 } 123 124 private static function template_cb($matches) { 125 126 } 127 128 // strips out unnecessary whitespace from a template 129 private static function template($t, $vars=array()) { 130 $t = preg_replace_callback('/\s+|<[^>]++>/', 131 array('self', 'template_cb'), 132 $t); 133 array_unshift($vars, $t); 134 $code = call_user_func_array('sprintf', $vars); 135 return $code; 136 } 137 138 private function lines_numberless($src) { 139 $lines = array(); 140 $lines_original = explode("\n", $src); 141 foreach($lines_original as $line) { 142 $l = $line; 143 $num = $this->wrap_line($l, $this->wrap_length); 144 // strip the newline if we're going to join it. Seems the easiest way to 145 // fix http://code.google.com/p/luminous/issues/detail?id=10 146 $l = substr($l, 0, -1); 147 $lines[] = $l; 148 } 149 $lines = implode("\n", $lines); 150 return $lines; 151 } 152 153 private function format_numberless($src) { 154 return LuminousHTMLTemplates::format( 155 LuminousHTMLTemplates::numberless_template, 156 array( 157 'height_css' => $this->height_css(), 158 'code' => $this->lines_numberless($src) 159 ) 160 ); 161 } 162 163 164 public function format($src) { 165 166 $line_numbers = false; 167 168 if ($this->link) $src = $this->linkify($src); 169 170 $code_block = null; 171 if ($this->line_numbers) { 172 $code_block = $this->format_numbered($src); 173 } 174 else { 175 $code_block = $this->format_numberless($src); 176 } 177 178 // convert </ABC> to </span> 179 $code_block = preg_replace('/(?<=<\/)[A-Z_0-9]+(?=>)/S', 'span', 180 $code_block); 181 // convert <ABC> to <span class=ABC> 182 $cb = create_function('$matches', 183 '$m1 = strtolower($matches[1]); 184 return "<span class=\'" . $m1 . "\'>"; 185 '); 186 $code_block = preg_replace_callback('/<([A-Z_0-9]+)>/', $cb, $code_block); 187 188 $format_data = array( 189 'language' => ($this->language === null)? '' : htmlentities($this->language), 190 'subelement' => $code_block, 191 'height_css' => $this->height_css() 192 ); 193 return LuminousHTMLTemplates::format( 194 $this->inline? LuminousHTMLTemplates::inline_template : 195 LuminousHTMLTemplates::container_template, 196 $format_data 197 ); 198 } 199 200 /** 201 * Detects and links URLs - callback 202 */ 203 protected function linkify_cb($matches) { 204 $uri = (isset($matches[1]) && strlen(trim($matches[1])))? $matches[0] 205 : "http://" . $matches[0]; 206 207 // we dont want to link if it would cause malformed HTML 208 $open_tags = array(); 209 $close_tags = array(); 210 preg_match_all("/<(?!\/)([^\s>]*).*?>/", $matches[0], $open_tags, 211 PREG_SET_ORDER); 212 preg_match_all("/<\/([^\s>]*).*?>/", $matches[0], $close_tags, 213 PREG_SET_ORDER); 214 215 if (count($open_tags) != count($close_tags)) 216 return $matches[0]; 217 if (isset($open_tags[0]) 218 && trim($open_tags[0][1]) !== trim($close_tags[0][1]) 219 ) 220 return $matches[0]; 221 222 $uri = strip_tags($uri); 223 224 $target = ($this->strict_standards)? '' : ' target="_blank"'; 225 return "<a href='{$uri}' class='link'{$target}>{$matches[0]}</a>"; 226 } 227 228 /** 229 * Detects and links URLs 230 */ 231 protected function linkify($src) { 232 if (stripos($src, "http") === false && stripos($src, "www") === false) 233 return $src; 234 235 $chars = "0-9a-zA-Z\$\-_\.+!\*,%"; 236 $src_ = $src; 237 // everyone stand back, I know regular expressions 238 $src = preg_replace_callback( 239 "@(?<![\w]) 240 (?:(https?://(?:www[0-9]*\.)?) | (?:www\d*\.) ) 241 242 # domain and tld 243 (?:[$chars]+)+\.[$chars]{2,} 244 # we don't include tags at the EOL because these are likely to be 245 # line-enclosing tags. 246 (?:[/$chars/?=\#;]+|&|<[^>]+>(?!$))* 247 @xm", 248 array($this, 'linkify_cb'), $src); 249 // this can hit a backtracking limit, in which case it nulls our string 250 // FIXME: see if we can make the above regex more resiliant wrt 251 // backtracking 252 if (preg_last_error() !== PREG_NO_ERROR) { 253 $src = $src_; 254 } 255 return $src; 256 } 257 258 259 private function format_numbered($src) { 260 261 $lines = '<span>' . 262 str_replace("\n", "\n</span><span>", $src, $num_replacements) . 263 "\n</span>"; 264 $num_lines = $num_replacements + 1; 265 266 $line_numbers = '<span>' . implode('</span><span>', 267 range($this->start_line, $this->start_line + $num_lines - 1, 1) 268 ) . '</span>'; 269 270 271 $format_data = array( 272 'line_number_digits' => strlen( (string)($this->start_line) + $num_lines ), // max number of digits in the line - this is used by the CSS 273 'start_line' => $this->start_line, 274 'height_css' => $this->height_css(), 275 'highlight_lines' => implode(',', $this->highlight_lines), 276 'code' => $lines, 277 'line_numbers' => $line_numbers 278 ); 279 280 return LuminousHTMLTemplates::format( 281 LuminousHTMLTemplates::numbered_template, 282 $format_data 283 ); 284 285 } 286} 287 288 289class LuminousFormatterHTMLInline extends LuminousFormatterHTML { 290 291 public function format($src) { 292 $this->line_numbers = false; 293 $this->height = 0; 294 $this->inline = true; 295 return parent::format($src); 296 } 297 298} 299 300 301class LuminousFormatterHTMLFullPage extends LuminousFormatterHTML { 302 protected $theme_css = null; 303 protected $css = null; 304 public function set_theme($css) { 305 $this->theme_css = $css; 306 } 307 protected function get_layout() { 308 // this path info shouldn't really be here 309 $path = luminous::root() . '/style/luminous.css'; 310 $this->css = file_get_contents($path); 311 } 312 public function format($src) { 313 $this->height = 0; 314 $this->get_layout(); 315 $fmted = parent::format($src); 316 return <<<EOF 317<!DOCTYPE html> 318<html> 319 <head> 320 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 321 <title></title> 322 <style type='text/css'> 323 body { 324 margin: 0; 325 } 326 /* luminous.css */ 327 {$this->css} 328 /* End luminous.css */ 329 /* Theme CSS */ 330 {$this->theme_css} 331 /* End theme CSS */ 332 </style> 333 </head> 334 <body> 335 <!-- Begin luminous code //--> 336 $fmted 337 <!-- End Luminous code //--> 338 </body> 339</html> 340 341EOF; 342 } 343} 344/// @endcond 345