Translations of this page?:

NsToC Syntax PlugIn

nstoc plugin by Matthias Watermann
Namespace Table-of-Content

Last updated on 2008-03-30. Provides Syntax.
Compatible with DokuWiki 2005-07-13+.

Similar to pageindex.

Tagged with list, namespace, toc.

A while ago I started a project involving lots of hierar­chi­cally orde­red pa­ges – just like a book with chapters, sub-chap­ters and para­graphs. To add (and up­date whenever a page was ad­ded/re­mo­ved/mer­ged) the ToC re­fe­ren­ces was a nec­ce­sary but quite stu­pid job1). After doing this pesky task for quite a few ti­mes I de­ci­ded to auto­mate it. — Enter ”nstoc”.

This plugin offers you the ability to gene­rate a Table Of Con­tents for a na­me­space with an optio­nal depth. It gene­rates a (possi­bly nested) list of head­lines used in all mat­ched pages.

One could say this plugin sees your whole Wiki as one huge docu­ment struc­tured by chap­ters (Wiki name­spaces), sub-chap­ters (the single pa­ges within a name­space) and ap­pro­pri­ate head­lines (H1…H5).

Usage

The basic markup is just:

	{{nstoc }}

This will create a nested list of all pages2) in the cur­rent name­space and all sub-na­me­spa­ces. Please note the space3) be­hind the ”nstoc” key­word: For­get­ting it will trigger Do­ku­Wiki's builtin media ren­de­rer which you most pro­bab­ly do not want here.

To limit the output to – say – two levels use

	{{nstoc 2}}

The result will be a list with all H1 and H2 head­lines in the cur­rent na­me­space's pages and all H1 head­lines in the pages of all sub-na­me­spaces of the cur­rent one while

	{{nstoc 3}}

will pro­duce a list with all H1/H2/H3 head­lines in the current na­me­space's pa­ges, all H1/H2 head­lines in the pages of all sub-name­spa­ces of the cur­rent one and all H1 head­lines in the pa­ges of all sub-sub-name­spaces.

Another way to limit the output is to expli­citly name the name­space:

	{{nstoc chapter2}}

This will show the head­lines (with unlimi­ted depth) in the ”chapter2” name­space.

You may, of course, combine the optional name­space and depth ar­gu­ments:

	{{nstoc chapter3 1}}

Here only the H1 head­lines of the pages in ”chapter3” will be shown.

Hints

Here are some tips which might be help­ful for you when wor­king with this plugin.

Order

The generated output – or, to be more precise: the order of the ge­ne­ra­ted list – might not always be what you'd expect. The reason for this: You, as a hu­man be­ing4), have a no­tion of meaning while the com­puter just knowns about data. To illu­strate this let's as­su­me you're wri­ting a book. Right now you've fini­shed (or at least crea­ted) this pa­ges:

  1. Preface
  2. Introduction
  3. First Chapter
  4. Second Chapter
  5. Appendix

When using ”nstoc” you'll most probably expect a list like the one above. But, alas, the real result would look like

  • Appendix
  • First Chapter
  • Introduction
  • Preface
  • Second Chapter

Not very helpful, is it? — The reason is simple: The only thing DokuWiki and this plugin has to deal with are (file and namespace) names. But, as it turns out, it's quite easy for you to take this fact to your advan­tage by choo­sing the right page names. For example, name the pages5) like this:

  • 00_preface
  • 01_introduction
  • 02_first_chapter
  • 03_second_chapter
  • 99_appendix

Theoretically you could even ommit all the alphas and just leave the di­gits. But I guess, that would take the compu­teri­sing of your work a bit too far. Any­way, as long as the pa­ge and name­space names sort in the inten­ded order, ”nstoc” will pro­duce useful out­put.

BTW: This discussion applys to name­space names as well. This means, that you should name your name­spaces accor­ding to their inten­ded po­si­tion (i.e. ac­cor­ding to their re­spec­tive posi­tion6) within your over­all pre­sen­ta­tion). If, for exam­ple, the first chap­ter of your book has seve­ral sub-chapters, you should name the name­space ”02_first_chap­ter7) and the pa­ges therein e.g. ”01_first_sub­ject”, ”02_se­cond_sub­ject” and so on. Of course, the head­lines there­in should be a little more mea­ning­ful for human readers.

Accessible pages

Starting from the 2007-01-08 release of this plugin all pages not acces­sible to the re­spec­tive cur­rent user/reader are omit­ted from the ge­ne­ra­ted list. In other words: Any user will see a ToC con­tai­ning only pages he/she may ac­tually read. This avoids the in­con­ve­nience for your readers seeing a page/head­line in the ToC but when trying to read it get­ting only an “ac­cess denied” mes­sage.

So, assuming you've setup your access control appro­pria­tely8) you don't have to worry about ex­po­sing (the exis­tence of) pa­ges which your users – or, at least, some of them – are not sup­po­sed to see.

Another benefit for you is that you don't have to mo­dify the pages con­tai­ning the ”nstoc” markup when­ever you add a name­space and/or page. To illu­strate this re­mem­ber our vir­tual book pro­ject out­lined above.

Suppose you've got the preface and second chapter fini­shed while still wor­king on the other parts. And you want any casual reader to see only those fini­shed pa­ges but not the work-in-pro­gress ones. So you'd write in your overview page

	{{nstoc 00_preface}}
	{{nstoc 03_second_chapter}}

Some weeks later the first chapter is ready for public review. To make them ac­ces­sible you'd change the over­view page to

	{{nstoc 00_preface}}
	{{nstoc 02_first_chapter}}
	{{nstoc 03_second_chapter}}

Then turning to chapter seven some time later again you'd change the over­view page:

	{{nstoc 00_preface}}
	{{nstoc 02_first_chapter}}
	{{nstoc 03_second_chapter}}
	{{nstoc 08_seventh_chapter}}

And so on …

Using access control you would initially set the book's name­space to be un­read­able by anyone but yourself. In the overview page you just insert:

	{{nstoc }}

Now, whenever you find one of your book's pages worth for public con­sump­tion you just add a line to your access control like

	book:00_preface @ALL    1

or whatever you feel appro­priate9). You won't have to change the book's over­view page ever again – at least, not to update the ”nstoc” markup, that is. Every­thing is ma­na­ged by this plugin and Doku­Wiki's access con­trol system.

Starting from the 2007-08-15 release of this plugin pages matched by the global 'hidepages' set­ting (i.e. a regu­lar expres­sion10)) will be omitted in the gene­ra­ted list as well11).

Index pages

If the name given after the ”nstoc” keyword resolves to a default page name (i.e. ”start” with an un­mo­di­fied Doku­Wiki in­stal­la­tion) the re­spec­tive name­space is used for ge­ne­ra­ting the ToC but not the page. The same happens if you're poin­ting to a page with the same name as a sub-name­space. In case you're gene­ra­ting a ToC for a name­space that inclu­des sub-name­spaces all those assumed in­dex pages12) are skip­ped as well. This fea­ture is inten­ded to avoid in­dexing pa­ges that are al­ready meant to be kind of over­view pages.

Root page

With earlier releases the root namespace had to be treated espe­cially. Star­ting with the 2007-08-12 release of this plugin the root name­space now is handled al­most as each other name­space. So you could use the basic markup

	{{nstoc }}

to generate a ToC with all available pages in your Doku­Wiki instal­la­tion.

Assuming a fairly structured installa­tion, how­ever, the pa­ges in the root name­space are most pro­bably some kind of star­ting point for one sub-name­space or another. Hence it seems sensible to use ”nstoc” in the root name­space in a more explicit way like

	{{nstoc intro_page}}
	{{nstoc ns1 2}}
	{{nstoc ns2 1}}
	{{nstoc ns3:ns3a}}

Such a markup will exclude the pages in the root name­space but show only those head­lines found in the speci­fied sub-name­spaces.

Numeric namespace names

Some people – as I've been told – do prefer to use nume­ric name­space names such as ”1”, ”23” or ”456”. Al­though this isn't a pro­blem as such for this plugin you must be care­ful when wri­ting the ns­toc markup. I've sta­ted above that gi­ving a name­space's name is enough to get all head­lines of that name­space (incl. its sub-name­spa­ces) with unli­mi­ted depth. So

	{{nstoc 23}}

should show the headlines of name­space ”23”, right? – Wrong: The plugin inter­prets this as a max. depth va­lue of 23 for the cur­rent name­space.

To make sure the ”23” is accepted as the name­space's name you have to use the 2-ar­gu­ments vari­ant i.e. gi­ving the max. depth va­lue as well:

	{{nstoc 23 4}}

Now the namespace called ”23” would get in­dexed up to a nes­ting depth of 4 levels. – Easy, isn't it?

Navigating

This plugin allows for relative addres­sing the desired name­space as well. Con­si­de­ring the book example above and assu­ming there's a sub-nam­espace in the first chapter called 03_im­por­tant_points let's sup­pose you're in the name­space of the second chapter (i.e. in 03_se­cond_chapter). Now you'd like to pro­vide links to the men­ti­oned pages for your rea­ders. You could do this by either use an abso­lute path like

	{{nstoc :book:02_first_chapter:03_important_points 2}}

or use a rela­tive path like

	{{nstoc ..:02_first_chapter:03_important_points 2}}

With this example it's only a difference of 3 charac­ters. But the dee­per your na­me­spaces are nested the more you save ty­ping. And – as an addi­tio­nal bene­fit – using rela­tive paths lea­ves those links intact if you move the whole book to an­other place: If – for instance – you de­cide to move/re­name the whole book name­space into a new my_books name­space un­der the new name big_bang (or what­ever) the abso­lute path above would no lon­ger show any links while the rela­tive one will still work as ex­pected13).

Besides DokuWiki's : (colon) path sepa­rator this plugin allows the stan­dard UNIX / (slash) as well. Hence the se­cond exam­ple above could be writ­ten

	{{nstoc ../02_first_chapter/03_important_points 2}}

as well. The respec­tive cur­rent name­space can be addres­sed as ./, the pa­rent name­space as ../ and the root name­space as /. This seems to be more intui­tive at least for those who are fami­liar with a shell com­mand­line.

Caching

At least as long as the name­space/page struc­ture you're in­dexing by this plugin is like­ly to change you should place the ”~~NOCACHE~~” direc­tive in those fi­les (pa­ges) which con­tain the ”nstoc” markup. That should make sure that the users al­ways get an actual/cur­rent ToC.

Installation

It's quite easy to integrate this plugin with your DokuWiki:

  1. Download the source archive (~9KB) and un­pack it in your Doku­Wiki plug­in di­rec­tory {dokuwiki}/lib/plugins (make sure, in­clu­ded sub­di­rec­to­ries are un­packed cor­rect­ly); this will create the directory {dokuwiki}/lib/plugins/nstoc.
  2. Make sure both the new direc­tory and the files therein are read­able by the web-server e.g.
    	chown apache:apache dokuwiki/lib/plugins/* -Rc
    

You might as well use the plugin manager for installing or updating this plugin.

Plugin Source

Here comes the GPLed PHP source14) for those who'd like to scan be­fore actu­ally in­stal­ling it:

<?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, 2008  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.15 2008/03/30 12:43:59 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>.
   * @param $aRenderer Object Reference to the <tt>Doku_Renderer_xhtml</tt>
   * object to use.
   * @return String
   * @private
   * @see render()
   */
  function _doMarkup(&$aList, &$aRenderer) {
    $divOpen = array_fill(0, 0xff, 0);  //XXX 255 levels as in "handle()"
    $curLvl = 0;
    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]) {
              $aRenderer->doc .= '</div>';
              --$divOpen[$curLvl];
            } // if
            ++$curLvl;
            $aRenderer->doc .= '<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]) {
              $aRenderer->doc .= '</div>';
              --$divOpen[$curLvl];
            } // if
            --$curLvl;
            $aRenderer->doc .= '</li></ul>';
            if (0 < $divOpen[$curLvl]) {
              $aRenderer->doc .= '</div>';
              --$divOpen[$curLvl];
            } // if
          } while ($curLvl > $l[0]);
          $aRenderer->doc .= '</li><li class="level' . $curLvl
            . '"><div class="li">';
          ++$divOpen[$curLvl];
        } else {
          // still the current nesting level
          if (0 < $divOpen[$curLvl]) {
            $aRenderer->doc .= '</div>';
          } // if
          $aRenderer->doc .= '</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]);
        //XXX Note that "_headerToLink()" is supposed to be a
        // _private_ method of the renderer class; so this code
        // will fail once DokuWiki is rewritten in PHP5 which
        // implements encapsulation of private methods and
        // properties:
        $aRenderer->doc .= $link . $aRenderer->_headerToLink($l[1])
          . '" title="' . $l[0] . '">' . $l[0] . '</a>';
      } // while
    } // while
 
    // Finally close all possibly open DIV/LI/UL elements
    while (0 < $curLvl) {
      if (0 < $divOpen[$curLvl]) {
        $aRenderer->doc .= '</div>';
      } // if
      $aRenderer->doc .= '</li></ul>';
      --$curLvl;
    } // while
  } // _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\s*(={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' =>  '2008-03-30',
      '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
 
    // Compute current page and namespace:
    $current = str_replace('/', ':', getID('id', FALSE));
    $dir = substr($current, 0, strrchr($current, ':'));
    if (! $dir) {
      // For unknown reasons the "strrchr()" call above
      // sometimes doesn't work ...
      for ($f = strlen($current); 0 < $f; --$f) {
        if (':' == $current{$f}) {
          $dir = substr($current, 0, $f);
          break;
        } // if
      } // for
    } // 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
    // 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 = (isset($conf['start']) && strlen($conf['start']))
      ? $conf['start']
      : '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 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 = (isset($conf['start']) && strlen($conf['start']))
          ? $conf['start']
          : '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 (isset($conf['hidepages']) && strlen($conf['hidepages'])) {
      $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
    } else {
      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
    } // if
 
    if (0 < count($pages)) {
      $this->_fixJS($aRenderer);  // check for old DokuWiki versions
      $this->_doMarkup($pages, $aRenderer);
    } // if
    return TRUE;
  } // render()
 
  //@}
} // class syntax_plugin_nstoc
} // if
//Setup VIM: ex: et ts=2 enc=utf-8 :
?>

Presentation

The accompanying CSS presentation rules:

div.level1 ul.nstoc,div.level2 ul.nstoc,div.level3 ul.nstoc,div.level4 ul.nstoc,div.level5 ul.nstoc,div.level6 ul.nstoc{margin:0;padding:0;}
ul.nstoc{list-style-position:inside;list-style-type:none;font-size:100%;margin:0;padding:0;text-align:left;line-height:1.4;}
ul.nstoc li{margin:0;padding:0 0 0 0.5ex;list-style-type:none;}
ul.nstoc li.level1{margin-top:0.3ex;padding:0;font-size:111.1%;font-weight:500;font-variant:small-caps;letter-spacing:1pt;background:inherit;color:#000;}
ul.nstoc li.level2{font-variant:normal;}
ul.nstoc li.level3{letter-spacing:normal;}
ul.nstoc li.level2,ul.nstoc li.level3,ul.nstoc li.level4,ul.nstoc li.level5,ul.nstoc li.level6,ul.nstoc li.level7,ul.nstoc li.level8{font-size:96.6%;padding-left:1.3ex;}
ul.nstoc li a,ul.nstoc li a.wikilink1{background:inherit;color:#003;border:none;font-size:inherit;font-variant:inherit;line-height:inherit;text-decoration:none;}
ul.nstoc li a.wikilink1:before,ul.nstoc li a.wikilink1:after{display:none;}
ul.nstoc li a:hover,ul.nstoc li a.wikilink1:hover{text-decoration:underline;}

Of course, you're free to modify this styles15) to suit your personal needs or aesthe­tics16).

Changes

2008-03-30:
* modified private '_doMarkup()' method to use the current renderer instance directly;
- removed obsoleted property '_sepChar' and private '_makeID()' method and updated 'render()' accordingly;

2008-03-28:
* modified CSS to ecplicitely overwrite some broken default settings;

2007-08-29:
* little doc corrections;

2007-08-26:
* modified 'handle()' and 'render()' to ignore nested index pages;

2007-08-15:
+ implemented use of global '$conf[“hidepages”]' setting;
* added GPL link and fixed some doc problems;

2007-08-12:
+ implemented new private '_path()' method and rewrote public 'handle()' to use it (thus allowing better relative navigation);
* various internal optimizations in several places; + added 'aDecLevel' argument in '_getHeadings()' interface to allow for handling namespaces, sub-pages and root pages differently;
* modified 'render()' to handle different page/list types;

2007-08-09:
+ added private members '_Chars' and '_Ents' and modified '_doMarkup()' to use them (so the lists are created only once when the object is instanciated instead of with each method call);

2007-08-08:
* modified 'handle()' to explicitely check for index pages;
* changed internal handling of directories vs. pages;
* slightly decreased font-size of 'level3'/'level4' LIs;
* modified anchor selector to inherit bg-colour;

2007-08-06:
+ added ':before/:after' selectors for links in case the default setting (by the main template) specifies something else;

2007-06-12:
# modified 'handle()' to fix handling of omitted namespace names;
* changed/corrected some doc comments;

2007-02-23:
* removed function test in '_fixJS()' (the PHP file might not be loaded);

2007-02-20:
+ implemented support for older DokuWikis;

2007-01-31:
* minor change in 'render()' setting up private '_sepChar' member;

2007-01-28:
* modified 'handle()' to slightly improve handling of sub-namespaces;

2007-01-14:
* modified RegEx in '_getHeadings()' to ignore empty headline markup and skip consecutive '=' characters used in patch files;
+ implemented private member '_callback' for use in 'render()' instead of a global callback for the global 'search()' function;

2007-01-08:
+ implemented utilization of access rights in 'render()';

2006-12-31:
+ initial release;

Matthias Watermann 2007-08-26

See also

Consult DokuWiki's access control docs.

Plugins by the same author

Discussion

Hints, comments, suggestions …

The idea of this plugin is interesting. It could be very usefull, but it doesn&