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 31     31   434069 use strict;
  31         66  
  31         1269  
3 31     31   154 use warnings;
  31         65  
  31         3419  
4              
5             our $VERSION = '0.500005';
6             $VERSION =~ tr/_//d;
7              
8 31     31   17111 use Module::Runtime qw(use_module);
  31         63614  
  31         239  
9 31     31   2202 use Carp qw(croak);
  31         57  
  31         2348  
10 31     31   20263 use Encode qw(decode FB_CROAK LEAVE_SRC);
  31         643798  
  31         4205  
11              
12 31     31   3097 use Moo;
  31         42649  
  31         356  
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 38     38 1 708 my ($self, $string) = @_;
38 38         178 $self->_transform($self->_parse($string));
39             }
40              
41             sub parse_file {
42 29     29 1 1558 my ($self, $file, $layers) = @_;
43 29 50       131 my $mode = defined $layers ? "<$layers" : '<:raw';
44 29 50       2159 open my $fh, $mode, $file or croak "Can't open $file: $!";
45 29         94 my $content = do { local $/; <$fh> };
  29         173  
  29         2130  
46 29 50       529 if (!defined $layers) {
47             # if it's valid UTF-8, decode that. otherwise, assume latin 1 and leave it.
48 29         138 eval { $content = decode('UTF-8', $content, FB_CROAK | LEAVE_SRC) };
  29         480  
49             }
50 29         2381 $self->parse_string($content);
51             }
52              
53             sub _transform {
54 38     38   127 my ($self, $data) = @_;
55              
56 38         199 my $release_class = $self->_release_class;
57 38         174 my $entry_class = $self->_entry_class;
58              
59             $self->_changelog_class->new(
60             (defined $data->{preamble} ? (preamble => $data->{preamble}) : ()),
61             releases => [
62             map {
63 220         21497 my $r = $_;
64             $release_class->new(
65 1100 100       3807 (map { defined $r->{$_} ? ($_ => $r->{$_}) : () }
66             qw(version line date raw_date note)),
67             ($_->{entries} ? (
68             entries => [
69 220 50       602 map { _trans_entry($entry_class, $_) } @{$_->{entries}},
  856         52386  
  220         845  
70             ],
71             ) : () ),
72             )
73 38 100       230 } reverse @{$data->{releases}},
  38         182  
74             ],
75             );
76             }
77              
78             sub _trans_entry {
79 1294     1294   3037 my ($entry_class, $entry) = @_;
80              
81             $entry_class->new(
82             line => $entry->{line},
83             text => $entry->{text},
84             $entry->{entries} ? (
85             entries => [
86 1294 100       32301 map { _trans_entry($entry_class, $_) } @{$entry->{entries}},
  438         24127  
  86         223  
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 130     130   632009 my ($self, $string) = @_;
107              
108 130         867 my $version_prefix = qr/version|revision/i;
109 130 50       945 if (defined(my $vp = $self->version_prefix)) {
110 0         0 $version_prefix = qr/$version_prefix|$vp/
111             }
112 130         3204 my $version_token = qr/$VERSION_REGEX(?:-TRIAL)?/;
113 130 100       948 if (defined(my $vt = $self->version_like)) {
114 101         1001 $version_token = qr/$version_token|$vt/
115             }
116              
117 130         375 my $raw_preamble = '';
118 130         386 my @releases;
119             my @indents;
120 130         303 my $line_number = -1;
121 130         1792 while ($string =~ /((.*?)(?:\r\n?|\n|\z))/g) {
122 29732         74467 my ($full_line, $line) = ($1, $2);
123             last
124 29732 100       56370 if !length $full_line;
125 29602         39693 $line_number++;
126              
127 29602 100       213046 if ( $line =~ /^(?:$version_prefix\s+)?($version_token)(?:[:;.-]?\s+(.*))?$/i ) {
    100          
128 2580         5242 my $version = $1;
129 2580         4375 my $note = $2;
130 2580         3974 my $date;
131             my $raw_date;
132 2580 100       5880 if (defined $note) {
133 2401         4970 ($date, $raw_date, $note) = _split_date($note);
134             }
135              
136 2580 100       18465 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 2580         5239 push @releases, $release;
146 2580         6017 @indents = ($release);
147 2580         30818 next;
148             }
149             elsif (!@indents) {
150 7564         58782 $raw_preamble .= $full_line,
151             next;
152             }
153              
154 19458 100       49575 if ( $line =~ /^[-_*+~#=\s]*$/ ) {
155 3467 100       8241 $indents[-1]{done}++
156             if @indents > 1;
157              
158 3467 50       6027 if (@indents) {
159 3467         6625 $indents[-1]{raw} .= $full_line;
160             }
161             else {
162 0         0 $releases[-1]{raw} .= $full_line;
163             }
164 3467         25504 next;
165             }
166              
167 15991         53057 $line =~ s/\s+$//;
168 15991         48482 $line =~ s/^(\s*)//;
169 15991         31361 my $indent = 1 + length _expand_tab($1);
170 15991         36501 my $change;
171             my $done;
172 15991         0 my $nest;
173 15991         26042 my $style = '';
174 15991 100       49192 if ( $line =~ /^\[\s*([^\[\]]*)\]$/ ) {
    100          
175 115         190 $done = 1;
176 115         168 $nest = 1;
177 115         187 $change = $1;
178 115         166 $style = '[]';
179 115         250 $change =~ s/\s+$//;
180             }
181             elsif ( $line =~ /^([-*+=#]+)\s+(.*)/ ) {
182 9106         15512 $style = $1;
183 9106         15715 $change = $2;
184             }
185             else {
186 6770         9581 $change = $line;
187 6770 100 100     35223 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 5615         12095 $indents[-1]{raw} .= $full_line;
202 5615         11081 $indents[-1]{text} .= " $change";
203 5615         49934 next;
204             }
205             }
206              
207 10376         15794 my $group;
208             my $nested;
209              
210 10376 100 100     50747 if ( !$nest && $indents[$indent]{nested} ) {
    100 66        
211 113         199 $nested = $group = $indents[$indent]{nested};
212             }
213             elsif ( !$nest && $indents[$indent]{nest} ) {
214 102         206 $nested = $group = $indents[$indent];
215             }
216             else {
217 10161         31886 ($group) = grep {defined} reverse @indents[ 0 .. $indent - 1 ];
  74454         118571  
218             }
219              
220 10376         57694 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 10376   100     18382 push @{ $group->{entries} ||= [] }, $entry;
  10376         27175  
230              
231 10376 100       22774 if ( $indent <= $#indents ) {
232 10312         18946 $#indents = $indent;
233             }
234              
235 10376         97055 $indents[$indent] = $entry;
236             }
237 130         242 my $preamble;
238 130 100       419 if (length $raw_preamble) {
239 88         278 $preamble = $raw_preamble;
240 88         462 $preamble =~ s/\A\s*\n//;
241 88         10309 $preamble =~ s/\s+\z//;
242 88         408 $preamble =~ s/\r\n?/\n/g;
243             }
244              
245 130         949 my @entries = @releases;
246 130         555 while ( my $entry = shift @entries ) {
247 12956 100       25146 push @entries, @{ $entry->{entries} } if $entry->{entries};
  3256         6830  
248 12956         16062 delete @{$entry}{qw(done nest nested)};
  12956         35818  
249             }
250             return {
251 130 100       2190 ( 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 2401     2401   3827 my $note = shift;
328 2401         3399 my $date;
329             my $parsed_date;
330             # munge date formats, save the remainder as note
331 2401 50 33     7800 if (defined $note && length $note) {
332 2401         5756 $note =~ s/^[^\w\s]*\s+//;
333 2401         5766 $note =~ s/\s+$//;
334              
335             # explicitly unknown dates
336 2401 100       56076 if ( $note =~ s{^($_UNKNOWN_DATE)}{} ) {
    100          
    100          
    100          
    100          
337 16         40 $parsed_date = $date = $1;
338             }
339              
340             # handle localtime-like timestamps
341             elsif ( $note =~ s{^($_LOCALTIME_DATE)}{} ) {
342 508         1037 $date = $1;
343 508   66     3300 $parsed_date = sprintf( '%d-%02d-%02d', $7, 1+$months{lc($2 || $3)}, $4 );
344 508 100       1093 if ($5) {
345             # unfortunately ignores TZ data ($6)
346 501         1476 $parsed_date .= sprintf( 'T%sZ', $5 );
347             }
348             }
349              
350             # RFC 2822
351             elsif ( $note =~ s{^($_RFC_2822_DATE)}{} ) {
352 2         10 $date = $1;
353             $parsed_date = sprintf( '%d-%02d-%02dT%s%s%02d:%02d',
354 2         31 $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         27 $date = $1;
360 7         43 $parsed_date = sprintf( '%sT%sZ', $2, $3 );
361 7         40 $note = $4 . $note;
362             }
363              
364             # start with W3CDTF, ignore rest
365             elsif ( $note =~ s{^($_ISO_8601_DATE)}{} ) {
366 875         2249 $parsed_date = $date = $1;
367 875         2171 $parsed_date =~ s{ }{T};
368 875         1524 $parsed_date =~ s{/}{-}g;
369              
370             # Add UTC TZ if date ends at H:M, H:M:S or H:M:S.FS
371 875 100 100     4842 $parsed_date .= 'Z'
      100        
372             if length($parsed_date) == 16
373             || length($parsed_date) == 19
374             || $parsed_date =~ m{\.\d+$};
375             }
376              
377 2401         5960 $note =~ s/^\s+//;
378             }
379              
380 2401   100     18330 defined $_ && !length $_ && undef $_ for ($parsed_date, $date, $note);
      100        
381              
382 2401         8172 return ($parsed_date, $date, $note);
383             }
384              
385             sub _expand_tab {
386 15991     15991   31610 my $string = "$_[0]";
387 15991         30611 $string =~ s/([^\t]*)\t/$1 . (" " x (8 - (length $1) % 8))/eg;
  4908         14970  
388 15991         34377 return $string;
389             }
390              
391             1;
392             __END__