File Coverage

blib/lib/Org/Document.pm
Criterion Covered Total %
statement 371 377 98.4
branch 161 174 92.5
condition 116 137 84.6
subroutine 26 26 100.0
pod 2 3 66.6
total 676 717 94.2


line stmt bran cond sub pod time code
1             package Org::Document;
2              
3 24     24   7509 use 5.010001;
  24         91  
4 24     24   11483 use locale;
  24         14666  
  24         126  
5 24     24   42970 use Log::ger;
  24         1308  
  24         119  
6 24     24   6734 use Moo;
  24         20203  
  24         163  
7 24     24   25132 no if $] >= 5.021_006, warnings => "locale";
  24         327  
  24         137  
8             extends 'Org::Element';
9              
10 24     24   14830 use List::MoreUtils qw(firstidx);
  24         312678  
  24         161  
11 24     24   38007 use Time::HiRes qw(gettimeofday tv_interval);
  24         32473  
  24         109  
12              
13             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
14             our $DATE = '2023-07-12'; # DATE
15             our $DIST = 'Org-Parser'; # DIST
16             our $VERSION = '0.559'; # VERSION
17              
18             has tags => (is => 'rw');
19             has todo_states => (is => 'rw');
20             has done_states => (is => 'rw');
21             has priorities => (is => 'rw');
22             has drawer_names => (is => 'rw');
23             has properties => (is => 'rw');
24             has radio_targets => (is => 'rw');
25              
26             has time_zone => (is => 'rw');
27              
28             has ignore_unknown_settings => (is => 'rw');
29              
30             our $tags_re = qr/:(?:[\w@]+:)+/u;
31             my $ls_re = qr/(?:(?<=[\015\012])|\A)/; # line start
32             my $le_re = qr/(?:\R|\z)/; # line end
33             our $arg_re = qr/(?: '(?<squote> [^']*)' |
34             "(?<dquote> [^"]*)" |
35             (?<bare> \S+) )
36             /x;
37             our $args_re = qr/(?: $arg_re (?:[ \t]+ $arg_re)*)/x;
38             my $tstamp_re = qr/(?:\[\d{4}-\d{2}-\d{2} [^\n\]]*\])/x;
39             my $act_tstamp_re = qr/(?: <\d{4}-\d{2}-\d{2} [^\n>]* >)/x;
40             my $fn_name_re = qr/(?:[^ \t\n:\]]+)/x;
41             my $text_re =
42             qr{
43             (?<link> \[\[(?<link_link> [^\]\n]+)\]
44             (?:\[(?<link_desc> (?:[^\]]|\R)+)\])?\]) |
45             (?<radio_target> <<<(?<rt_target> [^>\n]+)>>>) |
46             (?<target> <<(?<t_target> [^>\n]+)>>) |
47              
48             # timestamp & time range
49             (?<trange> (?<trange_ts1> $tstamp_re)--
50             (?<trange_ts2> $tstamp_re)) |
51             (?<tstamp> $tstamp_re) |
52             (?<act_trange> (?<act_trange_ts1> $act_tstamp_re)--
53             (?<act_trange_ts2> $act_tstamp_re)) |
54             (?<act_tstamp> $act_tstamp_re) |
55              
56             # footnote (num, name + def, name + inline definition)
57             (?<fn_num> \[(?<fn_num_num>\d+)\]) |
58             (?<fn_namedef> $ls_re \[fn:(?<fn_namedef_name> $fn_name_re)\]
59             [ \t]* (?<fn_namedef_def> [^ \t\n]+)) |
60             (?<fn_nameidef> \[fn:(?<fn_nameidef_name> $fn_name_re?):?
61             (?<fn_nameidef_def> ([^\n\]]+)?)\]) |
62              
63             (?<markup_start> (?:(?<=\s|\(|\{)|\A) # whitespace, open paren, open curly paren
64             [*/+=~_]
65             (?=\S)) |
66             (?<markup_end> (?<=\S)
67             [*/+=~_]
68             # actually emacs doesn't allow ! after markup
69             (?:(?=[ \t\n:;"',.!?\)*-])|\z)) |
70              
71             (?<plain_text> (?:[^\[<*/+=~_\n]+|.+?))
72             #(?<plain_text> .+?) # too dispersy
73             }sxi;
74              
75             # XXX parser must be fixed: block elements have different precedence instead of
76             # flat like this. a headline has the highest precedence and a block or a drawer
77             # cannot contain a headline (e.g. "#+BEGIN_SRC foo\n* header\n#+END_SRC" should
78             # not contain a literal "* header" text but that is a headline. currently, a
79             # block or a drawer swallows a headline.
80              
81             my $block_elems_re = # top level elements
82             qr/(?<block> $ls_re (?<block_begin_indent>[ \t]*)
83             \#\+BEGIN_(?<block_name>\w+)
84             (?:[ \t]+(?<block_raw_arg>[^\n]*))?\R
85             (?<block_content>(?:.|\R)*?)
86             \R(?<block_end_indent>[ \t]*)
87             \#\+END_\k<block_name> $le_re) |
88             (?<setting> $ls_re (?<setting_indent>[ \t]*) \#\+
89             (?<setting_name> \w+): (?: [ \t]+
90             (?<setting_raw_arg> [^\n]*))? $le_re) |
91             (?<fixedw> (?: $ls_re [ \t]* (?::[ ][^\n]* | :$) $le_re )+ ) |
92             (?<comment> $ls_re [ \t]*\#[^\n]*(?:\R\#[^\n]*)* (?:\R|\z)) |
93             (?<headline> $ls_re (?<h_bullet>\*+) [ \t]
94             (?<h_title>[^\n]*?)
95             (?:[ \t]+(?<h_tags> $tags_re))?[ \t]* $le_re) |
96             (?<li_header> $ls_re (?<li_indent>[ \t]*)
97             (?<li_bullet>[+*-]|\d+\.) [ \t]+
98             (?<li_checkbox> \[(?<li_cbstate> [ X-])\])?
99             (?: (?<li_dt> [^\n]+?) [ ]::)?) |
100             (?<table> (?: $ls_re [ \t]* \| [ \t]* \S[^\n]* $le_re)+) |
101             (?<drawer> $ls_re [ \t]* :(?<drawer_name> \w+): [ \t]*\R
102             (?<drawer_content>(?:.|\R)*?)
103             $ls_re [ \t]* :END:) |
104             (?<text> (?:[^#|:+*0-9\n-]+|\n+|.)+?)
105             #(?<text> .+?) # too dispersy
106             /msxi;
107              
108             sub _init_pass1 {
109 95     95   204 my ($self) = @_;
110 95         289 $self->tags([]);
111 95         270 $self->todo_states([]);
112 95         250 $self->done_states([]);
113 95         227 $self->priorities([]);
114 95         334 $self->properties({});
115 95         385 $self->drawer_names([qw/CLOCK LOGBOOK PROPERTIES/]);
116             # FEEDSTATUS
117 95         413 $self->radio_targets([]);
118             }
119              
120             sub _init_pass2 {
121 93     93   226 my ($self) = @_;
122 93 50 66     157 if (!@{ $self->todo_states } && !@{ $self->done_states }) {
  93         395  
  81         352  
123 81         290 $self->todo_states(['TODO']);
124 81         204 $self->done_states(['DONE']);
125             }
126 93 100       185 if (!@{ $self->priorities }) {
  93         379  
127 89         308 $self->priorities([qw/A B C/]);
128             }
129 93         411 $self->children([]);
130             }
131              
132             sub __parse_args {
133 57     57   105 my $args = shift;
134 57 100 66     274 return [] unless defined($args) && length($args);
135             #$log->tracef("args = %s", $args);
136 55         94 my @args;
137 55         946 while ($args =~ /$arg_re (?:\s+|\z)/xg) {
138 24 50   24   36155 if (defined $+{squote}) {
  24 50       9053  
  24         89639  
  165         1035  
139 0         0 push @args, $+{squote};
140             } elsif (defined $+{dquote}) {
141 0         0 push @args, $+{dquote};
142             } else {
143 165         1244 push @args, $+{bare};
144             }
145             }
146             #$log->tracef("\\\@args = %s", \@args);
147 55         1196 \@args;
148             }
149              
150             sub __format_args {
151 2     2   6 my ($args) = @_;
152 2         4 my @s;
153 2         6 for (@$args) {
154 4 50       23 if (/\A(?:[A-Za-z0-9_:-]+|\|)\z/) {
    0          
155 4         21 push @s, $_;
156             } elsif (/"/) {
157 0         0 push @s, qq('$_');
158             } else {
159 0         0 push @s, qq("$_");
160             }
161             }
162 2         22 join " ", @s;
163             }
164              
165             sub BUILD {
166 96     96 0 99654 my ($self, $args) = @_;
167 96 50       689 $self->document($self) unless $self->document;
168              
169 96 100       354 if (defined $args->{from_string}) {
170              
171             # NOTE: parsing is done twice. first pass will set settings (e.g. custom
172             # todo keywords set by #+TODO), scan for radio targets. after that we
173             # scan again to build the elements tree.
174              
175 95         368 $self->_init_pass1();
176 95         352 $self->_parse($args->{from_string}, 1);
177 93         1978 $self->_init_pass2();
178 93         309 $self->_parse($args->{from_string}, 2);
179             }
180             }
181              
182             # parse blocky elements: setting, blocks, headline, drawer
183             sub _parse {
184 188     188   452 my ($self, $str, $pass) = @_;
185 188         665 log_trace('-> _parse(%s, pass=%d)', $str, $pass);
186 188         857 my $t0 = [gettimeofday];
187              
188 188         374 my $last_el;
189              
190             my $last_headline;
191 188         305 my $last_headlines = [$self]; # [$doc, $last_hl_level1, $last_hl_lvl2, ...]
192 188         268 my $last_listitem;
193 188         278 my $last_lists = []; # [last_List_obj_for_indent_level0, ...]
194 188         316 my $parent;
195              
196             my @text;
197 188         2478 while ($str =~ /$block_elems_re/og) {
198 4729   100     16869 $parent = $last_listitem // $last_headline // $self;
      66        
199             #$log->tracef("TMP: parent=%s (%s)", ref($parent), $parent->_str);
200 4729         34378 my %m = %+;
201 4729 50       12568 next unless keys %m; # perlre bug?
202             #if ($log->is_trace) {
203             # # profiler shows that this is very heavy, so commenting this out
204             # $log->tracef("TMP: match block element: %s", \%+) if $pass==2;
205             #}
206              
207 4729 100       9532 if (defined $m{text}) {
208 4049         6764 push @text, $m{text};
209 4049         26091 next;
210             } else {
211 680 100       3174 if (@text) {
212 270         860 my $text = join("", @text);
213 270 100 100     1243 if ($last_el && $last_el->isa('Org::Element::ListItem')) {
214             # a list is broken by either: a) another list (where the
215             # bullet type or indent is different; handled in the
216             # handling of $m{li_header}) or b) by two blank lines, or c)
217             # by non-blank text that is indented less than or equal to
218             # the last list item's indent.
219              
220             # a single blank line does not break a list. a text that is
221             # more indented than the last list item's indent will become
222             # the child of that list item.
223              
224 85         351 my ($firstline, $restlines) = $text =~ /(.*?\r?\n)(.+)/s;
225 85 100       183 if ($restlines) {
226 17         56 $restlines =~ /\A([ \t]*)/;
227 17         39 my $restlineslevel = length($1);
228 17         65 my $listlevel = length($last_el->parent->indent);
229 17 100       43 if ($restlineslevel <= $listlevel) {
230 11         24 my $origparent = $parent;
231             # find lesser-indented list
232 11   33     26 $parent = $last_headline // $self;
233 11         31 for (my $i=$restlineslevel-1; $i>=0; $i--) {
234 2 50       6 if ($last_lists->[$i]) {
235 2         4 $parent = $last_lists->[$i];
236 2         4 last;
237             }
238             }
239 11         21 splice @$last_lists, $restlineslevel;
240 11         35 $self->_add_text($firstline, $origparent, $pass);
241 11         35 $self->_add_text($restlines, $parent, $pass);
242 11         94 goto SKIP1;
243             }
244             }
245             }
246 259         770 $self->_add_text($text, $parent, $pass);
247 270         701 SKIP1:
248             @text = ();
249 270         479 $last_el = undef;
250             }
251             }
252              
253 680         985 my $el;
254 680 100 100     12867 if ($m{block} && $pass == 2) {
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100          
255              
256 3         463 require Org::Element::Block;
257             $el = Org::Element::Block->new(
258             _str=>$m{block},
259             document=>$self, parent=>$parent,
260             begin_indent=>$m{block_begin_indent},
261             end_indent=>$m{block_end_indent},
262             name=>$m{block_name}, args=>__parse_args($m{block_raw_arg}),
263             raw_content=>$m{block_content},
264 3         20 );
265              
266             } elsif ($m{setting}) {
267              
268 60         4266 require Org::Element::Setting;
269 60         194 my $uc_setting_name = uc($m{setting_name});
270 60 100 100     201 if ($m{setting_indent} &&
271 8         33 !(grep { $_ eq $uc_setting_name }
272 8         28 @{Org::Element::Setting->indentable_settings})) {
273 6         12 push @text, $m{setting};
274 6         24 next;
275             } else {
276             $el = Org::Element::Setting->new(
277             pass => $pass,
278             _str=>$m{setting},
279             document=>$self, parent=>$parent,
280             indent => $m{setting_indent},
281             name=>$m{setting_name},
282             raw_arg => $m{setting_raw_arg},
283 54         221 args=>__parse_args($m{setting_raw_arg}),
284             );
285             }
286              
287             } elsif ($m{fixedw} && $pass == 2) {
288              
289 7         1007 require Org::Element::FixedWidthSection;
290             $el = Org::Element::FixedWidthSection->new(
291             pass => $pass,
292             _str=>$m{fixedw},
293 7         144 document=>$self, parent=>$parent,
294             );
295              
296             } elsif ($m{comment} && $pass == 2) {
297              
298 18         4515 require Org::Element::Comment;
299             $el = Org::Element::Comment->new(
300             _str=>$m{comment},
301 18         305 document=>$self, parent=>$parent,
302             );
303              
304             } elsif ($m{table} && $pass == 2) {
305              
306 6         1492 require Org::Element::Table;
307             $el = Org::Element::Table->new(
308             pass=>$pass,
309             _str=>$m{table},
310 6         92 document=>$self, parent=>$parent,
311             );
312              
313             } elsif ($m{drawer} && $pass == 2) {
314              
315 15         2508 require Org::Element::Drawer;
316 15         44 my $raw_content = $m{drawer_content};
317             $el = Org::Element::Drawer->new(
318             document=>$self, parent=>$parent,
319 15         269 name => uc($m{drawer_name}), pass => $pass,
320             );
321 14         61 $self->_add_text($raw_content, $el, $pass);
322              
323             # for properties, we also parse property lines from raw drawer
324             # content. this is currently separate from normal Org text parsing,
325             # i'm not clear yet on how to do this canonically.
326 14         66 $el->_parse_properties($raw_content);
327              
328             } elsif ($m{li_header} && $pass == 2) {
329              
330 90         2585 require Org::Element::List;
331 90         2136 require Org::Element::ListItem;
332              
333 90         229 my $level = length($m{li_indent});
334 90         159 my $bullet = $m{li_bullet};
335 90         139 my $indent = $m{li_indent};
336 90         134 my $dt = $m{li_dt};
337 90         118 my $cbstate = $m{li_cbstate};
338 90 100       264 my $type = defined($dt) ? 'D' :
    100          
339             $bullet =~ /^\d+\./ ? 'O' : 'U';
340 90 100       222 my $bstyle = $type eq 'O' ? '<N>.' : $bullet;
341              
342             # parent for list is the last listitem of a lesser-indented list (or
343             # last headline, or document)
344 90   66     205 $parent = $last_headline // $self;
345 90         217 for (my $i=$level-1; $i>=0; $i--) {
346 107 100       241 if ($last_lists->[$i]) {
347 27         63 $parent = $last_lists->[$i]->children->[-1];
348 27         37 last;
349             }
350             }
351              
352 90         141 my $list = $last_lists->[$level];
353 90 100 100     399 if (!$list || $list->type ne $type ||
      100        
354             $list->bullet_style ne $bstyle) {
355 51         1011 $list = Org::Element::List->new(
356             document => $self, parent => $parent,
357             indent=>$indent, type=>$type, bullet_style=>$bstyle,
358             );
359 51         7985 $last_lists->[$level] = $list;
360 51 100       194 $parent->children([]) if !$parent->children;
361 51         90 push @{ $parent->children }, $list;
  51         146  
362             }
363 90         147 $last_lists->[$level] = $list;
364              
365             # parent for list item is list
366 90         121 $parent = $list;
367              
368 90         1979 $el = Org::Element::ListItem->new(
369             document=>$self, parent=>$list,
370             indent=>$indent, bullet=>$bullet);
371 90 100       8280 $el->check_state($cbstate) if $cbstate;
372 90 100       214 $el->desc_term($self->_add_text_container($dt, $list, $pass))
373             if defined($dt);
374              
375 90         175 splice @$last_lists, $level+1;
376 90         163 $last_listitem = $el;
377              
378             } elsif ($m{headline} && $pass == 2) {
379              
380 171         8610 require Org::Element::Headline;
381 171         467 my $level = length $m{h_bullet};
382              
383             # parent is upper-level headline
384 171         267 $parent = undef;
385 171         496 for (my $i=$level-1; $i>=0; $i--) {
386 181 100       489 $parent = $last_headlines->[$i] and last;
387             }
388 171   33     368 $parent //= $self;
389              
390             $el = Org::Element::Headline->new(
391             _str=>$m{headline},
392 171         3482 document=>$self, parent=>$parent,
393             level=>$level,
394             );
395 171 100       36211 $el->tags(__split_tags($m{h_tags})) if ($m{h_tags});
396 171         360 my $title = $m{h_title};
397              
398             # recognize todo keyword
399             my $todo_kw_re = "(?:".
400 595         1579 join("|", map {quotemeta}
401             "COMMENT",
402 171         306 @{$self->todo_states}, @{$self->done_states}) . ")";
  171         490  
  171         419  
403 171 100       1760 if ($title =~ s/^($todo_kw_re)(\s+|\W)/$2/) {
404 38         101 my $state = $1;
405 38         157 $title =~ s/^\s+//;
406 38         119 $el->is_todo(1);
407 38         94 $el->todo_state($state);
408 38 100       70 $el->is_done((grep { $_ eq $state } @{ $self->done_states }) ? 1:0);
  52         228  
  38         94  
409             }
410              
411             # recognize priority cookie
412             my $prio_re = "(?:".
413 171         382 join("|", map {quotemeta} @{$self->priorities}) . ")";
  517         1163  
  171         450  
414 171 100       1032 if ($title =~ s/\[#($prio_re)\]\s*//) {
415 4         18 $el->priority($1);
416             }
417              
418             # recognize statistics cookie
419 171 100       581 if ($title =~ s!\[(\d+%|\d+/\d+)\]\s*!!o) {
420 6         27 $el->statistics_cookie($1);
421             }
422              
423 171         433 $el->title($self->_add_text_container($title, $parent, $pass));
424              
425 171         480 $last_headlines->[$el->level] = $el;
426 171         428 splice @$last_headlines, $el->level+1;
427 171         251 $last_headline = $el;
428 171         283 $last_listitem = undef;
429 171         513 $last_lists = [];
430             }
431              
432             # we haven't caught other matches to become element
433 670 50 66     35224 die "BUG1: no element" unless $el || $pass != 2;
434              
435 670 100       2034 $parent->children([]) if !$parent->children;
436 670         908 push @{ $parent->children }, $el;
  670         1380  
437 670         7163 $last_el = $el;
438             }
439              
440             # remaining text
441 184 100       471 if (@text) {
442 79         1091 $self->_add_text(join("", @text), $parent, $pass);
443             }
444 181         458 @text = ();
445              
446 181         1095 log_trace('<- _parse(), elapsed time=%.3fs',
447             tv_interval($t0, [gettimeofday]));
448             }
449              
450             sub _add_text_container {
451 185     185   2431 require Org::Element::Text;
452 185         557 my ($self, $str, $parent, $pass) = @_;
453 185         3411 my $container = Org::Element::Text->new(
454             document=>$self, parent=>$parent,
455             text=>'', style=>'',
456             );
457 185         17976 $self->_add_text($str, $container, $pass);
458             $container = $container->children->[0] if
459 185 100 100     579 $container->children && @{$container->children} == 1 &&
  183   66     1352  
460             $container->children->[0]->isa('Org::Element::Text');
461 185         653 $container;
462             }
463              
464             sub _add_text {
465 591     591   12582 require Org::Element::Text;
466 591         1388 my ($self, $str, $parent, $pass) = @_;
467 591   33     1278 $parent //= $self;
468             #$log->tracef("-> _add_text(%s, pass=%d)", $str, $pass);
469              
470 591         827 my @plain_text;
471 591         3977 while ($str =~ /$text_re/og) {
472 1976         14576 my %m = %+;
473             #if ($log->is_trace) {
474             # # profiler shows that this is very heavy, so commenting this out
475             # $log->tracef("TMP: match text: %s", \%+);
476             #}
477 1976         4111 my $el;
478              
479 1976 100 100     6790 if (defined $m{plain_text} && $pass == 2) {
480 1012         1936 push @plain_text, $m{plain_text};
481 1012         4504 next;
482             } else {
483 964 100       1913 if (@plain_text) {
484 117         531 $self->_add_plain_text(join("", @plain_text), $parent, $pass);
485 117         249 @plain_text = ();
486             }
487             }
488              
489 964 100 100     9648 if ($m{link} && $pass == 2) {
    100 33        
    50 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100          
490 6         1430 require Org::Element::Link;
491             $el = Org::Element::Link->new(
492             document => $self, parent => $parent,
493             link=>$m{link_link},
494 6         83 );
495 6 100 66     5109 if (defined($m{link_desc}) && length($m{link_desc})) {
496             $el->description(
497             $self->_add_text_container($m{link_desc},
498 3         12 $el, $pass));
499             }
500             } elsif ($m{radio_target}) {
501 4         449 require Org::Element::RadioTarget;
502             $el = Org::Element::RadioTarget->new(
503             pass => $pass,
504             document => $self, parent => $parent,
505             target=>$m{rt_target},
506 4         68 );
507             } elsif ($m{target} && $pass == 2) {
508 0         0 require Org::Element::Target;
509             $el = Org::Element::Target->new(
510             document => $self, parent => $parent,
511             target=>$m{t_target},
512 0         0 );
513             } elsif ($m{fn_num} && $pass == 2) {
514 1         466 require Org::Element::Footnote;
515             $el = Org::Element::Footnote->new(
516             document => $self, parent => $parent,
517 1         7 name=>$m{fn_num_num}, is_ref=>1,
518             );
519             } elsif ($m{fn_namedef} && $pass == 2) {
520 1         4 require Org::Element::Footnote;
521             $el = Org::Element::Footnote->new(
522             document => $self, parent => $parent,
523             name=>$m{fn_namedef_name},
524 1 50       35 is_ref=>$m{fn_namedef_def} ? 0:1,
525             );
526             $el->def($self->_add_text_container($m{fn_namedef_def},
527 1         8 $parent, $pass));
528             } elsif ($m{fn_nameidef} && $pass == 2) {
529 3         11 require Org::Element::Footnote;
530             $el = Org::Element::Footnote->new(
531             document => $self, parent => $parent,
532             name=>$m{fn_nameidef_name},
533             is_ref=>($m{fn_nameidef_def} ? 0:1) ||
534 3   100     69 !length($m{fn_nameidef_name}),
535             );
536             $el->def(length($m{fn_nameidef_def}) ?
537             $self->_add_text_container($m{fn_nameidef_def},
538 3 100       24 $parent, $pass) : undef);
539             } elsif ($m{trange} && $pass == 2) {
540 3         13 require Org::Element::TimeRange;
541 3         9 require Org::Element::Timestamp;
542 3         61 $el = Org::Element::TimeRange->new(
543             document => $self, parent => $parent,
544             );
545 3         86 my $opts = {allow_event_duration=>0, allow_repeater=>0};
546             $el->ts1(Org::Element::Timestamp->new(
547 3         59 _str=>$m{trange_ts1}, document=>$self, parent=>$parent));
548 3         71 $el->ts1->_parse_timestamp($m{trange_ts1}, $opts);
549             $el->ts2(Org::Element::Timestamp->new(
550 3         72 _str=>$m{trange_ts2}, document=>$self, parent=>$parent));
551 3         73 $el->ts2->_parse_timestamp($m{trange_ts2}, $opts);
552 3         48 $el->children([$el->ts1, $el->ts2]);
553             } elsif ($m{tstamp} && $pass == 2) {
554 26         1125 require Org::Element::Timestamp;
555             $el = Org::Element::Timestamp->new(
556 26         573 _str => $m{tstamp}, document => $self, parent => $parent,
557             );
558 26         7298 $el->_parse_timestamp($m{tstamp});
559             } elsif ($m{act_trange} && $pass == 2) {
560 5         517 require Org::Element::TimeRange;
561 5         507 require Org::Element::Timestamp;
562 5         112 $el = Org::Element::TimeRange->new(
563             document => $self, parent => $parent,
564             );
565 5         1665 my $opts = {allow_event_duration=>0, allow_repeater=>0};
566             $el->ts1(Org::Element::Timestamp->new(
567 5         81 _str=>$m{act_trange_ts1}, document=>$self, parent=>$parent));
568 5         2255 $el->ts1->_parse_timestamp($m{act_trange_ts1}, $opts);
569             $el->ts2(Org::Element::Timestamp->new(
570 3         83 _str=>$m{act_trange_ts2}, document=>$self, parent=>$parent));
571 3         97 $el->ts2->_parse_timestamp($m{act_trange_ts2}, $opts);
572 3         43 $el->children([$el->ts1, $el->ts2]);
573             } elsif ($m{act_tstamp} && $pass == 2) {
574 29         3816 require Org::Element::Timestamp;
575             $el = Org::Element::Timestamp->new(
576 29         513 _str => $m{act_tstamp}, document => $self, parent => $parent,
577             );
578 29         15471 $el->_parse_timestamp($m{act_tstamp});
579             } elsif ($m{markup_start} && $pass == 2) {
580 24         102 require Org::Element::Text;
581             $el = Org::Element::Text->new(
582             document => $self, parent => $parent,
583             style=>'', text=>$m{markup_start},
584 24         436 );
585             # temporary mark, we need to apply markup later
586 24         2155 $el->{_mu_start}++;
587             } elsif ($m{markup_end} && $pass == 2) {
588 48         196 require Org::Element::Text;
589             $el = Org::Element::Text->new(
590             document => $self, parent => $parent,
591             style=>'', text=>$m{markup_end},
592 48         898 );
593             # temporary mark, we need to apply markup later
594 48         872 $el->{_mu_end}++;
595             }
596 961 50 66     3273 die "BUG2: no element" unless $el || $pass != 2;
597 961 100       2405 $parent->children([]) if !$parent->children;
598 961         1237 push @{ $parent->children }, $el;
  961         6543  
599             }
600              
601             # remaining text
602 588 100 66     2008 if (@plain_text && $pass == 2) {
603 394 100       1562 $parent->children([]) if !$parent->children;
604 394         601 push @{$parent->children}, Org::Element::Text->new(
  394         8667  
605             text => join("", @plain_text), style=>'',
606             document=>$self, parent=>$parent);
607 394         19319 @plain_text = ();
608             }
609              
610 588 100       1535 if ($pass == 2) {
611 413         1165 $self->_apply_markup($parent);
612 413 100       547 if (@{$self->radio_targets}) {
  413         1250  
613 1         3 my $re = join "|", map {quotemeta} @{$self->radio_targets};
  2         9  
  1         3  
614 1         23 $re = qr/(?:$re)/i;
615 1         6 $self->_linkify_rt_recursive($re, $parent);
616             }
617 413   100     1410 my $c = $parent->children // [];
618             }
619              
620             #$log->tracef('<- _add_text()');
621             }
622              
623             # to keep parser's regexes simple and fast, we detect markup in regex rather
624             # simplistically (as text element) and then apply some more filtering & applying
625             # logic here
626              
627             sub _apply_markup {
628             #$log->trace("-> _apply_markup()");
629 413     413   788 my ($self, $parent) = @_;
630 413         579 my $last_index = 0;
631 413 100       1164 my $c = $parent->children or return;
632              
633 407         568 while (1) {
634             #$log->tracef("text cluster = %s", [map {$_->as_string} @$c]);
635             # find a new mu_start
636 429         651 my $mu_start_index = -1;
637 429         570 my $mu;
638 429         1014 for (my $i = $last_index; $i < @$c; $i++) {
639 831 100       2244 next unless $c->[$i]->{_mu_start};
640 22         30 $mu_start_index = $i; $mu = $c->[$i]->text;
  22         52  
641             #$log->tracef("found mu_start at %d (%s)", $i, $mu);
642 22         33 last;
643             }
644 429 100       936 unless ($mu_start_index >= 0) {
645             #$log->trace("no more mu_start found");
646 407         612 last;
647             }
648              
649             # check whether this is a valid markup (has text, has markup end, not
650             # interspersed with non-text, no more > 1 newlines)
651 22         32 my $mu_end_index = 0;
652 22         30 my $newlines = 0;
653 22         30 my $has_text;
654             my $has_unmarkable;
655 22         70 for (my $i=$mu_start_index+1; $i < @$c; $i++) {
656 60 100       154 if ($c->[$i]->isa('Org::Element::Text')) {
657 59         85 $has_text++;
658             } elsif (1) {
659             } else {
660             $has_unmarkable++; last;
661             }
662 60 100 100     176 if ($c->[$i]->{_mu_end} && $c->[$i]->text eq $mu) {
663             #$log->tracef("found mu_end at %d", $i);
664 13         21 $mu_end_index = $i; last;
  13         20  
665             }
666 47         97 my $text = $c->[$i]->as_string;
667 47         147 $newlines++ while $text =~ /\R/g;
668 47 100       126 last if $newlines > 1;
669             }
670 22   66     131 my $valid = $has_text && !$has_unmarkable
671             && $mu_end_index && $newlines <= 1;
672             #$log->tracef("mu candidate: start=%d, end=%s, ".
673             # "has_text=%s, has_unmarkable=%s, newlines=%d, valid=%s",
674             # $mu_start_index, $mu_end_index,
675             # $has_text, $has_unmarkable, $newlines, $valid
676             # );
677 22 100       52 if ($valid) {
678 24     24   237 no warnings 'once';
  24         53  
  24         27041  
679             my $mu_el = Org::Element::Text->new(
680             document => $self, parent => $parent,
681 13         283 style=>$Org::Element::Text::mu2style{$mu}, text=>'',
682             );
683 13         266 my @c2 = splice @$c, $mu_start_index,
684             $mu_end_index-$mu_start_index+1, $mu_el;
685             #$log->tracef("grouping %s", [map {$_->text} @c2]);
686 13         47 $mu_el->children(\@c2);
687 13         21 shift @c2;
688 13         35 pop @c2;
689 13         36 for (@c2) {
690 22         37 $_->{parent} = $mu_el;
691             }
692 13         36 $self->_merge_text_elements(\@c2);
693             # squish if only one child
694 13 100       31 if (@c2 == 1) {
695 12         41 $mu_el->text($c2[0]->text);
696 12         40 $mu_el->children(undef);
697             }
698             } else {
699 9         18 undef $c->[$mu_start_index]->{_mu_start};
700 9         17 $last_index++;
701             }
702             }
703 407         929 $self->_merge_text_elements($c);
704             #$log->trace("<- _apply_markup()");
705             }
706              
707             sub _merge_text_elements {
708 420     420   714 my ($self, $els) = @_;
709             #$log->tracef("-> _merge_text_elements(%s)", [map {$_->as_string} @$els]);
710 420 100       1048 return unless @$els >= 2;
711 110         182 my $i=-1;
712 110         158 while (1) {
713 462         635 $i++;
714 462 100       921 last if $i >= @$els;
715 352 100 100     1797 next if $els->[$i]->children || !$els->[$i]->isa('Org::Element::Text');
716 215   50     632 my $istyle = $els->[$i]->style // "";
717 215         298 while (1) {
718 298 100 100     1622 last if $i+1 >= @$els || $els->[$i+1]->children ||
      100        
719             !$els->[$i+1]->isa('Org::Element::Text');
720 104 100 50     318 last if ($els->[$i+1]->style // "") ne $istyle;
721             #$log->tracef("merging text[%d] '%s' with '%s'",
722             # $i, $els->[$i]->text, $els->[$i+1]->text);
723 83   50     243 $els->[$i]->{text} .= $els->[$i+1]->{text} // "";
724 83         235 splice @$els, $i+1, 1;
725             }
726             }
727             #$log->tracef("merge result = %s", [map {$_->as_string} @$els]);
728             #$log->trace("<- _merge_text_elements()");
729             }
730              
731             sub _linkify_rt_recursive {
732 8     8   31 require Org::Element::Text;
733 8         54 require Org::Element::Link;
734 8         16 my ($self, $re, $parent) = @_;
735 8         17 my $c = $parent->children;
736 8 100       32 return unless $c;
737 1         10 for (my $i=0; $i<@$c; $i++) {
738 12         19 my $el = $c->[$i];
739 12 100       32 if ($el->isa('Org::Element::Text')) {
740 7         56 my @split0 = split /\b($re)\b/, $el->text;
741 7 100       28 next unless @split0 > 1;
742 2         14 my @split;
743 2         5 for my $s (@split0) {
744 8 100       176 if ($s =~ /^$re$/) {
    100          
745 3         63 push @split, Org::Element::Link->new(
746             document=>$self, parent=>$parent,
747             link=>$s, description=>undef,
748             from_radio_target=>1,
749             );
750             } elsif (length $s) {
751 4         89 push @split, Org::Element::Text->new(
752             document=>$self, parent=>$parent,
753             text=>$s, style=>$el->style,
754             );
755             }
756             }
757 2         42 splice @$c, $i, 1, @split;
758             }
759 7         19 $self->_linkify_rt_recursive($re, $el);
760             }
761             }
762              
763             sub _add_plain_text {
764 117     117   476 require Org::Element::Text;
765 117         286 my ($self, $str, $parent, $pass) = @_;
766 117         2261 my $el = Org::Element::Text->new(
767             document=>$self, parent=>$parent, style=>'', text=>$str);
768 117 100       12056 $parent->children([]) if !$parent->children;
769 117         174 push @{ $parent->children }, $el;
  117         323  
770             }
771              
772             sub __split_tags {
773 7     7   111 [$_[0] =~ /:([^:]+)/g];
774             }
775              
776             sub load_element_modules {
777 2     2 1 936 require Module::List;
778 2         34071 require Module::Load;
779              
780 2         2309 my $mm = Module::List::list_modules("Org::Element::", {list_modules=>1});
781 2         2768 for (keys %$mm) {
782 38         695 Module::Load::load($_);
783             }
784             }
785              
786             sub cmp_priorities {
787 11     11 1 69 my ($self, $p1, $p2) = @_;
788              
789 11         27 my $pp = $self->priorities;
790 11     20   60 my $pos1 = firstidx {$_ eq $p1} @$pp;
  20         34  
791 11 100       47 return undef unless $pos1 >= 0; ## no critic: Subroutines::ProhibitExplicitReturnUndef
792 9     17   36 my $pos2 = firstidx {$_ eq $p2} @$pp;
  17         27  
793 9 100       40 return undef unless $pos2 >= 0; ## no critic: Subroutines::ProhibitExplicitReturnUndef
794 7         34 $pos1 <=> $pos2;
795             }
796              
797             1;
798             # ABSTRACT: Represent an Org document
799              
800             __END__
801              
802             =pod
803              
804             =encoding UTF-8
805              
806             =head1 NAME
807              
808             Org::Document - Represent an Org document
809              
810             =head1 VERSION
811              
812             This document describes version 0.559 of Org::Document (from Perl distribution Org-Parser), released on 2023-07-12.
813              
814             =head1 SYNOPSIS
815              
816             use Org::Document;
817              
818             # create a new Org document tree from string
819             my $org = Org::Document->new(from_string => <<EOF);
820             * heading 1a
821             some text
822             ** heading 2
823             * heading 1b
824             EOF
825              
826             =head1 DESCRIPTION
827              
828             Derived from L<Org::Element>.
829              
830             =head1 ATTRIBUTES
831              
832             =head2 tags => ARRAY
833              
834             List of tags for this file, usually set via #+FILETAGS.
835              
836             =head2 todo_states => ARRAY
837              
838             List of known (action-requiring) todo states. Default is ['TODO'].
839              
840             =head2 done_states => ARRAY
841              
842             List of known done (non-action-requiring) states. Default is ['DONE'].
843              
844             =head2 priorities => ARRAY
845              
846             List of known priorities. Default is ['A', 'B', 'C'].
847              
848             =head2 drawer_names => ARRAY
849              
850             List of known drawer names. Default is [qw/CLOCK LOGBOOK PROPERTIES/].
851              
852             =head2 properties => ARRAY
853              
854             File-wide properties.
855              
856             =head2 radio_targets => ARRAY
857              
858             List of radio target text.
859              
860             =head2 time_zone => ARRAY
861              
862             If set, will be passed to DateTime->new() (e.g. by L<Org::Element::Timestamp>).
863              
864             =head2 ignore_unknown_settings => bool
865              
866             If set to true, unknown settings will not cause a parse failure.
867              
868             =head1 METHODS
869              
870             =for Pod::Coverage BUILD
871              
872             =head2 new
873              
874             Usage:
875              
876             $doc = Org::Document->new(%args);
877              
878             Create document object. If C<from_string> argument is specified, will parse
879             the string. Otherwise, will create an empty document object. Arguments:
880              
881             =over
882              
883             =item * from_string
884              
885             String. String to parse into document object tree content.
886              
887             =back
888              
889             =head2 load_element_modules()
890              
891             Load all Org::Element::* modules. This is useful when wanting to work with
892             element objects retrieved from serialization, where the element modules have not
893             been loaded.
894              
895             =head2 cmp_priorities($p1, $p2) => -1|0|-1
896              
897             Compare two priorities C<$p1> and C<$p2>. Return result like Perl's C<cmp>: 0 if
898             the two are the same, -1 if C<$p1> is of I<higher> priority (since it's more to
899             the left position in priority list, which is sorted highest-first) than C<$p2>,
900             and 1 if C<$p2> is of I<lower> priority than C<$p1>.
901              
902             If either C<$p1> or C<$p2> has unknown priority, will return undef.
903              
904             Examples:
905              
906             $doc->cmp_priorities('A', 'A') # -> 0
907             $doc->cmp_priorities('A', 'B') # -> -1 (A is higher than B)
908             $doc->cmp_priorities('C', 'B') # -> 1 (C is lower than B)
909             $doc->cmp_priorities('X', 'A') # -> undef (X is unknown)
910              
911             Note that X could be known if there is a C<#+PRIORITIES> setting which defines
912             it.
913              
914             =head1 HOMEPAGE
915              
916             Please visit the project's homepage at L<https://metacpan.org/release/Org-Parser>.
917              
918             =head1 SOURCE
919              
920             Source repository is at L<https://github.com/perlancar/perl-Org-Parser>.
921              
922             =head1 AUTHOR
923              
924             perlancar <perlancar@cpan.org>
925              
926             =head1 CONTRIBUTING
927              
928              
929             To contribute, you can send patches by email/via RT, or send pull requests on
930             GitHub.
931              
932             Most of the time, you don't need to build the distribution yourself. You can
933             simply modify the code, then test via:
934              
935             % prove -l
936              
937             If you want to build the distribution (e.g. to try to install it locally on your
938             system), you can install L<Dist::Zilla>,
939             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
940             L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
941             Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
942             that are considered a bug and can be reported to me.
943              
944             =head1 COPYRIGHT AND LICENSE
945              
946             This software is copyright (c) 2023, 2022, 2021, 2020, 2019, 2017, 2016, 2015, 2014, 2013, 2012, 2011 by perlancar <perlancar@cpan.org>.
947              
948             This is free software; you can redistribute it and/or modify it under
949             the same terms as the Perl 5 programming language system itself.
950              
951             =head1 BUGS
952              
953             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Org-Parser>
954              
955             When submitting a bug or request, please include a test-file or a
956             patch to an existing test-file that illustrates the bug or desired
957             feature.
958              
959             =cut