Complete revamp of navigation menu on man pages
This removes the navigation menu on the right, leaving more space for the actual contents. Instead, there are now a few links/tabs at the top of the page. There's also a 'permalink' now. The previous navigation combined the selection of man page versions, translations and sections in a single menu. While handy in some cases, in most cases it was just slow and messy. It also didn't scale very well, some man pages have so many versions that it significantly affected the page load time. The 'locations' table has now also been moved into tab and is loaded asynchronously as well, for the same performance reasons. I had hoped that this new navigation would be much easier and more convenient, but honestly, it's still a mess. At least the new code is more maintainable, so perhaps I'll be able to make some incremental improvements in the future.
This commit is contained in:
parent
3f40896679
commit
20daba820f
3 changed files with 393 additions and 320 deletions
277
www/index.pl
277
www/index.pl
|
|
@ -67,10 +67,21 @@ TUWF::register(
|
|||
$self->resRedirect("/pkg/$sys->{short}/$pkgs->[0]{category}/$name".($ver ? "/$ver" :''), 'perm');
|
||||
},
|
||||
|
||||
qr{xml/search\.xml} => \&xmlsearch,
|
||||
qr{([^/]+)/([0-9a-f]{8})} => \&man,
|
||||
qr{([^/]+)/([0-9a-f]{8})/src} => \&src,
|
||||
qr{([^/]+)} => \&man,
|
||||
|
||||
# Redirect for a specific language for a man page.
|
||||
# I'm not a fan of this solution; might drop it in the future.
|
||||
qr{lang/([^/]+)/([^/]+)} => sub {
|
||||
my($s, $l, $n) = @_;
|
||||
my($m, undef) = $s->dbManPrefName($n, language => $l);
|
||||
return $s->resNotFound if !$m;
|
||||
$s->resRedirect("/$m->{name}/".substr($m->{hash}, 0, 8), 'temp');
|
||||
},
|
||||
|
||||
qr{xml/search\.xml} => \&xmlsearch,
|
||||
qr{json/tree\.json} => \&jsontree,
|
||||
);
|
||||
|
||||
TUWF::run();
|
||||
|
|
@ -441,7 +452,7 @@ sub pkg_info {
|
|||
my $f = $self->formValidate({ get => 's', required => 0});
|
||||
return $self->resNotFound if $f->{_err};
|
||||
|
||||
my $mans = $self->dbManInfo(package => $sel->{id}, results => 201, start => $f->{s}, sort => 'name');
|
||||
my $mans = $self->dbManInfo(package => $sel->{id}, results => 201, start => $f->{s}, sort => 'syspkgname');
|
||||
my $more = @$mans > 200 && pop @$mans;
|
||||
|
||||
# Latest version of this package determines last modification date of the page.
|
||||
|
|
@ -520,28 +531,42 @@ sub man_redir {
|
|||
};
|
||||
|
||||
|
||||
sub manjslist {
|
||||
my($self, $m) = @_;
|
||||
sub _man_langsect {
|
||||
my($self, $man) = @_;
|
||||
|
||||
# The structure we generate is described in the JS code.
|
||||
my %sys;
|
||||
push @{$sys{$_->{system}}}, $_ for (@$m);
|
||||
[
|
||||
map [ $self->{sysbyid}{$_}{name}, $self->{sysbyid}{$_}{full}, $self->{sysbyid}{$_}{short},
|
||||
do {
|
||||
my %pkgs;
|
||||
for(@{$sys{$_}}) {
|
||||
my $pn = "$_->{category}-$_->{package}-$_->{version}";
|
||||
$pkgs{$pn} = [ $_->{category}, $_->{package}, $_->{version}, [], $_->{released} ] if !$pkgs{$pn};
|
||||
push @{$pkgs{$pn}[3]}, [ $_->{section}, $_->{locale}, substr $_->{hash}, 0, 8 ];
|
||||
}
|
||||
[ grep
|
||||
delete($_->[4]) && ($_->[3] = [sort { $a->[0] cmp $b->[0] || ($a->[1]||'') cmp ($b->[1]||'') } @{$_->[3]}]),
|
||||
sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] || $b->[4] cmp $a->[4] } 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
|
||||
]
|
||||
# TODO: This is ugly, especially because clicking on a translation or
|
||||
# section, you can end up with a man page that is nowhere close to the man
|
||||
# page you're currently reading. Opening a version selector box might be a
|
||||
# better alternative.
|
||||
|
||||
my @sect = $self->dbManSections($man->{name});
|
||||
if(@sect > 1) {
|
||||
div id => 'sectionselect', class => 'hidden';
|
||||
for (@sect) {
|
||||
if($man->{section} eq $_) {
|
||||
i $_;
|
||||
} else {
|
||||
a href => "/$man->{name}.$_", $_;
|
||||
}
|
||||
txt ' ';
|
||||
}
|
||||
end;
|
||||
}
|
||||
|
||||
my @lang = $self->dbManLanguages($man->{name}, $man->{section});
|
||||
if(@lang > 1) {
|
||||
div id => 'langselect', class => 'hidden';
|
||||
(my $cur = $man->{locale}||'') =~ s/\..*//;
|
||||
for (@lang) {
|
||||
if(($_||'') eq $cur) {
|
||||
i $_ || 'default';
|
||||
} else {
|
||||
a href => $_ ? "/lang/$_/$man->{name}.$man->{section}" : "/$man->{name}.$man->{section}", $_ || 'default';
|
||||
}
|
||||
txt ' ';
|
||||
}
|
||||
end;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -560,63 +585,25 @@ sub man {
|
|||
}
|
||||
return $self->resNotFound() if !$man;
|
||||
|
||||
my $view = $self->formValidate({get => 'v', regex => qr/^[a-z2-7]+$/});
|
||||
$view = $view->{_err} ? '' : $view->{v};
|
||||
|
||||
# To be really correct, the last modification time of this page should be the
|
||||
# release date of the latest version of the man page, as that is displayed in
|
||||
# the menu. But let's just consider the content of the page as more
|
||||
# important, and use release date of the man page as last modification date.
|
||||
$self->setLastMod($man->{released});
|
||||
|
||||
$self->htmlHeader(title => $name);
|
||||
div id => 'nav', 'Sorry, this navigation menu won\'t display without Javascript. :-(';
|
||||
|
||||
h1;
|
||||
txt $man->{name}.' ';
|
||||
a href => "/$man->{name}/".substr($man->{hash}, 0, 8).'/src', 'source';
|
||||
div id => 'manbuttons';
|
||||
h1 $man->{name};
|
||||
ul 'data-hash' => $man->{hash}, 'data-name' => $man->{name}, 'data-section' => $man->{section}, 'data-locale' => $man->{locale}||'',
|
||||
'data-hasversions' => $self->dbManHasVersions($man->{name}, $man->{section}, $man->{locale}, $man->{hash});
|
||||
li; a href => "/$man->{name}/".substr($man->{hash}, 0, 8).'/src', 'source'; end;
|
||||
li; a href => "/$man->{name}/".substr($man->{hash}, 0, 8), 'permalink'; end;
|
||||
end;
|
||||
end;
|
||||
div id => 'manres', class => 'hidden';
|
||||
_man_langsect($self, $man);
|
||||
end;
|
||||
|
||||
div id => 'contents';
|
||||
my $c = $self->dbManContent($man->{hash});
|
||||
# TODO: Store/cache the result of fmt() in the database.
|
||||
pre; lit ManUtils::html(ManUtils::fmt_block $c); end;
|
||||
end;
|
||||
|
||||
div id => 'locations';
|
||||
h2 'Locations of this man page';
|
||||
table;
|
||||
thead; Tr;
|
||||
td 'System';
|
||||
td 'Package';
|
||||
td 'Version';
|
||||
td 'Name';
|
||||
td 'Filename';
|
||||
end; end;
|
||||
my @l = sort {
|
||||
$self->{sysbyid}{$a->{system}}{name} cmp $self->{sysbyid}{$b->{system}}{name}
|
||||
|| $self->{sysbyid}{$b->{system}}{relorder} <=> $self->{sysbyid}{$a->{system}}{relorder}
|
||||
|| $b->{released} cmp $a->{released}
|
||||
|| $a->{filename} cmp $b->{filename}
|
||||
} @{$self->dbManInfo(hash => $man->{hash})};
|
||||
for(@l) {
|
||||
Tr;
|
||||
td $self->{sysbyid}{$_->{system}}{full};
|
||||
td "$_->{category}/$_->{package}";
|
||||
td $_->{version};
|
||||
td;
|
||||
a href => "/$_->{name}", $_->{name} if $_->{name} ne $man->{name};
|
||||
txt $_->{name} if $_->{name} eq $man->{name};
|
||||
txt ".$_->{section}";
|
||||
end;
|
||||
td $_->{filename};
|
||||
end;
|
||||
}
|
||||
end;
|
||||
end;
|
||||
|
||||
my $m = $self->dbManInfo(name => $man->{name});
|
||||
$self->htmlFooter(js => { hash => substr($man->{hash}, 0, 8), name => $man->{name}, view => $view, mans => manjslist($self, $m) });
|
||||
$self->htmlFooter();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -648,6 +635,93 @@ sub xmlsearch {
|
|||
}
|
||||
|
||||
|
||||
sub jsontree {
|
||||
my $self = shift;
|
||||
|
||||
my $f = $self->formValidate(
|
||||
{ get => 'name', required => 0, maxlength => 256 },
|
||||
{ get => 'section', required => 0, maxlength => 32 },
|
||||
{ get => 'locale', required => 0, default => '', maxlength => 32 },
|
||||
{ get => 'cur', required => 0, default => '', regex => qr/^[a-fA-F0-9]{40}$/ },
|
||||
{ get => 'hash', required => 0, default => '', regex => qr/^[a-fA-F0-9]{40}$/ },
|
||||
);
|
||||
return $self->resNotFound() if $f->{_err} || (!$f->{hash} && !($f->{section} && $f->{name}));
|
||||
|
||||
my $l = $self->dbManInfo(sort => 'syspkgname', $f->{hash}
|
||||
? (hash => $f->{hash})
|
||||
: (name => $f->{name}, section => $f->{section}, locale => $f->{locale}));
|
||||
|
||||
# Convert the list into a tree
|
||||
my $tree = [];
|
||||
my($sys, $sysver, $pkg, $pkgver);
|
||||
for my $m (@$l) {
|
||||
my $sysname = $self->{sysbyid}{$m->{system}}{name};
|
||||
if(!$sys || $sysname ne $sys->{name}) {
|
||||
$sys = { name => $sysname, childs => [] };
|
||||
$sysver = undef;
|
||||
push @$tree, $sys;
|
||||
}
|
||||
|
||||
my $sysversion = $self->{sysbyid}{$m->{system}}{release} || '';
|
||||
if(!$sysver || $sysversion ne $sysver->{name}) {
|
||||
$sysver = { name => $sysversion, childs => [] };
|
||||
$pkg = undef;
|
||||
push @{$sys->{childs}}, $sysver;
|
||||
}
|
||||
|
||||
if(!$pkg || $m->{package} ne $pkg->{name}) {
|
||||
$pkg = { name => $m->{package}, i => $m->{category}, table => [] };
|
||||
$pkgver = undef;
|
||||
push @{$sysver->{childs}}, $pkg;
|
||||
}
|
||||
|
||||
push @{$pkg->{table}}, [
|
||||
$pkgver && $pkgver eq $m->{version} ? {name=>''} :
|
||||
{name => $m->{version}, href => "/pkg/$self->{sysbyid}{$m->{system}}{short}/$m->{category}/$m->{package}/$m->{version}"},
|
||||
{ name => "$m->{name}($m->{section})",
|
||||
$f->{hash} || lc($m->{hash}) eq lc($f->{cur}) ? ()
|
||||
: (href => sprintf('/%s/%s', $m->{name}, substr $m->{hash}, 0, 8))
|
||||
},
|
||||
{ name => substr($m->{hash}, 0, 8),
|
||||
$f->{hash} || lc($m->{hash}) eq lc($f->{cur}) ? ()
|
||||
: (href => sprintf('/%s/%s', $m->{name}, substr $m->{hash}, 0, 8))
|
||||
},
|
||||
{ name => $m->{filename} }
|
||||
];
|
||||
$pkgver = $m->{version};
|
||||
}
|
||||
|
||||
# Determine which elements to show/hide by default.
|
||||
# It might make more sense to do this in JS, but since I am utterly
|
||||
# incapable of writing maintainable JS I'm doing it here in order to keep the
|
||||
# JS stupid and simple.
|
||||
# TODO: Highlight systems/packages where the 'current' man page is?
|
||||
for my $sys (@$tree) {
|
||||
$sys->{expand} = 1 if $sys->{childs}[0]{name}; # Expand all systems that have named versions
|
||||
$sys->{expand} = 1 if $f->{hash}; # Expand everything on 'location'
|
||||
|
||||
my $i = 0;
|
||||
for my $sysver (@{$sys->{childs}}) {
|
||||
$i++;
|
||||
$sysver->{expand} = 1 if !$sysver->{name}; # Expand unnamed versions (since you can't click them)
|
||||
$sysver->{expand} = 1 if $f->{hash}; # Expand everything on 'location'
|
||||
$sysver->{hide} = 1 if $i > 3 && @{$sys->{childs}} > 5; # Show only the first 3 versions
|
||||
|
||||
for my $pkg (@{$sysver->{childs}}) {
|
||||
$pkg->{expand} = 1 if @{$sysver->{childs}} <= 3; # Expand everything if there's not too many things to expand
|
||||
$pkg->{expand} = 1 if $f->{hash}; # Expand everything on 'location'
|
||||
|
||||
# TODO: Show/Hide duplicate hashes?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Why JSON? Because TUWF::XML is pretty slow with many nodes
|
||||
$self->resHeader('Content-Type' => 'application/json; charset=UTF-8');
|
||||
lit(JSON::XS->new->ascii->encode($tree));
|
||||
}
|
||||
|
||||
|
||||
|
||||
package TUWF::Object;
|
||||
|
||||
|
|
@ -694,13 +768,6 @@ sub htmlFooter {
|
|||
| <a href="mailto:contact@manned.org">Contact</a>
|
||||
| <a href="https://g.blicky.net/manned.git/">Source</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';
|
||||
|
|
@ -746,26 +813,28 @@ sub dbManInfo {
|
|||
my %where = (
|
||||
$o{name} ? ('m.name = ?' => $o{name}) : (),
|
||||
$o{package} ? ('m.package = ?' => $o{package}) : (),
|
||||
$o{section} ? ('m.section = ?' => $o{section}) : (),
|
||||
defined($o{section}) ? ('m.section = ?' => $o{section}) : (),
|
||||
$o{locale} ? ('m.locale = ?' => $o{locale}) : (),
|
||||
defined($o{locale}) && !$o{locale} ? ('m.locale IS NULL' => 1) : (),
|
||||
$o{shorthash} ? (q{substring(m.hash from 1 for 4) = decode(?, 'hex')} => $o{shorthash}) : (),
|
||||
$o{hash} ? (q{m.hash = decode(?, 'hex')} => $o{hash}) : (),
|
||||
$o{start} ? ('m.name > ?' => $o{start}) : (),
|
||||
);
|
||||
|
||||
# TODO: Flags to indicate what to information to fetch
|
||||
$o{sort} ||= '';
|
||||
my $order =
|
||||
$o{sort} eq 'syspkgname' ? 'ORDER BY s.name, s.relorder DESC, p.name, v.released DESC, m.name, m.locale NULLS FIRST, m.filename' : '';
|
||||
|
||||
return $s->dbAll(q{
|
||||
SELECT p.system, p.category, p.name AS package, pv.version, pv.released, m.name, m.section, m.filename, m.locale, encode(m.hash, 'hex') AS hash
|
||||
FROM packages p
|
||||
JOIN package_versions pv ON p.id = pv.package
|
||||
JOIN man m ON m.package = pv.id
|
||||
SELECT p.system, p.category, p.name AS package, v.version, v.released, m.name, m.section, m.filename, m.locale, encode(m.hash, 'hex') AS hash
|
||||
FROM man m
|
||||
JOIN package_versions v ON v.id = m.package
|
||||
JOIN packages p ON p.id = v.package
|
||||
JOIN systems s ON s.id = p.system
|
||||
!W
|
||||
!s
|
||||
LIMIT ?
|
||||
},
|
||||
\%where,
|
||||
$o{sort} ? 'ORDER BY name' : '',
|
||||
$o{results}||10000
|
||||
);
|
||||
}, \%where, $order, $o{results}||10000);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -796,6 +865,7 @@ sub dbManPref {
|
|||
$o{sysid} ? ('p.system = ?' => $o{sysid}) : (),
|
||||
$o{package} ? ('p.id = ?' => $o{package}) : (),
|
||||
$o{pkgver} ? ('v.id = ?' => $o{pkgver}) : (),
|
||||
$o{language}? (q{substring(locale from '^[^.]+') = ?} => $o{language}) : (),
|
||||
);
|
||||
|
||||
# Criteria to determine a "preferred" man page:
|
||||
|
|
@ -854,6 +924,33 @@ sub dbManPrefName {
|
|||
}
|
||||
|
||||
|
||||
# Returns 1 of there are alternative versions of the given man page.
|
||||
sub dbManHasVersions {
|
||||
my($s, $name, $section, $locale, $hash) = @_;
|
||||
return $s->dbRow(
|
||||
q{SELECT 1 AS ok FROM man WHERE name = ? AND section = ? AND locale IS NOT DISTINCT FROM ? AND hash <> decode(?, 'hex') LIMIT 1},
|
||||
$name, $section, $locale, $hash
|
||||
)->{ok}||0;
|
||||
}
|
||||
|
||||
|
||||
# Returns all available languages for a man page
|
||||
sub dbManLanguages {
|
||||
my($s, $name, $section) = @_;
|
||||
return map $_->{lang}, @{$s->dbAll(q{SELECT DISTINCT substring(locale from '^[^.]+') AS lang
|
||||
FROM man WHERE name = ? AND section = ?
|
||||
ORDER BY substring(locale from '^[^.]+') NULLS FIRST
|
||||
}, $name, $section)};
|
||||
}
|
||||
|
||||
|
||||
# Returns all available languages for a man page
|
||||
sub dbManSections {
|
||||
my($s, $name) = @_;
|
||||
return map $_->{section}, @{$s->dbAll(q{SELECT DISTINCT section FROM man WHERE name = ? ORDER BY section}, $name)};
|
||||
}
|
||||
|
||||
|
||||
sub dbSystemGet {
|
||||
return shift->dbAll('SELECT id, name, release, short, relorder FROM systems ORDER BY name, relorder');
|
||||
}
|
||||
|
|
|
|||
74
www/man.css
74
www/man.css
|
|
@ -2,55 +2,33 @@
|
|||
|
||||
* { margin: 0; padding: 0; font-family: "Trebuchet MS", sans-serif; }
|
||||
html { background: #333; padding: 0 10px; }
|
||||
body { margin: 10px auto 50px auto; max-width: 1250px; border-collapse: separate; padding-bottom: 10px;
|
||||
-webkit-border-radius: 10px; -moz-border-radius: 10px;
|
||||
-webkit-box-shadow: 0 10px 10px #def; -moz-box-shadow: 0 10px 10px #def; box-shadow: 0 10px 10px #def; }
|
||||
body { margin: 10px auto 50px auto; max-width: 1250px; border-collapse: separate; padding-bottom: 10px; border-radius: 10px; box-shadow: 0 10px 10px #def; }
|
||||
h1 { font-size: 24px; font-weight: normal; color: #abc; }
|
||||
h1 a { font-size: 12px; vertical-align: top }
|
||||
h2 { font-size: 21px; margin-top: 40px; color: #468; font-weight: normal; clear: left }
|
||||
h2 + i { font-size: 12px; }
|
||||
h3 { font-size: 16px; margin-top: 20px; color: #468; font-weight: normal }
|
||||
dd { margin-left: 15px; }
|
||||
a { color: #048; }
|
||||
a:hover { text-decoration: underline; color: #48B;}
|
||||
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; }
|
||||
code { font-family: "Lucida Console", Monospace; font-size: 12px; background-color: #f0f8ff }
|
||||
.hidden { display: none!important; }
|
||||
|
||||
#header { padding: 4px 20px; border-bottom: 1px solid #888; font: 24px "Arial"; background: url('images/gradients.png') repeat-x;
|
||||
-webkit-border-radius: 8px 8px 0 0; -moz-border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0; }
|
||||
#header { padding: 4px 20px; border-bottom: 1px solid #888; font: 24px "Arial"; background: url('images/gradients.png') repeat-x; border-radius: 8px 8px 0 0; }
|
||||
#header a { color: #f8f8f8; text-decoration: none; font-weight: bold; }
|
||||
#header a:hover { background: none; }
|
||||
#header form { float: right; }
|
||||
#header input[type=text] { color: #000; width: 260px; padding: 1px 2px 1px 15px; border-radius: 12px 0 0 12px; border: 1px solid #444;
|
||||
-webkit-box-shadow: 1px 1px 3px #fff, -1px -1px 2px #234; -moz-shadow: 1px 1px 3px #fff, -1px -1px 2px #234; box-shadow: 1px 1px 3px #fff, -1px -1px 2px #234;
|
||||
background: url('images/gradients.png') 0 -105px repeat-x; height: 15px; }
|
||||
box-shadow: 1px 1px 3px #fff, -1px -1px 2px #234; background: url('images/gradients.png') 0 -105px repeat-x; height: 15px; }
|
||||
#header input[type=text]:hover, #header input[type=text]:focus { background: url('images/gradients.png') 0 -122px repeat-x; outline: none; }
|
||||
#header input[type=submit] { height: 23px; width: 62px; background-image: url('images/search.png'); border: 0; margin: 0 0 0 -5px; cursor: pointer }
|
||||
|
||||
#body { padding: 10px 10px 20px 10px; background: #fff }
|
||||
|
||||
#systems a,
|
||||
#charselect a,
|
||||
#nav dd dd a { color: #048; font-family: "Verdana"; font-weight: normal; text-decoration: none; padding: 3px 5px;
|
||||
-webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; }
|
||||
#systems a:hover,
|
||||
#charselect a:hover,
|
||||
#nav dd dd a:hover { background: #cde; }
|
||||
#charselect a { color: #048; font-family: "Verdana"; font-weight: normal; text-decoration: none; padding: 3px 5px; border-radius: 4px; }
|
||||
|
||||
#nav { background: #f0f8ff; color: #036; float: right; padding: 8px; width: 250px; margin-bottom: 10px;
|
||||
-webkit-border-radius: 8px; -moz-border-radius: 8px; border-radius: 8px; }
|
||||
#nav a.global, #nav i.global { float: right; font-family: "Verdana"; font-size: 13px; padding: 0px 5px; }
|
||||
#nav i.global { font-style: normal; color: #aaa }
|
||||
#nav dl > dt { font-weight: bold; }
|
||||
#nav a { font-size: 13px; }
|
||||
#nav dd dt a { text-decoration: none; }
|
||||
#nav .expand { text-decoration: none; padding-left: 5px; font-size: 16px }
|
||||
#nav dd .expand { font-size: 14px }
|
||||
#nav dl i { font-style: normal; font-size: 12px; padding-left: 7px; color: #aaa }
|
||||
#nav b { font-size: 13px; background: #cde; padding: 3px 5px;
|
||||
-webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; }
|
||||
#systems a:hover,
|
||||
#charselect a:hover { background: #cde; }
|
||||
|
||||
i.grayedout { color: #aaa; font-size: 13px; }
|
||||
|
||||
|
|
@ -63,7 +41,6 @@ i.grayedout { color: #aaa; font-size: 13px; }
|
|||
#systems li { display: block; float: left; width: 300px; min-height: 70px; margin: 20px 10px; padding-left: 60px }
|
||||
#systems span { display: block; margin-left: -55px; float: left; width: 50px; height: 50px; background-repeat: no-repeat; }
|
||||
#systems b { font-size: 24px; display: block }
|
||||
#systems .hidden { display: none; }
|
||||
|
||||
#charselect { float: right }
|
||||
#charselect b { padding: 3px }
|
||||
|
|
@ -96,9 +73,39 @@ i.grayedout { color: #aaa; font-size: 13px; }
|
|||
#pkgmans li { display: block; }
|
||||
#pkgmans i { color: #aaa; font-size: 13px; }
|
||||
|
||||
#manbuttons h1 { display: inline; margin: 0 20px 0 0; vertical-align: middle }
|
||||
#manbuttons ul { list-style-type: none; display: inline-block }
|
||||
#manbuttons li { display: inline-block }
|
||||
#manbuttons li a, #manbuttons li i { display: inline-block; outline: none; margin: 0 10px 0 0; padding: 5px 7px 8px 7px; text-decoration: none; border-top-left-radius: 7px 5px; border-top-left-radius: 7px 5px }
|
||||
#manbuttons li i { font-style: normal; text-decoration: line-through; color: #aaa }
|
||||
#manbuttons li a:hover, #manbuttons li a.active { background: #f0f8ff }
|
||||
|
||||
#closebtn { float: right; margin: -5px 0 -20px 0; text-decoration: none; outline: none; color: #036; font-weight: bold }
|
||||
|
||||
#manres { margin: 0 0 10px 0; width: 100%; padding: 10px; box-sizing: border-box; background: #f0f8ff; border-radius: 10px; border-left: 1px dashed #333; border-right: 1px dashed #333 }
|
||||
#manres i { color: #aaa; font-size: 13px; margin-left: 7px }
|
||||
#manres ul { list-style-type: none }
|
||||
#manres ul a { outline: none; text-decoration: none }
|
||||
#manres ul .oldver a { color: #aaa; font-size: 13px }
|
||||
#manres div > ul { margin-top: 5px } /* System names */
|
||||
#manres div > ul > li > a { color: #036; font-weight: bold } /* System names */
|
||||
#manres div > ul > li > ul { margin-left: 15px } /* System versions */
|
||||
#manres div > ul > li > ul > li > a { color: #000 } /* System versions */
|
||||
#manres div > ul > li > ul > li > ul { margin-left: 15px } /* Package names */
|
||||
#manres div > ul > li > ul > li > ul > li > a { color: #000 } /* Package names */
|
||||
#manres table { margin: 10px 0; border-collapse: collapse; }
|
||||
#manres td { padding: 1px 5px; font-size: 12px; }
|
||||
#manres td + td { border-left: 1px solid #ccc }
|
||||
#manres table { margin: 2px 10px; }
|
||||
#manres table tr td:nth-child(1) { min-width: 80px }
|
||||
|
||||
#langselect, #sectionselect { padding-left: 50px }
|
||||
#langselect a, #sectionselect a, #langselect i, #sectionselect i { padding: 3px 5px }
|
||||
|
||||
#contents { margin: 10px 0 0 0 }
|
||||
|
||||
#footer { height: 60px; clear: both; padding: 4px 10px; color: #f8f8f8; margin: 0 0 -20px 0;
|
||||
border-top: 1px solid #888; font-size: 13px; background: url('images/gradients.png') 0 -37px repeat-x;
|
||||
-webkit-border-radius: 0 0 8px 8px; -moz-border-radius: 0 0 8px 8px; border-radius: 0 0 8px 8px; }
|
||||
border-top: 1px solid #888; font-size: 13px; background: url('images/gradients.png') 0 -37px repeat-x; border-radius: 0 0 8px 8px; }
|
||||
#footer a { font-size: 13px; padding: 0; color: #f8f8f8; }
|
||||
#footer a:hover { background: none; }
|
||||
|
||||
|
|
@ -106,10 +113,9 @@ pre, pre * { font-family: "Lucida Console", Monospace; font-size: 15px }
|
|||
pre b { color: #369; font-weight: normal; }
|
||||
|
||||
#ds_box { position: absolute; top: 0; border: 1px solid $border$; border-top: none; background: #f0f8ff; cursor: pointer; z-index: 2 }
|
||||
#ds_box.hidden { display: none }
|
||||
#ds_box b { padding: 2px 0 0 10px; font-size: 12px; }
|
||||
#ds_box tr.selected { background: #fff; }
|
||||
#ds_box table { width: 100%; border: 0; background: none; margin: 0 }
|
||||
#ds_box table { width: 100% }
|
||||
#ds_box td { border: 0; padding: 1px 5px }
|
||||
#ds_box i { padding-left: 5px; color: #aaa; font-style: normal }
|
||||
|
||||
|
|
|
|||
362
www/man.js
362
www/man.js
|
|
@ -84,6 +84,9 @@ function setContent() {
|
|||
if(arguments[i] != null)
|
||||
arguments[0].appendChild(tag(arguments[i]));
|
||||
}
|
||||
function getText(obj) {
|
||||
return obj.textContent || obj.innerText || '';
|
||||
}
|
||||
function setText(obj, txt) {
|
||||
if(obj.textContent != null)
|
||||
obj.textContent = txt;
|
||||
|
|
@ -287,227 +290,194 @@ function dsResults(hr, obj) {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
/* What follows is specific to manned.org */
|
||||
|
||||
// Search box
|
||||
searchRedir = false;
|
||||
dsInit(byId('q'), '/xml/search.xml?q=', function(item, tr) {
|
||||
tr.appendChild(tag('td', item.getAttribute('name'), tag('i', '('+item.getAttribute('section')+')')));
|
||||
},
|
||||
function(item) {
|
||||
searchRedir = true;
|
||||
location.href = '/'+item.getAttribute('name')+'.'+item.getAttribute('section');
|
||||
return item.getAttribute('name')+'('+item.getAttribute('section')+')';
|
||||
},
|
||||
function() {
|
||||
if(!searchRedir) {
|
||||
var frm=byId('q');
|
||||
while(frm && frm.nodeName.toLowerCase() != 'form')
|
||||
frm = frm.parentNode;
|
||||
frm.submit();
|
||||
(function(){
|
||||
searchRedir = false;
|
||||
dsInit(byId('q'), '/xml/search.xml?q=', function(item, tr) {
|
||||
tr.appendChild(tag('td', item.getAttribute('name'), tag('i', '('+item.getAttribute('section')+')')));
|
||||
},
|
||||
function(item) {
|
||||
searchRedir = true;
|
||||
location.href = '/'+item.getAttribute('name')+'.'+item.getAttribute('section');
|
||||
return item.getAttribute('name')+'('+item.getAttribute('section')+')';
|
||||
},
|
||||
function() {
|
||||
if(!searchRedir) {
|
||||
var frm=byId('q');
|
||||
while(frm && frm.nodeName.toLowerCase() != 'form')
|
||||
frm = frm.parentNode;
|
||||
frm.submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
})();
|
||||
|
||||
|
||||
|
||||
// Efficiently pack an array of booleans into a string. (Uses something like
|
||||
// base32) Note: The resulting array after decoding may have a few more
|
||||
// elements than it had before decoding. These will be false.
|
||||
var bsCharacters = "abcdefghijklmnopqrstuvwxyz234567";
|
||||
|
||||
function bsEncode(a) {
|
||||
var v = 0;
|
||||
var b = 0;
|
||||
var r = '';
|
||||
for(var i=0; i<a.length; i++) {
|
||||
v = (v<<1) + (a[i]?1:0);
|
||||
if(++b == 5) {
|
||||
r += bsCharacters.charAt(v);
|
||||
v = b = 0;
|
||||
|
||||
// The tabs on man pages
|
||||
(function(){
|
||||
var ul = byId('manbuttons');
|
||||
if(!ul)
|
||||
return;
|
||||
var res = byId('manres');
|
||||
ul = byName(ul, 'ul')[0];
|
||||
|
||||
|
||||
var table = function(tbl, prop) {
|
||||
var t = tag('table', prop);
|
||||
for(row in tbl) {
|
||||
row = tbl[row];
|
||||
var r = tag('tr', {});
|
||||
for(col in row) {
|
||||
col = row[col];
|
||||
r.appendChild(tag('td',
|
||||
col.bold ? tag('b', col.name) :
|
||||
col.href ? tag('a', {href:col.href}, col.name) : col.name
|
||||
));
|
||||
}
|
||||
t.appendChild(r);
|
||||
}
|
||||
}
|
||||
if(!a.length || b > 0)
|
||||
r += bsCharacters.charAt(v<<(5-b));
|
||||
return r;
|
||||
}
|
||||
return t;
|
||||
};
|
||||
|
||||
function bsDecode(s) {
|
||||
var a = [];
|
||||
for(var i=0; i<s.length; i++) {
|
||||
var n = s.charCodeAt(i);
|
||||
n -= n >= 97 ? 97 : 24;
|
||||
a.push(!!((n>>4)&1), !!((n>>3)&1), !!((n>>2)&1), !!((n>>1)&1), !!(n&1));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
var treeoldver = function() {
|
||||
var lnk = this;
|
||||
var ul = lnk;
|
||||
var show = !lnk['data-shown'];
|
||||
lnk['data-shown'] = show;
|
||||
|
||||
while(ul.nodeName.toLowerCase() != 'ul')
|
||||
ul = ul.parentNode;
|
||||
var l = ul.childNodes;
|
||||
for(var i=0; i<l.length; i++) {
|
||||
if(l[i].nodeName.toLowerCase() == 'li' && l[i]['data-oldver'])
|
||||
setClass(l[i], 'hidden', !show);
|
||||
}
|
||||
|
||||
/* Structure of VARS.mans:
|
||||
[
|
||||
["System", "Full name", "short", [
|
||||
[ "category", "package", "version", [
|
||||
[ "section", "locale"||null ],
|
||||
...
|
||||
],
|
||||
oldvisible // <- this is only set by JS
|
||||
],
|
||||
...
|
||||
],
|
||||
oldvisible // <- this is only set by JS
|
||||
],
|
||||
...
|
||||
]
|
||||
|
||||
The godawful navigation code desperately needs a rewrite.
|
||||
*/
|
||||
|
||||
navShowLocales = false;
|
||||
navHasLocale = false;
|
||||
|
||||
function navCreate(nav) {
|
||||
setText(nav, '');
|
||||
|
||||
view = navSerialize();
|
||||
navHasLocale = false;
|
||||
var dl = tag('dl', null);
|
||||
|
||||
for(var i=0; i<VARS.mans.length; i++) {
|
||||
var sys = VARS.mans[i];
|
||||
|
||||
var isold = i > 0 && VARS.mans[i-1][0] == sys[0];
|
||||
if(typeof sys[4] === 'undefined')
|
||||
sys[4] = !isold;
|
||||
|
||||
var pkgnum = 0;
|
||||
var dd = tag('dd', null);
|
||||
|
||||
if(sys[4])
|
||||
for(var j=0; j<sys[3].length; j++)
|
||||
if(navCreatePkg(nav, view, dd, sys, j))
|
||||
pkgnum++;
|
||||
|
||||
if(!isold || sys[4])
|
||||
dl.appendChild(tag('dt', sys[1],
|
||||
isold || !VARS.mans[i+1] || VARS.mans[i+1][0] != sys[0] ? null : tag('a',
|
||||
{href:'#', _sysn: sys[0], _sysi:i, 'class':'expand',
|
||||
title: "Show/hide historical releases.",
|
||||
onclick: function() {
|
||||
for(var j=this._sysi+1; j<VARS.mans.length && VARS.mans[j][0] == this._sysn; j++)
|
||||
VARS.mans[j][4] = !VARS.mans[j][4];
|
||||
navCreate(nav);
|
||||
return false
|
||||
}}, VARS.mans[i+1][4] ? expanded_icon : collapsed_icon)
|
||||
));
|
||||
|
||||
if(sys[4] && pkgnum > 0)
|
||||
dl.appendChild(dd);
|
||||
}
|
||||
|
||||
navCreateLinks(nav);
|
||||
nav.appendChild(dl);
|
||||
}
|
||||
|
||||
|
||||
function navCreatePkg(nav, view, dd, sys, n) {
|
||||
var pkg = sys[3][n];
|
||||
|
||||
var isold = n > 0 && sys[3][n-1][0] == pkg[0] && sys[3][n-1][1] == pkg[1];;
|
||||
if(isold && !pkg[4])
|
||||
setText(lnk, (show ? '- ' : '+ ')+lnk['data-hidnum']+' older versions');
|
||||
return false;
|
||||
};
|
||||
|
||||
var mannum = 0;
|
||||
var pdd = tag('dd', null);
|
||||
for(var i=0; i<pkg[3].length; i++) {
|
||||
var man = pkg[3][i];
|
||||
var txt = man[0] + (man[1] ? '.'+man[1] : '');
|
||||
if(man[2] != VARS.hash && man[1])
|
||||
navHasLocale = true;
|
||||
if(man[2] == VARS.hash || (navShowLocales || !man[1])) {
|
||||
if(i > 0)
|
||||
pdd.appendChild(tag(' '));
|
||||
pdd.appendChild(man[2] == VARS.hash ? tag('b', txt) : tag('a', {href:'/'+VARS.name+'/'+man[2]+'?v='+view}, txt));
|
||||
mannum++;
|
||||
var treeexpand = function() {
|
||||
var sub = byName(this.parentNode, 'ul')[0] || byName(this.parentNode, 'table')[0];
|
||||
var exp = hasClass(sub, 'hidden');
|
||||
setClass(sub, 'hidden', !exp);
|
||||
setText(this, getText(this).replace(/^[^ ]+/, exp ? expanded_icon : collapsed_icon));
|
||||
return false;
|
||||
};
|
||||
|
||||
var treeitem = function(n) {
|
||||
var icon = n.name ? (n.expand ? expanded_icon : collapsed_icon)+' ' : '';
|
||||
return tag('li', n.hide ? {'class':'hidden', 'data-oldver':true} : {},
|
||||
tag('a', {href:'#', onclick: treeexpand}, icon+n.name),
|
||||
n.i ? tag('i', n.i) : null,
|
||||
n.childs ? treelist(n.childs, n.expand ? {} : {'class':'hidden'}) : null,
|
||||
n.table ? table(n.table, n.expand ? {} : {'class':'hidden'}) : null
|
||||
);
|
||||
};
|
||||
|
||||
var treelist = function(lst, prop) {
|
||||
var ul = tag('ul', prop);
|
||||
var hidden = 0;
|
||||
|
||||
for(i in lst) {
|
||||
var n = lst[i];
|
||||
if(n.hide)
|
||||
hidden++;
|
||||
ul.appendChild(treeitem(lst[i]));
|
||||
}
|
||||
}
|
||||
|
||||
if(mannum > 0) {
|
||||
dd.appendChild(tag('dt', tag('a', {href:'/pkg/'+sys[2]+'/'+pkg[0]+'/'+pkg[1]+'/'+pkg[2]}, pkg[1]),
|
||||
isold || !sys[3][n+1] || sys[3][n+1][0] != pkg[0] || sys[3][n+1][1] != pkg[1] ? null : tag('a',
|
||||
{href:'#', _pkgn: pkg[0]+'-'+pkg[1], _pkgi:n, 'class':'expand',
|
||||
title: 'Show/hide historical versions of this package',
|
||||
onclick: function() {
|
||||
for(var j=this._pkgi+1; j<sys[3].length && sys[3][j][0]+'-'+sys[3][j][1] == this._pkgn; j++)
|
||||
sys[3][j][4] = !sys[3][j][4];
|
||||
navCreate(nav);
|
||||
return false
|
||||
}}, sys[3][n+1][4] ? expanded_icon : collapsed_icon),
|
||||
tag('i', pkg[0] + ' / ' + pkg[2])));
|
||||
dd.appendChild(pdd);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if(hidden > 0)
|
||||
ul.appendChild(tag('li', {'class':'oldver'},
|
||||
tag('a', {href:'#', onclick: treeoldver, 'data-hidnum':hidden}, '+ '+hidden+' older versions')
|
||||
));
|
||||
return ul;
|
||||
};
|
||||
|
||||
var clearactive = function() {
|
||||
setClass(res, 'hidden', true);
|
||||
var l = byName(ul, 'a');
|
||||
for(var i=0; i<l.length; i++) {
|
||||
setClass(l[i], 'active', false);
|
||||
if(l[i]['data-obj'])
|
||||
setClass(l[i]['data-obj'], 'hidden', true);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function navCreateLinks(nav) {
|
||||
var t = (navShowLocales ? expanded_icon : collapsed_icon) + 'locales';
|
||||
nav.appendChild(!navHasLocale ? tag('i', {'class':'global'}, t) : tag('a',
|
||||
{ 'class': 'global',
|
||||
href: '#',
|
||||
title: 'Show/hide manuals in a non-standard locale.',
|
||||
onclick: function() { navShowLocales = !navShowLocales; navCreate(nav); return false }
|
||||
}, t
|
||||
));
|
||||
}
|
||||
var loading = tag('div', {'class':'hidden'}, 'Loading...');
|
||||
|
||||
var buttonclick = function() {
|
||||
var btn = this;
|
||||
var isactive = hasClass(btn, 'active');
|
||||
clearactive();
|
||||
if(isactive)
|
||||
return false;
|
||||
|
||||
// Serializes the current navigation view into a short string. The string is a
|
||||
// bsEncode()ed bit array, created as follows:
|
||||
// array.push(navShowLocales);
|
||||
// for(each system that has an expand button)
|
||||
// array.push(is the butten expanded or not);
|
||||
// for(each system)
|
||||
// for(each package that has an expand button)
|
||||
// array.push(is the button expanded or not);
|
||||
// Obviously, this means that the serialized view depends on the number and
|
||||
// order of systems and packages. The order is stable, the number may change
|
||||
// with database updates.
|
||||
function navSerialize() {
|
||||
var a = [navShowLocales];
|
||||
for(var i=0; i<VARS.mans.length; i++)
|
||||
if(i+1 < VARS.mans.length && VARS.mans[i+1][0] == VARS.mans[i][0] && (i == 0 || VARS.mans[i-1][0] != VARS.mans[i][0]))
|
||||
a.push(!!VARS.mans[i+1][4]);
|
||||
for(var i=0; i<VARS.mans.length; i++)
|
||||
for(var j=0; j<VARS.mans[i][3].length; j++)
|
||||
if(j+1 < VARS.mans[i][3].length && VARS.mans[i][3][j+1][0] == VARS.mans[i][3][j][0] && VARS.mans[i][3][j+1][1] == VARS.mans[i][3][j][1] && (j == 0 || VARS.mans[i][3][j-1][0] != VARS.mans[i][3][j][0] || VARS.mans[i][3][j-1][1] != VARS.mans[i][3][j][1]))
|
||||
a.push(!!VARS.mans[i][3][j+1][4]);
|
||||
return bsEncode(a).replace(/(.)a+$/, '$1');
|
||||
}
|
||||
if(btn['data-obj']) {
|
||||
setClass(btn['data-obj'], 'hidden', false);
|
||||
} else {
|
||||
setClass(loading, 'hidden', false);
|
||||
ajax(btn['data-url'], function(r) {
|
||||
setClass(loading, 'hidden', true);
|
||||
r = JSON.parse(r.responseText);
|
||||
btn['data-obj'] = tag('div', tag('p', btn['data-p']), treelist(r, {}));
|
||||
res.appendChild(btn['data-obj']);
|
||||
});
|
||||
}
|
||||
setClass(btn, 'active', true);
|
||||
setClass(res, 'hidden', false);
|
||||
return false;
|
||||
};
|
||||
|
||||
// And the reverse of the above.
|
||||
function navLoad(s) {
|
||||
var a = bsDecode(s);
|
||||
navShowLocales = !!a.shift();
|
||||
for(var i=0; i<VARS.mans.length; i++)
|
||||
if(i > 0 && VARS.mans[i-1][0] == VARS.mans[i][0])
|
||||
VARS.mans[i][4] = i > 1 && VARS.mans[i-2][0] == VARS.mans[i-1][0] ? VARS.mans[i-1][4] : !!a.shift();
|
||||
for(var i=0; i<VARS.mans.length; i++)
|
||||
for(var j=0; j<VARS.mans[i][3].length; j++)
|
||||
if(j > 0 && VARS.mans[i][3][j-1][0] == VARS.mans[i][3][j][0] && VARS.mans[i][3][j-1][1] == VARS.mans[i][3][j][1])
|
||||
VARS.mans[i][3][j][4] = j > 1 && VARS.mans[i][3][j-2][0] == VARS.mans[i][3][j-1][0] && VARS.mans[i][3][j-2][1] == VARS.mans[i][3][j-1][1] ? VARS.mans[i][3][j-1][4] : !!a.shift();
|
||||
}
|
||||
res.insertBefore(tag('a', {id:'closebtn', href:'#', onclick: clearactive}, 'X'), res.firstChild);
|
||||
res.appendChild(loading);
|
||||
|
||||
(function(){
|
||||
var name = ul.getAttribute('data-name');
|
||||
var hash = ul.getAttribute('data-hash');
|
||||
var section = ul.getAttribute('data-section');
|
||||
var locale = ul.getAttribute('data-locale');
|
||||
|
||||
ul.appendChild(tag('li', byId('sectionselect')
|
||||
? tag('a', {href:'#', onclick: buttonclick, 'data-obj': byId('sectionselect')}, 'sections')
|
||||
: tag('i', 'sections')
|
||||
));
|
||||
|
||||
ul.appendChild(tag('li', byId('langselect')
|
||||
? tag('a', {href:'#', onclick: buttonclick, 'data-obj': byId('langselect')}, 'translations')
|
||||
: tag('i', 'translations')
|
||||
));
|
||||
|
||||
ul.appendChild(tag('li', ul.getAttribute('data-hasversions') > 0
|
||||
? tag('a', {href:'#', onclick: buttonclick,
|
||||
'data-url': '/json/tree.json?name='+name+';section='+section+';locale='+locale+';cur='+hash,
|
||||
'data-p': 'Different versions of this manual page are available.'},
|
||||
'versions')
|
||||
: tag('i', 'versions')
|
||||
));
|
||||
|
||||
ul.appendChild(tag('li', tag('a', {href:'#', onclick: buttonclick,
|
||||
'data-url': '/json/tree.json?hash='+hash+';name='+name+';section='+section,
|
||||
'data-p': 'This manual page was found in the following locations.'},
|
||||
'locations')));
|
||||
})();
|
||||
})();
|
||||
|
||||
if(byId('nav')) {
|
||||
navLoad(VARS.view||'');
|
||||
navCreate(byId('nav'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// The "more..." links on the homepage.
|
||||
if(byId('systems')) {
|
||||
(function(){
|
||||
var sys = byId('systems');
|
||||
if(!sys)
|
||||
return;
|
||||
var f = function() {
|
||||
var l = byName(this.parentNode, 'a', 'hidden');
|
||||
for(var i=0; i<l.length; i++)
|
||||
|
|
@ -515,7 +485,7 @@ if(byId('systems')) {
|
|||
setClass(this, 'hidden', true);
|
||||
return false
|
||||
};
|
||||
var l = byClass(byId('systems'), 'a', 'more');
|
||||
var l = byClass(sys, 'a', 'more');
|
||||
for(var i=0; i<l.length; i++)
|
||||
l[i].onclick = f;
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue