File Coverage

blib/lib/HTML/ElementSuper.pm
Criterion Covered Total %
statement 27 238 11.3
branch 0 104 0.0
condition 0 24 0.0
subroutine 9 49 18.3
pod 14 19 73.6
total 50 434 11.5


line stmt bran cond sub pod time code
1             package HTML::ElementSuper;
2              
3             # Extend the HTML::Element class to allow the following:
4             # positional reporting
5             # content replacement
6             # masking (i.e., in the structure but invisible to traverse)
7             # content wrapping
8             # cloning of self and arbitrary elements
9              
10 1     1   5317 use strict;
  1         3  
  1         47  
11 1     1   6 use vars qw($VERSION @ISA $AUTOLOAD);
  1         2  
  1         81  
12 1     1   6 use Carp;
  1         7  
  1         99  
13 1     1   1078 use Data::Dumper;
  1         91261  
  1         2864  
14              
15             # Make sure we have access to the new methods. These were added sometime
16             # in early 2000 but we'll just anchor off of the new numbering system.
17 1     1   7106 use HTML::Element 3.01;
  1         105161  
  1         10  
18              
19             @ISA = qw(HTML::Element);
20              
21             $VERSION = '1.18';
22              
23             ### attr extension ###
24              
25             sub push_attr {
26 0     0 1   my $self = shift;
27 0           my($attr, @new) = @_;
28 0           my(%seen, @vals);
29 0 0         if (defined(my $spec = $self->attr($attr))) {
30 0           for my $v (split(/\s+/, $spec)) {
31 0 0         next if $seen{$v};
32 0           push(@vals, $seen{$v} = $v);
33             }
34             }
35 0           for my $v (grep { defined $_ } @new) {
  0            
36 0 0         next if $seen{$v};
37 0           push(@vals, $seen{$v} = $v);
38             }
39 0           $self->SUPER::attr($attr, join(' ', @vals));
40             }
41              
42             ### positional extension ###
43              
44             sub addr {
45 0     0 1   my $self = shift;
46 0           my $p = $self->parent;
47 0 0         return undef unless $p;
48 0           my @sibs = $p->content_list;
49 0           foreach my $i (0..$#sibs) {
50 0 0 0       return $i if defined $sibs[$i] && $sibs[$i] eq $self;
51             }
52 0           Carp::confess "major oops, no addr found for $self\n";
53             }
54              
55             sub position {
56             # Report coordinates by chasing addr's up the HTML::ElementSuper tree.
57             # We know we've reached the top when a) there is no parent, or b) the
58             # parent is some HTML::Element unable to report it's position.
59 0     0 1   my $p = shift;
60 0           my @pos;
61 0           while ($p) {
62 0           my $pp = $p->parent;
63 0 0 0       last unless ref $pp && $pp->isa(__PACKAGE__);
64 0           my $a = $p->addr;
65 0 0         unshift(@pos, $a) if defined $a;
66 0           $p = $pp;
67             }
68 0           @pos;
69             }
70              
71             sub depth {
72 0     0 1   my $self = shift;
73 0           my $depth = 0;
74 0           my $p = $self;
75 0           while ($p = $p->parent) {
76 0           ++$depth;
77             }
78 0           $depth;
79             }
80              
81             # Handy debugging tools
82              
83             sub push_position {
84             # Push positional coordinates into own content
85 0     0 0   my $self = shift;
86 0           $self->push_content(' (' . join(',', $self->position) . ')');
87             }
88              
89             sub push_depth {
90             # Push HTML tree depth into own content
91 0     0 0   my $self = shift;
92 0           $self->push_content('(' . $self->depth . ')');
93             }
94              
95             ### cloner extension ###
96              
97             sub clone {
98             # Clone HTML::Element style trees.
99             # Clone self unless told otherwise.
100             # Cloning comes in handy when distributing methods such as
101             # push_content - you don't want the same HTML::Element tree across
102             # multiple nodes, just a copy of it - since HTML::Element nodes only
103             # recognize one parent.
104             #
105             # Note: The new cloning functionality of HTML::Element is insufficent
106             # for our purposes. Syntax aside, the native clone() does not
107             # clone the element globs associated with a table...the globs
108             # continue to affect the original element structure.
109 0     0 1   my $self = shift;
110 0           my @args = @_;
111              
112 0 0         @args || push(@args, $self);
113 0           my($clone, $node, @clones);
114 0           my($VAR1, $VAR2, $VAR3);
115 0           $Data::Dumper::Purity = 1;
116 0           foreach $node (@args) {
117 0           _cloning($node, 1);
118 0           eval(Dumper($node));
119 0 0         carp("$@ $node") if $@;
120 0           _cloning($node, 0);
121 0           _cloning($VAR1, 0);
122             # Retie the watchdogs
123             $VAR1->traverse(sub {
124 0     0     my($node, $startflag) = @_;
125 0 0         return unless $startflag;
126 0 0         if ($node->can('watchdog')) {
127 0           $node->watchdog(1);
128 0 0         $node->watchdog->mask(1) if $node->mask;
129             }
130 0           1;
131 0 0         }, 'ignore_text') if ref $VAR1;
132 0           push(@clones, $VAR1);
133             }
134 0 0         $#clones ? @clones : $clones[0];
135             }
136              
137             sub _cloning {
138             # Ugh. We need to do this when we clone and happen to be masked,
139             # otherwise masked content will not make it into the clone.
140 0     0     my $node = shift;
141 0 0         return unless ref $node;
142 0 0         if (@_) {
143 0 0         if ($_[0]) {
144             $node->traverse(sub {
145 0     0     my($node, $startflag) = @_;
146 0 0         return unless $startflag;
147 0 0         $node->_clone_state(1) if $node->can('_clone_state');
148 0           1;
149 0           }, 'ignore_text');
150             }
151             else {
152             $node->traverse(sub {
153 0     0     my($node, $startflag) = @_;
154 0 0         return unless $startflag;
155 0 0         $node->_clone_state(0) if $node->can('_clone_state');
156 0           1;
157 0           }, 'ignore_text');
158             }
159             }
160 0 0 0       $node->can('watchdog') && $node->watchdog ? $node->watchdog->cloning : 0;
161             }
162              
163             sub _clone_state {
164 0     0     my($self, $state) = @_;
165 0 0         return 0 unless $self->watchdog;
166 0 0         if (defined $state) {
167 0 0         if ($state) {
168 0           $self->watchdog->cloning(1);
169             }
170             else {
171 0           $self->watchdog->cloning(0);
172             }
173             }
174 0           $self->watchdog->cloning;
175             }
176              
177              
178             ### maskable extension ###
179              
180             sub mask {
181 0     0 1   my($self, $mode) = @_;
182 0 0         if (defined $mode) {
183             # We count modes since masking can come from overlapping influences,
184             # theoretically.
185 0 0         if ($mode) {
186 0 0         if (! $self->{_mask}) {
187             # deactivate (mask) content
188 0 0         $self->watchdog(1) unless $self->watchdog;
189 0           $self->watchdog->mask(1);
190             }
191 0           ++$self->{_mask};
192             }
193             else {
194 0 0         --$self->{_mask} unless $self->{_mask} <= 0;
195 0 0         if (! $self->{_mask}) {
196             # activate (unmask) content
197 0 0         if ($self->watchdog_listref) {
198 0           $self->watchdog->mask(0);
199             }
200             else {
201 0           $self->watchdog(0);
202             }
203             }
204             }
205             }
206 0           $self->{_mask};
207             }
208              
209             sub starttag {
210 0     0 1   my $self = shift;
211 0 0         return '' if $self->mask;
212 0           $self->SUPER::starttag(@_);
213             }
214              
215             sub endtag {
216 0     0 1   my $self = shift;
217 0 0         return '' if $self->mask;
218 0           $self->SUPER::endtag(@_);
219             }
220              
221             sub starttag_XML {
222 0     0 1   my $self = shift;
223 0 0         return '' if $self->mask;
224 0           $self->SUPER::starttag_XML(@_);
225             }
226              
227             sub endtag_XML {
228 0     0 1   my $self = shift;
229 0 0         return '' if $self->mask;
230 0           $self->SUPER::endtag_XML(@_);
231             }
232              
233             # Oh, the horror! This used to be all that was necessary to implement
234             # masking -- overriding traverse. But the new HTML::Element does NOT
235             # call traverse on a per-element basis, so now when we're masked we have
236             # to play dead -- no tags, no content. To make matters worse, we can't
237             # just override the content method because the new traverse()
238             # implentation is playing directly wiht the data structures rather than
239             # calling content().
240             #
241             # See below for the current solution: HTML::ElementSuper::TiedContent
242             #
243             # For the time being, I've kept the old code and commentary here:
244             #
245             ## Routines that use traverse, such as as_HTML, are not called
246             ## on a per-element basis. as_HTML always belongs to the top level
247             ## element that initiated the call. A maskable element should not
248             ## be seen, though. Overriding as_HTML will not do the trick since
249             ## we cannot guarantee that the top level element is a maskable-aware
250             ## element with the overridden method. Therefore, for maskable
251             ## elements, we override traverse itself, which does get called on a
252             ## per-element basis. If this element is masked, simply return from
253             ## traverse, making this element truly invisible to parents. This
254             ## means that traverse is no longer guranteed to actually visit all
255             ## elements in the tree. For that, you must rely on the actual
256             ## contents of each element.
257             #sub traverse {
258             # my $self = shift;
259             # return if $self->mask;
260             # $self->SUPER::traverse(@_);
261             #}
262             #
263             #sub super_traverse {
264             # # Saftey net for catching wayward masked elements.
265             # my $self = shift;
266             # $self->SUPER::traverse(@_);
267             #}
268              
269             ### replacer extension ###
270              
271             sub replace_content {
272 0     0 1   my $self = shift;
273 0           $self->delete_content;
274 0           $self->push_content(@_);
275             }
276              
277             ### wrapper extension ###
278              
279             sub wrap_content {
280 0     0 1   my($self, $wrap) = @_;
281 0           my $content = $self->content;
282 0 0         if (ref $content) {
283 0           $wrap->push_content(@$content);
284 0           @$content = ($wrap);
285             }
286             else {
287 0           $self->push_content($wrap);
288             }
289 0           $wrap;
290             }
291              
292             ### watchdog extension ###
293              
294             sub watchdog_listref {
295 0     0 0   my $self = shift;
296 0 0         @_ ? $self->{_wa} = shift : $self->{_wa};
297             }
298              
299             sub watchdog {
300 0     0 0   my $self = shift;
301 0 0         if (@_) {
302 0 0         if ($_[0]) {
303             # Install the watchdog hash
304 0           my $wa = shift;
305 0 0         if (ref $wa eq 'ARRAY') {
306 0           $self->watchdog_listref($wa);
307             }
308             else {
309 0           $wa = $self->watchdog_listref;
310             }
311 0           my $cr = $self->content;
312 0           my @content = @$cr;
313 0           @$cr = ();
314 0           $self->{_wd} = tie @$cr, 'HTML::ElementSuper::ContentWatchdog';
315 0           @$cr = @content;
316 0 0         $self->{_wd}->watchdog($wa) if ref $wa eq 'ARRAY';
317             }
318             else {
319             # Release the watchdog
320 0           my @content = $self->{_wd}->fetchall; # in case it's masked
321 0           my $cr = $self->content;
322             # Delete obj ref before untie in order to hush -w
323 0           delete $self->{_wd};
324 0           untie @$cr;
325 0           @$cr = @content;
326             }
327             }
328 0           $self->{_wd};
329             }
330              
331             ###
332              
333             sub new {
334 0     0 1   my $that = shift;
335 0   0       my $class = ref($that) || $that;
336 0           my $self = $class->SUPER::new(@_);
337             # force init of content with array ref
338 0           $self->content_array_ref;
339 0           bless $self,$class;
340 0           $self;
341             }
342              
343             ### deprecated ###
344              
345             sub delete_attr {
346             # Deprecated by new HTML::Element functionality. Should now use
347             # attr($attr, undef) for attribute deletions. Still returning the old
348             # value here for backwards compatability.
349 0     0 0   my($self, $attr) = @_;
350 0           $attr = lc $attr;
351 0           my $old = $self->attr($attr);
352 0           $self->attr($attr, undef);
353 0           $old;
354             }
355              
356             ### temporary Overrides (until bugs fixed in HTML::Element) ###
357              
358             sub replace_with {
359 0     0 1   my $self = shift;
360 0           my $p = $self->parent;
361 0           $self->SUPER::replace_with(@_);
362 0           grep { $_->parent($p) } @_;
  0            
363 0           $self;
364             }
365              
366             ### bag o kludgy tricks ###
367              
368             {
369             package HTML::ElementSuper::ContentWatchdog;
370              
371 1     1   2058 use strict;
  1         2  
  1         125  
372 1     1   6 use Carp;
  1         2  
  1         86  
373 1     1   6 use vars qw( @ISA );
  1         1  
  1         59  
374 1     1   1107 use Tie::Array;
  1         1229  
  1         814  
375             @ISA = qw( Tie::Array );
376              
377             # I got tired of jumping through hoops dealing with the new
378             # HTML::Element semantics. Since I could no longer override traverse()
379             # I was having to go through all sorts of contortions to "hide"
380             # elements in the tree when masked. In a cohesive tree like
381             # HTML::ElementTable, this was still insufficient because globbed
382             # access to the masked elements still needed to be retained.
383             #
384             # The hoops in question involved either a) breaking containment all
385             # over the place, or b) overriding *all* content methods, or c)
386             # swapping in a doppleganger element for the masked element, which
387             # then involved overriding just about everything since the positional
388             # methods needed to look at the doppleganger, but everything else
389             # needed to look at the original.
390             #
391             # So here I provide a class for tying the content array and doing the
392             # right thing when masked. Note that starttag() and endtag() still
393             # need to be overridden, but this tied class should take care of
394             # traverse rifling through masked content.
395             #
396             # Note that all content manipulation works as expected, except for
397             # FETCH. This is intentional.
398             #
399             # Technically, this is not breaking containment since the content()
400             # method returns the content array reference. Even though this is a
401             # read-only method, we can still tie() over the array pointed to by
402             # the reference!
403             #
404             # See mask() for implementation.
405             #
406             # I'll probably go to programmer hell for this, but what the hey.
407             #
408             # UPDATE: Since I was already doing this for masking, I decided to to
409             # general content policing with the same mechanism, but only when
410             # requested via the watchdog parameter, passed as a code reference.
411             # Alas, this meant a full implmentation rather than just subclassing
412             # Tie::StdArray and overriding FETCH().
413              
414             # Object methods
415              
416 0     0     sub fetchall { @{shift->{_array}} }
  0            
417              
418             sub watchdog {
419 0     0     my($self, $classes_ref) = @_;
420 0 0         if ($classes_ref) {
421 0           $self->{watchdog} = {};
422 0           foreach (@$classes_ref) {
423 0           ++$self->{watchdog}{$_};
424             }
425             }
426 0           $self->{watchdog};
427             }
428              
429             sub permit {
430 0     0     my($self, @objects) = @_;
431 0 0         return 1 unless $self->{watchdog};
432 0           foreach (@objects) {
433 0   0       my $type = ref($_) || $_;
434 0           croak "Adoption of type $type, which is not of type " .
435 0 0         join(', ', sort keys %{$self->{watchdog}}) . "\n"
436             unless $self->{watchdog}{$type};
437             }
438 0           1;
439             }
440              
441             sub mask {
442 0     0     my $self = shift;
443 0 0         @_ ? $self->{mask} = shift : $self->{mask};
444             }
445              
446             sub cloning {
447 0     0     my $self = shift;
448 0 0         @_ ? $self->{cloning} = shift : $self->{cloning};
449             }
450              
451             # Tied array methods
452              
453             sub TIEARRAY {
454 0     0     my $that = shift;
455 0   0       my $class = (ref $that) || $that;
456 0           my $self = {};
457 0           bless $self, $class;
458 0           %$self = @_;
459 0           $self->{_array} = [];
460 0           $self;
461             }
462              
463             sub FETCH {
464 0     0     my($self, $k) = @_;
465 0 0 0       return if $self->{mask} && !$self->{cloning};
466 0           $self->{_array}[$k];
467             }
468              
469             sub STORE {
470 0     0     my($self, $k, $v) = @_;
471 0           my $vc = ref $v;
472 0 0         $self->permit($v) if $self->{watchdog};
473 0           $self->{_array}[$k] = $v;
474             }
475              
476             sub PUSH {
477 0     0     my $self = shift;
478 0 0         $self->permit(@_) if $self->{watchdog};
479 0           push(@{$self->{_array}}, @_);
  0            
480             }
481              
482             sub UNSHIFT {
483 0     0     my $self = shift;
484 0 0         $self->permit(@_) if $self->{watchdog};
485 0           unshift(@{$self->{_array}}, @_);
  0            
486             }
487              
488             sub SPLICE {
489 0     0     my($self, $offset, $length, @list) = @_;
490 0 0 0       if (@list && $self->{watchdog}) {
491 0           $self->permit(@list);
492             }
493 0           splice(@{$self->{_array}}, @_);
  0            
494             }
495              
496             #### The rest of these are just native ops on the inner array.
497              
498 0     0     sub FETCHSIZE { scalar @{shift->{_array}} }
  0            
499             sub STORESIZE {
500 0     0     my($self, $size) = @_;
501 0           $#{$self->{_array}} = $size - 1;
  0            
502             }
503 0     0     sub CLEAR { @{shift->{_array}} = () }
  0            
504 0     0     sub POP { pop(@{shift->{_array}}) }
  0            
505 0     0     sub SHIFT { shift(@{shift->{_array}}) }
  0            
506              
507             } ### End HTML::ElementSuper::ContentWatchdog
508              
509             1;
510             __END__