-#############################################################################\r
-# Pod/Checker.pm -- check pod documents for syntax errors\r
-#\r
-# Copyright (C) 1994-2000 by Bradford Appleton. All rights reserved.\r
-# This file is part of "PodParser". PodParser is free software;\r
-# you can redistribute it and/or modify it under the same terms\r
-# as Perl itself.\r
-#############################################################################\r
-\r
-package Pod::Checker;\r
-use strict;\r
-\r
-use vars qw($VERSION @ISA @EXPORT %VALID_COMMANDS %VALID_SEQUENCES);\r
-$VERSION = '1.60'; ## Current version of this package\r
-require 5.005; ## requires this Perl version or later\r
-\r
-use Pod::ParseUtils; ## for hyperlinks and lists\r
-\r
-=head1 NAME\r
-\r
-Pod::Checker, podchecker() - check pod documents for syntax errors\r
-\r
-=head1 SYNOPSIS\r
-\r
- use Pod::Checker;\r
-\r
- $num_errors = podchecker($filepath, $outputpath, %options);\r
-\r
- my $checker = new Pod::Checker %options;\r
- $checker->parse_from_file($filepath, \*STDERR);\r
-\r
-=head1 OPTIONS/ARGUMENTS\r
-\r
-C<$filepath> is the input POD to read and C<$outputpath> is\r
-where to write POD syntax error messages. Either argument may be a scalar\r
-indicating a file-path, or else a reference to an open filehandle.\r
-If unspecified, the input-file it defaults to C<\*STDIN>, and\r
-the output-file defaults to C<\*STDERR>.\r
-\r
-=head2 podchecker()\r
-\r
-This function can take a hash of options:\r
-\r
-=over 4\r
-\r
-=item B<-warnings> =E<gt> I<val>\r
-\r
-Turn warnings on/off. I<val> is usually 1 for on, but higher values\r
-trigger additional warnings. See L<"Warnings">.\r
-\r
-=back\r
-\r
-=head1 DESCRIPTION\r
-\r
-B<podchecker> will perform syntax checking of Perl5 POD format documentation.\r
-\r
-Curious/ambitious users are welcome to propose additional features they wish\r
-to see in B<Pod::Checker> and B<podchecker> and verify that the checks are\r
-consistent with L<perlpod>.\r
-\r
-The following checks are currently performed:\r
-\r
-=over 4\r
-\r
-=item *\r
-\r
-Unknown '=xxxx' commands, unknown 'XE<lt>...E<gt>' interior-sequences,\r
-and unterminated interior sequences.\r
-\r
-=item *\r
-\r
-Check for proper balancing of C<=begin> and C<=end>. The contents of such\r
-a block are generally ignored, i.e. no syntax checks are performed.\r
-\r
-=item *\r
-\r
-Check for proper nesting and balancing of C<=over>, C<=item> and C<=back>.\r
-\r
-=item *\r
-\r
-Check for same nested interior-sequences (e.g.\r
-C<LE<lt>...LE<lt>...E<gt>...E<gt>>).\r
-\r
-=item *\r
-\r
-Check for malformed or non-existing entities C<EE<lt>...E<gt>>.\r
-\r
-=item *\r
-\r
-Check for correct syntax of hyperlinks C<LE<lt>...E<gt>>. See L<perlpod>\r
-for details.\r
-\r
-=item *\r
-\r
-Check for unresolved document-internal links. This check may also reveal\r
-misspelled links that seem to be internal links but should be links\r
-to something else.\r
-\r
-=back\r
-\r
-=head1 DIAGNOSTICS\r
-\r
-=head2 Errors\r
-\r
-=over 4\r
-\r
-=item * empty =headn\r
-\r
-A heading (C<=head1> or C<=head2>) without any text? That ain't no\r
-heading!\r
-\r
-=item * =over on line I<N> without closing =back\r
-\r
-The C<=over> command does not have a corresponding C<=back> before the\r
-next heading (C<=head1> or C<=head2>) or the end of the file.\r
-\r
-=item * =item without previous =over\r
-\r
-=item * =back without previous =over\r
-\r
-An C<=item> or C<=back> command has been found outside a\r
-C<=over>/C<=back> block.\r
-\r
-=item * No argument for =begin\r
-\r
-A C<=begin> command was found that is not followed by the formatter\r
-specification.\r
-\r
-=item * =end without =begin\r
-\r
-A standalone C<=end> command was found.\r
-\r
-=item * Nested =begin's\r
-\r
-There were at least two consecutive C<=begin> commands without\r
-the corresponding C<=end>. Only one C<=begin> may be active at\r
-a time.\r
-\r
-=item * =for without formatter specification\r
-\r
-There is no specification of the formatter after the C<=for> command.\r
-\r
-=item * Apparent command =foo not preceded by blank line\r
-\r
-A command which has ended up in the middle of a paragraph or other command,\r
-such as\r
-\r
- =item one\r
- =item two <-- bad\r
-\r
-=item * unresolved internal link I<NAME>\r
-\r
-The given link to I<NAME> does not have a matching node in the current\r
-POD. This also happened when a single word node name is not enclosed in\r
-C<"">.\r
-\r
-=item * Unknown command "I<CMD>"\r
-\r
-An invalid POD command has been found. Valid are C<=head1>, C<=head2>,\r
-C<=head3>, C<=head4>, C<=over>, C<=item>, C<=back>, C<=begin>, C<=end>,\r
-C<=for>, C<=pod>, C<=cut>\r
-\r
-=item * Unknown interior-sequence "I<SEQ>"\r
-\r
-An invalid markup command has been encountered. Valid are:\r
-C<BE<lt>E<gt>>, C<CE<lt>E<gt>>, C<EE<lt>E<gt>>, C<FE<lt>E<gt>>,\r
-C<IE<lt>E<gt>>, C<LE<lt>E<gt>>, C<SE<lt>E<gt>>, C<XE<lt>E<gt>>,\r
-C<ZE<lt>E<gt>>\r
-\r
-=item * nested commands I<CMD>E<lt>...I<CMD>E<lt>...E<gt>...E<gt>\r
-\r
-Two nested identical markup commands have been found. Generally this\r
-does not make sense.\r
-\r
-=item * garbled entity I<STRING>\r
-\r
-The I<STRING> found cannot be interpreted as a character entity.\r
-\r
-=item * Entity number out of range\r
-\r
-An entity specified by number (dec, hex, oct) is out of range (1-255).\r
-\r
-=item * malformed link LE<lt>E<gt>\r
-\r
-The link found cannot be parsed because it does not conform to the\r
-syntax described in L<perlpod>.\r
-\r
-=item * nonempty ZE<lt>E<gt>\r
-\r
-The C<ZE<lt>E<gt>> sequence is supposed to be empty.\r
-\r
-=item * empty XE<lt>E<gt>\r
-\r
-The index entry specified contains nothing but whitespace.\r
-\r
-=item * Spurious text after =pod / =cut\r
-\r
-The commands C<=pod> and C<=cut> do not take any arguments.\r
-\r
-=item * Spurious =cut command\r
-\r
-A C<=cut> command was found without a preceding POD paragraph.\r
-\r
-=item * Spurious =pod command\r
-\r
-A C<=pod> command was found after a preceding POD paragraph.\r
-\r
-=item * Spurious character(s) after =back\r
-\r
-The C<=back> command does not take any arguments.\r
-\r
-=back\r
-\r
-=head2 Warnings\r
-\r
-These may not necessarily cause trouble, but indicate mediocre style.\r
-\r
-=over 4\r
-\r
-=item * multiple occurrence of link target I<name>\r
-\r
-The POD file has some C<=item> and/or C<=head> commands that have\r
-the same text. Potential hyperlinks to such a text cannot be unique then.\r
-This warning is printed only with warning level greater than one.\r
-\r
-=item * line containing nothing but whitespace in paragraph\r
-\r
-There is some whitespace on a seemingly empty line. POD is very sensitive\r
-to such things, so this is flagged. B<vi> users switch on the B<list>\r
-option to avoid this problem.\r
-\r
-=begin _disabled_\r
-\r
-=item * file does not start with =head\r
-\r
-The file starts with a different POD directive than head.\r
-This is most probably something you do not want.\r
-\r
-=end _disabled_\r
-\r
-=item * previous =item has no contents\r
-\r
-There is a list C<=item> right above the flagged line that has no\r
-text contents. You probably want to delete empty items.\r
-\r
-=item * preceding non-item paragraph(s)\r
-\r
-A list introduced by C<=over> starts with a text or verbatim paragraph,\r
-but continues with C<=item>s. Move the non-item paragraph out of the\r
-C<=over>/C<=back> block.\r
-\r
-=item * =item type mismatch (I<one> vs. I<two>)\r
-\r
-A list started with e.g. a bullet-like C<=item> and continued with a\r
-numbered one. This is obviously inconsistent. For most translators the\r
-type of the I<first> C<=item> determines the type of the list.\r
-\r
-=item * I<N> unescaped C<E<lt>E<gt>> in paragraph\r
-\r
-Angle brackets not written as C<E<lt>ltE<gt>> and C<E<lt>gtE<gt>>\r
-can potentially cause errors as they could be misinterpreted as\r
-markup commands. This is only printed when the -warnings level is\r
-greater than 1.\r
-\r
-=item * Unknown entity\r
-\r
-A character entity was found that does not belong to the standard\r
-ISO set or the POD specials C<verbar> and C<sol>.\r
-\r
-=item * No items in =over\r
-\r
-The list opened with C<=over> does not contain any items.\r
-\r
-=item * No argument for =item\r
-\r
-C<=item> without any parameters is deprecated. It should either be followed\r
-by C<*> to indicate an unordered list, by a number (optionally followed\r
-by a dot) to indicate an ordered (numbered) list or simple text for a\r
-definition list.\r
-\r
-=item * empty section in previous paragraph\r
-\r
-The previous section (introduced by a C<=head> command) does not contain\r
-any text. This usually indicates that something is missing. Note: A\r
-C<=head1> followed immediately by C<=head2> does not trigger this warning.\r
-\r
-=item * Verbatim paragraph in NAME section\r
-\r
-The NAME section (C<=head1 NAME>) should consist of a single paragraph\r
-with the script/module name, followed by a dash `-' and a very short\r
-description of what the thing is good for.\r
-\r
-=item * =headI<n> without preceding higher level\r
-\r
-For example if there is a C<=head2> in the POD file prior to a\r
-C<=head1>.\r
-\r
-=back\r
-\r
-=head2 Hyperlinks\r
-\r
-There are some warnings with respect to malformed hyperlinks:\r
-\r
-=over 4\r
-\r
-=item * ignoring leading/trailing whitespace in link\r
-\r
-There is whitespace at the beginning or the end of the contents of\r
-LE<lt>...E<gt>.\r
-\r
-=item * (section) in '$page' deprecated\r
-\r
-There is a section detected in the page name of LE<lt>...E<gt>, e.g.\r
-C<LE<lt>passwd(2)E<gt>>. POD hyperlinks may point to POD documents only.\r
-Please write C<CE<lt>passwd(2)E<gt>> instead. Some formatters are able\r
-to expand this to appropriate code. For links to (builtin) functions,\r
-please say C<LE<lt>perlfunc/mkdirE<gt>>, without ().\r
-\r
-=item * alternative text/node '%s' contains non-escaped | or /\r
-\r
-The characters C<|> and C</> are special in the LE<lt>...E<gt> context.\r
-Although the hyperlink parser does its best to determine which "/" is\r
-text and which is a delimiter in case of doubt, one ought to escape\r
-these literal characters like this:\r
-\r
- / E<sol>\r
- | E<verbar>\r
-\r
-=back\r
-\r
-=head1 RETURN VALUE\r
-\r
-B<podchecker> returns the number of POD syntax errors found or -1 if\r
-there were no POD commands at all found in the file.\r
-\r
-=head1 EXAMPLES\r
-\r
-See L</SYNOPSIS>\r
-\r
-=head1 INTERFACE\r
-\r
-While checking, this module collects document properties, e.g. the nodes\r
-for hyperlinks (C<=headX>, C<=item>) and index entries (C<XE<lt>E<gt>>).\r
-POD translators can use this feature to syntax-check and get the nodes in\r
-a first pass before actually starting to convert. This is expensive in terms\r
-of execution time, but allows for very robust conversions.\r
-\r
-Since PodParser-1.24 the B<Pod::Checker> module uses only the B<poderror>\r
-method to print errors and warnings. The summary output (e.g.\r
-"Pod syntax OK") has been dropped from the module and has been included in\r
-B<podchecker> (the script). This allows users of B<Pod::Checker> to\r
-control completely the output behavior. Users of B<podchecker> (the script)\r
-get the well-known behavior.\r
-\r
-=cut\r
-\r
-#############################################################################\r
-\r
-#use diagnostics;\r
-use Carp qw(croak);\r
-use Exporter;\r
-use Pod::Parser;\r
-\r
-@ISA = qw(Pod::Parser);\r
-@EXPORT = qw(&podchecker);\r
-\r
-my %VALID_COMMANDS = (\r
- 'pod' => 1,\r
- 'cut' => 1,\r
- 'head1' => 1,\r
- 'head2' => 1,\r
- 'head3' => 1,\r
- 'head4' => 1,\r
- 'over' => 1,\r
- 'back' => 1,\r
- 'item' => 1,\r
- 'for' => 1,\r
- 'begin' => 1,\r
- 'end' => 1,\r
- 'encoding' => 1,\r
-);\r
-\r
-my %VALID_SEQUENCES = (\r
- 'I' => 1,\r
- 'B' => 1,\r
- 'S' => 1,\r
- 'C' => 1,\r
- 'L' => 1,\r
- 'F' => 1,\r
- 'X' => 1,\r
- 'Z' => 1,\r
- 'E' => 1,\r
-);\r
-\r
-# stolen from HTML::Entities\r
-my %ENTITIES = (\r
- # Some normal chars that have special meaning in SGML context\r
- amp => '&', # ampersand\r
-'gt' => '>', # greater than\r
-'lt' => '<', # less than\r
- quot => '"', # double quote\r
-\r
- # PUBLIC ISO 8879-1986//ENTITIES Added Latin 1//EN//HTML\r
- AElig => 'Æ', # capital AE diphthong (ligature)\r
- Aacute => 'Á', # capital A, acute accent\r
- Acirc => 'Â', # capital A, circumflex accent\r
- Agrave => 'À', # capital A, grave accent\r
- Aring => 'Å', # capital A, ring\r
- Atilde => 'Ã', # capital A, tilde\r
- Auml => 'Ä', # capital A, dieresis or umlaut mark\r
- Ccedil => 'Ç', # capital C, cedilla\r
- ETH => 'Ð', # capital Eth, Icelandic\r
- Eacute => 'É', # capital E, acute accent\r
- Ecirc => 'Ê', # capital E, circumflex accent\r
- Egrave => 'È', # capital E, grave accent\r
- Euml => 'Ë', # capital E, dieresis or umlaut mark\r
- Iacute => 'Í', # capital I, acute accent\r
- Icirc => 'Î', # capital I, circumflex accent\r
- Igrave => 'Ì', # capital I, grave accent\r
- Iuml => 'Ï', # capital I, dieresis or umlaut mark\r
- Ntilde => 'Ñ', # capital N, tilde\r
- Oacute => 'Ó', # capital O, acute accent\r
- Ocirc => 'Ô', # capital O, circumflex accent\r
- Ograve => 'Ò', # capital O, grave accent\r
- Oslash => 'Ø', # capital O, slash\r
- Otilde => 'Õ', # capital O, tilde\r
- Ouml => 'Ö', # capital O, dieresis or umlaut mark\r
- THORN => 'Þ', # capital THORN, Icelandic\r
- Uacute => 'Ú', # capital U, acute accent\r
- Ucirc => 'Û', # capital U, circumflex accent\r
- Ugrave => 'Ù', # capital U, grave accent\r
- Uuml => 'Ü', # capital U, dieresis or umlaut mark\r
- Yacute => 'Ý', # capital Y, acute accent\r
- aacute => 'á', # small a, acute accent\r
- acirc => 'â', # small a, circumflex accent\r
- aelig => 'æ', # small ae diphthong (ligature)\r
- agrave => 'à', # small a, grave accent\r
- aring => 'å', # small a, ring\r
- atilde => 'ã', # small a, tilde\r
- auml => 'ä', # small a, dieresis or umlaut mark\r
- ccedil => 'ç', # small c, cedilla\r
- eacute => 'é', # small e, acute accent\r
- ecirc => 'ê', # small e, circumflex accent\r
- egrave => 'è', # small e, grave accent\r
- eth => 'ð', # small eth, Icelandic\r
- euml => 'ë', # small e, dieresis or umlaut mark\r
- iacute => 'í', # small i, acute accent\r
- icirc => 'î', # small i, circumflex accent\r
- igrave => 'ì', # small i, grave accent\r
- iuml => 'ï', # small i, dieresis or umlaut mark\r
- ntilde => 'ñ', # small n, tilde\r
- oacute => 'ó', # small o, acute accent\r
- ocirc => 'ô', # small o, circumflex accent\r
- ograve => 'ò', # small o, grave accent\r
- oslash => 'ø', # small o, slash\r
- otilde => 'õ', # small o, tilde\r
- ouml => 'ö', # small o, dieresis or umlaut mark\r
- szlig => 'ß', # small sharp s, German (sz ligature)\r
- thorn => 'þ', # small thorn, Icelandic\r
- uacute => 'ú', # small u, acute accent\r
- ucirc => 'û', # small u, circumflex accent\r
- ugrave => 'ù', # small u, grave accent\r
- uuml => 'ü', # small u, dieresis or umlaut mark\r
- yacute => 'ý', # small y, acute accent\r
- yuml => 'ÿ', # small y, dieresis or umlaut mark\r
-\r
- # Some extra Latin 1 chars that are listed in the HTML3.2 draft (21-May-96)\r
- copy => '©', # copyright sign\r
- reg => '®', # registered sign\r
- nbsp => "\240", # non breaking space\r
-\r
- # Additional ISO-8859/1 entities listed in rfc1866 (section 14)\r
- iexcl => '¡',\r
- cent => '¢',\r
- pound => '£',\r
- curren => '¤',\r
- yen => '¥',\r
- brvbar => '¦',\r
- sect => '§',\r
- uml => '¨',\r
- ordf => 'ª',\r
- laquo => '«',\r
-'not' => '¬', # not is a keyword in perl\r
- shy => '',\r
- macr => '¯',\r
- deg => '°',\r
- plusmn => '±',\r
- sup1 => '¹',\r
- sup2 => '²',\r
- sup3 => '³',\r
- acute => '´',\r
- micro => 'µ',\r
- para => '¶',\r
- middot => '·',\r
- cedil => '¸',\r
- ordm => 'º',\r
- raquo => '»',\r
- frac14 => '¼',\r
- frac12 => '½',\r
- frac34 => '¾',\r
- iquest => '¿',\r
-'times' => '×', # times is a keyword in perl\r
- divide => '÷',\r
-\r
-# some POD special entities\r
- verbar => '|',\r
- sol => '/'\r
-);\r
-\r
-##---------------------------------------------------------------------------\r
-\r
-##---------------------------------\r
-## Function definitions begin here\r
-##---------------------------------\r
-\r
-sub podchecker {\r
- my ($infile, $outfile, %options) = @_;\r
- local $_;\r
-\r
- ## Set defaults\r
- $infile ||= \*STDIN;\r
- $outfile ||= \*STDERR;\r
-\r
- ## Now create a pod checker\r
- my $checker = new Pod::Checker(%options);\r
-\r
- ## Now check the pod document for errors\r
- $checker->parse_from_file($infile, $outfile);\r
-\r
- ## Return the number of errors found\r
- return $checker->num_errors();\r
-}\r
-\r
-##---------------------------------------------------------------------------\r
-\r
-##-------------------------------\r
-## Method definitions begin here\r
-##-------------------------------\r
-\r
-##################################\r
-\r
-=over 4\r
-\r
-=item C<Pod::Checker-E<gt>new( %options )>\r
-\r
-Return a reference to a new Pod::Checker object that inherits from\r
-Pod::Parser and is used for calling the required methods later. The\r
-following options are recognized:\r
-\r
-C<-warnings =E<gt> num>\r
- Print warnings if C<num> is true. The higher the value of C<num>,\r
-the more warnings are printed. Currently there are only levels 1 and 2.\r
-\r
-C<-quiet =E<gt> num>\r
- If C<num> is true, do not print any errors/warnings. This is useful\r
-when Pod::Checker is used to munge POD code into plain text from within\r
-POD formatters.\r
-\r
-=cut\r
-\r
-## sub new {\r
-## my $this = shift;\r
-## my $class = ref($this) || $this;\r
-## my %params = @_;\r
-## my $self = {%params};\r
-## bless $self, $class;\r
-## $self->initialize();\r
-## return $self;\r
-## }\r
-\r
-sub initialize {\r
- my $self = shift;\r
- ## Initialize number of errors, and setup an error function to\r
- ## increment this number and then print to the designated output.\r
- $self->{_NUM_ERRORS} = 0;\r
- $self->{_NUM_WARNINGS} = 0;\r
- $self->{-quiet} ||= 0;\r
- # set the error handling subroutine\r
- $self->errorsub($self->{-quiet} ? sub { 1; } : 'poderror');\r
- $self->{_commands} = 0; # total number of POD commands encountered\r
- $self->{_list_stack} = []; # stack for nested lists\r
- $self->{_have_begin} = ''; # stores =begin\r
- $self->{_links} = []; # stack for internal hyperlinks\r
- $self->{_nodes} = []; # stack for =head/=item nodes\r
- $self->{_index} = []; # text in X<>\r
- # print warnings?\r
- $self->{-warnings} = 1 unless(defined $self->{-warnings});\r
- $self->{_current_head1} = ''; # the current =head1 block\r
- $self->parseopts(-process_cut_cmd => 1, -warnings => $self->{-warnings});\r
-}\r
-\r
-##################################\r
-\r
-=item C<$checker-E<gt>poderror( @args )>\r
-\r
-=item C<$checker-E<gt>poderror( {%opts}, @args )>\r
-\r
-Internal method for printing errors and warnings. If no options are\r
-given, simply prints "@_". The following options are recognized and used\r
-to form the output:\r
-\r
- -msg\r
-\r
-A message to print prior to C<@args>.\r
-\r
- -line\r
-\r
-The line number the error occurred in.\r
-\r
- -file\r
-\r
-The file (name) the error occurred in.\r
-\r
- -severity\r
-\r
-The error level, should be 'WARNING' or 'ERROR'.\r
-\r
-=cut\r
-\r
-# Invoked as $self->poderror( @args ), or $self->poderror( {%opts}, @args )\r
-sub poderror {\r
- my $self = shift;\r
- my %opts = (ref $_[0]) ? %{shift()} : ();\r
-\r
- ## Retrieve options\r
- chomp( my $msg = ($opts{-msg} || '')."@_" );\r
- my $line = (exists $opts{-line}) ? " at line $opts{-line}" : '';\r
- my $file = (exists $opts{-file}) ? " in file $opts{-file}" : '';\r
- unless (exists $opts{-severity}) {\r
- ## See if can find severity in message prefix\r
- $opts{-severity} = $1 if ( $msg =~ s/^\**\s*([A-Z]{3,}):\s+// );\r
- }\r
- my $severity = (exists $opts{-severity}) ? "*** $opts{-severity}: " : '';\r
-\r
- ## Increment error count and print message "\r
- ++($self->{_NUM_ERRORS})\r
- if(!%opts || ($opts{-severity} && $opts{-severity} eq 'ERROR'));\r
- ++($self->{_NUM_WARNINGS})\r
- if(!%opts || ($opts{-severity} && $opts{-severity} eq 'WARNING'));\r
- unless($self->{-quiet}) {\r
- my $out_fh = $self->output_handle() || \*STDERR;\r
- print $out_fh ($severity, $msg, $line, $file, "\n")\r
- if($self->{-warnings} || !%opts || $opts{-severity} ne 'WARNING');\r
- }\r
-}\r
-\r
-##################################\r
-\r
-=item C<$checker-E<gt>num_errors()>\r
-\r
-Set (if argument specified) and retrieve the number of errors found.\r
-\r
-=cut\r
-\r
-sub num_errors {\r
- return (@_ > 1) ? ($_[0]->{_NUM_ERRORS} = $_[1]) : $_[0]->{_NUM_ERRORS};\r
-}\r
-\r
-##################################\r
-\r
-=item C<$checker-E<gt>num_warnings()>\r
-\r
-Set (if argument specified) and retrieve the number of warnings found.\r
-\r
-=cut\r
-\r
-sub num_warnings {\r
- return (@_ > 1) ? ($_[0]->{_NUM_WARNINGS} = $_[1]) : $_[0]->{_NUM_WARNINGS};\r
-}\r
-\r
-##################################\r
-\r
-=item C<$checker-E<gt>name()>\r
-\r
-Set (if argument specified) and retrieve the canonical name of POD as\r
-found in the C<=head1 NAME> section.\r
-\r
-=cut\r
-\r
-sub name {\r
- return (@_ > 1 && $_[1]) ?\r
- ($_[0]->{-name} = $_[1]) : $_[0]->{-name};\r
-}\r
-\r
-##################################\r
-\r
-=item C<$checker-E<gt>node()>\r
-\r
-Add (if argument specified) and retrieve the nodes (as defined by C<=headX>\r
-and C<=item>) of the current POD. The nodes are returned in the order of\r
-their occurrence. They consist of plain text, each piece of whitespace is\r
-collapsed to a single blank.\r
-\r
-=cut\r
-\r
-sub node {\r
- my ($self,$text) = @_;\r
- if(defined $text) {\r
- $text =~ s/\s+$//s; # strip trailing whitespace\r
- $text =~ s/\s+/ /gs; # collapse whitespace\r
- # add node, order important!\r
- push(@{$self->{_nodes}}, $text);\r
- # keep also a uniqueness counter\r
- $self->{_unique_nodes}->{$text}++ if($text !~ /^\s*$/s);\r
- return $text;\r
- }\r
- @{$self->{_nodes}};\r
-}\r
-\r
-##################################\r
-\r
-=item C<$checker-E<gt>idx()>\r
-\r
-Add (if argument specified) and retrieve the index entries (as defined by\r
-C<XE<lt>E<gt>>) of the current POD. They consist of plain text, each piece\r
-of whitespace is collapsed to a single blank.\r
-\r
-=cut\r
-\r
-# set/return index entries of current POD\r
-sub idx {\r
- my ($self,$text) = @_;\r
- if(defined $text) {\r
- $text =~ s/\s+$//s; # strip trailing whitespace\r
- $text =~ s/\s+/ /gs; # collapse whitespace\r
- # add node, order important!\r
- push(@{$self->{_index}}, $text);\r
- # keep also a uniqueness counter\r
- $self->{_unique_nodes}->{$text}++ if($text !~ /^\s*$/s);\r
- return $text;\r
- }\r
- @{$self->{_index}};\r
-}\r
-\r
-##################################\r
-\r
-=item C<$checker-E<gt>hyperlink()>\r
-\r
-Add (if argument specified) and retrieve the hyperlinks (as defined by\r
-C<LE<lt>E<gt>>) of the current POD. They consist of a 2-item array: line\r
-number and C<Pod::Hyperlink> object.\r
-\r
-=back\r
-\r
-=cut\r
-\r
-# set/return hyperlinks of the current POD\r
-sub hyperlink {\r
- my $self = shift;\r
- if($_[0]) {\r
- push(@{$self->{_links}}, $_[0]);\r
- return $_[0];\r
- }\r
- @{$self->{_links}};\r
-}\r
-\r
-## overrides for Pod::Parser\r
-\r
-sub end_pod {\r
- ## Do some final checks and\r
- ## print the number of errors found\r
- my $self = shift;\r
- my $infile = $self->input_file();\r
-\r
- if(@{$self->{_list_stack}}) {\r
- my $list;\r
- while(($list = $self->_close_list('EOF',$infile)) &&\r
- $list->indent() ne 'auto') {\r
- $self->poderror({ -line => 'EOF', -file => $infile,\r
- -severity => 'ERROR', -msg => '=over on line ' .\r
- $list->start() . ' without closing =back' });\r
- }\r
- }\r
-\r
- # check validity of document internal hyperlinks\r
- # first build the node names from the paragraph text\r
- my %nodes;\r
- foreach($self->node()) {\r
- $nodes{$_} = 1;\r
- if(/^(\S+)\s+\S/) {\r
- # we have more than one word. Use the first as a node, too.\r
- # This is used heavily in perlfunc.pod\r
- $nodes{$1} ||= 2; # derived node\r
- }\r
- }\r
- foreach($self->idx()) {\r
- $nodes{$_} = 3; # index node\r
- }\r
- foreach($self->hyperlink()) {\r
- my ($line,$link) = @$_;\r
- # _TODO_ what if there is a link to the page itself by the name,\r
- # e.g. in Tk::Pod : L<Tk::Pod/"DESCRIPTION">\r
- if($link->node() && !$link->page() && $link->type() ne 'hyperlink') {\r
- my $node = $self->_check_ptree($self->parse_text($link->node(),\r
- $line), $line, $infile, 'L');\r
- if($node && !$nodes{$node}) {\r
- $self->poderror({ -line => $line || '', -file => $infile,\r
- -severity => 'ERROR',\r
- -msg => "unresolved internal link '$node'"});\r
- }\r
- }\r
- }\r
-\r
- # check the internal nodes for uniqueness. This pertains to\r
- # =headX, =item and X<...>\r
- if($self->{-warnings} && $self->{-warnings}>1) {\r
- foreach(grep($self->{_unique_nodes}->{$_} > 1,\r
- keys %{$self->{_unique_nodes}})) {\r
- $self->poderror({ -line => '-', -file => $infile,\r
- -severity => 'WARNING',\r
- -msg => "multiple occurrence of link target '$_'"});\r
- }\r
- }\r
-\r
- # no POD found here\r
- $self->num_errors(-1) if($self->{_commands} == 0);\r
-}\r
-\r
-# check a POD command directive\r
-sub command {\r
- my ($self, $cmd, $paragraph, $line_num, $pod_para) = @_;\r
- my ($file, $line) = $pod_para->file_line;\r
- ## Check the command syntax\r
- my $arg; # this will hold the command argument\r
- if (! $VALID_COMMANDS{$cmd}) {\r
- $self->poderror({ -line => $line, -file => $file, -severity => 'ERROR',\r
- -msg => "Unknown command '$cmd'" });\r
- }\r
- else { # found a valid command\r
- $self->{_commands}++; # delete this line if below is enabled again\r
-\r
- $self->_commands_in_paragraphs($paragraph, $pod_para);\r
-\r
- ##### following check disabled due to strong request\r
- #if(!$self->{_commands}++ && $cmd !~ /^head/) {\r
- # $self->poderror({ -line => $line, -file => $file,\r
- # -severity => 'WARNING',\r
- # -msg => "file does not start with =head" });\r
- #}\r
-\r
- # check syntax of particular command\r
- if($cmd eq 'over') {\r
- # check for argument\r
- $arg = $self->interpolate_and_check($paragraph, $line,$file);\r
- my $indent = 4; # default\r
- if($arg && $arg =~ /^\s*(\d+)\s*$/) {\r
- $indent = $1;\r
- }\r
- # start a new list\r
- $self->_open_list($indent,$line,$file);\r
- }\r
- elsif($cmd eq 'item') {\r
- # are we in a list?\r
- unless(@{$self->{_list_stack}}) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => '=item without previous =over' });\r
- # auto-open in case we encounter many more\r
- $self->_open_list('auto',$line,$file);\r
- }\r
- my $list = $self->{_list_stack}->[0];\r
- # check whether the previous item had some contents\r
- if(defined $self->{_list_item_contents} &&\r
- $self->{_list_item_contents} == 0) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => 'previous =item has no contents' });\r
- }\r
- if($list->{_has_par}) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => 'preceding non-item paragraph(s)' });\r
- delete $list->{_has_par};\r
- }\r
- # check for argument\r
- $arg = $self->interpolate_and_check($paragraph, $line, $file);\r
- if($arg && $arg =~ /(\S+)/) {\r
- $arg =~ s/[\s\n]+$//;\r
- my $type;\r
- if($arg =~ /^[*]\s*(\S*.*)/) {\r
- $type = 'bullet';\r
- $self->{_list_item_contents} = $1 ? 1 : 0;\r
- $arg = $1;\r
- }\r
- elsif($arg =~ /^\d+\.?\s+(\S*)/) {\r
- $type = 'number';\r
- $self->{_list_item_contents} = $1 ? 1 : 0;\r
- $arg = $1;\r
- }\r
- else {\r
- $type = 'definition';\r
- $self->{_list_item_contents} = 1;\r
- }\r
- my $first = $list->type();\r
- if($first && $first ne $type) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => "=item type mismatch ('$first' vs. '$type')"});\r
- }\r
- else { # first item\r
- $list->type($type);\r
- }\r
- }\r
- else {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => 'No argument for =item' });\r
- $arg = ' '; # empty\r
- $self->{_list_item_contents} = 0;\r
- }\r
- # add this item\r
- $list->item($arg);\r
- # remember this node\r
- $self->node($arg);\r
- }\r
- elsif($cmd eq 'back') {\r
- # check if we have an open list\r
- unless(@{$self->{_list_stack}}) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => '=back without previous =over' });\r
- }\r
- else {\r
- # check for spurious characters\r
- $arg = $self->interpolate_and_check($paragraph, $line,$file);\r
- if($arg && $arg =~ /\S/) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => 'Spurious character(s) after =back' });\r
- }\r
- # close list\r
- my $list = $self->_close_list($line,$file);\r
- # check for empty lists\r
- if(!$list->item() && $self->{-warnings}) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => 'No items in =over (at line ' .\r
- $list->start() . ') / =back list'});\r
- }\r
- }\r
- }\r
- elsif($cmd =~ /^head(\d+)/) {\r
- my $hnum = $1;\r
- $self->{"_have_head_$hnum"}++; # count head types\r
- if($hnum > 1 && !$self->{'_have_head_'.($hnum -1)}) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => "=head$hnum without preceding higher level"});\r
- }\r
- # check whether the previous =head section had some contents\r
- if(defined $self->{_commands_in_head} &&\r
- $self->{_commands_in_head} == 0 &&\r
- defined $self->{_last_head} &&\r
- $self->{_last_head} >= $hnum) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => 'empty section in previous paragraph'});\r
- }\r
- $self->{_commands_in_head} = -1;\r
- $self->{_last_head} = $hnum;\r
- # check if there is an open list\r
- if(@{$self->{_list_stack}}) {\r
- my $list;\r
- while(($list = $self->_close_list($line,$file)) &&\r
- $list->indent() ne 'auto') {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => '=over on line '. $list->start() .\r
- " without closing =back (at $cmd)" });\r
- }\r
- }\r
- # remember this node\r
- $arg = $self->interpolate_and_check($paragraph, $line,$file);\r
- $arg =~ s/[\s\n]+$//s;\r
- $self->node($arg);\r
- unless(length($arg)) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => "empty =$cmd"});\r
- }\r
- if($cmd eq 'head1') {\r
- $self->{_current_head1} = $arg;\r
- } else {\r
- $self->{_current_head1} = '';\r
- }\r
- }\r
- elsif($cmd eq 'begin') {\r
- if($self->{_have_begin}) {\r
- # already have a begin\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => q{Nested =begin's (first at line } .\r
- $self->{_have_begin} . ')'});\r
- }\r
- else {\r
- # check for argument\r
- $arg = $self->interpolate_and_check($paragraph, $line,$file);\r
- unless($arg && $arg =~ /(\S+)/) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => 'No argument for =begin'});\r
- }\r
- # remember the =begin\r
- $self->{_have_begin} = "$line:$1";\r
- }\r
- }\r
- elsif($cmd eq 'end') {\r
- if($self->{_have_begin}) {\r
- # close the existing =begin\r
- $self->{_have_begin} = '';\r
- # check for spurious characters\r
- $arg = $self->interpolate_and_check($paragraph, $line,$file);\r
- # the closing argument is optional\r
- #if($arg && $arg =~ /\S/) {\r
- # $self->poderror({ -line => $line, -file => $file,\r
- # -severity => 'WARNING',\r
- # -msg => "Spurious character(s) after =end" });\r
- #}\r
- }\r
- else {\r
- # don't have a matching =begin\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => '=end without =begin' });\r
- }\r
- }\r
- elsif($cmd eq 'for') {\r
- unless($paragraph =~ /\s*(\S+)\s*/) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => '=for without formatter specification' });\r
- }\r
- $arg = ''; # do not expand paragraph below\r
- }\r
- elsif($cmd =~ /^(pod|cut)$/) {\r
- # check for argument\r
- $arg = $self->interpolate_and_check($paragraph, $line,$file);\r
- if($arg && $arg =~ /(\S+)/) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => "Spurious text after =$cmd"});\r
- }\r
- if($cmd eq 'cut' && (!$self->{_PREVIOUS} || $self->{_PREVIOUS} eq 'cut')) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => "Spurious =cut command"});\r
- }\r
- if($cmd eq 'pod' && $self->{_PREVIOUS} && $self->{_PREVIOUS} ne 'cut') {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => "Spurious =pod command"});\r
- }\r
- }\r
- $self->{_commands_in_head}++;\r
- ## Check the interior sequences in the command-text\r
- $self->interpolate_and_check($paragraph, $line,$file)\r
- unless(defined $arg);\r
- }\r
-}\r
-\r
-sub _open_list\r
-{\r
- my ($self,$indent,$line,$file) = @_;\r
- my $list = Pod::List->new(\r
- -indent => $indent,\r
- -start => $line,\r
- -file => $file);\r
- unshift(@{$self->{_list_stack}}, $list);\r
- undef $self->{_list_item_contents};\r
- $list;\r
-}\r
-\r
-sub _close_list\r
-{\r
- my ($self,$line,$file) = @_;\r
- my $list = shift(@{$self->{_list_stack}});\r
- if(defined $self->{_list_item_contents} &&\r
- $self->{_list_item_contents} == 0) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => 'previous =item has no contents' });\r
- }\r
- undef $self->{_list_item_contents};\r
- $list;\r
-}\r
-\r
-# process a block of some text\r
-sub interpolate_and_check {\r
- my ($self, $paragraph, $line, $file) = @_;\r
- ## Check the interior sequences in the command-text\r
- # and return the text\r
- $self->_check_ptree(\r
- $self->parse_text($paragraph,$line), $line, $file, '');\r
-}\r
-\r
-sub _check_ptree {\r
- my ($self,$ptree,$line,$file,$nestlist) = @_;\r
- local($_);\r
- my $text = '';\r
- # process each node in the parse tree\r
- foreach(@$ptree) {\r
- # regular text chunk\r
- unless(ref) {\r
- # count the unescaped angle brackets\r
- # complain only when warning level is greater than 1\r
- if($self->{-warnings} && $self->{-warnings}>1) {\r
- my $count;\r
- if($count = tr/<>/<>/) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => "$count unescaped <> in paragraph" });\r
- }\r
- }\r
- $text .= $_;\r
- next;\r
- }\r
- # have an interior sequence\r
- my $cmd = $_->cmd_name();\r
- my $contents = $_->parse_tree();\r
- ($file,$line) = $_->file_line();\r
- # check for valid tag\r
- if (! $VALID_SEQUENCES{$cmd}) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => qq(Unknown interior-sequence '$cmd')});\r
- # expand it anyway\r
- $text .= $self->_check_ptree($contents, $line, $file, "$nestlist$cmd");\r
- next;\r
- }\r
- if(index($nestlist, $cmd) != -1) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => "nested commands $cmd<...$cmd<...>...>"});\r
- # _TODO_ should we add the contents anyway?\r
- # expand it anyway, see below\r
- }\r
- if($cmd eq 'E') {\r
- # preserve entities\r
- if(@$contents > 1 || ref $$contents[0] || $$contents[0] !~ /^\w+$/) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => 'garbled entity ' . $_->raw_text()});\r
- next;\r
- }\r
- my $ent = $$contents[0];\r
- my $val;\r
- if($ent =~ /^0x[0-9a-f]+$/i) {\r
- # hexadec entity\r
- $val = hex($ent);\r
- }\r
- elsif($ent =~ /^0\d+$/) {\r
- # octal\r
- $val = oct($ent);\r
- }\r
- elsif($ent =~ /^\d+$/) {\r
- # numeric entity\r
- $val = $ent;\r
- }\r
- if(defined $val) {\r
- if($val>0 && $val<256) {\r
- $text .= chr($val);\r
- }\r
- else {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => 'Entity number out of range ' . $_->raw_text()});\r
- }\r
- }\r
- elsif($ENTITIES{$ent}) {\r
- # known ISO entity\r
- $text .= $ENTITIES{$ent};\r
- }\r
- else {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => 'Unknown entity ' . $_->raw_text()});\r
- $text .= "E<$ent>";\r
- }\r
- }\r
- elsif($cmd eq 'L') {\r
- # try to parse the hyperlink\r
- my $link = Pod::Hyperlink->new($contents->raw_text());\r
- unless(defined $link) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => 'malformed link ' . $_->raw_text() ." : $@"});\r
- next;\r
- }\r
- $link->line($line); # remember line\r
- if($self->{-warnings}) {\r
- foreach my $w ($link->warning()) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => $w });\r
- }\r
- }\r
- # check the link text\r
- $text .= $self->_check_ptree($self->parse_text($link->text(),\r
- $line), $line, $file, "$nestlist$cmd");\r
- # remember link\r
- $self->hyperlink([$line,$link]);\r
- }\r
- elsif($cmd =~ /[BCFIS]/) {\r
- # add the guts\r
- $text .= $self->_check_ptree($contents, $line, $file, "$nestlist$cmd");\r
- }\r
- elsif($cmd eq 'Z') {\r
- if(length($contents->raw_text())) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => 'Nonempty Z<>'});\r
- }\r
- }\r
- elsif($cmd eq 'X') {\r
- my $idx = $self->_check_ptree($contents, $line, $file, "$nestlist$cmd");\r
- if($idx =~ /^\s*$/s) {\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => 'Empty X<>'});\r
- }\r
- else {\r
- # remember this node\r
- $self->idx($idx);\r
- }\r
- }\r
- else {\r
- # not reached\r
- croak 'internal error';\r
- }\r
- }\r
- $text;\r
-}\r
-\r
-# process a block of verbatim text\r
-sub verbatim {\r
- ## Nothing particular to check\r
- my ($self, $paragraph, $line_num, $pod_para) = @_;\r
-\r
- $self->_preproc_par($paragraph);\r
- $self->_commands_in_paragraphs($paragraph, $pod_para);\r
-\r
- if($self->{_current_head1} eq 'NAME') {\r
- my ($file, $line) = $pod_para->file_line;\r
- $self->poderror({ -line => $line, -file => $file,\r
- -severity => 'WARNING',\r
- -msg => 'Verbatim paragraph in NAME section' });\r
- }\r
-}\r
-\r
-# process a block of regular text\r
-sub textblock {\r
- my ($self, $paragraph, $line_num, $pod_para) = @_;\r
- my ($file, $line) = $pod_para->file_line;\r
-\r
- $self->_preproc_par($paragraph);\r
- $self->_commands_in_paragraphs($paragraph, $pod_para);\r
-\r
- # skip this paragraph if in a =begin block\r
- unless($self->{_have_begin}) {\r
- my $block = $self->interpolate_and_check($paragraph, $line,$file);\r
- if($self->{_current_head1} eq 'NAME') {\r
- if($block =~ /^\s*(\S+?)\s*[,-]/) {\r
- # this is the canonical name\r
- $self->{-name} = $1 unless(defined $self->{-name});\r
- }\r
- }\r
- }\r
-}\r
-\r
-sub _preproc_par\r
-{\r
- my $self = shift;\r
- $_[0] =~ s/[\s\n]+$//;\r
- if($_[0]) {\r
- $self->{_commands_in_head}++;\r
- $self->{_list_item_contents}++ if(defined $self->{_list_item_contents});\r
- if(@{$self->{_list_stack}} && !$self->{_list_stack}->[0]->item()) {\r
- $self->{_list_stack}->[0]->{_has_par} = 1;\r
- }\r
- }\r
-}\r
-\r
-# look for =foo commands at the start of a line within a paragraph, as for\r
-# instance the following which prints as "* one =item two".\r
-#\r
-# =item one\r
-# =item two\r
-#\r
-# Examples of =foo written in docs are expected to be indented in a verbatim\r
-# or marked up C<=foo> so won't be caught. A double-angle C<< =foo >> could\r
-# have the =foo at the start of a line, but that should be unlikely and is\r
-# easily enough dealt with by not putting a newline after the C<<.\r
-#\r
-sub _commands_in_paragraphs {\r
- my ($self, $str, $pod_para) = @_;\r
- while ($str =~ /[^\n]\n=([a-z][a-z0-9]+)/sg) {\r
- my $cmd = $1;\r
- my $pos = pos($str);\r
- if ($VALID_COMMANDS{$cmd}) {\r
- my ($file, $line) = $pod_para->file_line;\r
- my $part = substr($str, 0, $pos);\r
- $line += ($part =~ tr/\n//); # count of newlines\r
-\r
- $self->poderror\r
- ({ -line => $line, -file => $file,\r
- -severity => 'ERROR',\r
- -msg => "Apparent command =$cmd not preceded by blank line"});\r
- }\r
- }\r
-}\r
-\r
-1;\r
-\r
-__END__\r
-\r
-=head1 AUTHOR\r
-\r
-Please report bugs using L<http://rt.cpan.org>.\r
-\r
-Brad Appleton E<lt>bradapp@enteract.comE<gt> (initial version),\r
-Marek Rouchal E<lt>marekr@cpan.orgE<gt>\r
-\r
-Based on code for B<Pod::Text::pod2text()> written by\r
-Tom Christiansen E<lt>tchrist@mox.perl.comE<gt>\r
-\r
-B<Pod::Checker> is part of the Pod-Checker distribution, and is based on\r
-L<Pod::Parser>.\r
-\r
-=cut\r
-\r
+#############################################################################
+# Pod/Checker.pm -- check pod documents for syntax errors
+#
+# Copyright (C) 1994-2000 by Bradford Appleton. All rights reserved.
+# This file is part of "PodParser". PodParser is free software;
+# you can redistribute it and/or modify it under the same terms
+# as Perl itself.
+#############################################################################
+
+package Pod::Checker;
+use strict;
+
+use vars qw($VERSION @ISA @EXPORT %VALID_COMMANDS %VALID_SEQUENCES);
+$VERSION = '1.72'; ## Current version of this package
+require 5.005; ## requires this Perl version or later
+
+use Pod::ParseUtils; ## for hyperlinks and lists
+
+=head1 NAME
+
+Pod::Checker, podchecker() - check pod documents for syntax errors
+
+=head1 SYNOPSIS
+
+ use Pod::Checker;
+
+ $num_errors = podchecker($filepath, $outputpath, %options);
+
+ my $checker = new Pod::Checker %options;
+ $checker->parse_from_file($filepath, \*STDERR);
+
+=head1 OPTIONS/ARGUMENTS
+
+C<$filepath> is the input POD to read and C<$outputpath> is
+where to write POD syntax error messages. Either argument may be a scalar
+indicating a file-path, or else a reference to an open filehandle.
+If unspecified, the input-file it defaults to C<\*STDIN>, and
+the output-file defaults to C<\*STDERR>.
+
+=head2 podchecker()
+
+This function can take a hash of options:
+
+=over 4
+
+=item B<-warnings> =E<gt> I<val>
+
+Turn warnings on/off. I<val> is usually 1 for on, but higher values
+trigger additional warnings. See L<"Warnings">.
+
+=back
+
+=head1 DESCRIPTION
+
+B<podchecker> will perform syntax checking of Perl5 POD format documentation.
+
+Curious/ambitious users are welcome to propose additional features they wish
+to see in B<Pod::Checker> and B<podchecker> and verify that the checks are
+consistent with L<perlpod>.
+
+The following checks are currently performed:
+
+=over 4
+
+=item *
+
+Unknown '=xxxx' commands, unknown 'XE<lt>...E<gt>' interior-sequences,
+and unterminated interior sequences.
+
+=item *
+
+Check for proper balancing of C<=begin> and C<=end>. The contents of such
+a block are generally ignored, i.e. no syntax checks are performed.
+
+=item *
+
+Check for proper nesting and balancing of C<=over>, C<=item> and C<=back>.
+
+=item *
+
+Check for same nested interior-sequences (e.g.
+C<LE<lt>...LE<lt>...E<gt>...E<gt>>).
+
+=item *
+
+Check for malformed or non-existing entities C<EE<lt>...E<gt>>.
+
+=item *
+
+Check for correct syntax of hyperlinks C<LE<lt>...E<gt>>. See L<perlpod>
+for details.
+
+=item *
+
+Check for unresolved document-internal links. This check may also reveal
+misspelled links that seem to be internal links but should be links
+to something else.
+
+=back
+
+=head1 DIAGNOSTICS
+
+=head2 Errors
+
+=over 4
+
+=item * empty =headn
+
+A heading (C<=head1> or C<=head2>) without any text? That ain't no
+heading!
+
+=item * =over on line I<N> without closing =back
+
+The C<=over> command does not have a corresponding C<=back> before the
+next heading (C<=head1> or C<=head2>) or the end of the file.
+
+=item * =item without previous =over
+
+=item * =back without previous =over
+
+An C<=item> or C<=back> command has been found outside a
+C<=over>/C<=back> block.
+
+=item * No argument for =begin
+
+A C<=begin> command was found that is not followed by the formatter
+specification.
+
+=item * =end without =begin
+
+A standalone C<=end> command was found.
+
+=item * Nested =begin's
+
+There were at least two consecutive C<=begin> commands without
+the corresponding C<=end>. Only one C<=begin> may be active at
+a time.
+
+=item * =for without formatter specification
+
+There is no specification of the formatter after the C<=for> command.
+
+=item * Apparent command =foo not preceded by blank line
+
+A command which has ended up in the middle of a paragraph or other command,
+such as
+
+ =item one
+ =item two <-- bad
+
+=item * unresolved internal link I<NAME>
+
+The given link to I<NAME> does not have a matching node in the current
+POD. This also happened when a single word node name is not enclosed in
+C<"">.
+
+=item * Unknown command "I<CMD>"
+
+An invalid POD command has been found. Valid are C<=head1>, C<=head2>,
+C<=head3>, C<=head4>, C<=over>, C<=item>, C<=back>, C<=begin>, C<=end>,
+C<=for>, C<=pod>, C<=cut>
+
+=item * Unknown interior-sequence "I<SEQ>"
+
+An invalid markup command has been encountered. Valid are:
+C<BE<lt>E<gt>>, C<CE<lt>E<gt>>, C<EE<lt>E<gt>>, C<FE<lt>E<gt>>,
+C<IE<lt>E<gt>>, C<LE<lt>E<gt>>, C<SE<lt>E<gt>>, C<XE<lt>E<gt>>,
+C<ZE<lt>E<gt>>
+
+=item * nested commands I<CMD>E<lt>...I<CMD>E<lt>...E<gt>...E<gt>
+
+Two nested identical markup commands have been found. Generally this
+does not make sense.
+
+=item * garbled entity I<STRING>
+
+The I<STRING> found cannot be interpreted as a character entity.
+
+=item * Entity number out of range
+
+An entity specified by number (dec, hex, oct) is out of range (1-255).
+
+=item * malformed link LE<lt>E<gt>
+
+The link found cannot be parsed because it does not conform to the
+syntax described in L<perlpod>.
+
+=item * nonempty ZE<lt>E<gt>
+
+The C<ZE<lt>E<gt>> sequence is supposed to be empty.
+
+=item * empty XE<lt>E<gt>
+
+The index entry specified contains nothing but whitespace.
+
+=item * Spurious text after =pod / =cut
+
+The commands C<=pod> and C<=cut> do not take any arguments.
+
+=item * Spurious =cut command
+
+A C<=cut> command was found without a preceding POD paragraph.
+
+=item * Spurious =pod command
+
+A C<=pod> command was found after a preceding POD paragraph.
+
+=item * Spurious character(s) after =back
+
+The C<=back> command does not take any arguments.
+
+=back
+
+=head2 Warnings
+
+These may not necessarily cause trouble, but indicate mediocre style.
+
+=over 4
+
+=item * multiple occurrence of link target I<name>
+
+The POD file has some C<=item> and/or C<=head> commands that have
+the same text. Potential hyperlinks to such a text cannot be unique then.
+This warning is printed only with warning level greater than one.
+
+=item * line containing nothing but whitespace in paragraph
+
+There is some whitespace on a seemingly empty line. POD is very sensitive
+to such things, so this is flagged. B<vi> users switch on the B<list>
+option to avoid this problem.
+
+=begin _disabled_
+
+=item * file does not start with =head
+
+The file starts with a different POD directive than head.
+This is most probably something you do not want.
+
+=end _disabled_
+
+=item * previous =item has no contents
+
+There is a list C<=item> right above the flagged line that has no
+text contents. You probably want to delete empty items.
+
+=item * preceding non-item paragraph(s)
+
+A list introduced by C<=over> starts with a text or verbatim paragraph,
+but continues with C<=item>s. Move the non-item paragraph out of the
+C<=over>/C<=back> block.
+
+=item * =item type mismatch (I<one> vs. I<two>)
+
+A list started with e.g. a bullet-like C<=item> and continued with a
+numbered one. This is obviously inconsistent. For most translators the
+type of the I<first> C<=item> determines the type of the list.
+
+=item * I<N> unescaped C<E<lt>E<gt>> in paragraph
+
+Angle brackets not written as C<E<lt>ltE<gt>> and C<E<lt>gtE<gt>>
+can potentially cause errors as they could be misinterpreted as
+markup commands. This is only printed when the -warnings level is
+greater than 1.
+
+=item * Unknown entity
+
+A character entity was found that does not belong to the standard
+ISO set or the POD specials C<verbar> and C<sol>.
+
+=item * No items in =over
+
+The list opened with C<=over> does not contain any items.
+
+=item * No argument for =item
+
+C<=item> without any parameters is deprecated. It should either be followed
+by C<*> to indicate an unordered list, by a number (optionally followed
+by a dot) to indicate an ordered (numbered) list or simple text for a
+definition list.
+
+=item * empty section in previous paragraph
+
+The previous section (introduced by a C<=head> command) does not contain
+any text. This usually indicates that something is missing. Note: A
+C<=head1> followed immediately by C<=head2> does not trigger this warning.
+
+=item * Verbatim paragraph in NAME section
+
+The NAME section (C<=head1 NAME>) should consist of a single paragraph
+with the script/module name, followed by a dash `-' and a very short
+description of what the thing is good for.
+
+=item * =headI<n> without preceding higher level
+
+For example if there is a C<=head2> in the POD file prior to a
+C<=head1>.
+
+=back
+
+=head2 Hyperlinks
+
+There are some warnings with respect to malformed hyperlinks:
+
+=over 4
+
+=item * ignoring leading/trailing whitespace in link
+
+There is whitespace at the beginning or the end of the contents of
+LE<lt>...E<gt>.
+
+=item * (section) in '$page' deprecated
+
+There is a section detected in the page name of LE<lt>...E<gt>, e.g.
+C<LE<lt>passwd(2)E<gt>>. POD hyperlinks may point to POD documents only.
+Please write C<CE<lt>passwd(2)E<gt>> instead. Some formatters are able
+to expand this to appropriate code. For links to (builtin) functions,
+please say C<LE<lt>perlfunc/mkdirE<gt>>, without ().
+
+=item * alternative text/node '%s' contains non-escaped | or /
+
+The characters C<|> and C</> are special in the LE<lt>...E<gt> context.
+Although the hyperlink parser does its best to determine which "/" is
+text and which is a delimiter in case of doubt, one ought to escape
+these literal characters like this:
+
+ / E<sol>
+ | E<verbar>
+
+=back
+
+=head1 RETURN VALUE
+
+B<podchecker> returns the number of POD syntax errors found or -1 if
+there were no POD commands at all found in the file.
+
+=head1 EXAMPLES
+
+See L</SYNOPSIS>
+
+=head1 INTERFACE
+
+While checking, this module collects document properties, e.g. the nodes
+for hyperlinks (C<=headX>, C<=item>) and index entries (C<XE<lt>E<gt>>).
+POD translators can use this feature to syntax-check and get the nodes in
+a first pass before actually starting to convert. This is expensive in terms
+of execution time, but allows for very robust conversions.
+
+Since PodParser-1.24 the B<Pod::Checker> module uses only the B<poderror>
+method to print errors and warnings. The summary output (e.g.
+"Pod syntax OK") has been dropped from the module and has been included in
+B<podchecker> (the script). This allows users of B<Pod::Checker> to
+control completely the output behavior. Users of B<podchecker> (the script)
+get the well-known behavior.
+
+=cut
+
+#############################################################################
+
+#use diagnostics;
+use Carp qw(croak);
+use Exporter;
+use Pod::Parser;
+
+@ISA = qw(Pod::Parser);
+@EXPORT = qw(&podchecker);
+
+my %VALID_COMMANDS = (
+ 'pod' => 1,
+ 'cut' => 1,
+ 'head1' => 1,
+ 'head2' => 1,
+ 'head3' => 1,
+ 'head4' => 1,
+ 'over' => 1,
+ 'back' => 1,
+ 'item' => 1,
+ 'for' => 1,
+ 'begin' => 1,
+ 'end' => 1,
+ 'encoding' => 1,
+);
+
+my %VALID_SEQUENCES = (
+ 'I' => 1,
+ 'B' => 1,
+ 'S' => 1,
+ 'C' => 1,
+ 'L' => 1,
+ 'F' => 1,
+ 'X' => 1,
+ 'Z' => 1,
+ 'E' => 1,
+);
+
+# stolen from HTML::Entities
+my %ENTITIES = (
+ # Some normal chars that have special meaning in SGML context
+ amp => '&', # ampersand
+'gt' => '>', # greater than
+'lt' => '<', # less than
+ quot => '"', # double quote
+
+ # PUBLIC ISO 8879-1986//ENTITIES Added Latin 1//EN//HTML
+ AElig => 'Æ', # capital AE diphthong (ligature)
+ Aacute => 'Á', # capital A, acute accent
+ Acirc => 'Â', # capital A, circumflex accent
+ Agrave => 'À', # capital A, grave accent
+ Aring => 'Å', # capital A, ring
+ Atilde => 'Ã', # capital A, tilde
+ Auml => 'Ä', # capital A, dieresis or umlaut mark
+ Ccedil => 'Ç', # capital C, cedilla
+ ETH => 'Ð', # capital Eth, Icelandic
+ Eacute => 'É', # capital E, acute accent
+ Ecirc => 'Ê', # capital E, circumflex accent
+ Egrave => 'È', # capital E, grave accent
+ Euml => 'Ë', # capital E, dieresis or umlaut mark
+ Iacute => 'Í', # capital I, acute accent
+ Icirc => 'Î', # capital I, circumflex accent
+ Igrave => 'Ì', # capital I, grave accent
+ Iuml => 'Ï', # capital I, dieresis or umlaut mark
+ Ntilde => 'Ñ', # capital N, tilde
+ Oacute => 'Ó', # capital O, acute accent
+ Ocirc => 'Ô', # capital O, circumflex accent
+ Ograve => 'Ò', # capital O, grave accent
+ Oslash => 'Ø', # capital O, slash
+ Otilde => 'Õ', # capital O, tilde
+ Ouml => 'Ö', # capital O, dieresis or umlaut mark
+ THORN => 'Þ', # capital THORN, Icelandic
+ Uacute => 'Ú', # capital U, acute accent
+ Ucirc => 'Û', # capital U, circumflex accent
+ Ugrave => 'Ù', # capital U, grave accent
+ Uuml => 'Ü', # capital U, dieresis or umlaut mark
+ Yacute => 'Ý', # capital Y, acute accent
+ aacute => 'á', # small a, acute accent
+ acirc => 'â', # small a, circumflex accent
+ aelig => 'æ', # small ae diphthong (ligature)
+ agrave => 'à', # small a, grave accent
+ aring => 'å', # small a, ring
+ atilde => 'ã', # small a, tilde
+ auml => 'ä', # small a, dieresis or umlaut mark
+ ccedil => 'ç', # small c, cedilla
+ eacute => 'é', # small e, acute accent
+ ecirc => 'ê', # small e, circumflex accent
+ egrave => 'è', # small e, grave accent
+ eth => 'ð', # small eth, Icelandic
+ euml => 'ë', # small e, dieresis or umlaut mark
+ iacute => 'í', # small i, acute accent
+ icirc => 'î', # small i, circumflex accent
+ igrave => 'ì', # small i, grave accent
+ iuml => 'ï', # small i, dieresis or umlaut mark
+ ntilde => 'ñ', # small n, tilde
+ oacute => 'ó', # small o, acute accent
+ ocirc => 'ô', # small o, circumflex accent
+ ograve => 'ò', # small o, grave accent
+ oslash => 'ø', # small o, slash
+ otilde => 'õ', # small o, tilde
+ ouml => 'ö', # small o, dieresis or umlaut mark
+ szlig => 'ß', # small sharp s, German (sz ligature)
+ thorn => 'þ', # small thorn, Icelandic
+ uacute => 'ú', # small u, acute accent
+ ucirc => 'û', # small u, circumflex accent
+ ugrave => 'ù', # small u, grave accent
+ uuml => 'ü', # small u, dieresis or umlaut mark
+ yacute => 'ý', # small y, acute accent
+ yuml => 'ÿ', # small y, dieresis or umlaut mark
+
+ # Some extra Latin 1 chars that are listed in the HTML3.2 draft (21-May-96)
+ copy => '©', # copyright sign
+ reg => '®', # registered sign
+ nbsp => "\240", # non breaking space
+
+ # Additional ISO-8859/1 entities listed in rfc1866 (section 14)
+ iexcl => '¡',
+ cent => '¢',
+ pound => '£',
+ curren => '¤',
+ yen => '¥',
+ brvbar => '¦',
+ sect => '§',
+ uml => '¨',
+ ordf => 'ª',
+ laquo => '«',
+'not' => '¬', # not is a keyword in perl
+ shy => '',
+ macr => '¯',
+ deg => '°',
+ plusmn => '±',
+ sup1 => '¹',
+ sup2 => '²',
+ sup3 => '³',
+ acute => '´',
+ micro => 'µ',
+ para => '¶',
+ middot => '·',
+ cedil => '¸',
+ ordm => 'º',
+ raquo => '»',
+ frac14 => '¼',
+ frac12 => '½',
+ frac34 => '¾',
+ iquest => '¿',
+'times' => '×', # times is a keyword in perl
+ divide => '÷',
+
+# some POD special entities
+ verbar => '|',
+ sol => '/'
+);
+
+##---------------------------------------------------------------------------
+
+##---------------------------------
+## Function definitions begin here
+##---------------------------------
+
+sub podchecker {
+ my ($infile, $outfile, %options) = @_;
+ local $_;
+
+ ## Set defaults
+ $infile ||= \*STDIN;
+ $outfile ||= \*STDERR;
+
+ ## Now create a pod checker
+ my $checker = new Pod::Checker(%options);
+
+ ## Now check the pod document for errors
+ $checker->parse_from_file($infile, $outfile);
+
+ ## Return the number of errors found
+ return $checker->num_errors();
+}
+
+##---------------------------------------------------------------------------
+
+##-------------------------------
+## Method definitions begin here
+##-------------------------------
+
+##################################
+
+=over 4
+
+=item C<Pod::Checker-E<gt>new( %options )>
+
+Return a reference to a new Pod::Checker object that inherits from
+Pod::Parser and is used for calling the required methods later. The
+following options are recognized:
+
+C<-warnings =E<gt> num>
+ Print warnings if C<num> is true. The higher the value of C<num>,
+the more warnings are printed. Currently there are only levels 1 and 2.
+
+C<-quiet =E<gt> num>
+ If C<num> is true, do not print any errors/warnings. This is useful
+when Pod::Checker is used to munge POD code into plain text from within
+POD formatters.
+
+=cut
+
+## sub new {
+## my $this = shift;
+## my $class = ref($this) || $this;
+## my %params = @_;
+## my $self = {%params};
+## bless $self, $class;
+## $self->initialize();
+## return $self;
+## }
+
+sub initialize {
+ my $self = shift;
+ ## Initialize number of errors, and setup an error function to
+ ## increment this number and then print to the designated output.
+ $self->{_NUM_ERRORS} = 0;
+ $self->{_NUM_WARNINGS} = 0;
+ $self->{-quiet} ||= 0;
+ # set the error handling subroutine
+ $self->errorsub($self->{-quiet} ? sub { 1; } : 'poderror');
+ $self->{_commands} = 0; # total number of POD commands encountered
+ $self->{_list_stack} = []; # stack for nested lists
+ $self->{_have_begin} = ''; # stores =begin
+ $self->{_links} = []; # stack for internal hyperlinks
+ $self->{_nodes} = []; # stack for =head/=item nodes
+ $self->{_index} = []; # text in X<>
+ # print warnings?
+ $self->{-warnings} = 1 unless(defined $self->{-warnings});
+ $self->{_current_head1} = ''; # the current =head1 block
+ $self->parseopts(-process_cut_cmd => 1, -warnings => $self->{-warnings});
+}
+
+##################################
+
+=item C<$checker-E<gt>poderror( @args )>
+
+=item C<$checker-E<gt>poderror( {%opts}, @args )>
+
+Internal method for printing errors and warnings. If no options are
+given, simply prints "@_". The following options are recognized and used
+to form the output:
+
+ -msg
+
+A message to print prior to C<@args>.
+
+ -line
+
+The line number the error occurred in.
+
+ -file
+
+The file (name) the error occurred in.
+
+ -severity
+
+The error level, should be 'WARNING' or 'ERROR'.
+
+=cut
+
+# Invoked as $self->poderror( @args ), or $self->poderror( {%opts}, @args )
+sub poderror {
+ my $self = shift;
+ my %opts = (ref $_[0]) ? %{shift()} : ();
+
+ ## Retrieve options
+ chomp( my $msg = ($opts{-msg} || '')."@_" );
+ my $line = (exists $opts{-line}) ? " at line $opts{-line}" : '';
+ my $file = (exists $opts{-file}) ? " in file $opts{-file}" : '';
+ unless (exists $opts{-severity}) {
+ ## See if can find severity in message prefix
+ $opts{-severity} = $1 if ( $msg =~ s/^\**\s*([A-Z]{3,}):\s+// );
+ }
+ my $severity = (exists $opts{-severity}) ? "*** $opts{-severity}: " : '';
+
+ ## Increment error count and print message "
+ ++($self->{_NUM_ERRORS})
+ if(!%opts || ($opts{-severity} && $opts{-severity} eq 'ERROR'));
+ ++($self->{_NUM_WARNINGS})
+ if(!%opts || ($opts{-severity} && $opts{-severity} eq 'WARNING'));
+ unless($self->{-quiet}) {
+ my $out_fh = $self->output_handle() || \*STDERR;
+ print $out_fh ($severity, $msg, $line, $file, "\n")
+ if($self->{-warnings} || !%opts || $opts{-severity} ne 'WARNING');
+ }
+}
+
+##################################
+
+=item C<$checker-E<gt>num_errors()>
+
+Set (if argument specified) and retrieve the number of errors found.
+
+=cut
+
+sub num_errors {
+ return (@_ > 1) ? ($_[0]->{_NUM_ERRORS} = $_[1]) : $_[0]->{_NUM_ERRORS};
+}
+
+##################################
+
+=item C<$checker-E<gt>num_warnings()>
+
+Set (if argument specified) and retrieve the number of warnings found.
+
+=cut
+
+sub num_warnings {
+ return (@_ > 1) ? ($_[0]->{_NUM_WARNINGS} = $_[1]) : $_[0]->{_NUM_WARNINGS};
+}
+
+##################################
+
+=item C<$checker-E<gt>name()>
+
+Set (if argument specified) and retrieve the canonical name of POD as
+found in the C<=head1 NAME> section.
+
+=cut
+
+sub name {
+ return (@_ > 1 && $_[1]) ?
+ ($_[0]->{-name} = $_[1]) : $_[0]->{-name};
+}
+
+##################################
+
+=item C<$checker-E<gt>node()>
+
+Add (if argument specified) and retrieve the nodes (as defined by C<=headX>
+and C<=item>) of the current POD. The nodes are returned in the order of
+their occurrence. They consist of plain text, each piece of whitespace is
+collapsed to a single blank.
+
+=cut
+
+sub node {
+ my ($self,$text) = @_;
+ if(defined $text) {
+ $text =~ s/\s+$//s; # strip trailing whitespace
+ $text =~ s/\s+/ /gs; # collapse whitespace
+ # add node, order important!
+ push(@{$self->{_nodes}}, $text);
+ # keep also a uniqueness counter
+ $self->{_unique_nodes}->{$text}++ if($text !~ /^\s*$/s);
+ return $text;
+ }
+ @{$self->{_nodes}};
+}
+
+##################################
+
+=item C<$checker-E<gt>idx()>
+
+Add (if argument specified) and retrieve the index entries (as defined by
+C<XE<lt>E<gt>>) of the current POD. They consist of plain text, each piece
+of whitespace is collapsed to a single blank.
+
+=cut
+
+# set/return index entries of current POD
+sub idx {
+ my ($self,$text) = @_;
+ if(defined $text) {
+ $text =~ s/\s+$//s; # strip trailing whitespace
+ $text =~ s/\s+/ /gs; # collapse whitespace
+ # add node, order important!
+ push(@{$self->{_index}}, $text);
+ # keep also a uniqueness counter
+ $self->{_unique_nodes}->{$text}++ if($text !~ /^\s*$/s);
+ return $text;
+ }
+ @{$self->{_index}};
+}
+
+##################################
+
+=item C<$checker-E<gt>hyperlink()>
+
+Add (if argument specified) and retrieve the hyperlinks (as defined by
+C<LE<lt>E<gt>>) of the current POD. They consist of a 2-item array: line
+number and C<Pod::Hyperlink> object.
+
+=back
+
+=cut
+
+# set/return hyperlinks of the current POD
+sub hyperlink {
+ my $self = shift;
+ if($_[0]) {
+ push(@{$self->{_links}}, $_[0]);
+ return $_[0];
+ }
+ @{$self->{_links}};
+}
+
+## overrides for Pod::Parser
+
+sub end_pod {
+ ## Do some final checks and
+ ## print the number of errors found
+ my $self = shift;
+ my $infile = $self->input_file();
+
+ if(@{$self->{_list_stack}}) {
+ my $list;
+ while(($list = $self->_close_list('EOF',$infile)) &&
+ $list->indent() ne 'auto') {
+ $self->poderror({ -line => 'EOF', -file => $infile,
+ -severity => 'ERROR', -msg => '=over on line ' .
+ $list->start() . ' without closing =back' });
+ }
+ }
+
+ # check validity of document internal hyperlinks
+ # first build the node names from the paragraph text
+ my %nodes;
+ foreach($self->node()) {
+ $nodes{$_} = 1;
+ if(/^(\S+)\s+\S/) {
+ # we have more than one word. Use the first as a node, too.
+ # This is used heavily in perlfunc.pod
+ $nodes{$1} ||= 2; # derived node
+ }
+ }
+ foreach($self->idx()) {
+ $nodes{$_} = 3; # index node
+ }
+ foreach($self->hyperlink()) {
+ my ($line,$link) = @$_;
+ # _TODO_ what if there is a link to the page itself by the name,
+ # e.g. in Tk::Pod : L<Tk::Pod/"DESCRIPTION">
+ if($link->node() && !$link->page() && $link->type() ne 'hyperlink') {
+ my $node = $self->_check_ptree($self->parse_text($link->node(),
+ $line), $line, $infile, 'L');
+ if($node && !$nodes{$node}) {
+ $self->poderror({ -line => $line || '', -file => $infile,
+ -severity => 'ERROR',
+ -msg => "unresolved internal link '$node'"});
+ }
+ }
+ }
+
+ # check the internal nodes for uniqueness. This pertains to
+ # =headX, =item and X<...>
+ if($self->{-warnings} && $self->{-warnings}>1) {
+ foreach(grep($self->{_unique_nodes}->{$_} > 1,
+ keys %{$self->{_unique_nodes}})) {
+ $self->poderror({ -line => '-', -file => $infile,
+ -severity => 'WARNING',
+ -msg => "multiple occurrence of link target '$_'"});
+ }
+ }
+
+ # no POD found here
+ $self->num_errors(-1) if($self->{_commands} == 0);
+}
+
+# check a POD command directive
+sub command {
+ my ($self, $cmd, $paragraph, $line_num, $pod_para) = @_;
+ my ($file, $line) = $pod_para->file_line;
+ ## Check the command syntax
+ my $arg; # this will hold the command argument
+ if (! $VALID_COMMANDS{$cmd}) {
+ $self->poderror({ -line => $line, -file => $file, -severity => 'ERROR',
+ -msg => "Unknown command '$cmd'" });
+ }
+ else { # found a valid command
+ $self->{_commands}++; # delete this line if below is enabled again
+
+ $self->_commands_in_paragraphs($paragraph, $pod_para);
+
+ ##### following check disabled due to strong request
+ #if(!$self->{_commands}++ && $cmd !~ /^head/) {
+ # $self->poderror({ -line => $line, -file => $file,
+ # -severity => 'WARNING',
+ # -msg => "file does not start with =head" });
+ #}
+
+ # check syntax of particular command
+ if($cmd eq 'over') {
+ # check for argument
+ $arg = $self->interpolate_and_check($paragraph, $line,$file);
+ my $indent = 4; # default
+ if($arg && $arg =~ /^\s*(\d+)\s*$/) {
+ $indent = $1;
+ }
+ # start a new list
+ $self->_open_list($indent,$line,$file);
+ }
+ elsif($cmd eq 'item') {
+ # are we in a list?
+ unless(@{$self->{_list_stack}}) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => '=item without previous =over' });
+ # auto-open in case we encounter many more
+ $self->_open_list('auto',$line,$file);
+ }
+ my $list = $self->{_list_stack}->[0];
+ # check whether the previous item had some contents
+ if(defined $self->{_list_item_contents} &&
+ $self->{_list_item_contents} == 0) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => 'previous =item has no contents' });
+ }
+ if($list->{_has_par}) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => 'preceding non-item paragraph(s)' });
+ delete $list->{_has_par};
+ }
+ # check for argument
+ $arg = $self->interpolate_and_check($paragraph, $line, $file);
+ if($arg && $arg =~ /(\S+)/) {
+ $arg =~ s/[\s\n]+$//;
+ my $type;
+ if($arg =~ /^[*]\s*(\S*.*)/) {
+ $type = 'bullet';
+ $self->{_list_item_contents} = $1 ? 1 : 0;
+ $arg = $1;
+ }
+ elsif($arg =~ /^\d+\.?\s+(\S*)/) {
+ $type = 'number';
+ $self->{_list_item_contents} = $1 ? 1 : 0;
+ $arg = $1;
+ }
+ else {
+ $type = 'definition';
+ $self->{_list_item_contents} = 1;
+ }
+ my $first = $list->type();
+ if($first && $first ne $type) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => "=item type mismatch ('$first' vs. '$type')"});
+ }
+ else { # first item
+ $list->type($type);
+ }
+ }
+ else {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => 'No argument for =item' });
+ $arg = ' '; # empty
+ $self->{_list_item_contents} = 0;
+ }
+ # add this item
+ $list->item($arg);
+ # remember this node
+ $self->node($arg);
+ }
+ elsif($cmd eq 'back') {
+ # check if we have an open list
+ unless(@{$self->{_list_stack}}) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => '=back without previous =over' });
+ }
+ else {
+ # check for spurious characters
+ $arg = $self->interpolate_and_check($paragraph, $line,$file);
+ if($arg && $arg =~ /\S/) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => 'Spurious character(s) after =back' });
+ }
+ # close list
+ my $list = $self->_close_list($line,$file);
+ # check for empty lists
+ if(!$list->item() && $self->{-warnings}) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => 'No items in =over (at line ' .
+ $list->start() . ') / =back list'});
+ }
+ }
+ }
+ elsif($cmd =~ /^head(\d+)/) {
+ my $hnum = $1;
+ $self->{"_have_head_$hnum"}++; # count head types
+ if($hnum > 1 && !$self->{'_have_head_'.($hnum -1)}) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => "=head$hnum without preceding higher level"});
+ }
+ # check whether the previous =head section had some contents
+ if(defined $self->{_commands_in_head} &&
+ $self->{_commands_in_head} == 0 &&
+ defined $self->{_last_head} &&
+ $self->{_last_head} >= $hnum) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => 'empty section in previous paragraph'});
+ }
+ $self->{_commands_in_head} = -1;
+ $self->{_last_head} = $hnum;
+ # check if there is an open list
+ if(@{$self->{_list_stack}}) {
+ my $list;
+ while(($list = $self->_close_list($line,$file)) &&
+ $list->indent() ne 'auto') {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => '=over on line '. $list->start() .
+ " without closing =back (at $cmd)" });
+ }
+ }
+ # remember this node
+ $arg = $self->interpolate_and_check($paragraph, $line,$file);
+ $arg =~ s/[\s\n]+$//s;
+ $self->node($arg);
+ unless(length($arg)) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => "empty =$cmd"});
+ }
+ if($cmd eq 'head1') {
+ $self->{_current_head1} = $arg;
+ } else {
+ $self->{_current_head1} = '';
+ }
+ }
+ elsif($cmd eq 'begin') {
+ if($self->{_have_begin}) {
+ # already have a begin
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => q{Nested =begin's (first at line } .
+ $self->{_have_begin} . ')'});
+ }
+ else {
+ # check for argument
+ $arg = $self->interpolate_and_check($paragraph, $line,$file);
+ unless($arg && $arg =~ /(\S+)/) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => 'No argument for =begin'});
+ }
+ # remember the =begin
+ $self->{_have_begin} = "$line:$1";
+ }
+ }
+ elsif($cmd eq 'end') {
+ if($self->{_have_begin}) {
+ # close the existing =begin
+ $self->{_have_begin} = '';
+ # check for spurious characters
+ $arg = $self->interpolate_and_check($paragraph, $line,$file);
+ # the closing argument is optional
+ #if($arg && $arg =~ /\S/) {
+ # $self->poderror({ -line => $line, -file => $file,
+ # -severity => 'WARNING',
+ # -msg => "Spurious character(s) after =end" });
+ #}
+ }
+ else {
+ # don't have a matching =begin
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => '=end without =begin' });
+ }
+ }
+ elsif($cmd eq 'for') {
+ unless($paragraph =~ /\s*(\S+)\s*/) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => '=for without formatter specification' });
+ }
+ $arg = ''; # do not expand paragraph below
+ }
+ elsif($cmd =~ /^(pod|cut)$/) {
+ # check for argument
+ $arg = $self->interpolate_and_check($paragraph, $line,$file);
+ if($arg && $arg =~ /(\S+)/) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => "Spurious text after =$cmd"});
+ }
+ if($cmd eq 'cut' && (!$self->{_PREVIOUS} || $self->{_PREVIOUS} eq 'cut')) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => "Spurious =cut command"});
+ }
+ if($cmd eq 'pod' && $self->{_PREVIOUS} && $self->{_PREVIOUS} ne 'cut') {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => "Spurious =pod command"});
+ }
+ }
+ $self->{_commands_in_head}++;
+ ## Check the interior sequences in the command-text
+ $self->interpolate_and_check($paragraph, $line,$file)
+ unless(defined $arg);
+ }
+}
+
+sub _open_list
+{
+ my ($self,$indent,$line,$file) = @_;
+ my $list = Pod::List->new(
+ -indent => $indent,
+ -start => $line,
+ -file => $file);
+ unshift(@{$self->{_list_stack}}, $list);
+ undef $self->{_list_item_contents};
+ $list;
+}
+
+sub _close_list
+{
+ my ($self,$line,$file) = @_;
+ my $list = shift(@{$self->{_list_stack}});
+ if(defined $self->{_list_item_contents} &&
+ $self->{_list_item_contents} == 0) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => 'previous =item has no contents' });
+ }
+ undef $self->{_list_item_contents};
+ $list;
+}
+
+# process a block of some text
+sub interpolate_and_check {
+ my ($self, $paragraph, $line, $file) = @_;
+ ## Check the interior sequences in the command-text
+ # and return the text
+ $self->_check_ptree(
+ $self->parse_text($paragraph,$line), $line, $file, '');
+}
+
+sub _check_ptree {
+ my ($self,$ptree,$line,$file,$nestlist) = @_;
+ local($_);
+ my $text = '';
+ # process each node in the parse tree
+ foreach(@$ptree) {
+ # regular text chunk
+ unless(ref) {
+ # count the unescaped angle brackets
+ # complain only when warning level is greater than 1
+ if($self->{-warnings} && $self->{-warnings}>1) {
+ my $count;
+ if($count = tr/<>/<>/) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => "$count unescaped <> in paragraph" });
+ }
+ }
+ $text .= $_;
+ next;
+ }
+ # have an interior sequence
+ my $cmd = $_->cmd_name();
+ my $contents = $_->parse_tree();
+ ($file,$line) = $_->file_line();
+ # check for valid tag
+ if (! $VALID_SEQUENCES{$cmd}) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => qq(Unknown interior-sequence '$cmd')});
+ # expand it anyway
+ $text .= $self->_check_ptree($contents, $line, $file, "$nestlist$cmd");
+ next;
+ }
+ if(index($nestlist, $cmd) != -1) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => "nested commands $cmd<...$cmd<...>...>"});
+ # _TODO_ should we add the contents anyway?
+ # expand it anyway, see below
+ }
+ if($cmd eq 'E') {
+ # preserve entities
+ if(@$contents > 1 || ref $$contents[0] || $$contents[0] !~ /^\w+$/) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => 'garbled entity ' . $_->raw_text()});
+ next;
+ }
+ my $ent = $$contents[0];
+ my $val;
+ if($ent =~ /^0x[0-9a-f]+$/i) {
+ # hexadec entity
+ $val = hex($ent);
+ }
+ elsif($ent =~ /^0\d+$/) {
+ # octal
+ $val = oct($ent);
+ }
+ elsif($ent =~ /^\d+$/) {
+ # numeric entity
+ $val = $ent;
+ }
+ if(defined $val) {
+ if($val>0 && $val<256) {
+ $text .= chr($val);
+ }
+ else {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => 'Entity number out of range ' . $_->raw_text()});
+ }
+ }
+ elsif($ENTITIES{$ent}) {
+ # known ISO entity
+ $text .= $ENTITIES{$ent};
+ }
+ else {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => 'Unknown entity ' . $_->raw_text()});
+ $text .= "E<$ent>";
+ }
+ }
+ elsif($cmd eq 'L') {
+ # try to parse the hyperlink
+ my $link = Pod::Hyperlink->new($contents->raw_text());
+ unless(defined $link) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => 'malformed link ' . $_->raw_text() ." : $@"});
+ next;
+ }
+ $link->line($line); # remember line
+ if($self->{-warnings}) {
+ foreach my $w ($link->warning()) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => $w });
+ }
+ }
+ # check the link text
+ $text .= $self->_check_ptree($self->parse_text($link->text(),
+ $line), $line, $file, "$nestlist$cmd");
+ # remember link
+ $self->hyperlink([$line,$link]);
+ }
+ elsif($cmd =~ /[BCFIS]/) {
+ # add the guts
+ $text .= $self->_check_ptree($contents, $line, $file, "$nestlist$cmd");
+ }
+ elsif($cmd eq 'Z') {
+ if(length($contents->raw_text())) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => 'Nonempty Z<>'});
+ }
+ }
+ elsif($cmd eq 'X') {
+ my $idx = $self->_check_ptree($contents, $line, $file, "$nestlist$cmd");
+ if($idx =~ /^\s*$/s) {
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => 'Empty X<>'});
+ }
+ else {
+ # remember this node
+ $self->idx($idx);
+ }
+ }
+ else {
+ # not reached
+ croak 'internal error';
+ }
+ }
+ $text;
+}
+
+# process a block of verbatim text
+sub verbatim {
+ ## Nothing particular to check
+ my ($self, $paragraph, $line_num, $pod_para) = @_;
+
+ $self->_preproc_par($paragraph);
+ $self->_commands_in_paragraphs($paragraph, $pod_para);
+
+ if($self->{_current_head1} eq 'NAME') {
+ my ($file, $line) = $pod_para->file_line;
+ $self->poderror({ -line => $line, -file => $file,
+ -severity => 'WARNING',
+ -msg => 'Verbatim paragraph in NAME section' });
+ }
+}
+
+# process a block of regular text
+sub textblock {
+ my ($self, $paragraph, $line_num, $pod_para) = @_;
+ my ($file, $line) = $pod_para->file_line;
+
+ $self->_preproc_par($paragraph);
+ $self->_commands_in_paragraphs($paragraph, $pod_para);
+
+ # skip this paragraph if in a =begin block
+ unless($self->{_have_begin}) {
+ my $block = $self->interpolate_and_check($paragraph, $line,$file);
+ if($self->{_current_head1} eq 'NAME') {
+ if($block =~ /^\s*(\S+?)\s*[,-]/) {
+ # this is the canonical name
+ $self->{-name} = $1 unless(defined $self->{-name});
+ }
+ }
+ }
+}
+
+sub _preproc_par
+{
+ my $self = shift;
+ $_[0] =~ s/[\s\n]+$//;
+ if($_[0]) {
+ $self->{_commands_in_head}++;
+ $self->{_list_item_contents}++ if(defined $self->{_list_item_contents});
+ if(@{$self->{_list_stack}} && !$self->{_list_stack}->[0]->item()) {
+ $self->{_list_stack}->[0]->{_has_par} = 1;
+ }
+ }
+}
+
+# look for =foo commands at the start of a line within a paragraph, as for
+# instance the following which prints as "* one =item two".
+#
+# =item one
+# =item two
+#
+# Examples of =foo written in docs are expected to be indented in a verbatim
+# or marked up C<=foo> so won't be caught. A double-angle C<< =foo >> could
+# have the =foo at the start of a line, but that should be unlikely and is
+# easily enough dealt with by not putting a newline after the C<<.
+#
+sub _commands_in_paragraphs {
+ my ($self, $str, $pod_para) = @_;
+ while ($str =~ /[^\n]\n=([a-z][a-z0-9]+)/sg) {
+ my $cmd = $1;
+ my $pos = pos($str);
+ if ($VALID_COMMANDS{$cmd}) {
+ my ($file, $line) = $pod_para->file_line;
+ my $part = substr($str, 0, $pos);
+ $line += ($part =~ tr/\n//); # count of newlines
+
+ $self->poderror
+ ({ -line => $line, -file => $file,
+ -severity => 'ERROR',
+ -msg => "Apparent command =$cmd not preceded by blank line"});
+ }
+ }
+}
+
+1;
+
+__END__
+
+=head1 AUTHOR
+
+Please report bugs using L<http://rt.cpan.org>.
+
+Brad Appleton E<lt>bradapp@enteract.comE<gt> (initial version),
+Marek Rouchal E<lt>marekr@cpan.orgE<gt>
+
+Based on code for B<Pod::Text::pod2text()> written by
+Tom Christiansen E<lt>tchrist@mox.perl.comE<gt>
+
+B<Pod::Checker> is part of the Pod-Checker distribution, and is based on
+L<Pod::Parser>.
+
+=cut
+