File Coverage

blib/lib/Text/CaffeinatedMarkup/HTMLFormatter.pm
Criterion Covered Total %
statement 144 145 99.3
branch 62 70 88.5
condition 5 6 83.3
subroutine 14 14 100.0
pod 1 1 100.0
total 226 236 95.7


line stmt bran cond sub pod time code
1             package Text::CaffeinatedMarkup::HTMLFormatter;
2              
3 3     3   58461 use v5.10;
  3         10  
  3         129  
4 3     3   17 use strict;
  3         5  
  3         89  
5 3     3   14 use warnings;
  3         10  
  3         102  
6              
7 3     3   2790 use Log::Log4perl qw(:easy);
  3         148685  
  3         122  
8             Log::Log4perl->easy_init($OFF);
9              
10 3     3   6820 use Moo;
  3         68291  
  3         19  
11 3     3   10170 use Text::CaffeinatedMarkup::PullParser;
  3         11  
  3         150  
12 3     3   3166 use HTML::Escape qw/escape_html/;
  3         7067  
  3         5605  
13              
14              
15             my %tags = (
16             STRONG_OPEN => '',
17             STRONG_CLOSE => '',
18             EMPHASIS_OPEN => '',
19             EMPHASIS_CLOSE => '',
20             UNDERLINE_OPEN => '',
21             UNDERLINE_CLOSE => '',
22             DEL_OPEN => '',
23             DEL_CLOSE => '',
24              
25             PARAGRAPH_OPEN => '

',

26             PARAGRAPH_CLOSE => '

',
27              
28             BLOCKQUOTE_OPEN => '
',
29             BLOCKQUOTE_CLOSE=> '',
30              
31             BLOCKQUOTE_CITE_OPEN => '',
32             BLOCKQUOTE_CITE_CLOSE => '',
33             );
34              
35              
36             has 'tag_stack' => (is=>'rw',default=>sub{[]});
37             has 'is_paragraph_open' => (is=>'rw');
38             has 'num_breaks' => (is=>'rw');
39              
40             has 'is_in_row' => (is=>'rw');
41             has 'is_in_column' => (is=>'rw');
42             has 'row_has_num_columns' => (is=>'rw');
43             has 'row_columns' => (is=>'rw');
44              
45             sub format {
46 40     40 1 11332 my ($self, $pml) = @_;
47              
48 40         1049 my $parser = Text::CaffeinatedMarkup::PullParser->new(pml => $pml);
49              
50 40         253 my @tokens = $parser->get_all_tokens;
51            
52 36         73 my $output_html = '';
53 36         50 my $cur_column_html = '';
54 36         69 my $html = \$output_html;
55              
56 36         117 $self->num_breaks(0);
57              
58 36         88 $self->is_paragraph_open(0);
59 36         118 $self->is_in_row(0);
60 36         83 $self->row_has_num_columns(-1);
61 36         99 $self->row_columns([]);
62            
63 36         78 foreach my $token (@tokens) {
64              
65 171         593 my $type = $token->{type};
66              
67 171 100       305 if ($type eq 'NEWLINE') {
68             # Start storing breaks. We output as soon as we get something different
69             # (see the else). If there's only one then you get a BR, otherwise you
70             # get a paragraph.
71 37         82 $self->num_breaks( $self->num_breaks+1 );
72 37         56 next;
73             }
74             else {
75 134 100       281 unless ($type eq 'HEADER') {
76              
77 127 100       473 if ($self->num_breaks == 1) {
    100          
78 2         5 $$html .= '
';
79             }
80             elsif ($self->num_breaks > 1) {
81 13 50       65 $$html .= $self->_close_paragraph if $self->is_paragraph_open;
82 13         39 $$html .= $self->_open_paragraph;
83             }
84             }
85 134         248 $self->num_breaks(0);
86             }
87              
88 134 100       267 if ($type eq 'QUOTE') {
89              
90 2 50       10 $self->_close_paragraph if $self->is_paragraph_open;
91              
92 2         9 $$html .= $tags{BLOCKQUOTE_OPEN};
93 2         5 $$html .= $token->{body};
94              
95 2 100       8 if ($token->{cite}) {
96 1         8 $$html .= $tags{BLOCKQUOTE_CITE_OPEN}.$token->{cite}.$tags{BLOCKQUOTE_CITE_CLOSE};
97             }
98              
99 2         7 $$html .= $tags{BLOCKQUOTE_CLOSE};
100             }
101              
102 134 100       392 if ($type eq 'ROW') {
103 14 100       38 if ($self->is_in_row) {
104             # Finalise row
105              
106 7 50       23 if ($self->is_in_column) {
107 7 50       25 $cur_column_html .= $self->_close_paragraph if $self->is_paragraph_open;
108             # Already in a column, so output it to the column store
109 7         10 push @{$self->row_columns}, $cur_column_html;
  7         21  
110 7         10 $cur_column_html = '';
111             }
112              
113 7         14 $html = \$output_html;
114              
115 7         21 my $num_columns = $self->_num_columns_in_cur_row;
116              
117              
118              
119 7         28 $$html .= '
'."\n";
120              
121 7         10 foreach my $column (@{$self->row_columns}) {
  7         29  
122 12         47 $$html .= '
' . "\n$column" . "
\n";
123             }
124              
125 7         14 $$html .= "\n"; # End of row
126              
127             # Reset the columns when we close out the row rather than
128             # when starting so that you can always query "num columns"
129             # and it will be right in context for wherever the parsing is.
130 7         23 $self->row_columns([]);
131 7         12 $self->is_in_column(0);
132 7         17 $self->is_in_row(0);
133            
134             }
135             else {
136 7 100       52 $$html .= $self->_close_paragraph if $self->is_paragraph_open;
137 7         18 $self->is_in_row(1);
138             }
139 14         26 next;
140             }
141              
142 120 100       247 if ($type eq 'COLUMN') {
143             # TODO error if not in row!
144 12         21 $html = \$cur_column_html;
145              
146 12 100       39 if ($self->is_in_column) {
147 5 50       23 $cur_column_html .= $self->_close_paragraph if $self->is_paragraph_open;
148             # Already in a column, so output it to the column store
149 5         11 push @{$self->row_columns}, $cur_column_html;
  5         15  
150 5         10 $cur_column_html = '';
151             }
152            
153 12         27 $self->is_in_column(1);
154 12         53 $self->row_has_num_columns( $self->row_has_num_columns+1 );
155             }
156              
157 120 100       477 if ($type =~ /^(STRONG|EMPHASIS|UNDERLINE|DEL)$/o) {
158 12         63 TRACE "Type [$1]";
159 12         109 $$html .= $self->_match_tag($1);
160 12         30 next;
161             }
162              
163 108 100       246 if ($type eq 'LINK') {
164             # TODO - target
165 10         26 my $href = $token->{href};
166 10   66     38 my $text = $token->{text} || $token->{href};
167 10         48 $$html .= qq|$text|;
168 10         23 next;
169             }
170              
171 98 100       189 if ($type eq 'IMAGE') {
172 19         28 my @options;
173 19 100       46 if ($token->{options}) {
174 17         80 @options = split /,/,$token->{options};
175             }
176              
177 19         31 my $align = '';
178 19         25 my $height = '';
179 19         23 my $width = '';
180              
181 19         32 foreach my $option (@options) {
182 35 100       97 $align = ' class="pulled-left"' if $option eq '<<';
183 35 100       59 $align = ' class="pulled-right"' if $option eq '>>';
184 35 100       76 $align = ' class="stretched"' if $option eq '<>';
185 35 100       61 $align = ' class="centered"' if $option eq '><';
186              
187 35 100       91 if ($option =~ /^H(.+)$/) { $height = qq| height="$1px"| }
  11         39  
188 35 100       102 if ($option =~ /^W(.+)$/) { $width = qq| width="$1px"| }
  11         42  
189             }
190            
191 19         80 $$html .= '';
192 19         54 next;
193             }
194              
195 79 100       197 if ($type eq 'HEADER') {
196 7         35 $$html .= "\n{level}.'>'.$token->{text}.'{level}.">\n";
197 7         18 next;
198             }
199              
200 72 100       161 if ($type eq 'STRING') {
201 58 100       225 $$html .= $self->_open_paragraph unless $self->is_paragraph_open;
202 58         408 $$html .= escape_html($token->{content});
203 58         161 next;
204             }
205              
206              
207              
208             # Shouldn't get here!
209             # TODO error
210              
211             }
212              
213             # If there's a paragraph open, close it!
214 36 100       123 $output_html .= $tags{PARAGRAPH_CLOSE} if $self->is_paragraph_open;
215              
216 36         818 return $output_html;
217             }
218              
219             # ------------------------------------------------------------------------------
220              
221             sub _num_columns_in_cur_row {
222 7     7   12 my ($self) = @_;
223 7         9 return scalar @{$self->row_columns};
  7         18  
224             }
225              
226             # ------------------------------------------------------------------------------
227              
228             sub _match_tag {
229 12     12   28 my ($self, $type) = @_;
230              
231 12 100 100     22 if (@{$self->tag_stack} && $self->tag_stack->[0] eq $type) {
  12         96  
232             # Close tag
233 6         17 $self->_pop_stack;
234 6         23 return $tags{$type."_CLOSE"};
235             }
236             else {
237             # Open tag
238 6         10 my $html = '';
239             # If a paragraph isn't open then we need to open one!
240 6 100       24 $html = $self->_open_paragraph unless $self->is_paragraph_open;
241 6         14 $self->_push_stack($type);
242 6         25 return $html . $tags{$type."_OPEN"};
243             }
244 0         0 return;
245             }
246              
247             # ------------------------------------------------------------------------------
248              
249             sub _push_stack {
250 47     47   73 my ($self, $type) = @_;
251 47         58 unshift @{$self->tag_stack}, $type;
  47         165  
252             }
253              
254             # ------------------------------------------------------------------------------
255              
256             sub _pop_stack {
257 33     33   53 my ($self) = @_;
258 33         38 return shift @{$self->tag_stack};
  33         93  
259             }
260              
261             # ------------------------------------------------------------------------------
262              
263             sub _open_paragraph {
264 41     41   63 my ($self) = @_;
265 41 50       111 die "Can't open paragraph - already open!" if $self->is_paragraph_open;
266 41         99 $self->_push_stack('PARAGRAPH');
267 41         86 $self->is_paragraph_open(1);
268 41         113 return $tags{PARAGRAPH_OPEN};
269             }
270              
271             # ------------------------------------------------------------------------------
272              
273             sub _close_paragraph {
274 27     27   57 my ($self) = @_;
275 27 50       68 die "Can't close paragraph - already closed!" unless $self->is_paragraph_open;
276 27 50       88 die "Can't close paragraph - bad stack match" unless $self->tag_stack->[0] eq 'PARAGRAPH';
277 27         779 $self->_pop_stack;
278 27         63 $self->is_paragraph_open(0);
279 27         92 return $tags{PARAGRAPH_CLOSE}."\n";
280             }
281              
282             1;
283              
284             __END__