| 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 | 
||||||
| 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 | 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|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 | 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: |