File Coverage

lib/Wiki/JSON/HTML.pm
Criterion Covered Total %
statement 273 300 91.0
branch 96 126 76.1
condition 23 32 71.8
subroutine 26 27 96.3
pod 0 1 0.0
total 418 486 86.0


line stmt bran cond sub pod time code
1             package Wiki::JSON::HTML;
2              
3 8     8   110 use v5.16.3;
  8         26  
4              
5 8     8   39 use strict;
  8         14  
  8         278  
6 8     8   48 use warnings;
  8         14  
  8         439  
7              
8 8     8   38 use Moo;
  8         42  
  8         62  
9 8     8   9116 use Mojo::Util qw/xml_escape/;
  8         1603135  
  8         1396  
10 8     8   5280 use Mojo::URL;
  8         88201  
  8         60  
11              
12             has _wiki_json => ( is => 'lazy' );
13              
14             sub pre_html_json {
15 52     52 0 12351 my ( $self, $wiki_text, $template_callback, $options ) = @_;
16 52   50     274 $options //= {};
17 52         79 my @dom;
18 52         250 push @dom,
19             $self->_open_html_element( 'article', 0, { class => 'wiki-article' } );
20 52         1369 my $json =
21             $self->_wiki_json->parse( $wiki_text, { track_lines_for_errors => 1 } );
22              
23             # print Data::Dumper::Dumper $json;
24              
25 52         225 push @dom, @{ $self->_parse_output( $json, $template_callback, $options ) };
  52         195  
26 52         183 push @dom, $self->_close_html_element('article');
27 52         591 return \@dom;
28             }
29              
30             sub _build__wiki_json {
31 52     52   539 my $self = shift;
32 52         353 require Wiki::JSON;
33 52         171 return Wiki::JSON->new;
34             }
35              
36             sub _open_html_element {
37 377 50   377   3939 if ( @_ < 2 ) {
38 0         0 die '_open_html_element needs $self and $tag at least as arguments';
39             }
40 377         772 my ( $self, $tag, $self_closing, $attributes ) = @_;
41 377   100     1131 $self_closing //= 0;
42 377   100     959 $attributes //= {};
43 377 50       876 if ( 'HASH' ne ref $attributes ) {
44 0         0 die 'HTML attributes are not a HASHREF';
45             }
46             return {
47 377 100       1992 tag => $tag,
48             status => $self_closing ? 'self-close' : 'open',
49             attrs => $attributes,
50             };
51             }
52              
53             sub _close_html_element {
54 361 50   361   941 if ( @_ != 2 ) {
55 0         0 die
56             '_close_html_element accepts exactly the following arguments $self and $tag';
57             }
58 361         553 my ( $self, $tag ) = @_;
59             return {
60 361         1464 tag => $tag,
61             status => 'close',
62             };
63             }
64              
65             sub _html_string_content_to_pushable {
66 117     117   208 my ( $self, $content ) = @_;
67 117         426 $content =~ s/(?:\r|\n)/ /gs;
68 117         399 $content =~ s/ +/ /gs;
69 117         278 return $content;
70             }
71              
72             sub _parse_output_try_parse_plain_text {
73 167 50   167   330 if ( @_ != 6 ) {
74 0         0 die
75             '_parse_output_try_parse_plain_text needs $self, $dom, $element, $last_element_inline_element, $needs_closing_parragraph, $options';
76             }
77 167         394 my ( $self, $dom, $element, $last_element_inline_element,
78             $needs_closing_parragraph, $options )
79             = @_;
80 167         197 my $needs_next = 0;
81 167         188 my $found_text;
82 167 100       333 if ( 'HASH' ne ref $element ) {
83 114         138 $found_text = 1;
84 114 100       217 if ( !$last_element_inline_element ) {
85 106         247 ($needs_closing_parragraph) =
86             $self->_close_parragraph( $dom, $needs_closing_parragraph,
87             $options );
88             }
89 114 50       215 if ($element) {
90 114 100       252 if ( !$last_element_inline_element ) {
91 106         250 ($needs_closing_parragraph) =
92             $self->_open_parragraph( $dom, $needs_closing_parragraph, 0,
93             $options );
94             }
95 114         256 push @$dom, $self->_html_string_content_to_pushable($element);
96             }
97 114         154 $needs_next = 1;
98             }
99 167         356 return ( $needs_next, $needs_closing_parragraph, $found_text );
100             }
101              
102             sub _parse_output_try_parse_italic {
103 47 50   47   127 if ( @_ < 7 ) {
104 0         0 die 'Incorrect arguments _parse_output_try_parse_italic';
105             }
106 47         117 my ( $self, $dom, $element, $found_inline_element,
107             $needs_closing_parragraph, $template_callback, $options )
108             = @_;
109 47         74 my $needs_next;
110 47 100       132 if ( $element->{type} eq 'italic' ) {
111 3         22 $found_inline_element = 1;
112 3         18 ($needs_closing_parragraph) =
113             $self->_open_parragraph( $dom, $needs_closing_parragraph,
114             $found_inline_element, $options );
115 3         10 push @$dom, $self->_open_html_element('i');
116             push @$dom,
117             @{
118 3         8 $self->_parse_output(
119 3         19 $element->{output}, $template_callback,
120             { %$options, inside_inline_element => 1 }
121             )
122             };
123 3         16 push @$dom, $self->_close_html_element('i');
124 3         7 $needs_next = 1;
125             }
126 47         98 return ( $needs_next, $needs_closing_parragraph, $found_inline_element );
127             }
128              
129             sub _parse_output_try_parse_bold_and_italic {
130 48 50   48   126 if ( @_ < 7 ) {
131 0         0 die 'Incorrect arguments _parse_output_try_parse_bold_and_italic';
132             }
133 48         105 my ( $self, $dom, $element, $found_inline_element,
134             $needs_closing_parragraph, $template_callback, $options )
135             = @_;
136 48         101 my $needs_next;
137 48 100       132 if ( $element->{type} eq 'bold_and_italic' ) {
138 1         2 $found_inline_element = 1;
139 1         4 ($needs_closing_parragraph) =
140             $self->_open_parragraph( $dom, $needs_closing_parragraph,
141             $found_inline_element, $options );
142 1         4 push @$dom, $self->_open_html_element('b');
143 1         4 push @$dom, $self->_open_html_element('i');
144             push @$dom,
145             @{
146 1         2 $self->_parse_output(
147 1         8 $element->{output}, $template_callback,
148             { %$options, inside_inline_element => 1 }
149             )
150             };
151 1         5 push @$dom, $self->_close_html_element('i');
152 1         3 push @$dom, $self->_close_html_element('b');
153 1         3 $needs_next = 1;
154             }
155 48         100 return ( $needs_next, $needs_closing_parragraph, $found_inline_element );
156             }
157              
158             sub _parse_output_try_parse_bold {
159 53 50   53   116 if ( @_ < 7 ) {
160 0         0 die 'Incorrect arguments _parse_output_try_parse_bold';
161             }
162 53         165 my ( $self, $dom, $element, $found_inline_element,
163             $needs_closing_parragraph, $template_callback, $options )
164             = @_;
165 53         61 my $needs_next;
166 53 100       143 if ( $element->{type} eq 'bold' ) {
167 5         9 $found_inline_element = 1;
168 5         15 ($needs_closing_parragraph) =
169             $self->_open_parragraph( $dom, $needs_closing_parragraph,
170             $found_inline_element, $options );
171 5         13 push @$dom, $self->_open_html_element('b');
172             push @$dom,
173             @{
174 5         11 $self->_parse_output(
175 5         58 $element->{output}, $template_callback,
176             { %$options, inside_inline_element => 1 }
177             )
178             };
179 5         22 push @$dom, $self->_close_html_element('b');
180 5         10 $needs_next = 1;
181             }
182              
183 53         133 return ( $needs_next, $needs_closing_parragraph, $found_inline_element );
184             }
185              
186             sub _parse_output_try_parse_link {
187 15 50   15   33 if ( @_ < 6 ) {
188 0         0 die 'Incorrect arguments';
189             }
190 15         39 my ( $self, $dom, $element, $needs_closing_parragraph,
191             $found_inline_element, $options )
192             = @_;
193 15         16 my $needs_next;
194 15 100       67 if ( $element->{type} eq 'link' ) {
195 3         5 $found_inline_element = 1;
196 3         49 ($needs_closing_parragraph) =
197             $self->_open_parragraph( $dom, $needs_closing_parragraph,
198             $found_inline_element, $options );
199 3         8 my $real_link = $element->{link};
200 3 50 33     41 if ( $real_link !~ /^\w:/ && $real_link !~ m@^(?:/|\w+\.)@ ) {
201              
202             # TODO: Allow setting a base URL.
203 3         7 $real_link = '/' . $real_link;
204             }
205 3         22 push @$dom,
206             $self->_open_html_element( 'a', 0,
207             { href => $real_link =~ s/ /%20/gr } );
208             push @$dom,
209 3         11 $self->_html_string_content_to_pushable( $element->{title} );
210 3         17 push @$dom, $self->_close_html_element('a');
211 3         5 $needs_next = 1;
212             }
213 15         37 return ( $needs_next, $needs_closing_parragraph, $found_inline_element );
214             }
215              
216             sub _parse_output_try_parse_image {
217 12 50   12   26 if ( @_ < 6 ) {
218 0         0 die 'Incorrect arguments';
219             }
220 12         28 my ( $self, $dom, $element, $needs_closing_parragraph,
221             $found_inline_element, $options )
222             = @_;
223 12         18 my $needs_next;
224 12 100       37 if ( $element->{type} eq 'image' ) {
225             {
226 8         13 $needs_next = 1;
  8         13  
227 8         18 my $format = $element->{options}{format};
228 8   100     33 $format //= {};
229 8   100     42 my $is_inline = !$format->{thumb} && !$format->{frame};
230 8 100       19 if ($is_inline) {
231 3         21 $found_inline_element = 1;
232 3         9 ($needs_closing_parragraph) =
233             $self->_open_parragraph( $dom, $needs_closing_parragraph,
234             $found_inline_element, $options );
235             }
236             else {
237 5 50       16 if ( $options->{inside_inline_element} ) {
238             say STDERR
239             'Image found when the content is expected to be inline WIKI_LINE: '
240 0         0 . $element->{start_line};
241             }
242 5         16 ($needs_closing_parragraph) =
243             $self->_close_parragraph( $dom, $needs_closing_parragraph,
244             $options );
245             }
246 8         57 my $link_url = Mojo::URL->new( $element->{link} );
247 8         2492 my $is_video = $link_url->path =~ /\.(?:mp4|webm|ogg|3gp|mpeg)/;
248 8         1432 my $is_pdf = $link_url->path =~ /\.(?:pdf)/;
249              
250             my $pdf_element_attrs = sub {
251 2     2   5 my $page;
252             my $fragment;
253 2 100       10 if ( defined $element->{options}{page} ) {
254 1         3 $page = $element->{options}{page};
255 1         3 $fragment = "page=@{[0+$page]}";
  1         7  
256             }
257 2 100       8 if ( defined $fragment ) {
258 1         56 say $fragment;
259 1         11 $link_url->fragment($fragment);
260             }
261 2         22 return { src => "$link_url", };
262 8         963 };
263 8 100       29 if ($is_inline) {
264 3 50       10 if ($is_pdf) {
265 0         0 push @$dom,
266             $self->_open_html_element( 'iframe', 0,
267             $pdf_element_attrs->(), );
268 0         0 push @$dom, $self->_close_html_element('iframe');
269 0         0 next;
270             }
271 3 100       7 if ($is_video) {
272 1         9 push @$dom,
273             $self->_open_html_element(
274             'video', 1,
275             {
276             src => "" . $link_url,
277             }
278             );
279 1         18 next;
280             }
281 2   66     12 my $alt = $element->{options}{alt} // $element->{caption};
282 2 50       10 push @$dom,
283             $self->_open_html_element(
284             'img', 1,
285             {
286             src => "$link_url",
287             ( ( defined $alt ) ? ( alt => $alt ) : () )
288             }
289             );
290 2         31 next;
291             }
292 5         10 my $typeof = 'mw:File/Frame';
293 5 100       19 if ( $format->{thumb} ) {
294 1         2 $typeof = 'mw:File/Thumb';
295             }
296 5         27 push @$dom,
297             $self->_open_html_element( 'figure', 0, { typeof => $typeof } );
298              
299 5         15 my $alt = $element->{options}{alt};
300             {
301 5 100       10 if ($is_pdf) {
  5         16  
302 2         9 push @$dom,
303             $self->_open_html_element( 'iframe', 0,
304             $pdf_element_attrs->(), );
305 2         10 push @$dom, $self->_close_html_element('iframe');
306 2         6 next;
307             }
308 3 100       9 if ($is_video) {
309 1         6 push @$dom,
310             $self->_open_html_element(
311             'video', 1,
312             {
313             src => $link_url . ''
314             }
315             );
316 1         3 next;
317             }
318 2 100       11 push @$dom,
319             $self->_open_html_element(
320             'img', 1,
321             {
322             src => $link_url . '',
323             (
324             ( defined $alt )
325             ? ( alt => $alt )
326             : ()
327             )
328             }
329             );
330             }
331 5 50       16 if ( defined $element->{caption} ) {
332 5         14 push @$dom, $self->_open_html_element('figcaption');
333 5         16 push @$dom, $element->{caption};
334 5         11 push @$dom, $self->_close_html_element('figcaption');
335             }
336 5         13 push @$dom, $self->_close_html_element('figure');
337             }
338             }
339 12         34 return ( $needs_next, $needs_closing_parragraph, $found_inline_element );
340             }
341              
342             sub _parse_output_try_parse_template {
343 4     4   6 my ( $self, $dom, $element, $needs_closing_parragraph,
344             $found_inline_element, $template_callbacks, $options )
345             = @_;
346 4         4 my $needs_next;
347 4 50       7 if ( $element->{type} eq 'template' ) {
348 4         3 my $template = $element;
349 4         13 my $is_inline = $template_callbacks->{is_inline}->($template);
350 4 50 33     12 if ( $options->{inside_inline_element} && !$is_inline ) {
351             say STDERR
352             'No-inline (block) template found inside inline element WIKI_LINE: '
353 0         0 . $element->{start_line};
354             }
355 4 100       11 if ($is_inline) {
356 3         3 $found_inline_element = 1;
357 3         8 ($needs_closing_parragraph) =
358             $self->_open_parragraph( $dom, $needs_closing_parragraph,
359             $found_inline_element, $options );
360             }
361             else {
362 1         6 ($needs_closing_parragraph) =
363             $self->_close_parragraph( $dom, $needs_closing_parragraph,
364             $options );
365             }
366             my $parse_sub = sub {
367 0     0   0 my ( $wiki_text, $options ) = @_;
368 0         0 return $self->pre_html_json( $wiki_text, $template_callbacks,
369             $options );
370 4         16 };
371             my $open_html_element_sub = sub {
372 4     4   40 my ( $tag, $self_closing, $attrs ) = @_;
373 4 50       7 if ( !defined $tag ) {
374 0         0 die 'Tag is not optional';
375             }
376 4   50     5 $self_closing //= 0;
377 4   50     4 $attrs //= {};
378 4         6 return $self->_open_html_element( $tag, $self_closing, $attrs );
379 4         11 };
380             my $close_html_element_sub = sub {
381 4     4   20 my ($tag) = @_;
382 4 50       6 if ( !defined $tag ) {
383 0         0 die 'Tag is not optional';
384             }
385 4         14 return $self->_close_html_element($tag);
386 4         7 };
387 4         9 my $new_elements = $template_callbacks->{generate_elements}->(
388             $element, $options, $parse_sub, $open_html_element_sub,
389             $close_html_element_sub
390             );
391 4 50       13 if ( defined $new_elements ) {
392             {
393 4 50       3 if ( 'ARRAY' ne ref $new_elements ) {
  4         8  
394 0         0 warn
395             'Return from generate_elements is not an ArrayRef, user error';
396 0         0 next;
397             }
398 4         30 push @$dom, @$new_elements;
399             }
400             }
401             }
402 4         8 return ( $needs_next, $needs_closing_parragraph, $found_inline_element );
403             }
404              
405             sub _parse_output_try_parse_unordered_list {
406 16 50   16   43 if ( @_ < 6 ) {
407 0         0 die 'Incorrect number of parameters';
408             }
409 16         40 my ( $self, $dom, $element, $needs_closing_parragraph, $template_callback,
410             $options )
411             = @_;
412 16         24 my $needs_next;
413 16 100       74 if ( $element->{type} eq 'unordered_list' ) {
414 1 50       4 if ( $options->{inside_inline_element} ) {
415             say STDERR
416             'unordered list found when content is expected to be inline WIKI_LINE: ',
417 1         129 $element->{start_line};
418             }
419 1         8 ($needs_closing_parragraph) =
420             $self->_close_parragraph( $dom, $needs_closing_parragraph, $options );
421 1         3 my $elements = $element->{output};
422 1         4 push @$dom, $self->_open_html_element('ul');
423 1         4 for my $element (@$elements) {
424 4 50       12 if ( 'HASH' ne ref $element ) {
425 0         0 die 'List element is text and not hash';
426             }
427 4 50       11 if ( $element->{type} ne 'list_element' ) {
428 0         0 die 'List element is not a list_element';
429             }
430 4         10 push @$dom, $self->_open_html_element('li');
431             push @$dom,
432             @{
433 4         7 $self->_parse_output(
434 4         25 $element->{output}, $template_callback,
435             { %$options, is_list_element => 1 }
436             )
437             };
438 4         18 push @$dom, $self->_close_html_element('li');
439             }
440 1         4 push @$dom, $self->_close_html_element('ul');
441 1         3 $needs_next = 1;
442             }
443 16         43 return ( $needs_next, $needs_closing_parragraph );
444             }
445              
446             sub _parse_output_try_parse_hx {
447 44 50   44   91 if ( @_ < 6 ) {
448 0         0 die 'Incorrect arguments to _parse_output_try_parse_hx';
449             }
450 44         159 my ( $self, $dom, $element, $needs_closing_parragraph, $template_callback,
451             $options )
452             = @_;
453 44         75 my $needs_next;
454 44 100       183 if ( $element->{type} eq 'hx' ) {
455 28 50       80 if ( $options->{inside_inline_element} ) {
456             say STDERR
457             'HX found when the content is expected to be inline WIKI_LINE: '
458 0         0 . $element->{start_line};
459             }
460 28         74 ($needs_closing_parragraph) =
461             $self->_close_parragraph( $dom, $needs_closing_parragraph, $options );
462 28         48 my $hx_level = $element->{hx_level};
463              
464 28         207 push @$dom, $self->_open_html_element( xml_escape "h$hx_level" );
465             push @$dom,
466             @{
467 28         58 $self->_parse_output(
468 28         149 $element->{output}, $template_callback,
469             { %$options, inside_inline_element => 1 }
470             )
471             };
472 28         198 push @$dom, $self->_close_html_element( xml_escape "h$hx_level" );
473 28         61 $needs_next = 1;
474             }
475 44         100 return ( $needs_next, $needs_closing_parragraph );
476             }
477              
478             sub _parse_output {
479 93 50   93   249 if ( @_ < 3 ) {
480 0         0 die '_parse_output needs at least $self and $output';
481             }
482 93         215 my ( $self, $output, $template_callback, $options ) = @_;
483 93   50     208 $options //= {};
484 93         124 my @dom;
485 93         139 my $needs_closing_parragraph = 0;
486 93         117 my $first = 1;
487 93         165 my $last_element_inline_element;
488             my $last_element_text;
489              
490 93         283 for my $element (@$output) {
491 167         240 my $found_inline_element;
492             my $found_text;
493             {
494 167         195 my ($needs_next);
  167         338  
495 167         303 $options->{first} = $first;
496 167         275 $options->{last_element_text} = $last_element_text;
497 167         390 ( $needs_next, $needs_closing_parragraph, $found_text ) =
498             $self->_parse_output_try_parse_plain_text( \@dom, $element,
499             $last_element_inline_element, $needs_closing_parragraph,
500             $options );
501 167 100       388 next if $needs_next;
502              
503 53         202 ( $needs_next, $needs_closing_parragraph, $found_inline_element ) =
504             $self->_parse_output_try_parse_bold( \@dom, $element,
505             $found_inline_element, $needs_closing_parragraph,
506             $template_callback, $options );
507 53 100       149 next if $needs_next;
508 48         153 ( $needs_next, $needs_closing_parragraph, $found_inline_element ) =
509             $self->_parse_output_try_parse_bold_and_italic( \@dom, $element,
510             $found_inline_element, $needs_closing_parragraph,
511             $template_callback, $options );
512 48 100       96 next if $needs_next;
513 47         127 ( $needs_next, $needs_closing_parragraph, $found_inline_element ) =
514             $self->_parse_output_try_parse_italic( \@dom, $element,
515             $found_inline_element, $needs_closing_parragraph,
516             $template_callback, $options );
517 47 100       129 next if $needs_next;
518 44         132 ( $needs_next, $needs_closing_parragraph ) =
519             $self->_parse_output_try_parse_hx( \@dom, $element,
520             $needs_closing_parragraph, $template_callback, $options );
521 44 100       89 next if $needs_next;
522              
523 16         79 ( $needs_next, $needs_closing_parragraph ) =
524             $self->_parse_output_try_parse_unordered_list( \@dom, $element,
525             $needs_closing_parragraph, $template_callback, $options );
526 16 100       40 next if $needs_next;
527 15         43 ( $needs_next, $needs_closing_parragraph, $found_inline_element ) =
528             $self->_parse_output_try_parse_link( \@dom, $element,
529             $needs_closing_parragraph, $found_inline_element, $options );
530 15 100       36 next if $needs_next;
531 12         30 ( $needs_next, $needs_closing_parragraph, $found_inline_element ) =
532             $self->_parse_output_try_parse_image( \@dom, $element,
533             $needs_closing_parragraph, $found_inline_element, $options );
534 12 100       35 next if $needs_next;
535 4         8 ( $needs_next, $needs_closing_parragraph, $found_inline_element ) =
536             $self->_parse_output_try_parse_template( \@dom, $element,
537             $needs_closing_parragraph, $found_inline_element,
538             $template_callback, $options );
539 4 50       7 next if $needs_next;
540             }
541 167         260 $first = 0;
542 167         274 $last_element_inline_element = !!$found_inline_element;
543 167         307 $last_element_text = !!$found_text;
544             }
545 93         205 ($needs_closing_parragraph) =
546             $self->_close_parragraph( \@dom, $needs_closing_parragraph, $options );
547 93         460 return \@dom;
548             }
549              
550             sub _open_parragraph {
551 124 50   124   240 if ( @_ < 5 ) {
552 0         0 die 'Incorrect arguments';
553             }
554 124         227 my ( $self, $dom, $needs_closing_parragraph, $found_inline_element,
555             $options )
556             = @_;
557 124 100 100     479 if ( $options->{is_list_element} || $options->{inside_inline_element} ) {
558 45 100 100     119 if ( !$options->{first} && !$found_inline_element ) {
559 2         6 push @$dom, $self->_open_html_element( 'br', 1 );
560             }
561 45         107 return ($needs_closing_parragraph);
562             }
563 79 100       221 if ( !$needs_closing_parragraph ) {
564 70         156 push @$dom, $self->_open_html_element('p');
565 70         106 $needs_closing_parragraph = 1;
566             }
567 79         128 return ($needs_closing_parragraph);
568             }
569              
570             sub _close_parragraph {
571 234     234   405 my ( $self, $dom, $needs_closing_parragraph, $options ) = @_;
572 234 100       409 if ($needs_closing_parragraph) {
573 70         140 push @$dom, $self->_close_html_element('p');
574 70         94 $needs_closing_parragraph = 0;
575             }
576 234         409 return ($needs_closing_parragraph);
577             }
578             1