Difference between revisions of "Extension talk:Treeview5.php"

From Organic Design wiki
(explanation of voodoo and guids)
(Change source-code blocks to standard format)
 
(55 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 
{{ext-talk-msg|Tree view}}
 
{{ext-talk-msg|Tree view}}
A more logical way of approaching a treeview in MediaWiki is for the PHP extension part to concentrate on allowing bullet lists (and maybe numbered lists too) to work recusively with transclusion, and for the tree rendering to be handled by existing JavaScript components such as [http://www.destroydrop.com/javascripts/tree/ dTree] which are very good and packed with features such as persistence between pages.
+
Treeview5.1 is a new approach to the core recursive bullet-list code which doesn't require any voodoo, and should be compatible with all MediaWiki versions including 1.12.
  
{{Note|[[Extension:DTree.php]] has been created in the mean time to test dtree, the code will be integrated into [[Extension:Treeview5.php]].}}
+
== Parameters ==
 +
*'''root:''' The content of the root node. Later this will also affect how sub-trees work
 +
*'''id:''' A tree must be given a unique identifier if you want it's state to be persistent
 +
*'''openlevels:''' not implemented in 5.x yet
 +
*'''open:''' equals '''top''' | '''bottom'''
 +
*'''close:''' equals '''top''' | '''bottom'''
  
[[Extension:Treeview4.php|Treeview4]] already handles the recursion aspect well and has been written in a much more modular way than [[Extension:Treeview.php|Treeview3]] allowing it to be independent of the tree rendering. But in this version I'd like to make the list-recursion work without any parser-function syntax at all - i.e. essentially to fix the MediaWiki parser's limitation of not allowing transclusion to work properly with lists as in the following example.
+
== Livelets ==
<pre>
+
There are a couple of issues with the the new tree view with respect to [[Livelets]] which we discuss the details of here.
*Foo
 
**Bar
 
**{{:Baz}}
 
**Fodda
 
</pre>
 
The new version would allow the Baz article (if it were a bullet list) to be properly nested at the correct level in the parent tree. This would simply be based on the fact that the transclusion is directly after an asterisk. The following example would not be indented, the indentation of <code>{{:Baz}}</code> is determined by the article itself.
 
<pre>
 
*Foo
 
**Bar
 
{{:Baz}}
 
**Fodda
 
</pre>
 
  
 +
=== The document.write problem ===
 +
The first issue which has been resolved in the treeview code concerns the ''document.write'' statement used in the JavaScript code which is recommended in the ''dTree'' documentation for rendering the tree after its nodes have all been defined.
  
The ''#tree'' parser-function would still be used to make bullet lists into trees, but would be optional and would provide an interface to convert the transcluded bullet lists into the javascript required in [http://www.destroydrop.com/javascripts/tree/ dTree] (see [[Extension talk:Treeview5.php#Dtree API]]) to generate a tree.
+
A ''dTree'' is composed of JavaScript statements which execute as the page loads, so when the tree is rendered with a ''document.write'' statement, it's converted to a string (by the ''dTree'' object's ''toString'' method being executed) and then appended to the page at that point. This is fine under normal page load circumstances, but if the tree happens to be included within an AJAX request which populates some arbitrary part of the page after the page has loaded, then the page-appending behaviour of ''document.write'' will not achieve the desired result since it will append the tree to the end of the page rather than insert it at the location of the executing code. To get round this Treeview5 surrounds the script in a ''div'' element which is identified by an ''id'' attribute. The tree can then be converted to a string and written into the ''div'''s ''innerHTML'' property, using a statement similar to the following:
 +
<source lang="js">
 +
document.getElementById('{$id}').innerHTML = {$id}.toString();
 +
</source>
  
==Persistance==
+
=== The script execution problem ===
Persistance is auctomatically handled via cookies, see dtree.js, line 36.
+
The second problem cannot be solved by adjusting the Treeview code, it requires changes to be made to the Livelets extension itself. The problem is that when an article is retrieved via AJAX request instead of as a normal browser page load, the &lt;script&gt; elements will not get executed. This problem has not shown previously because Treeview versions prior to 5 were based on HTML tables containing ''onClick'' events rather than being composed completed of JavaScript. To solve this, the Livelet extension must execute a function on receipt of its content rather than just sending it straight to the ''innerHTML'' property of its target element. The function must populate the target as usual, and then execute the content of all the ''script'' contained within it.
  
 +
== How it works ==
 +
The initial problem was how to write a parser-function which could generate a tree which could be composed of other transcluded tree's (i.e. parser-functions which are recursively executed forming a larger whole structure together). This seemed impossible since there are no MediaWiki properties available in the environment which reveal the current depth that the parser-function expansion is operating at.
  
== Dtree API ==
+
But after thinking about the problem more closely, it turns out that the tree can be constructed without depth information by converting each tree row into a new format containing depth and tree id information which is protected from wiki-parsing. This information can then be converted into tree JavaScript after the parser has finished in the ''ParserAfterTidy'' hook.
See file api.html in the [http://www.destroydrop.com/javascripts/tree/ dTree] directory.
 
 
 
Basically this extension need to provide an interface from a parser function which deals with nested bullet lists to creating the javascript output. At the bullet list stage the php function [http://nz2.php.net/manual/en/function.htmlspecialchars.php htmlspecialchars] on the input to prevent cross site scripting. See [http://www.technicalinfo.net/papers/CSS.html HTML Code Injection and Cross-site scripting] for details.
 
  
==Open/Close parser function==
+
Here is the wikitext of an example tree which includes two transcluded tree's
Dedicated parser functions should be provided which inserts the open/close functionality of clades on the tree.
+
{|
 +
! Source || Rendered Tree
 +
|-
 +
|
 
<pre>
 
<pre>
<p><a href="javascript: d.openAll();">open all</a> | <a href="javascript: d.closeAll();">close all</a></p>
+
{{#tree:|open=top|close=top|
 +
*[[toplevel]]
 +
**bar
 +
***baz
 +
*foo
 +
**[[Bar]]
 +
***{{:Foo}}
 +
**[[Baz]]
 +
***{{:Bar}}
 +
}}
 
</pre>
 
</pre>
Note here that the javascript object is a tree called '''d'''. The functionality will not work for any other tree other than that named '''d'''.
+
|valign=top|{{#tree:|open=top|close=top|
 
+
*[[toplevel]]
==Tree example==
+
**bar
This example generates a tree similar to the example01.html provided with  [http://www.destroydrop.com/javascripts/tree/ dTree]
+
***baz
 
+
*foo
;Wikisyntax
+
**[[Bar]]
<pre>
+
***{{:Foo}}
{{#tree:openlevels=4|root='My example tree'|
+
**[[Baz]]
*Node 1
+
***{{:Bar}}
**Node 1.1
 
***Node 1.1.1
 
****Node 1.1.1.1
 
*Node 2
 
*Node 3
 
*Node 4
 
*My Pictures
 
**The trip to Iceland
 
**Mom's birthday
 
*Recycle bin
 
 
}}
 
}}
</pre>
+
|}
 
 
;Original javascript syntax;
 
<pre>
 
d = new dTree('d');
 
 
 
d.add(0,-1,'My example tree');
 
d.add(1,0,'Node 1','/wiki/index.php?title=Sandbox');
 
d.add(2,0,'Node 2','example01.html');
 
d.add(3,1,'Node 1.1','example01.html');
 
d.add(4,0,'Node 3','example01.html');
 
d.add(5,3,'Node 1.1.1','example01.html');
 
d.add(6,5,'Node 1.1.1.1','example01.html');
 
d.add(7,0,'Node 4','example01.html');
 
d.add(8,1,'Node 1.2','example01.html');
 
d.add(9,0,'My Pictures','example01.html','Pictures I\'ve taken over the years','','','img/imgfolder.gif');
 
d.add(10,9,'The trip to Iceland','example01.html','Pictures of Gullfoss and Geysir');
 
d.add(11,9,'Mom\'s birthday','example01.html');
 
d.add(12,0,'Recycle Bin','example01.html','','','img/trash.gif');
 
 
 
document.write(d);
 
</pre>
 
;Equivalent reordered javascript syntax;
 
<pre>
 
e = new dTree('e');
 
 
 
e.add(0,-1,'My example tree');
 
e.add(1,0,'Node 1','/wiki/index.php?title=Sandbox');
 
e.add(2,1,'Node 1.1','example01.html');
 
e.add(3,2,'Node 1.1.1','example01.html');
 
e.add(4,3,'Node 1.1.1.1','example01.html');
 
e.add(5,1,'Node 1.2','example01.html');
 
e.add(6,0,'Node 2','example01.html');
 
e.add(7,0,'Node 3','example01.html');
 
e.add(8,0,'Node 4','example01.html');
 
e.add(9,0,'My Pictures','example01.html','Pictures I\'ve taken over the years','','','img/imgfolder.gif');
 
e.add(10,9,'The trip to Iceland','example01.html','Pictures of Gullfoss and Geysir');
 
e.add(11,9,'Mom\'s birthday','example01.html');
 
e.add(12,0,'Recycle Bin','example01.html','','','img/trash.gif');
 
 
document.write(e);
 
 
 
</pre>
 
 
 
The current treeview5.php can be configured to use images from dtree during development.
 
{{code|<PHP>
 
iinclude("$IP/extensions/Treeview/Treeview5.php");
 
# Approximated by
 
$wgTreeViewImages['plus']  = "Nolines plus.gif";
 
$wgTreeViewImages['minus']  = "Nolines minus.gif";
 
$wgTreeViewImages['opened'] = "Folderopen.gif";
 
$wgTreeViewImages['closed'] = "Folder.gif";
 
$wgTreeViewImages['doc']    = "Page.gif";
 
$wgTreeViewImages['spacer'] = "Empty.gif";
 
  
# These images are needed if you have $wgTreeViewShowLines set
+
The ''expandTree'' method (the ''#tree'' parser-function hook) then converts all the matched rows into the following general structure:
$wgTreeViewImages['vert'] = 'Line.gif';
+
1{uniq}-{id}-{depth}-{item}-2{uniq}
$wgTreeViewImages['node'] = 'Join.gif';
+
Where ''uniq'' is a unique string representing treeview's in general, ''id'' is a unique string representing the current tree, ''depth'' is the depth of the current row with respect to the current tree root, and ''item'' is the wikitext within the current row. Each row is converted to the new structured format by the private ''formatRow'' method. The information is "protected" from the parser merely by the fact that it contains no markup characters, but this could be made more robust later.
$wgTreeViewImages['last'] = 'Joinbottom.gif';
 
</PHP>
 
}}
 
  
==Defining the root of the tree==
+
=== Notes about the format used ===
The root of the tree could be optionally specified with a parameter called ''root'' in the parser function options. It must be specified as either;
+
*If the same article is transcluded more than once it may not get processed again after the first time. This caching effect occurs at an internal level within the parser and is not affected by globals or parser properties. This means that information relating to specific items or sub-tree's could be repeated across transcluded instances of the same article within one or more trees in the page. This is not a problem in the case of the tree-id because only id's of root trees will be used in the final rendered tree.
 +
*There a potential problem which hasn't been tested yet, with a sub-tree being transcluded twice in consecutive nodes. The tree id is also used to determine when a new tree is starting (by checking if ''id'' has changed), so the repeated id problem arising from multiple adjacent transclusion would prevent the loop from knowing about the transition from one of the similar sub-trees to the next.
 +
*The actual pattern used in the current version is slightly different than the one shown here due to subsequent bug fixes. For example, the dashes used to separate the fields in the formatted rows have been changed to \x7F's. The examples shown are easier to read than the new ones, so they've been left as is. They still demonstrate the process used to render the tree in the current version.
  
'''unnamed'''
+
After the resulting formatted-row structure has been fully parsed by the wiki parser, the content portions will have been converted into HTML and the information portions remain unchanged. Below is the HTML source of the above example after parsing and printed at the start of the ''renderTree'' method (the ''ParserAfterTidy'' hook).
 
<pre>
 
<pre>
e.add(0,-1,'');
+
1tv47cb2fd61165f-47cb2fd6dadf4-0-<a href="/wiki/index.php?title=Toplevel&amp;action=edit" class="new" title="Toplevl">toplevel</a>-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6dadf4-1-bar-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6dadf4-2-baz-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6dadf4-0-foo-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6dadf4-1-<a href="/Bar" title="Bar">Bar</a>-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6dadf4-2-1tv47cb2fd61165f-47cb2fd6da03a-0-<a href="/Foo" title="Foo">Foo</a>-2tv47cb2fd61165f-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6da03a-1-bar-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6da03a-2-baz-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6da03a-0-foo-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6da03a-1-biz-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6da03a-2-1tv47cb2fd61165f-47cb2fd6d9c40-0-<a href="/Bar" title="Bar">bar</a>-2tv47cb2fd61165f-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6d9c40-1-baz2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6d9c40-2-biz2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6da03a-1-<a href="/wiki/index.php?title=Baz&amp;action=edit" class="new" title="Baz">Baz</a>-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6dadf4-1-<a href="/wiki/index.php?title=Baz&amp;action=edit" class="new" title="Baz">Baz</a>-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6dadf4-2-1tv47cb2fd61165f-47cb2fd6d9c40-0-<a href="/Bar" title="Bar">bar</a>-2tv47cb2fd61165f-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6d9c40-1-baz-2tv47cb2fd61165f
 +
1tv47cb2fd61165f-47cb2fd6d9c40-2-biz2tv47cb2fd61165f
 
</pre>
 
</pre>
or '''named'''
 
<pre>
 
e.add(0,-1,'My example tree');
 
</pre>
 
for the tree to be rendered.
 
  
{{code|<php>
+
The following regular expression is used to create an array, ''$matches'', each item of which contains a list of ''(,$id,$depth,,$icon,$item)'' extracted from the formatted rows in the page.
<?php
+
<source lang="php">
 +
preg_match_all("/1$u-(.+?)-([0-9]+)-({$u}3(.+?){$u}4)?(.*?)(?=[12]$u)/",$text,$matches,PREG_SET_ORDER)
 +
</source>
  
$in = "*Node 1
+
Following this extraction of row information into ''$matches'' is a loop that builds a new list called ''$rows'' which contains more detailed information than ''$matches'', and adjusts the depth values to account for transcluded sub-tree's. The ''$rows'' array is a list of ''($id,$depth,$icon,$item,$start)''. ''$id'' is the id of each root-tree (sub-tree's are not distinct from their root trees in the final rendered trees), ''$start'' is a boolean indicating whether or not the row is the first in a new root tree.
**Node 1.1
 
***Node 1.1.1
 
****Node 1.1.1.1
 
*Node 2
 
*Node 3
 
*Node 4
 
*My Pictures
 
**The trip to Iceland
 
**Mom's birthday
 
*Recycle bin";
 
  
$a = new treeview();
+
The main ''dTree'' JavaScript can then be constructed by looping through the ''$rows'' list and converting ordered depth-based information to the parent/child format required by ''dTree''. The indexes of each row within the ''$rows'' array are used as id's for the individual ''dTree'' nodes.
  
$a->makeDTree($in);
+
=== Adjusting depth to account for sub-tree transclusion ===
 +
The depth item in the ''$matches'' array is directly derived from the number of asterisks defined at the beginning of the row's wikitext. In the ''$rows'' array, the depth item has been adjusted to account for the depth of the tree at the point the sub-tree is transcluded, so that it can continue within-ward naturally. This adjustment is done by using the ''$depths'' array to associate each tree id with the current depth at each row where a new tree-id is encountered. The depths from ''$matches'' are added to the associated ''$depths'' values and stored in ''$rows''.
  
class treeview {
+
=== Root tree's and sub-tree's ===
 +
One of the difficulties with this extension has been in assessing from within a parser-function whether or not it's at the root level in terms of transclusion-depth. The new version doesn't require voodoo to determine this, it instead does the following pattern match on all the formatted rows in the page after wiki-parsing is complete (in the ''ParserAfterTidy'' hook).
 +
<source lang="php">
 +
if (!preg_match_all("/-1$u-(.+?)-/",$text,$subs)) $subs = array(1 => array());
 +
</source>
 +
This results in an array called ''$subs[1]'' which contains all the tree id's which are not transcluded. It does this by checking which
 +
rows have a preceding minus symbol, since top level ones will all be preceded by a ''p'' tag or a line start (see the example output of formatted rows above). An id is at root level if it is not in the ''$subs[1]'' array. This way is not as robust as it could be, and is also more computationally intensive than it could be.
  
  var $wikitext;
+
== CSS ==
 +
The dTree javascript library automatically makes links for the text beside folders. This is overridden if you explicitly set an external or internal link within the tree.
 +
It would be nice to distinguish the link colour between MediaWiki's links and the links automatically created beside folders.
 +
Below is the css that comes with dtree.
  
  function makeDTree(&$tmp) {
+
<source lang="css">
    $lines = split("\n", $tmp);
+
/*--------------------------------------------------|
 
+
| dTree 2.05 | www.destroydrop.com/javascript/tree/ |
    foreach($lines as $line) {
+
|---------------------------------------------------|
      $count = 0;
+
| Copyright (c) 2002-2003 Geir Landrö              |
      /**
+
|--------------------------------------------------*/
      * Note preg_replace expects the bullets to be at the start of the line
 
      **/
 
      $node = preg_replace("/\\*/", "", $line, -1, $count);
 
      print "$count<br />";
 
      print "OUTPUT=>$node<br />";
 
     
 
    }
 
 
 
   
 
    function addDTree($id,$pid,$name,$url,$title,$target, $icon,$iconOpe,$open) {
 
      return("mytree.add($id,$pid,$name,$url,$title,$target, $icon,$iconOpe,$open);");
 
     
 
    }
 
 
 
  }
 
  
 +
.dtree {
 +
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
 +
font-size: 11px;
 +
color: #666;
 +
white-space: nowrap;
 +
}
 +
.dtree img {
 +
border: 0px;
 +
vertical-align: middle;
 +
}
 +
.dtree a {
 +
color: #333;
 +
text-decoration: none;
 +
}
 +
.dtree a.node, .dtree a.nodeSel {
 +
white-space: nowrap;
 +
padding: 1px 2px 1px 2px;
 +
}
 +
.dtree a.node:hover, .dtree a.nodeSel:hover {
 +
color: #333;
 +
text-decoration: underline;
 +
}
 +
.dtree a.nodeSel {
 +
background-color: #c0d2ec;
 +
}
 +
.dtree .clip {
 +
overflow: hidden;
 
}
 
}
</php>
+
</source>
}}
 
 
 
== How it works ==
 
There are basically two stages to the way the treeview is working the first stage is the parser-function (the ''Tree'' function) which is designed to allow bullet lists to work continue with proper depth when transcluded. The result of the parser-function is a single bullet list which is surrounded in &lt;treeviewxxx> tags to be converted into the proper tree HTML. It was very difficult to get the transclusion to work properly and required some "voodoo".
 
 
 
=== Explanation of voodoo ===
 
The voodoo is required because the wiki parser works from deepest first to top level last, and protects tags inside GUID's which are not exposed to the parser which effectively puts them off-limits to any possible adjustment. The voodoo finds these protected GUID's and replaces them with normal bullet content which is not protected from further parsing and so becomes part of the higher level's content.
 
 
 
=== Explanation of guids ===
 
After the first stage of wiki-parsing (i.e. the processing of curly braces and protection of tag content) each tree will be a single wikitext bullet list to be processed by the tag hook (the ''treeview'' function). This function is essentially a two-stage process.
 
 
 
The first stage of this function converts the wikitext bullet list into list of items matchable by unique id's eg,
 
*[[foo]]
 
**[[bar]]
 
would become something like this:
 
UNIQxxxx1-<a href=.....</a>UNIQxxxx
 
UNIQxxxx2-<a href=.....</a>UNIQxxxx
 
The reason for this is that we need to protect the asterisks from being parsed otherwise they will be converted into UL's and LI's, but the rest of the content (i.e. the wikitext content in each bullet item) should be wiki-parsed as usual. We convert each row into a GUID-based structure so that the rows can be easily matched in the second stage without any possibility of any other content in the page or in a another tree being accidentally matched as well.
 
 
 
After this conversion all the rows of the current tree can be matched with a single ''preg_match_all''. Each row contains the depth info and the parsed wikitext of the row (and also an icon if the first item of content in a row was an image link). The code which does this conversion is as follows:
 
{{code|<php>
 
$text = preg_replace("/(?<=\\*)\\s*\\[\\[Image:(.+?)\\]\\]/","{$uid}3$1{$uid}4",$text);
 
$text = preg_replace_callback("/^(\\*+)(.*?)$/m",array($this,'protectTree'),$text);
 
$out  = $parser->parse($text,$parser->mTitle,$parser->mOptions,false,false);
 
$text = $out->getText();
 
</php>}}
 
  
The way to loop through the resulting matched rows and obtain their info to build the HTML or JS of a tree is as follows:
+
== Todo ==
{{code|<php>
+
{{Note|See [http://www.destroydrop.com/javascripts/tree/example/ Code generator] for example generation including various javascript options available}}
if (preg_match_all("/1{$uid}([0-9]+)-({$uid}3(.+?){$uid}4)?(.+?){$uid}2/",$text,$matches,PREG_SET_ORDER))
+
*Need to get inline images working - ''for simplicity it would be nice to hook into the '''img''' directory that comes with [http://www.destroydrop.com/javascripts/tree/ dDree]''
foreach ($matches as $row)
+
**''I think inline and othe images defined by the user should work off wiki uploaded images and only use dTree images as defaults - the same way the $wgTreeviewImages array works by allowing specified images to override the dTree defaults. The dTree/img folder should not be changed''
list(,$depth,,$icon,$html) = $row;
+
*Add '''open all'''/'''close all''' functionality
...
+
*Implement  openlevels arg as treeview maintains persistence now
</php>}}
+
** ''I think the javascript method '''openTo(1, true);''' handles this - see [http://www.destroydrop.com/javascripts/tree/example/ Code generator] source for an example''
 +
*'''root=name''' arg does not allow wikitext e.g. root=[[User:Nad]]
 +
*Allow rootless trees
 +
*Get roots working properly for transcluded subtrees - ''looks like dtree must have at least one root node to work''
 +
**''I've tested this and it doesn't need a root (or rather, it can have a number of root nodes)''
  
 
== See also ==
 
== See also ==
 
*[http://www.destroydrop.com/javascripts/tree/ dTree] (javascript based)
 
*[http://www.destroydrop.com/javascripts/tree/ dTree] (javascript based)
 
*[http://www.silpstream.com/blog/wp-dtree/  Slipstream wp-Dtree] (php based)
 
*[http://www.silpstream.com/blog/wp-dtree/  Slipstream wp-Dtree] (php based)

Latest revision as of 18:11, 22 May 2015

Info.svg This talk page pertains specifically to the development of this extension. For more general discussion about bugs and usage etc, please refer to the mediawiki.org talk page at MW:Extension talk:Tree view

Treeview5.1 is a new approach to the core recursive bullet-list code which doesn't require any voodoo, and should be compatible with all MediaWiki versions including 1.12.

Parameters

  • root: The content of the root node. Later this will also affect how sub-trees work
  • id: A tree must be given a unique identifier if you want it's state to be persistent
  • openlevels: not implemented in 5.x yet
  • open: equals top | bottom
  • close: equals top | bottom

Livelets

There are a couple of issues with the the new tree view with respect to Livelets which we discuss the details of here.

The document.write problem

The first issue which has been resolved in the treeview code concerns the document.write statement used in the JavaScript code which is recommended in the dTree documentation for rendering the tree after its nodes have all been defined.

A dTree is composed of JavaScript statements which execute as the page loads, so when the tree is rendered with a document.write statement, it's converted to a string (by the dTree object's toString method being executed) and then appended to the page at that point. This is fine under normal page load circumstances, but if the tree happens to be included within an AJAX request which populates some arbitrary part of the page after the page has loaded, then the page-appending behaviour of document.write will not achieve the desired result since it will append the tree to the end of the page rather than insert it at the location of the executing code. To get round this Treeview5 surrounds the script in a div element which is identified by an id attribute. The tree can then be converted to a string and written into the div's innerHTML property, using a statement similar to the following:

document.getElementById('{$id}').innerHTML = {$id}.toString();

The script execution problem

The second problem cannot be solved by adjusting the Treeview code, it requires changes to be made to the Livelets extension itself. The problem is that when an article is retrieved via AJAX request instead of as a normal browser page load, the <script> elements will not get executed. This problem has not shown previously because Treeview versions prior to 5 were based on HTML tables containing onClick events rather than being composed completed of JavaScript. To solve this, the Livelet extension must execute a function on receipt of its content rather than just sending it straight to the innerHTML property of its target element. The function must populate the target as usual, and then execute the content of all the script contained within it.

How it works

The initial problem was how to write a parser-function which could generate a tree which could be composed of other transcluded tree's (i.e. parser-functions which are recursively executed forming a larger whole structure together). This seemed impossible since there are no MediaWiki properties available in the environment which reveal the current depth that the parser-function expansion is operating at.

But after thinking about the problem more closely, it turns out that the tree can be constructed without depth information by converting each tree row into a new format containing depth and tree id information which is protected from wiki-parsing. This information can then be converted into tree JavaScript after the parser has finished in the ParserAfterTidy hook.

Here is the wikitext of an example tree which includes two transcluded tree's

Source Rendered Tree
{{#tree:|open=top|close=top|
*[[toplevel]]
**bar
***baz
*foo
**[[Bar]]
***{{:Foo}}
**[[Baz]]
***{{:Bar}}
}}

The expandTree method (the #tree parser-function hook) then converts all the matched rows into the following general structure:

1{uniq}-{id}-{depth}-{item}-2{uniq}

Where uniq is a unique string representing treeview's in general, id is a unique string representing the current tree, depth is the depth of the current row with respect to the current tree root, and item is the wikitext within the current row. Each row is converted to the new structured format by the private formatRow method. The information is "protected" from the parser merely by the fact that it contains no markup characters, but this could be made more robust later.

Notes about the format used

  • If the same article is transcluded more than once it may not get processed again after the first time. This caching effect occurs at an internal level within the parser and is not affected by globals or parser properties. This means that information relating to specific items or sub-tree's could be repeated across transcluded instances of the same article within one or more trees in the page. This is not a problem in the case of the tree-id because only id's of root trees will be used in the final rendered tree.
  • There a potential problem which hasn't been tested yet, with a sub-tree being transcluded twice in consecutive nodes. The tree id is also used to determine when a new tree is starting (by checking if id has changed), so the repeated id problem arising from multiple adjacent transclusion would prevent the loop from knowing about the transition from one of the similar sub-trees to the next.
  • The actual pattern used in the current version is slightly different than the one shown here due to subsequent bug fixes. For example, the dashes used to separate the fields in the formatted rows have been changed to \x7F's. The examples shown are easier to read than the new ones, so they've been left as is. They still demonstrate the process used to render the tree in the current version.

After the resulting formatted-row structure has been fully parsed by the wiki parser, the content portions will have been converted into HTML and the information portions remain unchanged. Below is the HTML source of the above example after parsing and printed at the start of the renderTree method (the ParserAfterTidy hook).

1tv47cb2fd61165f-47cb2fd6dadf4-0-<a href="/wiki/index.php?title=Toplevel&action=edit" class="new" title="Toplevl">toplevel</a>-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6dadf4-1-bar-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6dadf4-2-baz-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6dadf4-0-foo-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6dadf4-1-<a href="/Bar" title="Bar">Bar</a>-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6dadf4-2-1tv47cb2fd61165f-47cb2fd6da03a-0-<a href="/Foo" title="Foo">Foo</a>-2tv47cb2fd61165f-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6da03a-1-bar-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6da03a-2-baz-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6da03a-0-foo-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6da03a-1-biz-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6da03a-2-1tv47cb2fd61165f-47cb2fd6d9c40-0-<a href="/Bar" title="Bar">bar</a>-2tv47cb2fd61165f-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6d9c40-1-baz2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6d9c40-2-biz2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6da03a-1-<a href="/wiki/index.php?title=Baz&action=edit" class="new" title="Baz">Baz</a>-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6dadf4-1-<a href="/wiki/index.php?title=Baz&action=edit" class="new" title="Baz">Baz</a>-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6dadf4-2-1tv47cb2fd61165f-47cb2fd6d9c40-0-<a href="/Bar" title="Bar">bar</a>-2tv47cb2fd61165f-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6d9c40-1-baz-2tv47cb2fd61165f
1tv47cb2fd61165f-47cb2fd6d9c40-2-biz2tv47cb2fd61165f

The following regular expression is used to create an array, $matches, each item of which contains a list of (,$id,$depth,,$icon,$item) extracted from the formatted rows in the page.

preg_match_all("/1$u-(.+?)-([0-9]+)-({$u}3(.+?){$u}4)?(.*?)(?=[12]$u)/",$text,$matches,PREG_SET_ORDER)

Following this extraction of row information into $matches is a loop that builds a new list called $rows which contains more detailed information than $matches, and adjusts the depth values to account for transcluded sub-tree's. The $rows array is a list of ($id,$depth,$icon,$item,$start). $id is the id of each root-tree (sub-tree's are not distinct from their root trees in the final rendered trees), $start is a boolean indicating whether or not the row is the first in a new root tree.

The main dTree JavaScript can then be constructed by looping through the $rows list and converting ordered depth-based information to the parent/child format required by dTree. The indexes of each row within the $rows array are used as id's for the individual dTree nodes.

Adjusting depth to account for sub-tree transclusion

The depth item in the $matches array is directly derived from the number of asterisks defined at the beginning of the row's wikitext. In the $rows array, the depth item has been adjusted to account for the depth of the tree at the point the sub-tree is transcluded, so that it can continue within-ward naturally. This adjustment is done by using the $depths array to associate each tree id with the current depth at each row where a new tree-id is encountered. The depths from $matches are added to the associated $depths values and stored in $rows.

Root tree's and sub-tree's

One of the difficulties with this extension has been in assessing from within a parser-function whether or not it's at the root level in terms of transclusion-depth. The new version doesn't require voodoo to determine this, it instead does the following pattern match on all the formatted rows in the page after wiki-parsing is complete (in the ParserAfterTidy hook).

if (!preg_match_all("/-1$u-(.+?)-/",$text,$subs)) $subs = array(1 => array());

This results in an array called $subs[1] which contains all the tree id's which are not transcluded. It does this by checking which rows have a preceding minus symbol, since top level ones will all be preceded by a p tag or a line start (see the example output of formatted rows above). An id is at root level if it is not in the $subs[1] array. This way is not as robust as it could be, and is also more computationally intensive than it could be.

CSS

The dTree javascript library automatically makes links for the text beside folders. This is overridden if you explicitly set an external or internal link within the tree. It would be nice to distinguish the link colour between MediaWiki's links and the links automatically created beside folders. Below is the css that comes with dtree.

/*--------------------------------------------------|
| dTree 2.05 | www.destroydrop.com/javascript/tree/ |
|---------------------------------------------------|
| Copyright (c) 2002-2003 Geir Landrö               |
|--------------------------------------------------*/

.dtree {
	font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
	font-size: 11px;
	color: #666;
	white-space: nowrap;
}
.dtree img {
	border: 0px;
	vertical-align: middle;
}
.dtree a {
	color: #333;
	text-decoration: none;
}
.dtree a.node, .dtree a.nodeSel {
	white-space: nowrap;
	padding: 1px 2px 1px 2px;
}
.dtree a.node:hover, .dtree a.nodeSel:hover {
	color: #333;
	text-decoration: underline;
}
.dtree a.nodeSel {
	background-color: #c0d2ec;
}
.dtree .clip {
	overflow: hidden;
}

Todo

Note.svg Note: See Code generator for example generation including various javascript options available
  • Need to get inline images working - for simplicity it would be nice to hook into the img directory that comes with dDree
    • I think inline and othe images defined by the user should work off wiki uploaded images and only use dTree images as defaults - the same way the $wgTreeviewImages array works by allowing specified images to override the dTree defaults. The dTree/img folder should not be changed
  • Add open all/close all functionality
  • Implement openlevels arg as treeview maintains persistence now
    • I think the javascript method openTo(1, true); handles this - see Code generator source for an example
  • root=name arg does not allow wikitext e.g. root=User:Nad
  • Allow rootless trees
  • Get roots working properly for transcluded subtrees - looks like dtree must have at least one root node to work
    • I've tested this and it doesn't need a root (or rather, it can have a number of root nodes)

See also