File Coverage

blib/lib/Markdown/Perl.pm
Criterion Covered Total %
statement 179 183 97.8
branch 67 76 88.1
condition 21 24 87.5
subroutine 28 28 100.0
pod 5 5 100.0
total 300 316 94.9


line stmt bran cond sub pod time code
1             package Markdown::Perl;
2              
3 31     31   2450804 use strict;
  31         77  
  31         4573  
4 31     31   307 use warnings;
  31         85  
  31         1933  
5 31     31   455 use utf8;
  31         81  
  31         295  
6 31     31   1753 use feature ':5.24';
  31         135  
  31         24951  
7              
8 31     31   256 use Carp;
  31         93  
  31         3914  
9 31     31   73998 use English;
  31         266729  
  31         231  
10 31     31   51013 use Exporter 'import';
  31         74  
  31         1623  
11 31     31   77401 use Hash::Util 'lock_keys';
  31         332857  
  31         3032  
12 31     31   5270 use List::Util 'none';
  31         69  
  31         7709  
13 31     31   44761 use List::MoreUtils 'pairwise';
  31         1558242  
  31         363  
14 31     31   185996 use Markdown::Perl::BlockParser;
  31         197  
  31         7158  
15 31     31   40457 use Markdown::Perl::Inlines;
  31         663  
  31         3419  
16 31     31   426 use Markdown::Perl::HTML 'html_escape', 'decode_entities', 'parse_attributes';
  31         70  
  31         21938  
17 31     31   43241 use Readonly;
  31         467231  
  31         3614  
18 31     31   340 use Scalar::Util 'blessed';
  31         64  
  31         2011  
19              
20 31     31   11258 use parent 'Markdown::Perl::Options';
  31         134  
  31         330  
21              
22             our $VERSION = '1.11';
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 9766926 my ($class, @options) = @_;
29              
30 47         525 my $this = $class->SUPER::new(
31             mode => undef,
32             options => {},
33             local_options => {},
34             hooks => {});
35 47         561 $this->SUPER::set_options(options => @options);
36 47         89 lock_keys(%{$this});
  47         325  
37              
38 47         954 return $this;
39             }
40              
41             sub set_options {
42 1     1 1 387313 my ($this, @options) = &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
43 1         18 $this->SUPER::set_options(options => @options);
44 1         3 return;
45             }
46              
47             sub set_mode {
48 2     2 1 35 my ($this, $mode) = &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
49 2         22 $this->SUPER::set_mode(options => $mode);
50 2         4 return;
51             }
52              
53             Readonly::Array my @VALID_HOOKS => qw(resolve_link_ref yaml_metadata);
54              
55             sub set_hooks {
56 9     9 1 1137687 my ($this, %hooks) = &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
57 9         57 while (my ($k, $v) = each %hooks) {
58 9 50   15   131 if (none { $_ eq $k } @VALID_HOOKS) {
  15 100       209  
    50          
59 0         0 croak "Invalid hook name: ${k}";
60             } elsif (!defined $v) {
61 1         26 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         181 $this->{hooks}{$k} = $v;
66             }
67             }
68 9         34 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   106990 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     325370 if (!blessed($_[0]) || !$_[0]->isa(__PACKAGE__)) {
81 197         725 unshift @_, $default_this;
82             }
83 35141 100       139880 return @_ if defined wantarray;
84 17566         39478 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 27630457 &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
93 17566         44931 my $this = shift @_;
94 17566         47653 my $md = \(shift @_); # Taking a reference to avoid copying the input. is it useful?
95 17566         156928 $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         82642 my ($blocks, $linkrefs) = $this->_parse($md);
100 17562         47510 my $out = $this->_emit_html(0, 'root', $linkrefs, @{$blocks});
  17562         81492  
101 17562         81243 $this->{local_options} = {};
102 17562         284408 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   65642 my ($this, $md_or_ref) = &_get_this_and_args; ## no critic (ProhibitAmpersandSigils)
113 17563 50       96629 my $md = ref($md_or_ref) ? $md_or_ref : \$md_or_ref;
114              
115 17563         124399 my $parser = Markdown::Perl::BlockParser->new($this, $md);
116 17563         105903 my ($linkrefs, $blocks) = $parser->process();
117 17562 50       211629 return ($blocks, $linkrefs) if wantarray;
118 0         0 return $blocks;
119             }
120              
121             sub _render_inlines {
122 44565     44565   178391 my ($this, $linkrefs, @lines) = @_;
123 44565         222159 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 44277     44277   181280 my ($this, $tight_block, $parent_type, $linkrefs, @blocks) = @_;
129 44277         84487 my $out = '';
130 44277         86093 my $block_index = 0;
131 44277         102388 for my $bl (@blocks) {
132 76254         138466 $block_index++;
133 76254 100       588265 if ($bl->{type} eq 'break') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
134 282         870 $out .= "
\n";
135             } elsif ($bl->{type} eq 'heading') {
136 987         3018 my $l = $bl->{level};
137 987         2963 my $c = $bl->{content};
138 987 100       4521 $c = $this->_render_inlines($linkrefs, ref $c eq 'ARRAY' ? @{$c} : $c);
  541         2211  
139 987         11994 $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 987         6497 $out .= "$c\n";
141             } elsif ($bl->{type} eq 'code') {
142 5005         27270 my $c = $bl->{content};
143 5005         18420 html_escape($c, $this->get_html_escaped_code_characters);
144 5005         18524 my $i = '';
145 5005 100 66     19664 if ($this->get_code_blocks_info eq 'language' && $bl->{info}) {
146 3286         23241 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 3286         15164 decode_entities($l);
148 3286         14154 html_escape($l, $this->get_html_escaped_characters);
149 3286         8752 $i = " class=\"language-${l}\"";
150             }
151 5005         17875 $out .= "
$c
\n";
152             } elsif ($bl->{type} eq 'html') {
153 2405         10278 $out .= $bl->{content};
154             } elsif ($bl->{type} eq 'paragraph') {
155 43549         112796 my $html = '';
156 43549 100 100     152783 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 8009 100       85132 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       130 .($LAST_PAREN_MATCH{space} eq ' ' ? ' ' : "\n");
    50          
168 9         57 substr $bl->{content}[0], 0, $LAST_MATCH_END[0], '';
169             }
170             }
171 43549         126545 $html .= $this->_render_inlines($linkrefs, @{$bl->{content}});
  43549         237723  
172 43549 100       218558 if ($tight_block) {
    100          
173 12145         45843 $out .= $html;
174             } elsif ($this->get_render_naked_paragraphs) {
175 5         17 $out .= "${html}\n";
176             } else {
177 31399         154570 $out .= "

${html}

\n";
178             }
179             } elsif ($bl->{type} eq 'quotes') {
180 9747         23749 my $c = $this->_emit_html(0, 'quotes', $linkrefs, @{$bl->{content}});
  9747         41922  
181 9747         32250 $out .= "
\n${c}
\n";
182             } elsif ($bl->{type} eq 'list') {
183 14201         44413 my $type = $bl->{style}; # 'ol' or 'ul'
184 14201         28547 my $start = '';
185 14201         27351 my $num = $bl->{start_num};
186 14201         44695 my $loose = $bl->{loose};
187 14201 100 100     60808 $start = " start=\"${num}\"" if $type eq 'ol' && $num != 1;
188             $out .= "<${type}${start}>\n
  • "
  • 189             .join("\n
  • ",
  • 190 14201         38700 map { $this->_emit_html(!$loose, 'list', $linkrefs, @{$_->{content}}) } @{$bl->{items}})
      16898         34379  
      16898         120310  
      14201         45897  
    191             ."\n\n";
    192             } elsif ($bl->{type} eq 'table') {
    193 8         24 $out .= ''; '; '; '; " : '' } @align, @d); '; ';
    194 8 100       17 my @align = map { $_ ? " align=\"${_}\"" : '' } @{$bl->{content}{align}};
      15         84  
      8         32  
    195 8         16 my @h = map { $this->_render_inlines($linkrefs, $_) } @{$bl->{content}{headers}};
      15         62  
      8         24  
    196 8     15   221 $out .= join('', pairwise { "${b}" } @align, @h);
      15         73  
    197 8         40 $out .= '
    198 8 100       17 if (@{$bl->{content}{table}}) {
      8         43  
    199 6         14 $out .= '
    200 6         35 my $ms = $this->get_table_blocks_have_cells_for_missing_data;
    201 6         12 for my $l (@{$bl->{content}{table}}) {
      6         19  
    202 9         15 $out .= '
    203 9 50       18 my @d = map { defined ? $this->_render_inlines($linkrefs, $_) : $ms ? '' : undef } @{$l};
      16 100       68  
      9         20  
    204 9 50   16   118 $out .= join('', pairwise { defined $b ? "${b}
      16         90  
    205 9         72 $out .= '
    206             }
    207 6         17 $out .= '
    208             }
    209 8         43 $out .= '
    ';
    210             } elsif ($bl->{type} eq 'directive') {
    211 70         210 my $c = $this->_emit_html(0, 'directive', $linkrefs, @{$bl->{content}});
      70         445  
    212 70 100       515 my %attr = parse_attributes($bl->{attributes}) if defined $bl->{attributes}; ## no critic (ProhibitConditionalDeclaration)
    213 70         264 my @attr = ('div');
    214 70 100       818 push @attr, 'id="'.$attr{id}.'"' if exists $attr{id};
    215 70 100       388 unshift @{$attr{class}}, lc($bl->{name}) if defined $bl->{name};
      9         96  
    216 70 100       325 push @attr, 'class="'.join(' ', @{$attr{class}}).'"' if exists $attr{class};
      31         277  
    217 70 100       369 push @attr, map { sprintf 'data-%s="%s"', @{$_} } @{$attr{keys}} if exists $attr{keys};
      3         7  
      3         24  
      3         11  
    218 70         290 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 70         280 $out .= "<${tag}>\n${c}\n";
    222 70 100 66     303 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 70 100 100     441 if (defined $attr{junk} && $this->get_warn_for_unused_input()) {
    226 1         189 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 44277         229262 return $out;
    236             }
    237              
    238             1;
    239              
    240             __END__