Escape &<> when they appear in href text.
[sgt/halibut] / bk_html.c
index bb98eb8..e3202a5 100644 (file)
--- a/bk_html.c
+++ b/bk_html.c
  *    fragment should be used? (Though it should probably still be
  *    _there_ even if unused.)
  * 
- *  - new configurability:
- *     * a few new things explicitly labelled as `FIXME:
- *      configurable' or similar.
- *     * HTML flavour.
- *     * Some means of specifying the distinction between
- *      restrict-charset and output-charset. It seems to me that
- *      `html-charset' is output-charset, and that
- *      restrict-charset usually wants to be either output-charset
- *      or UTF-8 (the latter indicating that any Unicode character
- *      is fair game and it will be specified using &#foo; if it
- *      isn't in output-charset). However, since XHTML defaults to
- *      UTF-8 and it's fiddly to tell it otherwise, it's just
- *      possible that some user may need to set restrict-charset
- *      to their charset of choice while leaving _output_-charset
- *      at UTF-8. Figure out some configuration, and apply it.
- *
- *  - test all HTML flavours and ensure they validate sensibly. Fix
- *    remaining confusion issues such as <?xml?> and obsoleteness
- *    of <a name>.
- * 
- *  - proper naming of all fragment IDs. The ones for sections are
- *    fine; the ones for numbered list and bibliociteds are utter
- *    crap; the ones for indexes _might_ do but it might be worth
- *    giving some thought to how to do them better.
- * 
- *  - nonbreaking spaces.
- * 
- *  - free up all the data we have allocated while running this
- *    backend.
+ *  - In HHK index mode: subsidiary hhk entries (as in replacing
+ *    `foo, bar' with `foo\n\tbar') can be done by embedding
+ *    sub-<UL>s in the hhk file. This requires me getting round to
+ *    supporting that idiom in the rest of Halibut, but I thought
+ *    I'd record how it's done here in case I turn out to have
+ *    forgotten when I get there.
  */
 
 #include <stdio.h>
@@ -60,7 +37,7 @@
                           (p)->type == para_Title ? -1 : 0 )
 
 typedef struct {
-    int just_numbers;
+    int number_at_all, just_numbers;
     wchar_t *number_suffix;
 } sectlevel;
 
@@ -71,17 +48,25 @@ typedef struct {
     int ncdepths;
     int address_section, visible_version_id;
     int leaf_contains_contents, leaf_smallest_contents;
+    int navlinks;
+    int rellinks;
     char *contents_filename;
     char *index_filename;
     char *template_filename;
     char *single_filename;
-    char *template_fragment;
+    char *chm_filename, *hhp_filename, *hhc_filename, *hhk_filename;
+    char **template_fragments;
+    int ntfragments;
     char *head_end, *body_start, *body_end, *addr_start, *addr_end;
     char *body_tag, *nav_attr;
     wchar_t *author, *description;
+    wchar_t *index_text, *contents_text, *preamble_text, *title_separator;
+    wchar_t *nav_prev_text, *nav_next_text, *nav_up_text, *nav_separator;
+    wchar_t *index_main_sep, *index_multi_sep;
+    wchar_t *pre_versionid, *post_versionid;
     int restrict_charset, output_charset;
     enum {
-       HTML_3_2, HTML_4,
+       HTML_3_2, HTML_4, ISO_HTML,
        XHTML_1_0_TRANSITIONAL, XHTML_1_0_STRICT
     } htmlver;
     wchar_t *lquote, *rquote;
@@ -102,6 +87,13 @@ struct htmlfile {
     int last_fragment_number;
     int min_heading_depth;
     htmlsect *first, *last;           /* first/last highest-level sections */
+    /*
+     * The `temp' field is available for use in individual passes
+     * over the file list. For example, the HHK index generation
+     * uses it to ensure no index term references the same file
+     * more than once.
+     */
+    int temp;
 };
 
 struct htmlsect {
@@ -110,13 +102,14 @@ struct htmlsect {
     paragraph *title, *text;
     enum { NORMAL, TOP, INDEX } type;
     int contents_depth;
-    char *fragment;
+    char **fragments;
 };
 
 typedef struct {
     htmlfile *head, *tail;
     htmlfile *single, *index;
     tree234 *frags;
+    tree234 *files;
 } htmlfilelist;
 
 typedef struct {
@@ -136,6 +129,7 @@ typedef struct {
 typedef struct {
     htmlsect *section;
     char *fragment;
+    int generated, referenced;
 } htmlindexref;
 
 typedef struct {
@@ -145,12 +139,14 @@ typedef struct {
      * level.
      */
     FILE *fp;
-    int charset;
+    int charset, restrict_charset;
     charset_state cstate;
     int ver;
     enum {
        HO_NEUTRAL, HO_IN_TAG, HO_IN_EMPTY_TAG, HO_IN_TEXT
     } state;
+    int hackflags;                    /* used for icky .HH* stuff */
+    int hacklimit;                    /* text size limit, again for .HH* */
     /*
      * Stuff beyond here deals with the higher syntactic level: it
      * tracks how many levels of <ul> are currently open when
@@ -159,6 +155,21 @@ typedef struct {
     int contents_level;
 } htmloutput;
 
+/*
+ * Nasty hacks that modify the behaviour of htmloutput files. All
+ * of these are flag bits set in ho.hackflags. HO_HACK_QUOTEQUOTES
+ * has the same effect as the `quote_quotes' parameter to
+ * html_text_limit_internal, except that it's set globally on an
+ * entire htmloutput structure; HO_HACK_QUOTENOTHING suppresses
+ * quoting of any HTML special characters (for .HHP files);
+ * HO_HACK_OMITQUOTES completely suppresses the generation of
+ * double quotes at all (turning them into single quotes, for want
+ * of a better idea).
+ */
+#define HO_HACK_QUOTEQUOTES 1
+#define HO_HACK_QUOTENOTHING 2
+#define HO_HACK_OMITQUOTES 4
+
 static int html_fragment_compare(void *av, void *bv)
 {
     htmlfragment *a = (htmlfragment *)av;
@@ -171,11 +182,20 @@ static int html_fragment_compare(void *av, void *bv)
        return strcmp(a->fragment, b->fragment);
 }
 
+static int html_filename_compare(void *av, void *bv)
+{
+    char *a = (char *)av;
+    char *b = (char *)bv;
+
+    return strcmp(a, b);
+}
+
 static void html_file_section(htmlconfig *cfg, htmlfilelist *files,
                              htmlsect *sect, int depth);
 
 static htmlfile *html_new_file(htmlfilelist *list, char *filename);
-static htmlsect *html_new_sect(htmlsectlist *list, paragraph *title);
+static htmlsect *html_new_sect(htmlsectlist *list, paragraph *title,
+                              htmlconfig *cfg);
 
 /* Flags for html_words() flags parameter */
 #define NOTHING 0x00
@@ -194,9 +214,10 @@ static void element_attr(htmloutput *ho, char const *name, char const *value);
 static void element_attr_w(htmloutput *ho, char const *name,
                           wchar_t const *value);
 static void html_text(htmloutput *ho, wchar_t const *str);
+static void html_text_nbsp(htmloutput *ho, wchar_t const *str);
 static void html_text_limit(htmloutput *ho, wchar_t const *str, int maxlen);
 static void html_text_limit_internal(htmloutput *ho, wchar_t const *text,
-                                    int maxlen, int quote_quotes);
+                                    int maxlen, int quote_quotes, int nbsp);
 static void html_nl(htmloutput *ho);
 static void html_raw(htmloutput *ho, char *text);
 static void html_raw_as_attr(htmloutput *ho, char *text);
@@ -204,10 +225,12 @@ static void cleanup(htmloutput *ho);
 
 static void html_href(htmloutput *ho, htmlfile *thisfile,
                      htmlfile *targetfile, char *targetfrag);
+static void html_fragment(htmloutput *ho, char const *fragment);
 
 static char *html_format(paragraph *p, char *template_string);
 static char *html_sanitise_fragment(htmlfilelist *files, htmlfile *file,
                                    char *text);
+static char *html_sanitise_filename(htmlfilelist *files, char *text);
 
 static void html_contents_entry(htmloutput *ho, int depth, htmlsect *s,
                                htmlfile *thisfile, keywordlist *keywords,
@@ -225,10 +248,12 @@ static htmlconfig html_configure(paragraph *source) {
      */
     ret.leaf_level = 2;
     ret.achapter.just_numbers = FALSE;
+    ret.achapter.number_at_all = TRUE;
     ret.achapter.number_suffix = L": ";
     ret.nasect = 1;
     ret.asect = snewn(ret.nasect, sectlevel);
     ret.asect[0].just_numbers = TRUE;
+    ret.asect[0].number_at_all = TRUE;
     ret.asect[0].number_suffix = L" ";
     ret.ncdepths = 0;
     ret.contents_depths = 0;
@@ -236,17 +261,35 @@ static htmlconfig html_configure(paragraph *source) {
     ret.address_section = TRUE;
     ret.leaf_contains_contents = FALSE;
     ret.leaf_smallest_contents = 4;
+    ret.navlinks = TRUE;
+    ret.rellinks = TRUE;
     ret.single_filename = dupstr("Manual.html");
     ret.contents_filename = dupstr("Contents.html");
     ret.index_filename = dupstr("IndexPage.html");
     ret.template_filename = dupstr("%n.html");
-    ret.template_fragment = dupstr("%b");
+    ret.chm_filename = ret.hhp_filename = NULL;
+    ret.hhc_filename = ret.hhk_filename = NULL;
+    ret.ntfragments = 1;
+    ret.template_fragments = snewn(ret.ntfragments, char *);
+    ret.template_fragments[0] = dupstr("%b");
     ret.head_end = ret.body_tag = ret.body_start = ret.body_end =
        ret.addr_start = ret.addr_end = ret.nav_attr = NULL;
     ret.author = ret.description = NULL;
-    ret.restrict_charset = CS_ASCII;
+    ret.restrict_charset = CS_UTF8;
     ret.output_charset = CS_ASCII;
     ret.htmlver = HTML_4;
+    ret.index_text = L"Index";
+    ret.contents_text = L"Contents";
+    ret.preamble_text = L"Preamble";
+    ret.title_separator = L" - ";
+    ret.nav_prev_text = L"Previous";
+    ret.nav_next_text = L"Next";
+    ret.nav_up_text = L"Up";
+    ret.nav_separator = L" | ";
+    ret.index_main_sep = L": ";
+    ret.index_multi_sep = L", ";
+    ret.pre_versionid = L"[";
+    ret.post_versionid = L"]";
     /*
      * Default quote characters are Unicode matched single quotes,
      * falling back to ordinary ASCII ".
@@ -267,6 +310,10 @@ static htmlconfig html_configure(paragraph *source) {
                    ret.lquote = uadv(p->keyword);
                    ret.rquote = uadv(ret.lquote);
                }
+           } else if (!ustricmp(p->keyword, L"index")) {
+               ret.index_text = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"contents")) {
+               ret.contents_text = uadv(p->keyword);
            }
        }
     }
@@ -278,11 +325,32 @@ static htmlconfig html_configure(paragraph *source) {
            if (!ustrnicmp(k, L"xhtml-", 6))
                k++;                /* treat `xhtml-' and `html-' the same */
 
-           if (!ustricmp(k, L"html-charset")) {
-               char *csname = utoa_dup(uadv(k), CS_ASCII);
-               ret.restrict_charset = ret.output_charset =
-                   charset_from_localenc(csname);
-               sfree(csname);
+           if (!ustricmp(k, L"html-restrict-charset")) {
+               ret.restrict_charset = charset_from_ustr(&p->fpos, uadv(k));
+           } else if (!ustricmp(k, L"html-output-charset")) {
+               ret.output_charset = charset_from_ustr(&p->fpos, uadv(k));
+           } else if (!ustricmp(k, L"html-version")) {
+               wchar_t *vername = uadv(k);
+               static const struct {
+                   const wchar_t *name;
+                   int ver;
+               } versions[] = {
+                   {L"html3.2", HTML_3_2},
+                   {L"html4", HTML_4},
+                   {L"iso-html", ISO_HTML},
+                   {L"xhtml1.0transitional", XHTML_1_0_TRANSITIONAL},
+                   {L"xhtml1.0strict", XHTML_1_0_STRICT}
+               };
+               int i;
+
+               for (i = 0; i < (int)lenof(versions); i++)
+                   if (!ustricmp(versions[i].name, vername))
+                       break;
+
+               if (i == lenof(versions))
+                   error(err_htmlver, &p->fpos, vername);
+               else
+                   ret.htmlver = versions[i].ver;
            } else if (!ustricmp(k, L"html-single-filename")) {
                sfree(ret.single_filename);
                ret.single_filename = dupstr(adv(p->origkeyword));
@@ -296,14 +364,42 @@ static htmlconfig html_configure(paragraph *source) {
                sfree(ret.template_filename);
                ret.template_filename = dupstr(adv(p->origkeyword));
            } else if (!ustricmp(k, L"html-template-fragment")) {
-               sfree(ret.template_fragment);
-               ret.template_fragment = dupstr(adv(p->origkeyword));
+               char *frag = adv(p->origkeyword);
+               if (*frag) {
+                   while (ret.ntfragments--)
+                       sfree(ret.template_fragments[ret.ntfragments]);
+                   sfree(ret.template_fragments);
+                   ret.template_fragments = NULL;
+                   ret.ntfragments = 0;
+                   while (*frag) {
+                       ret.ntfragments++;
+                       ret.template_fragments =
+                           sresize(ret.template_fragments,
+                                   ret.ntfragments, char *);
+                       ret.template_fragments[ret.ntfragments-1] =
+                           dupstr(frag);
+                       frag = adv(frag);
+                   }
+               } else
+                   error(err_cfginsufarg, &p->fpos, p->origkeyword, 1);
            } else if (!ustricmp(k, L"html-chapter-numeric")) {
                ret.achapter.just_numbers = utob(uadv(k));
+           } else if (!ustricmp(k, L"html-chapter-shownumber")) {
+               ret.achapter.number_at_all = utob(uadv(k));
+           } else if (!ustricmp(k, L"html-suppress-navlinks")) {
+               ret.navlinks = !utob(uadv(k));
+           } else if (!ustricmp(k, L"html-rellinks")) {
+               ret.rellinks = utob(uadv(k));
            } else if (!ustricmp(k, L"html-chapter-suffix")) {
                ret.achapter.number_suffix = uadv(k);
            } else if (!ustricmp(k, L"html-leaf-level")) {
-               ret.leaf_level = utoi(uadv(k));
+               wchar_t *u = uadv(k);
+               if (!ustricmp(u, L"infinite") ||
+                   !ustricmp(u, L"infinity") ||
+                   !ustricmp(u, L"inf"))
+                   ret.leaf_level = -1;   /* represents infinity */
+               else
+                   ret.leaf_level = utoi(u);
            } else if (!ustricmp(k, L"html-section-numeric")) {
                wchar_t *q = uadv(k);
                int n = 0;
@@ -319,6 +415,21 @@ static htmlconfig html_configure(paragraph *source) {
                    ret.nasect = n+1;
                }
                ret.asect[n].just_numbers = utob(q);
+           } else if (!ustricmp(k, L"html-section-shownumber")) {
+               wchar_t *q = uadv(k);
+               int n = 0;
+               if (uisdigit(*q)) {
+                   n = utoi(q);
+                   q = uadv(q);
+               }
+               if (n >= ret.nasect) {
+                   int i;
+                   ret.asect = sresize(ret.asect, n+1, sectlevel);
+                   for (i = ret.nasect; i <= n; i++)
+                       ret.asect[i] = ret.asect[ret.nasect-1];
+                   ret.nasect = n+1;
+               }
+               ret.asect[n].number_at_all = utob(q);
            } else if (!ustricmp(k, L"html-section-suffix")) {
                wchar_t *q = uadv(k);
                int n = 0;
@@ -391,11 +502,66 @@ static htmlconfig html_configure(paragraph *source) {
                ret.leaf_contains_contents = utob(uadv(k));
            } else if (!ustricmp(k, L"html-leaf-smallest-contents")) {
                ret.leaf_smallest_contents = utoi(uadv(k));
+           } else if (!ustricmp(k, L"html-index-text")) {
+               ret.index_text = uadv(k);
+           } else if (!ustricmp(k, L"html-contents-text")) {
+               ret.contents_text = uadv(k);
+           } else if (!ustricmp(k, L"html-preamble-text")) {
+               ret.preamble_text = uadv(k);
+           } else if (!ustricmp(k, L"html-title-separator")) {
+               ret.title_separator = uadv(k);
+           } else if (!ustricmp(k, L"html-nav-prev-text")) {
+               ret.nav_prev_text = uadv(k);
+           } else if (!ustricmp(k, L"html-nav-next-text")) {
+               ret.nav_next_text = uadv(k);
+           } else if (!ustricmp(k, L"html-nav-up-text")) {
+               ret.nav_up_text = uadv(k);
+           } else if (!ustricmp(k, L"html-nav-separator")) {
+               ret.nav_separator = uadv(k);
+           } else if (!ustricmp(k, L"html-index-main-separator")) {
+               ret.index_main_sep = uadv(k);
+           } else if (!ustricmp(k, L"html-index-multiple-separator")) {
+               ret.index_multi_sep = uadv(k);
+           } else if (!ustricmp(k, L"html-pre-versionid")) {
+               ret.pre_versionid = uadv(k);
+           } else if (!ustricmp(k, L"html-post-versionid")) {
+               ret.post_versionid = uadv(k);
+           } else if (!ustricmp(k, L"html-mshtmlhelp-chm")) {
+               sfree(ret.chm_filename);
+               ret.chm_filename = dupstr(adv(p->origkeyword));
+           } else if (!ustricmp(k, L"html-mshtmlhelp-project")) {
+               sfree(ret.hhp_filename);
+               ret.hhp_filename = dupstr(adv(p->origkeyword));
+           } else if (!ustricmp(k, L"html-mshtmlhelp-contents")) {
+               sfree(ret.hhc_filename);
+               ret.hhc_filename = dupstr(adv(p->origkeyword));
+           } else if (!ustricmp(k, L"html-mshtmlhelp-index")) {
+               sfree(ret.hhk_filename);
+               ret.hhk_filename = dupstr(adv(p->origkeyword));
            }
        }
     }
 
     /*
+     * Enforce that the CHM and HHP filenames must either be both
+     * present or both absent. If one is present but not the other,
+     * turn both off.
+     */
+    if (!ret.chm_filename ^ !ret.hhp_filename) {
+       error(err_chmnames);
+       sfree(ret.chm_filename); ret.chm_filename = NULL;
+       sfree(ret.hhp_filename); ret.hhp_filename = NULL;
+    }
+    /*
+     * And if we're not generating an HHP, there's no need for HHC
+     * or HHK.
+     */
+    if (!ret.hhp_filename) {
+       sfree(ret.hhc_filename); ret.hhc_filename = NULL;
+       sfree(ret.hhk_filename); ret.hhk_filename = NULL;
+    }
+
+    /*
      * Now process fallbacks on quote characters.
      */
     while (*uadv(ret.rquote) && *uadv(uadv(ret.rquote)) &&
@@ -427,11 +593,15 @@ paragraph *html_config_filename(char *filename)
 }
 
 void html_backend(paragraph *sourceform, keywordlist *keywords,
-                 indexdata *idx, void *unused) {
+                 indexdata *idx, void *unused)
+{
     paragraph *p;
+    htmlsect *topsect;
     htmlconfig conf;
-    htmlfilelist files = { NULL, NULL, NULL, NULL, NULL };
+    htmlfilelist files = { NULL, NULL, NULL, NULL, NULL, NULL };
     htmlsectlist sects = { NULL, NULL }, nonsects = { NULL, NULL };
+    char *hhk_filename;
+    int has_index;
 
     IGNORE(unused);
 
@@ -447,6 +617,7 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
        p->private_data = NULL;
 
     files.frags = newtree234(html_fragment_compare);
+    files.files = newtree234(html_filename_compare);
 
     /*
      * Start by figuring out into which file each piece of the
@@ -457,20 +628,19 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
      * source form but needs to be consistently mentioned in
      * contents links.
      * 
-     * While we're here, we'll also invent the HTML fragment name
+     * While we're here, we'll also invent the HTML fragment name(s)
      * for each section.
      */
     {
-       htmlsect *topsect, *sect;
+       htmlsect *sect;
        int d;
 
-       topsect = html_new_sect(&sects, p);
+       topsect = html_new_sect(&sects, NULL, &conf);
        topsect->type = TOP;
        topsect->title = NULL;
        topsect->text = sourceform;
        topsect->contents_depth = contents_depth(conf, 0);
        html_file_section(&conf, &files, topsect, -1);
-       topsect->fragment = NULL;
 
        for (p = sourceform; p; p = p->next)
            if (is_heading_type(p->type)) {
@@ -481,7 +651,7 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                    continue;
                }
 
-               sect = html_new_sect(&sects, p);
+               sect = html_new_sect(&sects, p, &conf);
                sect->text = p->next;
 
                sect->contents_depth = contents_depth(conf, d+1) - (d+1);
@@ -495,21 +665,36 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
 
                html_file_section(&conf, &files, sect, d);
 
-               sect->fragment = html_format(p, conf.template_fragment);
-               sect->fragment = html_sanitise_fragment(&files, sect->file,
-                                                       sect->fragment);
+               {
+                   int i;
+                   for (i=0; i < conf.ntfragments; i++) {
+                       sect->fragments[i] =
+                           html_format(p, conf.template_fragments[i]);
+                       sect->fragments[i] =
+                           html_sanitise_fragment(&files, sect->file,
+                                                  sect->fragments[i]);
+                   }
+               }
            }
 
-       /* And the index. */
-       sect = html_new_sect(&sects, NULL);
-       sect->text = NULL;
-       sect->type = INDEX;
-       sect->parent = topsect;
-       html_file_section(&conf, &files, sect, 0);   /* peer of chapters */
-       sect->fragment = dupstr("Index");   /* FIXME: this _can't_ be right */
-       sect->fragment = html_sanitise_fragment(&files, sect->file,
-                                               sect->fragment);
-       files.index = sect->file;
+       /*
+        * And the index, if we have one. Note that we don't output
+        * an index as an HTML file if we're outputting one as a
+        * .HHK.
+        */
+       has_index = (count234(idx->entries) > 0);
+       if (has_index && !conf.hhk_filename) {
+           sect = html_new_sect(&sects, NULL, &conf);
+           sect->text = NULL;
+           sect->type = INDEX;
+           sect->parent = topsect;
+            sect->contents_depth = 0;
+           html_file_section(&conf, &files, sect, 0);   /* peer of chapters */
+           sect->fragments[0] = utoa_dup(conf.index_text, CS_ASCII);
+           sect->fragments[0] = html_sanitise_fragment(&files, sect->file,
+                                                       sect->fragments[0]);
+           files.index = sect->file;
+       }
     }
 
     /*
@@ -554,26 +739,37 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                 * won't attempt to add it to the contents or
                 * anything weird like that).
                 */
-               sect = html_new_sect(&nonsects, p);
+               sect = html_new_sect(&nonsects, p, &conf);
                sect->file = parent->file;
                sect->parent = parent;
                p->private_data = sect;
 
                /*
-                * FIXME: We need a much better means of naming
-                * these, possibly involving an additional
-                * configuration template. For the moment I'll just
-                * invent something completely stupid.
+                * Fragment IDs for these paragraphs will simply be
+                * `p' followed by an integer.
                 */
-               sect->fragment = snewn(40, char);
-               sprintf(sect->fragment, "frag%p", sect);
-               sect->fragment = html_sanitise_fragment(&files, sect->file,
-                                                       sect->fragment);
+               sect->fragments[0] = snewn(40, char);
+               sprintf(sect->fragments[0], "p%d",
+                       sect->file->last_fragment_number++);
+               sect->fragments[0] = html_sanitise_fragment(&files, sect->file,
+                                                           sect->fragments[0]);
            }
        }
     }
 
     /*
+     * Reset the fragment numbers in each file. I've just used them
+     * to generate `p' fragment IDs for non-section paragraphs
+     * (numbered list elements, bibliocited), and now I want to use
+     * them for `i' fragment IDs for index entries.
+     */
+    {
+       htmlfile *file;
+       for (file = files.head; file; file = file->next)
+           file->last_fragment_number = 0;
+    }
+
+    /*
      * Now sort out the index. This involves:
      * 
      *         - For each index term, we set up an htmlindex structure to
@@ -610,9 +806,9 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
         * Run over the document inventing fragments. Each fragment
         * is of the form `i' followed by an integer.
         */
-       lastsect = NULL;
+       lastsect = sects.head;         /* this is always the top section */
        for (p = sourceform; p; p = p->next) {
-           if (is_heading_type(p->type))
+           if (is_heading_type(p->type) && p->type != para_Title)
                lastsect = (htmlsect *)p->private_data;
 
            for (w = p->words; w; w = w->next)
@@ -621,6 +817,7 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                    indextag *tag;
                    int i;
 
+                   hr->referenced = hr->generated = FALSE;
                    hr->section = lastsect;
                    {
                        char buf[40];
@@ -687,37 +884,57 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
 #define listname(lt) ( (lt)==UL ? "ul" : (lt)==OL ? "ol" : "dl" )
 #define itemname(lt) ( (lt)==LI ? "li" : (lt)==DT ? "dt" : "dd" )
 
-           ho.fp = fopen(f->filename, "w");
+           if (!strcmp(f->filename, "-"))
+               ho.fp = stdout;
+           else
+               ho.fp = fopen(f->filename, "w");
+           if (!ho.fp)
+               error(err_cantopenw, f->filename);
+
            ho.charset = conf.output_charset;
+           ho.restrict_charset = conf.restrict_charset;
            ho.cstate = charset_init_state;
            ho.ver = conf.htmlver;
            ho.state = HO_NEUTRAL;
            ho.contents_level = 0;
+           ho.hackflags = 0;          /* none of these thankyouverymuch */
+           ho.hacklimit = -1;
 
            /* <!DOCTYPE>. */
            switch (conf.htmlver) {
              case HTML_3_2:
-               fprintf(ho.fp, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD "
-                       "HTML 3.2 Final//EN\">\n");
+               if (ho.fp)
+                   fprintf(ho.fp, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD "
+                           "HTML 3.2 Final//EN\">\n");
                break;
              case HTML_4:
-               fprintf(ho.fp, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML"
-                       " 4.01//EN\"\n\"http://www.w3.org/TR/html4/"
-                       "strict.dtd\">\n");
+               if (ho.fp)
+                   fprintf(ho.fp, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML"
+                           " 4.01//EN\"\n\"http://www.w3.org/TR/html4/"
+                           "strict.dtd\">\n");
+               break;
+             case ISO_HTML:
+               if (ho.fp)
+                   fprintf(ho.fp, "<!DOCTYPE HTML PUBLIC \"ISO/IEC "
+                           "15445:2000//DTD HTML//EN\">\n");
                break;
              case XHTML_1_0_TRANSITIONAL:
-               /* FIXME: <?xml?> to specify character encoding.
-                * This breaks HTML backwards compat, so perhaps avoid, or
-                * perhaps only emit when not using the default UTF-8? */
-               fprintf(ho.fp, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML"
-                       " 1.0 Transitional//EN\"\n\"http://www.w3.org/TR/"
-                       "xhtml1/DTD/xhtml1-transitional.dtd\">\n");
+               if (ho.fp) {
+                   fprintf(ho.fp, "<?xml version=\"1.0\" encoding=\"%s\"?>\n",
+                           charset_to_mimeenc(conf.output_charset));
+                   fprintf(ho.fp, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML"
+                           " 1.0 Transitional//EN\"\n\"http://www.w3.org/TR/"
+                           "xhtml1/DTD/xhtml1-transitional.dtd\">\n");
+               }
                break;
              case XHTML_1_0_STRICT:
-               /* FIXME: <?xml?> to specify character encoding. */
-               fprintf(ho.fp, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML"
-                       " 1.0 Strict//EN\"\n\"http://www.w3.org/TR/xhtml1/"
-                       "DTD/xhtml1-strict.dtd\">\n");
+               if (ho.fp) {
+                   fprintf(ho.fp, "<?xml version=\"1.0\" encoding=\"%s\"?>\n",
+                           charset_to_mimeenc(conf.output_charset));
+                   fprintf(ho.fp, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML"
+                           " 1.0 Strict//EN\"\n\"http://www.w3.org/TR/xhtml1/"
+                           "DTD/xhtml1-strict.dtd\">\n");
+               }
                break;
            }
 
@@ -761,7 +978,7 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
 
                assert(f->last);
                if (f->last != f->first && f->last->title) {
-                   html_text(&ho, L" - ");   /* FIXME: configurable? */
+                   html_text(&ho, conf.title_separator);
                    html_words(&ho, f->last->title->words, NOTHING,
                               f, keywords, &conf);
                }
@@ -769,13 +986,75 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
            element_close(&ho, "title");
            html_nl(&ho);
 
+           if (conf.rellinks) {
+
+               if (prevf) {
+                   element_empty(&ho, "link");
+                   element_attr(&ho, "rel", "previous");
+                   element_attr(&ho, "href", prevf->filename);
+                   html_nl(&ho);
+               }
+
+               if (f != files.head) {
+                   element_empty(&ho, "link");
+                   element_attr(&ho, "rel", "ToC");
+                   element_attr(&ho, "href", files.head->filename);
+                   html_nl(&ho);
+               }
+
+               if (conf.leaf_level > 0) {
+                   htmlsect *p = f->first->parent;
+                   assert(p == f->last->parent);
+                   if (p) {
+                       element_empty(&ho, "link");
+                       element_attr(&ho, "rel", "up");
+                       element_attr(&ho, "href", p->file->filename);
+                       html_nl(&ho);
+                   }
+               }
+
+               if (has_index && files.index && f != files.index) {
+                   element_empty(&ho, "link");
+                   element_attr(&ho, "rel", "index");
+                   element_attr(&ho, "href", files.index->filename);
+                   html_nl(&ho);
+               }
+
+               if (f->next) {
+                   element_empty(&ho, "link");
+                   element_attr(&ho, "rel", "next");
+                   element_attr(&ho, "href", f->next->filename);
+                   html_nl(&ho);
+               }
+
+           }
+
            if (conf.head_end)
                html_raw(&ho, conf.head_end);
 
+           /*
+            * Add any <head> data defined in specific sections
+            * that go in this file. (This is mostly to allow <meta
+            * name="AppleTitle"> tags for Mac online help.)
+            */
+           for (s = sects.head; s; s = s->next) {
+               if (s->file == f && s->text) {
+                   for (p = s->text;
+                        p && (p == s->text || p->type == para_Title ||
+                              !is_heading_type(p->type));
+                        p = p->next) {
+                       if (p->type == para_Config) {
+                           if (!ustricmp(p->keyword, L"html-local-head")) {
+                               html_raw(&ho, adv(p->origkeyword));
+                           }
+                       }
+                   }
+               }
+           }
+
            element_close(&ho, "head");
            html_nl(&ho);
 
-           /* FIXME: need to be able to specify replacement for this */
            if (conf.body_tag)
                html_raw(&ho, conf.body_tag);
            else
@@ -789,7 +1068,7 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
             * Write out a nav bar. Special case: we don't do this
             * if there is only one file.
             */
-           if (files.head != files.tail) {
+           if (conf.navlinks && files.head != files.tail) {
                element_open(&ho, "p");
                if (conf.nav_attr)
                    html_raw_as_attr(&ho, conf.nav_attr);
@@ -798,37 +1077,54 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                    element_open(&ho, "a");
                    element_attr(&ho, "href", prevf->filename);
                }
-               html_text(&ho, L"Previous");/* FIXME: conf? */
+               html_text(&ho, conf.nav_prev_text);
                if (prevf)
                    element_close(&ho, "a");
 
-               html_text(&ho, L" | ");     /* FIXME: conf? */
+               html_text(&ho, conf.nav_separator);
 
                if (f != files.head) {
                    element_open(&ho, "a");
                    element_attr(&ho, "href", files.head->filename);
                }
-               html_text(&ho, L"Contents");/* FIXME: conf? */
+               html_text(&ho, conf.contents_text);
                if (f != files.head)
                    element_close(&ho, "a");
 
-               html_text(&ho, L" | ");     /* FIXME: conf? */
+               /* We don't bother with "Up" links for leaf-level 1,
+                * as they would be identical to the "Contents" links. */
+               if (conf.leaf_level >= 2) {
+                   htmlsect *p = f->first->parent;
+                   assert(p == f->last->parent);
+                   html_text(&ho, conf.nav_separator);
+                   if (p) {
+                       element_open(&ho, "a");
+                       element_attr(&ho, "href", p->file->filename);
+                   }
+                   html_text(&ho, conf.nav_up_text);
+                   if (p) {
+                       element_close(&ho, "a");
+                   }
+               }
 
-               if (f != files.index) {
-                   element_open(&ho, "a");
-                   element_attr(&ho, "href", files.index->filename);
+               if (has_index && files.index) {
+                   html_text(&ho, conf.nav_separator);
+                   if (f != files.index) {
+                       element_open(&ho, "a");
+                       element_attr(&ho, "href", files.index->filename);
+                   }
+                   html_text(&ho, conf.index_text);
+                   if (f != files.index)
+                       element_close(&ho, "a");
                }
-               html_text(&ho, L"Index");/* FIXME: conf? */
-               if (f != files.index)
-                   element_close(&ho, "a");
 
-               html_text(&ho, L" | ");     /* FIXME: conf? */
+               html_text(&ho, conf.nav_separator);
 
                if (f->next) {
                    element_open(&ho, "a");
                    element_attr(&ho, "href", f->next->filename);
                }
-               html_text(&ho, L"Next");    /* FIXME: conf? */
+               html_text(&ho, conf.nav_next_text);
                if (f->next)
                    element_close(&ho, "a");
 
@@ -838,7 +1134,7 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
            prevf = f;
 
            /*
-            * Write out a prefix TOC for the file.
+            * Write out a prefix TOC for the file (if a leaf file).
             * 
             * We start by going through the section list and
             * collecting the sections which need to be added to
@@ -921,6 +1217,12 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                     */
                    displaying = TRUE;
                } else {
+                   /*
+                    * Doesn't belong in this file, but it may be
+                    * a descendant of a section which does, in
+                    * which case we should consider it for the
+                    * main TOC of this file (for non-leaf files).
+                    */
                    htmlsect *a, *ac;
                    int depth, adepth;
 
@@ -980,19 +1282,18 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                    element_open(&ho, htag);
 
                    /*
-                    * Provide anchor for cross-links to target.
-                    * 
-                    * FIXME: AIcurrentlyUI, this needs to be done
-                    * differently in XHTML because <a name> is
-                    * deprecated or obsolete.
+                    * Provide anchor(s) for cross-links to target.
                     * 
                     * (Also we'll have to do this separately in
                     * other paragraph types - NumberedList and
                     * BiblioCited.)
                     */
-                   element_open(&ho, "a");
-                   element_attr(&ho, "name", s->fragment);
-                   element_close(&ho, "a");
+                   {
+                       int i;
+                       for (i=0; i < conf.ntfragments; i++)
+                           if (s->fragments[i])
+                               html_fragment(&ho, s->fragments[i]);
+                   }
 
                    html_section_title(&ho, s, f, keywords, &conf, TRUE);
 
@@ -1124,9 +1425,10 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                                element_open(&ho, "p");
                                if (p->private_data) {
                                    htmlsect *s = (htmlsect *)p->private_data;
-                                   element_open(&ho, "a");
-                                   element_attr(&ho, "name", s->fragment);
-                                   element_close(&ho, "a");
+                                   int i;
+                                   for (i=0; i < conf.ntfragments; i++)
+                                       if (s->fragments[i])
+                                           html_fragment(&ho, s->fragments[i]);
                                }
                                html_nl(&ho);
                                html_words(&ho, p->kwtext, ALL,
@@ -1142,9 +1444,10 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                                element_open(&ho, "li");
                                if (p->private_data) {
                                    htmlsect *s = (htmlsect *)p->private_data;
-                                   element_open(&ho, "a");
-                                   element_attr(&ho, "name", s->fragment);
-                                   element_close(&ho, "a");
+                                   int i;
+                                   for (i=0; i < conf.ntfragments; i++)
+                                       if (s->fragments[i])
+                                           html_fragment(&ho, s->fragments[i]);
                                }
                                html_nl(&ho);
                                stackhead->itemtype = LI;
@@ -1211,7 +1514,7 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                            html_words(&ho, entry->text, MARKUP|LINKS,
                                       f, keywords, &conf);
 
-                           html_text(&ho, L": ");/* FIXME: configurable */
+                           html_text(&ho, conf.index_main_sep);
 
                            for (j = 0; j < hi->nrefs; j++) {
                                htmlindexref *hr =
@@ -1219,18 +1522,28 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                                paragraph *p = hr->section->title;
 
                                if (j > 0)
-                                   html_text(&ho, L", "); /* FIXME: conf */
+                                   html_text(&ho, conf.index_multi_sep);
 
                                html_href(&ho, f, hr->section->file,
                                          hr->fragment);
+                               hr->referenced = TRUE;
                                if (p && p->kwtext)
                                    html_words(&ho, p->kwtext, MARKUP|LINKS,
                                               f, keywords, &conf);
                                else if (p && p->words)
                                    html_words(&ho, p->words, MARKUP|LINKS,
                                               f, keywords, &conf);
-                               else
-                                   html_text(&ho, L"FIXME");
+                               else {
+                                   /*
+                                    * If there is no title at all,
+                                    * this must be because our
+                                    * target section is the
+                                    * preamble section and there
+                                    * is no title. So we use the
+                                    * preamble_text.
+                                    */
+                                   html_text(&ho, conf.preamble_text);
+                               }
                                element_close(&ho, "a");
                            }
                        }
@@ -1248,39 +1561,55 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                 */
                int done_version_ids = FALSE;
 
-               element_empty(&ho, "hr");
+               if (conf.address_section)
+                   element_empty(&ho, "hr");
 
                if (conf.body_end)
                    html_raw(&ho, conf.body_end);
 
                if (conf.address_section) {
+                   int started = FALSE;
+                   if (conf.htmlver == ISO_HTML) {
+                       /*
+                        * The ISO-HTML validator complains if
+                        * there isn't a <div> tag surrounding the
+                        * <address> tag. I'm uncertain of why this
+                        * should be - there appears to be no
+                        * mention of this in the ISO-HTML spec,
+                        * suggesting that it doesn't represent a
+                        * change from HTML 4, but nonetheless the
+                        * HTML 4 validator doesn't seem to mind.
+                        */
+                       element_open(&ho, "div");
+                   }
                    element_open(&ho, "address");
                    if (conf.addr_start) {
                        html_raw(&ho, conf.addr_start);
                        html_nl(&ho);
+                       started = TRUE;
                    }
                    if (conf.visible_version_id) {
-                       int started = FALSE;
                        for (p = sourceform; p; p = p->next)
                            if (p->type == para_VersionID) {
-                               if (!started)
-                                   element_open(&ho, "p");
-                               else
+                               if (started)
                                    element_empty(&ho, "br");
                                html_nl(&ho);
-                               html_text(&ho, L"[");   /* FIXME: conf? */
+                               html_text(&ho, conf.pre_versionid);
                                html_words(&ho, p->words, NOTHING,
                                           f, keywords, &conf);
-                               html_text(&ho, L"]");   /* FIXME: conf? */
+                               html_text(&ho, conf.post_versionid);
                                started = TRUE;
                            }
-                       if (started)
-                           element_close(&ho, "p");
                        done_version_ids = TRUE;
                    }
-                   if (conf.addr_end)
+                   if (conf.addr_end) {
+                       if (started)
+                           element_empty(&ho, "br");
                        html_raw(&ho, conf.addr_end);
+                   }
                    element_close(&ho, "address");
+                   if (conf.htmlver == ISO_HTML)
+                       element_close(&ho, "div");
                }
 
                if (!done_version_ids) {
@@ -1314,8 +1643,406 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
     }
 
     /*
-     * FIXME: Free all the working data.
+     * Before we start outputting the HTML Help files, check
+     * whether there's even going to _be_ an index file: we omit it
+     * if the index contains nothing.
+     */
+    hhk_filename = conf.hhk_filename;
+    if (hhk_filename) {
+       int ok = FALSE;
+       int i;
+       indexentry *entry;
+
+       for (i = 0; (entry = index234(idx->entries, i)) != NULL; i++) {
+           htmlindex *hi = (htmlindex *)entry->backend_data;
+
+           if (hi->nrefs > 0) {
+               ok = TRUE;             /* found an index entry */
+               break;
+           }
+       }
+
+       if (!ok)
+           hhk_filename = NULL;
+    }
+
+    /*
+     * Output the MS HTML Help supporting files, if requested.
+     *
+     * A good unofficial reference for these is <http://chmspec.nongnu.org/>.
      */
+    if (conf.hhp_filename) {
+       htmlfile *f;
+       htmloutput ho;
+
+       ho.charset = CS_CP1252;        /* as far as I know, HHP files are */
+       ho.restrict_charset = CS_CP1252;   /* hardwired to this charset */
+       ho.cstate = charset_init_state;
+       ho.ver = HTML_4;               /* *shrug* */
+       ho.state = HO_NEUTRAL;
+       ho.contents_level = 0;
+       ho.hackflags = HO_HACK_QUOTENOTHING;
+
+       ho.fp = fopen(conf.hhp_filename, "w");
+       if (!ho.fp)
+           error(err_cantopenw, conf.hhp_filename);
+
+       fprintf(ho.fp,
+               "[OPTIONS]\n"
+               /* Binary TOC required for Next/Previous nav to work */
+               "Binary TOC=Yes\n"
+               "Compatibility=1.1 or later\n"
+               "Compiled file=%s\n"
+               "Default Window=main\n"
+               "Default topic=%s\n"
+               "Display compile progress=Yes\n"
+               "Full-text search=Yes\n"
+               "Title=", conf.chm_filename, files.head->filename);
+
+       ho.hacklimit = 255;
+       html_words(&ho, topsect->title->words, NOTHING,
+                  NULL, keywords, &conf);
+
+       fprintf(ho.fp, "\n");
+
+       /*
+        * These two entries don't seem to be remotely necessary
+        * for a successful run of the help _compiler_, but
+        * omitting them causes the GUI Help Workshop to behave
+        * rather strangely if you try to load the help project
+        * into that and edit it.
+        */
+       if (conf.hhc_filename)
+           fprintf(ho.fp, "Contents file=%s\n", conf.hhc_filename);
+       if (hhk_filename)
+           fprintf(ho.fp, "Index file=%s\n", hhk_filename);
+
+       fprintf(ho.fp, "\n[WINDOWS]\nmain=\"");
+
+       ho.hackflags |= HO_HACK_OMITQUOTES;
+       ho.hacklimit = 255;
+       html_words(&ho, topsect->title->words, NOTHING,
+                  NULL, keywords, &conf);
+
+       fprintf(ho.fp, "\",\"%s\",\"%s\",\"%s\",,,,,,"
+               /* This first magic number is fsWinProperties, controlling
+                * Navigation Pane options and the like.
+                * Constants HHWIN_PROP_* in htmlhelp.h. */
+               "0x62520,,"
+               /* This second number is fsToolBarFlags, mainly controlling
+                * toolbar buttons. Constants HHWIN_BUTTON_*.
+                * NOTE: there are two pairs of bits for Next/Previous
+                * buttons: 7/8 (which do nothing useful), and 21/22
+                * (which work). (Neither of these are exposed in the HHW
+                * UI, but they work fine in HH.) We use the latter. */
+               "0x60304e,,,,,,,,0\n",
+               conf.hhc_filename ? conf.hhc_filename : "",
+               hhk_filename ? hhk_filename : "",
+               files.head->filename);
+
+       /*
+        * The [FILES] section is also not necessary for
+        * compilation (hhc appears to build up a list of needed
+        * files just by following links from the given starting
+        * points), but useful for loading the project into HHW.
+        */
+       fprintf(ho.fp, "\n[FILES]\n");
+       for (f = files.head; f; f = f->next)
+           fprintf(ho.fp, "%s\n", f->filename);
+
+       fclose(ho.fp);
+    }
+    if (conf.hhc_filename) {
+       htmlfile *f;
+       htmlsect *s, *a;
+       htmloutput ho;
+       int currdepth = 0;
+
+       ho.fp = fopen(conf.hhc_filename, "w");
+       if (!ho.fp)
+           error(err_cantopenw, conf.hhc_filename);
+
+       ho.charset = CS_CP1252;        /* as far as I know, HHC files are */
+       ho.restrict_charset = CS_CP1252;   /* hardwired to this charset */
+       ho.cstate = charset_init_state;
+       ho.ver = HTML_4;               /* *shrug* */
+       ho.state = HO_NEUTRAL;
+       ho.contents_level = 0;
+       ho.hackflags = HO_HACK_QUOTEQUOTES;
+
+       /*
+        * Magic DOCTYPE which seems to work for .HHC files. I'm
+        * wary of trying to change it!
+        */
+       fprintf(ho.fp, "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">\n"
+               "<HTML><HEAD>\n"
+               "<META HTTP-EQUIV=\"Content-Type\" "
+               "CONTENT=\"text/html; charset=%s\">\n"
+               "</HEAD><BODY><UL>\n",
+               charset_to_mimeenc(conf.output_charset));
+
+       for (f = files.head; f; f = f->next) {
+           /*
+            * For each HTML file, write out a contents entry.
+            */
+           int depth, leaf = TRUE;
+
+           /*
+            * Determine the depth of this file in the contents
+            * tree.
+            * 
+            * If the file contains no sections, it is assumed to
+            * have depth zero.
+            */
+           depth = 0;
+           if (f->first)
+               for (a = f->first->parent; a && a->type != TOP; a = a->parent)
+                   depth++;
+
+           /*
+            * Determine if this file is a leaf file, by
+            * trawling the section list to see if there's any
+            * section with an ancestor in this file but which
+            * is not itself in this file.
+            *
+            * Special case: for contents purposes, the TOP
+            * file is not considered to be the parent of the
+            * chapter files, so it's always a leaf.
+            * 
+            * A file with no sections in it is also a leaf.
+            */
+           if (f->first && f->first->type != TOP) {
+               for (s = f->first; s; s = s->next) {
+                   htmlsect *a;
+
+                   if (leaf && s->file != f) {
+                       for (a = s; a; a = a->parent)
+                           if (a->file == f) {
+                               leaf = FALSE;
+                               break;
+                           }
+                   }
+               }
+           }
+
+           /*
+            * Now write out our contents entry.
+            */
+           while (currdepth < depth) {
+               fprintf(ho.fp, "<UL>\n");
+               currdepth++;
+           }
+           while (currdepth > depth) {
+               fprintf(ho.fp, "</UL>\n");
+               currdepth--;
+           }
+           /* fprintf(ho.fp, "<!-- depth=%d -->", depth); */
+           fprintf(ho.fp, "<LI><OBJECT TYPE=\"text/sitemap\">"
+                   "<PARAM NAME=\"Name\" VALUE=\"");
+           ho.hacklimit = 255;
+           if (f->first->title)
+               html_words(&ho, f->first->title->words, NOTHING,
+                          NULL, keywords, &conf);
+           else if (f->first->type == INDEX)
+               html_text(&ho, conf.index_text);
+           fprintf(ho.fp, "\"><PARAM NAME=\"Local\" VALUE=\"%s\">"
+                   "<PARAM NAME=\"ImageNumber\" VALUE=\"%d\"></OBJECT>\n",
+                   f->filename, leaf ? 11 : 1);
+       }
+
+       while (currdepth > 0) {
+           fprintf(ho.fp, "</UL>\n");
+           currdepth--;
+       }
+
+       fprintf(ho.fp, "</UL></BODY></HTML>\n");
+
+       cleanup(&ho);
+    }
+    if (hhk_filename) {
+       htmlfile *f;
+       htmloutput ho;
+       indexentry *entry;
+       int i;
+
+       /*
+        * First make a pass over all HTML files and set their
+        * `temp' fields to zero, because we're about to use them.
+        */
+       for (f = files.head; f; f = f->next)
+           f->temp = 0;
+
+       ho.fp = fopen(hhk_filename, "w");
+       if (!ho.fp)
+           error(err_cantopenw, hhk_filename);
+
+       ho.charset = CS_CP1252;        /* as far as I know, HHK files are */
+       ho.restrict_charset = CS_CP1252;   /* hardwired to this charset */
+       ho.cstate = charset_init_state;
+       ho.ver = HTML_4;               /* *shrug* */
+       ho.state = HO_NEUTRAL;
+       ho.contents_level = 0;
+       ho.hackflags = HO_HACK_QUOTEQUOTES;
+
+       /*
+        * Magic DOCTYPE which seems to work for .HHK files. I'm
+        * wary of trying to change it!
+        */
+       fprintf(ho.fp, "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">\n"
+               "<HTML><HEAD>\n"
+               "<META HTTP-EQUIV=\"Content-Type\" "
+               "CONTENT=\"text/html; charset=%s\">\n"
+               "</HEAD><BODY><UL>\n",
+               charset_to_mimeenc(conf.output_charset));
+
+       /*
+        * Go through the index terms and output each one.
+        */
+       for (i = 0; (entry = index234(idx->entries, i)) != NULL; i++) {
+           htmlindex *hi = (htmlindex *)entry->backend_data;
+           int j;
+
+           if (hi->nrefs > 0) {
+               fprintf(ho.fp, "<LI><OBJECT TYPE=\"text/sitemap\">\n"
+                       "<PARAM NAME=\"Name\" VALUE=\"");
+               ho.hacklimit = 255;
+               html_words(&ho, entry->text, NOTHING,
+                          NULL, keywords, &conf);
+               fprintf(ho.fp, "\">\n");
+
+               for (j = 0; j < hi->nrefs; j++) {
+                   htmlindexref *hr =
+                       (htmlindexref *)hi->refs[j]->private_data;
+
+                   /*
+                    * Use the temp field to ensure we don't
+                    * reference the same file more than once.
+                    */
+                   if (!hr->section->file->temp) {
+                       fprintf(ho.fp, "<PARAM NAME=\"Local\" VALUE=\"%s\">\n",
+                               hr->section->file->filename);
+                       hr->section->file->temp = 1;
+                   }
+
+                   hr->referenced = TRUE;
+               }
+
+               fprintf(ho.fp, "</OBJECT>\n");
+
+               /*
+                * Now go through those files and re-clear the temp
+                * fields ready for the _next_ index term.
+                */
+               for (j = 0; j < hi->nrefs; j++) {
+                   htmlindexref *hr =
+                       (htmlindexref *)hi->refs[j]->private_data;
+                   hr->section->file->temp = 0;
+               }
+           }
+       }
+
+       fprintf(ho.fp, "</UL></BODY></HTML>\n");
+       cleanup(&ho);
+    }
+
+    /*
+     * Go through and check that no index fragments were referenced
+     * without being generated, or indeed vice versa.
+     * 
+     * (When I actually get round to freeing everything, this can
+     * probably be the freeing loop as well.)
+     */
+    for (p = sourceform; p; p = p->next) {
+       word *w;
+       for (w = p->words; w; w = w->next)
+           if (w->type == word_IndexRef) {
+               htmlindexref *hr = (htmlindexref *)w->private_data;
+
+               assert(!hr->referenced == !hr->generated);
+           }
+    }
+
+    /*
+     * Free all the working data.
+     */
+    {
+       htmlfragment *frag;
+       while ( (frag = (htmlfragment *)delpos234(files.frags, 0)) != NULL ) {
+           /*
+            * frag->fragment is dynamically allocated, but will be
+            * freed when we process the htmlsect structure which
+            * it is attached to.
+            */
+           sfree(frag);
+       }
+       freetree234(files.frags);
+    }
+    /*
+     * The strings in files.files are all owned by their containing
+     * htmlfile structures, so there's no need to free them here.
+     */
+    freetree234(files.files);
+    {
+       htmlsect *sect, *tmp;
+       sect = sects.head;
+       while (sect) {
+           int i;
+           tmp = sect->next;
+           for (i=0; i < conf.ntfragments; i++)
+               sfree(sect->fragments[i]);
+           sfree(sect->fragments);
+           sfree(sect);
+           sect = tmp;
+       }
+       sect = nonsects.head;
+       while (sect) {
+           int i;
+           tmp = sect->next;
+           for (i=0; i < conf.ntfragments; i++)
+               sfree(sect->fragments[i]);
+           sfree(sect->fragments);
+           sfree(sect);
+           sect = tmp;
+       }
+    }
+    {
+       htmlfile *file, *tmp;
+       file = files.head;
+       while (file) {
+           tmp = file->next;
+           sfree(file->filename);
+           sfree(file);
+           file = tmp;
+       }
+    }
+    {
+       int i;
+       indexentry *entry;
+       for (i = 0; (entry = index234(idx->entries, i)) != NULL; i++) {
+           htmlindex *hi = (htmlindex *)entry->backend_data;
+           sfree(hi);
+       }
+    }
+    {
+       paragraph *p;
+       word *w;
+       for (p = sourceform; p; p = p->next)
+           for (w = p->words; w; w = w->next)
+               if (w->type == word_IndexRef) {
+                   htmlindexref *hr = (htmlindexref *)w->private_data;
+                   assert(hr != NULL);
+                   sfree(hr->fragment);
+                   sfree(hr);
+               }
+    }
+    sfree(conf.asect);
+    sfree(conf.single_filename);
+    sfree(conf.contents_filename);
+    sfree(conf.index_filename);
+    sfree(conf.template_filename);
+    while (conf.ntfragments--)
+       sfree(conf.template_fragments[conf.ntfragments]);
+    sfree(conf.template_fragments);
 }
 
 static void html_file_section(htmlconfig *cfg, htmlfilelist *files,
@@ -1348,8 +2075,12 @@ static void html_file_section(htmlconfig *cfg, htmlfilelist *files,
         * we invent a fresh file and put this section at its head.
         * Otherwise, we put it in the same file as its parent
         * section.
+        * 
+        * Another special value of cfg->leaf_level is -1, which
+        * means infinity (i.e. it's considered to always be
+        * greater than depth).
         */
-       if (ldepth > cfg->leaf_level) {
+       if (cfg->leaf_level > 0 && ldepth > cfg->leaf_level) {
            /*
             * We know that sect->parent cannot be NULL. The only
             * circumstance in which it can be is if sect is at
@@ -1405,7 +2136,8 @@ static htmlfile *html_new_file(htmlfilelist *list, char *filename)
        list->head = ret;
     list->tail = ret;
 
-    ret->filename = dupstr(filename);
+    ret->filename = html_sanitise_filename(list, dupstr(filename));
+    add234(list->files, ret->filename);
     ret->last_fragment_number = 0;
     ret->min_heading_depth = INT_MAX;
     ret->first = ret->last = NULL;
@@ -1413,7 +2145,8 @@ static htmlfile *html_new_file(htmlfilelist *list, char *filename)
     return ret;
 }
 
-static htmlsect *html_new_sect(htmlsectlist *list, paragraph *title)
+static htmlsect *html_new_sect(htmlsectlist *list, paragraph *title,
+                              htmlconfig *cfg)
 {
     htmlsect *ret = snew(htmlsect);
 
@@ -1429,6 +2162,13 @@ static htmlsect *html_new_sect(htmlsectlist *list, paragraph *title)
     ret->parent = NULL;
     ret->type = NORMAL;
 
+    ret->fragments = snewn(cfg->ntfragments, char *);
+    {
+       int i;
+       for (i=0; i < cfg->ntfragments; i++)
+           ret->fragments[i] = NULL;
+    }
+
     return ret;
 }
 
@@ -1436,7 +2176,7 @@ static void html_words(htmloutput *ho, word *words, int flags,
                       htmlfile *file, keywordlist *keywords, htmlconfig *cfg)
 {
     word *w;
-    char *c;
+    char *c, *c2, *p, *q;
     int style, type;
 
     for (w = words; w; w = w->next) switch (w->type) {
@@ -1444,7 +2184,20 @@ static void html_words(htmloutput *ho, word *words, int flags,
        if (flags & LINKS) {
            element_open(ho, "a");
            c = utoa_dup(w->text, CS_ASCII);
-           element_attr(ho, "href", c);
+           c2 = snewn(1 + 10*strlen(c), char);
+           for (p = c, q = c2; *p; p++) {
+               if (*p == '&')
+                   q += sprintf(q, "&amp;");
+               else if (*p == '<')
+                   q += sprintf(q, "&lt;");
+               else if (*p == '>')
+                   q += sprintf(q, "&gt;");
+               else
+                   *q++ = *p;
+           }
+           *q = '\0';
+           element_attr(ho, "href", c2);
+           sfree(c2);
            sfree(c);
        }
        break;
@@ -1452,12 +2205,16 @@ static void html_words(htmloutput *ho, word *words, int flags,
       case word_LowerXref:
        if (flags & LINKS) {
            keyword *kwl = kw_lookup(keywords, w->text);
-           paragraph *p = kwl->para;
-           htmlsect *s = (htmlsect *)p->private_data;
+           paragraph *p;
+           htmlsect *s;
+
+           assert(kwl);
+           p = kwl->para;
+           s = (htmlsect *)p->private_data;
 
            assert(s);
 
-           html_href(ho, file, s->file, s->fragment);
+           html_href(ho, file, s->file, s->fragments[0]);
        }
        break;
       case word_HyperEnd:
@@ -1468,9 +2225,8 @@ static void html_words(htmloutput *ho, word *words, int flags,
       case word_IndexRef:
        if (flags & INDEXENTS) {
            htmlindexref *hr = (htmlindexref *)w->private_data;
-           element_open(ho, "a");
-           element_attr(ho, "name", hr->fragment);
-           element_close(ho, "a");
+           html_fragment(ho, hr->fragment);
+           hr->generated = TRUE;
        }
        break;
       case word_Normal:
@@ -1506,8 +2262,8 @@ static void html_words(htmloutput *ho, word *words, int flags,
            else
                html_text(ho, cfg->rquote);
        } else {
-           if (cvt_ok(ho->charset, w->text) || !w->alt)
-               html_text(ho, w->text);
+           if (!w->alt || cvt_ok(ho->restrict_charset, w->text))
+               html_text_nbsp(ho, w->text);
            else
                html_words(ho, w->alt, flags, file, keywords, cfg);
        }
@@ -1578,100 +2334,134 @@ static void html_charset_cleanup(htmloutput *ho)
 
     bytes = charset_from_unicode(NULL, NULL, outbuf, lenof(outbuf),
                                 ho->charset, &ho->cstate, NULL);
-    if (bytes > 0)
+    if (ho->fp && bytes > 0)
        fwrite(outbuf, 1, bytes, ho->fp);
 }
 
+static void return_mostly_to_neutral(htmloutput *ho)
+{
+    if (ho->fp) {
+       if (ho->state == HO_IN_EMPTY_TAG && is_xhtml(ho->ver)) {
+           fprintf(ho->fp, " />");
+       } else if (ho->state == HO_IN_EMPTY_TAG || ho->state == HO_IN_TAG) {
+           fprintf(ho->fp, ">");
+       }
+    }
+
+    ho->state = HO_NEUTRAL;
+}
+
 static void return_to_neutral(htmloutput *ho)
 {
     if (ho->state == HO_IN_TEXT) {
        html_charset_cleanup(ho);
-    } else if (ho->state == HO_IN_EMPTY_TAG && is_xhtml(ho->ver)) {
-       fprintf(ho->fp, " />");
-    } else if (ho->state == HO_IN_EMPTY_TAG || ho->state == HO_IN_TAG) {
-       fprintf(ho->fp, ">");
     }
 
-    ho->state = HO_NEUTRAL;
+    return_mostly_to_neutral(ho);
 }
 
 static void element_open(htmloutput *ho, char const *name)
 {
     return_to_neutral(ho);
-    fprintf(ho->fp, "<%s", name);
+    if (ho->fp)
+       fprintf(ho->fp, "<%s", name);
     ho->state = HO_IN_TAG;
 }
 
 static void element_close(htmloutput *ho, char const *name)
 {
     return_to_neutral(ho);
-    fprintf(ho->fp, "</%s>", name);
+    if (ho->fp)
+       fprintf(ho->fp, "</%s>", name);
     ho->state = HO_NEUTRAL;
 }
 
 static void element_empty(htmloutput *ho, char const *name)
 {
     return_to_neutral(ho);
-    fprintf(ho->fp, "<%s", name);
+    if (ho->fp)
+       fprintf(ho->fp, "<%s", name);
     ho->state = HO_IN_EMPTY_TAG;
 }
 
 static void html_nl(htmloutput *ho)
 {
     return_to_neutral(ho);
-    fputc('\n', ho->fp);
+    if (ho->fp)
+       fputc('\n', ho->fp);
 }
 
 static void html_raw(htmloutput *ho, char *text)
 {
     return_to_neutral(ho);
-    fputs(text, ho->fp);
+    if (ho->fp)
+       fputs(text, ho->fp);
 }
 
 static void html_raw_as_attr(htmloutput *ho, char *text)
 {
     assert(ho->state == HO_IN_TAG || ho->state == HO_IN_EMPTY_TAG);
-    fputc(' ', ho->fp);
-    fputs(text, ho->fp);
+    if (ho->fp) {
+       fputc(' ', ho->fp);
+       fputs(text, ho->fp);
+    }
 }
 
 static void element_attr(htmloutput *ho, char const *name, char const *value)
 {
     html_charset_cleanup(ho);
     assert(ho->state == HO_IN_TAG || ho->state == HO_IN_EMPTY_TAG);
-    fprintf(ho->fp, " %s=\"%s\"", name, value);
+    if (ho->fp)
+       fprintf(ho->fp, " %s=\"%s\"", name, value);
 }
 
 static void element_attr_w(htmloutput *ho, char const *name,
                           wchar_t const *value)
 {
     html_charset_cleanup(ho);
-    fprintf(ho->fp, " %s=\"", name);
-    html_text_limit_internal(ho, value, 0, TRUE);
+    if (ho->fp)
+       fprintf(ho->fp, " %s=\"", name);
+    html_text_limit_internal(ho, value, 0, TRUE, FALSE);
     html_charset_cleanup(ho);
-    fputc('"', ho->fp);
+    if (ho->fp)
+       fputc('"', ho->fp);
 }
 
 static void html_text(htmloutput *ho, wchar_t const *text)
 {
-    html_text_limit(ho, text, 0);
+    return_mostly_to_neutral(ho);
+    html_text_limit_internal(ho, text, 0, FALSE, FALSE);
+}
+
+static void html_text_nbsp(htmloutput *ho, wchar_t const *text)
+{
+    return_mostly_to_neutral(ho);
+    html_text_limit_internal(ho, text, 0, FALSE, TRUE);
 }
 
 static void html_text_limit(htmloutput *ho, wchar_t const *text, int maxlen)
 {
-    return_to_neutral(ho);
-    html_text_limit_internal(ho, text, maxlen, FALSE);
+    return_mostly_to_neutral(ho);
+    html_text_limit_internal(ho, text, maxlen, FALSE, FALSE);
 }
 
 static void html_text_limit_internal(htmloutput *ho, wchar_t const *text,
-                                    int maxlen, int quote_quotes)
+                                    int maxlen, int quote_quotes, int nbsp)
 {
     int textlen = ustrlen(text);
     char outbuf[256];
     int bytes, err;
 
+    if (ho->hackflags & (HO_HACK_QUOTEQUOTES | HO_HACK_OMITQUOTES))
+       quote_quotes = TRUE;           /* override the input value */
+
     if (maxlen > 0 && textlen > maxlen)
        textlen = maxlen;
+    if (ho->hacklimit >= 0) {
+       if (textlen > ho->hacklimit)
+           textlen = ho->hacklimit;
+       ho->hacklimit -= textlen;
+    }
 
     while (textlen > 0) {
        /* Scan ahead for characters we really can't display in HTML. */
@@ -1680,13 +2470,14 @@ static void html_text_limit_internal(htmloutput *ho, wchar_t const *text,
            if (text[lenbefore] == L'<' ||
                text[lenbefore] == L'>' ||
                text[lenbefore] == L'&' ||
-               (text[lenbefore] == L'"' && quote_quotes))
+               (text[lenbefore] == L'"' && quote_quotes) ||
+               (text[lenbefore] == L' ' && nbsp))
                break;
        lenafter = lenbefore;
        bytes = charset_from_unicode(&text, &lenafter, outbuf, lenof(outbuf),
                                     ho->charset, &ho->cstate, &err);
        textlen -= (lenbefore - lenafter);
-       if (bytes > 0)
+       if (bytes > 0 && ho->fp)
            fwrite(outbuf, 1, bytes, ho->fp);
        if (err) {
            /*
@@ -1695,23 +2486,35 @@ static void html_text_limit_internal(htmloutput *ho, wchar_t const *text,
             * we use an HTML numeric entity reference.
             */
            assert(textlen > 0);
-           fprintf(ho->fp, "&#%ld;", (long int)*text);
+           if (ho->fp)
+               fprintf(ho->fp, "&#%ld;", (long int)*text);
            text++, textlen--;
        } else if (lenafter == 0 && textlen > 0) {
            /*
             * We have encountered a character which is special to
             * HTML.
             */
-           if (*text == L'<')
-               fprintf(ho->fp, "&lt;");
-           else if (*text == L'>')
-               fprintf(ho->fp, "&gt;");
-           else if (*text == L'&')
-               fprintf(ho->fp, "&amp;");
-           else if (*text == L'"')
-               fprintf(ho->fp, "&quot;");
-           else
-               assert(!"Can't happen");
+           if (ho->fp) {
+               if (*text == L'"' && (ho->hackflags & HO_HACK_OMITQUOTES)) {
+                   fputc('\'', ho->fp);
+               } else if (ho->hackflags & HO_HACK_QUOTENOTHING) {
+                   fputc(*text, ho->fp);
+               } else {
+                   if (*text == L'<')
+                       fprintf(ho->fp, "&lt;");
+                   else if (*text == L'>')
+                       fprintf(ho->fp, "&gt;");
+                   else if (*text == L'&')
+                       fprintf(ho->fp, "&amp;");
+                   else if (*text == L'"')
+                       fprintf(ho->fp, "&quot;");
+                   else if (*text == L' ') {
+                       assert(nbsp);
+                       fprintf(ho->fp, "&nbsp;");
+                   } else
+                       assert(!"Can't happen");
+               }
+           }
            text++, textlen--;
        }
     }
@@ -1720,7 +2523,8 @@ static void html_text_limit_internal(htmloutput *ho, wchar_t const *text,
 static void cleanup(htmloutput *ho)
 {
     return_to_neutral(ho);
-    fclose(ho->fp);
+    if (ho->fp && ho->fp != stdout)
+       fclose(ho->fp);
 }
 
 static void html_href(htmloutput *ho, htmlfile *thisfile,
@@ -1742,6 +2546,15 @@ static void html_href(htmloutput *ho, htmlfile *thisfile,
     sfree(url);
 }
 
+static void html_fragment(htmloutput *ho, char const *fragment)
+{
+    element_open(ho, "a");
+    element_attr(ho, "name", fragment);
+    if (is_xhtml(ho->ver))
+       element_attr(ho, "id", fragment);
+    element_close(ho, "a");
+}
+
 static char *html_format(paragraph *p, char *template_string)
 {
     char *c, *t;
@@ -1785,6 +2598,7 @@ static char *html_format(paragraph *p, char *template_string)
            } else if (p->keyword && *p->keyword && fmt == 'k')
                ws = p->keyword;
            else
+               /* %N comes here; also failure cases of other fmts */
                w = p->words;
 
            if (ws) {
@@ -1838,6 +2652,13 @@ static char *html_sanitise_fragment(htmlfilelist *files, htmlfile *file,
        *q = '\0';
     }
 
+    /* If there's nothing left, make something valid up */
+    if (!*text) {
+       static const char anonfrag[] = "anon";
+       text = sresize(text, lenof(anonfrag), char);
+       strcpy(text, anonfrag);
+    }
+
     /*
      * Now we check for clashes with other fragment names, and
      * adjust this one if necessary by appending a hyphen followed
@@ -1864,17 +2685,91 @@ static char *html_sanitise_fragment(htmlfilelist *files, htmlfile *file,
     return text;
 }
 
+static char *html_sanitise_filename(htmlfilelist *files, char *text)
+{
+    /*
+     * Unceremoniously rip out any character that might cause
+     * difficulty in some filesystem or another, or be otherwise
+     * inconvenient.
+     * 
+     * That doesn't leave much punctuation. I permit alphanumerics
+     * and +-.=_ only.
+     */
+    char *p = text, *q = text;
+
+    while (*p) {
+       if ((*p>='A' && *p<='Z') ||
+           (*p>='a' && *p<='z') ||
+           (*p>='0' && *p<='9') ||
+           *p=='-' || *p=='_' || *p=='+' || *p=='.' || *p=='=')
+           *q++ = *p;
+       p++;
+    }
+    *q = '\0';
+
+    /* If there's nothing left, make something valid up */
+    if (!*text) {
+       static const char anonfrag[] = "anon.html";
+       text = sresize(text, lenof(anonfrag), char);
+       strcpy(text, anonfrag);
+    }
+
+    /*
+     * Now we check for clashes with other filenames, and adjust
+     * this one if necessary by appending a hyphen followed by a
+     * number just before the file extension (if any).
+     */
+    {
+       int len, extpos;
+       int suffix = 1;
+
+       p = NULL;
+
+       while (find234(files->files, text, NULL)) {
+           if (!p) {
+               len = strlen(text);
+               p = text;
+               text = snewn(len+20, char);
+
+               for (extpos = len; extpos > 0 && p[extpos-1] != '.'; extpos--);
+               if (extpos > 0)
+                   extpos--;
+               else
+                   extpos = len;
+           }
+
+           sprintf(text, "%.*s-%d%s", extpos, p, ++suffix, p+extpos);
+       }
+
+       if (p)
+           sfree(p);
+    }
+
+    return text;
+}
+
 static void html_contents_entry(htmloutput *ho, int depth, htmlsect *s,
                                htmlfile *thisfile, keywordlist *keywords,
                                htmlconfig *cfg)
 {
+    if (ho->contents_level >= depth && ho->contents_level > 0) {
+       element_close(ho, "li");
+       html_nl(ho);
+    }
+
     while (ho->contents_level > depth) {
        element_close(ho, "ul");
        ho->contents_level--;
+       if (ho->contents_level > 0) {
+           element_close(ho, "li");
+       }
+       html_nl(ho);
     }
 
     while (ho->contents_level < depth) {
+       html_nl(ho);
        element_open(ho, "ul");
+       html_nl(ho);
        ho->contents_level++;
     }
 
@@ -1882,10 +2777,10 @@ static void html_contents_entry(htmloutput *ho, int depth, htmlsect *s,
        return;
 
     element_open(ho, "li");
-    html_href(ho, thisfile, s->file, s->fragment);
+    html_href(ho, thisfile, s->file, s->fragments[0]);
     html_section_title(ho, s, thisfile, keywords, cfg, FALSE);
     element_close(ho, "a");
-    element_close(ho, "li");
+    /* <li> will be closed by a later invocation */
 }
 
 static void html_section_title(htmloutput *ho, htmlsect *s, htmlfile *thisfile,
@@ -1906,7 +2801,7 @@ static void html_section_title(htmloutput *ho, htmlsect *s, htmlfile *thisfile,
        else
            sl = &cfg->asect[cfg->nasect-1];
 
-       if (!sl)
+       if (!sl || !sl->number_at_all)
            number = NULL;
        else if (sl->just_numbers)
            number = s->title->kwtext2;
@@ -1923,9 +2818,16 @@ static void html_section_title(htmloutput *ho, htmlsect *s, htmlfile *thisfile,
                   thisfile, keywords, cfg);
     } else {
        assert(s->type != NORMAL);
-       if (s->type == TOP)
-           html_text(ho, L"Preamble");/* FIXME: configure */
+       /*
+        * If we're printing the full document title for _real_ and
+        * there isn't one, we don't want to print `Preamble' at
+        * the top of what ought to just be some text. If we need
+        * it in any other context such as TOCs, we need to print
+        * `Preamble'.
+        */
+       if (s->type == TOP && !real)
+           html_text(ho, cfg->preamble_text);
        else if (s->type == INDEX)
-           html_text(ho, L"Index");/* FIXME: configure */
+           html_text(ho, cfg->index_text);
     }
 }