package Text::Todo::Entry; # $AFresh1: Entry.pm,v 1.30 2010/02/16 01:13:12 andrew Exp $ use warnings; use strict; use Carp; use Class::Std::Utils; use version; our $VERSION = qv('0.2.0'); { my @attr_refs = \( my %text_of, my %tags_of, my %priority_of, my %completion_status_of, my %known_tags_of, ); # XXX Should the completion (x) be case sensitive? my $priority_completion_regex = qr{ ^ \s* (?i:(x \s* [\d-]* ) \s*)? (?i:\( ([A-Z]) \) \s*)? }xms; sub new { my ( $class, $options ) = @_; my $self = bless anon_scalar(), $class; my $ident = ident($self); $text_of{$ident} = q{}; if ( !ref $options ) { $options = { text => $options }; } elsif ( ref $options ne 'HASH' ) { croak 'Invalid parameter passed!'; } my %tags = ( context => q{@}, project => q{+}, ); if ( exists $options->{tags} && ref $options->{tags} eq 'HASH' ) { %tags = ( %tags, %{ $options->{tags} } ); } for my $tag ( keys %tags ) { $self->learn_tag( $tag, $tags{$tag} ); } $self->replace( $options->{text} ); return $self; } sub _parse_entry { my ($self) = @_; my $ident = ident($self); delete $tags_of{$ident}; delete $completion_status_of{$ident}; delete $priority_of{$ident}; my $text = $self->text || q{}; my $known_tags = $self->known_tags || {}; foreach my $tag ( keys %{$known_tags} ) { next if !defined $known_tags->{$tag}; next if !length $known_tags->{$tag}; my $sigal = quotemeta $known_tags->{$tag}; $tags_of{$ident}{$tag} = { map { $_ => q{} } $text =~ / (?:^|\s) $sigal (\S*)/gxms }; } my ( $completed, $priority ) = $text =~ / $priority_completion_regex /xms; $completion_status_of{$ident} = _clean_completed($completed); $priority_of{$ident} = $priority; return 1; } sub _clean_completed { my ($completed) = @_; $completed ||= q{}; $completed =~ s/^\s+|\s+$//gxms; if ( !$completed ) { return; } if ( $completed =~ s/(x)\s*//ixms ) { my $status = $1; if ($completed) { return $completed; } else { return $status; } } return; } sub replace { my ( $self, $text ) = @_; my $ident = ident($self); $text = defined $text ? $text : q{}; $text_of{$ident} = $text; return $self->_parse_entry; } sub learn_tag { my ( $self, $tag, $sigal ) = @_; $known_tags_of{ ident $self}{$tag} = $sigal; ## no critic strict no strict 'refs'; # Violates use strict, but allows code generation ## use critic if ( !$self->can( $tag . 's' ) ) { *{ $tag . 's' } = sub { my ($self) = @_; return $self->_tags($tag); }; } if ( !$self->can( 'in_' . $tag ) ) { *{ 'in_' . $tag } = sub { my ( $self, $item ) = @_; return $self->_is_in( $tag . 's', $item ); }; } return $self->_parse_entry; } sub _tags { my ( $self, $tag ) = @_; my $ident = ident($self); my @tags; if ( defined $tags_of{$ident}{$tag} ) { @tags = sort keys %{ $tags_of{$ident}{$tag} }; } return wantarray ? @tags : \@tags; } sub _is_in { my ( $self, $tags, $item ) = @_; return if !defined $item; foreach ( $self->$tags ) { return 1 if $_ eq $item; } return 0; } sub pri { my ( $self, $new_pri ) = @_; my $ident = ident($self); if ( $new_pri !~ /^[a-zA-Z]?$/xms ) { croak "Invalid priority [$new_pri]"; } $priority_of{$ident} = $new_pri; return $self->prepend(); } sub prepend { my ( $self, $addition ) = @_; my $new = $self->text; my @new; $new =~ s/$priority_completion_regex//xms; if ( $self->done ) { if ( $self->done !~ /^x/ixms ) { push @new, 'x'; } push @new, $self->done; } if ( $self->priority ) { push @new, '(' . $self->priority . ')'; } if ( defined $addition && length $addition ) { push @new, $addition; } return $self->replace( join q{ }, @new, $new ); } sub append { my ( $self, $addition ) = @_; return $self->replace( join q{ }, $self->text, $addition ); } ## no critic 'homonym' sub do { # This is what it is called in todo.sh ## use critic my ($self) = @_; my $ident = ident($self); if ( $self->done ) { return 1; } $completion_status_of{$ident} = sprintf "%04d-%02d-%02d", ( (localtime)[5] + 1900 ), ( (localtime)[4] + 1 ), ( (localtime)[3] ); return $self->prepend(); } sub done { my ($self) = @_; return $completion_status_of{ ident($self) }; } sub known_tags { my ($self) = @_; return $known_tags_of{ ident($self) }; } sub priority { my ($self) = @_; return $priority_of{ ident($self) }; } sub text { my ($self) = @_; return $text_of{ ident($self) }; } sub depri { my ($self) = @_; return $self->pri(q{}) } sub DESTROY { my ($self) = @_; my $ident = ident $self; foreach my $attr_ref (@attr_refs) { delete $attr_ref->{$ident}; } } } 1; # Magic true value required at end of module __END__ =head1 NAME Text::Todo::Entry - An object for manipulating an entry on a Text::Todo list =head1 VERSION Since the $VERSION can't be automatically included, here is the RCS Id instead, you'll have to look up $VERSION. $Id: Entry.pm,v 1.30 2010/02/16 01:13:12 andrew Exp $ =head1 SYNOPSIS use Text::Todo::Entry; my $entry = Text::Todo::Entry->new('text of entry'); $entry->append('+project'); if ($entry->in_project('project') && ! $entry->priority) { print $entry->text, "\n"; } =head1 DESCRIPTION This module creates entries in a Text::Todo list. It allows you to retrieve information about them and modify them. For more information see L =head1 INTERFACE =head2 new Creates an entry that can be manipulated. my $entry = Text::Todo::Entry->new([ 'text of entry' | { [ text => 'text of entry' ,] [ tags => { additional_arg => 'identfier' }, ] } ]); If you don't pass any text, creates a blank entry. See tags below for a description of additional tags. =head2 text Returns the text of the entry. print $entry->text, "\n"; =head2 pri Sets the priority of an entry. If the priority is set to an empty string, clears the priority. $entry->pri('B'); Acceptible entries are an empty string, A-Z or a-z. Anything else will cause an error. =head2 depri A convenience function that unsets priority by calling pri(''). $entry->depri; =head2 priority Returns the priority of an entry which may be an empty string if it is my $priority = $entry->priority; =head2 tags Each tag type generates two accessor functions {tag}s and in_{tag}. Default tags are context (@) and project (+). When creating a new object you can pass in new tags to recognize. my $entry = Text::Todo::Entry->new({ text => 'do something DUE:2011-01-01', tags => { due_date => 'DUE:' } }); my @due_dates = $entry->due_dates; then @due_dates is ( '2011-01-01' ); and you could also: if ($entry->in_due_date('2011-01-01')) { # do something } =over =item {tag}s @tags = $entry->{tag}s; =item in_{tag} returns true if $entry is in the tag, false if not. if ($entry->in_{tag}('tag')) { # do something } =back =head2 learn_tag($tag, $sigal) $entry->learn_tag('due_date', 'DUE:'); Teaches the entry about an additional tag, same as passing a tags argument to new(). See tags() You can simulate forgetting a tag by setting the sigal to undef or an empty string. =head2 known_tags $known_tags = $entry->known_tags; $known_tags by default would be: { context => '@', project => '+', } =head3 context These are matched as a word beginning with @. =over =item contexts =item in_context =back =head3 project This is matched as a word beginning with +. =over =item projects =item in_project =back =head2 replace Replaces the text of an entry with completely new text. Useful if there has been manual modification of the entry or just a new direction. $entry->replace('replacment text'); =head2 prepend Attaches text (with a trailing space) to the beginning of an entry. Puts it after the done() "x" and the priority() letter. $entry->prepend('NEED HELP'); =head2 append Adds text to the end of an entry. Useful for adding tags, or just additional information. $entry->append('@specific_store'); =head2 do Marks an entry as completed. $entry->do; Does this by prepending "x `date '%Y-%m-%d'`" to the beginning of the entry. =head2 done Returns true if an entry is marked complete and false if not. if (!my $status = $entry->done) { # remind me to do it } If the entry starts as 'x date', for example 'x 2010-01-01', $status is now '2010-01-01'. If the entry just starts with 'x', then $status will be 'x'. =head1 DIAGNOSTICS =head1 CONFIGURATION AND ENVIRONMENT Text::Todo::Entry requires no configuration files or environment variables. =head1 DEPENDENCIES Class::Std::Utils List::Util version =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS No bugs have been reported. Known limitations: Sometimes leading whitespace may get screwed up when making changes. It doesn't seem to be particularly a problem, but if you use whitespace to indent entries for some reason it could be. Please report any bugs or feature requests to C, or through the web interface at L. =head1 AUTHOR Andrew Fresh C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2009, Andrew Fresh C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.