File Coverage

blib/lib/CPAN/Changes/Parser.pm
Criterion Covered Total %
statement 144 146 98.6
branch 65 72 90.2
condition 30 34 88.2
subroutine 13 13 100.0
pod 2 2 100.0
total 254 267 95.1


line stmt bran cond sub pod time code
1             package CPAN::Changes::Parser;
2 30     30   276983 use strict;
  30         108  
  30         957  
3 30     30   209 use warnings;
  30         94  
  30         1697  
4              
5             our $VERSION = '0.500_001';
6             $VERSION =~ tr/_//d;
7              
8 30     30   2999 use Module::Runtime qw(use_module);
  30         9295  
  30         227  
9 30     30   1591 use Carp qw(croak);
  30         97  
  30         1883  
10 30     30   18456 use Encode qw(decode FB_CROAK LEAVE_SRC);
  30         324108  
  30         2296  
11              
12 30     30   2904 use Moo;
  30         46689  
  30         189  
13              
14             has _changelog_class => (
15             is => 'ro',
16             default => 'CPAN::Changes',
17             coerce => sub { use_module($_[0]) },
18             );
19             has _release_class => (
20             is => 'ro',
21             default => 'CPAN::Changes::Release',
22             coerce => sub { use_module($_[0]) },
23             );
24             has _entry_class => (
25             is => 'ro',
26             default => 'CPAN::Changes::Entry',
27             coerce => sub { use_module($_[0]) },
28             );
29             has version_like => (
30             is => 'ro',
31             );
32             has version_prefix => (
33             is => 'ro',
34             );
35              
36             sub parse_string {
37 37     37 1 588 my ($self, $string) = @_;
38 37         146 $self->_transform($self->_parse($string));
39             }
40              
41             sub parse_file {
42 28     28 1 1250 my ($self, $file, $layers) = @_;
43 28 50       127 my $mode = defined $layers ? "<$layers" : '<:raw';
44 28 50       1594 open my $fh, $mode, $file or croak "Can't open $file: $!";
45 28         101 my $content = do { local $/; <$fh> };
  28         149  
  28         1689  
46 28 50       173 if (!defined $layers) {
47             # if it's valid UTF-8, decode that. otherwise, assume latin 1 and leave it.
48 28         70 eval { $content = decode('UTF-8', $content, FB_CROAK | LEAVE_SRC) };
  28         172  
49             }
50 28         5498 $self->parse_string($content);
51             }
52              
53             sub _transform {
54 37     37   147 my ($self, $data) = @_;
55              
56 37         144 my $release_class = $self->_release_class;
57 37         126 my $entry_class = $self->_entry_class;
58              
59             $self->_changelog_class->new(
60             (defined $data->{preamble} ? (preamble => $data->{preamble}) : ()),
61             releases => [
62             map {
63 212         20097 my $r = $_;
64             $release_class->new(
65 1060 100       3112 (map { defined $r->{$_} ? ($_ => $r->{$_}) : () }
66             qw(version line date raw_date note)),
67             ($_->{entries} ? (
68             entries => [
69 212 50       460 map { _trans_entry($entry_class, $_) } @{$_->{entries}},
  846         47191  
  212         731  
70             ],
71             ) : () ),
72             )
73 37 100       240 } reverse @{$data->{releases}},
  37         163  
74             ],
75             );
76             }
77              
78             sub _trans_entry {
79 1284     1284   2478 my ($entry_class, $entry) = @_;
80              
81             $entry_class->new(
82             line => $entry->{line},
83             text => $entry->{text},
84             $entry->{entries} ? (
85             entries => [
86 1284 100       22415 map { _trans_entry($entry_class, $_) } @{$entry->{entries}},
  438         22029  
  86         197  
87             ],
88             ) : (),
89             );
90             }
91              
92             our $VERSION_REGEX = qr{
93             (?:
94             v [0-9]+ (?: (?:\.[0-9]+ )+ (?:_[0-9]+)? )?
95             |
96             (?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)?
97             |
98             [0-9]* \.[0-9]+ (?: _[0-9]+ )?
99             |
100             [0-9]+ (?: _[0-9]+ )?
101             )
102             (?: -TRIAL )?
103             }x;
104              
105             sub _parse {
106 129     129   1348317 my ($self, $string) = @_;
107              
108 129         771 my $version_prefix = qr/version|revision/i;
109 129 50       741 if (defined(my $vp = $self->version_prefix)) {
110 0         0 $version_prefix = qr/$version_prefix|$vp/
111             }
112 129         2163 my $version_token = qr/$VERSION_REGEX(?:-TRIAL)?/;
113 129 100       639 if (defined(my $vt = $self->version_like)) {
114 101         723 $version_token = qr/$version_token|$vt/
115             }
116              
117 129         312 my $raw_preamble = '';
118 129         253 my @releases;
119             my @indents;
120 129         222 my $line_number = -1;
121 129         1443 while ($string =~ /((.*?)(?:\r\n?|\n|\z))/g) {
122 29701         85197 my ($full_line, $line) = ($1, $2);
123             last
124 29701 100       55314 if !length $full_line;
125 29572         38222 $line_number++;
126              
127 29572 100       180808 if ( $line =~ /^(?:$version_prefix\s+)?($version_token)(?:[:;.-]?\s+(.*))?$/i ) {
    100          
128 2572         5718 my $version = $1;
129 2572         4237 my $note = $2;
130 2572         3731 my $date;
131             my $raw_date;
132 2572 100       4742 if (defined $note) {
133 2393         4806 ($date, $raw_date, $note) = _split_date($note);
134             }
135              
136 2572 100       14135 my $release = {
    100          
    100          
137             version => $version,
138             (defined $date ? (date => $date) : ()),
139             (defined $raw_date ? (raw_date => $raw_date) : ()),
140             (defined $note ? (note => $note) : ()),
141             raw => $full_line,
142             entries => [],
143             line => $line_number+1,
144             };
145 2572         5172 push @releases, $release;
146 2572         5154 @indents = ($release);
147 2572         20587 next;
148             }
149             elsif (!@indents) {
150 7561         47675 $raw_preamble .= $full_line,
151             next;
152             }
153              
154 19439 100       54205 if ( $line =~ /^[-_*+~#=\s]*$/ ) {
155 3460 100       8377 $indents[-1]{done}++
156             if @indents > 1;
157              
158 3460 50       6079 if (@indents) {
159 3460         6529 $indents[-1]{raw} .= $full_line;
160             }
161             else {
162 0         0 $releases[-1]{raw} .= $full_line;
163             }
164 3460         20881 next;
165             }
166              
167 15979         46782 $line =~ s/\s+$//;
168 15979         45652 $line =~ s/^(\s*)//;
169 15979         34048 my $indent = 1 + length _expand_tab($1);
170 15979         35070 my $change;
171             my $done;
172 15979         0 my $nest;
173 15979         21616 my $style = '';
174 15979 100       50919 if ( $line =~ /^\[\s*([^\[\]]*)\]$/ ) {
    100          
175 115         192 $done = 1;
176 115         144 $nest = 1;
177 115         223 $change = $1;
178 115         168 $style = '[]';
179 115         252 $change =~ s/\s+$//;
180             }
181             elsif ( $line =~ /^([-*+=#]+)\s+(.*)/ ) {
182 9101         16805 $style = $1;
183 9101         15868 $change = $2;
184             }
185             else {
186 6763         9946 $change = $line;
187 6763 100 100     33784 if (
      100        
      100        
188             defined $indents[-1]{text}
189             && !$indents[-1]{done}
190             && (
191             $indent > $#indents
192             || (
193             $indent == $#indents
194             && (
195             length $indents[-1]{style}
196             || $indent == 1
197             )
198             )
199             )
200             ) {
201 5613         12869 $indents[-1]{raw} .= $full_line;
202 5613         11584 $indents[-1]{text} .= " $change";
203 5613         43045 next;
204             }
205             }
206              
207 10366         14564 my $group;
208             my $nested;
209              
210 10366 100 100     44041 if ( !$nest && $indents[$indent]{nested} ) {
    100 66        
211 113         189 $nested = $group = $indents[$indent]{nested};
212             }
213             elsif ( !$nest && $indents[$indent]{nest} ) {
214 102         176 $nested = $group = $indents[$indent];
215             }
216             else {
217 10151         29162 ($group) = grep {defined} reverse @indents[ 0 .. $indent - 1 ];
  74419         121479  
218             }
219              
220 10366         43087 my $entry = {
221             text => $change,
222             line => $line_number+1,
223             done => $done,
224             nest => $nest,
225             nested => $nested,
226             style => $style,
227             raw => $full_line,
228             };
229 10366   100     14814 push @{ $group->{entries} ||= [] }, $entry;
  10366         26562  
230              
231 10366 100       21369 if ( $indent <= $#indents ) {
232 10302         17353 $#indents = $indent;
233             }
234              
235 10366         79428 $indents[$indent] = $entry;
236             }
237 129         257 my $preamble;
238 129 100       389 if (length $raw_preamble) {
239 87         388 $preamble = $raw_preamble;
240 87         309 $preamble =~ s/\A\s*\n//;
241 87         5289 $preamble =~ s/\s+\z//;
242 87         343 $preamble =~ s/\r\n?/\n/g;
243             }
244              
245 129         572 my @entries = @releases;
246 129         473 while ( my $entry = shift @entries ) {
247 12938 100       22930 push @entries, @{ $entry->{entries} } if $entry->{entries};
  3248         5800  
248 12938         15831 delete @{$entry}{qw(done nest nested)};
  12938         29270  
249             }
250             return {
251 129 100       1919 ( defined $preamble ? (preamble => $preamble) : () ),
252             raw_preamble => $raw_preamble,
253             releases => \@releases,
254             };
255             }
256              
257             my @months = qw(
258             Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
259             );
260             my %months = map {; lc $months[$_] => $_ } 0 .. $#months;
261             our ($_SHORT_MONTH) = map qr{$_}i, join '|', map quotemeta, @months;
262             our ($_SHORT_DAY) = map qr{$_}i, join '|', map quotemeta, qw(
263             Sun Mon Tue Wed Thu Fri Sat
264             );
265             our ($_UNKNOWN_DATE) = map qr{$_}i, join '|', map quotemeta, (
266             'Unknown Release Date',
267             'Unknown',
268             'Not Released',
269             'Development Release',
270             'Development',
271             'Developer Release',
272             );
273              
274             our $_LOCALTIME_DATE = qr{
275             (?:
276             (?:$_SHORT_DAY\s+)?
277             ($_SHORT_MONTH)\s+
278             |
279             ($_SHORT_MONTH)\s+
280             (?:$_SHORT_DAY\s+)
281             )
282             (\d{1,2})\s+ # date
283             (?: ([\d:]+)\s+ )? # time
284             (?: ([A-Z]+)\s+ )? # timezone
285             (\d{4}) # year
286             }x;
287              
288             our $_RFC_2822_DATE = qr{
289             $_SHORT_DAY,\s+
290             (\d{1,2})\s+
291             ($_SHORT_MONTH)\s+
292             (\d{4})\s+
293             (\d\d:\d\d:\d\d)\s+
294             ([+-])(\d{2})(\d{2})
295             }x;
296              
297             our $_DZIL_DATE = qr{
298             (\d{4}-\d\d-\d\d)\s+
299             (\d\d:\d\d(?::\d\d)?)(\s+[A-Za-z]+/[A-Za-z_-]+)
300             }x;
301              
302             our $_ISO_8601_DATE = qr{
303             \d\d\d\d # Year
304             (?:
305             [-/]\d\d # -Month
306             (?:
307             [-/]\d\d # -Day
308             (?:
309             [T\s]
310             \d\d:\d\d # Hour:Minute
311             (?:
312             :\d\d # :Second
313             (?: \.\d+ )? # .Fractional_Second
314             )?
315             (?:
316             Z # UTC
317             |
318             [+-]\d\d:\d\d # Hour:Minute TZ offset
319             (?: :\d\d )? # :Second TZ offset
320             )?
321             )?
322             )?
323             )?
324             }x;
325              
326             sub _split_date {
327 2393     2393   3699 my $note = shift;
328 2393         3443 my $date;
329             my $parsed_date;
330             # munge date formats, save the remainder as note
331 2393 50 33     8216 if (defined $note && length $note) {
332 2393         5873 $note =~ s/^[^\w\s]*\s+//;
333 2393         5067 $note =~ s/\s+$//;
334              
335             # explicitly unknown dates
336 2393 100       37627 if ( $note =~ s{^($_UNKNOWN_DATE)}{} ) {
    100          
    100          
    100          
    100          
337 16         70 $parsed_date = $date = $1;
338             }
339              
340             # handle localtime-like timestamps
341             elsif ( $note =~ s{^($_LOCALTIME_DATE)}{} ) {
342 508         1145 $date = $1;
343 508   66     3173 $parsed_date = sprintf( '%d-%02d-%02d', $7, 1+$months{lc($2 || $3)}, $4 );
344 508 100       1241 if ($5) {
345             # unfortunately ignores TZ data ($6)
346 501         1265 $parsed_date .= sprintf( 'T%sZ', $5 );
347             }
348             }
349              
350             # RFC 2822
351             elsif ( $note =~ s{^($_RFC_2822_DATE)}{} ) {
352 2         15 $date = $1;
353             $parsed_date = sprintf( '%d-%02d-%02dT%s%s%02d:%02d',
354 2         51 $4, 1+$months{lc $3}, $2, $5, $6, $7, $8 );
355             }
356              
357             # handle dist-zilla style, again ingoring TZ data
358             elsif ( $note =~ s{^($_DZIL_DATE)}{} ) {
359 7         61 $date = $1;
360 7         51 $parsed_date = sprintf( '%sT%sZ', $2, $3 );
361 7         54 $note = $4 . $note;
362             }
363              
364             # start with W3CDTF, ignore rest
365             elsif ( $note =~ s{^($_ISO_8601_DATE)}{} ) {
366 871         2692 $parsed_date = $date = $1;
367 871         2043 $parsed_date =~ s{ }{T};
368 871         1673 $parsed_date =~ s{/}{-}g;
369              
370             # Add UTC TZ if date ends at H:M, H:M:S or H:M:S.FS
371 871 100 100     4301 $parsed_date .= 'Z'
      100        
372             if length($parsed_date) == 16
373             || length($parsed_date) == 19
374             || $parsed_date =~ m{\.\d+$};
375             }
376              
377 2393         6292 $note =~ s/^\s+//;
378             }
379              
380 2393   100     15012 defined $_ && !length $_ && undef $_ for ($parsed_date, $date, $note);
      100        
381              
382 2393         7670 return ($parsed_date, $date, $note);
383             }
384              
385             sub _expand_tab {
386 15979     15979   32153 my $string = "$_[0]";
387 15979         33086 $string =~ s/([^\t]*)\t/$1 . (" " x (8 - (length $1) % 8))/eg;
  4908         18032  
388 15979         34328 return $string;
389             }
390              
391             1;
392             __END__