File Coverage

lib/Book/Bilingual/Reader.pm
Criterion Covered Total %
statement 109 117 93.1
branch 33 40 82.5
condition n/a
subroutine 15 17 88.2
pod 1 3 33.3
total 158 177 89.2


line stmt bran cond sub pod time code
1             package Book::Bilingual::Reader;
2             # ABSTRACT: A book reader class
3 1     1   59389 use Mojo::Base -base;
  1         161936  
  1         14  
4 1     1   441 use Book::Bilingual;
  1         2  
  1         6  
5 1     1   332 use Book::Bilingual::File;
  1         2  
  1         6  
6 1     1   31 use Path::Tiny qw/path/;
  1         2  
  1         32  
7 1     1   4 use Carp;
  1         2  
  1         1595  
8              
9             has 'book' => sub { Book::Bilingual->new}; # Book::Bilingual
10             has 'file'; # Book File
11             has '_ptr'; # Current pointer
12             has '_chapter_dlines'; # Href { loc => Dline } in chapter
13             has '_curr_dlineset'; # Href { loc => Dline } of current dlineset
14              
15             sub new { ## ($path :Path)
16 14 50   14 1 6794 croak 'Need args ($path)' unless @_ > 1;
17 14         41 my $self = $_[0]->SUPER::new({ file => path($_[1]) });
18              
19             # Initialize pointer
20 14         522 $self->_ptr([0,0,0]);
21              
22             # Setup book
23 14         123 $self->book(Book::Bilingual::File->new($_[1])->book);
24              
25             # Load current chapter
26 14         128 $self->_load_chapter();
27              
28             # Load current Dlineset
29 14         36 $self->_load_dlineset();
30              
31 14         86 return $self;
32             }
33             sub _load_chapter { ## () :> Self
34 14     14   16 my ($self) = @_;
35              
36             # Fetch chapter being pointed to
37 14         24 my $ch_idx = $self->_ptr->[0];
38 14         45 my $chapter = $self->book->chapters->[$ch_idx];
39              
40             # Empty the target hashref
41 14         72 $self->{_chapter_dlines} = {};
42              
43             # Iterate over all Dlinesets in chapter
44 14         14 my $dset_idx = 0;
45 14         19 foreach my $dset ($chapter->number, $chapter->title, @{$chapter->body}) {
  14         69  
46             # Store the default/target dline into _chapter_dlines
47 84         127 my $dline_idx = $dset->dline_count - 1;
48             # say "$ch_idx.$dset_idx.$dline_idx: ". $dset->target->str;
49 84         96 $self->{_chapter_dlines}{"$ch_idx.$dset_idx.$dline_idx"} = $dset->target;
50              
51 84         94 $dset_idx++;
52             }
53              
54 14         17 return $self;
55             }
56             sub _load_dlineset { ## () :> Self
57 14     14   16 my ($self) = @_;
58              
59             # Fetch dlineset being pointed to
60 14         19 my $ch_idx = $self->_ptr->[0];
61 14         41 my $chapter = $self->book->chapters->[$ch_idx];
62              
63             # Empty the target hashref
64 14         58 $self->{_curr_dlineset} = {};
65              
66             # Iterate over all Dlinesets in chapter
67 14         19 my $dset_idx = $self->_ptr->[1];
68 14         40 my $dset;
69 14 50       48 $dset = $chapter->number if $dset_idx eq 0;
70 14 50       41 $dset = $chapter->title if $dset_idx eq 1;
71 14 50       22 $dset = $chapter->body->[$dset_idx-2] if $dset_idx > 1;
72              
73 14         14 my $dline_idx = 0;
74 14         10 foreach my $dline (@{$dset->set}) {
  14         21  
75             # say "$ch_idx.$dset_idx.$dline_idx: ". $dline->str;
76 42         93 $self->{_curr_dlineset}{"$ch_idx.$dset_idx.$dline_idx"} = $dline;
77 42         54 $dline_idx++;
78             }
79              
80 14         16 return $self;
81             }
82              
83             sub html { ## () :> HTML
84 1     1 0 5 my ($self) = @_;
85              
86 1         2 my $ch_idx = $self->_ptr->[0];
87 1         4 my $dset_idx = $self->_ptr->[1];
88              
89             # Regex that matches other locations in the same dlineset
90             # e.g. '0.1.2' will match qr/^0\.1\.\d+$/
91 1         23 my $curr_dlineset_re = qr/^$ch_idx\.$dset_idx\.\d+$/;
92              
93             # Use the chapter Dline, except if the Dline is in the same Dlineset
94             # as the pointer, use the pointed to Dline
95             my @html = map {
96 6         14 my $loc = $_;
97 6 100       19 my $pointed = $loc =~ $curr_dlineset_re ? 1 : 0;
98             my $dline = $pointed
99 1         4 ? $self->_curr_dlineset->{join('.',@{$self->_ptr})}
100 6 100       13 : $self->_chapter_dlines->{$loc};
101              
102 6         19 my $ptr = $loc;
103 6         27 $ptr =~ s/\.(\d+)$/\.0/;
104              
105             # Render Dline depending on whether it is pointed to
106 6 100       16 $pointed ? _render_pointed($dline->class, $dline->str, $ptr)
107             : _render_normal($dline->class, $dline->str, $ptr);
108 1         3 } sort keys %{$self->_chapter_dlines};
  1         4  
109              
110             # Render to HTML
111 1         5 my $html = join '', @html;
112              
113             # say "\n$html\n";
114 1         7 return $html;
115             }
116             sub _render {
117 0     0   0 my ($dline) = @_;
118              
119 0         0 my $seg_idx = 0;
120             my @spans = map {
121 0 0       0 my $class = $seg_idx++ % 2 ? 'tgt-lang' : 'src-lang';
  0         0  
122 0         0 " $_";
123             } split(' /', $dline->str);
124              
125 0         0 return '
'.join('',@spans).'
';
126             }
127             sub _render_normal { ## ($class,$str,$ptr)
128 12     12   1906 my ($class,$str,$ptr) = @_;
129              
130 12         36 $str =~ s/^ \///; # Remove language mark
131 12         15 $str .= ' '; # Add a space at the end of the line
132              
133 12 100       33 return "\n

'.$str."

\n"
134             if $class eq 'chapter-number';
135              
136 8 100       17 return "\n

'.$str."

"
137             if $class eq 'chapter-title';
138              
139 6 100       15 return "\n\n
'.$str.''
140             if $class eq 'paragraph-start';
141              
142 3         10 return "$str";
143             }
144             sub _render_pointed { ## ($class,$str)
145 3     3   1207 my ($class,$str,$ptr) = @_;
146              
147 3         4 my $seg_class = '';
148             my @spans = map {
149             # Toggles segment class.
150             # One of: 'src-lang'|''. Always start as 'src-lang'.
151 3 100       11 $seg_class = $seg_class eq '' ? 'src-lang' : '';
  4         9  
152              
153             # If segment class is src-lang, return a span else as is
154 4 100       11 my $span = $seg_class ? "$_ " : $_;
155              
156             # Return span only if segment is non-empty, else empty string
157 4 50       22 $_ ? $span : '';
158             } split(' /', $str);
159              
160 3         10 my $wrapped = "".join('',@spans)."";
161              
162 3         7 return _render_normal($class,$wrapped,$ptr);
163             }
164              
165             sub _next_ptr { ## () :> undef | [Ptr,diff_chapter,diff_dlineset,diff_dline]
166 5     5   95 my ($self) = @_;
167              
168 5         9 my $ptr_0 = $self->_ptr->[0];
169 5         14 my $ptr_1 = $self->_ptr->[1];
170 5         14 my $ptr_2 = $self->_ptr->[2];
171              
172 5         14 my $max_chapter_idx
173             = $self->book->chapter_count - 1;
174 5         7 my $max_chapter_dlineset_idx
175             = $self->book->chapter_dlineset_count($ptr_0) - 1;
176 5         8 my $max_chapter_dlineset_dline_idx
177             = $self->book->chapter_dlineset_dline_len($ptr_0, $ptr_1) - 1;
178              
179             # Case 1: NOT end of current dlineset
180             # Return pointer to next dline in dlineset
181 5 100       14 return [join('.', $ptr_0, $ptr_1, $ptr_2+1), 0, 0, 1]
182             if $ptr_2 < $max_chapter_dlineset_dline_idx;
183              
184             # Case 2: End of current dlineset, NOT end of dlinesets in chapter
185             # Return pointer to first dline in next dlineset
186 3 100       10 return [join('.', $ptr_0, $ptr_1+1, 0), 0, 1, 1]
187             if $ptr_1 < $max_chapter_dlineset_idx;
188              
189             # Case 3: End of chapter dlineset, NOT end of chapters in book
190             # Return pointer to first dline in next chapter
191 2 100       8 return [join('.', $ptr_0+1, 0, 0), 0, 1, 1]
192             if $ptr_0 < $max_chapter_idx;
193              
194             # Case 4: End of book chapters
195             # Return undef
196 1         2 return undef;
197             }
198             sub _prev_ptr { ## () :> undef | [PtrStr,diff_chapter,diff_dlineset,diff_dline]
199 4     4   106 my ($self) = @_;
200              
201 4         20 my $ptr_0 = $self->_ptr->[0]; # Chapter_idx
202 4         13 my $ptr_1 = $self->_ptr->[1]; # Dlineset_idx
203 4         12 my $ptr_2 = $self->_ptr->[2]; # Dline_idx
204              
205 4         12 my $max_chapter_idx
206             = $self->book->chapter_count - 1;
207              
208             # Case 1: NOT at first Dline in current Dlineset
209             # Return pointer to prev dline in Dlineset
210 4 100       12 return [join('.', $ptr_0, $ptr_1, $ptr_2-1), 0, 0, 1]
211             if $ptr_2 > 0;
212              
213             # Case 2: At first Dline in current Dlineset, NOT at first Dlineset in Chapter
214             # Return pointer to prev Dlineset in Chapter
215 3 100       7 if ($ptr_1 > 0) {
216 1         3 my $Curr_chapter_Prev_dlineset_Max_dline_idx
217             = $self->book->chapter_dlineset_dline_len($ptr_0, $ptr_1-1) - 1;
218 1         6 return [join('.', $ptr_0,
219             $ptr_1-1,
220             $Curr_chapter_Prev_dlineset_Max_dline_idx), 0, 0, 1]
221             }
222              
223             # Case 3: At first Dlineset in chapter, NOT at first Chapter
224             # Return pointer to last Dline in last Dlineset in prev Chapter
225 2 100       5 if ($ptr_0 > 0) {
226 1         3 my $Prev_chapter_Max_dlineset_idx
227             = $self->book->chapter_dlineset_count($ptr_0-1) - 1;
228 1         3 my $Prev_chapter_Max_dlineset_Max_dline_idx
229             = $self->book->chapter_dlineset_dline_len(
230             $ptr_0-1, $Prev_chapter_Max_dlineset_idx) - 1;
231 1         5 return [join('.', $ptr_0-1,
232             $Prev_chapter_Max_dlineset_idx,
233             $Prev_chapter_Max_dlineset_Max_dline_idx), 0, 0, 1]
234             }
235              
236             # Case 4: At start of book
237             return undef
238 1         3 }
239             sub _max_loc {
240 1     1   411 (sort _cmp_loc (keys %{$_[0]}))[-1]
  1         6  
241             }
242             sub _cmp_loc ($$) {
243 5     5   1752 my ($a,$b) = @_;
244              
245             # Convert each element to a zero-prefixed 5 digit string
246 5         26 my $A = sprintf("%05d%05d%05d",split('\.',$a));
247 5         17 my $B = sprintf("%05d%05d%05d",split('\.',$b));
248              
249 5         14 return $A cmp $B;
250             }
251              
252 0     0 0   sub ptr { join '.', @{shift->_ptr} }
  0            
253              
254             =encoding utf8
255             =cut
256             =head1 ATTRIBUTES
257             =cut
258             =head2 _chapter_dlines
259              
260             The _chapter_dlines attribute stores the default Dline for each Dlineset
261             that makes up the chapter. In other words it has the Dline that should
262             be visible. It is a Hashref of a location to Dline. Example:
263              
264             _chapter_dlines = {
265             '0.0.2' => Dline({ class => 'chapter-number', str => ' /Chapter One' });
266             '0.1.2' => Dline({ class => 'chapter-title', str => ' /A Great Surprise' });
267             '0.2.13' => Dline({ class => 'paragraph-start',str => ' /"Mother, have ...' });
268             ...
269             }
270             =cut
271             =head2 _curr_dlineset
272              
273             The _curr_dlineset attribute stores the Dlineset that is currently being
274             pointed to. In other words, it is the sentence that is currently being
275             read and being transformed from the native language to the target
276             language. Example:
277              
278             _curr_dlineset = {
279             '0.1.0' => Dline({ class => 'chapter-title', str => 'ความประหลาดใจที่ยอดเยี่ยม /' });
280             '0.1.1' => Dline({ class => 'chapter-title', str => 'ความประหลาดใจที่ /Great' });
281             '0.1.2' => Dline({ class => 'chapter-title', str => ' /A Great Surprise' });
282             }
283             =cut
284             =head1 METHODS
285             =cut
286             =head2 _load_chapter() :> Self
287              
288             The _load_chapter() private method loads the chapter's default/targer
289             Dlines into the _chapter_dlines attribute. It loads the chapter
290             currently pointed to by the objects '_ptr' attribute.
291              
292             =cut
293             =head2 html() :> HTML
294              
295             The html() public method renders the current chapter into HTML.
296              
297             =cut
298             =head2 _render(Dline) :> HTML
299              
300             The _render(Dline) private function renders the given Dline into a
301             suitable HTML string.
302              
303             =cut
304             =head2 _render_normal(Dline) :> HTML
305              
306             The _render_normal(Dline) private method renders the given Dline into
307             the normal HTML output.
308              
309             This method reads the class of the given Dline and renders the
310             associated element. The Dline class is responsible for font-size and
311             white-space associated with the line but not colors.
312              
313             =cut
314             =head2 _render_pointed(Dline) :> HTML
315              
316             The _render_pointed(Dline) private method renders the pointed-to line.
317             The pointed-to line entire broken line is displayed with a background.
318             The source language has one set of font-color and background-color while
319             the target language has another set of font-color and background-color.
320              
321             We implement this by first wrapping the entire line in a span with class
322             "current-line". Then each segment is wrapped in a span as well. Segments
323             in the source language is has a class of src-lang. Segments in the
324             target language are unlabeled.
325              
326             Then create a new Dline with the same class as the input, but with the
327             generated HTML as the str. Then call _render_normal on this new Dline to
328             generate the final HTML.
329              
330             =cut
331              
332             =head2 _next_ptr() :> undef | [Ptr,diff_chapter,diff_dlineset,diff_dline]
333              
334             The _next_ptr() private method returns undef if no next pointer is
335             available. Otherwise, it returns the next pointer (Ptr), several boolean
336             flags indicating whether the next pointer points to a location in a
337             different chapter, a different dlineset or a different dline.
338              
339             =cut
340             =head2 _prev_ptr() :> undef | [PtrStr,diff_chapter,diff_dlineset,diff_dline]
341              
342             The _prev_ptr() private method returns undef if no previous pointer is
343             available. Otherwise, it returns the previous pointer (Ptr), several boolean
344             flags indicating whether the previous pointer points to a location in a
345             different chapter, a different dlineset or a different dline.
346              
347             =cut
348             =head2 _max_loc($href)
349              
350             Returns the max location of a href that has locations as key.
351              
352             =cut
353             =head2 _cmp_loc($a,$b)
354              
355             Returns -1, 0 or 1 depending on whether the left argument is stringwise
356             less than, equal to or greater than the right argument.
357              
358             $a < $b : -1
359             $a = $b : 0
360             $a > $b : 1
361              
362             =cut
363              
364             1;