# 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:
            case TypeList:
                /* convert integers, lists 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;
            }

            catch(SkipSignal ex)
            {
                return mon.outputFlag ? 2 : nil;
            }

            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_;
            }

            catch(SkipSignal ex)
            {
                return filter.txt_ = '' ? 2 : 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; }

        isActive = true
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   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_ = ''
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   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 = ')'
    ;

    /*
     *   <.tip> - style for displaying the text (desc) of tips. The opening and closing tip tags are
     *   automatically added before and after desc by the showTip() method.
     */
    tipStyleTag: StyleTag 'tip'
       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>'
    ;

    singleTypographicalQuoteTag: StyleTag 'sq'
        openText = '&lsquo;'
        closeText = '&rsquo';
    ;

    doubleTypographicalQuoteTag: StyleTag 'dq'
        openText = '&ldquo;'
        closeText = '&rdquo';
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   "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 looks 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].toLower();

                    /*
                     *   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.
                     */

                    switch(quoteStr)
                    {

                    case '<q>':

                        txt = txt.findReplace(quoteStr, quoteCount % 2 == 0
                                              ? '&ldquo;' : '&lsquo;', ReplaceOnce);

                        /* Increment our counter of opening quote marks */
                        quoteCount ++;
                        break;
                        /* If it's a closing smart quote */
                    case '</q>':

                        /*
                         *   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 --;
                        break;
                    case '<sq>':
                        txt = txt.findReplace(quoteStr, '&lsquo;', ReplaceOnce);
                        break;
                    case '</sq>':
                        txt = txt.findReplace(quoteStr, '&rsquo;', ReplaceOnce);
                        break;

                    case '<dq>':
                        txt = txt.findReplace(quoteStr, '&ldquo;', ReplaceOnce);
                        break;
                    case '</dq>':
                        txt = txt.findReplace(quoteStr, '&rdquo;', ReplaceOnce);
                        break;
                    }
                }

            } 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>(<s|d>?q|/<s|d>?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)
        {
            /* If we're not active, simply return val unchanged. */
            if(!isActive)
                return 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'

        isActive = true
    ;

    /*
     *   Display a list of strings supplied in the lst parameter as preformatted text, i.e. in a fixed
     *   width font with all spaces preserved and no intervention from the typographical output filter.
     *   This is intended to facilitate the display of diagrams or pictures composed of ASCII
     *   characters. For example:
     *
     *.     local diag = [
     *.    '+---   -----------',
     *.    '|    *         +  \',
     *.    '|    *     |   |   \',
     *.    '+-------------------'
     *.    ];
     *
     *.    showPreformatted(diag);
     */

    showPreformatted(lst)
    {
        /* If lst is supplied as a single-quoted string, convert it to a single-element list. */
        if(dataType(lst) == TypeSString)
            lst = [lst];

        "<pre>";
        foreach(local txt in lst)
        {
            txt = txt.findReplace(' ', '\ ', ReplaceAll);
            aioSay(txt);
            "\n";
        }
        "</pre>";
    }

    /*
     *   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;

        /* But if Narrator.tense is an object, we need to take the old tense from there. */
        if(Narrator.propType(&tense) == TypeObject)
            oldUsePastTense = Narrator.tense;
        /*
         *   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
        {
            /* if useOldPastTense is an object, it was taken from Narrator.tense. */
            if(dataType(oldUsePastTense == TypeObject))
                Narrator.tense = oldUsePastTense;
            /* Otherwise we needs to restore the value on gameMain, */
            else
                gameMain.usePastTense = oldUsePastTense;
        }
        /*
         *   Return the result.
         */
        return ret;
    }

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

    extraReport(msg, expandParam = true)
    {
        if(expandParam)
        {
            msg = buildMessage(nil, msg);

            /* Apply the style tag filter */
            msg = styleTagFilter.filterText(gOutStream, 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);
        }

    ;

    class SkipSignal: Exception

    ;

Adv3Lite Library Reference Manual
Generated on 13/06/2025 from adv3Lite version 2.2.1