File Coverage

blib/lib/Text/Amuse/Preprocessor/HTML.pm
Criterion Covered Total %
statement 141 146 96.5
branch 70 80 87.5
condition 36 41 87.8
subroutine 14 14 100.0
pod 2 2 100.0
total 263 283 92.9


line stmt bran cond sub pod time code
1             package Text::Amuse::Preprocessor::HTML;
2              
3 12     12   208185 use strict;
  12         53  
  12         351  
4 12     12   66 use warnings;
  12         26  
  12         266  
5 12     12   709 use utf8;
  12         47  
  12         86  
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.67';
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 12     12   7543 use IO::HTML qw/html_file/;
  12         158339  
  12         816  
41 12     12   7325 use HTML::PullParser;
  12         85570  
  12         484  
42 12     12   6239 use Text::Amuse::Utils;
  12         11225  
  12         26635  
43              
44             sub _preserve {
45 59     59   1859 my %keeptag = (
46             "em" => [[""], [""]],
47             "i" => [[""], [""]],
48             "u" => [[""], [""]],
49             "strong" => [[""], [""]],
50             "b" => [[""], [""]],
51             "blockquote" => ["\n\n", "\n"],
52             "ol" => ["\n\n", "\n\n"],
53             "ul" => ["\n\n", "\n\n"],
54             "li" => { ol => [ " 1. ", "\n\n"],
55             ul => [ " - ", "\n\n"],
56             },
57             "code" => [[""], [""]],
58             "a" => [[ "[[" ] , [ "]]" ]],
59             "pre" => [ "\n\n", "\n\n" ],
60             table => ["\n\n", "\n\n"],
61             "tr" => ["\n ", "" ],
62             "td" => [[" "], [" | "] ],
63             "th" => [[ " "], [" || "] ],
64             "dd" => ["\n\n", "\n\n"],
65             "dt" => ["\n***** ", "\n\n" ],
66             "h1" => ["\n* ", "\n\n"],
67             "h2" => ["\n* ", "\n\n"],
68             "h3" => ["\n** ", "\n\n"],
69             "h4" => ["\n*** ", "\n\n"],
70             "h5" => ["\n**** ", "\n\n"],
71             "h6" => ["\n***** ", "\n\n"],
72             "sup" => [[""], [""]],
73             "sub" => [[""], [""]],
74             "strike" => [[""], [""]],
75             "del" => [[""], [""]],
76             "p" => ["\n\n", "\n\n"],
77             "br" => ["\n
", "\n"],
78             "div" => ["\n\n", "\n\n"],
79             "center" => ["\n\n
\n", "\n
\n\n"],
80             "right" => ["\n\n\n", "\n\n\n"],
81             );
82 59         774 return %keeptag;
83             }
84              
85             =head1 FUNCTIONS
86              
87             =head2 html_to_muse($html_decoded_text)
88              
89             The first argument must be a decoded string with the HTML text.
90             Returns the L formatted body.
91              
92             =head2 html_file_to_muse($html_file)
93              
94             The first argument must be a filename.
95              
96             =cut
97              
98             sub html_to_muse {
99 39     39 1 20083 my ($rawtext, $opts) = @_;
100 39 50       116 return unless defined $rawtext;
101             # pack the things like hello there with space. Be careful
102             # with recursions.
103 39         99 return _html_to_muse(\$rawtext, $opts);
104             }
105              
106             sub html_file_to_muse {
107 20     20 1 24619 my ($text, $opts) = @_;
108 20 50       303 die "$text is not a file" unless (-f $text);
109 20         93 return _html_to_muse(html_file($text), $opts);
110             }
111              
112             sub _html_to_muse {
113 59     59   6077 my ($text, $options) = @_;
114 59   100     305 $options ||= {};
115 59         142 my %preserved = _preserve();
116 59         356 my $is_rtl = Text::Amuse::Utils::lang_code_is_rtl($options->{lang});
117 59 100       684 if ($is_rtl) {
118 2         5 delete $preserved{right};
119             }
120 59         292 my %opts = (
121             start => '"S", tagname, attr',
122             end => '"E", tagname',
123             text => '"T", dtext',
124             empty_element_tags => 1,
125             marked_sections => 1,
126             unbroken_text => 1,
127             ignore_elements => [qw(script style)],
128             );
129 59 100       207 if (ref($text) eq 'SCALAR') {
    50          
130 39         80 $opts{doc} = $text;
131             }
132             elsif (ref($text) eq 'GLOB') {
133 20         46 $opts{file} = $text;
134             }
135             else {
136 0         0 die "Nor a ref, nor a file!";
137             }
138              
139 59 50       307 my $p = HTML::PullParser->new(%opts) or die $!;
140 59         7356 my @textstack;
141             my @spanpile;
142 59         0 my @lists;
143 59         0 my @parspile;
144 59         139 my @tagpile = ('root');
145 59         93 my $current = '';
146 59         158 while (my $token = $p->get_token) {
147 1548         25701 my $type = shift @$token;
148             # starttag?
149 1548 100       3371 if ($type eq 'S') {
    100          
    50          
150 498         812 my $tag = shift @$token;
151 498         955 push @tagpile, $tag;
152 498         846 $current = $tag;
153 498         759 my $attr = shift @$token;
154             # see if processing of span or font are needed
155 498 100 66     2698 if (($tag eq 'span') or ($tag eq 'font')) {
    100 100        
    100 100        
156 71         149 $tag = _span_process_attr($attr);
157 71         134 push @spanpile, $tag;
158             }
159             elsif (($tag eq "ol") or ($tag eq "ul")) {
160 6         43 push @lists, $tag;
161             }
162             elsif (($tag eq 'p') or ($tag eq 'div')) {
163 117         341 $tag = _pars_process_attr($tag, $attr, { rtl => $is_rtl });
164 117         284 push @parspile, $tag;
165             }
166             # see if we want to skip it.
167 498 100 100     1545 if ((defined $tag) && (exists $preserved{$tag})) {
168              
169             # is it a list?
170 330 100       757 if (ref($preserved{$tag}) eq "HASH") {
171             # does it have a parent?
172 18 50       57 if (my $parent = $lists[$#lists]) {
173             push @textstack, "\n",
174             " " x $#lists,
175 18         58 $preserved{$tag}{$parent}[0];
176             } else {
177             push @textstack, "\n",
178 0         0 $preserved{$tag}{ul}[0];
179             }
180             }
181             # no? ok
182             else {
183 312         691 push @textstack, $preserved{$tag}[0];
184             }
185             }
186 498 100 100     2296 if ((defined $tag) &&
      100        
187             ($tag eq 'a') &&
188             (my $href = $attr->{href})) {
189 19         80 push @textstack, [ $href, "][" ];
190             }
191             }
192              
193             # stoptag?
194             elsif ($type eq 'E') {
195 478         747 $current = '';
196 478         748 my $tag = shift @$token;
197 478         752 my $expected = pop @tagpile;
198 478 100       983 if ($expected ne $tag) {
199 11         1005 warn "tagpile mismatch: $expected, $tag\n";
200             }
201              
202 478 100 66     2399 if (($tag eq 'span') or ($tag eq 'font')) {
    100 100        
    100 100        
203 71         110 $tag = pop @spanpile;
204             }
205             elsif (($tag eq "ol") or ($tag eq "ul")) {
206 6         9 $tag = pop @lists;
207             }
208             elsif (($tag eq 'p') or ($tag eq 'div')) {
209 118 100       238 if (@parspile) {
210 117         189 $tag = pop @parspile
211             }
212             }
213              
214 478 100 100     1708 if ($tag && (exists $preserved{$tag})) {
215 329 100       719 if (ref($preserved{$tag}) eq "HASH") {
216 18 50       57 if (my $parent = $lists[$#lists]) {
217 18         92 push @textstack, $preserved{$tag}{$parent}[1];
218             } else {
219 0         0 push @textstack, $preserved{$tag}{ul}[1];
220             }
221             } else {
222 311         932 push @textstack, $preserved{$tag}[1];
223             }
224             }
225             }
226             # regular text
227             elsif ($type eq 'T') {
228 572         849 my $line = shift @$token;
229             # Word &C. (and CKeditor), love the no-break space.
230             # but preserve it it's only whitespace in the line.
231 572         1245 $line =~ s/\r//gs;
232 572         1078 $line =~ s/\t/ /gs;
233             # at the beginning of the tag
234 572 100       1514 if ($current =~ m/^(p|div)$/) {
235 79 100       356 if ($line =~ m/\A\s*([\x{a0} ]+)\s*\z/) {
236 22         36 $line = "\n
\n";
237             }
238             }
239 572         1004 $line =~ s/\x{a0}/ /gs;
240             # remove leading spaces from these tags
241 572 100       1232 if ($current =~ m/^(h[1-6]|li|ul|ol|p|div)$/) {
242 116         403 $line =~ s/^\s+//gms;
243             }
244 572 100       1116 if ($current ne 'pre') {
245 565         1907 push @textstack, [ $line ];
246             }
247             else {
248 7         55 push @textstack, $line;
249             }
250             } else {
251 0         0 warn "which type? $type??\n"
252             }
253             }
254 59         831 my @current_text;
255             my @processed;
256 59         135 while (@textstack) {
257 1286         2083 my $text = shift(@textstack);
258 1286 100       2214 if (ref($text)) {
259 852         1941 push @current_text, @$text;
260             }
261             else {
262 434         816 push @processed, _merge_text_lines(\@current_text);
263 434         1118 push @processed, $text;
264             }
265             }
266 59         136 push @processed, _merge_text_lines(\@current_text);
267 59         260 my $full = join("", @processed);
268 59         444 $full =~ s/\n\n\n+/\n\n/gs;
269 59         1363 return $full;
270             }
271              
272             sub _cleanup_text_block {
273 325     325   499 my $parsed = shift;
274 325 50       637 return '' unless defined $parsed;
275             # here we are inside a single text block.
276 325         2292 $parsed =~ s/\s+/ /gs;
277             # print "<<<$parsed>>>\n";
278             # clean the footnotes.
279 325         886 $parsed =~ s!\[
280             \[
281             \#\w+ # the anchor
282             \]
283             \[
284             (<(sup|strong|em)>|\[)? # sup or [
285             \[*
286             (\d+) # the number
287             \]*
288             (|\])? # sup or ]
289             \] # close
290             \] # close
291             ![$3]!gx;
292              
293             # add a newline if missing
294             # unless ($parsed =~ m/\n\z/) {
295             # $parsed .= "\n";
296             # }
297 325         477 my $recursion = 0;
298 325   66     1539 while (($parsed =~ m!( )!) && ($recursion < 20)) {
299 41         260 $parsed =~ s!( +)()!$2$1!g;
300 41         292 $parsed =~ s!(<[^/]*?>)( +)!$2$1!g;
301 41         250 $recursion++;
302             }
303             # empty links artifacts.
304 325         680 $parsed =~ s/\[\[\]\]//g;
305 325         1984 $parsed =~ s/\s+/ /gs;
306 325         917 $parsed =~ s/\A\s+//;
307 325         1198 $parsed =~ s/\s+\z//;
308 325         623 $parsed =~ s/^\*/ */gm;
309             # print ">>>$parsed<<<\n";
310 325         746 return $parsed;
311             }
312              
313             sub _span_process_attr {
314 71     71   107 my $attr = shift;
315 71         97 my $tag;
316 71         202 my @attrsvalues = values %$attr;
317 71 100       588 if (grep(/italic/i, @attrsvalues)) {
    100          
318 8         16 $tag = "em";
319             }
320             elsif (grep(/bold/i, @attrsvalues)) {
321 8         36 $tag = "strong";
322             }
323             else {
324 55         109 $tag = undef;
325             }
326 71         144 return $tag;
327             }
328              
329             sub _pars_process_attr {
330 117     117   262 my ($tag, $attr, $opts) = @_;
331             # warn Dumper($attr);
332 117 100       267 if (my $style = $attr->{style}) {
333 25 100       137 if ($style =~ m/text-align:\s*center/i) {
334 5         11 $tag = 'center';
335             }
336 25 100 100     142 if (!$opts->{rtl} and $style =~ m/text-align:\s*right/i) {
337 7         15 $tag = 'right';
338             }
339 25 100       97 if ($style =~ m/padding-left:\s*\d/si) {
340 2         5 $tag = 'blockquote'
341             }
342             }
343 117 100       253 if (my $align = $attr->{align}) {
344 2 50       7 if ($align =~ m/center/i) {
345 0         0 $tag = 'center';
346             }
347 2 50 33     18 if (!$opts->{rtl} and $align =~ m/right/i) {
348 2         7 $tag = 'right';
349             }
350             }
351 117         271 return $tag;
352             }
353              
354             sub _merge_text_lines {
355 493     493   676 my $lines = shift;
356 493 100       1038 return '' unless @$lines;
357 325         741 my $text = join ('', @$lines);
358 325         624 @$lines = ();
359 325         570 return _cleanup_text_block($text);
360             }
361              
362             1;
363              
364              
365             =head1 AUTHOR, LICENSE, ETC.,
366              
367             See L
368              
369             =cut
370              
371             # Local Variables:
372             # tab-width: 8
373             # cperl-indent-level: 2
374             # End: