home *** CD-ROM | disk | FTP | other *** search
/ CLIX - Fazer Clix Custa Nix / CLIX-CD.cdr / mac / lib / HTML / Parser.pm < prev    next >
Text File  |  1997-04-07  |  10KB  |  371 lines

  1. package HTML::Parser;
  2.  
  3. # $Id: Parser.pm,v 2.6 1997/02/21 09:32:14 aas Exp $
  4.  
  5. =head1 NAME
  6.  
  7. HTML::Parser - SGML parser class
  8.  
  9. =head1 SYNOPSIS
  10.  
  11.  require HTML::Parser;
  12.  $p = HTML::Parser->new;  # should really a be subclass
  13.  $p->parse($chunk1);
  14.  $p->parse($chunk2);
  15.  #...
  16.  $p->eof;                 # signal end of document
  17.  
  18.  # Parse directly from file
  19.  $p->parse_file("foo.html");
  20.  # or
  21.  open(F, "foo.html") || die;
  22.  $p->parse_file(\*F);
  23.  
  24. =head1 DESCRIPTION
  25.  
  26. The C<HTML::Parser> will tokenize a HTML document when the $p->parse()
  27. method is called.  The document to parse can be supplied in arbitrary
  28. chunks.  Call $p->eof() the end of the document to flush any remaining
  29. text.  The return value from parse() is a reference to the parser
  30. object.
  31.  
  32. The $p->parse_file() method can be called to parse text from a file.
  33. The argument can be a filename or an already opened file handle. The
  34. return value from parse_file() is a reference to the parser object.
  35.  
  36. In order to make the parser do anything interesting, you must make a
  37. subclass where you override one or more of the following methods as
  38. appropriate:
  39.  
  40. =over 4
  41.  
  42. =item $self->declaration($decl)
  43.  
  44. This method is called when a I<markup declaration> has been
  45. recognized.  For typical HTML documents, the only declaration you are
  46. likely to find is <!DOCTYPE ...>.  The initial "<!" and ending ">" is
  47. not part of the string passed as argument.  Comments are removed and
  48. entities have B<not> been expanded yet.
  49.  
  50. =item $self->start($tag, $attr, $attrseq, $origtext)
  51.  
  52. This method is called when a complete start tag has been recognized.
  53. The first argument is the tag name (in lower case) and the second
  54. argument is a reference to a hash that contain all attributes found
  55. within the start tag.  The attribute keys are converted to lower case.
  56. Entities found in the attribute values are already expanded.  The
  57. third argument is a reference to an array with the lower case
  58. attribute keys in the original order.  The fourth argument is the
  59. original HTML text.
  60.  
  61.  
  62. =item $self->end($tag)
  63.  
  64. This method is called when an end tag has been recognized.  The
  65. argument is the lower case tag name.
  66.  
  67. =item $self->text($text)
  68.  
  69. This method is called when plain text in the document is recognized.
  70. The text is passed on unmodified and might contain multiple lines.
  71. Note that for efficiency reasons entities in the text are B<not>
  72. expanded.  You should call HTML::Entities::decode($text) before you
  73. process the text any further.
  74.  
  75. =item $self->comment($comment)
  76.  
  77. This method is called as comments are recognized.  The leading and
  78. trailing "--" sequences have been stripped off the comment text.
  79.  
  80. =back
  81.  
  82. The default implementation of these methods does nothing, I<i.e.,> the
  83. tokens are just ignored.
  84.  
  85. There is really nothing in the basic parser that is HTML specific, so
  86. it is likely that the parser can parse many kinds of SGML documents,
  87. but SGML has many obscure features (not implemented by this module)
  88. that prevent us from renaming this module as C<SGML::Parse>.
  89.  
  90. =head1 BUGS
  91.  
  92. You can instruct the parser to parse comments the way Netscape does it
  93. by calling the netscape_buggy_comment() method with a TRUE argument.
  94. This means that comments will always be terminated by the first
  95. occurence of "-->".
  96.  
  97. =head1 SEE ALSO
  98.  
  99. L<HTML::TreeBuilder>, L<HTML::HeadParser>, L<HTML::Entities>
  100.  
  101. =head1 COPYRIGHT
  102.  
  103. Copyright 1996 Gisle Aas. All rights reserved.
  104.  
  105. This library is free software; you can redistribute it and/or
  106. modify it under the same terms as Perl itself.
  107.  
  108. =head1 AUTHOR
  109.  
  110. Gisle Aas <aas@sn.no>
  111.  
  112. =cut
  113.  
  114.  
  115. use strict;
  116.  
  117. use HTML::Entities ();
  118. use vars qw($VERSION);
  119. $VERSION = sprintf("%d.%02d", q$Revision: 2.6 $ =~ /(\d+)\.(\d+)/);
  120.  
  121.  
  122. sub new
  123. {
  124.     my $class = shift;
  125.     my $self = bless { '_buf'              => '',
  126.                '_netscape_comment' => 0,
  127.              }, $class;
  128.     $self;
  129. }
  130.  
  131. # How does Netscape do it: It parse <xmp> in the depreceated 'literal'
  132. # mode, i.e. no tags are recognized until a </xmp> is found.
  133. # <listing> is parsed like <pre>, i.e. tags are recognized.  <listing>
  134. # are presentend in smaller font than <pre>
  135. #
  136. # Netscape does not parse this comment correctly (it terminates the comment
  137. # too early):
  138. #
  139. #    <! -- comment -- --> more comment -->
  140. #
  141. # Netscape does not allow space after the initial "<" in the start tag.
  142. # Like this "<a href='gisle'>"
  143. #
  144. # Netscape ignore '<!--' and '-->' within the <SCRIPT> tag.  This is used
  145. # as a trick to make non-script-aware browsers ignore the scripts.
  146.  
  147.  
  148. sub eof
  149. {
  150.     shift->parse(undef);
  151. }
  152.  
  153.  
  154. sub parse
  155. {
  156.     my $self = shift;
  157.     my $buf = \ $self->{'_buf'};
  158.     unless (defined $_[0]) {
  159.     # signals EOF (assume rest is plain text)
  160.     $self->text($$buf) if length $$buf;
  161.     $$buf = '';
  162.     return $self;
  163.     }
  164.     $$buf .= $_[0];
  165.  
  166.     # Parse html text in $$buf.  The strategy is to remove complete
  167.     # tokens from the beginning of $$buf until we can't deside whether
  168.     # it is a token or not, or the $$buf is empty.
  169.     while (1) {  # the loop will end by returning when text is parsed
  170.     # First we try to pull off any plain text (anything before a "<" char)
  171.     if ($$buf =~ s|^([^<]+)||) {
  172.         unless (length $$buf) {
  173.         my $text = $1;
  174.         # At the end of the buffer, we should not parse white space
  175.         # but leave it for parsing on the next round.
  176.         if ($text =~ s|(\s+)$||) {
  177.             $$buf = $1;
  178.                 # Same treatment for chopped up entites.
  179.         } elsif ($text =~ s/(&(?:(?:\#\d*)?|\w*))$//) {
  180.             $$buf = $1;
  181.         };
  182.         $self->text($text);
  183.         return $self;
  184.         } else {
  185.         $self->text($1);
  186.         }
  187.     # Netscapes buggy comments are easy to handle
  188.     } elsif ($self->{'_netscape_comment'} && $$buf =~ m|^(<!--)|) {
  189.         if ($$buf =~ s|^<!--(.*?)-->||s) {
  190.         $self->comment($1);
  191.         } else {
  192.         return $self;  # must wait until we see the end of it
  193.         }
  194.     # Then, markup declarations (usually either <!DOCTYPE...> or a comment)
  195.     } elsif ($$buf =~ s|^(<!)||) {
  196.         my $eaten = $1;
  197.         my $text = '';
  198.         my @com = ();  # keeps comments until we have seen the end
  199.         # Eat text and beginning of comment
  200.         while ($$buf =~ s|^(([^>]*?)--)||) {
  201.         $eaten .= $1;
  202.         $text .= $2;
  203.         # Look for end of comment
  204.         if ($$buf =~ s|^((.*?)--)||s) {
  205.             $eaten .= $1;
  206.             push(@com, $2);
  207.         } else {
  208.             # Need more data to get all comment text.
  209.             $$buf = $eaten . $$buf;
  210.             return $self;
  211.         }
  212.         }
  213.         # Can we finish the tag
  214.         if ($$buf =~ s|^([^>]*)>||) {
  215.         $text .= $1;
  216.         $self->declaration($text) if $text =~ /\S/;
  217.         # then tell about all the comments we found
  218.         for (@com) { $self->comment($_); }
  219.         } else {
  220.         $$buf = $eaten . $$buf;  # must start with it all next time
  221.         return $self;
  222.         }
  223.         # Should we look for 'processing instructions' <? ...> ??
  224.     #} elsif ($$buf =~ s|<\?||) {
  225.         # ...
  226.     # Then, look for a end tag
  227.     } elsif ($$buf =~ s|^</||) {
  228.         # end tag
  229.         if ($$buf =~ s|^([a-zA-Z][a-zA-Z0-9\.\-]*)\s*>||) {
  230.         $self->end(lc($1));
  231.         } elsif ($$buf =~ m|^[a-zA-Z]*[a-zA-Z0-9\.\-]*\s*$|) {
  232.         $$buf = "</" . $$buf;  # need more data to be sure
  233.         return $self;
  234.         } else {
  235.         # it is plain text after all
  236.         $self->text("</");
  237.         }
  238.     # Then, finally we look for a start tag
  239.     } elsif ($$buf =~ s|^<||) {
  240.         # start tag
  241.         my $eaten = '<';
  242.  
  243.         # This first thing we must find is a tag name.  RFC1866 says:
  244.         #   A name consists of a letter followed by letters,
  245.         #   digits, periods, or hyphens. The length of a name is
  246.         #   limited to 72 characters by the `NAMELEN' parameter in
  247.         #   the SGML declaration for HTML, 9.5, "SGML Declaration
  248.         #   for HTML".  In a start-tag, the element name must
  249.         #   immediately follow the tag open delimiter `<'.
  250.         if ($$buf =~ s|^(([a-zA-Z][a-zA-Z0-9\.\-]*)\s*)||) {
  251.         $eaten .= $1;
  252.         my $tag = lc $2;
  253.         my %attr;
  254.         my @attrseq;
  255.  
  256.         # Then we would like to find some attributes
  257.                 #
  258.                 # Arrgh!! Since stupid Netscape violates RCF1866 by
  259.                 # using "_" in attribute names (like "ADD_DATE") of
  260.                 # their bookmarks.html, we allow this too.
  261.         while ($$buf =~ s|^(([a-zA-Z][a-zA-Z0-9\.\-_]*)\s*)||) {
  262.             $eaten .= $1;
  263.             my $attr = lc $2;
  264.             my $val;
  265.             # The attribute might take an optional value (first we
  266.             # check for an unquoted value)
  267.             if ($$buf =~ s|(^=\s*([^\"\'>\s][^>\s]*)\s*)||) {
  268.             $eaten .= $1;
  269.             $val = $2;
  270.             HTML::Entities::decode($val);
  271.             # or quoted by " or '
  272.             } elsif ($$buf =~ s|(^=\s*([\"\'])(.*?)\2\s*)||s) {
  273.             $eaten .= $1;
  274.             $val = $3;
  275.             HTML::Entities::decode($val);
  276.                     # truncated just after the '=' or inside the attribute
  277.             } elsif ($$buf =~ m|^(=\s*)$| or
  278.                  $$buf =~ m|^(=\s*[\"\'].*)|s) {
  279.             $$buf = "$eaten$1";
  280.             return $self;
  281.             } else {
  282.             # assume attribute with implicit value
  283.             $val = $attr;
  284.             }
  285.             $attr{$attr} = $val;
  286.             push(@attrseq, $attr);
  287.         }
  288.  
  289.         # At the end there should be a closing ">"
  290.         if ($$buf =~ s|^>||) {
  291.             $self->start($tag, \%attr, \@attrseq, "$eaten>");
  292.         } elsif (length $$buf) {
  293.             # Not a conforming start tag, regard it as normal text
  294.             $self->text($eaten);
  295.         } else {
  296.             $$buf = $eaten;  # need more data to know
  297.             return $self;
  298.         }
  299.  
  300.         } elsif (length $$buf) {
  301.         $self->text($eaten);
  302.         } else {
  303.         $$buf = $eaten . $$buf;  # need more data to parse
  304.         return $self;
  305.         }
  306.  
  307.     } elsif (length $$buf) {
  308.         die; # This should never happen
  309.     } else {
  310.         # The buffer is empty now
  311.         return $self;
  312.     }
  313.     }
  314.     $self;
  315. }
  316.  
  317. sub netscape_buggy_comment
  318. {
  319.     my $self = shift;
  320.     my $old = $self->{'_netscape_comment'};
  321.     $self->{'_netscape_comment'} = shift if @_;
  322.     return $old;
  323. }
  324.  
  325. sub parse_file
  326. {
  327.     my($self, $file) = @_;
  328.     no strict 'refs';  # so that a symbol ref as $file works
  329.     local(*F);
  330.     unless (ref($file) || $file =~ /^\*[\w:]+$/) {
  331.     # Assume $file is a filename
  332.     open(F, $file) || die "Can't open $file: $!";
  333.     $file = \*F;
  334.     }
  335.     my $chunk = '';
  336.     while(read($file, $chunk, 2048)) {
  337.     $self->parse($chunk);
  338.     }
  339.     close($file);
  340.     $self->eof;
  341. }
  342.  
  343. sub text
  344. {
  345.     # my($self, $text) = @_;
  346. }
  347.  
  348. sub declaration
  349. {
  350.     # my($self, $decl) = @_;
  351. }
  352.  
  353. sub comment
  354. {
  355.     # my($self, $comment) = @_;
  356. }
  357.  
  358. sub start
  359. {
  360.     my($self, $tag, $attr, $attrseq, $origtext) = @_;
  361.     # $attr is reference to a HASH, $attrseq is reference to an ARRAY
  362. }
  363.  
  364. sub end
  365. {
  366.     my($self, $tag) = @_;
  367. }
  368.  
  369. 1;
  370.