<?php if (!defined('PmWiki')) exit();
/*
    The pagetoc script adds support for automatically generating
    a table of contents for a wiki page.

    Version 2.0.31 (development version; works with PmWiki 2.1.0 or above)

    Copyright 2004, 2005 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.
*/
SDV($TocSize,'smaller');
SDV($TocFloat,false);
SDV($TocToggle,true);
SDV($ToggleText, array('hide', 'show'));
  
$HTMLHeaderFmt['pagetoc'] = "<link rel='stylesheet' type='text/css' href='$PubDirUrl/recipes/pagetoc/pagetoc.css' media='screen' title='style (screen)' />";
$HTMLHeaderFmt['toggle'] = "<script type='text/javascript' src='$PubDirUrl/recipes/pagetoc/pagetoc.js'></script>";

## in-page cross-references
Markup('[[#|#','>nl1','/\[\[#([A-Za-z][-.:\w]*)\s*\|\s*#\]\]/e',
    "'[[#$1 | '.CrossReference(\$pagename,\$x,'$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]*)\^\]/e',"Shortcut(\$x,'$1')");
SDVA($LinkCleanser, array(
    '/`\..*?$/' => '...',
    "/\\[\\[([^|\\]]+)\\|\\s*(.*?)\\]\\]($SuffixPattern)/e" =>
            "MakeLink(\$pagename,PSS('$1'),PSS('$2'),'$3','\$LinkText')",
    "/\\[\\[([^\\]]+?)\\s*-+&gt;\\s*(.*?)\\]\\]($SuffixPattern)/e" =>
            "MakeLink(\$pagename,PSS('$2'),PSS('$1'),'$3','\$LinkText')",
    '/\\[\\[#([A-Za-z][-.:\\w]*)\\]\\]/' => "",
    "/\\[\\[(.*?)\\]\\]($SuffixPattern)/e" =>
            "MakeLink(\$pagename,PSS('$1'),NULL,'$2','\$LinkText')",
    '/[\\[\\{](.*?)\\|(.*?)[\\]\\}]/' => '$1',
    "/`(($GroupPattern([\\/.]))?($WikiWordPattern))/" => '$1',
    "/$GroupPattern\\/($WikiWordPattern)/" => '$1'
            ));

function CrossReference($pagename,$text,$anchor) {
  global $LinkCleanser;
  $r = Shortcut($text,$anchor);
  foreach ($LinkCleanser as $p => $c) $r = preg_replace($p,$c,$r);
  return $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";
  }
}

## [[##visibleanchor]]
SDV($VisibleAnchor,'&sect;');
SDV($VisibleAnchorLinks,false);
SDV($DefaultTocAnchor,'toc');
$RefOrTitle = ($VisibleAnchorLinks) ? 'href' : 'title';

## autonumber anchors
Markup('^!#','<links','/^(!+|Q?:)#(#?)/e',"'$1'.TocAnchor('$2')");
   
function TocAnchor($visible) {
  global $DefaultTocAnchor;
  static $toccounter;
  return "[[$visible#$DefaultTocAnchor" . ++$toccounter . "]]";
}

## (:markup:) that excludes heading markup examples
function HMarkupMarkup($pagename, $lead, $texta, $textb) {
  return "$lead<:block>" .
    Keep("<table class='markup' align='center'><tr><td class='markup1'><pre>" .
      wordwrap($texta, 70) .  "</pre></td></tr><tr><td class='markup2'>") .
    "\n$textb\n(:divend:)</td></tr></table>\n";
}

Markup('`markup','<markup',
  "/(^|\\(:nl:\\))\\(:markup:\\)[^\\S\n]*\\[([=@])((?:\n`\\.!+.*?)+)\\2\\]/seim",
  "HMarkupMarkup(\$pagename, '$1', PSS(str_replace('`.','','$3')), PSS('$3'))");
#Markup("`.",'>links',"/`\./",''); ## included in extendmarkup.php

## page table of contents
$IdPattern = "[A-Za-z][-.:\w]*";
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($MaxTocDepth,2);

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+(.*?))?:\)/','');
} else {
    Markup('[[##','<[[#','/\[\[##([A-Za-z][-.:\w]*)\]\]/e',
    "Keep(\"<span class='anchor'><a name='$1' id='$1' $RefOrTitle='#$1'>$VisibleAnchor</a></span>\",
    'L')");
}
Markup('toc','>nl1',
    '/\(:([#\*])?toc(?:-(float|hide))?(?:\s+anchors=(v)isible)?(?:\s+(.*?))?(?:\s+(Q))?:\)(.*)$/se', 
    "TableOfContents(\$pagename,'$1','$2',PSS('$4'),'$5',PSS('$6')).
    TocEntryAnchors('$3',PSS('$6'))");
SDV($TocBackFmt,'&uarr; Contents');
Markup('tocback','directives','/\(:toc-back(?:\s+(.*?))?:\)/e',
    "'[[#toc | '.TocLinkText(PSS('$1')).']]'");
Markup('tocpage','directives','/\(:toc-page\s+(.*?)(?:\s+self=([01]))?:\)/e',
    "RemoteTableOfContents(\$pagename,'$1','$2')");

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 (preg_match('/\(:([#\*])?toc(?:-(float|hide))?(?:\s+anchors=(v)isible)?(?:\s+(.*?))?(?:\s+(Q))?:\)(.*)$/se',$toctext,$m))
        $toc = str_replace('[[#',"[[$ref#",
            TableOfContents($tocname,$m[1],'page','',$m[5],PSS($m[6])));
    $TocHeaderFmt = $oTocHeader;
    return $toc;
}

function TocLinkText($text) {
    global $TocBackFmt;
    if ($text) $TocBackFmt = $text;
    return $TocBackFmt;
}

function TocEntryAnchors($visible,$text) {
    global $IdPattern;
    global $NumberToc;
    $outline   = CreateOutline($text);
    $indextree = $outline[0];
    $records   = $outline[1];
    foreach ($records as $record) {
        // chapter number, heading text, heading level, real level, wiki reference, matched expr, anchor id
        $chapternum  = $record[0];
        $headingtext = $record[1];
        $level       = $record[3];
        $matchedexpr = str_replace("\n", "", $record[5]);
        $levelstr    = str_pad("", $level, "!");
        if ($NumberToc) {
            $replacement = sprintf("%s#%s&ensp; %s", $levelstr, $chapternum, $headingtext);
        } else {
            $replacement = sprintf("%s%s", $levelstr, $headingtext);
        }
        $text = str_replace($matchedexpr, $replacement, $text);
    }
    return $text;
}

/**
 * Explode any single-dimensional array into a full blown tree structure,
 * based on the delimiters found in it's keys.
 *
 * @author  Kevin van Zonneveld <kevin@vanzonneveld.net>
 * @author  Lachlan Donald
 * @author  Takkie
 * @copyright 2008 Kevin van Zonneveld (http://kevin.vanzonneveld.net)
 * @license   http://www.opensource.org/licenses/bsd-license.php New BSD Licence
 * @version   SVN: Release: $Id: explodeTree.inc.php 89 2008-09-05 20:52:48Z kevin $
 * @link    http://kevin.vanzonneveld.net/
 *
 * @param array   $array
 * @param string  $delimiter
 * @param boolean $baseval
 *
 * @return array
 */
function explodeTree($array, $delimiter = '_', $baseval = false)
{
  if(!is_array($array)) return false;
  $splitRE   = '/' . preg_quote($delimiter, '/') . '/';
  $returnArr = array();
  foreach ($array as $key => $val) {
    // Get parent parts and the current leaf
    $parts  = preg_split($splitRE, $key, -1, PREG_SPLIT_NO_EMPTY);
    $leafPart = array_pop($parts);
 
    // Build parent structure
    // Might be slow for really deep and large structures
    $parentArr = &$returnArr;
    foreach ($parts as $part) {
      if (!isset($parentArr[$part])) {
        $parentArr[$part] = array();
      } elseif (!is_array($parentArr[$part])) {
        if ($baseval) {
          $parentArr[$part] = array('__base_val' => $parentArr[$part]);
        } else {
          $parentArr[$part] = array();
        }
      }
      $parentArr = &$parentArr[$part];
    }
 
    // Add the final part to the structure
    if (empty($parentArr[$leafPart])) {
      $parentArr[$leafPart] = $val;
    } elseif ($baseval && is_array($parentArr[$leafPart])) {
      $parentArr[$leafPart]['__base_val'] = $val;
    }
  }
  return $returnArr;
}

/**
 * Calculates the chapter numbers by the location of the TOC entry within the
 * tree. This means that it doesn't necessarily reflect the heading information.
 * To give a short example:
 *
 *     !A
 *     !!!!!B
 *     !!C
 *
 * This would cause the following chapter numbers:
 *
 *      1. A
 *      1.1. B
 *      1.2. C
 *
 * As you can see B and C are on the same level here. We can't "guess" a parent
 * for C here as there's no second level element. Nevertheless this is a really
 * special case which simply is bad style by the wiki writer.
 *
 * @param $arr      The tree which contains the outline with the indices.
 * @param $values   The list with all records.
 * @param $prefix   A prefix used to easily calculate the chapter numbers.
 * @param $level    The current level.
 */
function CalculateChapters( $arr, &$values, $prefix = "", $level = 1 ) {
    global $L1TocChar;
    
    $count = 1;
    foreach($arr as $k=>$v){

        if (strlen($prefix) == 0) {
            $chapter = sprintf("%d", $count);
        } else {
            $chapter = sprintf("%s%s%d", $prefix, $L1TocChar, $count);
        }

        // skip the baseval thingy. Not a real node.
        if($k == "__base_val") {
            continue;
        }

        $index     = ( is_array($v) ? $v["__base_val"] : $v );
        $record    = &$values[$index];
        $record[0] = $chapter;
        $record[3] = $level;
 
        if(is_array($v)){
            // this is what makes it recursive, rerun for childs
            CalculateChapters($v, $values, $chapter, $level + 1);
        }

        $count++;

    }
}

/**
 * Stupid helper function which combines the both path segments.
 *
 * @todo [01-Jun-2011:KASI]   I suspect that there's a better way to do this concatenation (see 'reduce' below)
 */
function jjoin($a, $b) {
    if (strlen($a) == 0) {
        return $b;
    } elseif (strlen($b) == 0) {
        return $a;
    } else {
        return $a . "/" . $b;
    }
}

/**
 * This function basically creates the outline. The outline is a tree structure where each node
 * contains an index to a list. The index points to a record with the corresponding TOC entry
 * data (this data isn't stored directly within the tree as using arrays as node values would
 * make the iteration somewhat more complicated since an array is used to identify a parental
 * node).
 *
 * Anyway this function works in 3 steps:
 *
 *   1. Create an array which maps (path -> index). The list will be filled with values
 *      accordingly.
 *   2. Translated this array into a tree structure.
 *   3. Run a postprocess which writes the "chapter" numbers to the records.
 *
 * @param $text   The input text which needs to be processed.
 *
 * @return   A pair which consists of the tree and the list with the reords. 
 */
function CreateOutline( $text ) {

    global $IdPattern,$DefaultTocAnchor,$OmitQMarkup;

    preg_match_all( "/\n(!+|Q?:)\s*(\[\[#+$IdPattern\]\]|#*)([^\n]*)/", $text, $match );

    $counter   = 0;
    $pathlist  = array();
    $list      = array();
    $stack     = array();
    $lastlevel = 0;

    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; 
        }

        $idpattern = preg_replace( "/^(\\[\\[#)#/", "$1", trim( $match[2][$i] ) );
        $idpattern = preg_replace( "/^#+/",'', $idpattern );
        $t         = preg_replace( '/%(center|right)%/','', $match[3][$i] );

        // get the urrent level
        $level     = strlen( $match[1][$i] );

        if( $level <= $lastlevel ) {
            // we've moved to a higher level, so drop all irrelevant
            // path segments from our stack
            $count = $lastlevel - $level + 1;
            while( $count > 0 ) {
                array_pop( $stack );
                $count--;
            }
        } else if( $level > ($lastlevel + 1) ) {
            // we've moved to a lower level, so we might need to push
            // some dummy elements (usually this won't happen if the
            // user hasn't written crap-like wiki text.
            $count = $level - $lastlevel - 2;
            while( $count > 0 ) {
                array_push( $stack, "" );
                $count--;
            }
        }

        $lastlevel = $level;

        // push the current heading text as a path segment onto the stack
        array_push( $stack, $t );

        // calculate the anchorid and the wiki reference
        $id = "";
        if( strpos($idpattern,'[#') == 1 ) {
            $id  = str_replace( '[', '', str_replace( ']]', '', $idpattern ) );
            $ref = str_replace( ']]', ' | ' .  CrossReference( $pagename, "$idpattern$t", preg_replace( "/\[\[#(.*?)\]\]/","$1",$idpattern)) . ']]', $idpattern );
        } else {
            $counter++;
            $id  = "#$DefaultTocAnchor$counter";
            $ref = "[[$id | " . CrossReference( $pagename, "[[#]]$t", "" ) . ']]';
        }

        // create a treepath from the current stack state
        $treepath              = array_reduce($stack, "jjoin", "");
 
        // get the index for the treenode and the list which contains the data
        $index                 = count( $pathlist );
        // chapter number, heading text, heading level, real leve, wiki reference, matched expr, anchor id
        $list    [ $index    ] = array( "", $t, $level, 0, $ref, $match[0][$i], $id );
        $pathlist[ $treepath ] = $index;

    }

    // now transform the simple list of (path->index) into an apropriate tree which
    // will represent the outline
    $indextree = explodeTree( $pathlist, '/', true );
 
    // calculate chapter numbers and save them
    CalculateChapters( $indextree, $list );

    // return the outline information
    return array( $indextree, $list );
}

/**
 * This function generates the TOC based upon the supplied outline data.
 *
 * @param $tree               The tree structure representing the outline. The nodes only contain indices to the records list.
 * @param $records            The list with the necessary outline information.
 * @param $listitemelement    The tag for list elements.
 * @param $listopenelement    The opener for list elements.
 * @param $listcloseelement   The closer for list elements.
 * @param $level              The current level of the TOC.
 *
 * @return   The chained TOC lists.
 */
function GenerateToc( $tree, $records, $listitemelement, $listopenelement, $listcloseelement, $level = 0 ) {
    
    global $MaxTocDepth, $NumberToc;

    // check whether the user specified a maximum depth, so we won't iterate any deeper level
    if( $MaxTocDepth > 0 ) {
        if( $level > $MaxTocDepth ) {
            return "";
        }
    }

    $result = sprintf( "<%s>", $listopenelement );
    foreach( $tree as $k=>$v ){

        // skip the baseval thingy. Not a real node.
        if( $k == "__base_val" ) {
            continue;
        }

        // get the index which is stored within the tree
        $index     = ( is_array($v) ? $v["__base_val"] : $v );

        // get the record data
        $record    = $records[$index];

        $chapter   = $record[0];      // chapter number
        $text      = $record[1];      // heading text
        $heading   = $record[2];      // heading level (f.e. !! = 2)
        $reallevel = $record[3];      // heading level (f.e. !! = 2); fixed in case the levels within the text was invalid
        $reference = $record[4];      // wiki reference (f.e. [[#bibo | Bibo]])
        $complete  = $record[5];      // the completely matched expression within the source
        $anchorid  = $record[6];      // the anchor ID (f.e. bibo)

        $sublist   = "";
        $numbering = "";
        if( $NumberToc ) {
            $numbering = $chapter . "&ensp;";
        }

        if( is_array($v) ) {
            // we've got child elements, so generate a contained list here
            $sublist = "\n".GenerateToc($v, $records, $listitemelement, $listopenelement, $listcloseelement, $level + 1);
        }

        // create the link for this current TOC entry
        $result .= sprintf("<%s>%s%s%s</%s>\n", $listitemelement, $numbering, $reference, $sublist, $listitemelement);

    }
    $result .= sprintf( "</%s>", $listcloseelement );
    return $result;
}

/**
 * This function is responsible for the creation of the TOC which is basically a chained list.
 * 
 * @param $pagename   The name of the page which has to be used as the input for the TOC.
 * @param $number     Either '*' or '#' in case the $NumberToc parameter shall be overridden.
 * @param $float      Render the TOC in a floated environment.
 * @param $title      The title to be used for the head of the TOC.
 * @param $includeq   Support Q Markup (???)
 * @param $text       The input text of the page.
 *
 * @return   The chained TOC lists.
 */
function TableOfContents($pagename,$number,$float,$title,$includeq,$text) {
    global $DefaultTocTitle,$TocHeaderFmt,$IdPattern,$NumberToc,$OmitQMarkup,
        $format,$L1TocChar,$DefaultTocAnchor,$TocFloat,$TocToggle,$HTMLHeaderFmt,
        $ToggleText;

    if( $includeq ) {
        $OmitQMarkup = (!$OmitQMarkup);
    }
    if( $float == 'float' ) {
        $TocFloat = (!$TocFloat);
    }
    if( ! $title ) {
        $title = $DefaultTocTitle;
    }
    $toc = str_replace('$TocTitle',$title,$TocHeaderFmt);
    if( $number=='*' ) {
        $NumberToc = false;
    } elseif( $number=='#' ) {
        $NumberToc = true;
    }

    // make some preparations for the elements that will be used for format specific output
    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>";
    } elseif( $float=='hide' ) { 
        return '';
    } else {
        $tocid  = ($float=='page') ? 'ptocid' : 'tocid';  // remote toc?
        $toggle = " (<a id=\"{$tocid}tog\" href=\"javascript:pagetoc_toggle('$tocid','{$ToggleText[0]}','{$ToggleText[1]}');\">{$ToggleText[0]}</a>)";
        $l      = 'li'; 
        $s      = ($NumberToc) ? 'ol' : 'ul'; 
        $sc     = "$s class='toc'";
        $f      = ($TocFloat) ? 'float' : '';
        if( $TocToggle ) {
            $toc    = "<div class='toc$f'><p>$toc$toggle</p>" . "<$sc id='$tocid'><$l>\$List</$l></$s></div>";
        } else {
            $toc    = "<div class='toc$f'><$sc id='$tocid'><$l>\$List</$l></$s></div>";
        }
    }

    // calculate the outline in order to generate the toc
    $outline   = CreateOutline($text);
    $indextree = $outline[0];
    $records   = $outline[1];
    $r         = GenerateToc($indextree, $records, $l, $sc, $s);

    if ($r!='') {
        // insert the toc
        $r = str_replace('$List',$r,$toc);
    }
    return $r;
}

?>