<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">#############################################################################
# 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 vars qw($VERSION);
$VERSION = 1.2;  ## 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;

  $syntax_okay = podchecker($filepath, $outputpath, %options);

  my $checker = new Pod::Checker %options;
  $checker-&gt;parse_from_file($filepath, \*STDERR);

=head1 OPTIONS/ARGUMENTS

C&lt;$filepath&gt; is the input POD to read and C&lt;$outputpath&gt; 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&lt;\*STDIN&gt;, and
the output-file defaults to C&lt;\*STDERR&gt;.

=head2 podchecker()

This function can take a hash of options:

=over 4

=item B&lt;-warnings&gt; =E&lt;gt&gt; I&lt;val&gt;

Turn warnings on/off. I&lt;val&gt; is usually 1 for on, but higher values
trigger additional warnings. See L&lt;"Warnings"&gt;.

=back

=head1 DESCRIPTION

B&lt;podchecker&gt; will perform syntax checking of Perl5 POD format documentation.

I&lt;NOTE THAT THIS MODULE IS CURRENTLY IN THE BETA STAGE!&gt;

It is hoped that curious/ambitious user will help flesh out and add the
additional features they wish to see in B&lt;Pod::Checker&gt; and B&lt;podchecker&gt;
and verify that the checks are consistent with L&lt;perlpod&gt;.

The following checks are currently preformed:

=over 4

=item *

Unknown '=xxxx' commands, unknown 'XE&lt;lt&gt;...E&lt;gt&gt;' interior-sequences,
and unterminated interior sequences.

=item *

Check for proper balancing of C&lt;=begin&gt; and C&lt;=end&gt;. 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&lt;=over&gt;, C&lt;=item&gt; and C&lt;=back&gt;.

=item *

Check for same nested interior-sequences (e.g. 
C&lt;LE&lt;lt&gt;...LE&lt;lt&gt;...E&lt;gt&gt;...E&lt;gt&gt;&gt;).

=item *

Check for malformed or nonexisting entities C&lt;EE&lt;lt&gt;...E&lt;gt&gt;&gt;.

=item *

Check for correct syntax of hyperlinks C&lt;LE&lt;lt&gt;...E&lt;gt&gt;&gt;. See L&lt;perlpod&gt;
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&lt;=head1&gt; or C&lt;=head2&gt;) without any text? That ain't no
heading!

=item * =over on line I&lt;N&gt; without closing =back

The C&lt;=over&gt; command does not have a corresponding C&lt;=back&gt; before the
next heading (C&lt;=head1&gt; or C&lt;=head2&gt;) or the end of the file.

=item * =item without previous =over

=item * =back without previous =over

An C&lt;=item&gt; or C&lt;=back&gt; command has been found outside a
C&lt;=over&gt;/C&lt;=back&gt; block.

=item * No argument for =begin

A C&lt;=begin&gt; command was found that is not followed by the formatter
specification.

=item * =end without =begin

A standalone C&lt;=end&gt; command was found.

=item * Nested =begin's

There were at least two consecutive C&lt;=begin&gt; commands without
the corresponding C&lt;=end&gt;. Only one C&lt;=begin&gt; may be active at
a time.

=item * =for without formatter specification

There is no specification of the formatter after the C&lt;=for&gt; command.

=item * unresolved internal link I&lt;NAME&gt;

The given link to I&lt;NAME&gt; does not have a matching node in the current
POD. This also happend when a single word node name is not enclosed in
C&lt;""&gt;.

=item * Unknown command "I&lt;CMD&gt;"

An invalid POD command has been found. Valid are C&lt;=head1&gt;, C&lt;=head2&gt;,
C&lt;=over&gt;, C&lt;=item&gt;, C&lt;=back&gt;, C&lt;=begin&gt;, C&lt;=end&gt;, C&lt;=for&gt;, C&lt;=pod&gt;,
C&lt;=cut&gt;

=item * Unknown interior-sequence "I&lt;SEQ&gt;"

An invalid markup command has been encountered. Valid are:
C&lt;BE&lt;lt&gt;E&lt;gt&gt;&gt;, C&lt;CE&lt;lt&gt;E&lt;gt&gt;&gt;, C&lt;EE&lt;lt&gt;E&lt;gt&gt;&gt;, C&lt;FE&lt;lt&gt;E&lt;gt&gt;&gt;, 
C&lt;IE&lt;lt&gt;E&lt;gt&gt;&gt;, C&lt;LE&lt;lt&gt;E&lt;gt&gt;&gt;, C&lt;SE&lt;lt&gt;E&lt;gt&gt;&gt;, C&lt;XE&lt;lt&gt;E&lt;gt&gt;&gt;, 
C&lt;ZE&lt;lt&gt;E&lt;gt&gt;&gt;

=item * nested commands I&lt;CMD&gt;E&lt;lt&gt;...I&lt;CMD&gt;E&lt;lt&gt;...E&lt;gt&gt;...E&lt;gt&gt;

Two nested identical markup commands have been found. Generally this
does not make sense.

=item * garbled entity I&lt;STRING&gt;

The I&lt;STRING&gt; 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;lt&gt;E&lt;gt&gt;

The link found cannot be parsed because it does not conform to the
syntax described in L&lt;perlpod&gt;.

=item * nonempty ZE&lt;lt&gt;E&lt;gt&gt;

The C&lt;ZE&lt;lt&gt;E&lt;gt&gt;&gt; sequence is supposed to be empty.

=item * empty XE&lt;lt&gt;E&lt;gt&gt;

The index entry specified contains nothing but whitespace.

=item * Spurious text after =pod / =cut

The commands C&lt;=pod&gt; and C&lt;=cut&gt; do not take any arguments.

=item * Spurious character(s) after =back

The C&lt;=back&gt; command does not take any arguments.

=back

=head2 Warnings

These may not necessarily cause trouble, but indicate mediocre style.

=over 4

=item * multiple occurence of link target I&lt;name&gt;

The POD file has some C&lt;=item&gt; and/or C&lt;=head&gt; commands that have
the same text. Potential hyperlinks to such a text cannot be unique then.

=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&lt;vi&gt; users switch on the B&lt;list&gt;
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&lt;=item&gt; 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&lt;=over&gt; starts with a text or verbatim paragraph,
but continues with C&lt;=item&gt;s. Move the non-item paragraph out of the
C&lt;=over&gt;/C&lt;=back&gt; block.

=item * =item type mismatch (I&lt;one&gt; vs. I&lt;two&gt;)

A list started with e.g. a bulletted C&lt;=item&gt; and continued with a
numbered one. This is obviously inconsistent. For most translators the
type of the I&lt;first&gt; C&lt;=item&gt; determines the type of the list.

=item * I&lt;N&gt; unescaped C&lt;E&lt;lt&gt;E&lt;gt&gt;&gt; in paragraph

Angle brackets not written as C&lt;E&lt;lt&gt;ltE&lt;gt&gt;&gt; and C&lt;E&lt;lt&gt;gtE&lt;gt&gt;&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&lt;verbar&gt; and C&lt;sol&gt;.

=item * No items in =over

The list opened with C&lt;=over&gt; does not contain any items.

=item * No argument for =item

C&lt;=item&gt; without any parameters is deprecated. It should either be followed
by C&lt;*&gt; 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&lt;=head&gt; command) does not contain
any text. This usually indicates that something is missing. Note: A 
C&lt;=head1&gt; followed immediately by C&lt;=head2&gt; does not trigger this warning.

=item * Verbatim paragraph in NAME section

The NAME section (C&lt;=head1 NAME&gt;) 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.

=back

=head2 Hyperlinks

There are some warnings wrt. 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;lt&gt;...E&lt;gt&gt;.

=item * (section) in '$page' deprecated

There is a section detected in the page name of LE&lt;lt&gt;...E&lt;gt&gt;, e.g.
C&lt;LE&lt;gt&gt;passwd(2)E&lt;gt&gt;&gt;. POD hyperlinks may point to POD documents only.
Please write C&lt;CE&lt;lt&gt;passwd(2)E&lt;gt&gt;&gt; instead. Some formatters are able
to expand this to appropriate code. For links to (builtin) functions,
please say C&lt;LE&lt;lt&gt;perlfunc/mkdirE&lt;gt&gt;&gt;, without ().

=item * alternative text/node '%s' contains non-escaped | or /

The characters C&lt;|&gt; and C&lt;/&gt; are special in the LE&lt;lt&gt;...E&lt;gt&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&lt;sol&gt;
  |     E&lt;verbar&gt;

=back

=head1 RETURN VALUE

B&lt;podchecker&gt; returns the number of POD syntax errors found or -1 if
there were no POD commands at all found in the file.

=head1 EXAMPLES

I&lt;[T.B.D.]&gt;

=head1 INTERFACE

While checking, this module collects document properties, e.g. the nodes
for hyperlinks (C&lt;=headX&gt;, C&lt;=item&gt;) and index entries (C&lt;XE&lt;lt&gt;E&lt;gt&gt;&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.

=cut

#############################################################################

use strict;
#use diagnostics;
use Carp;
use Exporter;
use Pod::Parser;

use vars qw(@ISA @EXPORT);
@ISA = qw(Pod::Parser);
@EXPORT = qw(&amp;podchecker);

use vars qw(%VALID_COMMANDS %VALID_SEQUENCES);

my %VALID_COMMANDS = (
    'pod'    =&gt;  1,
    'cut'    =&gt;  1,
    'head1'  =&gt;  1,
    'head2'  =&gt;  1,
    'over'   =&gt;  1,
    'back'   =&gt;  1,
    'item'   =&gt;  1,
    'for'    =&gt;  1,
    'begin'  =&gt;  1,
    'end'    =&gt;  1,
);

my %VALID_SEQUENCES = (
    'I'  =&gt;  1,
    'B'  =&gt;  1,
    'S'  =&gt;  1,
    'C'  =&gt;  1,
    'L'  =&gt;  1,
    'F'  =&gt;  1,
    'X'  =&gt;  1,
    'Z'  =&gt;  1,
    'E'  =&gt;  1,
);

# stolen from HTML::Entities
my %ENTITIES = (
 # Some normal chars that have special meaning in SGML context
 amp    =&gt; '&amp;',  # ampersand 
'gt'    =&gt; '&gt;',  # greater than
'lt'    =&gt; '&lt;',  # less than
 quot   =&gt; '"',  # double quote

 # PUBLIC ISO 8879-1986//ENTITIES Added Latin 1//EN//HTML
 AElig	=&gt; 'Æ',  # capital AE diphthong (ligature)
 Aacute	=&gt; 'Á',  # capital A, acute accent
 Acirc	=&gt; 'Â',  # capital A, circumflex accent
 Agrave	=&gt; 'À',  # capital A, grave accent
 Aring	=&gt; 'Å',  # capital A, ring
 Atilde	=&gt; 'Ã',  # capital A, tilde
 Auml	=&gt; 'Ä',  # capital A, dieresis or umlaut mark
 Ccedil	=&gt; 'Ç',  # capital C, cedilla
 ETH	=&gt; 'Ð',  # capital Eth, Icelandic
 Eacute	=&gt; 'É',  # capital E, acute accent
 Ecirc	=&gt; 'Ê',  # capital E, circumflex accent
 Egrave	=&gt; 'È',  # capital E, grave accent
 Euml	=&gt; 'Ë',  # capital E, dieresis or umlaut mark
 Iacute	=&gt; 'Í',  # capital I, acute accent
 Icirc	=&gt; 'Î',  # capital I, circumflex accent
 Igrave	=&gt; 'Ì',  # capital I, grave accent
 Iuml	=&gt; 'Ï',  # capital I, dieresis or umlaut mark
 Ntilde	=&gt; 'Ñ',  # capital N, tilde
 Oacute	=&gt; 'Ó',  # capital O, acute accent
 Ocirc	=&gt; 'Ô',  # capital O, circumflex accent
 Ograve	=&gt; 'Ò',  # capital O, grave accent
 Oslash	=&gt; 'Ø',  # capital O, slash
 Otilde	=&gt; 'Õ',  # capital O, tilde
 Ouml	=&gt; 'Ö',  # capital O, dieresis or umlaut mark
 THORN	=&gt; 'Þ',  # capital THORN, Icelandic
 Uacute	=&gt; 'Ú',  # capital U, acute accent
 Ucirc	=&gt; 'Û',  # capital U, circumflex accent
 Ugrave	=&gt; 'Ù',  # capital U, grave accent
 Uuml	=&gt; 'Ü',  # capital U, dieresis or umlaut mark
 Yacute	=&gt; 'Ý',  # capital Y, acute accent
 aacute	=&gt; 'á',  # small a, acute accent
 acirc	=&gt; 'â',  # small a, circumflex accent
 aelig	=&gt; 'æ',  # small ae diphthong (ligature)
 agrave	=&gt; 'à',  # small a, grave accent
 aring	=&gt; 'å',  # small a, ring
 atilde	=&gt; 'ã',  # small a, tilde
 auml	=&gt; 'ä',  # small a, dieresis or umlaut mark
 ccedil	=&gt; 'ç',  # small c, cedilla
 eacute	=&gt; 'é',  # small e, acute accent
 ecirc	=&gt; 'ê',  # small e, circumflex accent
 egrave	=&gt; 'è',  # small e, grave accent
 eth	=&gt; 'ð',  # small eth, Icelandic
 euml	=&gt; 'ë',  # small e, dieresis or umlaut mark
 iacute	=&gt; 'í',  # small i, acute accent
 icirc	=&gt; 'î',  # small i, circumflex accent
 igrave	=&gt; 'ì',  # small i, grave accent
 iuml	=&gt; 'ï',  # small i, dieresis or umlaut mark
 ntilde	=&gt; 'ñ',  # small n, tilde
 oacute	=&gt; 'ó',  # small o, acute accent
 ocirc	=&gt; 'ô',  # small o, circumflex accent
 ograve	=&gt; 'ò',  # small o, grave accent
 oslash	=&gt; 'ø',  # small o, slash
 otilde	=&gt; 'õ',  # small o, tilde
 ouml	=&gt; 'ö',  # small o, dieresis or umlaut mark
 szlig	=&gt; 'ß',  # small sharp s, German (sz ligature)
 thorn	=&gt; 'þ',  # small thorn, Icelandic
 uacute	=&gt; 'ú',  # small u, acute accent
 ucirc	=&gt; 'û',  # small u, circumflex accent
 ugrave	=&gt; 'ù',  # small u, grave accent
 uuml	=&gt; 'ü',  # small u, dieresis or umlaut mark
 yacute	=&gt; 'ý',  # small y, acute accent
 yuml	=&gt; 'ÿ',  # small y, dieresis or umlaut mark

 # Some extra Latin 1 chars that are listed in the HTML3.2 draft (21-May-96)
 copy   =&gt; '©',  # copyright sign
 reg    =&gt; '®',  # registered sign
 nbsp   =&gt; "\240", # non breaking space

 # Additional ISO-8859/1 entities listed in rfc1866 (section 14)
 iexcl  =&gt; '¡',
 cent   =&gt; '¢',
 pound  =&gt; '£',
 curren =&gt; '¤',
 yen    =&gt; '¥',
 brvbar =&gt; '¦',
 sect   =&gt; '§',
 uml    =&gt; '¨',
 ordf   =&gt; 'ª',
 laquo  =&gt; '«',
'not'   =&gt; '¬',    # not is a keyword in perl
 shy    =&gt; '­',
 macr   =&gt; '¯',
 deg    =&gt; '°',
 plusmn =&gt; '±',
 sup1   =&gt; '¹',
 sup2   =&gt; '²',
 sup3   =&gt; '³',
 acute  =&gt; '´',
 micro  =&gt; 'µ',
 para   =&gt; '¶',
 middot =&gt; '·',
 cedil  =&gt; '¸',
 ordm   =&gt; 'º',
 raquo  =&gt; '»',
 frac14 =&gt; '¼',
 frac12 =&gt; '½',
 frac34 =&gt; '¾',
 iquest =&gt; '¿',
'times' =&gt; '×',    # times is a keyword in perl
 divide =&gt; '÷',

# some POD special entities
 verbar =&gt; '|',
 sol =&gt; '/'
);

##---------------------------------------------------------------------------

##---------------------------------
## 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-&gt;parse_from_file($infile, $outfile);

    ## Return the number of errors found
    return $checker-&gt;num_errors();
}

##---------------------------------------------------------------------------

##-------------------------------
## Method definitions begin here
##-------------------------------

##################################

=over 4

=item C&lt;Pod::Checker-E&lt;gt&gt;new( %options )&gt;

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&lt;-warnings =E&lt;gt&gt; num&gt;
  Print warnings if C&lt;num&gt; is true. The higher the value of C&lt;num&gt;,
the more warnings are printed. Currently there are only levels 1 and 2.

C&lt;-quiet =E&lt;gt&gt; num&gt;
  If C&lt;num&gt; 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-&gt;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-&gt;{_NUM_ERRORS} = 0;
    $self-&gt;{-quiet} ||= 0;
    # set the error handling subroutine
    $self-&gt;errorsub($self-&gt;{-quiet} ? sub { 1; } : 'poderror');
    $self-&gt;{_commands} = 0; # total number of POD commands encountered
    $self-&gt;{_list_stack} = []; # stack for nested lists
    $self-&gt;{_have_begin} = ''; # stores =begin
    $self-&gt;{_links} = []; # stack for internal hyperlinks
    $self-&gt;{_nodes} = []; # stack for =head/=item nodes
    $self-&gt;{_index} = []; # text in X&lt;&gt;
    # print warnings?
    $self-&gt;{-warnings} = 1 unless(defined $self-&gt;{-warnings});
    $self-&gt;{_current_head1} = ''; # the current =head1 block
    $self-&gt;parseopts(-process_cut_cmd =&gt; 1, -warnings =&gt; $self-&gt;{-warnings});
}

##################################

=item C&lt;$checker-E&lt;gt&gt;poderror( @args )&gt;

=item C&lt;$checker-E&lt;gt&gt;poderror( {%opts}, @args )&gt;

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&lt;@args&gt;.

  -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-&gt;poderror( @args ), or $self-&gt;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-&gt;{_NUM_ERRORS}) 
        if(!%opts || ($opts{-severity} &amp;&amp; $opts{-severity} eq 'ERROR'));
    my $out_fh = $self-&gt;output_handle() || \*STDERR;
    print $out_fh ($severity, $msg, $line, $file, "\n")
      if($self-&gt;{-warnings} || !%opts || $opts{-severity} ne 'WARNING');
}

##################################

=item C&lt;$checker-E&lt;gt&gt;num_errors()&gt;

Set (if argument specified) and retrieve the number of errors found.

=cut

sub num_errors {
   return (@_ &gt; 1) ? ($_[0]-&gt;{_NUM_ERRORS} = $_[1]) : $_[0]-&gt;{_NUM_ERRORS};
}

##################################

=item C&lt;$checker-E&lt;gt&gt;name()&gt;

Set (if argument specified) and retrieve the canonical name of POD as
found in the C&lt;=head1 NAME&gt; section.

=cut

sub name {
    return (@_ &gt; 1 &amp;&amp; $_[1]) ?
        ($_[0]-&gt;{-name} = $_[1]) : $_[0]-&gt;{-name};  
}

##################################

=item C&lt;$checker-E&lt;gt&gt;node()&gt;

Add (if argument specified) and retrieve the nodes (as defined by C&lt;=headX&gt;
and C&lt;=item&gt;) of the current POD. The nodes are returned in the order of
their occurence. 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-&gt;{_nodes}}, $text);
        # keep also a uniqueness counter
        $self-&gt;{_unique_nodes}-&gt;{$text}++ if($text !~ /^\s*$/s);
        return $text;
    }
    @{$self-&gt;{_nodes}};
}

##################################

=item C&lt;$checker-E&lt;gt&gt;idx()&gt;

Add (if argument specified) and retrieve the index entries (as defined by
C&lt;XE&lt;lt&gt;E&lt;gt&gt;&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-&gt;{_index}}, $text);
        # keep also a uniqueness counter
        $self-&gt;{_unique_nodes}-&gt;{$text}++ if($text !~ /^\s*$/s);
        return $text;
    }
    @{$self-&gt;{_index}};
}

##################################

=item C&lt;$checker-E&lt;gt&gt;hyperlink()&gt;

Add (if argument specified) and retrieve the hyperlinks (as defined by
C&lt;LE&lt;lt&gt;E&lt;gt&gt;&gt;) of the current POD. They consist of an 2-item array: line
number and C&lt;Pod::Hyperlink&gt; object.

=back

=cut

# set/return hyperlinks of the current POD
sub hyperlink {
    my $self = shift;
    if($_[0]) {
        push(@{$self-&gt;{_links}}, $_[0]);
        return $_[0];
    }
    @{$self-&gt;{_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-&gt;input_file();
    my $out_fh = $self-&gt;output_handle();

    if(@{$self-&gt;{_list_stack}}) {
        # _TODO_ display, but don't count them for now
        my $list;
        while(($list = $self-&gt;_close_list('EOF',$infile)) &amp;&amp;
          $list-&gt;indent() ne 'auto') {
            $self-&gt;poderror({ -line =&gt; 'EOF', -file =&gt; $infile,
                -severity =&gt; 'ERROR', -msg =&gt; "=over on line " .
                $list-&gt;start() . " without closing =back" }); #"
        }
    }

    # check validity of document internal hyperlinks
    # first build the node names from the paragraph text
    my %nodes;
    foreach($self-&gt;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-&gt;idx()) {
        $nodes{$_} = 3; # index node
    }
    foreach($self-&gt;hyperlink()) {
        my ($line,$link) = @$_;
        # _TODO_ what if there is a link to the page itself by the name,
        # e.g. in Tk::Pod : L&lt;Tk::Pod/"DESCRIPTION"&gt;
        if($link-&gt;node() &amp;&amp; !$link-&gt;page() &amp;&amp; $link-&gt;type() ne 'hyperlink') {
            my $node = $self-&gt;_check_ptree($self-&gt;parse_text($link-&gt;node(),
                $line), $line, $infile, 'L');
            if($node &amp;&amp; !$nodes{$node}) {
                $self-&gt;poderror({ -line =&gt; $line || '', -file =&gt; $infile,
                    -severity =&gt; 'ERROR',
                    -msg =&gt; "unresolved internal link '$node'"});
            }
        }
    }

    # check the internal nodes for uniqueness. This pertains to
    # =headX, =item and X&lt;...&gt;
    foreach(grep($self-&gt;{_unique_nodes}-&gt;{$_} &gt; 1,
      keys %{$self-&gt;{_unique_nodes}})) {
        $self-&gt;poderror({ -line =&gt; '-', -file =&gt; $infile,
            -severity =&gt; 'WARNING',
            -msg =&gt; "multiple occurence of link target '$_'"});
    }

    ## Print the number of errors found
    my $num_errors = $self-&gt;num_errors();
    if ($num_errors &gt; 0) {
        printf $out_fh ("$infile has $num_errors pod syntax %s.\n",
                      ($num_errors == 1) ? "error" : "errors");
    }
    elsif($self-&gt;{_commands} == 0) {
        print $out_fh "$infile does not contain any pod commands.\n";
        $self-&gt;num_errors(-1);
    }
    else {
        print $out_fh "$infile pod syntax OK.\n";
    }
}

# check a POD command directive
sub command { 
    my ($self, $cmd, $paragraph, $line_num, $pod_para) = @_;
    my ($file, $line) = $pod_para-&gt;file_line;
    ## Check the command syntax
    my $arg; # this will hold the command argument
    if (! $VALID_COMMANDS{$cmd}) {
       $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file, -severity =&gt; 'ERROR',
                         -msg =&gt; "Unknown command '$cmd'" });
    }
    else { # found a valid command
        $self-&gt;{_commands}++; # delete this line if below is enabled again

        ##### following check disabled due to strong request
        #if(!$self-&gt;{_commands}++ &amp;&amp; $cmd !~ /^head/) {
        #    $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
        #         -severity =&gt; 'WARNING', 
        #         -msg =&gt; "file does not start with =head" });
        #}

        # check syntax of particular command
        if($cmd eq 'over') {
            # check for argument
            $arg = $self-&gt;interpolate_and_check($paragraph, $line,$file);
            my $indent = 4; # default
            if($arg &amp;&amp; $arg =~ /^\s*(\d+)\s*$/) {
                $indent = $1;
            }
            # start a new list
            $self-&gt;_open_list($indent,$line,$file);
        }
        elsif($cmd eq 'item') {
            # are we in a list?
            unless(@{$self-&gt;{_list_stack}}) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'ERROR', 
                     -msg =&gt; "=item without previous =over" });
                # auto-open in case we encounter many more
                $self-&gt;_open_list('auto',$line,$file);
            }
            my $list = $self-&gt;{_list_stack}-&gt;[0];
            # check whether the previous item had some contents
            if(defined $self-&gt;{_list_item_contents} &amp;&amp;
              $self-&gt;{_list_item_contents} == 0) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'WARNING', 
                     -msg =&gt; "previous =item has no contents" });
            }
            if($list-&gt;{_has_par}) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'WARNING', 
                     -msg =&gt; "preceding non-item paragraph(s)" });
                delete $list-&gt;{_has_par};
            }
            # check for argument
            $arg = $self-&gt;interpolate_and_check($paragraph, $line, $file);
            if($arg &amp;&amp; $arg =~ /(\S+)/) {
                $arg =~ s/[\s\n]+$//;
                my $type;
                if($arg =~ /^[*]\s*(\S*.*)/) {
                  $type = 'bullet';
                  $self-&gt;{_list_item_contents} = $1 ? 1 : 0;
                  $arg = $1;
                }
                elsif($arg =~ /^\d+\.?\s*(\S*)/) {
                  $type = 'number';
                  $self-&gt;{_list_item_contents} = $1 ? 1 : 0;
                  $arg = $1;
                }
                else {
                  $type = 'definition';
                  $self-&gt;{_list_item_contents} = 1;
                }
                my $first = $list-&gt;type();
                if($first &amp;&amp; $first ne $type) {
                    $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                       -severity =&gt; 'WARNING', 
                       -msg =&gt; "=item type mismatch ('$first' vs. '$type')"});
                }
                else { # first item
                    $list-&gt;type($type);
                }
            }
            else {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'WARNING', 
                     -msg =&gt; "No argument for =item" });
		$arg = ' '; # empty
                $self-&gt;{_list_item_contents} = 0;
            }
            # add this item
            $list-&gt;item($arg);
            # remember this node
            $self-&gt;node($arg);
        }
        elsif($cmd eq 'back') {
            # check if we have an open list
            unless(@{$self-&gt;{_list_stack}}) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                         -severity =&gt; 'ERROR', 
                         -msg =&gt; "=back without previous =over" });
            }
            else {
                # check for spurious characters
                $arg = $self-&gt;interpolate_and_check($paragraph, $line,$file);
                if($arg &amp;&amp; $arg =~ /\S/) {
                    $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                         -severity =&gt; 'ERROR', 
                         -msg =&gt; "Spurious character(s) after =back" });
                }
                # close list
                my $list = $self-&gt;_close_list($line,$file);
                # check for empty lists
                if(!$list-&gt;item() &amp;&amp; $self-&gt;{-warnings}) {
                    $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                         -severity =&gt; 'WARNING', 
                         -msg =&gt; "No items in =over (at line " .
                         $list-&gt;start() . ") / =back list"}); #"
                }
            }
        }
        elsif($cmd =~ /^head(\d+)/) {
            # check whether the previous =head section had some contents
            if(defined $self-&gt;{_commands_in_head} &amp;&amp;
              $self-&gt;{_commands_in_head} == 0 &amp;&amp;
              defined $self-&gt;{_last_head} &amp;&amp;
              $self-&gt;{_last_head} &gt;= $1) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'WARNING', 
                     -msg =&gt; "empty section in previous paragraph"});
            }
            $self-&gt;{_commands_in_head} = -1;
            $self-&gt;{_last_head} = $1;
            # check if there is an open list
            if(@{$self-&gt;{_list_stack}}) {
                my $list;
                while(($list = $self-&gt;_close_list($line,$file)) &amp;&amp;
                  $list-&gt;indent() ne 'auto') {
                    $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                         -severity =&gt; 'ERROR', 
                         -msg =&gt; "=over on line ". $list-&gt;start() .
                         " without closing =back (at $cmd)" });
                }
            }
            # remember this node
            $arg = $self-&gt;interpolate_and_check($paragraph, $line,$file);
            $arg =~ s/[\s\n]+$//s;
            $self-&gt;node($arg);
            unless(length($arg)) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'ERROR', 
                     -msg =&gt; "empty =$cmd"});
            }
            if($cmd eq 'head1') {
                $self-&gt;{_current_head1} = $arg;
            } else {
                $self-&gt;{_current_head1} = '';
            }
        }
        elsif($cmd eq 'begin') {
            if($self-&gt;{_have_begin}) {
                # already have a begin
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'ERROR', 
                     -msg =&gt; "Nested =begin's (first at line " .
                     $self-&gt;{_have_begin} . ")"});
            }
            else {
                # check for argument
                $arg = $self-&gt;interpolate_and_check($paragraph, $line,$file);
                unless($arg &amp;&amp; $arg =~ /(\S+)/) {
                    $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                         -severity =&gt; 'ERROR', 
                         -msg =&gt; "No argument for =begin"});
                }
                # remember the =begin
                $self-&gt;{_have_begin} = "$line:$1";
            }
        }
        elsif($cmd eq 'end') {
            if($self-&gt;{_have_begin}) {
                # close the existing =begin
                $self-&gt;{_have_begin} = '';
                # check for spurious characters
                $arg = $self-&gt;interpolate_and_check($paragraph, $line,$file);
                # the closing argument is optional
                #if($arg &amp;&amp; $arg =~ /\S/) {
                #    $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                #         -severity =&gt; 'WARNING', 
                #         -msg =&gt; "Spurious character(s) after =end" });
                #}
            }
            else {
                # don't have a matching =begin
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'ERROR', 
                     -msg =&gt; "=end without =begin" });
            }
        }
        elsif($cmd eq 'for') {
            unless($paragraph =~ /\s*(\S+)\s*/) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'ERROR', 
                     -msg =&gt; "=for without formatter specification" });
            }
            $arg = ''; # do not expand paragraph below
        }
        elsif($cmd =~ /^(pod|cut)$/) {
            # check for argument
            $arg = $self-&gt;interpolate_and_check($paragraph, $line,$file);
            if($arg &amp;&amp; $arg =~ /(\S+)/) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                      -severity =&gt; 'ERROR', 
                      -msg =&gt; "Spurious text after =$cmd"});
            }
        }
    $self-&gt;{_commands_in_head}++;
    ## Check the interior sequences in the command-text
    $self-&gt;interpolate_and_check($paragraph, $line,$file)
        unless(defined $arg);
    }
}

sub _open_list
{
    my ($self,$indent,$line,$file) = @_;
    my $list = Pod::List-&gt;new(
           -indent =&gt; $indent,
           -start =&gt; $line,
           -file =&gt; $file);
    unshift(@{$self-&gt;{_list_stack}}, $list);
    undef $self-&gt;{_list_item_contents};
    $list;
}

sub _close_list
{
    my ($self,$line,$file) = @_;
    my $list = shift(@{$self-&gt;{_list_stack}});
    if(defined $self-&gt;{_list_item_contents} &amp;&amp;
      $self-&gt;{_list_item_contents} == 0) {
        $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
            -severity =&gt; 'WARNING', 
            -msg =&gt; "previous =item has no contents" });
    }
    undef $self-&gt;{_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-&gt;_check_ptree(
        $self-&gt;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) {
            my $count;
            # count the unescaped angle brackets
            # complain only when warning level is greater than 1
            my $i = $_;
            if($count = $i =~ tr/&lt;&gt;/&lt;&gt;/) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                     -severity =&gt; 'WARNING', 
                     -msg =&gt; "$count unescaped &lt;&gt; in paragraph" })
                if($self-&gt;{-warnings} &amp;&amp; $self-&gt;{-warnings}&gt;1);
            }
            $text .= $i;
            next;
        }
        # have an interior sequence
        my $cmd = $_-&gt;cmd_name();
        my $contents = $_-&gt;parse_tree();
        ($file,$line) = $_-&gt;file_line();
        # check for valid tag
        if (! $VALID_SEQUENCES{$cmd}) {
            $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                 -severity =&gt; 'ERROR', 
                 -msg =&gt; qq(Unknown interior-sequence '$cmd')});
            # expand it anyway
            $text .= $self-&gt;_check_ptree($contents, $line, $file, "$nestlist$cmd");
            next;
        }
        if($nestlist =~ /$cmd/) {
            $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                 -severity =&gt; 'ERROR', 
                 -msg =&gt; "nested commands $cmd&lt;...$cmd&lt;...&gt;...&gt;"});
            # _TODO_ should we add the contents anyway?
            # expand it anyway, see below
        }
        if($cmd eq 'E') {
            # preserve entities
            if(@$contents &gt; 1 || ref $$contents[0] || $$contents[0] !~ /^\w+$/) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                    -severity =&gt; 'ERROR', 
                    -msg =&gt; "garbled entity " . $_-&gt;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&gt;0 &amp;&amp; $val&lt;256) {
                    $text .= chr($val);
                }
                else {
                    $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                        -severity =&gt; 'ERROR', 
                        -msg =&gt; "Entity number out of range " . $_-&gt;raw_text()});
                }
            }
            elsif($ENTITIES{$ent}) {
                # known ISO entity
                $text .= $ENTITIES{$ent};
            }
            else {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                    -severity =&gt; 'WARNING', 
                    -msg =&gt; "Unknown entity " . $_-&gt;raw_text()});
                $text .= "E&lt;$ent&gt;";
            }
        }
        elsif($cmd eq 'L') {
            # try to parse the hyperlink
            my $link = Pod::Hyperlink-&gt;new($contents-&gt;raw_text());
            unless(defined $link) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                    -severity =&gt; 'ERROR', 
                    -msg =&gt; "malformed link " . $_-&gt;raw_text() ." : $@"});
                next;
            }
            $link-&gt;line($line); # remember line
            if($self-&gt;{-warnings}) {
                foreach my $w ($link-&gt;warning()) {
                    $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                        -severity =&gt; 'WARNING', 
                        -msg =&gt; $w });
                }
            }
            # check the link text
            $text .= $self-&gt;_check_ptree($self-&gt;parse_text($link-&gt;text(),
                $line), $line, $file, "$nestlist$cmd");
            # remember link
            $self-&gt;hyperlink([$line,$link]);
        }
        elsif($cmd =~ /[BCFIS]/) {
            # add the guts
            $text .= $self-&gt;_check_ptree($contents, $line, $file, "$nestlist$cmd");
        }
        elsif($cmd eq 'Z') {
            if(length($contents-&gt;raw_text())) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                    -severity =&gt; 'ERROR', 
                    -msg =&gt; "Nonempty Z&lt;&gt;"});
            }
        }
        elsif($cmd eq 'X') {
            my $idx = $self-&gt;_check_ptree($contents, $line, $file, "$nestlist$cmd");
            if($idx =~ /^\s*$/s) {
                $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
                    -severity =&gt; 'ERROR', 
                    -msg =&gt; "Empty X&lt;&gt;"});
            }
            else {
                # remember this node
                $self-&gt;idx($idx);
            }
        }
        else {
            # not reached
            die "internal error";
        }
    }
    $text;
}

# process a block of verbatim text
sub verbatim { 
    ## Nothing particular to check
    my ($self, $paragraph, $line_num, $pod_para) = @_;

    $self-&gt;_preproc_par($paragraph);

    if($self-&gt;{_current_head1} eq 'NAME') {
        my ($file, $line) = $pod_para-&gt;file_line;
        $self-&gt;poderror({ -line =&gt; $line, -file =&gt; $file,
            -severity =&gt; 'WARNING',
            -msg =&gt; '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-&gt;file_line;

    $self-&gt;_preproc_par($paragraph);

    # skip this paragraph if in a =begin block
    unless($self-&gt;{_have_begin}) {
        my $block = $self-&gt;interpolate_and_check($paragraph, $line,$file);
        if($self-&gt;{_current_head1} eq 'NAME') {
            if($block =~ /^\s*(\S+?)\s*[,-]/) {
                # this is the canonical name
                $self-&gt;{-name} = $1 unless(defined $self-&gt;{-name});
            }
        }
    }
}

sub _preproc_par
{
    my $self = shift;
    $_[0] =~ s/[\s\n]+$//;
    if($_[0]) {
        $self-&gt;{_commands_in_head}++;
        $self-&gt;{_list_item_contents}++ if(defined $self-&gt;{_list_item_contents});
        if(@{$self-&gt;{_list_stack}} &amp;&amp; !$self-&gt;{_list_stack}-&gt;[0]-&gt;item()) {
            $self-&gt;{_list_stack}-&gt;[0]-&gt;{_has_par} = 1;
        }
    }
}

1;

__END__

=head1 AUTHOR

Brad Appleton E&lt;lt&gt;bradapp@enteract.comE&lt;gt&gt; (initial version),
Marek Rouchal E&lt;lt&gt;marek@saftsack.fs.uni-bayreuth.deE&lt;gt&gt;

Based on code for B&lt;Pod::Text::pod2text()&gt; written by
Tom Christiansen E&lt;lt&gt;tchrist@mox.perl.comE&lt;gt&gt;

=cut

</pre></body></html>