File Coverage

blib/lib/Org/Parser/Tiny.pm
Criterion Covered Total %
statement 69 126 54.7
branch 16 50 32.0
condition 8 20 40.0
subroutine 9 21 42.8
pod 3 3 100.0
total 105 220 47.7


line stmt bran cond sub pod time code
1             ## no critic: Modules::RequireExplicitPackage
2              
3 1     1   72638 use 5.010001;
  1         11  
4 1     1   6 use strict;
  1         1  
  1         24  
5 1     1   4 use warnings;
  1         10  
  1         1873  
6              
7             package Org::Parser::Tiny;
8              
9             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
10             our $DATE = '2020-02-06'; # DATE
11             our $DIST = 'Org-Parser-Tiny'; # DIST
12             our $VERSION = '0.003'; # 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   3 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         4 while ($linenum < @$lines) {
28 14         21 my $line = $lines->[$linenum];
29 14         16 $linenum++;
30 14 50       32 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       6 @undone_keywords = ("TODO") unless @undone_keywords;
43 1 50       4 @done_keywords = ("DONE") unless @done_keywords;
44 1         3 my $undone_re = join("|", @undone_keywords); $undone_re= qr/(?:$undone_re)/;
  1         16  
45 1         3 my $done_re = join("|", @done_keywords ); $done_re = qr/(?:$done_re)/;
  1         20  
46              
47             # stage 2: build nodes
48              
49             # a linear list of nodes
50 1         13 my @nodes = (
51             Org::Parser::Tiny::Node::Document->new(),
52             );
53              
54 1         3 $linenum = 0;
55 1         3 while ($linenum < @$lines) {
56 14         24 my $line = $lines->[$linenum];
57 14         16 $linenum++;
58 14 100       45 if ($line =~ /^(\*+) (.*)/) {
59             #say "D: got headline $line";
60 8         18 my $level = length($1);
61 8         15 my $title = $2;
62 8         23 my $node = Org::Parser::Tiny::Node::Headline->new(
63             _str => $line,
64             level => $level,
65             );
66              
67             # extract todo state
68 8 50       68 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 8         16 $node->{is_todo} = 0;
78 8         12 $node->{is_done} = 0;
79 8         12 $node->{todo_state} = "";
80             }
81              
82             # extract tags
83 8 100       23 if ($title =~ s/\s+:((?:\w+:)+)$//) {
84 1         4 my $tags = $1;
85 1         1 my @tags;
86 1         6 while ($tags =~ /(\w+)/g) {
87 1         5 push @tags, $1;
88             }
89 1         4 $node->{tags} = \@tags;
90             }
91              
92 8         18 $node->{title} = $title;
93              
94             # find the first node which has the lower level (or the root node)
95             # as the parent node
96 8         13 my $i = $#nodes;
97 8         17 while ($i >= 0) {
98 27 100 100     87 if ($i == 0 || $nodes[$i]{level} < $level) {
99 8         12 $node->{parent} = $nodes[$i];
100 8         10 push @{ $nodes[$i]{children} }, $node;
  8         16  
101 8         13 last;
102             }
103 19         34 $i--;
104             }
105 8         22 push @nodes, $node;
106             } else {
107 6         21 $nodes[-1]{preamble} .= $line;
108             }
109             }
110              
111 1         8 $nodes[0];
112             }
113              
114             sub parse {
115 1     1 1 1196 my ($self, $arg, $opts) = @_;
116 1 50       5 die "Please specify a defined argument to parse()\n" unless defined($arg);
117              
118 1   50     8 $opts ||= {};
119              
120 1         1 my $lines;
121 1         3 my $r = ref($arg);
122 1 50 0     4 if (!$r) {
    0 0        
    0          
    0          
123 1         7 $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         6 $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 9     9   21 my ($class, %args) = @_;
161 9   50     37 $args{children} //= [];
162 9         25 bless \%args, $class;
163             }
164              
165 0     0   0 sub parent { $_[0]{parent} }
166 3 50   3   24 sub children { $_[0]{children} || [] }
167 0     0   0 sub as_string { $_[0]{_str} }
168 0     0   0 sub children_as_string { join("", map { $_->as_string } @{ $_[0]->children }) }
  0         0  
  0         0  
169              
170             # abstract class: Org::Parser::Tiny::HasPreamble
171             package Org::Parser::Tiny::Node::HasPreamble;
172              
173             our @ISA = qw(Org::Parser::Tiny::Node);
174              
175             sub new {
176 9     9   22 my ($class, %args) = @_;
177 9   50     39 $args{preamble} //= "";
178 9         28 $class->SUPER::new(%args);
179             }
180              
181              
182             # class: Org::Parser::Tiny::Document: top level node
183             package Org::Parser::Tiny::Node::Document;
184              
185             our @ISA = qw(Org::Parser::Tiny::Node::HasPreamble);
186              
187             sub as_string {
188 0     0   0 $_[0]->{preamble} . $_[0]->children_as_string;
189             }
190              
191              
192             # class: Org::Parser::Tiny::Node::Headline: headline with its content
193             package Org::Parser::Tiny::Node::Headline;
194              
195             our @ISA = qw(Org::Parser::Tiny::Node::HasPreamble);
196              
197             sub level {
198 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{level} = $_[1] }
  0         0  
  0         0  
199 0         0 $_[0]{level};
200             }
201              
202             sub title {
203 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{title} = $_[1] }
  0         0  
  0         0  
204 0         0 $_[0]{title};
205             }
206              
207             sub is_todo {
208 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{is_todo} = $_[1] }
  0         0  
  0         0  
209 0         0 $_[0]{is_todo};
210             }
211              
212             sub is_done {
213 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{is_done} = $_[1] }
  0         0  
  0         0  
214 0         0 $_[0]{is_done};
215             }
216              
217             sub todo_state {
218 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{todo_state} = $_[1] }
  0         0  
  0         0  
219 0         0 $_[0]{todo_state};
220             }
221              
222             sub tags {
223 0 0   0   0 if (@_ > 1) { undef $_[0]{_str}; $_[0]{tags} = $_[1] }
  0         0  
  0         0  
224 0 0       0 $_[0]{tags} || [];
225             }
226              
227             sub as_string {
228             ($_[0]->{_str} //
229             join('',
230             "*" x $_[0]{level},
231             " ",
232             (length $_[0]{todo_state} ? "$_[0]{todo_state} " : ""),
233             "$_[0]{title}",
234 1         9 (defined $_[0]{tags} ? " :".join(":", @{ $_[0]{tags} }).":" : ""),
235             "\n",
236             )) .
237 2 50 66 2   20 $_[0]->{preamble};
    50          
238             }
239              
240             1;
241             # ABSTRACT: Parse Org documents with as little code (and no non-core deps) as possible
242              
243             __END__