=head1 NAME yahobuilder - yet another homepage builder =head1 README Inspired by Zope yahobuilder takes the idea of defining variables at different levels and substituting the closest one to offline html preparaion. Yahobuilder includes also a simple upload tool to a ftp server. Home of Yahobuilder is http://www.gs68.de. =head1 SYNOPSIS perl yahobuilder.pl -u to upload the site -r to erase the target directory default is create the offline html You need to set the variables $g_source_dir (here resides your homepage in yahobuilder format) and $g_target_dir (here yahobuilder will prepare the html version), as well as the ftp login info if you intend to use the upload, according your setup. =head1 DESCRIPTION Yahobuilder takes the idea to define variables in different levels (implemented in the directory structure of the source pages) from Zope and substitute them into the actual pages. To setup a a site using yahobuilder you write basically simple html in which you can insert the tag which yahobuilder substitutes with the content of the variable. There are three ways to define variables: =over 2 =item * files with the extension varname.var will be interpreted as variable with name varname. =item * short variables can be defined in a file folder.my_hp (in any directory), this variables must be typed into one line and name and content are separated by name~~~content. =item * there is also a predefined variable navigate_parents that can be used in conjunction with variables folder_title defined in each directory to build up a simple navigation structure. See attached example for details of the usage. =back Even the concept of yahobuilder is quite simple it is rather helpful in achieving a consistant site layout without huge effort. And you need not to use a complicated html editor. Please see the available example if you are interested in yahobuilder. By using the proper arguments yahobuilder can be also used to upload your offline prepared html to a ftp server. =head1 PREREQUISITES C C =head1 COREQUISITES none =head1 TODO Any other good suggestions that people send me! =head1 BUGS Probably, but at moment not known. =head1 AUTHOR Gerhard Spitzlsperger gerhard.spitzlsperger@gs68.de http://www.gs68.de =head1 SEE ALSO The source of a older version of the www.gs68.de site (where you probably got this file) is available as example. Please be aware that some links will not work, because I removed some bigger data files to reduce size. =head1 COPYRIGHT AND LICENSE Copyright (c) 2003, Gerhard Spitzlsperger. All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl. =head1 SCRIPT CATEGORIES Web =cut #!/usr/bin perl -w use strict; use Cwd; use Getopt::Std; use Net::FTP; use vars qw( $AUTOLOAD ); #------------------------------------------------------------------------------ # YOU NEED TO SET THESE VARIABLES TO YOUR NEEDS, YOU DON'T NEED # TO CHANGE SOMETHING BELOW # my $g_source_dir = "replace me"; my $g_target_dir = "replace me THIS DIRECTORY IS REASED BY THE -r OPTION "; my $host = "ftp....."; # the ftp host for upload my $login = "......"; my $password = "......"; #------------------------------------------------------------------------------ sub usage { print "Usage $0 [-u] [-r]\n"; print " -u to upload the site without local site construction\n"; print " -r to remove the target directory\n"; print " default is to process the offline html\n\n"; } my $VERSION = 0.05; my $g_dir_stack = my_directory_stack->new(); my $g_var_stack = my_variable_stack->new( pdir_stack => $g_dir_stack ); print "\n\nnow starting:\n\n\nyahobuilder.pl\n\n\n"; our($opt_r, $opt_u); getopts('ur'); if( $opt_r ) { # erase the target directory system("rm -r $g_target_dir/*"); } elsif( $opt_u ) { # upload to ftp host &upload(); } else { chdir($g_source_dir); &process_dir($g_source_dir); } #------------------------------------------------------------------------------ # subroutine definition #------------------------------------------------------------------------------ sub process_dir { my $dir = shift; chdir($dir); $g_dir_stack->push( $dir ); my @files = <*>; my @vars = (); # make id variable for folder my $v = new my_var(name => 'folder_id', content => "$dir", mode => 'id'); push(@vars, $v); # now putting file variables on the stack foreach my $fname (@files) { # this simple regex delivers the filename without extension # as no path can exist if( -f $fname && $fname =~ m!(\w+)\.var$!) { print "now working on file variable $fname\n"; my $v = new my_var(name => $1, fname => &get_act_source_dir() . "/$fname", mode => 'file'); push (@vars, $v); } elsif( -f $fname && $fname =~ /\.my_hp$/) { print "now working on $fname\n"; my @v = &process_my_hp_file( $fname ); push (@vars, @v); } } $g_var_stack->push( \@vars ); print "the actual directory is: ". $g_dir_stack->get_path(1) . "\n"; # create corresponding target directory if not existing if(! -d &get_act_target_dir() ) { system("mkdir " . &get_act_target_dir() ); } foreach my $fname (@files) { my $ad = getcwd(); if(-d $fname ) { print "$fname is a directory\n"; print ".. calling print_dir recursively\n"; &process_dir($fname); $g_dir_stack->pop(); chdir(&get_act_source_dir()); # we cannot simply go up because # of symlinks. print ".. now we are back to $fname\n"; } elsif( -f $fname && $fname =~ /\.html$/) { &process_html_file($fname); } elsif( -f $fname && $fname =~ /\.var$/) { # intentionally empty } elsif( -f $fname && $fname =~ /^\.my_hp$/) { # intentionally empty } elsif( -f $fname && $fname =~ /~$/) { # intentionally empty # we don't copy backup files } else { #print "$fname is copied unchanged to target directory\n"; my $asd = &get_act_source_dir(); my $atd = &get_act_target_dir(); # compare which files are unchanged, we decide # based on modification time and file size my $s_size = (stat("$asd/$fname"))[7]; my $t_size = (stat("$atd/$fname"))[7]; my $s_mtime = (stat("$asd/$fname"))[9]; my $t_mtime = (stat("$atd/$fname"))[9]; if( ($s_size != $t_size) || ($s_mtime >= $t_mtime) || (! -e "$atd/$fname")) { print " updated file: $asd/$fname \n"; system("cp $asd/$fname $atd"); } } } # remove vars of this dir from stack $g_var_stack->pop(); } sub process_html_file { my $fname = shift; my $buf = $g_var_stack->process_file(&get_act_source_dir() . "/$fname"); my $out_file = &get_act_target_dir() . "/$fname"; unless( open(FO, "> $out_file") ) { print STDERR "file $out_file cannot be opened\n\n"; return; } print FO $buf; close(FO); } sub process_my_hp_file { my $fname = shift; my @vars = (); unless( open(FH, "$fname") ) { print STDERR "file $fname cannot be opened\n\n"; return; } while( my $line = ) { chomp $line; if( $line =~ /~~~/ ) { my $t = $'; my $v = new my_var(name => $`, content => $t, mode => 'folder_title'); #' just not to confuse sytay highlighting of emacs push(@vars, $v); } } # clean up and return close(FH); return @vars; } sub get_act_source_dir { return $g_source_dir . $g_dir_stack->get_path(1); } sub get_act_target_dir { return $g_target_dir . $g_dir_stack->get_path(1); } #------------------------------------------------------------------------------ # subroutines for upload #------------------------------------------------------------------------------ sub upload { my $ftp = Net::FTP->new($host); $ftp->login($login, $password); if( !defined $ftp ) { print "could not login ... exiting\n"; exit( -1); } &upload_dir($g_target_dir, $ftp); $ftp->quit; } sub upload_dir { my $dir = shift; my $ftp = shift; chdir($dir); $g_dir_stack->push( $dir ); my @files = <*>; print "the actual directory is: ". $g_dir_stack->get_path(1) . "\n"; foreach my $fname (@files) { my $ad = getcwd(); if(-d $fname ) { print "$fname is a directory\n"; my $e = $ftp->cwd( $g_dir_stack->get_path(1) . "/$fname" ); print "e $e\n"; # if the directory doesn't exist make it if( ! $e ) { print "create " . $g_dir_stack->get_path(1) ."\n"; $ftp->mkdir( $g_dir_stack->get_path(1). "/$fname") } print ".. calling upload_dir recursively\n"; &upload_dir($fname, $ftp); $g_dir_stack->pop(); chdir(&get_act_target_dir()); # we cannot simply go up because # of symlinks. print ".. now we are back to $fname\n"; } else { my $asd = &get_act_target_dir(); # compare which files are unchanged, we decide # based on modification time my $s_mtime = (stat("$asd/$fname"))[9]; # this is the required filename on the server my $temp = $g_dir_stack->get_path(1); print " $temp/$fname \n"; my $f_mtime = $ftp->mdtm("$temp/$fname"); if( $s_mtime >= $f_mtime ) { print " updated file: $asd/$fname \n"; my $t = $ftp->put( "$asd/$fname", "$temp/$fname"); print " to $t \n\n"; } } } } #------------------------------------------------------------------------------ # Package Definition #------------------------------------------------------------------------------ package my_var; use strict; use vars qw( $AUTOLOAD ); use Carp; BEGIN { my %_attr_data = ( _content => [undef, 'read/write'], _fname => ['', 'read'] , _mode => ['file', 'read'] , _name => ['', 'read'] , _type => ['scalar', 'read/write'] ); my $_count = 0; sub _accessible { my ($self, $attr, $mode ) = @_; return $_attr_data{$attr}[1] =~ /$mode/; } sub _default_for { my ($self, $attr ) = @_; return $_attr_data{$attr}[0]; } sub _standard_keys { return keys %_attr_data; } sub get_count { return $_count; } sub _incr_count { ++$_count } sub _decr_count { --$_count } } sub new { my ($class, %arg ) = @_; my $self = bless {}, $class; foreach my $attr_name ($self->_standard_keys() ) { $attr_name =~ /^_(.*)/; my $arg_name = $1; if( exists $arg{$arg_name} ) { $self->{$attr_name} = $arg{$arg_name}; } else { $self->{$attr_name} = $self->_default_for($attr_name); } } $self->_incr_count(); return $self; } sub DESTROY { $_[0]->_decr_count(); } sub AUTOLOAD { no strict "refs"; my ($self, $new_value) = @_; # was it get....? if( $AUTOLOAD =~ /.*::get(_\w+)/ && $self->_accessible( $1, 'read')) { my $attr_name = $1; *{$AUTOLOAD} = sub { return $_[0]->{$attr_name} }; return $self->{$attr_name}; } # was it set....? if( $AUTOLOAD =~ /.*::set(_\w+)/ && $self->_accessible( $1, 'write')) { my $attr_name = $1; *{$AUTOLOAD} = sub { $_[0]->{$attr_name} = $_[1]; return; }; $self->{$attr_name} = $new_value; return; } # seems to be a failure croak "no such method: $AUTOLOAD"; } #------------------------------------------------------------------------------ package my_directory_stack; use strict; use vars qw( $AUTOLOAD ); use Carp; BEGIN { my %_attr_data = ( _stack => [undef, 'read'] ); my $_count = 0; sub _accessible { my ($self, $attr, $mode ) = @_; return $_attr_data{$attr}[1] =~ /$mode/; } sub _default_for { my ($self, $attr ) = @_; return $_attr_data{$attr}[0]; } sub _standard_keys { return keys %_attr_data; } sub get_count { return $_count; } sub _incr_count { ++$_count } sub _decr_count { --$_count } } sub new { my ($class, %arg ) = @_; my $self = bless {}, $class; # initialization of attributes foreach my $attr_name ($self->_standard_keys() ) { $attr_name =~ /^_(.*)/; my $arg_name = $1; if( exists $arg{$arg_name} ) { $self->{$attr_name} = $arg{$arg_name}; } else { $self->{$attr_name} = $self->_default_for($attr_name); } } # count instances $self->_incr_count(); # initially the stack is an empty list $self->{'_stack'} = []; return $self; } sub get_path { my $self = shift; my $level = shift || 1; my $max = shift || $#{$self->{'_stack'}}; my $r = ''; for(my $i=abs($level); $i <= $max; $i++) { $r .= '/' . $self->{'_stack'}->[$i]; } $r =~ s!/!! if( $level < 0 ); return $r; } sub push { my $self = shift; my $t = shift; push(@{$self->{'_stack'}}, $t); } sub pop { my $self = shift; return pop(@{$self->{'_stack'}}); } sub DESTROY { $_[0]->_decr_count(); } sub AUTOLOAD { no strict "refs"; my ($self, $new_value) = @_; # was it get....? if( $AUTOLOAD =~ /.*::get(_\w+)/ && $self->_accessible( $1, 'read')) { my $attr_name = $1; *{$AUTOLOAD} = sub { return $_[0]->{$attr_name} }; return $self->{$attr_name}; } # was it set....? if( $AUTOLOAD =~ /.*::set(_\w+)/ && $self->_accessible( $1, 'write')) { my $attr_name = $1; *{$AUTOLOAD} = sub { $_[0]->{$attr_name} = $_[1]; return; }; $self->{$attr_name} = $new_value; return; } # seems to be a failure croak "no such method: $AUTOLOAD"; } #------------------------------------------------------------------------------ package my_variable_stack; use strict; use vars qw( $AUTOLOAD ); use Carp; BEGIN { my %_attr_data = ( _stack => [undef, 'read'], _pdir_stack => [undef, 'read/write'], ); my $_count = 0; sub _accessible { my ($self, $attr, $mode ) = @_; return $_attr_data{$attr}[1] =~ /$mode/; } sub _default_for { my ($self, $attr ) = @_; return $_attr_data{$attr}[0]; } sub _standard_keys { return keys %_attr_data; } sub get_count { return $_count; } sub _incr_count { ++$_count } sub _decr_count { --$_count } } sub new { my ($class, %arg ) = @_; my $self = bless {}, $class; # initialization of attributes foreach my $attr_name ($self->_standard_keys() ) { $attr_name =~ /^_(.*)/; my $arg_name = $1; if( exists $arg{$arg_name} ) { $self->{$attr_name} = $arg{$arg_name}; } else { $self->{$attr_name} = $self->_default_for($attr_name); } } # count instances $self->_incr_count(); # initially the stack is an empty list $self->{'_stack'} = []; return $self; } sub push { my $self = shift; my $t = shift; push(@{$self->{'_stack'}}, $t); } sub pop { my $self = shift; return pop(@{$self->{'_stack'}}); } sub lookup_variable { my $self = shift; my $name = shift; my $result; my $i; # check some standard variables if( $name eq 'navigate_parents' ) { print "navigate ....\n"; $result = 'Home'; for($i=1; $i <= $#{$self->{'_stack'}}; $i++) { my @vars = @{$self->{'_stack'}->[$i]}; my $t; foreach my $j (@vars) { if('folder_title' eq $j->get_name() ) { $t = $j->get_content(); last; } } if( !defined $t ) { foreach my $j (@vars) { if('folder_id' eq $j->get_name() ) { $t = $j->get_content(); } } } # read the directory stack until the relevant level $result .= ' > ' . $t . ''; } print $result; } # check the stack $i=$#{$self->{'_stack'}}; while( $i >= 0 && !defined $result) { my @vars = @{$self->{'_stack'}->[$i]}; for( my $j=0; $j <= $#vars; $j++) { if($name eq $vars[$j]->get_name() ) { if( defined $vars[$j]->get_content() ) { $result = $vars[$j]->get_content(); } elsif( $vars[$j]->get_mode() eq 'file' ) { # content of file variables will be never set # because some variables which are called inside # might depend on the directory $result = $self->process_file( $vars[$j]->get_fname() ); } else { print STDERR "VARIABLE $name not " . "defined in this context!\n"; exit(1); } } } $i--; } if( !defined $result ) { print STDERR "VARIABLE $name not " . "defined in this context!\n"; exit(1); } print "now looking up: $name, ok\n"; #print "result:\n$result\n\n!!!!continue"; ; return $result; } sub process_file { my $self = shift; my $fname = shift; my $buf = ''; unless( open(FH, $fname) ) { print STDERR "file $fname cannot be opened\n\n"; return; } my @lines = ; while( @lines ) { my $line = shift @lines; # ? is necessary to force non greedy matching if( $line =~ m!! ) { my $var = $1; my $t = $self->lookup_variable($var); $line = ''; $line .= $` if defined $`; $line .= $t; # must be parsed again unshift(@lines, $') if defined $'; } $buf .= $line; } close(FH); return $buf; } sub print_var_stack { my $self; my $i=$#{$self->{'_stack'}}; print "var stack (len: $i)\n"; while( $i >= 0 ) { my @vars = @{$self->{'_stack'}->[$i]}; print "level $i:\n"; for( my $j=0; $j <= $#vars; $j++) { print ' ' . $vars[$j]->get_name() . $vars[$j]->get_fname() ."\n"; } print "\n"; $i--; } } sub DESTROY { $_[0]->_decr_count(); } sub AUTOLOAD { no strict "refs"; my ($self, $new_value) = @_; # was it get....? if( $AUTOLOAD =~ /.*::get(_\w+)/ && $self->_accessible( $1, 'read')) { my $attr_name = $1; *{$AUTOLOAD} = sub { return $_[0]->{$attr_name} }; return $self->{$attr_name}; } # was it set....? if( $AUTOLOAD =~ /.*::set(_\w+)/ && $self->_accessible( $1, 'write')) { my $attr_name = $1; *{$AUTOLOAD} = sub { $_[0]->{$attr_name} = $_[1]; return; }; $self->{$attr_name} = $new_value; return; } # seems to be a failure croak "no such method: $AUTOLOAD"; } 1;