File Coverage

blib/lib/Dancer/SearchApp/HTMLSnippet.pm
Criterion Covered Total %
statement 58 66 87.8
branch 12 22 54.5
condition 7 11 63.6
subroutine 7 8 87.5
pod 2 3 66.6
total 86 110 78.1


line stmt bran cond sub pod time code
1             package Dancer::SearchApp::HTMLSnippet;
2 1     1   66662 use strict;
  1         1  
  1         28  
3 1     1   463 use Filter::signatures;
  1         21293  
  1         34  
4 1     1   5 no warnings 'experimental::signatures';
  1         1  
  1         29  
5 1     1   613 use HTML::Restrict;
  1         125165  
  1         42  
6              
7 1     1   6 use vars qw($VERSION);
  1         1  
  1         662  
8             $VERSION = '0.06';
9              
10             =head1 NAME
11              
12             Dancer::SearchApp::HTMLSnippet - HTML snippet extractor
13              
14             =head1 SYNOPSIS
15              
16             my @document_snippets = Dancer::SearchApp::HTMLSnippet->extract_highlights(
17             html => $html,
18             hl_tag => '',
19             hl_end => '',
20             snippet_length => 150,
21             max_snippets => 8,
22             );
23              
24             =head1 METHODS
25              
26             =head2 C<< Dancer::SearchApp::HTMLSnippet->extract_highlights >>
27              
28             my @document_snippets = Dancer::SearchApp::HTMLSnippet->extract_highlights(
29             html => $html,
30             hl_tag => '',
31             hl_end => '',
32             snippet_length => 150,
33             max_snippets => 8,
34             );
35              
36             This extract the highlight snippets and metadata from the HTML
37             as prepared by Tika and highlightedd by Elasticsearch. It
38             returns a list of hash references, each containing a (well-formed)
39             HTML snippet containing the highlights and a C entry
40             noting the original page number if the snippet originated from
41             within a C<<

>> section (or crosses that)

42              
43             {
44             html => 'this is a result you searched for',
45             page => 42,
46             }
47              
48             =cut
49              
50 9 50   9 0 15 sub make_snippet( $html, $from, $to, $max_length ) {
  9 50       16  
  9         8  
  9         9  
  9         7  
  9         8  
  9         5  
51 9         8 my $start = $from->{start};
52 9         8 my $end = $to->{end};
53 9         15 my $fudge = int(( $max_length - ($end-$start)) / 2); # / for Filter::Simple
54            
55             # We want to start at something akin to a word boundary
56 9 100       37 if( substr($html,$start-$fudge-1,$fudge+2) =~ /\s+(.*)$/ ) {
57 8         11 $start-= length($1)
58             };
59              
60 9         9 $fudge = $max_length - ($end-$start);
61 9 50       28 $end += length($1)
62             if( substr($html,$end,$fudge) =~ /(.*)\s+/ );
63            
64             return +{
65 9         23 start => $start,
66             end => $end,
67             length => $end-$start,
68             };
69             };
70              
71             =head2 C<< Dancer::SearchApp::HTMLSnippet->extract_highlights >>
72              
73             my @hits = Dancer::SearchApp::HTMLSnippet->extract_highlights(
74             html => $html,
75             max_length => 300,
76             );
77            
78             for my $entry (@hits) {
79             print "Match: $entry->{start} ($entry->{length} bytes)\n";
80             };
81              
82             =cut
83              
84 2 50   2 1 9453 sub extract_highlights( $class, %options ) {
  2 50       8  
  2         3  
  2         12  
  2         2  
85 2   50     13 $options{ max_snippets } ||= 8;
86 2   50     4 $options{ max_length } ||= 150;
87 2   50     9 $options{ hl_tag } ||= '';
88 2   50     8 $options{ hl_end } ||= '';
89 2         3 my $html = $options{ html };
90 2         2 my @highlights;
91 2         60 while( $html =~ /(\Q$options{hl_tag}\E(.*?)\Q$options{hl_end}\E)/g ) {
92 1150         6896 push @highlights, {
93             start => pos($html)-length($1),
94             # Maybe count text outside of tags instead?!
95             length => length($1),
96             end => pos($html),
97             word => "$1",
98             };
99             };
100            
101             # Now, find the first matches (hardcoded, instead of finding the
102             # best snippets in the document)
103            
104 2         3 my @snippets;
105            
106 2 50       6 if( @highlights ) {
107             # We can stop once we are by
108 2         3 my $last = $highlights[-1]->{start};
109 2         2 my $curr = 0;
110 2         2 my $gather = 0;
111 2   100     14 while( @snippets < $options{ max_snippets }
112             and ($curr+$gather) < @highlights
113             ) {
114             # gather up as many highlights as fit for the next snippet
115             my $snippet_length = $highlights[$curr+$gather]->{end}
116             - $highlights[$curr]->{start}
117 31         35 ;
118 31 100       39 if( $snippet_length < $options{ max_length }) {
119 23         74 $gather++
120              
121             } else {
122             # Snippet got too long
123             # we should later readjust / center the snippet on the match(es)
124             push @snippets,
125             make_snippet( $html,
126             $highlights[$curr],
127             $highlights[$curr+$gather-1],
128 8         16 $options{ max_length });
129 8         7 $curr += $gather;
130 8         29 $gather = 0;
131             };
132             };
133            
134 2 100       5 if( ! @snippets) {
135             push @snippets,
136             make_snippet( $html,
137             $highlights[$curr],
138             $highlights[$curr],
139 1         16 $options{max_length});
140             };
141             };
142            
143             @snippets
144 2         267 }
145              
146             =head2 C<< Dancer::SearchApp::HTMLSnippet->cleanup_tika >>
147              
148             my $content = Dancer::SearchApp::HTMLSnippet->cleanup_tika( $html );
149              
150             Cleans up HTML output from Apache Tika.
151              
152             =cut
153              
154 0 0   0 1   sub cleanup_tika( $class, $html ) {
  0 0          
  0            
  0            
  0            
155 0           my $p = HTML::Restrict->new(
156             # rules => { div => ['class'], },
157             rules => {
158             #p => ['class'],
159             div => ['class', { class => qr/^page$/i }],
160             table => [],
161             tbody => [],
162             thead => [],
163             tr => [],
164             td => [],
165             a => [],
166             ul => [],
167             ol => [],
168             li => [],
169             },
170             replace_img => 1,
171             strip_enclosed_content => [ 'script', 'style', 'head' ]
172             );
173            
174             # rewrite the HTML to have the page numbers!
175             # Later
176 0           my $r = $p->process($html);
177             #warn $r;
178 0           return $r
179             }
180              
181             1;
182              
183             =head1 BUG TRACKER
184              
185             Please report bugs in this module via the RT CPAN bug queue at
186             L
187             or via mail to L.
188              
189             =head1 AUTHOR
190              
191             Max Maischein C
192              
193             =head1 COPYRIGHT (c)
194              
195             Copyright 2014-2016 by Max Maischein C.
196              
197             =head1 LICENSE
198              
199             This module is released under the same terms as Perl itself.
200              
201             =cut