It's better when it's simple

User Tools

Site Tools



NsToC Syntax Plugin

Compatible with DokuWiki


plugin Namespace Table-of-Content

Last updated on

This extension has not been updated in over 2 years. It may no longer be maintained or supported and may have compatibility issues.

Similar to tocselect

Tagged with listing, menu, namespace, navigation

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 neces­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).


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 built-in 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.


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


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 knows 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 omit all the alphas and just leave the di­gits. But I guess, that would take the compu­te­ri­zing 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 applies 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 spe­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?

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.


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.


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:

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;;
 * </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="">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
 * General Public License for more details.
 * </div>
 * @author <a href="">Matthias Watermann</a>
 * @version <tt>$Id: syntax_plugin_nstoc.php,v 1.16 2010/02/18 10:30:46 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>';
            } // if
            $aRenderer->doc .= '<ul class="nstoc"><li class="level'
              . $curLvl . '"><div class="li">';
          } while ($curLvl < $l[0]);
        } else if ($curLvl > $l[0]) {
          // need to close the current level
          do {
            if (0 < $divOpen[$curLvl]) {
              $aRenderer->doc .= '</div>';
            } // if
            $aRenderer->doc .= '</li></ul>';
            if (0 < $divOpen[$curLvl]) {
              $aRenderer->doc .= '</div>';
            } // if
          } while ($curLvl > $l[0]);
          $aRenderer->doc .= '</li><li class="level' . $curLvl
            . '"><div class="li">';
        } 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>';
    } // 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;
    } // if
    $localdir = realpath(dirname(__FILE__)) . '/';
    $webdir = DOKU_BASE . 'lib/plugins/nstoc/';
    $css = '';
    if (file_exists($localdir . 'style.css')) {
      @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
    } // 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,
        } // 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:
          // 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) {
      $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' =>  '',
      'date' =>  '2010-02-18',
      'name'  =>  'NsToC Syntax Plugin',
      'desc' =>  'Add a namespace\'s table of contents {'
        . '{nstoc  [namespace [maxdepth]]}}',
      'url' =>  '');
  } // 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, Doku_Handler $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));
    $f = strrchr($current, ':');
    $dir = (FALSE === $f)
      ? ''
      : substr($current, 0, $f);
    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);
        } // 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);
      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
        $args[0] = str_replace('/', ':', $args[0]);
        // Make the (assumed) depth value numeric:
        $args[1] *= 1;
    } // 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) {
        } 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
    } // 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 :


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 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).


* minor change in 'handle()' to avoid type conversion;

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

* modified CSS to explicitly overwrite some broken default settings;

* little doc corrections;

* modified 'handle()' and 'render()' to ignore nested index pages;

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

+ 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;

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

* modified 'handle()' to explicitly 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;

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

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

* removed function test in '_fixJS()' (the PHP file might not be loaded);

+ implemented support for older DokuWikis;

* minor change in 'render()' setting up private '_sepChar' member;

* modified 'handle()' to slightly improve handling of sub-namespaces;

* 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;

+ implemented utilization of access rights in 'render()';

+ initial release;

Matthias Watermann 2007-08-26

See also

Consult DokuWiki's access control docs.

Plugins by the same author


Hints, comments, suggestions …

(Please add new suggestions at the end to maintain some sort of chronological order.)

The idea of this plugin is interesting. It could be very useful, but it doesn't work in my DokuWiki installation… Do I have to change something before using it??
Could you provide some more information? What did you try, what did you expect and what happened actually?
Matthias Watermann 2007-01-30 12:57

I'm pretty happy with this plugin, but I will need to modify it some for my purposes. I have a news blurb site which is still very new. (I set up an example of what the home page will look like using this plug in in the sandbox. Currently, I am adding blurbs to the home page, then moving them to various categories when they get old. I would like to modify this plug in so that it only shows the first 5 second-level headings from each page. That way, I can just add the blurb to a specific category and it will automatically show up on the home page. I can strip out the top-level headings with the CSS, but will need to modify the plug-in itself to stop after 5 subheadings. I may also want to include the entire section for the top two of each page, but I need to play around with it more to see how it will work. Thanks for your good work on this one, I was going to start making one that does this sort of thing from scratch (and I have never programmed in PHP before, so it would be a real learning experience!) :p –Len 2007-01-30 12:30 p.m. -8:00.

Thanks for your input! I've had a look at your site to get the idea. (BTW: Your CSS isn't yet valid; both Firefox and Opera show a lot of warnings. Try to help you get it fixed.) And I'd recommend to either add a left margin/padding to the “ul.nstocCSS selector or use the “list-style-type: none” setting as shown in the CSS above. The 'hopping' LI markers don't look that good to me.
About your intended changes: I fail to see the reason to skip the first-level headings (apart from the fact that in some pages the first heading is a H2 instead of H1) – but that's not my concern. What really worries me is the markup the user has to type in. There are already four variants: (1) neither namespace nor max-depth, (2) only namespace, (3) only max-depth, (4) both namespace and max-depth. Now, we'd need to express additionally to (a) skip the H1, (b) use only up to “xH2s while (c) skipping all H3/H4/H5 lines.
The last point (c) seems to be the easiest one because the max-depth argument implicitly takes care of this: {{nstoc :legal 2}} would make sure that only H1 and H2 in the legal namespace will get used. However, once there will be sub-namespaces (like :legal:whatever) only the H1s of the pages therein will show up. So obviously this approach wouldn't work for you in the long term if used this way.
Point (b) (delimiting the H2s to use) kind of breaks the plugin's intended purpose insofar as the resulting list would no longer represent a complete TOC. On the other side I wonder whether a single page with several H2s shouldn't get split up into several pages of their own (probably in their own “sub-chapter” namespace).
That leaves (a) i.e. skipping H1. This as well doesn't fit with the plugin's purpose, I fear. And assuming that H1 should provide fairly relevant information I must admit that I fail to see the point in omitting it.
Well, although this reasoning doesn't seem to be very encouraging I think you could get what you want by slightly adjusting your point of view upon your wiki. Try to think of it as a big book like a reference of a programming language or such alike. You have the main topics to be covered which are the book's chapters (and wiki 1st level namespaces). So your 1st level index/start page could be nothing more than: “{{nstoc :book}}” This would produce an outline of all topics covered but no real content/information yet. As appropriate for each of the chapters, there would be either a page (providing content/information actually or just {{nstoc :book:chapter1}}) or sub-chapters (i.e. wiki 2nd level namespaces). And so on…
I'm aware that wiki users don't tend to think in terms like “document” or “structure” and alike but more in the lines of contents (leaving anything else to the “magic under the hood” i.e. the respective wiki software). But as a maintainer of a DokuWiki installation you've got the power to enforce some – say – “politics” about how to structure your web-presentation. And judging from your start page and your words above you've already given some thought to this matter. Hence I think you'll only need some “fine tuning” in your namespace usage and the distribution of contents between files (pages) therein. — Thinking such an approach to its end could mean that DokuWiki's built-in “index” (your URL) could provide all navigational aid required to use your site.
Having said that please feel free to followup as I very well may have missed your point.
Matthias Watermann 2007-01-31 12:29
Thanks, I know the site has only been up for a week or so, and it still needs tweaking. Before discovering DokuWiki, I have been running WikiServer internally on my company's intranet, so I just copied the content and reformatted it with DokuWiki's syntax. I agree about the hopping LI markers :o thanks for the hint. Also I agree that skipping H1 is not necessary, as I think I can make what I want work with CSS changes. Part of the problem is that some of the name spaces have pages that I don't want included in the TOC, but I guess that is easy to fix by moving them to a different namespace… so that's ok, really, come to think of it.

The one concern I have is that as these categories fill up with blurbs, the TOC will grow huge, which is why I wanted to limit to X number of headings (not X depth). So that, lets say the H1 and only the first 5 H2s under each H1 would be listed, with maybe a “…more” underneath which would point to the H1 location. –Len (2007-01-31 3:57 -8:00)
About name­spaces to not include: If it's not only a matter of quan­ti­ty pos­sib­ly DokuWiki's access control can help?
The other point is one (huge lists) I stumbled over myself recently. While it would not be too difficult to implement another number option as such the question is: What would it be supposed to mean? (a) The overall max. number of list entries, (b) the max. number of entries per namespace (in­clu­ding (b.a) all sub-namespaces or (b.b) just the respective current one), (c) the max. num­ber of en­tries per page, or (d) … — What­ever we'd decide the resulting list would match our intentions only with certain com­bi­na­tions of name­spa­ces, sub-name­spa­ces, pages and head­lines therein while with other combina­tions the list would get either too short or still too large.
Above I mentioned the book analogy several times already. In its shortest form “{{nstoc :book}}” would produce a complete TOC of the whole thing. But that doesn't mean that you must use the plugin that way and only that way. It's just one way intended to save you some typing. Let me introduce another analogy: a tree. The wiki's namespaces are the branches and the pages the leafs. Additionally we say that neither the tree trunk nor in­ter­me­dia­te bran­ches may have leafs (i.e. real con­tent pages). All namespaces (trunk and bran­ches) except the outermost ones contain just one index page like
	====== Name of this branch ======
	{{nstoc sub-ns-one 1}}
	{{nstoc sub-ns-two 1}}
	{{nstoc sub-ns-three 1}}

Since there's only one headline in such an index page it doesn't contribute too much to the overall list (the one that starts at the very top). But if the list is still too long, you could just hide all intermediate branches by using a top index like

	====== My presentation ======
	{{nstoc :ns1:ns1a:ns1a1:outermost 2}}
	{{nstoc :ns1:ns1b:ns1b1:outermost 2}}
	{{nstoc :ns2:ns2a:ns2a1:outermost 2}}

By explicitly naming the namespaces to use combined with the depth option you'd make sure that only the H1 and H2 headings of the pages in the re­spec­ti­ve outer­most name­space (branch, sub-chapter) show up in the final list. In case even that would give too huge a list you could name the pages explicitly (omit­ting those you do not want to show up in the list) e.g.

====== My presentation ======
	{{nstoc :ns1:ns1a:ns1a1:outermost:page1 1}}
	{{nstoc :ns1:ns1a:ns1a1:outermost:page4 3}}
	{{nstoc :ns1:ns1b:ns1b1:outermost 4}}
	{{nstoc :ns2:ns2a:ns2a1:outermost:page77 2}}

Well, that might not look too good at first glance, but I hope you'll get the idea. — Ad­mit­ted­ly this is a little more typing initially than just “{{nstoc }}”. But, after all, you've to do that only once when your tree has grown its in­ten­ded struc­ture. — Like every gardener, however, be careful with your plants ;-)
Matthias Watermann 2007-02-01 13:42

Hi, this plugin is exactly what I need and I am able to create the TOC of nested namespaces if I explicitly list them but the simple syntax {{nstoc }} simply doesn't work on my DokuWiki. Note: I'm not in the root namespace so it's not the special case mentioned in the article. If I include {{nstoc }} on page namespace:some_page, it ignores the other pages in namespace and includes only heading from some_page. And I certainly have enough permissions to view all pages as I'm in an admin group.
Well, it took some time to track that down. But (hopefully) it's fixed by now. Let me know if there are still problems.
Matthias Watermann 2007-06-12 13:26
It still doesn't work on my DokuWiki (version 2007-06-26). I was able to simulate the desired result with {{nstoc * 4 }}
Borek 2007-07-16 14:29
Thanks for your note. Please check again with the plugin's latest release. I've run several tests but was unable to reproduce what you're describing.
Matthias Watermann 2007-08-26 22:19

It would be nice if the plug in can search for specific tokens in the page, so I can create a resume page of interesting points.
Rodrigo Montes 2007-05-11 16:11
Hmmm, I can't see how that could be implemented conveniently. After all, how would you define “specific tokens” in a way that fits all DokuWiki installations? Most probably you'd end up with a (theoretically endless) list of tokens which has to be parsed & processed with each “candidate” (i.e. file) resulting in a lot of runtime overhead.
If, however, you have root access to your *NIX/Linux based web server you could get what you want by creating “pseudo namespaces” (in need for a better term) which solely consist (as far as the filesystem is concerned) of sym-links organized & named in a way that fits your needs.
While I haven't tested this idea in real life I imagine you could generate your “page of interesting points” that way. Just remember that all DokuWiki (and this plugin) “knows” is the name of your namespaces and pages. So, for example, if one of your “specific tokens” would be tennis you'd just create a namespace (i.e. directory) called “tennis” and sym-link all your tennis related pages (i.e. files, wherever they may reside originally in your namespace hierarchy) into that directory (possibly using other names for the sym-links as the original files if that helps to “tokenize” them). Of course, you have to review any relative hyperlinks in such a page but that's something you must do anyway whenever setting up and/or modifying a namespace.
Matthias Watermann 2007-06-12 13:32
I added a couple of lines at the start of function handle:
	function handle($aMatch, $aState, $aPos, &$aHandler) {
		$aMatch = str_replace('@YEAR@',date('Y'),$aMatch);
		$aMatch = str_replace('@MONTH@',date('n'),$aMatch);

This allows me to use @YEAR@ and @MONTH@ to insert the current year and month.
Erik Itland

While I can see your point I really don't think that something like that is in the “domain of responsibility” of this plugin. See my comments above.
Matthias Watermann 2007-06-12 13:49

Several quirks when I try to use this: * specifying namespaces only works if I use the full namespace path, starting with root * the TOC outputted shows from H1 down, without the page title; how do I make the page title appear?

Hi anonymous! If you want a list starting relative to the current namespace just specify it e.g.: {{nstoc .:sub_namespace}} or {{nstoc .:sub_namespace:pagename}}. – About “page title”: I'm not sure what you mean by that term. The filename? The namespace name? Anyway, the TOC gets generated based on the headings (and a page's title is usually the H1 heading).
Matthias Watermann 2007-07-26 12:05
I meant filename - is there anyway to get nstoc to list pages by the name of the page instead of the first H1 in the page? The reason is that many of the pages I am trying to get in the TOC have headings that are different from the page name. —Thanks
Sorry, no. I consider the filename just a technical item with no relevance to the user/reader. This plugin is supposed to generate a Table of Contents not a directory listing. Apart from that: The filename is subject to various modifications (e.g. lowercasing, replacement of certain characters etc.) so it would be quite hard to produce a readable string (at least if one's using not only plain ASCII characters). – BTW: Do you know about the “$conf['useheading']” setting? – Isn't it, well, confusing if in the URL shows up a filename of “tea” while the page's headline says “coffee”? – Anyway, there's always DokuWiki's “?do=index” CGI argument you could use to get a filename based overview.
Matthias Watermann 2007-08-12 15:26

I wish I could use only the first H1 in a document

I am using your excellent plugin here - I think the pages look better if I use an HTML H1 as the page title with nice size and colour and then DokuWiki H1s as sections like this - But because the plugin reads H1 as if it were a new document it will list all the H1s in a single document when all I really want is the first H1. If you click on Sitemap you will see what I mean. (I'll leave it for a day or two - from 19 Oct). I can understand I might have to revert back to the standard way but it would be nice if the algorithm allowed me to specify only the first H1 and NOT the rest?

Well, unless you're willing to properly structure your documents there isn't an easy solution, I'm afraid. Each single document can only have one main headline (H1). If there are more this indicates that (a) either the document should be split into two (or more) documents each of which having its own distinguished headline or (b) the document's markup doesn't represent its structure properly.
Another possible reason might be that your page authors are abusing the Hx for design instead of structure. If that's the case you should tell them the difference between a bold typeface and a document headline.
So whatever the reason might have been in the first place, as long as your document's markup reflects its structure you should be fine.
Matthias Watermann 2008/03/28 10:26

I like this plugin very much as it saves me a lot of redundant work, but I have a small issue with how the TOC is displayed. I get a new line after every bullet button, so the actual text is in a separate new line, which doesn't give me a particular nice layout.
This seems to be problem in Firefox (2) only. It doesn't appear in IE7 or IE6. Is this something which Firefox does wrong?
I haven't modified any CSS (Should I be copying the style.css somewhere?). Is there an easy way to resolve this and to remove the new line without changing the actual code of the plugin?
Max 2008-03-17

I've been a bit confused at first, since list bullets shouldn't show up at all in a nstoc list and I haven't been able to reproduce the problem (BTW: bullets, colours, indentations etc. all are a matter of CSS, meaning it's up to the browser how to handle them). However, after a while I realized that there might be conflicting CSS rules with DokuWiki's default styles18). To make a long story short: I've added some rules to the plugin's CSS file to enforce the nstoc layout even when used with DokuWiki's default CSS/template. I've verified it with both Opera and Firefox19) and it works fine.
So please try to re-install the plugin and see whether it works out for you.
Matthias Watermann 2008/03/28 10:14
I have exactly the opposite wish as one colleague above: I'd like the plugin to be able to skip the H1 header. I'm using the plugin in two ways:
a) to replace the standard DokuWiki TOC. In this case it doesn't really make sense to have the H1 heading in the TOC, just beneath the H1 heading :-)
b) to refer to a page from an other (“FAQ mode”). In this case I structure the main FAQ page myself and would like to list all headings but H1
An ideal solution could be to have some kind of '-/+' syntax: -1 skips H1, -2 skips H2 etc. So that


would show H3 but not H1 and not H2..?
Just a proposition
Frank, 2008/12/04 16:21

Make link hack
This hack fixing such errors as: * Incorrect romanization links handling * Makes inc number to duplicated headings

To do it you gotta replace this function on /nstoc/syntax.php

	function _makeID(&$aID) {
		$title = str_replace(':','',cleanID($aID));
		$title = ltrim($title,'0123456789._-');
		if(empty($title)) $title='section';
		// make sure tiles are unique
		$num = '';
				($num) ? $num++ : $num = 1;
		$title = $title.$num;
		$this->headers[] = $title;
		return $title;
	} // _makeID()

Kirill Bezrukov 2008/03/23 11:49

Hmmm, I'm not sure what you're trying to accomplish here. The headlines of a page/document must be unique in the first place (otherwise you'd not only confuse the readers but also break DokuWiki's automatic link generation). So if your pages have duplicated headlines you should correct those pages instead of a plugin which simply uses those pages.
Adding a number (or anything else) to a hypertext link fragment identifier will break that link. Please be aware that the private method you're “fixing” must return a value exactly as DokuWiki's internal link generation, which it does not in your variant. Apart from that: There's no object property named headers, so at least the code snippet above is incomplete.
As for “incorrect romanization” I really don't know what you're talking about. As the comments in the (original) source code state there are not only the official W3C rules to take into account when creating anchor names but an additional constraint imposed by DokuWiki to use only lowercase letters. Your “fix” fails on both.
I'd strongly discourage everybody from using the “fix” above: It will break your DokuWiki and possibly invalidate the page.
Matthias Watermann 2008/03/28 11:09
I got this code from \parser\xhtml.php line 997 function _headerToLink. May be the best way just use this function from DokuWiki core, but I don't know how.
About Romanization: if your wiki not in English, for example in Russian as I have, and you use $conf['deaccent'] = '2'; option (when Russian names are converted in translation (English letters) the headers in generated TOC have empty links (just #). And after I made this, it became fine.
And I just forgot about $headers array, it must be defined as plugin class global var and clearing before link generation — Kirill Bezrukov 2008/03/29 22:04
Following your suggestion I've modified the plugin to use the method you mentioned. As already discussed by private email this does not guard against poorly structured documents. I hope it helps, anyway.
Matthias Watermann 2008/03/30 20:09

Hi Matthias,

Did you consider using metadata rather than searching the wiki text in your _getHeadings() method? The TOC (admittedly a restricted set of headings) is available in array form through p_get_metadata($id,'description tableofcontents');. If the TOC is acceptable, it maybe a faster method for accessing the raw heading information.

Christopher Smith 2008/12/04 14:33

Great plugin! did a bit of a hack to allow me to exclude pages from getting indexed site-wide. I did this because I'm building a nav menu where each namespace has a file named sidebar and a file named nav, and I didn't want those to show up in the nstoc listing. In syntax.php, line 693:

$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, -3) != "nav")' // Added by DREW to exclude pages named "nav"
. '&& (substr($aFile, ' . $iLen . ') != ":' . $idx . '")) {'
. '$aData[] = $aFile;'
. '}'
. 'return TRUE;');

You can continue to copy and paste to add any other exclusions you'd like. Also, remember to change the number on the substr to be the number of letters in the filename you're trying to exclude (nav has 3 letters, so -3; sidebar has 7 letters, so -7). While I'm sure this isn't the best way to do it, it works for me. I could see this as being useful to be able to define per instance of nstoc, though, so perhaps a suggestion to the plugin is to add that? (ie: {{nstoc :namespace 1 excludeMe1 excludeMe2 …) Thanks for the great plugin! Andrew Petersen 2009/03/01 00:03

Thanks for this plugin. It looks like exactly what I was looking for. The only thing I would like to have would be to replace the contents of the built-in Table of Contents by the HTML output of this table. How could be this done? — Josef Meile 2009/04/21 20:00

Hmm, as far as I can see that's not easily possible. And I fail to see the benefit: While a page's TOC is meant to provide overview of the current page (i.e. the one actually displayed by the browser) the nstoc output gives the page titles of one or more name­spaces (possibly but not necessarily including the current page). In other words: Both lists serve a completely different purpose.
Matthias Watermann 2009/08/07 11:05

This plugin is great, but the 'title' attribute of the links is different to the title attribute created by DokuWiki for an internal link. E.g.: nstoc creates: <a class="wikilink1" title="Architectural Overview" href="/wiki/doku.php/work:intro­duction#architectural_overview">Architectural Overview</a> vs. DokuWiki: <a class="wikilink1" title="work:introduction" href="/wiki/doku.php/work:intro­duction#architectural_overview">architectural overview</a>. This means that offline processors can't use the title attribute for determining which page it points to e.g. offline plugin. Any chance you can say how to quickly change that?

I'm not sure about your point. The “title” attribute offers “advisory information about the element for which it is set” (see e.g. W3C), it is not intended for “determining which page it points to”. Usually it's displayed as a so-called “tooltip” window by visual brow­sers. The “href” attribute, however, “specifies the location of a Web resource” (see e.g. W3C) thus providing exactly the information you're obviously looking for.
Matthias Watermann 2009/08/07 10:57
Thanks for your reply. When using the offline plugin, the links created by nstoc are not converted correctly to local URLs. I assumed that the offline plugin was reading the HTML output of rendering a page and the only different between links that were converted correctly (manual links) and those that were not (nstoc) was the title attribute. I should probably hit up the offline plugin guys for the cause of this, they may be incorrectly assuming that the title attribute can help them find the referred page rather than parsing the href. If there is no convention for using the title attribute for this then its their problem I guess. - Lindsay Smith 09-08-2009

Nice plugin but it seems to me that there is a small bug if a page has same name than a namespace. In such a case, the page is ignored. For instance, if you have a namespace /organization and a page /organisation.txt the plugin will display /organisation/district_A.txt and /organisation/district_B.txt but not /organisation.txt which may introduce the other pages.

Maybe a patch should suppress ligne 643 in syntax.php
				. '&& (! is_dir($aBase .  "/" . substr($aFile, 0, -4)))'

Florent Chabaud 2009/12/23 20:51

What you're describing, Florent, is indeed an intended feature and documented above. I'd recommend to rename the pages you do not want to be skipped to something like oldname_overview.
Matthias Watermann 2010/02/18 11:17
@Florent: Thanks. This little “hack” solved exactly this problem I had
@Matthias: I understand the motivation of the current implementation, but e.g. in our environment, we completely got rid of the start pages and use pages with the same name as the namespaces as overview or index pages (usually using the nstoc plugin) (motivation: clean URLs). Renaming them is not an option because it would be against the intended results. Maybe this could become a configurable option for the plugin: To respect those “index” pages or not?
Frank Thommen 2010/05/27
@Florent: Cheers for the patch, works a treat. @Frank: +1. The current behavior may be intentional, but it “feels” broken when a significant number of your namespace headings disappear from the TOC, so their pages appear as children of the previous namespace.
Steve, 2010/05/29

I use your plugin to have a table of content at the beginning of a page.
The headings are automatically numbered by the numberedheadings plugin. Unfortunately nstoc does not display the numbers of the heading.
“ - heading” is shown instead of “1 heading”.
(the syntax of numberedheadings plugin is to add a '-' between the '=' and the headingtext e.g. ===== - heading =====)
Does anybody has an idea how to fix this? joachim 2010-02-08

This plugin reads the headings as they're stored in the filesystem. So, if there's a hyphen in the heading a hyphen is shown. You might want to have a look at another approach for numbered headings.
Matthias Watermann 2010/02/18 11:28

Don't work when ns name is not in English? I tried to set $conf['deaccent']=2, but still no result.
Yep I have the same problem in my wiki too,… namespaces in Greek don't show up, but this is inconsistent… some cases work some others don't, the mb_ extension is not available atm on that PHP environment could it be related ? — George PetsagourakisGeorge Petsagourakis

2011/02/13 12:54

I want to change the default green color but I don't know how to solve this. I try with the style.css in nstoc dir but it doesn't cnange anything .. Isma 2011/02/16 14:00

Hello, in handle function at line 488 why are you passing a string at param 3 for the substr function ? When cache is regenerated i got warning about this param which should be a long instead of a string…
I suggest that :
$dir = substr($current, 0, -mb_strlen(strrchr($current, ':')));


2011/10/11 09:18

Feature Requests

Numbered Headings

This plugin is almost perfect, but something is missing: I use the numbered heading plugin to automatically number my sections, but these numbers are not shown in the TOC build by this plugin. Maybe this could be solved by just using the built-in TOC generator of DokuWiki, so you can provide the same design and same features. ~~~~

Play well with publish plugin

Thanks for putting this together, I use it all over the place. My one request would be to see if you could make the plugin play well with publish plugin. I realize you can play well with every plugin, but draft is pretty popular. In particular, it would be great to have an option so that a page that is saved in draft form but does not currently have any published versions would not show up in the list.

List length

It would be great, if it is possible to restrict the headlinelist. like:

	[[nstoc [namespace [maxdepth][maxlength]]
In other words: just what com­pu­ters were made for.
actually, the head­lines in/of the pages are shown
space, ASCII char #32; not to be con­fu­sed with a blank, ASCII char #255
I hope …
i.e. the files but not the H1 head­line
or relevance
Note that there will be no collision with the page name 02_first_chapter since the latter inter­nally uses a file­name exten­sion.
what­ever that may mean for your indi­vi­dual in­stal­la­tion
You might find it worth­while to check out the experi­mental ACL Plugin.
see “man 7 regex” for details
Note that this plugin does not share Doku­Wiki's bug in hand­ling hidden pages if the RegEx is e.g. “0” but tests the $conf['hidepages'] set­ting pro­perly.
i.e. pages with the $conf['start'] name and pages with the same name as a sub-direc­tory
Please note that the pagemove plugin has problems with both internal and relative links. However, this doesn't affect the nstoc markup since it isn't recognized by pagemove anyway.
The comments within the source file are suit­able for the OSS doxygen tool, a do­cu­men­ta­tion sy­stem for C++, C, Java, Ob­jec­tive-C, Py­thon, IDL and to some ex­tent PHP, C#, and D. — Since I'm wor­king with dif­fe­rent pro­gram­ming lan­gua­ges it's a great ease to have one tool that handles the docs for all of them.
The source archive contains a commented and indented stylesheet for your in­for­ma­tion.
Just be careful when modifying a CSS file: both the order and the selector grou­pings are im­por­tant for CSS to work as intended/expected.
obsoleted by incorporating its ability into the Code plugin
which I do not use because it's broken in various ways
but I didn't bother to check with M$IE since that crap is broken anyway
plugin/nstoc.txt · Last modified: 2017-09-22 17:31 by bactram