Annotation of todotxt/Text-Todo/lib/Text/Todo.pm, Revision 1.25
1.1 andrew 1: package Text::Todo;
2:
1.25 ! andrew 3: # $AFresh1: Todo.pm,v 1.24 2010/01/22 18:15:06 andrew Exp $
1.1 andrew 4:
5: use warnings;
6: use strict;
7: use Carp;
8:
1.19 andrew 9: use Class::Std::Utils;
1.2 andrew 10: use Text::Todo::Entry;
1.5 andrew 11: use File::Spec;
12:
1.23 andrew 13: use version; our $VERSION = qv('0.1.1');
1.1 andrew 14:
1.2 andrew 15: {
16:
1.20 andrew 17: my @attr_refs = \(
18: my %path_of,
19:
20: my %list_of,
21: my %loaded_of,
1.25 ! andrew 22: my %known_tags_of,
1.20 andrew 23: );
1.19 andrew 24:
25: sub new {
26: my ( $class, $options ) = @_;
27:
28: my $self = bless anon_scalar(), $class;
29: my $ident = ident($self);
1.2 andrew 30:
1.5 andrew 31: $path_of{$ident} = {
1.14 andrew 32: todo_dir => undef,
33: todo_file => 'todo.txt',
34: done_file => undef,
1.5 andrew 35: };
36:
1.25 ! andrew 37: $known_tags_of{$ident} = {
! 38: context => '@',
! 39: project => '+',
! 40: };
! 41:
1.5 andrew 42: if ($options) {
43: if ( ref $options eq 'HASH' ) {
44: foreach my $opt ( keys %{$options} ) {
45: if ( exists $path_of{$ident}{$opt} ) {
46: $self->_path_to( $opt, $options->{$opt} );
47: }
1.25 ! andrew 48: elsif ( $opt eq 'tags' ) {
! 49: foreach my $tag ( keys %{ $options->{$opt} } ) {
! 50: $known_tags_of{$ident}{$tag}
! 51: = $options->{$opt}{$tag};
! 52: }
! 53: }
1.5 andrew 54: else {
1.14 andrew 55:
1.13 andrew 56: #carp "Invalid option [$opt]";
1.5 andrew 57: }
58: }
59: }
60: else {
61: if ( -d $options ) {
62: $self->_path_to( 'todo_dir', $options );
63: }
64: elsif ( $options =~ /\.txt$/ixms ) {
65: $self->_path_to( 'todo_file', $options );
66: }
67: else {
68: carp "Unknown options [$options]";
69: }
70: }
71: }
72:
73: my $file = $self->_path_to('todo_file');
74: if ( defined $file && -e $file ) {
75: $self->load();
76: }
1.2 andrew 77:
78: return $self;
79: }
80:
1.5 andrew 81: sub _path_to {
82: my ( $self, $type, $path ) = @_;
83: my $ident = ident($self);
84:
85: if ( $type eq 'todo_dir' ) {
86: if ($path) {
87: $path_of{$ident}{$type} = $path;
88: }
89: return $path_of{$ident}{$type};
90: }
91:
92: if ($path) {
93: my ( $volume, $directories, $file )
94: = File::Spec->splitpath($path);
95: $path_of{$ident}{$type} = $file;
96:
97: if ($volume) {
98: $directories = File::Spec->catdir( $volume, $directories );
99: }
100:
101: # XXX Should we save complete paths to each file, mebbe only if
102: # the dirs are different?
103: if ($directories) {
104: $path_of{$ident}{todo_dir} = $directories;
105: }
106: }
107:
108: if ( $type =~ /(todo|done|report)_file/xms ) {
109: if ( my ( $pre, $post )
110: = $path_of{$ident}{$type} =~ /^(.*)$1(.*)\.txt$/ixms )
111: {
112: foreach my $f qw( todo done report ) {
113: if ( !defined $path_of{$ident}{ $f . '_file' } ) {
114: $path_of{$ident}{ $f . '_file' }
115: = $pre . $f . $post . '.txt';
116: }
117: }
118: }
119: }
120:
121: if ( defined $path_of{$ident}{todo_dir} ) {
122: return File::Spec->catfile( $path_of{$ident}{todo_dir},
123: $path_of{$ident}{$type} );
124: }
125:
126: return;
127: }
128:
1.3 andrew 129: sub file {
1.2 andrew 130: my ( $self, $file ) = @_;
131: my $ident = ident($self);
132:
1.5 andrew 133: if ( defined $file && exists $path_of{$ident}{$file} ) {
134: $file = $self->_path_to($file);
135: }
136: else {
137: $file = $self->_path_to( 'todo_file', $file );
1.2 andrew 138: }
139:
1.5 andrew 140: return $file;
1.3 andrew 141: }
142:
143: sub load {
144: my ( $self, $file ) = @_;
145: my $ident = ident($self);
146:
1.8 andrew 147: $loaded_of{$ident} = undef;
148:
1.9 andrew 149: $file = $self->file($file);
150:
1.8 andrew 151: if ( $list_of{$ident} = $self->listfile($file) ) {
152: $loaded_of{$ident} = $file;
153: return 1;
154: }
155:
156: return;
157: }
158:
159: sub listfile {
160: my ( $self, $file ) = @_;
161:
1.5 andrew 162: $file = $self->file($file);
163:
164: if ( !defined $file ) {
1.8 andrew 165: carp q{file can't be found};
166: return;
1.5 andrew 167: }
168:
169: if ( !-e $file ) {
1.8 andrew 170: carp "file [$file] does not exist";
1.5 andrew 171: return;
172: }
1.2 andrew 173:
174: my @list;
175: open my $fh, '<', $file or croak "Couldn't open [$file]: $!";
176: while (<$fh>) {
177: s/\r?\n$//xms;
1.19 andrew 178: push @list, Text::Todo::Entry->new($_);
1.2 andrew 179: }
180: close $fh or croak "Couldn't close [$file]: $!";
181:
1.8 andrew 182: return wantarray ? @list : \@list;
1.2 andrew 183: }
184:
185: sub save {
186: my ( $self, $file ) = @_;
187: my $ident = ident($self);
188:
1.5 andrew 189: $file = $self->file($file);
190: if ( !defined $file ) {
1.6 andrew 191: croak q{todo file can't be found};
1.5 andrew 192: }
1.2 andrew 193:
194: open my $fh, '>', $file or croak "Couldn't open [$file]: $!";
195: foreach my $e ( @{ $list_of{$ident} } ) {
1.3 andrew 196: print {$fh} $e->text . "\n"
197: or croak "Couldn't print to [$file]: $!";
1.2 andrew 198: }
199: close $fh or croak "Couldn't close [$file]: $!";
200:
1.9 andrew 201: $loaded_of{$ident} = $file;
202:
1.2 andrew 203: return 1;
204: }
205:
206: sub list {
1.3 andrew 207: my ($self) = @_;
1.2 andrew 208: my $ident = ident($self);
1.6 andrew 209:
1.2 andrew 210: return if !$list_of{$ident};
1.6 andrew 211: return wantarray ? @{ $list_of{$ident} } : $list_of{$ident};
1.5 andrew 212: }
213:
214: sub listpri {
1.14 andrew 215: my ( $self, $pri ) = @_;
1.5 andrew 216:
1.14 andrew 217: my @list;
218: if ($pri) {
219: $pri = uc $pri;
220: if ( $pri !~ /^[A-Z]$/xms ) {
1.17 andrew 221: croak 'PRIORITY must a single letter from A to Z.';
1.14 andrew 222: }
223: @list = grep { defined $_->priority && $_->priority eq $pri }
224: $self->list;
225: }
226: else {
227: @list = grep { $_->priority } $self->list;
228: }
1.5 andrew 229:
230: return wantarray ? @list : \@list;
1.2 andrew 231: }
1.1 andrew 232:
1.3 andrew 233: sub add {
234: my ( $self, $entry ) = @_;
235: my $ident = ident($self);
236:
1.5 andrew 237: if ( !ref $entry ) {
1.25 ! andrew 238: $entry = Text::Todo::Entry->new(
! 239: { text => $entry,
! 240: tags => $self->_known_tags,
! 241: }
! 242: );
1.5 andrew 243: }
244: elsif ( ref $entry ne 'Text::Todo::Entry' ) {
245: croak(
246: 'entry is a ' . ref($entry) . ' not a Text::Todo::Entry!' );
247: }
248:
249: push @{ $list_of{$ident} }, $entry;
250:
251: return $entry;
252: }
253:
1.6 andrew 254: sub del {
1.5 andrew 255: my ( $self, $src ) = @_;
256: my $ident = ident($self);
257:
1.6 andrew 258: my $id = $self->_find_entry_id($src);
1.5 andrew 259:
260: my @list = $self->list;
1.6 andrew 261: my $entry = splice @list, $id, 1;
1.5 andrew 262: $list_of{$ident} = \@list;
263:
264: return $entry;
265: }
266:
267: sub move {
268: my ( $self, $entry, $dst ) = @_;
269: my $ident = ident($self);
270:
271: my $src = $self->_find_entry_id($entry);
272: my @list = $self->list;
273:
1.6 andrew 274: splice @list, $dst, 0, splice @list, $src, 1;
1.5 andrew 275:
276: $list_of{$ident} = \@list;
277:
278: return 1;
279: }
280:
1.6 andrew 281: sub listproj {
1.17 andrew 282: my ($self) = @_;
1.14 andrew 283: return $self->listtag('project');
284: }
285:
286: sub listcon {
1.17 andrew 287: my ($self) = @_;
1.14 andrew 288: return $self->listtag('context');
289: }
290:
291: sub listtag {
292: my ( $self, $tag ) = @_;
1.5 andrew 293: my $ident = ident($self);
1.17 andrew 294:
1.14 andrew 295: my $accessor = $tag . 's';
1.5 andrew 296:
1.14 andrew 297: my %available;
1.6 andrew 298: foreach my $e ( $self->list ) {
1.14 andrew 299: foreach my $p ( $e->$accessor ) {
300: $available{$p} = 1;
1.5 andrew 301: }
302: }
303:
1.14 andrew 304: my @tags = sort keys %available;
1.5 andrew 305:
1.17 andrew 306: return wantarray ? @tags : \@tags;
1.5 andrew 307: }
308:
1.25 ! andrew 309: sub _known_tags {
! 310: my ($self) = @_;
! 311: my $ident = ident($self);
! 312:
! 313: my @list = $self->list;
! 314:
! 315: foreach my $e (@list) {
! 316: my $kt = $e->known_tags;
! 317: foreach my $t ( keys %{$kt} ) {
! 318: if ( !exists $known_tags_of{$ident}{$t} ) {
! 319: $known_tags_of{$ident}{$t} = $kt->{$t};
! 320: }
! 321: }
! 322: }
! 323:
! 324: return $known_tags_of{$ident};
! 325: }
! 326:
! 327: sub listtags {
! 328: my ($self) = @_;
! 329: my $ident = ident($self);
! 330:
! 331: my @list = sort keys %{ $self->_known_tags };
! 332:
! 333: return wantarray ? @list : \@list;
! 334: }
! 335:
1.9 andrew 336: sub archive {
337: my ($self) = @_;
338: my $ident = ident($self);
339:
340: if ( !defined $loaded_of{$ident}
341: || $loaded_of{$ident} ne $self->file('todo_file') )
342: {
343: carp 'todo_file not loaded';
344: return;
345: }
346:
1.12 andrew 347: my $changed = 0;
1.9 andrew 348: ENTRY: foreach my $e ( $self->list ) {
349: if ( $e->done ) {
350: if ( $self->addto( 'done_file', $e ) && $self->del($e) ) {
1.12 andrew 351: $changed++;
1.9 andrew 352: }
353: else {
354: carp q{Couldn't archive entry [} . $e->text . ']';
355: last ENTRY;
356: }
357: }
1.14 andrew 358: elsif ( $e->text eq q{} ) {
359: if ( $self->del($e) ) {
1.12 andrew 360: $changed++;
361: }
362: else {
363: carp q{Couldn't delete blank entry};
364: last ENTRY;
365: }
366: }
1.9 andrew 367: }
368:
1.12 andrew 369: if ($changed) {
1.9 andrew 370: $self->save;
371: }
372:
1.12 andrew 373: return $changed;
1.9 andrew 374: }
1.8 andrew 375:
376: sub addto {
377: my ( $self, $file, $entry ) = @_;
378: my $ident = ident($self);
379:
380: $file = $self->file($file);
381: if ( !defined $file ) {
382: croak q{file can't be found};
383: }
384:
1.9 andrew 385: if ( ref $entry ) {
386: if ( ref $entry eq 'Text::Todo::Entry' ) {
387: $entry = $entry->text;
388: }
389: else {
390: carp 'Unknown ref [' . ref($entry) . ']';
391: return;
392: }
393: }
394:
1.8 andrew 395: open my $fh, '>>', $file or croak "Couldn't open [$file]: $!";
396: print {$fh} $entry, "\n"
397: or croak "Couldn't print to [$file]: $!";
398: close $fh or croak "Couldn't close [$file]: $!";
399:
400: if ( defined $loaded_of{$ident} && $file eq $loaded_of{$ident} ) {
401: return $self->load($file);
402: }
403:
404: return 1;
405: }
1.5 andrew 406:
407: sub _find_entry_id {
408: my ( $self, $entry ) = @_;
409: my $ident = ident($self);
410:
1.3 andrew 411: if ( ref $entry ) {
412: if ( ref $entry ne 'Text::Todo::Entry' ) {
413: croak( 'entry is a '
414: . ref($entry)
415: . ' not a Text::Todo::Entry!' );
416: }
1.5 andrew 417:
418: my @list = $self->list;
419: foreach my $id ( 0 .. $#list ) {
420: if ( $list[$id] eq $entry ) {
421: return $id;
422: }
423: }
1.3 andrew 424: }
1.5 andrew 425: elsif ( $entry =~ /^\d+$/xms ) {
426: return $entry;
1.3 andrew 427: }
428:
1.5 andrew 429: croak "Invalid entry [$entry]!";
1.3 andrew 430: }
1.20 andrew 431:
432: sub DESTROY {
433: my ($self) = @_;
434: my $ident = ident $self;
1.21 andrew 435:
1.20 andrew 436: foreach my $attr_ref (@attr_refs) {
437: delete $attr_ref->{$ident};
438: }
1.21 andrew 439:
440: return;
1.20 andrew 441: }
1.2 andrew 442: }
1.1 andrew 443:
1.2 andrew 444: 1; # Magic true value required at end of module
1.1 andrew 445: __END__
446:
447: =head1 NAME
448:
1.24 andrew 449: Text::Todo - Perl interface to todotxt files
1.1 andrew 450:
1.10 andrew 451:
1.6 andrew 452: =head1 VERSION
453:
1.10 andrew 454: Since the $VERSION can't be automatically included,
455: here is the RCS Id instead, you'll have to look up $VERSION.
1.6 andrew 456:
1.25 ! andrew 457: $Id: Todo.pm,v 1.24 2010/01/22 18:15:06 andrew Exp $
1.1 andrew 458:
459: =head1 SYNOPSIS
460:
461: use Text::Todo;
1.10 andrew 462:
463: my $todo = Text::Todo->new('todo/todo.txt');
464:
465: foreach my $e (sort { lc($_->text) cmp lc($e->text)} $todo->list) {
466: print $e->text, "\n";
467: }
468:
1.1 andrew 469:
470: =head1 DESCRIPTION
471:
1.10 andrew 472: This module is a basic interface to the todo.txt files as described by
473: Lifehacker and extended by members of their community.
474:
1.4 andrew 475: For more information see L<http://todotxt.com>
1.1 andrew 476:
1.10 andrew 477: This module supports the 3 axes of an effective todo list.
478: Priority, Project and Context.
479:
480: It does not support other notations or many of the more advanced features of
481: the todo.sh like plugins.
482:
483: It should be extensible, but and hopefully will be before a 1.0 release.
484:
485:
1.1 andrew 486: =head1 INTERFACE
487:
1.19 andrew 488: =head2 new
1.2 andrew 489:
1.10 andrew 490: new({
491: [ todo_dir => 'directory', ]
492: [ todo_file => 'filename in todo_dir', ]
493: [ done_file => 'filename in todo_dir', ]
494: [ report_file => 'filename in todo_dir', ]
495: });
496:
497: Allows you to set each item individually. todo_file defaults to todo.txt.
498:
1.19 andrew 499: new('path/to/todo.txt');
1.10 andrew 500:
501: Automatically sets todo_dir to 'path/to', todo_file to 'todo.txt'
502:
1.19 andrew 503: new('path/to')
504:
505: If you pass an existing directory to new, it will set todo_dir.
506:
507:
1.10 andrew 508: If you what you set matches (.*)todo(.*).txt it will automatically set
509: done_file to $1done$2.txt
510: and
511: report_file to $1report$2.txt.
512:
513: For example, new('todo/todo.shopping.txt') will set
514: todo_dir to 'todo',
515: todo_file to 'todo.shopping.txt',
516: done_file to 'done.shopping.txt',
517: and
518: report_file to 'report.shopping.txt'.
519:
1.9 andrew 520: =head2 file
521:
1.10 andrew 522: Allows you to read the paths to the files in use.
523: If as in the SYNOPSIS above you used $todo = new('todo/todo.txt').
524:
525: $todo_file = $todo->file('todo_file');
526:
527: then, $todo_file eq 'todo/todo.txt'
528:
1.2 andrew 529: =head2 load
1.16 andrew 530: - Reads a list from a file into the current object.
1.2 andrew 531:
1.10 andrew 532: Allows you to load a different file into the object.
533:
534: $todo->load('done_file');
535:
536: This effects the other functions that act on the list.
537:
1.2 andrew 538: =head2 save
1.16 andrew 539: - Writes the list to disk.
1.2 andrew 540:
1.10 andrew 541: $todo->save(['new/path/to/todo']);
542:
1.16 andrew 543: Either writes the current working file or the passed in argument
1.10 andrew 544: that can be recognized by file().
545:
546: If you specify a filename it will save to that file and update the paths.
547: Additional changes to the object work on that file.
548:
1.9 andrew 549: =head2 list
1.16 andrew 550: - get the curently loaded list
1.9 andrew 551:
1.10 andrew 552: my @todo_list = $todo->list;
553:
1.16 andrew 554: In list context returns a list, it scalar context returns an array reference to the list.
555:
1.9 andrew 556: =head2 listpri
1.16 andrew 557: - get the list items that are marked priority
1.3 andrew 558:
1.10 andrew 559: Like list, but only returns entries that have priority set.
560:
561: my @priority_list = $todo->listpri;
562:
1.16 andrew 563: Since this is so easy to write as:
564:
565: my @priority_list = grep { $_->priority } $todo->list;
566:
567: I think it may become depreciated unless there is demand.
568:
1.25 ! andrew 569: =head2 listtags
! 570:
! 571: Returns a list of the tags known to the list.
! 572:
! 573: =head2 listtag($tag)
1.16 andrew 574:
575: Returns tags found in the list sorted by name.
1.3 andrew 576:
1.16 andrew 577: If there were projects +GarageSale and +Shopping then
1.10 andrew 578:
1.16 andrew 579: my @projects = $todo->listtag('project');
1.10 andrew 580:
581: is the same as
582:
583: @projects = ( 'GarageSale', 'Shopping' );
1.16 andrew 584:
585: =head2 listcon
586: - Shortcut to listtag('context')
587:
588: =head2 listproj
589: - Shortcut to listtag('project')
1.10 andrew 590:
1.3 andrew 591: =head2 add
1.9 andrew 592:
1.10 andrew 593: Adds a new entry to the list.
594: Can either be a Text::Todo::Entry object or plain text.
595:
596: $todo->add('new todo entry');
597:
598: It then becomes $todo->list->[-1];
599:
1.9 andrew 600: =head2 del
601:
1.10 andrew 602: Remove an entry from the list, either the reference or by number.
603:
604: $removed_entry = $todo->del($entry);
605:
606: $entry can either be an Text::Todo::Entry in the list or the index of the
607: entry to delete.
608:
609: Note that entries are 0 indexed (as expected in perl) not starting at line 1.
610:
1.9 andrew 611: =head2 move
612:
1.10 andrew 613: $todo->move($entry, $new_pos);
614:
615: $entry can either be the number of the entry or the actual entry.
616: $new_pos is the new position to put it.
617:
618: Note that entries are 0 indexed (as expected in perl) not starting at line 1.
619:
1.9 andrew 620: =head2 archive
621:
1.10 andrew 622: $todo->archive
623:
624: Iterates over the list and for each done entry,
625: addto('done_file')
626: and
627: del($entry).
628: If any were archived it will then
629: save()
630: and
631: load().
632:
1.9 andrew 633: =head2 addto
634:
1.10 andrew 635: $todo->addto($file, $entry);
1.1 andrew 636:
1.10 andrew 637: Appends text to the file.
638: $file can be anyting recognized by file().
639: $entry can either be a Text::Todo::Entry or plain text.
1.1 andrew 640:
1.10 andrew 641: =head2 listfile
1.1 andrew 642:
1.10 andrew 643: @list = $todo->listfile($file);
1.1 andrew 644:
1.10 andrew 645: Read a file and returns a list like $todo->list but does not update the
646: internal list that is being worked with.
647: $file can be anyting recognized by file().
1.1 andrew 648:
649:
1.10 andrew 650: =head1 DIAGNOSTICS
1.1 andrew 651:
1.10 andrew 652: Most methods return undef on failure.
1.1 andrew 653:
1.10 andrew 654: Some more important methods are fatal.
1.1 andrew 655:
656:
657: =head1 CONFIGURATION AND ENVIRONMENT
658:
659: Text::Todo requires no configuration files or environment variables.
660:
1.10 andrew 661: Someday it should be able to read and use the todo.sh config file. This may
662: possibly be better done in a client that would use this module.
1.4 andrew 663:
1.1 andrew 664:
665: =head1 DEPENDENCIES
666:
1.19 andrew 667: Class::Std::Utils
1.10 andrew 668: File::Spec
669: version
1.1 andrew 670:
671:
672: =head1 INCOMPATIBILITIES
673:
674: None reported.
675:
676:
677: =head1 BUGS AND LIMITATIONS
678:
679: No bugs have been reported.
1.11 andrew 680:
681: Limitations:
682:
683: Currently there isn't an easy way to print out line numbers with the entry.
1.1 andrew 684:
685: Please report any bugs or feature requests to
686: C<bug-text-todo@rt.cpan.org>, or through the web interface at
687: L<http://rt.cpan.org>.
688:
689:
690: =head1 AUTHOR
691:
692: Andrew Fresh C<< <andrew@cpan.org> >>
693:
694:
695: =head1 LICENSE AND COPYRIGHT
696:
697: Copyright (c) 2009, Andrew Fresh C<< <andrew@cpan.org> >>. All rights reserved.
698:
699: This module is free software; you can redistribute it and/or
700: modify it under the same terms as Perl itself. See L<perlartistic>.
701:
702:
703: =head1 DISCLAIMER OF WARRANTY
704:
705: BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
706: FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
707: OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
708: PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
709: EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
710: WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
711: ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
712: YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
713: NECESSARY SERVICING, REPAIR, OR CORRECTION.
714:
715: IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
716: WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
717: REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
718: LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
719: OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
720: THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
721: RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
722: FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
723: SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
724: SUCH DAMAGES.
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>