package RDA::Object::Html; # $Id: Html.pm,v 1.13 2009/01/13 16:51:53 mschenke Exp $ # ARCS: $Header: /u02/webapps/workbench/cvs/RDA_4/src/scripting/lib/RDA/Object/Html.pm,v 1.13 2009/01/13 16:51:53 mschenke Exp $ # # Change History # 20090113 MSC Change the copyright notice. =head1 NAME RDA::Object::Html - Class Used for Objects to Manage HTML Data. =head1 SYNOPSIS require RDA::Object::Html; =head1 DESCRIPTION The objects of the C class are used to manage HTML data. It is a sub class of C. The following methods are available: =cut BEGIN { use Exporter; use RDA::Object::Sgml; } use strict; # Define the global public variables use vars qw($VERSION @ISA); $VERSION = sprintf("%d.%02d", q$Revision: 1.13 $ =~ /(\d+)\.(\d+)/); @ISA = qw(RDA::Object::Sgml); # Define the global private variables my %tb_emp = map {$_ => 1} qw(area base basefont bgsound br col embed frame hr img input isindex link meta param spacer wbr); my %tb_par =( 'dd' => [qw(dl)], 'dt' => [qw(dl)], 'li' => [qw(dir menu ol ul)], 'option' => [qw(form select)], 'select' => [qw(form)], 'td' => [qw(table tr)], 'textarea' => [qw(form)], 'th' => [qw(table tr)], 'tr' => [qw(table)], ); my %tb_phr = map {$_ => 1} qw(a abbr acronym b br basefont bdo big blink cite code dfn em embed font i img kbd nobr noembed q s samp small spacer span strike strong sub sup tt u var wbr); # Report the module version number sub Version { $VERSION; } =head2 S<$h = RDA::Object::Html-Enew([$debug])> The object constructor. C is represented by a blessed hash reference. Along with the keys inherited from C, the following special key is also used: =over 12 =item S > Auto fix HTML code indicator (false by default) =item S > Tag filter hash =back =cut sub new { my $cls = shift; my $dbg = shift; # Return the object reference bless RDA::Object::Sgml->new('H', $dbg ? 'HTML> ' : ''), $cls; } =head2 S<$h-Eeof> This method signals the end of the document, flushing any remaining buffered text. It returns a reference to the parser object. =cut sub eof { my $slf = shift; my $buf = \$slf->{'-buf'}; # Assume rest is text if (length($$buf)) { $slf->add_text($$buf); $$buf = ''; } # Insert pending text $slf->add_text; # Return the object reference return $slf; } =head2 S<$h-Efilter([$tag,...])> This method specifies the list of the tags to consider when parsing the document. It can automatically add additional tags to the list for resolving optional end tags. When the list is empty, it disables any tag filtering. It returns the parser object reference. =cut sub filter { my $slf = shift; # Update the tag filter list delete($slf->{'-tag'}); foreach my $tag (@_) { $tag = lc($tag); $slf->{'-tag'}->{$tag} = 1; if (exists($tb_par{$tag})) { map {$slf->{'-tag'}->{$_} = 1} @{$tb_par{$tag}}; } } # Return the object reference $slf; } =head2 S<$h-Efix([$flag])> This method indicates that the parser can fix misformed HTML code. It returns the parser object reference. =cut sub fix { my ($slf, $flg) = @_; # Update the indicator $slf->{'-fix'} = $flg; # Return the object reference $slf; } =head2 S<$h-Eparse($string)>; This method parses the specified string as the next HTML chunk. It returns a reference to the HTML object. =cut sub parse { my $slf = shift; my $buf = \$slf->{'-buf'}; my $dbg = $slf->{'-dbg'}; # When EOF, assume rest is text return $slf->eof unless defined($_[0]); # Filter out some characters $_[0] =~ s#\r\n#\n#g; $_[0] =~ s#\r#\n#g; # Parse HTML in the buffer $$buf .= $_[0]; TOKEN: while ($$buf !~ m#^(<\/|<\?|debug('HTML> Buffer') if $dbg; # Parse the next token if ($$buf =~ s#^([^<]+)##) # Plain text { # Extract any text before '<' characters $slf->add_text($1); last TOKEN unless length($$buf); } elsif ($$buf =~ s#^()##s) { # Need more data to get all data $$buf = $cur.$$buf; last TOKEN; } $slf->add_item('R', -dat => $2); } elsif ($$buf =~ s#^(]*?)--)##) { $cur .= $1; $txt .= $2; # Look for end of comment if ($$buf =~ s#^((.*?)--)##s) { $cur .= $1; push(@com, $2) if $2; } else { # Need more data to extract the comment $$buf = $cur.$$buf; last TOKEN; } } # Try to finish the declaration extraction if ($$buf =~ s#^([^>]*)>##) { $txt .= $1; $slf->add_item('D', -dat => $txt) if $txt; foreach my $com (@com) { $slf->add_item('R', -dat => $com); } } else { # Need more data to extract the declaration $$buf = $cur.$$buf; last TOKEN; } } elsif ($$buf =~ s#^)##) { # Close the tag $slf->_end_tag(lc($2)); } elsif ($$buf =~ m#^[a-zA-Z][a-zA-Z0-9\.\-]*\s*$#) { # Need more data for the end tag $$buf = 'add_text(")##) # Empty start tag { my $tag = lc($2); $slf->_add_tag($tag, {}, $3 || exists($tb_emp{$tag})); } elsif ($$buf =~ s#^<##) # Start Tag { my ($cur, $tag, %tbl); $cur = '<'; $slf->debug('HTML> ++ Start tag found') if $dbg; if ($$buf =~ s#^(([a-zA-Z][a-zA-Z0-9\.\-]*)\s*)##) { # Extract the tag name $cur .= $1; $tag = lc $2; # Extract attributes while ($$buf =~ s#^(([a-zA-Z][a-zA-Z0-9:_\.\-]*)\s*)##) { my ($nam); $cur .= $1; $nam = lc $2; if ($$buf =~ s#(^=\s*([^\042\047>\s][^>\s]*)\s*)##) { # Extract attribute value (unquoted) $cur .= $1; $tbl{$nam} = $slf->decode($2); } elsif ($$buf =~ s#(^=\s*([\042\047])(.*?)\2\s*)##s) { # Extract attribute value (quoted) $cur .= $1; $tbl{$nam} = $slf->decode($3); # truncated just after the '=' or inside the attribute } elsif ($$buf =~ m#^(=\s*)$# || $$buf =~ m#^(=\s*[\042\047].*)#s) { # Need more data to extract attribute $$buf = "$cur$1"; last TOKEN; } else { # Extract attribute value (implicit value) $tbl{$nam} = $nam; } } # Check start tag end if ($$buf =~ s#^(\/)?>##) { # Insert the tag in the object tree $slf->_add_tag($tag, \%tbl, $1 || exists($tb_emp{$tag})); } elsif ($$buf =~ m#^<# && $slf->{'-fix'}) { # Insert the tag in the object tree $slf->_add_tag($tag, \%tbl, $1 || exists($tb_emp{$tag})); } elsif (length($$buf)) { # Not a conforming HTML declaration, consider it as text $slf->add_text($cur); } else { # Need more data to parse the start tag $$buf = $cur; last TOKEN; } } elsif (length $$buf) { # Not a valid start tag, consider it as text $slf->add_text($cur); } else { # Need more data to parse the start tag $$buf = $cur.$$buf; last TOKEN; } } } # Return the object reference $slf; } sub _add_tag { my ($slf, $tag, $tbl, $flg) = @_; # When requested, filter the tag if (exists($slf->{'-flt'}->{'T'}) || (exists($slf->{'-tag'}) && !exists($slf->{'-tag'}->{$tag}))) { $slf->add_text(' ') unless exists($tb_phr{$tag}) && $tag ne 'br'; return; } # Terminate the current paragraph $slf->_end_p unless exists($tb_phr{$tag}); # Treat tags with optional end tag if (exists($tb_par{$tag})) { # Insert pending text $slf->add_text; # Retrieve its parent tag $slf->_find_parent($tag, $tb_par{$tag}); } # Disable text normalisation in PRE blocks $tbl->{'-txt'} = 0 if $tag eq 'pre'; # Create the tag element and insert in the list $slf->add_item('T', -nam => $tag, %$tbl); # Go to the next level when an end tag is expected $slf->push_item unless $flg; } sub _end_p { my ($slf) = @_; my ($cur, $nam); # Insert pending text $slf->add_text; # Close all phrase tags print $slf->{'-dbg'}."** Close tag 'p'\n" if $slf->{'-dbg'}; $cur = $slf->{'-cur'}; for (; index('HX', $cur->{'-typ'}) < 0 ; $cur = $slf->pop_item) { next unless $cur->{'-typ'} eq 'T'; $nam = $cur->{'-nam'}; if ($nam eq 'p') { $slf->pop_item; return; } return unless exists($tb_phr{$cur->{'-nam'}}); } } sub _end_tag { my ($slf, $tag) = @_; my ($cur, $nam, $tbl); # When requested, filter the tag if (exists($slf->{'-flt'}->{'T'}) || (exists($slf->{'-tag'}) && !exists($slf->{'-tag'}->{$tag}))) { $slf->add_text(' ') unless exists($tb_phr{$tag}) && $tag ne 'br'; return; } # Insert pending text $slf->add_text; # Close the tag $slf->save_stack; print $slf->{'-dbg'}."** Close tag $tag\n" if $slf->{'-dbg'}; $cur = $slf->{'-cur'}; $tbl = exists($tb_par{$tag}) ? $tb_par{$tag} : []; LEVEL: for (; index('HX', $cur->{'-typ'}) < 0 ; $cur = $slf->pop_item) { next unless $cur->{'-typ'} eq 'T'; $nam = $cur->{'-nam'}; if ($nam eq $tag) { $slf->pop_item; return; } for (@$tbl) { last LEVEL if $nam eq $_; } } # Ignore it when no corresponding tag has been found $slf->restore_stack; ++$slf->{'-err'}; print "ERR> Missing tag '$tag' !\n" if $slf->{'-dbg'}; } sub _find_parent { my ($slf, $tag, $tbl) = @_; my ($cur, $nam); # Close the tag $slf->save_stack; $cur = $slf->{'-cur'}; for (; index('HX', $cur->{'-typ'}) < 0 ; $cur = $slf->pop_item) { next unless $cur->{'-typ'} eq 'T'; $nam = $cur->{'-nam'}; for (@$tbl) { return if $_ eq $nam; } } # Ignore it when parent tag has not been found $slf->restore_stack; ++$slf->{'-err'}; print "ERR> Missing parent tag for '$tag' !\n" if $slf->{'-dbg'}; } =head2 S<$h-Etext> This method returns the text contained in the object. =cut sub text { my ($slf) = @_; my $buf = ''; $slf->traverse(\&_text, \$buf); $buf; } sub _text { my ($slf, $lvl, $flg, $buf) = @_; my $typ = $slf->{'-typ'}; if ($typ eq 'S') { $$buf .= $slf->{'-dat'} if $flg; } elsif ($typ eq 'T') { $$buf .= ' ' unless exists($tb_phr{$slf->{'-nam'}}) && $slf->{'-nam'} ne 'br'; } 1; } 1; __END__ =head1 SEE ALSO L, L, L, L, L, L, L, L, L, L, L, L =head1 COPYRIGHT NOTICE Copyright (c) 2002-2009, Oracle. All rights reserved. =head1 TRADEMARK NOTICE Oracle, JD Edwards, PeopleSoft, and Siebel are registered trademarks of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. =cut