package Pod::ProjectDocs::Parser;
use strict;
use warnings;
use base qw/Pod::Parser Class::Accessor::Fast Class::Data::Inheritable/;
use Pod::ParseUtils;
use URI::Escape;
use File::Spec;
use File::Basename;
use Pod::ProjectDocs::Template;
our $METHOD_REGEXP ||= qr/^(\w+).*/;
BEGIN {
our $HIGHLIGHTER;
eval {
require Syntax::Highlight::Universal;
$HIGHLIGHTER = Syntax::Highlight::Universal->new;
};
*highlighten = $HIGHLIGHTER ? sub {
my ($self, $type, $str) = @_;
$HIGHLIGHTER->highlight($type, $str);
} : sub { return $_[2] };
}
# most of code is borrowed from Pod::Xhtml
__PACKAGE__->mk_accessors(qw/components local_modules current_files_output_path/);
__PACKAGE__->mk_classdata($_) for qw/COMMANDS SEQ language/;
__PACKAGE__->COMMANDS( {
map { $_ => 1 } qw/pod head1 head2 head3 head4 item over back for begin end/
} );
__PACKAGE__->SEQ( {
B => \&seqB,
C => \&seqC,
E => \&seqE,
F => \&seqF,
I => \&seqI,
L => \&seqL,
S => \&seqS,
X => \&seqX,
Z => \&seqZ,
} );
########## New PUBLIC methods for this class
sub asString { my $self = shift; return $self->{buffer}; }
sub asStringRef { my $self = shift; return \$self->{buffer}; }
sub addHeadText { my $self = shift; $self->{HeadText} .= shift; }
sub addBodyOpenText { my $self = shift; $self->{BodyOpenText} .= shift; }
sub addBodyCloseText { my $self = shift; $self->{BodyCloseText} .= shift; }
########## Override methods in Pod::Parser
########## PUBLIC INTERFACE
sub parse_from_file {
my $self = shift;
$self->resetMe;
$self->SUPER::parse_from_file(@_);
}
sub parse_from_filehandle {
my $self = shift;
$self->resetMe;
$self->SUPER::parse_from_filehandle(@_);
}
########## INTERNALS
sub initialize {
my $self = shift;
$self->{TopLinks} = qq(<p><a href="#<<<G?TOP>>>" class="toplink">Top</a></p>) unless defined $self->{TopLinks};
$self->{MakeIndex} = 1 unless defined $self->{MakeIndex};
$self->{MakeMeta} = 1 unless defined $self->{MakeMeta};
$self->{FragmentOnly} = 0 unless defined $self->{FragmentOnly};
$self->{HeadText} = $self->{BodyOpenText} = $self->{BodyCloseText} = '';
$self->{LinkParser} ||= new Pod::Hyperlink;
$self->{IsFirstCommand} = 1;
$self->{FirstAnchor} = "TOP";
$self->SUPER::initialize();
}
sub command {
my ($parser, $command, $paragraph, $line_num, $pod_para) = @_;
my $ptree = $parser->parse_text( $paragraph, $line_num );
$pod_para->parse_tree( $ptree );
$parser->parse_tree->append( $pod_para );
}
sub verbatim {
my ($parser, $paragraph, $line_num, $pod_para) = @_;
$parser->parse_tree->append( $pod_para );
}
sub textblock {
my ($parser, $paragraph, $line_num, $pod_para) = @_;
my $ptree = $parser->parse_text( $paragraph, $line_num );
$pod_para->parse_tree( $ptree );
$parser->parse_tree->append( $pod_para );
}
sub end_pod {
my $self = shift;
my $ptree = $self->parse_tree;
# clean up tree ready for parse
foreach my $para (@$ptree) {
if ($para->{'-prefix'} eq '=') {
$para->{'TYPE'} = 'COMMAND';
} elsif (! @{$para->{'-ptree'}}) {
$para->{'-ptree'}->[0] = $para->{'-text'};
$para->{'TYPE'} = 'VERBATIM';
} else {
$para->{'TYPE'} = 'TEXT';
}
foreach (@{$para->{'-ptree'}}) {
unless (ref $_) { s/\n\s+$//; }
}
}
# now loop over each para and expand any html escapes or sequences
$self->_paraExpand( $_ ) foreach (@$ptree);
$self->{buffer} =~ s/(\n?)<\/pre>\s*<pre>/$1/sg; # concatenate 'pre' blocks
1 while $self->{buffer} =~ s/<pre>(\s+)<\/pre>/$1/sg;
$self->{buffer} = $self->_makeIndex . $self->{buffer} if $self->{MakeIndex};
$self->{buffer} =~ s/<<<G\?TOP>>>/$self->{FirstAnchor}/ge;
$self->{buffer} = join "\n", qq[<div class="pod">], $self->{buffer}, "</div>";
# Expand internal L<> links to the correct sections
$self->{buffer} =~ s/#<<<(.*?)>>>/'#' . $self->_findSection($1)/eg;
die "gotcha" if $self->{buffer} =~ /#<<</;
my $headblock = sprintf "%s\n%s\n\t<title>%s</title>\n",
qq(<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">),
qq(<html xmlns="http://www.w3.org/1999/xhtml" xml:lang=").$self->{Lang}.qq(">\n<head>),
_htmlEscape( $self->{doctitle} );
$headblock .= $self->_makeMeta if $self->{MakeMeta};
unless ($self->{FragmentOnly}) {
$self->{buffer} = $headblock . $self->{HeadText} . "</head>\n<body>\n" . $self->{BodyOpenText} . $self->{buffer};
$self->{buffer} .= $self->{BodyCloseText} . "</body>\n</html>\n";
}
# in stringmode we only accumulate the XHTML else we print it to the
# filehandle
unless ($self->{StringMode}) {
my $out_fh = $self->output_handle;
print $out_fh $self->{buffer};
}
}
########## Everything else is PRIVATE
sub resetMe {
my $self = shift;
$self->{'-ptree'} = new Pod::ParseTree;
$self->{'sections'} = [];
$self->{'listKind'} = [];
$self->{'listHasItems'} = [];
$self->{'dataSections'} = [];
$self->{'section_names'} = {};
$self->{'section_ids'} = {};
foreach (qw(inList titleflag )) { $self->{$_} = 0; }
foreach (qw(buffer doctitle)) { $self->{$_} = ''; }
}
sub parse_tree { return $_[0]->{'-ptree'}; }
sub _paraExpand {
my $self = shift;
my $para = shift;
# collapse interior sequences and strings
foreach ( @{$para->{'-ptree'}} ) {
$_ = (ref $_) ? $self->_handleSequence($_) : _htmlEscape( $_ );
}
# the parse tree has now been collapsed into a list of strings
if ($para->{TYPE} eq 'TEXT') {
return if @{$self->{dataSections}};
$self->_addTextblock( join('', @{$para->{'-ptree'}}) );
} elsif ($para->{TYPE} eq 'VERBATIM') {
return if @{$self->{dataSections}};
my $paragraph = "<pre>" . join('', @{$para->{'-ptree'}}) . "\n\n</pre>";
my $parent_list = $self->{listKind}[-1];
if ($parent_list && $parent_list == 2) {
$paragraph = "<dd>$paragraph</dd>";
}
$self->{buffer} .= $paragraph;
if ($self->{titleflag} != 0) {
$self->_setTitle( $paragraph );
warn "NAME followed by verbatim paragraph";
}
} elsif ($para->{TYPE} eq 'COMMAND') {
$self->_addCommand($para->{'-name'}, join('', @{$para->{'-ptree'}}), $para->{'-text'}, $para->{'-line'} )
} else {
warn "Unrecognized paragraph type $para->{TYPE} found at $self->{_INFILE} line $para->{'-line'}\n";
}
}
sub _addCommand {
my $self = shift;
my ($command, $paragraph, $raw_para, $line) = @_;
my $anchor;
unless (exists $self->COMMANDS->{$command}) {
warn "Unrecognized command '$command' skipped at $self->{_INFILE} line $line\n";
return;
}
for ($command) {
/^head1/ && do {
$anchor = $self->_addSection( 'head1', $paragraph );
$self->{buffer} .= qq(<h1 id="$anchor">$paragraph )
.($self->{TopLinks} ? $self->{TopLinks} : '').qq(</h1>)."\n\n";
if ($anchor eq 'NAME') { $self->{titleflag} = 1; }
last;
};
/^head([234])/ && do {
my $head_level = $1;
$anchor = $self->_addSection( "head${head_level}", $paragraph );
$self->{buffer} .= qq(<h${head_level} id="$anchor">$paragraph</h${head_level}>\n\n);
(my $method = $paragraph) =~ s#$METHOD_REGEXP#$1#;
if ( exists $self->{_source_code}{$method} ) {
$self->{buffer} .= qq{<p><a href="#" onclick="toggleCode('method_$method');return false;">[Source]</a></p>
<div class="method-source-code" id="method_$method">
<pre>\n\n};
$self->{buffer} .= $self->{_source_code}{$method};
$self->{buffer} .= qq{</pre></div>\n\n};
}
last;
};
/^item/ && do {
unless ($self->{inList}) {
warn "Not in list at $self->{_INFILE} line $line\n";
last;
}
$self->{listHasItems}[-1] = 1;
$self->{listCurrentParas}[-1] = 0;
# is this the first item in the list?
if (@{$self->{listKind}} && $self->{listKind}[-1] == 0) {
my $parent_list = $self->{listKind}[-2]; # this is a sub-list
if ($parent_list && $parent_list == 1) {
# <ul> sub lists must be in an <li> [BEGIN]
$self->{buffer} .= "<li>";
} elsif ($parent_list && $parent_list == 2) {
# <dl> sub lists must be in a <dd> [BEGIN]
$self->{buffer} .= "<dd>";
}
if ($paragraph eq '*') {
$self->{listKind}[-1] = 1;
$self->{buffer} .= "<ul>\n";
} else {
$self->{listKind}[-1] = 2;
$self->{buffer} .= "<dl>\n";
}
} else {
# close last list item's tag#
if ($self->{listKind}[-1] == 1) {
$self->{buffer} .= "</li>\n";
}
}
if (@{$self->{listKind}} && $self->{listKind}[-1] == 2) {
$self->{buffer} .= qq(\t<dt);
if ($self->{MakeIndex} >= 2) {
$anchor = $self->_addSection( 'list', $paragraph );
$self->{buffer} .= qq( id="$anchor");
}
$self->{buffer} .= ">";
$self->{buffer} .= qq($paragraph</dt>\n);
}
last;
};
/^over/ && do {
$self->{inList}++;
push @{$self->{listKind}}, 0;
push @{$self->{listHasItems}}, 0;
push @{$self->{sections}}, 'OVER';
push @{$self->{listCurrentParas}}, 0;
};
/^back/ && do {
if (--$self->{inList} < 0) {
warn "=back commands don't balance =overs at $self->{_INFILE} line $line\n";
last;
} elsif ($self->{listHasItems} == 0) {
warn "empty list at $self->{_INFILE} line $line\n";
last;
} elsif (@{$self->{listKind}} && $self->{listKind}[-1] == 1) {
$self->{buffer} .= "</li>\n</ul>\n\n";
} else {
$self->{buffer} .= "</dl>\n";
}
my $parent_list = $self->{listKind}[-2]; # this is a sub-list
if ($parent_list && $parent_list == 1) {
# <ul> sub lists must be in an <li> [END]
$self->{buffer} .= "</li>\n";
}
if ($parent_list && $parent_list == 2) {
# <dl> sub lists must be in a <dd> [END]
$self->{buffer} .= "</dd>\n";
}
if ($self->{sections}[-1] eq 'OVER')
{
pop @{$self->{sections}};
} else {
push @{$self->{sections}}, 'BACK';
}
pop @{$self->{listHasItems}};
pop @{$self->{listKind}};
pop @{$self->{listCurrentParas}};
last;
};
/^for/ && do {
my ($html) = $raw_para =~ /^\s*(?:pod2)?x?html\s+(.*)/;
$self->{buffer} .= $html if $html;
};
/^begin/ && do {
my ($ident) = $paragraph =~ /(\S+)/;
push @{$self->{dataSections}}, $ident;
last;
};
/^end/ && do {
my ($ident) = $paragraph =~ /(\S+)/;
unless (@{$self->{dataSections}}) {
warn "no corresponding '=begin $ident' marker at $self->{_INFILE} line $line\n";
last;
}
my $current_section = $self->{dataSections}[-1];
unless ($current_section eq $ident) {
warn "'=end $ident' doesn't match '=begin $current_section' at $self->{_INFILE} line $line\n";
last;
}
pop @{$self->{dataSections}};
last;
};
}
if ($anchor && $self->{IsFirstCommand})
{
$self->{FirstAnchor} = $anchor;
$self->{IsFirstCommand} = 0;
}
}
sub _addTextblock {
my $self = shift;
my $paragraph = shift;
if ($self->{titleflag} != 0) { $self->_setTitle( $paragraph ); }
if (! @{$self->{listKind}} || $self->{listKind}[-1] == 0) {
$self->{buffer} .= "<p>$paragraph</p>\n\n";
} elsif (@{$self->{listKind}} && $self->{listKind}[-1] == 1) {
if ($self->{listCurrentParas}[-1]++ == 0) {
$self->{buffer} .= "\t<li>$paragraph";
} else {
$self->{buffer} .= "\n<br /><br />$paragraph";
}
} else {
$self->{buffer} .= "\t\t<dd><p>$paragraph</p></dd>\n";
}
}
# expand interior sequences recursively, bottom up
sub _handleSequence {
my $self = shift;
my $seq = shift;
my $buffer = '';
foreach (@{$seq->{'-ptree'}}) {
if (ref $_) {
$buffer .= $self->_handleSequence($_);
} else {
$buffer .= _htmlEscape($_);
}
}
unless (exists $self->SEQ->{$seq->{'-name'}}) {
warn "Unrecognized special sequence '$seq->{'-name'}' skipped at $self->{_INFILE} line $seq->{'-line'}\n";
return $buffer;
}
return $self->SEQ->{$seq->{'-name'}}->($self, $buffer);
}
sub _makeIndexId {
my $arg = shift;
$arg =~ s/\W+/_/g;
$arg =~ s/^_+|_+$//g;
$arg =~ s/__+/_/g;
$arg = substr($arg, 0, 36);
return $arg;
}
sub _addSection {
my $self = shift;
my ($type, $htmlarg) = @_;
return unless defined $htmlarg;
my $index_id;
if ($self->{section_names}{$htmlarg}) {
$index_id = $self->{section_names}{$htmlarg};
} else {
$index_id = _makeIndexId($htmlarg);
if ($self->{section_ids}{$index_id}) {
$index_id .= "-" . ++$self->{section_ids}{$index_id};
} else {
$self->{section_ids}{$index_id}++;
}
$self->{section_names}{$htmlarg} = $index_id;
}
push( @{$self->{sections}}, [$type, $index_id, $htmlarg]);
return $index_id;
}
sub _findSection {
my $self = shift;
my ($htmlarg) = @_;
my $index_id;
if ($index_id = $self->{section_names}{$htmlarg}) {
return $index_id;
} else {
return _makeIndexId($htmlarg);
}
}
sub _get_elem_level {
my $elem = shift;
if (ref($elem))
{
my $type = $elem->[0];
if ($type =~ /^head(\d+)$/)
{
return $1;
}
else
{
return 0;
}
}
else
{
return 0;
}
}
sub _makeIndex {
my $self = shift;
$self->{FirstAnchor} = "TOP";
my $string = "<!-- INDEX START -->\n<h3 id=\"TOP\">Index</h3>\n<ul>\n";
$self->{FirstAnchor} = "TOP";
my $i = 0;
my $previous_level = 0;
for (my $i=0;$i< @{$self->{sections}} ; $i++)
{
local $_ = $self->{sections}->[$i];
my $next = ($self->{'sections'}->[$i+1] || "");
if (ref $_) {
my ($type, $href, $name) = @$_;
my $index_link = "";
my $next_level = _get_elem_level($next);
my $this_level = _get_elem_level($_) || $previous_level;
if ($this_level < $previous_level)
{
$index_link .=
("</ul>\n</li>\n" x ($previous_level - $this_level));
}
$index_link .= qq(\t<li><a href="#${href}">${name}</a>);
if ($next eq "OVER")
{
$index_link .= "<br />\n";
}
elsif ($next_level > $this_level)
{
$index_link .= "<br />\n";
$index_link .=
("<ul>\n<li>\n" x ($next_level - $this_level - 1)) .
"<ul>\n";
}
else
{
$index_link .= "</li>\n";
}
# $index_link = qq(<ul>$index_link</ul>) unless ($type eq 'head1');
$string .= $index_link;
} elsif ($_ eq 'OVER') {
$string .= qq(\t<ul>\n);
} elsif ($_ eq 'BACK') {
$string .= qq(\t</ul>\n</li>\n);
}
$previous_level = _get_elem_level($_) || $previous_level;
}
$string .=
("</ul>\n</li>\n" x ($previous_level-1)) . "</ul>\n";
$string .= "<hr />\n<!-- INDEX END -->\n\n";
return $string;
}
sub _makeMeta {
my $self = shift;
return
qq(\t<meta name="description" content="Pod documentation for ) . _htmlEscape( $self->{doctitle} ) . qq(" />\n)
. qq(\t<meta name="inputfile" content=") . _htmlEscape( $self->input_file ) . qq(" />\n)
. qq(\t<meta name="outputfile" content=") . _htmlEscape( $self->output_file ) . qq(" />\n)
. qq(\t<meta name="created" content=") . _htmlEscape( scalar(localtime) ) . qq(" />\n);
}
sub _setTitle {
my $self = shift;
my $paragraph = shift;
if ($paragraph =~ m/^(.+?) - /) {
$self->{doctitle} = $1;
} elsif ($paragraph =~ m/^(.+?): /) {
$self->{doctitle} = $1;
} elsif ($paragraph =~ m/^(.+?)\.pm/) {
$self->{doctitle} = $1;
} else {
$self->{doctitle} = substr($paragraph, 0, 80);
}
$self->{titleflag} = 0;
}
sub _htmlEscape {
my $txt = shift;
$txt =~ s/&(?!(amp|lt|gt|quot);)/&/g;
$txt =~ s/</</g;
$txt =~ s/>/>/g;
$txt =~ s/\"/"/g;
return $txt;
}
########## Sequence handlers
sub seqI { return '<i>' . $_[1] . '</i>'; }
sub seqB { return '<strong>' . $_[1] . '</strong>'; }
sub seqC { return '<code>' . $_[1] . '</code>'; }
sub seqF { return '<cite>' . $_[1] . '</cite>'; }
sub seqZ { return ''; }
sub seqL {
my ($self, $link) = @_;
$self->{LinkParser}->parse( $link );
my $kind = $self->{LinkParser}->type;
my $string = '';
if ($kind eq 'hyperlink') { #easy, a hyperlink
my $targ = _htmlEscape( $self->{LinkParser}->node );
my $text = _htmlEscape( $self->{LinkParser}->text );
$string = qq(<a href="$targ">$text</a>);
} elsif ($self->{LinkParser}->page eq '') { # a link to this page
# Post-process these links so we can things up to the correct sections
my $targ = $self->{LinkParser}->node;
my $text = _htmlEscape( $self->{LinkParser}->text );
$string = qq(<a href="#<<<$targ>>>">$text</a>);
} elsif ($kind eq 'item') { # link to the other page
my $targ = $self->_resolvePage($self->{LinkParser}->page);
my $node = $self->{LinkParser}->node;
my $text = _htmlEscape( $self->{LinkParser}->text );
$string = qq(<a href="$targ#$node">$text</a>);
} else {
my $targ = $self->_resolvePage($self->{LinkParser}->page);
my $text = _htmlEscape( $self->{LinkParser}->text );
$string = qq(<a href="$targ">$text</a>);
}
return $string;
}
sub _resolvePage {
my ($self, $page) = @_;
my $modules = $self->local_modules->{ $self->language } || [];
foreach my $module ( @$modules ) {
if ( $module->{name} eq $page ) {
my $targ = $self->_resolveRelPath( $module->{path} );
return $targ;
}
}
return $self->_makeLinkToCommunity($page);
}
sub _makeLinkToCommunity { "abstract method" }
sub _resolveRelPath {
my ($self, $path ) = @_;
my $curpath = $self->current_files_output_path;
my ($name, $dir) = File::Basename::fileparse $curpath, qr/\.html/;
return File::Spec->abs2rel($path, $dir);
}
sub seqS {
my $text = $_[1];
$text =~ s/\s/ /g;
return $text;
}
sub seqX {
my $self = shift;
my $arg = shift;
my $anchor = $self->_addSection( 'head1', $arg );
return qq[<span id="$anchor">$arg</span>];
}
sub seqE {
my $self = shift;
my $arg = shift;
my $rv;
if ($arg eq 'sol') {
$rv = '/';
} elsif ($arg eq 'verbar') {
$rv = '|';
} elsif ($arg =~ /^\d$/) {
$rv = "&#$arg;";
} elsif ($arg =~ /^0?x(\d+)$/) {
$rv = $1;
} else {
$rv = "&$arg;";
}
return $rv;
}
sub gen_html {
my($self, %args) = @_;
my $doc = $args{doc};
my $components = $args{components};
my $mgr_desc = $args{desc};
open(FILE, $doc->origin) or warn $!;
while(<FILE>) {
next unless /^\s*sub\s+(\w+)/;
my $method = $1;
my $sub = $_;
while(<FILE>){
$sub .= $_;
last if /^}/;
}
my $result = $self->highlighten("perl", $sub);
$self->{_source_code}{$method} = $result;
}
close(FILE);
$self->current_files_output_path( $doc->get_output_path );
$self->_prepare($doc, $components, $mgr_desc);
# local $SIG{__WARN__} = sub { };
$self->parse_from_file($doc->origin);
my $title = $self->_get_title;
$doc->title($title);
$self->current_files_output_path('');
return $self->asString;
}
sub _prepare {
my($self, $doc, $components, $mgr_desc) = @_;
my $charset = $doc->config->charset || 'UTF-8';
$self->{StringMode} = 1;
$self->{MakeMeta} = 0;
$self->{TopLinks} = $components->{arrow}->tag($doc);
$self->{MakeIndex} = $doc->config->index;
$self->{Lang} = $doc->config->lang;
$self->initialize();
$self->addHeadText($components->{css}->tag($doc));
$self->addHeadText(qq|<meta http-equiv="Content-Type" content="text/html; charset=$charset" />\n|);
$self->addHeadText(q| <script type="text/javascript">
function toggleCode( id ) {
if ( document.getElementById )
elem = document.getElementById( id );
else if ( document.all )
elem = eval( "document.all." + id );
else
return false;
elemStyle = elem.style;
if ( elemStyle.display != "block" ) {
elemStyle.display = "block"
} else {
elemStyle.display = "none"
}
return true;
}
document.writeln( "<style type=\"text/css\">div.method-source-code { display: none }</style>" )
</script>|);
$self->addBodyOpenText($self->_get_data($doc, $mgr_desc));
$self->addBodyCloseText(
qq|<div class="footer">generated by <a href="http://search.cpan.org/perldoc?|
.URI::Escape::uri_escape("Pod::ProjectDocs")
.qq|">Pod::ProjectDocs</a></div>|
);
}
sub _get_title {
my $self = shift;
my $name_node = 0;
my $title = '';
foreach my $node ( @{ $self->parse_tree } ) {
if ($node->{'-ptree'}[0] && $node->{'-ptree'}[0] eq 'NAME') {
$name_node = 1; next;
}
if($name_node == 1){
$title = join "", @{ $node->{'-ptree'} };
last;
}
}
$title =~ s/^\s*\S*\s*-\s(.*)$/$1/;
return $title;
}
sub _get_data {
my($self, $doc, $mgr_desc) = @_;
my $tt = Pod::ProjectDocs::Template->new;
my $text = $tt->process($doc, $doc->data, {
title => $doc->config->title,
desc => $doc->config->desc,
name => $doc->name,
outroot => $doc->config->outroot,
src => $doc->get_output_src_path,
mgr_desc => $mgr_desc,
});
return $text if $^O ne 'MSWin32';
while ( $text =~ s|href="(.*?)\\(.*?)"|href="$1/$2"| ) {
next;
}
return $text;
}
1;
__END__