File Coverage

blib/lib/Text/Buffer.pm
Criterion Covered Total %
statement 268 317 84.5
branch 59 96 61.4
condition 34 76 44.7
subroutine 41 45 91.1
pod 34 34 100.0
total 436 568 76.7


line stmt bran cond sub pod time code
1             package Text::Buffer;
2              
3 3     3   45701 use strict;
  3         8  
  3         105  
4 3     3   14 use vars qw($VERSION $DEBUG);
  3         6  
  3         154  
5              
6 3     3   14 use Carp;
  3         11  
  3         279  
7              
8             BEGIN {
9 3     3   19 $VERSION = '0.4';
10 3         11113 $DEBUG = 1;
11             }
12              
13             sub new {
14 4     4 1 19 my $proto = shift;
15 4   33     24 my $class = ref($proto) || $proto;
16 4         29 my $self = {
17             _debug => 0,
18             _buffer => [],
19             _currline => 0,
20             _modified => 0,
21             _autonewline => "unix",
22             _newline => "\n"
23             };
24              
25 4         12 bless( $self, $class );
26              
27 4         13 my %opts = @_;
28 4 50       13 if ($opts{debug}) { $self->{_debug} = $opts{debug}}
  0         0  
29 4         17 $self->_debug("Instantiated new object $class");
30 4 100       13 if ( $opts{file} ) {
    50          
31 3         7 $self->{file} = $opts{file};
32 3         10 $self->load();
33             }
34             elsif ( $opts{array} ) {
35 0 0       0 if ( ref( $opts{array} ) eq "ARRAY" ) {
36 0         0 foreach ( @{ $opts{array} } ) {
  0         0  
37 0         0 $self->append($_);
38             }
39             }
40 0         0 $self->setModified(1);
41             }
42 4         10 foreach (qw(autonewline)) {
43 4 100       22 $self->{"_$_"} = $opts{$_} if exists($opts{$_});
44             }
45              
46 4         16 return $self;
47             }
48              
49             sub load {
50 3     3 1 5 my $self = shift;
51 3   33     21 my $file = shift || $self->{file};
52 3 50       9 if ( !$file ) {
53 0         0 $self->_setError("No file to load specified");
54 0         0 return undef;
55             }
56 3         11 $self->_debug("Loading file $file");
57 3 50       199 if ( open( FIL, $file ) ) {
58 3         17 $self->_debug("clearing buffer and adding $file to buffer");
59 3         10 $self->clear();
60 3         71 while () {
61 19         41 $self->append($_);
62             }
63 3         36 close(FIL);
64 3         13 $self->_clearModified();
65 3         8 return 1;
66             }
67             else {
68 0         0 $self->_setError("Failed to load file $file");
69 0         0 return undef;
70             }
71 0         0 return 0;
72             }
73              
74             sub save {
75 3     3 1 7 my $self = shift;
76 3   33     11 my $file = shift || $self->{file};
77 3 50       8 if ( !$file ) {
78 0         0 $self->_setError("No file to save to specified");
79 0         0 return undef;
80             }
81              
82 3 50 66     21 if ( $self->{file} && $file eq $self->{file} && !$self->isModified() ) {
      33        
83 0         0 $self->_debug("Buffer not modified, not saving to file $file");
84 0         0 return 1;
85             }
86             else {
87 3         10 $self->_debug(
88             "Saving " . $self->getLineCount() . " lines to file $file" );
89             }
90              
91 3 50       1122788 if ( open( FIL, ">$file" ) ) {
92 3         34 $self->_debug("saving buffer to $file");
93 3         13 $self->goto('top');
94 3         13 my $str = $self->get();
95 3         7 my $cnt = 0;
96 3         29 while ( defined($str) ) {
97 20         75 $self->_debug("saving: '$str'");
98 20         60 $cnt++;
99 20         82 print FIL $str;
100 20         44 $str = $self->next();
101             }
102 3         250 close(FIL);
103 3         27 return $cnt;
104             }
105             else {
106 0         0 $self->_setError("Failed to load file $file");
107 0         0 return undef;
108             }
109              
110 0         0 return 0;
111             }
112              
113             sub clear {
114 3     3 1 10 my $self = shift;
115 3         5 @{ $self->{_buffer} } = ();
  3         13  
116 3         6 $self->{_currline} = 0;
117 3         5 return 1;
118             }
119              
120             #=============================================================
121             # Public Methods
122             #=============================================================
123             # Navigation methods
124             #-------------------------------------------------------------
125              
126             # Internal method returning the resulting array position (starting at 0)
127             sub _translateLinePos {
128 151     151   149 my $self = shift;
129 151   50     367 my $linenum = shift || return undef;
130 151         198 my $curr = $self->{_currline}; # Resulting line to return
131 151 100       820 if ( $linenum =~ /^[0-9]+$/ ) {
    100          
    100          
    100          
132 8         43 $curr = $linenum - 1;
133             }
134             elsif ( $linenum =~ /^[+-]\d+$/ ) {
135 122         6332 eval "\$curr=$curr$linenum";
136             }
137             elsif ( $linenum =~ /^(start|top|first)$/ ) {
138 17         29 $curr = 0;
139             }
140             elsif ( $linenum =~ /^(end|bottom|last)$/ ) {
141 3         7 $curr = $self->getLineCount() - 1;
142             }
143             else {
144 1         3 $self->_debug("Could not translate: $linenum");
145 1         3 return undef;
146             }
147              
148             # do sanity check now
149 150 100 100     816 if ( $curr < 0 || $curr >= $self->getLineCount() ) {
150 20         49 $self->_debug(
151             "Failed sanity check, current line would be out of bounds");
152 20         40 return undef;
153             }
154              
155 130         297 return $curr;
156             }
157              
158             sub goto {
159 151     151 1 1153 my $self = shift;
160 151         163 my $goto = shift;
161 151         274 my $curr = $self->_translateLinePos($goto);
162              
163 151 100       290 if ( !defined($curr) ) {
164 21         74 $self->_setError("Invalid line position: $goto");
165 21         70 return undef;
166             }
167              
168 130         511 $self->_debug( "goto $goto succeeded from array pos "
169             . $self->{_currline}
170             . " to $curr" );
171 130         202 $self->{_currline} = $curr;
172 130         237 return $self->getLineNumber();
173             }
174              
175             sub getLineCount {
176 191     191 1 974 my $self = shift;
177 191         181 return ( $#{ $self->{_buffer} } + 1 );
  191         910  
178             }
179              
180             sub getLineNumber {
181 137     137 1 151 my $self = shift;
182 137         1013 $self->_debug( "line is "
183             . ( $self->{_currline} + 1 )
184             . ", array pos is $self->{_currline}" );
185 137         398 return ( $self->{_currline} + 1 );
186             }
187              
188 20     20 1 33 sub isEOF { return shift->isEndOfBuffer() }
189              
190             sub isEndOfBuffer {
191 20     20 1 17 my $self = shift;
192 20         36 return ( $self->{_currline} >= $self->getLineCount() );
193             }
194 1     1 1 3 sub isEmpty { return ( shift->getLineCount() == 0 ) }
195              
196 0     0 1 0 sub isModified { return shift->{_modified}; }
197 25     25 1 31 sub setModified { my $self = shift; $self->_debug("Marking buffer modified"); $self->{_modified} = 1; }
  25         37  
  25         49  
198 3     3   4 sub _clearModified { my $self = shift; $self->_debug("Marking buffer unmodified"); $self->{_modified} = 0; }
  3         9  
  3         7  
199              
200             sub setAutoNewline {
201 1     1 1 2 my $self = shift;
202 1         2 my $newline = shift;
203 1 50 33     16 if (!$newline || $newline eq "off" || $newline eq "none") {
    50 33        
    0 33        
    0 0        
      0        
204 0         0 $self->{_autonewline} = ""; $self->{_newline} = "";
  0         0  
205             }
206             elsif ($newline eq "\n" || lc($newline) eq "unix") {
207 1         2 $self->{_autonewline} = "unix"; $self->{_newline} = "\n";
  1         2  
208             }
209             elsif ($newline eq "\r" || lc($newline) eq "mac") {
210 0         0 $self->{_autonewline} = "mac"; $self->{_newline} = "\r";
  0         0  
211             }
212             elsif ($newline eq "\r\n" || lc($newline) eq "windows") {
213 0         0 $self->{_autonewline} = "windows"; $self->{_newline} = "\r\n";
  0         0  
214             }
215             else {
216 0         0 $self->{_autonewline} = "other"; $self->{_newline} = "$newline";
  0         0  
217             }
218 1         4 return 1;
219             }
220              
221             sub getAutoNewline {
222 1     1 1 2 my $self = shift;
223 1         4 return $self->{_newline};
224             }
225              
226             sub next {
227 118     118 1 134 my $self = shift;
228 118   50     389 my $num = shift || 1;
229              
230             #FIXME should return all lines as array in array context
231 118 100       339 if ( !$self->goto("+$num") ) {
232 18         63 return undef;
233             }
234 100         216 return $self->get();
235             }
236              
237             sub previous {
238 1     1 1 2 my $self = shift;
239 1   50     9 my $num = shift || 1;
240              
241             #FIXME should return all lines as array in array context
242 1 50       5 if ( !$self->goto("-$num") ) {
243 0         0 return undef;
244             }
245 1         3 return $self->get();
246             }
247              
248             #-------------------------------------------------------------
249             # Searching methods
250             #-------------------------------------------------------------
251             sub find {
252 4     4 1 5 my $self = shift;
253 4   50     11 my $match = shift || return undef;
254             # TODO Add a more sophisticated interface, like
255             # find(regex => "\d+", startat => 'top', wrap => 1)
256 4         5 my $wrap = shift;
257 4         10 $match = $self->escapeRegexString($match);
258 4         8 $self->{_findstart} = 1; # Start at top, unless startline is defined
259 4         5 $self->{_findlast} = undef;
260 4         4 $self->{_findregex} = $match;
261 4         6 $self->{_findwrap} = $wrap;
262 4         15 $self->goto($self->{_findstart});
263 4         9 return $self->findNext();
264             }
265              
266             sub findNext {
267 6     6 1 8 my $self = shift;
268 6         8 my $match = $self->{_findregex};
269 6 50       15 return undef if !$match;
270             # Continue from current-line + 1 (avoid matchloop)
271 6 100       11 if (defined($self->{_findlast})) { $self->goto($self->{_findlast} + 1); }
  1         5  
272 6         9 my $line = $self->get();
273 6         68 my $MAXCOUNT = $self->getLineCount();
274 6         7 my $count = 0;
275 6   66     27 while (defined($line) && $count++ <= $MAXCOUNT) {
276 21         51 $self->_debug("Finding $match in line: '$line'");
277 21 100       92 if ($line =~ /$match/) {
278 2 50 33     8 if (defined($self->{_findlast}) && $self->{_currline} eq $self->{_findlast}) {
279 0         0 $self->_debug("Ohoh, should not have found same match again");
280 0         0 return undef;
281             }
282 2         11 $self->{_findlast} = $self->getLineNumber();
283 2         7 $self->_debug("Found match $match in line $self->{_findlast}");
284 2         4 return $self->getLineNumber();
285             }
286 19         32 $line = $self->next();
287 19 50 33     31 if ($self->isEOF() && $self->{_findwrap}) {
288 0         0 $self->goto('top');
289 0         0 $line = $self->get();
290             }
291             }
292 4         15 return undef;
293             }
294              
295             sub findPrevious {
296 0     0 1 0 return undef;
297             }
298              
299             #-------------------------------------------------------------
300             # Viewing/Editing methods
301             #-------------------------------------------------------------
302             sub get {
303 137     137 1 170 my $self = shift;
304 137         138 my $linenum = shift;
305 137 50       217 if ( defined($linenum) ) { $linenum = $self->_translateLinePos($linenum) }
  0         0  
306 137         201 else { $linenum = $self->{_currline} }
307 137 50       226 if ( !defined($linenum) ) {
308 0         0 $self->_setError("Invalid line position");
309 0         0 return undef;
310             }
311 137         143 my $line = $self->_appendAutoNewline(${ $self->{_buffer} }[$linenum]);
  137         357  
312 137 50       464 $self->_debug( "get line $linenum in array: "
313             . ( defined($line) ? $line : "*undef*" ) );
314 137         454 return $line;
315             }
316              
317             sub set {
318 23     23 1 39 my $self = shift;
319 23         32 my $line = shift;
320 23         22 my $linenum = shift;
321 23 50       42 if ( defined($linenum) ) { $linenum = $self->translateLinePos($linenum) }
  0         0  
322 23         43 else { $linenum = $self->{_currline} }
323 23 50       46 if ( !defined($line) ) {
324 0         0 $self->_setError("Cannot set undefined data for line $linenum");
325 0         0 return undef;
326             }
327 23         71 $self->_debug("set line $linenum in array: $line");
328 23 50 33     28 if ( !defined( ${ $self->{_buffer} }[$linenum] )
  23         68  
  23         90  
329             || ${ $self->{_buffer} }[$linenum] ne $line )
330             {
331 23         57 $self->setModified();
332             }
333              
334 23         29 ${ $self->{_buffer} }[$linenum] = $line;
  23         41  
335 23         58 return 1;
336             }
337              
338             # Insert before start of buffer
339             sub insert {
340 2     2 1 4 my $self = shift;
341 2         3 unshift( @{ $self->{_buffer} }, @_ );
  2         7  
342 2         7 return 1;
343             }
344              
345             sub append {
346 21     21 1 24 my $self = shift;
347 21         22 push( @{ $self->{_buffer} }, @_ );
  21         49  
348 21         64 return 1;
349             }
350              
351             sub delete {
352 0     0 1 0 my $self = shift;
353 0         0 splice( @{ $self->{_buffer} }, $self->{_currline}, 1 );
  0         0  
354 0         0 return $self->get();
355             }
356              
357             sub dumpAsString {
358 5     5 1 12 my $self = shift;
359             return
360 30 50       104 join( "", map { ( defined($_) ? $_ : "*undef*" ) } @{ $self->{_buffer} } )
  5         15  
  5         28  
361             if ( $self->{_buffer} )
362             && ( ref( $self->{_buffer} ) eq "ARRAY" )
363 5 50 33     39 && $#{ $self->{_buffer} } >= 0;
      33        
364 0         0 return "";
365             }
366              
367             sub replaceString {
368 1     1 1 3 my $self = shift;
369 1         2 my ($match,$with) = @_;
370 1         3 my $str = $self->get();
371 1 50       4 return undef if !defined($str);
372 1         5 $self->_debug( "Doing string replacement of '$match' with '$with' on string: $str" );
373 1         2 my $pos = 0;
374 1         1 my $index = 0;
375 1         2 my $count = 0;
376 1         2 my $MAXCOUNT = 1000;
377 1   66     14 while ($pos < length($str) && ($index = index($str,$match,$pos)) >= $pos && $count++ < $MAXCOUNT) {
      66        
378             # myfoobar, foo is at index 2, foo is replaced by bar,
379 2         9 $self->_debug("Found $match at $index (pos was $pos)");
380 2         6 $str = substr($str,0,$index) . $with . substr($str,$index + length($match));
381 2         3 $pos = $index + length($with);
382 2         6 $self->_debug("String is now $str, pos is $pos");
383             }
384 1 50       4 if ($count == $MAXCOUNT) { $self->setError("Maximum loopcount reached"); return undef; }
  0         0  
  0         0  
385 1 50       4 if ($count) {
386 1         3 $self->set($str);
387             }
388 1         4 return $count;
389             }
390              
391             sub replaceWildcard {
392 2     2 1 3 my $self = shift;
393 2         5 my ($match,$with,$opts) = @_;
394             # Replace wildcards with apropriate regex terms
395             # map '*' to '.*?'
396             # map '?' to '.'
397             # leave other things the same, but escape / and ()
398 2         7 $self->_debug("Doing wildcard replacement of '$match' with '$with'" );
399 2         6 $match = $self->escapeRegexString($match,"*.");
400 2         5 $match =~ s/\?/./g;
401 2         6 $match =~ s/\*/\\S*/g;
402 2         8 $self->_debug("After wildcard expansion: $match (was $_[1])");
403 2         5 return $self->replaceRegex($match, $with, $opts);
404             }
405              
406             sub replaceRegex {
407 4     4 1 6 my $self = shift;
408 4         5 my $match = shift;
409 4         3 my $with = shift;
410 4         6 my $opts = shift;
411 4 50       8 if ( !defined($opts) ) { $opts = "g"; }
  4         6  
412 4         7 my $count;
413 4         6 my $str = $self->get();
414 4 50       9 return undef if !defined($str);
415             # Be sure to escape our used seperation char for s//
416 4         18 $with =~ s?(^|[^\\])/?$1\\/?g;
417 4         15 $self->_debug(
418             "Doing regex replacement of '$match' with '$with' (opts: $opts) on string: $str" );
419 4         331 eval "\$count = (\$str =~ s/$match/$with/$opts)";
420              
421 4 50       15 if ($count) {
422 4         8 $self->set($str);
423             }
424 4         18 return $count;
425             }
426              
427             # replace is an alias for replaceRegex
428 1     1 1 5 sub replace { shift->replaceRegex(@_); }
429              
430             #=============================================================
431             # Utility functions / class methods
432             #=============================================================
433             sub convertWildcardToRegex {
434 2     2 1 6 my $self = shift;
435 2   50     10 my $string = shift || return "";
436 2         11 $self->_debug("convert wildcard '$string'");
437 2         13 $string = $self->escapeRegexString($string,"?*");
438 2         10 $string =~ s/\?/./g;
439 2         9 $string =~ s/\*/.*/g;
440             # $string =~ s/([\(\)\/])/\\$1/g;
441 2         12 $self->_debug("converted to regex: $string");
442 2         13 return $string;
443             }
444              
445             sub escapeRegexString {
446             # We need to escape all regex specific chars
447             # ignore chars will not be escaped
448 12     12 1 17 my $self = shift;
449 12   50     34 my $string = shift || return "";
450 12   100     42 my $ignorechars = shift || "";
451 12         15 my $regexchars = '\\/()[]{}+.*?'; #'
452 12         16 my $escapechars = "";
453 12         49 $self->_debug("escape string: '$string' ignoring: '$ignorechars' regex: '$regexchars'\n");
454             # Build a hash of chars to ignore and
455 12         44 my %chars = (map { $_ => 1 } split(//,$ignorechars));
  8         31  
456             # Now remove all unused ignored chars
457 12 100       61 foreach (split(//,$regexchars)) { $escapechars .= '\\' . $_ if !$chars{$_} }
  144         297  
458 12         262 $string =~ s/([$escapechars])/\\$1/g;
459 12         51 $self->_debug("escape: '$escapechars', string: '$string'");
460 12         56 return $string;
461             }
462              
463             sub convertStringToRegex {
464 2     2 1 6 return shift->escapeRegexString(@_);
465             }
466              
467             #-------------------------------------------------------------
468             # ErrorHandling Methods
469             #-------------------------------------------------------------
470 21     21   29 sub _setError { my $self = shift; $self->{error} = shift; }
  21         52  
471 0 0   0 1 0 sub isError { return ( shift->{'error'} ? 1 : 0 ); }
472              
473             sub getError {
474 4     4 1 7 my $self = shift;
475 4         8 my $error = $self->{error};
476 4         8 $self->_clearError();
477 4         21 return $error;
478             }
479 4     4   7 sub _clearError { shift->{error} = ""; }
480              
481             #=============================================================
482             # Private Methods
483             #=============================================================
484             # Only internal function for debug output
485             sub _appendAutoNewline {
486 137     137   147 my $self = shift;
487 137         156 my $text = shift;
488 137 100 66     418 return $text if (!$self->{_autonewline} || !$text);
489 98   50     220 my $newline = $self->{_newline} || "";
490 98         365 $text =~ s/[\r\n]+$//;
491 98         350 $self->_debug("appended autonewline " . $self->{_autonewline}. "'$newline' to '$text'");
492 98         249 return "$text$newline";
493             }
494              
495             sub _debuglevel {
496 1348     1348   1315 my $self = shift;
497 1348         1253 my $level = shift;
498 1348 100       2178 if (ref($self) eq __PACKAGE__) {
499 1316 50       2323 if (defined($level)) { $self->{_debug} = $level; }
  0         0  
500 1316         1925 $level = $self->{_debug};
501             # print "Object debug is $level (" . ref($self) . ")\n";
502             } else {
503 32 50       66 if (defined($level)) { $DEBUG = $level; }
  0         0  
504 32         40 $level = $DEBUG;
505             # print "Class debug is $level (" . ref($self) . ")\n";
506             }
507 1348         2541 return $level;
508             }
509              
510             sub _debug {
511 674     674   775 my $self = shift;
512 674         1007 my $lvl = $self->_debuglevel();
513 674 100       1210 if ( $self->_debuglevel() ) {
514 16         2426 print "[DEBUG$lvl] @_\n";
515             }
516             }
517              
518             1;
519             __END__