diff --git a/lib/MetaCPAN/Web/Controller/Lab.pm b/lib/MetaCPAN/Web/Controller/Lab.pm
index d43fcf2263..8d5611f9d0 100644
--- a/lib/MetaCPAN/Web/Controller/Lab.pm
+++ b/lib/MetaCPAN/Web/Controller/Lab.pm
@@ -18,7 +18,8 @@ __PACKAGE__->config(
sub lab : Path : Args(0) {
my ( $self, $c ) = @_;
- $c->stash( template => 'lab.html' );
+ $c->res->redirect( '/tools', 301 );
+ $c->detach;
}
sub dependencies : Local : Args(0) : Does('Sortable') {
diff --git a/lib/MetaCPAN/Web/Controller/Pod.pm b/lib/MetaCPAN/Web/Controller/Pod.pm
index 1b990dd119..ec58cc432e 100644
--- a/lib/MetaCPAN/Web/Controller/Pod.pm
+++ b/lib/MetaCPAN/Web/Controller/Pod.pm
@@ -1,11 +1,13 @@
package MetaCPAN::Web::Controller::Pod;
use HTML::Restrict;
+use HTML::TokeParser;
use Moose;
use Try::Tiny;
use URI;
use HTML::Escape qw(escape_html);
use Future;
+use Encode qw( encode decode DIE_ON_ERR LEAVE_SRC );
use namespace::autoclean;
@@ -170,6 +172,64 @@ sub view : Private {
}
}
+sub pod2html : Path('/pod2html') {
+ my ( $self, $c ) = @_;
+ my $pod;
+ if ( my $pod_file = $c->req->upload('pod_file') ) {
+ my $raw_pod = $pod_file->slurp;
+ eval {
+ $pod = decode( 'UTF-8', $raw_pod, DIE_ON_ERR | LEAVE_SRC );
+ 1;
+ } or $pod = decode( 'cp1252', $raw_pod );
+ }
+ elsif ( $pod = $c->req->parameters->{pod} ) {
+ }
+ else {
+ return;
+ }
+
+ $c->stash( { pod => $pod } );
+
+ my $html
+ = $c->model('API')
+ ->request( 'pod_render', undef, { pod => encode( 'UTF-8', $pod ) },
+ 'POST' )->get->{raw};
+
+ $html = $self->filter_html($html);
+
+ if ( $c->req->parameters->{raw} ) {
+ $c->res->content_type('text/html');
+ $c->res->body($html);
+ $c->detach;
+ }
+ else {
+ my ( $pod_name, $abstract );
+ my $p = HTML::TokeParser->new( \$html );
+ while ( my $t = $p->get_token ) {
+ my ( $type, $tag, $attr ) = @$t;
+ if ( $type eq 'S'
+ && $tag eq 'h1'
+ && $attr->{id}
+ && $attr->{id} eq 'NAME' )
+ {
+ my $name_section = $p->get_trimmed_text('h1');
+ if ($name_section) {
+ ( $pod_name, $abstract )
+ = $name_section =~ /(?:NAME\s+)?([^-]+)\s*-\s*(.*)/s;
+ }
+ last;
+ }
+ }
+ $c->stash(
+ {
+ pod_rendered => $html,
+ ( $pod_name ? ( pod_name => $pod_name ) : () ),
+ ( $abstract ? ( abstract => $abstract ) : () ),
+ }
+ );
+ }
+}
+
sub filter_html {
my ( $self, $html, $data ) = @_;
@@ -236,7 +296,7 @@ sub filter_html {
# bad protocol
return '';
}
- else {
+ elsif ($data) {
my $base = "https://st.aticpan.org/source/";
if ( $val =~ s{^/}{} ) {
$base .= "$data->{author}/$data->{release}/";
@@ -247,6 +307,9 @@ sub filter_html {
}
$val = URI->new_abs( $val, $base )->as_string;
}
+ else {
+ $val = '/static/images/gray.png';
+ }
}
$tag .= qq{ $attr="} . escape_html($val) . qq{"};
}
diff --git a/lib/MetaCPAN/Web/Controller/Tools.pm b/lib/MetaCPAN/Web/Controller/Tools.pm
new file mode 100644
index 0000000000..ed97107c40
--- /dev/null
+++ b/lib/MetaCPAN/Web/Controller/Tools.pm
@@ -0,0 +1,14 @@
+package MetaCPAN::Web::Controller::Tools;
+
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'MetaCPAN::Web::Controller' }
+
+sub tools : Path : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash( template => 'tools.html' );
+}
+
+__PACKAGE__->meta->make_immutable;
+1;
diff --git a/lib/MetaCPAN/Web/Model/API.pm b/lib/MetaCPAN/Web/Model/API.pm
index 2604ee69b5..706a45e242 100644
--- a/lib/MetaCPAN/Web/Model/API.pm
+++ b/lib/MetaCPAN/Web/Model/API.pm
@@ -15,6 +15,8 @@ use URI;
use URI::QueryParam;
use MetaCPAN::Web::Types qw( Uri );
use Try::Tiny qw( catch try );
+use HTTP::Request;
+use HTTP::Request::Common ();
my $loop;
@@ -86,33 +88,38 @@ sub request {
my $url = $self->api_secure->clone;
+ $method ||= $search ? 'POST' : 'GET';
+
# the order of the following 2 lines matters
# `path_query` is destructive
$url->path_query($path);
- for my $param ( keys %{ $params || {} } ) {
- $url->query_param( $param => $params->{$param} );
- }
my $current_url = $self->request_uri;
my $request_id = $self->request_id;
+ if ( $method eq 'GET' || $search ) {
+ for my $param ( keys %{ $params || {} } ) {
+ $url->query_param( $param => $params->{$param} );
+ }
+ }
- my $request = HTTP::Request->new(
+ my $request = HTTP::Request::Common->can($method)->(
+ $url,
(
- $method ? $method
- : $search ? 'POST'
- : 'GET',
+ $search
+ ? (
+ 'Content-Type' => 'application/json',
+ 'Content' => encode_json($search),
+ )
+ : $method eq 'POST' && $params ? (
+ 'Content_Type' => 'multipart/form-data',
+ 'Content' => $params,
+ )
+ : ()
),
- $url,
- [
- ( $search ? ( 'Content-Type' => 'application/json' ) : () ),
- ( $current_url ? ( 'Referer' => $current_url->as_string ) : () ),
- ( $request_id ? ( 'X-MetaCPAN-Request-ID' => $request_id ) : () ),
- ],
+ ( $current_url ? ( 'Referer' => $current_url->as_string ) : () ),
+ ( $request_id ? ( 'X-MetaCPAN-Request-ID' => $request_id ) : () ),
);
- # encode_json returns an octet string
- $request->add_content( encode_json($search) ) if $search;
-
$self->client->do_request( request => $request )->transform(
done => sub {
my $response = shift;
diff --git a/root/lab/list.html b/root/inc/tools-bar.html
similarity index 50%
rename from root/lab/list.html
rename to root/inc/tools-bar.html
index 783cd1e78d..da0ddbd24b 100644
--- a/root/lab/list.html
+++ b/root/inc/tools-bar.html
@@ -1,12 +1,15 @@
-
LAB - YOU ARE IN A TESTING ZONE
-
-<% USE Markdown -%>
-<% FILTER markdown %>
-## Experimental features
-
-Try them and
let us know how to improve them.
-
-* Dashboard - a personalised dashboard (for logged in authors)
-* Dependencies - list the dependencies of a module
-
-<% END %>
-
-
-
diff --git a/root/lab/dashboard.html b/root/lab/dashboard.html
index 644be5b626..19a1d04f43 100644
--- a/root/lab/dashboard.html
+++ b/root/lab/dashboard.html
@@ -1,4 +1,4 @@
-<% PROCESS "lab/list.html" %>
+<% PROCESS "inc/tools-bar.html" %>
diff --git a/root/pod/pod2html.html b/root/pod/pod2html.html
new file mode 100644
index 0000000000..f47ec27020
--- /dev/null
+++ b/root/pod/pod2html.html
@@ -0,0 +1,33 @@
+<%
+ title = 'Pod Renderer' _ (pod_name ? ' - ' _ pod_name : '')
+%>
+<% PROCESS "inc/tools-bar.html" %>
+
+
+
style="display: none"<% END %>><% pod_rendered | none %>
+
diff --git a/root/static/images/gray.png b/root/static/images/gray.png
new file mode 100644
index 0000000000..9d5fcba809
Binary files /dev/null and b/root/static/images/gray.png differ
diff --git a/root/static/js/cpan.js b/root/static/js/cpan.js
index d2382b2d1f..25ec64c02d 100644
--- a/root/static/js/cpan.js
+++ b/root/static/js/cpan.js
@@ -262,13 +262,16 @@ $(document).ready(function() {
}
}
- $('.anchors').find('h1,h2,h3,h4,h5,h6,dt').each(function() {
- if (this.id) {
- $(document.createElement('a')).attr('href', '#' + this.id).addClass('anchor').append(
- $(document.createElement('span')).addClass('fa fa-bookmark black')
- ).prependTo(this);
- }
- });
+ function create_anchors(top) {
+ top.find('h1,h2,h3,h4,h5,h6,dt').each(function() {
+ if (this.id) {
+ $(document.createElement('a')).attr('href', '#' + this.id).addClass('anchor').append(
+ $(document.createElement('span')).addClass('fa fa-bookmark black')
+ ).prependTo(this);
+ }
+ });
+ }
+ create_anchors($('.anchors'));
var module_source_href = $('#source-link').attr('href');
if (module_source_href) {
@@ -302,8 +305,7 @@ $(document).ready(function() {
$('.dropdown-toggle').dropdown();
- var index = $("#index");
- if (index) {
+ function format_index(index) {
index.wrap('
');
var container = index.parent().parent();
@@ -328,6 +330,10 @@ $(document).ready(function() {
container.addClass("pull-right");
}
}
+ var index = $("#index");
+ if (index.length) {
+ format_index(index);
+ }
['right'].forEach(function(side) {
var panel = $('#' + side + "-panel");
@@ -358,6 +364,78 @@ $(document).ready(function() {
if (changes.prop('scrollHeight') > changes.height()) {
$("#last-changes-toggle").show();
}
+
+ var pod2html_form = $('#metacpan-pod-renderer-form');
+ var pod2html_text = $('[name="pod"]', pod2html_form);
+ var pod2html_update = function(pod) {
+ if (!pod) {
+ pod = pod2html_text.get(0).value;
+ }
+ var submit = pod2html_form.find('input[type="submit"]');
+ submit.attr("disabled", "disabled");
+ var rendered = $('#metacpan-pod-renderer-output');
+ var loading = $('#metacpan-pod-renderer-loading');
+ var error = $('#metacpan-pod-renderer-error');
+ rendered.hide();
+ rendered.html('');
+ loading.show();
+ error.hide();
+ document.title = "Pod Renderer - metacpan.org";
+ $.ajax({
+ url: '/pod2html',
+ method: 'POST',
+ data: {
+ pod: pod,
+ raw: true
+ },
+ success: function(data, stat, req) {
+ rendered.html(data);
+ loading.hide();
+ error.hide();
+ var res = $('#NAME + p').text().match(/^([^-]+?)\s*-\s*(.*)/);
+ if (res) {
+ var title = res[0];
+ var abstract = res[1];
+ document.title = "Pod Renderer - " + title + " - metacpan.org";
+ }
+ var index = $("#index", rendered);
+ if (index.length) {
+ format_index(index);
+ }
+ create_anchors(rendered);
+ rendered.show();
+ submit.removeAttr("disabled");
+ },
+ error: function(data, stat) {
+ rendered.hide();
+ loading.hide();
+ error.html('Error rendering POD' +
+ (data && data.length ? ' - ' + data : ''));
+ error.show();
+ submit.removeAttr("disabled");
+ }
+ });
+ };
+ if (window.FileReader) {
+ $('input[type="file"]', pod2html_form).on('change', function(e) {
+ var files = this.files;
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ pod2html_text.get(0).value = e.target.result;
+ pod2html_update(pod2html_text.get(0).value);
+ };
+ reader.readAsText(file);
+ }
+ this.value = null;
+ });
+ }
+ pod2html_form.on('submit', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ pod2html_update();
+ });
});
function set_page_size(selector, storage_name) {
diff --git a/root/static/less/pod2html.less b/root/static/less/pod2html.less
new file mode 100644
index 0000000000..c686f11061
--- /dev/null
+++ b/root/static/less/pod2html.less
@@ -0,0 +1,50 @@
+#metacpan-pod-renderer-pod {
+ font-family: @font-family-monospace;
+ width: 100%;
+ height: 200px;
+ border-radius: 5px;
+ color: #333;
+}
+
+#metacpan-pod-renderer-file {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ padding: 0;
+ margin: 0;
+ position: absolute;
+}
+
+.metacpan-pod-renderer {
+ overflow: auto;
+
+ .button-line {
+ text-align: right;
+ }
+
+ .form-group:last-child {
+ margin-bottom: 0px;
+ }
+
+ .alert {
+ margin-top: 15px;
+ margin-bottom: 0px;
+ }
+
+ .panel-heading {
+ .fa-chevron-down {
+ display: none;
+ }
+ .fa-chevron-up {
+ display: inline-block;
+ }
+ &.collapsed {
+ .fa-chevron-down {
+ display: inline-block;
+ }
+ .fa-chevron-up {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/root/static/less/style.less b/root/static/less/style.less
index 1b5ab621ea..cabec8fba7 100644
--- a/root/static/less/style.less
+++ b/root/static/less/style.less
@@ -27,6 +27,7 @@
@import "home.less";
@import "syntaxhighlighter.less";
@import "login.less";
+@import "pod2html.less";
@linkColor: #3366cc;
@textColor: @black;
diff --git a/root/tools.html b/root/tools.html
new file mode 100644
index 0000000000..91486af98b
--- /dev/null
+++ b/root/tools.html
@@ -0,0 +1,18 @@
+<% PROCESS "inc/tools-bar.html" %>
+
+
+
Assorted Tools
+
+
+ - Pod Renderer - Render arbitrary Pod to HTML through MetaCPAN.
+
+
+ Lab - Experimental features TESTING ZONE
+
+
Try them and let us know how to improve them.
+
+
+ - Dashboard - a personalised dashboard (for logged in authors)
+ - Dependencies - list the dependencies of a module
+
+
diff --git a/root/wrapper.html b/root/wrapper.html
index a2fdb101ae..c4a35da210 100644
--- a/root/wrapper.html
+++ b/root/wrapper.html
@@ -38,10 +38,10 @@
icon = "newspaper-o",
},
{
- title = "Lab",
- path = ["/lab"],
+ title = "Tools",
+ path = ["/tools"],
class = 'hidden-xs',
- icon = "rocket",
+ icon = "wrench",
},
{
title = "API",