File Coverage

lib/Book/Bilingual/Reader.pm
Criterion Covered Total %
statement 127 135 94.0
branch 33 40 82.5
condition n/a
subroutine 17 19 89.4
pod 1 4 25.0
total 178 198 89.9


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

'.$str."

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

'.$str."

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