Difference between revisions of "Extension:Treeview5.php"

From Organic Design wiki
(use addslashes on $item in dTree.add)
(5.1.0 - should work for all versions including 1.12)
Line 10: Line 10:
 
if (!defined('MEDIAWIKI')) die('Not an entry point.');
 
if (!defined('MEDIAWIKI')) die('Not an entry point.');
  
define('TREEVIEW5_VERSION','5.0.3, 2008-02-27');
+
define('TREEVIEW5_VERSION','5.1.0, 2008-03-03');
  
 
# Set any unset images to default titles
 
# Set any unset images to default titles
 
if (!isset($wgTreeViewImages) || !is_array($wgTreeViewImages)) $wgTreeViewImages = array();
 
if (!isset($wgTreeViewImages) || !is_array($wgTreeViewImages)) $wgTreeViewImages = array();
 
# Keep track of JavaScript added to page to avoid doubleups
 
if (!isset($wgJS)) $wgJS = array();
 
  
 
$wgTreeView5Magic              = "tree"; # the parser-function name for trees
 
$wgTreeView5Magic              = "tree"; # the parser-function name for trees
Line 36: Line 33:
  
 
var $version  = TREEVIEW5_VERSION;
 
var $version  = TREEVIEW5_VERSION;
var $voo;      // used by voodoo code only
+
var $uniq    = '';     # uniq part of all tree id's
var $baseDir;   // internal absolute path to treeview directory
+
var $id       = '';      # id for specific tree
var $baseUrl;   // external URL to treeview directory (relative to domain)
+
var $baseDir = '';     # internal absolute path to treeview directory
var $useLines; // internal variable determining whether to render connector lines
+
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
  
 
/**
 
/**
Line 45: Line 45:
 
*/
 
*/
 
function __construct() {
 
function __construct() {
global $wgParser,$wgScriptPath,$wgTreeView5Magic,$wgTreeViewImages,$wgTreeViewShowLines;
+
global $wgOut,$wgHooks,$wgParser,$wgScriptPath,$wgJsMimeType,
 +
$wgTreeView5Magic,$wgTreeViewImages,$wgTreeViewShowLines;
  
# Convert image titles to file paths and obtain pixel width of items
+
# Add hooks
 +
$wgParser->setFunctionHook($wgTreeView5Magic,array($this,'expandTree'));
 +
$wgHooks['ParserAfterTidy'][] = array($this,'renderTree');
 +
 +
# 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('tv');
 +
 
 +
# Convert image titles to file paths and store as JS to update dTree
 
foreach ($wgTreeViewImages as $k => $v) {
 
foreach ($wgTreeViewImages as $k => $v) {
 
$title = Title::newFromText($v,NS_IMAGE);
 
$title = Title::newFromText($v,NS_IMAGE);
 
$image = Image::newFromTitle($title);
 
$image = Image::newFromTitle($title);
if ($image && $image->exists()) $wgTreeViewImages[$k] = $image->getURL();
+
$v = $image && $image->exists() ? $image->getURL() : $wgTreeViewImages[$k];
 +
$this->images .= "tree.icon['$k'] = '$v';";
 
}
 
}
  
$wgParser->setFunctionHook($wgTreeView5Magic,array($this,'pfunc'));
+
# Add link to output to load dtree.js script
+
$wgOut->addScript("<script type=\"$wgJsMimeType\" src=\"{$this->baseUrl}/dtree.js\" />\n");
$this->voo = uniqid('TVUNIQ');
+
}
$this->baseDir = dirname(__FILE__);
+
 
$this->baseUrl = preg_replace('|^.+(?=/extensions)|',$wgScriptPath,$this->baseDir);
 
$this->useLines = $wgTreeViewShowLines ? 'true' : 'false';
 
}
 
  
 
/**
 
/**
* Restructure recursive trees into a single bullet list surrounded by internal treeview5 tags
+
* Called after parser has finished (ParserAfterTidy) so all transcluded parts can be assembled into final trees
 
*/
 
*/
public function pfunc(&$parser) {
+
public function renderTree(&$parser, &$text) {
global $wgParser;
+
global $wgJsMimeType;
$id = uniqid("");
+
$u = $this->uniq;
$args = "id='$id'";
 
foreach (func_get_args() as $arg) if (!is_object($arg)) {
 
if (preg_match('/^(.+?=.+)$/',$arg,$m)) $args .= " $arg"; else $text = $arg;
 
}
 
  
# Black magic
+
# Determine which trees are sub trees
$text = $this->uniq[$id] = preg_replace(
+
# - there should be a more robust way to do this,
'/^\\*(\\**)\\s*(\\x07UNIQ.+?-treeview5(.+?)-.+?-QINU\\x07?)/me',
+
#  it's just based on the fact that all sub-tree's have a minus preceding their row data
'$this->uniq["$3"] ? preg_replace("/^(\\*+)/m","$1\$1",$this->uniq["$3"]) : "$1$2"',
+
if (!preg_match_all("/-1$u-(.+?)-/",$text,$subs)) $subs = array(1 => array());
$text
+
);
+
# Extract all the formatted tree rows in the page and if any, replace with dTree JavaScript
 +
if (preg_match_all("/1$u-(.+?)-([0-9]+)-({$u}3(.+?){$u}4)?(.*?)(?=[12]$u)/",$text,$matches,PREG_SET_ORDER)) {
  
# And a little more voodoo here
+
# PASS-1: build $rows array containing depth, and tree start/end information
$wgParser->setHook("treeview5$id",array($this,'tag'));
+
$rows  = array();
return "<treeview5$id $args>$text</treeview5$id>";
+
$depths = array(0 => 0); # depth of each tree root
}
+
$root  = 0;            # the id of the current root-tree (used as tree id in PASS2)
 +
$lastId = $lastDepth = 0;
 +
foreach ($matches as $k => $match) {
 +
list(,$id,$depth,,$icon,$item) = $match;
 +
$start = false;
 +
if ($id != $lastId) {
 +
if (!isset($depths[$id])) $depths[$id] = $depths[$lastId]+$lastDepth;
 +
if ($start = $root != $id && !in_array($id,$subs[1])) $depths[$root = $id] = 0;
 +
}
 +
if ($item) $rows[] = array($root,$depth+$depths[$id],$icon,addslashes($item),$start);
 +
$lastId    = $id;
 +
$lastDepth = $depth;
 +
}
  
/**
+
# PASS-2: build the JavaScript and replace into $text
* Convert a bullet list into a treeview
+
$parents = array(); # parent node for each depth
*/
+
$last = -1;
public function tag($text,$argv,&$parser) {
+
foreach ($rows as $node => $info) {
global $wgTreeViewImages,$wgJsMimeType;
+
$node++;
 +
list($id,$depth,$icon,$item,$start) = $info;
 +
$args = $this->args[$root];
 +
if (!isset($args['root'])) $args['root'] = ''; # tmp - need to handle rootless trees
 +
$end  = $node == count($rows) || $rows[$node][4];
 +
$add  = isset($args['root']) ? "tree.add(0,-1,'".$args['root']."');" : '';
  
$id  = 'tv'.$argv['id']; // id of this tree (must be valid JS variable identifier)
+
# First row of a new root-tree, initialise dTree JavaScript
$voo  = $this->voo;     // used by voodoo
+
if ($start) {
$root = isset($argv['root']) ? $argv['root'] : '';
+
$tree = "
 +
// TreeView{$this->version}
 +
tree = new dTree('dt$id');
 +
for (i in tree.icon) tree.icon[i] = '{$this->baseUrl}/'+tree.icon[i];{$this->images}
 +
tree.config.useLines = {$this->useLines};
 +
$add
 +
dt$id = tree;
 +
";
 +
}
  
# Protect the asterisk structure and wiki-parse the bullet tree
+
# Add the dTree node for this row
$text = preg_replace("/(?<=\\*)\\s*\\[\\[Image:(.+?)\\]\\]/","{$voo}3$1{$voo}4",$text);
+
if ($depth > $last) $parents[$depth] = $node-1;
$text = preg_replace_callback("/^(\\*+)(.*?)$/m",array($this,'protectTree'),$text);
+
$parent = $parents[$depth];
$out = $parser->parse($text,$parser->mTitle,$parser->mOptions,false,false);
+
$last  = $depth;
$text = $out->getText();
+
$tree .= "dt$id.add($node,$parent,'$item');\n";
  
# Extract the valid rows and process if any
+
# Last row of current root-tree, finalise dTree JavaScript and replace into $text
if (preg_match_all("/1{$voo}([0-9]+)-({$voo}3(.+?){$voo}4)?(.+?){$voo}2/",$text,$matches,PREG_SET_ORDER)) {
+
if ($end) {
+
$tree .= "document.write(dt$id);\n";
# Convert the custom images in $wgTreeViewImages into JS statements
+
$tree  = "<script type=\"$wgJsMimeType\">$tree\n</script>\n";
$images = '';
+
$text  = preg_replace("/^1$u-$id-.+?$/m",$tree,$text,1); # replace first occurence of this trees root-id
foreach ($wgTreeViewImages as $k => $v) $images .= "$id.icon['$k'] = '$v';";
+
}
 
# Build the dTree instantiation code
 
$dtree = "// TreeView{$this->version}
 
$id = new dTree('$id');
 
for (i in $id.icon) $id.icon[i] = '{$this->baseUrl}/'+$id.icon[i];
 
$images
 
$id.config.useLines = {$this->useLines};
 
$id.add(0,-1,'$root');
 
";
 
  
# Add the content to the dTree object
 
$path = array();
 
foreach ($matches as $n => $row) {
 
list(,$depth,,$icon,$item) = $row;
 
if ($depth > $last) $path[$depth-1] = $n;
 
$n++;
 
$parent = $path[$depth-1];
 
$item  = addslashes($item);
 
$dtree .= "${id}.add($n,$parent,'$item');\n";
 
$last  = $depth;
 
 
}
 
}
 +
}
  
# Output the dTree
+
$text = preg_replace("/1$u-.+?[\\r\\n]+/m",'',$text); # Remove all unreplaced row information
$dtree .= "document.write($id);\n";
+
return true;
}
 
  
return("<script type=\"$wgJsMimeType\">\n<!--\n$dtree-->\n</script>");
 
 
}
 
}
 +
  
 
/**
 
/**
* Protect asterisk bullet structure from wiki parser
+
* Expand #tree parser-functions (reformats tree rows for matching later) and store args
 
*/
 
*/
private function protectTree($m) {
+
public function expandTree(&$parser) {
return "1{$this->voo}".strlen($m[1])."-$m[2]{$this->voo}2\n";
+
 +
# Create a unique id for this tree
 +
$this->id = uniqid('');
 +
 +
# Store args for this tree for later use
 +
foreach (func_get_args() as $arg) if (!is_object($arg)) {
 +
if (preg_match('/^(.+?)\\s*=\\s*(.+)$/',$arg,$m)) $this->args[$this->id][$m[1]] = $m[2];
 +
else $text = $arg;
 +
}
 +
 +
# 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;
 
}
 
}
+
 
 +
 
 
/**
 
/**
* Add the javascript to the output object if not added yet and there is at least one tree
+
* 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
 
*/
 
*/
public function addJS() {
+
private function formatRow($m) {
global $wgOut,$wgJS,$wgJsMimeType;
+
return "1{$this->uniq}-{$this->id}-".(strlen($m[1])-1)."-$m[2]2{$this->uniq}";
if (isset($wgJS['TreeView5'])) return;
+
}
$wgJS['TreeView5'] = true;
+
$wgOut->addScript("<script type=\"$wgJsMimeType\" src=\"{$this->baseUrl}/dtree.js\" />\n");
 
}
 
 
}
 
}
 +
 
   
 
   
 
/**
 
/**
Line 160: Line 190:
 
global $wgTreeView5;
 
global $wgTreeView5;
 
$wgTreeView5 = new TreeView5();
 
$wgTreeView5 = new TreeView5();
$wgTreeView5->addJS(); # Make code unconditional for now due to parser caching
 
 
}
 
}
 +
 
   
 
   
 
/**
 
/**
Line 167: Line 197:
 
  */
 
  */
 
function wfTreeView5LanguageGetMagic(&$magicWords,$langCode = 0) {
 
function wfTreeView5LanguageGetMagic(&$magicWords,$langCode = 0) {
global $wgTreeView5,$wgTreeView5Magic;
+
global $wgTreeView5Magic;
$magicWords[$wgTreeView5Magic] = array(0,$wgTreeView5Magic);
+
$magicWords[$wgTreeView5Magic] = array($langCode,$wgTreeView5Magic);
 
return true;
 
return true;
 
}
 
}

Revision as of 21:08, 2 March 2008

<?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.
Voodoo.svg

In computer programming, "Voodoo", or "Magic" refers to techniques that are secret or not widely known, and may be deliberately kept secret. The Jargon File makes a distinction between "deep magic", which refers to code based on esoteric theoretical knowledge; "black magic" (voodoo), which refers to code based on techniques that appear to work but which lack a theoretical explanation; and "heavy wizardry", which refers to code based on obscure or undocumented intricacies of particular hardware or software.

At Organic Design the most common of these is extending an instance's class at runtime after it has been instantiated, a technique that can be used to provide additional hooks into existing code without requiring modification of code-base files. Another is reading in a class file, declaring it under a different name, then sub-classing that with a new class of the original name - that way the environment uses the new extended class thinking it's the original one.

See also

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

define('TREEVIEW5_VERSION','5.1.0, 2008-03-03');

  1. Set any unset images to default titles

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

$wgTreeView5Magic = "tree"; # the parser-function name for trees $wgTreeViewShowLines = false; # whether to render the dotted lines joining nodes $wgExtensionFunctions[] = 'wfSetupTreeView5'; $wgHooks['LanguageGetMagic'][] = 'wfTreeView5LanguageGetMagic';

$wgExtensionCredits['parserhook'][] = array( 'name' => 'Treeview5', 'author' => 'Nad, Sven', 'url' => 'http://www.mediawiki.org/wiki/Extension:Treeview', 'description' => 'Extends the wiki parser to allow bullet and numbered lists to work with recursion and optionally allows these to be rendered as collapsible trees using the free dTree JavaScript tree menu.', 'version' => TREEVIEW5_VERSION );

class TreeView5 {

var $version = TREEVIEW5_VERSION; var $uniq = ; # uniq part of all tree id's 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, $wgTreeView5Magic,$wgTreeViewImages,$wgTreeViewShowLines;

# Add hooks $wgParser->setFunctionHook($wgTreeView5Magic,array($this,'expandTree')); $wgHooks['ParserAfterTidy'][] = array($this,'renderTree');

# 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('tv');

# 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\" />\n"); }


/** * Called after parser has finished (ParserAfterTidy) so all transcluded parts can be assembled into final trees */ public function renderTree(&$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("/-1$u-(.+?)-/",$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("/1$u-(.+?)-([0-9]+)-({$u}3(.+?){$u}4)?(.*?)(?=[12]$u)/",$text,$matches,PREG_SET_ORDER)) {

# PASS-1: build $rows array containing depth, and tree start/end information $rows = array(); $depths = array(0 => 0); # depth of each tree root $root = 0; # the id of the current root-tree (used as tree id in PASS2) $lastId = $lastDepth = 0; foreach ($matches as $k => $match) { list(,$id,$depth,,$icon,$item) = $match; $start = false; if ($id != $lastId) { if (!isset($depths[$id])) $depths[$id] = $depths[$lastId]+$lastDepth; if ($start = $root != $id && !in_array($id,$subs[1])) $depths[$root = $id] = 0; } if ($item) $rows[] = array($root,$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 $last = -1; foreach ($rows as $node => $info) { $node++; list($id,$depth,$icon,$item,$start) = $info; $args = $this->args[$root]; if (!isset($args['root'])) $args['root'] = ; # tmp - need to handle rootless trees $end = $node == count($rows) || $rows[$node][4]; $add = isset($args['root']) ? "tree.add(0,-1,'".$args['root']."');" : ;

# First row of a new root-tree, initialise dTree JavaScript if ($start) { $tree = " // TreeView{$this->version} tree = new dTree('dt$id'); for (i in tree.icon) tree.icon[i] = '{$this->baseUrl}/'+tree.icon[i];{$this->images} tree.config.useLines = {$this->useLines}; $add dt$id = tree; "; }

# Add the dTree node for this row if ($depth > $last) $parents[$depth] = $node-1; $parent = $parents[$depth]; $last = $depth; $tree .= "dt$id.add($node,$parent,'$item');\n";

# Last row of current root-tree, finalise dTree JavaScript and replace into $text if ($end) { $tree .= "document.write(dt$id);\n"; $tree = "<script type=\"$wgJsMimeType\">$tree\n</script>\n"; $text = preg_replace("/^1$u-$id-.+?$/m",$tree,$text,1); # replace first occurence of this trees root-id }

} }

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

}


/** * Expand #tree parser-functions (reformats tree rows for matching later) and store args */ public function expandTree(&$parser) {

# Create a unique id for this tree $this->id = uniqid();

# Store args for this tree for later use foreach (func_get_args() as $arg) if (!is_object($arg)) { if (preg_match('/^(.+?)\\s*=\\s*(.+)$/',$arg,$m)) $this->args[$this->id][$m[1]] = $m[2]; else $text = $arg; }

# 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 "1{$this->uniq}-{$this->id}-".(strlen($m[1])-1)."-$m[2]2{$this->uniq}"; }

}


/**

* Called from $wgExtensionFunctions array when initialising extensions
*/

function wfSetupTreeView5() { global $wgTreeView5; $wgTreeView5 = new TreeView5(); }


/**

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

function wfTreeView5LanguageGetMagic(&$magicWords,$langCode = 0) { global $wgTreeView5Magic; $magicWords[$wgTreeView5Magic] = array($langCode,$wgTreeView5Magic); return true; }