Introduction

A patch against 0.24. Implements a new macro, [[ContentsTable]], which does the following: collects all headings used in the document; outputs a nicely formatted table of contents; automatically updates, maintains and synchronises numbering in both headings and contents table; also syncs local links from the contents table to the headings. --CiprianPopovici

Contents tables will be rendered in final version of the pages as well as in previews.

How this idea works

The patch contains a new file foreign_code/contents_table/heading.php, which is licensed under GPL v2. This shouldn't be a problem for 'Tavi, but if you want to use the code in other projects beware of the GPL restrictions.

I hope the patch is easy to understand and implement.

Discussion

That's exactly what I was going for: discouraging anchor linking. Since wiki's are ephemeral by nature, if you link to a heading (ie. 3.2.2) there's no guarantee that the contents at that heading will always be the same. But it should be possible to work around this, by generating anchors based on heading text instead of being random. -- CiprianPopovici

diff -ruN patch follows:

diff -ruN tavi/foreign_code/contents_table/headings.php tavi_contents_table/foreign_code/contents_table/headings.php
--- tavi/foreign_code/contents_table/headings.php	Thu Jan  1 02:00:00 1970
+++ tavi_contents_table/foreign_code/contents_table/headings.php	Mon Feb 21 16:03:28 2005
@@ -0,0 +1,103 @@
+<?php
+/********************************************************************************
+headings.php v2 Feb 2005, (c)Ciprian Popovici <ciprian@zuavra.net>
+
+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 version 2
+of the License, or (at your option) any later version.
+
+This program 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: <http://www.gnu.org/licenses/gpl.txt>
+********************************************************************************/
+
+  class HEADINGS_class {
+    var $headings = array();
+    var $level = 0;
+    var $contents = '';
+    
+    function HEADINGS_class($text='0') {
+      // reset internal vars
+      $this->headings=array();
+      $this->level=0;
+      $this->contents='';
+      // parse init value and create a default state
+      $a=explode('.',$text);
+      $this->level=count($a);
+      for ($i=1;$i<=count($a);$i++) {
+        $this->headings[$i]=(int)$a[$i-1];
+      }
+    }
+
+    function update($level,$head_start,$text,$head_end,$anchor='') {
+
+      // record old level
+      $old_level=$this->level;
+      // sanity check
+      if ($level<1) {
+        $level=1;
+      }
+      // update headings
+      if ($this->level<$level) {
+        for ($i=$this->level+1;$i<=$level;$i++) {
+          $this->headings[$i]=1;
+        }
+      }
+      elseif ($this->level==$level) {
+        $this->headings[$level]++;
+      }
+      else {
+        $this->headings[$level]++;
+      }
+      // update level
+      $this->level=$level;
+      // generate contents
+      // need to close list levels?
+      if ($old_level>$this->level) {
+        for ($i=0;$i<abs($old_level-$this->level);$i++) {
+          $this->contents.="</dl>\n";
+        }
+      }
+      // need to open list levels?
+      if ($old_level<$this->level) {
+        for ($i=0;$i<abs($this->level-$old_level);$i++) {
+          $this->contents.="<dl>\n";
+        }
+      }
+      // insert element
+      $id='';
+      for ($i=1;$i<=$this->level;$i++) {
+        $id.=$this->headings[$i].($i<$this->level?'.':'');
+      }
+      // record entry
+      $this->contents.='<dd>'.$id.' <a href="#'.$anchor.$id.'">'.strip_tags(parse_elements($text,1)).'</a>'."\n";
+      // output heading
+      return $this->display($head_start,$text,$head_end,$anchor);
+    }
+    
+    function display($head_start,$text,$head_end,$anchor='') {
+      $id='';
+      for ($i=1;$i<=$this->level;$i++) {
+        $id.=$this->headings[$i].($i<$this->level?'.':'');
+      }
+      $output = $head_start;
+      $output .= $id.') '.$text;
+      $output .= '<a name="'.$anchor.$id.'"></a>';
+      $output .= $head_end;
+      return $output;
+    }
+    
+    function get_contents() {
+      if ($this->level>1) {
+        // need to close list levels?
+        for ($i=0;$i<abs($this->level-1);$i++) {
+          $this->contents.="</dl>\n";
+        }
+      }
+      return $this->contents;
+    }
+  }
+
+?>
\ No newline at end of file
diff -ruN tavi/lib/defaults.php tavi_contents_table/lib/defaults.php
--- tavi/lib/defaults.php	Tue Sep  9 13:17:08 2003
+++ tavi_contents_table/lib/defaults.php	Mon Feb 21 16:20:20 2005
@@ -14,6 +14,11 @@
 // value.  This will override the default set here.
 //**********************************************************************
 
+// The following variables are used by the [[ContentsTable]] macro
+$ContentsTableEnabled = true;
+$ContentsTableId = md5(mt_rand()); // some random contents strongly recommended
+$ContentsTableMarker = '<!-- '.$ContentsTableId.' -->'; // HTML comment recommended
+
 // The following variables establish the format for WikiNames in this wiki.
 $UpperPtn = "[A-Z\xc0-\xde]";
 $LowerPtn = "[a-z\xdf-\xfe]";
@@ -252,7 +257,8 @@
                      'WantedPages'   => 'view_macro_wanted',
                      'TitleSearch'   => 'view_macro_titlesearch',
                      'PageLinks'     => 'view_macro_outlinks',
-                     'PageRefs'      => 'view_macro_refs'
+                     'PageRefs'      => 'view_macro_refs',
+                     'ContentsTable' => 'view_macro_contents',
                    );
 
 // $SaveMacroEngine determines what save macros will be called after a
diff -ruN tavi/lib/init.php tavi_contents_table/lib/init.php
--- tavi/lib/init.php	Fri Feb 22 16:46:08 2002
+++ tavi_contents_table/lib/init.php	Mon Feb 21 15:58:19 2005
@@ -9,6 +9,10 @@
 require('lib/messages.php');
 require('lib/pagestore.php');
 require('lib/rate.php');
+require_once('foreign_code/contents_table/headings.php');
+
+$ContentsTable=new HEADINGS_class();
+$ContentsTableVisible = false; 
 
 $PgTbl = $DBTablePrefix . 'pages';
 $IwTbl = $DBTablePrefix . 'interwiki';
diff -ruN tavi/parse/macros.php tavi_contents_table/parse/macros.php
--- tavi/parse/macros.php	Mon Sep  8 14:10:12 2003
+++ tavi_contents_table/parse/macros.php	Mon Feb 21 15:59:26 2005
@@ -355,6 +355,18 @@
   return html_code($text);
 }
 
+// Output a marker for the would-be header table of contents
+// and signal the creation of the marker via a global variable
+function view_macro_contents() {
+  global $ContentsTableMarker, $ContentsTableVisible, $ContentsTableEnabled;
+
+  if ($ContentsTableEnabled) {
+    $ContentsTableVisible = true;
+    return $ContentsTableMarker;
+  }
+  return '';
+}
+
 // Prepare a list of pages sorted by how many links to them exist.
 function view_macro_refs()
 {
diff -ruN tavi/parse/transforms.php tavi_contents_table/parse/transforms.php
--- tavi/parse/transforms.php	Mon Sep  8 15:11:38 2003
+++ tavi_contents_table/parse/transforms.php	Mon Feb 21 16:00:53 2005
@@ -504,6 +504,7 @@
 function parse_heading($text)
 {
   global $MaxHeading;
+  global $ContentsTable, $ContentsTableEnabled, $ContentsTableId;
 
   if(!preg_match('/^\s*(=+) (.*) (=+)\s*$/', $text, $result))
     { return $text; }
@@ -514,9 +515,19 @@
   if(($level = strlen($result[1])) > $MaxHeading)
     { $level = $MaxHeading; }
 
-  return new_entity(array('head_start', $level)) .
-         $result[2] .
-         new_entity(array('head_end', $level));
+  // if enabled, add header to table of contents
+  if ($ContentsTableEnabled) {
+    return $ContentsTable->update(
+      $level-1,
+      new_entity(array('head_start', $level)),
+      $result[2],
+      new_entity(array('head_end', $level)),
+      $ContentsTableId
+    );
+  }
+  return new_entity(array('head_start', $level)).
+      $result[2].
+      new_entity(array('head_end', $level));
 }
 
 function parse_htmlisms($text)
diff -ruN tavi/template/common.php tavi_contents_table/template/common.php
--- tavi/template/common.php	Fri Aug 29 18:03:13 2003
+++ tavi_contents_table/template/common.php	Mon Feb 21 16:02:25 2005
@@ -110,6 +110,7 @@
 function template_common_epilogue($args)
 {
   global $FindScript, $pagestore;
+  global $ContentsTableVisible, $ContentsTableMarker, $ContentsTableEnabled, $ContentsTable;
 
 ?>
 <div id="footer">
@@ -177,6 +178,14 @@
 </html>
 <?php
 
+  if ($ContentsTableEnabled && $ContentsTableVisible) {
+    $buffer=ob_get_contents();
+    ob_end_clean();
+    ob_start();
+    $buffer=str_replace($ContentsTableMarker,'<dl>'.$ContentsTable->get_contents().'</dl>',$buffer);
+    print $buffer;
+  }
+
   $size = ob_get_length();
   header("Content-Length: $size");
   ob_end_flush();

Same idea, different implementation

I've implemented the same idea in a different way (for Tavi 0.26). I won't explain much, I just wanted to have the patch here for completeness:


diff -burN release-0.26/lang/default.php toc/lang/default.php
--- release-0.26/lang/default.php    2006-10-22 00:22:25.893600456 +0200
+++ toc/lang/default.php    2006-10-22 00:23:03.692854096 +0200
@@ -87,6 +87,7 @@
 setConst('PARSE_ButtonPreview', 'Preview');
 setConst('PARSE_Preferences', 'Preferences');
 setConst('PARSE_History', 'history'); // Note the lowercase first character
+setConst('PARSE_TableOfContents', 'Contents');
 
 /* Template directory */
 // Note the change to use only TMPL_ as prefix instead of full TEMPLATE_
diff -burN release-0.26/lang/lang_de.php toc/lang/lang_de.php
--- release-0.26/lang/lang_de.php    2006-10-22 00:22:25.890600912 +0200
+++ toc/lang/lang_de.php    2006-10-22 00:23:03.444891792 +0200
@@ -87,6 +87,7 @@
 setConst('PARSE_ButtonPreview', 'Vorschau');
 setConst('PARSE_Preferences', 'Preferences');
 setConst('PARSE_History', 'verlauf'); // note lowercase first character
+setConst('PARSE_TableOfContents', 'Inhalt');
 
 /* Template directory */
 // Note the change to use only TMPL_ as prefix instead of full TEMPLATE_
diff -burN release-0.26/lib/defaults.php toc/lib/defaults.php
--- release-0.26/lib/defaults.php    2006-10-22 00:22:27.297387048 +0200
+++ toc/lib/defaults.php    2006-10-22 00:23:04.343755144 +0200
@@ -274,7 +274,8 @@
                    'diff_change'         => 'html_diff_change',
                    'diff_add'            => 'html_diff_add',
                    'diff_delete'         => 'html_diff_delete',
-                   'reflist'             => 'html_reflist'
+                   'reflist'             => 'html_reflist',
+                   'toc'                 => 'html_toc'
                  );
 
 // $ViewMacroEngine determines what macro names will be processed when
@@ -291,7 +292,8 @@
                      'TitleSearch'   => 'view_macro_titlesearch',
                      'PageLinks'     => 'view_macro_outlinks',
                      'PageRefs'      => 'view_macro_refs',
-                     'RefList'       => 'view_macro_reflist'
+                     'RefList'       => 'view_macro_reflist',
+                     'Toc'           => 'view_macro_toc'
                    );
 
 // $SaveMacroEngine determines what save macros will be called after a
diff -burN release-0.26/lib/init.php toc/lib/init.php
--- release-0.26/lib/init.php    2006-10-22 00:22:27.294387504 +0200
+++ toc/lib/init.php    2006-10-22 00:23:04.083794664 +0200
@@ -28,6 +28,9 @@
 $Entity = array();                      // Global parser entity list.
 
 $RefList = array(); // Array of referenced links, see view_macro_reflist
+
+$Toc = array();                         // Global table of contents
+
 // Strip slashes from incoming variables.
 
 if(get_magic_quotes_gpc())
diff -burN release-0.26/parse/html.php toc/parse/html.php
--- release-0.26/parse/html.php    2006-10-22 00:22:27.983282776 +0200
+++ toc/parse/html.php    2006-10-22 00:24:32.318380968 +0200
@@ -66,9 +66,13 @@
   { return "<p>"; }
 function html_paragraph_end()
   { return "</p>\n"; }
-function html_head_start($level)
-  { return "<h$level>"; }
-function html_head_end($level)
+function html_head_start($level, $count, $number) {
+  $anchor = '';
+  if (isset($count))
+    { $anchor = " id=\"s$count\""; }
+  return "<h$level$anchor>";
+}
+function html_head_end($level, $count, $number)
   { return "</h$level>"; }
 function html_nowiki($text)
   { return $text; }
@@ -473,4 +477,55 @@
   
   return $output;
 }
+
+function html_toc($maxLevel)
+{
+  global $MaxHeading;
+  global $Toc;
+  static $TocCount = 0;
+
+  if (count($Toc) == 0)
+    { return ''; }
+
+  if (!$maxLevel) { $maxLevel = 9; }
+  $maxLevel = min($MaxHeading, $maxLevel);
+  $maxLevel = max(1, $maxLevel);
+
+  $id = "toc$TocCount";
+
+  $output = "\n<div class=\"contents\" id=\"$id\">\n<h2>"
+          . PARSE_TableOfContents
+          . "</h2>\n"; //db
+
+  $level = 0;
+  $topLevel = $Toc[0]['level'] - 1;
+  $lastLevel = 0;
+
+  foreach ($Toc as $count => $value) {
+
+    $level = max(1, $value['level'] - $topLevel);
+    if ($level > $maxLevel) { continue; }
+    $num = $value['num'] == '' ? '' : $value['num'] . ' ';
+
+    if ($level > $lastLevel) {         // indent as needed
+      for ($i = $lastLevel + 1; $i <= $level; $i++)
+        { $output .= str_pad('',$i-1) . "<ul>\n"; }
+    }
+    else if ($level < $lastLevel) {    // outdent
+      for ($i = $lastLevel; $i > $level; $i--)
+        { $output .= str_pad('',$i-1) . "</ul>\n"; }
+    }
+
+    $output .= str_pad('',$level)
+            . "<li><a href=\"#s$count\">"
+            . $num . parse_elements($value['title']) . "</a></li>\n";
+
+    $lastLevel = $level;
+  }
+  for ($i = $level; $i >= 1; $i--)      // outdent
+    { $output .= str_pad('',$i-1) . "</ul>\n"; }
+
+  $output .= "<hr class=\"hidden\" /></div>\n";
+  return $output;
+}
 ?>
\ Kein Zeilenumbruch am Dateiende.
diff -burN release-0.26/parse/macros.php toc/parse/macros.php
--- release-0.26/parse/macros.php    2006-10-22 00:22:27.984282624 +0200
+++ toc/parse/macros.php    2006-10-22 00:23:08.871066888 +0200
@@ -479,4 +479,14 @@
 {
   return parse_elements(new_entity(array("reflist", $args)));
 }
+
+function view_macro_toc($args)
+{
+  global $PostParsing;
+  $PostParsing = 1;
+
+  if (!preg_match('/^[1-9]?$/', $args))
+    { return "[[Toc $args]]"; }
+  return new_entity(array('toc', $args));
+}
 ?>
diff -burN release-0.26/parse/main.php toc/parse/main.php
--- release-0.26/parse/main.php    2006-10-22 00:22:27.987282168 +0200
+++ toc/parse/main.php    2006-10-22 00:23:09.087034056 +0200
@@ -4,11 +4,13 @@
 // Master parser for 'Tavi.
 function parseText($text, $parsers, $object_name)
 {
-  global $Entity, $ParseObject;
+  global $Entity, $ParseObject, $PostParsing;
 
   $old_parse_object = $ParseObject;
   $ParseObject = $object_name;          // So parsers know what they're parsing.
 
+  $PostParsing = 0;                     // Some parsers need extra post-processing.
+
   $count  = count($parsers);
   $result = '';
 
@@ -31,10 +33,14 @@
   $line = '';
   for($i = 0; $i < $count; $i++)
     { $line = $parsers[$i]($line); }
+  $result = $result . $line;
+
+  if ($PostParsing)
+    { $result = post_parser($result); }
 
   $ParseObject = $old_parse_object;
 
-  return $result . $line;
+  return $result;
 }
 
 ?>
diff -burN release-0.26/parse/transforms.php toc/parse/transforms.php
--- release-0.26/parse/transforms.php    2006-10-22 00:22:27.982282928 +0200
+++ toc/parse/transforms.php    2006-10-22 00:23:04.785687960 +0200
@@ -74,6 +74,22 @@
   return $text;
 }
 
+function post_parser($text)
+{
+  // Some parsers need to perform extra action after the page has been processed
+  // completely once. They leave Entities that can be resolved now.
+
+  // Can't reuse parseText, because it would escape FlgChr's.
+  $result = '';
+  foreach(explode("\n", $text) as $line)
+  {
+    $line = $line . "\n";
+    $result .= parse_elements($line);
+  }
+
+  return $result;
+}
+
 function code_token($codetype, $code) 
 {
   global $FlgChr, $Entity;
@@ -286,7 +302,7 @@
   $cmd  = strtok($macro, ' ');
   $args = strtok('');
 
-  if($ViewMacroEngine[$cmd] != '')
+  if (isset($ViewMacroEngine[$cmd]) && $ViewMacroEngine[$cmd] != '')
     { 
       if ($cmd == 'Anchor') 
         { return new_entity(array('raw', $ViewMacroEngine[$cmd]($args)), 0); }
@@ -585,19 +601,47 @@
 {
   global $MaxHeading, $HeadingOffset;
 
-  if(!preg_match('/^\s*(=+) (.*) (=+)\s*$/', $text, $result))
-    { return $text; }
+  global $Toc;
+  static $TocLevels = array();
+  static $TocLastLevel = 0;
+  static $TocTopLevel = -1;
 
-  if(strlen($result[1]) != strlen($result[3]))
+  if(!preg_match('/^\s*(@?)(=+) (.*) \2\s*$/', $text, $result))
     { return $text; }
 
-  $level = strlen($result[1]) + $HeadingOffset; 
+  $level = strlen($result[2]) + $HeadingOffset; 
   if($level > $MaxHeading)
     { $level = $MaxHeading; }
 
-  return new_entity(array('head_start', $level)) .
-         $result[2] .
-         new_entity(array('head_end', $level));
+  $header_num = '';
+  if ($result[1] == '@') {  //heading numbering on
+    if ($TocTopLevel < 0)
+      { $TocTopLevel = $level - 1; }
+    if ($level > $TocLastLevel) {
+      for ($i = $TocLastLevel + 1; $i < $level; $i++)
+        { $TocLevels[$i] = 1; }
+      $TocLevels[$level] = 0;
+    }
+    $TocLastLevel = $level;
+    $TocLevels[$level]++;
+    for ($i = $TocTopLevel + 1; $i <= $level; $i++) {
+      if ($header_num != '')
+        { $header_num .= '.'; }
+      $header_num .= $TocLevels[$i];
+    }
+  }
+
+  $header_count = count($Toc);
+  $Toc[$header_count] = array(
+                          'level' => $level,
+                          'num' => $header_num,
+                          'title' => $result[3]
+                        );
+
+  return new_entity(array('head_start', $level, $header_count, $header_num)) .
+         ($header_num == '' ? '' : "$header_num ") .
+         $result[3] .
+         new_entity(array('head_end', $level, $header_count, $header_num));
 }
 
 function parse_htmlisms($text)
diff -burN release-0.26/template/wiki.css toc/template/wiki.css
--- release-0.26/template/wiki.css    2006-10-22 00:22:28.429214984 +0200
+++ toc/template/wiki.css    2006-10-22 00:23:09.450978728 +0200
@@ -65,6 +65,24 @@
 div#header hr
   { clear: both; }
 
+/* Table of contents */
+div.contents
+  { width: 12em;
+    float: right;
+    padding: 0.5em;
+    border-style: solid;
+    border-width: 1px;
+    border-color: black; }
+div.contents h2
+  { font-size: 100%; }
+div.contents ul
+  { list-style-type: none;
+    padding-left: 1em; }
+div.contents > ul
+  { padding-left: 0; }
+.hidden
+  { display: none; }
+
 /* Some php-syntax highlighting defaults */
 pre.phpsource { border-width: 1px; border-style: solid;  border-color: #000000; 
                 background-color: #d5d5d5;