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 |
||||
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__ |