On Nov 16, 2006, at 3:08 PM, html-template-users- [EMAIL PROTECTED] wrote:
> > Date: Wed, 15 Nov 2006 15:59:44 -0500 (EST) > From: Sam Tregar <[EMAIL PROTECTED]> > Subject: Re: [htmltmpl] The sf.net Subversion Repository and the > Phalanx One > To: Shlomi Fish <[EMAIL PROTECTED]> > Cc: html-template-users@lists.sourceforge.net > Message-ID: <[EMAIL PROTECTED]> > Content-Type: TEXT/PLAIN; charset=US-ASCII; format=flowed > > On Wed, 15 Nov 2006, Shlomi Fish wrote: > >> A question if I may. Why weren't the tests and other changes that >> were done to the Phalanx work on HTML-Template: >> >> * http://svn.perl.org/phalanx/HTML-Template/ >> * http://hew.ca/yapc/phalanx/slides/TABLE_OF_CONTENTS.html >> >> Integrated into the mainline HTML-Template at: >> >> https://svn.sourceforge.net/svnroot/html-template/ > > Lack of motivation, I suppose. As far as I know they never found or > fixed a bug in HTML::Template. At this point their repo is out of > date and it would be a painful process to do the merge. > Here is the summary of the Perl Seminar NY Phalanx project work on HTML::Template which we submitted to Sam Tregar on June 11, 2005. It was written in POD, so you might want to copy-and-paste it into a file and run perldoc on it. For good measure, I've placed a tarball of the final version of our work on HTML::Template here: http://thenceforward.net/perl/misc/HTML- Template-2.701.tar.gz (You can get a tarball of our work on Text::Template in the same directory.) Sam, we'd still welcome feedback. Jim Keenan =head1 SUMMARY This document summarizes work done on Perl module HTML::Template by members of Perl Seminar New York as part of the Phalanx project. We discuss what we were able to accomplish as of June 11, 2005, as well as what we were not able to do, what might remain to be done, or what we think should be done. =head1 Definitions ''We'' equals the Perl Seminar NY Phalanx hoplites who have contributed to revisions as of the above date: David H Adler (dha), Jim Keenan (jkeenan), Marc Prewitt (mprewitt) and Alex Gill (alexgill). ''You'' equals Sam Tregar. =head1 Diffs In order to generate meaningful diff files, we increased C<$VERSION> to 2.701. All diffs were generated against version 2.7 as taken from CPAN. (We made no reference to the Sourceforge version of the distribution.) We present diffs in three different formats, each of which has its merits: =over 4 =item svn diff Diff files generated by Subversion's C<svn diff> command run individually on each of the two files which have undergone revision so far: MANIFEST Template.pm Each diff file so generated will end with suffix C<.diff>. =item difftidy We run each of the revised files listed above through F<perltidy> and then run the results through the system's F<diff> program. This may -- or may not -- help to reduce spurious differences caused by, I<e.g.,> elimination of trailing whitespace. Each such diff file will end with suffix C<.ovn>. =item diffcode and diffpod We wrote a Perl script based on the work of Perl Seminar NY member Glenn Maciag which produces one diff file for differences in code (F<diffcode>) and a separate diff file for differences in documentation written in POD format (F<diffpod>). We found this useful for modules where code and POD are interleaved because the content is more focused yet the line numbers are preserved. =back Pick and choose! =head1 Code Coverage The Perlsemny Phalanx project used Paul Johnson's Devel::Cover module to measure the extent to which HTML::Template's test suite actually tested its code. The following table shows HTML::Template's code coverage as measured at two different points in time: just before we began our work, and where we stood on the above date after two joint hacking sessions. ''Version 175'' refers to entry 175 in the Phalanx Subversion repository and is, for all practical purposes, equivalent to the current version of HTML::Template found on CPAN, i.e., version 2.7. Coverage results are shown for statement, branch, condition and subroutine coverage. Version 175 File stmt branch cond sub Template.pm 85.2 63.5 50.0 84.9 Version 344 File stmt branch cond sub Template.pm 90.7 72.7 65.8 94.4 As the data suggest, HTML::Template already had a very high percentage -- 85.2% -- of its statements covered before Phalanx began work on it. However, the coverage of branches and conditions was lower, and some subroutines were as yet completely uncovered. As a result of Phalanx's work through early June 2005, statement coverage was increased to over 90%, branch coverage improved to over 72%, condition coverage increased by a quarter to over 65% and subroutine coverage is now at more than 94%. The major obstacle to even more improved coverage is technical: Certain features of HTML::Template are only available on OSes where one can install IPC::ShareLite, Gtop, etc. To date, we have not had access to such a system, so features such as C<shared_cache>, C<double_cache> and C<memory_debug> are so far untested. However, we feel that some of what we learned so far will be readily applicable to those features once we have access to them. So we can confidently expect even more improvements in code coverage as our work continues. =head2 A Note on POD Coverage In the table above we present only the first four columns in a typical Devel::Cover coverage report. While Devel::Cover can provide POD coverage as well, it does so only by relying on Perl extension Pod::Coverage. But Pod::Coverage, at least in its native format, only reports accurately on documentation provided that the module author has written the POD using subroutines as the titles of subsections. In certain cases you have done this but, like many other module authors, you have not done this for all subroutines -- nor do we think you necessarily should do this. We saw no reason to rewrite the documentation with new subsection headings simply to get a better looking statistic out of Pod::Coverage and, in turn, Devel::Cover. So we have not included the coverage column in the table above. =head1 Revisions of Code =head2 Refactoring of Code Repeated within C<new()> and C<_new_from_loop()> The following code appeared in both C<new()> (beginning at 961) and C<_new_from_loop()> (beginning at 1118): for (my $x = 0; $x <= $#_; $x += 2) { defined($_[($x + 1)]) or croak("HTML::Template->new() called with odd number of option parameters - should be of the form option => value"); $options->{lc($_[$x])} = $_[($x + 1)]; } It was refactored into a new subroutine, C<_load_supplied_options()>. Example: $options = _load_supplied_options( [EMAIL PROTECTED], $options); Here is the new subroutine: sub _load_supplied_options { my $argsref = shift; my $options = shift; for (my $x = 0; $x <= $#_; $x += 2) { defined($_[($x + 1)]) or croak("HTML::Template->new() called with odd number of option parameters - should be of the form option => value"); $options->{lc($_[$x])} = $_[($x + 1)]; } return $options; } =head2 Additional Error-Checking in C<new()> The documentation makes clear that the caching features can only be used when the source of a template is a file -- not when it is a string, an array or a filehandle. What would happen in the current version of HTML::Template if someone tried to activate caching when the template source was not a file? Experimentation showed that the program would die a horrible death. Multiple error messages would be emitted from subroutines several levels below C<new()>, but these error messages would not clearly point to the problem. For example, calling this script: #!/usr/local/bin/perl use strict; use warnings; use HTML::Template; my ($stemplate, $template_string); open my $fh, 'templates/simple.tmpl' or die "Couldn't open simple.tmpl for reading: $!"; { local $/; $template_string = <$fh>; seek $fh, 0, 0; } $stemplate = HTML::Template->new( type => 'scalarref', source => \$template_string, cache => 1, ); ... generated these messages: HTML::Template->new() : Cannot find file ''. at /usr/local/lib/perl5/site_perl/5.8.4/HTML/Template.pm line 1293 HTML::Template::_cache_key('HTML::Template=HASH(0x82b3cc)') called at /usr/local/lib/perl5/site_perl/5.8.4/HTML/Template.pm line 1269 HTML::Template::_commit_to_cache('HTML::Template=HASH (0x82b3cc)') called at /usr/local/lib/perl5/site_perl/5.8.4/HTML/Template.pm line 1197 HTML::Template::_init('HTML::Template=HASH(0x82b3cc)') called at /usr/local/lib/perl5/site_perl/5.8.4/HTML/Template.pm line 1083 HTML::Template::new('HTML::Template', 'type', 'scalarref', 'source', 'SCALAR(0x806f00)', 'cache', 1) called at templstr.pl line 16 We solved this problem by adding some additional error-checking code to the constructor which causes the program to C<croak> if the user attempts to call for caching with a non-file source. # check that cache options are not used with non-cacheable templates croak "Cannot have caching when template source is not file" if grep { exists($options->{$_}) } qw( filehandle arrayref scalarref) and grep {$options->{$_}} qw( cache blind_cache file_cache shared_cache double_cache double_file_cache ); All tests continued to pass. Adding this code to the constructor meant that some error-checking code in more internal subroutines (I<e.g.,> C<_cache_key>) could safely be eliminated, thereby increasing the total percentage of code covered. (From this point forward, we'll refer to this new code as the ''double-grep test.'') =head2 Elimination of Unnecessary Tests for Definedness of Variables =head3 Within C<new()> In two cases you call for the constructor to C<croak> unless a two-part condition is met. =over 4 =item * Beginning at 1032: if (exists($options->{filename})) { croak("HTML::Template->new called with empty filename parameter!") unless defined $options->{filename} and length $options-> {filename}; } =item * Beginning at 1048: if ($options->{file_cache}) { # make sure we have a file_cache_dir option croak("You must specify the file_cache_dir option if you want to use file_cache.") unless defined $options->{file_cache_dir} and length $options->{file_cache_dir}; ... =back When we performed coverage analysis on these two blocks, we noted that the case where the I<first> part of each C<unless> clause was not defined was uncovered. line err % !l l&&!r l&&r expr ----- --- ------ ------ ------ ------ ---- 1033 *** 66 0 1 58 defined $$options {'filename'} and length $$options{'filename'} 1050 *** 33 0 0 5 defined $$options {'file_cache_dir'} and length $$options{'file_cache_dir'} We hypothesized that in each case the first part of the condition was I<always> defined at this point in the program. If so, it would not need to be tested and could safely be eliminated. When we tried to pass C<undef> as a value in these two cases, the code now represented by C<_load_supplied_options> detected an 'unbalanced key-value pair', threw an error and caused the program to C<croak> -- I<before> either of these two blocks of code was reached. Hence, the two blocks could be simplified to read: =over 4 =item * if (exists($options->{filename})) { croak("HTML::Template->new called with empty filename parameter!") unless length $options->{filename}; } =item * if ($options->{file_cache}) { # make sure we have a file_cache_dir option croak("You must specify the file_cache_dir option if you want to use file_cache.") unless length $options->{file_cache_dir}; ... =back All tests continued to pass after the elimination of the superfluous tests for definedness. Since these lines no longer include an C<and> condition, these lines are no longer reported by Devel::Cover as conditions -- which means the total coverage ratio for conditions increases. =head3 Within C<_init()> In a way very similar to the cases just described, there are a number of situations where you call for the C<_init()> to C<croak> unless a two-part condition is met. =over 4 =item * Beginning at 1162: } elsif ($options->{double_file_cache}) { # try the normal cache, return if we have it. $self->_fetch_from_cache(); return if (defined $self->{param_map} and defined $self-> {parse_stack}); =item * Beginning at 1170: # put it in the local cache if we got it. $self->_commit_to_cache() if (defined $self->{param_map} and defined $self-> {parse_stack}); =item * Beginning at 1184: # if we got a cache hit, return return if (defined $self->{param_map} and defined $self-> {parse_stack}); =back In each case, the coverage reported indicated that the case where the first part of the condition was true while the second part was false was never being tested. line err % !l l&&!r l&&r expr ----- --- ------ ------ ------ ------ ---- 1165 *** 0 0 0 0 defined $$self {'param_map'} and defined $$self{'parse_stack'} 1171 *** 0 0 0 0 defined $$self {'param_map'} and defined $$self{'parse_stack'} 1185 *** 66 57 0 7 defined $$self {'param_map'} and defined $$self{'parse_stack'} We hypothesized that if C<$self-E<gt>{param_map}> is defined, C<$self-E<gt>{parse_stack}> must always be defined as well. In that case, we would only need the first part of the condition and could dispense with the second. While we don't yet understand C<param_map> and C<parse_stack> well enough to prove this point by logic, it was empirically verified by simplifying and testing the code. These three blocks were rewritten as follows: =over 4 =item * } elsif ($options->{double_file_cache}) { # try the normal cache, return if we have it. $self->_fetch_from_cache(); return if (defined $self->{param_map}); =item * # put it in the local cache if we got it. $self->_commit_to_cache() if (defined $self->{param_map}); =item * # if we got a cache hit, return return if (defined $self->{param_map}); =back All tests continued to pass once these changes were made. As was the case discussed above, since these lines no longer include an C<and> condition, these lines are no longer reported by Devel::Cover as conditions -- which means the total coverage ratio for conditions increases. =head2 Elimination of Unnecessary Tests for Existence of Variables =head3 Within C<_fetch_from_cache()> C<_fetch_from_cache()> begins as follows: sub _fetch_from_cache { my $self = shift; my $options = $self->{options}; # return if there's no file here return unless exists($options->{filename}); # line 1214 The coverage report on branches suggested that the 'true' branch of line 1214 -- where C<$options-E<gt>{filename}> did I<not> exist -- was never tested. line err % true false branch ----- --- ------ ------ ------ ------ 1214 *** 50 0 6 unless exists $$options {'filename'} C<_fetch_from_cache()>, however, is always called from within C<_init()> -- which, in turn, is always called from within C<new()> at a point I<after> we have guaranteed that C<$options-E<gt>{filename}> not only exists but is defined and of non-zero length. Line 1214 is therefore superfluous and may be eliminated, thereby increasing the branch coverage ratio. =head3 Within C<_cache_key()> C<_cache_key()> begins: sub _cache_key { my $self = shift; my $options = $self->{options}; # determine path to file unless already known my $filepath = $options->{filepath}; if (not defined $filepath) { $filepath = $self->_find_file($options->{filename}); confess("HTML::Template->new() : Cannot find file '$options->{filename}'.") unless defined($filepath); $options->{filepath} = $filepath; } # assemble pieces of the key my @key = ($filepath); push(@key, @{$options->{path}}) if $options->{path}; ... The branch part of the coverage report indicated that there was quite a bit of uncovered code here. line err % true false branch ----- --- ------ ------ ------ ------ 1291 *** 50 0 15 if (not defined $filepath) 1293 *** 0 0 0 unless defined $filepath 1300 *** 50 15 0 if $$options{'path'} The L<double-grep test|additional_errorchecking_in_new__> discussed above takes care of this coverage deficiency. It guarantees that, by this point in a program, C<$options-E<gt>{filepath}> is always defined. So quite a few lines of code can be eliminated outright. sub _cache_key { my $self = shift; my $options = $self->{options}; # assemble pieces of the key my @key = ($options->{filepath}); push(@key, @{$options->{path}}); ... All tests again pass. Re-running coverage analysis indicates that this block of code is now completely covered. =head3 Within C<_fetch_from_file_cache()> sub _fetch_from_file_cache { my $self = shift; my $options = $self->{options}; return unless exists($options->{filename}); ... Coverage analysis indicated that the 'true' branch here -- the case where C<$options-E<gt>{filename}> did I<not> exist -- was untested. line err % true false branch ----- --- ------ ------ ------ ------ 1334 *** 50 0 5 unless exists $$options {'filename'} Once again, the addition of the L<double-grep test| additional_errorchecking_in_new__> inside C<new()> guaranteed that this C<filename> must exist by this point. Hence, C<_fetch_from_file_cache()> could be rewritten to begin: sub _fetch_from_file_cache { my $self = shift; my $options = $self->{options}; ... All tests continued to pass, and because an entire statement was eliminated, both statement and branch coverage ratios increased. =head3 Within C<_find_file()> In C<_find_file()>, there exist these two lines of code: if (exists($ENV{HTML_TEMPLATE_ROOT}) and defined($ENV {HTML_TEMPLATE_ROOT})) { # Line 1541 ... if (exists($ENV{HTML_TEMPLATE_ROOT})) { # Line 1556 In the first line, the test for definedness is logically sufficient. In the second line, a test for definedness would be better than the test for mere existence. So these lines can each be rewritten as: if (defined($ENV{HTML_TEMPLATE_ROOT})) { All tests pass and the condition coverage ratio improves because the first line no longer includes a condition. =head2 Other Simplification of Code =head3 C<param()> We have not thoroughly plumbed the depths of C<param()>, so there is probably quite a bit of work left there for us to do. However, we did manage to simplify one block of code beginning at line 2479: if (!scalar(@_)) { croak("HTML::Template->param() : Single reference arg to param() must be a hash-ref! You gave me a $type.") unless $type eq 'HASH' or (ref($first) and UNIVERSAL::isa($first, 'HASH')); push(@_, %$first); The C<unless> condition is visibly complex, and we have learned through experience that complex conditions tend to contain code untested by a module's test suite. Sure enough, the coverage report showed for: =over 4 =item Statements line err stmt branch cond sub pod time code 2479 125 100 6445 if (! scalar(@_)) { 2480 *** 48 50 0 272 croak ("HTML::Template->param() : Single reference arg to param() must be a hash-ref! You gave me a $type.") *** 33 2481 unless $type eq 'HASH' or 2482 (ref($first) and UNIVERSAL::isa($first, 'HASH')); =item Branches line err % true false branch ----- --- ------ ------ ------ ------ 2479 100 48 77 if (not scalar @_) { } 2480 *** 50 0 48 unless $type eq 'HASH' or ref $first and UNIVERSAL::isa($first, 'HASH') =item Conditions and 3 conditions line err % !l l&&!r l&&r expr ----- --- ------ ------ ------ ------ ---- 2480 *** 0 0 0 0 ref $first and UNIVERSAL::isa($first, 'HASH') =back We tackled this code at our second HTML::Template joint hacking session. More precisely, we tackled it on our third round of beer at our second joint hacking session. We simplified the code to the following: if (!scalar(@_)) { croak("HTML::Template->param() : Single reference arg to param() must be a hash-ref! You gave me a $type.") unless $type eq 'HASH' or UNIVERSAL::isa($first, 'HASH'); push(@_, %$first); All the code now became coverable by tests. All tests passed. =head1 Revisions of Tests =head2 F<t/99-old-test-pl.t> This file, the sole test file in version 2.7, has been left unchanged. =head2 New F<t/*.t> Tests =over 4 =item F<t/01-bad-args.t> Test whether constructor fails with appropriate messages if passed bad or missing arguments. =item F<t/02-parse.t> Test previously untested code inside C<HTML::Template::_parse()>. Much remains to be done, as we were just beginning our Phalanx efforts when we were working in this area. =item F<t/03-associate.t> Test previously untested method C<HTML::Template::associateCGI()>. =item F<t/04-type-source.t> Test the 'type-source' style of constructor C<HTML::Template::new()>. $stemplate = HTML::Template->new( type => 'scalarref', source => \$template_string, ); And similarly for cases where the source is an array or a filehandle. =item F<t/05-blind-cache.t> Test the previously untested C<blind_cache> option to constructor C<HTML::Template::new()>. $template = HTML::Template->new( path => ['templates/'], filename => 'simple.tmpl', blind_cache => 1, ); I<Note:> According to the documentation, when the C<blind_cache> option is set to 1, ''the module behaves exactly as with normal caching but does not check to see if the file has changed on each request.'' Unfortunately, there is no documentation beyond this. Specifically, (a) there is no test for it in F<t/99-old-test-pl.t>; (b) there is no example of it in the documentation; and (c) we could find no references to it in the archives of the HTML::Template mailing list on Sourceforge. So we were left to guess at how to code up a test. We basically cloned the test in F<t/99-old-test-pl.t> beginning at line 370. We hypothesized that if we created an object with the C<blind_cache> option, slept for 1 second, then changed the template file and got the same output string notwithstanding the change in the template file, we could deem the test a pass. And it did indeed pass. But since our understanding of the C<blind_cache> option is limited, we don't yet know if the test was valid. =item F<t/06-file-cache-dir.t> Test edge cases in use of C<file_cache> and C<file_cache_dir> options to constructor C<HTML::Template::new()>. Example: test case where C<file_cache> option is set to C<undef> but a C<file_cache_dir> value is provided; examine error message. =item F<t/07-double-file-cache.t> Test the previous untested C<double_file_cache> option to C<HTML::Template::new()>. $template = HTML::Template->new( path => ['templates/'], filename => 'simple.tmpl', double_file_cache => 1, file_cache_dir => './blib/temp_cache_dir', ); =item F<t/08-cache-debug.t> Automate testing of information printed to STDERR when the C<cache_debug> option to C<HTML::Template::new()> is turned on. Tests for this functionality in F<t/99-old-test-pl.t> were ''non automated,'' I<i.e.>, in order to assess their results, the user would have had to turn the option on and visually inspect STDERR as the test were displayed on the terminal. Now the output is captured and analyzed automatically. =item F<t/09-caching-precluded.t> Test that C<HTML::Template::new()> now precludes the possibility of any of the six cache options having a true value if the template source is a filehandle, string or array. The constructor now does additional error-checking and, if a violation is found, the program dies and an appropriate error message is emitted via C<croak> and analyzed. =item F<t/10-param.t> Test edge cases in use of C<HTML::Template::param()>. More tests will probably be added as we understand this function better. =item F<t/11-non-file-templates.t> Test whether simple output is correctly created when the template source is a string, array or a filehandle. =back =head2 Supportive Testing Modules in F<t/testlib> We are using core module F<Test::More> throughout. In order to extend the test coverage as far as possible, however, we have in certain circumstances found it expedient to use functionality provided by other, non-core CPAN modules. Where appropriate, we have included this functionality underneath the F<t/testlib> directory. For example, to test HTML::Template's C<cache_debug> option, a user currently has to uncomment that option and observe STDERR as the tests are run. We supplement by providing tests in F<t/08-cache-debug.t> which automatically capture debugging messages printed to STDERR and comparing those messages to predicted values with F<Test::More> functions. We capture those debugging messages with parts of non-core CPAN distribution F<IO::Capture>, included under F<t/testlib>. =head2 The 700 Club Recognizing your feelings about HTML::Template, we are trying to make specific efforts to allay your fears that our Phalanx work will at some point cause HTML::Template to 'break'. One way that revisions to the module I<could> make it break would be changes to the constructor (or to functions called from within the constructor) which produced a different data structure within the HTML::Template object. We therefore have gone to some lengths to demonstrate that the object created by C<new()> in 2.701 is the same as that created by C<new()> in 2.7. We extracted a large portion of the tests found in F<t/99-old-test-pl.t> which included a new constructor call and placed them in six files with visibly distinct names: F<t/700-cons.t>, F<t/701-cons.t>, ... F<t/706-cons.t>: the 700 Club. In these test files we create an HTML::Template object with specified parameters with version 2.701 and then make exactly the same call with version 2.7. We then compare the two objects with C<Test::More::is_deeply()>. We do this by including F<HTML/Template.pm> under F<t/testlib>. The results were unambiguous: the object created by C<HTML::Template::new()> under 2.701 is the same as that created by the constructor under 2.7. =head1 Revisions of Documentation Changes proposed for the documentation in F<Template.pm> are most easily seen by examining the file F<diffpod> attached. When you look at F<diffpod> you should see that most changes proposed are cosmetic. For a more consistent look in the HTML representation of the docs, many variable names which previously appeared in the body font or inside quotation marks have now been placed in monospace font. Examples: 121c121 < If you called param() with a value like sam"my you'll get in trouble --- > If you called C<param()> with a value like sam"my you'll get in trouble 406c406 < to access the template text. You can use "filename => 'file.tmpl'" to --- > to access the template text. You can use C<filename => 'file.tmpl'> to A few spelling errors were found and corrected. Example: 2838c2841 < Just like C<param()>, C<query()> with no arguements returns all the --- > Just like C<param()>, C<query()> with no arguments returns all the In other cases the only change is elimination of trailing whitespace at the end of lines. Other than these changes no changes have been suggested so far for the documentation. However, since we still have more work to do on the code and tests, there may arise situations where we will suggest changes in the documentation as well. =head1 Recommendations We recommend that the tarball accompanying this message -- less the six tests in the ''700 Club'' -- be uploaded to CPAN as the next version of HTML::Template. In other words, we recommend that what we have called version 2.701 to renamed (in F<Template.pm>) version 2.71. There I<are> parts of HTML::Template for which we either (a) cannot yet write tests (because we haven't tried it on OSes which support features such as C<shared_cache>, C<double_cache> or C<memory_debug>) or have not yet written tests (parts of C<param()> and C<_parse>). Nonetheless, the substantial increases in statement, branch, condition and subroutine coverage we have achieved make HTML::Template stronger by providing greater assurance that future revisions will not cause it to ''break.'' In particular, we recommend that any patches you are currently considering for HTML::Template (such as those suggested on the HTML::Template mailing list) be applied against 2.71 rather than 2.70. Since we didn't change the functionality or interface of HTML::Template in any respect, no current users of HTML::Template would be required to upgrade to 2.71, nor would they get any surprises if they did so. But people installing HTML::Template for the first time would install a more thoroughly tested version of it. Thank you very much. =cut ------------------------------------------------------------------------- Take Surveys. Earn Cash. Influence the Future of IT Join SourceForge.net's Techsay panel and you'll get the chance to share your opinions on IT & business topics through brief surveys - and earn cash http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV _______________________________________________ Html-template-users mailing list Html-template-users@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/html-template-users