File Coverage

blib/lib/Mojo/DOM/Role/Analyzer.pm
Criterion Covered Total %
statement 158 165 95.7
branch 35 48 72.9
condition 14 23 60.8
subroutine 24 24 100.0
pod 10 10 100.0
total 241 270 89.2


line stmt bran cond sub pod time code
1             package Mojo::DOM::Role::Analyzer ;
2             $Mojo::DOM::Role::Analyzer::VERSION = '0.014';
3 3     3   63953 use strict;
  3         6  
  3         79  
4 3     3   21 use warnings;
  3         5  
  3         72  
5 3     3   510 use Role::Tiny;
  3         3699  
  3         18  
6 3     3   418 use Carp;
  3         5  
  3         285  
7              
8 3     3   1051 use overload "cmp" => sub { $_[0]->compare(@_) }, fallback => 1;
  3     13   858  
  3         20  
  13         8260  
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 828 return _closest(@_, 'up');
20             }
21              
22             # traverses the DOM downward to find the closest tag node
23             sub closest_down {
24 1     1 1 1831 return _closest(@_, 'down');
25             }
26              
27             sub _closest {
28 2     2   4 my $s = shift;
29 2         7 my $sel = $s->selector;
30 2         248 my $tag = shift;
31 2   50     10 my $dir = shift || 'up';
32 2 100       29 if ($dir ne 'up') {
33 1         3 $dir = 'down';
34             }
35              
36 2         6 my $found;
37 2 100       7 if ($dir eq 'up') {
38 1     3   5 $found = $s->root->find($tag)->grep(sub { ($s cmp $_) > 0 } );
  3         448  
39             } else {
40 1     3   2 $found = $s->root->find($tag)->grep(sub { ($s cmp $_) < 0 } );
  3         642  
41             }
42              
43 2 50       28 return 0 unless $found->size;
44              
45 2         11 my @selectors;
46 2         7 foreach my $f ($found->each) {
47 5         342 push @selectors, $f->selector;
48             }
49              
50 2 50       221 if (@selectors == 1) {
51 0         0 return $s->root->at($selectors[0]);
52             }
53              
54 2         10 my @sorted = sort { $s->root->at($a) cmp $s->root->at($b) } @selectors;
  4         25  
55 2 100       8 if ($dir eq 'up') {
56 1         9 return $s->root->at($sorted[-1]); # get furtherest from the top (closest to node of interest)
57             } else {
58 1         6 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 7206 my ($s, $sel1, $sel2);
80              
81             # function-like use of common: $dom->commont($dom1, $dom2)
82 5 100 66     22 if (ref $_[1] && ref $_[2]) {
83 2         4 $s = $_[0];
84 2         8 $sel1 = $_[1]->selector;
85 2         257 $sel2 = $_[2]->selector;
86             # DWIM syntax handling
87             } else {
88 3 100 66     23 if (!$_[1] && !$_[2]) { # $dom->at('div');
    50 33        
      33        
89 1         3 my $s = shift;
90 1         6 return $s->root->find($s->selector)->common;
91             } elsif ($_[1] && !ref $_[1] && !$_[2]) { # $dom->at('div.first')->common('p');
92 2         4 $s = shift;
93 2         6 $sel1 = $s->selector;
94 2         240 $sel2 = $s->root->at(shift)->selector;
95             }
96             }
97              
98 4         1191 my @t1_path = split / > /, $sel1;
99 4         20 my @t2_path = split / > /, $sel2;
100              
101 4         9 my @common_path;
102 4         8 foreach my $seg (@t1_path) {
103 12         18 my $seg2 = shift @t2_path;
104 12 100 66     43 last if !$seg2 || $seg ne $seg2;
105 8         12 push @common_path, $seg2;
106             }
107              
108 4         11 my $common_selector = join ' > ', @common_path;
109              
110 4         12 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 2519 my ($s, $sel1, $sel2) = _get_selectors(@_);
117              
118 15         55 my @t1_path = split / > /, $sel1;
119 15         35 my @t2_path = split / > /, $sel2;
120              
121 15         20 my $t1_len = scalar @t1_path;
122 15         19 my $t2_len = scalar @t2_path;
123              
124 15         22 my $equal = 0;
125 15         23 foreach my $p1 (@t1_path) {
126 47         51 $equal = 0;
127 47         58 my $p2 = shift(@t2_path);
128 47 50       88 last if !$p2;
129 47 100       74 if ($p1 eq $p2) {
130 32         34 $equal = 1;
131 32         46 next;
132             }
133 15         67 my ($p1_num) = $p1 =~ /child\((\d+)\)/;
134 15         43 my ($p2_num) = $p2 =~ /child\((\d+)\)/;
135              
136 15         71 return ($p1_num <=> $p2_num);
137             }
138 0 0       0 return 0 if $t1_len == $t2_len;
139 0 0       0 return $t1_len < $t2_len ? -1 : 1;
140             }
141              
142             sub distance {
143 1     1 1 834 my ($s, $sel1, $sel2) = _get_selectors(@_);
144              
145 1         5 my $common = $s->common($s->root->at($sel1), $s->root->at($sel2));
146 1         465 my $dist_leg1 = $s->root->at($sel1)->depth - $common->depth;
147 1         5 my $dist_leg2 = $s->root->at($sel2)->depth - $common->depth;
148              
149 1         6 return $dist_leg1 + $dist_leg2;
150             }
151              
152             sub depth {
153 23     23 1 2252 my $s = shift;
154 23         43 my $sel = $s->selector;
155 23         2401 my @parts = split /\s>\s/, $sel;
156 23         51 return scalar @parts;
157             }
158              
159             sub deepest {
160 1     1 1 449 my $s = shift;
161 1         2 my $deepest_depth = 0;
162 1     29   4 foreach my $c ($s->descendant_nodes->grep(sub { $_->type eq 'tag' })->each) {
  29         1010  
163 14         43 my $depth = $c->depth;
164 14 100       30 $deepest_depth = $depth if $depth > $deepest_depth;
165             }
166 1         12 return $deepest_depth;
167             }
168              
169             sub element_count {
170 1     1 1 5020 my $self = shift;
171 1     25   6 return $self->descendant_nodes->grep(sub { $_->type eq 'tag' })->size;
  25         871  
172             }
173              
174             # determine if one node is an ancestor to another
175             sub is_ancestor_to {
176 16     16 1 19 my $s = shift;
177 16         19 my $arg = shift;
178 16         41 my $sel1 = $s->selector;
179 16         1172 my $sel2 = $arg->selector;
180              
181 16 100       1566 return $sel2 =~ /^\Q$sel1\E/ ? 1 : 0;
182             }
183              
184             sub _get_selectors {
185 16     16   26 my ($s, $sel1, $sel2);
186 16 100       39 if (!$_[2]) {
187 2         3 $s = shift;
188 2         7 $sel1 = $s->selector;
189 2 50       236 if (ref $_[0]) {
190 0         0 $sel2 = $_[0]->selector;
191             } else {
192 2         11 $sel2 = $s->root->at($_[0])->selector;
193             }
194             } else {
195 14         48 $s = $_[0];
196 14         41 $sel1 = $_[1]->selector;
197 14         1661 $sel2 = $_[2]->selector;
198             }
199 16         2602 return ($s, $sel1, $sel2);
200             }
201              
202             sub tag_analysis {
203 1     1 1 1471 my $s = shift;
204 1         2 my $selector = shift;
205              
206 1 50       3 carp "A selector argument must be passed to the tag_analysis method"
207             unless $selector;
208              
209 1         23 my $ec = $s->find($selector)->common;
210 1         8 my @sub_enclosing_nodes = $ec->_gsec($selector, 1);
211              
212 1         3 foreach my $sn (@sub_enclosing_nodes) {
213 2 50 66     11 next if $sn->{all_tags_have_same_depth} || $sn->{top_level};
214 0         0 my $n = $s->at($sn->{selector});
215 0         0 my @enclosing_nodes = $n->_gsec($selector);
216 0         0 push @sub_enclosing_nodes, @enclosing_nodes;
217             }
218              
219 1         4 @sub_enclosing_nodes = sort { $a->{selector} cmp $b->{selector} } @sub_enclosing_nodes;
  1         4  
220              
221 1         4 return @sub_enclosing_nodes;
222              
223             }
224              
225             # get secondary enclosing tags
226             sub _gsec {
227 1     1   3 my $s = shift;
228 1         2 my $selector = shift;
229 1         1 my $top_level = shift;
230 1         2 my %props;
231              
232             my @sub_enclosing_nodes;
233              
234 1 50       4 if ($top_level) {
235 1         2 $props{top_level} = 1;
236 1         3 $props{selector} = $s->selector;
237 1         87 $props{size} = $s->find($selector)->size;
238 1         485 my ($depth_total, $same_depth, $classes) = $s->_calc_depth($selector);
239              
240 1         2 $props{classes} = $classes;
241 1         8 $props{direct_children} = $s->children($selector)->size;;
242 1         598 $props{avg_tag_depth} = ($depth_total / $props{size});
243 1         3 $props{all_tags_have_same_depth} = $same_depth;
244 1         2 push @sub_enclosing_nodes, \%props;
245             }
246              
247              
248 1         4 foreach my $c ($s->children->each) {
249 5 100       266 next if $c->tag eq $selector;
250 3         93 my $size = $c->find($selector)->size;
251 3 100       579 next unless $size;
252              
253 1         21 my $cdn_with_sel = $c->children($selector)->size;
254              
255 1         161 my ($depth_total, $same_depth, $classes) = $c->_calc_depth($selector);
256              
257 1         3 push @sub_enclosing_nodes, { selector => $c->selector,
258             size => $size,
259             classes => $classes,
260             avg_tag_depth => ($depth_total / $size),
261             all_tags_have_same_depth => $same_depth,
262             direct_children => $cdn_with_sel,
263             };
264             }
265              
266 1         5 return @sub_enclosing_nodes;
267              
268             }
269              
270             sub _calc_depth {
271 2     2   4 my $s = shift;
272 2         4 my $selector = shift;
273 2         2 my $depth_total;
274 2         3 my $same_depth = 1;
275 2         3 my $depth_tracker = undef;
276              
277 2         4 my %classes;
278 2         39 foreach my $t ($s->find($selector)->each) {
279 4 50       779 if ($t->attr('class')) {
280 4         63 my @classes = split ' ', $t->attr('class');
281 4         53 $classes{$t->attr('class')}++;
282              
283             # my @classes = split ' ', $t->attr('class');
284             # foreach my $cl (@classes) {
285             # $classes{$cl}++;
286             # }
287             }
288 4         55 my $depth = $t->depth;
289              
290 4 100 100     15 if ($depth_tracker && ($depth != $depth_tracker)) {
291 1         3 $same_depth = 0;
292             }
293              
294 4         6 $depth_tracker = $depth;
295 4         14 $depth_total += $depth;
296             }
297              
298 2         9 return ($depth_total, $same_depth, \%classes);
299             }
300              
301             1; # Magic true value
302             # ABSTRACT: miscellaneous methods for analyzing a DOM
303              
304             __END__