Escape &<> when they appear in href text.
[sgt/halibut] / bk_html.c
index 41a9f76..e3202a5 100644 (file)
--- a/bk_html.c
+++ b/bk_html.c
  *    sensible. Perhaps for the topmost section in the file, no
  *    fragment should be used? (Though it should probably still be
  *    _there_ even if unused.)
+ * 
+ *  - 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>
@@ -30,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;
 
@@ -41,17 +48,20 @@ 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 *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_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;
@@ -77,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 {
@@ -92,6 +109,7 @@ typedef struct {
     htmlfile *head, *tail;
     htmlfile *single, *index;
     tree234 *frags;
+    tree234 *files;
 } htmlfilelist;
 
 typedef struct {
@@ -127,6 +145,8 @@ typedef struct {
     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
@@ -135,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;
@@ -147,6 +182,14 @@ 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);
 
@@ -187,6 +230,7 @@ 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,
@@ -204,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;
@@ -215,10 +261,14 @@ 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.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");
@@ -234,6 +284,7 @@ static htmlconfig html_configure(paragraph *source) {
     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", ";
@@ -259,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);
            }
        }
     }
@@ -329,10 +384,22 @@ static htmlconfig html_configure(paragraph *source) {
                    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;
@@ -348,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;
@@ -432,6 +514,8 @@ static htmlconfig html_configure(paragraph *source) {
                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")) {
@@ -442,11 +526,42 @@ static htmlconfig html_configure(paragraph *source) {
                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)) &&
@@ -481,9 +596,11 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                  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);
@@ -500,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
@@ -514,7 +632,7 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
      * for each section.
      */
     {
-       htmlsect *topsect, *sect;
+       htmlsect *sect;
        int d;
 
        topsect = html_new_sect(&sects, NULL, &conf);
@@ -559,9 +677,13 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                }
            }
 
-       /* And the index, if we have one. */
+       /*
+        * 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) {
+       if (has_index && !conf.hhk_filename) {
            sect = html_new_sect(&sects, NULL, &conf);
            sect->text = NULL;
            sect->type = INDEX;
@@ -762,42 +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:
-               fprintf(ho.fp, "<!DOCTYPE HTML PUBLIC \"ISO/IEC "
-                       "15445:2000//DTD HTML//EN\">\n");
+               if (ho.fp)
+                   fprintf(ho.fp, "<!DOCTYPE HTML PUBLIC \"ISO/IEC "
+                           "15445:2000//DTD HTML//EN\">\n");
                break;
              case XHTML_1_0_TRANSITIONAL:
-               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");
+               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:
-               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");
+               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;
            }
 
@@ -849,6 +986,49 @@ 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);
 
@@ -888,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);
@@ -911,7 +1091,23 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
                if (f != files.head)
                    element_close(&ho, "a");
 
-               if (has_index) {
+               /* 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 (has_index && files.index) {
                    html_text(&ho, conf.nav_separator);
                    if (f != files.index) {
                        element_open(&ho, "a");
@@ -1365,7 +1561,8 @@ 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);
@@ -1446,6 +1643,309 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
     }
 
     /*
+     * 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.
      * 
@@ -1477,6 +1977,11 @@ void html_backend(paragraph *sourceform, keywordlist *keywords,
        }
        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;
@@ -1570,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
@@ -1627,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;
@@ -1666,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) {
@@ -1674,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;
@@ -1811,16 +2334,18 @@ 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->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, ">");
+    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;
@@ -1838,58 +2363,68 @@ static void return_to_neutral(htmloutput *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);
+    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)
@@ -1917,8 +2452,16 @@ static void html_text_limit_internal(htmloutput *ho, wchar_t const *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. */
@@ -1934,7 +2477,7 @@ static void html_text_limit_internal(htmloutput *ho, wchar_t const *text,
        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) {
            /*
@@ -1943,26 +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 if (*text == L' ') {
-               assert(nbsp);
-               fprintf(ho->fp, "&nbsp;");
-           } 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--;
        }
     }
@@ -1971,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,
@@ -2132,6 +2685,69 @@ 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)
@@ -2185,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;