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 = array();
33    var $box_content = array();
34
35    // the following are used in rendering and are set by _xhtml_boxopen()
36    var $_xb_colours      = array();
37    var $_content_colours = '';
38    var $_title_colours   = '';
39
40    function getType(){ return 'protected';}
41    function getAllowedTypes() { return array('container','substition','protected','disabled','formatting','paragraphs'); }
42    function getPType(){ return 'block';}
43
44    // must return a number lower than returned by native 'code' mode (200)
45    function getSort(){ return 195; }
46
47    // override default accepts() method to allow nesting
48    // - ie, to get the plugin accepts its own entry syntax
49    function accepts($mode) {
50        if ($mode == substr(get_class($this), 7)) return true;
51
52        return parent::accepts($mode);
53    }
54
55    /**
56     * Connect pattern to lexer
57     */
58    function connectTo($mode) {
59        $this->Lexer->addEntryPattern('<box>(?=.*?</box.*?>)',$mode,'plugin_box2');
60        $this->Lexer->addEntryPattern('<box\s[^\r\n\|]*?>(?=.*?</box.*?>)',$mode,'plugin_box2');
61        $this->Lexer->addEntryPattern('<box\|(?=[^\r\n]*?\>.*?</box.*?\>)',$mode,'plugin_box2');
62        $this->Lexer->addEntryPattern('<box\s[^\r\n\|]*?\|(?=[^\r\n]*?>.*?</box.*?>)',$mode,'plugin_box2');
63    }
64
65    function postConnect() {
66        $this->Lexer->addPattern('>', 'plugin_box2');
67        $this->Lexer->addExitPattern('</box.*?>', 'plugin_box2');
68    }
69
70    private function isTitleMode() {
71        return $this->title_mode[count($this->title_mode)-1];
72    }
73
74    private function isBoxContent() {
75        return $this->box_content[count($this->box_content)-1];
76    }
77
78    /**
79     * Handle the match
80     */
81    function handle($match, $state, $pos, Doku_Handler $handler){
82
83        switch ($state) {
84            case DOKU_LEXER_ENTER:
85                $data = $this->_boxstyle(trim(substr($match, 4, -1)));
86                if (substr($match, -1) == '|') {
87                    $this->title_mode[] = true;
88                    return array('title_open',$data, $pos);
89                } else {
90                    return array('box_open',$data, $pos);
91                }
92
93            case DOKU_LEXER_MATCHED:
94                if ($this->isTitleMode()) {
95                    array_pop( $this->title_mode );
96                    return array('box_open','', $pos);
97                } else {
98                    return array('data', $match, $pos);
99                }
100
101            case DOKU_LEXER_UNMATCHED:
102                if ($this->isTitleMode()) {
103                    return array('title', $match, $pos);
104                }
105
106                $handler->_addCall('cdata',array($match), $pos);
107                return false;
108            case DOKU_LEXER_EXIT:
109                $pos += strlen($match); // has to be done becvause the ending tag comes after $pos
110                $data = trim(substr($match, 5, -1));
111                $title =  ($data && $data[0] == "|") ? substr($data,1) : '';
112
113                return array('box_close', $title, $pos);
114
115        }
116        return false;
117    }
118
119    /**
120     * Create output
121     */
122    function render($mode, Doku_Renderer $renderer, $indata) {
123        global $ID, $ACT;
124
125        // $pos is for the current position in the wiki page
126        if (empty($indata)) return false;
127        list($instr, $data, $pos) = $indata;
128
129        if($mode == 'xhtml'){
130            switch ($instr) {
131                case 'title_open' :
132                    $this->title_mode[] = true;
133                    $renderer->doc .= $this->_xhtml_boxopen($renderer, $pos, $data);
134                    $renderer->doc .= '<h2 class="box_title"' . $this->_title_colours . '>';
135                    break;
136
137                case 'box_open' :
138                    if ($this->isTitleMode()) {
139                        array_pop( $this->title_mode );
140                        $this->box_content[] = true;
141                        $renderer->doc .= "</h2>\n<div class=\"box_content\"" . $this->_content_colours . '>';
142                    } else {
143                        $renderer->doc .= $this->_xhtml_boxopen($renderer, $pos, $data);
144
145                        if ( strlen( $this->_content_colours ) > 0 ) {
146	                        $this->box_content[] = true;
147							$renderer->doc .= '<div class="box_content"' . $this->_content_colours . '>';
148						}
149                    }
150                    break;
151
152                case 'title':
153                case 'data' :
154                    $output = $renderer->_xmlEntities($data);
155
156                    if ( $this->isTitleMode() ) {
157                        $hid = $renderer->_headerToLink($output,true);
158                        $renderer->doc .= '<a id="' . $hid . '" name="' . $hid . '">' . $output . '</a>';
159                        break;
160                    }
161
162                    $renderer->doc .= $output;
163                    break;
164
165                case 'box_close' :
166					if ( $this->isBoxContent() ) {
167                        array_pop( $this->box_content );
168	                    $renderer->doc .= "</div>\n";
169					}
170
171                    if ($data) {
172                        $renderer->doc .= '<p class="box_caption"' . $this->_title_colours . '>' . $renderer->_xmlEntities($data) . "</p>\n";
173                    }
174
175                    // insert the section edit button befor the box is closed - array_pop makes sure we take the last box
176                    if ( method_exists($renderer, "finishSectionEdit") ) {
177                        $renderer->nocache();
178                        $renderer->finishSectionEdit($pos);
179                    }
180
181                    $renderer->doc .= "\n" . $this->_xhtml_boxclose();
182
183                    break;
184            }
185
186            return true;
187        }
188        return false;
189    }
190
191    function _boxstyle($str) {
192        if (!strlen($str)) return array();
193
194        $styles = array();
195
196        $tokens = preg_split('/\s+/', $str, 9);                      // limit is defensive
197        foreach ($tokens as $token) {
198            if (preg_match('/^\d*\.?\d+(%|px|em|ex|pt|cm|mm|pi|in)$/', $token)) {
199                $styles['width'] = $token;
200                continue;
201            }
202
203            if (preg_match('/^(
204              (\#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))|        #colorvalue
205              (rgb\(([0-9]{1,3}%?,){2}[0-9]{1,3}%?\))     #rgb triplet
206              )$/x', $token)) {
207            if (preg_match('/^#[A-Za-z0-9_-]+$/', $token)) {
208                $styles['id'] = substr($token, 1);
209                continue;
210            }
211
212                $styles['colour'][] = $token;
213                continue;
214            }
215
216            if ( preg_match('/^(margin|padding)(-(left|right|top|bottom))?:\d+(%|px|em|ex|pt|cm|mm|pi|in)$/', $token)) {
217                $styles['spacing'][] = $token;
218            }
219
220            // restrict token (class names) characters to prevent any malicious data
221            if (preg_match('/[^A-Za-z0-9_-]/',$token)) continue;
222            $styles['class'] = (isset($styles['class']) ? $styles['class'].' ' : '').$token;
223        }
224        if (!empty($styles['colour'])) {
225            $styles['colour'] = $this->_box_colours($styles['colour']);
226        }
227
228        return $styles;
229    }
230
231    function _box_colours($colours) {
232        $triplets = array();
233
234        // only need the first four colours
235        if (count($colours) > 4) $colours = array_slice($colours,0,4);
236        foreach ($colours as $colour) {
237            $triplet[] = $this->_colourToTriplet($colour);
238        }
239
240        // there must be one colour to get here - the primary background
241        // calculate title background colour if not present
242        if (empty($triplet[1])) {
243            $triplet[1] = $triplet[0];
244        }
245
246        // calculate outer background colour if not present
247        if (empty($triplet[2])) {
248            $triplet[2] = $triplet[0];
249        }
250
251        // calculate border colour if not present
252        if (empty($triplet[3])) {
253            $triplet[3] = $triplet[0];
254        }
255
256        // convert triplets back to style sheet colours
257        $style_colours['content_background'] = 'rgb('.join(',',$triplet[0]).')';
258        $style_colours['title_background'] = 'rgb('.join(',',$triplet[1]).')';
259        $style_colours['outer_background'] = 'rgb('.join(',',$triplet[2]).')';
260        $style_colours['borders'] = 'rgb('.join(',',$triplet[3]).')';
261
262        return $style_colours;
263    }
264
265    function _colourToTriplet($colour) {
266        if ($colour[0] == '#') {
267            if (strlen($colour) == 4) {
268                // format #FFF
269                return array(hexdec($colour[1].$colour[1]),hexdec($colour[2].$colour[2]),hexdec($colour[3].$colour[3]));
270            } else {
271                // format #FFFFFF
272                return array(hexdec(substr($colour,1,2)),hexdec(substr($colour,3,2)), hexdec(substr($colour,5,2)));
273            }
274        } else {
275            // format rgb(x,y,z)
276            return explode(',',substr($colour,4,-1));
277        }
278    }
279
280    function _xhtml_boxopen($renderer, $pos, $styles) {
281        $class = 'class="box' . (isset($styles['class']) ? ' '.$styles['class'] : '') . (method_exists($renderer, "startSectionEdit") ? " " . $renderer->startSectionEdit($pos, array( 'target' => 'section', 'name' => 'box-' . $pos)) : "") . '"';
282        $style = isset($styles['width']) ? "width: {$styles['width']};" : '';
283        $style .= isset($styles['spacing']) ? implode(';', $styles['spacing']) : '';
284
285        if (isset($styles['colour'])) {
286            $style .= 'background-color:'.$styles['colour']['outer_background'].';';
287            $style .= 'border-color: '.$styles['colour']['borders'].';';
288
289            $this->_content_colours = 'style="background-color: '.$styles['colour']['content_background'].'; border-color: '.$styles['colour']['borders'].'"';
290            $this->_title_colours = 'style="background-color: '.$styles['colour']['title_background'].';"';
291
292        } else {
293            $this->_content_colours = '';
294            $this->_title_colours = '';
295        }
296
297        if (strlen($style)) $style = ' style="'.$style.'"';
298        if (array_key_exists('id', $styles)) {
299            $class = 'id="' . $styles['id'] . '" ' . $class;
300        }
301
302        $this->_xb_colours[] = $colours;
303
304        $html = "<div $class$style>\n";
305
306        // Don't do box extras if there is no style for them
307        if ( !empty($colours) ) {
308            $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";
309            $html .= '<div class="xbox"' . $colours . ">\n";
310        }
311
312        return $html;
313    }
314
315    function _xhtml_boxclose() {
316
317        $colours = array_pop($this->_xb_colours);
318
319        // Don't do box extras if there is no style for them
320        if ( !empty($colours) ) {
321            $html = "</div>\n";
322            $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";
323        }
324        $html .= '</div> <!-- Extras -->' . "\n";
325
326        return $html;
327    }
328
329}
330
331//Setup VIM: ex: et ts=4 enc=utf-8 :