File Coverage

blib/lib/Mojo/DOM/Role/Analyzer.pm
Criterion Covered Total %
statement 176 180 97.7
branch 44 54 81.4
condition 17 26 65.3
subroutine 24 24 100.0
pod 10 10 100.0
total 271 294 92.1


line stmt bran cond sub pod time code
1             package Mojo::DOM::Role::Analyzer ;
2             $Mojo::DOM::Role::Analyzer::VERSION = '0.015';
3 4     4   75478 use strict;
  4         11  
  4         134  
4 4     4   21 use warnings;
  4         9  
  4         97  
5 4     4   623 use Role::Tiny;
  4         4328  
  4         26  
6 4     4   718 use Carp;
  4         10  
  4         434  
7             #use Log::Log4perl::Shortcuts qw(:all);
8              
9              
10 4     4   1255 use overload "cmp" => sub { $_[0]->compare(@_) }, fallback => 1;
  4     13   976  
  4         30  
  13         10089  
11              
12             # wrap the find method so we can call the common method on collections
13             around find => sub {
14             my $orig = shift;
15             my $self = shift;
16             return $self->$orig(@_)->with_roles('+Extra');
17             };
18              
19             # traverses the DOM upward to find the closest tag node
20             sub closest_up {
21 1     1 1 1057 return _closest(@_, 'up');
22             }
23              
24             # traverses the DOM downward to find the closest tag node
25             sub closest_down {
26 1     1 1 2266 return _closest(@_, 'down');
27             }
28              
29             sub _closest {
30 2     2   6 my $s = shift;
31 2         7 my $sel = $s->selector;
32 2         307 my $tag = shift;
33 2   50     11 my $dir = shift || 'up';
34 2 100       7 if ($dir ne 'up') {
35 1         3 $dir = 'down';
36             }
37              
38 2         3 my $found;
39 2 100       7 if ($dir eq 'up') {
40 1     3   5 $found = $s->root->find($tag)->grep(sub { ($s cmp $_) > 0 } );
  3         493  
41             } else {
42 1     3   4 $found = $s->root->find($tag)->grep(sub { ($s cmp $_) < 0 } );
  3         684  
43             }
44              
45 2 50       36 return 0 unless $found->size;
46              
47 2         13 my @selectors;
48 2         8 foreach my $f ($found->each) {
49 5         443 push @selectors, $f->selector;
50             }
51              
52 2 50       274 if (@selectors == 1) {
53 0         0 return $s->root->at($selectors[0]);
54             }
55              
56 2         10 my @sorted = sort { $s->root->at($a) cmp $s->root->at($b) } @selectors;
  4         32  
57 2 100       12 if ($dir eq 'up') {
58 1         22 return $s->root->at($sorted[-1]); # get furtherest from the top (closest to node of interest)
59             } else {
60 1         4 return $s->root->at($sorted[0]); # get futherest from the bottom (closest to node of interest)
61             }
62              
63             }
64              
65             # find the common ancestor between a node and another node or group of nodes
66             sub common {
67             # uncomment to debug
68             # use Log::Log4perl::Shortcuts qw(:all); # for development only
69             # if (ref $_[0]) { logd ref $_[0]; } else { logd $_[0]; }
70             # if (ref $_[1]) { logd ref $_[1]; } else { logd $_[1]; }
71             # if (ref $_[2]) { logd ref $_[2]; } else { logd $_[2]; }
72              
73             # The argument handling is a bit confusing. Keep these important notes in mind while reading this code:
74              
75             # 1) This method is called on Mojo::DOM objects (obviously)
76             # 2) Don't confuse this method with its sister method also named "common"
77             # in Mojo::DOM::Collection::Extra which works with Mojo::Collection objects
78             # 3) The argument handling below works for the different types of common syntaxes noted
79             # below in the comments.
80              
81 5     5 1 8718 my ($s, $sel1, $sel2);
82              
83             # function-like use of common: $dom->commont($dom1, $dom2)
84 5 100 66     28 if (ref $_[1] && ref $_[2]) {
85 2         4 $s = $_[0];
86 2         8 $sel1 = $_[1]->selector;
87 2         275 $sel2 = $_[2]->selector;
88             # DWIM syntax handling
89             } else {
90 3 100 66     23 if (!$_[1] && !$_[2]) { # $dom->at('div');
    50 33        
      33        
91 1         2 my $s = shift;
92 1         6 return $s->root->find($s->selector)->common;
93             } elsif ($_[1] && !ref $_[1] && !$_[2]) { # $dom->at('div.first')->common('p');
94 2         4 $s = shift;
95 2         6 $sel1 = $s->selector;
96 2         270 $sel2 = $s->root->at(shift)->selector;
97             }
98             }
99              
100 4         1402 my @t1_path = split / > /, $sel1;
101 4         16 my @t2_path = split / > /, $sel2;
102              
103 4         7 my @common_path;
104 4         8 foreach my $seg (@t1_path) {
105 12         21 my $seg2 = shift @t2_path;
106 12 100 66     48 last if !$seg2 || $seg ne $seg2;
107 8         16 push @common_path, $seg2;
108             }
109              
110 4         12 my $common_selector = join ' > ', @common_path;
111              
112 4         25 return $s->root->at($common_selector);
113              
114             }
115              
116             # determine if a tag A comes before or after tag B in the dom
117             sub compare {
118 15     15 1 3056 my ($s, $sel1, $sel2) = _get_selectors(@_);
119              
120 15         61 my @t1_path = split / > /, $sel1;
121 15         45 my @t2_path = split / > /, $sel2;
122              
123 15         26 my $t1_len = scalar @t1_path;
124 15         25 my $t2_len = scalar @t2_path;
125              
126 15         22 my $equal = 0;
127 15         34 foreach my $p1 (@t1_path) {
128 47         59 $equal = 0;
129 47         74 my $p2 = shift(@t2_path);
130 47 50       110 last if !$p2;
131 47 100       146 if ($p1 eq $p2) {
132 32         43 $equal = 1;
133 32         57 next;
134             }
135 15         84 my ($p1_num) = $p1 =~ /child\((\d+)\)/;
136 15         50 my ($p2_num) = $p2 =~ /child\((\d+)\)/;
137              
138 15         86 return ($p1_num <=> $p2_num);
139             }
140 0 0       0 return 0 if $t1_len == $t2_len;
141 0 0       0 return $t1_len < $t2_len ? -1 : 1;
142             }
143              
144             sub distance {
145 1     1 1 1051 my ($s, $sel1, $sel2) = _get_selectors(@_);
146              
147 1         5 my $common = $s->common($s->root->at($sel1), $s->root->at($sel2));
148 1         601 my $dist_leg1 = $s->root->at($sel1)->depth - $common->depth;
149 1         7 my $dist_leg2 = $s->root->at($sel2)->depth - $common->depth;
150              
151 1         8 return $dist_leg1 + $dist_leg2;
152             }
153              
154             sub depth {
155 275     275 1 3060 my $s = shift;
156 275         571 my $sel = $s->selector;
157 275         92786 my @parts = split /\s>\s/, $sel;
158 275         784 return scalar @parts;
159             }
160              
161             sub deepest {
162 1     1 1 597 my $s = shift;
163 1         2 my $deepest_depth = 0;
164 1     29   5 foreach my $c ($s->descendant_nodes->grep(sub { $_->type eq 'tag' })->each) {
  29         1239  
165 14         47 my $depth = $c->depth;
166 14 100       35 $deepest_depth = $depth if $depth > $deepest_depth;
167             }
168 1         15 return $deepest_depth;
169             }
170              
171             sub element_count {
172 1     1 1 6107 my $self = shift;
173 1     25   8 return $self->descendant_nodes->grep(sub { $_->type eq 'tag' })->size;
  25         1068  
174             }
175              
176             # determine if one node is an ancestor to another
177             sub is_ancestor_to {
178 266     266 1 804265 my $s = shift;
179 266         370 my $arg = shift;
180 266         534 my $sel1 = $s->selector;
181 266         45102 my $sel2 = $arg->selector;
182              
183 266 100       87540 return $sel2 =~ /^\Q$sel1\E/ ? 1 : 0;
184             }
185              
186             sub _get_selectors {
187 16     16   29 my ($s, $sel1, $sel2);
188 16 100       47 if (!$_[2]) {
189 2         4 $s = shift;
190 2         6 $sel1 = $s->selector;
191 2 50       288 if (ref $_[0]) {
192 0         0 $sel2 = $_[0]->selector;
193             } else {
194 2         14 $sel2 = $s->root->at($_[0])->selector;
195             }
196             } else {
197 14         62 $s = $_[0];
198 14         42 $sel1 = $_[1]->selector;
199 14         2053 $sel2 = $_[2]->selector;
200             }
201 16         3149 return ($s, $sel1, $sel2);
202             }
203              
204             sub tag_analysis {
205 4     4 1 266232 my $s = shift;
206 4         10 my $selector = shift;
207              
208 4 50       15 carp "A selector argument must be passed to the tag_analysis method"
209             unless $selector;
210              
211 4         127 my $ec = $s->find($selector)->common;
212 4         40 my @sub_enclosing_nodes = $ec->_gsec($selector, 1);
213              
214 4         12 foreach my $sn (@sub_enclosing_nodes) {
215 23 100 100     104 next if $sn->{all_tags_have_same_depth} || $sn->{top_level};
216 6         31 my $n = $s->at($sn->{selector});
217 6         117440 my @enclosing_nodes = $n->_gsec($selector);
218 6         27 push @sub_enclosing_nodes, @enclosing_nodes;
219             }
220              
221             # cleanup
222 4         20 @sub_enclosing_nodes = sort { $a->{selector} cmp $b->{selector} } @sub_enclosing_nodes;
  54         90  
223 4         9 my $last_node;
224             my @filtered_enclosing_nodes;
225 4         8 foreach my $sen (@sub_enclosing_nodes) {
226 23 100       69 if (!$last_node) {
227 4         9 $last_node = $sen;
228 4         19 next;
229             }
230              
231 19 100       71 if ($s->at($last_node->{selector})->is_ancestor_to($s->at($sen->{selector}))) {
232 9 100 66     64 if ($last_node->{size} != $sen->{size} || $last_node->{direct_children} != $sen->{direct_children}) {
233 6         17 push @filtered_enclosing_nodes, $last_node;
234             }
235             } else {
236 10         37 push @filtered_enclosing_nodes, $last_node;
237             }
238 19         79 $last_node = $sen;
239             }
240 4         8 push @filtered_enclosing_nodes, $last_node;
241              
242 4         37 return @filtered_enclosing_nodes;
243              
244             }
245              
246             # get secondary enclosing tags
247             sub _gsec {
248 10     10   25 my $s = shift;
249 10         20 my $selector = shift;
250 10         34 my $top_level = shift;
251 10         23 my %props;
252              
253             my @sub_enclosing_nodes;
254              
255 10 100       33 if ($top_level) {
256 4         11 $props{top_level} = 1;
257 4         11 $props{selector} = $s->selector;
258 4         499 $props{size} = $s->find($selector)->size;
259 4         32714 my ($depth_total, $same_depth, $classes) = $s->_calc_depth($selector);
260              
261 4         11 $props{classes} = $classes;
262 4         27 $props{direct_children} = $s->children($selector)->size;;
263 4         4318 my $avg_depth = sprintf('%.3f', ($depth_total / $props{size}));
264 4         20 $avg_depth =~ s/\.0+$//g;
265 4         11 $props{avg_tag_depth} = $avg_depth;
266 4         10 $props{all_tags_have_same_depth} = $same_depth;
267 4         11 push @sub_enclosing_nodes, \%props;
268             }
269              
270 10         41 foreach my $c ($s->children->each) {
271 58 100       6294 next if $c->tag eq $selector;
272 53         2118 my $size = $c->find($selector)->size;
273 53 100       105500 next unless $size;
274              
275 19         69 my $cdn_with_sel = $c->children($selector)->size;
276 19         12346 my ($depth_total, $same_depth, $classes) = $c->_calc_depth($selector);
277              
278 19         231 my $avg_depth = sprintf('%.3f', ($depth_total / $size));
279 19         97 $avg_depth =~ s/\.0+$//g;
280 19         69 push @sub_enclosing_nodes, { selector => $c->selector,
281             size => $size,
282             classes => $classes,
283             avg_tag_depth => $avg_depth,
284             all_tags_have_same_depth => $same_depth,
285             direct_children => $cdn_with_sel,
286             };
287             }
288              
289 10         562 return @sub_enclosing_nodes;
290              
291             }
292              
293             sub _calc_depth {
294 23     23   49 my $s = shift;
295 23         36 my $selector = shift;
296 23         46 my $depth_total;
297 23         34 my $same_depth = 1;
298 23         46 my $depth_tracker = undef;
299              
300 23         41 my %classes;
301 23         600 foreach my $t ($s->find($selector)->each) {
302 256 100       106878 if ($t->attr('class')) {
303 73         1308 my @classes = split ' ', $t->attr('class');
304 73         1128 $classes{$t->attr('class')}++;
305              
306             # my @classes = split ' ', $t->attr('class');
307             # foreach my $cl (@classes) {
308             # $classes{$cl}++;
309             # }
310             }
311 256         4104 my $depth = $t->depth;
312              
313 256 100 100     988 if ($depth_tracker && ($depth != $depth_tracker)) {
314 50         89 $same_depth = 0;
315             }
316              
317 256         389 $depth_tracker = $depth;
318 256         498 $depth_total += $depth;
319             }
320              
321 23         199 return ($depth_total, $same_depth, \%classes);
322             }
323              
324             1; # Magic true value
325             # ABSTRACT: miscellaneous methods for analyzing a DOM
326              
327             __END__