File Coverage

blib/lib/Catalyst/Controller/POD.pm
Criterion Covered Total %
statement 32 34 94.1
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 44 46 95.6


line stmt bran cond sub pod time code
1             #
2             # This file is part of Catalyst-Controller-POD
3             #
4             # This software is Copyright (c) 2011 by Moritz Onken.
5             #
6             # This is free software, licensed under:
7             #
8             # The (three-clause) BSD License
9             #
10             package Catalyst::Controller::POD;
11             BEGIN {
12 1     1   47809 $Catalyst::Controller::POD::VERSION = '1.0.0';
13             }
14             # ABSTRACT: Serves PODs right from your Catalyst application
15 1     1   8 use warnings;
  1         2  
  1         27  
16 1     1   52 use strict;
  1         2  
  1         45  
17 1     1   6 use File::Find qw( find );
  1         2  
  1         129  
18 1     1   920 use File::ShareDir qw( dist_file );
  1         7681  
  1         99  
19 1     1   13 use File::Spec;
  1         2  
  1         31  
20 1     1   1896 use File::Slurp;
  1         20098  
  1         88  
21 1     1   2389 use Pod::Simple::Search;
  1         6419  
  1         43  
22 1     1   1321 use JSON::XS;
  1         11759  
  1         83  
23 1     1   2699 use Path::Class::File;
  1         60672  
  1         45  
24 1     1   2849 use Pod::POM;
  1         43686  
  1         57  
25 1     1   443 use XML::Simple;
  0            
  0            
26             use LWP::Simple;
27             use List::MoreUtils qw(uniq);
28             use Catalyst::Controller::POD::Template;
29              
30             use base "Catalyst::Controller";
31              
32             __PACKAGE__->mk_accessors(qw(_dist_dir inc namespaces self dir show_home_tab initial_module home_tab_content expanded_module_tree));
33              
34             __PACKAGE__->config(
35             self => 1,
36             namespaces => ["*"],
37             initial_module => "",
38             show_home_tab => 1,
39             expanded_module_tree => 0,
40             home_tab_content => <<HTML,
41             <div style="width:500px; margin:50px" class='x-box-blue' id='move-me'>
42             <div class="x-box-tl"><div class="x-box-tr"><div class="x-box-tc"></div></div></div>
43             <div class="x-box-ml"><div class="x-box-mr"><div class="x-box-mc">
44             <h3 style="margin-bottom:5px;">Search the CPAN</h3>
45             <input type="text" name="search" id="search" class="x-form-text" style='font-size: 20px; height: 31px'/>
46             <div style="padding-top:4px;">Type at least three characters</div>
47             </div></div></div>
48             <div class="x-box-bl"><div class="x-box-br"><div class="x-box-bc"></div></div></div>
49             </div>
50             HTML
51             );
52              
53             sub search : Local {
54             my ( $self, $c ) = @_;
55             my $k = $c->req->param("value");
56             my $s = $c->req->param("start");
57             my $url = new URI("http://search.cpan.org/search");
58             $url->query_form_hash(
59             query => $k,
60             mode => "module",
61             n => 50,
62             format => "xml",
63             s => $s
64             );
65             my $ua = new LWP::UserAgent;
66             $ua->timeout(15);
67             $c->log->debug("get url ".$url->canonical) if($c->debug);
68             my $response = $ua->get($url);
69             my $xml = $response->content;
70             my $data;
71             eval{ $data = XMLin($xml, keyattr => [] )};
72             if(@$) {
73             $c->res->body("[]");
74             return;
75             }
76             my $output = {count => $data->{matches}};
77             while(my($k,$v) = each %{$output->{module}}) {
78            
79             }
80             $c->res->body(encode_json($data));
81             }
82              
83              
84             sub module : Local {
85             my ( $self, $c, $module ) = @_;
86             my $search = Pod::Simple::Search->new->inc( $self->inc || 0 );
87             push( @{ $self->{dirs} }, $c->path_to('lib')->stringify )
88             if ( $self->{self} );
89             my $name2path =
90             $search->limit_glob($module)->survey( @{ $self->{dirs} } );
91             my $view = "Catalyst::Controller::POD::POM::View";
92             Pod::POM->default_view($view);
93             my $parser = Pod::POM->new( warn => 0 );
94             $view->_root( $self->_root($c) );
95             $view->_module($module);
96             my $pom;
97              
98             if ( $name2path->{$module} ) {
99             $c->log->debug("Getting POD from local store") if($c->debug);
100             $view->_toc( _get_toc( $name2path->{$module} ) );
101             $pom = $parser->parse_file( $name2path->{$module} )
102             || die $parser->error(), "\n";
103             } else {
104             $c->log->debug("Getting POD from CPAN") if($c->debug);
105             my $html = get( "http://search.cpan.org/perldoc?" . $module );
106             my $source;
107             if($html && $html =~ /.*<a href="(.*?)">Source<\/a>.*/) {
108             $html =~ s/.*<a href="(.*?)">Source<\/a>.*/$1/s;
109             $c->log->debug("Get source from http://search.cpan.org" . $html) if($c->debug);
110             $source = get( "http://search.cpan.org" . $html );
111             } else {
112             $source = "=head1 ERROR\n\nThis module could not be found.";
113             }
114             $view->_toc( _get_toc( $source ) );
115             $pom = $parser->parse_text($source)
116             || die $parser->error(), "\n";
117             }
118             Pod::POM->default_view("Catalyst::Controller::POD::POM::View");
119             $c->res->body( "$pom" );
120             }
121              
122             sub _get_toc {
123             my $source = shift;
124             my $toc;
125             my $parser = Pod::POM->new( warn => 0 );
126             my $view = "Pod::POM::View::TOC";
127             Pod::POM->default_view($view);
128             my $pom = $parser->parse($source);
129             $toc = $view->print($pom);
130             return encode_json( _toc_to_json( [], split( /\n/, $toc ) ) );
131             }
132              
133             sub _toc_to_json {
134             my $tree = shift;
135             my @sections = @_;
136             my @uniq = uniq( map { ( split(/\t/) )[0] } @sections );
137             foreach my $root (@uniq) {
138             next unless ($root);
139             push( @{$tree}, { text => $root } );
140             my ( @children, $start );
141             for (@sections) {
142             if ( $_ =~ /^\Q$root\E$/ ) {
143             $start = 1;
144             } elsif ( $start && $_ =~ /^\t(.*)$/ ) {
145             push( @children, $1 );
146             } elsif ( $start && $_ =~ /^[^\t]+/ ) {
147             last;
148             }
149             }
150             unless (@children) {
151             $tree->[-1]->{leaf} = \1;
152             next;
153             }
154             $tree->[-1]->{children} = [];
155             $tree->[-1]->{children} =
156             _toc_to_json( $tree->[-1]->{children}, @children );
157             }
158             return $tree;
159             }
160              
161             sub modules : Local {
162             my ( $self, $c, $find ) = @_;
163             my $search = Pod::Simple::Search->new->inc( $self->{inc} || 0 );
164             push( @{ $self->{dirs} }, $c->path_to('lib')->stringify )
165             if ( $self->{self} );
166             my $name2path = {};
167              
168             for ( @{ $self->{namespaces} } ) {
169             my $found =
170             Pod::Simple::Search->new->inc( $self->{inc} || 0 )
171             ->limit_glob($_)->survey( @{ $self->{dirs} } );
172             %{$name2path} = (
173             %{$name2path}, %{$found}
174             );
175             }
176            
177             my @modules;
178             while ( my ( $k, $v ) = each %$name2path ) {
179             next if($find && $k !~ /\Q$find\E/ig);
180             push( @modules, $k );
181             }
182             @modules = sort @modules;
183             my $json = _build_module_tree( [], "", @modules );
184             $c->res->body( encode_json($json) );
185             }
186              
187             sub _build_module_tree : Private {
188             my $tree = shift;
189             my $stack = shift;
190             my @modules = @_;
191             my @uniq = uniq( map { ( split(/::/) )[0] } @modules );
192             foreach my $root (@uniq) {
193             my $name = $stack ? $stack . "::" . $root : $root;
194             push( @{$tree}, { text => $root, name => $name } );
195             my @children;
196             for (@modules) {
197             if ( $_ =~ /^$root\:\:(.*)$/ ) {
198             push( @children, $1 );
199             }
200             }
201             unless (@children) {
202             $tree->[-1]->{leaf} = \1;
203             next;
204             }
205             $tree->[-1]->{children} = [];
206             $tree->[-1]->{children} =
207             _build_module_tree( $tree->[-1]->{children}, $name, @children );
208             }
209             return $tree;
210             }
211              
212             sub _root {
213             my ( $self, $c ) = @_;
214             my $index = $c->uri_for( __PACKAGE__->config->{path} );
215              
216             #$index =~ s/\/index//g;
217             return $index;
218             }
219              
220             sub new {
221             my $class = shift;
222             my $self = $class->next::method(@_);
223             my $file = Path::Class::File->new( 'share', 'docs.js' );
224             eval {
225             $file = Path::Class::File->new(
226             dist_file( 'Catalyst-Controller-POD', 'docs.js' ) );
227             } unless(-e $file);
228             $self->_dist_dir( $file->dir );
229             return $self;
230             }
231              
232              
233             sub index : Path : Args(0) {
234             my ( $self, $c ) = @_;
235             $c->res->content_type('text/html; charset=utf-8');
236             $c->response->body(
237             Catalyst::Controller::POD::Template->get(
238             $self->_root($c) . "/static"
239             )
240             );
241             }
242              
243             sub get_home_tab_content : Path("home_tab_content") {
244             my ( $self, $c ) = @_;
245             $c->response->body($self->home_tab_content);
246             }
247              
248             sub static : Path("static") {
249             my ( $self, $c, @file ) = @_;
250             my $file = File::Spec->catfile($self->_dist_dir, @file);
251             if ( $file[-1] eq "docs.js" ) {
252             my $data;
253             eval { $data = read_file( $file ) };
254             _replace_template_vars(\$data, "root", $self->_root($c));
255             _replace_template_vars(\$data, "initial_module", $self->initial_module);
256             _replace_template_vars(\$data, "show_home_tab", $self->show_home_tab ? "true" : "false");
257             _replace_template_vars(\$data, "expand_module_tree_on_load", $self->expanded_module_tree ? "true" : "false");
258             $c->res->content_type('application/json');
259             $c->response->body($data);
260             } else {
261             $c->serve_static_file($file);
262             }
263             }
264              
265             # A poor man's template module.
266             sub _replace_template_vars {
267             my ($data_ref, $var_name, $var_val) = @_;
268             $$data_ref =~ s/\[% $var_name %\]/$var_val/g;
269             }
270              
271             1;
272              
273              
274              
275             =pod
276              
277             =head1 NAME
278              
279             Catalyst::Controller::POD - Serves PODs right from your Catalyst application
280              
281             =head1 VERSION
282              
283             version 1.0.0
284              
285             =head1 SYNOPSIS
286              
287             Create a new controller and paste this code:
288              
289             package MyApp::Controller::YourNewController; # <-- Change this to your controller
290            
291             use strict;
292             use warnings;
293             use base 'Catalyst::Controller::POD';
294             __PACKAGE__->config(
295             inc => 1,
296             namespaces => [qw(Catalyst::Manual*)],
297             self => 1,
298             dirs => [qw()]
299             );
300             1;
301              
302             =head1 DESCRIPTION
303              
304             This is a catalyst controller which serves PODs. It allows you to browse through your local
305             repository of modules. On the front page of this controller is a search box
306             which uses CPAN's xml interface to retrieve the results. If you click on one of them
307             the POD is displayed in this application.
308              
309             Cross links in PODs are resolved and pop up as a new tab. If the module you clicked on is
310             not installed this controller fetches the source code from CPAN and creates the pod locally.
311             There is also a TOC which is always visible and scrolls the current POD to the selected section.
312              
313             It is written using a JavaScript framework called ExtJS (L<http://www.extjs.com>) which
314             generate beautiful and intuitive interfaces.
315              
316             Have a look at L<http://cpan.org/authors/id/P/PE/PERLER/pod-images/pod-encyclopedia-01.png>.
317              
318             B<< L<Catalyst::Plugin::Static::Simple> is required and has to be loaded. >>
319              
320             =head1 CONFIGURATION
321              
322             =over
323              
324             =item dirs (Arrayref)
325              
326             Search for modules in these directories.
327              
328             Defaults to C<[]>.
329              
330             =item expanded_module_tree (Boolean)
331              
332             Expand the module browser tree on initial page load.
333              
334             Defaults to C<1>
335              
336             =item home_tab_content (String)
337              
338             HTML to be displayed in the Home tab.
339              
340             Defaults to the existing CPAN search box.
341              
342             =item inc (Boolean)
343              
344             Search for modules in @INC. Set it to 1 or 0.
345              
346             Defaults to C<0>.
347              
348             =item initial_module (String)
349              
350             If this option is specified, a tab displaying the perldoc for the given module
351             will be opened on load. Handy if you wish to disable the home tab and specify
352             a specific module's perldoc as the initial page a user sees.
353              
354             Defaults to C<"">
355              
356             =item namespaces (Arrayref)
357              
358             Filter by namespaces. See L<Pod::Simple::Search> C<limit_glob> for syntax.
359              
360             Defaults to C<["*"]>
361              
362             =item self (Boolean)
363              
364             Search for modules in C<< $c->path_to( 'lib' ) >>.
365              
366             Defaults to C<1>.
367              
368             =item show_home_tab (Boolean)
369              
370             Show or hide the home tab.
371              
372             Defaults to C<1>
373              
374             =head1 NOTICE
375              
376             This module works fine for most PODs but there are a few which do not get rendered properly.
377             Please report any bug you find. See L</BUGS>.
378              
379             Have a look at L<Pod::Browser> which is a catalyst application running this controller. You
380             can use it as a stand-alone POD server.
381              
382             =head1 TODO
383              
384             Write more tests!
385              
386             =head1 CONTRIBUTORS
387              
388             Tristan Pratt
389              
390             =cut
391              
392             =head1 AUTHOR
393              
394             Moritz Onken
395              
396             =head1 COPYRIGHT AND LICENSE
397              
398             This software is Copyright (c) 2011 by Moritz Onken.
399              
400             This is free software, licensed under:
401              
402             The (three-clause) BSD License
403              
404             =cut
405              
406              
407             __END__
408