xref: /dokuwiki/lib/exe/css.php (revision 90e793b91a348ae2876ed14c8b0d0000192d55e5)
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',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('NL')) define('NL',"\n");
13require_once(DOKU_INC.'inc/init.php');
14
15// Main (don't run when UNIT test)
16if(!defined('SIMPLE_TEST')){
17    header('Content-Type: text/css; charset=utf-8');
18    css_out();
19}
20
21
22// ---------------------- functions ------------------------------
23
24/**
25 * Output all needed Styles
26 *
27 * @author Andreas Gohr <andi@splitbrain.org>
28 */
29function css_out(){
30    global $conf;
31    global $lang;
32    global $config_cascade;
33    global $INPUT;
34
35    if ($INPUT->str('s') == 'feed') {
36        $mediatypes = array('feed');
37        $type = 'feed';
38    } else {
39        $mediatypes = array('screen', 'all', 'print');
40        $type = '';
41    }
42
43    // decide from where to get the template
44    $tpl = trim(preg_replace('/[^\w-]+/','',$INPUT->str('t')));
45    if(!$tpl) $tpl = $conf['template'];
46
47    // The generated script depends on some dynamic options
48    $cache = new cache('styles'.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'].DOKU_BASE.$tpl.$type,'.css');
49
50    // load styl.ini
51    $styleini = css_styleini($tpl);
52
53    // if old 'default' userstyle setting exists, make it 'screen' userstyle for backwards compatibility
54    if (isset($config_cascade['userstyle']['default'])) {
55        $config_cascade['userstyle']['screen'] = $config_cascade['userstyle']['default'];
56    }
57
58    // cache influencers
59    $tplinc = tpl_basedir($tpl);
60    $cache_files = getConfigFiles('main');
61    $cache_files[] = $tplinc.'style.ini';
62    $cache_files[] = DOKU_CONF."tpl/$tpl/style.ini";
63    $cache_files[] = __FILE__;
64
65    // Array of needed files and their web locations, the latter ones
66    // are needed to fix relative paths in the stylesheets
67    $files = array();
68    foreach($mediatypes as $mediatype) {
69        $files[$mediatype] = array();
70        // load core styles
71        $files[$mediatype][DOKU_INC.'lib/styles/'.$mediatype.'.css'] = DOKU_BASE.'lib/styles/';
72        // load jQuery-UI theme
73        if ($mediatype == 'screen') {
74            $files[$mediatype][DOKU_INC.'lib/scripts/jquery/jquery-ui-theme/smoothness.css'] = DOKU_BASE.'lib/scripts/jquery/jquery-ui-theme/';
75        }
76        // load plugin styles
77        $files[$mediatype] = array_merge($files[$mediatype], css_pluginstyles($mediatype));
78        // load template styles
79        if (isset($styleini['stylesheets'][$mediatype])) {
80            $files[$mediatype] = array_merge($files[$mediatype], $styleini['stylesheets'][$mediatype]);
81        }
82        // load user styles
83        if(isset($config_cascade['userstyle'][$mediatype])){
84            $files[$mediatype][$config_cascade['userstyle'][$mediatype]] = DOKU_BASE;
85        }
86        // load rtl styles
87        // note: this adds the rtl styles only to the 'screen' media type
88        // @deprecated 2012-04-09: rtl will cease to be a mode of its own,
89        //     please use "[dir=rtl]" in any css file in all, screen or print mode instead
90        if ($mediatype=='screen') {
91            if($lang['direction'] == 'rtl'){
92                if (isset($styleini['stylesheets']['rtl'])) $files[$mediatype] = array_merge($files[$mediatype], $styleini['stylesheets']['rtl']);
93                if (isset($config_cascade['userstyle']['rtl'])) $files[$mediatype][$config_cascade['userstyle']['rtl']] = DOKU_BASE;
94            }
95        }
96
97        $cache_files = array_merge($cache_files, array_keys($files[$mediatype]));
98    }
99
100    // check cache age & handle conditional request
101    // This may exit if a cache can be used
102    http_cached($cache->cache,
103                $cache->useCache(array('files' => $cache_files)));
104
105    // start output buffering
106    ob_start();
107
108    // build the stylesheet
109    foreach ($mediatypes as $mediatype) {
110
111        // print the default classes for interwiki links and file downloads
112        if ($mediatype == 'screen') {
113            print '@media screen {';
114            css_interwiki();
115            css_filetypes();
116            print '}';
117        }
118
119        // load files
120        $css_content = '';
121        foreach($files[$mediatype] as $file => $location){
122            $display = str_replace(fullpath(DOKU_INC), '', fullpath($file));
123            $css_content .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n";
124            $css_content .= css_loadfile($file, $location);
125        }
126        switch ($mediatype) {
127            case 'screen':
128                print NL.'@media screen { /* START screen styles */'.NL.$css_content.NL.'} /* /@media END screen styles */'.NL;
129                break;
130            case 'print':
131                print NL.'@media print { /* START print styles */'.NL.$css_content.NL.'} /* /@media END print styles */'.NL;
132                break;
133            case 'all':
134            case 'feed':
135            default:
136                print NL.'/* START rest styles */ '.NL.$css_content.NL.'/* END rest styles */'.NL;
137                break;
138        }
139    }
140    // end output buffering and get contents
141    $css = ob_get_contents();
142    ob_end_clean();
143
144    // apply style replacements
145    $css = css_applystyle($css, $styleini['replacements']);
146
147    // parse less
148    $css = css_parseless($css);
149
150    // place all remaining @import statements at the top of the file
151    $css = css_moveimports($css);
152
153    // compress whitespace and comments
154    if($conf['compress']){
155        $css = css_compress($css);
156    }
157
158    // embed small images right into the stylesheet
159    if($conf['cssdatauri']){
160        $base = preg_quote(DOKU_BASE,'#');
161        $css = preg_replace_callback('#(url\([ \'"]*)('.$base.')(.*?(?:\.(png|gif)))#i','css_datauri',$css);
162    }
163
164    http_cached_finish($cache->cache, $css);
165}
166
167/**
168 * Uses phpless to parse LESS in our CSS
169 *
170 * most of this function is error handling to show a nice useful error when
171 * LESS compilation fails
172 *
173 * @param $css
174 * @return string
175 */
176function css_parseless($css) {
177    $less = new lessc();
178    try {
179        return $less->compile($css);
180    } catch(Exception $e) {
181        // get exception message
182        $msg = str_replace(array("\n", "\r", "'"), array(), $e->getMessage());
183
184        // try to use line number to find affected file
185        if(preg_match('/line: (\d+)$/', $msg, $m)){
186            $msg = substr($msg, 0, -1* strlen($m[0])); //remove useless linenumber
187            $lno = $m[1];
188
189            // walk upwards to last include
190            $lines = explode("\n", $css);
191            for($i=$lno-1; $i>=0; $i--){
192                if(preg_match('/\/(\* XXXXXXXXX )(.*?)( XXXXXXXXX \*)\//', $lines[$i], $m)){
193                    // we found it, add info to message
194                    $msg .= ' in '.$m[2].' at line '.($lno-$i);
195                    break;
196                }
197            }
198        }
199
200        // something went wrong
201        $error = 'A fatal error occured during compilation of the CSS files. '.
202            'If you recently installed a new plugin or template it '.
203            'might be broken and you should try disabling it again. ['.$msg.']';
204
205        echo ".dokuwiki:before {
206            content: '$error';
207            background-color: red;
208            display: block;
209            background-color: #fcc;
210            border-color: #ebb;
211            color: #000;
212            padding: 0.5em;
213        }";
214
215        exit;
216    }
217}
218
219/**
220 * Does placeholder replacements in the style according to
221 * the ones defined in a templates style.ini file
222 *
223 * This also adds the ini defined placeholders as less variables
224 * (sans the surrounding __ and with a ini_ prefix)
225 *
226 * @author Andreas Gohr <andi@splitbrain.org>
227 */
228function css_applystyle($css, $replacements) {
229    // we convert ini replacements to LESS variable names
230    // and build a list of variable: value; pairs
231    $less = '';
232    foreach((array) $replacements as $key => $value) {
233        $lkey = trim($key, '_');
234        $lkey = '@ini_'.$lkey;
235        $less .= "$lkey: $value;\n";
236
237        $replacements[$key] = $lkey;
238    }
239
240    // we now replace all old ini replacements with LESS variables
241    $css = strtr($css, $replacements);
242
243    // now prepend the list of LESS variables as the very first thing
244    $css = $less.$css;
245    return $css;
246}
247
248/**
249 * Load style ini contents
250 *
251 * Loads and merges style.ini files from template and config and prepares
252 * the stylesheet modes
253 *
254 * @author Andreas Gohr <andi@splitbrain.org>
255 * @param string $tpl the used template
256 * @return array with keys 'stylesheets' and 'replacements'
257 */
258function css_styleini($tpl) {
259    $stylesheets = array(); // mode, file => base
260    $replacements = array(); // placeholder => value
261
262    // load template's style.ini
263    $incbase = tpl_incdir($tpl);
264    $webbase = tpl_basedir($tpl);
265    $ini = $incbase.'style.ini';
266    if(file_exists($ini)){
267        $data = parse_ini_file($ini, true);
268
269        // stylesheets
270        if(is_array($data['stylesheets'])) foreach($data['stylesheets'] as $file => $mode){
271            $stylesheets[$mode][$incbase.$file] = $webbase;
272        }
273
274        // replacements
275        if(is_array($data['replacements'])){
276            $replacements = array_merge($replacements, $data['replacements']);
277        }
278    }
279
280    // load template's style.local.ini
281    // @deprecated 2013-08-03
282    $ini = $incbase.'style.local.ini';
283    if(file_exists($ini)){
284        $data = parse_ini_file($ini, true);
285
286        // stylesheets
287        if(is_array($data['stylesheets'])) foreach($data['stylesheets'] as $file => $mode){
288            $stylesheets[$mode][$incbase.$file] = $webbase;
289        }
290
291        // replacements
292        if(is_array($data['replacements'])){
293            $replacements = array_merge($replacements, $data['replacements']);
294        }
295    }
296
297    // load configs's style.ini
298    $incbase = dirname($ini).'/';
299    $webbase = DOKU_BASE;
300    $ini = DOKU_CONF."/tpl/$tpl/style.ini";
301    if(file_exists($ini)){
302        $data = parse_ini_file($ini, true);
303
304        // stylesheets
305        if(is_array($data['stylesheets'])) foreach($data['stylesheets'] as $file => $mode){
306            $stylesheets[$mode][$incbase.$file] = $webbase;
307        }
308
309        // replacements
310        if(is_array($data['replacements'])){
311            $replacements = array_merge($replacements, $data['replacements']);
312        }
313    }
314
315    return array(
316        'stylesheets' => $stylesheets,
317        'replacements' => $replacements
318    );
319}
320
321/**
322 * Prints classes for interwikilinks
323 *
324 * Interwiki links have two classes: 'interwiki' and 'iw_$name>' where
325 * $name is the identifier given in the config. All Interwiki links get
326 * an default style with a default icon. If a special icon is available
327 * for an interwiki URL it is set in it's own class. Both classes can be
328 * overwritten in the template or userstyles.
329 *
330 * @author Andreas Gohr <andi@splitbrain.org>
331 */
332function css_interwiki(){
333
334    // default style
335    echo 'a.interwiki {';
336    echo ' background: transparent url('.DOKU_BASE.'lib/images/interwiki.png) 0px 1px no-repeat;';
337    echo ' padding: 1px 0px 1px 16px;';
338    echo '}';
339
340    // additional styles when icon available
341    $iwlinks = getInterwiki();
342    foreach(array_keys($iwlinks) as $iw){
343        $class = preg_replace('/[^_\-a-z0-9]+/i','_',$iw);
344        if(@file_exists(DOKU_INC.'lib/images/interwiki/'.$iw.'.png')){
345            echo "a.iw_$class {";
346            echo '  background-image: url('.DOKU_BASE.'lib/images/interwiki/'.$iw.'.png)';
347            echo '}';
348        }elseif(@file_exists(DOKU_INC.'lib/images/interwiki/'.$iw.'.gif')){
349            echo "a.iw_$class {";
350            echo '  background-image: url('.DOKU_BASE.'lib/images/interwiki/'.$iw.'.gif)';
351            echo '}';
352        }
353    }
354}
355
356/**
357 * Prints classes for file download links
358 *
359 * @author Andreas Gohr <andi@splitbrain.org>
360 */
361function css_filetypes(){
362
363    // default style
364    echo '.mediafile {';
365    echo ' background: transparent url('.DOKU_BASE.'lib/images/fileicons/file.png) 0px 1px no-repeat;';
366    echo ' padding-left: 18px;';
367    echo ' padding-bottom: 1px;';
368    echo '}';
369
370    // additional styles when icon available
371    // scan directory for all icons
372    $exts = array();
373    if($dh = opendir(DOKU_INC.'lib/images/fileicons')){
374        while(false !== ($file = readdir($dh))){
375            if(preg_match('/([_\-a-z0-9]+(?:\.[_\-a-z0-9]+)*?)\.(png|gif)/i',$file,$match)){
376                $ext = strtolower($match[1]);
377                $type = '.'.strtolower($match[2]);
378                if($ext!='file' && (!isset($exts[$ext]) || $type=='.png')){
379                    $exts[$ext] = $type;
380                }
381            }
382        }
383        closedir($dh);
384    }
385    foreach($exts as $ext=>$type){
386        $class = preg_replace('/[^_\-a-z0-9]+/','_',$ext);
387        echo ".mf_$class {";
388        echo '  background-image: url('.DOKU_BASE.'lib/images/fileicons/'.$ext.$type.')';
389        echo '}';
390    }
391}
392
393/**
394 * Loads a given file and fixes relative URLs with the
395 * given location prefix
396 */
397function css_loadfile($file,$location=''){
398    if(!@file_exists($file)) return '';
399    $css = io_readFile($file);
400    if(!$location) return $css;
401
402    $css = preg_replace('#(url\([ \'"]*)(?!/|data:|http://|https://| |\'|")#','\\1'.$location,$css);
403    $css = preg_replace('#(@import\s+[\'"])(?!/|data:|http://|https://)#', '\\1'.$location, $css);
404
405    return $css;
406}
407
408/**
409 * Converte local image URLs to data URLs if the filesize is small
410 *
411 * Callback for preg_replace_callback
412 */
413function css_datauri($match){
414    global $conf;
415
416    $pre   = unslash($match[1]);
417    $base  = unslash($match[2]);
418    $url   = unslash($match[3]);
419    $ext   = unslash($match[4]);
420
421    $local = DOKU_INC.$url;
422    $size  = @filesize($local);
423    if($size && $size < $conf['cssdatauri']){
424        $data = base64_encode(file_get_contents($local));
425    }
426    if($data){
427        $url = '\'data:image/'.$ext.';base64,'.$data.'\'';
428    }else{
429        $url = $base.$url;
430    }
431    return $pre.$url;
432}
433
434
435/**
436 * Returns a list of possible Plugin Styles (no existance check here)
437 *
438 * @author Andreas Gohr <andi@splitbrain.org>
439 */
440function css_pluginstyles($mediatype='screen'){
441    global $lang;
442    $list = array();
443    $plugins = plugin_list();
444    foreach ($plugins as $p){
445        $list[DOKU_PLUGIN."$p/$mediatype.css"]  = DOKU_BASE."lib/plugins/$p/";
446        $list[DOKU_PLUGIN."$p/$mediatype.less"]  = DOKU_BASE."lib/plugins/$p/";
447        // alternative for screen.css
448        if ($mediatype=='screen') {
449            $list[DOKU_PLUGIN."$p/style.css"]  = DOKU_BASE."lib/plugins/$p/";
450            $list[DOKU_PLUGIN."$p/style.less"]  = DOKU_BASE."lib/plugins/$p/";
451        }
452        // @deprecated 2012-04-09: rtl will cease to be a mode of its own,
453        //     please use "[dir=rtl]" in any css file in all, screen or print mode instead
454        if($lang['direction'] == 'rtl'){
455            $list[DOKU_PLUGIN."$p/rtl.css"] = DOKU_BASE."lib/plugins/$p/";
456        }
457    }
458    return $list;
459}
460
461/**
462 * Move all @import statements in a combined stylesheet to the top so they
463 * aren't ignored by the browser.
464 *
465 * @author Gabriel Birke <birke@d-scribe.de>
466 */
467function css_moveimports($css)
468{
469    if(!preg_match_all('/@import\s+(?:url\([^)]+\)|"[^"]+")\s*[^;]*;\s*/', $css, $matches, PREG_OFFSET_CAPTURE)) {
470        return $css;
471    }
472    $newCss  = "";
473    $imports = "";
474    $offset  = 0;
475    foreach($matches[0] as $match) {
476        $newCss  .= substr($css, $offset, $match[1] - $offset);
477        $imports .= $match[0];
478        $offset   = $match[1] + strlen($match[0]);
479    }
480    $newCss .= substr($css, $offset);
481    return $imports.$newCss;
482}
483
484/**
485 * Very simple CSS optimizer
486 *
487 * @author Andreas Gohr <andi@splitbrain.org>
488 */
489function css_compress($css){
490    //strip comments through a callback
491    $css = preg_replace_callback('#(/\*)(.*?)(\*/)#s','css_comment_cb',$css);
492
493    //strip (incorrect but common) one line comments
494    $css = preg_replace('/(?<!:)\/\/.*$/m','',$css);
495
496    // strip whitespaces
497    $css = preg_replace('![\r\n\t ]+!',' ',$css);
498    $css = preg_replace('/ ?([;,{}\/]) ?/','\\1',$css);
499    $css = preg_replace('/ ?: /',':',$css);
500
501    // shorten colors
502    $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);
503
504    return $css;
505}
506
507/**
508 * Callback for css_compress()
509 *
510 * Keeps short comments (< 5 chars) to maintain typical browser hacks
511 *
512 * @author Andreas Gohr <andi@splitbrain.org>
513 */
514function css_comment_cb($matches){
515    if(strlen($matches[2]) > 4) return '';
516    return $matches[0];
517}
518
519//Setup VIM: ex: et ts=4 :
520