<?php if (!defined('PmWiki')) exit(); /* The pagetoc script adds support for automatically generating a table of contents for a wiki page. Copyright 2004-2025 John Rankin (john.rankin@affinity.co.nz) 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. 2024-01-18 fix $m[1] not defined in function TocLinkText PHP 8 warning Version 2.1.5 (production version; works with PmWiki 2.3.31 or above) Requires php 5.3 or above and is compatible with php 5.5 Updated to replace Markup_e with anonymous functions for php 7 Couple updates for php 8. */ $RecipeInfo['PageTableOfContents']['Version'] = '2025-01-22'; $FmtPV['$PageTableOfContentsVersion'] = "'PageTableOfContents version {$RecipeInfo['PageTableOfContents']['Version']}'"; // return version as a custom page variable SDV($TocSize,'smaller'); SDV($TocFloat,false); $HTMLStylesFmt['toc'] = " span.anchor { float: left; font-size: 60%; margin-left: -1em; width: 1em; position:relative; top:-0.1em; text-align: center; } span.anchor a { text-decoration: none; } span.anchor a:hover { text-decoration: underline; } ol.toc { text-indent:-25px; list-style: none; margin-bottom:5px; } ol ol.toc { margin-left:-24px; text-indent:-24px; }"; $HTMLStylesFmt['tocf'] = " div.tocfloat { font-size: $TocSize; margin-bottom: 10px; border-top: 1px dotted #555555; border-bottom: 1px dotted #555555; padding-top: 5px; width: 38%; float: right; margin-left: 10px; clear: right; margin-right:-21px; padding-right: 13px; padding-left: 13px; background-color: #eeeeee; } div.toc { font-size: $TocSize; border: 1px dotted #cccccc; background: #f7f7f7; margin-bottom: 10px; } div.toc p { background-color: #f9f6d6; padding: 5px; border-bottom: 1px dotted #cccccc; }"; SDV($ToggleText, array('hide', 'show')); $HTMLHeaderFmt['toctoggle'] = ($action=="print" || $action=="publish") ? '' : "<script type=\"text/javascript\"> function toctoggle(obj, hide, show) { var elstyle = document.getElementById(obj).style; var text = document.getElementById(obj + \"tog\"); if(!hide) { var hide = \"{$ToggleText[0]}\"; } if(!show) { var show = \"{$ToggleText[1]}\"; } if (elstyle.display == 'none') { elstyle.display = 'block'; text.innerHTML = hide; } else { elstyle.display = 'none'; text.innerHTML = show; } } </script>"; ## section cross-references Markup('secref','>nl1','/(`)?(Sec|SEC)\\(([A-Za-z][-.:\w]*)\\)/', function ($m) use (&$pagename) { return CrossReferenceList($pagename,$m[1],$m[2],$m[3]); }); function CrossReferenceList($pagename,$prefix,$ref,$anchor) { global $ReferenceList, $format; $txt = PageCrossReference($pagename,$anchor); if ($format=='pdf') { $anch = "$pagename.$anchor"; $ReferenceList[$anch] = $txt; return "$prefix$ref( $anch )"; } return "[[#$anchor | $txt]]"; } ## in-page cross-references Markup('[[#|#','>nl1','/\[\[#([A-Za-z][-.:\w]*)\s*\|\s+#\]\]/', function ($m) use (&$pagename) { return '[[#'.$m[1].' | '.PageCrossReference($pagename,$m[1]).']]'; }); Markup('[[#|*','<[[|','/\[\[#([A-Za-z][-.:\w]*)\s*\|\s*\*\]\]/', '[[#$1 | $1]]'); Markup('[[#|+','<[[|','/\[\[#([A-Za-z][-.:\w]*)\s*\|\s*\+\]\]/', '[[#$1 | Back to $1]]'); Markup("[^#",'<[[#|#','/\[\^#([A-Za-z][-.:\w]*)\^\]/', function ($m) use (&$pagename) { return PageCrossReference($pagename,$m[1],false); }); # if markup.php is not loaded, we need a link cleaner SDV($LinkCleanerEnabled, false); if (!$LinkCleanerEnabled) { function isClosure($r) { return (is_object($r) && ($r instanceof Closure)); } function cleanLinkText($pagename, $text) { global $LinkCleaner; foreach ($LinkCleaner as $p => $r) { $text = isClosure($r) ? preg_replace_callback($p,$r,$text) : preg_replace($p,$r,$text); } return $text; } SDVA($LinkCleaner, array( '/`\..*?$/' => '...', "/\\{(\\$.*?)\\}/" => '$1', "/\\[\\[([^|\\]]+)\\|\\s*(.*?)\\]\\]($SuffixPattern)/" => function ($m) use (&$pagename) { return MakeLink($pagename,$m[1],$m[2],$m[3],'$LinkText'); }, "/\\[\\[([^\\]]+?)\\s*-+>\\s*(.*?)\\]\\]($SuffixPattern)/" => function ($m) use (&$pagename) { return MakeLink($pagename,$m[2],$m[1],$m[3],'$LinkText'); }, '/\\[\\[#([A-Za-z][-.:\\w]*)\\]\\]/' => "", "/\\[\\[(.*?)\\]\\]($SuffixPattern)/" => function ($m) use (&$pagename) { return MakeLink($pagename,$m[1],NULL,$m[2],'$LinkText'); }, '/[\\[\\{](.*?)\\|(.*?)[\\]\\}]/' => '$1', "/`(($GroupPattern([\\/.]))?($WikiWordPattern))/" => '$1', "/$GroupPattern\\/($WikiWordPattern)/" => '$1' )); } function CrossReference($pagename,$text,$anchor) { $r = Shortcut($text,$anchor); return trim(cleanLinkText($pagename,$r)); } function Shortcut($text,$anchor) { if (preg_match("/\\[\\[#+$anchor\\]\\]\\n?([^\n]+)/",$text,$match)) { return preg_replace("/^[#*!:]+\s*/","", preg_replace("/([^!]+)!.+/","$1",$match[1])); } else { return "<em>$anchor</em> not found"; } } ## inter-page cross-references Markup('[[?#|#','>nl1', "/\[\[((?:$GroupPattern)[.\/])?($NamePattern)#([A-Za-z][-.:\w]*)\s*\|\s+#\]\]/", function ($m) use (&$pagename) { return '[['.$m[1].$m[2].'#'.$m[3].' | '.PageCrossReference(MakePageName($pagename,$m[1].$m[2]),$m[3]).']]'; }); function PageCrossReference($page,$anchor,$clean=true) { global $PCache; if ($PCache[$page]['=preview']) $p['text'] = $PCache[$page]['=preview']; else $p = RetrieveAuthPage($page,'read',false,READPAGE_CURRENT); return $clean ? CrossReference($page,$p['text'],$anchor) : Shortcut($p['text'],$anchor); } ## [[##visibleanchor]] SDV($VisibleAnchor,'§'); SDV($VisibleAnchorLinks,false); SDV($DefaultTocAnchor,'toc'); $RefOrTitle = ($VisibleAnchorLinks) ? 'href' : 'title'; ## autonumber anchors Markup('^!#','<links','/^(!+|Q?:)#(#?)/',"TocAnchor"); function TocAnchor($m) { global $DefaultTocAnchor; static $toccounter; return $m[1]."[[#".$m[2].$DefaultTocAnchor. ++$toccounter ."]]"; } ## (:markup:) that excludes heading markup examples function hmarkupHelper($m) { $lead = $m[1]; $txta = str_replace('`.','',$m[3]); $txtb = $m[3]; return "$lead<:block>" . Keep("<table class='markup' align='center'><tr><td class='markup1'><pre>" . wordwrap($txta, 70) . "</pre></td></tr><tr><td class='markup2'>") . "\n$txtb\n(:divend:)</td></tr></table>\n"; } Markup('`markup','<markup', "/(^|\\(:nl:\\))\\(:markup:\\)[^\\S\n]*\\[([=@])((?:\n`\\.!+.*?)+)\\2\\]/sim", "hmarkupHelper"); ## page table of contents $IdPattern = "[A-Za-z][-.:\w]*"; SDV ($format, 'html'); if ($format=='pdf') { SDV($DefaultTocTitle,'Contents'); SDV($TocHeaderFmt, '[[#toc]]<tbook:visual markup="bf">$TocTitle</tbook:visual>'); SDV($RemoteTocFmt, '<tbook:visual markup="bf">Contents of [[$Toc(#toc)]]</tbook:visual>'); } else { SDV($DefaultTocTitle,'On this page...'); SDV($TocHeaderFmt,'[[#toc]]<b>$TocTitle</b>'); SDV($RemoteTocFmt,'<b>On page [[$Toc(#toc)]]...</b>'); } SDV($NumberToc,true); SDV($L1TocChar, '.'); SDV($OmitQMarkup,false); SDV($ToggleMarkup, true); if ($action=="print" || $action=="publish") { Markup('[[##','<[[#','/\[\[##([A-Za-z][-.:\w]*)\]\]/','[[#$1]]'); if ($action=='publish') Markup('toc','>include', '/\(:([#\*])?toc(?:-(float|hide))?(?:\s+anchors=(v)isible)?(?:\s+(.*?))?:\)/', ''); Markup('tocback','directives','/\(:toc-back(?:\s+(.*?))?:\)/',''); Markup('toggle', 'directives', '/\\(:toggle(\*)?\s*(.*?):\\)/', ''); } else { Markup('[[##','<[[#','/\[\[##([A-Za-z][-.:\w]*)\]\]/', "anchorHelper"); if ($ToggleMarkup) { Markup('toggle', 'directives','/^(.*?)\\(:toggle(\*)?\s*(.*?):\\)/', "toggleHelper"); Markup('toghide', '>style', '/<(?:div|table|[oud]l)\s*[^>]*id=["\']([^"\']+)[^>]*>/', function ($m) { return FmtToggleHide($m[0], $m[1]); }); } } Markup('toc','>[[#|#', '/\(:([#\*])?toc(?:-(float|hide))?(?:\s+anchors=(v)isible)?(?:\s+(.*?))?(?:\s+(Q))?:\)(.*)$/s', function ($m) use (&$pagename) { return TableOfContents($pagename,$m[1],$m[2],$m[4],$m[5],$m[6]). TocEntryAnchors($m[3],$m[6]); }); SDV($TocBackFmt,'↑ Contents'); Markup('tocback','directives','/\(:toc-back(?:\s+(.*?))?:\)/',"TocLinkText"); Markup('tocpage','directives','/\(:toc-page\s+(.*?)(?:\s+self=([01]))?:\)/', function ($m) use (&$pagename) { SDV($m[2], ''); return RemoteTableOfContents($pagename,$m[1],$m[2]); }); function anchorHelper($m) { global $RefOrTitle, $VisibleAnchor; return Keep("<span class='anchor'>". "<a name='".$m[1]."' id='".$m[1]."' $RefOrTitle='#".$m[1]."'>". "$VisibleAnchor</a></span>", 'L'); } function toggleHelper($m) { return ($m[1] ? $m[1] : '<:block>'). '<div class="tog">'.Keep(FmtToggleLinks($m[2], $m[3])).'</div>'; } function FmtToggleLinks($hidden, $opts) { global $ToggleText, $ToggleHideID; $opt = ParseArgs($opts); $hide = $opt['hide'] ? $opt['hide'] : $ToggleText[0]; $show = $opt['show'] ? $opt['show'] : $ToggleText[1]; if ($opt['id']) $id = $opt['id']; else return "(:toggle$hidden $opts :)"; $txt = $hidden ? $show : $hide; if ($hidden) $ToggleHideID[] = $id; if ($opt['button']) { $tag = 'button'; $att = 'onclick'; } else { $tag = 'a'; $att = 'href'; } return "<$tag id='$id"."tog' $att=\"javascript:toctoggle('$id','$hide','$show');\">$txt</$tag>"; } function FmtToggleHide($tag, $id) { global $ToggleHideID; foreach((array)$ToggleHideID as $i) if ($id==$i) if (preg_match('/\s+(style=["\'])[^"\']*(display:)?/', $tag, $m)) return $m[2] ? $tag : str_replace($m[1], $m[1].'display: none; ', $tag); else return str_replace('>', " style='display: none'>", $tag); return $tag; } function RemoteTableOfContents($pagename,$ref,$self=0) { global $TocHeaderFmt,$RemoteTocFmt; $oTocHeader = $TocHeaderFmt; $TocHeaderFmt = str_replace('$Toc',$ref,$RemoteTocFmt); $tocname = MakePageName($pagename,$ref); if ($tocname==$pagename && $self==0) return ''; $tocpage=RetrieveAuthPage($tocname,'read',false); $toctext=@$tocpage['text']; if (strpos($toctext, '(:nogroupheader:)')===false) $toctext = $GLOBALS['GroupHeaderFmt'].$toctext; if (preg_match('/\(:([#\*])?toc(?:-(float|hide))?(?:\s+anchors=(v)isible)?(?:\s+(.*?))?(?:\s+(Q))?:\)(.*)$/s',$toctext,$m)) $toc = str_replace('[[#',"[[$ref#", TableOfContents($tocname,$m[1],'page','',$m[5],PSS($m[6]))); else $toc = ''; $TocHeaderFmt = $oTocHeader; return $toc; } function TocLinkText($m) { global $TocBackFmt; SDV($m[1], ''); $text = $m[1]; if ($text) $TocBackFmt = $text; return '[[#toc | '.$TocBackFmt.']]'; } function TocEntryAnchors($visible,$text) { global $IdPattern; return preg_replace_callback( "/\n(!+|Q:)\s*((\[\[#+$IdPattern\]\])|##?)?/", function ($m) use ($visible) { SDV($m[2], ''); return "\n".$m[1].InsertAnchor($visible,$m[1],$m[2]); }, $text); } function InsertAnchor($visible,$h,$mark) { global $OmitQMarkup, $NumberToc, $L1TocChar; static $l1,$l2,$toc1,$toc2; if ($h=='Q:' && $OmitQMarkup) return $mark; if ($mark=='') $visibility = ($visible=='') ? '#' : '##'; else $visibility = $mark; if ($h=='Q:') return $visibility; $r = ''; $len = strlen($h); if ($l1==0) { $l1 = $len; } else if ($len!=$l1 && $l2==0) { $l2 = $len; } # if ($l1==$len || $l2==$len) $r = $visibility; if ($l1==$len) { $toc1++; $toc2 = 0; $r = $visibility; if ($NumberToc) $r .= "$toc1$L1TocChar  "; } elseif ($l2==$len) { $toc2++; $r = $visibility; if ($NumberToc) $r .= "$toc1.$toc2  "; } return $r; } function TableOfContents($pagename,$number,$float,$title,$includeq,$text) { global $DefaultTocTitle,$TocHeaderFmt,$IdPattern,$NumberToc,$OmitQMarkup, $format,$L1TocChar,$DefaultTocAnchor,$TocFloat,$HTMLHeaderFmt, $ToggleText; if ($includeq) $OmitQMarkup = (!$OmitQMarkup); if ($float=='float') $TocFloat = (!$TocFloat); $l1 = 0; $l2 = 0; $l3 = 0; $q = 0; $prelen = 1; $counter = 0; $r = ''; $toc1 = 0; $spacer = ' '; if (!$title) $title = $DefaultTocTitle; $toc = str_replace('$TocTitle',$title,$TocHeaderFmt); if ($number=='*') $NumberToc = false; elseif ($number=='#') $NumberToc = true; $closel = 0; if ($format=='pdf') { $l = 'tbook:item'; $s = ($NumberToc) ? 'tbook:enumerate' : 'tbook:itemize'; $sc = $s; $toc = "<tbook:group class='toc'><tbook:p>$toc</tbook:p>". "<$sc><$l>\$List</$l></$s></tbook:group>"; $flag = '!'; } elseif ($float=='hide') { return ''; } else { $tocid = ($float=='page') ? 'ptocid' : 'tocid'; // remote toc? $toggle = " (<a id=\"{$tocid}tog\" href=\"javascript:toctoggle('$tocid');\">{$ToggleText[0]}</a>)"; $l = 'li'; $s = ($NumberToc) ? 'ol' : 'ul'; $sc = "$s class='toc'"; $f = ($TocFloat) ? 'float' : ''; $toc = "<div class='toc$f'><p>$toc$toggle</p>" . "<$sc id='$tocid'><$l>\$List</$l></$s></div>"; $flag = ''; } preg_match_all("/\n(!+|Q?:)\s*(\[\[#+$IdPattern\]\]|#*)([^\n]*)/",$text, $match); for ($i=0;$i<count($match[0]);$i++) { if ($match[1][$i]==':' || ($match[1][$i]=='Q:' && $OmitQMarkup)) { if ($match[2][$i] && $match[2][$i][0]=='#') $counter++; continue; } $len = ($match[1][$i]=='Q:') ? 9 : strlen($match[1][$i]); if ($len==9) $q = 1; if ($l1==0) { $l1 = $len; $prelen = $l1; } if ($len!=$l1 && $l2==0) { $l2 = $len; } if ($len!=$l1 && $len!=$l2 && $q==1 && $l3==0) { $l3 = $len; } if ($len==$l2 && $l1==9) { $len = $l1; } if ($len==$l3) { $len = $l2; } if ($len!=$prelen) { if ($len==$l1) { $r .= "</$l></$s>"; } else if ($len==$l2) { $r .= "<$sc><$l>"; $toc2 = 0; $closel = 0; } } if ($len==$l1 || $len==$l2) { $prelen = $len; if ($len==$l1) { $toc1++; $tocout = ($NumberToc) ? "$toc1$L1TocChar" : ''; } else { $toc2++; $tocout = ($NumberToc) ? "$toc1.$toc2" : ''; } $tocout = ($format=='pdf') ? '' : ($NumberToc ? (($toc1<10) ? $spacer : '')."$tocout$spacer" : $tocout); $m = preg_replace("/^(\\[\\[#)#/","$1",$match[2][$i]); $m = preg_replace("/^#+/",'',$m); $t = preg_replace('/%(center|right)%/','',$match[3][$i]); if ($closel==1) $r .= "\n<:block></$l><$l>"; $closel = 1; if (strpos($m,'[#')==1) $r .= $tocout.str_replace(']]', ' | '.$flag.CrossReference($pagename,"$m$t", preg_replace("/\[\[#(.*?)\]\]/","$1",$m)).']]',$m); else { $counter++; $r .= $tocout."[[#$DefaultTocAnchor$counter | ".$flag. CrossReference($pagename,"[[#]]$t","").']]'; } } } if ($prelen==$l2) $r .= "</$l></$s>"; if ($r!='') $r = str_replace('$List',$r,$toc); return $r; } ?>