output.t

documentation
#charset "us-ascii"
#include "advlite.h"

/*
 *   ***************************************************************************
 *   output.t
 *
 *   This module forms part of the adv3Lite library (c) 2012-24 Eric Eve, based
 *   heavily in parts on the equivalent code in adv3 (c) Micheal J. Roberts.
 */

/* ------------------------------------------------------------------------ */
/*
 *   The standard library output function.  We set this up as the default
 *   display function (for double-quoted strings and for "<< >>"
 *   embeddings).  Code can also call this directly to display items.  
 */
say(val)
{
    /* 
     *   Use the dmsg() function to pass a string value through the message substitution parameter
     *   filter and the enumTabObj to translate an enum into its string equivalent, otherwise output
     *   the value directly.
     */
    switch(dataType(val))
    {
    case TypeSString:
        dmsg(val);
        break;
    case TypeEnum:
        local str = enumTabObj.enumTab[val];
        if(str)
        {
            oSay(str);
            break;
        }
        /* Otherwise deliberate fall-through */
    default:
        oSay(val);
    }
           
           
    
    
//    if(dataType(val) == TypeSString)   
//    {       
//        dmsg(val);           
//    }    
//    else
//        oSay(val);
//
}

/* 
 *   Send a value straight to the output stream without any further message
 *   filtering.
 */
oSay(val)
{    
      outputManager.curOutputStream.writeToStream(val);
}

/* 
 *   A version of say() that avoids the cquote filter that can make havoc of
 *   some HTML strings, especially those generated by HRef (the cquote filter
 *   turns straight quotes into typographical ones, which is undesirable when
 *   straight quotes are used as part of HTML markup).
 */
htmlSay(val)
{
    try
    {
        
        /* Deactivate the cquote filter. */
        cquoteOutputFilter.deactivate();
        
        /* Display the text */
        oSay(val);
    }
    finally        
    {
        /* reactivate the cquote filter */
        cquoteOutputFilter.activate();
    }
}

/*  
 *   A version of say() that only produces output if the player can see obj (or,
 *   optionally, sense obj by some other sense passed as a canXXX method of the
 *   Query object via the prop parameter) */

senseSay(val, obj, prop = &canSee)
{
    if(Q.(prop)(gPlayerChar, obj))
        say(val);    
}


/* ------------------------------------------------------------------------ */
/*
 *   Generate a string for showing quoted text.  We simply enclose the
 *   text in a <Q>...</Q> tag sequence and return the result.  
 */
withQuotes(txt)
{
    return '<q><<txt>></q>';
}

/* ------------------------------------------------------------------------ */
/*
 *   Output Manager.  This object contains global code for displaying text
 *   on the console.
 *   
 *   The output manager is transient because we don't want its state to be
 *   saved and restored; the output manager state is essentially part of
 *   the intepreter user interface, which is not affected by save and
 *   restore.  
 */
transient outputManager: object
    /*
     *   Switch to a new active output stream.  Returns the previously
     *   active output stream, so that the caller can easily restore the
     *   old output stream if the new output stream is to be established
     *   only for a specific duration.  
     */
    setOutputStream(ostr)
    {
        local oldStr;

        /* remember the old stream for a moment */
        oldStr = curOutputStream;

        /* set the new output stream */
        curOutputStream = ostr;

        /* 
         *   return the old stream, so the caller can restore it later if
         *   desired 
         */
        return oldStr;
    }

    /* 
     *   run the given function, using the given output stream as the
     *   active default output stream 
     */
    withOutputStream(ostr, func)
    {
        /* establish the new stream */
        local oldStr = setOutputStream(ostr);

        /* make sure we restore the old active stream on the way out */
        try
        {
            /* invoke the callback */
            (func)();
        }
        finally
        {
            /* restore the old output stream */
            setOutputStream(oldStr);
        }
    }

    /* the current output stream - start with the main text stream */
    curOutputStream = mainOutputStream

    /* 
     *   Is the UI running in HTML mode?  This tells us if we have a full
     *   HTML UI or a text-only UI.  Full HTML mode applies if we're
     *   running on a Multimedia TADS interpreter, or we're using the Web
     *   UI, which runs in a separate browser and is thus inherently
     *   HTML-capable.
     *   
     *   (The result can't change during a session, since it's a function
     *   of the game and interpreter capabilities, so we store the result
     *   on the first evaluation to avoid having to recompute it on each
     *   query.  Since 'self' is a static object, we'll recompute this each
     *   time we run the program, which is important because we could save
     *   the game on one interpreter and resume the session on a different
     *   interpreter with different capabilities.)  
     */
    htmlMode = (self.htmlMode = checkHtmlMode())
;

/* ------------------------------------------------------------------------ */
/*
 *   Output Stream.  This class provides a stream-oriented interface to
 *   displaying text on the console.  "Stream-oriented" means that we write
 *   text as a sequential string of characters.
 *   
 *   Output streams are always transient, since they track the system user
 *   interface in the interpreter.  The interpreter does not save its UI
 *   state with a saved position, so objects such as output streams that
 *   track the UI state should not be saved either.  
 */
class OutputStream: PreinitObject
    /*
     *   Write a value to the stream.  If the value is a string, we'll
     *   display the text of the string; if it's nil, we'll ignore it; if
     *   it's anything else, we'll try to convert it to a string (with the
     *   toString() function) and display the resulting text.  
     */
    writeToStream(val)
    {
        
        /* if we have any prefix text, output it first */
        if(prefix != nil)
        {
            writeFromStream(prefix);
            prefix = nil;
        }
        
        /* convert the value to a string */
        switch(dataType(val))
        {
        case TypeSString:
            /* 
             *   it's a string - no conversion is needed, but if it's
             *   empty, it doesn't count as real output (so don't notify
             *   anyone, and don't set any output flags) 
             */
            if (val == '')
                return;
            break;
            
        case TypeNil:
        case TypeTrue:    
            /* nil or true - don't display anything for this */
            return;
            
        case TypeInt:
        case TypeObject:
            /* convert integers and objects to strings */
            val = toString(val);
            break;
        }

        /* run it through our output filters */
        val = applyFilters(val);

        /* 
         *   if, after filtering, we're not writing anything at all,
         *   there's nothing left to do 
         */
        if (val == nil || val == '')
            return;

        /* write the text to our underlying system stream */
        writeFromStream(val);
    }

    /*
     *   Watch the stream for output.  It's sometimes useful to be able to
     *   call out to some code and determine whether or not the code
     *   generated any text output.  This routine invokes the given
     *   callback function, monitoring the stream for output; if any
     *   occurs, we'll return true, otherwise we'll return nil.  
     */
    watchForOutput(func)
    {
        local mon;
        
        /* set up a monitor filter on the stream */
        addOutputFilter(mon = new MonitorFilter());

        /* catch any exceptions so we can remove our filter before leaving */
        try
        {
            /* invoke the callback */
            (func)();

            /* return the monitor's status, indicating if output occurred */
            return mon.outputFlag;
        }
        
        catch(ExitSignal ex)
        {
            return mon.outputFlag;
        }
        
        catch(ExitActionSignal ex)
        {
            return mon.outputFlag;
        }
        
        catch(AbortActionSignal ex)
        {
            return mon.outputFlag;
        }
        finally
        {
            /* remove our monitor filter */
            removeOutputFilter(mon);
        }
    }

    /*
     *   Call the given function, capturing all text output to this stream
     *   in the course of the function call.  Return a string containing
     *   the captured text.  
     */
    captureOutput(func, [args])
    {
        /* install a string capture filter */
        local filter = new StringCaptureFilter();
        addOutputFilter(filter);

        /* make sure we don't leave without removing our capturer */
        try
        {
            /* invoke the function */
            (func)(args...);

            /* return the text that we captured */
            return filter.txt_;
        }
                     
        
        finally
        {
            /* we're done with our filter, so remove it */
            removeOutputFilter(filter);
        }
    }
    
    /* 
     *   A Version of captureOutput that ignores an Exit Exception. This can be
     *   used to attempt to retrieve the string value of an output filter that
     *   threw an exit exeption.
     */
    captureOutputIgnoreExit(func, [args])    
    {
        /* install a string capture filter */
        local filter = new StringCaptureFilter();
        addOutputFilter(filter);

        /* make sure we don't leave without removing our capturer */
        try
        {
            /* invoke the function */
            (func)(args...);

            /* return the text that we captured */
            return filter.txt_;
        }
              
        catch(ExitSignal ex)
        {
            return filter.txt_;
        }
        
        catch(ExitActionSignal ex)
        {
            return filter.txt_;
        }
        
        catch(AbortActionSignal ex)
        {
            return filter.txt_;
        }
        
        finally
        {
            /* we're done with our filter, so remove it */
            removeOutputFilter(filter);
        }
    }
    
    

    /* my associated input manager, if I have one */
    myInputManager = nil

    /* dynamic construction */
    construct()
    {
        /* 
         *   Set up filter list.  Output streams are always transient, so
         *   make our filter list transient as well.  
         */
        filterList_ = new transient Vector(10);
    }

    /* execute pre-initialization */
    execute()
    {
        /* do the same set-up we would do for dynamic construction */
        construct();
    }

    /*
     *   Write text out from this stream; this writes to the lower-level
     *   stream underlying this stream.  This routine is intended to be
     *   called only from within this class.
     *   
     *   Each output stream is conceptually "stacked" on top of another,
     *   lower-level stream.  At the bottom of the stack is usually some
     *   kind of physical device, such as the display, or a file on disk.
     *   
     *   This method must be defined in each subclass to write to the
     *   appropriate underlying stream.  Most subclasses are specifically
     *   designed to sit atop a system-level stream, such as the display
     *   output stream, so most implementations of this method will call
     *   directly to a system-level output function.
     */
    writeFromStream(txt) { }

    /* 
     *   The list of active filters on this stream, in the order in which
     *   they are to be called.  This should normally be initialized to a
     *   Vector in each instance.  
     */
    filterList_ = []

    /*
     *   Add an output filter.  The argument is an object of class
     *   OutputFilter, or any object implementing the filterText() method.
     *   
     *   Filters are always arranged in a "stack": the last output filter
     *   added is the first one called during output.  This method thus
     *   adds the new filter at the "top" of the stack.  
     */
    addOutputFilter(filter)
    {
        /* add the filter to the end of our list */
        filterList_.append(filter);
    }

    /*
     *   Add an output filter at a given point in the filter stack: add
     *   the filter so that it is "below" the given existing filter in the
     *   stack.  This means that the new filter will be called just after
     *   the existing filter during output.
     *   
     *   If 'existingFilter' isn't in the stack of existing filters, we'll
     *   add the new filter at the "top" of the stack.
     */
    addOutputFilterBelow(newFilter, existingFilter)
    {
        /* find the existing filter in our list */
        local idx = filterList_.indexOf(existingFilter);

        /* 
         *   If we found the old filter, add the new filter below the
         *   existing filter in the stack, which is to say just before the
         *   old filter in our vector of filters (since we call the
         *   filters in reverse order of the list).
         *   
         *   If we didn't find the existing filter, simply add the new
         *   filter at the top of the stack, by appending the new filter
         *   at the end of the list.  
         */
        if (idx != nil)
            filterList_.insertAt(idx, newFilter);
        else
            filterList_.append(newFilter);
    }

    /*
     *   Remove an output filter.  Since filters are arranged in a stack,
     *   only the LAST output filter added may be removed.  It's an error
     *   to remove a filter other than the last one.  
     */
    removeOutputFilter(filter)
    {
        /* get the filter count */
        local len = filterList_.length();

        /* make sure it's the last filter */
        if (len == 0 || filterList_[len] != filter)
            t3DebugTrace(T3DebugBreak);

        /* remove the filter from my list */
        filterList_.removeElementAt(len);
    }

    /* call the filters */
    applyFilters(val)
    {
        /* 
         *   Run through the list, applying each filter in turn.  We work
         *   backwards through the list from the last element, because the
         *   filter list is a stack: the last element added is the topmost
         *   element of the stack, so it must be called first.  
         */
        for (local i in filterList_.length()..1 step -1 ; val != nil ; )
            val = filterList_[i].filterText(self, val);

        /* return the result of all of the filters */
        return val;
    }

    /* 
     *   Apply the current set of text transformation filters to a string.
     *   This applies only the non-capturing filters; we skip any capture
     *   filters.  
     */
    applyTextFilters(val)
    {
        /* run through the filter stack from top to bottom */
        for (local i in filterList_.length()..1 step -1 ; val != nil ; )
        {
            /* skip capturing filters */
            local f = filterList_[i];
            if (f.ofKind(CaptureFilter))
                continue;

            /* apply the filter */
            val = f.filterText(self, val);
        }

        /* return the result */
        return val;
    }
        

    /*
     *   Receive notification from the input manager that we have just
     *   ended reading a line of input from the keyboard.
     */
    inputLineEnd()
    {
        /* an input line ending doesn't look like a paragraph */
        justDidPara = nil;
    }

    /* 
     *   Internal state: we just wrote a paragraph break, and there has
     *   not yet been any intervening text.  By default, we set this to
     *   true initially, so that we suppress any paragraph breaks at the
     *   very start of the text.  
     */
    justDidPara = true

    /*
     *   Internal state: we just wrote a character that suppresses
     *   paragraph breaks that immediately follow.  In this state, we'll
     *   suppress any paragraph marker that immediately follows, but we
     *   won't suppress any other characters.  
     */
    justDidParaSuppressor = nil
    
    /*  Text to be output before anything else */
    prefix = nil
    
    /* Set the prefix to txt */
    setPrefix(txt)
    {
        prefix = txt;
    }
;

/*
 *   The OutputStream for the main text area.
 *   
 *   This object is transient because the output stream state is
 *   effectively part of the interpreter user interface, which is not
 *   affected by save and restore.  
 */
transient mainOutputStream: OutputStream
    /* 
     *   The main text area is the same place where we normally read
     *   command lines from the keyboard, so associate this output stream
     *   with the primary input manager. 
     */
    myInputManager = inputManager

    /* the current command transcript */
    curTranscript = nil

    /* we sit atop the system-level main console output stream */
    writeFromStream(txt)
    {
        /* write the text to the console */
        aioSay(txt);
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Paragraph manager.  We filter strings as they're about to be sent to
 *   the console to convert paragraph markers (represented in the source
 *   text using the "style tag" format, <.P>) into a configurable display
 *   rendering.
 *   
 *   We also process the zero-spacing paragraph, <.P0>.  This doesn't
 *   generate any output, but otherwise acts like a paragraph break in that
 *   it suppresses any paragraph breaks that immediately follow.
 *   
 *   The special marker <./P0> cancels the effect of a <.P0>.  This can be
 *   used if you want to ensure that a newline or paragraph break is
 *   displayed, even if a <.P0> was just displayed.
 *   
 *   Our special processing ensures that paragraph tags interact with one
 *   another and with other display elements specially:
 *   
 *   - A run of multiple consecutive paragraph tags is treated as a single
 *   paragraph tag.  This property is particularly important because it
 *   allows code to write out a paragraph marker without having to worry
 *   about whether preceding code or following code add paragraph markers
 *   of their own; if redundant markers are found, we'll filter them out
 *   automatically.
 *   
 *   - We can suppress paragraph markers following other specific
 *   sequences.  For example, if the paragraph break is rendered as a blank
 *   line, we might want to suppress an extra blank line for a paragraph
 *   break after an explicit blank line.
 *   
 *   - We can suppress other specific sequences following a paragraph
 *   marker.  For example, if the paragraph break is rendered as a newline
 *   plus a tab, we could suppress whitespace following the paragraph
 *   break.
 *   
 *   The paragraph manager should always be instantiated with transient
 *   instances, because this object's state is effectively part of the
 *   interpreter user interface, which doesn't participate in save and
 *   restore.  
 */
class ParagraphManager: OutputFilter
    /* 
     *   Rendering - this is what we display on the console to represent a
     *   paragraph break.  By default, we'll display a blank line.  
     */
    renderText = '\b'

    /*
     *   Flag: show or hide paragraph breaks immediately after input.  By
     *   default, we do not show paragraph breaks after an input line.  
     */
    renderAfterInput = nil

    /*
     *   Preceding suppression.  This is a regular expression that we
     *   match to individual characters.  If the character immediately
     *   preceding a paragraph marker matches this expression, we'll
     *   suppress the paragraph marker in the output.  By default, we'll
     *   suppress a paragraph break following a blank line, because the
     *   default rendering would add a redundant blank line.  
     */
    suppressBefore = static new RexPattern('\b')

    /*
     *   Following suppression.  This is a regular expression that we
     *   match to individual characters.  If the character immediately
     *   following a paragraph marker matches this expression, we'll
     *   suppress the character.  We'll apply this to each character
     *   following a paragraph marker in turn until we find one that does
     *   not match; we'll suppress all of the characters that do match.
     *   By default, we suppress additional blank lines after a paragraph
     *   break.  
     */
    suppressAfter = static new RexPattern('[\b\n]')

    /* pre-compile some regular expression patterns we use a lot */
    leadingMultiPat = static new RexPattern('(<langle><dot>[pP]0?<rangle>)+')
    leadingSinglePat = static new RexPattern(
        '<langle><dot>([pP]0?|/[pP]0)<rangle>')

    /* process a string that's about to be written to the console */
    filterText(ostr, txt)
    {
        local ret;
        
        /* we don't have anything in our translated string yet */
        ret = '';

        /* keep going until we run out of string to process */
        while (txt != '')
        {
            local len;
            local match;
            local p0;
            local unp0;
            
            /* 
             *   if we just wrote a paragraph break, suppress any
             *   character that matches 'suppressAfter', and suppress any
             *   paragraph markers that immediately follow 
             */
            if (ostr.justDidPara)
            {
                /* check for any consecutive paragraph markers */
                if ((len = rexMatch(leadingMultiPat, txt)) != nil)
                {
                    /* discard the consecutive <.P>'s, and keep going */
                    txt = txt.substr(len + 1);
                    continue;
                }

                /* check for a match to the suppressAfter pattern */
                if (rexMatch(suppressAfter, txt) != nil)
                {
                    /* discard the suppressed character and keep going */
                    txt = txt.substr(2);
                    continue;
                }
            }

            /* 
             *   we have a character other than a paragraph marker, so we
             *   didn't just scan a paragraph marker 
             */
            ostr.justDidPara = nil;

            /*
             *   if we just wrote a suppressBefore character, discard any
             *   leading paragraph markers 
             */
            if (ostr.justDidParaSuppressor
                && (len = rexMatch(leadingMultiPat, txt)) != nil)
            {
                /* remove the paragraph markers */
                txt = txt.substr(len + 1);

                /* 
                 *   even though we're not rendering the paragraph, note
                 *   that a logical paragraph just started 
                 */
                ostr.justDidPara = true;

                /* keep going */
                continue;
            }

            /* presume we won't find a <.p0> or <./p0> */
            p0 = unp0 = nil;

            /* find the next paragraph marker */
            match = rexSearch(leadingSinglePat, txt);
            if (match == nil)
            {
                /* 
                 *   there are no more paragraph markers - copy the
                 *   remainder of the input string to the output
                 */
                ret += txt;
                txt = '';

                /* we just did something other than a paragraph */
                ostr.justDidPara = nil;
            }
            else
            {
                /* add everything up to the paragraph break to the output */
                ret += txt.substr(1, match[1] - 1);

                /* get the rest of the string following the paragraph mark */
                txt = txt.substr(match[1] + match[2]);

                /* note if we found a <.p0> or <./p0> */
                p0 = (match[3] is in ('<.p0>', '<.P0>'));
                unp0 = (match[3] is in ('<./p0>', '<./P0>'));

                /* 
                 *   note that we just found a paragraph marker, unless
                 *   this is a <./p0> 
                 */
                ostr.justDidPara = !unp0;
            }

            /* 
             *   If the last character we copied out is a suppressBefore
             *   character, note for next time that we have a suppressor
             *   pending.  Likewise, if we found a <.p0> rather than a
             *   <.p>, this counts as a suppressor.  
             */
            ostr.justDidParaSuppressor =
                (p0 || rexMatch(suppressBefore,
                                ret.substr(ret.length(), 1)) != nil);

            /* 
             *   if we found a paragraph marker, and we didn't find a
             *   leading suppressor character just before it, add the
             *   paragraph rendering 
             */
            if (ostr.justDidPara && !ostr.justDidParaSuppressor)
                ret += renderText;
        }

        /* return the translated string */
        return ret;
    }
;

/* the paragraph manager for the main output stream */
transient mainParagraphManager: ParagraphManager
;

/* ------------------------------------------------------------------------ */
/*
 *   Output Filter
 */
class OutputFilter: object
    /* 
     *   Apply the filter - this should be overridden in each filter.  The
     *   return value is the result of filtering the string.
     *   
     *   'ostr' is the OutputStream to which the text is being written,
     *   and 'txt' is the original text to be displayed.  
     */
    filterText(ostr, txt) { return txt; }
;


/* ------------------------------------------------------------------------ */
/*
 *   Output monitor filter.  This is a filter that leaves the filtered
 *   text unchanged, but keeps track of whether any text was seen at all.
 *   Our 'outputFlag' is true if we've seen any output, nil if not.
 */
class MonitorFilter: OutputFilter
    /* filter text */
    filterText(ostr, val)
    {
        /* if the value is non-empty, note the output */
        if (val != nil && val != '')
            outputFlag = true;

        /* return the input value unchanged */
        return val;
    }

    /* flag: has any output occurred for this monitor yet? */
    outputFlag = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Capture Filter.  This is an output filter that simply captures all of
 *   the text sent through the filter, sending nothing out to the
 *   underlying stream.
 *   
 *   The default implementation simply discards the incoming text.
 *   Subclasses can keep track of the text in memory, in a file, or
 *   wherever desired.  
 */
class CaptureFilter: OutputFilter
    /*
     *   Filter the text.  We simply discard the text, passing nothing
     *   through to the underlying stream. 
     */
    filterText(ostr, txt)
    {
        /* leave nothing for the underlying stream */
        return nil;
    }
;

/*
 *   "Switchable" capture filter.  This filter can have its blocking
 *   enabled or disabled.  When blocking is enabled, we capture
 *   everything, leaving nothing to the underlying stream; when disabled,
 *   we pass everything through to the underyling stream unchanged.  
 */
class SwitchableCaptureFilter: CaptureFilter
    /* filter the text */
    filterText(ostr, txt)
    {
        /* 
         *   if we're blocking output, return nothing to the underlying
         *   stream; if we're disabled, return the input unchanged 
         */
        return (isBlocking ? nil : txt);
    }

    /*
     *   Blocking enabled: if this is true, we'll capture all text passed
     *   through us, leaving nothing to the underyling stream.  Blocking
     *   is enabled by default.  
     */
    isBlocking = true
;

/*
 *   String capturer.  This is an implementation of CaptureFilter that
 *   saves the captured text to a string.  
 */
class StringCaptureFilter: CaptureFilter
    /* filter text */
    filterText(ostr, txt)
    {        
        /* add the text to my captured text so far */
        addText(txt);
    }

    /* add to my captured text */
    addText(txt)
    {
        /* append the text to my string of captured text */
        txt_ += txt;
    }

    /* my captured text so far */
    txt_ = ''
;

/* 
 *   ImplicitAction announcement filter. This is applied just before text from
 *   an action routine is output to ensure than any pending implicit action
 *   reports are output before any text from the action routine itself.
 */
class ImplicitActionFilter: OutputFilter
    
    filterText(ostr, txt)    
    {      
        /* 
         *   This method should never be called if we don't have a current
         *   action, but just in case we return the text unchanged if there is
         *   no gAction.
         */
        if(gAction == nil)
            return txt;
        
        /* 
         *   If we do have have a gAction, prepend any pending implicit action
         *   announcements to the text we output.
         */
        return gAction.buildImplicitActionAnnouncement(true, !gAction.isImplicit)
            + txt;
    }   
;


/* ------------------------------------------------------------------------ */
/*
 *   Style tag.  This defines an HTML-like tag that can be used in output
 *   text to display an author-customizable substitution string.
 *   
 *   Each StyleTag object defines the name of the tag, which can be
 *   invoked in output text using the syntax "<.name>" - we require the
 *   period after the opening angle-bracket to plainly distinguish the
 *   sequence as a style tag, not a regular HTML tag.
 *   
 *   Each StyleTag also defines the text string that should be substituted
 *   for each occurrence of the "<.name>" sequence in output text, and,
 *   optionally, another string that is substituted for occurrences of the
 *   "closing" version of the tag, invoked with the syntax "<./name>".  
 */
class StyleTag: object
    /* name of the tag - the tag appears in source text in <.xxx> notation */
    tagName = ''

    /* 
     *   opening text - this is substituted for each instance of the tag
     *   without a '/' prefix 
     */
    openText = ''

    /* 
     *   Closing text - this is substituted for each instance of the tag
     *   with a '/' prefix (<./xxx>).  Note that non-container tags don't
     *   have closing text at all.  
     */
    closeText = ''
;

/*
 *   HtmlStyleTag - this is a subclass of StyleTag that provides different
 *   rendering depending on whether the interpreter is in HTML mode or not.
 *   In HTML mode, we display our htmlOpenText and htmlCloseText; when not
 *   in HTML mode, we display our plainOpenText and plainCloseText.
 */
class HtmlStyleTag: StyleTag
    openText = (outputManager.htmlMode ? htmlOpenText : plainOpenText)

    closeText = (outputManager.htmlMode ? htmlCloseText : plainCloseText)

    /* our HTML-mode opening and closing text */
    htmlOpenText = ''
    htmlCloseText = ''

    /* our plain (non-HTML) opening and closing text */
    plainOpenText = ''
    plainCloseText = ''
;

/*
 *   Define our default style tags.  We name all of these StyleTag objects
 *   so that authors can easily change the expansion text strings at
 *   compile-time with the 'modify' syntax, or dynamically at run-time by
 *   assigning new strings to the appropriate properties of these objects.
 */

/* 
 *   <.roomname> - we use this to display the room's name in the
 *   description of a room (such as in a LOOK AROUND command, or when
 *   entering a new location).  By default, we display the room name in
 *   boldface on a line by itself.  
 */
roomnameStyleTag: StyleTag 'roomname' '\n<b>' '</b><br>\n';

/* <.roomdesc> - we use this to display a room's long description */
roomdescStyleTag: StyleTag 'roomdesc' '' '';

/* <.roomcontents> - we use this to display a room's contents */
roomcontentsStyleTag: StyleTag 'roomcontents' '' '';

/* 
 *   <.roompara> - we use this to separate paragraphs within a room's long
 *   description 
 */
roomparaStyleTag: StyleTag 'roompara' '<.p>\n';

/* 
 *   <.inputline> - we use this to display the text actually entered by the
 *   user on a command line.  Note that this isn't used for the prompt text
 *   - it's used only for the command-line text itself.  
 */
inputlineStyleTag: HtmlStyleTag 'inputline'
    /* in HTML mode, switch in and out of TADS-Input font */
    htmlOpenText = '<font face="tads-input">'
    htmlCloseText = '</font>'

    /* in plain mode, do nothing */
    plainOpenText = ''
    plainCloseText = ''
;

/*
 *   <.a> (named in analogy to the HTML <a> tag) - we use this to display
 *   hyperlinked text.  Note that this goes *inside* an HTML <a> tag - this
 *   doesn't do the actual linking (the true <a> tag does that), but rather
 *   allows customized text formatting for hyperlinked text.  
 */
hyperlinkStyleTag: HtmlStyleTag 'a'
;

/* <.statusroom> - style for the room name in a status line */
statusroomStyleTag: HtmlStyleTag 'statusroom'
    htmlOpenText = '<b>'
    htmlCloseText = '</b>'
;

/* <.statusscore> - style for the score in a status line */
statusscoreStyleTag: HtmlStyleTag 'statusscore'
    htmlOpenText = '<i>'
    htmlCloseText = '</i>'
;

/* 
 *   <.parser> - style for messages explicitly from the parser.
 *   
 *   By default, we do nothing special with these messages.  Many games
 *   like to use a distinctive notation for parser messages, to make it
 *   clear that the messages are "meta" text that's not part of the story
 *   but rather specific to the game mechanics; one common convention is
 *   to put parser messages in [square brackets].
 *   
 *   If the game defines a special appearance for parser messages, for
 *   consistency it might want to use the same appearance for notification
 *   messages displayed with the <.notification> tag (see
 *   notificationStyleTag).  
 */
parserStyleTag: StyleTag 'parser'
    openText = ''
    closeText = ''
;

/* 
 *   <.notification> - style for "notification" messages, such as score
 *   changes and messages explaining how facilities (footnotes, exit
 *   lists) work the first time they come up.
 *   
 *   By default, we'll put notifications in parentheses.  Games that use
 *   [square brackets] for parser messages (i.e., for the <.parser> tag)
 *   might want to use the same notation here for consistency.  
 */
notificationStyleTag: StyleTag 'notification'
    openText = '('
    closeText = ')'
;

/*
 *   <.assume> - style for "assumption" messages, showing an assumption
 *   the parser is making.  This style is used for showing objects used by
 *   default when not specified in a command, objects that the parser
 *   chose despite some ambiguity, and implied commands.  
 */
assumeStyleTag: StyleTag 'assume'
    openText = '('
    closeText = ')'
;

/*
 *   <.announceObj> - style for object announcement messages.  The parser
 *   shows an object announcement for each object when a command is applied
 *   to multiple objects (TAKE ALL, DROP KEYS AND WALLET).  The
 *   announcement simply shows the object's name and a colon, to let the
 *   player know that the response text that follows applies to the
 *   announced object.  
 */
announceObjStyleTag: StyleTag 'announceObj'
    openText = '<b>'
    closeText = '</b>'
;





/* ------------------------------------------------------------------------ */
/*
 *   "Style tag" filter.  This is an output filter that expands our
 *   special style tags in output text.  
 */
styleTagFilter: OutputFilter, PreinitObject
    /* pre-compile our frequently-used tag search pattern */
    tagPattern = static new RexPattern(
        '<nocase><langle>%.(/?[a-z][a-z0-9]*)<rangle>')

    /* filter for a style tag */
    filterText(ostr, val)
    {
        local idx;
        
        /* search for our special '<.xxx>' tags, and expand any we find */
        idx = rexSearch(tagPattern, val);
        while (idx != nil)
        {
            local xlat;
            local afterOfs;
            local afterStr;
            
            /* ask the formatter to translate it */
            xlat = translateTag(rexGroup(1)[3]);

            /* get the part of the string that follows the tag */
            afterOfs = idx[1] + idx[2];
            afterStr = val.substr(idx[1] + idx[2]);

            /* 
             *   if we got a translation, replace it; otherwise, leave the
             *   original text intact 
             */
            if (xlat != nil)
            {
                /* replace the tag with its translation */
                val = val.substr(1, idx[1] - 1) + xlat + afterStr;

                /* 
                 *   figure the offset of the remainder of the string in
                 *   the replaced version of the string - this is the
                 *   length of the original part up to the replacement
                 *   text plus the length of the replacement text 
                 */
                afterOfs = idx[1] + xlat.length();
            }

            /* 
             *   search for the next tag, considering only the part of
             *   the string following the replacement text - we do not
             *   want to re-scan the replacement text for tags 
             */
            idx = rexSearch(tagPattern, afterStr);
                
            /* 
             *   If we found it, adjust the starting index of the match to
             *   its position in the actual string.  Note that we do this
             *   by adding the OFFSET of the remainder of the string,
             *   which is 1 less than its INDEX, because idx[1] is already
             *   a string index.  (An offset is one less than an index
             *   because the index of the first character is 1.)  
             */
            if (idx != nil)
                idx[1] += afterOfs - 1;
        }

        /* return the filtered value */
        return val;
    }

    /*
     *   Translate a tag 
     */
    translateTag(tag)
    {
        local isClose;
        local styleTag;
        
        /* if it's a close tag, so note and remove the leading slash */
        isClose = tag.startsWith('/');
        if (isClose)
            tag = tag.substr(2);

        /* look up the tag object in our table */
        styleTag = tagTable[tag];

        /* 
         *   if we found it, return the open or close text, as
         *   appropriate; otherwise return nil 
         */
        return (styleTag != nil
                ? (isClose ? styleTag.closeText : styleTag.openText)
                : nil);
    }

    /* preinitialization */
    execute()
    {
        /* create a lookup table for our style table */
        tagTable = new LookupTable();
        
        /* 
         *   Populate the table with all of the StyleTag instances.  Key
         *   by tag name, storing the tag object as the value for each
         *   key.  This will let us efficiently look up the StyleTag
         *   object given a tag name string.
         */
        forEachInstance(StyleTag, { tag: tagTable[tag.tagName] = tag });
    }

    /*
     *   Our tag translation table.  We'll initialize this during preinit
     *   to a lookup table with all of the defined StyleTag objects.  
     */
    tagTable = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Command Sequencer Filter.  This is an output filter that handles the
 *   special <.commandsep> tag for visual command separation.  This tag has
 *   the form of a style tag, but must be processed specially.
 *   
 *   <.commandsep> shows an appropriate separator between commands.  Before
 *   the first command output or after the last command output, this has no
 *   effect.  A run of multiple consecutive <.commandsep> tags is treated
 *   as a single tag.
 *   
 *   Between commands, we show gLibMessages.commandResultsSeparator.  After
 *   an input line and before the first command result text, we show
 *   gLibMessages.commandResultsPrefix.  After the last command result text
 *   before a new input line, we show gLibMessages.commandResultsSuffix.
 *   If we read two input lines, and there is no intervening text output at
 *   all, we show gLibMessages.commandResultsEmpty.
 *   
 *   The input manager should write a <.commandbefore> tag whenever it
 *   starts reading a command line, and a <.commandafter> tag whenever it
 *   finishes reading a command line.  
 */
enum stateReadingCommand, stateBeforeCommand, stateBeforeInterruption,
    stateInCommand, stateBetweenCommands, stateWriteThrough,
    stateNoCommand;

transient commandSequencer: OutputFilter
    /*
     *   Force the sequencer into mid-command mode.  This can be used to
     *   defeat the resequencing into before-results mode that occurs if
     *   any interactive command-line input must be read in the course of
     *   a command's execution.  
     */
    setCommandMode() { state_ = stateInCommand; }

    /*
     *   Internal routine: write the given text directly through us,
     *   skipping any filtering we'd otherwise apply. 
     */
    writeThrough(txt)
    {
        local oldState;

        /* remember our old state */
        oldState = state_;

        /* set our state to write-through */
        state_ = stateWriteThrough;

        /* make sure we reset things on the way out */
        try
        {
            /* write the text */
            say(txt);
        }
        finally
        {
            /* restore our old state */
            state_ = oldState;
        }
    }

    /* pre-compile our tag sequence pattern */
    patNextTag = static new RexPattern(
        '<nocase><langle><dot>'
        + 'command(sep|int|before|after|none|mid)'
        + '<rangle>')

    /*
     *   Apply our filter 
     */
    filterText(ostr, txt)
    {
        local ret;
        
        /* 
         *   if we're in write-through mode, simply pass the text through
         *   unchanged 
         */
        if (state_ == stateWriteThrough)
            return txt;

        /* scan for tags */
        for (ret = '' ; txt != '' ; )
        {
            local match;
            local cur;
            local tag;
            
            /* search for our next special tag sequence */
            match = rexSearch(patNextTag, txt);

            /* check to see if we found a tag */
            if (match == nil)
            {
                /* no more tags - the rest of the text is plain text */
                cur = txt;
                txt = '';
                tag = nil;
            }
            else
            {
                /* found a tag - get the plain text up to the tag */
                cur = txt.substr(1, match[1] - 1);
                txt = txt.substr(match[1] + match[2]);

                /* get the tag name */
                tag = rexGroup(1)[3];
            }

            /* process the plain text up to the tag, if any */
            if (cur != '')
            {
                /* check our state */
                switch(state_)
                {
                case stateReadingCommand:
                case stateWriteThrough:
                case stateInCommand:
                case stateNoCommand:
                    /* we don't need to add anything in these states */
                    break;

                case stateBeforeCommand:
                    /* 
                     *   We're waiting for the first command output, and we've
                     *   now found it.  Write the command results prefix
                     *   separator.
                     */
                    
                    /*
                     *   Command group prefix - this is displayed after a
                     *   command line and before the first command results shown
                     *   after the command line.
                     *
                     *   By default, we'll show the "zero-space paragraph"
                     *   marker, which acts like a paragraph break in that it
                     *   swallows up immediately following paragraph breaks, but
                     *   doesn't actually add any space. This will ensure that
                     *   we don't add any space between the command input line
                     *   and the next text.
                     */
                    
                    ret += BMsg(command results prefix, '<.p0>');

                    /* we're now inside some command result text */
                    state_ = stateInCommand;
                    break;

                case stateBeforeInterruption:
                    /*
                     *   An editing session has been interrupted, and we're
                     *   showing new output.  First, switch to normal
                     *   in-command mode - do this before doing anything
                     *   else, since we might recursively show some more
                     *   text in the course of canceling the input line.  
                     */
                    state_ = stateInCommand;

                    /*
                     *   Now tell the input manager that we're canceling
                     *   the input line that was under construction.  Don't
                     *   reset the input editor state, though, since we
                     *   might be able to resume editing the same line
                     *   later.  
                     */
                    inputManager.cancelInputInProgress(nil);

                    /* insert the command interruption prefix */
                    
                    /*
                     *   Command "interruption" group prefix.  This is displayed
                     *   after an interrupted command line - a command line
                     *   editing session that was interrupted by a timeout event
                     *   - just before the text that interrupted the command
                     *   line.
                     *
                     *   By default, we'll show a paragraph break here, to set
                     *   off the interrupting text from the command line under
                     *   construction.
                     */
                    
                    ret += BMsg(command interuption prefix, '<.p>');
                    break;

                case stateBetweenCommands:
                    /* 
                     *   We've been waiting for a new command to start
                     *   after seeing a <.commandsep> tag.  We now have
                     *   some text for the new command, so show a command
                     *   separator. 
                     */
                    
                    /*
                     *   Command separator - this is displayed after the results
                     *   from a command when another command is about to be
                     *   executed without any more user input.  That is, when a
                     *   command line contains more than one command, this
                     *   message is displayed between each successive command,
                     *   to separate the results visually.
                     *
                     *   This is not shown before the first command results
                     *   after a command input line, and is not shown after the
                     *   last results before a new input line.  Furthermore,
                     *   this is shown only between adjacent commands for which
                     *   output actually occurs; if a series of commands
                     *   executes without any output, we won't show any
                     *   separators between the silent commands.
                     *
                     *   By default, we'll just start a new paragraph.
                     */
                    
                    ret += BMsg(command results separator, '<.p>');

                    /* we're now inside some command result text */
                    state_ = stateInCommand;
                    break;
                }

                /* add the plain text */
                ret += cur;
            }

            /* if we found the tag, process it */
            switch(tag)
            {
            case 'none':
                /* switching to no-command mode */
                state_ = stateNoCommand;
                break;

            case 'mid':
                /* switching back to mid-command mode */
                state_ = stateInCommand;
                break;
                
            case 'sep':
                /* command separation - check our state */
                switch(state_)
                {
                case stateReadingCommand:
                case stateBeforeCommand:
                case stateBetweenCommands:
                case stateWriteThrough:
                    /* in these states, <.commandsep> has no effect */
                    break;

                case stateInCommand:
                    /* 
                     *   We're inside some command text.  <.commandsep>
                     *   tells us that we've reached the end of one
                     *   command's output, so any subsequent output text
                     *   belongs to a new command and thus must be visually
                     *   separated from the preceding text.  Don't add any
                     *   separation text yet, because we don't know for
                     *   sure that there will ever be any more output text;
                     *   instead, switch our state to between-commands, so
                     *   that any subsequent text will trigger addition of
                     *   a separator.  
                     */
                    state_ = stateBetweenCommands;
                    break;
                }
                break;

            case 'int':
                /* 
                 *   we've just interrupted reading a command line, due to
                 *   an expired timeout event - switch to the
                 *   before-interruption state 
                 */
                state_ = stateBeforeInterruption;
                break;

            case 'before':
                /* we're about to start reading a command */
                switch (state_)
                {
                case stateBeforeCommand:
                    /* 
                     *   we've shown nothing since the last command; show
                     *   the empty command separator 
                     */
                    
                    /*
                     *   Empty command results - this is shown when we read a
                     *   command line and then go back and read another without
                     *   having displaying anything.
                     *
                     *   By default, we'll return a message indicating that
                     *   nothing happened.
                     */
                    writeThrough(BMsg(command results empty, 
                                      'Nothing obvious {dummy}{happens}.<.p>'));
                    break;

                case stateBetweenCommands:
                case stateInCommand:
                    /* 
                     *   we've written at least one command result, so
                     *   show the after-command separator 
                     */
                    
                    /*
                     *   Command results suffix - this is displayed just before
                     *   a new command line is about to be read if any command
                     *   results have been shown since the last command line.
                     *
                     *   By default, we'll show nothing extra.
                     */
                    writeThrough(BMsg(command results suffix, ''));
                    break;

                default:
                    /* do nothing in other modes */
                    break;
                }

                /* switch to reading-command mode */
                state_ = stateReadingCommand;
                break;

            case 'after':
                /* 
                 *   We've just finished reading a command.  If we're
                 *   still in reading-command mode, switch to
                 *   before-command-results mode.  Don't switch if we're
                 *   in another state, since we must have switched to
                 *   another state already by a different route, in which
                 *   case we can ignore this notification.  
                 */
                if (state_ == stateReadingCommand)
                    state_ = stateBeforeCommand;
                break;
            }
        }

        /* return the results */
        return ret;
    }

    /* our current state - start out in before-command mode */
    state_ = stateBeforeCommand
;


/* ------------------------------------------------------------------------ */ 
 /* 
  *   quoteFilter: this loooks for smart quotes in the output and checks that
  *   they are balanced.
  *
  *   The problem with the smart quotes <q> </q> is that if one is missing, or a
  *   spurious one is added, the error is perpetrated throughout the rest of the
  *   game (or until a compensating error is located). The purpose of
  *   quoteFilter is (a) to report such errors (to make them easier to fix) and
  *   (b) to prevent them being propagated beyond a single turn. In the main
  *   this works by having quoteFilter take over responsibility for turning the
  *   <q> and </q> tags into the appropriate HTML entities rather than leaving
  *   it to the HTML rendering engine in the interpreter. The quoteFilter
  *   OutputFilter keeps its own track of whether a double quote or a single
  *   quote is rquired next, and resets this count at the start of each turn.
  */
quoteFilter: OutputFilter, InitObject
    filterText(ostr, txt) 
    { 
        local quoteRes, quoteStr;
        do
        {
            /* Look for <q> and </q> in the output */
            quoteRes = rexSearch(quotePat, txt);
            
            /* If we found <q> or </q>...*/
            if(quoteRes)
            {   
                /* 
                 *   Note whether it was an opening or a closing smart quote we
                 *   found.
                 */
                quoteStr = quoteRes[3];
                
                /* 
                 *   If it was an opening smart quote then replace it with an
                 *   opening double quote mark if we've had a net even number of
                 *   (or zero) opening quote marks this turn, otherwise replace
                 *   it with an opening single quote mark.
                 */
                if(quoteStr.toLower() == '<q>')
                {
                    txt = txt.findReplace(quoteStr, quoteCount % 2 == 0 
                                          ? '&ldquo;' : '&lsquo;', ReplaceOnce);

                    /* Increment our counter of opening quote marks */
                    quoteCount ++;                
                }
                /* Otherwise it's a closing smart quote */
                else
                {
                    /* 
                     *   Replace it with a closing double quote mark if we've
                     *   had a net even number of opening quotes so far on this
                     *   turn, otherwise replace it with a closing single quote
                     *   mark.
                     */
                    txt = txt.findReplace(quoteStr, quoteCount % 2 == 1 
                                          ? '&rdquo;' : '&rsquo;', ReplaceOnce);

                    /* Decrement our net quote count */
                    quoteCount --;                   
                    
                }
            }                      
                
        } while(quoteRes);

        /* 
         *   Return the filtered string, with <q> and </q> replaced with opening
         *   and closing quote marks.
         */
        return txt; 
    }
    
    /* 
     *   Our quoteCount is the net number of quote marks we've output this turn,
     *   i.e. the number of opening quote marks less the number of closing quote
     *   marks.
     */
    quoteCount = 0 
    
    /* Our rex pattern to match <q> and </q> */
    quotePat = static new RexPattern('<NoCase><langle>(q|/q)<rangle>')
    
    
    /* In Initialize this filter */
    execute()
    {
        /* Add this filter to the main output stream */
        mainOutputStream.addOutputFilter(self);
        
       /* 
        *   Set up a new prompt daemon to display a warning message about any
        *   unmatched quotes and zeroize our quoteCount each turn.
        */        
        if(defined(PromptDaemon) && new PromptDaemon(self, &quoteCheck));
       
    }
    
    /* 
     *   Should I show a warning when I find unmatched smart quotes over the 
     *   course of a turn? Displaying such a warning would probably look 
     *   intrusive in a released version, but might well be useful in a 
     *   version sent out to beta-testers (so it shouldn't be tied to a 
     *   version compiled for debugging). The showWarnings flag thus allows 
     *   the warning messages to be turned on and off as desired.
     */    
    showWarnings = true
    
    /* 
     *   The PromptDaemon set up in our execute() method at Initialization runs
     *   this method at the end of each turn. It checks to see if the number of
     *   opening smart quotes over the course of the turn just completed is the
     *   same as the number of closing smart quotes, and optionally prints a
     *   warning message if it is not.
     */    
    quoteCheck()
    {
       /* 
        *   If we have a non-zero quoteCount at the start of a turn, this means
        *   that the number of opening quotes output on the previous turn didn't
        *   match the number of closing quotes, so if our showWarnings property
        *   is true, display a message calling attention to the unmatched quotes
        *   (to enable the game author to fix them or beta-testers to report
        *   them)
        */
        if(quoteCount != 0 && showWarnings)
            "<FONT COLOR=RED><b>WARNING!!</b></FONT> Unmatched quotes on
            this turn; quoteCount = <<quoteCount>>. ";      
        
        /* 
         *   In any case we want to zeroize the quoteCount at the start of 
         *   each turn so that the first smart quote we encounter on the 
         *   turn will display correctly no matter what went before.
         */
        quoteCount = 0;
    }
    
;


/* ------------------------------------------------------------------------ */
/*
 *   Log Console output stream.  This is a simple wrapper for the system
 *   log console, which allows console-style output to be captured to a
 *   file, with full processing (HTML expansion, word wrapping, etc) but
 *   without displaying anything to the game window.
 *   
 *   This class should always be instantiated with transient instances,
 *   since the underlying system object doesn't participate in save/restore
 *   operations.  
 */
class LogConsole: OutputStream
    /*
     *   Utility method: create a log file, set up to capture all console
     *   output to the log file, run the given callback function, and then
     *   close the log file and restore the console output.  This can be
     *   used as a simple means of creating a file that captures the output
     *   of a command.  
     */
    captureToFile(filename, charset, width, func)
    {
        local con;
            
        /* set up a log console to do the capturing */
        con = new LogConsole(filename, charset, width);

        /* capture to the console and run our command */
        outputManager.withOutputStream(con, func);

        /* done with the console */
        con.closeConsole();
    }

    /* create a log console */
    construct(filename, charset, width)
    {
        /* inherit base class handling */
        inherited();
        
        /* create the system log console object */
        handle_ = logConsoleCreate(filename, charset, width);

        /* install the standard output filters */
        addOutputFilter(typographicalOutputFilter);
        addOutputFilter(new transient ParagraphManager());
        addOutputFilter(styleTagFilter);

    }

    /* 
     *   Close the console.  This closes the underlying system log console,
     *   which closes the operating system file.  No further text can be
     *   written to the console after it's closed.  
     */
    closeConsole()
    {
        /* close our underlying system console */
        logConsoleClose(handle_);

        /* 
         *   forget our handle, since it's no longer valid; setting the
         *   handle to nil will make it more obvious what's going on if
         *   someone tries to write more text after we've been closed 
         */
        handle_ = nil;
    }

    /* low-level stream writer - write to our system log console */
    writeFromStream(txt) { logConsoleSay(handle_, txt); }

    /* our system log console handle */
    handle_ = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   Output stream window.
 *   
 *   This is an abstract base class for UI widgets that have output
 *   streams, such as Banner Windows and Web UI windows.  This base class
 *   essentially handles the interior of the window, and leaves the details
 *   of the window's layout in the broader UI to subclasses.  
 */
class OutputStreamWindow: object
    /* 
     *   Invoke the given callback function, setting the default output
     *   stream to the window's output stream for the duration of the call.
     *   This allows invoking any code that writes to the current default
     *   output stream and displaying the result in the window.  
     */
    captureOutput(func)
    {
        /* make my output stream the global default */
        local oldStr = outputManager.setOutputStream(outputStream_);

        /* make sure we restore the default output stream on the way out */
        try
        {
            /* invoke the callback function */
            (func)();
        }
        finally
        {
            /* restore the original default output stream */
            outputManager.setOutputStream(oldStr);
        }
    }

    /* 
     *   Make my output stream the default in the output manager.  Returns
     *   the previous default output stream; the caller can note the return
     *   value and use it later to restore the original output stream via a
     *   call to outputManager.setOutputStream(), if desired.  
     */
    setOutputStream()
    {
        /* set my stream as the default */
        return outputManager.setOutputStream(outputStream_);
    }

    /*
     *   Create our output stream.  We'll create the appropriate output
     *   stream subclass and set it up with our default output filters.
     *   Subclasses can override this as needed to customize the filters.  
     */
    createOutputStream()
    {
        /* create a banner output stream */
        outputStream_ = createOutputStreamObj();

        /* set up the default filters */
        outputStream_.addOutputFilter(typographicalOutputFilter);
        outputStream_.addOutputFilter(new transient ParagraphManager());
        outputStream_.addOutputFilter(styleTagFilter);
    }

    /*
     *   Create the output stream object.  Subclasses can override this to
     *   create the appropriate stream subclass.  Note that the stream
     *   should always be created as a transient object.  
     */
    createOutputStreamObj() { return new transient OutputStream(); }

    /*
     *   My output stream - this is a transient OutputStream instance.
     *   Subclasses must create this explicitly by calling
     *   createOutputStream() when the underlying UI window is first
     *   created.  
     */
    outputStream_ = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Typographical effects output filter.  This filter looks for certain
 *   sequences in the text and converts them to typographical equivalents.
 *   Authors could simply write the HTML for the typographical markups in
 *   the first place, but it's easier to write the typewriter-like
 *   sequences and let this filter convert to HTML.
 *
 *   We perform the following conversions:
 *
 *   '---' -> &zwnbsp;&mdash;
 *.  '--' -> &zwnbsp;&ndash;
 *.  sentence-ending punctuation -> same + &ensp;
 *
 *   Since this routine is called so frequently, we hard-code the
 *   replacement strings, rather than using properties, for slightly faster
 *   performance.  Since this routine is so simple, games that want to
 *   customize the replacement style should simply replace this entire
 *   routine with a new routine that applies the customizations.
 *
 *   Note that we define this filter in the English-specific part of the
 *   library, because it seems almost certain that each language will want
 *   to customize it for local conventions.
 */
typographicalOutputFilter: OutputFilter
    filterText(ostr, val)
    {
        /*
         *   Look for sentence-ending punctuation, and put an 'en' space after each occurrence.
         *   Recognize ends of sentences even if we have closing quotes, parentheses, or other
         *   grouping characters following the punctuation.  Do this before the hyphen substitutions
         *   so that we can look for ordinary hyphens rather than all of the expanded versions. We
         *   don't do this if sentenceSpacer has been overridden to a single normal space.
         */
        if(sentenceSpacer != ' ')
            val = rexReplace(eosPattern, val, '%1'+sentenceSpacer, ReplaceAll);

        /* undo any abbreviations we mistook for sentence endings */
        val = rexReplace(abbrevPat, val, '%1. ', ReplaceAll);

        /*
         *   Replace dashes with typographical hyphens.  Three hyphens in a
         *   row become an em-dash, and two in a row become an en-dash.
         *   Note that we look for the three-hyphen sequence first, because
         *   if we did it the other way around, we'd incorrectly find the
         *   first two hyphens of each '---' sequence and replace them with
         *   an en-dash, causing us to miss the '---' sequences entirely.
         *   
         *   We put a no-break marker (\uFEFF) just before each hyphen, and
         *   an okay-to-break marker (\u200B) just after, to ensure that we
         *   won't have a line break between the preceding text and the
         *   hyphen, and to indicate that a line break is specifically
         *   allowed if needed to the right of the hyphen.  
         */
        val = val.findReplace(['---', '--'],
                              ['\uFEFF&mdash;\u200B', '\uFEFF&ndash;\u200B']);

        /* return the result */
        return val;
    }
    
    /* 
     *   The character to use between a full stop and the start of the next sentence. By
     *   default we use an n-space.
     */        
    sentenceSpacer = '\u2002'

    /*
     *   The end-of-sentence pattern.  This looks a bit complicated, but
     *   all we're looking for is a period, exclamation point, or question
     *   mark, optionally followed by any number of closing group marks
     *   (right parentheses or square brackets, closing HTML tags, or
     *   double or single quotes in either straight or curly styles), all
     *   followed by an ordinary space.
     *
     *   If a lower-case letter follows the space, though, we won't
     *   consider it a sentence ending.  This applies most commonly after
     *   quoted passages ending with what would normally be sentence-ending
     *   punctuation: "'Who are you?' he asked."  In these cases, the
     *   enclosing sentence isn't ending, so we don't want the extra space.
     *   We can tell the enclosing sentence isn't ending because a
     *   non-capital letter follows.
     *
     *   Note that we specifically look only for ordinary spaces.  Any
     *   sentence-ending punctuation that's followed by a quoted space or
     *   any typographical space overrides this substitution.
     */
    eosPattern = static new RexPattern(
        '<case>'
        + '('
        +   '[.!?]'
        +   '('
        +     '<rparen|rsquare|dquote|squote|\u2019|\u201D>'
        +     '|<langle><^rangle>*<rangle>'
        +   ')*'
        + ')'
        + ' +(?![-a-z])'
        )

    /* pattern for abbreviations that were mistaken for sentence endings */
    abbrevPat = static new RexPattern(
        '<nocase>%<(' + abbreviations + ')<dot>\u2002')

    /* 
     *   Common abbreviations.  These are excluded from being treated as
     *   sentence endings when they appear with a trailing period.
     *   
     *   Note that abbrevPat must be rebuilt manually if you change this on
     *   the fly - abbrevPat is static, so it picks up the initial value of
     *   this property at start-up, and doesn't re-evaluate it while the
     *   game is running.  
     */
    abbreviations = 'mr|mrs|ms|dr|prof'
;

/* 
 *   cquoteOutputFilter; this turns straight quotes into typographical quotes
 *   (and is based on an extension by Stephen Grsnade).
 */
cquoteOutputFilter: OutputFilter
    aggressive = true

    // Patterns for our searches
    patIsHTMLTag = static new RexPattern('<langle><^rangle>+<squote|dquote><^rangle>*<rangle>')
    patIsFormatTag = static new RexPattern('{[^}]+<squote>[^}]*}')
    patAggressive = static new RexPattern('(<alphanum|punct>)<squote>')
    patIsCont1Tag = static new RexPattern('(<alpha>)<squote>(s|m|d|ve|re|ll)')
    patIsCont2Tag = static new RexPattern('(<alpha>)n<squote>t')
    patIsPossTag = static new RexPattern('(<alpha>)s<squote>')
    
    filterText(ostr, val) {
	local ret;
        
        if(!isActive)
            return val;

	// Look for an HTML tag. We only need to find the first one,
	// because we'll be recursing through the string
	ret = rexSearch(patIsHTMLTag, val);
	if (ret == nil) {
	    // Look for a formatting tag
	    ret = rexSearch(patIsFormatTag, val);
	}
            
        
	// If we got a match either from the HTML or the formatting
	// tag, ignore that match recursively; that is, run the output
	// filter on the text before and after the match. This is
	// assuming that the whole start wasn't prefixed by a backslash
	// (since e.g. "\<font face='courier>" isn't really an HTML tag)
	if (ret != nil && (ret[1] == 1 ||
			   val.substr(ret[1] - 1, 1) != '\\')) {
	    return filterText(ostr, val.substr(1, ret[1] - 1)) + ret[3] +
		filterText(ostr, val.substr(ret[1] + ret[2],
					    val.length() - (ret[1]+ret[2])
					    + 1));
	}

	// Do the appropriate replacements. First, aggressive
	if (aggressive) {
	    val = rexReplace(patAggressive, val, '%1&rsquo;',
			     ReplaceAll);
            
            /* Also replace double quotes with curly quotes */
            val = val.findReplace([R'(^|%<|<space>)"', R'(<^space>)"'], 
                                  ['%1&ldquo;','%1&rdquo;']);
	}
	else {
	    // We recognize the contractions 's, 'm, 'd, 've, 're,
	    // 'll, and n't, as well as the plural possessive s'.
	    // (Possessive 's is handled by the contraction.) All
	    // must be preceeded by a letter.
	    val = rexReplace(patIsCont1Tag,
			     val, '%1&rsquo;%2', ReplaceAll);
	    val = rexReplace(patIsCont2Tag, val, '%1n&rsquo;t',
			     ReplaceAll);
	    val = rexReplace(patIsPossTag, val, '%1s&rsquo;',
			     ReplaceAll);
	}

	return val;
    }
    
    isActive = true
    
    activate() { isActive = true; }
    deactivate() { isActive = nil; }
;


/* ------------------------------------------------------------------------ */
/*
 *   Temporarily override the current narrative tense and invoke a callback
 *   function.
 */
withTense(usePastTense, callback)
{
    /*
     *   Remember the old value of the usePastTense flag.
     */
    local oldUsePastTense = gameMain.usePastTense;
    /*
     *   Set the new value.
     */
    gameMain.usePastTense = usePastTense;
    /*
     *   Invoke the callback (remembering the return value) and restore the
     *   usePastTense flag on our way out.
     */
    local ret;
    try { ret = callback(); }
    finally { gameMain.usePastTense = oldUsePastTense; }
    /*
     *   Return the result.
     */
    return ret;
}

/* 
 *   Display msg bypassing all filters except for the massage parameter
 *   substitutions; these may also be bypassed if the second (optional)
 *   parameter is nil.
 */

extraReport(msg, expandParam = true)
{
    if(expandParam)
        msg = buildMessage(nil, msg);
    
    gOutStream.writeFromStream(msg);
}


/*
 *   Basic conversation manager for use in adv3Liter and adv3Litest(when actor.t is not present) We
 *   just provide handling for <.reveal> and <.unreveal> in case game code uses them.
 */

conversationManager: OutputFilter, PreinitObject
    /*
     *   Custom extended tags.  Games and library extensions can add their
     *   own tag processing as needed, by using 'modify' to extend this
     *   object.  There are two things you have to do to add your own tags:
     *   
     *   First, add a 'customTags' property that defines a regular
     *   expression for your added tags.  This will be incorporated into
     *   the main pattern we use to look for tags.  Simply specify a
     *   string that lists your tags separated by "|" characters, like
     *   this:
     *   
     *   customTags = 'foo|bar'
     *   
     *   Second, define a doCustomTag() method to process the tags.  The
     *   filter routine will call your doCustomTag() method whenever it
     *   finds one of your custom tags in the output stream.  
     */
    customTags = nil
    doCustomTag(tag, arg) { /* do nothing by default */ }
    
    filterText(ostr, txt)
    {
        local start;
        
        
        /* scan for our special tags */
        for (start = 1 ; ; )
        {
            local match;
            local arg;
            local tag;
            local nxtOfs;           
            local args;
            
            /* scan for the next tag */
            match = rexSearch(tagPat, txt, start);
            
            /* if we didn't find it, we're done */
            if (match == nil)
                break;
            
            /* note the next offset */
            nxtOfs = match[1] + match[2];
            
            /* get the argument (the third group from the match) */
            arg = rexGroup(3);
            if (arg != nil)
                arg = arg[3];
            
            /* pick out the tag */
            tag = rexGroup(1)[3].toLower(); 
            
            
            /* check which tag we have */
            switch (tag)
            {
                
            case 'reveal':
                /* reveal the key by adding it to our database */
                
                args = arg.split('=');
                if(args.length > 1)
                {
                    arg = enumTabObj.getEnum(args[2]) ?? args[2];
                    
                    setRevealed(args[1], arg);
                }
                else                
                    setRevealed(arg);
                break;
                
                /* unreveal the key by removing it from our database */
            case 'unreveal':
                               
                setUnrevealed(arg);
                break;
                
                
            default:
                /* check for an extended tag */
                doCustomTag(tag, arg);
                break;
            }
            
            /* continue the search after this match */
            start = nxtOfs;
        }
        
        /* 
         *   remove the tags from the text by replacing every occurrence with an empty string, and
         *   return the result
         */
        return rexReplace(tagPat, txt, '', ReplaceAll);
    }
    
    tagPat = static new RexPattern(
        '<nocase><langle><dot>'
        + '(reveal|unreveal'
        + (customTags != nil ? '|' + customTags : '')        
        + ')(<space>+(<^rangle>+))?'
        + '<rangle>')
    
    setRevealed(tag, val?)
    {
        /* Note that our tag has been revealed */
        libGlobal.setRevealed(tag, val);
    }
    
;

Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1