1<?php
2if (! class_exists('syntax_plugin_nstoc')) {
3if (! 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:
10require_once(DOKU_PLUGIN . 'syntax.php');
11// library providing the global 'auth_aclcheck()' function:
12require_once(DOKU_INC . 'inc/auth.php');
13// library providing the global 'wl()' function:
14require_once(DOKU_INC . 'inc/common.php');
15// library providing the global 'search()' function:
16require_once(DOKU_INC . 'inc/search.php');
17// library providing the global 'cleanID()'/'getID()'/'wikiFN()' functions:
18require_once(DOKU_INC . 'inc/pageutils.php');
19
20/**
21 * <tt>syntax_plugin_nstoc.php </tt>- A PHP4 class that implements
22 * a <tt>DokuWiki</tt> plugin to generate a
23 * <em>namespace table of contents</em>.
24 *
25 * <p>
26 * Usage:<br>
27 * <tt>{{nstoc [namespace [maxdepth]]}}</tt>
28 * </p><pre>
29 *	Copyright (C) 2006, 2010  M.Watermann, D-10247 Berlin, FRG
30 *			All rights reserved
31 *		EMail : &lt;support@mwat.de&gt;
32 * </pre><div class="disclaimer">
33 * This program is free software; you can redistribute it and/or modify
34 * it under the terms of the GNU General Public License as published by
35 * the Free Software Foundation; either
36 * <a href="http://www.gnu.org/licenses/gpl.html">version 3</a> of the
37 * License, or (at your option) any later version.<br>
38 * This software is distributed in the hope that it will be useful,
39 * but WITHOUT ANY WARRANTY; without even the implied warranty of
40 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
41 * General Public License for more details.
42 * </div>
43 * @author <a href="mailto:support@mwat.de">Matthias Watermann</a>
44 * @version <tt>$Id: syntax_plugin_nstoc.php,v 1.17 2010/02/21 14:36:27 matthias Exp $</tt>
45 * @since created 23-Dec-2006
46 */
47class syntax_plugin_nstoc extends DokuWiki_Syntax_Plugin {
48
49	/**
50	 * @privatesection
51	 */
52	//@{
53
54	/**
55	 * Callback function for use by the global <tt>search()</tt> function.
56	 *
57	 * @private
58	 * @see render()
59	 */
60	var $_callback = NULL;
61
62	/**
63	 * HTML special characters to replace in <tt>render()</tt>.
64	 *
65	 * <p>
66	 * This property is used to avoid repeated memory allocations
67	 * inside the <tt>_doMarkup()</tt> loops.
68	 * </p>
69	 * @private
70	 * @since created 09-Aug-2007
71	 * @see _doMarkup()
72	 */
73	var $_Chars = array('&', '<', '>', '"');
74
75	/**
76	 * Entity replacements for HTML special characters.
77	 *
78	 * <p>
79	 * This property is used to avoid repeated memory allocations
80	 * inside the <tt>_doMarkup()</tt> loops.
81	 * </p>
82	 * @private
83	 * @since created 09-Aug-2007
84	 * @see _doMarkup()
85	 */
86	var $_Ents = array('&#38;', '&#60;', '&#62;', '&#34;');
87
88	/**
89	 * Lookup table for headlines ./. levels.
90	 *
91	 * @private
92	 * @since 12-Aug-2007
93	 * @see _getHeadings()
94	 */
95	var $_Hlevels = array('======' => 1, '=====' => 2,
96		'====' => 3, '===' => 4, '==' => 5, '=' => 6);
97
98	/**
99	 * Additional markup used with older DokuWiki installations.
100	 *
101	 * @private
102	 * @since created 20-Feb-2007
103	 * @see _fixJS()
104	 */
105	var $_JSmarkup = FALSE;
106
107	/**
108	 * Prepare the (X)HTML markup.
109	 *
110	 * <p>
111	 * Each entry of the given <tt>$aList</tt> (indexed by <em>page ID</em>)
112	 * is expected to be a list of arrays with the respective entry's level
113	 * at index <tt>0</tt> (zero) and the headline's text at index
114	 * <tt>1</tt> (one) the latter of which is used to construct the
115	 * respective hypertext link fragment identifier.
116	 * </p>
117	 * @param $aList Array The list of headlines in <tt>$aID</tt>.
118	 * @return String The list markup to add to the document.
119	 * @private
120	 * @see render()
121	 */
122	function _doMarkup(&$aList) {
123		$divOpen = array_fill(0, 0xff, 0);	//XXX 255 levels as in "handle()"
124		$curLvl = 0;
125		$markup = array();	// buffer to avoid string re-allocations
126		while (list($id, $ul) = each($aList)) {
127			unset($aList[$id]);	// free mem
128			$link = '<a class="wikilink1" href="' . wl($id) . '#';
129			while (list($a, $l) = each($ul)) {
130				if ($curLvl < $l[0]) {
131					// need to open a new level
132					do {
133						if (0 < $divOpen[$curLvl]) {
134							$markup[] = '</div>';
135							--$divOpen[$curLvl];
136						} // if
137						++$curLvl;
138						$markup[] = '<ul class="nstoc"><li class="level'
139							. $curLvl . '"><div class="li">';
140						++$divOpen[$curLvl];
141					} while ($curLvl < $l[0]);
142				} else if ($curLvl > $l[0]) {
143					// need to close the current level
144					do {
145						if (0 < $divOpen[$curLvl]) {
146							$markup[] = '</div>';
147							--$divOpen[$curLvl];
148						} // if
149						--$curLvl;
150						$markup[] .= '</li></ul>';
151						if (0 < $divOpen[$curLvl]) {
152							$markup[] = '</div>';
153							--$divOpen[$curLvl];
154						} // if
155					} while ($curLvl > $l[0]);
156					$markup[] = '</li><li class="level' . $curLvl
157						. '"><div class="li">';
158					++$divOpen[$curLvl];
159				} else {
160					// still the current nesting level
161					if (0 < $divOpen[$curLvl]) {
162						$markup[] = '</div>';
163					} // if
164					$markup[] = '</li><li class="level' . $curLvl
165						. '"><div class="li">';
166				} // if
167				// Prepare the current link by setting up
168				// the HREF and TITLE attributes as appropriate:
169				$l[0] = str_replace($this->_Chars, $this->_Ents, $l[1]);
170				$markup[] = $link
171					. ltrim(str_replace(':', '', cleanID($l[1])), '0123456789._-')
172					. '" title="' . $l[0] . '">' . $l[0] . '</a>';
173			} // while
174		} // while
175
176		// Finally close all possibly open DIV/LI/UL elements
177		while (0 < $curLvl) {
178			if (0 < $divOpen[$curLvl]) {
179				$markup[] = '</div>';
180			} // if
181			$markup[] = '</li></ul>';
182			--$curLvl;
183		} // while
184
185		// Return the list markup for the current document:
186		return implode('', $markup);
187	} // _doMarkup()
188
189	/**
190	 * Add markup to load JavaScript/CSS with older DokuWiki versions.
191	 *
192	 * @param $aRenderer Object The renderer used.
193	 * @private
194	 * @since created 20-Feb-2007
195	 * @see render()
196	 */
197	function _fixJS(&$aRenderer) {
198		if ($this->_JSmarkup) {
199			return;			// Markup already added (or not needed)
200		} // if
201
202		//XXX This test will break if that DokuWiki file gets renamed:
203		if (@file_exists(DOKU_INC . 'lib/exe/js.php')) {
204			// Assuming a fairly recent DokuWiki installation
205			// handling the plugin files on its own.
206			$this->_JSmarkup = TRUE;
207			return;
208		} // if
209
210		$localdir = realpath(dirname(__FILE__)) . '/';
211		$webdir = DOKU_BASE . 'lib/plugins/nstoc/';
212		$css = '';
213		if (file_exists($localdir . 'style.css')) {
214			ob_start();
215			@include($localdir . 'style.css');
216			// Remove whitespace from CSS and expand IMG paths:
217			if ($css = preg_replace(
218				array('|\s*/\x2A.*?\x2A/\s*|s', '|\s*([:;\{\},+!])\s*|',
219					'|(?:url\x28\s*)([^/])|', '|^\s*|', '|\s*$|'),
220				array(' ', '\1', 'url(' . $webdir . '\1'),
221				ob_get_contents())) {
222				$css = '<style type="text/css">' . $css . '</style>';
223			} // if
224			ob_end_clean();
225		} // if
226
227		$js = (file_exists($localdir . 'script.js'))
228			? '<script type="text/javascript" src="'
229				. $webdir . 'script.js"></script>'
230			: '';
231		if ($this->_JSmarkup = $css . $js) {
232			// Place the additional markup at top'o'page:
233			$aRenderer->doc = $this->_JSmarkup
234				. preg_replace('|\s*<p>\s*</p>\s*|', '', $aRenderer->doc);
235		} else {
236			// Neither CSS nor JS files found.
237			// Set member field to skip tests with next call:
238			$this->_JSmarkup = TRUE;
239		} // if
240	} // _fixJS()
241
242	/**
243	 * Get a list of the headlines in the given <tt>$aID</tt> page.
244	 *
245	 * <p>
246	 * Each entry of the returned zero-based list is an array with the
247	 * respective headline's level at index <tt>0</tt> (zero)
248	 * and the headline's text at index <tt>1</tt> (one).
249	 * </p>
250	 * @param $aID String The wiki ID to process.
251	 * @param $aStartLevel Integer The initial namespace depth.
252	 * @param $aMaxLevel Integer The max. nesting level allowed.
253	 * @param $aDecLevel Integer Number of levels to reduce the computed
254	 * level of the returned entries; either <tt>0</tt> (zero) or <tt>1</tt>.
255	 * @return Mixed An array (list) of headlines or <tt>FALSE</tt>
256	 * if no headline markup was found.
257	 * @private
258	 * @see render()
259	 */
260	function _getHeadings(&$aID, &$aStartLevel, &$aMaxLevel, &$aDecLevel) {
261		$absLvl = $aStartLevel + $aMaxLevel;
262		// The prepended colon is essential to make sure we're always
263		// starting with level "1" even if processing a page/file in
264		// the root namespace:
265		$cl = substr_count(':' . $aID, ':');
266		$hits = $result = array();
267		if ($c = preg_match_all('|\n[ \t]*(={2,6}?)[\t  ]*?([^=][^\n]*[^=])\s*?\1|U',
268		"\n" . io_readfile(wikiFN($aID), FALSE), $hits, PREG_SET_ORDER)) {
269			for ($i = 0; $c > $i; ++$i) {
270				if (($l = $cl + $this->_Hlevels[$hits[$i][1]])
271				&& ($l < $absLvl)) {
272					$result[] = array(
273						($l - $aStartLevel) - $aDecLevel,
274						$hits[$i][2]);
275				} // if
276				unset($hits[$i]);	// free mem
277			} // for
278		} // if
279
280		// Return the list only if there was something found:
281		return (0 < count($result))
282			? $result
283			: FALSE;
284	} // _getHeadings()
285
286	/**
287	 * Resolve the given <tt>$aPath</tt> in relation to the specified
288	 * <tt>$aNamespace</tt>.
289	 *
290	 * <p>
291	 * This method tries to resolve <em>relative</em> and <em>absolute</em>
292	 * pathnames depending on the given <tt>$aNamespace</tt> value.
293	 * </p><p>
294	 * Note that this implementation is not bulletproof but just uses
295	 * string operations for its intended purpose.
296	 * It's called by the public <tt>handle()</tt> method where further
297	 * checks are applied.
298	 * </p>
299	 * @param $aNamespace String The base namespace of <tt>$aPath</tt>:
300	 * @param $aPath String The (possibly relative) path to resolve.
301	 * @return String The absolute namespace/page name.
302	 * @private
303	 * @since created 11-Aug-2007
304	 * @see handle()
305	 * @static
306	 */
307	function _path($aNamespace, $aPath) {
308		// Make sure the NS ends with a colon:
309		if ($len = strlen($aNamespace)) {
310			if (':' != $aNamespace[--$len]) {
311				$aNamespace .= ':';
312			} // if
313		} else {
314			$aNamespace = ':';
315		} // if
316		if ($len = strlen($aPath)) {
317			if ('.' == $aPath) {
318				return $aNamespace;
319			} // if
320			// Check for absolute path:
321			if (':' == $aPath[0]) {
322				return $aPath;
323			} // if
324		} else {
325			// Empty path => return current namespace:
326			return $aNamespace;
327		} // if
328
329		// Check for relative paths:
330		if ((1 < $len)
331		&& ('.' == $aPath[0])) {
332			if (':' == $aPath[1]) {
333				return syntax_plugin_nstoc::_path($aNamespace,
334					substr($aPath, 2));
335			} // if
336
337			if ('.' == $aPath{1}) {
338				// We use "preg_split()" instead of "explode()" to
339				// omit empty entries:
340				$path = preg_split('|:|', $aNamespace, -1, PREG_SPLIT_NO_EMPTY);
341				if (count($path)) {
342					// Remove the last NS element:
343					array_pop($path);
344					// Rebuild the whole NS path:
345					$aNamespace = implode(':', $path);
346					return ((2 < $len) && (':' == $aPath[2]))
347						? syntax_plugin_nstoc::_path($aNamespace,
348							substr($aPath, 3))
349						: syntax_plugin_nstoc::_path($aNamespace,
350							substr($aPath, 2));
351				} // if
352				// Trying to go beyond the NS start ...
353				return ':';
354			} // if
355		} // if
356
357		return $aNamespace . $aPath;
358	} // _path()
359
360	//@}
361	/**
362	 * @publicsection
363	 */
364	//@{
365
366	/**
367	 * Tell the parser whether the plugin accepts syntax mode
368	 * <tt>$aMode</tt> within its own markup.
369	 *
370	 * @param $aMode String The requested syntaxmode.
371	 * @return Boolean <tt>FALSE</tt> always since no nested markup
372	 * is possible with this plugin.
373	 * @public
374	 */
375	function accepts($aMode) {
376		return FALSE;
377	} // accepts()
378
379	/**
380	 * Connect lookup pattern to lexer.
381	 *
382	 * @param $aMode String The desired rendermode.
383	 * @public
384	 * @see render()
385	 */
386	function connectTo($aMode) {
387		$this->Lexer->addSpecialPattern('\x7B\x7Bnstoc\s+[^\}\n\r]*\x7D\x7D',
388			$aMode, 'plugin_nstoc');
389	} // connectTo()
390
391	/**
392	 * Get an associative array with plugin info.
393	 *
394	 * <p>
395	 * The returned array holds the following fields:
396	 * <dl>
397	 * <dt>author</dt><dd>Author of the plugin</dd>
398	 * <dt>email</dt><dd>Email address to contact the author</dd>
399	 * <dt>date</dt><dd>Last modified date of the plugin in
400	 * <tt>YYYY-MM-DD</tt> format</dd>
401	 * <dt>name</dt><dd>Name of the plugin</dd>
402	 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd>
403	 * <dt>url</dt><dd>Website with more information on the plugin
404	 * (eg. syntax description)</dd>
405	 * </dl>
406	 * @return Array Information about this plugin class.
407	 * @public
408	 * @static
409	 */
410	function getInfo() {
411		return array(
412			'author'	=>	'Matthias Watermann',
413			'email'	=>	'support@mwat.de',
414			'date'	=>	'2010-02-21',
415			'name'	=>	'NsToC Syntax Plugin',
416			'desc'	=>	'Add a namespace\'s table of contents {'
417				. '{nstoc  [namespace [maxdepth]]}}',
418			'url'	=>	'http://www.dokuwiki.org/plugin:nstoc');
419	} // getInfo()
420
421	/**
422	 * Define how this plugin is handled regarding paragraphs.
423	 *
424	 * @return String <tt>"block"</tt> (open paragraphs need to be closed
425	 * before plugin output).
426	 * @public
427	 * @static
428	 */
429	function getPType() {
430		return 'block';
431	} // getPType()
432
433	/**
434	 * Where to sort in?
435	 *
436	 * @return Integer <tt>298</tt>
437	 * (smaller <tt>Doku_Parser_Mode_internallink</tt>).
438	 * @public
439	 * @static
440	 */
441	function getSort() {
442		return 298;
443	} // getSort()
444
445	/**
446	 * Get the type of syntax this plugin defines.
447	 *
448	 * @return String <tt>"substition"</tt> (i.e. <em>substitution</em>).
449	 * @public
450	 * @static
451	 */
452	function getType() {
453		return 'substition';	// sic! should be __substitution__
454	} // getType()
455
456	/**
457	 * Handler to prepare matched data for the rendering process.
458	 *
459	 * <p>
460	 * The <tt>$aState</tt> parameter gives the type of pattern
461	 * which triggered the call to this method:
462	 * </p><dl>
463	 * <dt>DOKU_LEXER_SPECIAL</dt>
464	 * <dd>a pattern set by <tt>addSpecialPattern()</tt></dd>
465	 * </dl><p>
466	 * Any other <tt>$aState</tt> value results in a no-op.
467	 * </p>
468	 * @param $aMatch String The text matched by the patterns.
469	 * @param $aState Integer The lexer state for the match; all states but
470	 * DOKU_LEXER_SPECIAL are ignored by this implementation.
471	 * @param $aPos Integer The character position of the matched text.
472	 * @param $aHandler Object Reference to the Doku_Handler object.
473	 * @return Array List of parsed data: Index
474	 * <tt>[0]</tt> holds the current <tt>$aState</tt>,
475	 * <tt>[1]</tt> the base namespace to process (possibly empty),
476	 * <tt>[2]</tt> the allowed nesting depth,
477	 * <tt>[3]</tt> the initial nesting depth of the given base namespace
478	 * and <tt>[4]</tt> a flag indicating whether to start with a file
479	 * (<tt>TRUE</tt>) or directory (<tt>FALSE</tt>).
480	 * @public
481	 * @see render()
482	 * @static
483	 */
484	function handle($aMatch, $aState, $aPos, &$aHandler) {
485		if (DOKU_LEXER_SPECIAL != $aState) {
486			// This causes "render()" to do nothing ...
487			return array(DOKU_LEXER_EXIT);
488		} // if
489
490		// Extract the 0|1|2 arguments:
491		$args = ($aMatch = substr($aMatch, 7, -2))
492			? preg_split('|\s+|', $aMatch, -1, PREG_SPLIT_NO_EMPTY)
493			: NULL;
494		switch (count($args)) {
495			case 0:
496				$args = array('', 0);
497				break;
498			case 1:
499				if (is_numeric($args[0])) {
500					// There's a depth value only, make it numeric:
501					$args[1] = $args[0] * 1;
502					$args[0] = '';
503				} else {
504					$args[0] = str_replace('/', ':', $args[0]);
505					// There's a namespace only, add depth value:
506					$args[1] = 0;
507				} // if
508				break;
509			default:
510				$args[0] = str_replace('/', ':', $args[0]);
511				// Make the (assumed) depth value numeric:
512				$args[1] *= 1;
513				break;
514		} // switch
515
516		// Compute current page and namespace:
517		$current = str_replace('/', ':', getID('id', FALSE));
518		$dir = '';
519		for ($f = strlen($current); 0 < $f; --$f) {
520			if (':' == $current[$f]) {
521				$dir = substr($current, 0, $f);
522				break;
523			} // if
524		} // for
525
526		// Resolve paths relative to current namespace
527		$args[0] = syntax_plugin_nstoc::_path($dir, $args[0]);
528
529		// Check whether we've got the index page of a namespace:
530		global $conf;
531		$idx = (empty($conf['start']))
532			? 'start'
533			: $conf['start'];
534		if ($args[0] == $idx) {
535			$args[0] = '';
536		} else {
537			$idx = ':' . $idx;
538			$f = strlen($idx) * -1;
539			if (substr($args[0], $f) == $idx) {
540				$args[0] = substr($args[0], 0, $f);
541			} // if
542		} // if
543
544		$f = 0;		// file flag
545		// Now check whether we've got in fact a valid namespace/page:
546		if ($ns = cleanID($args[0])) {
547			// To compute the actual nesting level we have to test
548			// whether the given ID refers to a file or directory.
549			if ($f = file_exists($fn = wikiFN($ns))) {
550				// If there is a file set the flag to FALSE if there's
551				// a directory (i.e. namespace) with the same name:
552				$f = (! is_dir(substr($fn, 0, -4)));
553			} // if
554			// Make the file flag's numeric so it's usable
555			// for computing the actual starting level:
556			$f *= 1;
557			// Compute the initial nesting level:
558			$args[0] = ($f)
559				? 2 + substr_count($ns, ':')
560				: 1 + substr_count($ns, ':');
561		} else {
562			// we're in the root namespace either explicitely or
563			// by an argument that resolved to root.
564			$args[0] = 1;
565		} // if
566
567		// Check the allowed nesting level value:
568		if (0 < $args[1]) {
569			if (! $f) {
570				// For directories we need extra levels
571				if ('' == $ns) {
572					++$args[1];
573				} else {
574					$args[1] += 2;
575				} // if
576			} // if
577		} else {
578			//XXX In case no depth argument was given we use a
579			// value of 255 which should be reasonably great enough
580			// (see "_doMarkup()").
581			$args[1] = 0xff;
582		} // if
583
584		// Finally prepare the data used by "render()":
585		return array(DOKU_LEXER_SPECIAL, $ns, $args[1], $args[0], (bool)$f);
586	} // handle()
587
588	/**
589	 * Handle the actual output creation.
590	 *
591	 * <p>
592	 * The method checks for the given <tt>$aFormat</tt> and returns
593	 * <tt>FALSE</tt> when a format isn't supported.
594	 * <tt>$aRenderer</tt> contains a reference to the renderer object
595	 * which is currently handling the rendering.
596	 * The contents of <tt>$aData</tt> is the return value of the
597	 * <tt>handle()</tt> method.
598	 * </p><p>
599	 * This implementation uses the precomputed values of <tt>$aData</tt>
600	 * to generate a list of headlines marked up as a (X)HTML list.
601	 * </p>
602	 * @param $aFormat String The output format to generate.
603	 * @param $aRenderer Object Reference to the <tt>Doku_Renderer_xhtml</tt>
604	 * object to use.
605	 * @param $aData Array The data created/returned by the
606	 * <tt>handle()</tt> method.
607	 * @return Boolean <tt>TRUE</tt> if rendered successfully, or
608	 * <tt>FALSE</tt> otherwise.
609	 * @public
610	 * @see handle()
611	 */
612	function render($aFormat, &$aRenderer, &$aData) {
613		if ('xhtml' != $aFormat) {
614			return FALSE;			// nothing to do for other formats
615		} // if
616		if (DOKU_LEXER_SPECIAL != $aData[0]) {
617			return TRUE;			// nothing to do for other states
618		} // if
619
620		global $conf;
621		$ids = array();
622		if ($aData[4]) {
623			// It's just a single file to process
624			$ids[0] = $aData[1];
625			// The var is recycled to hold the level decrement value
626			// used by "_getHeadings()" to compute the actual LI level
627			// attribute:
628			$aData[1] = -1;
629		} else {
630			// Unfortunately the global "search()" function isn't able
631			// to use methods (even static class methods) but insists
632			// on an ordinary function to be passed as a calltime
633			// argument (at least up to DokuWiki 2006-03-05).
634			// To avoid polluting the global namespace even more than
635			// it already is we use a private member function which we
636			// can pass to DokuWiki's global "search()" function.
637			if (! $this->_callback) {
638				$idx = (empty($conf['start']))
639					? 'start'
640					: $conf['start'];
641				$iLen = (strlen($idx) + 1) * -1;	// "+1" for the NS colon
642				// Here we filter out the "index" pages i.e. pages either
643				// named as configured in the global "$conf['start']" or
644				// with the same name as a sub-directory.
645				$this->_callback = create_function(
646				'&$aData, $aBase, $aFile, $aType, $aLvl, $opts',
647				'if (("f" == $aType) && (".txt" == substr($aFile, -4))'
648				. '&& (! is_dir($aBase .  "/" . substr($aFile, 0, -4)))'
649				. '&& ($aFile = pathID($aFile)) && ($aFile != "' . $idx . '")'
650				. '&& (substr($aFile, ' . $iLen . ') != ":' . $idx . '")) {'
651					. '$aData[] = $aFile;}'
652				. 'return TRUE;');
653			} // if
654			// Call DokuWiki's global search function:
655			if ('' == $aData[1]) {
656				search($ids, $conf['datadir'], $this->_callback,
657					FALSE, $aData[1], 0);
658				$aData[1] = 0;	// setup level decrement for "_getHeadings()"
659			} else {
660				search($ids, $conf['datadir'], $this->_callback,
661					FALSE, str_replace(':', '/', $aData[1]), 0);
662				$aData[1] = 1;	// setup level decrement for "_getHeadings()"
663			} // if
664			sort($ids);
665		} // if
666
667		global $USERINFO;
668		$g =& $USERINFO['grps'];	// Preparing references saves array ..
669		$u =& $_SERVER['REMOTE_USER'];	// .. lookups within the loops below.
670		$pages = array();
671		// To avoid repeated boolean and regEx tests if unneeded
672		// we unroll the loop saving lots of CPU cycles.
673		if (empty($conf['hidepages'])) {
674			while (list($i, $entry) = each($ids)) {
675				unset($ids[$i]);	// free mem
676				// Use only pages which are actually
677				// readable for the current user:
678				if ((0 < auth_aclcheck($entry, $u, $g))
679				&& ($i = $this->_getHeadings($entry, $aData[3],
680				$aData[2], $aData[1]))) {
681					$pages[$entry] = $i;
682				} // if
683			} // while
684			unset($entry, $i, $ids);	// free mem
685		} else {
686			$re = '/' . $conf['hidepages'] . '/ui';
687			while (list($i, $entry) = each($ids)) {
688				unset($ids[$i]);	// free mem
689				// Use only pages which are actually readable for the
690				// current user and not supposed to be "hidden":
691				if ((0 < auth_aclcheck($entry, $u, $g))
692				&& (! preg_match($re, ':' . $entry))
693				&& ($i = $this->_getHeadings($entry, $aData[3],
694				$aData[2], $aData[1]))) {
695					$pages[$entry] = $i;
696				} // if
697			} // while
698			unset($entry, $i, $ids, $re);	// free mem
699		} // if
700
701		if (0 < count($pages)) {
702			$this->_fixJS($aRenderer);	// check for old DokuWiki versions
703			$aRenderer->doc .= $this->_doMarkup($pages);
704		} // if
705
706		return TRUE;
707	} // render()
708
709	//@}
710} // class syntax_plugin_nstoc
711} // if
712?>
713