File Coverage

blib/lib/Bible/OBML/Gateway.pm
Criterion Covered Total %
statement 172 233 73.8
branch 18 48 37.5
condition 21 62 33.8
subroutine 28 49 57.1
pod 5 5 100.0
total 244 397 61.4


line stmt bran cond sub pod time code
1             package Bible::OBML::Gateway;
2             # ABSTRACT: Bible Gateway content conversion to Open Bible Markup Language
3              
4 2     2   395564 use 5.022;
  2         8  
5              
6 2     2   1273 use exact;
  2         94268  
  2         71  
7 2     2   6904 use exact::class;
  2         31843  
  2         7  
8 2     2   2071 use Bible::OBML;
  2         615987  
  2         19  
9 2     2   1189 use Bible::Reference;
  2         4  
  2         9  
10 2     2   925 use Mojo::ByteStream;
  2         6  
  2         81  
11 2     2   10 use Mojo::DOM;
  2         3  
  2         44  
12 2     2   1536 use Mojo::UserAgent;
  2         662930  
  2         26  
13 2     2   127 use Mojo::URL;
  2         5  
  2         14  
14 2     2   100 use Mojo::Util 'html_unescape';
  2         5  
  2         18151  
15              
16             our $VERSION = '2.12'; # VERSION
17              
18             has translation => 'NIV';
19             has reference => sub { Bible::Reference->new( bible => 'Protestant' ) };
20             has url => sub { Mojo::URL->new('https://www.biblegateway.com/passage/') };
21             has ua => sub {
22             my $ua = Mojo::UserAgent->new( max_redirects => 3 );
23             $ua->transactor->name( __PACKAGE__ . '/' . ( __PACKAGE__->VERSION // '2.0' ) );
24             return $ua;
25             };
26              
27 1     1 1 1642 sub translations ($self) {
  1         3  
  1         2  
28 1         3 my $translations;
29              
30             $self->ua->get( $self->url )->result->dom->find('select.search-dropdown option')->each( sub {
31 2   100 2   4232 my $class = $_->attr('class') || '';
32              
33 2 100       24 if ( $class eq 'lang' ) {
    50          
34 1         7 my @language = $_->text =~ /\-{3}(.+)\s\(([^\)]+)\)\-{3}/;
35 1         22 push( @$translations, {
36             language => $language[0],
37             acronym => $language[1],
38             } );
39             }
40             elsif ( not $class ) {
41 1         5 my @translation = $_->text =~ /\s*(.+)\s\(([^\)]+)\)/;
42 1         10 push( @{ $translations->[-1]{translations} }, {
  1         9  
43             translation => $translation[0],
44             acronym => $translation[1],
45             } );
46             }
47 1         5 } );
48              
49 1         42 return $translations;
50             }
51              
52 1     1 1 501642 sub structure ( $self, $translation = $self->translation ) {
  1         14  
  1         7  
  1         15  
53             return $self->ua->get(
54             $self->url->clone->path( $self->url->path . 'bcv/' )->query( { version => $translation } )
55 1         5 )->result->json->{data}[0];
56             }
57              
58 21     21   163 sub _retag ( $tag, $retag ) {
  21         42  
  21         40  
  21         59  
59 21         74 $tag->tag($retag);
60 21         352 delete $tag->attr->{$_} for ( keys %{ $tag->attr } );
  21         68  
61             }
62              
63 1     1 1 8 sub fetch ( $self, $reference, $translation = $self->translation ) {
  1         2  
  1         2  
  1         3  
  1         2  
64 1         6 my $runs = $self->reference
65             ->acronyms(0)->require_chapter_match(0)->require_book_ucfirst(0)
66             ->clear->in($reference)->as_runs;
67 1 50 33     6579 $reference = $runs->[0] unless ( @$runs != 1 or $runs->[0] !~ /\w\s*\d/ );
68              
69 1         8 my $result = $self->ua->get(
70             $self->url->query( {
71             version => $translation,
72             search => $reference,
73             } )
74             )->result;
75              
76 1 50 0     227 croak( $translation . ' "' . ( $reference // '(undef)' ) . '" did not match a chapter or run of verses' )
77             if ( $result->dom->at('div.content-section') );
78              
79 1         15 return Mojo::ByteStream->new( $result->body )->decode->to_string;
80             }
81              
82 1     1 1 147 sub parse ( $self, $html ) {
  1         4  
  1         3  
  1         2  
83 1 50       5 return unless ($html);
84              
85 1         12 my $dom = Mojo::DOM->new($html);
86              
87 1         14812 my $ref_display = $dom->at('div.bcv div.dropdown-display-text');
88 1 50 33     3084 croak('source appears to be invalid; check your inputs') unless ( $ref_display and $ref_display->text );
89              
90 1         69 my $reference = $ref_display->text;
91 1         35 my $translation = $dom->at('div.passage-col')->attr('data-translation');
92              
93 1 50       2075 croak('EXB (Extended Bible) translation not supported') if ( $translation eq 'EXB' );
94              
95 1         6 my $block = $dom->at('div.passage-text div.passage-content div:first-child');
96 1     8   5833 $block->find('*[data-link]')->each( sub { delete $_->attr->{'data-link'} } );
  8         2794  
97              
98 1         32 $html = $block->to_string;
99              
100 1         4076 $html =~ s`(\d+).(\d+)`$1/$2`g;
101 1         7 $html =~ s`(?:<){2,}(.*?)(?:\x{2019}>|(?:>){2,})`\x{201c}$1\x{201d}`g;
102 1         6 $html =~ s`(?:<)(.*?)(?:>|\x{2019})`\x{2018}$1\x{2019}`g;
103 1         5 $html =~ s`\\\w+``g;
104 1         14 $html =~ s/(?:\.\s*){2,}\./\x{2026}/;
105 1         4 $html =~ s/\x{200a}//g;
106              
107 1         9 $block = Mojo::DOM->new($html)->at('div');
108              
109 1 50       13262 $_->parent->strip if ( $_ = $block->find('div.poetry > h2')->first );
110              
111 1     142   1849 $block->descendant_nodes->grep( sub { $_->type eq 'comment' } )->each('remove');
  142         7313  
112 1         359 $block->find(
113             '.il-text, hidden, hr, .translation-note, span.inline-note, a.full-chap-link, b.inline-h3, top1'
114             )->each('remove');
115 1         10083 $block->find('.std-text, hgroup, b, em, versenum, char')->each('strip');
116             $block
117             ->find('i, .italic, .trans-change, .idiom, .catch-word, selah, span.selah')
118 1     0   6688 ->each( sub { _retag( $_, 'i' ) } );
  0         0  
119 1     0   8479 $block->find('.woj, u.jesus-speech')->each( sub { _retag( $_, 'woj' ) } );
  0         0  
120 1     0   2998 $block->find('.divine-name, .small-caps')->each( sub { _retag( $_, 'small_caps' ) } );
  0         0  
121              
122 10     10   2640 $block->find('sup')->grep( sub { length $_->text == 1 } )->each( sub {
123 0     0   0 $_->content( '-' . $_->content );
124 0         0 $_->strip;
125 1         3223 } );
126              
127 1         82 $self->reference->acronyms(1)->require_chapter_match(1)->require_book_ucfirst(1);
128              
129 1         55 my $footnotes = $block->at('div.footnotes');
130 1 50       1687 if ($footnotes) {
131             $footnotes->find('a.bibleref')->each( sub {
132 0   0 0   0 ( my $ref = $_->attr('data-bibleref') // '' ) =~ s/\.(\d+)\.(\d+)/ $1:$2/g;
133 0         0 $_->replace($ref);
134 0         0 } );
135 0         0 $footnotes->remove;
136             $footnotes = {
137             map {
138 0         0 '#' . $_->attr('id') => $self->reference->clear->in(
  0         0  
139             $_->at('span')->all_text
140             )->as_text
141             } $footnotes->find('ol li')->each
142             };
143             }
144              
145 1         43 my $crossrefs = $block->at('div.crossrefs');
146 1 50       1655 if ($crossrefs) {
147             $crossrefs->find('a.bibleref')->each( sub {
148 0   0 0   0 ( my $ref = $_->attr('data-bibleref') // '' ) =~ s/\.(\d+)\.(\d+)/ $1:$2/g;
149 0         0 $_->replace($ref);
150 1         19 } );
151 1         1244 $crossrefs->remove;
152             $crossrefs = {
153             map {
154 1         220 '#' . $_->attr('id') => $self->reference->clear->in(
  8         53725  
155             $_->at('a:last-child')->attr('data-bibleref')
156             )->refs
157             } $crossrefs->find('ol li')->each
158             };
159             }
160              
161             $block
162             ->find('span.text > a.bibleref')
163             ->map('parent')
164 0     0   0 ->grep( sub { $_->content =~ /^\[
165             ->each( sub {
166             $_->find('a')->each( sub {
167 0   0     0 ( my $ref = $_->attr('data-bibleref') // '' ) =~ s/\.(\d+)\.(\d+)/ $1:$2/g;
168 0         0 $_->replace($ref);
169 0     0   0 } );
170              
171 0         0 my $content = $_->content;
172 0         0 $content =~ s|\s+\[([^\]]+)\]|
173 0         0 '' . $self->reference->clear->in($1)->as_text . ''
174             |ge;
175              
176 0         0 $_->content($content);
177 1         24131 } );
178              
179             $block
180             ->find('i > a.bibleref, crossref > a.bibleref')
181             ->map('parent')
182 0     0   0 ->grep( sub { $_->children->size == 1 } )
183             ->each( sub {
184 0     0   0 my $a = $_->at('a:last-child');
185 0   0     0 ( my $ref = $_->attr('data-bibleref') // '' ) =~ s/\.(\d+)\.(\d+)/ $1:$2/g;
186              
187 0         0 $_->tag('sup');
188 0         0 $_->attr({
189             'class' => 'crossreference',
190             'data-cr' => $a->attr('data-bibleref'),
191             });
192              
193 0         0 $crossrefs = {
194             $a->attr('data-bibleref') => $self->reference->clear->in($ref)->refs
195             };
196 1         1830 } );
197              
198 1         1666 $block->find('a.bibleref')->each('strip');
199              
200             $block->find('sup.crossreference, sup.footnote')->each( sub {
201 8 50   8   5257 if ( $_->attr('class') eq 'footnote' ) {
    50          
202             $_->replace(
203             ( $footnotes->{ $_->attr('data-fn') } )
204 0 0       0 ? '' . $footnotes->{ $_->attr('data-fn') } . ''
205             : ''
206             );
207             }
208             elsif ( $_->attr('class') eq 'crossreference' ) {
209             $_->replace(
210             ( $crossrefs->{ $_->attr('data-cr') } )
211 8 50       331 ? '' . $crossrefs->{ $_->attr('data-cr') } . ''
212             : ''
213             );
214             }
215 1         1086 } );
216              
217             $block->find('footnote, crossref')->each( sub {
218 8     8   1531 _retag( $_, $_->tag );
219              
220             # patch for if there's an error in the source HTML where some footnotes
221             # or crossrefs should have a space but don't
222              
223 8         141 my $previous = $_;
224 8   66     29 $previous = $previous->previous_node while (
      33        
      66        
225             $previous and $previous->tag and
226             ( $previous->tag eq 'crossref' or $previous->tag eq 'footnote' )
227             );
228 8 50 33     1209 my $previous_char = substr( ( ($previous) ? $previous->all_text || $previous->content : '' ), -1 );
229              
230 8         571 my $next = $_;
231 8   66     28 $next = $next->next_node while (
      33        
      66        
232             $next and $next->tag and
233             ( $next->tag eq 'crossref' or $next->tag eq 'footnote' )
234             );
235 8 50 33     1278 my $next_char = substr( ( ($next) ? $next->all_text || $next->content : '' ), 0, 1 );
236              
237 8 50 33     690 $_->append(' ') if (
      33        
      33        
238             length $previous_char and
239             length $next_char and
240             $previous_char =~ /[:;,\w\!\.\?]/ and
241             $next_char =~ /\w/
242             );
243 1         406 } );
244              
245 1         27 _retag( $block, 'obml' );
246 1         38 $block->child_nodes->first->prepend( $block->new_tag( 'reference', $reference ) );
247              
248 1         706 $block->find('h3.chapter')->each('remove');
249 1     0   984 $block->find('h2 + h3')->each( sub { $_->tag('h4') } );
  0         0  
250 1     2   869 $block->find('h2, h3')->each( sub { _retag( $_, 'header' ) } );
  2         1299  
251 1     0   30 $block->find('h4')->each( sub { _retag( $_, 'sub_header' ) } );
  0         0  
252              
253 1     2   788 $block->find('.versenum')->grep( sub { $_->text =~ /^\s*\(/ } )->each('remove');
  2         1128  
254 1     0   73 $block->find('.chapternum + .versenum')->each( sub { $_->previous->remove } );
  0         0  
255 1     0   1128 $block->find('.chapternum + i > .versenum')->each( sub { $_->parent->previous->remove } );
  0         0  
256              
257             $block->find('.chapternum')->each( sub {
258 1     1   1018 _retag( $_, 'verse_number' );
259 1         41 $_->content(1);
260 1         1285 } );
261             $block->find('.versenum')->each( sub {
262 2     2   1194 _retag( $_, 'verse_number' );
263              
264 2         127 my $verse_number = $_->content;
265 2         224 $verse_number =~ s/^.*://g;
266 2         14 ($verse_number) = $verse_number =~ /(\d+)/;
267              
268 2         10 $_->content($verse_number);
269 1         209 } );
270              
271 1     5   170 $block->find('span.text')->each( sub { _retag( $_, 'text' ) } );
  5         1358  
272              
273             $block->find('table')->each( sub {
274             $_->find('tr')->each( sub {
275 0         0 $_->find('th')->each('remove');
276 0 0       0 unless ( $_->child_nodes->size ) {
277 0         0 $_->strip;
278             }
279             else {
280 0 0       0 $_->replace( join( '',
    0          
    0          
281             '',
282             $_->find('td text')->map('content')->join(', '),
283             (
284             ( $_->find('td text')->map('text')->last =~ /\W$/ ) ? '' :
285             ( $_->following_nodes->size ) ? '; ' : '.'
286             ),
287             ( ( $_->following_nodes->size ) ? ' ' : '' ),
288             ) );
289             }
290 0     0   0 } );
291              
292 0         0 $_->tag('div');
293 0         0 $_->content( '

' . $_->content . '

' );
294 1         43 } );
295              
296             $block->find('ul, ol')->each( sub {
297             $_->find('li')->each( sub {
298 0         0 $_->tag('text');
299 0         0 $_->find('text > text')->each('strip');
300 0 0 0     0 $_->append_content('
') if ( $_->next and $_->next->tag eq 'li' );
301 0     0   0 } );
302              
303 0         0 $_->tag('div');
304 0         0 $_->attr( class => 'left-1' );
305 0         0 $_->content( '

' . $_->content . '

' );
306 1         783 } );
307              
308 9         32 $block->find( join( ', ', map { 'div.left-' . $_ } 1 .. 9 ) )->each( sub {
309 0     0   0 my ($left) = $_->attr('class') =~ /\bleft\-(\d+)/;
310 0         0 $_->find('text')->each( sub { $_->attr( indent => $left ) } );
  0         0  
311 0         0 $_->strip;
312 1         1141 } );
313              
314 1     0   4313 $block->find('div.poetry')->each( sub { $_->attr( class => 'indent-1' ) } );
  0         0  
315 9         61 $block->find( join( ', ', map { '.indent-' . $_ } 1 .. 9 ) )->each( sub {
316 0     0   0 my ($indent) = $_->attr('class') =~ /\bindent\-(\d+)/;
317             $_->find('text')->each( sub {
318 0   0     0 $_->attr( indent => $indent + ( $_->attr('indent') || 0 ) );
319 0         0 } );
320 0         0 $_->strip;
321 1         795 } );
322              
323 1         4514 $block->find( join( ', ', map { '.indent-' . $_ . '-breaks' } 1 .. 5 ) )->each('remove');
  5         23  
324              
325             $block->find('text[indent]')->each( sub {
326 0     0   0 my $level = $_->attr('indent');
327 0         0 _retag( $_, 'indent' );
328 0         0 $_->attr( level => $level );
329 1         2752 } );
330 1         944 $block->find('text')->each('strip');
331              
332             $block->find('indent + indent')->each( sub {
333 0 0   0   0 if ( $_->previous->attr('level') eq $_->attr('level') ) {
334 0         0 $_->previous->append_content( ' ' . $_->content );
335 0         0 $_->remove;
336             }
337 1         1714 } );
338              
339 1     2   784 $block->find('p')->each( sub { _retag( $_, 'p' ) } );
  2         833  
340              
341 1 50 33     29 $block->at('p')->prepend_content('1')
342             if ( $block->at('p') and not $block->at('p')->at('verse_number') );
343              
344 1         1882 $block->find('div, span, u, sup, bk, verse, start-chapter')->each('strip');
345              
346 1   33     2507 $_->each('strip') while ( $_ = $block->find('i > i') and $_->size );
347              
348 1         733 $html = html_unescape( $block->to_string );
349              
350 1         939 $html =~ s/

[ ]+/

/g; # remove spaces immediately after a "

"

351 1         11 $html =~ s/,([A-z]|)/, $1/g; # fix missing spaces after commas (error in source HTML)
352 1         5 $html =~ s/([A-z])()/$1 $2/g; # fix missing spaces before (error in source HTML)
353 1         121 $html =~ s/([a-z])([A-Z])/$1 $2/g; # fix missing spaces from collapsed words (error in source HTML)
354              
355             # hack: add back in any missing spaces after some punctuation
356 1         156 $html =~ s/([a-z])([:;,!?])([A-Za-z])/$1$2 $3/g;
357              
358 1         257 return $html;
359             }
360              
361 1     1 1 6773 sub get ( $self, $reference, $translation = $self->translation ) {
  1         3  
  1         3  
  1         7  
  1         17  
362 1         7 return Bible::OBML->new->html( $self->parse( $self->fetch( $reference, $translation ) ) );
363             }
364              
365             1;
366              
367             __END__