]> iEval git - html-element-library.git/blob - lib/HTML/Element/Library.pm
523c2111b8cc3987f0bf5a12000bf61dfd5666bd
[html-element-library.git] / lib / HTML / Element / Library.pm
1 package HTML::Element::Library;
2
3 use 5.006001;
4 use strict;
5 use warnings;
6
7
8 our $DEBUG = 0;
9 #our $DEBUG = 1;
10
11 use Array::Group qw(:all);
12 use Carp qw(confess);
13 use Data::Dumper;
14 use HTML::Element;
15 use List::MoreUtils qw/:all/;
16 use Params::Validate qw(:all);
17 use Scalar::Listify;
18 #use Tie::Cycle;
19 use List::Rotation::Cycle;
20
21 our %EXPORT_TAGS = ( 'all' => [ qw() ] );
22 our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
23 our @EXPORT = qw();
24
25
26
27 our $VERSION = '3.53';
28
29
30 # Preloaded methods go here.
31
32 sub HTML::Element::siblings {
33 my $element = shift;
34 my $p = $element->parent;
35 return () unless $p;
36 $p->content_list;
37 }
38
39 sub HTML::Element::passover {
40 my ($tree, $child_id) = @_;
41
42 #warn "ARGS: my ($tree, $child)";
43
44 my $exodus = $tree->look_down(id => $child_id);
45
46 my @s = HTML::Element::siblings($exodus);
47
48 warn "sibling count", scalar @s;
49 warn "siblings", join ':', @s;
50
51 for my $s (@s) {
52 warn "SIBLING: $s";
53 warn "ref sib", ref $s;
54 next unless ref $s;
55 if ($s->attr('id') eq $child_id) {
56 ;
57 } else {
58 $s->delete;
59 }
60 }
61
62 return $exodus; # Goodbye Egypt! http://en.wikipedia.org/wiki/Passover
63
64 }
65
66 sub HTML::Element::sibdex {
67
68 my $element = shift;
69 firstidx { $_ eq $element } $element->siblings
70
71 }
72
73 sub HTML::Element::addr { goto &HTML::Element::sibdex }
74
75 sub HTML::Element::replace_content {
76 my $elem = shift;
77 $elem->delete_content;
78 $elem->push_content(@_);
79 }
80
81 sub HTML::Element::wrap_content {
82 my($self, $wrap) = @_;
83 my $content = $self->content;
84 if (ref $content) {
85 $wrap->push_content(@$content);
86 @$content = ($wrap);
87 }
88 else {
89 $self->push_content($wrap);
90 }
91 $wrap;
92 }
93
94 sub HTML::Element::Library::super_literal {
95 my($text) = @_;
96
97 HTML::Element->new('~literal', text => $text);
98 }
99
100
101 sub HTML::Element::position {
102 # Report coordinates by chasing addr's up the
103 # HTML::ElementSuper tree. We know we've reached
104 # the top when a) there is no parent, or b) the
105 # parent is some HTML::Element unable to report
106 # it's position.
107 my $p = shift;
108 my @pos;
109 while ($p) {
110 my $a = $p->addr;
111 unshift(@pos, $a) if defined $a;
112 $p = $p->parent;
113 }
114 @pos;
115 }
116
117
118 sub HTML::Element::content_handler {
119 my ($tree, $id_name, $content) = @_;
120
121 $tree->set_child_content(id => $id_name, $content);
122
123 }
124
125
126 sub make_counter {
127 my $i = 1;
128 sub {
129 shift() . ':' . $i++
130 }
131 }
132
133
134 sub HTML::Element::iter {
135 my ($tree, $p, @data) = @_;
136
137 # warn 'P: ' , $p->attr('id') ;
138 # warn 'H: ' , $p->as_HTML;
139
140 # my $id_incr = make_counter;
141 my @item = map {
142 my $new_item = clone $p;
143 $new_item->replace_content($_);
144 # $new_item->attr('id', $id_incr->( $p->attr('id') ));
145 $new_item;
146 } @data;
147
148 $p->replace_with(@item);
149
150 }
151
152
153 sub HTML::Element::iter2 {
154
155 my $tree = shift;
156
157 #warn "INPUT TO TABLE2: ", Dumper \@_;
158
159 my %p = validate(
160 @_, {
161 wrapper_ld => { default => ['_tag' => 'dl'] },
162 wrapper_data => 1,
163 wrapper_proc => { default => undef },
164 item_ld => { default => sub {
165 my $tree = shift;
166 [
167 $tree->look_down('_tag' => 'dt'),
168 $tree->look_down('_tag' => 'dd')
169 ];
170 }
171 },
172 item_data => { default => sub { my ($wrapper_data) = @_;
173 shift(@{$wrapper_data}) ;
174 }},
175 item_proc => {
176 default => sub {
177 my ($item_elems, $item_data, $row_count) = @_;
178 $item_elems->[$_]->replace_content($item_data->[$_]) for (0,1) ;
179 $item_elems;
180 }},
181 splice => { default => sub {
182 my ($container, @item_elems) = @_;
183 $container->splice_content(0, 2, @item_elems);
184 }
185 },
186 debug => {default => 0}
187 }
188 );
189
190 warn "wrapper_data: " . Dumper $p{wrapper_data} if $p{debug} ;
191
192 my $container = ref_or_ld($tree, $p{wrapper_ld});
193 warn "wrapper_(preproc): " . $container->as_HTML if $p{debug} ;
194 $p{wrapper_proc}->($container) if defined $p{wrapper_proc} ;
195 warn "wrapper_(postproc): " . $container->as_HTML if $p{debug} ;
196
197 my $_item_elems = $p{item_ld}->($container);
198
199
200
201 my $row_count;
202 my @item_elem;
203 {
204 my $item_data = $p{item_data}->($p{wrapper_data});
205 last unless defined $item_data;
206
207 warn Dumper("item_data", $item_data);
208
209
210 my $item_elems = [ map { $_->clone } @{$_item_elems} ] ;
211
212 if ($p{debug}) {
213 for (@{$item_elems}) {
214 warn "ITEM_ELEMS ", $_->as_HTML;
215 }
216 }
217
218 my $new_item_elems = $p{item_proc}->($item_elems, $item_data, ++$row_count);
219
220 if ($p{debug}) {
221 for (@{$new_item_elems}) {
222 warn "NEWITEM_ELEMS ", $_->as_HTML;
223 }
224 }
225
226
227 push @item_elem, @{$new_item_elems} ;
228
229 redo;
230 }
231
232 warn "pushing " . @item_elem . " elems " if $p{debug} ;
233
234 $p{splice}->($container, @item_elem);
235
236 }
237
238 sub HTML::Element::dual_iter {
239 my ($parent, $data) = @_;
240
241 my ($prototype_a, $prototype_b) = $parent->content_list;
242
243 # my $id_incr = make_counter;
244
245 my $i;
246
247 @$data %2 == 0 or
248 confess 'dataset does not contain an even number of members';
249
250 my @iterable_data = ngroup 2 => @$data;
251
252 my @item = map {
253 my ($new_a, $new_b) = map { clone $_ } ($prototype_a, $prototype_b) ;
254 $new_a->splice_content(0,1, $_->[0]);
255 $new_b->splice_content(0,1, $_->[1]);
256 #$_->attr('id', $id_incr->($_->attr('id'))) for ($new_a, $new_b) ;
257 ($new_a, $new_b)
258 } @iterable_data;
259
260 $parent->splice_content(0, 2, @item);
261
262 }
263
264
265 sub HTML::Element::set_child_content {
266 my $tree = shift;
267 my $content = pop;
268 my @look_down = @_;
269
270 my $content_tag = $tree->look_down(@look_down);
271
272 unless ($content_tag) {
273 warn "criteria [@look_down] not found";
274 return;
275 }
276
277 $content_tag->replace_content($content);
278
279 }
280
281 sub HTML::Element::highlander {
282 my ($tree, $local_root_id, $aref, @arg) = @_;
283
284 ref $aref eq 'ARRAY' or confess
285 "must supply array reference";
286
287 my @aref = @$aref;
288 @aref % 2 == 0 or confess
289 "supplied array ref must have an even number of entries";
290
291 warn __PACKAGE__ if $DEBUG;
292
293 my $survivor;
294 while (my ($id, $test) = splice @aref, 0, 2) {
295 warn $id if $DEBUG;
296 if ($test->(@arg)) {
297 $survivor = $id;
298 last;
299 }
300 }
301
302
303 my @id_survivor = (id => $survivor);
304 my $survivor_node = $tree->look_down(@id_survivor);
305 # warn $survivor;
306 # warn $local_root_id;
307 # warn $node;
308
309 warn "survivor: $survivor" if $DEBUG;
310 warn "tree: " . $tree->as_HTML if $DEBUG;
311
312 $survivor_node or die "search for @id_survivor failed in tree($tree): " . $tree->as_HTML;
313
314 my $survivor_node_parent = $survivor_node->parent;
315 $survivor_node = $survivor_node->clone;
316 $survivor_node_parent->replace_content($survivor_node);
317
318 warn "new tree: " . $tree->as_HTML if $DEBUG;
319
320 $survivor_node;
321 }
322
323
324 sub HTML::Element::highlander2 {
325 my $tree = shift;
326
327 my %p = validate(@_, {
328 cond => { type => ARRAYREF },
329 cond_arg => { type => ARRAYREF,
330 default => []
331 },
332 debug => { default => 0 }
333 }
334 );
335
336
337 my @cond = @{$p{cond}};
338 @cond % 2 == 0 or confess
339 "supplied array ref must have an even number of entries";
340
341 warn __PACKAGE__ if $p{debug};
342
343 my @cond_arg = @{$p{cond_arg}};
344
345 my $survivor; my $then;
346 while (my ($id, $if_then) = splice @cond, 0, 2) {
347
348 warn $id if $p{debug};
349 my ($if, $_then);
350
351 if (ref $if_then eq 'ARRAY') {
352 ($if, $_then) = @$if_then;
353 } else {
354 ($if, $_then) = ($if_then, sub {});
355 }
356
357 if ($if->(@cond_arg)) {
358 $survivor = $id;
359 $then = $_then;
360 last;
361 }
362
363 }
364
365 my @ld = (ref $survivor eq 'ARRAY')
366 ? @$survivor
367 : (id => $survivor)
368 ;
369
370 warn "survivor: ", $survivor if $p{debug};
371 warn "survivor_ld: ", Dumper \@ld if $p{debug};
372
373
374 my $survivor_node = $tree->look_down(@ld);
375
376 $survivor_node or confess
377 "search for @ld failed in tree($tree): " . $tree->as_HTML;
378
379 my $survivor_node_parent = $survivor_node->parent;
380 $survivor_node = $survivor_node->clone;
381 $survivor_node_parent->replace_content($survivor_node);
382
383
384 # **************** NEW FUNCTIONALITY *******************
385
386 # apply transforms on survivor node
387
388
389 warn "SURV::pre_trans " . $survivor_node->as_HTML if $p{debug};
390 $then->($survivor_node, @cond_arg);
391 warn "SURV::post_trans " . $survivor_node->as_HTML if $p{debug};
392
393 # **************** NEW FUNCTIONALITY *******************
394
395
396
397
398 $survivor_node;
399 }
400
401
402 sub overwrite_action {
403 my ($mute_node, %X) = @_;
404
405 $mute_node->attr($X{local_attr}{name} => $X{local_attr}{value}{new});
406 }
407
408
409 sub HTML::Element::overwrite_attr {
410 my $tree = shift;
411
412 $tree->mute_elem(@_, \&overwrite_action);
413 }
414
415
416
417 sub HTML::Element::mute_elem {
418 my ($tree, $mute_attr, $closures, $post_hook) = @_;
419
420 warn "my mute_node = $tree->look_down($mute_attr => qr/.*/) ;";
421 my @mute_node = $tree->look_down($mute_attr => qr/.*/) ;
422
423 for my $mute_node (@mute_node) {
424 my ($local_attr,$mute_key) = split /\s+/, $mute_node->attr($mute_attr);
425 my $local_attr_value_current = $mute_node->attr($local_attr);
426 my $local_attr_value_new = $closures->{$mute_key}->($tree, $mute_node, $local_attr_value_current);
427 $post_hook->(
428 $mute_node,
429 tree => $tree,
430 local_attr => {
431 name => $local_attr,
432 value => {
433 current => $local_attr_value_current,
434 new => $local_attr_value_new
435 }
436 }
437 ) if ($post_hook) ;
438 }
439 }
440
441
442
443 sub HTML::Element::table {
444
445 my ($s, %table) = @_;
446
447 my $table = {};
448
449 # use Data::Dumper; warn Dumper \%table;
450
451 # ++$DEBUG if $table{debug} ;
452
453
454 # Get the table element
455 $table->{table_node} = $s->look_down(id => $table{gi_table});
456 $table->{table_node} or confess
457 "table tag not found via (id => $table{gi_table}";
458
459 # Get the prototype tr element(s)
460 my @table_gi_tr = listify $table{gi_tr} ;
461 my @iter_node = map
462 {
463 my $tr = $table->{table_node}->look_down(id => $_);
464 $tr or confess "tr with id => $_ not found";
465 $tr;
466 } @table_gi_tr;
467
468 warn "found " . @iter_node . " iter nodes " if $DEBUG;
469 # tie my $iter_node, 'Tie::Cycle', \@iter_node;
470 my $iter_node = List::Rotation::Cycle->new(@iter_node);
471
472 # warn $iter_node;
473 warn Dumper ($iter_node, \@iter_node) if $DEBUG;
474
475 # $table->{content} = $table{content};
476 #$table->{parent} = $table->{table_node}->parent;
477
478
479 # $table->{table_node}->detach;
480 # $_->detach for @iter_node;
481
482 my @table_rows;
483
484 {
485 my $row = $table{tr_data}->($table, $table{table_data});
486 last unless defined $row;
487
488 # get a sample table row and clone it.
489 my $I = $iter_node->next;
490 warn "I: $I" if $DEBUG;
491 my $new_iter_node = $I->clone;
492
493
494 $table{td_data}->($new_iter_node, $row);
495 push @table_rows, $new_iter_node;
496
497 redo;
498 }
499
500 if (@table_rows) {
501
502 my $replace_with_elem = $s->look_down(id => shift @table_gi_tr) ;
503 for (@table_gi_tr) {
504 $s->look_down(id => $_)->detach;
505 }
506
507 $replace_with_elem->replace_with(@table_rows);
508
509 }
510
511 }
512
513 sub ref_or_ld {
514
515 my ($tree, $slot) = @_;
516
517 if (ref($slot) eq 'CODE') {
518 $slot->($tree);
519 } else {
520 $tree->look_down(@$slot);
521 }
522 }
523
524
525
526 sub HTML::Element::table2 {
527
528 my $tree = shift;
529
530
531
532 my %p = validate(
533 @_, {
534 table_ld => { default => ['_tag' => 'table'] },
535 table_data => 1,
536 table_proc => { default => undef },
537
538 tr_ld => { default => ['_tag' => 'tr'] },
539 tr_data => { default => sub { my ($self, $data) = @_;
540 shift(@{$data}) ;
541 }},
542 tr_base_id => { default => undef },
543 tr_proc => { default => sub {} },
544 td_proc => 1,
545 debug => {default => 0}
546 }
547 );
548
549 warn "INPUT TO TABLE2: ", Dumper \@_ if $p{debug};
550
551 warn "table_data: " . Dumper $p{table_data} if $p{debug} ;
552
553 my $table = {};
554
555 # use Data::Dumper; warn Dumper \%table;
556
557 # ++$DEBUG if $table{debug} ;
558
559 # Get the table element
560 warn 1;
561 $table->{table_node} = ref_or_ld( $tree, $p{table_ld} ) ;
562 warn 2;
563 $table->{table_node} or confess
564 "table tag not found via " . Dumper($p{table_ld}) ;
565
566 warn "table: " . $table->{table_node}->as_HTML if $p{debug};
567
568
569 # Get the prototype tr element(s)
570 my @proto_tr = ref_or_ld( $table->{table_node}, $p{tr_ld} ) ;
571
572 warn "found " . @proto_tr . " iter nodes " if $p{debug};
573
574 @proto_tr or return ;
575
576 if ($p{debug}) {
577 warn $_->as_HTML for @proto_tr;
578 }
579 my $proto_tr = List::Rotation::Cycle->new(@proto_tr);
580
581 my $tr_parent = $proto_tr[0]->parent;
582 warn "parent element of trs: " . $tr_parent->as_HTML if $p{debug};
583
584 my $row_count;
585
586 my @table_rows;
587
588 {
589 my $row = $p{tr_data}->($table, $p{table_data}, $row_count);
590 warn "data row: " . Dumper $row if $p{debug};
591 last unless defined $row;
592
593 # wont work: my $new_iter_node = $table->{iter_node}->clone;
594 my $new_tr_node = $proto_tr->next->clone;
595 warn "new_tr_node: $new_tr_node" if $p{debug};
596
597 $p{tr_proc}->($tree, $new_tr_node, $row, $p{tr_base_id}, ++$row_count)
598 if defined $p{tr_proc};
599
600 warn "data row redux: " . Dumper $row if $p{debug};
601 warn 3.3;
602
603 $p{td_proc}->($new_tr_node, $row);
604 push @table_rows, $new_tr_node;
605
606 warn 4.4;
607
608 redo;
609 }
610
611 $_->detach for @proto_tr;
612
613 $tr_parent->push_content(@table_rows) if (@table_rows) ;
614
615 }
616
617
618 sub HTML::Element::unroll_select {
619
620 my ($s, %select) = @_;
621
622 my $select = {};
623
624 my $select_node = $s->look_down(id => $select{select_label});
625
626 my $option = $select_node->look_down('_tag' => 'option');
627
628 # warn $option;
629
630
631 $option->detach;
632
633 while (my $row = $select{data_iter}->($select{data}))
634 {
635 # warn Dumper($row);
636 my $o = $option->clone;
637 $o->attr('value', $select{option_value}->($row));
638 $o->attr('SELECTED', 1) if ($select{option_selected}->($row)) ;
639
640 $o->replace_content($select{option_content}->($row));
641 $select_node->push_content($o);
642 }
643
644
645 }
646
647
648
649 sub HTML::Element::set_sibling_content {
650 my ($elt, $content) = @_;
651
652 $elt->parent->splice_content($elt->pindex + 1, 1, $content);
653
654 }
655
656 sub HTML::TreeBuilder::parse_string {
657 my ($package, $string) = @_;
658
659 my $h = HTML::TreeBuilder->new;
660 HTML::TreeBuilder->parse($string);
661
662 }
663
664
665
666 1;
667 __END__
668 # Below is stub documentation for your module. You'd better edit it!
669
670 =head1 NAME
671
672 HTML::Element::Library - HTML::Element convenience functions
673
674 =head1 SYNOPSIS
675
676 use HTML::Element::Library;
677 use HTML::TreeBuilder;
678
679 =head1 DESCRIPTION
680
681 This method provides API calls for common actions on trees when using
682 L<HTML::Tree>.
683
684 =head1 METHODS
685
686 The test suite contains examples of each of these methods in a
687 file C<t/$method.t>
688
689 =head2 Positional Querying Methods
690
691 =head3 $elem->siblings
692
693 Return a list of all nodes under the same parent.
694
695 =head3 $elem->sibdex
696
697 Return the index of C<$elem> into the array of siblings of which it is
698 a part. L<HTML::ElementSuper> calls this method C<addr> but I don't think
699 that is a descriptive name. And such naming is deceptively close to the
700 C<address> function of C<HTML::Element>. HOWEVER, in the interest of
701 backwards compatibility, both methods are available.
702
703 =head3 $elem->addr
704
705 Same as sibdex
706
707 =head3 $elem->position()
708
709 Returns the coordinates of this element in the tree it inhabits.
710 This is accomplished by succesively calling addr() on ancestor
711 elements until either a) an element that does not support these
712 methods is found, or b) there are no more parents. The resulting
713 list is the n-dimensional coordinates of the element in the tree.
714
715 =head2 Element Decoration Methods
716
717 =head3 HTML::Element::Library::super_literal($text)
718
719 In L<HTML::Element>, Sean Burke discusses super-literals. They are
720 text which does not get escaped. Great for includng Javascript in
721 HTML. Also great for including foreign language into a document.
722
723 So, you basically toss C<super_literal> your text and back comes
724 your text wrapped in a C<~literal> element.
725
726 One of these days, I'll around to writing a nice C<EXPORT> section.
727
728 =head2 Tree Rewriting Methods
729
730 =head3 $elem->replace_content(@new_elem)
731
732 Replaces all of C<$elem>'s content with C<@new_elem>.
733
734 =head3 $elem->wrap_content($wrapper_element)
735
736 Wraps the existing content in the provided element. If the provided element
737 happens to be a non-element, a push_content is performed instead.
738
739 =head3 $elem->set_child_content(@look_down, $content)
740
741 This method looks down $tree using the criteria specified in @look_down using the the HTML::Element look_down() method.
742
743 After finding the node, it detaches the node's content and pushes $content as the node's content.
744
745 =head3 $tree->content_handler($sid_value , $content)
746
747 This is a convenience method. Because the look_down criteria will often simply be:
748
749 id => 'fixme'
750
751 to find things like:
752
753 <a id=fixme href=http://www.somesite.org>replace_content</a>
754
755 You can call this method to shorten your typing a bit. You can simply type
756
757 $elem->content_handler( fixme => 'new text' )
758
759 Instead of typing:
760
761 $elem->set_child_content(sid => 'fixme', 'new text')
762
763 =head3 $tree->highlander($subtree_span_id, $conditionals, @conditionals_args)
764
765 This allows for "if-then-else" style processing. Highlander was a movie in
766 which only one would survive. Well, in terms of a tree when looking at a
767 structure that you want to process in C<if-then-else> style, only one child
768 will survive. For example, given this HTML template:
769
770 <span klass="highlander" id="age_dialog">
771 <span id="under10">
772 Hello, does your mother know you're
773 using her AOL account?
774 </span>
775 <span id="under18">
776 Sorry, you're not old enough to enter
777 (and too dumb to lie about your age)
778 </span>
779 <span id="welcome">
780 Welcome
781 </span>
782 </span>
783
784 We only want one child of the C<span> tag with id C<age_dialog> to remain
785 based on the age of the person visiting the page.
786
787 So, let's setup a call that will prune the subtree as a function of age:
788
789 sub process_page {
790 my $age = shift;
791 my $tree = HTML::TreeBuilder->new_from_file('t/html/highlander.html');
792
793 $tree->highlander
794 (age_dialog =>
795 [
796 under10 => sub { $_[0] < 10} ,
797 under18 => sub { $_[0] < 18} ,
798 welcome => sub { 1 }
799 ],
800 $age
801 );
802
803 And there we have it. If the age is less than 10, then the node with
804 id C<under10> remains. For age less than 18, the node with id C<under18>
805 remains.
806 Otherwise our "else" condition fires and the child with id C<welcome> remains.
807
808 =head3 $tree->highlander2($tree, $conditionals, @conditionals_args)
809
810 Right around the same time that C<table2()> came into being, Seamstress
811 began to tackle tougher and tougher processing problems. It became clear that
812 a more powerful highlander was needed... one that not only snipped the tree
813 of the nodes that should not survive, but one that allows for
814 post-processing of the survivor node. And one that was more flexible with
815 how to find the nodes to snip.
816
817 Thus (drum roll) C<highlander2()>.
818
819 So let's look at our HTML which requires post-selection processing:
820
821 <span klass="highlander" id="age_dialog">
822 <span id="under10">
823 Hello, little <span id=age>AGE</span>-year old,
824 does your mother know you're using her AOL account?
825 </span>
826 <span id="under18">
827 Sorry, you're only <span id=age>AGE</span>
828 (and too dumb to lie about your age)
829 </span>
830 <span id="welcome">
831 Welcome, isn't it good to be <span id=age>AGE</span> years old?
832 </span>
833 </span>
834
835 In this case, a branch survives, but it has dummy data in it. We must take
836 the surviving segment of HTML and rewrite the age C<span> with the age.
837 Here is how we use C<highlander2()> to do so:
838
839 sub replace_age {
840 my $branch = shift;
841 my $age = shift;
842 $branch->look_down(id => 'age')->replace_content($age);
843 }
844
845 my $if_then = $tree->look_down(id => 'age_dialog');
846
847 $if_then->highlander2(
848 cond => [
849 under10 => [
850 sub { $_[0] < 10} ,
851 \&replace_age
852 ],
853 under18 => [
854 sub { $_[0] < 18} ,
855 \&replace_age
856 ],
857 welcome => [
858 sub { 1 },
859 \&replace_age
860 ]
861 ],
862 cond_arg => [ $age ]
863 );
864
865 We pass it the tree (C<$if_then>), an arrayref of conditions
866 (C<cond>) and an arrayref of arguments which are passed to the
867 C<cond>s and to the replacement subs.
868
869 The C<under10>, C<under18> and C<welcome> are id attributes in the
870 tree of the siblings of which only one will survive. However,
871 should you need to do
872 more complex look-downs to find the survivor,
873 then supply an array ref instead of a simple
874 scalar:
875
876
877 $if_then->highlander2(
878 cond => [
879 [class => 'r12'] => [
880 sub { $_[0] < 10} ,
881 \&replace_age
882 ],
883 [class => 'z22'] => [
884 sub { $_[0] < 18} ,
885 \&replace_age
886 ],
887 [class => 'w88'] => [
888 sub { 1 },
889 \&replace_age
890 ]
891 ],
892 cond_arg => [ $age ]
893 );
894
895
896 =head3 $tree->overwrite_attr($mutation_attr => $mutating_closures)
897
898 This method is designed for taking a tree and reworking a set of nodes in
899 a stereotyped fashion. For instance let's say you have 3 remote image
900 archives, but you don't want to put long URLs in your img src
901 tags for reasons of abstraction, re-use and brevity. So instead you do this:
902
903 <img src="/img/smiley-face.jpg" fixup="src lnc">
904 <img src="/img/hot-babe.jpg" fixup="src playboy">
905 <img src="/img/footer.jpg" fixup="src foobar">
906
907 and then when the tree of HTML is being processed, you make this call:
908
909 my %closures = (
910 lnc => sub { my ($tree, $mute_node, $attr_value)= @_; "http://lnc.usc.edu$attr_value" },
911 playboy => sub { my ($tree, $mute_node, $attr_value)= @_; "http://playboy.com$attr_value" }
912 foobar => sub { my ($tree, $mute_node, $attr_value)= @_; "http://foobar.info$attr_value" }
913 )
914
915 $tree->overwrite_attr(fixup => \%closures) ;
916
917 and the tags come out modified like so:
918
919 <img src="http://lnc.usc.edu/img/smiley-face.jpg" fixup="src lnc">
920 <img src="http://playboy.com/img/hot-babe.jpg" fixup="src playboy">
921 <img src="http://foobar.info/img/footer.jpg" fixup="src foobar">
922
923 =head3 $tree->mute_elem($mutation_attr => $mutating_closures, [ $post_hook ] )
924
925 This is a generalization of C<overwrite_attr>. C<overwrite_attr>
926 assumes the return value of the
927 closure is supposed overwrite an attribute value and does it for you.
928 C<mute_elem> is a more general function which does nothing but
929 hand the closure the element and let it mutate it as it jolly well pleases :)
930
931 In fact, here is the implementation of C<overwrite_attr>
932 to give you a taste of how C<mute_attr> is used:
933
934 sub overwrite_action {
935 my ($mute_node, %X) = @_;
936
937 $mute_node->attr($X{local_attr}{name} => $X{local_attr}{value}{new});
938 }
939
940
941 sub HTML::Element::overwrite_attr {
942 my $tree = shift;
943
944 $tree->mute_elem(@_, \&overwrite_action);
945 }
946
947
948
949
950 =head2 Tree-Building Methods: Unrolling an array via a single sample element (<ul> container)
951
952 This is best described by example. Given this HTML:
953
954 <strong>Here are the things I need from the store:</strong>
955 <ul>
956 <li class="store_items">Sample item</li>
957 </ul>
958
959 We can unroll it like so:
960
961 my $li = $tree->look_down(class => 'store_items');
962
963 my @items = qw(bread butter vodka);
964
965 $tree->iter($li => @items);
966
967 To produce this:
968
969
970 <html>
971 <head></head>
972 <body>Here are the things I need from the store:
973 <ul>
974 <li class="store_items">bread</li>
975 <li class="store_items">butter</li>
976 <li class="store_items">vodka</li>
977 </ul>
978 </body>
979 </html>
980
981 =head2 Tree-Building Methods: Unrolling an array via n sample elements (<dl> container)
982
983 C<iter()> was fine for awhile, but some things
984 (e.g. definition lists) need a more general function to make them easy to
985 do. Hence C<iter2()>. This function will be explained by example of unrolling
986 a simple definition list.
987
988 So here's our mock-up HTML from the designer:
989
990 <dl class="dual_iter" id="service_plan">
991 <dt>
992 Artist
993 </dt>
994 <dd>
995 A person who draws blood.
996 </dd>
997
998 <dt>
999 Musician
1000 </dt>
1001 <dd>
1002 A clone of Iggy Pop.
1003 </dd>
1004
1005 <dt>
1006 Poet
1007 </dt>
1008 <dd>
1009 A relative of Edgar Allan Poe.
1010 </dd>
1011
1012 <dt class="adstyle">sample header</dt>
1013 <dd class="adstyle2">sample data</dd>
1014
1015 </dl>
1016
1017
1018 And we want to unroll our data set:
1019
1020 my @items = (
1021 ['the pros' => 'never have to worry about service again'],
1022 ['the cons' => 'upfront extra charge on purchase'],
1023 ['our choice' => 'go with the extended service plan']
1024 );
1025
1026
1027 Now, let's make this problem a bit harder to show off the power of C<iter2()>.
1028 Let's assume that we want only the last <dt> and it's accompanying <dd>
1029 (the one with "sample data") to be used as the sample data
1030 for unrolling with our data set. Let's further assume that we want them to
1031 remain in the final output.
1032
1033 So now, the API to C<iter2()> will be discussed and we will explain how our
1034 goal of getting our data into HTML fits into the API.
1035
1036 =over 4
1037
1038 =item * wrapper_ld
1039
1040 This is how to look down and find the container of all the elements we will
1041 be unrolling. The <dl> tag is the container for the dt and dd tags we will be
1042 unrolling.
1043
1044 If you pass an anonymous subroutine, then it is presumed that execution of
1045 this subroutine will return the HTML::Element representing the container tag.
1046 If you pass an array ref, then this will be dereferenced and passed to
1047 C<HTML::Element::look_down()>.
1048
1049 default value: C<< ['_tag' => 'dl'] >>
1050
1051 Based on the mock HTML above, this default is fine for finding our container
1052 tag. So let's move on.
1053
1054 =item * wrapper_data
1055
1056 This is an array reference of data that we will be putting into the container.
1057 You must supply this. C<@items> above is our C<wrapper_data>.
1058
1059 =item * wrapper_proc
1060
1061 After we find the container via C<wrapper_ld>, we may want to pre-process
1062 some aspect of this tree. In our case the first two sets of dt and dd need
1063 to be removed, leaving the last dt and dd. So, we supply a C<wrapper_proc>
1064 which will do this.
1065
1066 default: undef
1067
1068 =item * item_ld
1069
1070 This anonymous subroutine returns an array ref of C<HTML::Element>s that will
1071 be cloned and populated with item data
1072 (item data is a "row" of C<wrapper_data>).
1073
1074 default: returns an arrayref consisting of the dt and dd element inside the
1075 container.
1076
1077 =item * item_data
1078
1079 This is a subroutine that takes C<wrapper_data> and retrieves one "row"
1080 to be "pasted" into the array ref of C<HTML::Element>s found via C<item_ld>.
1081 I hope that makes sense.
1082
1083 default: shifts C<wrapper_data>.
1084
1085 =item * item_proc
1086
1087 This is a subroutine that takes the C<item_data> and the C<HTML::Element>s
1088 found via C<item_ld> and produces an arrayref of C<HTML::Element>s which will
1089 eventually be spliced into the container.
1090
1091 Note that this subroutine MUST return the new items. This is done
1092 So that more items than were passed in can be returned. This is
1093 useful when, for example, you must return 2 dts for an input data item.
1094 And when would you do this? When a single term has multiple spellings
1095 for instance.
1096
1097 default: expects C<item_data> to be an arrayref of two elements and
1098 C<item_elems> to be an arrayref of two C<HTML::Element>s. It replaces the
1099 content of the C<HTML::Element>s with the C<item_data>.
1100
1101 =item * splice
1102
1103 After building up an array of C<@item_elems>, the subroutine passed as
1104 C<splice> will be given the parent container HTML::Element and the
1105 C<@item_elems>. How the C<@item_elems> end up in the container is up to this
1106 routine: it could put half of them in. It could unshift them or whatever.
1107
1108 default: C<< $container->splice_content(0, 2, @item_elems) >>
1109 In other words, kill the 2 sample elements with the newly generated
1110 @item_elems
1111
1112 =back
1113
1114 So now that we have documented the API, let's see the call we need:
1115
1116 $tree->iter2(
1117 # default wrapper_ld ok.
1118 wrapper_data => \@items,
1119 wrapper_proc => sub {
1120 my ($container) = @_;
1121
1122 # only keep the last 2 dts and dds
1123 my @content_list = $container->content_list;
1124 $container->splice_content(0, @content_list - 2);
1125 },
1126
1127 # default item_ld is fine.
1128 # default item_data is fine.
1129 # default item_proc is fine.
1130 splice => sub {
1131 my ($container, @item_elems) = @_;
1132 $container->unshift_content(@item_elems);
1133 },
1134 debug => 1,
1135 );
1136
1137
1138 =head2 Tree-Building Methods: Select Unrolling
1139
1140 The C<unroll_select> method has this API:
1141
1142 $tree->unroll_select(
1143 select_label => $id_label,
1144 option_value => $closure, # how to get option value from data row
1145 option_content => $closure, # how to get option content from data row
1146 option_selected => $closure, # boolean to decide if SELECTED
1147 data => $data # the data to be put into the SELECT
1148 data_iter => $closure # the thing that will get a row of data
1149 );
1150
1151 Here's an example:
1152
1153 $tree->unroll_select(
1154 select_label => 'clan_list',
1155 option_value => sub { my $row = shift; $row->clan_id },
1156 option_content => sub { my $row = shift; $row->clan_name },
1157 option_selected => sub { my $row = shift; $row->selected },
1158 data => \@query_results,
1159 data_iter => sub { my $data = shift; $data->next }
1160 )
1161
1162
1163
1164 =head2 Tree-Building Methods: Table Generation
1165
1166 Matthew Sisk has a much more intuitive (imperative)
1167 way to generate tables via his module
1168 L<HTML::ElementTable|HTML::ElementTable>.
1169 However, for those with callback fever, the following
1170 method is available. First, we look at a nuts and bolts way to build a table
1171 using only standard L<HTML::Tree> API calls. Then the C<table> method
1172 available here is discussed.
1173
1174 =head3 Sample Model
1175
1176 package Simple::Class;
1177
1178 use Set::Array;
1179
1180 my @name = qw(bob bill brian babette bobo bix);
1181 my @age = qw(99 12 44 52 12 43);
1182 my @weight = qw(99 52 80 124 120 230);
1183
1184
1185 sub new {
1186 my $this = shift;
1187 bless {}, ref($this) || $this;
1188 }
1189
1190 sub load_data {
1191 my @data;
1192
1193 for (0 .. 5) {
1194 push @data, {
1195 age => $age[rand $#age] + int rand 20,
1196 name => shift @name,
1197 weight => $weight[rand $#weight] + int rand 40
1198 }
1199 }
1200
1201 Set::Array->new(@data);
1202 }
1203
1204
1205 1;
1206
1207
1208 =head4 Sample Usage:
1209
1210 my $data = Simple::Class->load_data;
1211 ++$_->{age} for @$data
1212
1213 =head3 Inline Code to Unroll a Table
1214
1215 =head4 HTML
1216
1217 <html>
1218
1219 <table id="load_data">
1220
1221 <tr> <th>name</th><th>age</th><th>weight</th> </tr>
1222
1223 <tr id="iterate">
1224
1225 <td id="name"> NATURE BOY RIC FLAIR </td>
1226 <td id="age"> 35 </td>
1227 <td id="weight"> 220 </td>
1228
1229 </tr>
1230
1231 </table>
1232
1233 </html>
1234
1235
1236 =head4 The manual way (*NOT* recommended)
1237
1238 require 'simple-class.pl';
1239 use HTML::Seamstress;
1240
1241 # load the view
1242 my $seamstress = HTML::Seamstress->new_from_file('simple.html');
1243
1244 # load the model
1245 my $o = Simple::Class->new;
1246 my $data = $o->load_data;
1247
1248 # find the <table> and <tr>
1249 my $table_node = $seamstress->look_down('id', 'load_data');
1250 my $iter_node = $table_node->look_down('id', 'iterate');
1251 my $table_parent = $table_node->parent;
1252
1253
1254 # drop the sample <table> and <tr> from the HTML
1255 # only add them in if there is data in the model
1256 # this is achieved via the $add_table flag
1257
1258 $table_node->detach;
1259 $iter_node->detach;
1260 my $add_table;
1261
1262 # Get a row of model data
1263 while (my $row = shift @$data) {
1264
1265 # We got row data. Set the flag indicating ok to hook the table into the HTML
1266 ++$add_table;
1267
1268 # clone the sample <tr>
1269 my $new_iter_node = $iter_node->clone;
1270
1271 # find the tags labeled name age and weight and
1272 # set their content to the row data
1273 $new_iter_node->content_handler($_ => $row->{$_})
1274 for qw(name age weight);
1275
1276 $table_node->push_content($new_iter_node);
1277
1278 }
1279
1280 # reattach the table to the HTML tree if we loaded data into some table rows
1281
1282 $table_parent->push_content($table_node) if $add_table;
1283
1284 print $seamstress->as_HTML;
1285
1286
1287
1288 =head3 $tree->table() : API call to Unroll a Table
1289
1290 require 'simple-class.pl';
1291 use HTML::Seamstress;
1292
1293 # load the view
1294 my $seamstress = HTML::Seamstress->new_from_file('simple.html');
1295 # load the model
1296 my $o = Simple::Class->new;
1297
1298 $seamstress->table
1299 (
1300 # tell seamstress where to find the table, via the method call
1301 # ->look_down('id', $gi_table). Seamstress detaches the table from the
1302 # HTML tree automatically if no table rows can be built
1303
1304 gi_table => 'load_data',
1305
1306 # tell seamstress where to find the tr. This is a bit useless as
1307 # the <tr> usually can be found as the first child of the parent
1308
1309 gi_tr => 'iterate',
1310
1311 # the model data to be pushed into the table
1312
1313 table_data => $o->load_data,
1314
1315 # the way to take the model data and obtain one row
1316 # if the table data were a hashref, we would do:
1317 # my $key = (keys %$data)[0]; my $val = $data->{$key}; delete $data->{$key}
1318
1319 tr_data => sub { my ($self, $data) = @_;
1320 shift(@{$data}) ;
1321 },
1322
1323 # the way to take a row of data and fill the <td> tags
1324
1325 td_data => sub { my ($tr_node, $tr_data) = @_;
1326 $tr_node->content_handler($_ => $tr_data->{$_})
1327 for qw(name age weight) }
1328
1329 );
1330
1331
1332 print $seamstress->as_HTML;
1333
1334
1335
1336 =head4 Looping over Multiple Sample Rows
1337
1338 * HTML
1339
1340 <html>
1341
1342 <table id="load_data" CELLPADDING=8 BORDER=2>
1343
1344 <tr> <th>name</th><th>age</th><th>weight</th> </tr>
1345
1346 <tr id="iterate1" BGCOLOR="white" >
1347
1348 <td id="name"> NATURE BOY RIC FLAIR </td>
1349 <td id="age"> 35 </td>
1350 <td id="weight"> 220 </td>
1351
1352 </tr>
1353 <tr id="iterate2" BGCOLOR="#CCCC99">
1354
1355 <td id="name"> NATURE BOY RIC FLAIR </td>
1356 <td id="age"> 35 </td>
1357 <td id="weight"> 220 </td>
1358
1359 </tr>
1360
1361 </table>
1362
1363 </html>
1364
1365
1366 * Only one change to last API call.
1367
1368 This:
1369
1370 gi_tr => 'iterate',
1371
1372 becomes this:
1373
1374 gi_tr => ['iterate1', 'iterate2']
1375
1376 =head3 $tree->table2() : New API Call to Unroll a Table
1377
1378 After 2 or 3 years with C<table()>, I began to develop
1379 production websites with it and decided it needed a cleaner
1380 interface, particularly in the area of handling the fact that
1381 C<id> tags will be the same after cloning a table row.
1382
1383 First, I will give a dry listing of the function's argument parameters.
1384 This will not be educational most likely. A better way to understand how
1385 to use the function is to read through the incremental unrolling of the
1386 function's interface given in conversational style after the dry listing.
1387 But take your pick. It's the same information given in two different
1388 ways.
1389
1390 =head4 Dry/technical parameter documentation
1391
1392 C<< $tree->table2(%param) >> takes the following arguments:
1393
1394 =over
1395
1396 =item * C<< table_ld => $look_down >> : optional
1397
1398 How to find the C<table> element in C<$tree>. If C<$look_down> is an
1399 arrayref, then use C<look_down>. If it is a CODE ref, then call it,
1400 passing it C<$tree>.
1401
1402 Defaults to C<< ['_tag' => 'table'] >> if not passed in.
1403
1404 =item * C<< table_data => $tabular_data >> : required
1405
1406 The data to fill the table with. I<Must> be passed in.
1407
1408 =item * C<< table_proc => $code_ref >> : not implemented
1409
1410 A subroutine to do something to the table once it is found.
1411 Not currently implemented. Not obviously necessary. Just
1412 created because there is a C<tr_proc> and C<td_proc>.
1413
1414 =item * C<< tr_ld => $look_down >> : optional
1415
1416 Same as C<table_ld> but for finding the table row elements. Please note
1417 that the C<tr_ld> is done on the table node that was found I<instead>
1418 of the whole HTML tree. This makes sense. The C<tr>s that you want exist
1419 below the table that was just found.
1420
1421 Defaults to C<< ['_tag' => 'tr'] >> if not passed in.
1422
1423 =item * C<< tr_data => $code_ref >> : optional
1424
1425 How to take the C<table_data> and return a row. Defaults to:
1426
1427 sub { my ($self, $data) = @_;
1428 shift(@{$data}) ;
1429 }
1430
1431 =item * C<< tr_proc => $code_ref >> : optional
1432
1433 Something to do to the table row we are about to add to the
1434 table we are making. Defaults to a routine which makes the C<id>
1435 attribute unique:
1436
1437 sub {
1438 my ($self, $tr, $tr_data, $tr_base_id, $row_count) = @_;
1439 $tr->attr(id => sprintf "%s_%d", $tr_base_id, $row_count);
1440 }
1441
1442 =item * C<< td_proc => $code_ref >> : required
1443
1444 This coderef will take the row of data and operate on the C<td> cells that
1445 are children of the C<tr>. See C<t/table2.t> for several usage examples.
1446
1447 Here's a sample one:
1448
1449 sub {
1450 my ($tr, $data) = @_;
1451 my @td = $tr->look_down('_tag' => 'td');
1452 for my $i (0..$#td) {
1453 $td[$i]->splice_content(0, 1, $data->[$i]);
1454 }
1455 }
1456
1457 =cut
1458
1459 =head4 Conversational parameter documentation
1460
1461 The first thing you need is a table. So we need a look down for that. If you
1462 don't give one, it defaults to
1463
1464 ['_tag' => 'table']
1465
1466 What good is a table to display in without data to display?!
1467 So you must supply a scalar representing your tabular
1468 data source. This scalar might be an array reference, a C<next>able iterator,
1469 a DBI statement handle. Whatever it is, it can be iterated through to build
1470 up rows of table data.
1471 These two required fields (the way to find the table and the data to
1472 display in the table) are C<table_ld> and C<table_data>
1473 respectively. A little more on C<table_ld>. If this happens to be a CODE ref,
1474 then execution
1475 of the code ref is presumed to return the C<HTML::Element>
1476 representing the table in the HTML tree.
1477
1478 Next, we get the row or rows which serve as sample C<tr> elements by doing
1479 a C<look_down> from the C<table_elem>. While normally one sample row
1480 is enough to unroll a table, consider when you have alternating
1481 table rows. This API call would need one of each row so that it can
1482 cycle through the
1483 sample rows as it loops through the data.
1484 Alternatively, you could always just use one row and
1485 make the necessary changes to the single C<tr> row by
1486 mutating the element in C<tr_proc>,
1487 discussed below. The default C<tr_ld> is
1488 C<< ['_tag' => 'tr'] >> but you can overwrite it. Note well, if you overwrite
1489 it with a subroutine, then it is expected that the subroutine will return
1490 the C<HTML::Element>(s)
1491 which are C<tr> element(s).
1492 The reason a subroutine might be preferred is in the case
1493 that the HTML designers gave you 8 sample C<tr> rows but only one
1494 prototype row is needed.
1495 So you can write a subroutine, to splice out the 7 rows you don't need
1496 and leave the one sample
1497 row remaining so that this API call can clone it and supply it to
1498 the C<tr_proc> and C<td_proc> calls.
1499
1500 Now, as we move through the table rows with table data,
1501 we need to do two different things on
1502 each table row:
1503
1504 =over 4
1505
1506 =item * get one row of data from the C<table_data> via C<tr_data>
1507
1508 The default procedure assumes the C<table_data> is an array reference and
1509 shifts a row off of it:
1510
1511 sub { my ($self, $data) = @_;
1512 shift(@{$data}) ;
1513 }
1514
1515 Your function MUST return undef when there is no more rows to lay out.
1516
1517 =item * take the C<tr> element and mutate it via C<tr_proc>
1518
1519 The default procedure simply makes the id of the table row unique:
1520
1521 sub { my ($self, $tr, $tr_data, $row_count, $root_id) = @_;
1522 $tr->attr(id => sprintf "%s_%d", $root_id, $row_count);
1523 }
1524
1525 =back
1526
1527 Now that we have our row of data, we call C<td_proc> so that it can
1528 take the data and the C<td> cells in this C<tr> and process them.
1529 This function I<must> be supplied.
1530
1531
1532 =head3 Whither a Table with No Rows
1533
1534 Often when a table has no rows, we want to display a message
1535 indicating this to the view. Use conditional processing to decide what
1536 to display:
1537
1538 <span id=no_data>
1539 <table><tr><td>No Data is Good Data</td></tr></table>
1540 </span>
1541 <span id=load_data>
1542 <html>
1543
1544 <table id="load_data">
1545
1546 <tr> <th>name</th><th>age</th><th>weight</th> </tr>
1547
1548 <tr id="iterate">
1549
1550 <td id="name"> NATURE BOY RIC FLAIR </td>
1551 <td id="age"> 35 </td>
1552 <td id="weight"> 220 </td>
1553
1554 </tr>
1555
1556 </table>
1557
1558 </html>
1559
1560 </span>
1561
1562
1563
1564
1565 =head1 SEE ALSO
1566
1567 =over
1568
1569 =item * L<HTML::Tree>
1570
1571 A perl package for creating and manipulating HTML trees
1572
1573 =item * L<HTML::ElementTable>
1574
1575 An L<HTML::Tree> - based module which allows for manipulation of HTML
1576 trees using cartesian coordinations.
1577
1578 =item * L<HTML::Seamstress>
1579
1580 An L<HTML::Tree> - based module inspired by
1581 XMLC (L<http://xmlc.enhydra.org>), allowing for dynamic
1582 HTML generation via tree rewriting.
1583
1584 =head1 TODO
1585
1586 =over
1587
1588 =item * highlander2
1589
1590 currently the API expects the subtrees to survive or be pruned to be
1591 identified by id:
1592
1593 $if_then->highlander2([
1594 under10 => sub { $_[0] < 10} ,
1595 under18 => sub { $_[0] < 18} ,
1596 welcome => [
1597 sub { 1 },
1598 sub {
1599 my $branch = shift;
1600 $branch->look_down(id => 'age')->replace_content($age);
1601 }
1602 ]
1603 ],
1604 $age
1605 );
1606
1607 but, it should be more flexible. the C<under10>, and C<under18> are
1608 expected to be ids in the tree... but it is not hard to have a check to
1609 see if this field is an array reference and if it, then to do a look
1610 down instead:
1611
1612 $if_then->highlander2([
1613 [class => 'under10'] => sub { $_[0] < 10} ,
1614 [class => 'under18'] => sub { $_[0] < 18} ,
1615 [class => 'welcome'] => [
1616 sub { 1 },
1617 sub {
1618 my $branch = shift;
1619 $branch->look_down(id => 'age')->replace_content($age);
1620 }
1621 ]
1622 ],
1623 $age
1624 );
1625
1626
1627
1628 =cut
1629
1630 =head1 SEE ALSO
1631
1632 L<HTML::Seamstress>
1633
1634 =head1 AUTHOR
1635
1636 Terrence Brannon, E<lt>tbone@cpan.orgE<gt>
1637
1638 Many thanks to BARBIE for his RT bug report.
1639
1640 =head1 COPYRIGHT AND LICENSE
1641
1642 Copyright (C) 2004 by Terrence Brannon
1643
1644 This library is free software; you can redistribute it and/or modify
1645 it under the same terms as Perl itself, either Perl version 5.8.4 or,
1646 at your option, any later version of Perl 5 you may have available.
1647
1648
1649 =cut
This page took 0.110023 seconds and 3 git commands to generate.