File Coverage

blib/lib/Pod/Github.pm
Criterion Covered Total %
statement 94 99 94.9
branch 31 46 67.3
condition n/a
subroutine 14 14 100.0
pod 1 2 50.0
total 140 161 86.9


line stmt bran cond sub pod time code
1             package Pod::Github;
2 2     2   89573 use strict;
  2         15  
  2         61  
3 2     2   12 use warnings;
  2         4  
  2         60  
4 2     2   13 use Carp qw(croak);
  2         3  
  2         91  
5 2     2   641 use Encode;
  2         14397  
  2         120  
6 2     2   593 use File::Slurp qw(read_file);
  2         20341  
  2         103  
7 2     2   457 use parent 'Pod::Markdown';
  2         448  
  2         10  
8              
9             our $VERSION = '0.03';
10              
11             my $DATA_KEY = '_Pod_Github_';
12              
13             sub new {
14 2     2 1 1015 my $class = shift;
15 2         8 my %args = @_;
16              
17 2         17 my $self = $class->SUPER::new();
18 2         476 $self->{$DATA_KEY} = \%args;
19              
20 2         6 return $self;
21             }
22              
23             sub _should_exclude_section {
24 6     6   10 my ($self, $heading) = @_;
25 6 50       7 my @include = @{$self->{$DATA_KEY}{include} || []};
  6         24  
26 6 100       8 my @exclude = @{$self->{$DATA_KEY}{exclude} || []};
  6         15  
27              
28 6 50       11 if (@include) {
29 0         0 return not grep { $_ eq $heading } @include;
  0         0  
30             }
31             else {
32 6         13 return grep { $_ eq $heading } @exclude;
  3         10  
33             }
34             }
35              
36             sub _should_inline_section {
37 5     5   10 my ($self, $heading) = @_;
38 5 100       6 my @inline = @{$self->{$DATA_KEY}{inline} || []};
  5         14  
39              
40 5         8 return grep { $_ eq $heading } @inline;
  4         8  
41             }
42              
43             # Output markdown content $name if configured via the '$name' or '${name}-file' options
44             # We assume UTF-8.
45             # Outputting a header may break meta_tags, but these are not supported.
46             sub _include_markdown {
47 4     4   8 my ($self, $name) = @_;
48              
49 4         5 my $conf = $self->{$DATA_KEY};
50              
51             my $content = $conf->{$name} ? $conf->{$name}
52 4 50       12 : $conf->{$name . '-file'} ? scalar read_file($conf->{$name . '-file'})
    50          
53             : undef
54             ;
55              
56 4 50       12 if (defined $content) {
57 0         0 print { $self->{output_fh} } Encode::encode('UTF-8', $content);
  0         0  
58             }
59             }
60              
61             # Called when rendering an indented block. Detect if it's a code block and convert
62             # to Github Flavored Markdown.
63             sub _indent_verbatim {
64 4     4   4339 my ($self, $paragraph) = @_;
65              
66 4         15 $paragraph = $self->SUPER::_indent_verbatim($paragraph);
67              
68 4 50       107 if ($self->{$DATA_KEY}{'syntax-highlight'}) {
69             # Github code blocks don't need indentation, so we can remove it.
70 4         9 $paragraph = join "\n", map { s/^\s{4}//; $_ } split /\n/, $paragraph;
  8         20  
  8         16  
71              
72             # Enclose the paragraph in ``` and specify the language
73 4         12 $paragraph = sprintf( "```%s\n%s\n```", $self->_syntax($paragraph), $paragraph );
74             }
75              
76 4         9 return $paragraph;
77             }
78              
79             # Called just before output. We carry out most operations here:
80             # - Skipping or inlining headings
81             # - Converting headings to title case
82             # - Codifying OPTIONS, METHODS etc.
83             # - Adding header and/or footer
84             sub end_Document {
85 2     2 0 3866 my ($self) = @_;
86              
87             # We are about to output the finished markdown, but do our custom
88             # processing first. The text resides in $self->_private->{stacks}->[0]
89 2 50       14 @{ $self->_private->{stacks} } == 1 or die "Invalid state: stacks > 1";
  2         5  
90              
91 2         14 my $conf = $self->{$DATA_KEY};
92              
93 2         3 my @stack = @{ $self->_private->{stacks}[0] };
  2         4  
94 2         11 my @new;
95 2         3 my $skip_until_level = 0;
96              
97 2         3 for my $para (@stack) {
98             # Is this paragraph a heading?
99 12 100       34 if ($para =~ /^(#+) (.*)/) {
100 6         17 my ($level, $heading) = (length $1, $2);
101              
102 6 100       10 if ($skip_until_level) {
103             # We are skipping over everything until we reach a heading of level
104             # $skip_until_level
105 1 50       3 if ($level > $skip_until_level) {
106 0         0 next;
107             }
108             else {
109             # Not skipping anymore.
110 1         1 $skip_until_level = 0;
111             }
112             }
113              
114 6 100       13 if ($self->_should_exclude_section($heading)) {
    100          
115 1         2 $skip_until_level = $level;
116             }
117             elsif ($self->_should_inline_section($heading)) {
118             # Remove the header (first line), but keep the content
119 2         6 $para =~ s/^.*(\n|$)//;
120 2 50       6 push @new, $para if $para ne "\n";
121             }
122             else {
123 3 50       8 if ($conf->{'title-case'}) {
124 3         7 $heading = _title_case($heading);
125             }
126              
127 3 50       8 if ($conf->{'shift-headings'}) {
128 3         4 $level += $conf->{'shift-headings'};
129             }
130              
131 3         6 my $new_heading = ('#' x $level) . ' ' . $heading;
132 3         9 $para =~ s/^.*(?=\n|$)/$new_heading/;
133              
134 3         8 push @new, $para;
135             }
136             }
137             else {
138             # Non-heading content
139 6 100       11 push @new, $para unless $skip_until_level;
140             }
141             }
142              
143 2         8 $self->_private->{stacks}[0] = \@new;
144 2         12 $self->_private->{states}[-1]{blocks} = scalar @new;
145              
146 2         11 $self->_include_markdown('header');
147              
148 2         9 $self->SUPER::end_Document;
149              
150 2         122 $self->_include_markdown('footer');
151             }
152              
153             # Syntax guesser, lifted from Pod::Markdown::Github
154             sub _syntax {
155 4     4   6 my ($self, $paragraph) = @_;
156              
157 4 100       26 return ( $paragraph =~ /(\b(sub|my|use|shift)\b|\$self|\=\>|\$_|\@_)/ )
158             ? 'perl'
159             : '';
160             }
161              
162             # Uses John Gruber's TitleCase.pl under MIT license.
163             sub _title_case {
164 3     3   9 my @small_words = qw( (?
165 3         8 my $small_re = join '|', @small_words;
166              
167 3         14 my $apos = qr/ (?: ['’] [[:lower:]]* )? /x;
168              
169 3         6 $_ = shift;
170              
171 3         8 s{\A\s+}{}, s{\s+\z}{};
172 3 50       9 $_ = lc $_ if not /[[:lower:]]/;
173 3         227 s{
174             \b (_*) (?:
175             ( (?<=[ ][/\\]) [[:alpha:]]+ [-_[:alpha:]/\\]+ | # file path or
176             [-_[:alpha:]]+ [@.:] [-_[:alpha:]@.:/]+ $apos ) # URL, domain, or email
177             |
178             ( (?i: $small_re ) $apos ) # or small word (case-insensitive)
179             |
180             ( [[:alpha:]] [[:lower:]'’()\[\]{}]* $apos ) # or word w/o internal caps
181             |
182             ( [[:alpha:]] [[:alpha:]'’()\[\]{}]* $apos ) # or some other word
183             ) (_*) \b
184             }{
185 3 50       21 $1 . (
    50          
    50          
186             defined $2 ? $2 # preserve URL, domain, or email
187             : defined $3 ? "\L$3" # lowercase small word
188             : defined $4 ? "\u\L$4" # capitalize word w/o internal caps
189             : $5 # preserve other kinds of word
190             ) . $6
191             }xeg;
192              
193              
194             # Exceptions for small words: capitalize at start and end of title
195 3         96 s{
196             ( \A [[:punct:]]* # start of title...
197             | [:.;?!][ ]+ # or of subsentence...
198             | [ ]['"“‘(\[][ ]* ) # or of inserted subphrase...
199             ( $small_re ) \b # ... followed by small word
200             }{$1\u\L$2}xig;
201              
202 3         56 s{
203             \b ( $small_re ) # small word...
204             (?= [[:punct:]]* \Z # ... at the end of the title...
205             | ['"’”)\]] [ ] ) # ... or of an inserted subphrase?
206             }{\u\L$1}xig;
207              
208             # Exceptions for small words in hyphenated compound words
209             ## e.g. "in-flight" -> In-Flight
210 3         49 s{
211             \b
212             (?
213             ( $small_re )
214             (?= -[[:alpha:]]+) # lookahead for "-someword"
215             }{\u\L$1}xig;
216              
217             ## # e.g. "Stand-in" -> "Stand-In" (Stand is already capped at this point)
218 3         46 s{
219             \b
220             (?
221             ( [[:alpha:]]+- ) # $1 = first word and hyphen, should already be properly capped
222             ( $small_re ) # ... followed by small word
223             (?! - ) # Negative lookahead for another '-'
224             }{$1\u$2}xig;
225              
226 3         15 return $_;
227             }
228              
229             1;
230              
231             __END__