=head1 NAME

HTML::Seamstress - Perl extension for HTML generation via tree rewriting

=head1 SYNOPSIS

=head2 HTML "adulterated" with id attributes

 <html>
 <head>
   <title>Hello World</title>
 </head>
 <body>
 <h1>Hello World</h1>
   <p>Hello, my name is <span id="name">ah, Clem</span>.
   <p>Today's date is <span id="date">Oct 6, 2001</span>.
 </body>
 </html>

=head2 Perl finds nodes in the tree and rewrites them:

  use HTML::Seamstress; # HTML::Seamstress HTML::TreeBuilder :)
  my $tree = HTML::Seamstress->new_from_file($html_file);
  $tree->name_handler('bob');
  $tree->date_handler(`date`);
  
  sub name_handler {
    my ($tree, $name) = @_;

    my $name_tag = $tree->look_down('id', 'name');
    $name_tag->detach_content; # delete dummy content ("ah, Clem")
    $name_tag->push_content($name);
  }

  sub date_handler {
    my ($tree, $date) = @_;

    my $name_tag = $tree->look_down('id', 'date');
    $name_tag->detach_content; # delete dummy content ("Oct 6, 2001")
    $name_tag->push_content($date);
  }

=head3 Or with convenience methods:

  use HTML::Seamstress; 
  my $tree = HTML::Seamstress->new_from_file($html_file);
  $tree->content_handler(name => 'bob');
  $tree->content_handler(date => `date`);


=head1 DESCRIPTION

From reading L<HTML::Tree::Scanning>, we know that HTML has a tree
structure. HTML::Seamstress is a subclass of L<HTML::TreeBuilder>
which makes it a little easier to perform common HTML templating
operations as tree rewriting.

=head2 Text Substitution == Node rewriting

The L</SYNOPSIS> gave an example of text substitution. From a
tree-writing perspective, text substitution involves an in-place
change to the content of a node of an HTML tree.

There are two methods for rewriting the content of a node. The more powerful
method is C<set_child_content>. The more convenient is C<content_handler>.

=head3 $tree->set_child_content(@look_down, $content)

This method looks down $tree using the criteria specified in 
C<@look_down>
using the the HTML::Element C<look_down()> method. 

After finding the node, it detaches the node's content
and pushes $content as the node's content.

=head3 $tree->content_handler($sid_value , $content)

This is a convenience method. Because the look_down criteria will often
simply be:

   sid => 'fixup'

to find things like:

   <a sid=fixup href=http://www.somesite.org>replace_content</a>

You can call this method to shorten your typing a bit:

   $tree->content_handler( fixup => 'new text' )

Instead of typing:

  $tree->set_child_content(sid => 'fixup', 'new text') 


=head3 set_content()

=head2 Conditional Processing (aka if/unless) == Node Deletion 

In tree rewriting terms, an C<if> directive is used to decide whether a 
particular node of the HTML tree is preserved or deleted.

For example, given this Template-style HTML:

 [% IF age < 10 %]
       Hello, does your mother know you're 
       using her AOL account?
 [% ELSIF age < 18 %]
       Sorry, you're not old enough to enter 
       (and too dumb to lie about your age)
 [% ELSE %]
       Welcome
 [% END %]

Here is the HTML and Perl for Seamstress:

  <span id=age_handler>
    <span id="under10">
       Hello, does your mother know you're 
       using her AOL account?
    </span>
    <span id="under18">
       Sorry, you're not old enough to enter 
       (and too dumb to lie about your age)
    </span>
    <span id="welcome">
       Welcome
    </span>
  </span>

 package HTML::Seamstress
 use HTML::Seamstress;
 my $tree = HTML::Seamstress->new();
 $tree->parse_file($filename);
 $tree->age_handler($age);
 print $tree->as_HTML;

 sub age_handler {
   my ($tree, $age) = @_;
   my $SPAN = $tree->look_down('id', 'age_handler');
   if ($age < 10) {
    $SPAN->look_down('id', $_)->detach for qw(under18 welcome);
   } elsif ($age < 18) {
    $SPAN->look_down('id', $_)->detach for qw(under10 welcome);
  } else {
        $SPAN->look_down('id', $_)->detach for qw(under10 under18);
  }

 }


=head2 Looping (e.g. Table Unrolling) == Child Replication

=head3 Sample Model

 package Simple::Class;
 
 use Set::Array;
 
 my @name   = qw(bob bill brian babette bobo bix);
 my @age    = qw(99  12   44    52      12   43);
 my @weight = qw(99  52   80   124     120  230);
 
 
 sub new {
     my $this = shift;
     bless {}, ref($this) || $this;
 }
 
 sub load_data {
     my @data;
 
     for (0 .. 5) {
 	push @data, { 
 	    age    => $age[rand $#age] + int rand 20,
 	    name   => shift @name,
 	    weight => $weight[rand $#weight] + int rand 40
 	    }
     }
 
   Set::Array->new(@data);
 }
 
 
 1;


=head4 Sample Usage:

       my $data = Simple::Class->load_data;
       ++$_->{age} for @$data

=head3 Inline Code to Unroll a Table

=head4 HTML

 <html>
 
   <table id="load_data">
 
     <tr>  <th>name</th><th>age</th><th>weight</th> </tr>
 
     <tr id="iterate">
 
         <td id="name">   NATURE BOY RIC FLAIR  </td>
         <td id="age">    35                    </td>
         <td id="weight"> 220                   </td>
 
     </tr>
 
   </table>
 
 </html>


=head4 Perl

 require 'simple-class.pl';
 use HTML::Seamstress;
 
 # load the view
 my $seamstress = HTML::Seamstress->new_from_file('simple.html');
 
 # load the model
 my $o = Simple::Class->new;
 my $data = $o->load_data;
 
 # find the <table> and <tr> 
 my $table_node = $seamstress->look_down('id', 'load_data');
 my $iter_node  = $table_node->look_down('id', 'iterate');
 my $table_parent = $table_node->parent;
 
 
 # drop the sample <table> and <tr> from the HTML
 # only add them in if there is data in the model
 # this is achieved via the $add_table flag
 
 $table_node->detach;
 $iter_node->detach;
 my $add_table;
 
 # Get a row of model data
 while (my $row = shift @$data) {
 
   # We got row data. Set the flag indicating ok to hook the table into the HTML
   ++$add_table;
 
   # clone the sample <tr>
   my $new_iter_node = $iter_node->clone;
 
   # find the tags labeled name age and weight and 
   # set their content to the row data
   $new_iter_node->content_handler($_ => $row->{$_}) 
     for qw(name age weight);
 
   $table_node->push_content($new_iter_node);
 
 }
 
 # reattach the table to the HTML tree if we loaded data into some table rows
 
 $table_parent->push_content($table_node) if $add_table;
 
 print $seamstress->as_HTML;
 


=head3 Seamstress API call to Unroll a Table

 require 'simple-class.pl';
 use HTML::Seamstress;
 
 # load the view
 my $seamstress = HTML::Seamstress->new_from_file('simple.html');
 # load the model
 my $o = Simple::Class->new;
 
 $seamstress->table
   (
    # tell seamstress where to find the table, via the method call
    # ->look_down('id', $gi_table). Seamstress detaches the table from the
    # HTML tree automatically if no table rows can be built
 
      gi_table    => 'load_data',
 
    # tell seamstress where to find the tr. This is a bit useless as
    # the <tr> usually can be found as the first child of the parent
 
      gi_tr       => 'iterate',
      
    # the model data to be pushed into the table
 
      table_data  => $o->load_data,
 
    # the way to take the model data and obtain one row
    # if the table data were a hashref, we would do:
    # my $key = (keys %$data)[0]; my $val = $data->{$key}; delete $data->{$key}
 
      tr_data     => sub { my ($self, $data) = @_;
 			  shift(@{$data}) ;
 			},
 
    # the way to take a row of data and fill the <td> tags
 
      td_data     => sub { my ($tr_node, $tr_data) = @_;
 			  $tr_node->content_handler($_ => $tr_data->{$_})
 			    for qw(name age weight) }
 
   );
 
 
 print $seamstress->as_HTML;


=head3 Looping over Multiple Sample Rows

* HTML

 <html>
 
   <table id="load_data" CELLPADDING=8 BORDER=2>
 
     <tr>  <th>name</th><th>age</th><th>weight</th> </tr>
 
     <tr id="iterate1" BGCOLOR="white" >
 
         <td id="name">   NATURE BOY RIC FLAIR  </td>
         <td id="age">    35                    </td>
         <td id="weight"> 220                   </td>
 
     </tr>
     <tr id="iterate2" BGCOLOR="#CCCC99">
 
         <td id="name">   NATURE BOY RIC FLAIR  </td>
         <td id="age">    35                    </td>
         <td id="weight"> 220                   </td>
 
     </tr>
 
   </table>
 
 </html>


* Only one change to last API call. 

This:

	gi_tr       => 'iterate',

becomes this:

	gi_tr       => ['iterate1', 'iterate2']

=head3 Whither a Table with No Rows

Often when a table has no rows, we want to display a message
indicating this to the view. Use conditional processing to decide what
to display:

	<span id=no_data>
		<table><tr><td>No Data is Good Data</td></tr></table>
	</span>
	<span id=load_data>
 <html>
 
   <table id="load_data">
 
     <tr>  <th>name</th><th>age</th><th>weight</th> </tr>
 
     <tr id="iterate">
 
         <td id="name">   NATURE BOY RIC FLAIR  </td>
         <td id="age">    35                    </td>
         <td id="weight"> 220                   </td>
 
     </tr>
 
   </table>
 
 </html>

	</span>



=head2 EXPORT

None by default.



=head1 SEE ALSO

=head2 HTML Templating as Tree Rewriting: Part I: "If Statements"

L<http://perlmonks.org/index.pl?node_id=302606>

=head2 HTATR II: HTML table generation via DWIM tree rewriting

L<http://perlmonks.org/index.pl?node_id=303188>

=head2 Los Angeles Perl Mongers Talk on HTML::Seamstress

L<http://www.metaperl.com>

=head2 XMLC, A similar framework for Java

L<http://xmlc.enhydra.org>

=head2 Similar Frameworks for Perl 

Two other frameworks come to mind. Both are stricter with regard
to the correctness of the HTML and both use a different means for 
node lookup and rewrite.

For me, Seamstress was a natural extension of my love for 
HTML::TreeBuilder, but if I had an XML job to do, I really think I would
reach for Petal. It is quite sweet.

=over

=item * L<Petal>

Based on Zope's TAL, this is a very nice and complete framework that is
the basis of MkDoc, a XML application server.

=item * L<XML::LibXML>

By the XML guru Matt Sergeant, who is also the author of AxKit, another XML 
application server.

=back

=head1 AUTHOR

Terrence Brannon, E<lt>tbone@cpan.org<gt>

=head1 COPYRIGHT AND LICENSE

Copyright 2002-2004 by Terrence Brannon.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself. 

=cut
