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 @@
diff --git a/root/lab.html b/root/lab.html deleted file mode 100644 index 2c052fd9f1..0000000000 --- a/root/lab.html +++ /dev/null @@ -1,18 +0,0 @@ -<% PROCESS "lab/list.html" %> - -
-

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 %>> + Error rendering POD<% IF pod_error; ' - ' _ pod_error; END %> +
+
+
+
+
+
+
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

+ + + +

Lab - Experimental features TESTING ZONE

+ +

Try them and let us know how to improve them.

+ + +
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",