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