Sandbox
<?php /**
* TreeAndMenu extension - Adds #tree, #menu and #star parser functions which contain bullet-lists to be rendered as collapsible treeview's or dropdown menus * * See http://www.mediawiki.org/wiki/Extension:TreeAndMenu for installation and usage details * See http://www.organicdesign.co.nz/Extension_talk:TreeAndMenu.php for development notes and disucssion * * @file * @ingroup Extensions * @author Aran Dunkley User:Nad * @copyright © 2007 Aran Dunkley * @licence GNU General Public Licence 2.0 or later */
if( !defined( 'MEDIAWIKI' ) ) die( 'Not an entry point.' );
define( 'TREEANDMENU_VERSION','2.0.1, 2012-01-28' );
// 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 $wgStarMagic = "star"; // the parser-function name for radial star menus $wgTreeViewShowLines = false; // whether to render the dotted lines joining nodes $wgExtensionFunctions[] = 'wfSetupTreeAndMenu'; $wgHooks['LanguageGetMagic'][] = 'wfTreeAndMenuLanguageGetMagic';
$wgExtensionCredits['parserhook'][] = array( 'path' => __FILE__, 'name' => 'TreeAndMenu', 'author' => array( 'Nad', 'Sven' ), 'url' => 'http://www.mediawiki.org/wiki/Extension:TreeAndMenu', 'description' => 'Adds #tree, #menu and #star 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, $wgJsMimeType, $wgExtensionAssetsPath, $wgResourceModules, $wgTreeMagic, $wgMenuMagic, $wgStarMagic, $wgTreeViewImages, $wgTreeViewShowLines;
// Add hooks $wgParser->setFunctionHook( $wgTreeMagic, array( $this,'expandTree' ) ); $wgParser->setFunctionHook( $wgMenuMagic, array( $this,'expandMenu' ) ); $wgParser->setFunctionHook( $wgStarMagic, array( $this,'expandStar' ) ); $wgHooks['ParserAfterTidy'][] = array( $this, 'renderTreeAndMenu' );
// Update general tree paths and properties $this->baseDir = dirname( __FILE__ ); $this->baseUrl = $wgExtensionAssetsPath . '/' . basename( dirname( __FILE__ ) ); $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 ) { $image = wfLocalFile( $v ); $v = ( is_object( $image ) && $image->exists() ) ? $image->getURL() : $wgTreeViewImages[$k]; $this->images .= "tree.icon['$k'] = '$v';"; } }
/** * 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 #star parser-functions */ public function expandStar() { $args = func_get_args(); return 'not done yet'; }
/** * 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 $text = ; foreach( $args as $arg ) if ( preg_match( '/^(\\w+?)\\s*=\\s*(.+)$/s', $arg, $m ) ) $args[$m[1]] = $m[2]; else $text = $arg;
// If root, parse as wikitext if( isset( $args['root'] ) ) { $p = clone $parser; $o = clone $parser->mOptions; $o->mTidy = $o->mEnableLimitReport = false; $html = $p->parse( $args['root'], $parser->mTitle, $o, false, true )->getText(); $args['root'] = addslashes( $html ); }
// 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, $wgOut;
$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, addslashes( $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 $node = 0; $last = -1; $nodes = ; $opennodes = array(); foreach( $rows as $i => $info ) { $node++; list( $id, $depth, $icon, $item, $start ) = $info; $objid = $this->uniqname . preg_replace( '/\W/', , $id ); $args = $this->args[$id]; $type = $args['type']; $end = $i == count( $rows )-1 || $rows[$i+1][4]; if( !isset( $args['root'] ) ) $args['root'] = ; // tmp - need to handle rootless trees $openlevels = isset( $args['openlevels'] ) ? $args['openlevels']+1 : 0; if( $start ) $node = 1;
// Append node script for this row if( $depth > $last ) $parents[$depth] = $node-1; $parent = $parents[$depth]; if( $type == 'tree' ) { $nodes .= "$objid.add($node, $parent, '$item');\n"; if( $depth > 0 && $openlevels > $depth ) $opennodes[$parent] = true; } 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' ) {
static $dtree = false; if( !$dtree ) { $wgOut->addScriptFile( $this->baseUrl . '/dtree.js' ); $dtree = true; }
// Finalise a tree $add = isset( $args['root'] ) ? "tree.add(0,-1,'".$args['root']."');" : ; $top = $bottom = $root = $opennodesjs = ; foreach( array_keys( $opennodes ) as $i ) $opennodesjs .= "$objid.o($i);"; foreach( $args as $arg => $pos ) if( ( $pos == 'top' || $pos == 'bottom' || $pos == 'root' ) && ( $arg == 'open' || $arg == 'close' ) ) $$pos .= "<a href=\"javascript: $objid.{$arg}All();\"> {$arg} all</a> ";
if( $top ) $top = "$top
"; if( $bottom ) $bottom = "$bottom
";// Define the script to build this tree $script = "// TreeAndMenu-{$this->version}\ntree = new dTree('$objid'); for (i in tree.icon) tree.icon[i] = '{$this->baseUrl}/'+tree.icon[i];{$this->images} tree.config.useLines = {$this->useLines}; $add $objid = tree; $nodes document.getElementById('$id').innerHTML = $objid.toString(); $opennodesjs for(i in window.tamOnload_$objid) { window.tamOnload_{$objid}[i](); }"; $wgOut->addScript( "<script type=\"$wgJsMimeType\">$(function(){\n$script\n});</script>" );
$html = "$top$bottom";$html .= "<script type=\"$wgJsMimeType\">window.tamOnload_$objid=[]</script>";
} 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 = "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 *'),); } } }"; $wgOut->addScript( "<script type=\"$wgJsMimeType\">$(function(){\n$script\n});</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(); }
/**
* Reserve magic words */
function wfTreeAndMenuLanguageGetMagic( &$magicWords, $langCode = 0 ) { global $wgTreeMagic, $wgMenuMagic, $wgStarMagic; $magicWords[$wgTreeMagic] = array( 0, $wgTreeMagic ); $magicWords[$wgMenuMagic] = array( 0, $wgMenuMagic ); $magicWords[$wgStarMagic] = array( 0, $wgStarMagic ); return true; }



