xref: /dokuwiki/lib/exe/css.php (revision 0e65a4846cba3dee39cbd53b702a833788e6fec0)
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    $tpl = trim(preg_replace('/[^\w-]+/','',$INPUT->str('t')));
44    if($tpl){
45        $tplinc = DOKU_INC.'lib/tpl/'.$tpl.'/';
46        $tpldir = DOKU_BASE.'lib/tpl/'.$tpl.'/';
47    }else{
48        $tplinc = tpl_incdir();
49        $tpldir = tpl_basedir();
50    }
51
52    // The generated script depends on some dynamic options
53    $cache = new cache('styles'.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'].DOKU_BASE.$tplinc.$type,'.css');
54
55    // load template styles
56    $tplstyles = array();
57    if(@file_exists($tplinc.'style.ini')){
58        $ini = parse_ini_file($tplinc.'style.ini',true);
59        foreach($ini['stylesheets'] as $file => $mode){
60            $tplstyles[$mode][$tplinc.$file] = $tpldir;
61        }
62    }
63
64    // start output buffering
65    ob_start();
66
67    foreach($mediatypes as $mediatype) {
68        // Array of needed files and their web locations, the latter ones
69        // are needed to fix relative paths in the stylesheets
70        $files   = array();
71        // load core styles
72        $files[DOKU_INC.'lib/styles/'.$mediatype.'.css'] = DOKU_BASE.'lib/styles/';
73        // load jQuery-UI theme
74        if ($mediatype == 'screen') {
75            $files[DOKU_INC.'lib/scripts/jquery/jquery-ui-theme/smoothness.css'] = DOKU_BASE.'lib/scripts/jquery/jquery-ui-theme/';
76        }
77        // load plugin styles
78        $files = array_merge($files, css_pluginstyles($mediatype));
79        // load template styles
80        if (isset($tplstyles[$mediatype])) {
81            $files = array_merge($files, $tplstyles[$mediatype]);
82        }
83        // if old 'default' userstyle setting exists, make it 'screen' userstyle for backwards compatibility
84        if (isset($config_cascade['userstyle']['default'])) {
85            $config_cascade['userstyle']['screen'] = $config_cascade['userstyle']['default'];
86        }
87        // load user styles
88        if(isset($config_cascade['userstyle'][$mediatype])){
89            $files[$config_cascade['userstyle'][$mediatype]] = DOKU_BASE;
90        }
91        // load rtl styles
92        // note: this adds the rtl styles only to the 'screen' media type
93        // @deprecated 2012-04-09: rtl will cease to be a mode of its own,
94        //     please use "[dir=rtl]" in any css file in all, screen or print mode instead
95        if ($mediatype=='screen') {
96            if($lang['direction'] == 'rtl'){
97                if (isset($tplstyles['rtl'])) $files = array_merge($files, $tplstyles['rtl']);
98            }
99        }
100
101        $cache_files = array_merge(array_keys($files), getConfigFiles('main'));
102        $cache_files[] = $tplinc.'style.ini';
103        $cache_files[] = __FILE__;
104
105        // check cache age & handle conditional request
106        // This may exit if a cache can be used
107        http_cached($cache->cache,
108                    $cache->useCache(array('files' => $cache_files)));
109
110        // build the stylesheet
111
112        // print the default classes for interwiki links and file downloads
113        if ($mediatype == 'screen') {
114            css_interwiki();
115            css_filetypes();
116        }
117
118        // load files
119        $css_content = '';
120        foreach($files as $file => $location){
121            $css_content .= css_loadfile($file, $location);
122        }
123        switch ($mediatype) {
124            case 'screen':
125                print NL.'@media screen { /* START screen styles */'.NL.$css_content.NL.'} /* /@media END screen styles */'.NL;
126                break;
127            case 'print':
128                print NL.'@media print { /* START print styles */'.NL.$css_content.NL.'} /* /@media END print styles */'.NL;
129                break;
130            case 'all':
131            case 'feed':
132            default:
133                print NL.'/* START rest styles */ '.NL.$css_content.NL.'/* END rest styles */'.NL;
134                break;
135        }
136    }
137    // end output buffering and get contents
138    $css = ob_get_contents();
139    ob_end_clean();
140
141    // apply style replacements
142    $css = css_applystyle($css,$tplinc);
143
144    // place all @import statements at the top of the file
145    $css = css_moveimports($css);
146
147    // compress whitespace and comments
148    if($conf['compress']){
149        $css = css_compress($css);
150    }
151
152    // embed small images right into the stylesheet
153    if($conf['cssdatauri']){
154        $base = preg_quote(DOKU_BASE,'#');
155        $css = preg_replace_callback('#(url\([ \'"]*)('.$base.')(.*?(?:\.(png|gif)))#i','css_datauri',$css);
156    }
157
158    http_cached_finish($cache->cache, $css);
159}
160
161/**
162 * Does placeholder replacements in the style according to
163 * the ones defined in a templates style.ini file
164 *
165 * @author Andreas Gohr <andi@splitbrain.org>
166 */
167function css_applystyle($css,$tplinc){
168    if(@file_exists($tplinc.'style.ini')){
169        $ini = parse_ini_file($tplinc.'style.ini',true);
170        $css = strtr($css,$ini['replacements']);
171    }
172    return $css;
173}
174
175/**
176 * Prints classes for interwikilinks
177 *
178 * Interwiki links have two classes: 'interwiki' and 'iw_$name>' where
179 * $name is the identifier given in the config. All Interwiki links get
180 * an default style with a default icon. If a special icon is available
181 * for an interwiki URL it is set in it's own class. Both classes can be
182 * overwritten in the template or userstyles.
183 *
184 * @author Andreas Gohr <andi@splitbrain.org>
185 */
186function css_interwiki(){
187
188    // default style
189    echo 'a.interwiki {';
190    echo ' background: transparent url('.DOKU_BASE.'lib/images/interwiki.png) 0px 1px no-repeat;';
191    echo ' padding: 1px 0px 1px 16px;';
192    echo '}';
193
194    // additional styles when icon available
195    $iwlinks = getInterwiki();
196    foreach(array_keys($iwlinks) as $iw){
197        $class = preg_replace('/[^_\-a-z0-9]+/i','_',$iw);
198        if(@file_exists(DOKU_INC.'lib/images/interwiki/'.$iw.'.png')){
199            echo "a.iw_$class {";
200            echo '  background-image: url('.DOKU_BASE.'lib/images/interwiki/'.$iw.'.png)';
201            echo '}';
202        }elseif(@file_exists(DOKU_INC.'lib/images/interwiki/'.$iw.'.gif')){
203            echo "a.iw_$class {";
204            echo '  background-image: url('.DOKU_BASE.'lib/images/interwiki/'.$iw.'.gif)';
205            echo '}';
206        }
207    }
208}
209
210/**
211 * Prints classes for file download links
212 *
213 * @author Andreas Gohr <andi@splitbrain.org>
214 */
215function css_filetypes(){
216
217    // default style
218    echo '.mediafile {';
219    echo ' background: transparent url('.DOKU_BASE.'lib/images/fileicons/file.png) 0px 1px no-repeat;';
220    echo ' padding-left: 18px;';
221    echo ' padding-bottom: 1px;';
222    echo '}';
223
224    // additional styles when icon available
225    // scan directory for all icons
226    $exts = array();
227    if($dh = opendir(DOKU_INC.'lib/images/fileicons')){
228        while(false !== ($file = readdir($dh))){
229            if(preg_match('/([_\-a-z0-9]+(?:\.[_\-a-z0-9]+)*?)\.(png|gif)/i',$file,$match)){
230                $ext = strtolower($match[1]);
231                $type = '.'.strtolower($match[2]);
232                if($ext!='file' && (!isset($exts[$ext]) || $type=='.png')){
233                    $exts[$ext] = $type;
234                }
235            }
236        }
237        closedir($dh);
238    }
239    foreach($exts as $ext=>$type){
240        $class = preg_replace('/[^_\-a-z0-9]+/','_',$ext);
241        echo ".mf_$class {";
242        echo '  background-image: url('.DOKU_BASE.'lib/images/fileicons/'.$ext.$type.')';
243        echo '}';
244    }
245}
246
247/**
248 * Loads a given file and fixes relative URLs with the
249 * given location prefix
250 */
251function css_loadfile($file,$location=''){
252    if(!@file_exists($file)) return '';
253    $css = io_readFile($file);
254    if(!$location) return $css;
255
256    $css = preg_replace('#(url\([ \'"]*)(?!/|data:|http://|https://| |\'|")#','\\1'.$location,$css);
257    $css = preg_replace('#(@import\s+[\'"])(?!/|data:|http://|https://)#', '\\1'.$location, $css);
258
259    return $css;
260}
261
262/**
263 * Converte local image URLs to data URLs if the filesize is small
264 *
265 * Callback for preg_replace_callback
266 */
267function css_datauri($match){
268    global $conf;
269
270    $pre   = unslash($match[1]);
271    $base  = unslash($match[2]);
272    $url   = unslash($match[3]);
273    $ext   = unslash($match[4]);
274
275    $local = DOKU_INC.$url;
276    $size  = @filesize($local);
277    if($size && $size < $conf['cssdatauri']){
278        $data = base64_encode(file_get_contents($local));
279    }
280    if($data){
281        $url = 'data:image/'.$ext.';base64,'.$data;
282    }else{
283        $url = $base.$url;
284    }
285    return $pre.$url;
286}
287
288
289/**
290 * Returns a list of possible Plugin Styles (no existance check here)
291 *
292 * @author Andreas Gohr <andi@splitbrain.org>
293 */
294function css_pluginstyles($mediatype='screen'){
295    global $lang;
296    $list = array();
297    $plugins = plugin_list();
298    foreach ($plugins as $p){
299        $list[DOKU_PLUGIN."$p/$mediatype.css"]  = DOKU_BASE."lib/plugins/$p/";
300        // alternative for screen.css
301        if ($mediatype=='screen') {
302            $list[DOKU_PLUGIN."$p/style.css"]  = DOKU_BASE."lib/plugins/$p/";
303        }
304        // @deprecated 2012-04-09: rtl will cease to be a mode of its own,
305        //     please use "[dir=rtl]" in any css file in all, screen or print mode instead
306        if($lang['direction'] == 'rtl'){
307            $list[DOKU_PLUGIN."$p/rtl.css"] = DOKU_BASE."lib/plugins/$p/";
308        }
309    }
310    return $list;
311}
312
313/**
314 * Move all @import statements in a combined stylesheet to the top so they
315 * aren't ignored by the browser.
316 *
317 * @author Gabriel Birke <birke@d-scribe.de>
318 */
319function css_moveimports($css)
320{
321    if(!preg_match_all('/@import\s+(?:url\([^)]+\)|"[^"]+")\s*[^;]*;\s*/', $css, $matches, PREG_OFFSET_CAPTURE)) {
322        return $css;
323    }
324    $newCss  = "";
325    $imports = "";
326    $offset  = 0;
327    foreach($matches[0] as $match) {
328        $newCss  .= substr($css, $offset, $match[1] - $offset);
329        $imports .= $match[0];
330        $offset   = $match[1] + strlen($match[0]);
331    }
332    $newCss .= substr($css, $offset);
333    return $imports.$newCss;
334}
335
336/**
337 * Very simple CSS optimizer
338 *
339 * @author Andreas Gohr <andi@splitbrain.org>
340 */
341function css_compress($css){
342    //strip comments through a callback
343    $css = preg_replace_callback('#(/\*)(.*?)(\*/)#s','css_comment_cb',$css);
344
345    //strip (incorrect but common) one line comments
346    $css = preg_replace('/(?<!:)\/\/.*$/m','',$css);
347
348    // strip whitespaces
349    $css = preg_replace('![\r\n\t ]+!',' ',$css);
350    $css = preg_replace('/ ?([;,{}\/]) ?/','\\1',$css);
351    $css = preg_replace('/ ?: /',':',$css);
352
353    // shorten colors
354    $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);
355
356    return $css;
357}
358
359/**
360 * Callback for css_compress()
361 *
362 * Keeps short comments (< 5 chars) to maintain typical browser hacks
363 *
364 * @author Andreas Gohr <andi@splitbrain.org>
365 */
366function css_comment_cb($matches){
367    if(strlen($matches[2]) > 4) return '';
368    return $matches[0];
369}
370
371//Setup VIM: ex: et ts=4 :
372