File Coverage

blib/lib/CPAN/Changes/Parser/KeepAChangeLog.pm
Criterion Covered Total %
statement 47 47 100.0
branch 19 22 86.3
condition 1 2 50.0
subroutine 5 5 100.0
pod 1 1 100.0
total 73 77 94.8


line stmt bran cond sub pod time code
1             package CPAN::Changes::Parser::KeepAChangeLog;
2              
3 1     1   146416 use strict;
  1         2  
  1         40  
4 1     1   6 use warnings;
  1         3  
  1         86  
5              
6             our $VERSION = '0.1.1';
7              
8 1     1   721 use Moo;
  1         9861  
  1         6  
9             extends 'CPAN::Changes::Parser';
10              
11             has '+version_like' => (
12             is => 'ro',
13             default => sub { qr/Unreleased/i },
14             );
15              
16             sub parse_string {
17 2     2 1 29390 my ($self, $string, @rest) = @_;
18 2 50       12 return undef unless defined $string;
19              
20 2         11 my $transformed = _kac_to_cpan_changes_spec($string);
21 2 100       10 return undef unless defined $transformed;
22              
23 1         27 return $self->SUPER::parse_string($transformed, @rest);
24             }
25              
26             # Transform Keep a Changelog 1.1.0-ish Markdown into CPAN::Changes::Spec-ish text.
27             # Returns transformed string, or undef if it doesn't look like KaC / can't be transformed.
28             sub _kac_to_cpan_changes_spec {
29 2     2   6 my ($in) = @_;
30              
31             # Quick “does this plausibly look like Keep a Changelog?” gate.
32             # We require at least one release heading of the KaC form.
33 2 100       21 return undef unless $in =~ /^\s*##\s+\[(?:Unreleased|[^\]]+)\]/m;
34              
35 1         3 my @out;
36 1         3 my $saw_release = 0;
37              
38 1         26 for my $line (split /\n/, $in, -1) {
39             # Drop CR if file is CRLF
40 18         38 $line =~ s/\r\z//;
41              
42             # Drop KaC link reference definitions (commonly at bottom), e.g.:
43             # [1.0.0]: https://example/compare/v0.9.0...v1.0.0
44             # [Unreleased]: https://example/compare/v1.0.0...HEAD
45 18 100       56 if ($line =~ /^\s*\[[^\]]+\]:\s+\S+/) {
46 2         4 next;
47             }
48              
49             # Release headings:
50             # ## [1.1.0] - 2024-01-31
51             # ## [1.1.0]
52             # ## [Unreleased]
53 16 100       69 if ($line =~ /^\s*##\s+\[([^\]]+)\]\s*(?:-\s*([0-9]{4}-[0-9]{2}-[0-9]{2}))?\s*$/) {
54 2         10 my ($ver, $date) = ($1, $2);
55              
56             # Normalise Unreleased to a CPAN::Changes-ish release line with an allowed “date” token.
57 2 100       9 if ($ver =~ /\AUnreleased\z/i) {
58 1         3 push @out, "Unreleased Not Released";
59             }
60             else {
61             # If date is missing, KaC is still KaC, but CPAN::Changes parser expects something date-like.
62             # We'll accept missing date and set it to "Unknown" (allowed by CPAN::Changes::Spec).
63 1   50     3 $date //= 'Unknown';
64 1         19 push @out, "$ver $date";
65             }
66              
67 2         6 $saw_release = 1;
68 2         6 next;
69             }
70              
71             # Category headings:
72             # ### Added
73             # ### Fixed
74             # Map to CPAN group marker:
75             # [Added]
76 14 100       41 if ($line =~ /^\s*###\s+(.+?)\s*$/) {
77 3         9 my $group = $1;
78              
79             # Be conservative: ignore empty/odd headings that are likely Markdown scaffolding.
80 3         7 $group =~ s/\s+\z//;
81 3         7 $group =~ s/\A\s+//;
82              
83             # If it’s something like “Links” or “Changelog” we still *could* treat it as a group,
84             # but KaC categories are the main use. We'll accept any text, as CPAN::Changes groups are free-form.
85 3         12 push @out, "[$group]";
86 3         8 next;
87             }
88              
89             # Top-level title:
90             # # Changelog
91             # Treat as preamble. CPAN::Changes parser supports preamble, so keep it (as plain text).
92 11 100       32 if ($line =~ /^\s*#\s+(.+?)\s*$/) {
93 1         6 push @out, $1;
94 1         3 next;
95             }
96              
97             # Keep bullets but normalise to "- " for consistency.
98             # Preserve indentation for nested bullets.
99 10 100       31 if ($line =~ /^(\s*)[*-]\s+(.*)$/) {
100 3         14 push @out, $1 . "- " . $2;
101 3         7 next;
102             }
103              
104             # Otherwise: pass through line as-is.
105 7         16 push @out, $line;
106             }
107              
108 1 50       8 return undef unless $saw_release;
109              
110             # Basic sanity: after transform, we should still have at least one release line
111             # that the base parser is likely to recognise. We can't call its private regex here,
112             # but we can assert we emitted something release-like.
113 1         6 my $out = join("\n", @out);
114 1 50       32 return undef unless $out =~ /^\s*(?:Unreleased|[0-9A-Za-z_.]+)\s+(?:\d{4}-\d{2}-\d{2}|Unknown|Not Released)\b/m;
115              
116 1         8 return $out;
117             }
118              
119             1;
120              
121             __END__