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