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