File Coverage

blib/lib/HTML/ExtractText.pm
Criterion Covered Total %
statement 114 116 98.2
branch 42 52 80.7
condition 23 32 71.8
subroutine 24 25 96.0
pod 6 6 100.0
total 209 231 90.4


line stmt bran cond sub pod time code
1             package HTML::ExtractText;
2              
3 4     4   234150 use strict;
  4         8  
  4         133  
4 4     4   20 use warnings;
  4         400  
  4         292  
5              
6             our $VERSION = '1.002004'; # VERSION
7              
8 4     4   2361 use Try::Tiny;
  4         4788  
  4         238  
9 4     4   23 use Scalar::Util qw/blessed/;
  4         4  
  4         182  
10 4     4   14 use Carp qw/croak/;
  4         7  
  4         149  
11 4     4   1500 use Devel::TakeHashArgs;
  4         1354  
  4         194  
12 4     4   1638 use Mojo::DOM;
  4         299318  
  4         360  
13 0     0   0 use overload q|""| => sub { shift->error },
14 4     4   44 q|%{}| => sub { shift->last_results };
  4     7   6  
  4         66  
  7         47352  
15              
16             sub new {
17 13     13 1 860214 my $self = bless [], shift;
18 13 50       144 get_args_as_hash( \@_, \ my %args,
19             { # these are optional with defaults
20             separator => "\n",
21             ignore_not_found => 1,
22             },
23             [ ], # these are mandatory (none, ATM)
24             [ qw/separator ignore_not_found/ ], # only these args are valid ones
25             ) or croak $@;
26              
27 13         557 $self->$_( $args{$_} ) for keys %args;
28              
29 13         60 return $self;
30             }
31              
32             sub extract {
33 11     11 1 3716 my ( $self, $what, $html, $obj ) = @_;
34              
35 11         36 $self->error( undef );
36 11         32 $self->last_results( undef );
37              
38 11 50       45 ref $what eq 'HASH'
39             or return $self->_set_error('First argument to '
40             . 'extract_text() must be a hashref');
41              
42 11 50       30 defined $html
43             or return $self->_set_error('Second argument to extract_text() is '
44             . 'an undef, expected HTML');
45              
46 11 100       34 if ( defined $obj ) {
47 3 100       21 blessed $obj
48             or return $self->_set_error('Third argument must be an object');
49              
50 2         8 for ( keys %$what ) {
51 2 100       26 $obj->can($_)
52             or return $self->_set_error(
53             'The object your provided does not implement the ->'
54             . $_ . '() method that you requested in the first'
55             . ' argument',
56             );
57             }
58             }
59              
60              
61 9         73 my $dom = Mojo::DOM->new( $html );
62              
63 9         5072 my $did_have_error = 0;
64 9         62 for my $selector ( sort keys %$what ) {
65 23         32 my $result;
66              
67             try {
68 23     23   865 my @results = $self->_extract( $dom, $selector, $what, );
69 23 50 66     913 die "NOT FOUND\n"
70             if not @results and not $self->ignore_not_found;
71              
72 22 100       65 if ( defined (my $sep = $self->separator) ) {
73 10         62 $result = join $sep, @results;
74             }
75             else {
76 12         51 $result = [ @results ];
77             }
78              
79             } catch {
80 1     1   11 chomp($_);
81 1         4 $self->error("ERROR: [$selector]: $_");
82 1         1 $result = "ERROR: $_";
83 1         4 $did_have_error = 1;
84 23         231 };
85              
86 23         470 $what->{ $selector } = $result;
87             }
88              
89 9 100       34 if ( defined $obj ) {
90 1         3 for ( keys %$what ) {
91 1         5 $obj->$_( $what->{ $_ } );
92             }
93             }
94              
95 9         31 $self->last_results( $what );
96              
97 9 100       25 if ( $did_have_error ) {
98 1         8 return;
99             }
100             else {
101 8         78 return $what;
102             }
103             }
104              
105             sub separator {
106 39     39 1 68 my $self = shift;
107 39 100       106 if ( @_ ) { $self->[0]->{SEPARATOR} = shift; }
  15         82  
108 39         161 return $self->[0]->{SEPARATOR};
109             }
110              
111             sub ignore_not_found {
112 16     16 1 28 my $self = shift;
113 16 100       49 if ( @_ ) { $self->[0]->{IGNORE_NOT_FOUND} = shift; }
  14         200  
114 16         74 return $self->[0]->{IGNORE_NOT_FOUND};
115             }
116              
117             sub last_results {
118 33     33 1 51 my $self = shift;
119 33 100       86 if ( @_ ) { $self->[0]->{LAST_RESULTS} = shift; }
  20         39  
120 33         127 return $self->[0]->{LAST_RESULTS};
121             }
122              
123             sub error {
124 17     17 1 383 my $self = shift;
125 17 100       54 if ( @_ ) { $self->[0]->{ERROR} = shift; }
  14         32  
126 17         37 return $self->[0]->{ERROR};
127             }
128              
129             sub _set_error {
130 2     2   3 my ( $self, $error ) = @_;
131 2         3 $self->error( $error );
132 2         10 return;
133             }
134              
135             sub _process {
136 27     27   80 my $tag = $_->tag;
137 27 100       541 return _all_text($_) unless $tag =~ /input|img/;
138              
139 7 100 100     63 return $_->attr('alt')//''
      100        
      66        
      66        
140             if $tag eq 'img' or
141             ($tag eq 'input' and ($_->attr('type')//'') eq 'image');
142              
143 4   100     119 return $_->attr('value')//'';
144             }
145              
146             sub _extract {
147 22     22   46 my ( $self, $dom, $selector, $what ) = @_;
148             return $dom->find( $what->{ $selector } )
149 22     28   89 ->map( sub { $self->_process( @_ ) } )->each;
  28         10513  
150             }
151              
152             # The _all_text & _text functions copied from Mojo::DOM 6.66.
153             sub _all_text {
154 20     20   29 my ( $dom ) = @_;
155 20         25 my $trim = 1;
156              
157             # Detect "pre" tag
158 20         49 my $tree = $dom->tree;
159 20 50 33     288 map { $_->[1] eq 'pre' and $trim = 0 } Mojo::DOM::_ancestors( $dom ), $tree
  22 50       684  
160             if $trim && $tree->[0] ne 'root';
161              
162 20         56 return _text( [Mojo::DOM::_nodes($tree)], $trim );
163             }
164              
165             sub _text {
166 22     22   251 my ( $nodes, $trim ) = @_;
167              
168             # Merge successive text nodes.
169 22         29 my $i = 0;
170 22         79 while ( my $next = $nodes->[$i + 1] ) {
171 4 50 50     34 ++$i and next unless $nodes->[$i][0] eq 'text' && $next->[0] eq 'text';
      66        
172 0         0 splice @$nodes, $i, 2, ['text', $nodes->[$i][1] . $next->[1]];
173             }
174              
175 22         34 my $text = '';
176 22         45 for my $node ( @$nodes ) {
177 26         37 my $type = $node->[0];
178 26         29 my $chunk = '';
179              
180             # Text.
181 26 100 66     84 if ( $type eq 'text' ) {
    100          
    50          
182 23         30 $chunk = $node->[1];
183 23 50       50 if ( $trim ) {
184 23         78 $chunk =~ s/^\s+//;
185 23         47 $chunk =~ s/\s+$//;
186 23         46 $chunk =~ s/\s+/ /g;
187             }
188             }
189             # CDATA or raw text.
190             elsif ( $type eq 'cdata' || $type eq 'raw' ) {
191 1         3 $chunk = $node->[1];
192             }
193             # Nested tag.
194             elsif ( $type eq 'tag' ) {
195 4     4   5025 no warnings 'recursion';
  4         9  
  4         959  
196 2 50       8 $chunk = _text( [Mojo::DOM::_nodes($node)], 1, $node->[1] eq 'pre' ? 0 : $trim );
197             }
198              
199             # Add leading whitespace if punctuation allows it.
200 26 100 100     93 $chunk = " $chunk" if $text =~ /\S\z/ && $chunk =~ /^[^.!?,;:\s]+/;
201              
202             # Trim whitespace blocks.
203 26 100 66     171 $text .= $chunk if $chunk =~ /\S+/ || !$trim;
204             }
205              
206 22         128 return $text;
207             }
208              
209              
210             q|
211             Programming is 10% science, 20% ingenuity,
212             and 70% getting the ingenuity to work with the science.
213             |;
214              
215             __END__