File Coverage

blib/lib/HTML/FromText.pm
Criterion Covered Total %
statement 205 212 96.7
branch 80 90 88.8
condition 19 26 73.0
subroutine 39 41 95.1
pod 18 18 100.0
total 361 387 93.2


line stmt bran cond sub pod time code
1 6     6   67563 use strict;
  6         16  
  6         955  
2 6     6   37 use warnings;
  6         12  
  6         479  
3             package HTML::FromText;
4             {
5             $HTML::FromText::VERSION = '2.07';
6             }
7             # ABSTRACT: converts plain text to HTML
8              
9              
10 6     6   6679 use Email::Find::addrspec 0.09 qw[$Addr_spec_re];
  6         5815  
  6         1389  
11 6     6   38 use Exporter 5.58 qw[import];
  6         113  
  6         221  
12 6     6   12812 use HTML::Entities 1.26 qw[encode_entities];
  6         60320  
  6         875  
13 6     6   56 use Scalar::Util 1.12 qw[blessed];
  6         137  
  6         540  
14 6     6   8216 use Text::Tabs 98.1128 qw[expand];
  6         6407  
  6         28083  
15              
16             our @EXPORT = qw[text2html];
17             our @DECORATORS = qw[urls email bold underline];
18             our $PROTOCOLS = qr/
19             afs | cid | ftp | gopher |
20             http | https | mid | news |
21             nntp | prospero | telnet | wais
22             /x;
23              
24              
25             sub new {
26 39     39 1 13004 my ($class, $options) = @_;
27 39   100     120 $options ||= {};
28 39 50       119 $class->_croak("Options must be a hash reference")
29             if ref($options) ne 'HASH';
30              
31 39         484 my %options = (
32             metachars => 1,
33             urls => 0,
34             email => 0,
35             bold => 0,
36             underline => 0,
37              
38             pre => 0,
39              
40             lines => 0,
41             spaces => 0,
42              
43             paras => 0,
44             bullets => 0,
45             numbers => 0,
46             headings => 0,
47             title => 0,
48             blockparas => 0,
49             blockquotes => 0,
50             blockcode => 0,
51             tables => 0,
52              
53 39         72 %{ $options },
54             );
55              
56 39         148 my %self = (
57             options => \%options,
58             text => '',
59             html => '',
60             );
61              
62 39   33     446 return bless \%self, blessed($class) || $class;
63             }
64              
65              
66             sub parse {
67 39     39 1 939 my ($self, $text) = @_;
68              
69 39         232 $text = join "\n", expand( split /\n/, $text );
70              
71 39         1307 $self->{text} = $text;
72 39         101 $self->{html} = $text;
73 39         64 $self->{paras} = undef;
74              
75 39         53 my $options = $self->{options};
76              
77 39 50       145 $self->metachars if $options->{metachars};
78              
79 39 100       885 if ( $options->{pre} ) { $self->pre }
  1 100       4  
    100          
80 2         9 elsif ( $options->{lines} ) { $self->lines }
81 29         78 elsif ( $options->{paras} ) { $self->paras }
82              
83 39   66     332 $options->{$_} and $self->$_ foreach @DECORATORS;
84              
85 39         150 return $self->{html};
86             }
87              
88              
89             sub text2html {
90 13     13 1 10625 my ($text, %options) = @_;
91 13         50 HTML::FromText->new(\%options)->parse($text);
92             }
93              
94              
95             sub pre {
96 1     1 1 2 my ($self) = @_;
97 1         5 $self->{html} = join $self->{html}, '
', '
';
98             }
99              
100              
101             sub lines {
102 2     2 1 26 my ($self) = @_;
103 2 100       12 $self->{html} =~ s[ ][ ]g if $self->{options}->{spaces};
104 2         13 $self->{html} =~ s[$][
]gm;
105 2         9 $self->{html} =~ s[^][
];
106 2         9 $self->{html} =~ s[$][];
107             }
108              
109              
110             sub paras {
111 29     29 1 42 my ($self) = @_;
112              
113 29         46 my $options = $self->{options};
114 29         120 my @paras = split /\n{2,}/, $self->{html};
115 29         78 my %paras = map { $_, { text => $paras[$_], html => undef } } 0 .. $#paras;
  44         258  
116 29         69 $self->{paras} = \%paras;
117              
118 29 100       78 $self->{paras}->{0}->{html} = join(
119             $self->{paras}->{0}->{text},
120             q[

], "

\n"
121             ) if $options->{title};
122              
123 29 100       68 $self->headings if $options->{headings};
124 29 100       71 $self->bullets if $options->{bullets};
125 29 100       97 $self->numbers if $options->{numbers};
126              
127 29 100       80 $self->tables if $options->{tables};
128              
129 29 100       409 if ( $options->{blockparas} ) { $self->blockparas }
  3 100       11  
    100          
130 2         7 elsif ( $options->{blockquotes} ) { $self->blockquotes }
131 4         16 elsif ( $options->{blockcode} ) { $self->blockcode }
132              
133 29     17   158 $self->_manipulate_paras(sub { qq[

$_[0]

\n] });
  17         73  
134              
135 19         89 $self->{html} = join "\n", map $paras{$_}->{html},
136 29         194 sort { $a <=> $b } keys %paras;
137             }
138              
139              
140             sub headings {
141 1     1 1 3 my ($self) = @_;
142 1         6 my $heading = qr/\d+\./;
143              
144             $self->_manipulate_paras(sub{
145 4     4   6 my ($text) = @_;
146 4 100       63 return unless $text =~ m[^((?:$heading)+)\s+];
147              
148 3         5 my $depth; $depth++ for split /\./, $1;
  3         15  
149              
150 3         16 qq[$text\n];
151 1         11 });
152             }
153              
154              
155             sub bullets {
156 5     5 1 9 my ($self) = @_;
157 5         95 $self->_format_list( qr/[*]/, 'ul', 'hft-bullets' );
158 5         64 $self->_format_list( qr/[-]/, 'ul', 'hft-bullets' );
159             }
160              
161              
162             sub numbers {
163 1     1 1 2 my ($self) = @_;
164 1         6 $self->_format_list( qr/[0-9]/, 'ol', 'hft-numbers');
165             }
166              
167              
168             sub tables {
169 7     7 1 11 my ($self) = @_;
170              
171             $self->_manipulate_paras(sub{
172 8     8   24 my ($text) = $self->_remove_indent( $_[0] );
173              
174 8         28 my @lines = split /\n/, $text;
175 8         69 my $columns = $self->_table_find_columns(
176             $self->_table_initial_spaces( split //, $lines[0] ),
177             [ @lines[1 .. $#lines] ],
178             );
179              
180 8 100       61 return unless $columns;
181 6         18 $self->_table_create( $columns, \@lines );
182 7         39 });
183             }
184              
185              
186             sub blockparas {
187 3     3 1 7 my ($self) = @_;
188 3         6 my $paras = $self->{paras};
189              
190             $self->_manipulate_paras(sub{
191 4     4   14 my ($text) = $self->_remove_indent( $_[0], 1 );
192 4         12 my ($pnum, $paras) = @_[1,2];
193 4 50       10 return unless $text;
194              
195 4 100       30 $self->_consolidate_blocks(
196             ( exists $paras->{$pnum - 1} ? $paras->{$pnum -1} : undef ),
197             'blockparas', 1,
198             qq[

$text

\n],
199             );
200 3         19 });
201             }
202              
203              
204             sub blockquotes {
205 2     2 1 4 my ($self) = @_;
206 2         6 my $paras = $self->{paras};
207              
208             $self->_manipulate_paras(sub {
209 2     2   8 my ($text) = $self->_remove_indent( $_[0], 1 );
210 2 50       8 return unless $text;
211              
212 2         15 $text =~ s[\n|$][
\n]g;
213              
214 2         13 qq[
$text
\n];
215 2         13 });
216             }
217              
218              
219             sub blockcode {
220 4     4 1 10 my ($self) = @_;
221 4         8 my $paras = $self->{paras};
222              
223             $self->_manipulate_paras(sub {
224 5     5   16 my ($text) = $self->_remove_indent( $_[0], 1 );
225 5         12 my ($pnum, $paras) = @_[1,2];
226 5 50       16 return unless $text;
227              
228 5         14 $text =~ s[^][
]; 
229 5         17 $text =~ s[$][];
230 5 100       37 $self->_consolidate_blocks(
231             ( exists $paras->{$pnum - 1} ? $paras->{$pnum -1} : undef ),
232             'blockcode', 0,
233             qq[
$text
\n],
234             );
235 4         25 });
236             }
237              
238              
239             sub urls {
240 3     3 1 6 my ($self) = @_;
241 3         265 $self->{html} =~ s[\b((?:$PROTOCOLS):[^\s<]+[\w/])]
242             [$1]og;
243             }
244              
245              
246             sub email {
247 1     1 1 2 my ($self) = @_;
248 1         185 $self->{html} =~ s[($Addr_spec_re)]
249             [$1]og;
250             }
251              
252              
253             sub underline {
254 3     3 1 6 my ($self) = @_;
255 3         44 $self->{html} =~ s[(?:^|(?<=\W))((_)([^\\_\n]*(?:\\.[^\\_\n]*)*)(_))(?:(?=\W)|$)]
256             [$3]g;
257             }
258              
259              
260             sub bold {
261 4     4 1 8 my ($self) = @_;
262 4         69 $self->{html} =~ s[(?:^|(?<=\W))((\*)([^\\\*\n]*(?:\\.[^\\\*\n]*)*)(\*))(?:(?=\W)|$)]
263             [$3]g;
264             }
265              
266              
267             sub metachars {
268 39     39 1 56 my ($self) = @_;
269 39         122 $self->{html} = encode_entities( $self->{html} );
270             }
271              
272             # private
273              
274             sub _croak {
275 0     0   0 my ($class, @error) = @_;
276 0         0 require Carp;
277 0         0 Carp::croak(@error);
278             }
279              
280             sub _carp {
281 0     0   0 my ($class, @error) = @_;
282 0         0 require Carp;
283 0         0 Carp::carp(@error);
284             }
285              
286             sub _format_list {
287 11     11   40 my ($self, $identifier, $parent, $class) = @_;
288              
289             $self->_manipulate_paras(sub {
290 12     12   18 my ($text) = @_;
291 12 100       274 return unless $text =~ m[^\s*($identifier)\s+];
292              
293 6         18 my ($pos, $html, @open) = (-1, '');
294 6         171 foreach my $line ( split /\n(?=\s*$identifier)/, $text ) {
295 28         288 $line =~ s[(\s*)$identifier][];
296 28         57 my $line_pos = length $1;
297 28 100       62 if ($line_pos > $pos) {
    100          
298 16         123 $html .= (' ' x $line_pos) . qq[<$parent class="$class">\n];
299 16         26 push @open, $line_pos;
300             } elsif ($line_pos < $pos) {
301 4         44 until ( $open[-1] <= $line_pos ) {
302 6         24 $html .= (' ' x pop @open) . "\n";
303             }
304             }
305 28         98 $html .= (' ' x ($pos = $line_pos)) . "
  • $line
  • \n";
    306             }
    307 6         39 $html .= "\n"x@open;
    308 11         91 });
    309             }
    310              
    311             sub _manipulate_paras {
    312 57     57   89 my ($self, $action) = @_;
    313              
    314 57         78 my $paras = $self->{paras};
    315              
    316 57         66 foreach my $pnum ( sort { $a <=> $b } keys %{$paras}) {
      32         85  
      57         184  
    317 83         109 my $para = $paras->{$pnum};
    318 83 100       274 $para->{html} = $action->($para->{text}, $pnum, $paras)
    319             unless $para->{html};
    320             }
    321             }
    322              
    323             sub _table_initial_spaces {
    324 8     8   82 my ($self, @chars) = @_;
    325              
    326 8         12 my %spaces;
    327 8         20 foreach ( 0 .. $#chars ) {
    328 271         352 my ($open_space) = grep { !defined( $_->{end} ) } values %spaces;
      265         505  
    329 271 100       444 if ( $chars[$_] eq ' ' ) {
    330 85 100       258 $spaces{$_} = {start => $_, end => undef} unless $open_space;
    331             } else {
    332 186 100 100     448 if ( $open_space && $_ - $open_space->{start} > 1 ) {
    333 9         19 $open_space->{end} = $_ - 1;
    334             } else {
    335 177 100       375 delete $spaces{$open_space->{start}} if $open_space;
    336             }
    337             }
    338             }
    339 8         65 return \%spaces;
    340             }
    341              
    342             sub _table_find_columns {
    343 8     8   14 my ($self, $spaces, $lines) = @_;
    344 8 100       9 return unless keys %{$spaces};
      8         27  
    345 6         10 my %spots;
    346 6         8 foreach my $line ( @{$lines} ) {
      6         14  
    347 21         23 foreach my $pos ( sort { $a <=> $b } keys %{$spaces} ) {
      22         33  
      21         44  
    348 36         37 my $key;
    349 36 100       89 $key = $spaces->{$pos}->{start}
    350             if substr( $line, $spaces->{$pos}->{start}, 1 ) eq ' ';
    351 36 100 100     154 $key = $spaces->{$pos}->{end}
    352             if substr( $line, $spaces->{$pos}->{end}, 1 ) eq ' ' && ! $key;
    353 36 50       55 if ( $key ) {
    354 36         47 $spots{$key}++;
    355 36 50 66     128 $spots{$spaces->{$pos}->{start}}++
    356             if $spots{$spaces->{$pos}->{start}} && $key ne $spaces->{$pos}->{start};
    357 36 100       111 $spots{$spaces->{$pos}->{end}}++
    358             if $key ne $spaces->{$pos}->{end};
    359             } else {
    360 0         0 delete $spaces->{$pos};
    361             }
    362             }
    363 21         58 foreach my $spot (sort {$b <=> $a} keys %spots) {
      61         74  
    364 54 100       105 if ( substr( $line, $spot, 1 ) ne ' ' ) {
    365 1         2 delete $spots{$spot};
    366             }
    367 54 100       110 if ( exists $spaces->{$spot}) {
    368 18         23 my $space = $spaces->{$spot};
    369 18 100 66     86 if ( exists $spots{$space->{start}} && $spots{$space->{end}}) {
    370 17         43 delete $spots{$spot};
    371             }
    372             }
    373             }
    374             }
    375              
    376              
    377 6         17 my @spots = grep { $spots{$_} == @{$lines} } sort { $a <=> $b } keys %spots;
      9         13  
      9         24  
      3         6  
    378 9 100       66 return @spots ? join( '', (
    379             map {
    380 6 50       19 my $ret = 'A' . ( $spots[$_] - ( $_ == 0 ? 0 : $spots[$_ - 1] ) );
    381 9 50       45 $ret eq 'A0' ? () : $ret;
    382             } 0 .. $#spots
    383             ), 'A*' ) : undef;
    384             }
    385              
    386             sub _table_create {
    387 6     6   10 my ($self, $columns, $lines) = @_;
    388              
    389 6         10 my $table = qq[\n]; \n",
    390 6         15 foreach my $line ( @{$lines} ) {
      6         15  
    391 72         149 $table .= join( '',
    392             '
    ',
    393             join(
    394             '',
    395 27         84 map { s/^\s+//; s/\s$//; $_ } unpack $columns, $line
      72         117  
      72         208  
    396             ),
    397             "
    398             );
    399             }
    400 6         52 $table .= "
    \n";
    401             }
    402              
    403             sub _remove_indent {
    404 19     19   29 my ($self, $text, $strict) = @_;
    405 19 50 66     124 return if $text !~ m[^(\s+).+(?:\n\1.+)*$] && $strict;
    406 19 100       188 $text =~ s[^$1][]mg if $1;
    407 19         46 return $text;
    408             }
    409              
    410             sub _consolidate_blocks {
    411 9     9   17 my ($self, $prev_para, $class, $keep_inner, $html) = @_;
    412 9 100 66     65 if ( $prev_para && $prev_para->{html} =~ m[
    <(\w+)>] ) {
    413 2 100       25 my $inner_tag = $keep_inner ? '' : qr[];
    414 2         23 $prev_para->{html} =~ s[$inner_tag][];
    415 2         30 $html =~ s[
    $inner_tag][];
    416             }
    417 9         42 return $html;
    418             }
    419              
    420             1;
    421              
    422             __END__