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