<?php if (!defined('PmWiki')) exit();

/*****************************************************************\
* emenu2.php - intelligent expanding menu's for pmwiki            *
* Copyright (C) 2007  Pieter Wuille                               *
* Some of this code is based on emenu.php by Douglas Stebila.     *
*                                                                 *
* 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 plugin provides expanding menu's for use in a sidebar eg.
 * It  will automatically expand the tree to show the currently visible page,
 * but expansion of any nodes can be controlled through markup.
 *
 * Expandable items will get a '+' prefix, while expanded items will get a
 * '-' prefix. In case these items are WikiLinks, this + or - will be added
 * to the visible part of the link, making it clickable and compatible with
 * the gemini/fixflow skins, which put all links in the sidebar on their own
 * line. There is no limit on the number of indentation levels.
 *
 * A menu is written by using a normal list (using '*'s for indentation)
 * enclosed within (:emenu2:) ... (:emenu2end:) directives.
 * A menu item can be made visible/expanded by writing
 * (:emenu2expand Group.Page:). (:emenu2collapse:) can revert this. The current
 * page is made expanded/visible by default, but this can be reverted with
 * (:emenu2collapse {$FullName}).
 *
 * This plugin is somewhat like TreeMenu (but completely server-side, without
 * javascript, and somewhat like ExpandingMenu (but more intelligent and
 * flexible). It replaces this last one.
 *
 */

$RecipeInfo['ExpandingMenu2']['Version'] = '2007-08-04';

# expand and collapse markup
Markup ('emenu2exp', 'fulltext', '/\\(:emenu2expand ([^:]*):\\)/',"emenuexp_e");

function emenu2exp_e($m) {
  return eMenuExpand("$m[1]"),1);
}

Markup ('emenu2col', '>emenu2exp', '/\\(:emenu2collapse ([^:]*):\\)/',"emenu2col_e");

function emenu2col_e($m) {
  return eMenuExpand("$m[1]"),-1);
}

# handle emenu's
MarkUp ('emenu2', '<links', '/^\\(:(emenu2|emenu2end):\\)/e',"emenu2_e");

function emenu2_e($m) {
  extract($GLOBALS['MarkupToHTML']);
  return eMenu("$m[1]",'$pagename');
}

MarkUp ('emenu2line', '>emenu2', '/^(.*)/e',"emenu2line_e");

function emenu2line_e($m) {
  return eMenuLine("$m[1]");
}

# handle (:emenu2:) ... (:emenu2end:)
# stores lines in a global variable, and pass these lines to eMenuConvert in the end
function eMenu($pos,$pagename) {
  global $eMenuActive,$eMenuLines;
  if ($pos == 'emenu2') { /* begin */
    if (!$eMenuActive) {
      $eMenuActive=1;
      $eMenuLines=array();
      return;
    }
  }
  if ($pos == 'emenu2end') { /* end */
    if ($eMenuActive) {
      $eMenuActive=0;
      return "<:block><div class='emenu'>".eMenuConvert($eMenuLines,$pagename)."<:block></div>";
    }
  }
  return "<:block>";
}

# add line to global variable, or pass-through if not within a (:emenu2:) construct
function eMenuLine($line) {
  global $eMenuActive,$eMenuLines;
  if ($eMenuActive) {
    array_push($eMenuLines,$line);
  } else { /* pass through */
    return $line;
  }
}

# handle (:emenu2expand:) or (:emenu2collapse:)
# a (:emenu2expand [pagename]:) will cause this page's fullname to be stored
# in a associative array. The value associated with that key is the difference
# between the number of expands and the number of collapses. If this number
# is strictly positive, nodes referring to that page will always be visible
# and expanded.
function eMenuExpand($links,$mode) {
  global $eMenuExpand;
  foreach (split('/ +/',$links) as $link) { /* split in links */
    $pn=ResolvePageName($link); /* resolve them */
    $fn=PageVar($pn,'$FullName'); /* find fullname */
    $eMenuExpand[$fn]+=$mode; /* and change that page's expansion mode */
  }
}

# split code in links and non-links
# the result is an array, containing
#   - array($code, $target, $view) for links
#   - array($code, NULL, NULL) for other parts
function eMenuParseLinks($code,$group) {
  $ret = array();
  $pos=0;
  preg_match_all("/\\[\\[([^\\]]+)\\]\\]/",$code,$matches,PREG_SET_ORDER | PREG_OFFSET_CAPTURE); /* find all links */
  foreach ($matches as $link) { /* loop over all links */
    if ($link[0][1]>$pos) array_push($ret,array(substr($code,$pos,$link[0][1]-$pos),NULL,NULL)); /* add skipped non-link part to result */
    $pos=$link[0][1]+strlen($link[0][0]); /* end position of current link */
    $view=$link[1][0];
    $l=str_replace("~","Profile/",$view); /* handle [[~name]] links */
    $l=str_replace("!","Category/",$l); /* handle [[!category]] links */
    $l=str_replace("(","",$l); /* handle [[(foo.)bar]] links */
    $l=str_replace(")","",$l); 
    $l=str_replace(" ","",$l); /* remove spaces */
    preg_match("/^([^\\.:\\/]*?:)?(?:([^\\.:\\/]*?)([\\.\\/]))?([^\\.:\\/]*?)(?:\\|(.*))?\$/",$l,$lmatch); /* parse link (1:map, 2:group, 3:separator, 4:page, 5:view) */
    if ($lmatch[3]=='/' && $lmatch[1]=='') $view=$lmatch[4];
    $view=preg_replace("/\\(.*?\\)/","",$view); /* remove ( ) from view */
    if ($lmatch[5]!='') $view=$lmatch[5];
    if ($lmatch[1]=='') {
      if ($lmatch[2]=='') $lmatch[2]=$group;
      array_push($ret,array($link[0][0],$lmatch[2].'.'.$lmatch[4],$view));
    } else {
      array_push($ret,array($link[0][0],$lmatch[1].$lmatch[2].'.'.$lmatch[4],$view));
    }
  }
  if (strlen($code)>$pos) array_push($ret,array(substr($code,$pos),NULL,NULL)); /* add final non-link part */
  return $ret;
}

# transform a parse-array (as returned by eMenuParseLinks) back into wiki code
# it is possible to give a prefix, which will be prefixed to the first piece
# in the parse-array. In case this first piece is a link, it will be prefixed
# to the links's visible part ($view)
function eMenuGenLine($parsed,$prefix) {
  $ret='';
  foreach ($parsed as $parse) {
    if ($prefix == '') { 
      $ret .= $parse[0];
    } else {
      if (isset($parse[1])) {
        $ret .= "[[$parse[1]|$prefix$parse[2]]]";
      } else {
        $ret .= "$prefix$parse[0]";
      }
      $prefix='';
    }
  }
  return $ret;
}

# take a number of lines (as given between (:emenu2:) and (:emenu2end:)) and
# drop some of them. 
function eMenuConvert($lines,$pagename) {
  global $eMenuExpand,$eMenuCurrentExpanded;
  $tree = array(); /* internal tree representation; is in array of entries: array($parse,$parent or -1,$expanded,$children,$level) */
  $mapje = array(); /* stack that gives index of all ancestors (maps level to index) */
  $pos = 0;
  $maxlev = 0; /* valid entries in $mapje */
  if (!isset($eMenuCurrentExpanded)) {
    $eMenuCurrentExpanded=1;
    eMenuExpand($pagename,1); /* expand currently viewed page */
  }
  foreach ($lines as $line) {
    $level=strspn($line,"*")+1;
    $spaces=strspn(substr($line,$level-1)," ");
    $parent = -1;
    $lind=substr($line,$level+$spaces-1); /* take visible part of line (no *** and whitespace) */
    if ($level<=$maxlev+1 && isset($mapje[$level-1])) $parent=$mapje[$level-1]; /* find parent */
    $tree[$parent][3]=1; /* out parent has child now */
    $mapje[$level]=$pos; /* we can become ancestor */
    $maxlev=$level;
    $parsed=eMenuParseLinks($lind,$group); /* parse entry itself */
    $tree[$pos] = array($parsed,$parent,0,0,$level-1); /* store new value in tree */
    foreach ($parsed as $link) { /* loop over all parsed parts */
      if (isset($link[1]) && isset($eMenuExpand[$link[1]]) && $eMenuExpand[$link[1]]>0) { /* check if it is a link, and needs expanding */
        $loop=$pos;
        while ($loop>=0) { /* loop over all ancestors */
          $tree[$loop][2]=1; /* make them expanded */
          $loop=$tree[$loop][1]; /* go level up */
        }
        break; /* node already expanded, don't need to check other links anymore */
      }
    }
    $pos++;
  }
  $ret = "";
  foreach ($tree as $line) { /* now loop over tree to see what needs showing */
    if ($line[1]==-1 || $tree[$line[1]][2]==1) { /* if entry has no parent, or parent is expanded */
      $ret .= str_repeat('*',$line[4]); /* put ***'s */
      $pre = ''; /* normally no prefix */
      if ($line[3]==1 && $line[2]==1) $pre="'''-''' "; /* unless children & expanded: - */
      if ($line[3]==1 && $line[2]==0) $pre="'''+''' "; /* unless children & !expanded: + */
      $ret .= eMenuGenLine($line[0],$pre)."\n"; /* generate menu line */
    }
  }
  PRR(); /* instruct pmwiki to reprocess our output (as it contains newline, requires re-splitting, ...) */
  return $ret;
}

?>