File Coverage

blib/lib/Text/Amuse/Preprocessor/HTML.pm
Criterion Covered Total %
statement 131 136 96.3
branch 68 78 87.1
condition 30 33 90.9
subroutine 12 12 100.0
pod 2 2 100.0
total 243 261 93.1


line stmt bran cond sub pod time code
1             package Text::Amuse::Preprocessor::HTML;
2              
3 11     11   217572 use strict;
  11         48  
  11         376  
4 11     11   59 use warnings;
  11         21  
  11         265  
5 11     11   688 use utf8;
  11         44  
  11         85  
6             # use Data::Dumper;
7             require Exporter;
8              
9             our @ISA = qw(Exporter);
10              
11             # Items to export into callers namespace by default. Note: do not export
12             # names by default without a very good reason. Use EXPORT_OK instead.
13             # Do not simply export all your public functions/methods/constants.
14              
15             our @EXPORT_OK = qw( html_to_muse html_file_to_muse );
16              
17             our $VERSION = '0.59';
18              
19             =encoding utf8
20              
21             =head1 NAME
22              
23             Text::Amuse::Preprocessor::HTML - HTML importer
24              
25             =head1 DESCRIPTION
26              
27             This module tries its best to convert the HTML into an acceptable
28             Muse string. It's not perfect, though, and some manual adjustment is
29             needed if there are tables or complicated structures.
30              
31             =head1 SYNOPSIS
32              
33             use utf8;
34             use Text::Amuse::Preprocessor::HTML qw/html_to_muse/;
35             my $html = '

Your text here... & " ò àùć

'
36             my $muse = html_to_muse($html);
37              
38             =cut
39              
40 11     11   7151 use IO::HTML qw/html_file/;
  11         146396  
  11         2599  
41 11     11   7549 use HTML::PullParser;
  11         79673  
  11         23962  
42              
43             my %preserved = (
44             "em" => [[""], [""]],
45             "i" => [[""], [""]],
46             "u" => [[""], [""]],
47             "strong" => [[""], [""]],
48             "b" => [[""], [""]],
49             "blockquote" => ["\n\n", "\n"],
50             "ol" => ["\n\n", "\n\n"],
51             "ul" => ["\n\n", "\n\n"],
52             "li" => { ol => [ " 1. ", "\n\n"],
53             ul => [ " - ", "\n\n"],
54             },
55             "code" => [[""], [""]],
56             "a" => [[ "[[" ] , [ "]]" ]],
57             "pre" => [ "\n\n", "\n\n" ],
58             table => ["\n\n", "\n\n"],
59             "tr" => ["\n ", "" ],
60             "td" => [[" "], [" | "] ],
61             "th" => [[ " "], [" || "] ],
62             "dd" => ["\n\n", "\n\n"],
63             "dt" => ["\n***** ", "\n\n" ],
64             "h1" => ["\n* ", "\n\n"],
65             "h2" => ["\n* ", "\n\n"],
66             "h3" => ["\n** ", "\n\n"],
67             "h4" => ["\n*** ", "\n\n"],
68             "h5" => ["\n**** ", "\n\n"],
69             "h6" => ["\n***** ", "\n\n"],
70             "sup" => [[""], [""]],
71             "sub" => [[""], [""]],
72             "strike" => [[""], [""]],
73             "del" => [[""], [""]],
74             "p" => ["\n\n", "\n\n"],
75             "br" => ["\n
", "\n"],
76             "div" => ["\n\n", "\n\n"],
77             "center" => ["\n\n
\n", "\n
\n\n"],
78             "right" => ["\n\n\n", "\n\n\n"],
79            
80             );
81              
82             =head1 FUNCTIONS
83              
84             =head2 html_to_muse($html_decoded_text)
85              
86             The first argument must be a decoded string with the HTML text.
87             Returns the L formatted body.
88              
89             =head2 html_file_to_muse($html_file)
90              
91             The first argument must be a filename.
92              
93             =cut
94              
95             sub html_to_muse {
96 39     39 1 22137 my ($rawtext) = @_;
97 39 50       121 return unless defined $rawtext;
98             # pack the things like hello there with space. Be careful
99             # with recursions.
100 39         101 return _html_to_muse(\$rawtext);
101             }
102              
103             sub html_file_to_muse {
104 17     17 1 26133 my ($text) = @_;
105 17 50       268 die "$text is not a file" unless (-f $text);
106 17         80 return _html_to_muse(html_file($text));
107             }
108              
109             sub _html_to_muse {
110 56     56   5030 my $text = shift;
111 56         305 my %opts = (
112             start => '"S", tagname, attr',
113             end => '"E", tagname',
114             text => '"T", dtext',
115             empty_element_tags => 1,
116             marked_sections => 1,
117             unbroken_text => 1,
118             ignore_elements => [qw(script style)],
119             );
120 56 100       208 if (ref($text) eq 'SCALAR') {
    50          
121 39         84 $opts{doc} = $text;
122             }
123             elsif (ref($text) eq 'GLOB') {
124 17         41 $opts{file} = $text;
125             }
126             else {
127 0         0 die "Nor a ref, nor a file!";
128             }
129              
130 56 50       341 my $p = HTML::PullParser->new(%opts) or die $!;
131 56         6791 my @textstack;
132             my @spanpile;
133 56         0 my @lists;
134 56         0 my @parspile;
135 56         123 my @tagpile = ('root');
136 56         87 my $current = '';
137 56         157 while (my $token = $p->get_token) {
138 1341         16568 my $type = shift @$token;
139             # starttag?
140 1341 100       4747 if ($type eq 'S') {
    100          
    50          
141 421         658 my $tag = shift @$token;
142 421         745 push @tagpile, $tag;
143 421         665 $current = $tag;
144 421         616 my $attr = shift @$token;
145             # see if processing of span or font are needed
146 421 100 66     2260 if (($tag eq 'span') or ($tag eq 'font')) {
    100 100        
    100 100        
147 47         85 $tag = _span_process_attr($attr);
148 47         76 push @spanpile, $tag;
149             }
150             elsif (($tag eq "ol") or ($tag eq "ul")) {
151 6         14 push @lists, $tag;
152             }
153             elsif (($tag eq 'p') or ($tag eq 'div')) {
154 103         236 $tag = _pars_process_attr($tag, $attr);
155 103         218 push @parspile, $tag;
156             }
157             # see if we want to skip it.
158 421 100 100     1371 if ((defined $tag) && (exists $preserved{$tag})) {
159              
160             # is it a list?
161 310 100       699 if (ref($preserved{$tag}) eq "HASH") {
162             # does it have a parent?
163 18 50       46 if (my $parent = $lists[$#lists]) {
164             push @textstack, "\n",
165             " " x $#lists,
166 18         65 $preserved{$tag}{$parent}[0];
167             } else {
168             push @textstack, "\n",
169 0         0 $preserved{$tag}{ul}[0];
170             }
171             }
172             # no? ok
173             else {
174 292         638 push @textstack, $preserved{$tag}[0];
175             }
176             }
177 421 100 100     1931 if ((defined $tag) &&
      100        
178             ($tag eq 'a') &&
179             (my $href = $attr->{href})) {
180 15         62 push @textstack, [ $href, "][" ];
181             }
182             }
183              
184             # stoptag?
185             elsif ($type eq 'E') {
186 422         596 $current = '';
187 422         651 my $tag = shift @$token;
188 422         636 my $expected = pop @tagpile;
189 422 100       841 if ($expected ne $tag) {
190 1         126 warn "tagpile mismatch: $expected, $tag\n";
191             }
192              
193 422 100 66     2125 if (($tag eq 'span') or ($tag eq 'font')) {
    100 100        
    100 100        
194 47         72 $tag = pop @spanpile;
195             }
196             elsif (($tag eq "ol") or ($tag eq "ul")) {
197 6         11 $tag = pop @lists;
198             }
199             elsif (($tag eq 'p') or ($tag eq 'div')) {
200 104 100       223 if (@parspile) {
201 103         167 $tag = pop @parspile
202             }
203             }
204              
205 422 100 100     1383 if ($tag && (exists $preserved{$tag})) {
206 311 100       672 if (ref($preserved{$tag}) eq "HASH") {
207 18 50       39 if (my $parent = $lists[$#lists]) {
208 18         61 push @textstack, $preserved{$tag}{$parent}[1];
209             } else {
210 0         0 push @textstack, $preserved{$tag}{ul}[1];
211             }
212             } else {
213 293         892 push @textstack, $preserved{$tag}[1];
214             }
215             }
216             }
217             # regular text
218             elsif ($type eq 'T') {
219 498         736 my $line = shift @$token;
220             # Word &C. (and CKeditor), love the no-break space.
221             # but preserve it it's only whitespace in the line.
222 498         1103 $line =~ s/\r//gs;
223 498         948 $line =~ s/\t/ /gs;
224             # at the beginning of the tag
225 498 100       1303 if ($current =~ m/^(p|div)$/) {
226 71 100       287 if ($line =~ m/\A\s*([\x{a0} ]+)\s*\z/) {
227 22         39 $line = "\n
\n";
228             }
229             }
230 498         957 $line =~ s/\x{a0}/ /gs;
231             # remove leading spaces from these tags
232 498 100       1059 if ($current =~ m/^(h[1-6]|li|ul|ol|p|div)$/) {
233 108         355 $line =~ s/^\s+//gms;
234             }
235 498 100       846 if ($current ne 'pre') {
236 491         1728 push @textstack, [ $line ];
237             }
238             else {
239 7         28 push @textstack, $line;
240             }
241             } else {
242 0         0 warn "which type? $type??\n"
243             }
244             }
245 56         812 my @current_text;
246             my @processed;
247 56         120 while (@textstack) {
248 1170         1763 my $text = shift(@textstack);
249 1170 100       1938 if (ref($text)) {
250 766         1709 push @current_text, @$text;
251             }
252             else {
253 404         706 push @processed, _merge_text_lines(\@current_text);
254 404         979 push @processed, $text;
255             }
256             }
257 56         136 push @processed, _merge_text_lines(\@current_text);
258 56         233 my $full = join("", @processed);
259 56         410 $full =~ s/\n\n\n+/\n\n/gs;
260 56         670 return $full;
261             }
262              
263             sub _cleanup_text_block {
264 294     294   477 my $parsed = shift;
265 294 50       560 return '' unless defined $parsed;
266             # here we are inside a single text block.
267 294         2106 $parsed =~ s/\s+/ /gs;
268             # print "<<<$parsed>>>\n";
269             # clean the footnotes.
270 294         710 $parsed =~ s!\[
271             \[
272             \#\w+ # the anchor
273             \]
274             \[
275             (<(sup|strong|em)>|\[)? # sup or [
276             \[*
277             (\d+) # the number
278             \]*
279             (|\])? # sup or ]
280             \] # close
281             \] # close
282             ![$3]!gx;
283              
284             # add a newline if missing
285             # unless ($parsed =~ m/\n\z/) {
286             # $parsed .= "\n";
287             # }
288 294         412 my $recursion = 0;
289 294   66     1469 while (($parsed =~ m!( )!) && ($recursion < 20)) {
290 41         262 $parsed =~ s!( +)()!$2$1!g;
291 41         298 $parsed =~ s!(<[^/]*?>)( +)!$2$1!g;
292 41         235 $recursion++;
293             }
294             # empty links artifacts.
295 294         596 $parsed =~ s/\[\[\]\]//g;
296 294         1859 $parsed =~ s/\s+/ /gs;
297 294         784 $parsed =~ s/\A\s+//;
298 294         1100 $parsed =~ s/\s+\z//;
299 294         518 $parsed =~ s/^\*/ */gm;
300             # print ">>>$parsed<<<\n";
301 294         677 return $parsed;
302             }
303              
304             sub _span_process_attr {
305 47     47   73 my $attr = shift;
306 47         72 my $tag;
307 47         129 my @attrsvalues = values %$attr;
308 47 100       278 if (grep(/italic/i, @attrsvalues)) {
    100          
309 8         17 $tag = "em";
310             }
311             elsif (grep(/bold/i, @attrsvalues)) {
312 8         16 $tag = "strong";
313             }
314             else {
315 31         55 $tag = undef;
316             }
317 47         98 return $tag;
318             }
319              
320             sub _pars_process_attr {
321 103     103   258 my ($tag, $attr) = @_;
322             # warn Dumper($attr);
323 103 100       243 if (my $style = $attr->{style}) {
324 19 100       105 if ($style =~ m/text-align:\s*center/i) {
325 5         11 $tag = 'center';
326             }
327 19 100       79 if ($style =~ m/text-align:\s*right/i) {
328 6         31 $tag = 'right';
329             }
330 19 100       57 if ($style =~ m/padding-left:\s*\d/si) {
331 2         5 $tag = 'blockquote'
332             }
333             }
334 103 100       201 if (my $align = $attr->{align}) {
335 2 50       8 if ($align =~ m/center/i) {
336 0         0 $tag = 'center';
337             }
338 2 50       11 if ($align =~ m/right/i) {
339 2         5 $tag = 'right';
340             }
341             }
342 103         218 return $tag;
343             }
344              
345             sub _merge_text_lines {
346 460     460   639 my $lines = shift;
347 460 100       934 return '' unless @$lines;
348 294         714 my $text = join ('', @$lines);
349 294         533 @$lines = ();
350 294         521 return _cleanup_text_block($text);
351             }
352              
353             1;
354              
355              
356             =head1 AUTHOR, LICENSE, ETC.,
357              
358             See L
359              
360             =cut
361              
362             # Local Variables:
363             # tab-width: 8
364             # cperl-indent-level: 2
365             # End: