xref: /plugin/box2/syntax.php (revision bd01c8ec14d1117f4ace103a8b4b80a4101bb9e1)
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 * @author     i-net software <tools@inetsoftware.de>
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	var $title_pos = array();
32	var $title_name = array();
33
34	// the following are used in rendering and are set by _xhtml_boxopen()
35	var $_xb_colours      = array();
36	var $_content_colours = '';
37	var $_title_colours   = '';
38
39	/**
40	 * return some info
41	 */
42	function getInfo(){
43		return array(
44        'author' => 'Christopher Smith / i-net software',
45        'email'  => 'tools@inetsoftware.de',
46        'date'   => '2010-04-21 (original 2008-11-11)',
47        'name'   => 'Box Plugin',
48        'desc'   => 'Boxes with titles, colour and rounded corners.
49                     Syntax: <box width class colours|title> ... </box|caption>
50                     width, class, colours title & caption are optional.
51                     The title can include some wiki markup, the box
52                     contents can include almost any wiki markup.',
53        'url'    => 'http://www.inetsoftware.de/other-products/dokuwiki-plugins/boxes',
54		);
55	}
56
57	function getType(){ return 'protected';}
58	function getAllowedTypes() { return array('container','substition','protected','disabled','formatting','paragraphs'); }
59	function getPType(){ return 'block';}
60
61	// must return a number lower than returned by native 'code' mode (200)
62	function getSort(){ return 195; }
63
64	// override default accepts() method to allow nesting
65	// - ie, to get the plugin accepts its own entry syntax
66	function accepts($mode) {
67		if ($mode == substr(get_class($this), 7)) return true;
68
69		return parent::accepts($mode);
70	}
71
72	/**
73	 * Connect pattern to lexer
74	 */
75	function connectTo($mode) {
76		$this->Lexer->addEntryPattern('<box>(?=.*?</box.*?>)',$mode,'plugin_box');
77		$this->Lexer->addEntryPattern('<box\s[^\r\n\|]*?>(?=.*?</box.*?>)',$mode,'plugin_box');
78		$this->Lexer->addEntryPattern('<box\|(?=[^\r\n]*?\>.*?</box.*?\>)',$mode,'plugin_box');
79		$this->Lexer->addEntryPattern('<box\s[^\r\n\|]*?\|(?=[^\r\n]*?>.*?</box.*?>)',$mode,'plugin_box');
80	}
81
82	function postConnect() {
83		$this->Lexer->addPattern('>', 'plugin_box');
84		$this->Lexer->addExitPattern('</box.*?>', 'plugin_box');
85	}
86
87	/**
88	 * Handle the match
89	 */
90	function handle($match, $state, $pos, &$handler){
91
92		switch ($state) {
93			case DOKU_LEXER_ENTER:
94				$data = $this->_boxstyle(trim(substr($match, 4, -1)));
95				if (substr($match, -1) == '|') {
96					$this->title_mode = true;
97					return array('title_open',$data, $pos);
98				} else {
99					return array('box_open',$data, $pos);
100				}
101
102			case DOKU_LEXER_MATCHED:
103				if ($this->title_mode) {
104					$this->title_mode = false;
105					return array('box_open','', $pos);
106				} else {
107					return array('data', $match, $pos);
108				}
109
110			case DOKU_LEXER_UNMATCHED:
111				if ($this->title_mode) {
112					return array('data', $match, $pos);
113				}
114
115				$handler->_addCall('cdata',array($match), $pos);
116				return false;
117			case DOKU_LEXER_EXIT:
118				$pos += strlen($match); // has to be done becvause the ending tag comes after $pos
119				$data = trim(substr($match, 5, -1));
120				$title =  ($data && $data{0} == "|") ? substr($data,1) : '';
121
122				return array('box_close', $title, $pos);
123
124		}
125		return false;
126	}
127
128	/**
129	 * Create output
130	 */
131	function render($mode, &$renderer, $indata) {
132		global $ID, $ACT;
133
134		// $pos is for the current position in the wiki page
135		if (empty($indata)) return false;
136		list($instr, $data, $pos) = $indata;
137
138		if($mode == 'xhtml'){
139			switch ($instr) {
140				case 'title_open' :
141					$this->title_mode = true;
142					$this->title_pos[] = $pos; // Start Position for Section Editing
143					$renderer->doc .= $this->_xhtml_boxopen($data);
144					$renderer->doc .= "<h2 class='box_title " . (method_exists($renderer, "finishSectionEdit") ? $renderer->startSectionEdit($pos, 'section', 'box') : "") . " '{$this->_title_colours}>";
145					break;
146
147				case 'box_open' :
148					if ($this->title_mode) {
149						$this->title_mode = false;
150						$renderer->doc .= "</h2>\n<div class='box_content'{$this->_content_colours}>";
151					} else {
152						$this->title_pos[] = $pos; // Start Position for Section Editing
153						$this->title_name[] = 'box_' . 'no-title' . '_' . md5(time());
154						$renderer->doc .= $this->_xhtml_boxopen($data)."<div class='box_content'{$this->_content_colours}>";
155					}
156					break;
157
158				case 'data' :
159					$output = $renderer->_xmlEntities($data);
160
161					if ( $this->title_mode ) {
162						$this->title_name[] = 'box_' . cleanID($output) . '_' . md5($output);
163						$hid = $renderer->_headerToLink($output,true);
164						$renderer->doc .= '<a id="' . $hid . '" name="' . $hid . '">' . $output . '</a>';
165						break;
166					}
167
168					$renderer->doc .= $output;
169					break;
170
171				case 'box_close' :
172					$renderer->doc .= "</div>\n";
173
174					if ($data) {
175						$renderer->doc .= "<p class='box_caption'{$this->_title_colours}>".$renderer->_xmlEntities($data)."</p>\n";
176					}
177
178					// insert the section edit button befor the box is closed - array_pop makes sure we take the last box
179					if ( $this->getConf('allowSectionEdit') && $ACT != 'preview' ) {
180						$renderer->nocache();
181
182
183						if ( auth_quickaclcheck($ID) > AUTH_READ ) {
184							$title = array_pop($this->title_name); // Clean up
185							if ( method_exists($renderer, "finishSectionEdit") ) {
186								$renderer->finishSectionEdit($pos);
187							}
188						}
189					}
190
191					$renderer->doc .= $this->_xhtml_boxclose();
192
193					break;
194			}
195
196			return true;
197		}
198		return false;
199	}
200
201	function _boxstyle($str) {
202		if (!strlen($str)) return array();
203
204		$styles = array();
205
206		$tokens = preg_split('/\s+/', $str, 9);                      // limit is defensive
207		foreach ($tokens as $token) {
208			if (preg_match('/^\d*\.?\d+(%|px|em|ex|pt|cm|mm|pi|in)$/', $token)) {
209				$styles['width'] = $token;
210				continue;
211			}
212
213			if (preg_match('/^(
214              (\#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))|        #colorvalue
215              (rgb\(([0-9]{1,3}%?,){2}[0-9]{1,3}%?\))     #rgb triplet
216              )$/x', $token)) {
217			$styles['colour'][] = $token;
218			continue;
219              }
220
221              // restrict token (class names) characters to prevent any malicious data
222              if (preg_match('/[^A-Za-z0-9_-]/',$token)) continue;
223              $styles['class'] = (isset($styles['class']) ? $styles['class'].' ' : '').$token;
224		}
225		if (!empty($styles['colour'])) {
226			$styles['colour'] = $this->_box_colours($styles['colour']);
227		}
228
229		return $styles;
230	}
231
232	function _box_colours($colours) {
233		$triplets = array();
234
235		// only need the first four colours
236		if (count($colours) > 4) $colours = array_slice($colours,0,4);
237		foreach ($colours as $colour) {
238			$triplet[] = $this->_colourToTriplet($colour);
239		}
240
241		// there must be one colour to get here - the primary background
242		// calculate title background colour if not present
243		if (empty($triplet[1])) {
244			$triplet[1] = $triplet[0];
245		}
246
247		// calculate outer background colour if not present
248		if (empty($triplet[2])) {
249			$triplet[2] = $triplet[0];
250		}
251
252		// calculate border colour if not present
253		if (empty($triplet[3])) {
254			$triplet[3] = $triplet[0];
255		}
256
257		// convert triplets back to style sheet colours
258		$style_colours['content_background'] = 'rgb('.join(',',$triplet[0]).')';
259		$style_colours['title_background'] = 'rgb('.join(',',$triplet[1]).')';
260		$style_colours['outer_background'] = 'rgb('.join(',',$triplet[2]).')';
261		$style_colours['borders'] = 'rgb('.join(',',$triplet[3]).')';
262
263		return $style_colours;
264	}
265
266	function _colourToTriplet($colour) {
267		if ($colour{0} == '#') {
268			if (strlen($colour) == 4) {
269				// format #FFF
270				return array(hexdec($colour{1}.$colour{1}),hexdec($colour{2}.$colour{2}),hexdec($colour{3}.$colour{3}));
271			} else {
272				// format #FFFFFF
273				return array(hexdec(substr($colour,1,2)),hexdec(substr($colour,3,2)), hexdec(substr($colour,5,2)));
274			}
275		} else {
276			// format rgb(x,y,z)
277			return explode(',',substr($colour,4,-1));
278		}
279	}
280
281	function _xhtml_boxopen($styles) {
282		$class = 'class="box' . (isset($styles['class']) ? ' '.$styles['class'] : '') . '"';
283		$style = isset($styles['width']) ? "width: {$styles['width']};" : '';
284
285		if (isset($styles['colour'])) {
286			$colours = 'background-color: '.$styles['colour']['outer_background'].'; ';
287			$colours .= '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			$colours = '';
294
295			$this->_content_colours = '';
296			$this->_title_colours = '';
297		}
298
299		if ($style || $colours) $style = ' style="'.$style.' '.$colours.'"';
300		if ($colours) $colours = ' style="'.$colours.'"';
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 :