package CPAN::Changes; use strict; use warnings; our $VERSION = '0.500005'; $VERSION =~ tr/_//d; use Sub::Quote qw(qsub); use Types::Standard qw(ArrayRef HashRef InstanceOf); use CPAN::Changes::Release; use Moo; my $release_type = (InstanceOf['CPAN::Changes::Release'])->plus_coercions( HashRef ,=> qsub q{ CPAN::Changes::Release->new($_[0]) }, ); has _releases => ( is => 'rw', init_arg => 'releases', isa => ArrayRef[$release_type], coerce => 1, default => qsub q{ [] }, ); has preamble => ( is => 'rw', default => '', ); sub clone { my $self = shift; my %attrs = %$self; $attrs{releases} = [ map $_->clone, @{delete $self->{_releases}} ]; return (ref $self)->new(%attrs, @_); } # backcompat sub releases { my ($self, @args) = @_; if (@args > 1 or @args == 1 && ref $args[0] ne 'ARRAY') { @args = [ @args ]; } @{ $self->_releases(@args) }; } sub _numify_version { my $version = shift; $version = _fix_version($version); $version =~ s/_//g; if ($version =~ s/^v//i || $version =~ tr/.// > 1) { my @parts = split /\./, $version; my $n = shift @parts; $version = sprintf(join('.', '%s', ('%03s' x @parts)), $n, @parts); } $version += 0; return $version; } sub _fix_version { my $version = shift; return 0 unless defined $version; my $v = ($version =~ s/^v//i); $version =~ s/[^\d\._].*//; $version =~ s/\.[._]+/./; $version =~ s/[._]*_[._]*/_/g; $version =~ s/\.{2,}/./g; $v ||= $version =~ tr/.// > 1; $version ||= 0; return (($v ? 'v' : '') . $version); } sub find_release { my ($self, $version) = @_; my ($release) = grep { $_->version eq $version } @{ $self->_releases }; return $release if $release; $version = _numify_version($version) || return undef; ($release) = grep { _numify_version($_->version) == $version } @{ $self->_releases }; return $release; } sub reversed { my ($self) = @_; return $self->clone(releases => [ reverse @{ $self->_releases } ]); } sub serialize { my ($self, %opts) = @_; if ($opts{reverse}) { $self = $self->reversed; } my $width = $opts{width} || 75; my @styles = @{ $opts{styles} || ['', '[]', '-', '*'] }; my @indents = @{ $opts{indents} || ['', ' ', ''] }; my $out = $self->preamble || ''; $out .= "\n\n" if $out; for my $release (reverse @{$self->_releases}) { my $styles = \@styles; my $indents = \@indents; if ( !$opts{styles} and ( grep { length($styles->[1]) > 1 && length($indents->[0] . $styles->[1] . $_->text) > $width } @{ $release->entries } or !grep { $_->has_entries } @{ $release->entries } ) ) { $styles = [ '', '-', '*' ]; } $out .= "\n" unless $out eq '' || $out =~ /\n\n\z/; $out .= $release->serialize( %opts, indents => $indents, styles => $styles, width => $width - length $indents->[0], ); } return $out; } require CPAN::Changes::Parser; # :( i know people use these our $W3CDTF_REGEX = $CPAN::Changes::Parser::_ISO_8601_DATE; our $UNKNOWN_VALS = $CPAN::Changes::Parser::_UNKNOWN_DATE; sub load { my ($class, $filename, %args) = @_; $args{version_like} = $args{next_token} if exists $args{next_token}; require CPAN::Changes::Parser; CPAN::Changes::Parser->new(%args)->parse_file($filename); } sub load_string { my ($class, $string, %args) = @_; $args{version_like} = $args{next_token} if exists $args{next_token}; require CPAN::Changes::Parser; CPAN::Changes::Parser->new(%args)->parse_string($string); } sub add_release { my ($self, @new_releases) = @_; @new_releases = map { $release_type->coerce($_) } @new_releases; my @releases = @{ $self->_releases }; for my $new_release (@new_releases) { my $version = _numify_version($new_release->version); for my $release (@releases) { if (_numify_version($release->version) == $version) { $release = $new_release; undef $new_release; } } } push @releases, grep { defined } @new_releases; $self->_releases(\@releases); return 1; } sub delete_release { my ($self, @versions) = @_; my @releases = @{ $self->_releases }; for my $version (map { _numify_version($_) } @versions) { @releases = grep { _numify_version($_->version) != $version } @releases; } $self->_releases(\@releases); } sub release { my ($self, $version) = @_; $self->find_release($version); } sub delete_empty_groups { my ($self) = @_; for my $release ( @{ $self->_releases } ) { $release->delete_empty_groups; } } 1; __END__ =head1 NAME CPAN::Changes - Parser for CPAN style change logs =head1 SYNOPSIS use CPAN::Changes; my $changes = CPAN::Changes->load('Changes'); $changes->release('0.01'); =head1 DESCRIPTION It is standard practice to include a Changes file in your distribution. The purpose the Changes file is to help a user figure out what has changed since the last release. People have devised many ways to write the Changes file. A preliminary specification has been created (L) to encourage module authors to write clear and concise Changes. This module will help users programmatically read and write Changes files that conform to the specification. =head1 METHODS =head2 new ( %args ) Creates a B object. =head3 %args =over 4 =item preamble The preamble section of the changelog. =item releases An arrayref of L objects. =back =head2 load ( $filename, %args ) Creates a new B object by parsing the given file via L. =head2 load_string ( $filename, %args ) Creates a new B object by parsing the given string via L. =head2 preamble ( [ $preamble ] ) Gets or sets the preamble section. =head2 releases ( [ @releases ] ) Gets or sets the list of releases as L objects. =head2 add_release ( @releases ) Adds the given releases to the change log. If a release of the same version exists, it will be overwritten. =head2 delete_release ( @versions ) Removes the given versions from change log. =head2 find_release ( $version ) Finds a release with the given version. =head2 reversed Returns a new B object with the releases in the opposite order. =head2 clone ( %attrs ) Returns a new C object with the given attributes changed. =head2 serialize ( %options ) Returns the change log as a string suitable for saving as a F file. =over 4 =item width The width to wrap lines at. By default, lines will be wrapped at 75 characters. =item styles An array reference of styles to use when outputting the entries, one for each level of change. The first entry is used for the release entry itself. The styles can be either a single character to prefix change lines or two characters to use as a prefix and suffix. =item indents An array reference of indent strings to use when outputting the entries. =item reverse (legacy) If true, releases will be output in reversed order. =item group_sort (legacy) A code reference used to sort the groups in the releases. =back =head1 LEGACY METHODS =head2 delete_empty_groups Removes empty groups. =head2 release An alias for find_release. =head1 AUTHOR haarg - Graham Knop (cpan:HAARG) =head1 CONTRIBUTORS Brian Cassidy =head1 COPYRIGHT Copyright (c) 2011-2015 the CPAN::Changes L and L as listed above. =head1 LICENSE This library is free software and may be distributed under the same terms as perl itself. See L. =cut