Add an error check for correct formatting in Deflate uncompressed
[sgt/halibut] / bk_whlp.c
index 91b07c1..d15d029 100644 (file)
--- a/bk_whlp.c
+++ b/bk_whlp.c
@@ -1,12 +1,10 @@
 /*
  * Windows Help backend for Halibut
- * 
- * TODO:
- *  - allow user to specify section contexts.
  */
 
 #include <stdio.h>
 #include <stdlib.h>
+#include <ctype.h>
 #include <assert.h>
 
 #include "halibut.h"
@@ -17,10 +15,19 @@ struct bk_whlp_state {
     indexdata *idx;
     keywordlist *keywords;
     WHLP_TOPIC curr_topic;
+    int charset;
+    charset_state cstate;
     FILE *cntfp;
     int cnt_last_level, cnt_workaround;
 };
 
+typedef struct {
+    int charset;
+    wchar_t *bullet, *lquote, *rquote, *titlepage, *sectsuffix, *listsuffix;
+    wchar_t *contents_text;
+    char *filename;
+} whlpconf;
+
 /*
  * Indexes of fonts in our standard font descriptor set.
  */
@@ -36,31 +43,124 @@ enum {
     FONT_RULE
 };
 
-static void whlp_rdaddwc(rdstringc *rs, word *text);
-static int whlp_convert(wchar_t *s, int maxlen,
-                       char **result, int hard_spaces);
+static void whlp_rdaddwc(rdstringc *rs, word *text, whlpconf *conf,
+                        charset_state *state);
+static void whlp_rdadds(rdstringc *rs, const wchar_t *text, whlpconf *conf,
+                       charset_state *state);
 static void whlp_mkparagraph(struct bk_whlp_state *state,
-                            int font, word *text, int subsidiary);
-static void whlp_navmenu(struct bk_whlp_state *state, paragraph *p);
+                            int font, word *text, int subsidiary,
+                            whlpconf *conf);
+static void whlp_navmenu(struct bk_whlp_state *state, paragraph *p,
+                        whlpconf *conf);
 static void whlp_contents_write(struct bk_whlp_state *state,
                                int level, char *text, WHLP_TOPIC topic);
+static void whlp_wtext(struct bk_whlp_state *state, const wchar_t *text);
     
+paragraph *whlp_config_filename(char *filename)
+{
+    return cmdline_cfg_simple("winhelp-filename", filename, NULL);
+}
+
+static whlpconf whlp_configure(paragraph *source) {
+    paragraph *p;
+    whlpconf ret;
+
+    /*
+     * Defaults.
+     */
+    ret.charset = CS_CP1252;
+    ret.bullet = L"\x2022\0-\0\0";
+    ret.lquote = L"\x2018\0\x2019\0\"\0\"\0\0";
+    ret.rquote = uadv(ret.lquote);
+    ret.filename = dupstr("output.hlp");
+    ret.titlepage = L"Title page";
+    ret.contents_text = L"Contents";
+    ret.sectsuffix = L": ";
+    ret.listsuffix = L".";
+
+    /*
+     * Two-pass configuration so that we can pick up global config
+     * (e.g. `quotes') before having it overridden by specific
+     * config (`win-quotes'), irrespective of the order in which
+     * they occur.
+     */
+    for (p = source; p; p = p->next) {
+       if (p->type == para_Config) {
+           if (!ustricmp(p->keyword, L"quotes")) {
+               if (*uadv(p->keyword) && *uadv(uadv(p->keyword))) {
+                   ret.lquote = uadv(p->keyword);
+                   ret.rquote = uadv(ret.lquote);
+               }
+           }
+       }
+    }
+
+    for (p = source; p; p = p->next) {
+       p->private_data = NULL;
+       if (p->type == para_Config) {
+           /*
+            * In principle we should support a `winhelp-charset'
+            * here. We don't, because my WinHelp output code
+            * doesn't know how to change character set. Once I
+            * find out, I'll support it.
+            */
+           if (p->parent && !ustricmp(p->keyword, L"winhelp-topic")) {
+               /* Store the topic name in the private_data field of the
+                * containing section. */
+               p->parent->private_data = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"winhelp-filename")) {
+               sfree(ret.filename);
+               ret.filename = dupstr(adv(p->origkeyword));
+           } else if (!ustricmp(p->keyword, L"winhelp-bullet")) {
+               ret.bullet = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"winhelp-section-suffix")) {
+               ret.sectsuffix = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"winhelp-list-suffix")) {
+               ret.listsuffix = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"winhelp-contents-titlepage")) {
+               ret.titlepage = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"winhelp-quotes")) {
+               if (*uadv(p->keyword) && *uadv(uadv(p->keyword))) {
+                   ret.lquote = uadv(p->keyword);
+                   ret.rquote = uadv(ret.lquote);
+               }
+           } else if (!ustricmp(p->keyword, L"contents")) {
+               ret.contents_text = uadv(p->keyword);
+           }
+       }
+    }
+
+    /*
+     * Now process fallbacks on quote characters and bullets.
+     */
+    while (*uadv(ret.rquote) && *uadv(uadv(ret.rquote)) &&
+          (!cvt_ok(ret.charset, ret.lquote) ||
+           !cvt_ok(ret.charset, ret.rquote))) {
+       ret.lquote = uadv(ret.rquote);
+       ret.rquote = uadv(ret.lquote);
+    }
+
+    while (*ret.bullet && *uadv(ret.bullet) &&
+          !cvt_ok(ret.charset, ret.bullet))
+       ret.bullet = uadv(ret.bullet);
+
+    return ret;
+}
+
 void whlp_backend(paragraph *sourceform, keywordlist *keywords,
-                 indexdata *idx) {
+                 indexdata *idx, void *unused) {
     WHLP h;
-    char *filename, *cntname;
+    char *cntname;
     paragraph *p, *lastsect;
     struct bk_whlp_state state;
     WHLP_TOPIC contents_topic;
     int i;
     int nesting;
     indexentry *ie;
+    int done_contents_topic = FALSE;
+    whlpconf conf;
 
-    filename = "output.hlp";          /* FIXME: configurability */
-    cntname = "output.cnt";           /* corresponding contents file */
-
-    state.cntfp = fopen(cntname, "wb");
-    state.cnt_last_level = -1; state.cnt_workaround = 0;
+    IGNORE(unused);
 
     h = state.h = whlp_new();
     state.keywords = keywords;
@@ -89,22 +189,38 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
     whlp_create_font(h, "Courier New", WHLP_FONTFAM_SANS, 18,
                     WHLP_FONT_STRIKEOUT, 0, 0, 0);
 
+    conf = whlp_configure(sourceform);
+
+    state.charset = conf.charset;
+
     /*
-     * Loop over the source form finding out whether the user has
-     * specified particular help topic names for anything.
+     * Ensure the output file name has a .hlp extension. This is
+     * required since we must create the .cnt file in parallel with
+     * it.
      */
-    for (p = sourceform; p; p = p->next) {
-       p->private_data = NULL;
-       if (p->type == para_Config && p->parent) {
-           if (!ustricmp(p->keyword, L"winhelp-topic")) {
-               char *topicname;
-               whlp_convert(uadv(p->keyword), 0, &topicname, 0);
-               /* Store the topic name in the private_data field of the
-                * containing section. */
-               p->parent->private_data = topicname;
-           }
+    {
+       int len = strlen(conf.filename);
+       if (len < 4 || conf.filename[len-4] != '.' ||
+           tolower(conf.filename[len-3] != 'h') ||
+           tolower(conf.filename[len-2] != 'l') ||
+           tolower(conf.filename[len-1] != 'p')) {
+           char *newf;
+           newf = snewn(len + 5, char);
+           sprintf(newf, "%s.hlp", conf.filename);
+           sfree(conf.filename);
+           conf.filename = newf;
+           len = strlen(newf);
        }
+       cntname = snewn(len+1, char);
+       sprintf(cntname, "%.*s.cnt", len-4, conf.filename);
+    }
+
+    state.cntfp = fopen(cntname, "wb");
+    if (!state.cntfp) {
+       error(err_cantopenw, cntname);
+       return;
     }
+    state.cnt_last_level = -1; state.cnt_workaround = 0;
 
     /*
      * Loop over the source form registering WHLP_TOPICs for
@@ -119,15 +235,18 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
            p->type == para_UnnumberedChapter ||
            p->type == para_Heading ||
            p->type == para_Subsect) {
-           char *topicid = p->private_data;
+
+           rdstringc rs = { 0, 0, NULL };
            char *errstr;
 
-           p->private_data = whlp_register_topic(h, topicid, &errstr);
+           whlp_rdadds(&rs, (wchar_t *)p->private_data, &conf, NULL);
+
+           p->private_data = whlp_register_topic(h, rs.text, &errstr);
            if (!p->private_data) {
                p->private_data = whlp_register_topic(h, NULL, NULL);
-               error(err_winhelp_ctxclash, &p->fpos, topicid, errstr);
+               error(err_winhelp_ctxclash, &p->fpos, rs.text, errstr);
            }
-           sfree(topicid);
+           sfree(rs.text);
        }
     }
 
@@ -135,20 +254,77 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
      * Loop over the index entries, preparing final text forms for
      * each one.
      */
-    for (i = 0; (ie = index234(idx->entries, i)) != NULL; i++) {
-       rdstringc rs = {0, 0, NULL};
-       whlp_rdaddwc(&rs, ie->text);
-       ie->backend_data = rs.text;
+    {
+       indexentry *ie_prev = NULL;
+       int nspaces = 1;
+
+       for (i = 0; (ie = index234(idx->entries, i)) != NULL; i++) {
+           rdstringc rs = {0, 0, NULL};
+           charset_state state = CHARSET_INIT_STATE;
+           whlp_rdaddwc(&rs, ie->text, &conf, &state);
+
+           if (ie_prev) {
+               /*
+                * It appears that Windows Help's index mechanism
+                * is inherently case-insensitive. Therefore, if two
+                * adjacent index terms compare equal apart from
+                * case, I'm going to append nonbreaking spaces to
+                * the end of the second one so that Windows will
+                * treat them as distinct.
+                * 
+                * This is nasty because we're depending on our
+                * case-insensitive comparison having the same
+                * semantics as the Windows one :-/ but I see no
+                * alternative.
+                */
+               wchar_t *a, *b;
+
+               a = ufroma_dup((char *)ie_prev->backend_data, conf.charset);
+               b = ufroma_dup(rs.text, conf.charset);
+               if (!ustricmp(a, b)) {
+                   int j;
+                   for (j = 0; j < nspaces; j++)
+                       whlp_rdadds(&rs, L"\xA0", &conf, &state);
+                   /*
+                    * Add one to nspaces, so that if another term
+                    * appears which is equivalent to the previous
+                    * two it'll acquire one more space.
+                    */
+                   nspaces++;
+               } else
+                   nspaces = 1;
+               sfree(a);
+               sfree(b);
+           }
+
+           whlp_rdadds(&rs, NULL, &conf, &state);
+
+           ie->backend_data = rs.text;
+
+           /*
+            * Only move ie_prev on if nspaces==1 (since when we
+            * have three or more adjacent terms differing only in
+            * case, we will want to compare with the _first_ of
+            * them because that won't have had any extra spaces
+            * added on which will foul up the comparison).
+            */
+           if (nspaces == 1)
+               ie_prev = ie;
+       }
     }
 
     whlp_prepare(h);
 
     /* ------------------------------------------------------------------
-     * Do the contents page, containing title, preamble and
-     * copyright.
+     * Begin the contents page.
      */
-
-    whlp_begin_topic(h, contents_topic, "Contents", "DB(\"btn_up\")", NULL);
+    {
+       rdstringc rs = {0, 0, NULL};
+       whlp_rdadds(&rs, conf.contents_text, &conf, NULL);
+       whlp_begin_topic(h, contents_topic, rs.text, "DB(\"btn_up\")", NULL);
+       state.curr_topic = contents_topic;
+       sfree(rs.text);
+    }
 
     /*
      * The manual title goes in the non-scroll region, and also
@@ -159,9 +335,11 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
        for (p = sourceform; p; p = p->next) {
            if (p->type == para_Title) {
                whlp_begin_para(h, WHLP_PARA_NONSCROLL);
-               whlp_mkparagraph(&state, FONT_TITLE, p->words, FALSE);
-               whlp_rdaddwc(&rs, p->words);
+               state.cstate = charset_init_state;
+               whlp_mkparagraph(&state, FONT_TITLE, p->words, FALSE, &conf);
+               whlp_wtext(&state, NULL);
                whlp_end_para(h);
+               whlp_rdaddwc(&rs, p->words, &conf, NULL);
            }
        }
        if (rs.text) {
@@ -169,37 +347,22 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
            fprintf(state.cntfp, ":Title %s\r\n", rs.text);
            sfree(rs.text);
        }
-       whlp_contents_write(&state, 1, "Title page", contents_topic);
-       /* FIXME: configurability in that string */
-    }
-
-    /*
-     * Next comes the preamble, which just goes into the ordinary
-     * scrolling region.
-     */
-    for (p = sourceform; p; p = p->next) {
-       if (p->type == para_Preamble) {
-           whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
-           whlp_begin_para(h, WHLP_PARA_SCROLL);
-           whlp_mkparagraph(&state, FONT_NORMAL, p->words, FALSE);
-           whlp_end_para(h);
+       {
+           rdstringc rs2 = {0,0,NULL};
+           whlp_rdadds(&rs2, conf.titlepage, &conf, NULL);
+           whlp_contents_write(&state, 1, rs2.text, contents_topic);
+           sfree(rs2.text);
        }
     }
 
     /*
-     * The copyright goes to two places, again: into the contents
-     * page and also into the system section.
+     * Put the copyright into the system section.
      */
     {
        rdstringc rs = {0, 0, NULL};
        for (p = sourceform; p; p = p->next) {
-           if (p->type == para_Copyright) {
-               whlp_para_attr(h, WHLP_PARA_SPACEBELOW, 12);
-               whlp_begin_para(h, WHLP_PARA_SCROLL);
-               whlp_mkparagraph(&state, FONT_NORMAL, p->words, FALSE);
-               whlp_end_para(h);
-               whlp_rdaddwc(&rs, p->words);
-           }
+           if (p->type == para_Copyright)
+               whlp_rdaddwc(&rs, p->words, &conf, NULL);
        }
        if (rs.text) {
            whlp_copyright(h, rs.text);
@@ -207,17 +370,6 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
        }
     }
 
-    /*
-     * Now do the primary navigation menu.
-     */
-    for (p = sourceform; p; p = p->next) {
-       if (p->type == para_Chapter ||
-           p->type == para_Appendix ||
-           p->type == para_UnnumberedChapter)
-           whlp_navmenu(&state, p);
-    }
-
-    state.curr_topic = contents_topic;
     lastsect = NULL;
 
     /* ------------------------------------------------------------------
@@ -234,8 +386,6 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
       case para_BR:
       case para_Biblio:                       /* only touch BiblioCited */
       case para_VersionID:
-      case para_Copyright:
-      case para_Preamble:
       case para_NoCite:
       case para_Title:
        break;
@@ -258,6 +408,26 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
       case para_UnnumberedChapter:
       case para_Heading:
       case para_Subsect:
+
+       if (!done_contents_topic) {
+           paragraph *p;
+
+           /*
+            * If this is the first section title we've seen, then
+            * we're currently still in the contents topic. We
+            * should therefore finish up the contents page by
+            * writing a nav menu.
+            */
+           for (p = sourceform; p; p = p->next) {
+               if (p->type == para_Chapter ||
+                   p->type == para_Appendix ||
+                   p->type == para_UnnumberedChapter)
+                   whlp_navmenu(&state, p, &conf);
+           }
+
+           done_contents_topic = TRUE;
+       }
+
        if (lastsect && lastsect->child) {
            paragraph *q;
            /*
@@ -265,22 +435,25 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
             * were in.
             */
            for (q = lastsect->child; q; q = q->sibling)
-               whlp_navmenu(&state, q);
+               whlp_navmenu(&state, q, &conf);
        }
        {
            rdstringc rs = {0, 0, NULL};
            WHLP_TOPIC new_topic, parent_topic;
            char *macro, *topicid;
+           charset_state cstate = CHARSET_INIT_STATE;
 
            new_topic = p->private_data;
            whlp_browse_link(h, state.curr_topic, new_topic);
            state.curr_topic = new_topic;
 
            if (p->kwtext) {
-               whlp_rdaddwc(&rs, p->kwtext);
-               rdaddsc(&rs, ": ");    /* FIXME: configurability */
+               whlp_rdaddwc(&rs, p->kwtext, &conf, &cstate);
+               whlp_rdadds(&rs, conf.sectsuffix, &conf, &cstate);
            }
-           whlp_rdaddwc(&rs, p->words);
+           whlp_rdaddwc(&rs, p->words, &conf, &cstate);
+           whlp_rdadds(&rs, NULL, &conf, &cstate);
+
            if (p->parent == NULL)
                parent_topic = contents_topic;
            else
@@ -332,12 +505,14 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
            sfree(rs.text);
 
            whlp_begin_para(h, WHLP_PARA_NONSCROLL);
+           state.cstate = charset_init_state;
            if (p->kwtext) {
-               whlp_mkparagraph(&state, FONT_TITLE, p->kwtext, FALSE);
+               whlp_mkparagraph(&state, FONT_TITLE, p->kwtext, FALSE, &conf);
                whlp_set_font(h, FONT_TITLE);
-               whlp_text(h, ": ");    /* FIXME: configurability */
+               whlp_wtext(&state, conf.sectsuffix);
            }
-           whlp_mkparagraph(&state, FONT_TITLE, p->words, FALSE);
+           whlp_mkparagraph(&state, FONT_TITLE, p->words, FALSE, &conf);
+           whlp_wtext(&state, NULL);
            whlp_end_para(h);
 
            lastsect = p;
@@ -349,11 +524,14 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
        whlp_para_attr(h, WHLP_PARA_ALIGNMENT, WHLP_ALIGN_CENTRE);
        whlp_begin_para(h, WHLP_PARA_SCROLL);
        whlp_set_font(h, FONT_RULE);
-#define TEN "\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0"
+       state.cstate = charset_init_state;
+#define TEN L"\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0\xA0"
 #define TWENTY TEN TEN
 #define FORTY TWENTY TWENTY
 #define EIGHTY FORTY FORTY
-       whlp_text(h, EIGHTY);
+       state.cstate = charset_init_state;
+       whlp_wtext(&state, EIGHTY);
+       whlp_wtext(&state, NULL);
 #undef TEN
 #undef TWENTY
 #undef FORTY
@@ -362,6 +540,7 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
        break;
 
       case para_Normal:
+      case para_Copyright:
       case para_DescribedThing:
       case para_Description:
       case para_BiblioCited:
@@ -374,12 +553,14 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
            whlp_set_tabstop(h, 72, WHLP_ALIGN_LEFT);
            whlp_begin_para(h, WHLP_PARA_SCROLL);
            whlp_set_font(h, FONT_NORMAL);
+           state.cstate = charset_init_state;
            if (p->type == para_Bullet) {
-               whlp_text(h, "\x95");
+               whlp_wtext(&state, conf.bullet);
            } else {
-               whlp_mkparagraph(&state, FONT_NORMAL, p->kwtext, FALSE);
-               whlp_text(h, ".");
+               whlp_mkparagraph(&state, FONT_NORMAL, p->kwtext, FALSE, &conf);
+               whlp_wtext(&state, conf.listsuffix);
            }
+           whlp_wtext(&state, NULL);
            whlp_tab(h);
        } else {
            whlp_para_attr(h, WHLP_PARA_LEFTINDENT,
@@ -387,12 +568,15 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
            whlp_begin_para(h, WHLP_PARA_SCROLL);
        }
 
+       state.cstate = charset_init_state;
+
        if (p->type == para_BiblioCited) {
-           whlp_mkparagraph(&state, FONT_NORMAL, p->kwtext, FALSE);
-           whlp_text(h, " ");
+           whlp_mkparagraph(&state, FONT_NORMAL, p->kwtext, FALSE, &conf);
+           whlp_wtext(&state, L" ");
        }
 
-       whlp_mkparagraph(&state, FONT_NORMAL, p->words, FALSE);
+       whlp_mkparagraph(&state, FONT_NORMAL, p->words, FALSE, &conf);
+       whlp_wtext(&state, NULL);
        whlp_end_para(h);
        break;
 
@@ -405,8 +589,7 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
         */
        {
            word *w;
-           wchar_t *t, *e;
-           char *c;
+           wchar_t *t, *e, *tmp;
 
            for (w = p->words; w; w = w->next) if (w->type == word_WeakCode) {
                t = w->text;
@@ -421,6 +604,7 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
 
                whlp_para_attr(h, WHLP_PARA_LEFTINDENT, 72*nesting);
                whlp_begin_para(h, WHLP_PARA_SCROLL);
+               state.cstate = charset_init_state;
                while (e && *e && *t) {
                    int n;
                    int ec = *e;
@@ -432,16 +616,19 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
                        whlp_set_font(h, FONT_BOLD_CODE);
                    else
                        whlp_set_font(h, FONT_CODE);
-                   whlp_convert(t, n, &c, FALSE);
-                   whlp_text(h, c);
-                   sfree(c);
+                   tmp = snewn(n+1, wchar_t);
+                   ustrncpy(tmp, t, n);
+                   tmp[n] = L'\0';
+                   whlp_wtext(&state, tmp);
+                   whlp_wtext(&state, NULL);
+                   state.cstate = charset_init_state;
+                   sfree(tmp);
                    t += n;
                    e += n;
                }
                whlp_set_font(h, FONT_CODE);
-               whlp_convert(t, 0, &c, FALSE);
-               whlp_text(h, c);
-               sfree(c);
+               whlp_wtext(&state, t);
+               whlp_wtext(&state, NULL);
                whlp_end_para(h);
            }
        }
@@ -449,7 +636,7 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
     }
 
     fclose(state.cntfp);
-    whlp_close(h, filename);
+    whlp_close(h, conf.filename);
 
     /*
      * Loop over the index entries, cleaning up our final text
@@ -458,6 +645,9 @@ void whlp_backend(paragraph *sourceform, keywordlist *keywords,
     for (i = 0; (ie = index234(idx->entries, i)) != NULL; i++) {
        sfree(ie->backend_data);
     }
+
+    sfree(conf.filename);
+    sfree(cntname);
 }
 
 static void whlp_contents_write(struct bk_whlp_state *state,
@@ -485,27 +675,30 @@ static void whlp_contents_write(struct bk_whlp_state *state,
     fputc('\n', state->cntfp);
 }
 
-static void whlp_navmenu(struct bk_whlp_state *state, paragraph *p) {
-    whlp_begin_para(state->h, WHLP_PARA_NONSCROLL);
+static void whlp_navmenu(struct bk_whlp_state *state, paragraph *p,
+                        whlpconf *conf) {
+    whlp_begin_para(state->h, WHLP_PARA_SCROLL);
     whlp_start_hyperlink(state->h, (WHLP_TOPIC)p->private_data);
+    state->cstate = charset_init_state;
     if (p->kwtext) {
-       whlp_mkparagraph(state, FONT_NORMAL, p->kwtext, TRUE);
+       whlp_mkparagraph(state, FONT_NORMAL, p->kwtext, TRUE, conf);
        whlp_set_font(state->h, FONT_NORMAL);
-       whlp_text(state->h, ": ");    /* FIXME: configurability */
+       whlp_wtext(state, conf->sectsuffix);
     }
-    whlp_mkparagraph(state, FONT_NORMAL, p->words, TRUE);
+    whlp_mkparagraph(state, FONT_NORMAL, p->words, TRUE, conf);
+    whlp_wtext(state, NULL);
     whlp_end_hyperlink(state->h);
     whlp_end_para(state->h);
 
 }
 
 static void whlp_mkparagraph(struct bk_whlp_state *state,
-                            int font, word *text, int subsidiary) {
+                            int font, word *text, int subsidiary,
+                            whlpconf *conf) {
     keyword *kwl;
     int deffont = font;
     int currfont = -1;
     int newfont;
-    char *c;
     paragraph *xref_target = NULL;
 
     for (; text; text = text->next) switch (text->type) {
@@ -582,24 +775,27 @@ static void whlp_mkparagraph(struct bk_whlp_state *state,
            whlp_set_font(state->h, newfont);
        }
        if (removeattr(text->type) == word_Normal) {
-           if (whlp_convert(text->text, 0, &c, TRUE))
-               whlp_text(state->h, c);
+           if (cvt_ok(conf->charset, text->text) || !text->alt)
+               whlp_wtext(state, text->text);
            else
-               whlp_mkparagraph(state, deffont, text->alt, FALSE);
-           sfree(c);
+               whlp_mkparagraph(state, deffont, text->alt, FALSE, conf);
        } else if (removeattr(text->type) == word_WhiteSpace) {
-           whlp_text(state->h, " ");
+           whlp_wtext(state, L" ");
        } else if (removeattr(text->type) == word_Quote) {
-           whlp_text(state->h,
-                     quoteaux(text->aux) == quote_Open ? "\x91" : "\x92");
-                                      /* FIXME: configurability */
+           whlp_wtext(state,
+                      quoteaux(text->aux) == quote_Open ?
+                      conf->lquote : conf->rquote);
        }
        break;
     }
 }
 
-static void whlp_rdaddwc(rdstringc *rs, word *text) {
-    char *c;
+static void whlp_rdaddwc(rdstringc *rs, word *text, whlpconf *conf,
+                        charset_state *state) {
+    charset_state ourstate = CHARSET_INIT_STATE;
+
+    if (!state)
+       state = &ourstate;
 
     for (; text; text = text->next) switch (text->type) {
       case word_HyperLink:
@@ -625,73 +821,72 @@ static void whlp_rdaddwc(rdstringc *rs, word *text) {
        assert(text->type != word_CodeQuote &&
               text->type != word_WkCodeQuote);
        if (removeattr(text->type) == word_Normal) {
-           if (whlp_convert(text->text, 0, &c, FALSE))
-               rdaddsc(rs, c);
+           if (cvt_ok(conf->charset, text->text) || !text->alt)
+               whlp_rdadds(rs, text->text, conf, state);
            else
-               whlp_rdaddwc(rs, text->alt);
-           sfree(c);
+               whlp_rdaddwc(rs, text->alt, conf, state);
        } else if (removeattr(text->type) == word_WhiteSpace) {
-           rdaddc(rs, ' ');
+           whlp_rdadds(rs, L" ", conf, state);
        } else if (removeattr(text->type) == word_Quote) {
-           rdaddc(rs, quoteaux(text->aux) == quote_Open ? '\x91' : '\x92');
-                                      /* FIXME: configurability */
+           whlp_rdadds(rs, quoteaux(text->aux) == quote_Open ?
+                       conf->lquote : conf->rquote, conf, state);
        }
        break;
     }
+
+    if (state == &ourstate)
+       whlp_rdadds(rs, NULL, conf, state);
 }
 
-/*
- * Convert a wide string into a string of chars. If `result' is
- * non-NULL, mallocs the resulting string and stores a pointer to
- * it in `*result'. If `result' is NULL, merely checks whether all
- * characters in the string are feasible for the output character
- * set.
- *
- * Return is nonzero if all characters are OK. If not all
- * characters are OK but `result' is non-NULL, a result _will_
- * still be generated!
- */
-static int whlp_convert(wchar_t *s, int maxlen,
-                       char **result, int hard_spaces) {
-    /*
-     * FIXME. Currently this is ISO8859-1 only.
-     */
-    int doing = (result != 0);
-    int ok = TRUE;
-    char *p = NULL;
-    int plen = 0, psize = 0;
-
-    if (maxlen <= 0)
-       maxlen = -1;
-
-    for (; *s && maxlen != 0; s++, maxlen--) {
-       wchar_t c = *s;
-       char outc;
-
-       if ((c >= 32 && c <= 126) ||
-           (c >= 160 && c <= 255)) {
-           /* Char is OK. */
-           if (c == 32 && hard_spaces)
-               outc = '\240';
-           else
-               outc = (char)c;
-       } else {
-           /* Char is not OK. */
-           ok = FALSE;
-           outc = 0xBF;               /* approximate the good old DEC `uh?' */
-       }
-       if (doing) {
-           if (plen >= psize) {
-               psize = plen + 256;
-               p = resize(p, psize);
-           }
-           p[plen++] = outc;
+static void whlp_rdadds(rdstringc *rs, const wchar_t *text, whlpconf *conf,
+                       charset_state *state)
+{
+    charset_state ourstate = CHARSET_INIT_STATE;
+    int textlen = text ? ustrlen(text) : 0;
+    char outbuf[256];
+    int ret;
+
+    if (!state)
+       state = &ourstate;
+
+    while (textlen > 0 &&
+          (ret = charset_from_unicode(&text, &textlen, outbuf,
+                                      lenof(outbuf)-1,
+                                      conf->charset, state, NULL)) > 0) {
+       outbuf[ret] = '\0';
+       rdaddsc(rs, outbuf);
+    }
+
+    if (text == NULL || state == &ourstate) {
+       if ((ret = charset_from_unicode(NULL, 0, outbuf, lenof(outbuf)-1,
+                                       conf->charset, state, NULL)) > 0) {
+           outbuf[ret] = '\0';
+           rdaddsc(rs, outbuf);
        }
     }
-    if (doing) {
-       p = resize(p, plen+1);
-       p[plen] = '\0';
-       *result = p;
+}
+
+static void whlp_wtext(struct bk_whlp_state *state, const wchar_t *text)
+{
+    int textlen = text ? ustrlen(text) : 0;
+    char outbuf[256];
+    int ret;
+
+    while (textlen > 0 &&
+          (ret = charset_from_unicode(&text, &textlen, outbuf,
+                                      lenof(outbuf)-1,
+                                      state->charset, &state->cstate,
+                                      NULL)) > 0) {
+       outbuf[ret] = '\0';
+       whlp_text(state->h, outbuf);
+    }
+
+    if (text == NULL) {
+       if ((ret = charset_from_unicode(NULL, 0, outbuf, lenof(outbuf)-1,
+                                       state->charset, &state->cstate,
+                                       NULL)) > 0) {
+           outbuf[ret] = '\0';
+           whlp_text(state->h, outbuf);
+       }
     }
-    return ok;
 }