1<?php
2if (! class_exists('syntax_plugin_deflist')) {
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_DEFLIST', 'plugin_deflist');
12
13/**
14 * <tt>syntax_plugin_deflist.php </tt>- A PHP4 class that implements
15 * a <tt>DokuWiki</tt> plugin for <tt>definition list</tt> elements.
16 *
17 * <p>
18 * Definition list pattern:<br>
19 * <tt>?? Term :: Term definition !!</tt>
20 * </p>
21 * <pre>
22 *	Copyright (C) 2005, 2007 DFG/M.Watermann, D-10247 Berlin, FRG
23 *			All rights reserved
24 *		EMail : &lt;support@mwat.de&gt;
25 * </pre>
26 * <p>
27 * <em>Credits:</em> This plugin was inspired by ideas of
28 * <a href="http://wiki.splitbrain.org/plugin:definition_list"
29 * target="_blank">Stephane Chamberland</a> and <a target="_blank"
30 * href="http://wiki.splitbrain.org/plugin:definitions">Pavel
31 * Vitis</a> both of whom wrote a similar plugin that <em>almost</em>
32 * worked.
33 * </p>
34 * <div class="disclaimer">
35 * This program is free software; you can redistribute it and/or modify
36 * it under the terms of the GNU General Public License as published by
37 * the Free Software Foundation; either
38 * <a href="http://www.gnu.org/licenses/gpl.html">version 3</a> of the
39 * License, or (at your option) any later version.<br>
40 * This software is distributed in the hope that it will be useful,
41 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
43 * General Public License for more details.
44 * </div>
45 * @author <a href="mailto:support@mwat.de">Matthias Watermann</a>
46 * @version <tt>$Id: syntax_plugin_deflist.php,v 1.14 2007/08/15 12:36:20 matthias Exp $</tt>
47 * @since created 05-Aug-2005
48 */
49class syntax_plugin_deflist extends DokuWiki_Syntax_Plugin {
50
51	/**
52	 * @privatesection
53	 */
54	//@{
55
56	/**
57	 * Convert the specified <tt>$aID</tt> to a valid XHTML
58	 * fragment identifier.
59	 *
60	 * <p>
61	 * <a href="http://www.w3.org/TR/xhtml1/#guidelines" target="_blank">
62	 * XHTML 1</a> (section C.8, Fragment Identifiers) gives the regex
63	 * <tt>[A-Za-z][A-Za-z0-9:_.-]*</tt> for valid identifiers. Here
64	 * it's slightly reduced to <tt>[A-Za-z][A-Za-z0-9_]+</tt> i.e.
65	 * all non alphanumeric characters are replaced by underscores.
66	 * </p>
67	 * @param $aID String The raw ID string.
68	 * @return String
69	 * @private
70	 * @since created 24-Aug-2005
71	 * @see render()
72	 */
73	function _makeID(&$aID) {
74		static $CHARS;
75		if (! is_array($CHARS)) {
76			$CHARS = array('|[^A-Za-z0-9_]|', // replace invalid characters
77				'|_{2,}|',		// reduce multiple underscores
78				'|^[^A-Za-z]+|',	// remove invalid leading chars
79				'|_+$|');		// remove trailing underscores
80		} // if
81		// As long as DokuWiki (in contrast to W3C) doesn't allow uppercase
82		// letters in internal anchor names we've to use 'strtolower()'
83		// here as well to make the anchors work within DokuWiki.
84		return strtolower(preg_replace($CHARS, array('_', '_'),
85			utf8_deaccent($aID, 0)));
86	} // _makeID()
87
88	//@}
89	/**
90	 * @publicsection
91	 */
92	//@{
93
94	/**
95	 * Tell the parser whether the plugin accepts syntax mode
96	 * <tt>$aMode</tt> within its own markup.
97	 *
98	 * <p>
99	 * This method mostly returns <tt>TRUE</tt> since all other types
100	 * are allowed within a definition list's <tt>DD</tt> sections.
101	 * Only another definition list is denied since <em>nested DLs are
102	 * currently not supported</em>.
103	 * </p>
104	 * @param $aMode String The requested syntaxmode.
105	 * @return Boolean <tt>TRUE</tt> unless <tt>$aMode</tt>
106	 * is <tt>plugin_deflist</tt> (which would result in a
107	 * <tt>FALSE</tt> method result).
108	 * @public
109	 * @see getAllowedTypes()
110	 */
111	function accepts($aMode) {
112		return (PLUGIN_DEFLIST != $aMode);
113	} // accepts()
114
115	/**
116	 * Connect lookup pattern to lexer.
117	 *
118	 * @param $aMode String The desired rendermode.
119	 * @public
120	 * @see render()
121	 */
122	function connectTo($aMode) {
123		if (PLUGIN_DEFLIST == $aMode) {
124			return;
125		} // if
126		// We have to use assertion patterns here to make sure the DD sections
127		// are UNMATCHED since only those are subject to further substitution.
128		$this->Lexer->addEntryPattern(
129			'\n\x20{2,}\s*\x3F\x3F(?s).+?(?=::(?s).*!!\n\n)',
130			$aMode, PLUGIN_DEFLIST);
131		$this->Lexer->addEntryPattern(
132			'\n\t+\s*\x3F\x3F(?s).+?(?=::(?s).*!!\n\n)',
133			$aMode, PLUGIN_DEFLIST);
134		$this->Lexer->addPattern(
135			'\n\x20{2,}\s*\x3F\x3F(?s).+?\s*(?=::(?s).*?!!)', PLUGIN_DEFLIST);
136		$this->Lexer->addPattern(
137			'\n\t+\s*\x3F\x3F(?s).+?\s*(?=::(?s).*?!!)', PLUGIN_DEFLIST);
138	} // connectTo()
139
140	/**
141	 * Get an associative array with plugin info.
142	 *
143	 * <p>
144	 * The returned array holds the following fields:
145	 * <dl>
146	 * <dt>author</dt><dd>Author of the plugin</dd>
147	 * <dt>email</dt><dd>Email address to contact the author</dd>
148	 * <dt>date</dt><dd>Last modified date of the plugin in
149	 * <tt>YYYY-MM-DD</tt> format</dd>
150	 * <dt>name</dt><dd>Name of the plugin</dd>
151	 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd>
152	 * <dt>url</dt><dd>Website with more information on the plugin
153	 * (eg. syntax description)</dd>
154	 * </dl>
155	 * @return Array Information about this plugin class.
156	 * @public
157	 * @static
158	 */
159	function getInfo() {
160		return array(
161			'author' =>	'Matthias Watermann',
162			'email' =>	'support@mwat.de',
163			'date' =>	'2007-08-15',
164			'name' =>	'Definition List Syntax Plugin',
165			'desc' =>	'(X)HTML style Definition Lists [ ?? Term :: Definition !! ]',
166			'url' =>	'http://wiki.splitbrain.org/plugin:deflist');
167	} // getInfo()
168
169	/**
170	 * Define how this plugin is handled regarding paragraphs.
171	 *
172	 * <p>
173	 * This method is important for correct XHTML nesting. It returns
174	 * one of the following values:
175	 * </p>
176	 * <dl>
177	 * <dt>normal</dt><dd>The plugin can be used inside paragraphs.</dd>
178	 * <dt>block</dt><dd>Open paragraphs need to be closed before
179	 * plugin output.</dd>
180	 * <dt>stack</dt><dd>Special case: Plugin wraps other paragraphs.</dd>
181	 * </dl>
182	 * @return String <tt>'normal'</tt> instead of the (correct) 'block'
183	 * since otherwise the current DokuWiki parser would put all
184	 * substitutions within a DD section in separate paragraphs.
185	 * @public
186	 * @static
187	 */
188	function getPType() {
189		return 'normal';
190	} // getPType()
191
192	/**
193	 * Where to sort in?
194	 *
195	 * @return Integer <tt>18</tt>, an arbitrary value smaller
196	 * <tt>Doku_Parser_Mode_preformated</tt> (20).
197	 * @public
198	 * @static
199	 */
200	function getSort() {
201		return 18;
202	} // getSort()
203
204	/**
205	 * Get the type of syntax this plugin defines.
206	 *
207	 * @return String <tt>'container'</tt>.
208	 * @public
209	 * @static
210	 */
211	function getType() {
212		return 'container';
213	} // getType()
214
215	/**
216	 * Handler to prepare matched data for the rendering process.
217	 *
218	 * <p>
219	 * The <tt>$aState</tt> parameter gives the type of pattern
220	 * which triggered the call to this method:
221	 * </p>
222	 * <dl>
223	 * <dt>DOKU_LEXER_ENTER</dt>
224	 * <dd>a pattern set by <tt>addEntryPattern()</tt>.</dd>
225	 * <dt>DOKU_LEXER_MATCHED</dt>
226	 * <dd>a pattern set by <tt>addPattern()</tt> (here: DT data).</dd>
227	 * <dt>DOKU_LEXER_EXIT</dt>
228	 * <dd> a pattern set by <tt>addExitPattern()</tt>.</dd>
229	 * <dt>DOKU_LEXER_UNMATCHED</dt>
230	 * <dd>ordinary text encountered within the plugin's syntax mode
231	 * which doesn't match any pattern (here: DD data).</dd>
232	 * </dl>
233	 * @param $aMatch String The text matched by the patterns.
234	 * @param $aState Integer The lexer state for the match.
235	 * @param $aPos Integer The character position of the matched text.
236	 * @param $aHandler Object Reference to the Doku_Handler object.
237	 * @return Array Index <tt>[0]</tt> holds the current
238	 * <tt>$aState</tt>, index <tt>[1]</tt> the match (as a list of
239	 * entries) prepared for the <tt>render()</tt> method.
240	 * @public
241	 * @see render()
242	 * @static
243	 */
244	function handle($aMatch, $aState, $aPos, &$aHandler) {
245		static $ESCDELIMS;	// static constants to avoid the runtime overhead
246		static $UNDELIMS; // of re-creating the arrays on each method call
247		if (! is_array($ESCDELIMS)) {
248			$ESCDELIMS = array('\?', '\!', '\:');
249		} // if
250		if (! is_array($UNDELIMS)) {
251			$UNDELIMS = array('?', '!', ':');
252		} // if
253		switch ($aState) {
254			case DOKU_LEXER_ENTER:
255				// fall through to extract initial DTs
256			case DOKU_LEXER_MATCHED:	// DTs
257				$aMatch = preg_split('|\n+(\s*\?\?)\s*|', $aMatch,
258					-1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
259				$dts = array();
260				$c = count($aMatch);
261				for ($i = 0; $c > $i; ++$i) {
262					if ($i & 1) {
263						$dts[] = array($aMatch[$i - 1],
264							str_replace($ESCDELIMS, $UNDELIMS,
265								trim($aMatch[$i])));
266						$aMatch[$i - 1] = $aMatch[$i] = NULL;
267					} else {
268						$aMatch[$i] = strlen(
269							str_replace('  ', "\t", $aMatch[$i])) - 2;
270					} // if
271				} // for
272				return array($aState, $dts);
273			case DOKU_LEXER_UNMATCHED:	// DDs
274				$aMatch = preg_split('|\s*(::\s*.*?!!)|s', $aMatch,
275					-1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
276				$mark = FALSE;	// indication for kind of DD entry
277				$c = count($aMatch);
278				$hits = $dds = array();
279				for ($i = 0; $c > $i; ++$i) {
280					if (preg_match('|::\s*(.*?)\s*!!|s', $aMatch[$i], $hits)) {
281						$mark = 0;	// complete DD w/o substitution(s)
282					} else if (preg_match('|::\s*(.*)|s', $aMatch[$i], $hits)) {
283						$mark = -1;	// DD part before substitution(s)
284					} else if (preg_match('|(.*?)\s*!!|s', $aMatch[$i], $hits)) {
285						$mark = +1;	// DD part behind substitution(s)
286					} else {
287						$mark = TRUE;	// DD part between substitutions
288						$hits[1] = $aMatch[$i];
289					} // if
290					$dds[] = array(
291						str_replace($ESCDELIMS, $UNDELIMS, $hits[1]) => $mark);
292				} // for
293				return array($aState, $dds);
294			case DOKU_LEXER_EXIT:
295				// end of list
296			default:
297				return array($aState);
298		} // switch
299	} // handle()
300
301	/**
302	 * Add exit pattern to lexer.
303	 *
304	 * <p>
305	 * Two consecutive linefeeds mark the end'o'list.
306	 * </p>
307	 * @note Access <em>public</em>
308	 */
309	function postConnect() {
310		$this->Lexer->addExitPattern('(?<=!!)\n(?=\n)', PLUGIN_DEFLIST);
311	} // postConnect()
312
313	/**
314	 * Handle the actual output creation.
315	 *
316	 * <p>
317	 * The method tests the given <tt>$aFormat</tt> returning
318	 * <tt>FALSE</tt> if it's not supported. <tt>$aRenderer</tt>
319	 * contains a reference to the renderer object which is currently
320	 * handling the rendering. The contents of <tt>$aData</tt> is the
321	 * return value of the <tt>handle()</tt> method.
322	 * </p>
323	 * @param $aFormat String The output format to being tendered.
324	 * @param $aRenderer Object A reference to the renderer object.
325	 * @param $aData Array The data created by the <tt>handle()</tt>
326	 * method.
327	 * @return Boolean <tt>TRUE</tt> if rendered successfully, or
328	 * <tt>FALSE</tt> otherwise.
329	 * @public
330	 * @see handle()
331	 * @static
332	 */
333	function render($aFormat, &$aRenderer, &$aData) {
334		if ('xhtml' != $aFormat) {
335			return FALSE;
336		} // if
337		static $LEVEL = 1;		// current nesting level
338		static $INDD = array();	// marks whether there's an open DD
339		static $CHARS;	static $ENTS;	// HTML special chars
340		if (! is_array($CHARS)) {
341			$CHARS = array('&','<', '>');
342		} // if
343		if (! is_array($ENTS)) {
344			$ENTS = array('&#38;', '&#60;', '&#62;');
345		} // if
346		// XXX: All those <p> and </p> tags handled here are just kind
347		// of workaround problems with the current DokuWiki renderer.
348		// Basically they are __wrong__ here but, alas, without them
349		// invalid HTML would be generated :-(
350		// If and when DokuWiki becomes more statefull the superflous
351		// tags should be removed.
352		switch ($aData[0]) {
353			case DOKU_LEXER_ENTER:
354				// since we have to use PType 'normal' we must close
355				// the current paragraph
356				$hits = array();
357				if (preg_match('|\s*<p>\s*$|i', $aRenderer->doc, $hits)) {
358					$aRenderer->doc = substr($aRenderer->doc,
359						0, -strlen($hits[0])) . '<dl>';
360				} else {
361					$aRenderer->doc .= '</p><dl>';
362				} // if
363				// fall through to render initial DTs
364			case DOKU_LEXER_MATCHED:
365				foreach ($aData[1] as $dt) {
366					$diff = $dt[0] - $LEVEL;
367					if (0 < $diff) {
368						// going UP __one__ level
369						++$LEVEL;
370						$hits = array();
371						if (preg_match('|\s*<dd>\s*<p>\s*$|i',
372							$aRenderer->doc, $hits)) {
373							$aRenderer->doc = substr($aRenderer->doc,
374								0, -strlen($hits[0])) . '<dd><dl>';
375						} else {
376							$aRenderer->doc .= (preg_match(
377								'|\s*</d[dt]>\s*$|i', $aRenderer->doc))
378									? '<dd><dl>'
379									: '</dd><dd><dl>';
380						} // if
381					} else if (0 > $diff) {
382						do {	// going back some levels
383							--$LEVEL;
384							$aRenderer->doc .= (isset($INDD[$LEVEL]))
385								? '</dl>'
386								: '</dl></dd>';
387						} while (0 > ++$diff);
388					// ELSE: no level change
389					} // if
390					$hits = array();
391					if (preg_match('|\s*<p>\s*$|i', $aRenderer->doc, $hits)) {
392						// remove unneeded P
393						$aRenderer->doc = substr($aRenderer->doc,
394							0, -strlen($hits[0]));
395					} // if
396					$id = $this->_makeID($dt[1]);
397					// see http://www.w3.org/TR/xhtml1/#h-4.10
398					$aRenderer->doc .= '<dt><a id="' . $id . '" name="'
399						. $id . '">' . str_replace($CHARS, $ENTS, $dt[1])
400						. '</a></dt>';
401				} // foreach
402				return TRUE;
403			case DOKU_LEXER_UNMATCHED:
404				$c = count($aData[1]);
405				for ($i = 0; $c > $i; ++$i) {
406					list($dd, $mark) = each($aData[1][$i]);
407					$dd = str_replace($CHARS, $ENTS, $dd);
408					if (TRUE === $mark) {
409						// part between substitutions
410						if (isset($INDD[$LEVEL])) {
411							if (strlen($dd)) {
412								$aRenderer->doc .= $dd;
413							} // if
414						} else {
415							$tabs = str_repeat("\t", $LEVEL);
416							$aRenderer->doc .= (strlen($dd))
417								? '</dl><p>' . $dd
418								: '</dl>';
419							$INDD[--$LEVEL] = TRUE;
420						} // if
421					} else if (0 == $mark) {
422						// complete definition w/o substitutions
423						if (strlen($dd)) {
424							$aRenderer->doc .= '<dd><p>' . $dd . '</p></dd>';
425						} // if
426					} else if (0 > $mark) {
427						// DD part before substitutions
428						$aRenderer->doc .= (strlen($dd))
429							? '<dd><p>' . $dd
430							: '<dd><p>';
431						$INDD[$LEVEL] = TRUE;
432					} else if (0 < $mark) {
433						// DD part behind substitutions
434						if (isset($INDD[$LEVEL])) {
435							if (strlen($dd)) {
436								$aRenderer->doc .= $dd . '</p></dd>';
437							} else {
438								$hits = array();
439								if (preg_match('|\s*<p>\s*$|i',
440									$aRenderer->doc, $hits)) {
441									$aRenderer->doc = substr(
442										$aRenderer->doc, 0,
443										-strlen($hits[0])) . '</dd>';
444								} else {
445									$aRenderer->doc .= '</p></dd>';
446								} // if
447							} // if
448							unset($INDD[$LEVEL]);
449						} // if
450						// ELSE: doesn't ever happen with non-empty $dd
451					} // if
452				} // for
453				return TRUE;
454			case DOKU_LEXER_EXIT:
455				// Close all possibly open lists:
456				while (0 < --$LEVEL) {
457					$aRenderer->doc .= '</dl></dd>';
458				} // while
459				// Since we have to use PType 'normal' we must open
460				// a new paragraph for the following text
461				$aRenderer->doc = preg_replace('|\s*<p>\s*</p>\s*|', '',
462					$aRenderer->doc) . '</dl><p>';
463				$INDD = array();
464				$LEVEL = 1;
465			default:
466				return TRUE;
467		} // switch
468	} // render()
469
470	//@}
471} // class syntax_plugin_deflist
472} // if
473?>
474