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   112806 use strict;
  2         10  
  2         46  
3 2     2   8 use warnings;
  2         3  
  2         53  
4 2     2   10 use Carp qw(croak);
  2         3  
  2         70  
5 2     2   902 use Encode;
  2         16199  
  2         113  
6 2     2   1255 use Path::Tiny;
  2         20839  
  2         87  
7 2     2   727 use parent 'Pod::Markdown';
  2         463  
  2         12  
8              
9             our $VERSION = '0.04';
10              
11             my $DATA_KEY = '_Pod_Github_';
12              
13             sub new {
14 2     2 1 1931 my $class = shift;
15 2         6 my %args = @_;
16              
17 2         12 my $self = $class->SUPER::new();
18 2         368 $self->{$DATA_KEY} = \%args;
19              
20 2         4 return $self;
21             }
22              
23             sub _should_exclude_section {
24 6     6   8 my ($self, $heading) = @_;
25 6 50       7 my @include = @{$self->{$DATA_KEY}{include} || []};
  6         22  
26 6 100       7 my @exclude = @{$self->{$DATA_KEY}{exclude} || []};
  6         17  
27              
28 6 50       11 if (@include) {
29 0         0 return not grep { $_ eq $heading } @include;
  0         0  
30             }
31             else {
32 6         12 return grep { $_ eq $heading } @exclude;
  3         10  
33             }
34             }
35              
36             sub _should_inline_section {
37 5     5   8 my ($self, $heading) = @_;
38 5 100       6 my @inline = @{$self->{$DATA_KEY}{inline} || []};
  5         14  
39              
40 5         9 return grep { $_ eq $heading } @inline;
  4         9  
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   6 my ($self, $name) = @_;
48              
49 4         7 my $conf = $self->{$DATA_KEY};
50              
51             my $content = $conf->{$name} ? $conf->{$name}
52 4 50       12 : $conf->{$name . '-file'} ? path($conf->{$name . '-file'})->slurp_utf8
    50          
53             : undef
54             ;
55              
56 4 50       11 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   4659 my ($self, $paragraph) = @_;
65              
66 4         13 $paragraph = $self->SUPER::_indent_verbatim($paragraph);
67              
68 4 50       111 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         17  
  8         16  
71              
72             # Enclose the paragraph in ``` and specify the language
73 4         11 $paragraph = sprintf( "```%s\n%s\n```", $self->_syntax($paragraph), $paragraph );
74             }
75              
76 4         8 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 3487 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       20 @{ $self->_private->{stacks} } == 1 or die "Invalid state: stacks > 1";
  2         7  
90              
91 2         14 my $conf = $self->{$DATA_KEY};
92              
93 2         3 my @stack = @{ $self->_private->{stacks}[0] };
  2         4  
94 2         10 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         16 my ($level, $heading) = (length $1, $2);
101              
102 6 100       11 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       12 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         7 $para =~ s/^.*(\n|$)//;
120 2 50       5 push @new, $para if $para ne "\n";
121             }
122             else {
123 3 50       12 if ($conf->{'title-case'}) {
124 3         8 $heading = _title_case($heading);
125             }
126              
127 3 50       7 if ($conf->{'shift-headings'}) {
128 3         5 $level += $conf->{'shift-headings'};
129             }
130              
131 3         6 my $new_heading = ('#' x $level) . ' ' . $heading;
132 3         10 $para =~ s/^.*(?=\n|$)/$new_heading/;
133              
134 3         8 push @new, $para;
135             }
136             }
137             else {
138             # Non-heading content
139 6 100       13 push @new, $para unless $skip_until_level;
140             }
141             }
142              
143 2         32 $self->_private->{stacks}[0] = \@new;
144 2         16 $self->_private->{states}[-1]{blocks} = scalar @new;
145              
146 2         11 $self->_include_markdown('header');
147              
148 2         8 $self->SUPER::end_Document;
149              
150 2         121 $self->_include_markdown('footer');
151             }
152              
153             # Syntax guesser, lifted from Pod::Markdown::Github
154             sub _syntax {
155 4     4   9 my ($self, $paragraph) = @_;
156              
157 4 100       25 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   20 my @small_words = qw( (?
165 3         10 my $small_re = join '|', @small_words;
166              
167 3         9 my $apos = qr/ (?: ['’] [[:lower:]]* )? /x;
168              
169 3         5 $_ = shift;
170              
171 3         8 s{\A\s+}{}, s{\s+\z}{};
172 3 50       16 $_ = lc $_ if not /[[:lower:]]/;
173 3         249 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       27 $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         95 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         69 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         50 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         55 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         14 return $_;
227             }
228              
229             1;
230              
231             __END__