File Coverage

blib/lib/Markdown/Perl.pm
Criterion Covered Total %
statement 179 183 97.8
branch 67 76 88.1
condition 22 24 91.6
subroutine 28 28 100.0
pod 5 5 100.0
total 301 316 95.2


line stmt bran cond sub pod time code
1             package Markdown::Perl;
2              
3 31     31   510474 use strict;
  31         54  
  31         1143  
4 31     31   325 use warnings;
  31         71  
  31         1385  
5 31     31   152 use utf8;
  31         97  
  31         216  
6 31     31   1439 use feature ':5.24';
  31         103  
  31         5896  
7              
8 31     31   210 use Carp;
  31         66  
  31         2259  
9 31     31   16964 use English;
  31         96097  
  31         174  
10 31     31   13287 use Exporter 'import';
  31         54  
  31         1069  
11 31     31   18674 use Hash::Util 'lock_keys';
  31         110754  
  31         201  
12 31     31   3435 use List::Util 'none';
  31         49  
  31         4104  
13 31     31   19898 use List::MoreUtils 'pairwise';
  31         565733  
  31         247  
14 31     31   61736 use Markdown::Perl::BlockParser;
  31         132  
  31         1803  
15 31     31   19525 use Markdown::Perl::Inlines;
  31         113  
  31         2051  
16 31     31   261 use Markdown::Perl::HTML 'html_escape', 'decode_entities', 'parse_attributes';
  31         53  
  31         2089  
17 31     31   19440 use Readonly;
  31         140578  
  31         2196  
18 31     31   213 use Scalar::Util 'blessed';
  31         46  
  31         1432  
19              
20 31     31   152 use parent 'Markdown::Perl::Options';
  31         52  
  31         229  
21              
22             our $VERSION = '1.13'; # remember to bump in App::pmarkdown too.
23              
24             our @EXPORT_OK = qw(convert set_options set_mode set_hooks);
25             our %EXPORT_TAGS = (all => \@EXPORT_OK);
26              
27             sub new {
28 47     47 1 3180610 my ($class, @options) = @_;
29              
30 47         400 my $this = $class->SUPER::new(
31             mode => undef,
32             options => {},
33             local_options => {},
34             hooks => {});
35 47         342 $this->SUPER::set_options(options => @options);
36 47         65 lock_keys(%{$this});
  47         220  
37              
38 47         763 return $this;
39             }
40              
41             sub set_options {
42 1     1 1 339430 my ($this, @options) = &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
43 1         15 $this->SUPER::set_options(options => @options);
44 1         2 return;
45             }
46              
47             sub set_mode {
48 2     2 1 20 my ($this, $mode) = &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
49 2         11 $this->SUPER::set_mode(options => $mode);
50 2         3 return;
51             }
52              
53             Readonly::Array my @VALID_HOOKS => qw(resolve_link_ref yaml_metadata);
54              
55             sub set_hooks {
56 9     9 1 421922 my ($this, %hooks) = &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
57 9         66 while (my ($k, $v) = each %hooks) {
58 9 50   15   106 if (none { $_ eq $k } @VALID_HOOKS) {
  15 100       212  
    50          
59 0         0 croak "Invalid hook name: ${k}";
60             } elsif (!defined $v) {
61 1         23 delete $this->{hooks}{$k};
62             } elsif (ref $v ne 'CODE') {
63 0         0 carp 'Hook target must be a CODE reference';
64             } else {
65 8         209 $this->{hooks}{$k} = $v;
66             }
67             }
68 9         35 return;
69             }
70              
71             # Returns @_, unless the first argument is not blessed as a Markdown::Perl
72             # object, in which case it returns a default object.
73             my $default_this = Markdown::Perl->new();
74              
75             sub _get_this_and_args { ## no critic (RequireArgUnpacking)
76 35141 50   35141   58942 return unless @_;
77             # We could use `$this isa Markdown::Perl` that does not require to test
78             # blessedness first. However this requires 5.31.6 which is not in Debian
79             # stable as of writing this.
80 35141 100 66     134924 if (!blessed($_[0]) || !$_[0]->isa(__PACKAGE__)) {
81 197         473 unshift @_, $default_this;
82             }
83 35141 100       68590 return @_ if defined wantarray;
84 17566         19655 return;
85             }
86              
87             # Takes a string and converts it to HTML. Can be called as a free function or as
88             # class method. In the latter case, provided options override those set in the
89             # class constructor.
90             # Both the input and output are unicode strings.
91             sub convert { ## no critic (RequireArgUnpacking)
92 17566     17566 1 10650612 &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
93 17566         20713 my $this = shift @_;
94 17566         24562 my $md = \(shift @_); # Taking a reference to avoid copying the input. is it useful?
95 17566         64410 $this->SUPER::set_options(local_options => @_);
96              
97             # TODO: introduce an HtmlRenderer object that carries the $linkrefs states
98             # around (instead of having to pass it in all the calls).
99 17563         33293 my ($blocks, $linkrefs) = $this->_parse($md);
100 17562         26082 my $out = $this->_emit_html(0, 'root', $linkrefs, @{$blocks});
  17562         40776  
101 17562         37999 $this->{local_options} = {};
102 17562         136676 return $out;
103             }
104              
105             # This is an internal call for now because the structure of the parse tree is
106             # not defined.
107             # Note that while convert() takes care not to copy the md argument, this is not
108             # the case of this method, however, it can receive a scalar ref instead of a
109             # scalar, to avoid the copy.
110             # TODO: create a BlockTree class and document it, then make this be public.
111             sub _parse {
112 17563     17563   23063 my ($this, $md_or_ref) = &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
113 17563 50       30027 my $md = ref($md_or_ref) ? $md_or_ref : \$md_or_ref;
114              
115 17563         68318 my $parser = Markdown::Perl::BlockParser->new($this, $md);
116 17563         46791 my ($linkrefs, $blocks) = $parser->process();
117 17562 50       106885 return ($blocks, $linkrefs) if wantarray;
118 0         0 return $blocks;
119             }
120              
121             sub _render_inlines {
122 44877     44877   80770 my ($this, $linkrefs, @lines) = @_;
123 44877         91408 return Markdown::Perl::Inlines::render($this, $linkrefs, @lines);
124             }
125              
126             # TODO: move this to a separate package and split the method in smaller chunks.
127             sub _emit_html { ## no critic (ProhibitExcessComplexity)
128 44262     44262   90325 my ($this, $tight_block, $parent_type, $linkrefs, @blocks) = @_;
129 44262         52950 my $out = '';
130 44262         44525 my $block_index = 0;
131 44262         58388 for my $bl (@blocks) {
132 76581         83041 $block_index++;
133 76581 100       259294 if ($bl->{type} eq 'break') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
134 259         444 $out .= "
\n";
135             } elsif ($bl->{type} eq 'heading') {
136 1050         1709 my $l = $bl->{level};
137 1050         1822 my $c = $bl->{content};
138 1050 100       2857 $c = $this->_render_inlines($linkrefs, ref $c eq 'ARRAY' ? @{$c} : $c);
  520         1269  
139 1050         7592 $c =~ s/^[ \t]+|[ \t]+$//g; # Only the setext headings spec asks for this, but this can’t hurt atx heading where this can’t change anything.
140 1050         3630 $out .= "$c\n";
141             } elsif ($bl->{type} eq 'code') {
142 4870         8274 my $c = $bl->{content};
143 4870         10012 html_escape($c, $this->get_html_escaped_code_characters);
144 4870         7148 my $i = '';
145 4870 100 66     9594 if ($this->get_code_blocks_info eq 'language' && $bl->{info}) {
146 3106         10714 my $l = $bl->{info} =~ s/\s.*//r; # The spec does not really cover this behavior so we’re using Perl notion of whitespace here.
147 3106         6646 decode_entities($l);
148 3106         6153 html_escape($l, $this->get_html_escaped_characters);
149 3106         5625 $i = " class=\"language-${l}\"";
150             }
151 4870         11569 $out .= "
$c
\n";
152             } elsif ($bl->{type} eq 'html') {
153 2455         4883 $out .= $bl->{content};
154             } elsif ($bl->{type} eq 'paragraph') {
155 43800         52615 my $html = '';
156 43800 100 100     83270 if ((
      100        
      100        
157             $this->get_allow_task_list_markers eq 'list'
158             && $parent_type eq 'list'
159             && $block_index == 1)
160             || $this->get_allow_task_list_markers eq 'always'
161             ) {
162 8080 100       27241 if ($bl->{content}[0] =~ m/ ^ \s* \[ (? [ xX] ) \] (? \s | $ ) /x) {
163             $html =
164             '
165             .($LAST_PAREN_MATCH{marker} eq ' ' ? '' : 'checked="" ')
166             .'disabled="" type="checkbox">'
167 9 100       84 .($LAST_PAREN_MATCH{space} eq ' ' ? ' ' : "\n");
    50          
168 9         42 substr $bl->{content}[0], 0, $LAST_MATCH_END[0], '';
169             }
170             }
171 43800         55193 $html .= $this->_render_inlines($linkrefs, @{$bl->{content}});
  43800         79718  
172 43800 100       111689 if ($tight_block) {
    100          
173 12278         28035 $out .= $html;
174             } elsif ($this->get_render_naked_paragraphs) {
175 5         9 $out .= "${html}\n";
176             } else {
177 31517         68129 $out .= "

${html}

\n";
178             }
179             } elsif ($bl->{type} eq 'quotes') {
180 9716         11299 my $c = $this->_emit_html(0, 'quotes', $linkrefs, @{$bl->{content}});
  9716         24509  
181 9716         18056 $out .= "
\n${c}
\n";
182             } elsif ($bl->{type} eq 'list') {
183 14355         22399 my $type = $bl->{style}; # 'ol' or 'ul'
184 14355         15355 my $start = '';
185 14355         20333 my $num = $bl->{start_num};
186 14355         18711 my $loose = $bl->{loose};
187 14355 100 100     37896 $start = " start=\"${num}\"" if $type eq 'ol' && $num != 1;
188             $out .= "<${type}${start}>\n
  • "
  • 189             .join("\n
  • ",
  • 190 14355         22739 map { $this->_emit_html(!$loose, 'list', $linkrefs, @{$_->{content}}) } @{$bl->{items}})
      16915         21324  
      16915         43651  
      14355         27932  
    191             ."\n\n";
    192             } elsif ($bl->{type} eq 'table') {
    193 7         13 $out .= ''; '; '; '; " : '' } @align, @d); '; ';
    194 7 100       9 my @align = map { $_ ? " align=\"${_}\"" : '' } @{$bl->{content}{align}};
      13         33  
      7         16  
    195 7         12 my @h = map { $this->_render_inlines($linkrefs, $_) } @{$bl->{content}{headers}};
      13         23  
      7         12  
    196 7     13   64 $out .= join('', pairwise { "${b}" } @align, @h);
      13         38  
    197 7         22 $out .= '
    198 7 100       8 if (@{$bl->{content}{table}}) {
      7         14  
    199 6         8 $out .= '
    200 6         19 my $ms = $this->get_table_blocks_have_cells_for_missing_data;
    201 6         7 for my $l (@{$bl->{content}{table}}) {
      6         11  
    202 9         10 $out .= '
    203 9 50       12 my @d = map { defined ? $this->_render_inlines($linkrefs, $_) : $ms ? '' : undef } @{$l};
      16 100       47  
      9         17  
    204 9 50   16   105 $out .= join('', pairwise { defined $b ? "${b}
      16         64  
    205 9         38 $out .= '
    206             }
    207 6         10 $out .= '
    208             }
    209 7         17 $out .= '
    ';
    210             } elsif ($bl->{type} eq 'directive') {
    211 69         163 my $c = $this->_emit_html(0, 'directive', $linkrefs, @{$bl->{content}});
      69         236  
    212 69 100       257 my %attr = parse_attributes($bl->{attributes}) if defined $bl->{attributes}; ## no critic (ProhibitConditionalDeclaration)
    213 69         167 my @attr = ('div');
    214 69 100       211 push @attr, 'id="'.$attr{id}.'"' if exists $attr{id};
    215 69 100       175 unshift @{$attr{class}}, lc($bl->{name}) if defined $bl->{name};
      12         91  
    216 69 100       261 push @attr, 'class="'.join(' ', @{$attr{class}}).'"' if exists $attr{class};
      46         329  
    217 69 100       215 push @attr, map { sprintf 'data-%s="%s"', @{$_} } @{$attr{keys}} if exists $attr{keys};
      2         22  
      2         15  
      2         7  
    218 69         182 my $tag = join(' ', @attr);
    219             # TODO: the inline content is ignored for now
    220             # TODO we should add a hook to process the directive in custom cases.
    221 69         168 $out .= "<${tag}>\n${c}\n";
    222 69 100 100     242 if (defined $bl->{inline} && $this->get_warn_for_unused_input()) {
    223 1         132 carp 'Unused inline content in a directive block: '.$bl->{inline};
    224             }
    225 69 100 100     281 if (defined $attr{junk} && $this->get_warn_for_unused_input()) {
    226 1         219 carp 'Unused attribute content in a directive block: '.$attr{junk};
    227             }
    228             } else {
    229 0         0 confess 'Unexpected block type when rendering HTML output: '.$bl->{type};
    230             }
    231             }
    232             # Note: a final new line should always be appended to $out. This is not
    233             # guaranteed when the last element is HTML and the input file did not contain
    234             # a final new line, unless the option force_final_new_line is set.
    235 44262         133688 return $out;
    236             }
    237              
    238             1;
    239              
    240             __END__