Add an error check for correct formatting in Deflate uncompressed
[sgt/halibut] / bk_text.c
index bde11bb..4f8cdd7 100644 (file)
--- a/bk_text.c
+++ b/bk_text.c
@@ -10,8 +10,8 @@
 typedef enum { LEFT, LEFTPLUS, CENTRE } alignment;
 typedef struct {
     alignment align;
-    int just_numbers;
-    wchar_t underline;
+    int number_at_all, just_numbers;
+    wchar_t *underline;
     wchar_t *number_suffix;
 } alignstruct;
 
@@ -23,16 +23,29 @@ typedef struct {
     int nasect;
     int include_version_id;
     int indent_preambles;
+    int charset;
     word bullet;
+    wchar_t *lquote, *rquote, *rule;
+    char *filename;
+    wchar_t *listsuffix, *startemph, *endemph;
 } textconfig;
 
-static int text_convert(wchar_t *, char **);
+typedef struct {
+    FILE *fp;
+    int charset;
+    charset_state state;
+} textfile;
+
+static void text_heading(textfile *, word *, word *, word *, alignstruct,
+                        int, int, textconfig *);
+static void text_rule(textfile *, int, int, textconfig *);
+static void text_para(textfile *, word *, wchar_t *, word *, int, int, int,
+                     textconfig *);
+static void text_codepara(textfile *, word *, int, int);
+static void text_versionid(textfile *, word *, textconfig *);
 
-static void text_heading(FILE *, word *, word *, word *, alignstruct, int,int);
-static void text_rule(FILE *, int, int);
-static void text_para(FILE *, word *, char *, word *, int, int, int);
-static void text_codepara(FILE *, word *, int, int);
-static void text_versionid(FILE *, word *);
+static void text_output(textfile *, const wchar_t *);
+static void text_output_many(textfile *, int, wchar_t);
 
 static alignment utoalign(wchar_t *p) {
     if (!ustricmp(p, L"centre") || !ustricmp(p, L"center"))
@@ -44,6 +57,8 @@ static alignment utoalign(wchar_t *p) {
 
 static textconfig text_configure(paragraph *source) {
     textconfig ret;
+    paragraph *p;
+    int n;
 
     /*
      * Non-negotiables.
@@ -52,6 +67,7 @@ static textconfig text_configure(paragraph *source) {
     ret.bullet.alt = NULL;
     ret.bullet.type = word_Normal;
     ret.atitle.just_numbers = FALSE;   /* ignored */
+    ret.atitle.number_at_all = TRUE;   /* ignored */
 
     /*
      * Defaults.
@@ -62,156 +78,271 @@ static textconfig text_configure(paragraph *source) {
     ret.listindentafter = 3;
     ret.width = 68;
     ret.atitle.align = CENTRE;
-    ret.atitle.underline = L'=';
+    ret.atitle.underline = L"\x2550\0=\0\0";
     ret.achapter.align = LEFT;
     ret.achapter.just_numbers = FALSE;
-    ret.achapter.number_suffix = ustrdup(L": ");
-    ret.achapter.underline = L'-';
+    ret.achapter.number_at_all = TRUE;
+    ret.achapter.number_suffix = L": ";
+    ret.achapter.underline = L"\x203E\0-\0\0";
     ret.nasect = 1;
-    ret.asect = mknewa(alignstruct, ret.nasect);
+    ret.asect = snewn(ret.nasect, alignstruct);
     ret.asect[0].align = LEFTPLUS;
     ret.asect[0].just_numbers = TRUE;
-    ret.asect[0].number_suffix = ustrdup(L" ");
-    ret.asect[0].underline = L'\0';
+    ret.asect[0].number_at_all = TRUE;
+    ret.asect[0].number_suffix = L" ";
+    ret.asect[0].underline = L"\0";
     ret.include_version_id = TRUE;
     ret.indent_preambles = FALSE;
-    ret.bullet.text = ustrdup(L"-");
-
-    for (; source; source = source->next) {
-       if (source->type == para_Config) {
-           if (!ustricmp(source->keyword, L"text-indent")) {
-               ret.indent = utoi(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-indent-code")) {
-               ret.indent_code = utoi(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-width")) {
-               ret.width = utoi(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-list-indent")) {
-               ret.listindentbefore = utoi(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-listitem-indent")) {
-               ret.listindentafter = utoi(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-chapter-align")) {
-               ret.achapter.align = utoalign(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-chapter-underline")) {
-               ret.achapter.underline = *uadv(source->keyword);
-           } else if (!ustricmp(source->keyword, L"text-chapter-numeric")) {
-               ret.achapter.just_numbers = utob(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-chapter-suffix")) {
-               ret.achapter.number_suffix = ustrdup(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-section-align")) {
-               wchar_t *p = uadv(source->keyword);
+    ret.bullet.text = L"\x2022\0-\0\0";
+    ret.rule = L"\x2500\0-\0\0";
+    ret.filename = dupstr("output.txt");
+    ret.startemph = L"_\0_\0\0";
+    ret.endemph = uadv(ret.startemph);
+    ret.listsuffix = L".";
+    ret.charset = CS_ASCII;
+    /*
+     * Default quote characters are Unicode matched single quotes,
+     * falling back to the TeXlike `'.
+     */
+    ret.lquote = L"\x2018\0\x2019\0`\0'\0\0";
+    ret.rquote = uadv(ret.lquote);
+
+    /*
+     * Two-pass configuration so that we can pick up global config
+     * (e.g. `quotes') before having it overridden by specific
+     * config (`text-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) {
+       if (p->type == para_Config) {
+           if (!ustricmp(p->keyword, L"text-indent")) {
+               ret.indent = utoi(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-charset")) {
+               ret.charset = charset_from_ustr(&p->fpos, uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-filename")) {
+               sfree(ret.filename);
+               ret.filename = dupstr(adv(p->origkeyword));
+           } else if (!ustricmp(p->keyword, L"text-indent-code")) {
+               ret.indent_code = utoi(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-width")) {
+               ret.width = utoi(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-list-indent")) {
+               ret.listindentbefore = utoi(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-listitem-indent")) {
+               ret.listindentafter = utoi(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-chapter-align")) {
+               ret.achapter.align = utoalign(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-chapter-underline")) {
+               ret.achapter.underline = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"text-chapter-numeric")) {
+               ret.achapter.just_numbers = utob(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-chapter-shownumber")) {
+               ret.achapter.number_at_all = utob(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-chapter-suffix")) {
+               ret.achapter.number_suffix = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"text-section-align")) {
+               wchar_t *q = uadv(p->keyword);
                int n = 0;
-               if (uisdigit(*p)) {
-                   n = utoi(p);
-                   p = uadv(p);
+               if (uisdigit(*q)) {
+                   n = utoi(q);
+                   q = uadv(q);
                }
                if (n >= ret.nasect) {
                    int i;
-                   ret.asect = resize(ret.asect, n+1);
+                   ret.asect = sresize(ret.asect, n+1, alignstruct);
                    for (i = ret.nasect; i <= n; i++)
                        ret.asect[i] = ret.asect[ret.nasect-1];
                    ret.nasect = n+1;
                }
-               ret.asect[n].align = utoalign(p);
-           } else if (!ustricmp(source->keyword, L"text-section-underline")) {
-               wchar_t *p = uadv(source->keyword);
+               ret.asect[n].align = utoalign(q);
+           } else if (!ustricmp(p->keyword, L"text-section-underline")) {
+               wchar_t *q = uadv(p->keyword);
                int n = 0;
-               if (uisdigit(*p)) {
-                   n = utoi(p);
-                   p = uadv(p);
+               if (uisdigit(*q)) {
+                   n = utoi(q);
+                   q = uadv(q);
                }
                if (n >= ret.nasect) {
                    int i;
-                   ret.asect = resize(ret.asect, n+1);
+                   ret.asect = sresize(ret.asect, n+1, alignstruct);
                    for (i = ret.nasect; i <= n; i++)
                        ret.asect[i] = ret.asect[ret.nasect-1];
                    ret.nasect = n+1;
                }
-               ret.asect[n].underline = *p;
-           } else if (!ustricmp(source->keyword, L"text-section-numeric")) {
-               wchar_t *p = uadv(source->keyword);
+               ret.asect[n].underline = q;
+           } else if (!ustricmp(p->keyword, L"text-section-numeric")) {
+               wchar_t *q = uadv(p->keyword);
                int n = 0;
-               if (uisdigit(*p)) {
-                   n = utoi(p);
-                   p = uadv(p);
+               if (uisdigit(*q)) {
+                   n = utoi(q);
+                   q = uadv(q);
                }
                if (n >= ret.nasect) {
                    int i;
-                   ret.asect = resize(ret.asect, n+1);
+                   ret.asect = sresize(ret.asect, n+1, alignstruct);
                    for (i = ret.nasect; i <= n; i++)
                        ret.asect[i] = ret.asect[ret.nasect-1];
                    ret.nasect = n+1;
                }
-               ret.asect[n].just_numbers = utob(p);
-           } else if (!ustricmp(source->keyword, L"text-section-suffix")) {
-               wchar_t *p = uadv(source->keyword);
+               ret.asect[n].just_numbers = utob(q);
+           } else if (!ustricmp(p->keyword, L"text-section-shownumber")) {
+               wchar_t *q = uadv(p->keyword);
                int n = 0;
-               if (uisdigit(*p)) {
-                   n = utoi(p);
-                   p = uadv(p);
+               if (uisdigit(*q)) {
+                   n = utoi(q);
+                   q = uadv(q);
                }
                if (n >= ret.nasect) {
                    int i;
-                   ret.asect = resize(ret.asect, n+1);
+                   ret.asect = sresize(ret.asect, n+1, alignstruct);
                    for (i = ret.nasect; i <= n; i++)
                        ret.asect[i] = ret.asect[ret.nasect-1];
                    ret.nasect = n+1;
                }
-               ret.asect[n].number_suffix = ustrdup(p);
-           } else if (!ustricmp(source->keyword, L"text-title-align")) {
-               ret.atitle.align = utoalign(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-title-underline")) {
-               ret.atitle.underline = *uadv(source->keyword);
-           } else if (!ustricmp(source->keyword, L"text-versionid")) {
-               ret.include_version_id = utob(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-indent-preamble")) {
-               ret.indent_preambles = utob(uadv(source->keyword));
-           } else if (!ustricmp(source->keyword, L"text-bullet")) {
-               ret.bullet.text = uadv(source->keyword);
+               ret.asect[n].number_at_all = utob(q);
+           } else if (!ustricmp(p->keyword, L"text-section-suffix")) {
+               wchar_t *q = uadv(p->keyword);
+               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, alignstruct);
+                   for (i = ret.nasect; i <= n; i++) {
+                       ret.asect[i] = ret.asect[ret.nasect-1];
+                   }
+                   ret.nasect = n+1;
+               }
+               ret.asect[n].number_suffix = q;
+           } else if (!ustricmp(p->keyword, L"text-title-align")) {
+               ret.atitle.align = utoalign(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-title-underline")) {
+               ret.atitle.underline = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"text-versionid")) {
+               ret.include_version_id = utob(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-indent-preamble")) {
+               ret.indent_preambles = utob(uadv(p->keyword));
+           } else if (!ustricmp(p->keyword, L"text-bullet")) {
+               ret.bullet.text = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"text-rule")) {
+               ret.rule = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"text-list-suffix")) {
+               ret.listsuffix = uadv(p->keyword);
+           } else if (!ustricmp(p->keyword, L"text-emphasis")) {
+               if (*uadv(p->keyword) && *uadv(uadv(p->keyword))) {
+                   ret.startemph = uadv(p->keyword);
+                   ret.endemph = uadv(ret.startemph);
+               }
+           } else if (!ustricmp(p->keyword, L"text-quotes")) {
+               if (*uadv(p->keyword) && *uadv(uadv(p->keyword))) {
+                   ret.lquote = uadv(p->keyword);
+                   ret.rquote = uadv(ret.lquote);
+               }
            }
        }
     }
 
+    /*
+     * Now process fallbacks on quote characters, underlines, the
+     * rule character, the emphasis 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 (*uadv(ret.endemph) && *uadv(uadv(ret.endemph)) &&
+          (!cvt_ok(ret.charset, ret.startemph) ||
+           !cvt_ok(ret.charset, ret.endemph))) {
+       ret.startemph = uadv(ret.endemph);
+       ret.endemph = uadv(ret.startemph);
+    }
+
+    while (*ret.atitle.underline && *uadv(ret.atitle.underline) &&
+          !cvt_ok(ret.charset, ret.atitle.underline))
+       ret.atitle.underline = uadv(ret.atitle.underline);
+    
+    while (*ret.achapter.underline && *uadv(ret.achapter.underline) &&
+          !cvt_ok(ret.charset, ret.achapter.underline))
+       ret.achapter.underline = uadv(ret.achapter.underline);
+
+    for (n = 0; n < ret.nasect; n++) {
+       while (*ret.asect[n].underline && *uadv(ret.asect[n].underline) &&
+              !cvt_ok(ret.charset, ret.asect[n].underline))
+           ret.asect[n].underline = uadv(ret.asect[n].underline);
+    }
+    
+    while (*ret.bullet.text && *uadv(ret.bullet.text) &&
+          !cvt_ok(ret.charset, ret.bullet.text))
+       ret.bullet.text = uadv(ret.bullet.text);
+
+    while (*ret.rule && *uadv(ret.rule) &&
+          !cvt_ok(ret.charset, ret.rule))
+       ret.rule = uadv(ret.rule);
+
     return ret;
 }
 
+paragraph *text_config_filename(char *filename)
+{
+    return cmdline_cfg_simple("text-filename", filename, NULL);
+}
+
 void text_backend(paragraph *sourceform, keywordlist *keywords,
-                 indexdata *idx) {
+                 indexdata *idx, void *unused) {
     paragraph *p;
     textconfig conf;
     word *prefix, *body, *wp;
     word spaceword;
-    FILE *fp;
-    char *prefixextra;
-    int nesting, nestindent;
+    textfile tf;
+    wchar_t *prefixextra;
+    int nesting, nestbase, nestindent;
     int indentb, indenta;
-    int done_copyright;
 
+    IGNORE(unused);
     IGNORE(keywords);                 /* we don't happen to need this */
     IGNORE(idx);                      /* or this */
 
     conf = text_configure(sourceform);
 
     /*
-     * Determine the output file name, and open the output file
-     *
-     * FIXME: want configurable output file names here. For the
-     * moment, we'll just call it `output.txt'.
+     * Open the output file.
      */
-    fp = fopen("output.txt", "w");
-    if (!fp) {
-       error(err_cantopenw, "output.txt");
+    if (!strcmp(conf.filename, "-"))
+       tf.fp = stdout;
+    else
+       tf.fp = fopen(conf.filename, "w");
+    if (!tf.fp) {
+       error(err_cantopenw, conf.filename);
        return;
     }
+    tf.charset = conf.charset;
+    tf.state = charset_init_state;
 
     /* Do the title */
     for (p = sourceform; p; p = p->next)
        if (p->type == para_Title)
-           text_heading(fp, NULL, NULL, p->words,
-                        conf.atitle, conf.indent, conf.width);
+           text_heading(&tf, NULL, NULL, p->words,
+                        conf.atitle, conf.indent, conf.width, &conf);
 
     nestindent = conf.listindentbefore + conf.listindentafter;
-    nesting = (conf.indent_preambles ? 0 : -conf.indent);
-    done_copyright = FALSE;
+    nestbase = (conf.indent_preambles ? 0 : -conf.indent);
+    nesting = nestbase;
 
     /* Do the main document */
     for (p = sourceform; p; p = p->next) switch (p->type) {
@@ -229,7 +360,7 @@ void text_backend(paragraph *sourceform, keywordlist *keywords,
        break;
       case para_LcontPop:
        nesting -= nestindent;
-       assert(nesting >= 0);
+       assert(nesting >= nestbase);
        break;
 
        /*
@@ -240,7 +371,6 @@ void text_backend(paragraph *sourceform, keywordlist *keywords,
       case para_BR:
       case para_Biblio:                       /* only touch BiblioCited */
       case para_VersionID:
-      case para_Copyright:
       case para_NoCite:
       case para_Title:
        break;
@@ -251,36 +381,24 @@ void text_backend(paragraph *sourceform, keywordlist *keywords,
       case para_Chapter:
       case para_Appendix:
       case para_UnnumberedChapter:
-       /*
-        * The copyright should come after the preamble but before
-        * the first chapter title.
-        */
-       if (!done_copyright) {
-           paragraph *p;
-
-           for (p = sourceform; p; p = p->next)
-               if (p->type == para_Copyright)
-                   text_para(fp, NULL, NULL, p->words,
-                             conf.indent + nesting, 0, conf.width - nesting);
-           done_copyright = TRUE;
-       }
-       text_heading(fp, p->kwtext, p->kwtext2, p->words,
-                    conf.achapter, conf.indent, conf.width);
+       text_heading(&tf, p->kwtext, p->kwtext2, p->words,
+                    conf.achapter, conf.indent, conf.width, &conf);
        nesting = 0;
        break;
 
       case para_Heading:
       case para_Subsect:
-       text_heading(fp, p->kwtext, p->kwtext2, p->words,
+       text_heading(&tf, p->kwtext, p->kwtext2, p->words,
                     conf.asect[p->aux>=conf.nasect ? conf.nasect-1 : p->aux],
-                    conf.indent, conf.width);
+                    conf.indent, conf.width, &conf);
        break;
 
       case para_Rule:
-       text_rule(fp, conf.indent + nesting, conf.width - nesting);
+       text_rule(&tf, conf.indent + nesting, conf.width - nesting, &conf);
        break;
 
       case para_Normal:
+      case para_Copyright:
       case para_DescribedThing:
       case para_Description:
       case para_BiblioCited:
@@ -293,7 +411,7 @@ void text_backend(paragraph *sourceform, keywordlist *keywords,
            indenta = conf.listindentafter;
        } else if (p->type == para_NumberedList) {
            prefix = p->kwtext;
-           prefixextra = ".";         /* FIXME: configurability */
+           prefixextra = conf.listsuffix;
            indentb = conf.listindentbefore;
            indenta = conf.listindentafter;
        } else if (p->type == para_Description) {
@@ -318,9 +436,9 @@ void text_backend(paragraph *sourceform, keywordlist *keywords,
            wp = NULL;
            body = p->words;
        }
-       text_para(fp, prefix, prefixextra, body,
+       text_para(&tf, prefix, prefixextra, body,
                  conf.indent + nesting + indentb, indenta,
-                 conf.width - nesting - indentb - indenta);
+                 conf.width - nesting - indentb - indenta, &conf);
        if (wp) {
            wp->next = NULL;
            free_word_list(body);
@@ -328,7 +446,7 @@ void text_backend(paragraph *sourceform, keywordlist *keywords,
        break;
 
       case para_Code:
-       text_codepara(fp, p->words,
+       text_codepara(&tf, p->words,
                      conf.indent + nesting + conf.indent_code,
                      conf.width - nesting - 2 * conf.indent_code);
        break;
@@ -338,75 +456,52 @@ void text_backend(paragraph *sourceform, keywordlist *keywords,
     if (conf.include_version_id) {
        for (p = sourceform; p; p = p->next)
            if (p->type == para_VersionID)
-               text_versionid(fp, p->words);
+               text_versionid(&tf, p->words, &conf);
     }
 
     /*
      * Tidy up
      */
-    fclose(fp);
-    {
-       int i;
-       sfree(conf.achapter.number_suffix);
-       for (i = 0; i < conf.nasect; i++)
-           sfree(conf.asect[i].number_suffix);
-       sfree(conf.asect);
-       sfree(conf.bullet.text);
-    }
+    text_output(&tf, NULL);           /* end charset conversion */
+    if (tf.fp != stdout)
+       fclose(tf.fp);
+    sfree(conf.asect);
+    sfree(conf.filename);
 }
 
-/*
- * 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 text_convert(wchar_t *s, char **result) {
-    /*
-     * FIXME. Currently this is ISO8859-1 only.
-     */
-    int doing = (result != 0);
-    int ok = TRUE;
-    char *p = NULL;
-    int plen = 0, psize = 0;
-
-    for (; *s; s++) {
-       wchar_t c = *s;
-       char outc;
-
-       if ((c >= 32 && c <= 126) ||
-           (c >= 160 && c <= 255)) {
-           /* Char is OK. */
-           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 text_output(textfile *tf, const wchar_t *s)
+{
+    char buf[256];
+    int ret, len;
+    const wchar_t **sp;
+
+    if (!s) {
+       sp = NULL;
+       len = 1;
+    } else {
+       sp = &s;
+       len = ustrlen(s);
     }
-    if (doing) {
-       p = resize(p, plen+1);
-       p[plen] = '\0';
-       *result = p;
+
+    while (len > 0) {
+       ret = charset_from_unicode(sp, &len, buf, lenof(buf),
+                                  tf->charset, &tf->state, NULL);
+       if (!sp)
+           len = 0;
+       fwrite(buf, 1, ret, tf->fp);
     }
-    return ok;
 }
 
-static void text_rdaddwc(rdstringc *rs, word *text, word *end) {
-    char *c;
+static void text_output_many(textfile *tf, int n, wchar_t c)
+{
+    wchar_t s[2];
+    s[0] = c;
+    s[1] = L'\0';
+    while (n--)
+       text_output(tf, s);
+}
 
+static void text_rdaddw(rdstring *rs, word *text, word *end, textconfig *cfg) {
     for (; text && text != end; text = text->next) switch (text->type) {
       case word_HyperLink:
       case word_HyperEnd:
@@ -433,47 +528,50 @@ static void text_rdaddwc(rdstringc *rs, word *text, word *end) {
        if (towordstyle(text->type) == word_Emph &&
            (attraux(text->aux) == attr_First ||
             attraux(text->aux) == attr_Only))
-           rdaddc(rs, '_');           /* FIXME: configurability */
+           rdadds(rs, cfg->startemph);
        else if (towordstyle(text->type) == word_Code &&
                 (attraux(text->aux) == attr_First ||
                  attraux(text->aux) == attr_Only))
-           rdaddc(rs, '`');           /* FIXME: configurability */
+           rdadds(rs, cfg->lquote);
        if (removeattr(text->type) == word_Normal) {
-           if (text_convert(text->text, &c))
-               rdaddsc(rs, c);
+           if (cvt_ok(cfg->charset, text->text) || !text->alt)
+               rdadds(rs, text->text);
            else
-               text_rdaddwc(rs, text->alt, NULL);
-           sfree(c);
+               text_rdaddw(rs, text->alt, NULL, cfg);
        } else if (removeattr(text->type) == word_WhiteSpace) {
-           rdaddc(rs, ' ');
+           rdadd(rs, L' ');
        } else if (removeattr(text->type) == word_Quote) {
-           rdaddc(rs, quoteaux(text->aux) == quote_Open ? '`' : '\'');
-                                      /* FIXME: configurability */
+           rdadds(rs, quoteaux(text->aux) == quote_Open ?
+                  cfg->lquote : cfg->rquote);
        }
        if (towordstyle(text->type) == word_Emph &&
            (attraux(text->aux) == attr_Last ||
             attraux(text->aux) == attr_Only))
-           rdaddc(rs, '_');           /* FIXME: configurability */
+           rdadds(rs, cfg->endemph);
        else if (towordstyle(text->type) == word_Code &&
                 (attraux(text->aux) == attr_Last ||
                  attraux(text->aux) == attr_Only))
-           rdaddc(rs, '\'');          /* FIXME: configurability */
+           rdadds(rs, cfg->rquote);
        break;
     }
 }
 
-static int text_width(word *);
+static int text_width(void *, word *);
 
-static int text_width_list(word *text) {
+static int text_width_list(void *ctx, word *text) {
     int w = 0;
     while (text) {
-       w += text_width(text);
+       w += text_width(ctx, text);
        text = text->next;
     }
     return w;
 }
 
-static int text_width(word *text) {
+static int text_width(void *ctx, word *text) {
+    textconfig *cfg = (textconfig *)ctx;
+    int wid;
+    int attr;
+
     switch (text->type) {
       case word_HyperLink:
       case word_HyperEnd:
@@ -482,19 +580,35 @@ static int text_width(word *text) {
       case word_XrefEnd:
       case word_IndexRef:
        return 0;
+    }
+
+    assert(text->type < word_internal_endattrs);
+
+    wid = 0;
+    attr = towordstyle(text->type);
+    if (attr == word_Emph || attr == word_Code) {
+       if (attraux(text->aux) == attr_Only ||
+           attraux(text->aux) == attr_First)
+           wid += ustrwid(attr == word_Emph ? cfg->startemph : cfg->lquote,
+                          cfg->charset);
+    }
+    if (attr == word_Emph || attr == word_Code) {
+       if (attraux(text->aux) == attr_Only ||
+           attraux(text->aux) == attr_Last)
+           wid += ustrwid(attr == word_Emph ? cfg->startemph : cfg->lquote,
+                          cfg->charset);
+    }
 
+    switch (text->type) {
       case word_Normal:
       case word_Emph:
       case word_Code:
       case word_WeakCode:
-       return (((text->type == word_Emph ||
-                 text->type == word_Code)
-                ? (attraux(text->aux) == attr_Only ? 2 :
-                   attraux(text->aux) == attr_Always ? 0 : 1)
-                : 0) +
-               (text_convert(text->text, NULL) ?
-                ustrlen(text->text) :
-                text_width_list(text->alt)));
+       if (cvt_ok(cfg->charset, text->text) || !text->alt)
+           wid += ustrwid(text->text, cfg->charset);
+       else
+           wid += text_width_list(ctx, text->alt);
+       return wid;
 
       case word_WhiteSpace:
       case word_EmphSpace:
@@ -506,38 +620,36 @@ static int text_width(word *text) {
       case word_WkCodeQuote:
        assert(text->type != word_CodeQuote &&
               text->type != word_WkCodeQuote);
-       return (((towordstyle(text->type) == word_Emph ||
-                 towordstyle(text->type) == word_Code)
-                ? (attraux(text->aux) == attr_Only ? 2 :
-                   attraux(text->aux) == attr_Always ? 0 : 1)
-                : 0) + 1);
+       if (removeattr(text->type) == word_Quote) {
+           if (quoteaux(text->aux) == quote_Open)
+               wid += ustrwid(cfg->lquote, cfg->charset);
+           else
+               wid += ustrwid(cfg->rquote, cfg->charset);
+       } else
+           wid++;                     /* space */
     }
-    return 0;                         /* should never happen */
+
+    return wid;
 }
 
-static void text_heading(FILE *fp, word *tprefix, word *nprefix, word *text,
-                        alignstruct align, int indent, int width) {
-    rdstringc t = { 0, 0, NULL };
+static void text_heading(textfile *tf, word *tprefix, word *nprefix,
+                        word *text, alignstruct align,
+                        int indent, int width, textconfig *cfg) {
+    rdstring t = { 0, 0, NULL };
     int margin, length;
     int firstlinewidth, wrapwidth;
     wrappedline *wrapping, *p;
 
-    if (align.just_numbers && nprefix) {
-       char *c;
-       text_rdaddwc(&t, nprefix, NULL);
-       if (text_convert(align.number_suffix, &c)) {
-           rdaddsc(&t, c);
-           sfree(c);
-       }
-    } else if (!align.just_numbers && tprefix) {
-       char *c;
-       text_rdaddwc(&t, tprefix, NULL);
-       if (text_convert(align.number_suffix, &c)) {
-           rdaddsc(&t, c);
-           sfree(c);
+    if (align.number_at_all) {
+       if (align.just_numbers && nprefix) {
+           text_rdaddw(&t, nprefix, NULL, cfg);
+           rdadds(&t, align.number_suffix);
+       } else if (!align.just_numbers && tprefix) {
+           text_rdaddw(&t, tprefix, NULL, cfg);
+           rdadds(&t, align.number_suffix);
        }
     }
-    margin = length = (t.text ? strlen(t.text) : 0);
+    margin = length = ustrwid(t.text ? t.text : L"", cfg->charset);
 
     if (align.align == LEFTPLUS) {
        margin = indent - margin;
@@ -550,67 +662,70 @@ static void text_heading(FILE *fp, word *tprefix, word *nprefix, word *text,
        wrapwidth = indent + width;
     }
 
-    wrapping = wrap_para(text, firstlinewidth, wrapwidth, text_width);
+    wrapping = wrap_para(text, firstlinewidth, wrapwidth,
+                        text_width, cfg, 0);
     for (p = wrapping; p; p = p->next) {
-       text_rdaddwc(&t, p->begin, p->end);
-       length = (t.text ? strlen(t.text) : 0);
+       text_rdaddw(&t, p->begin, p->end, cfg);
+       length = ustrwid(t.text ? t.text : L"", cfg->charset);
        if (align.align == CENTRE) {
            margin = (indent + width - length)/2;
            if (margin < 0) margin = 0;
        }
-       fprintf(fp, "%*s%s\n", margin, "", t.text);
-       if (align.underline != L'\0') {
-           char *u, uc;
-           wchar_t uw[2];
-           uw[0] = align.underline; uw[1] = L'\0';
-           text_convert(uw, &u);
-           uc = u[0];
-           sfree(u);
-           fprintf(fp, "%*s", margin, "");
-           while (length--)
-               putc(uc, fp);
-           putc('\n', fp);
+       text_output_many(tf, margin, L' ');
+       text_output(tf, t.text);
+       text_output(tf, L"\n");
+       if (*align.underline) {
+           text_output_many(tf, margin, L' ');
+           while (length > 0) {
+               text_output(tf, align.underline);
+               length -= ustrwid(align.underline, cfg->charset);
+           }
+           text_output(tf, L"\n");
        }
        if (align.align == LEFTPLUS)
            margin = indent;
        else
            margin = 0;
        sfree(t.text);
-       t = empty_rdstringc;
+       t = empty_rdstring;
     }
     wrap_free(wrapping);
-    putc('\n', fp);
+    text_output(tf, L"\n");
 
     sfree(t.text);
 }
 
-static void text_rule(FILE *fp, int indent, int width) {
-    while (indent--) putc(' ', fp);
-    while (width--) putc('-', fp);     /* FIXME: configurability! */
-    putc('\n', fp);
-    putc('\n', fp);
+static void text_rule(textfile *tf, int indent, int width, textconfig *cfg) {
+    text_output_many(tf, indent, L' ');
+    while (width > 0) {
+       text_output(tf, cfg->rule);
+       width -= ustrwid(cfg->rule, cfg->charset);
+    }
+    text_output_many(tf, 2, L'\n');
 }
 
-static void text_para(FILE *fp, word *prefix, char *prefixextra, word *text,
-                     int indent, int extraindent, int width) {
+static void text_para(textfile *tf, word *prefix, wchar_t *prefixextra,
+                     word *text, int indent, int extraindent, int width,
+                     textconfig *cfg) {
     wrappedline *wrapping, *p;
-    rdstringc pfx = { 0, 0, NULL };
+    rdstring pfx = { 0, 0, NULL };
     int e;
     int firstlinewidth = width;
 
     if (prefix) {
-       text_rdaddwc(&pfx, prefix, NULL);
+       text_rdaddw(&pfx, prefix, NULL, cfg);
        if (prefixextra)
-           rdaddsc(&pfx, prefixextra);
-       fprintf(fp, "%*s%s", indent, "", pfx.text);
+           rdadds(&pfx, prefixextra);
+       text_output_many(tf, indent, L' ');
+       text_output(tf, pfx.text);
        /* If the prefix is too long, shorten the first line to fit. */
-       e = extraindent - strlen(pfx.text);
+       e = extraindent - ustrwid(pfx.text ? pfx.text : L"", cfg->charset);
        if (e < 0) {
            firstlinewidth += e;       /* this decreases it, since e < 0 */
            if (firstlinewidth < 0) {
                e = indent + extraindent;
                firstlinewidth = width;
-               fprintf(fp, "\n");
+               text_output(tf, L"\n");
            } else
                e = 0;
        }
@@ -618,39 +733,42 @@ static void text_para(FILE *fp, word *prefix, char *prefixextra, word *text,
     } else
        e = indent + extraindent;
 
-    wrapping = wrap_para(text, firstlinewidth, width, text_width);
+    wrapping = wrap_para(text, firstlinewidth, width,
+                        text_width, cfg, 0);
     for (p = wrapping; p; p = p->next) {
-       rdstringc t = { 0, 0, NULL };
-       text_rdaddwc(&t, p->begin, p->end);
-       fprintf(fp, "%*s%s\n", e, "", t.text);
+       rdstring t = { 0, 0, NULL };
+       text_rdaddw(&t, p->begin, p->end, cfg);
+       text_output_many(tf, e, L' ');
+       text_output(tf, t.text);
+       text_output(tf, L"\n");
        e = indent + extraindent;
        sfree(t.text);
     }
     wrap_free(wrapping);
-    putc('\n', fp);
+    text_output(tf, L"\n");
 }
 
-static void text_codepara(FILE *fp, word *text, int indent, int width) {
+static void text_codepara(textfile *tf, word *text, int indent, int width) {
     for (; text; text = text->next) if (text->type == word_WeakCode) {
-       char *c;
-       text_convert(text->text, &c);
-       if (strlen(c) > (size_t)width) {
-           /* FIXME: warn */
-       }
-       fprintf(fp, "%*s%s\n", indent, "", c);
-       sfree(c);
+       int wid = ustrwid(text->text, tf->charset);
+       if (wid > width)
+           error(err_text_codeline, &text->fpos, wid, width);
+       text_output_many(tf, indent, L' ');
+       text_output(tf, text->text);
+       text_output(tf, L"\n");
     }
 
-    putc('\n', fp);
+    text_output(tf, L"\n");
 }
 
-static void text_versionid(FILE *fp, word *text) {
-    rdstringc t = { 0, 0, NULL };
+static void text_versionid(textfile *tf, word *text, textconfig *cfg) {
+    rdstring t = { 0, 0, NULL };
 
-    rdaddc(&t, '[');                  /* FIXME: configurability */
-    text_rdaddwc(&t, text, NULL);
-    rdaddc(&t, ']');                  /* FIXME: configurability */
+    rdadd(&t, L'[');
+    text_rdaddw(&t, text, NULL, cfg);
+    rdadd(&t, L']');
+    rdadd(&t, L'\n');
 
-    fprintf(fp, "%s\n", t.text);
+    text_output(tf, t.text);
     sfree(t.text);
 }