File Coverage

blib/lib/Org/Element/Headline.pm
Criterion Covered Total %
statement 166 168 98.8
branch 70 98 71.4
condition 40 51 78.4
subroutine 20 21 95.2
pod 12 14 85.7
total 308 352 87.5


line stmt bran cond sub pod time code
1             package Org::Element::Headline;
2              
3 16     16   815 use 5.010;
  16         57  
4 16     16   90 use locale;
  16         29  
  16         95  
5 16     16   528 use Log::ger;
  16         33  
  16         109  
6              
7 16     16   3617 use Moo;
  16         41  
  16         183  
8             extends 'Org::Element';
9             with 'Org::ElementRole';
10             with 'Org::ElementRole::Block';
11              
12             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
13             our $DATE = '2023-08-05'; # DATE
14             our $DIST = 'Org-Parser'; # DIST
15             our $VERSION = '0.560'; # VERSION
16              
17             has level => (is => 'rw');
18             has title => (is => 'rw');
19             has priority => (is => 'rw');
20             has tags => (is => 'rw');
21             has is_todo => (is => 'rw');
22             has is_done => (is => 'rw');
23             has todo_state => (is => 'rw');
24             has statistics_cookie => (is => 'rw');
25              
26             # old name, deprecated since 2014-07-17, will be removed in the future
27 0     0 0 0 sub todo_priority { shift->priority(@_) }
28              
29             sub extra_walkables {
30 161     161 1 290 my $self = shift;
31 161         368 grep {defined} ($self->title);
  161         662  
32             }
33              
34             sub header_as_string {
35 24     24 0 39 my ($self) = @_;
36 24 100       90 return $self->_str if defined $self->_str;
37             join("",
38             "*" x $self->level,
39             " ",
40             $self->is_todo ? $self->todo_state." " : "",
41             $self->priority ? "[#".$self->priority."] " : "",
42             $self->statistics_cookie ? "[".$self->statistics_cookie."] " : "",
43             $self->title->as_string,
44             $self->tags && @{$self->tags} ?
45 1 50 33     19 " :".join(":", @{$self->tags}).":" : "",
  0 50       0  
    50          
    50          
46             "\n");
47             }
48              
49             sub as_string {
50 24     24 1 945 my ($self) = @_;
51 24         40 $self->header_as_string . $self->children_as_string;
52             }
53              
54             sub get_tags {
55 6     6 1 4848 my ($self, $name) = @_;
56 6   100     9 my @res = @{ $self->tags // [] };
  6         34  
57             $self->walk_parents(
58             sub {
59 7     7   13 my ($el, $parent) = @_;
60 7 100       46 return 1 unless $parent->isa('Org::Element::Headline');
61 1 50       5 if ($parent->tags) {
62 1         1 for my $tag (@{ $parent->tags }) {
  1         4  
63 1 50       2 push @res, $tag unless grep { $_ eq $tag } @res;
  1         6  
64             }
65             }
66 1         4 1;
67 6         46 });
68 6         28 for my $tag (@{ $self->document->tags }) {
  6         21  
69 6 50       13 push @res, $tag unless grep { $_ eq $tag } @res;
  9         24  
70             }
71 6         29 @res;
72             }
73              
74             sub get_active_timestamp {
75 5     5 1 920 my ($self) = @_;
76              
77 5         18 for my $s ($self->title, $self) {
78 8         13 my $ats;
79             $s->walk(
80             sub {
81 24     24   38 my ($el) = @_;
82 24 100       44 return if $ats;
83 19 100 100     225 $ats = $el if $el->isa('Org::Element::Timestamp') &&
84             $el->is_active;
85             }
86 8         46 );
87 8 100       47 return $ats if $ats;
88             }
89 2         10 return;
90             }
91              
92             sub is_leaf {
93 4     4 1 860 my ($self) = @_;
94              
95 4 100       23 return 1 unless $self->children;
96              
97 2         4 my $res;
98 2         4 for my $child (@{ $self->children }) {
  2         6  
99             $child->walk(
100             sub {
101 2 50   2   7 return if defined($res);
102 2         5 my ($el) = @_;
103 2 50       9 if ($el->isa('Org::Element::Headline')) {
104 2         3 $res = 0;
105 2         20 goto EXIT_WALK;
106             }
107             }
108 2         14 );
109             }
110             EXIT_WALK:
111 2   50     9 $res //= 1;
112 2         9 $res;
113             }
114              
115             sub promote_node {
116 8     8 1 3524 my ($self, $num_levels) = @_;
117 8   100     36 $num_levels //= 1;
118 8 50       21 return if $num_levels == 0;
119 8 50       19 $self->die("Please specify a positive number of levels") if $num_levels < 0;
120              
121 8         17 for my $i (1..$num_levels) {
122              
123 9         20 my $l = $self->level;
124 9 100       21 last if $l <= 1;
125 8         12 $l--;
126 8         16 $self->level($l);
127              
128 8         20 $self->_str(undef);
129              
130 8         17 my $parent = $self->parent;
131 8         15 my $siblings = $parent->children;
132 8         28 my $pos = $self->seniority;
133              
134             # our children stay as children
135              
136             # our right sibling headline(s) become children
137 8         14 while (1) {
138 11         21 my $s = $siblings->[$pos+1];
139 11 100 66     53 last unless $s && $s->isa('Org::Element::Headline')
      100        
140             && $s->level > $l;
141 3 100       13 $self->children([]) unless defined $self->children;
142 3         6 push @{$self->children}, $s;
  3         11  
143 3         6 splice @$siblings, $pos+1, 1;
144 3         11 $s->parent($self);
145             }
146              
147             # our parent headline can become sibling if level is the same
148 8 100 100     43 if ($parent->isa('Org::Element::Headline') && $parent->level == $l) {
149 2         6 splice @$siblings, $pos, 1;
150 2         5 my $gparent = $parent->parent;
151 2         3 splice @{$gparent->children}, $parent->seniority+1, 0, $self;
  2         8  
152 2         13 $self->parent($gparent);
153             }
154              
155             }
156             }
157              
158             sub demote_node {
159 6     6 1 2084 my ($self, $num_levels) = @_;
160 6   100     23 $num_levels //= 1;
161 6 50       14 return if $num_levels == 0;
162 6 50       13 $self->die("Please specify a positive number of levels") if $num_levels < 0;
163              
164 6         13 for my $i (1..$num_levels) {
165              
166 8         18 my $l = $self->level;
167 8         11 $l++;
168 8         18 $self->level($l);
169              
170 8         14 $self->_str(undef);
171              
172             # prev sibling can become parent
173 8         18 my $ps = $self->prev_sibling;
174 8 100 66     38 if ($ps && $ps->isa('Org::Element::Headline') && $ps->level < $l) {
      100        
175 1         3 splice @{$self->parent->children}, $self->seniority, 1;
  1         5  
176 1 50       8 $ps->children([]) if !defined($ps->children);
177 1         2 push @{$ps->children}, $self;
  1         4  
178 1         4 $self->parent($ps);
179             }
180              
181             }
182             }
183              
184             sub promote_branch {
185 1     1 1 919 my ($self, $num_levels) = @_;
186 1   50     9 $num_levels //= 1;
187 1 50       4 return if $num_levels == 0;
188 1 50       5 $self->die("Please specify a positive number of levels") if $num_levels < 0;
189              
190 1         4 for my $i (1..$num_levels) {
191 1 50       7 last if $self->level <= 1;
192 1         9 $_->promote_node() for $self->find('Headline');
193             }
194             }
195              
196             sub demote_branch {
197 1     1 1 928 my ($self, $num_levels) = @_;
198 1   50     7 $num_levels //= 1;
199 1 50       4 return if $num_levels == 0;
200 1 50       4 $self->die("Please specify a positive number of levels") if $num_levels < 0;
201              
202 1         5 for my $i (1..$num_levels) {
203 1         5 $_->demote_node() for $self->find('Headline');
204             }
205             }
206              
207             sub get_drawer {
208 13     13 1 21 my $self = shift;
209 13   50     28 my $wanted_drawer_name = shift || "PROPERTIES";
210              
211 13 50       19 for my $d (@{$self->children||[]}) {
  13         43  
212 28         82 log_trace("seeking $wanted_drawer_name drawer in child: %s (%s)", $d->as_string, ref($d));
213 28 100 100     186 next unless ($d->isa('Org::Element::Drawer')
      66        
214             && $d->name eq $wanted_drawer_name
215             && $d->properties);
216 13         35 return $d;
217             }
218             }
219              
220             sub get_property {
221 12     12 1 985 my ($self, $name, $search_parent, $search_docprop) = @_;
222             #$log->tracef("-> get_property(%s, search_par=%s)", $name, $search_parent);
223 12         68 my $parent = $self->parent;
224              
225 12         23 my $propd = $self->get_drawer("PROPERTIES");
226             return $propd->properties->{$name} if
227 12 100 66     63 $propd && defined $propd->properties->{$name};
228              
229 7 100 66     25 if ($parent && $search_parent) {
230 2         5 while ($parent) {
231 3 50       9 if ($parent->isa('Org::Element::Headline')) {
232 3         8 my $res = $parent->get_property($name, 0, 0);
233 3 100       13 return $res if defined $res;
234             }
235 1         3 $parent = $parent->parent;
236             }
237             }
238              
239 5 100 100     18 if ($search_docprop // 1) {
240 4         11 log_trace("Getting property from document's .properties");
241 4         30 return $self->document->properties->{$name};
242             }
243 1         3 undef;
244             }
245              
246             sub update_statistics_cookie {
247 8     8 1 962 my $self = shift;
248              
249 8         18 my $statc = $self->statistics_cookie;
250 8 100       17 return unless $statc;
251              
252 4         7 my $num_done = 0;
253 4         7 my $num_total = 0;
254              
255             # count using checks on first-level list's children, or from first-level
256             # subheadlines
257 4   100     6 for my $chld (@{ $self->children // [] }) {
  4         28  
258 2 100       14 if ($chld->isa("Org::Element::Headline")) {
    50          
259 1         4 for my $el (@{ $self->children }) {
  1         5  
260 3 50       10 next unless $el->isa("Org::Element::Headline");
261 3 50       10 if ($el->is_todo) {
262 3         4 $num_total++;
263 3 100       8 $num_done++ if $el->is_done;
264             }
265             }
266 1         2 last;
267             } elsif ($chld->isa("Org::Element::List")) {
268 1         7 for my $el (@{ $self->children }) {
  1         6  
269 1 50       8 next unless $el->isa("Org::Element::List");
270 1         3 for my $el2 (@{ $el->children }) {
  1         4  
271 3 50       11 next unless $el2->isa("Org::Element::ListItem");
272 3         7 my $state = $el2->check_state;
273 3 50       16 if (defined $state) {
274 3         7 $num_total++;
275 3 100       10 $num_done++ if $state eq 'X';
276             }
277             }
278             }
279 1         2 last;
280             }
281             }
282              
283 4         13 undef $self->{_str}; # we modify content
284 4 100       17 if ($statc =~ /%/) {
285 1 50       10 $self->statistics_cookie(
286             sprintf("%d%%", $num_total == 0 ? 0 : $num_done/$num_total * 100));
287             } else {
288 3         17 $self->statistics_cookie(sprintf("%d/%d", $num_done, $num_total));
289             }
290             }
291              
292             1;
293             # ABSTRACT: Represent Org headline
294              
295             __END__
296              
297             =pod
298              
299             =encoding UTF-8
300              
301             =head1 NAME
302              
303             Org::Element::Headline - Represent Org headline
304              
305             =head1 VERSION
306              
307             This document describes version 0.560 of Org::Element::Headline (from Perl distribution Org-Parser), released on 2023-08-05.
308              
309             =head1 DESCRIPTION
310              
311             Derived from L<Org::Element>.
312              
313             =for Pod::Coverage ^(header_as_string|as_string|todo_priority)$
314              
315             =head1 ATTRIBUTES
316              
317             =head2 level => INT
318              
319             Level of headline (e.g. 1, 2, 3). Corresponds to the number of bullet stars.
320              
321             =head2 title => OBJ
322              
323             L<Org::Element::Text> representing the headline title
324              
325             =head2 priority => STR
326              
327             String (optional) representing priority.
328              
329             =head2 tags => ARRAY
330              
331             Arrayref (optional) containing list of defined tags.
332              
333             =head2 is_todo => BOOL
334              
335             Whether this headline is a TODO item.
336              
337             =head2 is_done => BOOL
338              
339             Whether this TODO item is in a done state (state which requires no more action,
340             e.g. DONE). Only meaningful if headline is a TODO item.
341              
342             =head2 todo_state => STR
343              
344             TODO state.
345              
346             =head2 statistics_cookie => STR
347              
348             Statistics cookie, e.g. '5/10' or '50%'. TODO: there might be more than one
349             statistics cookie.
350              
351             =head1 METHODS
352              
353             =head2 $el->get_tags() => ARRAY
354              
355             Get tags for this headline. A headline can define tags or inherit tags from its
356             parent headline (or from document).
357              
358             =head2 $el->get_active_timestamp() => ELEMENT
359              
360             Get the first active timestamp element for this headline, either in the title or
361             in the child elements.
362              
363             =head2 $el->is_leaf() => BOOL
364              
365             Returns true if element doesn't contain subtrees.
366              
367             =head2 $el->promote_node([$num_levels])
368              
369             Promote (decrease the level) of this headline node. $level specifies number of
370             levels, defaults to 1. Won't further promote if already at level 1.
371             Illustration:
372              
373             * h1
374             ** h2 <-- promote 1 level
375             *** h3
376             *** h3b
377             ** h4
378             * h5
379              
380             becomes:
381              
382             * h1
383             * h2
384             *** h3
385             *** h3b
386             ** h4
387             * h5
388              
389             =head2 $el->demote_node([$num_levels])
390              
391             Does the opposite of promote_node().
392              
393             =head2 $el->promote_branch([$num_levels])
394              
395             Like promote_node(), but all children headlines will also be promoted.
396             Illustration:
397              
398             * h1
399             ** h2 <-- promote 1 level
400             *** h3
401             **** grandkid
402             *** h3b
403              
404             ** h4
405             * h5
406              
407             becomes:
408              
409             * h1
410             * h2
411             ** h3
412             *** grandkid
413             ** h3b
414              
415             ** h4
416             * h5
417              
418             =head2 $el->demote_branch([$num_levels])
419              
420             Does the opposite of promote_branch().
421              
422             =head2 $el->get_property($name, $search_parent) => VALUE
423              
424             Search for property named $name in the PROPERTIES drawer. If $search_parent is
425             set to true (default is false), will also search in upper-level properties
426             (useful for searching for inherited property, like foo_ALL). Return undef if
427             property cannot be found.
428              
429             Regardless of $search_parent setting, file-wide properties will be consulted if
430             property is not found in the headline's properties drawer.
431              
432             =head2 $el->get_drawer([$drawer_name]) => VALUE
433              
434             Return an entire drawer as an Org::Element::Drawer object. By default, return the
435             PROPERTIES drawer. If you want LOGBOOK or some other drawer, ask for it by name.
436              
437             =head2 $el->update_statistics_cookie
438              
439             Update the statistics cookies by recalculating the number of TODO and
440             checkboxes.
441              
442             Will do nothing if the headline does not have any statistics cookie.
443              
444             =head1 HOMEPAGE
445              
446             Please visit the project's homepage at L<https://metacpan.org/release/Org-Parser>.
447              
448             =head1 SOURCE
449              
450             Source repository is at L<https://github.com/perlancar/perl-Org-Parser>.
451              
452             =head1 AUTHOR
453              
454             perlancar <perlancar@cpan.org>
455              
456             =head1 CONTRIBUTING
457              
458              
459             To contribute, you can send patches by email/via RT, or send pull requests on
460             GitHub.
461              
462             Most of the time, you don't need to build the distribution yourself. You can
463             simply modify the code, then test via:
464              
465             % prove -l
466              
467             If you want to build the distribution (e.g. to try to install it locally on your
468             system), you can install L<Dist::Zilla>,
469             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
470             L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
471             Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
472             that are considered a bug and can be reported to me.
473              
474             =head1 COPYRIGHT AND LICENSE
475              
476             This software is copyright (c) 2023, 2022, 2021, 2020, 2019, 2017, 2016, 2015, 2014, 2013, 2012, 2011 by perlancar <perlancar@cpan.org>.
477              
478             This is free software; you can redistribute it and/or modify it under
479             the same terms as the Perl 5 programming language system itself.
480              
481             =head1 BUGS
482              
483             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Org-Parser>
484              
485             When submitting a bug or request, please include a test-file or a
486             patch to an existing test-file that illustrates the bug or desired
487             feature.
488              
489             =cut