package MojoX::Plugin::PODRenderer;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::Asset::File;
use Mojo::ByteStream 'b';
use Mojo::DOM;
use Mojo::Util qw(slurp url_escape class_to_path xml_escape);
use Pod::Simple::HTML;
use Pod::Simple::Search;
use boolean;
use Class::MOP;
use File::Find;
our $VERSION = '0.01';
# Paths to search
my @PATHS = map { $_, "$_/pods" } @INC;
sub register {
my ($self, $app, $conf) = @_;
my $preprocess = $conf->{preprocess} || 'ep';
$app->renderer->add_handler(
$conf->{name} || 'pod' => sub {
my ($renderer, $c, $output, $options) = @_;
# Preprocess and render
my $handler = $renderer->handlers->{$preprocess};
return undef unless $handler->($renderer, $c, $output, $options);
$$output = _pod_to_html($$output);
return 1;
}
);
# Perldoc browser
return $app->routes->any(
'/perldoc/*module' => {module => 'DocIndex'} => \&_perldoc
);
}
# ------------------------------------------------------------------------------
sub _process_found_file {
my ($name2path, $path2name) = @_;
warn "2path %s - 2name %s \n", $name2path, $path2name;
}
# ------------------------------------------------------------------------------
sub _generateIndex {
my $self = shift;
my ($lib) = grep "script\/\.\.\/lib", @INC;
my ($name2path, $path2name) = ({},{}); # It's an owl!
find(
{
wanted => sub {
return unless $_ =~ /\.(pm|pl|pod)$/;
my $path = $File::Find::name;
my $name = $path;
$name =~ s/^$lib\/?//;
$name =~ s/\.(pm|pl|pod)$//g;
$name =~ s!/!::!g;
$path2name->{$path} = $name;
$name2path->{$name} = $path;
},
},
$lib
);
my $guides = [];
my $modules = {};
foreach my $path (sort keys %$path2name) {
my $name = $path2name->{$path};
if ($path =~ /\.pod$/) { # guide
(my $url = '/perldoc/'.class_to_path($name)) =~ s/\.pm$/\.pod/;
push @{$guides}, { name => $name, has_doc => true, path => $url };
}
else { # module
(my $url = '/perldoc/'.class_to_path($name)) =~ s/\.pm//;
# Check whether it actually has pod
my $search = Pod::Simple::Search->new();
my $has_pod = $search->contains_pod($path);
my $section = 'other';
if ( $name =~ /::Role::/) { $section = 'roles' }
elsif ($name =~ /::Models::/) { $section = 'models' }
elsif ($name =~ /::Controllers::/) { $section = 'controllers' }
elsif ($name =~ /::Adapter::/) { $section = 'adapters' }
elsif ($name =~ /::Plugins?::/) { $section = 'plugins' }
push @{$modules->{$section}}, { name => $name, has_doc => $has_pod?true:false, path => $url };
}
}
my ($template, undef) = $self->app->renderer->render(
$self,
{
template => 'perldoc/perldocindex',
partial => 1,
handler => 'ep',
title => "Index",
guides => $guides,
modules => $modules,
}
);
$self->render(inline => $template);
$self->res->headers->content_type('text/html;charset="UTF-8"');
return;
}
# ------------------------------------------------------------------------------
sub _perldoc {
my $self = shift;
my $module = $self->param('module');
$module =~ s/\.pod$//;
if ($module eq 'DocIndex') {
return _generateIndex($self);
}
my $path = Pod::Simple::Search->new->find($module, @PATHS) || '';
# Check whether the file we're dealing with is a perl module with embedded
# pod or whether it's a pure pod doc.
# If the extension is "pod" then it's a standalone. If it's "pm" then there
# will be source code.
my $extension = ($path =~ /\.(pm|pod)$/)[0];
# Convert the full module name to a perl package
my $package = $module;
$package =~ s!/!::!g;
my $file_name = ($module =~ /(\w+)(\.(pm|pod))?$/)[0];
# If we're looking at perl source then we want to know if we're expecting the
# doc view or the source view.
my $is_perl_source = false;
my $linked_file_name = '';
if ($extension && $extension eq 'pm') {
# We know if we're viewing the source as the extension of the module name
# passed in will have the pm extension.
$is_perl_source = true if $module =~ /\.pm$/;
if ($is_perl_source) {
$linked_file_name = $file_name;
}
else {
$linked_file_name = $file_name . '.pm'; # Link is source
}
}
my $html = undef;
if (!-e $path) {
# Redirect to the index page
return _generateIndex($self);
}
else {
my $slurped = slurp $path;
$html = $is_perl_source ? "<pre>".xml_escape($slurped)."</pre>" : _pod_to_html($slurped);
# Ensure % gets escaped before going into the template
# for perl source files.
$html =~ s/^( *)\%/$1<%='%'%>/gm;
}
# TODO ATTRIBUTES ==== TODO Autoinsert
# Introspect the class to find the attributes
_parse_attributes(\$html, $package, $module) if !$is_perl_source && ($html =~ /\[\[ATTRIBUTES\]\]/);
# Rewrite links
my $dom = Mojo::DOM->new("$html");
my $perldoc = $self->url_for('/perldoc/');
$dom->find('a[href]')->each(
sub {
my $attrs = shift->attrs;
$attrs->{href} =~ s!%3A%3A!/!gi
if $attrs->{href} =~ s!^
http://search\.cpan\.org/perldoc\?!$perldoc!;
}
);
# Rewrite code blocks for syntax highlighting
$dom->find('pre')->each(
sub {
my $e = shift;
return if $e->all_text =~ /^\s*\$\s+/m;
my $attrs = $e->attrs;
my $class = $attrs->{class};
$attrs->{class} = defined $class ? "$class prettyprint" : 'prettyprint';
}
);
# Rewrite headers
my $url = $self->req->url->clone;
my (%anchors, @parts);
$dom->find('h1, h2, h3')->each(
sub {
my $e = shift;
# Anchor and text
my $name = my $text = $e->all_text;
$name =~ s/\s+/_/g;
$name =~ s/[^\w\-]//g;
my $anchor = $name;
my $i = 1;
$anchor = $name . $i++ while $anchors{$anchor}++;
# Rewrite
push @parts, [] if $e->type eq 'h1' || !@parts;
my $link_text = $text;
$link_text =~ s/\[.*\]//;
$link_text =~ s/\(.*\)//;
push @{$parts[-1]}, $text, $url->fragment($anchor)->to_abs;
$e->replace_content(
$self->link_to(
$text => $url->fragment('toc')->to_abs,
class => 'mojoscroll',
id => $anchor
)
);
}
);
# Format h2's if they're method names
$dom->find('h2')->each(
sub {
my $e = shift;
my $text = $e->all_text;
if ($text !~ /\[(.+)\] *(\w+) *\((.*)\)/) {
return;
}
my ($type, $name, $args) = ($text =~ /\[(.+)\] *(\w+) *\((.*)\)/);
$e->replace_content(
'<span class="code">'
.'<span class="return-type">['.$type.']</span> '
."$name "
.'<span class="arg-list">('.$args.')</span>'
.'</span>'
);
}
);
# Reformat PRE blocks (again - need to combine this possibly with the mojo written one above)
if (!$is_perl_source) {
$dom->find('pre')->each(
sub {
my $e = shift;
my $re = qr/\@(param|returns|named|throws) (.+)/;
my $context = 'before';
my $has_seen_tags = false;
my %parts = (
before => [[]], after => [[]],
param => [], returns => [],
named => [], throws => [],
);
if ($e->all_text =~ $re) {
foreach my $line (split "\n", $e->all_text) {
if ($line =~ /^ *$/) { # Blank lines switch
$context = $has_seen_tags ? 'after' : 'before';
}
if ($line =~ $re) {
$context = $1; # One of the tag contexts
$line = $2;
$has_seen_tags = true;
push @{$parts{$context}},[]; # Create a new array for the new context
}
if (defined $context) {
# Get the last item of this type, and add to it.
$line =~ s/^ *// if ($context !~ /before|after/);
push @{$parts{ $context }->[-1]}, $line;
next;
}
}
# Output the parts - we do this by appending to the original element
# in reverse order and then removing the original.
# Output AFTER
if (scalar @{$parts{after}->[0]}) {
$e->append('<pre>' . join(" ",@{$parts{after}->[0]}) . '</pre>');
}
if (@{$parts{returns}} || @{$parts{param}} || @{$parts{named}}) {
my $block = '<div class="tag-table-block">';
# Output Parameters
if (scalar @{$parts{param}}) {
$block .= __start_table( 'parameters', '3' );
foreach my $param (@{$parts{param}}) {
(my $whole_line = join ' ',@$param ) =~ /(\S+) +\[([^\]]+)\] +(.+)/;
$block .= qq|<tr><td class="code">$1</td><td class="italic">$2</td><td>$3</td></tr>|;
}
$block .= '</table>';
}
# Output Named Parameters
if (scalar @{$parts{named}}) {
$block .= __start_table( 'named parameters', '3' );
foreach my $param (@{$parts{named}}) {
(my $whole_line = join ' ',@$param ) =~ /(\S+) +\[([^\]]+)\] +(.+)/;
$block .= qq|<tr><td class="code">$1</td><td class="italic">$2</td><td>$3</td></tr>|;
}
$block .= '</table>';
}
# Output Return
if (scalar @{$parts{returns}}) {
$block .= __start_table( 'returns', '1' );
my $whole_line = join ' ', @{$parts{returns}->[0]};
$block .= qq|<tr><td>$whole_line</td></tr>|;
$block .= '</table>';
}
# Output Throws
if (scalar @{$parts{throws}}) {
$block .= __start_table( 'throws', '1' );
foreach my $param (@{$parts{throws}}) {
my $whole_line = join ' ', @{$parts{throws}->[0]};
$block .= qq|<tr><td>$whole_line</td></tr>|;
}
$block .= '</table>';
}
$block .= '</div>';
$e->append( $block );
}
# Output BEFORE
if (scalar @{$parts{before}->[0]}) {
$e->append( '<pre class="prettyprint">' . join(" ",@{$parts{before}->[0]}) . '</pre>');
}
# Remove the original element
$e->remove;
}
}
);
}
# Try to find a title
my $title = 'Perldoc';
$dom->find('h1 + p')->first(sub { $title = shift->text });
# Combine everything to a proper response
$self->content_for(perldoc => "$dom");
my $template_name = $is_perl_source ? 'perlsource' : 'perldoc';
my ($template, undef) = $self->app->renderer->render(
$self,
{
template => 'perldoc/'.$template_name,
partial => 1,
handler => 'ep',
title => $title,
linked_file => $linked_file_name,
parts => \@parts,
}
);
$self->render(inline => $template);
$self->res->headers->content_type('text/html;charset="UTF-8"');
return;
}
# ------------------------------------------------------------------------------
sub __start_table {
my ($name, $span) = @_;
return qq|<table class="tag-table"><tr><th colspan="$span">$name</th></tr>|;
}
# ------------------------------------------------------------------------------
sub _pod_to_html {
return undef unless defined(my $pod = shift);
# Block
$pod = $pod->() if ref $pod eq 'CODE';
my $parser = Pod::Simple::HTML->new;
$parser->force_title('');
$parser->html_header_before_title('');
$parser->html_header_after_title('');
$parser->html_footer('');
$parser->output_string(\(my $output));
return $@ unless eval { $parser->parse_string_document("$pod"); 1 };
# Filter
$output =~ s!<a name='___top' class='dummyTopAnchor'\s*?></a>\n!!g;
$output =~ s!<a class='u'.*?name=".*?"\s*>(.*?)</a>!$1!sg;
return $output;
}
# ------------------------------------------------------------------------------
sub _parse_attributes {
my ($html_r, $package, $module) = @_;
$module =~ s/\.pm$//;
require "$module.pm";
my $meta = Class::MOP::Class->initialize($package);
my %local_attributes = ();
my %inherited_attributes = ();
if ($meta->can("get_attribute_list")) {
foreach my $attr ($meta->get_attribute_list) {
$local_attributes{$attr} = 1;
}
}
if ($meta->can("get_all_attributes")) {
foreach my $attr ($meta->get_all_attributes) {
if (!exists $local_attributes{$attr->name}) {
$inherited_attributes{$attr->name} = 1;
}
}
}
my $replace = '';
my $local = join(", ", sort keys %local_attributes);
my $inherited = join(", ", sort keys %inherited_attributes);
if ($local and $inherited) { $local .= ', ' };
if ($local or $inherited) {
$replace = qq|<div class="code">$local<em>$inherited</em></div><br>|;
}
$$html_r =~ s/\[\[ATTRIBUTES\]\]/$replace/;
return;
}
# ==============================================================================
1;
=head1 NAME
MojoX::Plugin::PODRenderer
=head1 SYNOPSIS
use MojoX::Plugin::PODRenderer;
$self->plugin( 'MojoX::Plugin::PODRenderer' );
=head1 DESCRIPTION
Perl pod rendering plugin. Based on the original Mojo::PODRenderer.
=head1 METHODS
=head2 [void] register( $app, $conf )
Called by Mojo app to register the plugin
@param app [mojo application] ref to the mojo application
@param conf [hash] configuration hash
=cut