# TWiki ($wikiversion has version info)
#
# Copyright (C) 2002 Slava Kozlov,
#
# 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.
#
# 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. See the
# GNU General Public License for more details, published at
# http://www.gnu.org/copyleft/gpl.html
#
# =========================
# =========================
package TWiki::Plugins::TreePlugin;
use TWiki::Func;
use TWiki::Plugins::TreePlugin::TWikiNode;
use TWiki::Plugins::TreePlugin::ListNodeFormatter;
use TWiki::Plugins::TreePlugin::ColorNodeFormatter;
use TWiki::Plugins::TreePlugin::FormatOutlineNodeFormatter;
use TWiki::Plugins::TreePlugin::HOutlineNodeFormatter;
use TWiki::Plugins::TreePlugin::ImgNodeFormatter;
# =========================
use vars qw(
$web $topic $user $installWeb $VERSION $debug $INTREE
%FormatMap $RootLabel
);
$VERSION = '$Rev: 11234 $';
$RootLabel = "_RootLabel_"; # what we use to label the root of a tree if not a topic
# =========================
sub initPlugin
{
( $topic, $web, $user, $installWeb ) = @_;
# check for Plugins.pm versions
if( $TWiki::Plugins::VERSION < 1 ) {
&TWiki::Func::writeWarning( "Version mismatch between TreePlugin and Plugins.pm" );
return 0;
}
# Get plugin preferences, the variable defined by: * Set EXAMPLE = ...
# $exampleCfgVar = &TWiki::Prefs::getPreferencesValue( "TreePlugin_EXAMPLE" ) || "default";
# Get plugin debug flag
$debug = &TWiki::Func::getPreferencesFlag( "TreePlugin_DEBUG" );
&TWiki::Func::writeDebug( "installWeb: $installWeb" ) if $debug;
my $cgi = &TWiki::Func::getCgiQuery();
if( ! $cgi ) {
return 0;
}
# Plugin correctly initialized
&TWiki::Func::writeDebug( "- TWiki::Plugins::TreePlugin::initPlugin( $web.$topic ) is OK" ) if $debug;
return 1;
}
# =========================
sub commonTagsHandler
{
### my ( $text, $topic, $web ) = @_; # do not uncomment, use $_[0], $_[1]... instead
#&TWiki::Func::writeDebug( "- TreePlugin::commonTagsHandler( $_[2].$_[1] )" ) if $debug;
#$_[0] =~ s/%CREATECHILD%/&handleCreateChildInput(0, $_[2])/geo;
#$_[0] =~ s/%CREATECHILD{(.*?)}%/&handleCreateChildInput($1, $_[2])/geo;
$_[0] =~ s/%TREEVIEW%/&handleTreeView($_[1], $_[2], "")/geo;
$_[0] =~ s/%TREEVIEW{(.*?)}%/&handleTreeView($_[1], $_[2], $1)/geo;
}
# given attribute and formatter
# returns \n-seperated list of topics,
# each topic line is composed of
# topicname|modtime|author|summary (if applicable)
sub _getSearchString {
my ($attrWeb, $attributes, $formatter) = @_;
my $attrForm = TWiki::Func::extractNameValuePair( $attributes, "form" ) || "";
my $searchVal = ".*";
my $searchScope = "topic";
# not functioning
# if ($attrForm) {
# $searchVal = "%META:FORM\{.*name=\\\"$attrForm\\\"\}%";
# $searchScope = "text";
# }
my $searchWeb = ($attrWeb) ? $attrWeb : "all" ;
my $searchTmpl = "\$topic|%TIME%|%AUTHOR%";
# optimization: remember not to save heavy memory values
if ($formatter->data("format") && $formatter->data("format") =~ m/\$(summary|text)/) {
$formatter->data("fullSubs", 1);
$searchTmpl .= "|\$summary";
}
# ok. make the topic list and return it (use this routine for now)
# hopefully there'll be an optimized one later
return TWiki::Func::expandCommonVariables( "%SEARCH{search=\"$searchVal\" web=\"$searchWeb\" format=\"$searchTmpl\" scope=\"$searchScope\" regex=\"on\" nosearch=\"on\" nototal=\"on\" noempty=\"on\"}%");
}
# bugs re recursion:
# 1) doesn't remember webs so: recursion across webs is problematic
# 2) if two topics with identical names in different webs AND
# both have a TREEVIEW tag -> the second will be excluded
$AGdebugmsg = " AG debug message ";
# bugs re recursion:
# 1) doesn't remember webs so: recursion across webs is problematic
# 2) if two topics with identical names in different webs AND
# both have a TREEVIEW tag -> the second will be excluded
sub handleTreeView {
my ($topic, $web, $attributes) = @_;
my $cgi = &TWiki::Func::getCgiQuery();
my $plist = $cgi->query_string();
$plist .= "\&" if $plist;
$CurrUrl = $cgi->url . $cgi->path_info() . "?" . $plist;
# $CurrUrl =~ s/\&/\&/go;
my $attrWeb = TWiki::Func::extractNameValuePair( $attributes, "web" ) || $web || "";
my $attrTopic = TWiki::Func::extractNameValuePair( $attributes, "topic" ) || $RootLabel; # ie, do all web, needs to be nonempty
cgiOverride(\$attrWeb, "treeweb");
cgiOverride(\$attrTopic, "treetopic");
cgiOverride(\$attrFormatting, "formatting");
# we've expanded TREESEARCH on this topic before, we won't repeat
# return "" if ($TreeTopics{$topic});
# # global hash, record this object and attrTopic too (as initial seed)
# $TreeTopics{$topic} = 1;
# $TreeTopics{$attrTopic} = 1;
my $attrHeader = "
" || "";
$attrHeader .= "\n" if ($attrHeader); # to enable |-tables formatting
my $attrFormat = TWiki::Func::extractNameValuePair( $attributes, "format" ) || "";
$attrFormat .= "\n" if ($attrFormat); # to enable |-tables formatting
my $attrFormatBranch = TWiki::Func::extractNameValuePair( $attributes, "formatbranch" ) || "";
my $attrFormatting = TWiki::Func::extractNameValuePair( $attributes, "formatting" ) || "";
my $attrStoplevel = TWiki::Func::extractNameValuePair( $attributes, "stoplevel" ) || 999;
my $doBookView = TWiki::Func::extractNameValuePair( $attributes, "bookview" ) || "";
my $attrLevelPrefix = TWiki::Func::extractNameValuePair( $attributes, "levelprefix" ) || "";
cgiOverride(\$attrFormatting, "formatting");
# set the type of formatting
my $formatter = setFormatter($attrFormatting);
$formatter->data("stoplevel", $attrStoplevel);
$formatter->data("url", $CurrUrl);
# if bookView, read bookview file as format
if ($doBookView) {
$formatter->data("format", &TWiki::Func::readTemplate( "booktree" ));
} else {
# else set the format(s), if any
$formatter->data("format", $attrFormat) if ($attrFormat);
$formatter->data("branchformat",$attrFormatBranch) if ($attrFormatBranch);
$formatter->data("levelprefix",$attrLevelPrefix) if ($attrLevelPrefix);
}
# get search results
my $search = _getSearchString($attrWeb, $attributes, $formatter);
my %nodes = ();
my $root = _findTWikiNode($RootLabel, \%nodes); # make top dog node
# loop thru topics
foreach (split /\n/, $search) {
my ($topic, $modTime, $author, $summary) = split /\|/; # parse out data
# get parent
my( $meta, $text ) = &TWiki::Func::readTopic( $attrWeb, $topic );
my $ref=0;
if ($TWiki::Plugins::VERSION < 1.1)
{
$ref = $meta->findOne( "TOPICPARENT" );
}
else
{
$ref = $meta->get( "TOPICPARENT" );
}
my %par = (defined $ref ? %$ref : ());
my $parent = ( %par )
? _findTWikiNode($par{"name"}, \%nodes) # yes i have a parent, get it
: $root; # otherwise root's my parent
# create my node (or if it's already created get it)
my $node = _findTWikiNode($topic, \%nodes);
$node->data("author", $author);
$node->data("web", $attrWeb);
$node->data("modTime", $modTime);
# big memory items, only save if need to
if ( $formatter->data("fullSubs") ) {
$node->data("summary", $summary);
$node->data("text", $text);
$node->data("meta", $meta);
}
$node->data("parent", $parent);
$parent->add_child($node); # hook me up
}
return "" unless $nodes{$attrTopic}; # nope, the wanted node ain't here
$root->name(" "); # change root's name so it don't show up, hack
# format the tree & parse TWiki tags and rendering
my $renderedTree = "";
if( $attrTopic ne $RootLabel ) {
# running from a given topic
$renderedTree = TWiki::Func::expandCommonVariables
(
$attrHeader.$nodes{$attrTopic}->toHTMLFormat($formatter),
$attrTopic,
$attrWeb);
} else {
# no starting topic given so do all topics
my %rootnodes = %{_findRootsBreakingCycles( \%nodes )};
foreach my $i ( sort keys( %rootnodes ) ) {
$renderedTree .= TWiki::Func::expandCommonVariables
($attrHeader.$rootnodes{$i}->toHTMLFormat($formatter),
$attrTopic,
$attrWeb);
}
}
#$renderedTree = $AGdebugmsg . $renderedTree;
$renderedTree = "
" . $renderedTree . "
";
#SL: Substitute $index in the rendered tree, $index is most useful to implement menus in combination with TreeBrowserPlugin
if (defined $formatter->data("format"))
{
my $Index=0;
$renderedTree =~ s/\$Index/$Index++;$Index/egi;
}
return $renderedTree;
}
sub _findRootsBreakingCycles {
my ($hashMappingNamesToNodes) = @_;
my %roots = ();
$AGdebugmsg = "";
foreach my $i ( sort keys( %$hashMappingNamesToNodes ) ) {
my $ultimateParentNode = _findUltimateParentBreakingCycles( ${$hashMappingNamesToNodes}{$i} );
$roots{$ultimateParentNode->name()} = $ultimateParentNode;
}
return \%roots;
}
sub _findUltimateParentBreakingCycles {
my $orignode = shift;
my $node = $orignode;
my %alreadyvisited = ();
while( $parent = _findParent( $node ) ) {
# break cycles
if( $alreadyvisited{$parent->name()} ) {
$AGdebugmsg = $AGdebugmsg . "pre-rm:" . $parent->toStringNonRecursive() . " \n";
$parent->remove_child( $node );
$AGdebugmsg = $AGdebugmsg . "post-rm:" . $parent->toStringNonRecursive() . " \n";
$AGdebugmsg =
$AGdebugmsg
. $parent->name() ."<-". $node->name() ." \n \n";
my $cycleroot = TWiki::Plugins::TreePlugin::TWikiNode->new($parent->name() . " cycle...");
my $cycleleaf = TWiki::Plugins::TreePlugin::TWikiNode->new($node->name() . " ...cycle");
$node->data("parent",$cycleroot);
$cycleroot->add_child($node);
$parent->add_child($cycleleaf);
# TBD: give some indication of cycle broken
return $cycleroot;
}
else {
$alreadyvisited{$parent->name()} = 1;
}
# move up
$node = $parent;
}
$AGdebugmsg =
$AGdebugmsg
. "findUltimateParent("
. $orignode->name()
. ")"
. "="
. $node->name()
. " ";
return $node;
}
sub _findParent {
my $node = shift;
$AGdebugmsg =
$AGdebugmsg
. "findParent("
. $node->name()
. ")"
. "="
. ($node->data("parent") ? $node->data("parent")->name() : "no-parent")
. " ";
return $node->data("parent");
}
# lazy variable init
# ned to abstract this at some point
sub setFormatter {
my ($name) = @_;
# my $formatter = $FormatMap{$name};
# return $formatter if $formatter;
my $formatter;
# -- look up how to do case in Perl! :)
if ($name eq "ullist") {
$formatter = new TWiki::Plugins::TreePlugin::ListNodeFormatter("
", "
");
} elsif ($name =~ m/coloroutline(.*)/ ) {
my $attrs = $1;
$attrs =~ s/^://;
$formatter = new TWiki::Plugins::TreePlugin::ColorNodeFormatter($attrs);
} elsif ($name =~ m/imageoutline(.*)/ ) {
my $attrs = $1;
$attrs =~ s/^://;
$formatter = new TWiki::Plugins::TreePlugin::ImgNodeFormatter(split(/:/,$attrs));
} elsif ($name eq "ollist") {
$formatter = new TWiki::Plugins::TreePlugin::ListNodeFormatter(" ", " ");
} elsif ($name eq "hlist") {
$formatter = new TWiki::Plugins::TreePlugin::HOutlineNodeFormatter(
" \$outnum \$topic \$summary");
} else {
$name = "outline";
$formatter = new TWiki::Plugins::TreePlugin::FormatOutlineNodeFormatter(
"\$outnum \$topic ");
}
# remember and return
return $formatter;
# $FormatMap{$name} = $formatter;
}
# TBD: so far as I can tell, the code below is mis-commented.
# It is not finding a child of a node via the lookup;
# instead, it is finding an entry for the node itself.
sub _findTWikiNode {
my ($name, $hash) = @_;
my $node = $hash->{$name}; # look for node
if (! $node) { # create if not there
$node = TWiki::Plugins::TreePlugin::TWikiNode->new($name);
$hash->{$name} = $node;
}
return $node;
}
# use cgi var to override given variable ref
sub cgiOverride {
my $variable = shift;
my $paramname = shift;
my $cgi = &TWiki::Func::getCgiQuery();
if( ! $cgi ) {
return;
}
else {
my $tmp = $cgi->param( $paramname );
$$variable = $tmp if( $tmp );
}
}
# allow other classes to see the installation web
sub installWeb {
return $installWeb;
}
sub handleCreateChildInput {
my ($args, $web) = @_;
my ($addtext, $meta, $text);
if( TWiki::Func::topicExists( $web, "AddUnder" ) ) {
( $meta, $addtext ) = &TWiki::Func::readTopic( $web, "AddUnder" );
}
# need to reread to get the meta of this topic
( $meta, $text ) = &TWiki::Func::readTopic( $web, $topic );
# change meta according to new attributes (reuse $meta for object props)
if ($args) {
$meta = setMetaFromAttr($meta, $args);
}
# put in fields data (if this is going to be a form)
my $formfields;
# so: is new topic to have a form? if so, put in new fields
my $ref=0;
if ($TWiki::Plugins::VERSION < 1.1)
{
$ref = $meta->findOne( "FORM" );
}
else
{
$ref = $meta->get( "FORM" );
}
my %form = (defined $ref ? %$ref : ());
if( %form ) {
my $name = $form{"name"};
$formfields = &TWiki::Form::getFieldParams($meta);
my $forminput = "";
$addtext =~ s/%FORMINPUT%/$forminput/e;
} else {
$addtext =~ s/%FORMINPUT%//g;
}
$addtext =~ s/%ADDFORM%/$formfields/g;
return $addtext;
}
# changes the passed meta, to the given fields value of the given args
sub setMetaFromAttr {
my ($meta, $args) = @_;
# no matter what, no inherited values in child's form
if (&TWiki::extractNameValuePair($args, "resetform")){
$meta->remove ("FIELD");
return $meta;
}
# get this form name
my $ref=0;
if ($TWiki::Plugins::VERSION < 1.1)
{
$ref = $meta->findOne( "FORM" );
}
else
{
$ref = $meta->get( "FORM" );
}
my %form = (defined $ref ? %$ref : ());
my $name = $form{"name"} if ( %form ) || "";
# get new form name, if any
my $newform = TWiki::extractNameValuePair($args, "form");
# if newform & different, just set new form name (& delete all fields)
if ($newform && $newform ne $name) {
$meta->put( "FORM", ( "name" => $newform) );
$meta->remove ("FIELD");
}
my $fields = TWiki::extractNameValuePair($args, "fields");
# put in new fields into $meta
# hash of fields
my %f = map { split( /=/, $_ ) }
grep { /[^\=]*\=[^\=]*$/ }
split (/\s*,\s*/, $fields) ;
foreach (keys %f) {
my @a = ( "name" => $_, "value" => $f{$_} );
$meta->put( "FIELD", @a );
}
return $meta;
}
1;