From 43eca4c20e670659d3dc711010c05a83314859d7 Mon Sep 17 00:00:00 2001 From: Yorhel Date: Mon, 2 Jul 2012 16:23:43 +0200 Subject: [PATCH] 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. --- www/index.pl | 110 +++++++++++++++++++++++++++------------------------ www/man.css | 5 ++- www/man.js | 61 ++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 54 deletions(-) create mode 100644 www/man.js diff --git a/www/index.pl b/www/index.pl index d3b6e03..168ac61 100755 --- a/www/index.pl +++ b/www/index.pl @@ -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. | About manned.org | Contact'; 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'; diff --git a/www/man.css b/www/man.css index f48ef8f..949c9f1 100644 --- a/www/man.css +++ b/www/man.css @@ -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; diff --git a/www/man.js b/www/man.js new file mode 100644 index 0000000..e528555 --- /dev/null +++ b/www/man.js @@ -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', { }, ) -> 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 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); + } +} +