#!/usr/bin/perl
use CGI;
use File::stat;
use DB_File;
use strict;
use Digest::MD5;
require "ctime.pl";
# my $AppTitle = "Secret Project Whooping Crane";
my $AppTitle = "Legend Help Depot";
my $Root = "/legend";
# my $Root = "$ENV{HOME}/legend/legend";
my $HelpPath = "help/spells:help/skills:help/general:help/abilities";
my $CacheDir = "/tmp/help.cache";
my $NAcross = 6;
my %Topics = ();
my %Seen = ();
my @TopicList= ();
my @HelpPath = split(":", $HelpPath);
sub mkdirFor {
my ($file) = @_;
my $dir;
my @paths;
@paths = split("/", $file);
pop @paths;
if ($paths[0] ne "") {
$dir = shift @paths;
mkdir($dir, 0755);
}
while ($#paths >= 0) {
$dir = "$dir/" . $paths[0];
mkdir($dir, 0755);
shift @paths;
}
}
sub rewriteFile {
my ($file, @data) = @_;
open (DATA, ">$file.tmp");
print DATA @data;
close (DATA) or return;
unlink("$file");
rename("$file.tmp", $file);
sleep(5);
}
sub refreshTopicCache {
my $dir;
my ($topic, $alias);
my ($s1, $s2);
mkdirFor("$CacheDir/x");
open(IDX, "$Root/help/help.idx") or return;
tie %Topics, 'DB_File', "$CacheDir/_topics.db", O_CREAT|O_RDWR, 0666, $DB_BTREE or return $!;
while () {
chomp;
next if (/^#/);
/['"]?([^:]+)['"]?:\s+(.*)/;
$alias = $1;
$topic = $2;
$alias =~ s/[^a-z ]//gi;
$alias =~ s/^\s+//;
$alias =~ s/\s+$//;
$topic =~ s/^\s+//;
$topic =~ s/\s+$//;
next if ($alias eq "");
$Topics{lc($alias)} = lc($topic);
}
close(IDX);
for $dir (@HelpPath) {
opendir(DIR, "$Root/$dir");
for (grep (!/^\./, readdir(DIR))) {
next unless (s/\.hlp$//);
$Topics{lc($_)} = lc($_);
}
}
}
sub checkCache {
my ($cache, $orig) = @_;
my ($scache, $sorig);
$scache = stat("$cache");
$sorig = stat("$orig");
unless ($scache and $sorig) {
return -1;
}
if ($scache->mtime >= $sorig->mtime) {
## Use existing cache db
return 1;
}
else {
return -1;
}
}
sub indexTopics {
my $dir;
my ($topic, $alias);
if (checkCache("$CacheDir/_topics.db", "$Root/help/help.idx") > 0) {
tie %Topics, 'DB_File', "$CacheDir/_topics.db", O_RDONLY,
0666, $DB_BTREE or return $!;
}
else {
$_ = refreshTopicCache();
return $_ if ($_);
}
@TopicList = sort {
return -1 if (substr($a, 0, length($b)) eq $b);
return 1 if (substr($b, 0, length($a)) eq $a);
$a cmp $b;
} keys %Topics;
return undef;
}
sub findHelpFile {
my ($topic) = @_;
my ($dir);
for my $dir (@HelpPath) {
if (-f "$Root/$dir/$topic.hlp") {
return "$Root/$dir/$topic.hlp";
}
}
return undef;
}
sub getHelpText {
my ($helpname, $hlpfile) = @_;
my $found = 0;
my @help = ();
my $dir;
$hlpfile = findHelpFile($helpname) unless defined($hlpfile);
if (!defined($hlpfile)) {
return ();
}
## Block access to restricted files?
if (0) {
$_ = $hlpfile;
s/\.hlp$/\.res/;
if (-f $_) {
return ("This help file is restricted.");
}
}
open(HELP, $hlpfile);
push @help, ;
map {chomp; s/^\s+//; s/\s+$//;} @help;
close HELP;
return () if ($#help < 0);
if (open(KEY, "$Root/$dir/$helpname.key")) {
$_ = ;
s/^\s+//;
s/\s+$//;
s/\s+/, /g;
close(KEY);
}
else {
$_ = $helpname;
}
unshift @help, "TOPIC: " . uc($_);
return @help;
}
sub sanitize {
$_[0] =~ s/&/&/g;
$_[0] =~ s/</g;
$_[0] =~ s/>/>/g;
return $_[0];
}
sub crossLink {
my ($in, $topic, $alias) = @_;
my $tmp = $topic;
# Don't need to encode topic -- javascript does this for us.
#$tmp =~ s/([^a-z0-9])/sprintf("%%%02x", ord($1))/egi;
# But we do need to ditch spaces.
$tmp =~ s/\s/_/g;
$in =~ s^([\s,.!:;"'])($alias)([\s,.!:;"'])^$1$2$3^i;
$Seen{$topic} = 1;
return $in;
}
sub decorate {
my ($owntopic, @in) = @_;
my @out = ();
my %state = ();
my ($alias, $topic);
for (@in) {
s@<([^>]+)>@<\001span class="syntax-argument"\002$1\001/span\002>@g;
$_ = sanitize($_);
s@\001@<@g;
s@\002@>@g;
if (/^\s*(topic):\s+(.*)\s*$/i) {
push @out, "Help for $2
";
push @out, "
";
next;
}
if (/^\s*(syntax|usage):\s+(.*)\s*$/i) {
unless ($state{syntax}) {
push @out, "";
}
$state{syntax} = 1;
push @out, "syntax: $2
";
next;
}
if ($state{syntax}) {
delete $state{syntax};
push @out, "
";
}
if (/^\s*$/) {
if ($state{para}) {
push @out, "
";
}
push @out, "";
$state{para} = 1;
next;
}
## Zero-width assertion \b here isn't really good enough, need
## spaces. So insert spaces at ends to simplify matching. :P
$_ = " $_ "; # bleh
for $alias (@TopicList) {
my $upper = uc($alias);
$topic = $Topics{$alias};
if (/[\s,.!:;"']$alias[\s,.!:;"']/i
and !exists($Seen{$topic})
and !/<[^>]*\s$alias\s.*>/i
and ($topic ne $Topics{$owntopic})) {
$_ = crossLink($_, $topic, $alias);
}
## ALWAYS crosslink HELP FOO
elsif (/HELP $upper/) {
$_ = crossLink($_, $topic, $alias);
}
}
$_ =~ s/^ (.*) $/$1/;
## If short and ends in a colon, consider it a headline.
if (/:\s*$/ && length($_) < 50) {
$_ = "$_
";
}
## If multiple sequences of doubled whitespace, consider
## it block text.
elsif (/\s{2}\S.*\s{2}/) {
$_ = "$_
";
}
## If colon delimiting, consider preformatted but without
## fixed font.
elsif (/\s:\s/) {
$_ = "$_
";
}
push @out, $_;
}
pop @out if ($out[$#out] eq "
");
return @out;
}
sub getHelpHTML {
my ($topic) = @_;
my $hlpfile;
my $htmlfile;
my @help;
my @html;
my $status;
## first value returned is:
## 1 if cached
## 0 if stale and refreshed
## -1 if not cached
$topic = $Topics{$topic};
$hlpfile = findHelpFile("$topic");
$htmlfile = "$CacheDir/$topic.html";
$htmlfile =~ s/\s/_/g;
$status = checkCache($htmlfile, $hlpfile);
if ($status > 0) {
open(HTML, $htmlfile);
@help = ;
close(HTML);
return ($status, @help);
}
@help = getHelpText($topic, $hlpfile);
if ($#help < 0) {
return ($status);
}
@html = map {"$_\n";} decorate(lc($_), @help);
rewriteFile($htmlfile, @html);
return ($status, @html);
}
sub mainPage {
my @out = ();
push @out, <$AppTitle
HTML
#
push @out, <
return @out;
}
sub htmlForTopics {
my @topics = @_;
my @html;
my $n = 0;
push @html, '';
push @html, " ";
for (@topics) {
#push @html, " - $_
";
if ((++$n % $NAcross) == 0) {
push @html, "
";
push @html, " ";
}
push @html, " | $_ | ";
}
push @html, "
";
push @html, "
";
return @html;
}
sub genTopicList {
my @out;
my %initials = ();
my $initial;
push @out, 'Topic List
';
push @out, '';
for (@TopicList) {
$initials{lc(substr($_, 0, 1))} = 1;
}
for $initial (sort keys %initials) {
#$initial = uc($initial);
push @out, "" . uc($initial) . "";
}
push @out, "
";
push @out, "search";
for $initial (sort keys %initials) {
#$initial = uc($initial);
push @out, "";
push @out, '
' . uc($initial) . '
';
push @out, htmlForTopics(sort grep /^$initial/i, @TopicList);
push @out, "";
}
push @out, '';
push @out, '
Search
';
push @out, '
';
push @out, '
';
push @out, '
';
push @out, '
';
push @out, "
";
push @out, '';
return @out;
}
sub getTopicList {
my @html;
my $md5 = new Digest::MD5;
my $t;
if (checkCache("$CacheDir/_topics.html", "$CacheDir/_topics.db") > 0) {
## use cached HTML
open(HTML, "$CacheDir/_topics.html");
@html = ;
close(HTML);
my $s = stat("$CacheDir/_topics.html");
$t = $s->mtime;
}
else {
@html = map {"$_\n";} genTopicList();
rewriteFile("$CacheDir/_topics.html", @html);
$t = time();
}
$md5->add(@html);
return ($t, $md5->hexdigest, @html);
}
sub searchTopicList {
my ($search) = @_;
my @matches;
my @html;
@html = ("Search results for \"$search\":");
@matches = grep /\b$search/io, @TopicList;
push @html, htmlForTopics(sort @matches);
if ($#matches == 0) {
push @html, "";
}
elsif (exists($Topics{lc($search)})) {
push @html, "";
}
return map {"$_\n";} @html;
}
sub cgiMode {
my ($err) = @_;
my $cgi = new CGI;
## Given query params, operate as an AJAX back-end. Do not
## emit complete document.
if ($_ = $cgi->param("topic")) {
my ($status, @help) = getHelpHTML(lc($_));
print $cgi->header;
if ($#help < 0) {
print "No help found for \"$_\".\n";
}
else {
print @help;
}
untie (%Topics);
exit 0;
}
if ($cgi->param("topiclist")) {
my ($t, $md5, @html) = getTopicList();
my $ct = ctime($t);
chomp $ct;
print $cgi->header(-etag => $md5, "-last-modified" => $ct);
print @html;
untie (%Topics);
exit 0;
}
if ($_ = $cgi->param("topicSearch")) {
print $cgi->header;
print searchTopicList($_);
untie (%Topics);
exit 0;
}
print $cgi->header;
print $cgi->start_html(
-title => "$AppTitle",
-style => { -src => 'help.css' },
-script => { -src => 'help.js' },
-onLoad => "loader()",
);
if ($err) {
print "$err\n";
}
else {
print mainPage();
}
print $cgi->end_html;
}
sub cliMode {
my ($err) = @_;
my %uniq;
my @vals;
if ($ARGV[0] eq "-precache") {
for (values %Topics) {
$uniq{$_} = $_;
}
@vals = sort keys %uniq;
print $#vals + 1, " topics to precache.\n";
for (@vals) {
$_ = lc($_);
print "Precaching \"$_\"... ";
## Erase %Seen on each pass.
%Seen = ();
my ($status, @html) = getHelpHTML($_);
print "already cached.\n" if ($status > 0);
print "refreshed.\n" if ($status == 0);
print "precached.\n" if ($status < 0);
}
print "Precached ", $#vals+1, " topics.\n";
exit 0;
}
}
sub main {
my $err = indexTopics();
if ($ARGV[0] =~ /^-/) {
cliMode($err);
}
else {
cgiMode($err);
}
untie (%Topics);
exit 0;
}
main();