File Coverage

blib/lib/Bible/OBML/Gateway.pm
Criterion Covered Total %
statement 158 219 72.1
branch 15 42 35.7
condition 5 26 19.2
subroutine 28 49 57.1
pod 5 5 100.0
total 211 341 61.8


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 1     1   224460 use 5.020;
  1         9  
5              
6 1     1   477 use exact;
  1         35909  
  1         4  
7 1     1   2790 use exact::class;
  1         11774  
  1         4  
8 1     1   756 use Bible::OBML;
  1         626895  
  1         15  
9 1     1   448 use Bible::Reference;
  1         2  
  1         6  
10 1     1   225 use Mojo::ByteStream;
  1         4  
  1         37  
11 1     1   7 use Mojo::DOM;
  1         2  
  1         24  
12 1     1   643 use Mojo::UserAgent;
  1         241854  
  1         9  
13 1     1   51 use Mojo::URL;
  1         3  
  1         3  
14 1     1   33 use Mojo::Util 'html_unescape';
  1         2  
  1         4654  
15              
16             our $VERSION = '2.05'; # VERSION
17              
18             has translation => 'NIV';
19             has url => Mojo::URL->new('https://www.biblegateway.com/passage/');
20             has ua => sub {
21             my $ua = Mojo::UserAgent->new( max_redirects => 3 );
22             $ua->transactor->name( __PACKAGE__ . '/' . ( __PACKAGE__->VERSION // '2.0' ) );
23             return $ua;
24             };
25             has reference => Bible::Reference->new(
26             bible => 'Protestant',
27             sorting => 1,
28             );
29              
30 1     1 1 1320 sub translations ($self) {
  1         2  
  1         3  
31 1         1 my $translations;
32              
33             $self->ua->get( $self->url )->result->dom->find('select.search-dropdown option')->each( sub {
34 2   100 2   3008 my $class = $_->attr('class') || '';
35              
36 2 100       31 if ( $class eq 'lang' ) {
    50          
37 1         5 my @language = $_->text =~ /\-{3}(.+)\s\(([^\)]+)\)\-{3}/;
38 1         16 push( @$translations, {
39             language => $language[0],
40             acronym => $language[1],
41             } );
42             }
43             elsif ( not $class ) {
44 1         5 my @translation = $_->text =~ /\s*(.+)\s\(([^\)]+)\)/;
45 1         10 push( @{ $translations->[-1]{translations} }, {
  1         9  
46             translation => $translation[0],
47             acronym => $translation[1],
48             } );
49             }
50 1         6 } );
51              
52 1         21 return $translations;
53             }
54              
55 1     1 1 2186 sub structure ( $self, $translation = $self->translation ) {
  1         3  
  1         5  
  1         11  
56             return $self->ua->get(
57             $self->url->clone->path( $self->url->path . 'bcv/' )->query( { version => $translation } )
58 1         7 )->result->json->{data}[0];
59             }
60              
61 21     21   182 sub _retag ( $tag, $retag ) {
  21         32  
  21         28  
  21         30  
62 21         55 $tag->tag($retag);
63 21         272 delete $tag->attr->{$_} for ( keys %{ $tag->attr } );
  21         42  
64             }
65              
66 1     1 1 8 sub fetch ( $self, $reference, $translation = $self->translation ) {
  1         2  
  1         1  
  1         3  
  1         3  
67 1         4 my $runs = $self->reference->require_verse_match(0)->acronyms(0)->clear->in($reference)->as_runs;
68 1 50 33     3962 $reference = $runs->[0] unless ( @$runs != 1 or $runs->[0] !~ /\w\s*\d/ );
69              
70 1         6 my $result = $self->ua->get(
71             $self->url->query( {
72             version => $translation,
73             search => $reference,
74             } )
75             )->result;
76              
77 1 50 0     137 croak( $translation . ' "' . ( $reference // '(undef)' ) . '" did not match a chapter or run of verses' )
78             if ( $result->dom->at('div.content-section') );
79              
80 1         12 return Mojo::ByteStream->new( $result->body )->decode->to_string;
81             }
82              
83 1     1 1 208 sub parse ( $self, $html ) {
  1         2  
  1         3  
  1         2  
84 1 50       6 return unless ($html);
85              
86 1         15 my $dom = Mojo::DOM->new($html);
87              
88 1         10987 my $ref_display = $dom->at('div.bcv div.dropdown-display-text');
89 1 50 33     1007 croak('source appears to be invalid; check your inputs') unless ( $ref_display and $ref_display->text );
90 1         70 my $reference = $ref_display->text;
91              
92 1 50       35 croak('EXB (Extended Bible) translation not supported')
93             if ( $dom->at('div.translation div.dropdown-display-text')->text eq 'Expanded Bible' );
94              
95 1         746 my $block = $dom->at('div.passage-text div.passage-content div:first-child');
96 1     8   1401 $block->find('*[data-link]')->each( sub { delete $_->attr->{'data-link'} } );
  8         3106  
97              
98 1         28 $html = $block->to_string;
99              
100 1         3688 $html =~ s`(\d+).(\d+)`$1/$2`g;
101 1         47 $html =~ s`(?:<){2,}(.*?)(?:\x{2019}>|(?:>){2,})`\x{201c}$1\x{201d}`g;
102 1         40 $html =~ s`(?:<)(.*?)(?:>|\x{2019})`\x{2018}$1\x{2019}`g;
103 1         46 $html =~ s`\\\w+``g;
104 1         49 $html =~ s/(?:\.\s*){2,}\./\x{2026}/;
105              
106 1         6 $block = Mojo::DOM->new($html)->at('div');
107              
108 1 50       8973 $_->parent->strip if ( $_ = $block->find('div.poetry > h2')->first );
109              
110 1     142   1901 $block->descendant_nodes->grep( sub { $_->type eq 'comment' } )->each('remove');
  142         5777  
111 1         270 $block
112             ->find('.il-text, hidden, hr, .translation-note, span.inline-note, a.full-chap-link, b.inline-h3')
113             ->each('remove');
114 1         10752 $block->find('.std-text, hgroup, b, em, versenum')->each('strip');
115             $block
116             ->find('i, .italic, .trans-change, .idiom, .catch-word, selah, span.selah')
117 1     0   6222 ->each( sub { _retag( $_, 'i' ) } );
  0         0  
118 1     0   10501 $block->find('.woj, u.jesus-speech')->each( sub { _retag( $_, 'woj' ) } );
  0         0  
119 1     0   3456 $block->find('.divine-name, .small-caps')->each( sub { _retag( $_, 'small_caps' ) } );
  0         0  
120              
121 10     10   2145 $block->find('sup')->grep( sub { length $_->text == 1 } )->each( sub {
122 0     0   0 $_->content( '-' . $_->content );
123 0         0 $_->strip;
124 1         4072 } );
125              
126 1         61 $self->reference->require_verse_match(1)->acronyms(1);
127              
128 1         33 my $footnotes = $block->at('div.footnotes');
129 1 50       1683 if ($footnotes) {
130             $footnotes->find('a.bibleref')->each( sub {
131 0   0 0   0 ( my $ref = $_->attr('data-bibleref') // '' ) =~ s/\.(\d+)\.(\d+)/ $1:$2/g;
132 0         0 $_->replace($ref);
133 0         0 } );
134 0         0 $footnotes->remove;
135             $footnotes = {
136             map {
137 0         0 '#' . $_->attr('id') => $self->reference->clear->in(
  0         0  
138             $_->at('span')->all_text
139             )->as_text
140             } $footnotes->find('ol li')->each
141             };
142             }
143              
144 1         5 my $crossrefs = $block->at('div.crossrefs');
145 1 50       1018 if ($crossrefs) {
146             $crossrefs->find('a.bibleref')->each( sub {
147 0   0 0   0 ( my $ref = $_->attr('data-bibleref') // '' ) =~ s/\.(\d+)\.(\d+)/ $1:$2/g;
148 0         0 $_->replace($ref);
149 1         10 } );
150 1         1385 $crossrefs->remove;
151             $crossrefs = {
152             map {
153 1         238 '#' . $_->attr('id') => $self->reference->clear->in(
  8         36605  
154             $_->at('a:last-child')->attr('data-bibleref')
155             )->refs
156             } $crossrefs->find('ol li')->each
157             };
158             }
159              
160             $block
161             ->find('span.text > a.bibleref')
162             ->map('parent')
163 0     0   0 ->grep( sub { $_->content =~ /^\[
164             ->each( sub {
165             $_->find('a')->each( sub {
166 0   0     0 ( my $ref = $_->attr('data-bibleref') // '' ) =~ s/\.(\d+)\.(\d+)/ $1:$2/g;
167 0         0 $_->replace($ref);
168 0     0   0 } );
169              
170 0         0 my $content = $_->content;
171 0         0 $content =~ s|\s+\[([^\]]+)\]|
172 0         0 '' . $self->reference->clear->in($1)->as_text . ''
173             |ge;
174              
175 0         0 $_->content($content);
176 1         16573 } );
177              
178             $block
179             ->find('i > a.bibleref, crossref > a.bibleref')
180             ->map('parent')
181 0     0   0 ->grep( sub { $_->children->size == 1 } )
182             ->each( sub {
183 0     0   0 my $a = $_->at('a:last-child');
184 0   0     0 ( my $ref = $_->attr('data-bibleref') // '' ) =~ s/\.(\d+)\.(\d+)/ $1:$2/g;
185              
186 0         0 $_->tag('sup');
187 0         0 $_->attr({
188             'class' => 'crossreference',
189             'data-cr' => $a->attr('data-bibleref'),
190             });
191              
192 0         0 $crossrefs = {
193             $a->attr('data-bibleref') => $self->reference->clear->in($ref)->refs
194             };
195 1         1441 } );
196              
197 1         2040 $block->find('a.bibleref')->each('strip');
198              
199             $block->find('sup.crossreference, sup.footnote')->each( sub {
200 8 50   8   3853 if ( $_->attr('class') eq 'footnote' ) {
    50          
201             $_->replace(
202             ( $footnotes->{ $_->attr('data-fn') } )
203 0 0       0 ? '' . $footnotes->{ $_->attr('data-fn') } . ''
204             : ''
205             );
206             }
207             elsif ( $_->attr('class') eq 'crossreference' ) {
208             $_->replace(
209             ( $crossrefs->{ $_->attr('data-cr') } )
210 8 50       278 ? '' . $crossrefs->{ $_->attr('data-cr') } . ''
211             : ''
212             );
213             }
214 1         1121 } );
215              
216 1     8   284 $block->find('footnote, crossref')->each( sub { _retag( $_, $_->tag ) } );
  8         1395  
217              
218 1         24 _retag( $block, 'obml' );
219 1         31 $block->child_nodes->first->prepend( $block->new_tag( 'reference', $reference ) );
220              
221 1         514 $block->find('h3.chapter')->each('remove');
222 1     0   824 $block->find('h2 + h3')->each( sub { $_->tag('h4') } );
  0         0  
223 1     2   947 $block->find('h2, h3')->each( sub { _retag( $_, 'header' ) } );
  2         1182  
224 1     0   20 $block->find('h4')->each( sub { _retag( $_, 'sub_header' ) } );
  0         0  
225              
226 1     2   756 $block->find('.versenum')->grep( sub { $_->text =~ /^\s*\(/ } )->each('remove');
  2         1042  
227 1     0   45 $block->find('.chapternum + .versenum')->each( sub { $_->previous->remove } );
  0         0  
228 1     0   1055 $block->find('.chapternum + i > .versenum')->each( sub { $_->parent->previous->remove } );
  0         0  
229              
230             $block->find('.chapternum')->each( sub {
231 1     1   986 _retag( $_, 'verse_number' );
232 1         32 $_->content(1);
233 1         1120 } );
234             $block->find('.versenum')->each( sub {
235 2     2   1081 _retag( $_, 'verse_number' );
236              
237 2         53 my $verse_number = $_->content;
238 2         156 $verse_number =~ s/^.*://g;
239 2         10 ($verse_number) = $verse_number =~ /(\d+)/;
240              
241 2         8 $_->content($verse_number);
242 1         170 } );
243              
244 1     5   127 $block->find('span.text')->each( sub { _retag( $_, 'text' ) } );
  5         1174  
245              
246             $block->find('table')->each( sub {
247             $_->find('tr')->each( sub {
248 0         0 $_->find('th')->each('remove');
249 0 0       0 unless ( $_->child_nodes->size ) {
250 0         0 $_->strip;
251             }
252             else {
253 0 0       0 $_->replace( join( '',
    0          
    0          
254             '',
255             $_->find('td text')->map('content')->join(', '),
256             (
257             ( $_->find('td text')->map('text')->last =~ /\W$/ ) ? '' :
258             ( $_->following_nodes->size ) ? '; ' : '.'
259             ),
260             ( ( $_->following_nodes->size ) ? ' ' : '' ),
261             ) );
262             }
263 0     0   0 } );
264              
265 0         0 $_->tag('div');
266 0         0 $_->content( '

' . $_->content . '

' );
267 1         37 } );
268              
269             $block->find('ul, ol')->each( sub {
270             $_->find('li')->each( sub {
271 0         0 $_->tag('text');
272 0         0 $_->find('text > text')->each('strip');
273 0 0 0     0 $_->append_content('
') if ( $_->next and $_->next->tag eq 'li' );
274 0     0   0 } );
275              
276 0         0 $_->tag('div');
277 0         0 $_->attr( class => 'left-1' );
278 0         0 $_->content( '

' . $_->content . '

' );
279 1         862 } );
280              
281 9         25 $block->find( join( ', ', map { 'div.left-' . $_ } 1 .. 9 ) )->each( sub {
282 0     0   0 my ($left) = $_->attr('class') =~ /\bleft\-(\d+)/;
283 0         0 $_->find('text')->each( sub { $_->attr( indent => $left ) } );
  0         0  
284 0         0 $_->strip;
285 1         1100 } );
286              
287 1     0   4382 $block->find('div.poetry')->each( sub { $_->attr( class => 'indent-1' ) } );
  0         0  
288 9         26 $block->find( join( ', ', map { '.indent-' . $_ } 1 .. 9 ) )->each( sub {
289 0     0   0 my ($indent) = $_->attr('class') =~ /\bindent\-(\d+)/;
290             $_->find('text')->each( sub {
291 0   0     0 $_->attr( indent => $indent + ( $_->attr('indent') || 0 ) );
292 0         0 } );
293 0         0 $_->strip;
294 1         799 } );
295              
296 1         4571 $block->find( join( ', ', map { '.indent-' . $_ . '-breaks' } 1 .. 5 ) )->each('remove');
  5         19  
297              
298             $block->find('text[indent]')->each( sub {
299 0     0   0 my $level = $_->attr('indent');
300 0         0 _retag( $_, 'indent' );
301 0         0 $_->attr( level => $level );
302 1         2724 } );
303 1         835 $block->find('text')->each('strip');
304              
305             $block->find('indent + indent')->each( sub {
306 0 0   0   0 if ( $_->previous->attr('level') eq $_->attr('level') ) {
307 0         0 $_->previous->append_content( ' ' . $_->content );
308 0         0 $_->remove;
309             }
310 1         1389 } );
311              
312 1     2   646 $block->find('p')->each( sub { _retag( $_, 'p' ) } );
  2         671  
313              
314 1 50 33     22 $block->at('p')->prepend_content('1')
315             if ( $block->at('p') and not $block->at('p')->at('verse_number') );
316              
317 1         630 $block->find('div, span, u, sup, bk')->each('strip');
318              
319 1         1857 $html = html_unescape( $block->to_string );
320 1         774 $html =~ s/

[ ]+/

/g;

321              
322 1         126 return $html;
323             }
324              
325 1     1 1 5504 sub get ( $self, $reference, $translation = $self->translation ) {
  1         3  
  1         2  
  1         4  
  1         11  
326 1         8 return Bible::OBML->new->html( $self->parse( $self->fetch( $reference, $translation ) ) );
327             }
328              
329             1;
330              
331             __END__