File Coverage

blib/lib/Pod/Github.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Pod::Github;
2 1     1   44965 use strict;
  1         3  
  1         23  
3 1     1   4 use warnings;
  1         2  
  1         22  
4 1     1   5 use Carp qw(croak);
  1         1  
  1         36  
5 1     1   344 use Encode;
  1         7099  
  1         64  
6 1     1   89 use File::Slurp qw(read_file);
  0            
  0            
7             use parent 'Pod::Markdown';
8              
9             our $VERSION = '0.01';
10              
11             my $DATA_KEY = '_Pod_Github_';
12              
13             sub new {
14             my $class = shift;
15             my %args = @_;
16              
17             my $self = $class->SUPER::new();
18             $self->{$DATA_KEY} = \%args;
19              
20             return $self;
21             }
22              
23             sub _should_exclude_section {
24             my ($self, $heading) = @_;
25             my @include = @{$self->{$DATA_KEY}{include} || []};
26             my @exclude = @{$self->{$DATA_KEY}{exclude} || []};
27              
28             if (@include) {
29             return not grep { $_ eq $heading } @include;
30             }
31             else {
32             return grep { $_ eq $heading } @exclude;
33             }
34             }
35              
36             sub _should_inline_section {
37             my ($self, $heading) = @_;
38             my @inline = @{$self->{$DATA_KEY}{inline} || []};
39              
40             return grep { $_ eq $heading } @inline;
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             my ($self, $name) = @_;
48              
49             my $conf = $self->{$DATA_KEY};
50              
51             my $content = $conf->{$name} ? $conf->{$name}
52             : $conf->{$name . '-file'} ? scalar read_file($conf->{$name . '-file'})
53             : undef
54             ;
55              
56             if (defined $content) {
57             print { $self->{output_fh} } Encode::encode('UTF-8', $content);
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             my ($self, $paragraph) = @_;
65              
66             $paragraph = $self->SUPER::_indent_verbatim($paragraph);
67              
68             if ($self->{$DATA_KEY}{'syntax-highlight'}) {
69             # Github code blocks don't need indentation, so we can remove it.
70             $paragraph = join "\n", map { s/^\s{4}//; $_ } split /\n/, $paragraph;
71              
72             # Enclose the paragraph in ``` and specify the language
73             $paragraph = sprintf( "```%s\n%s\n```", $self->_syntax($paragraph), $paragraph );
74             }
75              
76             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             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             @{ $self->_private->{stacks} } == 1 or die "Invalid state: stacks > 1";
90              
91             my $conf = $self->{$DATA_KEY};
92              
93             my @stack = @{ $self->_private->{stacks}[0] };
94             my @new;
95             my $skip_until_level = 0;
96              
97             for my $para (@stack) {
98             # Is this paragraph a heading?
99             if ($para =~ /^(#+) (.*)/) {
100             my ($level, $heading) = (length $1, $2);
101              
102             if ($skip_until_level) {
103             # We are skipping over everything until we reach a heading of level
104             # $skip_until_level
105             if ($level > $skip_until_level) {
106             next;
107             }
108             else {
109             # Not skipping anymore.
110             $skip_until_level = 0;
111             }
112             }
113              
114             if ($self->_should_exclude_section($heading)) {
115             $skip_until_level = $level;
116             }
117             elsif ($self->_should_inline_section($heading)) {
118             # Remove the header (first line), but keep the content
119             $para =~ s/^.*(\n|$)//;
120             push @new, $para if $para ne "\n";
121             }
122             else {
123             if ($conf->{'title-case'}) {
124             $heading = _title_case($heading);
125             }
126              
127             if ($conf->{'shift-headings'}) {
128             $level += $conf->{'shift-headings'};
129             }
130              
131             my $new_heading = ('#' x $level) . ' ' . $heading;
132             $para =~ s/^.*(?=\n|$)/$new_heading/;
133              
134             push @new, $para;
135             }
136             }
137             else {
138             # Non-heading content
139             push @new, $para unless $skip_until_level;
140             }
141             }
142              
143             $self->_private->{stacks}[0] = \@new;
144             $self->_private->{states}[-1]{blocks} = scalar @new;
145              
146             $self->_include_markdown('header');
147              
148             $self->SUPER::end_Document;
149              
150             $self->_include_markdown('footer');
151             }
152              
153             # Syntax guesser, lifted from Pod::Markdown::Github
154             sub _syntax {
155             my ($self, $paragraph) = @_;
156              
157             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             my @small_words = qw( (?
165             my $small_re = join '|', @small_words;
166              
167             my $apos = qr/ (?: ['’] [[:lower:]]* )? /x;
168              
169             $_ = shift;
170              
171             s{\A\s+}{}, s{\s+\z}{};
172             $_ = lc $_ if not /[[:lower:]]/;
173             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             $1 . (
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             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             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             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             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             return $_;
227             }
228              
229             1;
230              
231             __END__