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 |
||||||
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 |
||||||
80 | "right" => ["\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 |
||||||
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|strong|em)>|\])? # 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: |