File Coverage

lib/Pod/PseudoPod/DOM/Role/HTML.pm
Criterion Covered Total %
statement 259 271 95.5
branch 51 68 75.0
condition 11 16 68.7
subroutine 69 71 97.1
pod 0 53 0.0
total 390 479 81.4


line stmt bran cond sub pod time code
1             package Pod::PseudoPod::DOM::Role::HTML;
2             # ABSTRACT: an HTML formatter role for PseudoPod DOM trees
3              
4 16     16   12611 use strict;
  16         40  
  16         538  
5 16     16   94 use warnings;
  16         38  
  16         466  
6              
7 16     16   92 use Moose::Role;
  16         39  
  16         184  
8              
9 16     16   108332 use HTML::Entities;
  16         85679  
  16         1588  
10 16     16   161 use Scalar::Util 'blessed';
  16         42  
  16         902  
11 16     16   725 use MIME::Base64 'encode_base64url';
  16         940  
  16         30203  
12              
13             requires 'type';
14             has 'add_body_tags', is => 'ro', default => 0;
15             has 'emit_environments', is => 'ro', default => sub { {} };
16             has 'anchors', is => 'rw', default => sub { {} };
17              
18             sub get_anchor
19             {
20 425     425 0 781 my $self = shift;
21 425         1074 my $anchor = $self->emit_kids( encode => 'index_anchor' );
22 425         1645 return encode_base64url( $anchor );
23             }
24              
25             sub get_link_for_anchor
26             {
27 53     53 0 136 my ($self, $anchor) = @_;
28 53         1856 my $anchors = $self->anchors;
29              
30 53 50       211 return unless my $heading = $anchors->{$anchor};
31 53         1848 my $filename = $heading->link;
32 53         159 my $target = $heading->get_anchor;
33 53         715 my $title = $heading->get_link_text;
34              
35 53         215 return $filename, $target, $title;
36             }
37              
38             sub resolve_anchors
39             {
40 14     14 0 117 my $self = shift;
41 14         557 my $anchors = $self->anchors;
42              
43 14         55 for my $anchor (@{ $self->anchor })
  14         504  
44             {
45 53         165 my $a = $anchor->emit_kids;
46 53         244 $anchors->{$anchor->emit_kids} = $anchor;
47             }
48             }
49              
50             sub get_index_entries
51             {
52 20     20 0 224 my ($self, $seen) = @_;
53 20   50     166 $seen ||= {};
54              
55 20         45 my @entries;
56              
57 20         64 for my $entry (@{ $self->index })
  20         827  
58             {
59 199         583 my $text = $entry->emit_kids( encode => 'index_anchor' );
60 199         7507 $entry->id( ++$seen->{ $text } );
61 199         589 push @entries, $entry;
62             }
63              
64 20         160 return @entries;
65             }
66              
67 50     50 0 574 sub accept_targets { qw( html HTML xhtml XHTML ) }
68       0 0   sub encode_E_contents {}
69              
70             my %characters = (
71             acute => sub { '&' . shift . 'acute;' },
72             grave => sub { '&' . shift . 'grave;' },
73             uml => sub { '&' . shift . 'uml;' },
74             cedilla => sub { '&' . shift . 'cedil;' },
75             opy => sub { '©' },
76             dash => sub { '—' },
77             lusmn => sub { '±' },
78             mp => sub { '&' },
79             rademark => sub { '™' },
80             );
81              
82             sub emit_character
83             {
84 143     143 0 372 my ($self, %args) = @_;
85 143         294 my $content = eval { $self->emit_kids };
  143         449  
86              
87 143 50       410 return '' unless defined $content;
88              
89 143 50       806 if (my ($char, $class) = $content =~ /(\w)(\w+)/)
90             {
91 143 100       647 return $characters{$class}->($char) if exists $characters{$class};
92             }
93              
94 52   50     177 $args{encode} ||= '';
95 52         240 my $char = Pod::Escapes::e2char( $content );
96 52 100       1198 return $char if $args{encode} =~ /^(index_|id$)/;
97              
98 26         116 return $self->handle_encoding( $char );
99             }
100              
101             sub emit
102             {
103 5301     5301 0 8849 my $self = shift;
104 5301         162664 my $type = $self->type;
105 5301         11267 my $emit = 'emit_' . $type;
106              
107 5301         16076 $self->$emit( @_ );
108             }
109              
110             sub emit_document
111             {
112 41     41 0 110 my $self = shift;
113              
114 41 100       2020 return $self->emit_body if $self->add_body_tags;
115 40         229 return $self->emit_kids( @_ );
116             }
117              
118             sub extract_headings
119             {
120 1     1 0 4 my ($self, %args) = @_;
121 1         3 my @headings;
122              
123 1         2 for my $kid (@{ $self->children })
  1         41  
124             {
125 47 100       1402 next unless $kid->type eq 'header';
126 8 100       36 next if $kid->exclude_from_toc( $args{max_depth} );
127 5         18 push @headings, $kid;
128             }
129              
130 1         5 return \@headings;
131             }
132              
133             sub emit_toc
134             {
135 1     1 0 163 my $self = shift;
136 1         5 my $headings = $self->extract_headings;
137              
138 1         40 return $self->walk_headings( $headings, filename => $self->filename );
139             }
140              
141             sub walk_headings
142             {
143 1     1 0 6 my ($self, $headings, %args) = @_;
144 1   50     10 $args{indent} ||= '';
145              
146 1         4 my $toc = '';
147              
148 1         3 for my $heading (@$headings)
149             {
150 5         10 $toc .= $args{indent};
151              
152 5 50       25 if (blessed($heading))
153             {
154 5         18 $toc .= '<li>' . $heading->get_heading_link( %args );
155             }
156             else
157             {
158 0         0 my $indent = $args{indent} . ' ';
159             $toc .= qq|\n$args{indent}|
160             . $args{indent} . qq|<ul>\n|
161             . $self->walk_headings( $heading, %args, indent => $indent )
162 0         0 . $args{indent} . qq|</ul>\n|;
163              
164             }
165              
166 5         17 $toc .= qq|</li>\n|;
167             }
168              
169 1         10 return $toc . qq|\n|;
170             }
171              
172             sub get_heading_link
173             {
174 5     5 0 17 my ($self, %args) = @_;
175              
176 5         12 my $content = $self->emit_kids;
177 5   50     180 my $filename = $self->filename || '';
178 5         14 my $frag = $self->get_anchor;
179              
180 5         60 $content =~ s/^\*//;
181 5         31 return qq|<a href="$filename#$frag">$content</a>|;
182             }
183              
184             sub emit_body
185             {
186 1     1 0 2 my $self = shift;
187 1         6 return <<END_HTML_HEAD . $self->emit_kids( @_ ) . <<END_HTML;
188             <html>
189             <head>
190             <link rel="stylesheet" href="../css/style.css" type="text/css" />
191             </head>
192             <body>
193              
194             END_HTML_HEAD
195             </body>
196             </html>
197             END_HTML
198             }
199              
200             sub emit_kids
201             {
202 2258     2258 0 3835 my $self = shift;
203 2258         3578 join '', map { $_->emit( @_ ) } @{ $self->children };
  3570         26233  
  2258         73978  
204             }
205              
206             sub emit_header
207             {
208 114     114 0 295 my $self = shift;
209 114         327 my $content = $self->emit_kids( @_ );
210 114         4034 my $id_node = $self->anchor;
211 114 100       506 my $id = $id_node ? $id_node->get_anchor : $self->get_anchor;
212 114         1528 my $no_toc = $content =~ s/^\*//;
213 114         3980 my $level = 'h' . ($self->level + 1);
214 114 100       461 my $anchor = $id_node ? $self->emit_index( @_ ) : '';
215              
216 114         659 return qq|<$level id="$id">$anchor$content</$level>\n\n|;
217             }
218              
219             sub emit_plaintext
220             {
221 3140     3140 0 7203 my ($self, %args) = @_;
222 3140         103722 my $content = $self->content;
223 3140 50       7703 $content = '' unless defined $content;
224 3140         8465 $self->handle_encoding( $content, %args );
225             }
226              
227             sub handle_encoding
228             {
229 3166     3166 0 7141 my ($self, $content, %args) = @_;
230              
231 3166 100       7329 if (my $encode = $args{encode})
232             {
233 1212         2465 my $method = 'encode_' . $encode;
234 1212         3904 return $self->$method( $content, %args );
235             }
236              
237 1954         4516 return $self->encode_text( $content, %args );
238             }
239              
240 26     26 0 186 sub encode_none { $_[1] }
241              
242             sub encode_split
243             {
244 39     39 0 166 my ($self, $content, %args) = @_;
245 39         99 my $target = $args{target};
246             return join $args{joiner},
247 39         746 map { $self->encode_text( $_ ) } split /\s*\Q$target\E\s*/, $content;
  78         194  
248             }
249              
250             sub encode_text
251             {
252 2032     2032 0 3907 my ($self, $text) = @_;
253              
254 16     16   195 use Carp;
  16         57  
  16         21144  
255 2032 50       4243 unless (defined $text)
256             {
257 0         0 confess 'no text';
258             }
259 2032         4979 $text = encode_entities($text);
260 2032         30434 $text =~ s/\s*---\s*/&#8213;/g;
261 2032         3516 $text =~ s/\s*--\s*/&mdash;/g;
262              
263 2032         9725 return $text;
264             }
265              
266             sub encode_id
267             {
268 0     0 0 0 my ($self, $text) = @_;
269 0         0 $text =~ s/<.+?>//g;
270 0         0 $text =~ s/\W//g;
271 0         0 return lc $text;
272             }
273              
274             sub encode_index_anchor
275             {
276 813     813 0 1710 my ($self, $text) = @_;
277              
278 813         1627 $text =~ s/^\*//;
279 813         3086 $text =~ s/[\s"]//g;
280              
281 813         4612 return $text;
282             }
283              
284             sub encode_index_key
285             {
286 17     17 0 35 my ($self, $text) = @_;
287 17         88 $text =~ s/^\s+|\s+$//g;
288 17         190 return $text;
289             }
290              
291             sub encode_verbatim_text
292             {
293 317     317 0 707 my ($self, $text) = @_;
294 317         850 return encode_entities( $text );
295             }
296              
297             sub emit_literal
298             {
299 13     13 0 38 my $self = shift;
300 13         32 my @kids;
301              
302 13 50       424 if (my $title = $self->title)
303             {
304 13         78 my $target = $title->emit_kids( encode => 'none' );
305             @kids = map
306             {
307 13         83 $_->emit_kids(
308             encode => 'split', target => $target, joiner => "</p>\n\n<p>",
309             )
310 13         40 } @{ $self->children };
  13         467  
311             }
312             else
313             {
314 0         0 @kids = map { $_->emit_kids( @_ ) } @{ $self->children };
  0         0  
  0         0  
315             }
316              
317 13         120 return qq|<div class="literal"><p>|
318             . join( "\n", @kids )
319             . qq|</p></div>\n\n|;
320             }
321              
322             sub emit_anchor
323             {
324 1     1 0 3 my $self = shift;
325 1         5 return qq|<a name="| . $self->get_anchor . qq|"></a>|;
326             }
327              
328             sub emit_number_item
329             {
330 41     41 0 93 my $self = shift;
331 41         1409 my $marker = $self->marker;
332 41 50       188 my $number = $marker ? qq| number="$marker"| : '';
333 41         161 return "<li$number>" . $self->emit_kids . "</li>\n\n";
334             }
335              
336             sub emit_text_item
337             {
338 145     145 0 316 my $self = shift;
339 145         4778 my $kids = $self->children;
340 145 50       392 return "<li></li>\n\n" unless @$kids;
341              
342 145         347 my $first = shift @$kids;
343 145 100       417 return '<li>' . $first->emit( @_ ) . qq|</li>\n\n| unless @$kids;
344              
345             return "<li><p>" . $first->emit . "</p>\n\n"
346 106         299 . join( '', map { $_->emit } @$kids ) . "</li>\n\n";
  106         323  
347             }
348              
349             sub emit_verbatim
350             {
351 42     42 0 94 my $self = shift;
352 42         140 return "<pre><code>" . $self->emit_kids( encode => 'verbatim_text', @_ )
353             . "</code></pre>\n\n";
354             }
355              
356 150     150 0 470 sub emit_italics { shift->emit_tagged_kids( 'em', @_ ) }
357 211     211 0 655 sub emit_code { shift->emit_tagged_kids( 'code', @_ ) }
358 30     30 0 139 sub emit_bold { shift->emit_tagged_kids( 'strong', @_ ) }
359 14     14 0 266 sub emit_superscript { shift->emit_tagged_kids( 'sup', @_ ) }
360 14     14 0 85 sub emit_subscript { shift->emit_tagged_kids( 'sub', @_ ) }
361 40     40 0 187 sub emit_file { shift->emit_tagged_kids( 'em', @_ ) }
362              
363             sub emit_tagged_kids
364             {
365 459     459 0 1205 my ($self, $tag, %args) = @_;
366 459         1558 my $kids = $self->emit_kids( encode => 'verbatim_text', %args );
367 459   100     5828 $args{encode} ||= '';
368              
369 459 100       1809 return $kids if $args{encode} =~ /^(index_|id$)/;
370 273         1197 return qq|<$tag>$kids</$tag>|;
371             }
372              
373             sub emit_footnote
374             {
375 14     14 0 65 my $self = shift;
376 14         78 return ' <span class="footnote">' . $self->emit_kids . '</span>';
377             }
378              
379             sub emit_url
380             {
381 14     14 0 44 my $self = shift;
382 14         64 my $url = $self->emit_kids;
383 14         107 return qq|<a class="url" href="$url">$url</a>|;
384             }
385              
386             sub emit_link
387             {
388 53     53 0 121 my $self = shift;
389 53         160 my $anchor = $self->emit_kids;
390              
391 53         228 my ($file, $frag, $text) = $self->get_link_for_anchor( $anchor );
392 53         284 return qq|<a href="$file#$frag">$text</a>|;
393             }
394              
395 16     16   170 use constant { BEFORE => 0, AFTER => 1 };
  16         66  
  16         5115  
396              
397             my %block_items =
398             (
399             programlisting => [ qq|<div class="programlisting">\n\n|, q|</div>| ],
400             sidebar => [ qq|<div class="sidebar">\n\n|, q|</div>| ],
401             epigraph => [ qq|<div class="epigraph">\n\n|, q|</div>| ],
402             blockquote => [ qq|<div class="blockquote">\n\n|, q|</div>| ],
403             );
404              
405             while (my ($tag, $values) = each %block_items)
406             {
407             my $sub = sub
408             {
409 56     56   132 my $self = shift;
        56      
        56      
        56      
410 56         1850 my $title = $self->title;
411 56         1925 my $env = $self->emit_environments;
412              
413             return $self->make_basic_block( $env->{$tag}, $title, @_ )
414 56 100       211 if exists $env->{$tag};
415              
416             # deal with title somehow
417 54         254 return $values->[BEFORE]
418             . $self->make_block_title( $title )
419             . $self->emit_kids . $values->[AFTER]
420             . "\n\n";
421             };
422              
423 16     16   139 do { no strict 'refs'; *{ 'emit_' . $tag } = $sub };
  16         38  
  16         8555  
424             }
425              
426             my %invisibles = map { $_ => 1 } qw( index anchor );
427              
428             sub emit_paragraph
429             {
430 481     481 0 889 my $self = shift;
431 481         788 my @kids = @{ $self->children };
  481         15215  
432 481         1061 my $has_visible_text = grep { ! exists $invisibles{ $_->type } } @kids;
  1465         43026  
433 481 50       1204 return $self->emit_kids( @_ ) unless $has_visible_text;
434              
435 481 100 66     14699 my $attrs = @kids && $kids[0]->type =~ /^(?:anchor|index)$/
436             ? $self->get_anchored_paragraph_attrs( shift @kids )
437             : '';
438              
439             # inlined emit_kids() here to reflect any anchor manipulation
440 481         1219 my $content = join '', map { $_->emit( @_ ) } @kids;
  1439         3641  
441 481         3110 return "<p$attrs>" . $content . qq|</p>\n\n|;
442             }
443              
444             sub get_anchored_paragraph_attrs
445             {
446 26     26 0 104 my ($self, $tag) = @_;
447 26         818 my $type = $tag->type;
448              
449 26 50       221 if ($type eq 'anchor')
    50          
450             {
451 0         0 my $content = $tag->get_anchor;
452 0         0 return qq| id="$content"|;
453             }
454             elsif ($type eq 'index')
455             {
456 26         167 my $content = $tag->get_anchor . $tag->id;
457 26         117 return qq| id="$content"|;
458             }
459             }
460              
461             my %parent_items =
462             (
463             text_list => [ qq|<ul>\n\n|, q|</ul>| ],
464             bullet_list => [ qq|<ul>\n\n|, q|</ul>| ],
465             bullet_item => [ qq|<li>|, q|</li>| ],
466             number_list => [ qq|<ol>\n\n|, q|</ol>| ],
467             );
468              
469             while (my ($tag, $values) = each %parent_items)
470             {
471             my $sub = sub
472             {
473 230     230   459 my $self = shift;
        230      
        230      
        230      
474 230         701 return $values->[BEFORE] . $self->emit_kids( @_ ) . $values->[AFTER]
475             . "\n\n";
476             };
477              
478 16     16   152 do { no strict 'refs'; *{ 'emit_' . $tag } = $sub };
  16         46  
  16         14329  
479             }
480              
481             sub emit_block
482             {
483 83     83 0 230 my $self = shift;
484 83 100       2753 my $title = $self->title ? $self->title->emit_kids : '';
485 83         2868 my $target = $self->target;
486              
487 83 100       2897 if (my $environment = $self->emit_environments->{$target})
    100          
488             {
489 2         6 $target = $environment;
490             }
491             elsif (my $meth = $self->can( 'emit_' . $target))
492             {
493 55         250 return $self->$meth( @_ );
494             }
495              
496 28         1096 return $self->make_basic_block( $self->target, $title, @_ );
497             }
498              
499             sub emit_html
500             {
501 13     13 0 47 my $self = shift;
502 13         61 return $self->emit_kids( encode => 'none' );
503             }
504              
505             sub make_basic_block
506             {
507 30     30 0 464 my ($self, $target, $title, @rest) = @_;
508              
509 30         111 $title = $self->make_block_title( $title );
510              
511 30         160 return qq|<div class="$target">\n$title|
512             . $self->emit_kids( @rest )
513             . qq|</div>|;
514             }
515              
516             sub make_block_title
517             {
518 84     84 0 235 my ($self, $title) = @_;
519              
520 84 100 100     686 return '' unless defined $title and length $title;
521 28         164 return qq|<p class="title">$title</p>\n|;
522             }
523              
524             sub emit_index
525             {
526 196     196 0 356 my $self = shift;
527 196         542 my $content = $self->get_anchor;
528 196 100       8423 $content .= $self->id if $self->type eq 'index';
529              
530 196         750 return qq|<a name="$content"></a>|;
531             }
532              
533             sub emit_index_link
534             {
535 17     17 0 34 my $self = shift;
536 17         529 my $id = $self->id;
537 17         47 my $frag = $self->get_anchor . $id;
538 17         801 my $file = $self->link;
539 17         160 return qq|<a href="$file#$frag">$id</a>|;
540             }
541              
542             sub emit_table
543             {
544 13     13 0 49 my $self = shift;
545 13 50       515 my $title = $self->title ? $self->title->emit_kids : '';
546              
547 13         61 my $content = qq|<table>\n|;
548 13 50       109 $content .= qq|<caption>$title</caption>\n| if $title;
549 13         69 $content .= $self->emit_kids;
550 13         60 $content .= qq|</table>\n\n|;
551              
552 13         1072 return $content;
553             }
554              
555             sub emit_headrow
556             {
557 13     13 0 47 my $self = shift;
558              
559             # kids should be cells
560 13         36 my $content = '<tr>';
561              
562 13         39 for my $kid (@{ $self->children })
  13         573  
563             {
564 26         109 $content .= '<th>' . $kid->emit_kids . '</th>';
565             }
566              
567 13         63 return $content . "</tr>\n";
568             }
569              
570             sub emit_row
571             {
572 26     26 0 1451 my $self = shift;
573              
574 26         161 return '<tr>' . $self->emit_kids . qq|</tr>\n|;
575             }
576              
577             sub emit_cell
578             {
579 52     52 0 190 my $self = shift;
580 52         126 return '<td>' . $self->emit_kids . qq|</td>\n|;
581             }
582              
583             sub emit_figure
584             {
585 13     13 0 39 my $self = shift;
586 13         556 my $caption = $self->caption;
587 13         118 my $anchor = $self->anchor;
588 13 50       86 my $id = defined $anchor ? ' id="' . $anchor->get_anchor . '"' : '';
589 13         345 my $file = $self->file->emit_kids;
590 13         57 my $content = qq|<p$id>|;
591              
592 13 50       85 $content .= $anchor if $anchor;
593 13         55 $content .= qq|<img src="$file" />|;
594 13 50       75 $content .= qq|<br />\n<em>$caption</em>| if $caption;
595 13         37 $content .= qq|</p>\n\n|;
596              
597 13         48 return $content;
598             }
599              
600             1;
601              
602             __END__
603              
604             =pod
605              
606             =encoding UTF-8
607              
608             =head1 NAME
609              
610             Pod::PseudoPod::DOM::Role::HTML - an HTML formatter role for PseudoPod DOM trees
611              
612             =head1 VERSION
613              
614             version 1.20210620.2004
615              
616             =head1 AUTHOR
617              
618             chromatic <chromatic@wgz.org>
619              
620             =head1 COPYRIGHT AND LICENSE
621              
622             This software is copyright (c) 2021 by chromatic.
623              
624             This is free software; you can redistribute it and/or modify it under
625             the same terms as the Perl 5 programming language system itself.
626              
627             =cut