#ifdef RCSID
static char RCSid[] =
"$Header: d:/cvsroot/tads/TADS2/Output.c,v 1.2 1999/05/17 02:52:12 MJRoberts Exp $";
#endif

/* 
 *   Copyright (c) 1987, 1988 by Michael J. Roberts.  All Rights Reserved.
 *   
 *   Please see the accompanying license file, LICENSE.TXT, for information
 *   on using and copying this software.  
 */
/*
Name
  output     - TADS Interpreter and Compiler formatted Output routines
Function
  Provides formatted output support.  Text that is sent through outformat()
  is displayed with word-wrap so that words fill the line but are not broken
  across lines.  Other routines allow flushing the output buffer, displaying
  blank lines, and logging output to a file as well as to the display.
Notes
  We use the global variables G_os_pagelength and G_os_linewidth, defined
  in the OS code, to determine the size of the display area and to determine
  the number of lines to display between page pauses (MORE prompts).  Line
  breaking and MORE prompting can be handled by the OS code if desired;
  the USE_MORE macro controls this setting.
Returns
  None
Modified
  04/05/92 MJRoberts     - TADS 2.0 changes
  03/29/92 MJRoberts     - fix unfound formatstring handling
  08/01/91 MJRoberts     - no more mode when debugger is running
  07/18/91 MJRoberts     - improve t_outline [more] behavior
  07/01/91 MJRoberts     - Mac porting changes
  06/05/91 MJRoberts     - add format string support
  03/27/91 MJRoberts     - debugger enhancements
  03/10/91 MJRoberts     - integrate John's qa-scripter mods
  04/24/90 MJRoberts     - add \^ (equivalent to calling "caps()" intrinsic)
  04/16/89 MJRoberts     - add outcformat for compressed strings
  10/30/88 MJRoberts     - add outhide() and outshow() functions
  10/29/88 MJRoberts     - break lines on hyphens
  12/22/87 MJRoberts     - created
*/

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>

#include "os.h"
#include "std.h"
#include "run.h"
#include "voc.h"
#include "tio.h"
#include "mcm.h"
#include "dbg.h"
#include "cmap.h"

/* 
 *   use our own isxxx - anything outside the US ASCII range is not reliably
 *   classifiable by the normal C isxxx routines 
 */
#define outissp(c) (((uchar)(c)) <= 127 && isspace((uchar)(c)))
#define outisal(c) (((uchar)(c)) <= 127 && isalpha((uchar)(c)))
#define outisdg(c) (((uchar)(c)) <= 127 && isdigit((uchar)(c)))
#define outisup(c) (((uchar)(c)) <= 127 && isupper((uchar)(c)))
#define outislo(c) (((uchar)(c)) <= 127 && islower((uchar)(c)))


/*
 *   Turn on formatter-level MORE mode, EXCEPT under any of the following
 *   conditions:
 *   
 *   - this is a MAC OS port
 *.  - this is an HTML TADS interpreter
 *.  - USE_OS_LINEWRAP is defined
 *   
 *   Formatter-level MORE mode and formatter-level line wrapping go together;
 *   you can't have one without the other.  So, if USE_OS_LINEWRAP is
 *   defined, we must also use OS-level MORE mode, which means we don't want
 *   formatter-level MORE mode.
 *   
 *   For historical reasons, we check specifically for MAC_OS.  This was the
 *   first platform for which OS-level MORE mode and OS-level line wrapping
 *   were invented; at the time, we foolishly failed to anticipate that more
 *   platforms might eventually come along with the same needs, so we coded a
 *   test for MAC_OS rather than some more abstract marker.  For
 *   compatibility, we retain this specific test.
 *   
 *   USE_OS_LINEWRAP is intended as the more abstract marker we should
 *   originally have used.  A port should #define USE_OS_LINEWRAP in its
 *   system-specific os_xxx.h header to turn on OS-level line wrapping and
 *   OS-level MORE mode.  Ports should avoid adding new #ifndef tests for
 *   specific platforms here; we've only retained the MAC_OS test because we
 *   don't want to break the existing MAC_OS port.  
 */
#ifndef MAC_OS
# ifndef USE_HTML
#  ifndef USE_OS_LINEWRAP
#   define USE_MORE                   /* activate formatter-level more-mode */
#  endif /* USE_OS_LINEWRAP */
# endif /* USE_HTML */
#endif /* MAC_OS */

/* 
 *   In HTML mode, don't use MORE mode.  Note that we explicitly turn MORE
 *   mode OFF, even though we won't have turned it on above, because it might
 *   have been turned on by an os_xxx.h header.  This is here for historical
 *   reasons; in particular, some of the HTML interpreter builds include
 *   headers that were originally written for the normal builds for those
 *   same platforms, and those original headers explicitly #define USE_MORE
 *   somewhere.  So, to be absolutely sure we get it right here, we have to
 *   explicitly turn off USE_MORE when compiling for HTML mode.  
 */
#ifdef USE_HTML
# ifdef USE_MORE
#  undef USE_MORE
# endif
#endif

/*
 *   QSPACE is the special character for a quoted space (internally, the
 *   sequence "\ " (backslash-space) is converted to QSPACE).  It must not
 *   be any printable character.  The value here may need to be changed in
 *   the extremely unlikely event that TADS is ever ported to an EBCDIC
 *   machine.
 */
#define QSPACE 26

/*
 *   QTAB is a special hard tab character indicator.  We use this when we
 *   need to generate a hard tab to send to the underlying output layer
 *   (in particular, we use this to send hard tabs to the HTML formatter
 *   when we're in HTML mode).  
 */
#define QTAB 25


/* maximum width of the display */
#define MAXWIDTH  OS_MAXWIDTH


/* ------------------------------------------------------------------------ */
/*
 *   Globals and statics.  These should really be moved into a context
 *   structure, so that the output formatter subsystem could be shared
 *   among multiple clients.  For now, there's no practical problem using
 *   statics, because we only need a single output subsystem at one time.  
 */

/* current script (command input) file */
extern osfildef *scrfp;

/*
 *   This should be TRUE if the output should have two spaces after a
 *   period (or other such punctuation. It should generally be TRUE for
 *   fixed-width fonts, and FALSE for proportional fonts.  
 */
static int doublespace = 1;

/*
 *   Log file handle and name.  If we're copying output to a log file,
 *   these will tell us about the file.  
 */
osfildef *logfp;
static char logfname[OSFNMAX];

/* flag indicating whether output has occurred since last check */
static uchar outcnt;

/* flag indicating whether hidden output has occurred */
static uchar hidout;

/* flag indicating whether to show (TRUE) or hide (FALSE) output */
static uchar outflag;

/* flag indicating whether output is hidden for debugging purposes */
int dbghid;

/*
 *   Current recursion level in formatter invocation
 */
static int G_recurse = 0;

/* active stream in current recursion level */
static struct out_stream_info *G_cur_stream;

/* watchpoint mode flag */
static uchar outwxflag;

/*
 *   User filter function.  When this function is set, we'll invoke this
 *   function for each string that's displayed through the output
 *   formatter.
 */
static objnum G_user_filter = MCMONINV;


/* ------------------------------------------------------------------------ */
/* 
 *   Hack to run with TADS 2.0 with minimal reworking.  Rather than using
 *   an allocated output layer context, store our subsystem context
 *   information in some statics.  This is less clean than using a real
 *   context, but doesn't create any practical problems as we don't need
 *   to share the output formatter subsystem among multiple simultaneous
 *   callers.  
 */
static runcxdef *runctx;                               /* execution context */
static uchar    *fmsbase;                        /* format string area base */
static uchar    *fmstop;                          /* format string area top */
static objnum    cmdActor;                                 /* current actor */

/* forward declarations of static functions */
static void outstring_stream(struct out_stream_info *stream, char *s);
static void outchar_noxlat_stream(struct out_stream_info *stream, char c);
static char out_parse_entity(char *outbuf, size_t outbuf_size,
                             char **sp, size_t *slenp);


/* ------------------------------------------------------------------------ */
/*
 *   HTML lexical analysis mode
 */
#define HTML_MODE_NORMAL  0                    /* normal text, not in a tag */
#define HTML_MODE_TAG     1                         /* parsing inside a tag */
#define HTML_MODE_SQUOTE  2           /* in a single-quoted string in a tag */
#define HTML_MODE_DQUOTE  3           /* in a double-quoted string in a tag */

/*
 *   HTML parsing mode flag for <BR> tags.  We defer these until we've
 *   read the full tag in order to obey an HEIGHT attribute we find.  When
 *   we encounter a <BR>, we figure out whether we think we'll need a
 *   flush or a blank line; if we find a HEIGHT attribute, we may change
 *   this opinion.  
 */
#define HTML_DEFER_BR_NONE   0                           /* no pending <BR> */
#define HTML_DEFER_BR_FLUSH  1                   /* only need an outflush() */
#define HTML_DEFER_BR_BLANK  2                        /* need an outblank() */

/*
 *   If we're compiling for an HTML-enabled underlying output subsystem,
 *   we want to call the underlying OS layer when switching in and out of
 *   HTML mode.  If the underlying system doesn't process HTML, we don't
 *   need to let it know anything about HTML mode. 
 */
#ifdef USE_HTML
# define out_start_html(stream) os_start_html()
# define out_end_html(stream)   os_end_html()
#else
# define out_start_html(stream)
# define out_end_html(stream)
#endif


/* ------------------------------------------------------------------------ */
/*
 *   Output formatter stream state structure.  This structure encapsulates
 *   the state of an individual output stream.  
 */
typedef struct out_stream_info out_stream_info;
struct out_stream_info
{
    /* low-level display routine (va_list version) */
    void (*do_print)(out_stream_info *stream, const char *str);
    
    /* current line position and output column */
    uchar linepos;
    uchar linecol;

    /* number of lines on the screen (since last MORE prompt) */
    int linecnt;

    /* output buffer */
    char linebuf[MAXWIDTH];

    /* 
     *   attribute buffer - we keep one attribute entry for each character in
     *   the line buffer 
     */
    int attrbuf[MAXWIDTH];

    /* current attribute for text we're buffering into linebuf */
    int cur_attr;

    /* last attribute we wrote to the osifc layer */
    int os_attr;

    /* CAPS mode - next character output is converted to upper-case */
    uchar capsflag;

    /* NOCAPS mode - next character output is converted to lower-case */
    uchar nocapsflag;

    /* ALLCAPS mode - all characters output are converted to upper-case */
    uchar allcapsflag;

    /* capture information */
    mcmcxdef *capture_ctx;           /* memory context to use for capturing */
    mcmon     capture_obj;                /* object holding captured output */
    uint      capture_ofs;                /* write offset in capture object */
    int       capturing;                 /* true -> we are capturing output */

    /* "preview" state for line flushing */
    int preview;

    /* flag indicating that we just flushed a new line */
    int just_did_nl;

    /* this output stream uses "MORE" mode */
    int use_more_mode;

    /* 
     *   This output stream uses OS-level line wrapping - if this is set,
     *   the output formatter will not insert a newline at the end of a
     *   line that it's flushing for word wrapping, but will instead let
     *   the underlying OS display layer handle the wrapping. 
     */
    int os_line_wrap;

    /*
     *   Flag indicating that the underlying output system wants to
     *   receive its output as HTML.
     *   
     *   If this is true, we'll pass through HTML to the underlying output
     *   system, and in addition generate HTML sequences for certain
     *   TADS-native escapes (for example, we'll convert the "\n" sequence
     *   to a <BR> sequence).
     *   
     *   If this is false, we'll do just the opposite: we'll remove HTML
     *   from the output stream and convert it into normal text sequences. 
     */
    int html_target;

    /*
     *   Flag indicating that the target uses plain text.  If this flag is
     *   set, we won't add the OS escape codes for highlighted characters. 
     */
    int plain_text_target;
    
    /* 
     *   Flag indicating that the caller is displaying HTML.  We always
     *   start off in text mode; the client can switch to HTML mode by
     *   displaying a special escape sequence, and can switch back to text
     *   mode by displaying a separate special escape sequence.  
     */
    int html_mode;

    /* current lexical analysis mode */
    unsigned int html_mode_flag;

    /* <BR> defer mode */
    unsigned int html_defer_br;

    /* 
     *   HTML "ignore" mode - we suppress all output when parsing the
     *   contents of a <TITLE> or <ABOUTBOX> tag 
     */
    int html_in_ignore;

    /*
     *   HTML <TITLE> mode - when we're in this mode, we're gathering the
     *   title (i.e., we're inside a <TITLE> tag's contents).  We'll copy
     *   characters to the title buffer rather than the normal output
     *   buffer, and then call os_set_title() when we reach the </TITLE>
     *   tag.  
     */
    int html_in_title;

    /* buffer for the title */
    char html_title_buf[256];

    /* pointer to next available character in title buffer */
    char *html_title_ptr;

    /* quoting level */
    int html_quote_level;

    /*
     *   Parsing mode flag for ALT attributes.  If we're parsing a tag
     *   that allows ALT, such as IMG or SOUND, we'll set this flag, then
     *   insert the ALT text if we encounter it during parsing.  
     */
    int html_allow_alt;
};

/*
 *   Default output converter.  This is the output converter for the
 *   standard display.  Functions in the public interface that do not
 *   specify an output converter will use this converter by default.  
 */
static out_stream_info G_std_disp;

/*
 *   Log file converter.  This is the output converter for a log file.
 *   Whenever we open a log file, we'll initialize this converter; as we
 *   display text to the main display, we'll also copy it to the log file.
 *   
 *   We maintain an entire separate conversion context for the log file,
 *   so that we can perform a different set of conversions on it.  We may
 *   want, for example, to pass HTML text through to the OS display
 *   subsystem (this is the case for the HTML-enabled interpreter), but
 *   we'll still want to convert log file output to text.  By keeping a
 *   separate display context for the log file, we can format output to
 *   the log file using an entirely different style than we do for the
 *   display. 
 */
static out_stream_info G_log_disp;


/* ------------------------------------------------------------------------ */
/*
 *   low-level output handlers for the standard display and log file 
 */

/* standard display printer */
static void do_std_print(out_stream_info *stream, const char *str)
{
    VARUSED(stream);
    
    /* display the text through the OS layer */
    os_printz(str);
}

/* log file printer */
static void do_log_print(out_stream_info *stream, const char *str)
{
    VARUSED(stream);

    /* display to the log file */
    if (logfp != 0 && G_os_moremode)
    {
        os_fprintz(logfp, str);
        osfflush(logfp);
    }
}


/* ------------------------------------------------------------------------ */
/* 
 *   initialize a generic output formatter state structure 
 */
static void out_state_init(out_stream_info *stream)
{
    /* start out at the first column */
    stream->linepos = 0;
    stream->linecol = 0;
    stream->linebuf[0] = '\0';

    /* set normal text attributes */
    stream->cur_attr = 0;
    stream->os_attr = 0;

    /* start out at the first line */
    stream->linecnt = 0;

    /* we're not in either "caps", "nocaps", or "allcaps" mode yet */
    stream->capsflag = stream->nocapsflag = stream->allcapsflag = FALSE;

    /* we're not capturing yet */
    stream->capturing = FALSE;
    stream->capture_obj = MCMONINV;

    /* we aren't previewing a line yet */
    stream->preview = 0;

    /* we haven't flushed a new line yet */
    stream->just_did_nl = FALSE;

    /* presume this stream does not use "MORE" mode */
    stream->use_more_mode = FALSE;

    /* presume this stream uses formatter-level line wrapping */
    stream->os_line_wrap = FALSE;

    /* assume that the underlying system is not HTML-enabled */
    stream->html_target = FALSE;

    /* presume this target accepts OS highlighting sequences */
    stream->plain_text_target = FALSE;

    /* start out in text mode */
    stream->html_mode = FALSE;

    /* start out in "normal" lexical state */
    stream->html_mode_flag = HTML_MODE_NORMAL;

    /* not in an ignored tag yet */
    stream->html_in_ignore = FALSE;

    /* not in title mode yet */
    stream->html_in_title = FALSE;

    /* not yet deferring line breaks */
    stream->html_defer_br = HTML_DEFER_BR_NONE;

    /* not yet in quotes */
    stream->html_quote_level = 0;

    /* not in an ALT tag yet */
    stream->html_allow_alt = FALSE;
}


/* ------------------------------------------------------------------------ */
/*
 *   initialize a standard display stream 
 */
static void out_init_std(out_stream_info *stream)
{
    /* there's no user output filter function yet */
    out_set_filter(MCMONINV);
    
    /* initialize the basic stream state */
    out_state_init(stream);

    /* set up the low-level output routine */
    G_std_disp.do_print = do_std_print;

#ifdef USE_MORE
    /* 
     *   We're compiled for MORE mode, and we're not compiling for an
     *   underlying HTML formatting layer, so use MORE mode for the
     *   standard display stream.  
     */
    stream->use_more_mode = TRUE;
#else
    /*
     *   We're compiled for OS-layer (or HTML-layer) MORE handling.  For
     *   this case, use OS-layer (or HTML-layer) line wrapping as well.  
     */
    stream->os_line_wrap = TRUE;
#endif

#ifdef USE_HTML
    /* 
     *   if we're compiled for HTML mode, set the standard output stream
     *   so that it knows it has an HTML target - this will ensure that
     *   HTML tags are passed through to the underlying stream, and that
     *   we generate HTML equivalents for our own control sequences 
     */
    stream->html_target = TRUE;
#endif
}

/*
 *   initialize a standard log file stream 
 */
static void out_init_log(out_stream_info *stream)
{
    /* initialize the basic stream state */
    out_state_init(stream);

    /* set up the low-level output routine */
    stream->do_print = do_log_print;

    /* use plain text in the log file stream */
    stream->plain_text_target = TRUE;
}



/* ------------------------------------------------------------------------ */
/* 
 *   table of '&' character name sequences 
 */
struct amp_tbl_t
{
    /* entity name */
    const char *cname;

    /* HTML Unicode character value */
    uint        html_cval;

    /* native character set expansion */
    char       *expan;
};

/*
 *   HTML entity mapping table.  When we're in non-HTML mode, we keep our
 *   own expansion table so that we can map HTML entity names into the
 *   local character set.
 *   
 *   The entries in this table must be in sorted order (by HTML entity
 *   name), because we use a binary search to find an entity name in the
 *   table.  
 */
static struct amp_tbl_t amp_tbl[] =
{
    { "AElig", 198, 0 },
    { "Aacute", 193, 0 },
    { "Abreve", 258, 0 },
    { "Acirc", 194, 0 },
    { "Agrave", 192, 0 },
    { "Alpha", 913, 0 },
    { "Aogon", 260, 0 },
    { "Aring", 197, 0 },
    { "Atilde", 195, 0 },
    { "Auml", 196, 0 },
    { "Beta", 914, 0 },
    { "Cacute", 262, 0 },
    { "Ccaron", 268, 0 },
    { "Ccedil", 199, 0 },
    { "Chi", 935, 0 },
    { "Dagger", 8225, 0 },
    { "Dcaron", 270, 0 },
    { "Delta", 916, 0 },
    { "Dstrok", 272, 0 },
    { "ETH", 208, 0 },
    { "Eacute", 201, 0 },
    { "Ecaron", 282, 0 },
    { "Ecirc", 202, 0 },
    { "Egrave", 200, 0 },
    { "Eogon", 280, 0 },
    { "Epsilon", 917, 0 },
    { "Eta", 919, 0 },
    { "Euml", 203, 0 },
    { "Gamma", 915, 0 },
    { "Iacute", 205, 0 },
    { "Icirc", 206, 0 },
    { "Igrave", 204, 0 },
    { "Iota", 921, 0 },
    { "Iuml", 207, 0 },
    { "Kappa", 922, 0 },
    { "Lacute", 313, 0 },
    { "Lambda", 923, 0 },
    { "Lcaron", 317, 0 },
    { "Lstrok", 321, 0 },
    { "Mu", 924, 0 },
    { "Nacute", 323, 0 },
    { "Ncaron", 327, 0 },
    { "Ntilde", 209, 0 },
    { "Nu", 925, 0 },
    { "OElig", 338, 0 },
    { "Oacute", 211, 0 },
    { "Ocirc", 212, 0 },
    { "Odblac", 336, 0 },
    { "Ograve", 210, 0 },
    { "Omega", 937, 0 },
    { "Omicron", 927, 0 },
    { "Oslash", 216, 0 },
    { "Otilde", 213, 0 },
    { "Ouml", 214, 0 },
    { "Phi", 934, 0 },
    { "Pi", 928, 0 },
    { "Prime", 8243, 0 },
    { "Psi", 936, 0 },
    { "Racute", 340, 0 },
    { "Rcaron", 344, 0 },
    { "Rho", 929, 0 },
    { "Sacute", 346, 0 },
    { "Scaron", 352, 0 },
    { "Scedil", 350, 0 },
    { "Sigma", 931, 0 },
    { "THORN", 222, 0 },
    { "Tau", 932, 0 },
    { "Tcaron", 356, 0 },
    { "Tcedil", 354, 0 },
    { "Theta", 920, 0 },
    { "Uacute", 218, 0 },
    { "Ucirc", 219, 0 },
    { "Udblac", 368, 0 },
    { "Ugrave", 217, 0 },
    { "Upsilon", 933, 0 },
    { "Uring", 366, 0 },
    { "Uuml", 220, 0 },
    { "Xi", 926, 0 },
    { "Yacute", 221, 0 },
    { "Yuml", 376, 0 },
    { "Zacute", 377, 0 },
    { "Zcaron", 381, 0 },
    { "Zdot", 379, 0 },
    { "Zeta", 918, 0 },
    { "aacute", 225, 0 },
    { "abreve", 259, 0 },
    { "acirc", 226, 0 },
    { "acute", 180, 0 },
    { "aelig", 230, 0 },
    { "agrave", 224, 0 },
    { "alefsym", 8501, 0 },
    { "alpha", 945, 0 },
    { "amp", '&', 0 },
    { "and", 8743, 0 },
    { "ang", 8736, 0 },
    { "aogon", 261, 0 },
    { "aring", 229, 0 },
    { "asymp", 8776, 0 },
    { "atilde", 227, 0 },
    { "auml", 228, 0 },
    { "bdquo", 8222, 0 },
    { "beta", 946, 0 },
    { "breve", 728, 0 },
    { "brvbar", 166, 0 },
    { "bull", 8226, 0 },
    { "cacute", 263, 0 },
    { "cap", 8745, 0 },
    { "caron", 711, 0 },
    { "ccaron", 269, 0 },
    { "ccedil", 231, 0 },
    { "cedil", 184, 0 },
    { "cent", 162, 0 },
    { "chi", 967, 0 },
    { "circ", 710, 0 },
    { "clubs", 9827, 0 },
    { "cong", 8773, 0 },
    { "copy", 169, 0 },
    { "crarr", 8629, 0 },
    { "cup", 8746, 0 },
    { "curren", 164, 0 },
    { "dArr", 8659, 0 },
    { "dagger", 8224, 0 },
    { "darr", 8595, 0 },
    { "dblac", 733, 0 },
    { "dcaron", 271, 0 },
    { "deg", 176, 0 },
    { "delta", 948, 0 },
    { "diams", 9830, 0 },
    { "divide", 247, 0 },
    { "dot", 729, 0 },
    { "dstrok", 273, 0 },
    { "eacute", 233, 0 },
    { "ecaron", 283, 0 },
    { "ecirc", 234, 0 },
    { "egrave", 232, 0 },
    { "emdash", 8212, 0 },
    { "empty", 8709, 0 },
    { "endash", 8211, 0 },
    { "eogon", 281, 0 },
    { "epsilon", 949, 0 },
    { "equiv", 8801, 0 },
    { "eta", 951, 0 },
    { "eth", 240, 0 },
    { "euml", 235, 0 },
    { "exist", 8707, 0 },
    { "fnof", 402, 0 },
    { "forall", 8704, 0 },
    { "frac12", 189, 0 },
    { "frac14", 188, 0 },
    { "frac34", 190, 0 },
    { "frasl", 8260, 0 },
    { "gamma", 947, 0 },
    { "ge", 8805, 0 },
    { "gt", '>', 0 },
    { "hArr", 8660, 0 },
    { "harr", 8596, 0 },
    { "hearts", 9829, 0 },
    { "hellip", 8230, 0 },
    { "iacute", 237, 0 },
    { "icirc", 238, 0 },
    { "iexcl", 161, 0 },
    { "igrave", 236, 0 },
    { "image", 8465, 0 },
    { "infin", 8734, 0 },
    { "int", 8747, 0 },
    { "iota", 953, 0 },
    { "iquest", 191, 0 },
    { "isin", 8712, 0 },
    { "iuml", 239, 0 },
    { "kappa", 954, 0 },
    { "lArr", 8656, 0 },
    { "lacute", 314, 0 },
    { "lambda", 955, 0 },
    { "lang", 9001, 0 },
    { "laquo", 171, 0 },
    { "larr", 8592, 0 },
    { "lcaron", 318, 0 },
    { "lceil", 8968, 0 },
    { "ldq", 8220, 0 },
    { "ldquo", 8220, 0 },
    { "le", 8804, 0 },
    { "lfloor", 8970, 0 },
    { "lowast", 8727, 0 },
    { "loz", 9674, 0 },
    { "lsaquo", 8249, 0 },
    { "lsq", 8216, 0 },
    { "lsquo", 8216, 0 },
    { "lstrok", 322, 0 },
    { "lt", '<', 0 },
    { "macr", 175, 0 },
    { "mdash", 8212, 0 },
    { "micro", 181, 0 },
    { "middot", 183, 0 },
    { "minus", 8722, 0 },
    { "mu", 956, 0 },
    { "nabla", 8711, 0 },
    { "nacute", 324, 0 },
    { "nbsp", QSPACE, 0 },
    { "ncaron", 328, 0 },
    { "ndash", 8211, 0 },
    { "ne", 8800, 0 },
    { "ni", 8715, 0 },
    { "not", 172, 0 },
    { "notin", 8713, 0 },
    { "nsub", 8836, 0 },
    { "ntilde", 241, 0 },
    { "nu", 957, 0 },
    { "oacute", 243, 0 },
    { "ocirc", 244, 0 },
    { "odblac", 337, 0 },
    { "oelig", 339, 0 },
    { "ogon", 731, 0 },
    { "ograve", 242, 0 },
    { "oline", 8254, 0 },
    { "omega", 969, 0 },
    { "omicron", 959, 0 },
    { "oplus", 8853, 0 },
    { "or", 8744, 0 },
    { "ordf", 170, 0 },
    { "ordm", 186, 0 },
    { "oslash", 248, 0 },
    { "otilde", 245, 0 },
    { "otimes", 8855, 0 },
    { "ouml", 246, 0 },
    { "para", 182, 0 },
    { "part", 8706, 0 },
    { "permil", 8240, 0 },
    { "perp", 8869, 0 },
    { "phi", 966, 0 },
    { "pi", 960, 0 },
    { "piv", 982, 0 },
    { "plusmn", 177, 0 },
    { "pound", 163, 0 },
    { "prime", 8242, 0 },
    { "prod", 8719, 0 },
    { "prop", 8733, 0 },
    { "psi", 968, 0 },
    { "quot", '"', 0 },
    { "rArr", 8658, 0 },
    { "racute", 341, 0 },
    { "radic", 8730, 0 },
    { "rang", 9002, 0 },
    { "raquo", 187, 0 },
    { "rarr", 8594, 0 },
    { "rcaron", 345, 0 },
    { "rceil", 8969, 0 },
    { "rdq", 8221, 0 },
    { "rdquo", 8221, 0 },
    { "real", 8476, 0 },
    { "reg", 174, 0 },
    { "rfloor", 8971, 0 },
    { "rho", 961, 0 },
    { "rsaquo", 8250, 0 },
    { "rsq", 8217, 0 },
    { "rsquo", 8217, 0 },
    { "sacute", 347, 0 },
    { "sbquo", 8218, 0 },
    { "scaron", 353, 0 },
    { "scedil", 351, 0 },
    { "sdot", 8901, 0 },
    { "sect", 167, 0 },
    { "shy", 173, 0 },
    { "sigma", 963, 0 },
    { "sigmaf", 962, 0 },
    { "sim", 8764, 0 },
    { "spades", 9824, 0 },
    { "sub", 8834, 0 },
    { "sube", 8838, 0 },
    { "sum", 8721, 0 },
    { "sup", 8835, 0 },
    { "sup1", 185, 0 },
    { "sup2", 178, 0 },
    { "sup3", 179, 0 },
    { "supe", 8839, 0 },
    { "szlig", 223, 0 },
    { "tau", 964, 0 },
    { "tcaron", 357, 0 },
    { "tcedil", 355, 0 },
    { "there4", 8756, 0 },
    { "theta", 952, 0 },
    { "thetasym", 977, 0 },
    { "thorn", 254, 0 },
    { "thorn", 254, 0 },
    { "tilde", 732, 0 },
    { "times", 215, 0 },
    { "trade", 8482, 0 },
    { "uArr", 8657, 0 },
    { "uacute", 250, 0 },
    { "uarr", 8593, 0 },
    { "ucirc", 251, 0 },
    { "udblac", 369, 0 },
    { "ugrave", 249, 0 },
    { "uml", 168, 0 },
    { "upsih", 978, 0 },
    { "upsilon", 965, 0 },
    { "uring", 367, 0 },
    { "uuml", 252, 0 },
    { "weierp", 8472, 0 },
    { "xi", 958, 0 },
    { "yacute", 253, 0 },
    { "yen", 165, 0 },
    { "yuml", 255, 0 },
    { "zacute", 378, 0 },
    { "zcaron", 382, 0 },
    { "zdot", 380, 0 },
    { "zeta", 950, 0 }
};


/* ------------------------------------------------------------------------ */
/*
 *   turn on CAPS mode for a stream 
 */
static void outcaps_stream(out_stream_info *stream)
{
    /* turn on CAPS mode */
    stream->capsflag = TRUE;

    /* turn off NOCAPS and ALLCAPS mode */
    stream->nocapsflag = FALSE;
    stream->allcapsflag = FALSE;
}

/*
 *   turn on NOCAPS mode for a stream 
 */
static void outnocaps_stream(out_stream_info *stream)
{
    /* turn on NOCAPS mode */
    stream->nocapsflag = TRUE;

    /* turn off CAPS and ALLCAPS mode */
    stream->capsflag = FALSE;
    stream->allcapsflag = FALSE;
}

/*
 *   turn on or off ALLCAPS mode for a stream 
 */
static void outallcaps_stream(out_stream_info *stream, int all_caps)
{
    /* set the ALLCAPS flag */
    stream->allcapsflag = all_caps;

    /* clear the CAPS and NOCAPS flags */
    stream->capsflag = FALSE;
    stream->nocapsflag = FALSE;
}

/* ------------------------------------------------------------------------ */
/*
 *   write a string to a stream 
 */
static void stream_print(out_stream_info *stream, char *str)
{
    /* call the stream's do_print method */
    (*stream->do_print)(stream, str);
}

/*
 *   Write out a line 
 */
static void t_outline(out_stream_info *stream, int nl,
                      const char *txt, const int *attr)
{
    extern int scrquiet;

    /* 
     *   Check the "script quiet" mode - this indicates that we're reading
     *   a script and not echoing output to the display.  If this mode is
     *   on, and we're writing to the display, suppress this write.  If
     *   the mode is off, or we're writing to another stream (such as the
     *   log file), show the output as normal.  
     */
    if (!scrquiet || stream != &G_std_disp)
    {
        size_t i;
        char buf[MAXWIDTH];
        char *dst;
        
        /*
         *   Check to see if we've reached the end of the screen, and if
         *   so run the MORE prompt.  Note that we don't make this check
         *   at all if USE_MORE is undefined, since this means that the OS
         *   layer code is taking responsibility for pagination issues.
         *   We also don't display a MORE prompt when reading from a
         *   script file.
         *   
         *   Note that we suppress the MORE prompt if nl == 0, since this
         *   is used to flush a partial line of text without starting a
         *   new line (for example, when displaying a prompt where the
         *   input will appear on the same line following the prompt).
         *   
         *   Skip the MORE prompt if this stream doesn't use it.  
         */
        if (stream->use_more_mode
            && scrfp == 0
            && G_os_moremode
            && nl != 0 && nl != 4
            && stream->linecnt++ >= G_os_pagelength)
        {
            /* display the MORE prompt */
            out_more_prompt();
        }

        /*
         *   Display the text.  Run through the text in pieces; each time the
         *   attributes change, set attributes at the osifc level. 
         */
        for (i = 0, dst = buf ; txt[i] != '\0' ; ++i)
        {
            /* if the attribute is changing, notify osifc */
            if (attr != 0 && attr[i] != stream->os_attr)
            {
                /* flush the preceding text */
                if (dst != buf)
                {
                    *dst = '\0';
                    stream_print(stream, buf);
                }

                /* set the new attribute */
                os_set_text_attr(attr[i]);

                /* remember this as the last OS attribute */
                stream->os_attr = attr[i];

                /* start with a fresh buffer */
                dst = buf;
            }

            /* buffer this character */
            *dst++ = txt[i];
        }

        /* flush the last chunk of text */
        if (dst != buf)
        {
            *dst = '\0';
            stream_print(stream, buf);
        }
    }
}

/* ------------------------------------------------------------------------ */
/*
 *   Flush the current line to the display.  The 'nl' argument specifies
 *   what kind of flushing to do:
 *   
 *   0: flush the current line but do not start a new line; more text will
 *   follow on the current line.  This is used, for example, to flush text
 *   after displaying a prompt and before waiting for user input.
 *   
 *   1: flush the line and start a new line.
 *   
 *   2: flush the line as though starting a new line, but don't add an
 *   actual newline character to the output, since the underlying OS
 *   display code will handle this.  Instead, add a space after the line.
 *   (This differs from mode 0 in that mode 0 shouldn't add anything at
 *   all after the line.)
 *   
 *   3: "preview" mode.  Flush the line, but do not start a new line, and
 *   retain the current text in the buffer.  This is used for systems that
 *   handle the line wrapping in the underlying system code to flush a
 *   partially filled line that will need to be flushed again later.
 *   
 *   4: same as mode 0, but used for internal buffer flushes only.  Do not
 *   involve the underlying OS layer in this type of flush - simply flush
 *   our buffers with no separation.  
 */

/* flush a given output stream */
static void outflushn_stream(out_stream_info *stream, int nl)
{
    int i;

    /* null-terminate the current output line buffer */
    stream->linebuf[stream->linepos] = '\0';

    /* note the position of the last character to display */
    i = stream->linepos - 1;

    /* if we're adding anything, remove trailing spaces */
    if (nl != 0 && nl != 4)
    {
        /* look for last non-space character */
        for ( ; i >= 0 && outissp(stream->linebuf[i]) ; --i) ;
    }

    /* check the output mode */
    if (nl == 3)
    {
        /* 
         *   this is the special "preview" mode -- only display the part
         *   that we haven't already previewed for this same line
         */
        if (i + 1 > stream->preview)
        {
            /* write out the line */
            t_outline(stream, 0, &stream->linebuf[stream->preview],
                      &stream->attrbuf[stream->preview]);

            /* skip past the part we wrote */
            stream->preview += strlen(&stream->linebuf[stream->preview]);
        }
    }
    else
    {
        char *suffix;           /* extra text to add after the flushed text */
        int   countnl = 0;         /* true if line counts for [more] paging */

        /* null-terminate the buffer at the current position */
        stream->linebuf[++i] = '\0';

        /* check the mode */
        switch(nl)
        {
        case 0:
        case 3:
        case 4:
            /* no newline - just flush out what we have with no suffix */
            suffix = 0;
            break;

        case 1:
            /* 
             *   Add a newline.  If there's nothing in the current line,
             *   or we just wrote out a newline, do not add an extra
             *   newline. 
             */
            if (stream->linecol != 0 || !stream->just_did_nl)
            {
                /* add a newline after the text */
                suffix = "\n";
                
                /* count the line in the page size */
                countnl = 1;
            }
            else
            {
                /* don't add a newline */
                suffix = 0;
            }
            break;

        case 2:
            /* 
             *   we're going to depend on the underlying OS output layer
             *   to do line breaking, so don't add a newline, but do add a
             *   space, so that the underlying OS layer knows we have a
             *   word break here 
             */
            suffix = " ";
            break;
        }

        /* 
         *   display the line, as long as we have something buffered to
         *   display; even if we don't, display it if our column is
         *   non-zero and we didn't just do a newline, since this must
         *   mean that we've flushed a partial line and are just now doing
         *   the newline 
         */
        if (stream->linebuf[stream->preview] != '\0'
            || (stream->linecol != 0 && !stream->just_did_nl))
        {
            /* write it out */
            t_outline(stream, countnl, &stream->linebuf[stream->preview],
                      &stream->attrbuf[stream->preview]);

            /* write the suffix, if any */
            if (suffix != 0)
                t_outline(stream, 0, suffix, 0);
        }

        /* generate an HTML line break if necessary */
        if (nl == 1 && stream->html_mode && stream->html_target)
            t_outline(stream, 0, "<BR HEIGHT=0>", 0);

        if (nl == 0)
        {
            /* we're not displaying a newline, so flush what we have */
            os_flush();
        }
        else
        {
            /* we displayed a newline, so reset the column position */
            stream->linecol = 0;
        }

        /* reset the line output buffer position */
        stream->linepos = stream->preview = 0;

        /* 
         *   If we just output a newline, note it.  If we didn't just
         *   output a newline, but we did write out anything else, note
         *   that we're no longer at the start of a line on the underlying
         *   output device.  
         */
        if (nl == 1)
            stream->just_did_nl = TRUE;
        else if (stream->linebuf[stream->preview] != '\0')
            stream->just_did_nl = FALSE;
    }

    /* 
     *   If the osifc-level attributes don't match the current attributes,
     *   bring the osifc layer up to date.  This is necessary in cases where
     *   we set attributes immediately before asking for input - we
     *   essentially need to flush the attributes without flushing any text.
     */
    if (stream->cur_attr != stream->os_attr)
    {
        /* set the osifc attributes */
        os_set_text_attr(stream->cur_attr);

        /* remember the new attributes as the current osifc attributes */
        stream->os_attr = stream->cur_attr;
    }
}

/* ------------------------------------------------------------------------ */
/*
 *   Determine if we're showing output.  Returns true if output should be
 *   displayed, false if it should be suppressed.  We'll note the output
 *   for hidden display accounting as needed. 
 */
static int out_is_hidden()
{
    /* check the output flag */
    if (!outflag)
    {
        /* trace the hidden output if desired */
        if (dbghid && !hidout)
            trchid();

        /* note the hidden output */
        hidout = 1;

        /* 
         *   unless we're showing hidden text in the debugger, we're
         *   suppressing output, so return true 
         */
        if (!dbghid)
            return TRUE;
    }

    /* we're not suppressing output */
    return FALSE;
}

/* ------------------------------------------------------------------------ */
/*
 *   Display a blank line to the given stream 
 */
static void outblank_stream(out_stream_info *stream)
{
    /* flush the stream */
    outflushn_stream(stream, 1);

    /* if generating for an HTML display target, add an HTML line break */
    if (stream->html_mode && stream->html_target)
        outstring_stream(stream, "<BR>");

    /* write out the newline */
    t_outline(stream, 1, "\n", 0);
}

/* ------------------------------------------------------------------------ */
/*
 *   Generate a tab for a "\t" sequence in the game text.
 *   
 *   Standard (non-HTML) version: we'll generate enough spaces to take us
 *   to the next tab stop.
 *   
 *   HTML version: if we're in native HTML mode, we'll just generate a
 *   <TAB MULTIPLE=4>; if we're not in HTML mode, we'll generate a hard
 *   tab character, which the HTML formatter will interpret as a <TAB
 *   MULTIPLE=4>.  
 */
static void outtab_stream(out_stream_info *stream)
{
    /* check to see what the underlying system is expecting */
    if (stream->html_target)
    {
        /* the underlying system is HTML - check for HTML mode */
        if (stream->html_mode)
        {
            /* we're in HTML mode, so use the HTML <TAB> tag */
            outstring_stream(stream, "<TAB MULTIPLE=4>");
        }
        else
        {
            /* we're not in HTML mode, so generate a hard tab character */
            outchar_noxlat_stream(stream, QTAB);
        }
    }
    else
    {
        int maxcol;

        /* 
         *   We're not in HTML mode - expand the tab with spaces.  Figure
         *   the maximum column: if we're doing our own line wrapping, never
         *   go beyond the actual display width. 
         */
        maxcol = (stream->os_line_wrap ? OS_MAXWIDTH : G_os_linewidth);

        /* add the spaces */
        do
        {
            stream->attrbuf[stream->linepos] = stream->cur_attr;
            stream->linebuf[stream->linepos++] = ' ';
            ++(stream->linecol);
        } while (((stream->linecol + 1) & 3) != 0
                 && stream->linecol < maxcol);
    }
}


/* ------------------------------------------------------------------------ */
/*
 *   Flush a line 
 */
static void out_flushline(out_stream_info *stream, int padding)
{
    /* 
     *   check to see if we're using the underlying display layer's line
     *   wrapping 
     */
    if (stream->os_line_wrap)
    {
        /*
         *   In the HTML version, we don't need the normal *MORE*
         *   processing, since the HTML layer will handle that.
         *   Furthermore, we don't need to provide actual newline breaks
         *   -- that happens after the HTML is parsed, so we don't have
         *   enough information here to figure out actual line breaks.
         *   So, we'll just flush out our buffer whenever it fills up, and
         *   suppress newlines.
         *   
         *   Similarly, if we have OS-level MORE processing, don't try to
         *   figure out where the line breaks go -- just flush our buffer
         *   without a trailing newline whenever the buffer is full, and
         *   let the OS layer worry about formatting lines and paragraphs.
         *   
         *   If we're using padding, use mode 2.  If we don't want padding
         *   (which is the case if we completely fill up the buffer
         *   without finding any word breaks), write out in mode 0, which
         *   just flushes the buffer exactly like it is.  
         */
        outflushn_stream(stream, padding ? 2 : 4);
    }
    else
    {
        /*
         *   Normal mode - we process the *MORE* prompt ourselves, and we
         *   are responsible for figuring out where the actual line breaks
         *   go.  Use outflush() to generate an actual newline whenever we
         *   flush out our buffer.  
         */
        outflushn_stream(stream, 1);
    }
}


/* ------------------------------------------------------------------------ */
/*
 *   Write a character to an output stream without translation
 */
static void outchar_noxlat_stream(out_stream_info *stream, char c)
{
    int  i;
    int  qspace;
    
    /* check for the special quoted space character */
    if (c == QSPACE)
    {
        /* it's a quoted space - note it and convert it to a regular space */
        qspace = 1;
        c = ' ';
    }
    else if (c == QTAB)
    {
        /* it's a hard tab - convert it to an ordinary tab */
        c = '\t';
        qspace = 0;
    }
    else
    {
        /* translate any whitespace character to a regular space character */
        if (outissp(c))
            c = ' ';
        
        /* it's not a quoted space */
        qspace = 0;
    }

    /* check for the caps/nocaps flags */
    if ((stream->capsflag || stream->allcapsflag) && outisal(c))
    {
        /* capsflag is set, so capitalize this character */
        if (outislo(c))
            c = toupper(c);

        /* okay, we've capitalized something; clear flag */
        stream->capsflag = 0;
    }
    else if (stream->nocapsflag && outisal(c))
    {
        /* nocapsflag is set, so minisculize this character */
        if (outisup(c))
            c = tolower(c);

        /* clear the flag now that we've done the job */
        stream->nocapsflag = 0;
    }

    /* if in capture mode, simply capture the character */
    if (stream->capturing)
    {
        uchar *p;

        /* if we have a valid capture object, copy to it */
        if (stream->capture_obj != MCMONINV)
        {
            /* lock the object holding the captured text */
            p = mcmlck(stream->capture_ctx, stream->capture_obj);
            
            /* make sure the capture object is big enough */
            if (mcmobjsiz(stream->capture_ctx, stream->capture_obj)
                <= stream->capture_ofs)
            {
                /* expand the object by another 256 bytes */
                p = mcmrealo(stream->capture_ctx, stream->capture_obj,
                             (ushort)(stream->capture_ofs + 256));
            }
            
            /* add this character */
            *(p + stream->capture_ofs++) = c;
            
            /* unlock the capture object */
            mcmtch(stream->capture_ctx, stream->capture_obj);
            mcmunlck(stream->capture_ctx, stream->capture_obj);
        }

        /*
         *   we're done - we don't want to actually display the character
         *   while capturing 
         */
        return;
    }

    /* add the character to out output buffer, flushing as needed */
    if (stream->linecol + 1 < G_os_linewidth)
    {
        /* 
         *   there's room for this character, so add it to the buffer 
         */
        
        /* ignore non-quoted space at start of line */
        if (outissp(c) && c != '\t' && stream->linecol == 0 && !qspace)
            return;

        /* is this a non-quoted space not at the start of the line? */
        if (outissp(c) && c != '\t' && stream->linecol != 0 && !qspace)
        {
            int  pos1 = stream->linepos - 1;
            char p = stream->linebuf[pos1];     /* check previous character */

            /* ignore repeated spaces - collapse into a single space */
            if (outissp(p))
                return;

            /*
             *   Certain punctuation requires a double space: a period, a
             *   question mark, an exclamation mark, or a colon; or any of
             *   these characters followed by any number of single and/or
             *   double quotes.  First, scan back to before any quotes, if
             *   are on one now, then check the preceding character; if
             *   it's one of the punctuation marks requiring a double
             *   space, add this space a second time.  (In addition to
             *   scanning back past quotes, scan past parentheses,
             *   brackets, and braces.)  Don't double the spacing if we're
             *   not in the normal doublespace mode; some people may
             *   prefer single spacing after punctuation, so we make this
             *   a run-time option.  
             */
            if (doublespace)
            {
                /* find the previous relevant punctuation character */
                while (pos1 &&
                       (p == '"' || p == '\'' || p == ')' || p == ']'
                        || p == '}'))
                {
                    p = stream->linebuf[--pos1];
                }
                if ( p == '.' || p == '?' || p == '!' || p == ':' )
                {
                    /* a double-space is required after this character */
                    stream->attrbuf[stream->linepos] = stream->cur_attr;
                    stream->linebuf[stream->linepos++] = c;
                    ++(stream->linecol);
                }
            }
        }

        /* add this character to the buffer */
        stream->attrbuf[stream->linepos] = stream->cur_attr;
        stream->linebuf[stream->linepos++] = c;

        /* advance the output column position */
        ++(stream->linecol);
        return;
    }

    /*
     *   The line would overflow if this character were added.  Find the
     *   most recent word break, and output the line up to the previous
     *   word.  Note that if we're trying to output a space, we'll just
     *   add it to the line buffer.  If the last character of the line
     *   buffer is already a space, we won't do anything right now.  
     */
    if (outissp(c) && c != '\t' && !qspace)
    {
        /* this is a space, so we're at a word break */
        if (stream->linebuf[stream->linepos - 1] != ' ')
        {
            stream->attrbuf[stream->linepos] = stream->cur_attr;
            stream->linebuf[stream->linepos++] = ' ';
        }
        return;
    }
    
    /*
     *   Find the most recent word break: look for a space or dash, starting
     *   at the end of the line.  
     *   
     *   If we're about to write a hyphen, we want to skip all contiguous
     *   hyphens, because we want to keep them together as a single
     *   punctuation mark; then keep going in the normal manner, which will
     *   keep the hyphens plus the word they're attached to together as a
     *   single unit.  If spaces precede the sequence of hyphens, include
     *   the prior word as well.  
     */
    i = stream->linepos - 1;
    if (c == '-')
    {
        /* skip any contiguous hyphens at the end of the line */
        for ( ; i >= 0 && stream->linebuf[i] == '-' ; --i) ;
        
        /* skip any spaces preceding the sequence of hyphens */
        for ( ; i >= 0 && outissp(stream->linebuf[i]) ; --i) ;
    }

    /* 
     *   Now find the preceding space.  If we're doing our own wrapping
     *   (i.e., we're not using OS line wrapping), then look for the
     *   nearest hyphen as well. 
     */
    for ( ; i >= 0 && !outissp(stream->linebuf[i])
          && !(!stream->os_line_wrap && stream->linebuf[i] == '-') ; --i) ;

    /* check to see if we found a good place to break */
    if (i < 0)
    {
        /* 
         *   we didn't find any good place to break - flush the entire
         *   line as-is, breaking arbitrarily in the middle of a word 
         */
        out_flushline(stream, FALSE);

        /* 
         *   we've completely cleared out the line buffer, so reset all of
         *   the line buffer counters 
         */
        stream->linepos = 0;
        stream->linecol = 0;
        stream->linebuf[0] = '\0';
    }
    else
    {
        char brkchar;
        char tmpbuf[MAXWIDTH];
        int tmpattr[MAXWIDTH];
        size_t tmpcnt;

        /* remember the word-break character */        
        brkchar = stream->linebuf[i];

        /* null-terminate the line buffer */        
        stream->linebuf[stream->linepos] = '\0';

        /* the next line starts after the break - save a copy */
        tmpcnt = strlen(&stream->linebuf[i+1]);
        memcpy(tmpbuf, &stream->linebuf[i+1], tmpcnt + 1);
        memcpy(tmpattr, &stream->attrbuf[i+1], tmpcnt * sizeof(tmpattr[0]));

        /* 
         *   terminate the buffer at the space or after the hyphen,
         *   depending on where we broke 
         */
        if (outissp(brkchar))
            stream->linebuf[i] = '\0';
        else
            stream->linebuf[i+1] = '\0';

        /* write out everything up to the word break */
        out_flushline(stream, TRUE);

        /* copy the next line into line buffer */
        memcpy(stream->linebuf, tmpbuf, tmpcnt + 1);
        memcpy(stream->attrbuf, tmpattr, tmpcnt * sizeof(tmpattr[0]));
        stream->linepos = tmpcnt;

        /* 
         *   figure what column we're now in - count all of the printable
         *   characters in the new line 
         */
        for (stream->linecol = 0, i = 0 ; i < stream->linepos ; ++i)
        {
            /* if it's printable, count it */
            if (((unsigned char)stream->linebuf[i]) >= 26)
                ++(stream->linecol);
        }
    }
    
    /* add the new character to buffer */
    stream->attrbuf[stream->linepos] = stream->cur_attr;
    stream->linebuf[stream->linepos++] = c;

    /* advance the column counter */
    ++(stream->linecol);
}

/* ------------------------------------------------------------------------ */
/*
 *   Write out a character, translating to the local system character set
 *   from the game's internal character set. 
 */
static void outchar_stream(out_stream_info *stream, char c)
{
    outchar_noxlat_stream(stream, cmap_i2n(c));
}

/* 
 *   write out a string, translating to the local system character set 
 */
static void outstring_stream(out_stream_info *stream, char *s)
{
    /* write out each character in the string */
    for ( ; *s ; ++s)
        outchar_stream(stream, *s);
}

/* 
 *   write out a string without translation 
 */
static void outstring_noxlat_stream(out_stream_info *stream, char *s)
{
    for ( ; *s ; ++s)
        outchar_noxlat_stream(stream, *s);
}


/* ------------------------------------------------------------------------ */
/*
 *   Write out an HTML character value, translating to the local character
 *   set.  
 */
static void outchar_html_stream(out_stream_info *stream,
                                unsigned int htmlchar)
{
    struct amp_tbl_t *ampptr;

    /* 
     *   search for a mapping entry for this entity, in case it's defined
     *   in an external mapping file 
     */
    for (ampptr = amp_tbl ;
         ampptr < amp_tbl + sizeof(amp_tbl)/sizeof(amp_tbl[0]) ; ++ampptr)
    {
        /* if this is the one, stop looking */
        if (ampptr->html_cval == htmlchar)
            break;
    }

    /* 
     *   If we found a mapping table entry, and the entry has an expansion
     *   from the external character mapping table file, use the external
     *   expansion; otherwise, use the default expansion.  
     */
    if (ampptr >= amp_tbl + sizeof(amp_tbl)/sizeof(amp_tbl[0])
        || ampptr->expan == 0)
    {
        char xlat_buf[50];

        /* 
         *   there's no external mapping table file expansion -- use the
         *   default OS mapping routine 
         */
        os_xlat_html4(htmlchar, xlat_buf, sizeof(xlat_buf));
        outstring_noxlat_stream(stream, xlat_buf);
    }
    else
    {
        /* 
         *   use the explicit mapping from the mapping table file 
         */
        outstring_noxlat_stream(stream, ampptr->expan);
    }
}


/* ------------------------------------------------------------------------ */
/*
 *   Enter a recursion level.  Returns TRUE if the caller should proceed
 *   with the operation, FALSE if not.
 *   
 *   If we're making a recursive call, thereby re-entering the formatter,
 *   and this stream is not the same as the enclosing stream, we want to
 *   ignore this call and suppress any output to this stream, so we'll
 *   return FALSE.  
 */
static int out_push_stream(out_stream_info *stream)
{
    /* 
     *   if we're already in the formatter, and the new stream doesn't
     *   match the enclosing recursion level's stream, tell the caller to
     *   abort the operation 
     */
    if (G_recurse != 0 && G_cur_stream != stream)
        return FALSE;
    
    /* note the active stream */
    G_cur_stream = stream;

    /* count the entry */
    ++G_recurse;

    /* tell the caller to proceed */
    return TRUE;
}

/*
 *   Leave a recursion level 
 */
static void out_pop_stream()
{
    /* count the exit */
    --G_recurse;
}

/* ------------------------------------------------------------------------ */
/*
 *   nextout() returns the next character in a string, and updates the
 *   string pointer and remaining length.  Returns zero if no more
 *   characters are available in the string.  
 */
/* static char nextout(char **s, uint *len); */
#define nextout(s, len) ((char)(*(len) == 0 ? 0 : (--(*(len)), *((*(s))++))))


/* ------------------------------------------------------------------------ */
/*
 *   display a string of a given length to a given stream 
 */
static int outformatlen_stream(out_stream_info *stream,
                               char *s, size_t slen)
{
    char     c;
    int      done = 0;
    char     fmsbuf[40];       /* space for constructing translation string */
    uint     fmslen;
    char    *f = 0;
    char    *f1;
    int      infmt = 0;

    /* 
     *   This routine can recurse because of format strings ("%xxx%"
     *   sequences).  When we recurse, we want to ensure that the
     *   recursion is directed to the original stream only.  So, note the
     *   current stream statically in case we re-enter the formatter. 
     */
    if (!out_push_stream(stream))
        return 0;

    /* get the first character */
    c = nextout(&s, &slen);

    /* if we have anything to show, show it */
    while (c != '\0')
    {
        /* check if we're collecting translation string */
        if (infmt)
        {
            /*
             *   if the string is too long for our buffer, or we've come
             *   across a backslash (illegal in a format string), or we've
             *   come across an HTML-significant character ('&' or '<') in
             *   HTML mode, we must have a stray percent sign; dump the
             *   whole string so far and act as though we have no format
             *   string 
             */
            if (c == '\\'
                || f == &fmsbuf[sizeof(fmsbuf)]
                || (stream->html_mode && (c == '<' || c == '&')))
            {
                outchar_stream(stream, '%');
                for (f1 = fmsbuf ; f1 < f ; ++f1)
                    outchar_stream(stream, *f1);
                infmt = 0;

                /* process this character again */
                continue;
            }
            else if (c == '%' && f == fmsbuf)       /* double percent sign? */
            {
                outchar_stream(stream, '%');       /* send out a single '%' */
                infmt = 0;       /* no longer processing translation string */
            }
            else if (c == '%')   /* found end of string? translate it if so */
            {
                uchar *fms;
                int    initcap = FALSE;
                int    allcaps = FALSE;
                char   fmsbuf_srch[sizeof(fmsbuf)];

                /* null-terminate the string */
                *f = '\0';

                /* check for an init cap */
                if (outisup(fmsbuf[0]))
                {
                    /* 
                     *   note the initial capital, so that we follow the
                     *   original capitalization in the substituted string 
                     */
                    initcap = TRUE;

                    /* 
                     *   if the second letter is capitalized as well,
                     *   capitalize the entire substituted string 
                     */
                    if (fmsbuf[1] != '\0' && outisup(fmsbuf[1]))
                    {
                        /* use all caps */
                        allcaps = TRUE;
                    }
                }

                /* convert the entire string to lower case for searching */
                strcpy(fmsbuf_srch, fmsbuf);
                os_strlwr(fmsbuf_srch);
                
                /* find the string in the format string table */
                fmslen = strlen(fmsbuf_srch);
                for (fms = fmsbase ; fms < fmstop ; )
                {
                    uint propnum;
                    uint len;

                    /* get the information on this entry */
                    propnum = osrp2(fms);
                    len = osrp2(fms + 2) - 2;

                    /* check for a match */
                    if (len == fmslen &&
                        !memcmp(fms + 4, fmsbuf_srch, (size_t)len))
                    {
                        int old_all_caps;

                        /* note the current ALLCAPS mode */
                        old_all_caps = stream->allcapsflag;
                        
                        /* 
                         *   we have a match - set the appropriate
                         *   capitalization mode 
                         */
                        if (allcaps)
                            outallcaps_stream(stream, TRUE);
                        else if (initcap)
                            outcaps_stream(stream);

                        /* 
                         *   evaluate the associated property to generate
                         *   the substitution text 
                         */
                        runppr(runctx, cmdActor, (prpnum)propnum, 0);

                        /* turn off ALLCAPS mode */
                        outallcaps_stream(stream, old_all_caps);

                        /* no need to look any further */
                        break;
                    }

                    /* move on to next formatstring if not yet found */
                    fms += len + 4;
                }

                /* if we can't find it, dump the format string as-is */
                if (fms == fmstop)
                {
                    outchar_stream(stream, '%');
                    for (f1 = fmsbuf ; f1 < f ; ++f1)
                        outchar_stream(stream, *f1);
                    outchar_stream(stream, '%');
                }

                /* no longer reading format string */
                infmt = 0;
            }
            else
            {
                /* copy this character of the format string */
                *f++ = c;
            }

            /* move on to the next character and continue scanning */
            c = nextout(&s, &slen);
            continue;
        }
        
        /*
         *   If we're parsing HTML here, and we're inside a tag, skip
         *   characters until we reach the end of the tag.  
         */
        if (stream->html_mode_flag != HTML_MODE_NORMAL)
        {
            switch(stream->html_mode_flag)
            {
            case HTML_MODE_TAG:
                /* 
                 *   keep skipping up to the closing '>', but note when we
                 *   enter any quoted section 
                 */
                switch(c)
                {
                case '>':
                    /* we've reached the end of the tag */
                    stream->html_mode_flag = HTML_MODE_NORMAL;

                    /* if we have a deferred <BR>, process it now */
                    switch(stream->html_defer_br)
                    {
                    case HTML_DEFER_BR_NONE:
                        /* no deferred <BR> */
                        break;

                    case HTML_DEFER_BR_FLUSH:
                        outflushn_stream(stream, 1);
                        break;

                    case HTML_DEFER_BR_BLANK:
                        outblank_stream(stream);
                        break;
                    }

                    /* no more deferred <BR> pending */
                    stream->html_defer_br = HTML_DEFER_BR_NONE;

                    /* no more ALT attribute allowed */
                    stream->html_allow_alt = FALSE;
                    break;

                case '"':
                    /* enter a double-quoted string */
                    stream->html_mode_flag = HTML_MODE_DQUOTE;
                    break;

                case '\'':
                    /* enter a single-quoted string */
                    stream->html_mode_flag = HTML_MODE_SQUOTE;
                    break;

                default:
                    /* if it's alphabetic, note the attribute name */
                    if (outisal(c))
                    {
                        char attrname[128];
                        char attrval[256];
                        char *dst;

                        /* gather up the attribute name */
                        for (dst = attrname ;
                             dst + 1 < attrname + sizeof(attrname) ; )
                        {
                            /* store this character */
                            *dst++ = c;

                            /* get the next character */
                            c = nextout(&s, &slen);

                            /* if it's not alphanumeric, stop scanning */
                            if (!outisal(c) && !outisdg(c))
                                break;
                        }

                        /* null-terminate the result */
                        *dst++ = '\0';

                        /* gather the value if present */
                        if (c == '=')
                        {
                            char qu;
                            
                            /* skip the '=' */
                            c = nextout(&s, &slen);

                            /* if we have a quote, so note */
                            if (c == '"' || c == '\'')
                            {
                                /* remember the quote */
                                qu = c;

                                /* skip it */
                                c = nextout(&s, &slen);
                            }
                            else
                            {
                                /* no quote */
                                qu = 0;
                            }

                            /* read the value */
                            for (dst = attrval ;
                                 dst + 1 < attrval + sizeof(attrval) ; )
                            {
                                /* store this character */
                                *dst++ = c;

                                /* read the next one */
                                c = nextout(&s, &slen);
                                if (c == '\0')
                                {
                                    /* 
                                     *   we've reached the end of the
                                     *   string, and we're still inside
                                     *   this attribute - abandon the
                                     *   attribute but note that we're
                                     *   inside a quoted string if
                                     *   necessary 
                                     */
                                    if (qu == '"')
                                        stream->html_mode_flag =
                                            HTML_MODE_DQUOTE;
                                    else if (qu == '\'')
                                        stream->html_mode_flag =
                                            HTML_MODE_SQUOTE;
                                    else
                                        stream->html_mode_flag
                                            = HTML_MODE_TAG;

                                    /* stop scanning the string */
                                    break;
                                }

                                /* 
                                 *   if we're looking for a quote, check
                                 *   for the closing quote; otherwise,
                                 *   check for alphanumerics 
                                 */
                                if (qu != 0)
                                {
                                    /* if this is our quote, stop scanning */
                                    if (c == qu)
                                        break;
                                }
                                else
                                {
                                    /* if it's non-alphanumeric, we're done */
                                    if (!outisal(c) && !outisdg(c))
                                        break;
                                }
                            }

                            /* skip the closing quote, if necessary */
                            if (qu != 0 && c == qu)
                                c = nextout(&s, &slen);

                            /* null-terminate the value string */
                            *dst = '\0';
                        }
                        else
                        {
                            /* no value */
                            attrval[0] = '\0';
                        }

                        /* 
                         *   see if we recognize it, and it's meaningful
                         *   in the context of the current tag 
                         */
                        if (!stricmp(attrname, "height")
                            && stream->html_defer_br != HTML_DEFER_BR_NONE)
                        {
                            int ht;
                                
                            /*
                             *   If the height is zero, always treat this
                             *   as a non-blanking flush.  If it's one,
                             *   treat it as we originally planned to.  If
                             *   it's greater than one, add n blank lines.
                             */
                            ht = atoi(attrval);
                            if (ht == 0)
                            {
                                /* always use non-blanking flush */
                                stream->html_defer_br = HTML_DEFER_BR_FLUSH;
                            }
                            else if (ht == 1)
                            {
                                /* keep original setting */
                            }
                            else
                            {
                                for ( ; ht > 0 ; --ht)
                                    outblank_stream(stream);
                            }
                        }
                        else if (!stricmp(attrname, "alt")
                                 && !stream->html_in_ignore
                                 && stream->html_allow_alt)
                        {
                            /* write out the ALT string */
                            outstring_stream(stream, attrval);
                        }

                        /* 
                         *   since we already read the next character,
                         *   simply loop back immediately 
                         */
                        continue;
                    }
                    break;
                }
                break;

            case HTML_MODE_DQUOTE:
                /* if we've reached the closing quote, return to tag state */
                if (c == '"')
                    stream->html_mode_flag = HTML_MODE_TAG;
                break;

            case HTML_MODE_SQUOTE:
                /* if we've reached the closing quote, return to tag state */
                if (c == '\'')
                    stream->html_mode_flag = HTML_MODE_TAG;
                break;
            }

            /* 
             *   move on to the next character, and start over with the
             *   new character 
             */
            c = nextout(&s, &slen);
            continue;
        }

        /*
         *   If we're in a title, and this isn't the start of a new tag,
         *   skip the character - we suppress all regular text output
         *   inside a <TITLE> ... </TITLE> sequence. 
         */
        if (stream->html_in_ignore && c != '<')
        {
            /* check for entities */
            char cbuf[50];
            if (c == '&')
            {
                /* translate the entity */
                c = out_parse_entity(cbuf, sizeof(cbuf), &s, &slen);
            }
            else
            {
                /* it's an ordinary character - copy it out literally */
                cbuf[0] = c;
                cbuf[1] = '\0';

                /* get the next character */
                c = nextout(&s, &slen);
            }

            /* 
             *   if we're gathering a title, and there's room in the title
             *   buffer for more (always leaving room for a null
             *   terminator), add this to the title buffer 
             */
            if (stream->html_in_title)
            {
                char *cbp;
                for (cbp = cbuf ; *cbp != '\0' ; ++cbp)
                {
                    /* if there's room, add it */
                    if (stream->html_title_ptr + 1 <
                        stream->html_title_buf
                        + sizeof(stream->html_title_buf))
                        *stream->html_title_ptr++ = *cbp;
                }
            }

            /* don't display anything in an ignore section */
            continue;
        }
        
        if ( c == '%' )                              /* translation string? */
        {
            infmt = 1;
            f = fmsbuf;
        }
        else if ( c == '\\' )                       /* special escape code? */
        {
            c = nextout(&s, &slen);
            
            if (stream->capturing && c != '^' && c != 'v' && c != '\0')
            {
                outchar_stream(stream, '\\');
                outchar_stream(stream, c);

                /* keep the \- and also put out the next two chars */
                if (c == '-')
                {
                    outchar_stream(stream, nextout(&s, &slen));
                    outchar_stream(stream, nextout(&s, &slen));
                }
            }
            else
            {
                switch(c)
                {
                case 'H':                                /* HTML mode entry */
                    /* turn on HTML mode in the renderer */
                    switch(c = nextout(&s, &slen))
                    {
                    case '-':
                        /* if we have an HTML target, notify it */
                        if (stream->html_target)
                        {
                            /* flush its stream */
                            outflushn_stream(stream, 0);

                            /* tell the OS layer to switch to normal mode */
                            out_end_html(stream);
                        }

                        /* switch to normal mode */
                        stream->html_mode = FALSE;
                        break;

                    case '+':
                    default:
                        /* if we have an HTML target, notify it */
                        if (stream->html_target)
                        {
                            /* flush the underlying stream */
                            outflushn_stream(stream, 0);

                            /* tell the OS layer to switch to HTML mode */
                            out_start_html(stream);
                        }
                        
                        /* switch to HTML mode */
                        stream->html_mode = TRUE;

                        /* 
                         *   if the character wasn't a "+", it's not part
                         *   of the "\H" sequence, so display it normally 
                         */
                        if (c != '+' && c != 0)
                            outchar_stream(stream, c);
                        break;
                    }

                    /* this sequence doesn't result in any actual output */
                    break;

                case 'n':                                       /* newline? */
                    outflushn_stream(stream, 1);        /* yes, output line */
                    break;
                    
                case 't':                                           /* tab? */
                    outtab_stream(stream);
                    break;
                    
                case 'b':                                    /* blank line? */
                    outblank_stream(stream);
                    break;
                    
                case '\0':                               /* line ends here? */
                    done = 1;
                    break;

                case ' ':                                   /* quoted space */
                    if (stream->html_target && stream->html_mode)
                    {
                        /* 
                         *   we're generating for an HTML target and we're
                         *   in HTML mode - generate the HTML non-breaking
                         *   space 
                         */
                        outstring_stream(stream, "&nbsp;");
                    }
                    else
                    {
                        /* 
                         *   we're not in HTML mode - generate our
                         *   internal quoted space character 
                         */
                        outchar_stream(stream, QSPACE);
                    }
                    break;

                case '^':                      /* capitalize next character */
                    stream->capsflag = 1;
                    stream->nocapsflag = 0;
                    break;

                case 'v':
                    stream->nocapsflag = 1;
                    stream->capsflag = 0;
                    break;

                case '(':
                    /* generate HTML if in the appropriate mode */
                    if (stream->html_mode && stream->html_target)
                    {
                        /* send HTML to the renderer */
                        outstring_stream(stream, "<B>");
                    }
                    else
                    {
                        /* turn on the 'hilite' attribute */
                        stream->cur_attr |= OS_ATTR_HILITE;
                    }
                    break;

                case ')':
                    /* generate HTML if in the appropriate mode */
                    if (stream->html_mode && stream->html_target)
                    {
                        /* send HTML to the renderer */
                        outstring_stream(stream, "</B>");
                    }
                    else
                    {
                        /* turn off the 'hilite' attribute */
                        stream->cur_attr &= ~OS_ATTR_HILITE;
                    }
                    break;

                case '-':
                    outchar_stream(stream, nextout(&s, &slen));
                    outchar_stream(stream, nextout(&s, &slen));
                    break;
                    
                default:                 /* just pass invalid escapes as-is */
                    outchar_stream(stream, c);
                    break;
                }
            }
        }
        else if (!stream->html_target
                 && stream->html_mode
                 && (c == '<' || c == '&'))
        {
            /*
             *   We're in HTML mode, but the underlying target does not
             *   accept HTML sequences.  It appears we're at the start of
             *   an "&" entity or a tag sequence, so parse it, remove it,
             *   and replace it (if possible) with a text-only equivalent. 
             */
            if (c == '<')
            {
                char tagbuf[50];
                char *dst;
                int is_end_tag;
                
                /* skip the opening '<' */
                c = nextout(&s, &slen);

                /* note if this is a closing tag */
                if (c == '/' || c == '\\')
                {
                    /* it's an end tag - note it and skip the slash */
                    is_end_tag = TRUE;
                    c = nextout(&s, &slen);
                }
                else
                    is_end_tag = FALSE;
                
                /* 
                 *   find the end of the tag name - the tag continues to
                 *   the next space, '>', or end of line 
                 */
                for (dst = tagbuf ; c != '\0' && c != ' ' && c != '>' ;
                     c = nextout(&s, &slen))
                {
                    /* add this to the tag buffer if it fits */
                    if (dst < tagbuf + sizeof(tagbuf) - 1)
                        *dst++ = c;
                }

                /* null-terminate the tag name */
                *dst = '\0';

                /*
                 *   Check to see if we recognize the tag.  We only
                 *   recognize a few simple tags that map easily to
                 *   character mode. 
                 */
                if (!stricmp(tagbuf, "br"))
                {
                    /* 
                     *   line break - if there's anything buffered up,
                     *   just flush the current line, otherwise write out
                     *   a blank line 
                     */
                    if (stream->html_in_ignore)
                        /* suppress breaks in ignore mode */;
                    else if (stream->linepos != 0)
                        stream->html_defer_br = HTML_DEFER_BR_FLUSH;
                    else
                        stream->html_defer_br = HTML_DEFER_BR_BLANK;
                }
                else if (!stricmp(tagbuf, "b")
                         || !stricmp(tagbuf, "i")
                         || !stricmp(tagbuf, "em")
                         || !stricmp(tagbuf, "strong"))
                {
                    int attr;
                    
                    /* choose the attribute flag */
                    switch (tagbuf[0])
                    {
                    case 'b':
                    case 'B':
                        attr = OS_ATTR_BOLD;
                        break;

                    case 'i':
                    case 'I':
                        attr = OS_ATTR_ITALIC;
                        break;

                    case 'e':
                    case 'E':
                        attr = OS_ATTR_EM;
                        break;

                    case 's':
                    case 'S':
                        attr = OS_ATTR_STRONG;
                        break;
                    }
                    
                    /* bold on/off - send out appropriate os-layer code */
                    if (stream->html_in_ignore)
                    {
                        /* suppress any change in 'ignore' mode */
                    }
                    else if (!is_end_tag)
                    {
                        /* turn on the selected attribute */
                        stream->cur_attr |= attr;
                    }
                    else
                    {
                        /* turn off the selected attribute */
                        stream->cur_attr &= ~attr;
                    }
                }
                else if (!stricmp(tagbuf, "p"))
                {
                    /* paragraph - send out a blank line */
                    if (!stream->html_in_ignore)
                        outblank_stream(stream);
                }
                else if (!stricmp(tagbuf, "tab"))
                {
                    /* tab - send out a \t */
                    if (!stream->html_in_ignore)
                        outtab_stream(stream);
                }
                else if (!stricmp(tagbuf, "img") || !stricmp(tagbuf, "sound"))
                {
                    /* IMG and SOUND - allow ALT attributes */
                    stream->html_allow_alt = TRUE;
                }
                else if (!stricmp(tagbuf, "hr"))
                {
                    int rem;
                    
                    if (!stream->html_in_ignore)
                    {
                        /* start a new line */
                        outflushn_stream(stream, 1);

                        /* write out underscores to the display width */
                        for (rem = G_os_linewidth - 1 ; rem > 0 ; )
                        {
                            char dashbuf[100];
                            int cur;
                            
                            /* do as much as we can on this pass */
                            cur = rem;
                            if ((size_t)cur > sizeof(dashbuf) - 1)
                                cur = sizeof(dashbuf) - 1;
                            
                            /* do a buffer-full of dashes */
                            memset(dashbuf, '_', cur);
                            dashbuf[cur] = '\0';
                            outstring_stream(stream, dashbuf);
                            
                            /* deduct this from the total */
                            rem -= cur;
                        }

                        /* put a blank line after the underscores */
                        outblank_stream(stream);
                    }
                }
                else if (!stricmp(tagbuf, "q"))
                {
                    unsigned int htmlchar;

                    if (!stream->html_in_ignore)
                    {
                        /* if it's an open quote, increment the level */
                        if (!is_end_tag)
                            ++(stream->html_quote_level);
                        
                        /* add the open quote */
                        htmlchar =
                            (!is_end_tag
                             ? ((stream->html_quote_level & 1) == 1
                                ? 8220 : 8216)
                             : ((stream->html_quote_level & 1) == 1
                                ? 8221 : 8217));
                        
                        /* 
                         *   write out the HTML character, translated to
                         *   the local character set 
                         */
                        outchar_html_stream(stream, htmlchar);
                        
                        /* if it's a close quote, decrement the level */
                        if (is_end_tag)
                            --(stream->html_quote_level);
                    }
                }
                else if (!stricmp(tagbuf, "title"))
                {
                    /* 
                     *   Turn ignore mode on or off as appropriate, and
                     *   turn on or off title mode as well.
                     */
                    if (is_end_tag)
                    {
                        /* 
                         *   note that we're leaving an ignore section and
                         *   a title section 
                         */
                        --(stream->html_in_ignore);
                        --(stream->html_in_title);

                        /* 
                         *   if we're no longer in a title, call the OS
                         *   layer to tell it the title string, in case it
                         *   wants to change the window title or otherwise
                         *   make use of the title 
                         */
                        if (stream->html_in_title == 0)
                        {
                            /* null-terminate the title string */
                            *stream->html_title_ptr = '\0';
                            
                            /* tell the OS about the title */
                            os_set_title(stream->html_title_buf);
                        }
                    }
                    else
                    {
                        /* 
                         *   if we aren't already in a title, set up to
                         *   capture the title into the title buffer 
                         */
                        if (!stream->html_in_title)
                            stream->html_title_ptr = stream->html_title_buf;

                        /* 
                         *   note that we're in a title and in an ignore
                         *   section, since nothing within gets displayed 
                         */
                        ++(stream->html_in_ignore);
                        ++(stream->html_in_title);
                    }
                }
                else if (!stricmp(tagbuf, "aboutbox"))
                {
                    /* turn ignore mode on or off as appropriate */
                    if (is_end_tag)
                        --(stream->html_in_ignore);
                    else
                        ++(stream->html_in_ignore);
                }

                /* suppress everything up to the next '>' */
                stream->html_mode_flag = HTML_MODE_TAG;

                /* 
                 *   continue with the current character; since we're in
                 *   html tag mode, we'll skip everything until we get to
                 *   the closing '>' 
                 */
                continue;
            }
            else if (c == '&')
            {
                /* parse it */
                char  xlat_buf[50];
                c = out_parse_entity(xlat_buf, sizeof(xlat_buf), &s, &slen);

                /* write it out (we've already translated it) */
                outstring_noxlat_stream(stream, xlat_buf);
                
                /* proceed with the next character */
                continue;
            }
        }
        else
        {
            /* normal character */
            outchar_stream(stream, c);
        }

        /* move on to the next character, unless we're finished */
        if (done)
            c = '\0';
        else
            c = nextout(&s, &slen);
    }

    /* if we ended up inside what looked like a format string, dump string */
    if (infmt)
    {
        outchar_stream(stream, '%');
        for (f1 = fmsbuf ; f1 < f ; ++f1)
            outchar_stream(stream, *f1);
    }

    /* exit a recursion level */
    out_pop_stream();

    /* success */
    return 0;
}

/* ------------------------------------------------------------------------ */
/*
 *   Parse an HTML entity markup 
 */
static char out_parse_entity(char *outbuf, size_t outbuf_size,
                             char **sp, size_t *slenp)
{
    char  ampbuf[10];
    char *dst;
    char *orig_s;
    size_t orig_slen;
    const struct amp_tbl_t *ampptr;
    size_t lo, hi, cur;
    char  c;

    /* 
     *   remember where the part after the '&' begins, so we can come back
     *   here later if necessary 
     */
    orig_s = *sp;
    orig_slen = *slenp;

    /* get the character after the ampersand */
    c = nextout(sp, slenp);

    /* if it's numeric, parse the number */
    if (c == '#')
    {
        uint val;
        
        /* skip the '#' */
        c = nextout(sp, slenp);

        /* check for hex */
        if (c == 'x' || c == 'X')
        {
            /* skip the 'x' */
            c = nextout(sp, slenp);

            /* read the hex number */
            for (val = 0 ; isxdigit((uchar)c) ; c = nextout(sp, slenp))
            {
                /* accumulate the current digit into the value */
                val *= 16;
                if (outisdg(c))
                    val += c - '0';
                else if (c >= 'a' && c <= 'f')
                    val += c - 'a' + 10;
                else
                    val += c - 'A' + 10;
            }
        }
        else
        {
            /* read the number */
            for (val = 0 ; outisdg(c) ; c = nextout(sp, slenp))
            {
                /* accumulate the current digit into the value */
                val *= 10;
                val += c - '0';
            }
        }
        
        /* if we found a ';' at the end, skip it */
        if (c == ';')
            c = nextout(sp, slenp);
        
        /* translate the character into the output buffer */
        os_xlat_html4(val, outbuf, outbuf_size);
        
        /* we're done with this character */
        return c;
    }

    /*
     *   Parse the sequence after the '&'.  Parse up to the closing
     *   semicolon, or any non-alphanumeric, or until we fill up the buffer.
     */
    for (dst = ampbuf ;
         c != '\0' && (outisdg(c) || outisal(c))
             && dst < ampbuf + sizeof(ampbuf) - 1 ;
         *dst++ = c, c = nextout(sp, slenp)) ;
    
    /* null-terminate the name */
    *dst = '\0';
    
    /* do a binary search for the name */
    lo = 0;
    hi = sizeof(amp_tbl)/sizeof(amp_tbl[0]) - 1;
    for (;;)
    {
        int diff;
        
        /* if we've converged, look no further */
        if (lo > hi || lo >= sizeof(amp_tbl)/sizeof(amp_tbl[0]))
        {
            ampptr = 0;
            break;
        }
        
        /* split the difference */
        cur = lo + (hi - lo)/2;
        ampptr = &amp_tbl[cur];
        
        /* see where we are relative to the target item */
        diff = strcmp(ampptr->cname, ampbuf);
        if (diff == 0)
        {
            /* this is it */
            break;
        }
        else if (diff > 0)
        {
            /* make sure we don't go off the end */
            if (cur == hi && cur == 0)
            {
                /* we've failed to find it */
                ampptr = 0;
                break;
            }
            
            /* this one is too high - check the lower half */
            hi = (cur == hi ? hi - 1 : cur);
        }
        else
        {
            /* this one is too low - check the upper half */
            lo = (cur == lo ? lo + 1 : cur);
        }
    }
    
    /* skip to the appropriate next character */
    if (c == ';')
    {
        /* name ended with semicolon - skip the semicolon */
        c = nextout(sp, slenp);
    }
    else if (ampptr != 0)
    {
        int skipcnt;
        
        /* found the name - skip its exact length */
        skipcnt = strlen(ampptr->cname);
        for (*sp = orig_s, *slenp = orig_slen ; skipcnt != 0 ;
             c = nextout(sp, slenp), --skipcnt) ;
    }
    
    /* if we found the entry, write out the character */
    if (ampptr != 0)
    {
        /* 
         *   if this one has an external mapping table entry, use the mapping
         *   table entry; otherwise, use the default OS routine mapping 
         */
        if (ampptr->expan != 0)
        {
            /* 
             *   we have an explicit expansion from the mapping table file -
             *   use it 
             */
            size_t copylen = strlen(ampptr->expan);
            if (copylen > outbuf_size - 1)
                copylen = outbuf_size - 1;

            memcpy(outbuf, ampptr->expan, copylen);
            outbuf[copylen] = '\0';
        }
        else
        {
            /* 
             *   there's no mapping table expansion - use the default OS code
             *   expansion 
             */
            os_xlat_html4(ampptr->html_cval, outbuf, outbuf_size);
        }
    }
    else
    {
        /* 
         *   didn't find it - output the '&' literally, then back up and
         *   output the entire sequence following 
         */
        *sp = orig_s;
        *slenp = orig_slen;
        c = nextout(sp, slenp);

        /* fill in the '&' return value */
        outbuf[0] = '&';
        outbuf[1] = '\0';
    }

    /* return the next character */
    return c;
}
                      


/* ------------------------------------------------------------------------ */
/*
 *   Initialize the output formatter 
 */
void out_init()
{
    /* not yet hiding output */
    outflag = 1;
    outcnt = 0;
    hidout = 0;

    /* initialize the standard display stream */
    out_init_std(&G_std_disp);

    /* initialize the log file stream */
    out_init_log(&G_log_disp);
}


/* ------------------------------------------------------------------------ */
/* 
 *   initialize the property translation table 
 */
void tiosetfmt(tiocxdef *ctx, runcxdef *rctx, uchar *fbase, uint flen)
{
    VARUSED(ctx);
    fmsbase = fbase;
    fmstop = fbase + flen;
    runctx = rctx;
}


/* ------------------------------------------------------------------------ */
/*
 *   Map an HTML entity to a local character value.  The character table
 *   reader will call this routine during initialization if it finds HTML
 *   entities in the mapping table file.  We'll remember these mappings
 *   for use in translating HTML entities to the local character set.
 *   
 *   Note that the standard run-time can only display a single character
 *   set, so every HTML entity that we display must be mapped to the
 *   single active native character set.  
 */
void tio_set_html_expansion(unsigned int html_char_val,
                            const char *expansion, size_t expansion_len)
{
    struct amp_tbl_t *p;

    /* find the character value */
    for (p = amp_tbl ;
         p < amp_tbl + sizeof(amp_tbl)/sizeof(amp_tbl[0]) ; ++p)
    {
        /* if this is the one, store it */
        if (p->html_cval == html_char_val)
        {
            /* allocate space for it */
            p->expan = (char *)osmalloc(expansion_len + 1);

            /* save it */
            memcpy(p->expan, expansion, expansion_len);
            p->expan[expansion_len] = '\0';

            /* no need to look any further */
            return;
        }
    }
}


/* ------------------------------------------------------------------------ */
/*
 *   Write out a c-style (null-terminated) string.
 */
int outformat(char *s)
{
    return outformatlen(s, strlen(s));
}


/* ------------------------------------------------------------------------ */
/*
 *   This routine sends out a string, one character at a time (via outchar).
 *   Escape codes ('\n', and so forth) are handled here.
 */
int outformatlen(char *s, uint slen)
{
    char     c;
    uint     orig_slen;
    char    *orig_s;
    int      ret;
    int      called_filter;

    /* presume we'll return success */
    ret = 0;

    /* presume we won't call the filter function */
    called_filter = FALSE;

    /* if there's a user filter function to invoke, call it */
    if (G_user_filter != MCMONINV)
    {
        /* push the string */
        runpstr(runctx, s, slen, 1);

        /* call the filter */
        runfn(runctx, G_user_filter, 1);

        /* 
         *   note that we called the filter, so that we'll remove the
         *   result of the filter from the stack before we return 
         */
        called_filter = TRUE;

        /* if the result is a string, use it in place of the original text */
        if (runtostyp(runctx) == DAT_SSTRING)
        {
            runsdef val;
            uchar *p;

            /* pop the value */
            runpop(runctx, &val);

            /* 
             *   get the text from the string, and use it as a replacement
             *   for the original string 
             */
            p = val.runsv.runsvstr;
            slen = osrp2(p) - 2;
            s = (char *)(p + 2);

            /* 
             *   push the string back onto the stack - this will ensure
             *   that the string stays referenced while we're working, so
             *   that the garbage collector won't delete it 
             */
            runrepush(runctx, &val);
        }
    }

    /* remember the original string, before we scan the first character */
    orig_s = s;
    orig_slen = slen;

    /* get the first character to display */
    c = nextout(&s, &slen);

    /* if the string is non-empty, note that we've displayed something */
    if (c != 0)
        outcnt = 1;

    /* check to see if we're hiding output */
    if (out_is_hidden())
        goto done;

    /* if the debugger is showing watchpoints, suppress all output */
    if (outwxflag)
        goto done;

    /* display the string */
    ret = outformatlen_stream(&G_std_disp, orig_s, orig_slen);

    /* if there's a log file, write to the log file as well */
    if (logfp != 0)
    {
        outformatlen_stream(&G_log_disp, orig_s, orig_slen);
        osfflush(logfp);
    }

done:
    /* if we called the filter, remove the result from the stack */
    if (called_filter)
        rundisc(runctx);

    /* return the result from displaying to the screen */
    return ret;
}

/* ------------------------------------------------------------------------ */
/*
 *   Display a blank line 
 */
void outblank()
{
    /* note that we've displayed something */
    outcnt = 1;

    /* check to see if we're hiding output */
    if (out_is_hidden())
        return;

    /* generate the newline to the standard display */
    outblank_stream(&G_std_disp);

    /* if we're logging, generate the newline to the log file as well */
    if (logfp != 0)
    {
        outblank_stream(&G_log_disp);
        osfflush(logfp);
    }
}


/* ------------------------------------------------------------------------ */
/*
 *   outcaps() - sets an internal flag which makes the next letter output
 *   a capital, whether it came in that way or not.  Set the same state in
 *   both formatters (standard and log).  
 */
void outcaps(void)
{
    outcaps_stream(&G_std_disp);
    outcaps_stream(&G_log_disp);
}

/*
 *   outnocaps() - sets the next letter to a miniscule, whether it came in
 *   that way or not.  
 */
void outnocaps(void)
{
    outnocaps_stream(&G_std_disp);
    outnocaps_stream(&G_log_disp);
}

/* ------------------------------------------------------------------------ */
/*
 *   Open a log file 
 */
int tiologopn(tiocxdef *ctx, char *fn)
{
    /* if there's an old log file, close it */
    if (tiologcls(ctx))
        return 1;

    /* save the filename for later */
    strcpy(logfname, fn);

    /* open the new file */
    logfp = osfopwt(fn, OSFTLOG);

    /* 
     *   Reset the log file's output formatter state, since we're opening
     *   a new file.  
     */
    out_init_log(&G_log_disp);

    /* 
     *   Set the log file's HTML source mode flag to the same value as is
     *   currently being used in the main display stream, so that it will
     *   interpret source markups the same way that the display stream is
     *   going to.  
     */
    G_log_disp.html_mode = G_std_disp.html_mode;

    /* return 0 on success, non-zero on failure */
    return (logfp == 0);
}

/*
 *   Close the log file 
 */
int tiologcls(tiocxdef *ctx)
{
    /* if we have a file, close it */
    if (logfp != 0)
    {
        /* close the handle */
        osfcls(logfp);

        /* set the system file type to "log file" */
        os_settype(logfname, OSFTLOG);

        /* forget about our log file handle */
        logfp = 0;
    }

    /* success */
    return 0;
}

/* ------------------------------------------------------------------------ */
/*
 *   Write text explicitly to the log file.  This can be used to add
 *   special text (such as prompt text) that would normally be suppressed
 *   from the log file.  When more mode is turned off, we don't
 *   automatically copy text to the log file; any text that the caller
 *   knows should be in the log file during times when more mode is turned
 *   off can be explicitly added with this function.
 *   
 *   If nl is true, we'll add a newline at the end of this text.  The
 *   caller should not include any newlines in the text being displayed
 *   here.  
 */
void out_logfile_print(char *txt, int nl)
{
    /* if there's no log file, there's nothing to do */
    if (logfp == 0)
        return;

    /* add the text */
    os_fprintz(logfp, txt);

    /* add a newline if desired */
    if (nl)
    {
        /* add a normal newline */
        os_fprintz(logfp, "\n");

        /* if the logfile is an html target, write an HTML line break */
        if (G_log_disp.html_target && G_log_disp.html_mode)
            os_fprintz(logfp, "<BR HEIGHT=0>\n");
    }

    /* flush the output */
    osfflush(logfp);
}

/* ------------------------------------------------------------------------ */
/*
 *   Set the current MORE mode 
 */
int setmore(int state)
{
    int oldstate = G_os_moremode;
    
    G_os_moremode = state;
    return oldstate;
}

/* ------------------------------------------------------------------------ */
/*
 *   Run the MORE prompt.  If the output layer takes responsibility for
 *   pagination issues (i.e., USE_MORE is defined), we'll simply display
 *   the prompt and wait for input.  Otherwise, the OS layer controls the
 *   MORE prompt, so we'll call the OS-layer function to display the
 *   prompt.  
 */
void out_more_prompt()
{
#ifdef USE_MORE
    /*
     *   USE_MORE defined - we take responsibility for pagination.  Show
     *   our default MORE prompt and wait for a keystroke.  
     */

    int done;
    int next_page;

    /* display the "MORE" prompt */
    os_printz("[More]");
    os_flush();

    /* wait for an acceptable keystroke */
    for (done = FALSE ; !done ; )
    {
        os_event_info_t evt;
        
        /* get an event */
        switch(os_get_event(0, FALSE, &evt))
        {
        case OS_EVT_KEY:
            switch(evt.key[0])
            {
            case ' ':
                /* stop waiting, show one page */
                done = TRUE;
                next_page = TRUE;
                break;
                
            case '\r':
            case '\n':
                /* stop waiting, show one line */
                done = TRUE;
                next_page = FALSE;
                break;

            default:
                /* ignore any other keystrokes */
                break;
            }
            break;

        case OS_EVT_EOF:
            /* end of file - there's nothing to wait for now */
            done = TRUE;
            next_page = TRUE;

            /* don't use more prompts any more, as the user can't respond */
            G_os_moremode = FALSE;
            break;

        default:
            /* ignore other events */
            break;
        }
    }

    /* 
     *   Remove the prompt from the screen by backing up and overwriting
     *   it with spaces.  (Note that this assumes that we're running in
     *   some kind of terminal or character mode with a fixed-pitch font;
     *   if that's not the case, the OS layer should be taking
     *   responsibility for pagination anyway, so this code shouldn't be
     *   in use in the first place.)  
     */
    os_printz("\r      \r");

    /* 
     *   if they pressed the space key, it means that we should show an
     *   entire new page, so reset the line count to zero; otherwise,
     *   we'll want to display another MORE prompt at the very next line,
     *   so leave the line count alone 
     */
    if (next_page)
        G_std_disp.linecnt = 0;

#else /* USE_MORE */

    /*
     *   USE_MORE is undefined - this means that the OS layer is taking
     *   all responsibility for pagination.  We must ask the OS layer to
     *   display the MORE prompt, because we can't make any assumptions
     *   about what the prompt looks like.  
     */

    os_more_prompt();

#endif /* USE_MORE */
}

/* ------------------------------------------------------------------------ */
/*
 *   reset output 
 */
void outreset(void)
{
    G_std_disp.linecnt = 0;
}

/* ------------------------------------------------------------------------ */
/*
 *   Determine if HTML mode is active.  Returns true if so, false if not.
 *   Note that this merely indicates whether an "\H+" sequence is
 *   currently active -- this will return true after an "\H+" sequence,
 *   even on text-only interpreters.  
 */
int tio_is_html_mode()
{
    /* return the current HTML mode flag for the standard display stream */
    return G_std_disp.html_mode;
}


/* ------------------------------------------------------------------------ */
/*
 *   Capture routines.  Capture affects only the standard display output
 *   stream; there's no need to capture information redundantly in the log
 *   file stream.  
 */

/*
 *   Begin/end capturing 
 */
void tiocapture(tiocxdef *tioctx, mcmcxdef *memctx, int flag)
{
    if (flag)
    {
        /* create a new object if necessary */
        if (G_std_disp.capture_obj == MCMONINV)
        {
            mcmalo(memctx, 256, &G_std_disp.capture_obj);
            mcmunlck(memctx, G_std_disp.capture_obj);
        }

        /* remember the memory context */
        G_std_disp.capture_ctx = memctx;
    }

    /* 
     *   remember capture status in the standard output stream as well as
     *   the log stream 
     */
    G_std_disp.capturing = flag;
    G_log_disp.capturing = flag;
}

/* clear all captured output */
void tioclrcapture(tiocxdef *tioctx)
{
    G_std_disp.capture_ofs = 0;
}

/* clear captured output back to a given size */
void tiopopcapture(tiocxdef *tioctx, uint orig_size)
{
    G_std_disp.capture_ofs = orig_size;
}

/* get the object handle of the captured output */
mcmon tiogetcapture(tiocxdef *ctx)
{
    return G_std_disp.capture_obj;
}

/* get the amount of text captured */
uint tiocapturesize(tiocxdef *ctx)
{
    return G_std_disp.capture_ofs;
}

/* ------------------------------------------------------------------------ */
/* 
 *   set the current actor 
 */
void tiosetactor(tiocxdef *ctx, objnum actor)
{
    VARUSED(ctx);
    cmdActor = actor;
}

/*
 *   get the current actor 
 */
objnum tiogetactor(tiocxdef *ctx)
{
    VARUSED(ctx);
    return cmdActor;
}
          
/* ------------------------------------------------------------------------ */
/*
 *   Flush the output line.  We'll write to both the standard display and
 *   the log file, as needed.  
 */
void outflushn(int nl)
{
    /* flush the display stream */
    outflushn_stream(&G_std_disp, nl);

    /* flush the log stream, if we have an open log file */
    if (logfp != 0)
    {
        outflushn_stream(&G_log_disp, nl);
        fflush(logfp);
    }
}

/*
 *   flush the current line, and start a new line 
 */
void outflush(void)
{
    /* use the common flushing routine in mode 1 (regular newline) */
    outflushn(1);
}

/* ------------------------------------------------------------------------ */
/*
 *   Hidden text routines
 */

/* 
 *   outhide - hide output in the standard display stream 
 */
void outhide(void)
{
    outflag = 0;
    outcnt = 0;
    hidout = 0;
}

/*
 *   Check output status.  Indicate whether output is currently hidden,
 *   and whether any hidden output has occurred.  
 */
void outstat(int *hidden, int *output_occurred)
{
    *hidden = !outflag;
    *output_occurred = outcnt;
}

/* set the flag to indicate that output has occurred */
void outsethidden(void)
{
    outcnt = 1;
    hidout = 1;
}

/*
 *   outshow() - turns output back on, and returns TRUE (1) if any output
 *   has occurred since the last outshow(), FALSE (0) otherwise.
 */
int outshow(void)
{
    /* turn output back on */
    outflag = 1;

    /* if we're debugging, note the end of hidden output */
    if (dbghid && hidout)
    {
        hidout = 0;
        trcsho();
    }

    /* return the flag indicating whether hidden output occurred */
    return outcnt;
}

/* ------------------------------------------------------------------------ */
/*
 *   start/end watchpoint evaluation - suppress all dstring output 
 */
void outwx(int flag)
{
    outwxflag = flag;
}


/* ------------------------------------------------------------------------ */
/*
 *   Set the user filter function.  Setting this to MCMONINV clears the
 *   filter. 
 */
void out_set_filter(objnum filter_fn)
{
    /* remember the filter function */
    G_user_filter = filter_fn;
}

/* ------------------------------------------------------------------------ */
/*
 *   Set the double-space mode 
 */
void out_set_doublespace(int dbl)
{
    /* remember the new setting */
    doublespace = dbl;
}

