<?php
if (! class_exists('syntax_plugin_nstoc')) {
if (! defined('DOKU_PLUGIN')) {
	if (! defined('DOKU_INC')) {
		define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/');
	} // if
	define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
} // if
// Include parent class:
require_once(DOKU_PLUGIN . 'syntax.php');
// library providing the global 'auth_aclcheck()' function:
require_once(DOKU_INC . 'inc/auth.php');
// library providing the global 'wl()' function:
require_once(DOKU_INC . 'inc/common.php');
// library providing the global 'search()' function:
require_once(DOKU_INC . 'inc/search.php');
// library providing the global 'cleanID()'/'getID()'/'wikiFN()' functions:
require_once(DOKU_INC . 'inc/pageutils.php');

/**
 * <tt>syntax_plugin_nstoc.php </tt>- A PHP4 class that implements
 * a <tt>DokuWiki</tt> plugin to generate a
 * <em>namespace table of contents</em>.
 *
 * <p>
 * Usage:<br>
 * <tt>{{nstoc [namespace [maxdepth]]}}</tt>
 * </p><pre>
 *	Copyright (C) 2006, 2010  M.Watermann, D-10247 Berlin, FRG
 *			All rights reserved
 *		EMail : &lt;support@mwat.de&gt;
 * </pre><div class="disclaimer">
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either
 * <a href="http://www.gnu.org/licenses/gpl.html">version 3</a> of the
 * License, or (at your option) any later version.<br>
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * </div>
 * @author <a href="mailto:support@mwat.de">Matthias Watermann</a>
 * @version <tt>$Id: syntax_plugin_nstoc.php,v 1.17 2010/02/21 14:36:27 matthias Exp $</tt>
 * @since created 23-Dec-2006
 */
class syntax_plugin_nstoc extends DokuWiki_Syntax_Plugin {

	/**
	 * @privatesection
	 */
	//@{

	/**
	 * Callback function for use by the global <tt>search()</tt> function.
	 *
	 * @private
	 * @see render()
	 */
	var $_callback = NULL;

	/**
	 * HTML special characters to replace in <tt>render()</tt>.
	 *
	 * <p>
	 * This property is used to avoid repeated memory allocations
	 * inside the <tt>_doMarkup()</tt> loops.
	 * </p>
	 * @private
	 * @since created 09-Aug-2007
	 * @see _doMarkup()
	 */
	var $_Chars = array('&', '<', '>', '"');

	/**
	 * Entity replacements for HTML special characters.
	 *
	 * <p>
	 * This property is used to avoid repeated memory allocations
	 * inside the <tt>_doMarkup()</tt> loops.
	 * </p>
	 * @private
	 * @since created 09-Aug-2007
	 * @see _doMarkup()
	 */
	var $_Ents = array('&#38;', '&#60;', '&#62;', '&#34;');

	/**
	 * Lookup table for headlines ./. levels.
	 *
	 * @private
	 * @since 12-Aug-2007
	 * @see _getHeadings()
	 */
	var $_Hlevels = array('======' => 1, '=====' => 2,
		'====' => 3, '===' => 4, '==' => 5, '=' => 6);

	/**
	 * Additional markup used with older DokuWiki installations.
	 *
	 * @private
	 * @since created 20-Feb-2007
	 * @see _fixJS()
	 */
	var $_JSmarkup = FALSE;

	/**
	 * Prepare the (X)HTML markup.
	 *
	 * <p>
	 * Each entry of the given <tt>$aList</tt> (indexed by <em>page ID</em>)
	 * is expected to be a list of arrays with the respective entry's level
	 * at index <tt>0</tt> (zero) and the headline's text at index
	 * <tt>1</tt> (one) the latter of which is used to construct the
	 * respective hypertext link fragment identifier.
	 * </p>
	 * @param $aList Array The list of headlines in <tt>$aID</tt>.
	 * @return String The list markup to add to the document.
	 * @private
	 * @see render()
	 */
	function _doMarkup(&$aList) {
		$divOpen = array_fill(0, 0xff, 0);	//XXX 255 levels as in "handle()"
		$curLvl = 0;
		$markup = array();	// buffer to avoid string re-allocations
		while (list($id, $ul) = each($aList)) {
			unset($aList[$id]);	// free mem
			$link = '<a class="wikilink1" href="' . wl($id) . '#';
			while (list($a, $l) = each($ul)) {
				if ($curLvl < $l[0]) {
					// need to open a new level
					do {
						if (0 < $divOpen[$curLvl]) {
							$markup[] = '</div>';
							--$divOpen[$curLvl];
						} // if
						++$curLvl;
						$markup[] = '<ul class="nstoc"><li class="level'
							. $curLvl . '"><div class="li">';
						++$divOpen[$curLvl];
					} while ($curLvl < $l[0]);
				} else if ($curLvl > $l[0]) {
					// need to close the current level
					do {
						if (0 < $divOpen[$curLvl]) {
							$markup[] = '</div>';
							--$divOpen[$curLvl];
						} // if
						--$curLvl;
						$markup[] .= '</li></ul>';
						if (0 < $divOpen[$curLvl]) {
							$markup[] = '</div>';
							--$divOpen[$curLvl];
						} // if
					} while ($curLvl > $l[0]);
					$markup[] = '</li><li class="level' . $curLvl
						. '"><div class="li">';
					++$divOpen[$curLvl];
				} else {
					// still the current nesting level
					if (0 < $divOpen[$curLvl]) {
						$markup[] = '</div>';
					} // if
					$markup[] = '</li><li class="level' . $curLvl
						. '"><div class="li">';
				} // if
				// Prepare the current link by setting up
				// the HREF and TITLE attributes as appropriate:
				$l[0] = str_replace($this->_Chars, $this->_Ents, $l[1]);
				$markup[] = $link
					. ltrim(str_replace(':', '', cleanID($l[1])), '0123456789._-')
					. '" title="' . $l[0] . '">' . $l[0] . '</a>';
			} // while
		} // while

		// Finally close all possibly open DIV/LI/UL elements
		while (0 < $curLvl) {
			if (0 < $divOpen[$curLvl]) {
				$markup[] = '</div>';
			} // if
			$markup[] = '</li></ul>';
			--$curLvl;
		} // while

		// Return the list markup for the current document:
		return implode('', $markup);
	} // _doMarkup()

	/**
	 * Add markup to load JavaScript/CSS with older DokuWiki versions.
	 *
	 * @param $aRenderer Object The renderer used.
	 * @private
	 * @since created 20-Feb-2007
	 * @see render()
	 */
	function _fixJS(&$aRenderer) {
		if ($this->_JSmarkup) {
			return;			// Markup already added (or not needed)
		} // if

		//XXX This test will break if that DokuWiki file gets renamed:
		if (@file_exists(DOKU_INC . 'lib/exe/js.php')) {
			// Assuming a fairly recent DokuWiki installation
			// handling the plugin files on its own.
			$this->_JSmarkup = TRUE;
			return;
		} // if

		$localdir = realpath(dirname(__FILE__)) . '/';
		$webdir = DOKU_BASE . 'lib/plugins/nstoc/';
		$css = '';
		if (file_exists($localdir . 'style.css')) {
			ob_start();
			@include($localdir . 'style.css');
			// Remove whitespace from CSS and expand IMG paths:
			if ($css = preg_replace(
				array('|\s*/\x2A.*?\x2A/\s*|s', '|\s*([:;\{\},+!])\s*|',
					'|(?:url\x28\s*)([^/])|', '|^\s*|', '|\s*$|'),
				array(' ', '\1', 'url(' . $webdir . '\1'),
				ob_get_contents())) {
				$css = '<style type="text/css">' . $css . '</style>';
			} // if
			ob_end_clean();
		} // if

		$js = (file_exists($localdir . 'script.js'))
			? '<script type="text/javascript" src="'
				. $webdir . 'script.js"></script>'
			: '';
		if ($this->_JSmarkup = $css . $js) {
			// Place the additional markup at top'o'page:
			$aRenderer->doc = $this->_JSmarkup
				. preg_replace('|\s*<p>\s*</p>\s*|', '', $aRenderer->doc);
		} else {
			// Neither CSS nor JS files found.
			// Set member field to skip tests with next call:
			$this->_JSmarkup = TRUE;
		} // if
	} // _fixJS()

	/**
	 * Get a list of the headlines in the given <tt>$aID</tt> page.
	 *
	 * <p>
	 * Each entry of the returned zero-based list is an array with the
	 * respective headline's level at index <tt>0</tt> (zero)
	 * and the headline's text at index <tt>1</tt> (one).
	 * </p>
	 * @param $aID String The wiki ID to process.
	 * @param $aStartLevel Integer The initial namespace depth.
	 * @param $aMaxLevel Integer The max. nesting level allowed.
	 * @param $aDecLevel Integer Number of levels to reduce the computed
	 * level of the returned entries; either <tt>0</tt> (zero) or <tt>1</tt>.
	 * @return Mixed An array (list) of headlines or <tt>FALSE</tt>
	 * if no headline markup was found.
	 * @private
	 * @see render()
	 */
	function _getHeadings(&$aID, &$aStartLevel, &$aMaxLevel, &$aDecLevel) {
		$absLvl = $aStartLevel + $aMaxLevel;
		// The prepended colon is essential to make sure we're always
		// starting with level "1" even if processing a page/file in
		// the root namespace:
		$cl = substr_count(':' . $aID, ':');
		$hits = $result = array();
		if ($c = preg_match_all('|\n[ \t]*(={2,6}?)[\t  ]*?([^=][^\n]*[^=])\s*?\1|U',
		"\n" . io_readfile(wikiFN($aID), FALSE), $hits, PREG_SET_ORDER)) {
			for ($i = 0; $c > $i; ++$i) {
				if (($l = $cl + $this->_Hlevels[$hits[$i][1]])
				&& ($l < $absLvl)) {
					$result[] = array(
						($l - $aStartLevel) - $aDecLevel,
						$hits[$i][2]);
				} // if
				unset($hits[$i]);	// free mem
			} // for
		} // if

		// Return the list only if there was something found:
		return (0 < count($result))
			? $result
			: FALSE;
	} // _getHeadings()

	/**
	 * Resolve the given <tt>$aPath</tt> in relation to the specified
	 * <tt>$aNamespace</tt>.
	 *
	 * <p>
	 * This method tries to resolve <em>relative</em> and <em>absolute</em>
	 * pathnames depending on the given <tt>$aNamespace</tt> value.
	 * </p><p>
	 * Note that this implementation is not bulletproof but just uses
	 * string operations for its intended purpose.
	 * It's called by the public <tt>handle()</tt> method where further
	 * checks are applied. 
	 * </p>
	 * @param $aNamespace String The base namespace of <tt>$aPath</tt>:
	 * @param $aPath String The (possibly relative) path to resolve.
	 * @return String The absolute namespace/page name.
	 * @private
	 * @since created 11-Aug-2007
	 * @see handle()
	 * @static
	 */
	function _path($aNamespace, $aPath) {
		// Make sure the NS ends with a colon:
		if ($len = strlen($aNamespace)) {
			if (':' != $aNamespace[--$len]) {
				$aNamespace .= ':';
			} // if
		} else {
			$aNamespace = ':';
		} // if
		if ($len = strlen($aPath)) {
			if ('.' == $aPath) {
				return $aNamespace;
			} // if
			// Check for absolute path:
			if (':' == $aPath[0]) {
				return $aPath;
			} // if
		} else {
			// Empty path => return current namespace:
			return $aNamespace;
		} // if

		// Check for relative paths:
		if ((1 < $len)
		&& ('.' == $aPath[0])) {
			if (':' == $aPath[1]) {
				return syntax_plugin_nstoc::_path($aNamespace,
					substr($aPath, 2));
			} // if

			if ('.' == $aPath{1}) {
				// We use "preg_split()" instead of "explode()" to
				// omit empty entries:
				$path = preg_split('|:|', $aNamespace, -1, PREG_SPLIT_NO_EMPTY);
				if (count($path)) {
					// Remove the last NS element:
					array_pop($path);
					// Rebuild the whole NS path:
					$aNamespace = implode(':', $path);
					return ((2 < $len) && (':' == $aPath[2]))
						? syntax_plugin_nstoc::_path($aNamespace,
							substr($aPath, 3))
						: syntax_plugin_nstoc::_path($aNamespace,
							substr($aPath, 2));
				} // if
				// Trying to go beyond the NS start ...
				return ':';
			} // if
		} // if

		return $aNamespace . $aPath;
	} // _path()

	//@}
	/**
	 * @publicsection
	 */
	//@{

	/**
	 * Tell the parser whether the plugin accepts syntax mode
	 * <tt>$aMode</tt> within its own markup.
	 *
	 * @param $aMode String The requested syntaxmode.
	 * @return Boolean <tt>FALSE</tt> always since no nested markup
	 * is possible with this plugin.
	 * @public
	 */
	function accepts($aMode) {
		return FALSE;
	} // accepts()

	/**
	 * Connect lookup pattern to lexer.
	 *
	 * @param $aMode String The desired rendermode.
	 * @public
	 * @see render()
	 */
	function connectTo($aMode) {
		$this->Lexer->addSpecialPattern('\x7B\x7Bnstoc\s+[^\}\n\r]*\x7D\x7D',
			$aMode, 'plugin_nstoc');
	} // connectTo()

	/**
	 * Get an associative array with plugin info.
	 *
	 * <p>
	 * The returned array holds the following fields:
	 * <dl>
	 * <dt>author</dt><dd>Author of the plugin</dd>
	 * <dt>email</dt><dd>Email address to contact the author</dd>
	 * <dt>date</dt><dd>Last modified date of the plugin in
	 * <tt>YYYY-MM-DD</tt> format</dd>
	 * <dt>name</dt><dd>Name of the plugin</dd>
	 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd>
	 * <dt>url</dt><dd>Website with more information on the plugin
	 * (eg. syntax description)</dd>
	 * </dl>
	 * @return Array Information about this plugin class.
	 * @public
	 * @static
	 */
	function getInfo() {
		return array(
			'author'	=>	'Matthias Watermann',
			'email'	=>	'support@mwat.de',
			'date'	=>	'2010-02-21',
			'name'	=>	'NsToC Syntax Plugin',
			'desc'	=>	'Add a namespace\'s table of contents {'
				. '{nstoc  [namespace [maxdepth]]}}',
			'url'	=>	'http://www.dokuwiki.org/plugin:nstoc');
	} // getInfo()

	/**
	 * Define how this plugin is handled regarding paragraphs.
	 *
	 * @return String <tt>"block"</tt> (open paragraphs need to be closed
	 * before plugin output).
	 * @public
	 * @static
	 */
	function getPType() {
		return 'block';
	} // getPType()

	/**
	 * Where to sort in?
	 *
	 * @return Integer <tt>298</tt>
	 * (smaller <tt>Doku_Parser_Mode_internallink</tt>).
	 * @public
	 * @static
	 */
	function getSort() {
		return 298;
	} // getSort()

	/**
	 * Get the type of syntax this plugin defines.
	 *
	 * @return String <tt>"substition"</tt> (i.e. <em>substitution</em>).
	 * @public
	 * @static
	 */
	function getType() {
		return 'substition';	// sic! should be __substitution__
	} // getType()

	/**
	 * Handler to prepare matched data for the rendering process.
	 *
	 * <p>
	 * The <tt>$aState</tt> parameter gives the type of pattern
	 * which triggered the call to this method:
	 * </p><dl>
	 * <dt>DOKU_LEXER_SPECIAL</dt>
	 * <dd>a pattern set by <tt>addSpecialPattern()</tt></dd>
	 * </dl><p>
	 * Any other <tt>$aState</tt> value results in a no-op.
	 * </p>
	 * @param $aMatch String The text matched by the patterns.
	 * @param $aState Integer The lexer state for the match; all states but
	 * DOKU_LEXER_SPECIAL are ignored by this implementation.
	 * @param $aPos Integer The character position of the matched text.
	 * @param $aHandler Object Reference to the Doku_Handler object.
	 * @return Array List of parsed data: Index
	 * <tt>[0]</tt> holds the current <tt>$aState</tt>,
	 * <tt>[1]</tt> the base namespace to process (possibly empty),
	 * <tt>[2]</tt> the allowed nesting depth,
	 * <tt>[3]</tt> the initial nesting depth of the given base namespace
	 * and <tt>[4]</tt> a flag indicating whether to start with a file
	 * (<tt>TRUE</tt>) or directory (<tt>FALSE</tt>).
	 * @public
	 * @see render()
	 * @static
	 */
	function handle($aMatch, $aState, $aPos, &$aHandler) {
		if (DOKU_LEXER_SPECIAL != $aState) {
			// This causes "render()" to do nothing ...
			return array(DOKU_LEXER_EXIT);
		} // if

		// Extract the 0|1|2 arguments:
		$args = ($aMatch = substr($aMatch, 7, -2))
			? preg_split('|\s+|', $aMatch, -1, PREG_SPLIT_NO_EMPTY)
			: NULL;
		switch (count($args)) {
			case 0:
				$args = array('', 0);
				break;
			case 1:
				if (is_numeric($args[0])) {
					// There's a depth value only, make it numeric:
					$args[1] = $args[0] * 1;
					$args[0] = '';
				} else {
					$args[0] = str_replace('/', ':', $args[0]);
					// There's a namespace only, add depth value:
					$args[1] = 0;
				} // if
				break;
			default:
				$args[0] = str_replace('/', ':', $args[0]);
				// Make the (assumed) depth value numeric:
				$args[1] *= 1;
				break;
		} // switch

		// Compute current page and namespace:
		$current = str_replace('/', ':', getID('id', FALSE));
		$dir = '';
		for ($f = strlen($current); 0 < $f; --$f) {
			if (':' == $current[$f]) {
				$dir = substr($current, 0, $f);
				break;
			} // if
		} // for

		// Resolve paths relative to current namespace
		$args[0] = syntax_plugin_nstoc::_path($dir, $args[0]);

		// Check whether we've got the index page of a namespace: 
		global $conf;
		$idx = (empty($conf['start']))
			? 'start'
			: $conf['start'];
		if ($args[0] == $idx) {
			$args[0] = '';
		} else {
			$idx = ':' . $idx;
			$f = strlen($idx) * -1;
			if (substr($args[0], $f) == $idx) {
				$args[0] = substr($args[0], 0, $f);
			} // if
		} // if

		$f = 0;		// file flag
		// Now check whether we've got in fact a valid namespace/page:
		if ($ns = cleanID($args[0])) {
			// To compute the actual nesting level we have to test
			// whether the given ID refers to a file or directory.
			if ($f = file_exists($fn = wikiFN($ns))) {
				// If there is a file set the flag to FALSE if there's
				// a directory (i.e. namespace) with the same name:
				$f = (! is_dir(substr($fn, 0, -4)));
			} // if
			// Make the file flag's numeric so it's usable
			// for computing the actual starting level:
			$f *= 1;
			// Compute the initial nesting level:
			$args[0] = ($f)
				? 2 + substr_count($ns, ':')
				: 1 + substr_count($ns, ':');
		} else {
			// we're in the root namespace either explicitely or
			// by an argument that resolved to root.
			$args[0] = 1;
		} // if

		// Check the allowed nesting level value:
		if (0 < $args[1]) {
			if (! $f) {
				// For directories we need extra levels
				if ('' == $ns) {
					++$args[1];
				} else {
					$args[1] += 2;
				} // if
			} // if
		} else {
			//XXX In case no depth argument was given we use a
			// value of 255 which should be reasonably great enough
			// (see "_doMarkup()").
			$args[1] = 0xff;
		} // if

		// Finally prepare the data used by "render()":
		return array(DOKU_LEXER_SPECIAL, $ns, $args[1], $args[0], (bool)$f);
	} // handle()

	/**
	 * Handle the actual output creation.
	 *
	 * <p>
	 * The method checks for the given <tt>$aFormat</tt> and returns
	 * <tt>FALSE</tt> when a format isn't supported.
	 * <tt>$aRenderer</tt> contains a reference to the renderer object
	 * which is currently handling the rendering.
	 * The contents of <tt>$aData</tt> is the return value of the
	 * <tt>handle()</tt> method.
	 * </p><p>
	 * This implementation uses the precomputed values of <tt>$aData</tt>
	 * to generate a list of headlines marked up as a (X)HTML list.
	 * </p>
	 * @param $aFormat String The output format to generate.
	 * @param $aRenderer Object Reference to the <tt>Doku_Renderer_xhtml</tt>
	 * object to use.
	 * @param $aData Array The data created/returned by the
	 * <tt>handle()</tt> method.
	 * @return Boolean <tt>TRUE</tt> if rendered successfully, or
	 * <tt>FALSE</tt> otherwise.
	 * @public
	 * @see handle()
	 */
	function render($aFormat, &$aRenderer, &$aData) {
		if ('xhtml' != $aFormat) {
			return FALSE;			// nothing to do for other formats
		} // if
		if (DOKU_LEXER_SPECIAL != $aData[0]) {
			return TRUE;			// nothing to do for other states
		} // if

		global $conf;
		$ids = array();
		if ($aData[4]) {
			// It's just a single file to process
			$ids[0] = $aData[1];
			// The var is recycled to hold the level decrement value
			// used by "_getHeadings()" to compute the actual LI level
			// attribute:
			$aData[1] = -1;
		} else {
			// Unfortunately the global "search()" function isn't able
			// to use methods (even static class methods) but insists
			// on an ordinary function to be passed as a calltime
			// argument (at least up to DokuWiki 2006-03-05).
			// To avoid polluting the global namespace even more than
			// it already is we use a private member function which we
			// can pass to DokuWiki's global "search()" function.
			if (! $this->_callback) {
				$idx = (empty($conf['start']))
					? 'start'
					: $conf['start'];
				$iLen = (strlen($idx) + 1) * -1;	// "+1" for the NS colon
				// Here we filter out the "index" pages i.e. pages either
				// named as configured in the global "$conf['start']" or
				// with the same name as a sub-directory.
				$this->_callback = create_function(
				'&$aData, $aBase, $aFile, $aType, $aLvl, $opts',
				'if (("f" == $aType) && (".txt" == substr($aFile, -4))'
				. '&& (! is_dir($aBase .  "/" . substr($aFile, 0, -4)))'
				. '&& ($aFile = pathID($aFile)) && ($aFile != "' . $idx . '")'
				. '&& (substr($aFile, ' . $iLen . ') != ":' . $idx . '")) {'
					. '$aData[] = $aFile;}'
				. 'return TRUE;');
			} // if
			// Call DokuWiki's global search function:
			if ('' == $aData[1]) {
				search($ids, $conf['datadir'], $this->_callback,
					FALSE, $aData[1], 0);
				$aData[1] = 0;	// setup level decrement for "_getHeadings()"
			} else {
				search($ids, $conf['datadir'], $this->_callback,
					FALSE, str_replace(':', '/', $aData[1]), 0);
				$aData[1] = 1;	// setup level decrement for "_getHeadings()"
			} // if
			sort($ids);
		} // if

		global $USERINFO;
		$g =& $USERINFO['grps'];	// Preparing references saves array ..
		$u =& $_SERVER['REMOTE_USER'];	// .. lookups within the loops below.
		$pages = array();
		// To avoid repeated boolean and regEx tests if unneeded
		// we unroll the loop saving lots of CPU cycles.
		if (empty($conf['hidepages'])) {
			while (list($i, $entry) = each($ids)) {
				unset($ids[$i]);	// free mem
				// Use only pages which are actually
				// readable for the current user:
				if ((0 < auth_aclcheck($entry, $u, $g))
				&& ($i = $this->_getHeadings($entry, $aData[3],
				$aData[2], $aData[1]))) {
					$pages[$entry] = $i;
				} // if
			} // while
			unset($entry, $i, $ids);	// free mem
		} else {
			$re = '/' . $conf['hidepages'] . '/ui';
			while (list($i, $entry) = each($ids)) {
				unset($ids[$i]);	// free mem
				// Use only pages which are actually readable for the
				// current user and not supposed to be "hidden":
				if ((0 < auth_aclcheck($entry, $u, $g))
				&& (! preg_match($re, ':' . $entry))
				&& ($i = $this->_getHeadings($entry, $aData[3],
				$aData[2], $aData[1]))) {
					$pages[$entry] = $i;
				} // if
			} // while
			unset($entry, $i, $ids, $re);	// free mem
		} // if

		if (0 < count($pages)) {
			$this->_fixJS($aRenderer);	// check for old DokuWiki versions
			$aRenderer->doc .= $this->_doMarkup($pages);
		} // if

		return TRUE;
	} // render()

	//@}
} // class syntax_plugin_nstoc
} // if
?>
