File Coverage

blib/lib/Org/Parser/Tiny.pm
Criterion Covered Total %
statement 74 131 56.4
branch 17 54 31.4
condition 8 20 40.0
subroutine 11 22 50.0
pod 3 3 100.0
total 113 230 49.1


line stmt bran cond sub pod time code
1             ## no critic: Modules::RequireExplicitPackage
2              
3 1     1   65704 use 5.010001;
  1         12  
4 1     1   5 use strict;
  1         2  
  1         19  
5 1     1   5 use warnings;
  1         10  
  1         1856  
6              
7             package Org::Parser::Tiny;
8              
9             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
10             our $DATE = '2020-02-07'; # DATE
11             our $DIST = 'Org-Parser-Tiny'; # DIST
12             our $VERSION = '0.005'; # VERSION
13              
14             sub new {
15 0     0 1 0 my $class = shift;
16 0         0 bless {}, $class;
17             }
18              
19             sub _parse {
20 1     1   4 my ($self, $lines, $opts) = @_;
21              
22             # stage 1: get todo keywords
23              
24 1         2 my @undone_keywords;
25             my @done_keywords;
26 1         2 my $linenum = 0;
27 1         3 while ($linenum < @$lines) {
28 17         24 my $line = $lines->[$linenum];
29 17         21 $linenum++;
30 17 50       36 if ($line =~ s/^#\+TODO:\s*//) {
31 0 0       0 my ($undone_keywords, $done_keywords) =
32             $line =~ /^\s*(.*?)\s*\|\s*(.*?)\s*$/
33             or die "Line $linenum: Invalid #+TODO: please use ... | ...";
34 0         0 while ($undone_keywords =~ /(\w+)/g) {
35 0         0 push @undone_keywords, $1;
36             }
37 0         0 while ($done_keywords =~ /(\w+)/g) {
38 0         0 push @done_keywords, $1;
39             }
40             }
41             }
42 1 50       4 @undone_keywords = ("TODO") unless @undone_keywords;
43 1 50       5 @done_keywords = ("DONE") unless @done_keywords;
44 1         3 my $undone_re = join("|", @undone_keywords); $undone_re= qr/(?:$undone_re)/;
  1         15  
45 1         4 my $done_re = join("|", @done_keywords ); $done_re = qr/(?:$done_re)/;
  1         21  
46              
47             # stage 2: build nodes
48              
49             # a linear list of nodes
50 1         10 my @nodes = (
51             Org::Parser::Tiny::Node::Document->new(),
52             );
53              
54 1         2 $linenum = 0;
55 1         5 while ($linenum < @$lines) {
56 17         30 my $line = $lines->[$linenum];
57 17         19 $linenum++;
58 17 100       56 if ($line =~ /^(\*+) (.*)/) {
59             #say "D: got headline $line";
60 11         24 my $level = length($1);
61 11         21 my $title = $2;
62 11         26 my $node = Org::Parser::Tiny::Node::Headline->new(
63             _str => $line,
64             level => $level,
65             );
66              
67             # extract todo state
68 11 50       80 if ($title =~ s/\s*($undone_re)\s+//) {
    50          
69 0         0 $node->{is_todo} = 1;
70 0         0 $node->{is_done} = 0;
71 0         0 $node->{todo_state} = $1;
72             } elsif ($title =~ s/\s*($done_re)\s+//) {
73 0         0 $node->{is_todo} = 1;
74 0         0 $node->{is_done} = 1;
75 0         0 $node->{todo_state} = $1;
76             } else {
77 11         24 $node->{is_todo} = 0;
78 11         15 $node->{is_done} = 0;
79 11         18 $node->{todo_state} = "";
80             }
81              
82             # extract tags
83 11 100       26 if ($title =~ s/\s+:((?:\w+:)+)$//) {
84 1         3 my $tags = $1;
85 1         2 my @tags;
86 1         6 while ($tags =~ /(\w+)/g) {
87 1         4 push @tags, $1;
88             }
89 1         4 $node->{tags} = \@tags;
90             }
91              
92 11         21 $node->{title} = $title;
93              
94             # find the first node which has the lower level (or the root node)
95             # as the parent node
96 11         20 my $i = $#nodes;
97 11         71 while ($i >= 0) {
98 44 100 100     120 if ($i == 0 || $nodes[$i]{level} < $level) {
99 11         13 $node->{parent} = $nodes[$i];
100 11         12 push @{ $nodes[$i]{children} }, $node;
  11         24  
101 11         18 last;
102             }
103 33         49 $i--;
104             }
105 11         30 push @nodes, $node;
106             } else {
107 6         18 $nodes[-1]{preamble} .= $line;
108             }
109             }
110              
111 1         8 $nodes[0];
112             }
113              
114             sub parse {
115 1     1 1 1115 my ($self, $arg, $opts) = @_;
116 1 50       5 die "Please specify a defined argument to parse()\n" unless defined($arg);
117              
118 1   50     9 $opts ||= {};
119              
120 1         2 my $lines;
121 1         2 my $r = ref($arg);
122 1 50 0     3 if (!$r) {
    0 0        
    0          
    0          
123 1         9 $lines = [split /^/, $arg];
124             } elsif ($r eq 'ARRAY') {
125 0         0 $lines = [@$arg];
126             } elsif ($r eq 'GLOB' || blessed($arg) && $arg->isa('IO::Handle')) {
127             #$lines = split(/^/, join("", <$arg>));
128 0         0 $lines = [<$arg>];
129             } elsif ($r eq 'CODE') {
130 0         0 my @chunks;
131 0         0 while (defined(my $chunk = $arg->())) {
132 0         0 push @chunks, $chunk;
133             }
134 0         0 $lines = [split /^/, (join "", @chunks)];
135             } else {
136 0         0 die "Invalid argument, please supply a ".
137             "string|arrayref|coderef|filehandle\n";
138             }
139 1         4 $self->_parse($lines, $opts);
140             }
141              
142             sub parse_file {
143 0     0 1 0 my ($self, $filename, $opts) = @_;
144 0   0     0 $opts ||= {};
145              
146 0         0 my $content = do {
147 0 0       0 open my($fh), "<", $filename or die "Can't open $filename: $!\n";
148 0         0 local $/;
149 0         0 scalar(<$fh>);
150             };
151              
152 0         0 $self->parse($content, $opts);
153             }
154              
155              
156             # abstract class: Org::Parser::Tiny::Node
157             package Org::Parser::Tiny::Node;
158              
159             sub new {
160 12     12   28 my ($class, %args) = @_;
161 12   50     45 $args{children} //= [];
162 12         35 bless \%args, $class;
163             }
164              
165             sub parent {
166 0 0   0   0 if (@_ > 1) {
167 0         0 $_[0]{parent} = $_[1];
168             }
169 0         0 $_[0]{parent};
170             }
171              
172             sub children {
173 11 50   11   32 if (@_ > 1) {
174 0         0 $_[0]{children} = $_[1];
175             }
176 11 50       62 $_[0]{children} || [];
177             }
178              
179 0     0   0 sub as_string { $_[0]{_str} }
180              
181 8     8   11 sub children_as_string { join("", map { $_->as_string } @{ $_[0]->children }) }
  6         12  
  8         16  
182              
183             # abstract class: Org::Parser::Tiny::HasPreamble
184             package Org::Parser::Tiny::Node::HasPreamble;
185              
186             our @ISA = qw(Org::Parser::Tiny::Node);
187              
188             sub new {
189 12     12   31 my ($class, %args) = @_;
190 12   50     48 $args{preamble} //= "";
191 12         32 $class->SUPER::new(%args);
192             }
193              
194              
195             # class: Org::Parser::Tiny::Document: top level node
196             package Org::Parser::Tiny::Node::Document;
197              
198             our @ISA = qw(Org::Parser::Tiny::Node::HasPreamble);
199              
200             sub as_string {
201 0     0   0 $_[0]->{preamble} . $_[0]->children_as_string;
202             }
203              
204              
205             # class: Org::Parser::Tiny::Node::Headline: headline with its content
206             package Org::Parser::Tiny::Node::Headline;
207              
208             our @ISA = qw(Org::Parser::Tiny::Node::HasPreamble);
209              
210             sub level {
211 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{level} = $_[1] }
  0         0  
  0         0  
212 0         0 $_[0]{level};
213             }
214              
215             sub title {
216 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{title} = $_[1] }
  0         0  
  0         0  
217 0         0 $_[0]{title};
218             }
219              
220             sub is_todo {
221 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{is_todo} = $_[1] }
  0         0  
  0         0  
222 0         0 $_[0]{is_todo};
223             }
224              
225             sub is_done {
226 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{is_done} = $_[1] }
  0         0  
  0         0  
227 0         0 $_[0]{is_done};
228             }
229              
230             sub todo_state {
231 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{todo_state} = $_[1] }
  0         0  
  0         0  
232 0         0 $_[0]{todo_state};
233             }
234              
235             sub tags {
236 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{tags} = $_[1] }
  0         0  
  0         0  
237 0 0       0 $_[0]{tags} || [];
238             }
239              
240             sub header_as_string {
241             ($_[0]->{_str} //
242             join('',
243             "*" x $_[0]{level},
244             " ",
245             (length $_[0]{todo_state} ? "$_[0]{todo_state} " : ""),
246             "$_[0]{title}",
247 8 50 66 8   40 (defined $_[0]{tags} ? " :".join(":", @{ $_[0]{tags} }).":" : ""),
  1 50       8  
248             "\n",
249             ));
250             }
251              
252             sub as_string {
253             $_[0]->header_as_string .
254             $_[0]->{preamble} .
255 8     8   19 $_[0]->children_as_string;
256             }
257              
258             1;
259             # ABSTRACT: Parse Org documents with as little code (and no non-core deps) as possible
260              
261             __END__