Extension:TreeAndMenu

From Organic Design wiki
Revision as of 10:58, 17 April 2008 by Nad (talk | contribs) (add odd and even classes to allow stripes)

<?php /**

Info.svg These are the MediaWiki extensions we're using and/or developing. Please refer to the information on the mediawiki.org wiki for installation and usage details. Extensions here which have no corresponding mediawiki article are either not ready for use or have been superseded. You can also browse our extension code in our local Subversion repository or our GitHub mirror.

if (!defined('MEDIAWIKI')) die('Not an entry point.');

define('TREEANDMENU_VERSION','1.0.0, 2008-04-17');

  1. Set any unset images to default titles

if (!isset($wgTreeViewImages) || !is_array($wgTreeViewImages)) $wgTreeViewImages = array();

$wgTreeMagic = "tree"; # the parser-function name for trees $wgMenuMagic = "menu"; # the parser-function name for dropdown menus $wgTreeViewShowLines = false; # whether to render the dotted lines joining nodes $wgExtensionFunctions[] = 'wfSetupTreeAndMenu'; $wgHooks['LanguageGetMagic'][] = 'wfTreeAndMenuLanguageGetMagic';

$wgExtensionCredits['parserhook'][] = array( 'name' => 'TreeAndMenu', 'author' => 'Nad, Sven', 'url' => 'http://www.mediawiki.org/wiki/Extension:Treeview', 'description' => 'Adds #tree and #menu parser functions which contain bullet-lists to be rendered as collapsible treeview\'s or dropdown menus. The treeview\'s use the dTree JavaScript tree menu, and the dropdown menu\'s use Son of Suckerfish', 'version' => TREEANDMENU_VERSION );

class TreeAndMenu {

var $version = TREEANDMENU_VERSION; var $uniq = ; # uniq part of all tree id's var $uniqname = 'tam'; # input name for uniqid var $id = ; # id for specific tree var $baseDir = ; # internal absolute path to treeview directory var $baseUrl = ; # external URL to treeview directory (relative to domain) var $images = ; # internal JS to update dTree images var $useLines = true; # internal variable determining whether to render connector lines var $args = array(); # args for each tree


/** * Constructor */ function __construct() { global $wgOut,$wgHooks,$wgParser,$wgScriptPath,$wgJsMimeType, $wgTreeMagic,$wgMenuMagic,$wgTreeViewImages,$wgTreeViewShowLines;

# Add hooks $wgParser->setFunctionHook($wgTreeMagic,array($this,'expandTree')); $wgParser->setFunctionHook($wgMenuMagic,array($this,'expandMenu')); $wgHooks['ParserAfterTidy'][] = array($this,'renderTreeAndMenu');

# Update general tree paths and properties $this->baseDir = dirname(__FILE__); $this->baseUrl = preg_replace('|^.+(?=[/\\\\]extensions)|',$wgScriptPath,$this->baseDir); $this->useLines = $wgTreeViewShowLines ? 'true' : 'false'; $this->uniq = uniqid($this->uniqname);

# Convert image titles to file paths and store as JS to update dTree foreach ($wgTreeViewImages as $k => $v) { $title = Title::newFromText($v,NS_IMAGE); $image = Image::newFromTitle($title); $v = $image && $image->exists() ? $image->getURL() : $wgTreeViewImages[$k]; $this->images .= "tree.icon['$k'] = '$v';"; }

# Add link to output to load dtree.js script $wgOut->addScript("<script type=\"$wgJsMimeType\" src=\"{$this->baseUrl}/dtree.js\"></script>\n"); }


/** * Expand #tree parser-functions */ public function expandTree() { $args = func_get_args(); return $this->expandTreeAndMenu('tree',$args); }

/** * Expand #menu parser-functions */ public function expandMenu() { $args = func_get_args(); return $this->expandTreeAndMenu('menu',$args); }

/** * Expand either kind of parser-function (reformats tree rows for matching later) and store args */ private function expandTreeAndMenu($magic,$args) { $parser = array_shift($args);

# Store args for this tree for later use foreach ($args as $arg) if (preg_match('/^(\\w+?)\\s*=\\s*(.+)$/s',$arg,$m)) $args[$m[1]] = $m[2]; else $text = $arg;

# Create a unique id for this tree or use id supplied in args and store args wrt id $this->id = isset($args['id']) ? $args['id'] : uniqid(); $args['type'] = $magic; $this->args[$this->id] = $args;

# Reformat tree rows for matching in ParserAfterStrip $text = preg_replace('/(?<=\\*)\\s*\\[\\[Image:(.+?)\\]\\]/',"{$this->uniq}3$1{$this->uniq}4",$text); $text = preg_replace_callback('/^(\\*+)(.*?)$/m',array($this,'formatRow'),$text);

return $text; }


/** * Reformat tree bullet structure recording row, depth and id in a format which is not altered by wiki-parsing * - format is: 1{uniq}-{id}-{depth}-{item}-2{uniq} * - sequences of this format will be matched in ParserAfterTidy and converted into dTree JavaScript * - NOTE: we can't encode a unique row-id because if the same tree instranscluded twice a cached version * may be used (even if parser-cache disabled) this also means that tree id's may be repeated */ private function formatRow($m) { return "\x7f1{$this->uniq}\x7f{$this->id}\x7f".(strlen($m[1])-1)."\x7f$m[2]\x7f2{$this->uniq}"; }


/** * Called after parser has finished (ParserAfterTidy) so all transcluded parts can be assembled into final trees */ public function renderTreeAndMenu(&$parser,&$text) { global $wgJsMimeType; $u = $this->uniq;

# Determine which trees are sub trees # - there should be a more robust way to do this, # it's just based on the fact that all sub-tree's have a minus preceding their row data if (!preg_match_all("/\x7f\x7f1$u\x7f(.+?)\x7f/",$text,$subs)) $subs = array(1 => array());

# Extract all the formatted tree rows in the page and if any, replace with dTree JavaScript if (preg_match_all("/\x7f1$u\x7f(.+?)\x7f([0-9]+)\x7f({$u}3(.+?){$u}4)?(.*?)(?=\x7f[12]$u)/",$text,$matches,PREG_SET_ORDER)) {

# PASS-1: build $rows array containing depth, and tree start/end information $rows = array(); $depths = array( => 0); # depth of each tree root $rootId = ; # the id of the current root-tree (used as tree id in PASS2) $lastId = ; $lastDepth = 0; foreach ($matches as $match) { list(,$id,$depth,,$icon,$item) = $match; $start = false; if ($id != $lastId) { if (!isset($depths[$id])) $depths[$id] = $depths[$lastId]+$lastDepth; if ($start = $rootId != $id && !in_array($id,$subs[1])) $depths[$rootId = $id] = 0; } if ($item) $rows[] = array($rootId,$depth+$depths[$id],$icon,$item,$start); $lastId = $id; $lastDepth = $depth; }

# PASS-2: build the JavaScript and replace into $text $parents = array(); # parent node for each depth $parity = array(); # keep track of odd/even rows for each depth $last = -1; $nodes = ; foreach ($rows as $node => $info) { $node++; list($id,$depth,$icon,$item,$start) = $info; $args = $this->args[$id]; $type = $args['type']; if (!isset($args['root'])) $args['root'] = ; # tmp - need to handle rootless trees $end = $node == count($rows) || $rows[$node][4];

# Append node script for this row if ($depth > $last) $parents[$depth] = $node-1; $parent = $parents[$depth]; if ($type == 'tree') $nodes .= "{$this->uniqname}$id.add($node,$parent,'".addslashes($item)."');\n"; else { if (!$start) {

if ($depth < $last) $nodes .= str_repeat('',$last-$depth); elseif ($depth > $last) $nodes .= "\n

    "; } $parity[$depth] = isset($parity[$depth]) ? $parity[$depth]^1 : 0; $class = $parity[$depth] ? 'odd' : 'even'; $nodes .= "
  • $item"; if ($depth >= $rows[$node][1]) $nodes .= "
  • \n";

    } $last = $depth;

    # Last row of current root, surround nodes dtree or menu script and div etc if ($end) { $class = isset($args['class']) ? $args['class'] : "d$type"; if ($type == 'tree') {

    # Finalise a tree $add = isset($args['root']) ? "tree.add(0,-1,'".$args['root']."');" : ; $top = $bottom = $root = ; foreach ($args as $arg => $pos) if (($pos == 'top' || $pos == 'bottom' || $pos == 'root') && ($arg == 'open' || $arg == 'close')) $$pos .= "<a href=\"javascript: {$this->uniqname}$id.{$arg}All();\"> {$arg} all</a> ";

    if ($top) $top = "

     $top

    "; if ($bottom) $bottom = "

     $bottom

    ";

    if ($root) $add = "tree.add(0,-1,'$root');";

    $html = "$top

    <script type=\"$wgJsMimeType\"> // TreeAndMenu{$this->version} tree = new dTree('{$this->uniqname}$id'); for (i in tree.icon) tree.icon[i] = '{$this->baseUrl}/'+tree.icon[i];{$this->images} tree.config.useLines = {$this->useLines}; $add {$this->uniqname}$id = tree; $nodes document.getElementById('$id').innerHTML = {$this->uniqname}$id.toString(); </script>

    $bottom";

    } else {

    # Finalise a menu

    if ($depth > 0) $nodes .= str_repeat('

',$depth);

$nodes = preg_replace("/<(a.*? )title=\".+?\".*?>/","<$1>",$nodes); # IE has problems with title attribute in suckerfish menus $html = "

    \n$nodes

<script type=\"$wgJsMimeType\"> if (window.attachEvent) { var sfEls = document.getElementById('$id').getElementsByTagName('li'); for (var i=0; i<sfEls.length; i++) { sfEls[i].onmouseover=function() { this.className+=' sfhover'; } sfEls[i].onmouseout=function() { this.className=this.className.replace(new RegExp(' sfhover *'),); } } } </script> "; }

$text = preg_replace("/\x7f1$u\x7f$id\x7f.+?$/m",$html,$text,1); # replace first occurence of this trees root-id $nodes = ; $last = -1; } } }

$text = preg_replace("/\x7f1$u\x7f.+?[\\r\\n]+/m",,$text); # Remove all unreplaced row information return true; }

}


/**

* Called from $wgExtensionFunctions array when initialising extensions
*/

function wfSetupTreeAndMenu() { global $wgTreeAndMenu; $wgTreeAndMenu = new TreeAndMenu(); }


/**

* Needed in MediaWiki >1.8.0 for magic word hooks to work properly
*/

function wfTreeAndMenuLanguageGetMagic(&$magicWords,$langCode = 0) { global $wgTreeMagic,$wgMenuMagic; $magicWords[$wgTreeMagic] = array($langCode,$wgTreeMagic); $magicWords[$wgMenuMagic] = array($langCode,$wgMenuMagic); return true; }