File Coverage

blib/lib/Mojo/DOM/Role/Analyzer.pm
Criterion Covered Total %
statement 138 140 98.5
branch 35 42 83.3
condition 12 20 60.0
subroutine 23 23 100.0
pod 10 10 100.0
total 218 235 92.7


line stmt bran cond sub pod time code
1             package Mojo::DOM::Role::Analyzer ;
2             $Mojo::DOM::Role::Analyzer::VERSION = '0.013';
3 3     3   73874 use strict;
  3         6  
  3         95  
4 3     3   17 use warnings;
  3         6  
  3         78  
5 3     3   951 use Role::Tiny;
  3         4498  
  3         19  
6 3     3   537 use Carp;
  3         8  
  3         368  
7              
8 3     3   1287 use overload "cmp" => sub { $_[0]->compare(@_) }, fallback => 1;
  3     13   1015  
  3         25  
  13         10097  
9              
10             # wrap the find method so we can call the common method on collections
11             around find => sub {
12             my $orig = shift;
13             my $self = shift;
14             return $self->$orig(@_)->with_roles('+Extra');
15             };
16              
17             # traverses the DOM upward to find the closest tag node
18             sub closest_up {
19 1     1 1 1141 return _closest(@_, 'up');
20             }
21              
22             # traverses the DOM downward to find the closest tag node
23             sub closest_down {
24 1     1 1 2356 return _closest(@_, 'down');
25             }
26              
27             sub _closest {
28 2     2   6 my $s = shift;
29 2         6 my $sel = $s->selector;
30 2         309 my $tag = shift;
31 2   50     12 my $dir = shift || 'up';
32 2 100       7 if ($dir ne 'up') {
33 1         2 $dir = 'down';
34             }
35              
36 2         4 my $found;
37 2 100       8 if ($dir eq 'up') {
38 1     3   4 $found = $s->root->find($tag)->grep(sub { ($s cmp $_) > 0 } );
  3         512  
39             } else {
40 1     3   5 $found = $s->root->find($tag)->grep(sub { ($s cmp $_) < 0 } );
  3         711  
41             }
42              
43 2 50       36 return 0 unless $found->size;
44              
45 2         16 my @selectors;
46 2         11 foreach my $f ($found->each) {
47 5         438 push @selectors, $f->selector;
48             }
49              
50 2 50       275 if (@selectors == 1) {
51 0         0 return $s->root->at($selectors[0]);
52             }
53              
54 2         11 my @sorted = sort { $s->root->at($a) cmp $s->root->at($b) } @selectors;
  4         18  
55 2 100       28 if ($dir eq 'up') {
56 1         10 return $s->root->at($sorted[-1]); # get furtherest from the top (closest to node of interest)
57             } else {
58 1         5 return $s->root->at($sorted[0]); # get futherest from the bottom (closest to node of interest)
59             }
60              
61             }
62              
63             # find the common ancestor between a node and another node or group of nodes
64             sub common {
65             # uncomment to debug
66             # use Log::Log4perl::Shortcuts qw(:all); # for development only
67             # if (ref $_[0]) { logd ref $_[0]; } else { logd $_[0]; }
68             # if (ref $_[1]) { logd ref $_[1]; } else { logd $_[1]; }
69             # if (ref $_[2]) { logd ref $_[2]; } else { logd $_[2]; }
70              
71             # The argument handling is a bit confusing. Keep these important notes in mind while reading this code:
72              
73             # 1) This method is called on Mojo::DOM objects (obviously)
74             # 2) Don't confuse this method with its sister method also named "common"
75             # in Mojo::DOM::Collection::Extra which works with Mojo::Collection objects
76             # 3) The argument handling below works for the different types of common syntaxes noted
77             # below in the comments.
78              
79 5     5 1 9133 my ($s, $sel1, $sel2);
80              
81             # function-like use of common: $dom->commont($dom1, $dom2)
82 5 100 66     30 if (ref $_[1] && ref $_[2]) {
83 2         6 $s = $_[0];
84 2         11 $sel1 = $_[1]->selector;
85 2         282 $sel2 = $_[2]->selector;
86             # DWIM syntax handling
87             } else {
88 3 100 66     29 if (!$_[1] && !$_[2]) { # $dom->at('div');
    50 33        
      33        
89 1         3 my $s = shift;
90 1         9 return $s->root->find($s->selector)->common;
91             } elsif ($_[1] && !ref $_[1] && !$_[2]) { # $dom->at('div.first')->common('p');
92 2         3 $s = shift;
93 2         7 $sel1 = $s->selector;
94 2         278 $sel2 = $s->root->at(shift)->selector;
95             }
96             }
97              
98 4         1424 my @t1_path = split / > /, $sel1;
99 4         15 my @t2_path = split / > /, $sel2;
100              
101 4         10 my @common_path;
102 4         10 foreach my $seg (@t1_path) {
103 12         20 my $seg2 = shift @t2_path;
104 12 100 66     49 last if !$seg2 || $seg ne $seg2;
105 8         19 push @common_path, $seg2;
106             }
107              
108 4         14 my $common_selector = join ' > ', @common_path;
109              
110 4         14 return $s->root->at($common_selector);
111              
112             }
113              
114             # determine if a tag A comes before or after tag B in the dom
115             sub compare {
116 15     15 1 2984 my ($s, $sel1, $sel2) = _get_selectors(@_);
117              
118 15         68 my @t1_path = split / > /, $sel1;
119 15         42 my @t2_path = split / > /, $sel2;
120              
121 15         32 foreach my $p1 (@t1_path) {
122 47         72 my $p2 = shift(@t2_path);
123 47 100       107 next if $p1 eq $p2;
124 15         42 my ($p1_tag, $p1_num) = split /:/, $p1;
125 15         39 my ($p2_tag, $p2_num) = split /:/, $p2;
126              
127 15 50       34 next if $p1_num eq $p2_num;
128 15         97 return $p1_num cmp $p2_num;
129             }
130             }
131              
132             sub distance {
133 1     1 1 998 my ($s, $sel1, $sel2) = _get_selectors(@_);
134              
135 1         5 my $common = $s->common($s->root->at($sel1), $s->root->at($sel2));
136 1         563 my $dist_leg1 = $s->root->at($sel1)->depth - $common->depth;
137 1         5 my $dist_leg2 = $s->root->at($sel2)->depth - $common->depth;
138              
139 1         8 return $dist_leg1 + $dist_leg2;
140             }
141              
142             sub depth {
143 23     23 1 2853 my $s = shift;
144 23         47 my $sel = $s->selector;
145 23         2922 my @parts = split /\s>\s/, $sel;
146 23         63 return scalar @parts;
147             }
148              
149             sub deepest {
150 1     1 1 551 my $s = shift;
151 1         3 my $deepest_depth = 0;
152 1     29   6 foreach my $c ($s->descendant_nodes->grep(sub { $_->type eq 'tag' })->each) {
  29         1235  
153 14         77 my $depth = $c->depth;
154 14 100       35 $deepest_depth = $depth if $depth > $deepest_depth;
155             }
156 1         16 return $deepest_depth;
157             }
158              
159             sub element_count {
160 1     1 1 6166 my $self = shift;
161 1     25   8 return $self->descendant_nodes->grep(sub { $_->type eq 'tag' })->size;
  25         1121  
162             }
163              
164             # determine if one node is an ancestor to another
165             sub is_ancestor_to {
166 25     25 1 42 my $s = shift;
167 25         37 my $arg = shift;
168 25         59 my $sel1 = $s->selector;
169 25         2464 my $sel2 = $arg->selector;
170              
171 25 100       3125 return $sel2 =~ /^\Q$sel1\E/ ? 1 : 0;
172             }
173              
174             sub _get_selectors {
175 16     16   25 my ($s, $sel1, $sel2);
176 16 100       62 if (!$_[2]) {
177 2         3 $s = shift;
178 2         7 $sel1 = $s->selector;
179 2 50       294 if (ref $_[0]) {
180 0         0 $sel2 = $_[0]->selector;
181             } else {
182 2         14 $sel2 = $s->root->at($_[0])->selector;
183             }
184             } else {
185 14         58 $s = $_[0];
186 14         39 $sel1 = $_[1]->selector;
187 14         2136 $sel2 = $_[2]->selector;
188             }
189 16         3301 return ($s, $sel1, $sel2);
190             }
191              
192             sub tag_analysis {
193 1     1 1 1873 my $s = shift;
194 1         3 my $selector = shift;
195              
196 1 50       5 carp "A selector argument must be passed to the tag_analysis method"
197             unless $selector;
198              
199 1         31 my $common = $s->find($selector)->common;
200              
201 1         5 my @sub_enclosing_nodes;
202 1         6 @sub_enclosing_nodes = $common->_gsec($selector, $common->selector);
203              
204 1         5 foreach my $sn (@sub_enclosing_nodes) {
205 2 100       7 next if $sn->{all_tags_have_same_depth};
206 1         5 my $n = $s->at($sn->{selector});
207 1         562 my $ec = $n->find($selector)->common;
208 1         7 my @enclosing_nodes = $ec->_gsec($selector);
209 1         5 push @sub_enclosing_nodes, @enclosing_nodes;
210             }
211              
212             # cleanup any unnecessary nodes at top of the array wrapping the smallest enconpassing dom
213             # my $total_tags = $s->find($selector)->size;
214             # my $number_of_tags = grep { $_->{size} == $total_tags } @sub_enclosing_nodes;
215             # splice @sub_enclosing_nodes, 0, $number_of_tags - 1;
216 1         10 @sub_enclosing_nodes = sort { $a->{selector} cmp $b->{selector} } @sub_enclosing_nodes;
  1         8  
217              
218 1         7 return @sub_enclosing_nodes;
219             }
220              
221              
222             # get secondary enclosing tags
223             sub _gsec {
224 2     2   87 my $s = shift;
225 2         4 my $selector = shift;
226 2         4 my $top_level_selector = shift;
227              
228 2 50       8 carp "A selector argument must be passed to the tag_analysis method"
229             unless $selector;
230              
231 2         3 my @sub_enclosing_nodes;
232 2 100       21 foreach my $c ($top_level_selector ? $s->root->find($top_level_selector)->each : $s->children->each) {
233 6         1384 my $size = $c->find($selector)->size;
234 6 100       1748 next unless $size;
235              
236 2         7 my $depth_total;
237 2         4 my $same_depth = 1;
238 2         4 my $depth_tracker = undef;
239              
240 2         46 foreach my $t ($c->find($selector)->each) {
241 4         918 my $depth = $t->depth;
242              
243 4 100 100     18 if ($depth_tracker && ($depth != $depth_tracker)) {
244 1         3 $same_depth = 0;
245             }
246              
247 4         7 $depth_tracker = $depth;
248 4         10 $depth_total += $depth;
249             }
250 2         10 push @sub_enclosing_nodes, { selector => $c->selector,
251             size => $size,
252             avg_tag_depth => ($depth_total / $size),
253             all_tags_have_same_depth => $same_depth };
254             }
255 2         100 return @sub_enclosing_nodes;
256             }
257              
258             1; # Magic true value
259             # ABSTRACT: miscellaneous methods for analyzing a DOM
260              
261             __END__