diff --git a/data/System/TopicRecursePlugin.txt b/data/System/TopicRecursePlugin.txt
new file mode 100644
index 0000000..5499fcd
--- /dev/null
+++ b/data/System/TopicRecursePlugin.txt
@@ -0,0 +1,33 @@
+%META:TOPICPARENT{name="Plugins"}%
+---+ Topic Recurse Plugin
+
+%SHORTDESCRIPTION%
+
+
+
+Aims to provide an alternative to:
+ * Foswiki:Extensions.TreePlugin, which is hard-wired to topic parent relationship, and doesn't allow delayed macros in the format string
+ * Foswiki:Extensions.DBCachePlugin DBRECURSE, which has a subtly different query syntax and bypasses the configured Query and Search implementations
+
+%INCLUDE{"VarTOPICRECURSE"}%
+
+---++ Installation
+%$INSTALL_INSTRUCTIONS%
+
+---++ Plugin Info
+
+| Author: | Foswiki:Main.PaulHarvey |
+| Copyright: | © 2010-2011, Paul.W.Harvey@csiro.au TRIN http://trin.org.au/ & %BR%\
+ Centre for Australian National Biodiversity Research http://anbg.gov.au/cpbr %BR%\
+ © 2008-2011, Foswiki Contributors |
+| License: | GPL ([[http://www.gnu.org/copyleft/gpl.html][GNU General Public License]]) |
+| Release: | %$RELEASE% |
+| Version: | %$VERSION% |
+| Change History: | |
+| 04 Nov 2010 (0.1) | Foswikitask:Item9874 - Initial version |
+| Home: | http://foswiki.org/Extensions/%TOPIC% |
+| Support: | http://foswiki.org/Support/%TOPIC% |
+| Development: | http://foswiki.org/Development/%TOPIC% |
diff --git a/data/System/VarTOPICRECURSE.txt b/data/System/VarTOPICRECURSE.txt
new file mode 100644
index 0000000..2cd8498
--- /dev/null
+++ b/data/System/VarTOPICRECURSE.txt
@@ -0,0 +1,79 @@
+%META:TOPICINFO{author="ProjectContributor" date="1287902127" format="1.1" version="$Rev$"}%
+%META:TOPICPARENT{name="Macros"}%
+
%X% This plugin is incomplete. "%X%" against parameters, tokens means they aren't yet implemented. Lack of an %X% means the functionality exists or is untested...
+
+#VarTOPICRECURSE
+---+++ TOPICRECURSE{"< topic >" ...} -- traverse related topics
+ * Traverse topics related by some %SYSTEMWEB%.QuerySearch expression in a recursive (tree-like) pattern, Eg. =parent.name='$super'=
+ * Syntax: =%TOPICRECURSE{"< topic >" ...}%=
+ * Supported parameters:
+ | *Parameter:* | *Description:* | *Default:* |
+ | ="Web.SomeTopic"= %BR% \
+ =root="Web.SomeTopic"= | 'Root', or starting topic. The initial value of =$super= in the =query= param on the first iteration. | =%BASETOPIC%= (current topic) |
+ | =query="<= [[%SYSTEMWEB%.QuerySearch][QuerySearch expression]] =>"= | %SYSTEMWEB%.QuerySearch expression to apply at each recursion step; may be thought of as generating a list of children for a given parent. See [[#QueryTokens][query tokens]] for a list of special tokens allowed in the query string. | =parent.name='$supertopic'= |
+ | =header="..."= %BR% \
+ =footer="..."= | Custom format results: see [[FormattedSearch]] for usage & examples | =*Search: '$rootquery'*$n= %BR% =*Total: $ntopics*= |
+ | =format="..."= %BR% \
+ =formatbranch="..."= %BR% \
+ =formatleaf="..."= | Custom format results: see [[FormattedSearch]] for usage & examples; "branch" format is for nodes which have descendents; "leaf" format is for terminating nodes for which there are no others below. | =$indent* [[$web.$topic][$topic]]= |
+ | =separator="..."= | Separator _between_ search hits. See [[#FormatTokens]] | =$n= |
+ | =nodelimit= | Maximum number of nodes to be displayed. 0 = no limit. | =999= |
+ | =depthlimit= | Maximum depth to recurse to. 0 = no limit. | =0= |
+ | =breadthlimit= %X% | Maximum number of nodes to be displayed _at a given depth_. 0 = no limit. | =0= |
+
+#QueryTokens
+---++++ Query Tokens
+Each "iteration" is a query that produces a list of children under a particular parent which itself is a result of a query under some super-parent (and so-on). That query may have the following tokens:
+| *Name* | *Expands To* |
+| =$root=, =$rootweb=, =$roottopic= | =root= param |
+| =$super=, =$superweb=, =$supertopic= | The topic under consideration (which will be the parent to the topics in the query result) in a given iteration. Same as =$root[web%VBAR%topic]= on first iteration. |
+| =$depth= | The =$super= topic's zero-based depth below =$root=. |
+| =$nodeindex= | The =$super= topic's zero-based index among all rendered nodes of the entire =TOPICRECURSE= expression. |
+| =$siblingindex= | The =$super= topic's zero-based index among its siblings. |
+%X% No other [[FormatTokens][tokens]] are supported in the =query= param
+
+#FormatTokens
+---++++ Standard Tokens
+| *Name* | *Expands To* |
+| =$web= | Name of the web |
+| =$topic= | Topic name |
+| =$parent= | Name of parent topic; empty if not set |
+| =$locked= %X% | LOCKED flag (if any) |
+| =$date= %X% epoch seconds | Time stamp of last topic update, e.g. =%GMTIME{"$day $mon $year - $hour:$min"}%= |
+| =$isodate= %X% | Time stamp of last topic update, e.g. =%GMTIME{"$year-$mo-$dayT$hour:$minZ"}%= |
+| =$index= | number of total results - can be used as a running counter in the =format=, or in the =footer=. This =$index= is not affected by web based partitioning of results. |
+| =$item= | the full name of a result item - in a SEARCH context, equivalent to =$web.$topic= |
+| =$rev= | Number of last topic revision, e.g. =4= |
+| =$username= | Login name of last topic update, e.g. =jsmith= |
+| =$wikiname= | Wiki user name of last topic update, e.g. =JohnSmith= |
+| =$wikiusername= | Wiki user name of last topic update, like =%USERSWEB%.JohnSmith= |
+| =$createdate= %X% epoch seconds | Time stamp of topic revision 1 |
+| =$createusername= | Login name of topic revision 1, e.g. =jsmith= |
+| =$createwikiname= | Wiki user name of topic revision 1, e.g. =JohnSmith= |
+| =$createwikiusername= | Wiki user name of topic revision 1, e.g. =%USERSWEB%.JohnSmith= |
+| =$summary= %X% | Topic summary, just the plain text, all formatting and line breaks removed; up to 162 characters |
+| =$formname= | The name of the form attached to the topic; empty if none |
+| =$formfield(name)= %X% =(works but bypasses Foswiki::Form::...renderFor*= | The field value of a form field; for example, if FAQWhatIsWikiWiki was a search hit, =$formfield(TopicClassification)= would get expanded to =%QUERY{"'FAQWhatIsWikiWiki'/TopicClassification"}%=. This applies only to topics that have a [[DataForms][DataForm]]. For multi-line textfields new lines are replace by an HTML <br /> |
+| =$ntopics= | Number of topics found in current web. This is the current topic count, not the total number of topics |
+| =$nhits= | Number of hits if =multiple="on"=. Cumulative across all topics in current web. Identical to =$ntopics= unless =multiple="on"= |
+%INCLUDE{"FormatTokens"}%
+
+---+++ =TOPICRECURSE= Format Tokens
+| *Name* | *Expands To* |
+| =$depth= | Branch depth, starting at 1 (the level below the root node) |
+| =$indent= | 3 spaces per depth |
+| =$indent(<string>)= | Repeats =<string>= _depth_ times |
+| =$ntopicsdepth= %X% | Number of topics found in current web at the current _depth_ |
+| =$ntopicsbranch= %X% | Number of topics found in current web in the current branch |
+| =$indexdepth= %X% | Running counter of topics found in current web at the current _depth_ |
+| =$indexbranch= %X% | Running counter of topics found in current web in the current branch |
+| =$nodepath= %X% | Comma separated list of =$indexdepth= for all depths between the root node and current, representing the "path" by =$indexdepth= to the current node. |
+
+---++++ Separator tokens
+| *Name* | *Expands To* |
+| =$nextweb=, =$nexttopic=, =$nextwebtopic= %X% | =$web.$topic= of the next node |
+| =$prevweb=, =$prevtopic=, =$prevwebtopic= %X% | =$web.$topic= of the previous node |
+%STOPINCLUDE%
+---
+*Related topics:* FormattedSearch, QuerySearch
+
diff --git a/lib/Foswiki/Plugins/TopicRecursePlugin.pm b/lib/Foswiki/Plugins/TopicRecursePlugin.pm
new file mode 100644
index 0000000..11be40e
--- /dev/null
+++ b/lib/Foswiki/Plugins/TopicRecursePlugin.pm
@@ -0,0 +1,118 @@
+# See bottom of file for default license and copyright information
+
+=begin TML
+
+---+ package TopicRecursePlugin
+
+=cut
+
+package Foswiki::Plugins::TopicRecursePlugin;
+use strict;
+use warnings;
+
+use Foswiki::Func (); # The plugins API
+use Foswiki::Plugins (); # For the API version
+
+our $VERSION = '$Rev$ (06-11-2010)';
+our $RELEASE = '0.1.0';
+
+our $SHORTDESCRIPTION =
+ 'Query topics recursively, inspired by DBCachePlugin\'s DBRECURSE';
+our $NO_PREFS_IN_TOPIC = 1;
+
+my $coreLoaded;
+my $debuglevel;
+
+=begin TML
+
+---++ initPlugin($topic, $web, $user) -> $boolean
+ * =$topic= - the name of the topic in the current CGI query
+ * =$web= - the name of the web in the current CGI query
+ * =$user= - the login name of the user
+ * =$installWeb= - the name of the web the plugin topic is in
+ (usually the same as =$Foswiki::cfg{SystemWebName}=)
+=cut
+
+sub initPlugin {
+ my ( $topic, $web, $user, $installWeb ) = @_;
+
+ # check for Plugins.pm versions
+ if ( $Foswiki::Plugins::VERSION < 2.0 ) {
+ Foswiki::Func::writeWarning( 'Version mismatch between ',
+ __PACKAGE__, ' and Plugins.pm' );
+ return 0;
+ }
+
+ $coreLoaded = 0;
+ Foswiki::Func::registerTagHandler( 'TOPICRECURSE', \&_TOPICRECURSE );
+
+ # Plugin correctly initialized
+ return 1;
+}
+
+sub _TOPICRECURSE {
+ my ( $session, $params, $topic, $web, $topicObject ) = @_;
+
+ if ( not $coreLoaded ) {
+ require Foswiki::Plugins::TopicRecursePlugin::Core;
+ Foswiki::Plugins::TopicRecursePlugin::Core::init();
+ $coreLoaded = 1;
+ }
+
+ return Foswiki::Plugins::TopicRecursePlugin::Core::TOPICRECURSE( $session,
+ $params, $topic, $web, $topicObject );
+}
+
+sub writeDebug {
+ my ( $message, $method, $level, $package, $refdebuglevel ) = @_;
+ my @lines;
+
+ if ( not defined $refdebuglevel ) {
+ $refdebuglevel =
+ ( $debuglevel
+ || $Foswiki::cfg{Plugins}{TopicRecursePlugin}{Debug}
+ || 0 );
+ }
+ if ( $refdebuglevel and ( not defined $level or $level <= $refdebuglevel ) )
+ {
+ @lines = split( /[\r\n]+/, $message );
+ foreach my $line (@lines) {
+ my @packparts = split( /::/, ( $package || __PACKAGE__ ) );
+ my $logline = '::'
+ . $packparts[ scalar(@packparts) - 1 ]
+ . "::$method():\t$line\n";
+
+ if ( defined &Foswiki::Func::writeDebug ) {
+ Foswiki::Func::writeDebug($logline);
+ }
+ else { # CLI
+ print STDERR $logline;
+ }
+ }
+ }
+
+ return;
+}
+
+1;
+
+__END__
+Foswiki - The Free and Open Source Wiki, http://foswiki.org/
+
+Copyright (C) 2010-2011 Paul.W.Harvey@csiro.au, TRIN http://trin.org.au/ &
+Centre for Australian National Biodiversity Research http://anbg.gov.au/cpbr
+Copyright (C) 2008-2010 Foswiki Contributors. Foswiki Contributors
+are listed in the AUTHORS file in the root of this distribution.
+NOTE: Please extend that file, not this notice.
+
+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. For
+more details read LICENSE in the root of this distribution.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+As per the GPL, removal of this notice is prohibited.
diff --git a/lib/Foswiki/Plugins/TopicRecursePlugin/Config.spec b/lib/Foswiki/Plugins/TopicRecursePlugin/Config.spec
new file mode 100644
index 0000000..0503b84
--- /dev/null
+++ b/lib/Foswiki/Plugins/TopicRecursePlugin/Config.spec
@@ -0,0 +1,8 @@
+# ---+ Extensions
+# ---++ TopicRecursePlugin
+# ---+++ Debug
+# **NUMBER**
+# 0 = no debug log output, 1 = some, 2 = lots, 3 = maximum, 4 = insane
+$Foswiki::cfg{Plugins}{TopicRecursePlugin}{Debug} = 0;
+
+1;
diff --git a/lib/Foswiki/Plugins/TopicRecursePlugin/Core.pm b/lib/Foswiki/Plugins/TopicRecursePlugin/Core.pm
new file mode 100644
index 0000000..0818736
--- /dev/null
+++ b/lib/Foswiki/Plugins/TopicRecursePlugin/Core.pm
@@ -0,0 +1,185 @@
+# See bottom of file for default license and copyright information
+
+=begin TML
+
+---+ package TopicRecursePlugin
+
+=cut
+
+package Foswiki::Plugins::TopicRecursePlugin::Core;
+use strict;
+use warnings;
+
+use Assert;
+use Data::Dumper;
+use Foswiki();
+use Foswiki::Func();
+use Foswiki::Plugins::TopicRecursePlugin();
+use Foswiki::Plugins::TopicRecursePlugin::Node();
+
+sub init {
+
+ return;
+}
+
+sub extractParams {
+ my ( $params, @list ) = @_;
+ my %args;
+
+ foreach my $item (@list) {
+ $args{$item} = $params->{$item};
+ }
+ $args{header} ||= '*Search: \'$rootquery\' from $root* $n';
+ $args{format} = '$indent* [[$web.$topic][$topic]]';
+ $args{formatbranch} ||= $args{format};
+ $args{formatleaf} ||= $args{format};
+ $args{separator} ||= '$n';
+ $args{footer} ||= '*Total: $ntopics*';
+ $args{nodelimit} ||= '999';
+
+ return \%args;
+}
+
+sub TOPICRECURSE {
+ my ( $session, $params, $baseTopic, $baseWeb, $topicObject ) = @_;
+ my $currentWeb = $session->{webName};
+ my $currentTopic = $session->{topicName};
+ my %queryArgs = %{$params};
+ my $rootParam = $params->{root} || $params->{_DEFAULT};
+ my ( $rootWeb, $rootTopic ) =
+ Foswiki::Func::normalizeWebTopicName( $currentWeb, $rootParam );
+ my $rootNode;
+ my $result;
+
+ $rootNode = Foswiki::Plugins::TopicRecursePlugin::Node->new(
+ undef, undef,
+ webtopic => "$rootWeb.$rootTopic",
+ query => $params->{query} || 'parent.name=\'$supertopic\'',
+ queryArgs => \%queryArgs
+ );
+
+ if ($rootNode) {
+ writeDebug( "rootNode: $rootNode->{webtopic}", 'TOPICRECURSE', 4 );
+ my $spec = extractParams(
+ $params,
+ qw(header formatbranch formatleaf separator footer),
+ qw(nodelimit depthlimit breadthlimit)
+ );
+ my @renderednodes = formatNodes( $rootNode, $spec );
+ my $separator = Foswiki::expandStandardEscapes( $spec->{separator} );
+ writeDebug(
+ "Separator: $separator, rendered: " . Dumper( \@renderednodes ),
+ 'TOPICRECURSE', 4 );
+ $result = renderNode( $rootNode, $spec->{header} )
+ . join( $separator,
+ @renderednodes, renderNode( $rootNode, $spec->{footer} ) );
+ }
+ else {
+ $result =
+ 'Error executing query: '
+ . $params->{query}
+ . '';
+ }
+
+ return $result;
+}
+
+sub formatNodes {
+ my ( $node, $spec ) = @_;
+ my @renderednodes;
+
+ writeDebug( "$node->{webtopic}: rendering..." . Dumper($spec),
+ 'TOPICRECURSE', 4 );
+
+ # Is a branch?
+ if ( $node->hasNext() ) {
+ my $result = renderNode( $node, $spec->{formatbranch} );
+
+ if ( not $node->isRoot() ) {
+ push( @renderednodes, $result );
+ }
+ }
+ else {
+
+ # is a leaf.
+ my $result = renderNode( $node, $spec->{formatleaf} );
+
+ if ( not $node->isRoot() ) {
+ push( @renderednodes, $result );
+ }
+ }
+ while ( $node->hasNext() ) {
+ my $child = $node->next();
+ push( @renderednodes, formatNodes( $child, $spec ) );
+ }
+
+ return @renderednodes;
+}
+
+sub renderNode {
+ my ( $node, $format ) = @_;
+ my %tokens = ( $node->getStandardTokens() );
+
+ return execFormat( $node, \%tokens, $format );
+}
+
+sub execFormat {
+ my ( $node, $tokens, $format ) = @_;
+ my $result = $format;
+
+ writeDebug( "Format: $format", 'execFormat', 4 );
+ $result =~
+ s/(\$([a-z]+)(\(([^\)]+)\))?)\b/execToken($node, $tokens, $1, $2, $4)/ge;
+ $result = Foswiki::expandStandardEscapes($result);
+
+ return $result;
+}
+
+sub execToken {
+ my ( $node, $tokens, $completetoken, $tokenname, $tokenarg ) = @_;
+ my $result;
+
+ if ( exists $tokens->{$tokenname} ) {
+ $result = $tokens->{$tokenname};
+ }
+ elsif ( exists $tokens->{"$tokenname()"} ) {
+ $result = $tokens->{"$tokenname()"}->( $node, $tokenarg );
+ }
+ else {
+ $result = $completetoken;
+ }
+ writeDebug( "\$$tokenname($tokenarg) = '$result' depth: $node->{depth}",
+ 'execToken', 4 );
+
+ return $result;
+}
+
+sub writeDebug {
+ my ( $message, $method, $level ) = @_;
+
+ return Foswiki::Plugins::TopicRecursePlugin::writeDebug( $message, $method,
+ $level, __PACKAGE__ );
+}
+
+1;
+
+__END__
+Foswiki - The Free and Open Source Wiki, http://foswiki.org/
+
+Copyright (C) 2010-2011 Paul.W.Harvey@csiro.au, TRIN http://trin.org.au/ &
+Centre for Australian National Biodiversity Research http://anbg.gov.au/cpbr
+Copyright (C) 2008-2010 Foswiki Contributors. Foswiki Contributors
+are listed in the AUTHORS file in the root of this distribution.
+NOTE: Please extend that file, not this notice.
+
+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. For
+more details read LICENSE in the root of this distribution.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+As per the GPL, removal of this notice is prohibited.
diff --git a/lib/Foswiki/Plugins/TopicRecursePlugin/DEPENDENCIES b/lib/Foswiki/Plugins/TopicRecursePlugin/DEPENDENCIES
new file mode 100644
index 0000000..e69de29
diff --git a/lib/Foswiki/Plugins/TopicRecursePlugin/MANIFEST b/lib/Foswiki/Plugins/TopicRecursePlugin/MANIFEST
new file mode 100644
index 0000000..ca1f3f8
--- /dev/null
+++ b/lib/Foswiki/Plugins/TopicRecursePlugin/MANIFEST
@@ -0,0 +1,6 @@
+data/System/TopicRecursePlugin.txt 0644
+data/System/VarTOPICRECURSE.txt 0644
+lib/Foswiki/Plugins/TopicRecursePlugin.pm 0644
+lib/Foswiki/Plugins/TopicRecursePlugin/Config.spec 0644
+lib/Foswiki/Plugins/TopicRecursePlugin/Core.pm 0644
+lib/Foswiki/Plugins/TopicRecursePlugin/Node.pm 0644
diff --git a/lib/Foswiki/Plugins/TopicRecursePlugin/Node.pm b/lib/Foswiki/Plugins/TopicRecursePlugin/Node.pm
new file mode 100644
index 0000000..99227d9
--- /dev/null
+++ b/lib/Foswiki/Plugins/TopicRecursePlugin/Node.pm
@@ -0,0 +1,445 @@
+# See bottom of file for default license and copyright information
+
+=begin TML
+
+---+ package TopicRecursePlugin::Node
+
+=cut
+
+package Foswiki::Plugins::TopicRecursePlugin::Node;
+use strict;
+use warnings;
+
+use Assert;
+use Data::Dumper;
+use Foswiki::Func();
+use Foswiki::Plugins::TopicRecursePlugin();
+
+use Foswiki::Iterator();
+our @ISA = ('Foswiki::Iterator');
+
+sub new {
+ my ( $class, $rootNode, $superNode, %args ) = @_;
+ my $this = bless( \%args, $class );
+
+ # writeDebug("args: " . Dumper(\%args), 'new', 4);
+ ASSERT( defined $this->{webtopic} );
+ ( $this->{web}, $this->{topic} ) =
+ Foswiki::Func::normalizeWebTopicName( '', $this->{webtopic} );
+ ASSERT( $this->{web} and $this->{topic} );
+
+ $this->{superNode} = $superNode;
+ $this->{rootNode} = $rootNode;
+ if ( not defined $this->{rootNode} ) {
+ ASSERT( not defined $this->{superNode} );
+ $this->{rootNode} = $this;
+ $this->{superNode} = $this;
+ $this->{isRoot} = 1;
+ $this->{depth} = 0;
+ $this->{nodecount} = 0;
+ $this->{nodeindex} = 0;
+ $this->{siblingindex} = 0;
+ }
+ else {
+ ASSERT( defined $this->{superNode} );
+ $this->{isRoot} = 0;
+ ASSERT( not defined $this->{nodecount} );
+ $this->{nodeindex} = $this->{rootNode}->{nodecount};
+ $this->{rootNode}->{nodecount} += 1;
+ $this->{depth} ||= $this->{superNode}->{depth} + 1;
+ writeDebug( "$this->{webtopic} has depth $this->{depth}", 'new', 4 );
+ }
+
+ # Only root node has the initial query
+ # Only root node has the query args
+ # Only root node has a total nodecount
+ # Root node = zero depth, nodeindex, siblingindex; subsequent nodes non-zero
+ writeDebug( <<"HERE", 'new', 4 );
+$this->{superNode}->{webtopic} now has child \@depth,siblingindex,nodeindex=$this->{depth},$this->{siblingindex},$this->{nodeindex}:\t$this->{webtopic}
+HERE
+ if ( $this->{isRoot} ) {
+ ASSERT( $this->{query} );
+ ASSERT( defined $this->{queryArgs} );
+ ASSERT( defined $this->{nodecount} );
+ ASSERT(
+ not( $this->{depth} or $this->{nodeindex} or $this->{siblingindex} )
+ );
+ }
+ else {
+ ASSERT( not defined $this->{query} );
+ ASSERT( not defined $this->{queryArgs} );
+ ASSERT( not defined $this->{nodecount} );
+ ASSERT( $this->{depth} > 0 );
+ ASSERT( defined $this->{nodeindex} );
+ ASSERT( defined $this->{siblingindex} );
+ }
+ ASSERT( not defined $this->{childnodes} );
+ $this->{childNodes} = [];
+ ASSERT( not defined $this->{childCursor} );
+
+ # -1 because we have to ->next() to populate the first child
+ $this->{childCursor} = 0 - 1;
+
+ #Lazily build query string when we call getQueryIterator()
+ $this->{ourQuery} ||= '';
+ $this->{queryIterator} = undef;
+ $this->{queryIteratorExhausted} = 0;
+
+ return $this;
+}
+
+sub destroy {
+ my ($this) = @_;
+
+ if ( $this->{topicObject} ) {
+ $this->{topicObject}->finish();
+ }
+ $this->{topicObject} = undef;
+ $this->{topicObjectFirstRev} = undef;
+ $this->{web} = undef;
+ $this->{topic} = undef;
+ $this->{webtopic} = undef;
+ $this->{superNode} = undef;
+ $this->{rootNode} = undef;
+ $this->{isRoot} = undef;
+ $this->{childNodes} = undef;
+ $this->{childCursor} = undef;
+ $this->{depth} = undef;
+ $this->{nodecount} = undef;
+ $this->{nodeindex} = undef;
+ $this->{siblingindex} = undef;
+ $this->{query} = undef;
+ $this->{ourQuery} = undef;
+ $this->{queryArgs} = undef;
+ $this->{queryIterator} = undef;
+ $this->{queryIteratorExhausted} = undef;
+
+ return;
+}
+
+###############################################################################
+# Foswiki::Iterator interface to the child nodes.
+# Is there a next child node?
+sub hasNext {
+ my ($this) = @_;
+ my $haveNext = 0;
+ my $populatedChildren = scalar( @{ $this->{childNodes} } );
+
+ ASSERT( $this->{childCursor} < $populatedChildren );
+
+ # Are we at the end of our copy of the query resultset already?
+ if ( $this->{childCursor} == $populatedChildren - 1 ) {
+
+ # And does the query resultset have more to give us?
+ if ( not $this->{queryIteratorExhausted} ) {
+ my $queryArgs = $this->{rootNode}->{queryArgs};
+ if (
+ $this->getQueryIterator()->hasNext()
+ and (
+ (
+ not $queryArgs->{nodelimit}
+ or $this->{rootNode}->{nodecount} <
+ $queryArgs->{nodelimit}
+ )
+ and ( not $queryArgs->{depthlimit}
+ or $this->{depth} < $queryArgs->{depthlimit} )
+ )
+ )
+ {
+ $haveNext = 1;
+ writeDebug(
+ "$this->{webtopic} has more than "
+ . ( $this->{childCursor} + 1 )
+ . " children",
+ 'hasNext', 4
+ );
+ }
+ else {
+
+ # We were already on the last element of the resultset.
+ $this->{queryIteratorExhausted} = 1;
+ writeDebug(
+ "$this->{webtopic} has "
+ . ( $this->{childCursor} + 1 )
+ . " children, no more left",
+ 'hasNext', 4
+ );
+ }
+ }
+ }
+ else {
+ $haveNext = 1;
+ }
+
+ return $haveNext;
+}
+
+# Get the next child node
+sub next {
+ my ($this) = @_;
+ my $populatedChildren = scalar( @{ $this->{childNodes} } );
+ my $nextNode;
+
+ ASSERT( $this->{childCursor} < $populatedChildren );
+
+ # We work on the assumption that there is a next node either in our list or
+ # in the query iterator. So, do we need to add to list from query iterator?
+ if ( $this->{childCursor} == $populatedChildren - 1 ) {
+ my $nextWebTopic = $this->getQueryIterator()->next();
+
+ ASSERT($nextWebTopic);
+ $this->{childCursor} += 1;
+ $nextNode = Foswiki::Plugins::TopicRecursePlugin::Node->new(
+ $this->{rootNode},
+ $this->{superNode},
+ webtopic => $nextWebTopic,
+ siblingindex => $this->{childCursor},
+ depth => $this->{depth} + 1
+ );
+ push( @{ $this->{childNodes} }, $nextNode );
+ ASSERT( ( $this->{childCursor} + 1 ) ==
+ scalar( @{ $this->{childNodes} } ) );
+ }
+ else {
+
+ # We already have this node
+ $nextNode = $this->{childNodes}->[ $this->{childCursor} ];
+ $this->{childCursor} += 1;
+ writeDebug( "$this->{webtopic} already had child $nextNode->{webtopic}",
+ 'next', 4 );
+ }
+ ASSERT($nextNode);
+
+ return $nextNode;
+}
+
+# Reset the child node cursor to the start
+sub reset {
+ my ($this) = @_;
+
+ # minus one because the first node needs a ->next(), *then* the cursor is @0
+ $this->{childCursor} = 0 - 1;
+
+ return;
+}
+###############################################################################
+
+sub getQueryString {
+ my ($this) = @_;
+ my $rootNode = $this->{rootNode};
+ my $superNode = $this->{superNode};
+
+ if ( not $this->{ourQuery} ) {
+ my $query = $rootNode->{query};
+
+ ASSERT($query);
+ $query =~ s/\$root\b/$rootNode->{webtopic}/g;
+ $query =~ s/\$rootweb\b/$rootNode->{web}/g;
+ $query =~ s/\$roottopic\b/$rootNode->{topic}/g;
+ $query =~ s/\$super\b/$this->{webtopic}/g;
+ $query =~ s/\$superweb\b/$this->{web}/g;
+ $query =~ s/\$supertopic\b/$this->{topic}/g;
+ $query =~ s/\$depth\b/$this->{depth}/g;
+ $query =~ s/\$nodeindex\b/$this->{nodeindex}/g;
+ $query =~ s/\$siblingindex\b/$this->{siblingindex}/g;
+ $this->{ourQuery} = $query;
+ writeDebug( "$this->{webtopic} now has query: '$this->{ourQuery}'",
+ 'getQueryString', 4 );
+ }
+ ASSERT( $this->{ourQuery} );
+
+ return $this->{ourQuery};
+}
+
+sub getQueryIterator {
+ my ($this) = @_;
+
+ if ( not $this->{queryIterator} ) {
+ my %queryArgs = %{ $this->{rootNode}->{queryArgs} };
+
+ $queryArgs{web} = $this->{web};
+ $this->{queryIterator} =
+ Foswiki::Func::query( $this->getQueryString(), undef, \%queryArgs );
+ writeDebug( "$this->{webtopic} created queryIterator",
+ 'getQueryIterator', 4 );
+ }
+ ASSERT( $this->{queryIterator} );
+
+ return $this->{queryIterator};
+}
+
+sub depth {
+ my ($this) = @_;
+
+ return $this->{depth};
+}
+
+sub siblingindex {
+ my ($this) = @_;
+
+ return $this->{siblingindex};
+}
+
+sub nodeindex {
+ my ($this) = @_;
+
+ return $this->{nodeindex};
+}
+
+sub web {
+ my ($this) = @_;
+
+ return $this->{web};
+}
+
+sub topic {
+ my ($this) = @_;
+
+ return $this->{topic};
+}
+
+sub webtopic {
+ my ($this) = @_;
+
+ return $this->{webtopic};
+}
+
+sub super {
+ my ($this) = @_;
+
+ return $this->{superNode};
+}
+
+sub root {
+ my ($this) = @_;
+
+ return $this->{rootNode};
+}
+
+sub isRoot {
+ my ($this) = @_;
+
+ return $this->{isRoot};
+}
+
+sub getStandardTokens {
+ my ($this) = @_;
+ my ( $createdate, $createcuid ) =
+ Foswiki::Func::getRevisionInfo( $this->{web}, $this->{topic}, 1 ),
+
+ return (
+ super => $this->{superNode}->{webtopic},
+ superweb => $this->{superNode}->{web},
+ supertopic => $this->{superNode}->{topic},
+ root => $this->{rootNode}->{webtopic},
+ rootweb => $this->{rootNode}->{web},
+ roottopic => $this->{rootNode}->{topic},
+ web => $this->{web},
+ topic => $this->{topic},
+ depth => $this->{depth},
+ rootquery => $this->{rootNode}->{query},
+ siblingindex => $this->{siblingindex},
+ nodeindex => $this->{nodeindex},
+ indent => $this->buildIndent(' '),
+ 'indent()' => sub { $this->buildIndent(@_) },
+ 'parent()' => sub { $this->getTopicObject()->getParent() },
+ 'date()' => sub { $this->getTopicObject()->getRevisionInfo()->{date} },
+ index => $this->{nodeindex},
+ item => $this->{webtopic},
+ 'rev()' => sub { $this->getTopicObject()->getLoadedRev() },
+ 'username()' =>
+ sub { $this->getTopicObject()->getRevisionInfo()->{author} },
+ 'wikiname()' => sub {
+ Foswiki::Func::getWikiName(
+ $this->getTopicObject()->getRevisionInfo()->{author} );
+ },
+ 'wikiusername()' => sub {
+ Foswiki::Func::getWikiUserName(
+ $this->getTopicObject()->getRevisionInfo()->{author} );
+ },
+ 'createdate()' =>
+ sub { $this->getTopicObject(1)->getRevisionInfo()->{date} },
+ 'createusername()' =>
+ sub { $this->getTopicObject(1)->getRevisionInfo()->{author} },
+ 'createwikiname()' => sub {
+ Foswiki::Func::getWikiName(
+ $this->getTopicObject(1)->getRevisionInfo()->{author} );
+ },
+ 'createwikiusername()' => sub {
+ Foswiki::Func::getWikiUserName(
+ $this->getTopicObject(1)->getRevisionInfo()->{author} );
+ },
+ 'formname()' => sub { $this->getTopicObject()->getFormName() },
+
+ # TODO: Make this actually do the SEARCH equivalent (bypassing renderFor)
+ 'formfield()' => sub { $this->getTopicObject()->get( 'FIELD', $_[0] ) },
+ ntopics => $this->{rootNode}->{nodecount},
+
+ # SMELL: What the...
+ nhits => $this->{rootNode}->{nodecount},
+ );
+}
+
+sub getTopicObject {
+ my ( $this, $rev ) = @_;
+ my $theTopic;
+
+ if ($rev) {
+ if ( not $this->{topicObjectFirstRev} ) {
+ $this->{topicObjectFirstRev} =
+ Foswiki::Func::Meta->new( $Foswiki::Plugins::SESSION,
+ $this->{web}, $this->{topic} );
+ $this->{topicObjectFirstRev}->load($rev);
+ }
+ $theTopic = $this->{topicObjectFirstRev};
+ }
+ elsif ( not $this->{topicObject} ) {
+ ( $this->{topicObject} ) =
+ Foswiki::Func::readTopic( $this->{web}, $this->{topic} );
+ $theTopic = $this->{topicObject};
+ }
+
+ return $theTopic;
+}
+
+sub buildIndent {
+ my ( $this, $string ) = @_;
+ my $result = '';
+
+ foreach ( 1 .. ( $this->{depth} ) ) {
+ $result .= $string;
+ }
+ writeDebug( "Did string indent: '$result'", 'buildIndent', 4 );
+
+ return $result;
+}
+
+sub writeDebug {
+ my ( $message, $method, $level ) = @_;
+
+ return Foswiki::Plugins::TopicRecursePlugin::writeDebug( $message, $method,
+ $level, __PACKAGE__ );
+}
+
+1;
+
+__END__
+Foswiki - The Free and Open Source Wiki, http://foswiki.org/
+
+Copyright (C) 2010-2011 Paul.W.Harvey@csiro.au, TRIN http://trin.org.au/ &
+Centre for Australian National Biodiversity Research http://anbg.gov.au/cpbr
+Copyright (C) 2008-2010 Foswiki Contributors. Foswiki Contributors
+are listed in the AUTHORS file in the root of this distribution.
+NOTE: Please extend that file, not this notice.
+
+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. For
+more details read LICENSE in the root of this distribution.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+As per the GPL, removal of this notice is prohibited.
diff --git a/lib/Foswiki/Plugins/TopicRecursePlugin/build.pl b/lib/Foswiki/Plugins/TopicRecursePlugin/build.pl
new file mode 100755
index 0000000..f8cb0aa
--- /dev/null
+++ b/lib/Foswiki/Plugins/TopicRecursePlugin/build.pl
@@ -0,0 +1,55 @@
+#!/usr/bin/perl -w
+#
+# Example build class. Copy this file to the equivalent place in your
+# plugin or contrib and edit.
+#
+# Read the comments at the top of lib/Foswiki/Contrib/Build.pm for
+# details of how the build process works, and what files you
+# have to provide and where.
+#
+# Requires the environment variable FOSWIKI_LIBS (a colon-separated path
+# list) to be set to point at the build system and any required dependencies.
+# Usage: ./build.pl [-n] [-v] [target]
+# where [target] is the optional build target (build, test,
+# install, release, uninstall), test is the default.
+# Two command-line options are supported:
+# -n Don't actually do anything, just print commands
+# -v Be verbose
+#
+
+# Standard preamble
+use strict;
+
+BEGIN {
+ unshift @INC, split( /:/, $ENV{FOSWIKI_LIBS} );
+}
+
+use Foswiki::Contrib::Build;
+
+# Declare our build package
+package BuildBuild;
+use Foswiki::Contrib::Build;
+our @ISA = qw( Foswiki::Contrib::Build );
+
+sub new {
+ my $class = shift;
+ return bless( $class->SUPER::new( "QueryRecursePlugin" ), $class );
+}
+
+# Example: Override the build target
+sub target_build {
+ my $this = shift;
+
+ $this->SUPER::target_build();
+
+ # Do other build stuff here
+}
+
+package main;
+
+# Create the build object
+my $build = new BuildBuild();
+
+# Build the target on the command line, or the default target
+$build->build($build->{target});
+