xref: /plugin/box2/syntax.php (revision 271e2c1e7a8d1ecf7d7e9c7559f29d116dc640be)
1<?php
2/**
3 * Box Plugin: Draw highlighting boxes around wiki markup
4 *
5 * Syntax:     <box width% classes|title>
6 *   width%    width of the box, must use % unit
7 *   classes   one or more classes used to style the box, several predefined styles included in style.css
8 *   padding   can be defined with each direction or as composite
9 *   margin    can be defined with each direction or as composite
10 *   title     (optional) all text after '|' will be rendered above the main code text with a
11 *             different style.
12 *
13 * Acknowledgements:
14 *  Rounded corners based on snazzy borders by Stu Nicholls (http://www.cssplay.co.uk/boxes/snazzy)
15 *  which is in turn based on nifty corners by Alessandro Fulciniti (http://pro.html.it/esempio/nifty/)
16 *
17 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
18 * @author     Christopher Smith <chris@jalakai.co.uk>
19 * @author     i-net software <tools@inetsoftware.de>
20 */
21
22if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
23if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
24require_once(DOKU_PLUGIN.'syntax.php');
25
26/**
27 * All DokuWiki plugins to extend the parser/rendering mechanism
28 * need to inherit from this class
29 */
30class syntax_plugin_box2 extends DokuWiki_Syntax_Plugin {
31
32    var $title_mode = false;
33    var $title_pos = array();
34    var $title_name = array();
35
36    // the following are used in rendering and are set by _xhtml_boxopen()
37    var $_xb_colours      = array();
38    var $_content_colours = '';
39    var $_title_colours   = '';
40
41    function getType(){ return 'protected';}
42    function getAllowedTypes() { return array('container','substition','protected','disabled','formatting','paragraphs'); }
43    function getPType(){ return 'block';}
44
45    // must return a number lower than returned by native 'code' mode (200)
46    function getSort(){ return 195; }
47
48    // override default accepts() method to allow nesting
49    // - ie, to get the plugin accepts its own entry syntax
50    function accepts($mode) {
51        if ($mode == substr(get_class($this), 7)) return true;
52
53        return parent::accepts($mode);
54    }
55
56    /**
57     * Connect pattern to lexer
58     */
59    function connectTo($mode) {
60        $this->Lexer->addEntryPattern('<box>(?=.*?</box.*?>)',$mode,'plugin_box2');
61        $this->Lexer->addEntryPattern('<box\s[^\r\n\|]*?>(?=.*?</box.*?>)',$mode,'plugin_box2');
62        $this->Lexer->addEntryPattern('<box\|(?=[^\r\n]*?\>.*?</box.*?\>)',$mode,'plugin_box2');
63        $this->Lexer->addEntryPattern('<box\s[^\r\n\|]*?\|(?=[^\r\n]*?>.*?</box.*?>)',$mode,'plugin_box2');
64    }
65
66    function postConnect() {
67        $this->Lexer->addPattern('>', 'plugin_box2');
68        $this->Lexer->addExitPattern('</box.*?>', 'plugin_box2');
69    }
70
71    /**
72     * Handle the match
73     */
74    function handle($match, $state, $pos, Doku_Handler $handler){
75
76        switch ($state) {
77            case DOKU_LEXER_ENTER:
78                $data = $this->_boxstyle(trim(substr($match, 4, -1)));
79                if (substr($match, -1) == '|') {
80                    $this->title_mode = true;
81                    return array('title_open',$data, $pos);
82                } else {
83                    return array('box_open',$data, $pos);
84                }
85
86            case DOKU_LEXER_MATCHED:
87                if ($this->title_mode) {
88                    $this->title_mode = false;
89                    return array('box_open','', $pos);
90                } else {
91                    return array('data', $match, $pos);
92                }
93
94            case DOKU_LEXER_UNMATCHED:
95                if ($this->title_mode) {
96                    return array('data', $match, $pos);
97                }
98
99                $handler->_addCall('cdata',array($match), $pos);
100                return false;
101            case DOKU_LEXER_EXIT:
102                $pos += strlen($match); // has to be done becvause the ending tag comes after $pos
103                $data = trim(substr($match, 5, -1));
104                $title =  ($data && $data{0} == "|") ? substr($data,1) : '';
105
106                return array('box_close', $title, $pos);
107
108        }
109        return false;
110    }
111
112    /**
113     * Create output
114     */
115    function render($mode, Doku_Renderer $renderer, $indata) {
116        global $ID, $ACT;
117
118        // $pos is for the current position in the wiki page
119        if (empty($indata)) return false;
120        list($instr, $data, $pos) = $indata;
121
122        if($mode == 'xhtml'){
123            switch ($instr) {
124                case 'title_open' :
125                    $this->title_mode = true;
126                    $this->title_pos[] = $pos; // Start Position for Section Editing
127                    $renderer->doc .= $this->_xhtml_boxopen($data);
128                    $renderer->doc .= "<h2 class='box_title " . (method_exists($renderer, "finishSectionEdit") ? $renderer->startSectionEdit($pos, 'section', 'box') : "") . " '{$this->_title_colours}>";
129                    break;
130
131                case 'box_open' :
132                    if ($this->title_mode) {
133                        $this->title_mode = false;
134                        $renderer->doc .= "</h2>\n<div class='box_content'{$this->_content_colours}>";
135                    } else {
136                        $this->title_pos[] = $pos; // Start Position for Section Editing
137                        $this->title_name[] = 'box_' . 'no-title' . '_' . md5(time());
138                        $renderer->doc .= $this->_xhtml_boxopen($data)."<div class='box_content'{$this->_content_colours}>";
139                    }
140                    break;
141
142                case 'data' :
143                    $output = $renderer->_xmlEntities($data);
144
145                    if ( $this->title_mode ) {
146                        $this->title_name[] = 'box_' . cleanID($output) . '_' . md5($output);
147                        $hid = $renderer->_headerToLink($output,true);
148                        $renderer->doc .= '<a id="' . $hid . '" name="' . $hid . '">' . $output . '</a>';
149                        break;
150                    }
151
152                    $renderer->doc .= $output;
153                    break;
154
155                case 'box_close' :
156                    $renderer->doc .= "</div>\n";
157
158                    if ($data) {
159                        $renderer->doc .= "<p class='box_caption'{$this->_title_colours}>".$renderer->_xmlEntities($data)."</p>\n";
160                    }
161
162                    // insert the section edit button befor the box is closed - array_pop makes sure we take the last box
163                    if ( $this->getConf('allowSectionEdit') && $ACT != 'preview' ) {
164                        $renderer->nocache();
165
166
167                        if ( auth_quickaclcheck($ID) > AUTH_READ ) {
168                            $title = array_pop($this->title_name); // Clean up
169                            if ( method_exists($renderer, "finishSectionEdit") ) {
170                                $renderer->finishSectionEdit($pos);
171                            }
172                        }
173                    }
174
175                    $renderer->doc .= $this->_xhtml_boxclose();
176
177                    break;
178            }
179
180            return true;
181        }
182        return false;
183    }
184
185    function _boxstyle($str) {
186        if (!strlen($str)) return array();
187
188        $styles = array();
189
190        $tokens = preg_split('/\s+/', $str, 9);                      // limit is defensive
191        foreach ($tokens as $token) {
192            if (preg_match('/^\d*\.?\d+(%|px|em|ex|pt|cm|mm|pi|in)$/', $token)) {
193                $styles['width'] = $token;
194                continue;
195            }
196
197            if (preg_match('/^(
198              (\#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))|        #colorvalue
199              (rgb\(([0-9]{1,3}%?,){2}[0-9]{1,3}%?\))     #rgb triplet
200              )$/x', $token)) {
201                $styles['colour'][] = $token;
202                continue;
203            }
204
205            if ( preg_match('/^(margin|padding)(-(left|right|top|bottom))?:\d+(%|px|em|ex|pt|cm|mm|pi|in)$/', $token)) {
206                $styles['spacing'][] = $token;
207            }
208
209            // restrict token (class names) characters to prevent any malicious data
210            if (preg_match('/[^A-Za-z0-9_-]/',$token)) continue;
211            $styles['class'] = (isset($styles['class']) ? $styles['class'].' ' : '').$token;
212        }
213        if (!empty($styles['colour'])) {
214            $styles['colour'] = $this->_box_colours($styles['colour']);
215        }
216
217        return $styles;
218    }
219
220    function _box_colours($colours) {
221        $triplets = array();
222
223        // only need the first four colours
224        if (count($colours) > 4) $colours = array_slice($colours,0,4);
225        foreach ($colours as $colour) {
226            $triplet[] = $this->_colourToTriplet($colour);
227        }
228
229        // there must be one colour to get here - the primary background
230        // calculate title background colour if not present
231        if (empty($triplet[1])) {
232            $triplet[1] = $triplet[0];
233        }
234
235        // calculate outer background colour if not present
236        if (empty($triplet[2])) {
237            $triplet[2] = $triplet[0];
238        }
239
240        // calculate border colour if not present
241        if (empty($triplet[3])) {
242            $triplet[3] = $triplet[0];
243        }
244
245        // convert triplets back to style sheet colours
246        $style_colours['content_background'] = 'rgb('.join(',',$triplet[0]).')';
247        $style_colours['title_background'] = 'rgb('.join(',',$triplet[1]).')';
248        $style_colours['outer_background'] = 'rgb('.join(',',$triplet[2]).')';
249        $style_colours['borders'] = 'rgb('.join(',',$triplet[3]).')';
250
251        return $style_colours;
252    }
253
254    function _colourToTriplet($colour) {
255        if ($colour{0} == '#') {
256            if (strlen($colour) == 4) {
257                // format #FFF
258                return array(hexdec($colour{1}.$colour{1}),hexdec($colour{2}.$colour{2}),hexdec($colour{3}.$colour{3}));
259            } else {
260                // format #FFFFFF
261                return array(hexdec(substr($colour,1,2)),hexdec(substr($colour,3,2)), hexdec(substr($colour,5,2)));
262            }
263        } else {
264            // format rgb(x,y,z)
265            return explode(',',substr($colour,4,-1));
266        }
267    }
268
269    function _xhtml_boxopen($styles) {
270        $class = 'class="box' . (isset($styles['class']) ? ' '.$styles['class'] : '') . '"';
271        $style = isset($styles['width']) ? "width: {$styles['width']};" : '';
272        $style .= isset($styles['spacing']) ? implode(';', $styles['spacing']) : '';
273
274        if (isset($styles['colour'])) {
275            $style .= 'background-color:'.$styles['colour']['outer_background'].';';
276            $style .= 'border-color: '.$styles['colour']['borders'].';';
277
278            $this->_content_colours = 'style="background-color: '.$styles['colour']['content_background'].'; border-color: '.$styles['colour']['borders'].'"';
279            $this->_title_colours = 'style="background-color: '.$styles['colour']['title_background'].';"';
280
281        } else {
282            $this->_content_colours = '';
283            $this->_title_colours = '';
284        }
285
286        if (strlen($style)) $style = ' style="'.$style.'"';
287
288        $this->_xb_colours[] = $colours;
289
290        $html = "<div $class$style>\n";
291
292        // Don't do box extras if there is no style for them
293        if ( !empty($colours) ) {
294            $html .="  <b class='xtop'><b class='xb1'$colours>&nbsp;</b><b class='xb2'$colours>&nbsp;</b><b class='xb3'$colours>&nbsp;</b><b class='xb4'$colours>&nbsp;</b></b>\n";
295            $html .="  <div class='xbox'$colours>\n";
296        }
297
298        return $html;
299    }
300
301    function _xhtml_boxclose() {
302
303        $colours = array_pop($this->_xb_colours);
304
305        // Don't do box extras if there is no style for them
306        if ( !empty($colours) ) {
307            $html = "  </div>\n";
308            $html .= "  <b class='xbottom'><b class='xb4'$colours>&nbsp;</b><b class='xb3'$colours>&nbsp;</b><b class='xb2'$colours>&nbsp;</b><b class='xb1'$colours>&nbsp;</b></b>\n";
309        }
310        $html .= "</div> <!-- Extras -->\n";
311
312        return $html;
313    }
314
315}
316
317//Setup VIM: ex: et ts=4 enc=utf-8 :