1<?php
2if (! class_exists('syntax_plugin_lists')) {
3	if (! defined('DOKU_PLUGIN')) {
4		if (! defined('DOKU_INC')) {
5			define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/');
6		} // if
7		define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
8	} // if
9	// Include parent class:
10	require_once(DOKU_PLUGIN . 'syntax.php');
11	define('PLUGIN_LISTS', 'plugin_lists');
12
13/**
14 * <tt>syntax_plugin_lists.php </tt>- A PHP4 class that implements
15 * a <tt>DokuWiki</tt> plugin for <tt>un/ordered lists</tt> block
16 * elements.
17 *
18 * <p>
19 * Usage:<br>
20 * <tt>  * unordered item &lt;</tt>
21 * <tt>  - ordered item &lt;</tt>
22 * </p>
23 * <pre>
24 *	Copyright (C) 2005, 2007  DFG/M.Watermann, D-10247 Berlin, FRG
25 *			All rights reserved
26 *		EMail : &lt;support@mwat.de&gt;
27 * </pre>
28 * <div class="disclaimer">
29 * This program is free software; you can redistribute it and/or modify
30 * it under the terms of the GNU General Public License as published by
31 * the Free Software Foundation; either
32 * <a href="http://www.gnu.org/licenses/gpl.html">version 3</a> of the
33 * License, or (at your option) any later version.<br>
34 * This software is distributed in the hope that it will be useful,
35 * but WITHOUT ANY WARRANTY; without even the implied warranty of
36 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
37 * General Public License for more details.
38 * </div>
39 * @author <a href="mailto:support@mwat.de">Matthias Watermann</a>
40 * @version <tt>$Id: syntax_plugin_lists.php,v 1.4 2007/08/15 12:36:19 matthias Exp $</tt>
41 * @since created 29-Aug-2005
42 */
43class syntax_plugin_lists extends DokuWiki_Syntax_Plugin {
44
45	/**
46	 * @publicsection
47	 */
48	//@{
49
50	/**
51	 * Tell the parser whether the plugin accepts syntax mode
52	 * <tt>$aMode</tt> within its own markup.
53	 *
54	 * @param $aMode String The requested syntaxmode.
55	 * @return Boolean <tt>TRUE</tt> unless <tt>$aMode</tt> is
56	 * <tt>PLUGIN_LISTS</tt> (which would result in a
57	 * <tt>FALSE</tt> method result).
58	 * @public
59	 * @see getAllowedTypes()
60	 */
61	function accepts($aMode) {
62		return (PLUGIN_LISTS != $aMode);
63	} // accepts()
64
65	/**
66	 * Connect lookup pattern to lexer.
67	 *
68	 * @param $aMode String The desired rendermode.
69	 * @public
70	 * @see render()
71	 */
72	function connectTo($aMode) {
73		if (PLUGIN_LISTS == $aMode) {
74			return;
75		} // if
76		$this->Lexer->addEntryPattern(
77			'\n\x20{2,}[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n\n)',
78			$aMode, PLUGIN_LISTS);
79		$this->Lexer->addPattern(
80			'\n\x20{2,}[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n)', PLUGIN_LISTS);
81		$this->Lexer->addEntryPattern(
82			'\n\t+\s*[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n\n)',
83			$aMode, PLUGIN_LISTS);
84		$this->Lexer->addPattern(
85			'\n\t+\s*[\x2A\x2D]\s*(?=(?s).*?[^\x5C]\x3C\n)', PLUGIN_LISTS);
86	} // connectTo()
87
88	/**
89	 * Get an associative array with plugin info.
90	 *
91	 * <p>
92	 * The returned array holds the following fields:
93	 * <dl>
94	 * <dt>author</dt><dd>Author of the plugin</dd>
95	 * <dt>email</dt><dd>Email address to contact the author</dd>
96	 * <dt>date</dt><dd>Last modified date of the plugin in
97	 * <tt>YYYY-MM-DD</tt> format</dd>
98	 * <dt>name</dt><dd>Name of the plugin</dd>
99	 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd>
100	 * <dt>url</dt><dd>Website with more information on the plugin
101	 * (eg. syntax description)</dd>
102	 * </dl>
103	 * @return Array Information about this plugin class.
104	 * @public
105	 * @static
106	 */
107	function getInfo() {
108		return array(
109			'author' =>	'Matthias Watermann',
110			'email' =>	'support@mwat.de',
111			'date' =>	'2007-08-15',
112			'name' =>	'List Syntax Plugin',
113			'desc' =>	'Add HTML Style Un/Ordered Lists',
114			'url' =>	'http://wiki.splitbrain.org/plugin:lists');
115	} // getInfo()
116
117	/**
118	 * Define how this plugin is handled regarding paragraphs.
119	 *
120	 * <p>
121	 * This method is important for correct XHTML nesting. It returns
122	 * one of the following values:
123	 * </p>
124	 * <dl>
125	 * <dt>normal</dt><dd>The plugin can be used inside paragraphs.</dd>
126	 * <dt>block</dt><dd>Open paragraphs need to be closed before
127	 * plugin output.</dd>
128	 * <dt>stack</dt><dd>Special case: Plugin wraps other paragraphs.</dd>
129	 * </dl>
130	 * @return String <tt>'normal'</tt> .
131	 * @public
132	 * @static
133	 */
134	function getPType() {
135		return 'normal';
136	} // getPType()
137
138	/**
139	 * Where to sort in?
140	 *
141	 * @return Integer <tt>8</tt>, an arbitrary value smaller
142	 * <tt>Doku_Parser_Mode_listblock</tt> (10).
143	 * @public
144	 * @static
145	 */
146	function getSort() {
147		// class 'Doku_Parser_Mode_preformated' returns 20
148		// class 'Doku_Parser_Mode_listblock' returns 10
149		return 8;
150	} // getSort()
151
152	/**
153	 * Get the type of syntax this plugin defines.
154	 *
155	 * @return String <tt>'container'</tt>.
156	 * @public
157	 * @static
158	 */
159	function getType() {
160		return 'container';
161	} // getType()
162
163	/**
164	 * Handler to prepare matched data for the rendering process.
165	 *
166	 * <p>
167	 * The <tt>$aState</tt> parameter gives the type of pattern
168	 * which triggered the call to this method:
169	 * </p>
170	 * <dl>
171	 * <dt>DOKU_LEXER_ENTER</dt>
172	 * <dd>a pattern set by <tt>addEntryPattern()</tt></dd>
173	 * <dt>DOKU_LEXER_MATCHED</dt>
174	 * <dd>a pattern set by <tt>addPattern()</tt></dd>
175	 * <dt>DOKU_LEXER_EXIT</dt>
176	 * <dd> a pattern set by <tt>addExitPattern()</tt></dd>
177	 * <dt>DOKU_LEXER_SPECIAL</dt>
178	 * <dd>a pattern set by <tt>addSpecialPattern()</tt></dd>
179	 * <dt>DOKU_LEXER_UNMATCHED</dt>
180	 * <dd>ordinary text encountered within the plugin's syntax mode
181	 * which doesn't match any pattern.</dd>
182	 * </dl>
183	 * @param $aMatch String The text matched by the patterns.
184	 * @param $aState Integer The lexer state for the match.
185	 * @param $aPos Integer The character position of the matched text.
186	 * @param $aHandler Object Reference to the Doku_Handler object.
187	 * @return Array Index <tt>[0]</tt> holds the current
188	 * <tt>$aState</tt>, index <tt>[1]</tt> the match prepared for
189	 * the <tt>render()</tt> method.
190	 * @public
191	 * @see render()
192	 * @static
193	 */
194	function handle($aMatch, $aState, $aPos, &$aHandler) {
195		static $CHARS; static $ENTS;
196		if (! is_array($CHARS)) {
197			$CHARS = array('&','<', '>');
198		} // if
199		if (! is_array($ENTS)) {
200			$ENTS = array('&#38;', '&#60;', '&#62;');
201		} // if
202		switch ($aState) {
203			case DOKU_LEXER_ENTER:
204				// fall through
205			case DOKU_LEXER_MATCHED:
206				$hits = array();
207				if (preg_match('|\n*((\s*)(.))|', $aMatch, $hits)) {
208					return array($aState, $hits[3],
209						strlen(str_replace('  ', "\t", $hits[2])));
210				} // if
211				return array($aState, $aMatch);
212			case DOKU_LEXER_UNMATCHED:
213				$hits = array();
214				if (preg_match('|^\s*\x3C$|', $aMatch, $hits)) {
215					return array(DOKU_LEXER_UNMATCHED, '', +1);
216				} // if
217				if (preg_match('|(.*?)\s+\x3C$|s', $aMatch, $hits)) {
218					return array(DOKU_LEXER_UNMATCHED,
219						str_replace($CHARS, $ENTS,
220							str_replace('\<', '<', $hits[1])), +1);
221				} // if
222				if (preg_match('|(.*[^\x5C])\x3C$|s', $aMatch, $hits)) {
223					return array(DOKU_LEXER_UNMATCHED,
224						str_replace($CHARS, $ENTS,
225							str_replace('\<', '<', $hits[1])), +1);
226				} // if
227				return array(DOKU_LEXER_UNMATCHED,
228					str_replace($CHARS, $ENTS,
229						str_replace('\<', '<', $aMatch)), -1);
230			case DOKU_LEXER_EXIT:
231				// end of list
232			default:
233				return array($aState);
234		} // switch
235	} // handle()
236
237	/**
238	 * Add exit pattern to lexer.
239	 *
240	 * @public
241	 */
242	function postConnect() {
243		// make sure the RegEx 'eats' only _one_ LF:
244		$this->Lexer->addExitPattern('(?<=\x3C)\n(?=\n)', PLUGIN_LISTS);
245	} // postConnect()
246
247	/**
248	 * Handle the actual output creation.
249	 *
250	 * <p>
251	 * The method checks for the given <tt>$aFormat</tt> and returns
252	 * <tt>FALSE</tt> when a format isn't supported. <tt>$aRenderer</tt>
253	 * contains a reference to the renderer object which is currently
254	 * handling the rendering. The contents of <tt>$aData</tt> is the
255	 * return value of the <tt>handle()</tt> method.
256	 * </p>
257	 * @param $aFormat String The output format to generate.
258	 * @param $aRenderer Object A reference to the renderer object.
259	 * @param $aData Array The data created by the <tt>handle()</tt>
260	 * method.
261	 * @return Boolean <tt>TRUE</tt> if rendered successfully, or
262	 * <tt>FALSE</tt> otherwise.
263	 * @public
264	 * @see handle()
265	 */
266	function render($aFormat, &$aRenderer, &$aData) {
267		if ('xhtml' != $aFormat) {
268			return FALSE;
269		} // if
270		static $LISTS = array('*' => 'ul', '-' => 'ol');
271		static $LEVEL = 1;	// initial nesting level
272		static $INLI = array();	// INLI[LEVEL] :: 0==open LI, 1==open LI/P
273		static $CURRENT = array();	// CURRENT[LEVEL] :: * | -
274		switch ($aData[0]) {
275			case DOKU_LEXER_ENTER:
276				$CURRENT[$LEVEL] = $aData[1];
277				$hits = array();
278				if (preg_match('|\s*<p>\s*$|i', $aRenderer->doc, $hits)) {
279					$hits = -strlen($hits[0]);
280					$aRenderer->doc = substr($aRenderer->doc, 0, $hits)
281						. '<' . $LISTS[$aData[1]] . '>';
282				} else {
283					$aRenderer->doc .= '</p><' . $LISTS[$aData[1]] . '>';
284				} // if
285				// fall through to handle first item
286			case DOKU_LEXER_MATCHED:
287				// $aData[0] :: match state
288				// $aData[1] :: * | -
289				// $aData[2] :: nesting level
290				$diff = $aData[2] - $LEVEL;
291				if (0 < $diff) {	// going up one level
292					$CURRENT[++$LEVEL] = $aData[1];
293					$hits = array();
294					if (preg_match('|</li>\s*$|', $aRenderer->doc)) {
295						// need to open a new LI
296						$aRenderer->doc .= '<li class="level' . ($LEVEL - 1)
297							. '"><' . $LISTS[$CURRENT[$LEVEL]] . '>';
298						$INLI[$LEVEL - 1] = 0; // no closing P needed
299					} else if (preg_match('|\s*<li[^>]*>\s*<p>\s*$|',
300					$aRenderer->doc, $hits)) {
301						// replace rudimentary LI
302						$hits = -strlen($hits[0]);
303						$aRenderer->doc = substr($aRenderer->doc, 0, $hits)
304							. '<li class="level' . ($LEVEL - 1)
305							. '"><' . $LISTS[$CURRENT[$LEVEL]] . '>';
306						$INLI[$LEVEL - 1] = 0; // no closing P needed
307					} else {	// possibly open LI
308						if (isset($INLI[$LEVEL - 1])) {
309							if (0 < $INLI[$LEVEL - 1]) {	// open LI P
310								$aRenderer->doc .= '</p><'
311									. $LISTS[$aData[1]] . '>';
312								$INLI[$LEVEL - 1] = 0;
313							} else {	// open LI
314								$aRenderer->doc .= '<'
315									. $LISTS[$aData[1]] . '>';
316							} // if
317						} else {	// no open LI
318							$aRenderer->doc .= '<li class="level'
319								. ($LEVEL - 1) . '"><'
320								. $LISTS[$aData[1]] . '>';
321							$INLI[$LEVEL - 1] = 0; // no closing P needed
322						} // if
323					} // if
324				} else if (0 > $diff) {	// going back some levels
325					do {
326						--$LEVEL;
327						$aRenderer->doc .= '</'
328							. $LISTS[$CURRENT[$LEVEL + 1]] . '>';
329						if (isset($INLI[$LEVEL])) {
330							$aRenderer->doc .= (0 < $INLI[$LEVEL])
331								? '</p></li>'
332								: '</li>';
333						} // if
334					} while (0 > ++$diff);
335				} else if ($aData[1] !=  $CURRENT[$LEVEL]) {
336					// list type changed
337					if (isset($INLI[$LEVEL])) {
338						$aRenderer->doc .= (0 < $INLI[$LEVEL])
339							? '</p></li>'
340							: '</li>';
341					} // if
342					$aRenderer->doc .= '</' . $LISTS[$CURRENT[$LEVEL]]
343						. '><' . $LISTS[$aData[1]] . '>';
344					$CURRENT[$LEVEL] = $aData[1];
345				} // if
346				$aRenderer->doc .= '<li class="level' . $LEVEL . '"><p>';
347				$INLI[$LEVEL] = 1;	// closing P needed
348				return TRUE;
349			case DOKU_LEXER_UNMATCHED:
350				// $aData[0] :: match state
351				// $aData[1] :: text
352				// $aData[2] :: +1(EoT), -1(start/inbetween)
353				if (0 < $aData[2]) {
354					// last part of item's text
355					if (strlen($aData[1])) {
356						if (isset($INLI[$LEVEL])) {
357							$aRenderer->doc .= (0 < $INLI[$LEVEL]) // LI P
358								? $aData[1] . '</p></li>'
359								: '<p>' . $aData[1] . '</p></li>';
360						} else {	// no LI
361							if (1 < $LEVEL) {	// assume a trailing LI text
362								--$LEVEL;
363								$aRenderer->doc .= '</'
364									. $LISTS[$CURRENT[$LEVEL + 1]] . '><p>'
365									. $aData[1] . '</p></li>';
366							} else {
367//XXX: There must be no data w/o context; the markup is broken. Whatever we
368// could do it would be WRONG (and break XHMTL validity); hence comment:
369								$aRenderer->doc .= '<!-- '. $aData[1] .' -->';
370							} // if
371						} // if
372					} else {	// empty data
373						$hits = array();
374						if (preg_match('|\s*<li[^>]*>\s*<p>\s*$|',
375						$aRenderer->doc, $hits)) {
376							$hits = -strlen($hits[0]);
377							// remove empty list item
378							$aRenderer->doc = substr($aRenderer->doc, 0, $hits);
379						} else if (preg_match('|\s*<p>\s*$|',
380						$aRenderer->doc, $hits)) {
381							$hits = -strlen($hits[0]);
382							$aRenderer->doc =
383								substr($aRenderer->doc, 0, $hits) . '</li>';
384						} else if (isset($INLI[$LEVEL])) {
385							$aRenderer->doc .= (0 < $INLI[$LEVEL])
386								? '</p></li>'
387								: '</li>';
388						} // if
389					} // if
390					unset($INLI[$LEVEL]);
391				} else {
392					// item part between substitutions or nested blocks
393					if (isset($INLI[$LEVEL])) {
394						if (0 < $INLI[$LEVEL]) {	// LI P
395							$aRenderer->doc .= $aData[1];
396							$INLI[$LEVEL] = 1;
397						} else {	// LI
398							$aRenderer->doc .= '<p>' . $aData[1];
399						} // if
400					} else {	// data w/o context
401						if (1 < $LEVEL) {	// assume a trailing LI text
402							--$LEVEL;
403							$aRenderer->doc .= '</'
404								. $LISTS[$CURRENT[$LEVEL + 1]] . '><p>'
405								. $aData[1];
406							$INLI[$LEVEL] = 1;
407						} else {
408							$aRenderer->doc .= $aData[1];
409						} // if
410					} // if
411				} // if
412				return TRUE;
413			case DOKU_LEXER_EXIT:
414				while (1 < $LEVEL) {
415					--$LEVEL;
416					$aRenderer->doc .= '</'. $LISTS[$CURRENT[$LEVEL + 1]] .'>';
417					if (isset($INLI[$LEVEL])) {
418						$aRenderer->doc .= (0 < $INLI[$LEVEL])
419							? '</p></li>'
420							: '</li>';
421					} // if
422				} // while
423				// Since we have to use PType 'normal' we must open
424				// a new paragraph for the following text
425				$aRenderer->doc = preg_replace('|\s*<p>\s*</p>\s*|', '',
426					$aRenderer->doc) . '</'. $LISTS[$CURRENT[$LEVEL]] .'><p>';
427				$CURRENT = $INLI = array();
428				$LEVEL = 1;
429			default:
430				return TRUE;
431		} // switch
432	} // render()
433
434	//@}
435} // class syntax_plugin_lists
436} // if
437?>
438