From ae763a5fbe0fd27ba74eccc93599e9f24cc8a146 Mon Sep 17 00:00:00 2001 From: Graham Knop Date: Wed, 17 Jan 2024 15:41:53 +0100 Subject: [PATCH] split table of contents from pod content Split the index from the rest of the pod content, allowing it to be templated differently. This allows its markup to be included with the static content, rather than being assembled in javascript. This should make it easier to format it differently in the future, and should also fix the scroll position changing on page load. --- lib/MetaCPAN/Web/Controller/Pod.pm | 1 + lib/MetaCPAN/Web/Controller/Pod2HTML.pm | 24 +++-- lib/MetaCPAN/Web/Model/API/Author.pm | 2 + lib/MetaCPAN/Web/Model/API/Pod.pm | 7 +- lib/MetaCPAN/Web/RenderUtil.pm | 17 +++- root/pod.tx | 7 +- root/pod2html/pod2html.tx | 7 +- root/static/js/cpan.js | 115 +++++++++++++----------- root/static/less/pod.less | 80 ++++++----------- 9 files changed, 141 insertions(+), 119 deletions(-) diff --git a/lib/MetaCPAN/Web/Controller/Pod.pm b/lib/MetaCPAN/Web/Controller/Pod.pm index 35226b9ba6b..bb683bfd1af 100644 --- a/lib/MetaCPAN/Web/Controller/Pod.pm +++ b/lib/MetaCPAN/Web/Controller/Pod.pm @@ -79,6 +79,7 @@ sub view : Private { canonical => $canonical, documented_module => $documented_module, pod => $pod->{pod_html}, + pod_index => $pod->{pod_index}, template => 'pod.tx', } ); diff --git a/lib/MetaCPAN/Web/Controller/Pod2HTML.pm b/lib/MetaCPAN/Web/Controller/Pod2HTML.pm index 45deaa90a33..564b0f022fa 100644 --- a/lib/MetaCPAN/Web/Controller/Pod2HTML.pm +++ b/lib/MetaCPAN/Web/Controller/Pod2HTML.pm @@ -30,20 +30,30 @@ sub pod2html : Path : Args(0) { show_errors => 1, } )->get; - my $html = $pod_data->{pod_html} // q{}; + my $html = $pod_data->{pod_html} // q{}; + my $index = $pod_data->{pod_index} // q{}; my $results = { - pod => $pod, - pod_rendered => $html, - pod_name => $pod_data->{pod_name}, - abstract => $pod_data->{abstract}, + pod => $pod, + pod_html => $html, + pod_index => $index, + pod_name => $pod_data->{pod_name}, + abstract => $pod_data->{abstract}, }; if ( $c->req->parameters->{raw} ) { $c->res->content_type('text/html'); - $c->res->body($html); + $c->res->body( '' . $html ); $c->detach; } - $c->stash($results); + elsif ( $c->req->accepts('application/json') ) { + $c->stash( { + current_view => 'JSON', + json => $results, + } ); + } + else { + $c->stash($results); + } } __PACKAGE__->meta->make_immutable; diff --git a/lib/MetaCPAN/Web/Model/API/Author.pm b/lib/MetaCPAN/Web/Model/API/Author.pm index 8d0bc280d1f..fd0319d3cd6 100644 --- a/lib/MetaCPAN/Web/Model/API/Author.pm +++ b/lib/MetaCPAN/Web/Model/API/Author.pm @@ -66,6 +66,8 @@ sub get { sub get_multiple { my ( $self, @authors ) = @_; + return Future->done( { took => 0, total => 0, authors => [] } ) + if !@authors; return $self->request( '/author/by_ids', { id => [ map uc, @authors ] } ) ->transform( done => sub { diff --git a/lib/MetaCPAN/Web/Model/API/Pod.pm b/lib/MetaCPAN/Web/Model/API/Pod.pm index 9c2fa4d7e51..523fb79b8ee 100644 --- a/lib/MetaCPAN/Web/Model/API/Pod.pm +++ b/lib/MetaCPAN/Web/Model/API/Pod.pm @@ -2,7 +2,7 @@ package MetaCPAN::Web::Model::API::Pod; use Moose; use namespace::autoclean; -use MetaCPAN::Web::RenderUtil qw( filter_html ); +use MetaCPAN::Web::RenderUtil qw( split_index filter_html ); use Encode qw( encode ); use Future (); @@ -75,8 +75,11 @@ sub _with_filter { sub { my $data = shift; if ( my $raw = $data->{raw} ) { + my ( $index, $body ) = split_index($raw); + $data->{pod_index} + = filter_html( $index, $data->{path} ? $data : () ); $data->{pod_html} - = filter_html( $raw, $data->{path} ? $data : () ); + = filter_html( $body, $data->{path} ? $data : () ); } return Future->done($data); }; diff --git a/lib/MetaCPAN/Web/RenderUtil.pm b/lib/MetaCPAN/Web/RenderUtil.pm index bc6cca58b92..ccf5ab99074 100644 --- a/lib/MetaCPAN/Web/RenderUtil.pm +++ b/lib/MetaCPAN/Web/RenderUtil.pm @@ -12,6 +12,7 @@ use Digest::MD5 (); our @EXPORT_OK = qw( filter_html gravatar_image + split_index ); sub filter_html { @@ -41,6 +42,7 @@ sub filter_html { i => [], li => ['id'], ol => [], + nav => [], p => [], pre => [ { class => qr/^line-numbers$/, @@ -59,7 +61,7 @@ sub filter_html { td => [qw( colspan rowspan )], tr => [], u => [], - ul => [ { id => qr/^index$/ } ], + ul => [], # # SVG tags. @@ -146,4 +148,17 @@ sub gravatar_image { $grav_id, $size // 80; } +sub split_index { + my ($html) = @_; + + # this will hopefully be done by the API in the future + $html =~ s{\A\n?)}{}ms; + + # both of these regexes are kind of ugly, but we know the content produced + # by the API, so it should still work fine. + $html =~ s{\A\n*}{}s; + my $pod_index = $1; + return ( $pod_index, $html ); +} + 1; diff --git a/root/pod.tx b/root/pod.tx index d67671a1ce1..a319517b609 100644 --- a/root/pod.tx +++ b/root/pod.tx @@ -34,8 +34,13 @@ %% } %% override page_content -> { +%% if $pod_index { + +%% }
- %% if $pod { %% $pod | mark_raw; %% } diff --git a/root/pod2html/pod2html.tx b/root/pod2html/pod2html.tx index 19926748efe..7b9b3986140 100644 --- a/root/pod2html/pod2html.tx +++ b/root/pod2html/pod2html.tx @@ -32,6 +32,11 @@
-
[% $pod_rendered | raw %]
+
+ + [% $pod_html | raw %]
%% } diff --git a/root/static/js/cpan.js b/root/static/js/cpan.js index 8634db39e7a..dc8866edd68 100644 --- a/root/static/js/cpan.js +++ b/root/static/js/cpan.js @@ -51,32 +51,6 @@ function togglePanel(side, visible) { return false; } -function toggleTOC() { - var container = $('#index-container'); - if (container.length == 0) return false; - var visible = !container.hasClass('hide-index'); - var index = $('#index'); - var newHeight = 0; - if (!visible) { - newHeight = index.get(0).scrollHeight; - } - index.animate({ - height: newHeight - }, { - duration: 200, - complete: function() { - if (newHeight > 0) { - index.css({ - height: 'auto' - }); - } - } - }); - MetaCPAN.storage.setItem('hideTOC', (visible ? 1 : 0)); - container.toggleClass('hide-index'); - return false; -} - function setFavTitle(button) { button.attr('title', button.hasClass('active') ? 'Remove from favorites' : 'Add to favorites'); return; @@ -298,25 +272,54 @@ $(document).ready(function() { $('.dropdown-toggle').dropdown(); - function format_index(index) { - index.wrap('
'); - var container = index.parent().parent(); + function format_toc(toc) { + 'use strict'; + if (MetaCPAN.storage.getItem('hideTOC') == 1) { + toc.classList.add("hide-toc"); + } - var index_hidden = MetaCPAN.storage.getItem('hideTOC') == 1; - index.before( - '
Contents' + ' [ ]
'); + const toc_header = toc.querySelector('.toc-header'); + const toc_body = toc.querySelector('ul'); - $('.toggle-index').on('click', function(e) { + toc_header.insertAdjacentHTML('beforeend', + ' [ ]' + ); + toc_header.querySelector('.toggle-toc').addEventListener('click', e => { e.preventDefault(); - toggleTOC(); + const currentVisible = !toc.classList.contains('hide-toc'); + MetaCPAN.storage.setItem('hideTOC', currentVisible ? 1 : 0); + + const fullHeight = toc_body.scrollHeight; + + if (currentVisible) { + const trans = toc_body.style.transition; + toc_body.style.transition = ''; + + requestAnimationFrame(() => { + toc_body.style.height = fullHeight + 'px'; + toc_body.style.transition = trans; + toc.classList.toggle('hide-toc'); + + requestAnimationFrame(() => { + toc_body.style.height = null; + }); + }); + } else { + const finish = e => { + toc_body.removeEventListener('transitionend', finish); + toc_body.style.height = null; + }; + + toc_body.addEventListener('transitionend', finish); + toc_body.style.height = fullHeight + 'px'; + toc.classList.toggle('hide-toc'); + } }); - if (index_hidden) { - container.addClass("hide-index"); - } } - var index = $("#index"); - if (index.length) { - format_index(index); + + let toc = document.querySelector(".content .toc") + if (toc) { + format_toc(toc); } $('a[href*="/search?"]').on('click', function() { @@ -366,24 +369,28 @@ $(document).ready(function() { url: '/pod2html', method: 'POST', data: { - pod: pod, - raw: true + pod: pod + }, + headers: { + Accept: "application/json" }, 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); + var title = data.pod_title; + var abstract = data.pod_abstract; + document.title = "Pod Renderer - " + title + " - metacpan.org"; + rendered.html( + '' + + data.pod_html + ); + var toc = $("nav", rendered); + if (toc.length) { + format_toc(toc[0]); } create_anchors(rendered); + loading.hide(); + error.hide(); rendered.show(); submit.removeAttr("disabled"); }, diff --git a/root/static/less/pod.less b/root/static/less/pod.less index b93ad6cd9c3..960c3cd7d69 100644 --- a/root/static/less/pod.less +++ b/root/static/less/pod.less @@ -84,89 +84,63 @@ } } -ul#index, #index ul { - padding: 0; - margin: 0; - list-style-type: none; +.content .toc { + display: inline-block; + border: 1px solid rgb(215, 215, 215); + padding: 3px 10px; - ul { - margin-left: 1.5em; - } - - li { - margin-bottom: 0; - margin-top: 0.4em; - line-height: 1.3em; - } -} - -#index-container { + overflow-x: auto; + overflow-y: hidden; margin-bottom: 40px; @media (min-width: @screen-md-max) { float: right; margin-left: 50px; + max-width: 400px; } - overflow-y: hidden; - - .index-header { + .toc-header { text-align: center; line-height: 24px; } - .index-border { - display: inline-block; - border: 1px solid rgb(215, 215, 215); - padding: 3px 10px; + > ul { + overflow-y: hidden; + transition: height 0.3s ease-out; } - #index { - overflow-y: hidden; + ul { + padding: 0; + margin: 0; + list-style-type: none; + + ul { + margin-left: 1.5em; + } + + li { + margin-bottom: 0; + margin-top: 0.4em; + line-height: 1.3em; + } } .toggle-show { display: none; } - &.hide-index { + &.hide-toc { .toggle-hide { display: none; } .toggle-show { display: inline; } - #index { + > ul { height: 0; } } - .toggle-index-right { - color: #000; - float: right; - border: 0; - margin: -6px -7px 0 -7px; - } - - .toggle-left { - display: none; - } - - &.pull-right { - .index-border { - margin-left: 5px; - max-width: 225px; - overflow-x: auto; - } - - .toggle-right { - display: none; - visibility: collapse; - } - .toggle-left { - display: inline-block; - } - } } .pod p.pod-error {