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   162646 use strict;
  11         33  
  11         276  
4 11     11   44 use warnings;
  11         19  
  11         214  
5 11     11   1384 use utf8;
  11         34  
  11         50  
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   5610 use IO::HTML qw/html_file/;
  11         121234  
  11         614  
41 11     11   5873 use HTML::PullParser;
  11         64148  
  11         19151  
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 15842 my ($rawtext) = @_;
97 39 50       93 return unless defined $rawtext;
98             # pack the things like hello there with space. Be careful
99             # with recursions.
100 39         82 return _html_to_muse(\$rawtext);
101             }
102              
103             sub html_file_to_muse {
104 17     17 1 17564 my ($text) = @_;
105 17 50       205 die "$text is not a file" unless (-f $text);
106 17         57 return _html_to_muse(html_file($text));
107             }
108              
109             sub _html_to_muse {
110 56     56   3842 my $text = shift;
111 56         225 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       176 if (ref($text) eq 'SCALAR') {
    50          
121 39         72 $opts{doc} = $text;
122             }
123             elsif (ref($text) eq 'GLOB') {
124 17         30 $opts{file} = $text;
125             }
126             else {
127 0         0 die "Nor a ref, nor a file!";
128             }
129              
130 56 50       254 my $p = HTML::PullParser->new(%opts) or die $!;
131 56         5420 my @textstack;
132             my @spanpile;
133 56         0 my @lists;
134 56         0 my @parspile;
135 56         98 my @tagpile = ('root');
136 56         79 my $current = '';
137 56         129 while (my $token = $p->get_token) {
138 1341         13416 my $type = shift @$token;
139             # starttag?
140 1341 100       2372 if ($type eq 'S') {
    100          
    50          
141 421         539 my $tag = shift @$token;
142 421         609 push @tagpile, $tag;
143 421         557 $current = $tag;
144 421         526 my $attr = shift @$token;
145             # see if processing of span or font are needed
146 421 100 66     1795 if (($tag eq 'span') or ($tag eq 'font')) {
    100 100        
    100 100        
147 47         73 $tag = _span_process_attr($attr);
148 47         62 push @spanpile, $tag;
149             }
150             elsif (($tag eq "ol") or ($tag eq "ul")) {
151 6         8 push @lists, $tag;
152             }
153             elsif (($tag eq 'p') or ($tag eq 'div')) {
154 103         188 $tag = _pars_process_attr($tag, $attr);
155 103         159 push @parspile, $tag;
156             }
157             # see if we want to skip it.
158 421 100 100     1153 if ((defined $tag) && (exists $preserved{$tag})) {
159              
160             # is it a list?
161 310 100       542 if (ref($preserved{$tag}) eq "HASH") {
162             # does it have a parent?
163 18 50       30 if (my $parent = $lists[$#lists]) {
164             push @textstack, "\n",
165             " " x $#lists,
166 18         53 $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         466 push @textstack, $preserved{$tag}[0];
175             }
176             }
177 421 100 100     1598 if ((defined $tag) &&
      100        
178             ($tag eq 'a') &&
179             (my $href = $attr->{href})) {
180 15         50 push @textstack, [ $href, "][" ];
181             }
182             }
183              
184             # stoptag?
185             elsif ($type eq 'E') {
186 422         516 $current = '';
187 422         529 my $tag = shift @$token;
188 422         559 my $expected = pop @tagpile;
189 422 100       699 if ($expected ne $tag) {
190 1         53 warn "tagpile mismatch: $expected, $tag\n";
191             }
192              
193 422 100 66     1708 if (($tag eq 'span') or ($tag eq 'font')) {
    100 100        
    100 100        
194 47         59 $tag = pop @spanpile;
195             }
196             elsif (($tag eq "ol") or ($tag eq "ul")) {
197 6         7 $tag = pop @lists;
198             }
199             elsif (($tag eq 'p') or ($tag eq 'div')) {
200 104 100       182 if (@parspile) {
201 103         136 $tag = pop @parspile
202             }
203             }
204              
205 422 100 100     1116 if ($tag && (exists $preserved{$tag})) {
206 311 100       518 if (ref($preserved{$tag}) eq "HASH") {
207 18 50       31 if (my $parent = $lists[$#lists]) {
208 18         48 push @textstack, $preserved{$tag}{$parent}[1];
209             } else {
210 0         0 push @textstack, $preserved{$tag}{ul}[1];
211             }
212             } else {
213 293         720 push @textstack, $preserved{$tag}[1];
214             }
215             }
216             }
217             # regular text
218             elsif ($type eq 'T') {
219 498         612 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         860 $line =~ s/\r//gs;
223 498         740 $line =~ s/\t/ /gs;
224             # at the beginning of the tag
225 498 100       1038 if ($current =~ m/^(p|div)$/) {
226 71 100       232 if ($line =~ m/\A\s*([\x{a0} ]+)\s*\z/) {
227 22         29 $line = "\n
\n";
228             }
229             }
230 498         697 $line =~ s/\x{a0}/ /gs;
231             # remove leading spaces from these tags
232 498 100       908 if ($current =~ m/^(h[1-6]|li|ul|ol|p|div)$/) {
233 108         280 $line =~ s/^\s+//gms;
234             }
235 498 100       759 if ($current ne 'pre') {
236 491         1352 push @textstack, [ $line ];
237             }
238             else {
239 7         20 push @textstack, $line;
240             }
241             } else {
242 0         0 warn "which type? $type??\n"
243             }
244             }
245 56         662 my @current_text;
246             my @processed;
247 56         91 while (@textstack) {
248 1170         1383 my $text = shift(@textstack);
249 1170 100       1614 if (ref($text)) {
250 766         1360 push @current_text, @$text;
251             }
252             else {
253 404         560 push @processed, _merge_text_lines(\@current_text);
254 404         801 push @processed, $text;
255             }
256             }
257 56         91 push @processed, _merge_text_lines(\@current_text);
258 56         159 my $full = join("", @processed);
259 56         344 $full =~ s/\n\n\n+/\n\n/gs;
260 56         517 return $full;
261             }
262              
263             sub _cleanup_text_block {
264 294     294   340 my $parsed = shift;
265 294 50       500 return '' unless defined $parsed;
266             # here we are inside a single text block.
267 294         1684 $parsed =~ s/\s+/ /gs;
268             # print "<<<$parsed>>>\n";
269             # clean the footnotes.
270 294         555 $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         353 my $recursion = 0;
289 294   66     1203 while (($parsed =~ m!( )!) && ($recursion < 20)) {
290 41         211 $parsed =~ s!( +)()!$2$1!g;
291 41         232 $parsed =~ s!(<[^/]*?>)( +)!$2$1!g;
292 41         197 $recursion++;
293             }
294             # empty links artifacts.
295 294         473 $parsed =~ s/\[\[\]\]//g;
296 294         1504 $parsed =~ s/\s+/ /gs;
297 294         667 $parsed =~ s/\A\s+//;
298 294         875 $parsed =~ s/\s+\z//;
299 294         434 $parsed =~ s/^\*/ */gm;
300             # print ">>>$parsed<<<\n";
301 294         575 return $parsed;
302             }
303              
304             sub _span_process_attr {
305 47     47   57 my $attr = shift;
306 47         53 my $tag;
307 47         103 my @attrsvalues = values %$attr;
308 47 100       213 if (grep(/italic/i, @attrsvalues)) {
    100          
309 8         14 $tag = "em";
310             }
311             elsif (grep(/bold/i, @attrsvalues)) {
312 8         9 $tag = "strong";
313             }
314             else {
315 31         41 $tag = undef;
316             }
317 47         78 return $tag;
318             }
319              
320             sub _pars_process_attr {
321 103     103   175 my ($tag, $attr) = @_;
322             # warn Dumper($attr);
323 103 100       203 if (my $style = $attr->{style}) {
324 19 100       84 if ($style =~ m/text-align:\s*center/i) {
325 5         8 $tag = 'center';
326             }
327 19 100       58 if ($style =~ m/text-align:\s*right/i) {
328 6         19 $tag = 'right';
329             }
330 19 100       46 if ($style =~ m/padding-left:\s*\d/si) {
331 2         4 $tag = 'blockquote'
332             }
333             }
334 103 100       160 if (my $align = $attr->{align}) {
335 2 50       5 if ($align =~ m/center/i) {
336 0         0 $tag = 'center';
337             }
338 2 50       10 if ($align =~ m/right/i) {
339 2         3 $tag = 'right';
340             }
341             }
342 103         193 return $tag;
343             }
344              
345             sub _merge_text_lines {
346 460     460   533 my $lines = shift;
347 460 100       746 return '' unless @$lines;
348 294         548 my $text = join ('', @$lines);
349 294         441 @$lines = ();
350 294         381 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: