[BACK]Return to Entry.pm CVS log [TXT][DIR] Up to [local] / todotxt / Text-Todo / lib / Text / Todo

Annotation of todotxt/Text-Todo/lib/Text/Todo/Entry.pm, Revision 1.29

1.1       andrew      1: package Text::Todo::Entry;
                      2:
1.29    ! andrew      3: # $AFresh1: Entry.pm,v 1.28 2010/02/14 00:50:56 andrew Exp $
1.1       andrew      4:
                      5: use warnings;
                      6: use strict;
                      7: use Carp;
                      8:
1.22      andrew      9: use Class::Std::Utils;
1.1       andrew     10:
1.26      andrew     11: use version; our $VERSION = qv('0.1.1');
1.1       andrew     12:
                     13: {
1.22      andrew     14:
1.23      andrew     15:     my @attr_refs = \(
                     16:         my %text_of,
                     17:
                     18:         my %tags_of,
                     19:         my %priority_of,
                     20:         my %completion_status_of,
                     21:         my %known_tags_of,
                     22:     );
1.3       andrew     23:
1.8       andrew     24:     # XXX Should the completion (x) be case sensitive?
1.14      andrew     25:     my $priority_completion_regex = qr{
1.8       andrew     26:         ^ \s*
1.17      andrew     27:         (?i:(x \s* [\d-]* ) \s*)?
                     28:         (?i:\( ([A-Z]) \)   \s*)?
1.14      andrew     29:     }xms;
1.8       andrew     30:
1.22      andrew     31:     sub new {
                     32:         my ( $class, $options ) = @_;
                     33:
                     34:         my $self = bless anon_scalar(), $class;
                     35:         my $ident = ident($self);
1.1       andrew     36:
1.28      andrew     37:         $text_of{$ident} = q{};
                     38:
1.13      andrew     39:         if ( !ref $options ) {
                     40:             $options = { text => $options };
                     41:         }
                     42:         elsif ( ref $options ne 'HASH' ) {
                     43:             croak 'Invalid parameter passed!';
                     44:         }
                     45:
1.28      andrew     46:         my %tags = (
1.13      andrew     47:             context => q{@},
                     48:             project => q{+},
1.28      andrew     49:         );
1.13      andrew     50:
                     51:         if ( exists $options->{tags} && ref $options->{tags} eq 'HASH' ) {
1.29    ! andrew     52:             %tags = ( %tags, %{ $options->{tags} } );
1.13      andrew     53:         }
                     54:
1.28      andrew     55:         for my $tag ( keys %tags ) {
                     56:             $self->learn_tag( $tag, $tags{$tag} );
1.13      andrew     57:         }
                     58:
1.14      andrew     59:         $self->replace( $options->{text} );
1.1       andrew     60:
                     61:         return $self;
                     62:     }
                     63:
1.28      andrew     64:     sub _parse_entry {
                     65:         my ($self) = @_;
1.1       andrew     66:         my $ident = ident($self);
                     67:
1.28      andrew     68:         delete $tags_of{$ident};
                     69:         delete $completion_status_of{$ident};
                     70:         delete $priority_of{$ident};
                     71:
                     72:         my $text       = $self->text       || q{};
                     73:         my $known_tags = $self->known_tags || {};
                     74:
                     75:         foreach my $tag ( keys %{$known_tags} ) {
                     76:             next if !defined $known_tags->{$tag};
                     77:             next if !length $known_tags->{$tag};
                     78:
                     79:             my $sigal = quotemeta $known_tags->{$tag};
                     80:             $tags_of{$ident}{$tag}
                     81:                 = { map { $_ => q{} } $text =~ / (?:^|\s) $sigal (\S*)/gxms };
                     82:         }
1.1       andrew     83:
1.17      andrew     84:         my ( $completed, $priority )
1.8       andrew     85:             = $text =~ / $priority_completion_regex /xms;
1.1       andrew     86:
1.17      andrew     87:         $completion_status_of{$ident} = _clean_completed($completed);
1.18      andrew     88:         $priority_of{$ident}          = $priority;
1.17      andrew     89:
1.1       andrew     90:         return 1;
                     91:     }
                     92:
1.17      andrew     93:     sub _clean_completed {
                     94:         my ($completed) = @_;
                     95:
                     96:         $completed ||= q{};
                     97:         $completed =~ s/^\s+|\s+$//gxms;
                     98:
1.18      andrew     99:         if ( !$completed ) {
1.17      andrew    100:             return;
                    101:         }
                    102:
1.18      andrew    103:         if ( $completed =~ s/(x)\s*//ixms ) {
1.17      andrew    104:             my $status = $1;
                    105:             if ($completed) {
                    106:                 return $completed;
                    107:             }
                    108:             else {
                    109:                 return $status;
                    110:             }
                    111:         }
                    112:
                    113:         return;
                    114:     }
                    115:
1.28      andrew    116:     sub replace {
                    117:         my ( $self, $text ) = @_;
                    118:         my $ident = ident($self);
                    119:
                    120:         $text = defined $text ? $text : q{};
                    121:
                    122:         $text_of{$ident} = $text;
                    123:
                    124:         return $self->_parse_entry;
                    125:     }
                    126:
                    127:     sub learn_tag {
                    128:         my ( $self, $tag, $sigal ) = @_;
                    129:         $known_tags_of{ ident $self}{$tag} = $sigal;
                    130:
                    131:         ## no critic strict
                    132:         no strict 'refs';    # Violates use strict, but allows code generation
                    133:         ## use critic
                    134:
                    135:         if ( !$self->can( $tag . 's' ) ) {
                    136:             *{ $tag . 's' } = sub {
                    137:                 my ($self) = @_;
                    138:                 return $self->_tags($tag);
                    139:             };
                    140:         }
                    141:
                    142:         if ( !$self->can( 'in_' . $tag ) ) {
                    143:             *{ 'in_' . $tag } = sub {
                    144:                 my ( $self, $item ) = @_;
                    145:                 return $self->_is_in( $tag . 's', $item );
                    146:             };
                    147:         }
                    148:
                    149:         return $self->_parse_entry;
1.27      andrew    150:     }
                    151:
1.3       andrew    152:     sub _tags {
                    153:         my ( $self, $tag ) = @_;
1.1       andrew    154:         my $ident = ident($self);
                    155:
1.28      andrew    156:         my @tags;
                    157:         if ( defined $tags_of{$ident}{$tag} ) {
                    158:             @tags = sort keys %{ $tags_of{$ident}{$tag} };
                    159:         }
1.3       andrew    160:         return wantarray ? @tags : \@tags;
1.1       andrew    161:     }
                    162:
1.3       andrew    163:     sub _is_in {
1.5       andrew    164:         my ( $self, $tags, $item ) = @_;
1.28      andrew    165:         return if !defined $item;
                    166:         foreach ( $self->$tags ) {
1.25      andrew    167:             return 1 if $_ eq $item;
                    168:         }
                    169:         return 0;
1.1       andrew    170:     }
                    171:
1.11      andrew    172:     sub pri {
1.8       andrew    173:         my ( $self, $new_pri ) = @_;
                    174:         my $ident = ident($self);
                    175:
                    176:         if ( $new_pri !~ /^[a-zA-Z]?$/xms ) {
                    177:             croak "Invalid priority [$new_pri]";
                    178:         }
                    179:
                    180:         $priority_of{$ident} = $new_pri;
                    181:
                    182:         return $self->prepend();
                    183:     }
                    184:
1.1       andrew    185:     sub prepend {
                    186:         my ( $self, $addition ) = @_;
                    187:
                    188:         my $new = $self->text;
1.8       andrew    189:         my @new;
                    190:
                    191:         $new =~ s/$priority_completion_regex//xms;
1.1       andrew    192:
1.10      andrew    193:         if ( $self->done ) {
1.20      andrew    194:             if ( $self->done !~ /^x/ixms ) {
1.18      andrew    195:                 push @new, 'x';
                    196:             }
1.9       andrew    197:             push @new, $self->done;
1.1       andrew    198:         }
1.8       andrew    199:
                    200:         if ( $self->priority ) {
                    201:             push @new, '(' . $self->priority . ')';
                    202:         }
                    203:
                    204:         if ( defined $addition && length $addition ) {
                    205:             push @new, $addition;
1.1       andrew    206:         }
                    207:
1.14      andrew    208:         return $self->replace( join q{ }, @new, $new );
1.1       andrew    209:     }
                    210:
                    211:     sub append {
                    212:         my ( $self, $addition ) = @_;
1.14      andrew    213:         return $self->replace( join q{ }, $self->text, $addition );
1.1       andrew    214:     }
                    215:
1.14      andrew    216:     ## no critic 'homonym'
                    217:     sub do {    # This is what it is called in todo.sh
                    218:         ## use critic
1.7       andrew    219:         my ($self) = @_;
1.8       andrew    220:         my $ident = ident($self);
1.7       andrew    221:
1.10      andrew    222:         if ( $self->done ) {
1.7       andrew    223:             return 1;
                    224:         }
                    225:
1.18      andrew    226:         $completion_status_of{$ident} = sprintf "%04d-%02d-%02d",
                    227:             ( (localtime)[5] + 1900 ),
                    228:             ( (localtime)[4] + 1 ),
                    229:             ( (localtime)[3] );
1.8       andrew    230:
                    231:         return $self->prepend();
1.7       andrew    232:     }
                    233:
1.9       andrew    234:     sub done {
                    235:         my ($self) = @_;
1.27      andrew    236:         return $completion_status_of{ ident($self) };
1.9       andrew    237:     }
1.28      andrew    238:     sub known_tags { my ($self) = @_; return $known_tags_of{ ident($self) }; }
                    239:     sub priority   { my ($self) = @_; return $priority_of{ ident($self) }; }
                    240:     sub text       { my ($self) = @_; return $text_of{ ident($self) }; }
                    241:     sub depri      { my ($self) = @_; return $self->pri(q{}) }
1.9       andrew    242:
1.23      andrew    243:     sub DESTROY {
                    244:         my ($self) = @_;
                    245:         my $ident = ident $self;
                    246:         foreach my $attr_ref (@attr_refs) {
                    247:             delete $attr_ref->{$ident};
                    248:         }
                    249:     }
1.1       andrew    250: }
                    251: 1;    # Magic true value required at end of module
                    252: __END__
                    253:
                    254: =head1 NAME
                    255:
1.12      andrew    256: Text::Todo::Entry - An object for manipulating an entry on a Text::Todo list
1.1       andrew    257:
                    258:
                    259: =head1 VERSION
                    260:
1.12      andrew    261: Since the $VERSION can't be automatically included,
                    262: here is the RCS Id instead, you'll have to look up $VERSION.
                    263:
1.29    ! andrew    264:     $Id: Entry.pm,v 1.28 2010/02/14 00:50:56 andrew Exp $
1.1       andrew    265:
                    266:
                    267: =head1 SYNOPSIS
                    268:
                    269:     use Text::Todo::Entry;
                    270:
1.12      andrew    271:     my $entry = Text::Todo::Entry->new('text of entry');
                    272:
                    273:     $entry->append('+project');
                    274:
                    275:     if ($entry->in_project('project') && ! $entry->priority) {
                    276:         print $entry->text, "\n";
                    277:     }
                    278:
                    279:
1.1       andrew    280: =head1 DESCRIPTION
                    281:
1.12      andrew    282: This module creates entries in a Text::Todo list.
                    283: It allows you to retrieve information about them and modify them.
                    284:
                    285: For more information see L<http://todotxt.com>
1.1       andrew    286:
                    287:
                    288: =head1 INTERFACE
                    289:
1.22      andrew    290: =head2 new
1.12      andrew    291:
                    292: Creates an entry that can be manipulated.
1.1       andrew    293:
1.13      andrew    294:     my $entry = Text::Todo::Entry->new([
                    295:     'text of entry' | {
                    296:         [ text => 'text of entry' ,]
                    297:         [ tags => { additional_arg => 'identfier' }, ]
                    298:     } ]);
1.12      andrew    299:
                    300: If you don't pass any text, creates a blank entry.
1.1       andrew    301:
1.13      andrew    302: See tags below for a description of additional tags.
                    303:
1.1       andrew    304: =head2 text
                    305:
1.12      andrew    306: Returns the text of the entry.
                    307:
                    308:     print $entry->text, "\n";
                    309:
                    310: =head2 pri
                    311:
                    312: Sets the priority of an entry. If the priority is set to an empty string,
                    313: clears the priority.
                    314:
                    315:     $entry->pri('B');
                    316:
                    317: Acceptible entries are an empty string, A-Z or a-z. Anything else will cause
                    318: an error.
                    319:
                    320: =head2 depri
                    321:
                    322: A convenience function that unsets priority by calling pri('').
                    323:
                    324:     $entry->depri;
                    325:
1.1       andrew    326: =head2 priority
                    327:
1.12      andrew    328: Returns the priority of an entry which may be an empty string if it is
1.1       andrew    329:
1.12      andrew    330:     my $priority = $entry->priority;
1.1       andrew    331:
1.12      andrew    332: =head2 tags
1.1       andrew    333:
1.12      andrew    334: Each tag type generates two accessor functions {tag}s and in_{tag}.
1.1       andrew    335:
1.27      andrew    336: Default tags are context (@) and project (+).
1.13      andrew    337:
                    338: When creating a new object you can pass in new tags to recognize.
                    339:
                    340:     my $entry = Text::Todo::Entry->new({
                    341:         text => 'do something DUE:2011-01-01',
                    342:         tags => { due_date => 'DUE:' }
                    343:     });
                    344:
                    345:     my @due_dates = $entry->due_dates;
                    346:
1.27      andrew    347: then @due_dates is ( '2011-01-01' );
1.13      andrew    348:
                    349: and you could also:
                    350:
                    351:     if ($entry->in_due_date('2011-01-01')) {
                    352:         # do something
                    353:     }
                    354:
1.12      andrew    355:
                    356: =over
1.27      andrew    357:
1.12      andrew    358: =item {tag}s
1.1       andrew    359:
1.12      andrew    360:     @tags = $entry->{tag}s;
1.7       andrew    361:
1.12      andrew    362: =item in_{tag}
1.7       andrew    363:
1.12      andrew    364: returns true if $entry is in the tag, false if not.
1.1       andrew    365:
1.12      andrew    366:     if ($entry->in_{tag}('tag')) {
                    367:         # do something
                    368:     }
1.10      andrew    369:
1.12      andrew    370: =back
1.28      andrew    371:
                    372: =head2 learn_tag($tag, $sigal)
                    373:
                    374:     $entry->learn_tag('due_date', 'DUE:');
                    375:
                    376: Teaches the entry about an additional tag, same as passing a tags argument to
                    377: new(). See tags()
                    378:
                    379: You can simulate forgetting a tag by setting the sigal to undef or an empty
                    380: string.
                    381:
                    382: =head2 known_tags
                    383:
                    384:     $known_tags = $entry->known_tags;
                    385:
                    386: $known_tags by default would be:
                    387:
                    388:     { context => '@',
                    389:       project => '+',
                    390:     }
                    391:
1.1       andrew    392:
1.12      andrew    393: =head3 context
1.1       andrew    394:
1.12      andrew    395: These are matched as a word beginning with @.
1.1       andrew    396:
                    397: =over
                    398:
1.12      andrew    399: =item contexts
1.1       andrew    400:
1.12      andrew    401: =item in_context
1.1       andrew    402:
1.12      andrew    403: =back
                    404:
                    405: =head3 project
                    406:
                    407: This is matched as a word beginning with +.
                    408:
                    409: =over
1.1       andrew    410:
1.12      andrew    411: =item projects
1.1       andrew    412:
1.12      andrew    413: =item in_project
1.1       andrew    414:
                    415: =back
                    416:
1.12      andrew    417: =head2 replace
                    418:
                    419: Replaces the text of an entry with completely new text.  Useful if there has
                    420: been manual modification of the entry or just a new direction.
                    421:
                    422:     $entry->replace('replacment text');
                    423:
                    424: =head2 prepend
                    425:
                    426: Attaches text (with a trailing space) to the beginning of an entry.  Puts it
                    427: after the done() "x" and the priority() letter.
                    428:
                    429:     $entry->prepend('NEED HELP');
                    430:
                    431: =head2 append
                    432:
                    433: Adds text to the end of an entry.
                    434: Useful for adding tags, or just additional information.
                    435:
                    436:     $entry->append('@specific_store');
                    437:
                    438: =head2 do
                    439:
                    440: Marks an entry as completed.
                    441:
                    442:     $entry->do;
                    443:
1.18      andrew    444: Does this by prepending "x `date '%Y-%m-%d'`" to the beginning of the entry.
1.12      andrew    445:
                    446: =head2 done
                    447:
                    448: Returns true if an entry is marked complete and false if not.
1.18      andrew    449:
                    450:     if (!my $status = $entry->done) {
1.12      andrew    451:         # remind me to do it
                    452:     }
                    453:
1.18      andrew    454: If the entry starts as 'x date', for example 'x 2010-01-01', $status is now
                    455: '2010-01-01'.
                    456: If the entry just starts with 'x', then $status will be 'x'.
1.12      andrew    457:
                    458: =head1 DIAGNOSTICS
1.1       andrew    459:
                    460: =head1 CONFIGURATION AND ENVIRONMENT
                    461:
                    462: Text::Todo::Entry requires no configuration files or environment variables.
                    463:
                    464:
1.12      andrew    465: =head1 DEPENDENCIES
1.1       andrew    466:
1.12      andrew    467: Class::Std::Utils
                    468: List::Util
                    469: version
1.1       andrew    470:
                    471:
                    472: =head1 INCOMPATIBILITIES
                    473:
                    474: None reported.
                    475:
                    476:
                    477: =head1 BUGS AND LIMITATIONS
                    478:
1.12      andrew    479: No bugs have been reported.
                    480:
                    481: Known limitations:
1.1       andrew    482:
1.12      andrew    483: Sometimes leading whitespace may get screwed up when making changes.  It
                    484: doesn't seem to be particularly a problem, but if you use whitespace to indent
                    485: entries for some reason it could be.
1.1       andrew    486:
                    487: Please report any bugs or feature requests to
                    488: C<bug-text-todo@rt.cpan.org>, or through the web interface at
                    489: L<http://rt.cpan.org>.
                    490:
                    491:
                    492: =head1 AUTHOR
                    493:
                    494: Andrew Fresh  C<< <andrew@cpan.org> >>
                    495:
                    496:
                    497: =head1 LICENSE AND COPYRIGHT
                    498:
                    499: Copyright (c) 2009, Andrew Fresh C<< <andrew@cpan.org> >>. All rights reserved.
                    500:
                    501: This module is free software; you can redistribute it and/or
                    502: modify it under the same terms as Perl itself. See L<perlartistic>.
                    503:
                    504:
                    505: =head1 DISCLAIMER OF WARRANTY
                    506:
                    507: BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
                    508: FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
                    509: OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
                    510: PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
                    511: EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
                    512: WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
                    513: ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
                    514: YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
                    515: NECESSARY SERVICING, REPAIR, OR CORRECTION.
                    516:
                    517: IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
                    518: WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
                    519: REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
                    520: LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
                    521: OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
                    522: THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
                    523: RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
                    524: FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
                    525: SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
                    526: SUCH DAMAGES.

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>