File Coverage

lib/Changes/Change.pm
Criterion Covered Total %
statement 98 105 93.3
branch 21 36 58.3
condition 28 47 59.5
subroutine 25 25 100.0
pod 13 15 86.6
total 185 228 81.1


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## Changes file management - ~/lib/Changes/Change.pm
3             ## Version v0.1.2
4             ## Copyright(c) 2023 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2022/11/23
7             ## Modified 2025/07/28
8             ## All rights reserved
9             ##
10             ##
11             ## This program is free software; you can redistribute it and/or modify it
12             ## under the same terms as Perl itself.
13             ##----------------------------------------------------------------------------
14             package Changes::Change;
15             BEGIN
16             {
17 19     19   92435 use strict;
  19         36  
  19         698  
18 19     19   83 use warnings;
  19         35  
  19         974  
19 19     19   95 use warnings::register;
  19         27  
  19         955  
20 19     19   658 use parent qw( Module::Generic );
  19         452  
  19         131  
21 19     19   240733 use vars qw( $VERSION );
  19         46  
  19         1057  
22 19     19   431 our $VERSION = 'v0.1.2';
23             };
24              
25 19     19   125 use strict;
  19         36  
  19         417  
26 19     19   126 use warnings;
  19         38  
  19         10678  
27              
28             sub init
29             {
30 47     47 1 217045 my $self = shift( @_ );
31 47         1365 $self->{line} = undef;
32 47         134 $self->{marker} = undef;
33 47         176 $self->{max_width} = 0;
34 47         180 $self->{nl} = "\n";
35 47         158 $self->{raw} = undef;
36 47         169 $self->{spacer1} = undef;
37 47         143 $self->{spacer2} = undef;
38 47         116 $self->{text} = undef;
39 47         139 $self->{wrapper} = undef;
40 47         124 $self->{_init_strict_use_sub} = 1;
41 47 50       364 $self->SUPER::init( @_ ) || return( $self->pass_error );
42 47         459164 $self->{_reset} = 1;
43 47         143 $self->{_reset_normalise} = 1;
44 47         153 return( $self );
45             }
46              
47             sub as_string
48             {
49 49     49 1 6570 my $self = shift( @_ );
50 49 50 66     324 if( !exists( $self->{_reset} ) ||
      33        
51             !defined( $self->{_reset} ) ||
52             !CORE::length( $self->{_reset} ) )
53             {
54 43 100 66     637 if( exists( $self->{_cache_value} ) &&
    50 66        
      33        
55             defined( $self->{_cache_value} ) &&
56             length( $self->{_cache_value} ) )
57             {
58 4         29 return( $self->{_cache_value} );
59             }
60             elsif( defined( $self->{raw} ) && length( "$self->{raw}" ) )
61             {
62 39         516 return( $self->{raw} );
63             }
64             }
65 6         24 my $nl = $self->nl;
66 6   50     9382 my $str = $self->new_scalar( ( $self->spacer1 // '' ) . ( $self->marker // '-' ) . ( $self->spacer2 // '' ) );
      100        
      50        
67 6         10007 my $max = $self->max_width;
68 6 100 100     147885 if( $max > 0 && ( $self->normalise->length + $str->length ) > $max )
69             {
70 1         68158 my $text;
71 1 100       5 my @spaces = map{ $_ eq "\t" ? "\t" : ' ' } split( //, "$str" );
  3         18  
72 1         5 my $sep = join( '', @spaces );
73 1         8 my $wrapper = $self->wrapper;
74 1 50       791 if( $self->_is_code( $wrapper ) )
    50          
    0          
75             {
76             # try-catch
77 0         0 local $@;
78             $text = eval
79 0         0 {
80 0         0 $wrapper->( $self->normalise->scalar, ( $max - $str->length ) );
81             };
82 0 0       0 if( $@ )
83             {
84 0 0       0 warn( "Warning only: an error occurred while calling the wrapper calback with ", $self->normalise->length, " bytes of change text and a maximum width of ", ( $max - $str->length ), " characters: $@\n" ) if( $self->_warnings_is_enabled );
85             }
86             }
87             elsif( $self->_load_class( 'Text::Wrap' ) )
88             {
89             # Silence the use of $Text::Wrap::columns used once
90 19     19   137 no warnings 'once';
  19         30  
  19         19906  
91             # We need to reduce $max by as much indentation there is
92 1         4353 local $Text::Wrap::columns = ( $max - $str->length );
93 1         22962 $text = Text::Wrap::wrap( '', '', $self->normalise->scalar );
94             }
95             elsif( $self->_load_class( 'Text::Format' ) )
96             {
97 0         0 my $fmt = Text::Format->new({
98             columns => ( $max - $str->length ),
99             extraSpace => 0,
100             firstIndent => 0,
101             });
102 0         0 $text = $fmt->format( $self->normalise->scalar );
103             }
104            
105 1 50 33     9683 if( defined( $text ) && length( "$text" ) )
106             {
107 1         13 $str->append( join( "\n$sep", split( /\r?\n/, "$text" ) ) );
108             }
109             }
110             else
111             {
112 5         92065 $str->append( $self->normalise );
113             }
114 6         131 $str->append( $nl );
115 6         11259 $self->{_cache_value} = $str;
116 6         19 CORE::delete( $self->{_reset} );
117 6         43 return( $str );
118             }
119              
120             sub freeze
121             {
122 41     41 0 87 my $self = shift( @_ );
123 41         198 CORE::delete( @$self{qw( _reset _reset_normalise )} );
124 41         183 return( $self );
125             }
126              
127 41     41 1 201684 sub line { return( shift->reset(@_)->_set_get_number( 'line', @_ ) ); }
128              
129 55     55 1 586992 sub marker { return( shift->reset(@_)->_set_get_scalar_as_object( 'marker', @_ ) ); }
130              
131 51     51 1 269900 sub max_width { return( shift->_set_get_number( 'max_width', @_ ) ); }
132              
133 55     55 1 381949 sub nl { return( shift->reset(@_)->_set_get_scalar_as_object( 'nl', @_ ) ); }
134              
135             sub normalise
136             {
137 10     10 1 2971 my $self = shift( @_ );
138 10 100 33     130 if( (
      100        
      66        
139             !exists( $self->{_reset_normalise} ) ||
140             !defined( $self->{_reset_normalise} ) ||
141             !CORE::length( $self->{_reset_normalise} )
142             ) && exists( $self->{_normalised} ) &&
143             $self->_is_a( $self->{_normalised} => 'Module::Generic::Scalar' ) )
144             {
145 2         109 return( $self->{_normalised} );
146             }
147 8         62 my $str = $self->text->clone;
148 8 50       6764 return( $str ) if( $str->is_empty );
149 8 100       94 if( $str->index( "\n" ) != -1 )
150             {
151 2         49954 $str->replace( qr/[[:blank:]\h]*\n[[:blank:]\h]*/ => ' ' );
152             }
153 8         231857 $self->{_normalised} = $str;
154 8         15906 CORE::delete( $self->{_reset_normalise} );
155 8         63 return( $str );
156             }
157              
158             sub prefix
159             {
160 4     4 1 3323 my $self = shift( @_ );
161 4   50     19 my $s = ( $self->spacer1 // '' ) . ( $self->marker // '' ) . ( $self->spacer2 // '' );
      50        
      50        
162 4         4111 return( $self->new_scalar( \$s ) );
163             }
164              
165 46     46 1 495104 sub raw { return( shift->_set_get_scalar_as_object( 'raw', @_ ) ); }
166              
167             sub reset
168             {
169 366     366 0 948 my $self = shift( @_ );
170 366 100 33     3685 if( (
      100        
171             !exists( $self->{_reset} ) ||
172             !defined( $self->{_reset} ) ||
173             !CORE::length( $self->{_reset} )
174             ) && scalar( @_ ) )
175             {
176 47         204 $self->{_reset} = scalar( @_ );
177 47         262 $self->{_reset_normalise} = 1;
178             }
179 366         1931 return( $self );
180             }
181              
182             # space before the marker
183 58     58 1 499774 sub spacer1 { return( shift->reset(@_)->_set_get_scalar_as_object( 'spacer1', @_ ) ); }
184              
185             # space after the marker
186 58     58 1 815675 sub spacer2 { return( shift->reset(@_)->_set_get_scalar_as_object( 'spacer2', @_ ) ); }
187              
188 99     99 1 570020 sub text { return( shift->reset(@_)->_set_get_scalar_as_object( 'text', @_ ) ); }
189              
190             # We do not use the reset here, because just setting a wrap callback has no direct impact on the output
191 1     1 1 21 sub wrapper { return( shift->_set_get_code( 'wrapper', @_ ) ); }
192              
193             sub DESTROY
194             {
195             # <https://perldoc.perl.org/perlobj#Destructors>
196 6     6   12869 CORE::local( $., $@, $!, $^E, $? );
197 6         16 my $self = CORE::shift( @_ );
198 6 50       27 CORE::return if( !CORE::defined( $self ) );
199 6 50       65 CORE::return if( ${^GLOBAL_PHASE} eq 'DESTRUCT' );
200             };
201              
202             1;
203             # NOTE: POD
204             __END__
205              
206             =encoding utf-8
207              
208             =head1 NAME
209              
210             Changes::Change - Changes object class
211              
212             =head1 SYNOPSIS
213              
214             use Changes::Change;
215             my $this = Changes::Change->new(
216             line => 12,
217             marker => '-',
218             max_width => 68,
219             spacer1 => "\t",
220             # Defaults to just one space
221             spacer2 => undef,
222             text => "This is a change note",
223             wrapper => sub
224             {
225             my( $text, $width ) = @_;
226             require Text::Wrap;
227             local $Text::Wrap::columns = $width;
228             my $result = Text::Wrap::wrap( '', '', "$text" );
229             return( $result );
230             }
231             ) || die( Changes::Change->error, "\n" );
232              
233             =head1 VERSION
234              
235             v0.1.2
236              
237             =head1 DESCRIPTION
238              
239             This represents a change line within a release. A change line is usually represented by some indentation spaces, followed by a marker such as a dash, a space and a text:
240              
241             - This is a change note
242              
243             A change text can be written on a very long line or broken into lines of C<max_width>. You can change this value with L</max_width> and by default it is 0, which means it will be all on one line.
244              
245             =head1 METHODS
246              
247             =head2 as_string
248              
249             Returns a L<scalar object|Module::Generic::Scalar> of the change line. This information is cached unless other information has been changed.
250              
251             Also, if nothing was changed and L</raw> is set with a value, that value will be returned instead.
252              
253             If L</wrapper> is defined, the perl code reference set will be called by providing it the text of the change and the adjusted width to use. The actual width is the width of the change text with any leading spaces and characters as specified with L</spacer1>, L</spacer2> and L</marker>.
254              
255             If the callback dies, this exception will be caught and displayed as a warning if C<use warnings> is enabled.
256              
257             If no callback is specified, it will attempt to load L<Text::Wrap> (a perl core module) and L<Text::Format> in this order.
258              
259             If none of it is possible, the change text will simply not be wrapped.
260              
261             If an error occurred, it returns an L<error|Module::Generic/error>
262              
263             The resulting string is terminated by the carriage return sequence defined with L</nl>
264              
265             It returns a L<scalar object|Module::Generic::Scalar>
266              
267             =for Pod::Coverage freeze
268              
269             =head2 line
270              
271             Sets or gets an integer representing the line number where this line containing the change information was found in the original C<Changes> file. If this object was instantiated separately, then obviously this value will be C<undef>
272              
273             =head2 marker
274              
275             Sets or gets the character representing the marker preceding the text of the change. This is usually a dash.
276              
277             It returns a L<scalar object|Module::Generic::Scalar>
278              
279             =head2 max_width
280              
281             Sets or gets the change line maximum width. The line width includes any spaces and characters at the beginning of the line, as set with L</spacer1>, L</spacer2> and L</marker> and not just the text of the change itself.
282              
283             It returns a L<number object|Module::Generic::Number>
284              
285             =head2 nl
286              
287             Sets or gets the new line character, which defaults to C<\n>
288              
289             It returns a L<number object|Module::Generic::Number>
290              
291             =head2 normalise
292              
293             This returns a "normalised" version of the change text, which means that if the change text is wrapped and has new lines with possibly preceding and trailing spaces, those will be replaced by a single space.
294              
295             It does not modify the original change text.
296              
297             It returns a L<scalar object|Module::Generic::Scalar>
298              
299             =head2 prefix
300              
301             Read-only. This returns what precedes the text of the change, which is an optional leading space, and a marker such as a dash.
302              
303             It returns a L<scalar object|Module::Generic::Scalar>
304              
305             =head2 raw
306              
307             Sets or gets the raw version of the line as found in the C<Changes> file. If set and nothing has been changed, this will be returned by L</as_string> instead of computing the formatting of the change.
308              
309             It returns a L<scalar object|Module::Generic::Scalar>
310              
311             =for Pod::Coverage reset
312              
313             =head2 spacer1
314              
315             Sets or gets the leading space, if any, found before the marker.
316              
317             It returns a L<scalar object|Module::Generic::Scalar>
318              
319             =head2 spacer2
320              
321             Sets or gets the space found after the marker and before the text of the change.
322              
323             It returns a L<scalar object|Module::Generic::Scalar>
324              
325             =head2 text
326              
327             Sets or gets the text o the change. If the text is broken into multiple lines in the C<Changes> file, it will be collected as on L<scalar object|Module::Generic::Scalar> here.
328              
329             It returns a L<scalar object|Module::Generic::Scalar>
330              
331             =head2 wrapper
332              
333             Sets or gets a code reference as a callback mechanism to return a properly wrapped change text. This allows flexibility beyond the default use of L<Text::Wrap> and L<Text::Format>
334              
335             =head1 AUTHOR
336              
337             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
338              
339             =head1 SEE ALSO
340              
341             L<Changes>, L<Changes::Release>, L<Changes::Group>, L<Changes::Version>, L<Changes::NewLine>
342              
343             =head1 COPYRIGHT & LICENSE
344              
345             Copyright(c) 2022 DEGUEST Pte. Ltd.
346              
347             All rights reserved
348              
349             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
350              
351             =cut