/*************************************************************************************************
 * CGI script for full-text search
 *                                                      Copyright (C) 2000-2003 Mikio Hirabayashi
 * This file is part of QDBM, Quick Database Manager.
 * QDBM is free software; you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License or any later version.  QDBM is distributed in the hope that it will be
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
 * details.
 * You should have received a copy of the GNU Lesser General Public License along with QDBM; if
 * not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 * 02111-1307 USA.
 *************************************************************************************************/


#include <depot.h>
#include <cabin.h>
#include <odeum.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

#undef TRUE
#define TRUE           1                 /* boolean true */
#undef FALSE
#define FALSE          0                 /* boolean false */

#define QG_CONFFILE    "qfts.conf"       /* name of the configuration file */
#define QG_DEFENC      "US-ASCII"        /* default encoding */
#define QG_DEFLANG     "en"              /* default language */
#define QG_DEFTITLE    "QDBM on WWW"     /* default title */
#define QG_DEFINDEX    "casket"          /* directory containing database files */
#define QG_DEFPREFIX   "./"              /* prefix of the URI of a document */
#define QG_RDATAMAX    262144            /* max size of data to read */
#define QG_DEFMAX      16                /* default max number of shown documents */
#define QG_SUMTOP      24                /* number of adopted words as top of summary */
#define QG_SUMWIDTH    16                /* number of adopted words around a keyword */
#define QG_SUMWORDMAX  96                /* max number of words in summary */
#define QG_KEYWORDS    16                /* number of shown keywords */

enum {
  QG_UNITAND,
  QG_UNITOR
};


/* global variables */
const char *scriptname;                  /* name of the script */
const char *enc;                         /* encoding of the page */
const char *lang;                        /* language of the page */
const char *title;                       /* title of the page */


/* function prototypes */
int main(int argc, char **argv);
int fwmatch(const char *str, const char *key);
int bwmatch(const char *str, const char *key);
const char *skiplabel(const char *str);
CBMAP *getparams(void);
void senderror(int code, const char *tag, const char *message);
void htmlprintf(const char *format, ...);
void printmime(void);
void printdecl(void);
void printhead(void);
void showform(const char *phrase, int unit, int max);
CBLIST *getwords(const char *phrase);
char *replaceuri(const char *uri);
void showresult(const CBLIST *words, const char *phrase, int unit, int max, int skip,
                const char *index, const char *prefix, const char *diridx);
void showsummary(const ODDOC *doc, const CBLIST *kwords, const char *phrase, int unit, int max);
CBMAP *listtomap(const CBLIST *list);
void showwords(int id, const CBLIST *words, const char *phrase, int unit, int max,
               const char *index, const char *prefix, const char *diridx);
void showhelp(const char *help);


/* main routine */
int main(int argc, char **argv){
  CBMAP *params;
  CBLIST *lines, *words;
  const char *tmp, *index, *prefix, *diridx, *phrase, *help;
  int i, unit, max, skip, id;
  /* set configurations */
  scriptname = argv[0];
  if((tmp = getenv("SCRIPT_NAME")) != NULL) scriptname = tmp;
  enc = NULL;
  lang = NULL;
  title = NULL;
  index = NULL;
  prefix = NULL;
  diridx = NULL;
  help = NULL;
  if((lines = cbreadlines(QG_CONFFILE)) != NULL){
    for(i = 0; i < cblistnum(lines); i++){
      tmp = cblistval(lines, i, NULL);
      if(fwmatch(tmp, "encoding:")){
        enc = skiplabel(tmp);
      } else if(fwmatch(tmp, "lang:")){
        lang = skiplabel(tmp);
      } else if(fwmatch(tmp, "title:")){
        title = skiplabel(tmp);
      } else if(fwmatch(tmp, "index:")){
        index = skiplabel(tmp);
      } else if(fwmatch(tmp, "prefix:")){
        prefix = skiplabel(tmp);
      } else if(fwmatch(tmp, "diridx:")){
        diridx = skiplabel(tmp);
      } else if(fwmatch(tmp, "help:")){
        help = skiplabel(tmp);
      }
    }
  }
  if(!enc) enc = QG_DEFENC;
  if(!lang) lang = QG_DEFLANG;
  if(!title) title = QG_DEFTITLE;
  if(!index) index = QG_DEFINDEX;
  if(!prefix) prefix = QG_DEFPREFIX;
  if(!help) help = "";
  /* read parameters */
  phrase = NULL;
  unit = QG_UNITAND;
  max = 0;
  skip = 0;
  id = 0;
  params = getparams();
  if((tmp = cbmapget(params, "phrase", -1, NULL)) != NULL) phrase = tmp;
  if((tmp = cbmapget(params, "unit", -1, NULL)) != NULL) unit = atoi(tmp);
  if((tmp = cbmapget(params, "max", -1, NULL)) != NULL) max = atoi(tmp);
  if((tmp = cbmapget(params, "skip", -1, NULL)) != NULL) skip = atoi(tmp);
  if((tmp = cbmapget(params, "id", -1, NULL)) != NULL) id = atoi(tmp);
  if(!phrase) phrase = "";
  if(max < 1) max = QG_DEFMAX;
  if(skip < 0) skip = 0;
  /* show page */
  printmime();
  printdecl();
  htmlprintf("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"%s\" lang=\"%s\">\n",
             lang, lang);
  printhead();
  htmlprintf("<body>\n");
  showform(phrase, unit, max);
  htmlprintf("<div class=\"result\">\n");
  words = getwords(phrase);
  if(id > 0){
    showwords(id, words, phrase, unit, max, index, prefix, diridx);
  } else if(cblistnum(words) > 0){
    showresult(words, phrase, unit, max, skip, index, prefix, diridx);
  } else {
    showhelp(help);
  }
  cblistclose(words);
  htmlprintf("</div>\n");
  htmlprintf("</body>\n");
  htmlprintf("</html>\n");
  /* release resources */
  cbmapclose(params);
  if(lines) cblistclose(lines);
  return 0;
}


/* forward matching */
int fwmatch(const char *str, const char *key){
  int len, i;
  len = strlen(key);
  for(i = 0; i < len; i++){
    if(str[i] != key[i] || str[i] == '\0') return FALSE;
  }
  return TRUE;
}


/* backward matching */
int bwmatch(const char *str, const char *key){
  int slen, klen, i;
  slen = strlen(str);
  klen = strlen(key);
  for(i = 1; i <= klen; i++){
    if(str[slen-i] != key[klen-i] || i > slen) return FALSE;
  }
  return TRUE;
}


/* skip the label of a line */
const char *skiplabel(const char *str){
  if(!(str = strchr(str, ':'))) return str;
  str++;
  while(*str != '\0' && (*str == ' ' || *str == '\t')){
    str++;
  }
  return str;
}


/* get a map of the CGI parameters */
CBMAP *getparams(void){
  CBMAP *params;
  CBLIST *pairs;
  char *rbuf, *buf, *key, *val, *dkey, *dval;
  const char *tmp;
  int i, len, c;
  params = cbmapopen();
  rbuf = NULL;
  buf = NULL;
  if((tmp = getenv("CONTENT_LENGTH")) != NULL && (len = atoi(tmp)) > 0 && len <= QG_RDATAMAX){
    rbuf = malloc(len + 1);
    for(i = 0; i < len && (c = getchar()) != EOF; i++){
      rbuf[i] = c;
    }
    rbuf[i] = '\0';
    if(i == len) buf = rbuf;
  } else {
    buf = getenv("QUERY_STRING");
  }
  if(buf != NULL){
    buf = cbmemdup(buf, -1);
    pairs = cbsplit(buf, -1, "&");
    for(i = 0; i < cblistnum(pairs); i++){
      key = cbmemdup(cblistval(pairs, i, NULL), -1);
      if((val = strchr(key, '=')) != NULL){
        *(val++) = '\0';
        dkey = cburldecode(key, NULL);
        dval = cburldecode(val, NULL);
        cbmapput(params, dkey, -1, dval, -1, FALSE);
        free(dval);
        free(dkey);
      }
      free(key);
    }
    cblistclose(pairs);
    free(buf);
  }
  free(rbuf);
  return params;
}


/* send error status */
void senderror(int code, const char *tag, const char *message){
  printf("Status: %d %s\r\n", code, tag);
  printf("Content-Type: text/plain; charset=US-ASCII\r\n");
  printf("\r\n");
  printf("%s\n", message);
}


/* HTML-oriented printf */
void htmlprintf(const char *format, ...){
  va_list ap;
  char *tmp;
  unsigned char c;
  va_start(ap, format);
  while(*format != '\0'){
    if(*format == '%'){
      format++;
      switch(*format){
      case 's':
        tmp = va_arg(ap, char *);
        if(!tmp) tmp = "(null)";
        printf("%s", tmp);
        break;
      case 'd':
        printf("%d", va_arg(ap, int));
        break;
      case '@':
        tmp = va_arg(ap, char *);
        if(!tmp) tmp = "(null)";
        while(*tmp){
          switch(*tmp){
          case '&': printf("&amp;"); break;
          case '<': printf("&lt;"); break;
          case '>': printf("&gt;"); break;
          case '"': printf("&quot;"); break;
          default: putchar(*tmp); break;
          }
          tmp++;
        }
        break;
      case '?':
        tmp = va_arg(ap, char *);
        if(!tmp) tmp = "(null)";
        while(*tmp){
          c = *(unsigned char *)tmp;
          if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
             (c >= '0' && c <= '9') || (c != '\0' && strchr("_-.", c))){
            putchar(c);
          } else if(c == ' '){
            putchar('+');
          } else {
            printf("%%%02X", c);
          }
          tmp++;
        }
        break;
      case '%':
        putchar('%');
        break;
      }
    } else {
      putchar(*format);
    }
    format++;
  }
  va_end(ap);
}


/* print mime headers */
void printmime(void){
  printf("Content-Type: text/html; charset=%s\r\n", enc);
  printf("Cache-Control: no-cache, must-revalidate\r\n");
  printf("Pragma: no-cache\r\n");
  printf("\r\n");
  fflush(stdout);
}


/* print the declarations of XHTML */
void printdecl(void){
  htmlprintf("<?xml version=\"1.0\" encoding=\"%s\"?>\n", enc);
  htmlprintf("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" "
             "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n");
}


/* print headers */
void printhead(void){
  htmlprintf("<head>\n");
  htmlprintf("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=%s\" />\n", enc);
  htmlprintf("<meta http-equiv=\"Content-Style-Type\" content=\"text/css\" />\n");
  htmlprintf("<link rel=\"contents\" href=\"./\" />\n");
  htmlprintf("<title>%@</title>\n", title);
  htmlprintf("<style type=\"text/css\">\n");
  htmlprintf("body { background-color: #eeeeee; color: #111111;"
             " margin: 0em 0em; padding: 0em 0em; }\n");
  htmlprintf("h1,h2,p { margin: 0.5em 0.8em; }\n");
  htmlprintf("a { color: #0022aa; text-decoration: none; }\n");
  htmlprintf("a:hover { color: #1144ff; text-decoration: underline; }\n");
  htmlprintf("a.head { color: #111111; text-decoration: none; }\n");
  htmlprintf("em { font-weight: bold; font-style: normal; color: #001199; }\n");
  htmlprintf("form { background-color: #ddddee; margin: 0em 0em; padding: 0.7em 0.8em;"
             " border-bottom: 1pt solid #bbbbcc; }\n");
  htmlprintf("dd { margin: 0.1em 0.3em 0.1em 1.8em; font-size: small; }\n");
  htmlprintf(".result { margin: 0em 0em; padding: 0.5em 1.0em; }\n");
  htmlprintf(".title { font-weight: bold; }\n");
  htmlprintf(".summary { background-color: #ddddee; padding: 0.2em 0.3em;"
             " border: 1pt solid #bbbbcc; }\n");
  htmlprintf(".summary em { background-color: #ddeeee; color: #001188;"
             " border: 1pt solid #ccccdd; padding: 0.0em 0.2em; }\n");
  htmlprintf(".note { margin: 0.5em 0.5em; text-align: right; color: #666666; }\n");
  htmlprintf(".blur { color: #666666; }\n");
  htmlprintf("</style>\n");
  htmlprintf("</head>\n");
}


/* show search form */
void showform(const char *phrase, int unit, int max){
  int i;
  htmlprintf("<form action=\"%@\" method=\"get\">\n", scriptname);
  htmlprintf("<div>\n");
  htmlprintf("Phrase: <input type=\"text\" name=\"phrase\" value=\"%@\" size=\"48\" />\n",
             phrase);
  htmlprintf("<select name=\"unit\">\n");
  htmlprintf("<option value=\"%d\"%s>all of the words</option>\n",
             QG_UNITAND, unit == QG_UNITAND ? " selected=\"selected\"" : "");
  htmlprintf("<option value=\"%d\"%s>any of the words</option>\n",
             QG_UNITOR, unit == QG_UNITOR ? " selected=\"selected\"" : "");
  htmlprintf("</select>\n");
  htmlprintf("<select name=\"max\">\n");
  for(i = 1; i <= 256; i *= 2){
    htmlprintf("<option value=\"%d\"%s>%d per page</option>\n",
               i, i == max ? " selected=\"selected\"" : "", i);
  }
  htmlprintf("</select>\n");
  htmlprintf("<input type=\"submit\" value=\"Search\" />\n");
  htmlprintf("</div>\n");
  htmlprintf("</form>\n");
}


/* break phrase into words */
CBLIST *getwords(const char *phrase){
  CBLIST *words, *tmp;
  char *normal;
  int i;
  words = cblistopen();
  tmp = odbreaktext(phrase);
  for(i = 0; i < cblistnum(tmp); i++){
    normal = odnormalizeword(cblistval(tmp, i, NULL));
    if(strlen(normal) > 0) cblistpush(words, normal, -1);
    free(normal);
  }
  cblistclose(tmp);
  return words;
}


/* replace a local path to its URI */
char *replaceuri(const char *uri){
  char *buf;
  int i;
  buf = cbmemdup(uri, -1);
  for(i = 0; buf[i] != '\0'; i++){
    if(buf[i] == '\\') buf[i] = '/';
  }
  return buf;
}


/* show search result */
void showresult(const CBLIST *words, const char *phrase, int unit, int max, int skip,
                const char *index, const char *prefix, const char *diridx){
  ODEUM *odeum;
  ODPAIR *pairs, *last, *tmp;
  ODDOC *doc;
  const char *word, *uri, *title, *date, *author;
  char *ubuf;
  int i, j, pnum, lnum, hnum, tnum, ulen;
  double ival;
  if(!(odeum = odopen(index, OD_OREADER))){
    htmlprintf("<p>The index cannot be open because of `%@'.</p>\n", dperrmsg(dpecode));
    return;
  }
  htmlprintf("<p>");
  last = NULL;
  lnum = 0;
  for(i = 0; i < cblistnum(words); i++){
    word = cblistval(words, i, NULL);
    if(!(pairs = odsearch(odeum, word, -1, &pnum))) continue;
    if((hnum = odsearchdnum(odeum, word)) < 0) hnum = 0;
    ival = odlogarithm(hnum);
    ival = (ival * ival) / 2.0;
    if(ival < 2.0) ival = 2.0;
    for(j = 0; j < pnum; j++){
      pairs[j].score /= ival;
    }
    if(last) htmlprintf(" %@ ", unit == QG_UNITOR ? "+" : "*");
    htmlprintf("<em>%@</em> (%d)", word, hnum);
    if(last){
      if(unit == QG_UNITOR){
        tmp = odpairsor(last, lnum, pairs, pnum, &tnum);
      } else {
        tmp = odpairsand(last, lnum, pairs, pnum, &tnum);
      }
      free(last);
      free(pairs);
      last = tmp;
      lnum = tnum;
    } else {
      last = pairs;
      lnum = pnum;
    }
  }
  htmlprintf(" = <em>%d</em> hits</p>\n", lnum);
  if(last && lnum > 0){
    for(i = skip; i < lnum && i < max + skip; i++){
      if(!(doc = odgetbyid(odeum, last[i].id))) continue;
      ubuf = replaceuri(oddocuri(doc));
      if(diridx && bwmatch(ubuf, diridx)){
        ulen = strlen(ubuf) - strlen(diridx);
        if(ulen > 1 && ubuf[ulen-1] == '/') ubuf[ulen] = '\0';
      }
      uri = ubuf;
      if(fwmatch(uri, QG_DEFPREFIX)) uri += strlen(QG_DEFPREFIX);
      title = oddocgetattr(doc, "title");
      date = oddocgetattr(doc, "date");
      author = oddocgetattr(doc, "author");
      htmlprintf("<dl>\n");
      htmlprintf("<dt class=\"blur\">%d: <a href=\"%@%@\" class=\"title\">%@</a> (%d pt.)</dt>\n",
                 i + 1, prefix, uri,
                 title && strlen(title) > 0 ? title : "(untitled)", last[i].score);
      showsummary(doc, words, phrase, unit, max);
      htmlprintf("<dd class=\"blur\">");
      htmlprintf("%@%@", prefix, uri);
      if(date) htmlprintf(" (%@)", date);
      if(author) htmlprintf(" (%@)", author);
      htmlprintf("</dd>\n");
      htmlprintf("</dl>\n");
      free(ubuf);
      oddocclose(doc);
    }
  } else {
    htmlprintf("<p>No document hits.</p>\n");
  }
  if(last) free(last);
  htmlprintf("<div class=\"note\">");
  if(skip > 0){
    htmlprintf("<a href=\"%@?phrase=%?&amp;unit=%d&amp;max=%d&amp;skip=%d\">[PREV]</a>",
               scriptname, phrase, unit, max, skip - max);
  } else {
    htmlprintf("<span class=\"blur\">[PREV]</span>");
  }
  htmlprintf(" ");
  if(i < lnum){
    htmlprintf("<a href=\"%@?phrase=%?&amp;unit=%d&amp;max=%d&amp;skip=%d\">[NEXT]</a>",
               scriptname, phrase, unit, max, skip + max);
  } else {
    htmlprintf("<span class=\"blur\">[NEXT]</span>");
  }
  htmlprintf("</div>\n");
  htmlprintf("<div class=\"note\">The index contains %d documents and %d words.</div>\n",
             oddnum(odeum), odwnum(odeum));
  odclose(odeum);
}


/* show summary of a document */
void showsummary(const ODDOC *doc, const CBLIST *kwords, const char *phrase, int unit, int max){
  const CBLIST *nwords, *awords;
  CBMAP *kmap, *map;
  const char *normal, *asis;
  int i, j, num, lnum, nwsiz, awsiz, pv, bi, first, em;
  htmlprintf("<dd class=\"summary\">");
  num = QG_SUMWORDMAX;
  nwords = oddocnwords(doc);
  awords = oddocawords(doc);
  kmap = listtomap(kwords);
  map = listtomap(kwords);
  lnum = cblistnum(nwords);
  first = TRUE;
  em = FALSE;
  for(i = 0; i < lnum && i < QG_SUMTOP; i++){
    normal = cblistval(nwords, i, &nwsiz);
    asis = cblistval(awords, i, &awsiz);
    if(awsiz < 1) continue;
    cbmapout(map, normal, nwsiz);
    if(normal[0] != '\0' && cbmapget(kmap, normal, nwsiz, NULL)){
      if(!first) htmlprintf(" ");
      if(!em) htmlprintf("<em>");
      htmlprintf("%@", asis);
      em = TRUE;
    } else {
      if(em) htmlprintf("</em>");
      if(!first) htmlprintf(" ");
      htmlprintf("%@", asis);
      em = FALSE;
    }
    first = FALSE;
    num--;
  }
  if(em) htmlprintf("</em>");
  htmlprintf(" ...");
  em = FALSE;
  pv = i;
  while(i < lnum){
    if(cbmaprnum(map) < 1){
      cbmapclose(map);
      map = listtomap(kwords);
    }
    normal = cblistval(nwords, i, &nwsiz);
    if(cbmapget(map, normal, nwsiz, NULL)){
      bi = i - QG_SUMWIDTH / 2;
      bi = bi > pv ? bi : pv;
      for(j = bi; j < lnum && j <= bi + QG_SUMWIDTH; j++){
        normal = cblistval(nwords, j, &nwsiz);
        asis = cblistval(awords, j, &awsiz);
        if(awsiz < 1) continue;
        cbmapout(map, normal, nwsiz);
        if(normal[0] != '\0' && cbmapget(kmap, normal, nwsiz, NULL)){
          htmlprintf(" ");
          if(!em) htmlprintf("<em>");
          htmlprintf("%@", asis);
          em = TRUE;
        } else {
          if(em) htmlprintf("</em>");
          htmlprintf(" ");
          htmlprintf("%@", asis);
          em = FALSE;
        }
        num--;
      }
      if(em) htmlprintf("</em>");
      htmlprintf(" ...");
      em = FALSE;
      i = j + 1;
      pv = i;
    } else {
      i++;
    }
    if(num <= 0) break;
  }
  htmlprintf(" <a href=\"%@?id=%d&amp;phrase=%?&amp;unit=%d&amp;max=%d\">[more]</a>",
             scriptname, oddocid(doc), phrase, unit, max);
  cbmapclose(map);
  cbmapclose(kmap);
  htmlprintf("</dd>\n");
}


/* get a map made from a list */
CBMAP *listtomap(const CBLIST *list){
  CBMAP *map;
  const char *tmp;
  int i, tsiz;
  map = cbmapopen();
  for(i = 0; i < cblistnum(list); i++){
    tmp = cblistval(list, i, &tsiz);
    cbmapput(map, tmp, tsiz, "", 0, FALSE);
  }
  return map;
}


/* show words in a document */
void showwords(int id, const CBLIST *words, const char *phrase, int unit, int max,
               const char *index, const char *prefix, const char *diridx){
  ODEUM *odeum;
  ODDOC *doc;
  const CBLIST *awords, *nwords;
  CBLIST *kwords;
  CBMAP *kmap;
  const char *uri, *title, *date, *author, *kword, *asis, *normal;
  char *ubuf;
  int i, wnum, nwsiz, awsiz, first, em, ulen;
  if(!(odeum = odopen(index, OD_OREADER))){
    htmlprintf("<p>The index cannot be open because of `%@'.</p>\n", dperrmsg(dpecode));
    return;
  }
  if((doc = odgetbyid(odeum, id)) != NULL){
    ubuf = replaceuri(oddocuri(doc));
    if(diridx && bwmatch(ubuf, diridx)){
      ulen = strlen(ubuf) - strlen(diridx);
      if(ulen > 1 && ubuf[ulen-1] == '/') ubuf[ulen] = '\0';
    }
    uri = ubuf;
    if(fwmatch(uri, QG_DEFPREFIX)) uri += strlen(QG_DEFPREFIX);
    title = oddocgetattr(doc, "title");
    date = oddocgetattr(doc, "date");
    author = oddocgetattr(doc, "author");
    awords = oddocawords(doc);
    nwords = oddocnwords(doc);
    htmlprintf("<div>ID: %d</div>\n", oddocid(doc));
    htmlprintf("<div>URI: <a href=\"%@%@\">%@%@</a></div>\n", prefix, uri, prefix, uri);
    if(title) htmlprintf("<div>Title: <span class=\"title\">%@</span></div>\n", title);
    if(date) htmlprintf("<div>Date: %@</div>\n", date);
    if(author) htmlprintf("<div>Author: %@</div>\n", author);
    kwords = oddockwords(doc, QG_KEYWORDS, odeum);
    htmlprintf("<div>Keywords: ");
    for(i = 0; i < cblistnum(kwords); i++){
      kword = cblistval(kwords, i, NULL);
      if(i > 0) htmlprintf(", ");
      htmlprintf("<a href=\"%@?phrase=%?+%?&amp;&amp;unit=%d&amp;max=%d\">%@</a>",
                 scriptname, phrase, kword, unit, max, kword);
    }
    htmlprintf("</div>\n");
    cblistclose(kwords);
    wnum = cblistnum(awords);
    kmap = listtomap(words);
    htmlprintf("<dl>\n");
    htmlprintf("<dt>Words: %d (or more)</dt>\n", wnum);
    htmlprintf("<dd class=\"summary\">");
    first = TRUE;
    em = FALSE;
    for(i = 0; i < wnum; i++){
      normal = cblistval(nwords, i, &nwsiz);
      asis = cblistval(awords, i, &awsiz);
      if(awsiz < 1) continue;
      if(normal[0] != '\0' && cbmapget(kmap, normal, nwsiz, NULL)){
        if(!first) htmlprintf(" ");
        if(!em) htmlprintf("<em>");
        htmlprintf("%@", asis);
        em = TRUE;
      } else {
        if(em) htmlprintf("</em>");
        if(!first) htmlprintf(" ");
        htmlprintf("%@", asis);
        em = FALSE;
      }
      first = FALSE;
    }
    if(em) htmlprintf("</em>");
    htmlprintf(" ...");
    htmlprintf("</dd>\n");
    htmlprintf("</dl>\n");
    cbmapclose(kmap);
    free(ubuf);
    oddocclose(doc);
  } else {
    htmlprintf("<p>Retrieving the document failed because of `%@'.</p>\n", dperrmsg(dpecode));
  }
  odclose(odeum);
}


/* show help message */
void showhelp(const char *help){
  htmlprintf("<h1>%@</h1>\n", title);
  htmlprintf("<p>Input search words into"
             " the above form and push the [Search] button.</p>\n");
  if(strlen(help) > 0) htmlprintf("%s\n", help);
  htmlprintf("<div class=\"note\">Powered by QDBM %@.</div>\n",
             dpversion);
}



/* END OF FILE */
