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.
A while ago I started a project involving lots of hierarchically ordered pages – just like a book with chapters, sub-chapters and paragraphs.
To add (and update whenever a page was added/removed/merged) the ToC references was a neccesary but quite stupid job1).
After doing this pesky task for quite a few times I decided to automate it. —
Enter ”nstoc”.
This plugin offers you the ability to generate a Table Of Contents for a namespace with an optional depth. It generates a (possibly nested) list of headlines used in all matched pages.
One could say this plugin sees your whole Wiki as one huge document structured by chapters (Wiki namespaces), sub-chapters (the single pages within a namespace) and appropriate headlines (H1…H5).
The basic markup is just:
{{nstoc }}
This will create a nested list of all pages2) in the current namespace and all sub-namespaces.
Please note the space3) behind the ”nstoc” keyword:
Forgetting it will trigger DokuWiki's builtin media renderer which you most probably 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 headlines in the current namespace's pages and all H1 headlines in the pages of all sub-namespaces of the current one while
{{nstoc 3}}
will produce a list with all H1/H2/H3 headlines in the current namespace's pages, all H1/H2 headlines in the pages of all sub-namespaces of the current one and all H1 headlines in the pages of all sub-sub-namespaces.
Another way to limit the output is to explicitly name the namespace:
{{nstoc chapter2}}
This will show the headlines (with unlimited depth) in the ”chapter2” namespace.
You may, of course, combine the optional namespace and depth arguments:
{{nstoc chapter3 1}}
Here only the H1 headlines of the pages in ”chapter3” will be shown.
Here are some tips which might be helpful for you when working with this plugin.
The generated output – or, to be more precise: the order of the generated list – might not always be what you'd expect. The reason for this: You, as a human being4), have a notion of meaning while the computer just knowns about data. To illustrate this let's assume you're writing a book. Right now you've finished (or at least created) this pages:
When using ”nstoc” you'll most probably expect a list like the one above.
But, alas, the real result would look like
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 advantage by choosing the right page names. For example, name the pages5) like this:
Theoretically you could even ommit all the alphas and just leave the digits.
But I guess, that would take the computerising of your work a bit too far.
Anyway, as long as the page and namespace names sort in the intended order, ”nstoc” will produce useful output.
BTW: This discussion applys to namespace names as well.
This means, that you should name your namespaces according to their intended position (i.e. according to their respective position6) within your overall presentation).
If, for example, the first chapter of your book has several sub-chapters, you should name the namespace ”02_first_chapter”7) and the pages therein e.g. ”01_first_subject”, ”02_second_subject” and so on.
Of course, the headlines therein should be a little more meaningful for human readers.
Starting from the 2007-01-08 release of this plugin all pages not accessible to the respective current user/reader are omitted from the generated list. In other words: Any user will see a ToC containing only pages he/she may actually read. This avoids the inconvenience for your readers seeing a page/headline in the ToC but when trying to read it getting only an “access denied” message.
So, assuming you've setup your access control appropriately8) you don't have to worry about exposing (the existence of) pages which your users – or, at least, some of them – are not supposed to see.
Another benefit for you is that you don't have to modify the pages containing the ”nstoc” markup whenever you add a namespace and/or page.
To illustrate this remember our virtual book project outlined above.
Suppose you've got the preface and second chapter finished while still working on the other parts. And you want any casual reader to see only those finished pages but not the work-in-progress 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 accessible you'd change the overview 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 overview 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 namespace to be unreadable 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 consumption you just add a line to your access control like
book:00_preface @ALL 1
or whatever you feel appropriate9).
You won't have to change the book's overview page ever again – at least, not to update the ”nstoc” markup, that is.
Everything is managed by this plugin and DokuWiki's access control system.
Starting from the 2007-08-15 release of this plugin pages matched by the global 'hidepages' setting (i.e. a regular expression10)) will be omitted in the generated list as well11).
If the name given after the ”nstoc” keyword resolves to a default page name (i.e. ”start” with an unmodified DokuWiki installation) the respective namespace is used for generating the ToC but not the page.
The same happens if you're pointing to a page with the same name as a sub-namespace.
In case you're generating a ToC for a namespace that includes sub-namespaces all those assumed index pages12) are skipped as well.
This feature is intended to avoid indexing pages that are already meant to be kind of overview pages.
With earlier releases the root namespace had to be treated especially. Starting with the 2007-08-12 release of this plugin the root namespace now is handled almost as each other namespace. So you could use the basic markup
{{nstoc }}
to generate a ToC with all available pages in your DokuWiki installation.
Assuming a fairly structured installation, however, the pages in the root namespace are most probably some kind of starting point for one sub-namespace or another.
Hence it seems sensible to use ”nstoc” in the root namespace 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 namespace but show only those headlines found in the specified sub-namespaces.
Some people – as I've been told – do prefer to use numeric namespace names such as ”1”, ”23” or ”456”.
Although this isn't a problem as such for this plugin you must be careful when writing the nstoc markup.
I've stated above that giving a namespace's name is enough to get all headlines of that namespace (incl. its sub-namespaces) with unlimited depth.
So
{{nstoc 23}}
should show the headlines of namespace ”23”, right? –
Wrong: The plugin interprets this as a max. depth value of 23 for the current namespace.
To make sure the ”23” is accepted as the namespace's name you have to use the 2-arguments variant i.e. giving the max. depth value as well:
{{nstoc 23 4}}
Now the namespace called ”23” would get indexed up to a nesting depth of 4 levels. – Easy, isn't it?
This plugin allows for relative addressing the desired namespace as well.
Considering the book example above and assuming there's a sub-namespace in the first chapter called 03_important_points let's suppose you're in the namespace of the second chapter (i.e. in 03_second_chapter).
Now you'd like to provide links to the mentioned pages for your readers.
You could do this by either use an absolute path like
{{nstoc :book:02_first_chapter:03_important_points 2}}
or use a relative path like
{{nstoc ..:02_first_chapter:03_important_points 2}}
With this example it's only a difference of 3 characters.
But the deeper your namespaces are nested the more you save typing.
And – as an additional benefit – using relative paths leaves those links intact if you move the whole book to another place:
If – for instance – you decide to move/rename the whole book namespace into a new my_books namespace under the new name big_bang (or whatever) the absolute path above would no longer show any links while the relative one will still work as expected13).
Besides DokuWiki's : (colon) path separator this plugin allows the standard UNIX / (slash) as well.
Hence the second example above could be written
{{nstoc ../02_first_chapter/03_important_points 2}}
as well.
The respective current namespace can be addressed as ./, the parent namespace as ../ and the root namespace as /.
This seems to be more intuitive at least for those who are familiar with a shell commandline.
At least as long as the namespace/page structure you're indexing by this plugin is likely to change you should place the ”~~NOCACHE~~” directive in those files (pages) which contain the ”nstoc” markup.
That should make sure that the users always get an actual/current ToC.
It's quite easy to integrate this plugin with your DokuWiki:
{dokuwiki}/lib/plugins (make sure, included subdirectories are unpacked correctly); this will create the directory {dokuwiki}/lib/plugins/nstoc.chown apache:apache dokuwiki/lib/plugins/* -Rc
You might as well use the plugin manager for installing or updating this plugin.
Here comes the GPLed PHP source14) for those who'd like to scan before actually installing 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 : <support@mwat.de> * </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('&', '<', '>', '"'); /** * 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 : ?>
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 aesthetics16).
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
Consult DokuWiki's access control docs.
Hints, comments, suggestions …
The idea of this plugin is interesting. It could be very usefull, 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 http://jigsaw.w3.org/css-validator/ to help you get it fixed.) And I'd recommend to either add a left margin/padding to the ”ul.nstoc” CSS 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 aH2instead ofH1) – 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 theH1, (b) use only up to ”x”H2s while (c) skipping allH3/H4/H5lines.
The last point (c) seems to be the easiest one because the max-depth argument implicitely takes care of this:{{nstoc :legal 2}}would make sure that only H1 and H2 in thelegalnamespace will get used. However, once there'll be sub-namespaces (like:legal:whatever) only theH1s 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 theH2s 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 severalH2s shouldn't get split up into several pages of their own (probably in their own “sub-chapter” namespace).
That leaves (a) i.e. skippingH1. This as well doesn't fit with the plugin's purpose, I fear. And assuming thatH1should provide fairly releveant 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'd 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 builtin “index” (yourhttp://www.patentblurb.com/doku.php?idx=wikiURL) 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 Doku'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 namespaces to