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