[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.28

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

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