1<?php
2/**
3 * DokuWiki plugin Diagram, Main component.
4 *
5 * Constructs diagrams.
6 * See a full description at http://nikita.melnichenko.name/projects/dokuwiki-diagram/.
7 *
8 * Should work with any DokuWiki version >= 20070626.
9 * Tested with DokuWiki versions 20090214, 20091225, 20110525a, 20121013, 20130510a,
10 * 20131208, 20140505e, 20140929d, 20150810a, 20160626e, 20170219e, 20180422a, 20200729.
11 *
12 * Install to lib/plugins/diagram/syntax/main.php.
13 *
14 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
15 * @author Nikita Melnichenko [http://nikita.melnichenko.name]
16 * @copyright Copyright 2007-2021, Nikita Melnichenko
17 *
18 * Thanks for help to:
19 * - Anika Henke <anika[at]selfthinker.org>
20 */
21
22// includes
23if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
24if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
25require_once(DOKU_PLUGIN.'syntax.php');
26
27/**
28 * DokuWiki plugin Diagram, Main component.
29 *
30 * Constructs diagrams.
31 */
32class syntax_plugin_diagram_main extends DokuWiki_Syntax_Plugin
33{
34	/**
35	 * Tag name in wiki text.
36	 *
37	 * @staticvar string
38	 */
39	var $tag_name = 'diagram';
40
41	/**
42	 * Splitter tag name in wiki text.
43	 *
44	 * Must be = syntax_plugin_diagram_splitter::$tag_name.
45	 * Copied for compability with PHP4.
46	 *
47	 * @staticvar string
48	 */
49	var $tag_name_splitter = '_diagram_';
50
51	/**
52	 * CSS classes used in the diagram.
53	 *
54	 * @staticvar array
55	 */
56	var $css_classes = array(
57		/* spacers */
58		'spacer-horizontal' => 'd-sh',
59		'spacer-vertical' => 'd-sv',
60		/* block */
61		'block' => 'd-b',
62		/* connection borders */
63		'border-right-solid' => 'd-brs',
64		'border-right-dashed' => 'd-brd',
65		'border-bottom-solid' => 'd-bbs',
66		'border-bottom-dashed' => 'd-bbd',
67		/* arrow directions */
68		'arrow-top' => 'd-at',
69		'arrow-right' => 'd-ar',
70		'arrow-bottom' => 'd-ab',
71		'arrow-left' => 'd-al',
72		'arrow-inside' => 'd-ai'
73		);
74
75	/**
76	 * Get syntax type.
77	 *
78	 * @return string one of the mode types defined in $PARSER_MODES in parser.php
79	 */
80	function getType ()
81	{
82		// containers are complex modes that can contain many other modes
83		// plugin generates table, so type should be container
84		return 'container';
85	}
86
87	/**
88	 * Get paragraph type.
89	 *
90	 * Defines how this syntax is handled regarding paragraphs. This is important
91	 * for correct XHTML nesting. Should return one of the following:
92	 * - 'normal' - The plugin can be used inside paragraphs
93	 * - 'block'  - Open paragraphs need to be closed before plugin output
94	 * - 'stack'  - Special case. Plugin wraps other paragraphs.
95	 *
96	 * @return string
97	 */
98	function getPType ()
99	{
100		// table cannot be put inside paragraphs
101		return 'block';
102	}
103
104	/**
105	 * Get position of plugin's mode in decision list.
106	 *
107	 * @return integer
108	 */
109	function getSort ()
110	{
111		// position doesn't matter
112		return 999;
113	}
114
115	/**
116	 * Connect pattern to lexer.
117	 *
118	 * @param string $mode
119	 */
120	function connectTo ($mode)
121	{
122		// parse all content in one shot
123		$this->Lexer->addSpecialPattern('<'.$this->tag_name.'>.*?</'.$this->tag_name.'>',
124			$mode, 'plugin_diagram_main');
125	}
126
127	/**
128	 * Handle the match.
129	 *
130	 * @param string $match
131	 * @param integer $state one of lexer states defined in lexer.php
132	 * @param integer $pos position of first
133	 * @param Doku_Handler $handler
134	 * @return array data for rendering
135	 */
136	function handle ($match, $state, $pos, Doku_Handler $handler)
137	{
138		// strip tags
139		$tag_name_len = strlen($this->tag_name);
140		$content = substr($match, $tag_name_len + 2, strlen($match) - 2 * $tag_name_len - 5);
141
142		// parse content using Splitter component
143		$calls = p_get_instructions('<'.$this->tag_name_splitter.'>'
144			.$content.'</'.$this->tag_name_splitter.'>');
145		// compose commands and abbreviations
146		list($commands, $abbrs) = $this->_genCommandsAndAbbrs($calls);
147
148		// compose internal specification of table
149		$framework = $this->_genFramework($commands);
150
151		return array(
152			'framework' => $framework,
153			'abbreviations' => $abbrs
154			);
155	}
156
157	/**
158	 * Create XHTML text.
159	 *
160	 * @param string $mode render mode; only 'xhtml' supported
161	 * @param Doku_Renderer $renderer
162	 * @param array $data data from handler
163	 * @return bool
164	 */
165	function render ($mode, Doku_Renderer $renderer, $data)
166	{
167		if ($mode != 'xhtml')
168			return false;
169
170		// add generated code to document
171		$renderer->doc .= "\n".$this->_renderDiagram(
172			$data['framework'], $data['abbreviations']);
173		return true;
174	}
175
176	/**
177	 * Compose commands and abbreviations from wiki calls.
178	 *
179	 * Supported abbreviation parameters:
180	 * - border-color (CSS property)
181	 * - background-color (CSS property)
182	 * - text-align (CSS property)
183	 * - padding (CSS property)
184	 *
185	 * @param array $calls DokuWiki calls
186	 * @return array array($commands, $abbreviations)
187	 */
188	function _genCommandsAndAbbrs ($calls)
189	{
190		$diagram_entered = false;
191		$commands = array();
192		$abbrs = array();
193		$line_index = -1;
194
195		// handle call list
196		foreach ($calls as $call)
197		{
198			// get plugin related info from call
199			if ($call[0] == 'plugin' && $call[1][0] == 'diagram_splitter')
200			{
201				$data = $call[1][1];
202				$diagram_call_state = $call[1][2];
203			}
204			else
205			{
206				$data = null;
207				$diagram_call_state = 0;
208			}
209
210			// wait until entering to diagram
211			if (!$diagram_entered	&& $diagram_call_state != DOKU_LEXER_ENTER)
212				continue;
213			// just entered: skipping
214			if (!$diagram_entered)
215			{
216				$diagram_entered = true;
217				continue;
218			}
219
220			// exited from diagram: stop handling
221			if ($diagram_call_state == DOKU_LEXER_EXIT)
222				break;
223
224			// received newline: start new line
225			if ($diagram_call_state == DOKU_LEXER_MATCHED && $data['type'] == 'newline')
226			{
227				// avoid unset lines of commands
228				if ($line_index >= 0 && !array_key_exists($line_index, $commands))
229					$commands[$line_index] = array ();
230				// increment index
231				$line_index++;
232				// stop catching calls for abbr
233				$abbr_met = false;
234				if (isset($catching_abbr))
235					unset($catching_abbr);
236				continue;
237			}
238			// must receive first newline before processing commands
239			if ($line_index < 0)
240				continue;
241
242			// received command: add to line of commands
243			if ($diagram_call_state == DOKU_LEXER_MATCHED && $data['type'] == 'command')
244			{
245				// deny commands after first abbreviation in line
246				if (!$abbr_met)
247					$commands[$line_index][] = $data['command'];
248				// stop catching calls for last abbr
249				$abbr_met = false;
250				if (isset($catching_abbr))
251					unset($catching_abbr);
252				continue;
253			}
254
255			// received abbreviation: add to line of abbrs and start catching calls
256			if ($diagram_call_state == DOKU_LEXER_MATCHED && $data['type'] == 'abbr eval')
257			{
258				$abbr_met = true;
259				$abbrs[$line_index][$data['abbr']]['content'] = array();
260				$abbrs[$line_index][$data['abbr']]['params'] = array();
261				// override some parameters by user values
262				if (isset ($data['params']))
263				{
264					$params = explode(';', $data['params']);
265					foreach ($params as $param)
266					{
267						list ($key, $value) = explode(':', $param);
268						$key = trim ($key);
269						$value = trim ($value);
270						$is_valid = false;
271						switch ($key)
272						{
273							case 'border-color':
274							case 'background-color':
275								$is_valid = $this->_validateCSSColor ($value);
276								break;
277							case 'text-align':
278								$is_valid = $this->_validateCSSTextAlign ($value);
279								break;
280							case 'padding':
281								$is_valid = $this->_validateCSSPadding ($value);
282								break;
283						}
284						if ($is_valid)
285							$abbrs[$line_index][$data['abbr']]['params'][$key] = $value;
286					}
287				}
288				$catching_abbr = &$abbrs[$line_index][$data['abbr']]['content'];
289				continue;
290			}
291
292			// received raw unmatched text and catching is on: add cdata call to abbr
293			if ($diagram_call_state == DOKU_LEXER_UNMATCHED && isset($catching_abbr))
294			{
295				$catching_abbr[] = array('cdata', array($data['text']), $call[2]);
296				continue;
297			}
298
299			// received arbitrary call and catching is on: add call to abbr
300			if (isset($catching_abbr))
301				$catching_abbr[] = $call;
302
303			// skip everything else
304		}
305
306		// remove trailing garbage
307		for ($i = 0; $i < count($commands); $i++)
308		{
309			$line_length = count($commands[$i]);
310			// remove last element, if no abbreviations found,
311			// because last delimiter is a 'border' of the table
312			// do not care, if someone specified garbage after last delimiter
313			if ($line_length > 0 && !array_key_exists($i, $abbrs))
314				unset($commands[$i][$line_length - 1]);
315		}
316
317		return array($commands, $abbrs);
318	}
319
320	/**
321	 * Generate table's framework.
322	 *
323	 * Framework: array(row number => array (column number => cell spec))
324	 *   + array('n_rows' => number of rows, 'n_cols' => number of columns).
325	 * cell_spec: array(
326	 *   'colspan' => colspan (optional),
327	 *   'rowspan' => rowspan (optional),
328	 *   'classes' => array(css class),
329	 *   'text' => text for diagram block or abbreviation (optional),
330	 *   'content' => raw xhtml code to paste into cell, if 'text' key isn't set (optional)
331	 *   ).
332	 *
333	 * @author Nikita Melnichenko [http://nikita.melnichenko.name]
334	 * @author Olesya Melnichenko [http://melnichenko.name]
335	 *
336	 * @param array $commands specification scheme
337	 * @return array
338	 */
339	function _genFramework ($commands)
340	{
341		// store number of rows
342		$res['n_rows'] = count($commands) * 2;
343		// number of columns is computed below
344		$res['n_cols'] = 0;
345
346		for ($i = 0, $ir = 0; $i < count($commands); $i++, $ir += 2)
347		{
348			for ($j = 0, $jr = 0; $j < count($commands[$i]); $j++)
349			{
350				// leading and trailing spaces are already ignored by splitter component
351				$cell_text = $commands[$i][$j];
352				// split command to connection and arrow commands
353				list($conn_command, $arrow_command) = $this->_splitCommand($cell_text);
354				// 2x2 connection specs for current command
355				$conn_cells = null;
356
357				switch ($conn_command)
358				{
359					// === empty ===
360
361					case "":
362						$conn_cells = $this->_connectionCells('nnnn', $arrow_command);
363						break;
364
365					// === solid or dashed lines ===
366
367					// +     +     +
368					//
369					//
370					//
371					//
372					//
373					// +     +-----+
374					//       |
375					//       |
376					//       |
377					//       |
378					//       |
379					// +     +     +
380					case ",":
381						$conn_cells = $this->_connectionCells('nssn', $arrow_command);
382						break;
383					case "F":
384						$conn_cells = $this->_connectionCells('nddn', $arrow_command);
385						break;
386
387					// +     +     +
388					//
389					//
390					//
391					//
392					//
393					// +-----+     +
394					//       |
395					//       |
396					//       |
397					//       |
398					//       |
399					// +     +     +
400					case ".":
401						$conn_cells = $this->_connectionCells('nnss', $arrow_command);
402						break;
403					case "7":
404						$conn_cells = $this->_connectionCells('nndd', $arrow_command);
405						break;
406
407					// +     +     +
408					//
409					//
410					//
411					//
412					//
413					// +-----+-----+
414					//       |
415					//       |
416					//       |
417					//       |
418					//       |
419					// +     +     +
420					case "v":
421						$conn_cells = $this->_connectionCells('nsss', $arrow_command);
422						break;
423					case "V":
424						$conn_cells = $this->_connectionCells('nddd', $arrow_command);
425						break;
426
427					// +     +     +
428					//       |
429					//       |
430					//       |
431					//       |
432					//       |
433					// +     +     +
434					//       |
435					//       |
436					//       |
437					//       |
438					//       |
439					// +     +     +
440					case "!":
441						$conn_cells = $this->_connectionCells('snsn', $arrow_command);
442						break;
443					case ":":
444						$conn_cells = $this->_connectionCells('dndn', $arrow_command);
445						break;
446
447					// +     +     +
448					//       |
449					//       |
450					//       |
451					//       |
452					//       |
453					// +-----+-----+
454					//       |
455					//       |
456					//       |
457					//       |
458					//       |
459					// +     +     +
460					case "+":
461						$conn_cells = $this->_connectionCells('ssss', $arrow_command);
462						break;
463					case "%":
464						$conn_cells = $this->_connectionCells('dddd', $arrow_command);
465						break;
466
467					// +     +     +
468					//
469					//
470					//
471					//
472					//
473					// +-----+-----+
474					//
475					//
476					//
477					//
478					//
479					// +     +     +
480					case "-":
481						$conn_cells = $this->_connectionCells('nsns', $arrow_command);
482						break;
483					case "~":
484						$conn_cells = $this->_connectionCells('ndnd', $arrow_command);
485						break;
486
487					// +     +     +
488					//       |
489					//       |
490					//       |
491					//       |
492					//       |
493					// +     +-----+
494					//
495					//
496					//
497					//
498					//
499					// +     +     +
500					case "`":
501						$conn_cells = $this->_connectionCells('ssnn', $arrow_command);
502						break;
503					case "L":
504						$conn_cells = $this->_connectionCells('ddnn', $arrow_command);
505						break;
506
507					// +     +     +
508					//       |
509					//       |
510					//       |
511					//       |
512					//       |
513					// +-----+     +
514					//
515					//
516					//
517					//
518					//
519					// +     +     +
520					case "'":
521						$conn_cells = $this->_connectionCells('snns', $arrow_command);
522						break;
523					case "J":
524						$conn_cells = $this->_connectionCells('dnnd', $arrow_command);
525						break;
526
527					// +     +     +
528					//       |
529					//       |
530					//       |
531					//       |
532					//       |
533					// +-----+-----+
534					//
535					//
536					//
537					//
538					//
539					// +     +     +
540					case "^":
541						$conn_cells = $this->_connectionCells('ssns', $arrow_command);
542						break;
543					case "A":
544						$conn_cells = $this->_connectionCells('ddnd', $arrow_command);
545						break;
546
547					// +     +     +
548					//       |
549					//       |
550					//       |
551					//       |
552					//       |
553					// +-----+     +
554					//       |
555					//       |
556					//       |
557					//       |
558					//       |
559					// +     +     +
560					case "(":
561						$conn_cells = $this->_connectionCells('snss', $arrow_command);
562						break;
563					case "C":
564						$conn_cells = $this->_connectionCells('dndd', $arrow_command);
565						break;
566
567					// +     +     +
568					//       |
569					//       |
570					//       |
571					//       |
572					//       |
573					// +     +-----+
574					//       |
575					//       |
576					//       |
577					//       |
578					//       |
579					// +     +     +
580					case ")":
581						$conn_cells = $this->_connectionCells('sssn', $arrow_command);
582						break;
583					case "D":
584						$conn_cells = $this->_connectionCells('dddn', $arrow_command);
585						break;
586
587					// === mixed lines ===
588
589					// +     +     +
590					//
591					//
592					//
593					//
594					//
595					// +- - -+- - -+
596					//       |
597					//       |
598					//       |
599					//       |
600					//       |
601					// +     +     +
602					case "y":
603						$conn_cells = $this->_connectionCells('ndsd', $arrow_command);
604						break;
605
606					// +     +     +
607					//       |
608					//
609					//       |
610					//
611					//       |
612					// +-----+-----+
613					//       |
614					//
615					//       |
616					//
617					//       |
618					// +     +     +
619					case "*":
620						$conn_cells = $this->_connectionCells('dsds', $arrow_command);
621						break;
622
623					// +     +     +
624					//       |
625					//
626					//       |
627					//
628					//       |
629					// +     +-----+
630					//       |
631					//
632					//       |
633					//
634					//       |
635					// +     +     +
636					case "}":
637						$conn_cells = $this->_connectionCells('dsdn', $arrow_command);
638						break;
639
640					// +     +     +
641					//       |
642					//
643					//       |
644					//
645					//       |
646					// +-----+     +
647					//       |
648					//
649					//       |
650					//
651					//       |
652					// +     +     +
653					case "{":
654						$conn_cells = $this->_connectionCells('dnds', $arrow_command);
655						break;
656
657					// +     +     +
658					//       |
659					//       |
660					//       |
661					//       |
662					//       |
663					// +     +- - -+
664					//       |
665					//       |
666					//       |
667					//       |
668					//       |
669					// +     +     +
670					case "]":
671						$conn_cells = $this->_connectionCells('sdsn', $arrow_command);
672						break;
673
674					// +     +     +
675					//       |
676					//       |
677					//       |
678					//       |
679					//       |
680					// +- - -+     +
681					//       |
682					//       |
683					//       |
684					//       |
685					//       |
686					// +     +     +
687					case "[":
688						$conn_cells = $this->_connectionCells('snsd', $arrow_command);
689						break;
690
691					// +     +     +
692					//       |
693					//       |
694					//       |
695					//       |
696					//       |
697					// +- - -+- - -+
698					//
699					//
700					//
701					//
702					//
703					// +     +     +
704					case "h":
705						$conn_cells = $this->_connectionCells('sdnd', $arrow_command);
706						break;
707
708					// +     +     +
709					//       |
710					//       |
711					//       |
712					//       |
713					//       |
714					// +- - -+- - -+
715					//       |
716					//       |
717					//       |
718					//       |
719					//       |
720					// +     +     +
721					case "#":
722						$conn_cells = $this->_connectionCells('sdsd', $arrow_command);
723						break;
724
725					// +     +     +
726					//
727					//
728					//
729					//
730					//
731					// +-----+-----+
732					//       |
733					//
734					//       |
735					//
736					//       |
737					// +     +     +
738					case "p":
739						$conn_cells = $this->_connectionCells('nsds', $arrow_command);
740						break;
741
742					// +     +     +
743					//       |
744					//
745					//       |
746					//
747					//       |
748					// +-----+-----+
749					//
750					//
751					//
752					//
753					//
754					// +     +     +
755					case "b":
756						$conn_cells = $this->_connectionCells('dsns', $arrow_command);
757						break;
758
759					// === box ===
760
761					default:
762						$res[$ir][$jr] = $this->_boxCell(6, 2, $cell_text);
763						$jr += 6;
764				}
765
766				// apply connection cells to the result
767				if (!is_null($conn_cells))
768				{
769					// we must have a proper order of creation of elements of framework, do not use list() here
770					$res[$ir][$jr] = $conn_cells[0];
771					$res[$ir][$jr + 1] = $conn_cells[1];
772					$res[$ir + 1][$jr] = $conn_cells[2];
773					$res[$ir + 1][$jr + 1] = $conn_cells[3];
774					$jr += 2;
775				}
776			}
777
778			// compute number of columns
779			if ($res['n_cols'] < $jr)
780				$res['n_cols'] = $jr;
781		}
782
783		return $res;
784	}
785
786	/**
787	 * Split command to connection part and arrow part.
788	 *
789	 * @param string $command
790	 * @return array array($connection_command, $arrow_command)
791	 */
792	function _splitCommand ($command)
793	{
794		$command_parts = explode('@', $command, 2);
795		if (!isset($command_parts[1]) || !preg_match("/^[0-9a-f]{1,2}$/i", $command_parts[1]))
796			$command_parts[1] = 0;
797		else
798		{
799			// convert to bits: 'a' -> '0xa', 'ab' -> '0xba'
800			// see docs and params of _connectionCells
801			$v = $command_parts[1];
802			if (strlen($v) == 2)
803				$v = $v[1].$v[0];
804			$command_parts[1] = intval($v, 16);
805		}
806		return $command_parts;
807	}
808
809	/**
810	 * Generate box cell spec.
811	 *
812	 * Box is an entity with wiki text.
813	 *
814	 * @param integer $width colspan
815	 * @param integer $height rowspan
816	 * @param string $text box text or abbreviation
817	 * @param string $border css border
818	 * @param string $background_color css color
819	 * @return array cell spec
820	 */
821	function _boxCell ($width, $height, $text)
822	{
823		return array(
824			'colspan' => $width,
825			'rowspan' => $height,
826			'classes' => array($this->css_classes['block']),
827			'text' => $text
828			);
829	}
830
831	/**
832	 * Generate 2x2 pattern of connections cells.
833	 *
834	 * Each connection cell provides connection lines using its borders.
835	 * They could also contain divs with arrowheads.
836	 *
837	 * @param string $border_spec 4 chars containing line type in top, right, bottom, left directions;
838	 *   line type chars are: 's' for solid, 'd' for dashed, 'n' for no line
839	 * @param int $arrow_spec 8 bits are used:
840	 *   the first 4 bits indicate if arrow exists (=1) or not (=0) in top, right, bottom, left directions,
841	 *   the next 4 bits indicate if arrowhead look inside (=1) or outside (=0) in top, right, bottom, left directions,
842	 * @return array array(cell_{0,0}, cell_{0,1}, cell_{1,0}, cell_{1,1})
843	 */
844	function _connectionCells ($border_spec, $arrow_spec)
845	{
846		// direction numbers: top (0), right (1), bottom (2), left (3)
847		// cell numbers: {0,0} -> 0, {0,1} -> 1, {1,0} -> 2, {1,1} -> 3
848		// +     +     +
849		//       |
850		//
851		// cell  0  cell
852		//  0        1
853		//       |
854		// +- 3 -+- 1 -+
855		//       |
856		//
857		// cell  2  cell
858		//  2        3
859		//       |
860		// +     +     +
861
862		// init
863		for ($i = 0; $i < 4; $i++)
864			$cells[$i] = array('classes' => array());
865
866		// fill borders
867		if ($border_spec[0] != 'n')
868			$cells[0]['classes'][] = $this->_borderClass($border_spec[0], 'right');
869		if ($border_spec[1] != 'n')
870			$cells[1]['classes'][] = $this->_borderClass($border_spec[1], 'bottom');
871		if ($border_spec[2] != 'n')
872			$cells[2]['classes'][] = $this->_borderClass($border_spec[2], 'right');
873		if ($border_spec[3] != 'n')
874			$cells[0]['classes'][] = $this->_borderClass($border_spec[3], 'bottom');
875
876		// div elements with arrows, direction to cell number mapping
877		// 0 -> 1, 1 -> 3. 2 -> 2, 3 -> 0
878		// +     +     +
879		//       |
880		//
881		//    0  0>>1
882		//    ^
883		//    ^  |
884		// +- 3 -+- 1 -+
885		//       |  v
886		//          v
887		//    2<<2  3
888		//
889		//       |
890		// +     +     +
891
892		// fill primary arrow classes
893		if ($arrow_spec & (1 << 0))
894		{
895			$cells[1]['classes'][] = $this->css_classes['arrow-top'];
896			$cells[1]['content'] = '<div />';
897		}
898		if ($arrow_spec & (1 << 1))
899		{
900			$cells[3]['classes'][] = $this->css_classes['arrow-right'];
901			$cells[3]['content'] = '<div />';
902		}
903		if ($arrow_spec & (1 << 2))
904		{
905			$cells[2]['classes'][] = $this->css_classes['arrow-bottom'];
906			$cells[2]['content'] = '<div />';
907		}
908		if ($arrow_spec & (1 << 3))
909		{
910			$cells[0]['classes'][] = $this->css_classes['arrow-left'];
911			$cells[0]['content'] = '<div />';
912		}
913		// fill arrowhead direction
914		if ($arrow_spec & (1 << (0 + 4)))
915			$cells[1]['classes'][] = $this->css_classes['arrow-inside'];
916		if ($arrow_spec & (1 << (1 + 4)))
917			$cells[3]['classes'][] = $this->css_classes['arrow-inside'];
918		if ($arrow_spec & (1 << (2 + 4)))
919			$cells[2]['classes'][] = $this->css_classes['arrow-inside'];
920		if ($arrow_spec & (1 << (3 + 4)))
921			$cells[0]['classes'][] = $this->css_classes['arrow-inside'];
922
923		// clear
924		for ($i = 0; $i < 4; $i++)
925			if (empty($cells[$i]['classes']))
926				unset($cells[$i]['classes']);
927
928		return $cells;
929	}
930
931	/**
932	 * Generate border CSS class for connection cell.
933	 *
934	 * @param string $type 's' for solid, 'd' for dashed
935	 * @param string $direction 'right'or 'bottom'
936	 * @return string class name
937	 */
938	function _borderClass ($type, $direction)
939	{
940		if ($type != 's' && $type != 'd')
941			return 'error';
942		$key = "border-$direction-".($type == 's' ? 'solid' : 'dashed');
943		return $this->css_classes[$key];
944	}
945
946	/**
947	 * Generate table with diagram.
948	 *
949	 * @param array $framework table framework generated by _genFramework
950	 * @param array $abbrs information about abbreviations
951	 * @return string xhtml table
952	 */
953	function _renderDiagram ($framework, $abbrs)
954	{
955		$n_rows = $framework['n_rows'];
956		$n_cols = $framework['n_cols'];
957
958		// output table
959		$table = '<table class="diagram">'."\n";
960		// create horizontal spacer row
961		// first cell is for column of vertical spacers
962		$table .= "\t<tr>\n\t\t<td></td>\n";
963		for ($i = 0; $i < $n_cols; $i++)
964			$table .= "\t\t<td class=\"".$this->css_classes['spacer-horizontal']."\"><div /></td>\n";
965		$table .= "\t</tr>\n";
966		// create diagram rows
967		for ($i = 0; $i < $n_rows; $i++)
968		{
969			// get table row spec
970			$row = array_key_exists($i, $framework) ? $framework[$i] : array ();
971			// line number
972			$line_index = $i / 2;
973
974			// output tr
975			// first cell is for column of vertical spacers
976			$table .= "\t<tr>\n\t\t<td class=\"".$this->css_classes['spacer-vertical']."\"><div /></td>\n";
977			foreach ($row as $cell)
978			{
979				// generate cell content and update style
980				$cell_content = '';
981				// empty cell or connection cell
982				if (!isset($cell['text']))
983				{
984					if (isset($cell['content']))
985						$cell_content = $cell['content'];
986				}
987				// cell with abbreviation
988				else if (array_key_exists($line_index, $abbrs) && array_key_exists($cell['text'], $abbrs[$line_index]))
989				{
990					$cell_content = $this->_renderWikiCalls ($abbrs[$line_index][$cell['text']]['content']);
991					$cell['style'] = $this->_generateBlockStyle ($abbrs[$line_index][$cell['text']]['params']);
992				}
993				// cell with unrecognized abbreviation
994				else
995					$cell_content = $cell['text'];
996
997				// output td
998				$table .= "\t\t<td"
999					.(isset($cell['classes']) && !empty($cell['classes']) ? ' class="'.implode(' ', $cell['classes']).'"' : '')
1000					.($cell['style'] != '' ? ' style="'.$cell["style"].'"' : '')
1001					.(isset($cell['colspan']) ? ' colspan="'.$cell["colspan"].'"' : '')
1002					.(isset($cell['rowspan']) ? ' rowspan="'.$cell["rowspan"].'"' : '')
1003					.'>'
1004					.$cell_content
1005					."</td>\n";
1006			}
1007			$table .= "\t</tr>\n";
1008		}
1009		$table .= "</table>\n";
1010
1011		return $table;
1012	}
1013
1014	/**
1015	 * Generate CSS style for diagram block.
1016	 *
1017	 * @param array $params supported block CSS parameters
1018	 * @return string css style
1019	 */
1020	function _generateBlockStyle ($params)
1021	{
1022		$css_props = array();
1023		foreach ($params as $param => $value)
1024			$css_props[] = "$param: $value;";
1025		return implode(' ', $css_props);
1026	}
1027
1028	/**
1029	 * Render wiki instructions.
1030	 *
1031	 * @param array $calls DokuWiki calls
1032	 * @return string xhtml markup
1033	 */
1034	function _renderWikiCalls ($calls)
1035	{
1036		return p_render('xhtml', $calls, $info);
1037	}
1038
1039	/**
1040	 * Check if given color will not break css style.
1041	 *
1042	 * @param string $color checked string
1043	 * @return true, if string is good for css
1044	 */
1045	function _validateCSSColor ($color)
1046	{
1047		// color name; for ex. 'green'
1048		if (preg_match("/^[a-z]+$/", $color))
1049			return true;
1050		// short number notation; for ex. '#e73'
1051		if (preg_match("/^#[0-9a-fA-F]{3}$/", $color))
1052			return true;
1053		// full number notation; for ex. '#ef703f'
1054		if (preg_match("/^#[0-9a-fA-F]{6}$/", $color))
1055			return true;
1056		// rgb notation; for ex. 'rgb(11,22,33)' or 'rgb(11%,22%,33%)'
1057		if (preg_match("/^rgb\([ ]*[0-9]{1,3}[ ]*,[ ]*[0-9]{1,3}[ ]*,[ ]*[0-9]{1,3}[ ]*\)$/", $color))
1058			return true;
1059		if (preg_match("/^rgb\([ ]*[0-9]{1,3}%[ ]*,[ ]*[0-9]{1,3}%[ ]*,[ ]*[0-9]{1,3}%[ ]*\)$/", $color))
1060			return true;
1061		return false;
1062	}
1063
1064	/**
1065	 * Check if given value is proper for css text-align.
1066	 *
1067	 * @param string $value checked string
1068	 * @return true, if string is good as a value for css text-align
1069	 */
1070	function _validateCSSTextAlign ($value)
1071	{
1072		return $value == 'center' || $value == 'justify' || $value == 'left' || $value == 'right';
1073	}
1074
1075	/**
1076	 * Check if given value is proper for css padding.
1077	 *
1078	 * @param string $value checked string
1079	 * @return true, if string is good as a value for css padding
1080	 */
1081	function _validateCSSPadding ($value)
1082	{
1083		if (preg_match("/^((auto|[0-9]+px|[0-9]+%|[0-9]+em)[ ]*){1,4}$/", $value))
1084			return true;
1085		return false;
1086	}
1087}
1088