1<?php 2/** 3 * DokuWiki StyleSheet creator 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../../../').'/'); 10if(!defined('NOSESSION')) define('NOSESSION',true); // we do not use a session or authentication here (better caching) 11if(!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT',1); // we gzip ourself here 12if(!defined('EPUB_DIR')) define('EPUB_DIR',realpath(dirname(__FILE__).'/../').'/'); 13require_once(DOKU_INC.'inc/init.php'); 14if(!defined('DOKU_TPLINC')) define('DOKU_TPLINC', tpl_incdir()); 15 16// ---------------------- functions ------------------------------ 17 18/** 19 * Output all needed Styles 20 * 21 * @author Andreas Gohr <andi@splitbrain.org> 22 */ 23function epub_css_out($path) 24{ 25 global $conf; 26 global $lang; 27 global $config_cascade; 28 global $INPUT; 29 30 31 $mediatypes = array('screen', 'all'); 32 $type = ''; 33 34 35 36 $tpl = $conf['template']; 37 38 // load styl.ini 39 $styleini = css_styleini($tpl); 40 41 // if old 'default' userstyle setting exists, make it 'screen' userstyle for backwards compatibility 42 if (isset($config_cascade['userstyle']['default'])) { 43 $config_cascade['userstyle']['screen'] = $config_cascade['userstyle']['default']; 44 } 45 46 // Array of needed files and their web locations, the latter ones 47 // are needed to fix relative paths in the stylesheets 48 $files = array(); 49 foreach($mediatypes as $mediatype) { 50 $files[$mediatype] = array(); 51 // load core styles 52 $files[$mediatype][DOKU_INC.'lib/styles/'.$mediatype.'.css'] = DOKU_BASE.'lib/styles/'; 53 // load jQuery-UI theme 54 if ($mediatype == 'screen') { 55 $files[$mediatype][DOKU_INC.'lib/scripts/jquery/jquery-ui-theme/smoothness.css'] = DOKU_BASE.'lib/scripts/jquery/jquery-ui-theme/'; 56 } 57 // load plugin styles 58 $files[$mediatype] = array_merge($files[$mediatype], css_pluginstyles($mediatype)); 59 // load template styles 60 if (isset($styleini['stylesheets'][$mediatype])) { 61 $files[$mediatype] = array_merge($files[$mediatype], $styleini['stylesheets'][$mediatype]); 62 } 63 // load user styles 64 if(!empty($config_cascade['userstyle'][$mediatype])) { 65 foreach($config_cascade['userstyle'][$mediatype] as $userstyle) { 66 $files[$mediatype][$userstyle] = DOKU_BASE; 67 } 68 } 69 } 70 71 72 73 $css=""; 74 75 // build the stylesheet 76 foreach ($mediatypes as $mediatype) { 77 78 // print the default classes for interwiki links and file downloads 79 if ($mediatype == 'screen') { 80 $css .= '@media screen {'; 81 css_interwiki($css); 82 css_filetypes($css); 83 $css .= '}'; 84 } 85 86 87$xcl = 'plugins/popularity|usermanager |plugins/upgrade|plugins/acl|plugins/plugin|plugins/auth|plugins/config|plugins/revert|_imgdetail.css' 88. '|plugins/fckg|plugins/ckgedit|plugins/edittable' 89. '|_media_popup.css|_media_fullscreen.css|_fileuploader.css|_toc.css|_search.css|_recent.css|_diff.css|_edit.css|_forms.css|_admin.css'; 90 91 92 // load files 93 $css_content = ''; 94 foreach($files[$mediatype] as $file => $location){ 95 if(preg_match('#' .$xcl . '#',$file)) continue; 96 $display = str_replace(fullpath(DOKU_INC), '', fullpath($file)); 97 $css_content .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n"; 98 $css_content .= css_loadfile($file, $location); 99 } 100 switch ($mediatype) { 101 case 'screen': 102 $css .= NL.'@media screen { /* START screen styles */'.NL.$css_content.NL.'} /* /@media END screen styles */'.NL; 103 break; 104 case 'all': 105 default: 106 $css .= NL.'/* START rest styles */ '.NL.$css_content.NL.'/* END rest styles */'.NL; 107 break; 108 } 109 } 110 111 // apply style replacements 112 $css = css_applystyle($css, $styleini['replacements']); 113 114 115 116 // parse less 117 $css = css_parseless($css); 118 119 $compress = false; 120 $compress=$conf['plugin']['epub']['compress']; 121 122 if($compress) { 123 echo "cmpressing CSS\n"; 124 $css = css_compress($css); 125 } 126 127 128 // embed small images right into the stylesheet 129 // if($conf['cssdatauri']){ 130 // $base = preg_quote(DOKU_BASE,'#'); 131 // $css = preg_replace_callback('#(url\([ \'"]*)('.$base.')(.*?(?:\.(png|gif)))#i','css_datauri',$css); 132 //} 133 134 io_saveFile($path . 'Styles/style.css' ,$css); 135} 136 137/** 138 * Uses phpless to parse LESS in our CSS 139 * 140 * most of this function is error handling to show a nice useful error when 141 * LESS compilation fails 142 * 143 * @param $css 144 * @return string 145 */ 146function css_parseless($css) { 147 $less = new lessc(); 148 $less->importDir[] = DOKU_INC; 149 150 try { 151 return $less->compile($css); 152 } catch(Exception $e) { 153 // get exception message 154 $msg = str_replace(array("\n", "\r", "'"), array(), $e->getMessage()); 155 156 // try to use line number to find affected file 157 if(preg_match('/line: (\d+)$/', $msg, $m)){ 158 $msg = substr($msg, 0, -1* strlen($m[0])); //remove useless linenumber 159 $lno = $m[1]; 160 161 // walk upwards to last include 162 $lines = explode("\n", $css); 163 for($i=$lno-1; $i>=0; $i--){ 164 if(preg_match('/\/(\* XXXXXXXXX )(.*?)( XXXXXXXXX \*)\//', $lines[$i], $m)){ 165 // we found it, add info to message 166 $msg .= ' in '.$m[2].' at line '.($lno-$i); 167 break; 168 } 169 } 170 } 171 172 // something went wrong 173 $error = 'A fatal error occured during compilation of the CSS files. '. 174 'If you recently installed a new plugin or template it '. 175 'might be broken and you should try disabling it again. ['.$msg.']'; 176 177 echo "$error\n"; 178 179 exit; 180 } 181} 182 183/** 184 * Does placeholder replacements in the style according to 185 * the ones defined in a templates style.ini file 186 * 187 * This also adds the ini defined placeholders as less variables 188 * (sans the surrounding __ and with a ini_ prefix) 189 * 190 * @author Andreas Gohr <andi@splitbrain.org> 191 */ 192function css_applystyle($css, $replacements) { 193 // we convert ini replacements to LESS variable names 194 // and build a list of variable: value; pairs 195 $less = ''; 196 foreach((array) $replacements as $key => $value) { 197 $lkey = trim($key, '_'); 198 $lkey = '@ini_'.$lkey; 199 $less .= "$lkey: $value;\n"; 200 201 $replacements[$key] = $lkey; 202 } 203 204 // we now replace all old ini replacements with LESS variables 205 $css = strtr($css, $replacements); 206 207 // now prepend the list of LESS variables as the very first thing 208 $css = $less.$css; 209 return $css; 210} 211 212/** 213 * Load style ini contents 214 * 215 * Loads and merges style.ini files from template and config and prepares 216 * the stylesheet modes 217 * 218 * @author Andreas Gohr <andi@splitbrain.org> 219 * @param string $tpl the used template 220 * @return array with keys 'stylesheets' and 'replacements' 221 */ 222function css_styleini($tpl) { 223 $stylesheets = array(); // mode, file => base 224 $replacements = array(); // placeholder => value 225 226 // load template's style.ini 227 $incbase = tpl_incdir($tpl); 228 $webbase = tpl_basedir($tpl); 229 $ini = $incbase.'style.ini'; 230 if(file_exists($ini)){ 231 $data = parse_ini_file($ini, true); 232 233 // stylesheets 234 if(is_array($data['stylesheets'])) foreach($data['stylesheets'] as $file => $mode){ 235 $stylesheets[$mode][$incbase.$file] = $webbase; 236 } 237 238 // replacements 239 if(is_array($data['replacements'])){ 240 $replacements = array_merge($replacements, css_fixreplacementurls($data['replacements'],$webbase)); 241 } 242 } 243 244 // load configs's style.ini 245 $webbase = DOKU_BASE; 246 $ini = DOKU_CONF."tpl/$tpl/style.ini"; 247 $incbase = dirname($ini).'/'; 248 if(file_exists($ini)){ 249 $data = parse_ini_file($ini, true); 250 251 // stylesheets 252 if(isset($data['stylesheets']) && is_array($data['stylesheets'])) foreach($data['stylesheets'] as $file => $mode){ 253 $stylesheets[$mode][$incbase.$file] = $webbase; 254 } 255 256 // replacements 257 if(isset($data['replacements']) && is_array($data['replacements'])){ 258 $replacements = array_merge($replacements, css_fixreplacementurls($data['replacements'],$webbase)); 259 } 260 } 261 262 return array( 263 'stylesheets' => $stylesheets, 264 'replacements' => $replacements 265 ); 266} 267 268/** 269 * Amend paths used in replacement relative urls, refer FS#2879 270 * 271 * @author Chris Smith <chris@jalakai.co.uk> 272 */ 273function css_fixreplacementurls($replacements, $location) { 274 foreach($replacements as $key => $value) { 275 $replacements[$key] = preg_replace('#(url\([ \'"]*)(?!/|data:|http://|https://| |\'|")#','\\1'.$location,$value); 276 } 277 return $replacements; 278} 279 280/** 281 * Prints classes for interwikilinks 282 * 283 * Interwiki links have two classes: 'interwiki' and 'iw_$name>' where 284 * $name is the identifier given in the config. All Interwiki links get 285 * an default style with a default icon. If a special icon is available 286 * for an interwiki URL it is set in it's own class. Both classes can be 287 * overwritten in the template or userstyles. 288 * 289 * @author Andreas Gohr <andi@splitbrain.org> 290 */ 291function css_interwiki(&$css){ 292 293 // default style 294 $css .= 'a.interwiki {'; 295 $css .= ' background: transparent url('.DOKU_BASE.'lib/images/interwiki.png) 0px 1px no-repeat;'; 296 $css .= ' padding: 1px 0px 1px 16px;'; 297 $css .= '}'; 298 299 // additional styles when icon available 300 $iwlinks = getInterwiki(); 301 foreach(array_keys($iwlinks) as $iw){ 302 $class = preg_replace('/[^_\-a-z0-9]+/i','_',$iw); 303 if(@file_exists(DOKU_INC.'lib/images/interwiki/'.$iw.'.png')){ 304 $css .= "a.iw_$class {"; 305 $css .= ' background-image: url('.DOKU_BASE.'lib/images/interwiki/'.$iw.'.png)'; 306 $css .= '}'; 307 }elseif(@file_exists(DOKU_INC.'lib/images/interwiki/'.$iw.'.gif')){ 308 $css .= "a.iw_$class {"; 309 $css .= ' background-image: url('.DOKU_BASE.'lib/images/interwiki/'.$iw.'.gif)'; 310 $css .= '}'; 311 } 312 } 313} 314 315/** 316 * Prints classes for file download links 317 * 318 * @author Andreas Gohr <andi@splitbrain.org> 319 */ 320function css_filetypes(&$css){ 321 322 // default style 323 $css .= '.mediafile {'; 324 $css .= ' background: transparent url('.DOKU_BASE.'lib/images/fileicons/file.png) 0px 1px no-repeat;'; 325 $css .= ' padding-left: 18px;'; 326 $css .= ' padding-bottom: 1px;'; 327 $css .= '}'; 328 329 // additional styles when icon available 330 // scan directory for all icons 331 $exts = array(); 332 if($dh = opendir(DOKU_INC.'lib/images/fileicons')){ 333 while(false !== ($file = readdir($dh))){ 334 if(preg_match('/([_\-a-z0-9]+(?:\.[_\-a-z0-9]+)*?)\.(png|gif)/i',$file,$match)){ 335 $ext = strtolower($match[1]); 336 $type = '.'.strtolower($match[2]); 337 if($ext!='file' && (!isset($exts[$ext]) || $type=='.png')){ 338 $exts[$ext] = $type; 339 } 340 } 341 } 342 closedir($dh); 343 } 344 foreach($exts as $ext=>$type){ 345 $class = preg_replace('/[^_\-a-z0-9]+/','_',$ext); 346 $css .= ".mf_$class {"; 347 $css .= ' background-image: url('.DOKU_BASE.'lib/images/fileicons/'.$ext.$type.')'; 348 $css .= '}'; 349 } 350} 351 352/** 353 * Loads a given file and fixes relative URLs with the 354 * given location prefix 355 */ 356function css_loadfile($file,$location=''){ 357 $css_file = new DokuCssFile($file); 358 return $css_file->load($location); 359} 360 361/** 362 * Helper class to abstract loading of css/less files 363 * 364 * @author Chris Smith <chris@jalakai.co.uk> 365 */ 366class DokuCssFile { 367 368 protected $filepath; // file system path to the CSS/Less file 369 protected $location; // base url location of the CSS/Less file 370 private $relative_path = null; 371 372 public function __construct($file) { 373 $this->filepath = $file; 374 } 375 376 /** 377 * Load the contents of the css/less file and adjust any relative paths/urls (relative to this file) to be 378 * relative to the dokuwiki root: the web root (DOKU_BASE) for most files; the file system root (DOKU_INC) 379 * for less files. 380 * 381 * @param string $location base url for this file 382 * @return string the CSS/Less contents of the file 383 */ 384 public function load($location='') { 385 if (!file_exists($this->filepath)) return ''; 386 387 $css = io_readFile($this->filepath); 388 if (!$location) return $css; 389 390 $this->location = $location; 391 392 $css = preg_replace_callback('#(url\( *)([\'"]?)(.*?)(\2)( *\))#',array($this,'replacements'),$css); 393 $css = preg_replace_callback('#(@import\s+)([\'"])(.*?)(\2)#',array($this,'replacements'),$css); 394 395 return $css; 396 } 397 398 /** 399 * Get the relative file system path of this file, relative to dokuwiki's root folder, DOKU_INC 400 * 401 * @return string relative file system path 402 */ 403 private function getRelativePath(){ 404 405 if (is_null($this->relative_path)) { 406 $basedir = array(DOKU_INC); 407 408 $basedir = array_map('preg_quote_cb', $basedir); 409 $regex = '/^('.join('|',$basedir).')/'; 410 $this->relative_path = preg_replace($regex, '', dirname($this->filepath)); 411 } 412 413 return $this->relative_path; 414 } 415 416 /** 417 * preg_replace callback to adjust relative urls from relative to this file to relative 418 * to the appropriate dokuwiki root location as described in the code 419 * 420 * @param array see http://php.net/preg_replace_callback 421 * @return string see http://php.net/preg_replace_callback 422 */ 423 public function replacements($match) { 424 425 // not a relative url? - no adjustment required 426 if (preg_match('#^(/|data:|https?://)#',$match[3])) { 427 return $match[0]; 428 } 429 // a less file import? - requires a file system location 430 else if (substr($match[3],-5) == '.less') { 431 if ($match[3]{0} != '/') { 432 $match[3] = $this->getRelativePath() . '/' . $match[3]; 433 } 434 } 435 // everything else requires a url adjustment 436 else { 437 $match[3] = $this->location . $match[3]; 438 } 439 440 return join('',array_slice($match,1)); 441 } 442} 443 444/** 445 * Convert local image URLs to data URLs if the filesize is small 446 * 447 * Callback for preg_replace_callback 448 */ 449function css_datauri($match){ 450 global $conf; 451 452 $pre = unslash($match[1]); 453 $base = unslash($match[2]); 454 $url = unslash($match[3]); 455 $ext = unslash($match[4]); 456 457 $local = DOKU_INC.$url; 458 $size = @filesize($local); 459 if($size && $size < $conf['cssdatauri']){ 460 $data = base64_encode(file_get_contents($local)); 461 } 462 if($data){ 463 $url = 'data:image/'.$ext.';base64,'.$data; 464 }else{ 465 $url = $base.$url; 466 } 467 return $pre.$url; 468} 469 470 471/** 472 * Returns a list of possible Plugin Styles (no existance check here) 473 * 474 * @author Andreas Gohr <andi@splitbrain.org> 475 */ 476function css_pluginstyles($mediatype='screen'){ 477 $list = array(); 478 $plugins = plugin_list(); 479 foreach ($plugins as $p){ 480 $list[DOKU_PLUGIN."$p/$mediatype.css"] = DOKU_BASE."lib/plugins/$p/"; 481 $list[DOKU_PLUGIN."$p/$mediatype.less"] = DOKU_BASE."lib/plugins/$p/"; 482 // alternative for screen.css 483 if ($mediatype=='screen') { 484 $list[DOKU_PLUGIN."$p/style.css"] = DOKU_BASE."lib/plugins/$p/"; 485 $list[DOKU_PLUGIN."$p/style.less"] = DOKU_BASE."lib/plugins/$p/"; 486 } 487 } 488 return $list; 489} 490 491/** 492 * Very simple CSS optimizer 493 * 494 * @author Andreas Gohr <andi@splitbrain.org> 495 */ 496function css_compress($css){ 497 //strip comments through a callback 498 $css = preg_replace_callback('#(/\*)(.*?)(\*/)#s','css_comment_cb',$css); 499 500 //strip (incorrect but common) one line comments 501 $css = preg_replace('/(?<!:)\/\/.*$/m','',$css); 502 503 // strip whitespaces 504 $css = preg_replace('![\r\n\t ]+!',' ',$css); 505 $css = preg_replace('/ ?([;,{}\/]) ?/','\\1',$css); 506 $css = preg_replace('/ ?: /',':',$css); 507 508 // number compression 509 $css = preg_replace('/([: ])0+(\.\d+?)0*((?:pt|pc|in|mm|cm|em|ex|px)\b|%)(?=[^\{]*[;\}])/', '$1$2$3', $css); // "0.1em" to ".1em", "1.10em" to "1.1em" 510 $css = preg_replace('/([: ])\.(0)+((?:pt|pc|in|mm|cm|em|ex|px)\b|%)(?=[^\{]*[;\}])/', '$1$2', $css); // ".0em" to "0" 511 $css = preg_replace('/([: ]0)0*(\.0*)?((?:pt|pc|in|mm|cm|em|ex|px)(?=[^\{]*[;\}])\b|%)/', '$1', $css); // "0.0em" to "0" 512 $css = preg_replace('/([: ]\d+)(\.0*)((?:pt|pc|in|mm|cm|em|ex|px)(?=[^\{]*[;\}])\b|%)/', '$1$3', $css); // "1.0em" to "1em" 513 $css = preg_replace('/([: ])0+(\d+|\d*\.\d+)((?:pt|pc|in|mm|cm|em|ex|px)(?=[^\{]*[;\}])\b|%)/', '$1$2$3', $css); // "001em" to "1em" 514 515 // shorten attributes (1em 1em 1em 1em -> 1em) 516 $css = preg_replace('/(?<![\w\-])((?:margin|padding|border|border-(?:width|radius)):)([\w\.]+)( \2)+(?=[;\}]| !)/', '$1$2', $css); // "1em 1em 1em 1em" to "1em" 517 $css = preg_replace('/(?<![\w\-])((?:margin|padding|border|border-(?:width)):)([\w\.]+) ([\w\.]+) \2 \3(?=[;\}]| !)/', '$1$2 $3', $css); // "1em 2em 1em 2em" to "1em 2em" 518 519 // shorten colors 520 $css = preg_replace("/#([0-9a-fA-F]{1})\\1([0-9a-fA-F]{1})\\2([0-9a-fA-F]{1})\\3(?=[^\{]*[;\}])/", "#\\1\\2\\3", $css); 521 522 return $css; 523} 524 525/** 526 * Callback for css_compress() 527 * 528 * Keeps short comments (< 5 chars) to maintain typical browser hacks 529 * 530 * @author Andreas Gohr <andi@splitbrain.org> 531 */ 532function css_comment_cb($matches){ 533 if(strlen($matches[2]) > 4) return ''; 534 return $matches[0]; 535} 536 537//Setup VIM: ex: et ts=4 : 538