File Coverage

lib/Loctools/Markdown/Parser.pm
Criterion Covered Total %
statement 142 142 100.0
branch 60 60 100.0
condition 23 24 95.8
subroutine 5 5 100.0
pod 0 4 0.0
total 230 235 97.8


line stmt bran cond sub pod time code
1             package Loctools::Markdown::Parser;
2              
3             # References:
4             #
5             # 1. Markdown: Syntax
6             # https://daringfireball.net/projects/markdown/syntax
7             #
8             # 2. GitHub Flavored Markdown Spec
9             # https://github.github.com/gfm/
10              
11 1     1   442 use strict;
  1         4  
  1         1372  
12              
13             sub new {
14 76     76 0 352865 my ($class) = @_;
15              
16 76         205 my $self = {};
17 76         173 bless($self, $class);
18              
19 76         317 $self->init;
20              
21 76         165 return $self;
22             }
23              
24             sub init {
25 152     152 0 245 my ($self) = @_;
26              
27 152         348 $self->{ast} = [];
28 152         282 $self->{stack} = [];
29 152         254 $self->{accum} = [];
30 152         266 $self->{mode} = 'p';
31 152         281 $self->{context} = {};
32 152         290 $self->{counter} = 0;
33             }
34              
35             sub process_accumulated {
36 227     227 0 380 my ($self) = @_;
37              
38 227 100       311 if (scalar(@{$self->{accum}}) == 0) {
  227         550  
39 63         114 return;
40             }
41              
42 164         266 my $ast_node;
43              
44 164 100       411 if ($self->{mode} eq 'blockquote') {
    100          
45             my @a = map {
46 24         73 $_ =~ s/^> //s; $_;
  24         72  
47 9         14 } @{$self->{accum}};
  9         20  
48 9         31 my $text = join("\n", @a);
49 9         24 my $child = Loctools::Markdown::Parser->new;
50             $ast_node = {
51             kind => $self->{mode},
52 9         53 children => $child->parse($text)
53             };
54             } elsif ($self->{mode} eq 'li') {
55 32         50 my $text = join("\n", @{$self->{accum}});
  32         81  
56 32         77 my $child = Loctools::Markdown::Parser->new;
57             $ast_node = {
58             kind => $self->{mode},
59             children => $child->parse($text),
60             context => $self->{context}
61 32         79 };
62             } else {
63 123         189 my $text;
64             my $kind;
65              
66 123         182 my $is_indented = 1;
67             map {
68 163 100       344 if ($is_indented) {
69 126 100       515 $is_indented = undef unless $_ =~ m/^ {4,}/;
70             }
71 123         169 } @{$self->{accum}};
  123         306  
72              
73 123 100       232 if ($is_indented) {
74             my @a = map {
75 6         23 $_ =~ s/^ {4}//; $_;
  6         17  
76 3         7 } @{$self->{accum}};
  3         7  
77 3         10 $text = join("\n", @a);
78 3         6 $kind = 'pre';
79             } else {
80 120         177 $text = join("\n", @{$self->{accum}});
  120         317  
81 120         207 $kind = $self->{mode};
82             }
83              
84 123 100 100     451 if ($kind eq 'p' && $text =~ m/^<.*>$/s) {
85 11         22 $kind = 'html';
86             }
87              
88             $ast_node = {
89 123         395 text => $text,
90             kind => $kind
91             };
92 123 100       174 if (scalar keys %{$self->{context}} > 0) {
  123         389  
93 6         14 $ast_node->{context} = $self->{context};
94             }
95             }
96              
97 164         242 push @{$self->{ast}}, $ast_node;
  164         325  
98 164         360 $self->{accum} = [];
99 164         267 $self->{mode} = 'p';
100 164         330 $self->{context} = {};
101             }
102              
103             sub parse {
104 76     76 0 249 my ($self, $md) = @_;
105 76         183 $self->init;
106              
107 76         456 my @lines = split(/(\n+)/, $md);
108 76 100       326 if ($lines[$#lines] =~ m/^\n$/) {
109 8         25 push @lines, '';
110             }
111              
112 76         175 foreach my $line (@lines) {
113 424 100 100     1602 if ($self->{mode} ne 'pre' && $line =~ m/^([ \t]+)\S/) {
114 26         60 my $spaces_len = length($1);
115 26         41 my $prefix_len = 0;
116 26 100 66     103 if ($self->{context} && $self->{context}->{prefix} ne '') {
117 16         30 $prefix_len = length($self->{context}->{prefix});
118             }
119 26         35 $spaces_len -= $prefix_len;
120 26 100 100     82 if ($spaces_len > 0 && $spaces_len <= 3) {
121 9         19 my $spaces = ' ' x $spaces_len;
122 9         72 $line =~ s/^$spaces//;
123             }
124             }
125              
126 424         754 my $out_line = $line;
127              
128 424 100       798 if ($line =~ m/^\n{2,}$/) {
129 37         86 process_accumulated($self);
130              
131             # Remove two line breaks
132             # since we will get them back by
133             # inserting a new AST block which will be
134             # surrounded by newlines in the output.
135 37         138 $out_line =~ s/^\n\n//s;
136              
137 37         62 push @{$self->{ast}}, {
  37         121  
138             kind => 'whitespace',
139             text => $out_line
140             };
141             }
142              
143 424 100       953 if ($line =~ m/^\n+$/) {
144 174         300 next;
145             }
146              
147 250 100       342 if (scalar @{$self->{accum}} > 0) {
  250         551  
148 133 100       258 if ($line =~ m/^=+$/) {
149 1         3 $self->{mode} = 'h1';
150 1         4 $self->{context}->{setext} = $line;
151 1         2 next;
152             }
153              
154 132 100       246 if ($line =~ m/^-+$/) {
155 1         3 $self->{mode} = 'h2';
156 1         2 $self->{context}->{setext} = $line;
157 1         2 next;
158             }
159             }
160              
161             # Determine the block mode.
162 248         393 my $mode = 'p';
163 248         300 my $context;
164 248 100       517 if ($line =~ m/^(#+)\s+/) {
165 12         40 my $level = length($1);
166 12 100       30 $level = 6 if $level > 6;
167 12         26 $mode = 'h'.$level;
168 12         19 $out_line = $line;
169 12         65 $out_line =~ s/^#+\s+//;
170             }
171              
172 248 100       476 if ($line eq '') {
173 8         12 $mode = 'whitespace';
174             }
175              
176 248 100       481 if ($line =~ m/^>/s) {
177 17         28 $mode = 'blockquote';
178             }
179              
180 248 100       786 if ($line =~ m/^((\*\s*){3,}|(-\s*){3,}|(=\s*){3,})$/) {
181 7         12 $mode = 'hr';
182             }
183              
184 248 100       613 if ($line =~ m/^(```|~~~)(.*)\s*$/) {
185 8         16 $mode = 'pre';
186 8         34 $context = {
187             text => $1,
188             info => $2
189             };
190 8         28 undef $out_line;
191             }
192              
193 248 100 100     566 if ($self->{mode} eq 'pre' && $line eq $self->{context}->{text}) {
194 4         13 process_accumulated($self);
195 4         13 next;
196             }
197              
198 244 100 100     518 if ($self->{mode} eq 'blockquote' && $mode eq 'p') {
199 7         12 $mode = 'blockquote';
200             }
201              
202 244 100       536 if ($line =~ m/^(\d+\.\s)/) {
203 17         46 process_accumulated($self);
204 17         32 $mode = 'li';
205 17         66 $context = {
206             prefix => $1,
207             type => 'ol',
208             };
209 17         40 $out_line = $line;
210 17         104 $out_line =~ s/\d+\.\s//;
211             }
212              
213 244 100 100     849 if ($mode ne 'hr' && $line =~ m/^([\-\*]\s)/) {
214 15         39 process_accumulated($self);
215 15         27 $mode = 'li';
216 15         55 $context = {
217             prefix => $1,
218             type => 'ul'
219             };
220 15         30 $out_line = $line;
221 15         68 $out_line =~ s/[\-\*]\s//;
222             }
223              
224 244 100 100     779 if ($mode eq 'p' && $self->{mode} =~ m/(pre|li)/) {
225 32         65 $mode = $self->{mode};
226              
227 32         55 my $len = length($self->{context}->{prefix});
228 32         68 my $spaces = ' ' x $len;
229 32         45 $out_line = $line;
230 32         206 $out_line =~ s/^$spaces//;
231             }
232              
233 244 100       534 if ($mode ne $self->{mode}) {
234 78         192 process_accumulated($self);
235 78         144 $self->{mode} = $mode;
236 78         154 $self->{context} = $context;
237             }
238              
239 244 100       457 if (defined $out_line) {
240 240         339 push @{$self->{accum}}, $out_line;
  240         629  
241             }
242             }
243              
244 76         197 process_accumulated($self);
245              
246 76         291 return $self->{ast};
247             }
248              
249             1;