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