syntax_plugin_nstoc.php - A PHP4 class that implements * a DokuWiki plugin to generate a * namespace table of contents. * *
* Usage:
* {{nstoc [namespace [maxdepth]]}}
*
* Copyright (C) 2006, 2010 M.Watermann, D-10247 Berlin, FRG * All rights reserved * EMail : <support@mwat.de> *
* This property is used to avoid repeated memory allocations * inside the _doMarkup() loops. *
* @private * @since created 09-Aug-2007 * @see _doMarkup() */ var $_Chars = array('&', '<', '>', '"'); /** * Entity replacements for HTML special characters. * ** This property is used to avoid repeated memory allocations * inside the _doMarkup() loops. *
* @private * @since created 09-Aug-2007 * @see _doMarkup() */ var $_Ents = array('&', '<', '>', '"'); /** * 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. * ** Each entry of the given $aList (indexed by page ID) * is expected to be a list of arrays with the respective entry's level * at index 0 (zero) and the headline's text at index * 1 (one) the latter of which is used to construct the * respective hypertext link fragment identifier. *
* @param $aList Array The list of headlines in $aID. * @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 = '\s*
\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 $aID page. * ** Each entry of the returned zero-based list is an array with the * respective headline's level at index 0 (zero) * and the headline's text at index 1 (one). *
* @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 0 (zero) or 1. * @return Mixed An array (list) of headlines or FALSE * 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 $aPath in relation to the specified * $aNamespace. * ** This method tries to resolve relative and absolute * pathnames depending on the given $aNamespace value. *
* Note that this implementation is not bulletproof but just uses * string operations for its intended purpose. * It's called by the public handle() method where further * checks are applied. *
* @param $aNamespace String The base namespace of $aPath: * @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 * $aMode within its own markup. * * @param $aMode String The requested syntaxmode. * @return Boolean FALSE 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. * ** The returned array holds the following fields: *
* The $aState parameter gives the type of pattern * which triggered the call to this method: *
* Any other $aState value results in a no-op. *
* @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 * [0] holds the current $aState, * [1] the base namespace to process (possibly empty), * [2] the allowed nesting depth, * [3] the initial nesting depth of the given base namespace * and [4] a flag indicating whether to start with a file * (TRUE) or directory (FALSE). * @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. * ** The method checks for the given $aFormat and returns * FALSE when a format isn't supported. * $aRenderer contains a reference to the renderer object * which is currently handling the rendering. * The contents of $aData is the return value of the * handle() method. *
* This implementation uses the precomputed values of $aData * to generate a list of headlines marked up as a (X)HTML list. *
* @param $aFormat String The output format to generate. * @param $aRenderer Object Reference to the Doku_Renderer_xhtml * object to use. * @param $aData Array The data created/returned by the * handle() method. * @return Boolean TRUE if rendered successfully, or * FALSE 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 ?>