1<?php
2/**
3 * Box Plugin: Draw highlighting boxes around wiki markup
4 *
5 * Syntax:     <box w=width% h=height% classes|title>
6 *   width in %/px/...
7 *   height in %/px/..
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     Joachim Schäfer <mail@joachimschaefer.de>, based on the box plugin by Christopher Smith <chris@jalakai.co.uk>
18 */
19
20if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
21if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
22require_once(DOKU_PLUGIN.'syntax.php');
23
24/**
25 * All DokuWiki plugins to extend the parser/rendering mechanism
26 * need to inherit from this class
27 */
28class syntax_plugin_box extends DokuWiki_Syntax_Plugin {
29
30    var $title_mode = false;
31
32    // the following are used in rendering and are set by _xhtml_boxopen()
33    var $_xb_colours      = '';
34    var $_content_colours = '';
35    var $_title_colours   = '';
36
37    /**
38     * return some info
39     */
40    function getInfo(){
41      return array(
42        'author' => 'Christopher Smith',
43        'email'  => 'chris@jalakai.co.uk',
44        'date'   => '2008-11-11',
45        'name'   => 'Box Plugin',
46        'desc'   => 'Boxes with titles, colour and rounded corners.
47                     Syntax: <box width class colours|title> ... </box|caption>
48                     width, class, colours title & caption are optional.
49                     The title can include some wiki markup, the box
50                     contents can include almost any wiki markup.',
51        'url'    => 'http://www.dokuwiki.org/plugin:boxes',
52      );
53    }
54
55    function getType(){ return 'protected';}
56    function getAllowedTypes() { return array('container','substition','protected','disabled','formatting','paragraphs'); }
57    function getPType(){ return 'block';}
58
59    // must return a number lower than returned by native 'code' mode (200)
60    function getSort(){ return 195; }
61
62    // override default accepts() method to allow nesting
63    // - ie, to get the plugin accepts its own entry syntax
64    function accepts($mode) {
65        if ($mode == substr(get_class($this), 7)) return true;
66
67        return parent::accepts($mode);
68    }
69
70    /**
71     * Connect pattern to lexer
72     */
73    function connectTo($mode) {
74      $this->Lexer->addEntryPattern('<box>(?=.*?</box.*?>)',$mode,'plugin_box');
75      $this->Lexer->addEntryPattern('<box\s[^\r\n\|]*?>(?=.*?</box.*?>)',$mode,'plugin_box');
76      $this->Lexer->addEntryPattern('<box\|(?=[^\r\n]*?\>.*?</box.*?\>)',$mode,'plugin_box');
77      $this->Lexer->addEntryPattern('<box\s[^\r\n\|]*?\|(?=[^\r\n]*?>.*?</box.*?>)',$mode,'plugin_box');
78    }
79
80    function postConnect() {
81      $this->Lexer->addPattern('>', 'plugin_box');
82      $this->Lexer->addExitPattern('</box.*?>', 'plugin_box');
83    }
84
85    /**
86     * Handle the match
87     */
88    function handle($match, $state, $pos, &$handler){
89
90        switch ($state) {
91            case DOKU_LEXER_ENTER:
92                $data = $this->_boxstyle(trim(substr($match, 4, -1)));
93                if (substr($match, -1) == '|') {
94                    $this->title_mode = true;
95                    return array('title_open',$data);
96                } else {
97                    return array('box_open',$data);
98                }
99
100            case DOKU_LEXER_MATCHED:
101                if ($this->title_mode) {
102                    $this->title_mode = false;
103                    return array('box_open','');
104                } else {
105                    return array('data', $match);
106                }
107
108            case DOKU_LEXER_UNMATCHED:
109                $handler->_addCall('cdata',array($match), $pos);
110                return false;
111
112            case DOKU_LEXER_EXIT:
113                $data = trim(substr($match, 5, -1));
114                $title =  ($data && $data{0} == "|") ? substr($data,1) : '';
115
116                return array('box_close', $title);
117
118        }
119        return false;
120    }
121
122    /**
123     * Create output
124     */
125    function render($mode, &$renderer, $indata) {
126
127      if (empty($indata)) return false;
128      list($instr, $data) = $indata;
129
130      if($mode == 'xhtml'){
131          switch ($instr) {
132          case 'title_open' :
133            $this->title_mode = true;
134            $renderer->doc .= $this->_xhtml_boxopen($data)."<p class='box_title'{$this->_title_colours}>";
135            break;
136
137          case 'box_open' :
138            if ($this->title_mode) {
139              $this->title_mode = false;
140              $renderer->doc .= "</p>\n<div class='box_content'{$this->_content_colours}>";
141            } else {
142              $renderer->doc .= $this->_xhtml_boxopen($data)."<div class='box_content'{$this->_content_colours}>";
143            }
144            break;
145
146          case 'data' :
147            $renderer->doc .= $renderer->_xmlEntities($data);
148            break;
149
150          case 'box_close' :
151            $renderer->doc .= "</div>\n";
152
153            if ($data) {
154              $renderer->doc .= "<p class='box_caption'{$this->_title_colours}>".$renderer->_xmlEntities($data)."</p>\n";
155            }
156            $renderer->doc .= $this->_xhtml_boxclose();
157            break;
158        }
159
160        return true;
161      }
162      return false;
163    }
164
165    function _boxstyle($str) {
166      if (!strlen($str)) return array();
167
168      $styles = array();
169
170      $tokens = preg_split('/\s+/', $str, 9);                      // limited
171      foreach ($tokens as $token) {
172	  // the new syntax requires a w= in front of the width parameter, and a h= in front of height
173	  // the two bytes are substracted from the token
174          if (preg_match('/^w=\d*\.?\d+(%|px|em|ex|pt|cm|mm|pi|in)$/', $token)) {
175            // $styles['width'] = $token;
176            $styles['width'] = substr($token,2);
177            continue;
178          }
179
180		  if (preg_match('/^h=\d*\.?\d+(%|px|em|ex|pt|cm|mm|pi|in)$/', $token)) {
181            // $styles['width'] = $token;
182            $styles['height'] = substr($token,2);
183            continue;
184          }
185
186          if (preg_match('/^(
187              (\#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))|        #colorvalue
188              (rgb\(([0-9]{1,3}%?,){2}[0-9]{1,3}%?\))     #rgb triplet
189              )$/x', $token)) {
190            $styles['colour'][] = $token;
191            continue;
192          }
193
194          // restrict token (class names) characters to prevent any malicious data
195          if (preg_match('/[^A-Za-z0-9_-]/',$token)) continue;
196          $styles['class'] = (isset($styles['class']) ? $styles['class'].' ' : '').$token;
197      }
198      if (!empty($styles['colour'])) {
199        $styles['colour'] = $this->_box_colours($styles['colour']);
200      }
201
202      return $styles;
203    }
204
205    function _box_colours($colours) {
206      $triplets = array();
207
208      // only need the first four colours
209      if (count($colours) > 4) $colours = array_slice($colours,0,4);
210      foreach ($colours as $colour) {
211        $triplet[] = $this->_colourToTriplet($colour);
212      }
213
214      // there must be one colour to get here - the primary background
215      // calculate title background colour if not present
216      if (empty($triplet[1])) {
217        $triplet[1] = $triplet[0];
218      }
219
220      // calculate outer background colour if not present
221      if (empty($triplet[2])) {
222        $triplet[2] = $triplet[0];
223      }
224
225      // calculate border colour if not present
226      if (empty($triplet[3])) {
227        $triplet[3] = $triplet[0];
228      }
229
230      // convert triplets back to style sheet colours
231      $style_colours['content_background'] = 'rgb('.join(',',$triplet[0]).')';
232      $style_colours['title_background'] = 'rgb('.join(',',$triplet[1]).')';
233      $style_colours['outer_background'] = 'rgb('.join(',',$triplet[2]).')';
234      $style_colours['borders'] = 'rgb('.join(',',$triplet[3]).')';
235
236      return $style_colours;
237    }
238
239    function _colourToTriplet($colour) {
240      if ($colour{0} == '#') {
241        if (strlen($colour) == 4) {
242          // format #FFF
243          return array(hexdec($colour{1}.$colour{1}),hexdec($colour{2}.$colour{2}),hexdec($colour{3}.$colour{3}));
244        } else {
245          // format #FFFFFF
246          return array(hexdec(substr($colour,1,2)),hexdec(substr($colour,3,2)), hexdec(substr($colour,5,2)));
247        }
248      } else {
249        // format rgb(x,y,z)
250        return explode(',',substr($colour,4,-1));
251      }
252    }
253
254    function _xhtml_boxopen($styles) {
255      $class = 'class="box' . (isset($styles['class']) ? ' '.$styles['class'] : '') . '"';
256      if (isset($styles['width'])) {
257          if (isset($styles['height'])) {
258      	  // $style = (isset($styles['width']) && isset($styles['height'])) ? "width: {$styles['width']}; height: {$styles['height']};" : '';
259          $style = "width: {$styles['width']}; height: {$styles['height']};";
260      	  }
261      	  else {
262          $style = "width: {$styles['width']};";
263     	  }
264      }
265      else {
266          if (isset($styles['height'])) {
267          $style = "height: {$styles['height']};";
268          }
269      }
270
271      if (isset($styles['colour'])) {
272        $colours = 'background-color: '.$styles['colour']['outer_background'].'; ';
273        $colours .= 'border-color: '.$styles['colour']['borders'].';';
274
275        $this->_content_colours = 'style="background-color: '.$styles['colour']['content_background'].'; border-color: '.$styles['colour']['borders'].'"';
276        $this->_title_colours = 'style="background-color: '.$styles['colour']['title_background'].';"';
277
278      } else {
279        $colours = '';
280
281        $this->_content_colours = '';
282        $this->_title_colours = '';
283      }
284
285      if ($style || $colours) $style = ' style="'.$style.' '.$colours.'"';
286      if ($colours) $colours = ' style="'.$colours.'"';
287
288      $this->_xb_colours = $colours;
289
290      $html = "<div $class$style>\n";
291      $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";
292      $html .="  <div class='xbox'$colours>\n";
293
294      return $html;
295    }
296
297    function _xhtml_boxclose() {
298
299      $colours = $this->_xb_colours;
300
301      $html = "  </div>\n";
302      $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";
303      $html .= "</div>\n";
304
305      return $html;
306    }
307
308}
309
310//Setup VIM: ex: et ts=4 enc=utf-8 :