File Coverage

blib/lib/HTML/Element/Tiny.pm
Criterion Covered Total %
statement 218 239 91.2
branch 57 74 77.0
condition 25 38 65.7
subroutine 60 67 89.5
pod 18 18 100.0
total 378 436 86.7


line stmt bran cond sub pod time code
1 6     6   241998 use strict;
  6         16  
  6         454  
2             local $^W = 1;
3              
4             package HTML::Element::Tiny;
5              
6 6     6   37 use vars qw($VERSION %HAS @TAGS %DEFAULT_CLOSED %DEFAULT_NEWLINE %TAG_CLASS);
  6         10  
  6         747  
7             $VERSION = '0.006';
8 6     6   214 use 5.004;
  6         34  
  6         695  
9             BEGIN {
10             # @TAGS =
11             # qw( a abbr acronym address area b base bdo big blockquote body br
12             # button caption cite code col colgroup dd del div dfn dl dt em
13             # fieldset form frame frameset h1 h2 h3 h4 h5 h6 head hr html i
14             # iframe img input ins kbd label legend li link map meta noframes
15             # noscript object ol optgroup option p param pre q samp script select
16             # small span strong style sub sup table tbody td textarea tfoot th
17             # thead title tr tt ul var );
18 6     6   17 %DEFAULT_CLOSED = map { $_ => 1 }
  66         159  
19             qw( area base br col frame hr img input meta param link );
20 6         22 %DEFAULT_NEWLINE = map { $_ => 1 }
  42         206  
21             qw( html head body div p tr table );
22 6     6   31 use vars qw(%_modver);
  6         24  
  6         891  
23 6         24 %_modver = (
24             Clone => '0.28',
25             );
26 6         16 for my $module (qw(HTML::Entities Clone)) {
27 12   100     87 my $modver = $_modver{$module} || 0;
28 12 50   6   1062 $HAS{$module} = eval "use $module $modver (); 1"
  6     6   6896  
  6         43969  
  6         117  
  6         2799  
  0         0  
  0         0  
29             unless defined $HAS{$module};
30             }
31             }
32              
33 6     6   39 use Scalar::Util ();
  6         10  
  6         138  
34 6     6   101 use Carp ();
  6         12  
  6         1228  
35              
36             #use overload (
37             # q{""} => 'as_string',
38             # q{0+} => sub { Scalar::Util::refaddr($_[0]) },
39             # fallback => 1,
40             #);
41              
42             sub TAG () { 0 }
43             sub ID () { 1 }
44             sub CLASS () { 2 }
45             sub ATTR () { 3 }
46             sub CHILDREN () { 4 }
47              
48             %TAG_CLASS = (
49             -text => "-Text",
50             -base => 'HTML::Element::Tiny',
51             -default => 'HTML::Element::Tiny',
52             );
53              
54             sub _tag_class {
55 498     498   727 my ($class, $tag) = @_;
56 498         667 my $tag_lookup;
57             {
58 6     6   34 no strict 'refs';
  6         11  
  6         13679  
  498         692  
59 498 50 50     516 if (exists ${$class . '::'}{TAG_CLASS}
  498         2157  
  498         2740  
60 498         598 and *{${$class . '::'}{TAG_CLASS}}{HASH}) {
61 498         576 $tag_lookup = \%{$class . '::TAG_CLASS'};
  498         1397  
62             # XXX should this really be the case? it seems like a very sane default.
63 498   66     1378 $tag_lookup->{-base} ||= $class;
64 498   66     1330 $tag_lookup->{-default} ||= $class;
65             } else {
66 0         0 $tag_lookup = {};
67             }
68             }
69 498         769 my $tag_class;
70 498         920 for my $href ($tag_lookup, \%TAG_CLASS) {
71 785 100       2061 if ($tag_class = $href->{$tag}) {
72 212         1377 $tag_class =~ s/^-/$href->{-base}::/;
73 212         370 last;
74             }
75             }
76 498   33     1848 $tag_class ||= $tag_lookup->{-default} || $TAG_CLASS{-default};
      66        
77            
78 498         2140 return $tag_class;
79             }
80              
81             sub new {
82 498     498 1 5432 my ($class, $arg, $extra) = @_;
83 498 100       1116 unless (ref $arg) {
84 211         400 return bless \$arg => _tag_class($class, '-text');
85             }
86 287 50       562 Carp::confess "no tag: @$arg" unless @$arg;
87 287         459 my $tag = shift @$arg;
88 287 100       804 my $attr = ref $arg->[0] eq 'HASH' ? shift @$arg : {};
89 287 100       970 @{$attr}{keys %$extra} = (values %$extra) if $extra;
  272         696  
90 287   100     2232 my $self = bless [
91             $tag,
92             delete $attr->{id},
93             [ split /\s+/, delete $attr->{class} || '' ],
94             $attr,
95             [ ],
96             ] => _tag_class($class, $tag);
97 287 100       1394 Scalar::Util::weaken($self->[ATTR]->{-parent})
98             if $self->[ATTR]->{-parent};
99 287         457 @{$self->[CHILDREN]} = map { $class->new($_, { -parent => $self }) } @$arg;
  287         665  
  479         1996  
100 287         863 return $self;
101             }
102              
103 872     872 1 5043 sub children { @{$_[0]->[CHILDREN]} }
  872         2679  
104 3078     3078 1 163060 sub parent { $_[0]->[ATTR]->{-parent} }
105 575     575 1 3380 sub tag { $_[0]->[TAG] }
106 37     37 1 324 sub id { $_[0]->[ID] }
107 13     13 1 16 sub class { join " ", @{$_[0]->[CLASS]} }
  13         58  
108 275     275 1 306 sub classes { @{$_[0]->[CLASS]} }
  275         787  
109              
110             # _match needs to use accessors despite being internal because it may touch
111             # non-arrayref subclasses like -Text
112             sub _match {
113 638     638   790 my ($self, $spec) = @_;
114             return (
115             (defined $spec->{id} ? $spec->{id} eq ($self->id || '') : 1) &&
116             ($spec->{-tag} ? $spec->{-tag} eq ($self->tag) : 1) &&
117             ($spec->{class} ? (
118             # 'there are no parts of $spec->{class} that do not have a matching
119             # entry in $self->classes' -- easier than saying all/all
120             ! grep {
121             my $c = $_;
122             ! grep { $_ eq $c } $self->classes
123             } split /\s+/, $spec->{class}
124             ) : 1) &&
125             (! grep {
126 638   66     3241 $_ ne 'id' and $_ ne '-tag' and $_ ne 'class' and
127             $spec->{$_} ne ($self->attr($_) || '')
128             } keys %$spec)
129             );
130             }
131              
132             sub _spec_to_str {
133 0     0   0 my $spec = shift;
134 0         0 return join " ", map { "$_=$spec->{$_}" } sort keys %$spec;
  0         0  
135             }
136              
137 15     15   71 sub _iter (&) { bless $_[0] => 'HTML::Element::Tiny::Iterator' }
138 9     9   64 sub _coll (@) { HTML::Element::Tiny::Collection->new(@_) }
139              
140             sub find_iter {
141 7     7 1 14 my ($self, $spec) = @_;
142 7         22 my $iter = $self->iter;
143             return _iter {
144             {
145 19 100   19   23 return unless defined(my $next = $iter->next);
  87         178  
146 80 100       345 redo unless $next->_match($spec);
147 12         36 return $next;
148             }
149 7         41 };
150             }
151              
152             sub find {
153 5     5 1 14 my ($self, $spec) = @_;
154             # id should short-circuit
155 5 50       17 return grep( { defined && length } $spec->{id} )
  5 50       43  
156             ? _coll($self->find_iter($spec)->next)
157             : $self->all->filter($spec);
158             }
159              
160             sub find_one {
161 6     6 1 1292 my ($self, $spec) = @_;
162 6         21 my $iter = $self->find_iter($spec);
163 6         28 my $elem = $iter->next;
164 6 50       20 unless ($elem) {
165 0         0 Carp::croak "no element found for " . _spec_to_str($spec);
166             }
167 6 50       23 if (my $next = $iter->next) {
168 0         0 Carp::croak "not exactly one element: found $elem, $next";
169             }
170 6         68 return $elem;
171             }
172              
173             sub all {
174 7     7 1 41 return _coll($_[0]->_all);
175             }
176              
177             sub _all {
178 300     300   334 my $self = shift;
179 300         482 return $self, map({ $_->_all } $self->children );
  515         948  
180             }
181            
182             sub iter {
183 8     8 1 24 my $self = shift;
184 8         17 my @queue = $self;
185             return _iter {
186 103 100   103   239 return unless @queue;
187 95         124 my $next = shift @queue;
188 95         200 unshift @queue, $next->children;
189 95         274 return $next;
190 8         50 };
191             }
192              
193             sub attr {
194 47     47 1 1980 my ($self, $arg) = @_;
195 47 100       143 if (ref $arg eq 'HASH') {
    50          
196 12         50 while (my ($k, $v) = each %$arg) {
197 12 100       56 if ($k eq 'id') { $self->[ID] = $v }
  5 100       16  
198 1         4 elsif ($k eq 'class') { @{$self->[CLASS]} = split /\s+/, $v; }
  1         7  
199 6         30 else { $self->[ATTR]->{$k} = $v; }
200             }
201 12         34 return $self;
202             } elsif (not ref $arg) {
203 35 100       98 return $self->[ID] if $arg eq 'id';
204 19 100       63 return $self->class if $arg eq 'class';
205 6         29 return $self->[ATTR]->{$arg};
206             }
207 0         0 Carp::croak "invalid argument to attr(): '$arg' (must be hashref or scalar)";
208             }
209              
210             sub _Clone_clone {
211 0     0   0 my ($self, $extra) = @_;
212 0         0 my $clone = Clone::clone($self);
213 0         0 delete $clone->[ATTR]->{-parent};
214 0 0 0     0 $clone->attr($extra) if $extra and %$extra;
215 0         0 return $clone;
216             }
217              
218             sub _my_clone {
219 251     251   414 my ($self, $extra) = @_;
220 251         294 my %attr = %{$self->[ATTR]};
  251         851  
221 251         701 delete $attr{-parent};
222 251 100       1809 my $clone = bless [
223             $self->[TAG],
224             $self->[ID],
225             [ $self->classes ],
226 251         592 { %attr, %{$extra || {}} },
227             [],
228             ] => ref $self;
229 251         585 $clone->append($self->children);
230 251         577 return $clone;
231             }
232              
233             my $clone_type = sprintf "_%s_clone", (grep { $HAS{$_} } qw(Clone))[0] || 'my';
234             sub clone {
235 250     250 1 351 my ($self, $extra) = @_;
236 6     6   98 my $clone = do { no strict 'refs'; &$clone_type($self, $extra) };
  6         13  
  6         6006  
  250         280  
  250         593  
237              
238 250 50       1123 Scalar::Util::weaken($clone->[ATTR]->{-parent})
239             if $clone->[ATTR]->{-parent};
240 250         833 return $clone;
241             }
242              
243             sub _new_children {
244 254     254   303 my $self = shift;
245 439 100       1701 return map {
    100          
246 254         364 Scalar::Util::blessed($_)
247             ? $_->parent
248             ? $_->clone({ -parent => $self })
249             : $_->attr({ -parent => $self })
250             : ref($self)->new($_, { -parent => $self })
251             } @_;
252             }
253              
254             sub prepend {
255 1     1 1 2 my $self = shift;
256 1         1 unshift @{ $self->[CHILDREN] }, $self->_new_children(@_);
  1         4  
257             }
258            
259             sub append {
260 253     253 1 304 my $self = shift;
261 253         271 push @{ $self->[CHILDREN] }, $self->_new_children(@_);
  253         632  
262             }
263              
264             sub remove_child {
265 2     2 1 4 my $self = shift;
266 2         3 my (%idx, %obj);
267 2         5 for (@_) {
268 4 100       15 if (Scalar::Util::blessed($_)) {
269 3         14 $obj{Scalar::Util::refaddr($_)}++;
270             } else {
271 1         4 $idx{$_}++;
272             }
273             }
274 2         5 my @children;
275             my @removed;
276 2         3 for my $i (0..$#{$self->[CHILDREN]}) {
  2         7  
277 5         8 my $child = $self->[CHILDREN]->[$i];
278 5 100 100     29 if ($idx{$i} or $obj{Scalar::Util::refaddr($child)}) {
279 4         14 $child->attr({ -parent => undef });
280 4         11 push @removed, $child;
281             } else {
282 1         3 push @children, $child;
283             }
284             }
285 2         5 $self->[CHILDREN] = \@children;
286 2         56 return _coll(@removed);
287             }
288              
289             sub as_HTML {
290 10     10 1 19 my ($self, $arg) = @_;
291 10   50     43 $arg ||= {};
292 10         27 my $str = "<$self->[TAG]";
293 10         13 for ( sort grep { !/^-/ } keys %{$self->[ATTR]}, qw(id class) ) {
  22         81  
  10         32  
294 21         50 my $val = $self->attr($_);
295 21 100 100     123 $str .= qq{ $_="} . $self->attr($_) . qq{"}
      66        
296             if defined $val and ($_ ne 'class' or length($val));
297             }
298             # $str .= qq{ id="$self->[ID]"} if $self->[ID];
299             # $str .= qq{ class="} . $self->class . qq{"} if @{$self->classes};
300             # $str .= qq{ $_="$self->[ATTR]->{$_}"}
301             # for sort grep { !/^-/ } keys %{$self->[ATTR]};
302 10 100       33 if ($DEFAULT_CLOSED{$self->[TAG]}) {
303 2         5 $str .= ' />';
304             } else {
305 8         27 $str .= '>' . join("", map { $_->as_HTML } $self->children);
  5         21  
306 8         719 $str .= "[TAG]>";
307             }
308 10 50       27 $str .= "\n" if $DEFAULT_NEWLINE{$self->[TAG]};
309 10         60 return $str;
310             }
311              
312             #sub as_string {
313             # my ($self) = @_;
314             # my $str = $self->tag;
315             # $str .= qq{ id="} . $self->id . q{"} if $self->id;
316             # $str .= qq{ class="} . $self->class . q{"} if $self->classes;
317             # return "<$str>";
318             #}
319              
320             package HTML::Element::Tiny::Text;
321              
322 6     6   44 BEGIN { use vars qw(@ISA); @ISA = 'HTML::Element::Tiny' }
  6     6   10  
  6         353  
  6         4772  
323              
324 38     38   56 sub children { return () }
325 222     222   738 sub _all { return $_[0] }
326 430     430   4110 sub tag { '-text' }
327 187     187   783 sub parent { return () }
328 4     4   28 sub id { return }
329 0     0   0 sub class { return }
330 16     16   78 sub classes { return () }
331 187 50   187   951 sub attr { return ref $_[1] ? $_[0] : (); }
332 0     0   0 sub clone { return $_[0] }
333 0     0   0 sub append { die "unimplemented" }
334 0     0   0 sub remove_child { die "unimplemented" }
335              
336             my %ENT_MAP = (
337             '&' => '&',
338             '<' => '<',
339             '>' => '>',
340             '"' => '"',
341             "'" => ''',
342             );
343              
344             sub as_HTML {
345 5 50   5   12 return HTML::Entities::encode_entities(${$_[0]})
  0         0  
346             if $HTML::Element::Tiny::HAS_HTML_ENTITIES;
347 5         7 my $str = ${$_[0]};
  5         15  
348 5         13 $str =~ s/([<>&'"])/$ENT_MAP{$1}/eg;
  7         32  
349 5         19 return $str;
350             }
351              
352             package HTML::Element::Tiny::Iterator;
353              
354 122     122   326 sub next { $_[0]->() }
355              
356             package HTML::Element::Tiny::Collection;
357              
358             sub new {
359 25     25   56 my $class = shift;
360 25   66     277 my $self = bless [ @_ ] => ref $class || $class;
361 25 100       195 return wantarray ? @$self : $self;
362             }
363              
364 14     14   3459 sub size { scalar @{$_[0]} }
  14         78  
365              
366             sub each {
367 1     1   2 my ($self, $code) = @_;
368 1         4 for (@$self) { $code->() }
  5         13  
369 1         3 return $self;
370             }
371              
372             sub one {
373 3     3   4 my $self = shift;
374 3 100       299 Carp::croak "not exactly one element (@$self)" unless @$self == 1;
375 1         3 return $self->[0];
376             }
377              
378 1     1   1053 sub all { @{$_[0]} }
  1         11  
379              
380             sub map {
381 1     1   5 my ($self, $code) = @_;
382 1         4 return map { $code->() } @$self;
  5         13  
383             }
384              
385             sub filter {
386 11     11   1221 my ($self, $spec) = @_;
387 11         33 return $self->new(grep { $_->_match($spec) } @$self);
  102         287  
388             }
389              
390 6     6   1242 BEGIN { *grep = \&filter }
391              
392             sub not {
393 3     3   9 my ($self, $spec) = @_;
394 3         26 return $self->new(grep { ! $_->_match($spec) } @$self);
  456         984  
395             }
396              
397             sub attr {
398 1     1   3 my ($self, $arg) = @_;
399             return ref $arg
400 5     5   11 ? $self->each(sub { $_->attr($arg) })
401 0 0   0     : $self->map(sub { grep { defined && length } $_->attr($arg) })
  0            
402 1 50       10 ;
403             }
404              
405             1;
406             __END__