Generate nav bar in JS + various optimizations

This allows for a more dynamic nav bar without inserting insanely huge
HTML code in the page (as the previous version did in some cases) and
without having to contact the server again.

The 'intro' man page has around 1500 versions, and only generates a page
of ~52KiB (~9.5KiB after compression). The previous HTML version was
106KiB (~10.3KiB after compression). Page generation times have been
improved on the server side (by 50ms for the intro man page), but I've
no idea how significant the effect is of JS is. Feels fast enough,
though.
This commit is contained in:
Yorhel 2012-07-02 16:23:43 +02:00
parent 1f4328a72c
commit 43eca4c20e
3 changed files with 122 additions and 54 deletions

View file

@ -7,6 +7,7 @@ use IPC::Open2;
use IO::Select;
use Encode 'encode_utf8', 'decode_utf8';
use Time::HiRes 'tv_interval', 'gettimeofday';
use JSON::XS;
use Cwd 'abs_path';
our $ROOT;
@ -243,44 +244,6 @@ sub browsepkg {
}
sub manselect {
my($self, $lst, $selhash) = @_;
return if !@$lst;
$selhash ||= '';
my %sys;
push @{$sys{$_->{system}}}, $_ for (@$lst);
dl id => 'nav';
my $lastname = '';
for my $sys (sort { my $x=$self->{sysbyid}{$a}; my $y=$self->{sysbyid}{$b}; $x->{name} cmp $y->{name} or $y->{relorder} <=> $x->{relorder} } keys %sys) {
my %pkgs;
push @{$pkgs{"$_->{package}-$_->{version}"}}, $_ for @{$sys{$sys}};
dt $lastname eq $self->{sysbyid}{$sys}{name} ? (class => 'oldrelease') : (), $self->{sysbyid}{$sys}{full};
dd;
for my $pkg (sort { $pkgs{$a}[0]{package} cmp $pkgs{$b}[0]{package} || $pkgs{$b}[0]{released} cmp $pkgs{$a}[0]{released} } keys %pkgs) {
dl;
dt;
txt $pkgs{$pkg}[0]{package};
i $pkgs{$pkg}[0]{version};
end;
dd;
for my $man (sort { $a->{section} cmp $b->{section} || ($a->{locale}||'') cmp ($b->{locale}||'') } @{$pkgs{$pkg}}) {
my $t = $man->{locale} ? "$man->{section}.$man->{locale}" : $man->{section};
a href => sprintf('/%s/%s', $man->{name}, substr $man->{hash}, 0, 8), $t if $selhash ne $man->{hash};
b $t if $selhash eq $man->{hash};
}
end;
end;
}
end 'dd';
$lastname = $self->{sysbyid}{$sys}{name};
}
end 'dl';
}
# TODO: Store/cache the result of this of this function in the database.
sub manfmt {
my $c = shift;
@ -332,10 +295,48 @@ sub manfmt {
waitpid $pid, 0;
$ret = decode_utf8($ret);
$ret =~ s/[\t\s\r\n]+$//;
return $ret;
}
sub manjslist {
my($self, $m) = @_;
# For JS: (Already sorted)
# [
# ["System", "Full name", [
# [ "package", "version", [
# [ "section", "locale"||null ],
# ...
# ],
# ],
# ...
# ],
# ],
# ...
# ]
my %sys;
push @{$sys{$_->{system}}}, $_ for (@$m);
[
map [ $self->{sysbyid}{$_}{name}, $self->{sysbyid}{$_}{full},
do {
my %pkgs;
for(@{$sys{$_}}) {
my $pn = "$_->{package}-$_->{version}";
$pkgs{$pn} = [ $_->{package}, $_->{version}, [], $_->{released} ] if !$pkgs{$pn};
push @{$pkgs{$pn}[2]}, [ $_->{section}, $_->{locale}, substr $_->{hash}, 0, 8 ];
}
[ grep
delete($_->[3]) && ($_->[2] = [sort { $a->[0] cmp $b->[0] || ($a->[1]||'') cmp ($b->[1]||'') } @{$_->[2]}]),
sort { $a->[0] cmp $b->[0] || $b->[3] cmp $a->[3] } values %pkgs ];
}
],
sort { my $x=$self->{sysbyid}{$a}; my $y=$self->{sysbyid}{$b}; $x->{name} cmp $y->{name} or $y->{relorder} <=> $x->{relorder} } keys %sys
]
}
# Given the name and optionally the hash of a man page, check with a list of
# man pages with the same name to select the right one for display.
sub getman {
@ -348,8 +349,9 @@ sub getman {
$_->{hash} =~ /^$hash/ && return $_ for (@$list);
}
# If that failed, sort the list based on some heuristics.
my @l = sort {
# If that failed, use some heuristics
my $cmp = sub {
local($a,$b) = @_;
# English or non-locale packages always win
!(($a->{locale}||'') =~ /^(en|$)/) != !(($b->{locale}||'') =~ /^(en|$)/)
? (($a->{locale}||'') =~ /^(en|$)/ ? -1 : 1)
@ -376,9 +378,11 @@ sub getman {
? $a->{section} cmp $b->{section}
# Fallback to hash if nothing else matters (guarantees the order is at least stable)
: $a->{hash} cmp $b->{hash};
} @$list;
};
return $l[0];
my $winner = $list->[0];
$cmp->($winner, $_) > 0 and ($winner = $_) for (@$list);
return $winner;
}
@ -390,7 +394,7 @@ sub man {
my $man = getman($self, $name, $hash, $m);
$self->htmlHeader(title => $name);
manselect $self, $m, $man->{hash};
dl id => 'nav', ' '; # To be filled in by JS
h1 $man->{name};
p;
@ -401,8 +405,7 @@ sub man {
div id => 'contents';
my $c = $self->dbManContent($man->{hash});
($c = GrottyParser::html(manfmt $c)) =~ s/[\t\s\r\n]+$//; # TODO: <- Do this in GrottyParser
pre; lit $c; end;
pre; lit GrottyParser::html(manfmt $c); end;
end;
div id => 'locations';
@ -437,7 +440,7 @@ sub man {
end;
end;
$self->htmlFooter;
$self->htmlFooter(js => { hash => substr($man->{hash}, 0, 8), name => $man->{name}, mans => manjslist($self, $m) });
}
@ -465,11 +468,6 @@ sub htmlHeader {
html;
head;
Link rel => 'stylesheet', type => 'text/css', href => '/man.css';
style type => 'text/css';
lit 'thead tr { font-weight: bold; border-bottom: 1px solid #ccc }';
lit 'table td { border-left: 1px solid #ccc; padding: 0 3px }';
lit 'table { border-collapse: collapse }';
end;
title $o{title}.' - manned.org';
end 'head';
body;
@ -487,7 +485,7 @@ sub htmlHeader {
sub htmlFooter {
my $self = shift;
my($self, %o) = @_;
br style => 'clear: both';
end;
@ -495,6 +493,14 @@ sub htmlFooter {
lit 'All manual pages are copyrighted by their respective authors.
| <a href="/info/about">About manned.org</a> | <a href="mailto:contact@manned.org">Contact</a>';
end;
if($o{js}) {
script type => 'text/javascript';
lit 'VARS = ';
lit(JSON::XS->new->ascii->encode($o{js}));
lit ';';
end;
}
script type => 'text/javascript', src => '/man.js', '';
end;
end 'html';

View file

@ -16,8 +16,9 @@ dd { margin-left: 15px; }
a { color: #048; font-family: "Verdana"; font-weight: normal; text-decoration: underline; padding: 3px 5px;
-webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; }
a:hover { text-decoration: none; background: #cde; }
table { background: #eee; border: 5px solid #f8f8f8; margin: 10px 0; }
td { padding: 1px 5px; font-size: 12px; }
table { background: #eee; border: 5px solid #f8f8f8; margin: 10px 0; border-collapse: collapse; }
thead tr { font-weight: bold; border-bottom: 1px solid #ccc }
td { padding: 1px 5px; font-size: 12px; border-left: 1px solid #ccc; }
#header { padding: 4px 20px; border-bottom: 1px solid #888; font: 24px "Arial";
-webkit-border-radius: 8px 8px 0 0; -moz-border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;

61
www/man.js Normal file
View file

@ -0,0 +1,61 @@
/* The following functions are part of a minimal JS library I wrote for VNDB.org */
function byId(n) {
return document.getElementById(n)
}
/* wrapper around DOM element creation
* tag('string') -> createTextNode
* tag('tagname', tag(), 'string', ..) -> createElement(), appendChild(), ..
* tag('tagname', { class: 'meh', title: 'Title' }) -> createElement(), setAttribute()..
* tag('tagname', { <attributes> }, <elements>) -> create, setattr, append */
function tag() {
if(arguments.length == 1)
return typeof arguments[0] != 'object' ? document.createTextNode(arguments[0]) : arguments[0];
var el = typeof document.createElementNS != 'undefined'
? document.createElementNS('http://www.w3.org/1999/xhtml', arguments[0])
: document.createElement(arguments[0]);
for(var i=1; i<arguments.length; i++) {
if(arguments[i] == null)
continue;
if(typeof arguments[i] == 'object' && !arguments[i].appendChild) {
for(attr in arguments[i]) {
if(attr == 'style')
el.setAttribute(attr, arguments[i][attr]);
else
el[ attr == 'class' ? 'className' : attr == 'for' ? 'htmlFor' : attr ] = arguments[i][attr];
}
} else
el.appendChild(tag(arguments[i]));
}
return el;
}
/* What follows is specific to manned.org */
if(byId('nav')) {
var nav = byId('nav');
for(var i=0; i<VARS.mans.length; i++) {
var dt = tag('dt', VARS.mans[i][1]);
var dd = tag('dd', null);
for(var j=0; j<VARS.mans[i][2].length; j++) {
var pkg = VARS.mans[i][2][j];
var pdt = tag('dt', pkg[0], tag('i', pkg[1]));
var pdd = tag('dd', null);
for(var k=0; k<pkg[2].length; k++) {
var man = pkg[2][k];
var txt = man[0] + (man[1] ? '.'+man[1] : '');
if(k > 0)
pdd.appendChild(tag(' '));
pdd.appendChild(man[2] == VARS.hash ? tag('b', txt) : tag('a', {href:'/'+VARS.name+'/'+man[2]}, txt));
}
dd.appendChild(pdt);
dd.appendChild(pdd);
}
nav.appendChild(dt);
nav.appendChild(dd);
}
}