File Coverage

lib/Pod/PseudoPod/DOM/Role/PML.pm
Criterion Covered Total %
statement 29 312 9.2
branch 0 90 0.0
condition 0 16 0.0
subroutine 10 71 14.0
pod 0 59 0.0
total 39 548 7.1


line stmt bran cond sub pod time code
1             package Pod::PseudoPod::DOM::Role::PML;
2             # ABSTRACT: an PML formatter role for PseudoPod DOM trees
3              
4 1     1   781 use strict;
  1         2  
  1         25  
5 1     1   5 use warnings;
  1         2  
  1         19  
6              
7 1     1   4 use Moose::Role;
  1         2  
  1         5  
8 1     1   4493 use 5.010;
  1         3  
9              
10 1     1   4 use HTML::Entities;
  1         1  
  1         60  
11 1     1   6 use Scalar::Util 'blessed';
  1         1  
  1         1822  
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 last_level
19             {
20 0     0 0   state $level = 0;
21 0 0         $level = $_[1] if @_ > 1;
22 0           return $level;
23             }
24              
25             sub get_anchor {
26 0     0 0   $_[0]->emit_kids( encode => 'index_anchor' );
27             }
28              
29             sub get_link_for_anchor
30             {
31 0     0 0   my ($self, $anchor) = @_;
32 0           my $anchors = $self->anchors;
33              
34 0 0         return unless my $heading = $anchors->{$anchor};
35 0           my $filename = $heading->link;
36 0           my $target = $heading->get_anchor;
37 0           my $title = $heading->get_link_text;
38              
39             # index_anchor encoding
40 0 0         $target = 'sec.' . $target unless $target =~ /^(sec|chp)\./;
41              
42 0           return $filename, $target, $title;
43             }
44              
45             sub resolve_anchors
46             {
47 0     0 0   my $self = shift;
48 0           my $anchors = $self->anchors;
49              
50 0           for my $anchor (@{ $self->anchor })
  0            
51             {
52 0           my $a = $anchor->emit_kids;
53 0           $anchors->{$anchor->emit_kids} = $anchor;
54             }
55             }
56              
57             sub get_index_entries
58             {
59 0     0 0   my ($self, $seen) = @_;
60 0   0       $seen ||= {};
61              
62 0           my @entries;
63              
64 0           for my $entry (@{ $self->index })
  0            
65             {
66 0           my $text = $entry->emit_kids( encode => 'index_anchor' );
67 0           $entry->id( ++$seen->{ $text } );
68 0           push @entries, $entry;
69             }
70              
71 0           return @entries;
72             }
73              
74 0     0 0   sub accept_targets { qw( pml PML ) }
75       0 0   sub encode_E_contents {}
76              
77             my %characters = (
78             acute => sub { '&' . shift . 'acute;' },
79             grave => sub { '&' . shift . 'grave;' },
80             uml => sub { '&' . shift . 'uml;' },
81             cedilla => sub { '&' . shift . 'cedil;' },
82             opy => sub { '©' },
83             dash => sub { '—' },
84             lusmn => sub { '±' },
85             mp => sub { '&' },
86             rademark => sub { '™' },
87             );
88              
89             sub emit_character
90             {
91 0     0 0   my ($self, %args) = @_;
92 0           my $content = eval { $self->emit_kids };
  0            
93              
94 0 0         return '' unless defined $content;
95              
96 0 0         if (my ($char, $class) = $content =~ /(\w)(\w+)/)
97             {
98 0 0         return $characters{$class}->($char) if exists $characters{$class};
99             }
100              
101 0   0       $args{encode} ||= '';
102 0 0         return $content if $args{encode} =~ /^(index_|id$)/;
103              
104 0           return $self->handle_encoding( $content );
105             }
106              
107             sub emit
108             {
109 0     0 0   my $self = shift;
110 0           my $type = $self->type;
111 0           my $emit = 'emit_' . $type;
112              
113 0           $self->$emit( @_ );
114             }
115              
116             sub emit_document
117             {
118 0     0 0   my $self = shift;
119              
120 0 0         return $self->emit_body if $self->add_body_tags;
121 0           return $self->emit_kids( @_ );
122             }
123              
124             sub extract_headings
125             {
126 0     0 0   my ($self, %args) = @_;
127 0           my @headings;
128              
129 0           for my $kid (@{ $self->children })
  0            
130             {
131 0 0         next unless $kid->type eq 'header';
132 0 0         next if $kid->exclude_from_toc( $args{max_depth} );
133 0           push @headings, $kid;
134             }
135              
136 0           return \@headings;
137             }
138              
139             sub emit_toc
140             {
141 0     0 0   my $self = shift;
142 0           my $headings = $self->extract_headings;
143              
144 0           return $self->walk_headings( $headings, filename => $self->filename );
145             }
146              
147             sub walk_headings
148             {
149 0     0 0   my ($self, $headings, %args) = @_;
150 0   0       $args{indent} ||= '';
151              
152 0           my $toc = '';
153              
154 0           for my $heading (@$headings)
155             {
156 0           $toc .= $args{indent};
157              
158 0 0         if (blessed($heading))
159             {
160 0           $toc .= '<li>' . $heading->get_heading_link( %args );
161             }
162             else
163             {
164 0           my $indent = $args{indent} . ' ';
165             $toc .= qq|\n$args{indent}|
166             . $args{indent} . qq|<ul>\n|
167             . $self->walk_headings( $heading, %args, indent => $indent )
168 0           . $args{indent} . qq|</ul>\n|;
169              
170             }
171              
172 0           $toc .= qq|</li>\n|;
173             }
174              
175 0           return $toc . qq|\n|;
176             }
177              
178             sub get_heading_link
179             {
180 0     0 0   my ($self, %args) = @_;
181              
182 0           my $content = $self->emit_kids;
183 0   0       my $filename = $self->filename || '';
184 0           my $frag = $self->get_anchor;
185              
186 0           $content =~ s/^\*//;
187 0           return qq|<a href="$filename#$frag">$content</a>|;
188             }
189              
190             sub emit_body {
191 0     0 0   my $self = shift;
192 0           my $head = <<END_PML_HEAD;
193             <?xml version="1.0" encoding="UTF-8"?>
194             <!DOCTYPE chapter SYSTEM "local/xml/markup.dtd">
195             END_PML_HEAD
196              
197 0           my $foot = <<END_PML_FOOT;
198             </chapter>
199             END_PML_FOOT
200              
201 0           return $head
202             . $self->emit_kids( @_ )
203             . $self->maybe_close_section(1)
204             . $foot;
205             }
206              
207             sub emit_kids
208             {
209 0     0 0   my $self = shift;
210 0           join '', map { $_->emit( @_ ) } @{ $self->children };
  0            
  0            
211             }
212              
213             sub maybe_close_section {
214 0     0 0   my ($self, $level) = @_;
215 0           my $last_level = $self->last_level;
216              
217             # nest this section if the previous section is at a lower level
218             # note that this handles the first level, yippee!
219 0 0         if ($level > $last_level) {
220 0           $self->last_level( $level );
221 0           return '';
222             }
223              
224             # close this section, keep last section if it's at the same level
225 0 0         return "</sect$level>\n" if $last_level == $level;
226              
227             # close all sections until it reaches this level
228 0           my $closings = '';
229              
230 0           while ($last_level >= $level) {
231 0           $closings .= "</sect$last_level>\n";
232 0           $last_level--;
233             }
234              
235 0           $self->last_level( $level );
236 0           return $closings;
237             }
238              
239             sub emit_header {
240 0     0 0   my $self = shift;
241 0           my $l = $self->level;
242 0 0         return $self->emit_chapter(@_) if $l == 0;
243              
244 0           my $prefix = $self->maybe_close_section($l);
245 0           my $content = $self->emit_kids( @_ );
246 0           my $id_node = $self->anchor;
247 0 0         my $id = $id_node ? $id_node->get_anchor : $self->get_anchor;
248 0           my $no_toc = $content =~ s/^\*//;
249 0           my $level = 'sect' . $l;
250 0 0         my $anchor = $id_node ? $self->emit_index( @_ ) : '';
251 0           $self->last_level( $l );
252              
253             # XXX: handle anchor
254 0           return qq|$prefix<$level id="sec.$id">\n<title>$content</title>\n\n|;
255             }
256              
257             sub emit_chapter {
258 0     0 0   my $self = shift;
259              
260 0           my $content = $self->emit_kids( @_ );
261 0           my $id_node = $self->anchor;
262 0 0         my $id = $id_node ? $id_node->get_anchor : $self->get_anchor;
263 0           my $no_toc = $content =~ s/^\*//;
264 0 0         my $anchor = $id_node ? $self->emit_index( @_ ) : '';
265 0 0         $id = 'chp.' . $id unless $id =~ /^chp\./;
266              
267 0           $self->last_level(0);
268 0           return qq|<chapter id="$id">\n <title>$content</title>\n\n|;
269             }
270              
271             sub emit_plaintext
272             {
273 0     0 0   my ($self, %args) = @_;
274 0           my $content = $self->content;
275 0 0         $content = '' unless defined $content;
276 0           $self->handle_encoding( $content, %args );
277             }
278              
279             sub handle_encoding
280             {
281 0     0 0   my ($self, $content, %args) = @_;
282              
283 0 0         if (my $encode = $args{encode})
284             {
285 0           my $method = 'encode_' . $encode;
286 0           return $self->$method( $content, %args );
287             }
288              
289 0           return $self->encode_text( $content, %args );
290             }
291              
292 0     0 0   sub encode_none { $_[1] }
293              
294             sub encode_split
295             {
296 0     0 0   my ($self, $content, %args) = @_;
297 0           my $target = $args{target};
298             return join $args{joiner},
299 0           map { $self->encode_text( $_ ) } split /\s*\Q$target\E\s*/, $content;
  0            
300             }
301              
302             sub encode_text
303             {
304 0     0 0   my ($self, $text) = @_;
305              
306 1     1   8 use Carp;
  1         2  
  1         1063  
307 0 0         unless (defined $text)
308             {
309 0           confess 'no text';
310             }
311 0           $text =~ s/\s*---\s*/&#8213;/g;
312 0           $text =~ s/\s*--\s*/&mdash;/g;
313              
314 0           return $text;
315             }
316              
317             sub encode_id
318             {
319 0     0 0   my ($self, $text) = @_;
320 0           $text =~ s/<.+?>//g;
321 0           $text =~ s/\W//g;
322 0           return lc $text;
323             }
324              
325             sub encode_index_anchor
326             {
327 0     0 0   my ($self, $text) = @_;
328              
329 0           $text =~ s/^\*//;
330 0           $text =~ s/([\x20-\x40\x5B-\x60\x7B-\x7E])/ord($1)/eg;
  0            
331 0           $text =~ s/^(sec|chp)46/$1\./;
332              
333 0           return $text;
334             }
335              
336             sub encode_index_key
337             {
338 0     0 0   my ($self, $text) = @_;
339 0           $text =~ s/^\s+|\s+$//g;
340 0           return $text;
341             }
342              
343             sub encode_verbatim_text
344             {
345 0     0 0   my ($self, $text) = @_;
346 0           return encode_entities( $text, '<>&' );
347             }
348              
349             sub emit_literal
350             {
351 0     0 0   my $self = shift;
352 0           my @kids;
353              
354 0 0         if (my $title = $self->title)
355             {
356 0           my $target = $title->emit_kids( encode => 'none' );
357             @kids = map
358             {
359 0           $_->emit_kids(
360             encode => 'split', target => $target, joiner => "</p>\n\n<p>",
361             )
362 0           } @{ $self->children };
  0            
363             }
364             else
365             {
366 0           @kids = map { $_->emit_kids( @_ ) } @{ $self->children };
  0            
  0            
367             }
368              
369 0           return qq|<div class="literal"><p>|
370             . join( "\n", @kids )
371             . qq|</p></div>\n\n|;
372             }
373              
374             sub emit_anchor
375             {
376 0     0 0   my $self = shift;
377             # XXX: fix anchors
378 0           return qq||;
379 0           return qq|<a name="| . $self->get_anchor . qq|"></a>|;
380             }
381              
382             sub emit_number_item
383             {
384 0     0 0   my $self = shift;
385 0           my $marker = $self->marker;
386 0 0         my $number = $marker ? qq| number="$marker"| : '';
387 0           return "<li$number><p>" . $self->emit_kids . "<\/p></li>\n\n";
388             }
389              
390             sub emit_text_item
391             {
392 0     0 0   my $self = shift;
393 0           my $kids = $self->children;
394 0 0         return "<li></li>\n\n" unless @$kids;
395              
396 0           my $first = shift @$kids;
397 0 0         return '<li><p>' . $first->emit( @_ ) . qq|</p></li>\n\n| unless @$kids;
398              
399             return "<li><p>" . $first->emit . "</p>\n\n"
400 0           . join( '', map { $_->emit } @$kids ) . "</li>\n\n";
  0            
401             }
402              
403             sub emit_verbatim
404             {
405 0     0 0   my $self = shift;
406 0           return $self->emit_kids( encode => 'verbatim_text', @_ ) . "\n\n";
407             }
408              
409 0     0 0   sub emit_bold { shift->emit_kids( @_ ) }
410              
411 0     0 0   sub emit_italics { shift->emit_tagged_kids( 'emph', @_ ) }
412 0     0 0   sub emit_code { shift->emit_tagged_kids( 'class', @_ ) }
413 0     0 0   sub emit_superscript { shift->emit_tagged_kids( 'sup', @_ ) }
414 0     0 0   sub emit_subscript { shift->emit_tagged_kids( 'sub', @_ ) }
415 0     0 0   sub emit_file { shift->emit_tagged_kids( 'filename', @_ ) }
416              
417             sub emit_tagged_kids
418             {
419 0     0 0   my ($self, $tag, %args) = @_;
420 0           my $kids = $self->emit_kids( encode => 'verbatim_text', %args );
421 0   0       $args{encode} ||= '';
422              
423 0 0         return $kids if $args{encode} =~ /^(index_|id$)/;
424 0           return qq|<$tag>$kids</$tag>|;
425             }
426              
427             sub emit_footnote
428             {
429 0     0 0   my $self = shift;
430 0           return '<footnote><p>' . $self->emit_kids . '</p></footnote>';
431             }
432              
433             sub emit_url
434             {
435 0     0 0   my $self = shift;
436 0           my $url = $self->emit_kids;
437 0           return qq|<url protocol="http:">$url</url>|;
438             }
439              
440             sub emit_link
441             {
442 0     0 0   my $self = shift;
443 0           my $anchor = $self->emit_kids;
444              
445 0           my ($file, $frag, $text) = $self->get_link_for_anchor( $anchor );
446 0           return qq|<xref linkend="$frag">$text</xref>|;
447             }
448              
449 1     1   7 use constant { BEFORE => 0, AFTER => 1 };
  1         2  
  1         212  
450              
451             my %block_items =
452             (
453             programlisting => [ qq|<code language="perl">\n|, q|</code>| ],
454             epigraph => [ qq|<div class="epigraph">\n\n|, q|</div>| ],
455             blockquote => [ qq|<div class="blockquote">\n\n|, q|</div>| ],
456             );
457              
458             while (my ($tag, $values) = each %block_items)
459             {
460             my $sub = sub
461             {
462 0     0     my $self = shift;
463 0           my $title = $self->title;
464 0           my $env = $self->emit_environments;
465              
466             return $self->make_basic_block( $env->{$tag}, $title, @_ )
467 0 0         if exists $env->{$tag};
468              
469             # deal with title somehow
470 0           return $values->[BEFORE]
471             . $self->make_block_title( $title )
472             . $self->emit_kids( encode => 'none' )
473             . $values->[AFTER]
474             . "\n\n";
475             };
476              
477 1     1   6 do { no strict 'refs'; *{ 'emit_' . $tag } = $sub };
  1         2  
  1         577  
478             }
479              
480             sub emit_sidebar {
481 0     0 0   my $self = shift;
482 0           my $title = $self->title;
483 0 0         my $title_tag = $title ? qq| <title>$title</title>\n| : '';
484              
485             # deal with title somehow
486 0           return qq|<sidebar>\n$title_tag|
487             . $self->emit_kids
488             . qq|</sidebar>\n\n|;
489             }
490              
491             sub emit_tip {
492 0     0 0   my $self = shift;
493 0           my $title = $self->title;
494 0 0         $title = $title->emit_kids( @_ ) if $title;
495 0 0         my $title_tag = $title ? qq| <title>$title</title>\n| : '';
496              
497             # deal with title somehow
498 0           return qq|<sidebar>\n$title_tag|
499             . $self->emit_kids
500             . qq|</sidebar>\n\n|;
501             }
502              
503             my %invisibles = map { $_ => 1 } qw( index anchor );
504              
505             sub emit_paragraph
506             {
507 0     0 0   my $self = shift;
508 0           my @kids = @{ $self->children };
  0            
509 0           my $has_visible_text = grep { ! exists $invisibles{ $_->type } } @kids;
  0            
510 0 0         return $self->emit_kids( @_ ) unless $has_visible_text;
511              
512 0 0 0       my $attrs = @kids && $kids[0]->type =~ /^(?:anchor|index)$/
513             ? $self->get_anchored_paragraph_attrs( shift @kids )
514             : '';
515              
516             # inlined emit_kids() here to reflect any anchor manipulation
517 0           my $content = join '', map { $_->emit( @_ ) } @kids;
  0            
518 0           return "<p$attrs>" . $content . qq|</p>\n\n|;
519             }
520              
521             sub get_anchored_paragraph_attrs
522             {
523 0     0 0   my ($self, $tag) = @_;
524 0           my $type = $tag->type;
525              
526 0 0         if ($type eq 'anchor')
    0          
527             {
528 0           my $content = $tag->get_anchor;
529 0           return qq| id="$content"|;
530             }
531             elsif ($type eq 'index')
532             {
533 0           my $content = $tag->get_anchor . $tag->id;
534 0           return qq| id="index.$content"|;
535             }
536             }
537              
538             my %parent_items =
539             (
540             text_list => [ qq|<ul>\n\n|, q|</ul>| ],
541             bullet_list => [ qq|<ul>\n\n|, q|</ul>| ],
542             bullet_item => [ qq|<li><p>|, q|</p></li>| ],
543             number_list => [ qq|<ol>\n\n|, q|</ol>| ],
544             );
545              
546             while (my ($tag, $values) = each %parent_items)
547             {
548             my $sub = sub
549             {
550 0     0     my $self = shift;
551 0           return $values->[BEFORE] . $self->emit_kids( @_ ) . $values->[AFTER]
552             . "\n\n";
553             };
554              
555 1     1   7 do { no strict 'refs'; *{ 'emit_' . $tag } = $sub };
  1         1  
  1         727  
556             }
557              
558             sub emit_block
559             {
560 0     0 0   my $self = shift;
561 0 0         my $title = $self->title ? $self->title->emit_kids : '';
562 0           my $target = $self->target;
563              
564 0 0         if (my $environment = $self->emit_environments->{$target})
    0          
565             {
566 0           $target = $environment;
567             }
568             elsif (my $meth = $self->can( 'emit_' . $target))
569             {
570 0           return $self->$meth( @_ );
571             }
572              
573 0           return $self->make_basic_block( $self->target, $title, @_ );
574             }
575              
576             sub emit_html
577             {
578 0     0 0   my $self = shift;
579 0           return $self->emit_kids( encode => 'none' );
580             }
581              
582             sub emit_screen {
583 0     0 0   my $self = shift;
584 0           return qq|<code language="session">\n|
585             . $self->emit_kids( encode => 'none' )
586             . qq|</code>\n|;
587             }
588              
589             sub make_basic_block
590             {
591 0     0 0   my ($self, $target, $title, @rest) = @_;
592              
593 0           $title = $self->make_block_title( $title );
594              
595 0           return qq|<div class="$target">\n$title|
596             . $self->emit_kids( @rest )
597             . qq|</div>|;
598             }
599              
600             sub make_block_title
601             {
602 0     0 0   my ($self, $title) = @_;
603              
604 0 0 0       return '' unless defined $title and length $title;
605 0           return qq|<p class="title">$title</p>\n|;
606             }
607              
608             sub emit_index
609             {
610 0     0 0   my $self = shift;
611 0           my $content = $self->get_anchor;
612 0 0         $content .= $self->id if $self->type eq 'index';
613              
614             # XXX: improve index links
615 0           return qq||;
616 0           return qq|<a name="$content"></a>|;
617             }
618              
619             sub emit_index_link
620             {
621 0     0 0   my $self = shift;
622 0           my $id = $self->id;
623 0           my $frag = $self->get_anchor . $id;
624 0           my $file = $self->link;
625 0           return qq|<a href="$file#$frag">$id</a>|;
626             }
627              
628             sub emit_table
629             {
630 0     0 0   my $self = shift;
631 0 0         my $title = $self->title ? $self->title->emit_kids : '';
632              
633 0           my $content = qq|<table style="outerlines">\n|;
634 0 0         $content .= qq|<title>$title</title>\n| if $title;
635 0           $content .= $self->emit_kids;
636 0           $content .= qq|</table>\n\n|;
637              
638 0           return $content;
639             }
640              
641             sub emit_headrow
642             {
643 0     0 0   my $self = shift;
644              
645             # kids should be cells
646 0           my $content = '<thead>';
647              
648 0           for my $kid (@{ $self->children })
  0            
649             {
650 0           $content .= '<col>' . $kid->emit_kids . '</col>';
651             }
652              
653 0           return $content . "</thead>\n";
654             }
655              
656             sub emit_row
657             {
658 0     0 0   my $self = shift;
659              
660 0           return '<row>' . $self->emit_kids . qq|</row>\n|;
661             }
662              
663             sub emit_cell
664             {
665 0     0 0   my $self = shift;
666 0           return '<col>' . $self->emit_kids . qq|</col>\n|;
667             }
668              
669             sub emit_figure
670             {
671 0     0 0   my $self = shift;
672 0           my $caption = $self->caption;
673 0           my $anchor = $self->anchor;
674 0 0         my $id = defined $anchor ? ' id="' . $anchor->get_anchor . '"' : '';
675 0           my $file = $self->file->emit_kids;
676 0           my $content = qq|<p$id>|;
677              
678 0 0         $content .= $anchor if $anchor;
679 0           $content .= qq|<img src="$file" />|;
680 0 0         $content .= qq|<br />\n<em>$caption</em>| if $caption;
681 0           $content .= qq|</p>\n\n|;
682              
683 0           return $content;
684             }
685              
686             1;
687              
688             __END__
689              
690             =pod
691              
692             =encoding UTF-8
693              
694             =head1 NAME
695              
696             Pod::PseudoPod::DOM::Role::PML - an PML formatter role for PseudoPod DOM trees
697              
698             =head1 VERSION
699              
700             version 1.20210620.2040
701              
702             =head1 AUTHOR
703              
704             chromatic <chromatic@wgz.org>
705              
706             =head1 COPYRIGHT AND LICENSE
707              
708             This software is copyright (c) 2021 by chromatic.
709              
710             This is free software; you can redistribute it and/or modify it under
711             the same terms as the Perl 5 programming language system itself.
712              
713             =cut