* @author: Reini Urban
*
* Known problems:
* - MacIE will not work with jshide.
* - it will crash with old markup and Apache2 (?)
* - Certain corner-edges will not work with TOC_FULL_SYNTAX.
* I believe I fixed all of them now, but who knows?
* - bug #969495 "existing labels not honored" seems to be fixed.
*/
if (!defined('TOC_FULL_SYNTAX'))
define('TOC_FULL_SYNTAX', 1);
class WikiPlugin_CreateToc
extends WikiPlugin
{
function getName() {
return _("CreateToc");
}
function getDescription() {
return _("Automatically link headers at the top");
}
function getVersion() {
return preg_replace("/[Revision: $]/", '',
"\$Revision: 1.31 $");
}
function getDefaultArguments() {
return array( 'pagename' => '[pagename]', // TOC of another page here?
// or headers=1,2,3 is also possible.
'headers' => "!!!,!!,!", // "!!!"=>h1, "!!"=>h2, "!"=>h3
'noheader' => 0, // omit
Table of Contents
'position' => 'right', // or left
'with_toclink' => 0, // link back to TOC
'jshide' => 0, // collapsed TOC as DHTML button
'extracollapse' => 1, // provide an entry +/- link to collapse
'liststyle' => 'dl', // 'dl' or 'ul' or 'ol'
'indentstr' => ' ',
'with_counter' => 0,
);
}
// Initialisation of toc counter
function _initTocCounter() {
$counter = array(1=>1, 2=>0, 3=>0);
return $counter;
}
// Update toc counter with a new title
function _tocCounter(&$counter, $level) {
$counter[$level]++;
$level--;
for($i = $level; $i > 0; $i--) {
$counter[$level] = 0;
}
}
// Get string corresponding to the current title
function _getCounter(&$counter, $level) {
$str=$counter[3];
for($i = 2; $i > 0; $i--) {
if($counter[$i] != 0)
$str .= '.'.$counter[$i];
}
return $str;
}
function preg_quote ($heading) {
return str_replace(array("/",".","?","*"),
array('\/','\.','\?','\*'), $heading);
}
// Get HTML header corresponding to current level (level is set of !)
function _getHeader($level) {
$count = substr_count($level,'!');
switch ($count) {
case 1: $h = "h4"; break;
case 2: $h = "h3"; break;
case 3: $h = "h2"; break;
}
return $h;
}
function _quote($heading) {
if (TOC_FULL_SYNTAX ) {
$theading = TransformInline($heading);
if ($theading)
return preg_quote($theading->asXML(), "/");
else
return XmlContent::_quote(preg_quote($heading, "/"));
} else {
return XmlContent::_quote(preg_quote($heading, "/"));
}
}
/*
* @param $hstart id (in $content) of heading start
* @param $hend id (in $content) of heading end
*/
function searchHeader ($content, $start_index, $heading,
$level, &$hstart, &$hend, $basepage=false) {
$hstart = 0;
$hend = 0;
$h = $this->_getHeader($level);
$qheading = $this->_quote($heading);
for ($j=$start_index; $j < count($content); $j++) {
if (is_string($content[$j])) {
if (preg_match("/<$h>$qheading<\/$h>/",
$content[$j]))
return $j;
}
elseif (isa($content[$j], 'cached_link'))
{
if (method_exists($content[$j],'asXML')) {
$content[$j]->_basepage = $basepage;
$content[$j] = $content[$j]->asXML();
} else
$content[$j] = $content[$j]->asString();
// shortcut for single wikiword or link headers
if ($content[$j] == $heading
and substr($content[$j-1],-4,4) == "<$h>"
and substr($content[$j+1],0,5) == "$h>")
{
$hstart = $j-1;
$hend = $j+1;
return $j; // single wikiword
}
elseif (TOC_FULL_SYNTAX) {
//DONE: To allow "!! WikiWord link" or !! http://anylink/
// Check against joined content (after cached_plugininvocation).
// The first link is the anchor then.
if (preg_match("/<$h>(?!.*<\/$h>)/", $content[$j-1])) {
$hstart = $j-1;
$joined = '';
for ($k=max($j-1,$start_index);$kasXML();
else
$joined .= $content[$k]->asString();
if (preg_match("/<$h>$qheading<\/$h>/",$joined)) {
$hend=$k;
return $k;
}
}
}
}
}
}
trigger_error("Heading <$h> $heading $h> not found\n", E_USER_NOTICE);
return 0;
}
/** prevent from duplicate anchors,
* beautify spaces: " " => "_" and not "x20."
*/
function _nextAnchor($s) {
static $anchors = array();
$s = str_replace(' ','_',$s);
$i = 1;
$anchor = $s;
while (!empty($anchors[$anchor])) {
$anchor = sprintf("%s_%d",$s,$i++);
}
$anchors[$anchor] = $i;
return $anchor;
}
// Feature request: proper nesting; multiple levels (e.g. 1,3)
function extractHeaders (&$content, &$markup, $backlink=0,
$counter=0, $levels=false, $basepage='')
{
if (!$levels) $levels = array(1,2);
$tocCounter = $this->_initTocCounter();
reset($levels);
sort($levels);
$headers = array();
$j = 0;
for ($i=0; $i 3) continue;
if (preg_match('/^\s*(!{'.$level.','.$level.'})([^!].*)$/',
$content[$i], $match))
{
$this->_tocCounter($tocCounter, $level);
if (!strstr($content[$i],'#[')) {
$s = trim($match[2]);
$anchor = $this->_nextAnchor($s);
$manchor = MangleXmlIdentifier($anchor);
$texts = $s;
if($counter) {
$texts = $this->_getCounter($tocCounter, $level).' '.$s;
}
$headers[] = array('text' => $texts,
'anchor' => $anchor,
'level' => $level);
// Change original wikitext, but that is useless art...
$content[$i] = $match[1]." #[|$manchor][$s|#TOC]";
// And now change the to be printed markup (XmlTree):
// Search $s line in markup
/* Url for backlink */
$url = WikiURL(new WikiPageName($basepage,false,"TOC"));
$j = $this->searchHeader($markup->_content, $j, $s,
$match[1], $hstart, $hend,
$markup->_basepage);
if ($j and isset($markup->_content[$j])) {
$x = $markup->_content[$j];
$qheading = $this->_quote($s);
if ($counter)
$counterString = $this->_getCounter($tocCounter, $level);
if (($hstart === 0) && is_string($markup->_content[$j])) {
if ($backlink) {
if ($counter)
$anchorString = "$counterString - \$2";
else
$anchorString = "\$2";
} else {
$anchorString = "";
if ($counter)
$anchorString .= "$counterString - ";
}
if ($x = preg_replace('/()('.$qheading.')(<\/h\d>)/',
"\$1$anchorString\$2\$3",$x,1)) {
if ($backlink) {
$x = preg_replace('/()('.$qheading.')(<\/h\d>)/',
"\$1$anchorString\$3",
$markup->_content[$j],1);
}
$markup->_content[$j] = $x;
}
} else {
$x = $markup->_content[$hstart];
$h = $this->_getHeader($match[1]);
if ($backlink) {
if ($counter)
$anchorString = "\$1$counterString - ";
else {
/* Not possible to make a backlink on a
* title with a WikiWord */
$anchorString = "\$1";
}
}
else {
$anchorString = "\$1";
if ($counter)
$anchorString .= "$counterString - ";
}
$x = preg_replace("/(<$h>)(?!.*<\/$h>)/",
$anchorString, $x, 1);
if ($backlink) {
$x = preg_replace("/(<$h>)(?!.*<\/$h>)/",
$anchorString,
$markup->_content[$hstart],1);
}
$markup->_content[$hstart] = $x;
}
}
}
}
}
}
return $headers;
}
function run($dbi, $argstr, &$request, $basepage) {
extract($this->getArgs($argstr, $request));
if ($pagename) {
// Expand relative page names.
$page = new WikiPageName($pagename, $basepage);
$pagename = $page->name;
}
if (!$pagename) {
return $this->error(_("no page specified"));
}
if ($jshide and isBrowserIE() and browserDetect("Mac")) {
//trigger_error(_("jshide set to 0 on Mac IE"), E_USER_NOTICE);
$jshide = 0;
}
$page = $dbi->getPage($pagename);
$current = $page->getCurrentRevision();
//FIXME: I suspect this only to crash with Apache2
if (!$current->get('markup') or $current->get('markup') < 2) {
if (in_array(php_sapi_name(),array('apache2handler','apache2filter'))) {
trigger_error(_("CreateToc disabled for old markup"), E_USER_WARNING);
return '';
}
}
$content = $current->getContent();
$html = HTML::div(array('class' => 'toc','id'=>'toc','align' => $align));
if ($liststyle == 'dl')
$list = HTML::dl(array('id'=>'toclist','class' => 'toc'));
elseif ($liststyle == 'ul')
$list = HTML::ul(array('id'=>'toclist','class' => 'toc'));
elseif ($liststyle == 'ol')
$list = HTML::ol(array('id'=>'toclist','class' => 'toc'));
if (!strstr($headers,",")) {
$headers = array($headers);
} else {
$headers = explode(",",$headers);
}
$levels = array();
foreach ($headers as $h) {
//replace !!! with level 1, ...
if (strstr($h,"!")) {
$hcount = substr_count($h,'!');
$level = min(max(1, $hcount),3);
$levels[] = $level;
} else {
$level = min(max(1, (int) $h), 3);
$levels[] = $level;
}
}
if (TOC_FULL_SYNTAX)
require_once("lib/InlineParser.php");
if ($headers = $this->extractHeaders($content, $dbi->_markup,
$with_toclink, $with_counter,
$levels, $basepage))
{
foreach ($headers as $h) {
// proper heading indent
$level = $h['level'];
$indent = 3 - $level;
$link = new WikiPageName($pagename,$page,$h['anchor']);
$li = WikiLink($link,'known',$h['text']);
if ($liststyle == 'dl')
$list->pushContent(HTML::dt(HTML::raw
(str_repeat($indentstr,$indent)),$li));
else
$list->pushContent(HTML::li(HTML::raw
(str_repeat($indentstr,$indent)),$li));
}
}
$list->setAttr('style','display:'.($jshide?'none;':'block;'));
$html->pushContent(Javascript("
function toggletoc(a) {
toc=document.getElementById('toclist')
toctoggle=document.getElementById('toctoggle')
if (toc.style.display=='none') {
toc.style.display='block'
a.title='"._("Click to hide the TOC")."'
toctoggle.innerHTML='[-]'
} else {
toc.style.display='none';
a.title='"._("Click to display")."'
toctoggle.innerHTML='[+]'
}
}"));
if ($extracollapse)
$toclink = HTML(_("Table Of Contents"),
" ",
HTML::a(array('name'=>'TOC',
'id'=>'toctoggle',
'class'=>'wikiaction',
'title'=>_("Click to display to TOC"),
'onclick'=>"toggletoc(this)"),
$jshide?'[+]':'[-]'));
else
$toclink = HTML::a(array('name'=>'TOC',
'class'=>'wikiaction',
'title'=>_("Click to display"),
'onclick'=>"toggletoc(this)"),
_("Table Of Contents"),
HTML::span(array('style'=>'display:none',
'id'=>'toctoggle')," "));
$html->pushContent(HTML::h4($toclink));
$html->pushContent($list);
return $html;
}
};
// $Log: not supported by cvs2svn $
// Revision 1.30 2006/12/22 17:49:38 rurban
// fix quoting
//
// Revision 1.29 2006/04/15 12:26:54 rurban
// need basepage for subpages like /Remove (within CreateTOC)
//
// Revision 1.28 2005/10/12 06:15:25 rurban
// just aesthetics
//
// Revision 1.27 2005/10/10 19:50:45 rurban
// fix the missing formatting problems, add with_counter arg by ?? (20050106), Thanks to ManuelVacelet for the testcase
//
// Revision 1.26 2004/09/20 14:07:16 rurban
// fix Constant toc_full_syntax already defined warning
//
// Revision 1.25 2004/07/08 20:30:07 rurban
// plugin->run consistency: request as reference, added basepage.
// encountered strange bug in AllPages (and the test) which destroys ->_dbi
//
// Revision 1.24 2004/06/28 13:27:03 rurban
// CreateToc disabled for old markup and Apache2 only
//
// Revision 1.23 2004/06/28 13:13:58 rurban
// CreateToc disabled for old markup
//
// Revision 1.22 2004/06/15 14:56:37 rurban
// more allow_call_time_pass_reference false fixes
//
// Revision 1.21 2004/06/13 09:45:23 rurban
// display bug workaround for MacIE browsers, jshide: 0
//
// Revision 1.20 2004/05/11 13:57:46 rurban
// enable TOC_FULL_SYNTAX per default
// don't $header to disable css formatting for such anchors
// => $header
//
// Revision 1.19 2004/05/08 16:59:27 rurban
// requires optional TOC_FULL_SYNTAX constnat to enable full link and
// wikiword syntax in headers.
//
// Revision 1.18 2004/04/29 21:55:15 rurban
// fixed TOC backlinks with USE_PATH_INFO false
// with_toclink=1, sf.net bug #940682
//
// Revision 1.17 2004/04/26 19:43:03 rurban
// support most cases of header markup. fixed duplicate MangleXmlIdentifier name
//
// Revision 1.16 2004/04/26 14:46:14 rurban
// better comments
//
// Revision 1.14 2004/04/21 04:29:50 rurban
// write WikiURL consistently (not WikiUrl)
//
// Revision 1.12 2004/03/22 14:13:53 rurban
// fixed links to equal named headers
//
// Revision 1.11 2004/03/15 09:52:59 rurban
// jshide button: dynamic titles
//
// Revision 1.10 2004/03/14 20:30:21 rurban
// jshide button
//
// Revision 1.9 2004/03/09 19:24:20 rurban
// custom indentstr
// h2 toc header
//
// Revision 1.8 2004/03/09 19:05:12 rurban
// new liststyle arg. default: dl (no bullets)
//
// Revision 1.7 2004/03/09 11:51:54 rurban
// support jshide=1: DHTML button hide/unhide TOC
//
// Revision 1.6 2004/03/09 10:25:37 rurban
// slightly better formatted TOC indentation
//
// Revision 1.5 2004/03/09 08:57:10 rurban
// convert space to "_" instead of "x20." in anchors
// proper heading indent
// handle duplicate headers
// allow multiple headers like "!!!,!!" or "1,2"
//
// Revision 1.4 2004/03/02 18:21:29 rurban
// typo: ref=>href
//
// Revision 1.1 2004/03/01 18:10:28 rurban
// first version, without links, anchors and jscript folding
//
//
// For emacs users
// Local Variables:
// mode: php
// tab-width: 8
// c-basic-offset: 4
// c-hanging-comment-ender-p: nil
// indent-tabs-mode: nil
// End:
?>