output.t

documentation
 #charset "us-ascii"
 
 /* 
  *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
  *   
  *   TADS 3 Library - output formatting
  *   
  *   This module defines the framework for displaying output text.  
  */
 
 /* include the library header */
 #include "adv3.h"
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   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)
 {
     /* send output to the active output stream */
     outputManager.curOutputStream.writeToStream(val);
 }
 
 /* ------------------------------------------------------------------------ */
 /*
  *   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
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   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)
     {
         /* 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:
             /* nil - 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;
         }
         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])
     {
         local filter;
 
         /* install a string capture filter */
         addOutputFilter(filter = new StringCaptureFilter());
 
         /* 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);
         }
     }
 
     /* 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)
     {
         local idx;
         
         /* find the existing filter in our list */
         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 is 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 = filterList_.length() ; i != 0 ; --i)
         {
             /* apply this filter */
             val = filterList_[i].filterText(self, val);
 
             /* if that filter left us with nothing, we're done */
             if (val == nil)
                 break;
         }
 
         /* return the result of all of the filters */
         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
 ;
 
 /*
  *   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 */
         tadsSay(txt);
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /* 
  *   Generate a string to show hyperlinked text.  If we're not in HTML
  *   mode, we'll simply return the text without the hyperlink; otherwise,
  *   we'll return the text with a hyperlink to the given HREF.  
  */
 aHref(href, txt, ...)
 {
     /* check for HTML mode */
     if (systemInfo(SysInfoInterpClass) == SysInfoIClassHTML)
     {
         local title;
 
         /* if there's a title, retrieve it */
         title = (argcount >= 3 ? getArg(3) : nil);
 
         /* we're in HTML mode - generate a <a> tag enclosing the text */
         return '<a href="' + href + '"'
             + (title != nil ? ' title="' + title + '"' : '')
             + '><.a>' + txt + '<./a></a>';
     }
     else
     {
         /* plain text mode - just return the text unchanged */
         return txt;
     }
 }
 
 /* 
  *   Generate a string to show hyperlinked text, with alternate text if
  *   we're not in HTML mode.  If we're in HTML mode, we'll return
  *   linkedTxt linked to the given HREF; if we're in plain text mode,
  *   we'll return the alternate text as-is.  
  */
 aHrefAlt(href, linkedText, altText, ...)
 {
     /* check HTML mode */
     if (systemInfo(SysInfoInterpClass) == SysInfoIClassHTML)
     {
         local title;
 
         /* if there's a title, retrieve it */
         title = (argcount >= 4 ? getArg(4) : nil);
 
         /* we're in HTML mode - generate an <A> tag for the linked text */
         return '<a href="' + href + '"'
             + (title != nil ? ' title="' + title + '"' : '')
             + '><.a>' + linkedText + '<./a></a>';
     }
     else
     {
         /* plain text mode - just return the alternate text */
         return altText;
     }
 }
 
 /*
  *   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>';
 }
 
 /* ------------------------------------------------------------------------ */
 /*
  *   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: object
     /*
      *   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: object
     /* 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 = (systemInfo(SysInfoInterpClass) == SysInfoIClassHTML
                 ? htmlOpenText : plainOpenText)
 
     closeText = (systemInfo(SysInfoInterpClass) == SysInfoIClassHTML
                  ? 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' '' '';
 
 /* 
  *   <.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
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   MessageBuilder - this object provides a general text substitution
  *   mechanism.  Text to be substituted is enclosed in {curly braces}.
  *   Within the braces, we have the substitution parameter name, which can
  *   be in the following formats:
  *   
  *   id
  *.  id obj
  *.  id1/id2 obj
  *.  id1 obj/id2
  *   
  *   The ID string gives the type of substitution to perform.  The ID's
  *   all come from a table, which is specified by the language-specific
  *   subclass, so the ID's can vary by language (to allow for natural
  *   template-style parameter names for each language).  If the ID is in
  *   two pieces (id1 and id2), we concatenate the two pieces together with
  *   a slash between to form the name we seek in the table - so {the/he
  *   dobj} and {the dobj/he} are equivalent, and both look up the
  *   identifier 'the/he'.  If a two-part identifier is given, and the
  *   identifier isn't found in the table, we'll try looking it up with the
  *   parts reversed: if we see {he/the dobj}, we'll first try finding
  *   'he/the', and if that fails we'll look for 'the/he'.
  *   
  *   If 'obj' is present, it specificies the target object providing the
  *   text to be substitutued; this is a string passed to the current
  *   Action, and is usually something like 'actor', 'dobj', or 'iobj'.
  *   
  *   One instance of this class, called langMessageBuilder, should be
  *   created by the language-specific library.  
  */
 class MessageBuilder: PreinitObject
     /* pre-compile some regular expressions we use a lot */
     patUpper = static new RexPattern('<upper>')
     patAllCaps = static new RexPattern('<upper><upper>')
     patIdObjSlashId = static new RexPattern(
         '(<^space|/>+)<space>+(<^space|/>+)(/<^space|/>+)')
     patIdObj = static new RexPattern('(<^space>+)<space>+(<^space>+)')
     patIdSlash = static new RexPattern('([^/]+)/([^/]+)')
 
     /*
      *   Given a source string with substitution parameters, generate the
      *   expanded message with the appropriate text in place of the
      *   parameters. 
      */
     generateMessage(orig)
     {
         local result;
 
         /* we have nothing in the result string so far */
         result = '';
 
         /* keep going until we run out of substitution parameters */
         for (;;)
         {
             local idx;
             local paramStr;
             local paramName;
             local paramObj;
             local info;
             local initCap, allCaps;
             local targetObj;
             local newText;
             local prop;
 
             /* get the position of the next brace */
             idx = orig.find('{');
 
             /* 
              *   if there are no braces, the rest of the string is simply
              *   literal text; add the entire remainder of the string to
              *   the result, and we're done 
              */
             if (idx == nil)
             {
                 result += processResult(genLiteral(orig));
                 break;
             }
 
             /* add everything up to the brace to the result string */
             result += processResult(genLiteral(orig.substr(1, idx - 1)));
 
             /* 
              *   lop off everything up to and including the brace from the
              *   source string, since we're done with the part up to the
              *   brace now 
              */
             orig = orig.substr(idx + 1);
             
             /* 
              *   if the brace was the last thing in the source string, or
              *   it's a stuttered brace, add it literally to the result 
              */
             if (orig.length() == 0)
             {
                 /* 
                  *   nothing follows - add a literal brace to the result,
                  *   and we're done 
                  */
                 result += processResult('{');
                 break;
             }
             else if (orig.substr(1, 1) == '{')
             {
                 /* 
                  *   it's a stuttered brace - add a literal brace to the
                  *   result 
                  */
                 result += processResult('{');
 
                 /* remove the second brace from the source */
                 orig = orig.substr(2);
 
                 /* we're finished processing this brace - go back for more */
                 continue;
             }
 
             /* find the closing brace */
             idx = orig.find('}');
 
             /* 
              *   if there is no closing brace, include the brace and
              *   whatever follows as literal text 
              */
             if (idx == nil)
             {
                 /* add the literal brace to the result */
                 result += processResult('{');
 
                 /* we're done with the brace - go back for more */
                 continue;
             }
 
             /* 
              *   Pull out everything up to the brace as the parameter
              *   text.
              */
             paramStr = orig.substr(1, idx - 1);
 
             /* assume for now that we will have no parameter object */
             paramObj = nil;
 
             /* 
              *   drop everything up to and including the closing brace,
              *   since we've pulled it out for processing now 
              */
             orig = orig.substr(idx + 1);
 
             /* 
              *   Note the capitalization of the first two letters.  If
              *   they're both lower-case, we won't adjust the case of the
              *   substitution text at all.  If the first is a capital and
              *   the second isn't, we'll capitalize the first letter of
              *   the replacement text.  If they're both capitals, we'll
              *   capitalize the entire replacement text. 
              */
             initCap = (rexMatch(patUpper, paramStr) != nil);
             allCaps = (rexMatch(patAllCaps, paramStr) != nil);
 
             /* lower-case the entire parameter string for matching */
             paramStr = paramStr.toLower();
 
             /* perform any language-specific rewriting on the string */
             paramStr = langRewriteParam(paramStr);
 
             /*
              *   Figure out which format we have.  The allowable formats
              *   are:
              *   
              *   id
              *   obj
              *.  id obj
              *.  id1/id2
              *.  id1/id2 obj
              *.  id1 obj/id2
              */
             if (rexMatch(patIdObjSlashId, paramStr) != nil)
             {
                 /* we have the id1 obj/id2 format */
                 paramName = rexGroup(1)[3] + rexGroup(3)[3];
                 paramObj = rexGroup(2)[3];
             }
             else if (rexMatch(patIdObj, paramStr) != nil)
             {
                 /* we have 'id obj' or 'id1/id2 obj' */
                 paramName = rexGroup(1)[3];
                 paramObj = rexGroup(2)[3];
             }
             else
             {
                 /* we have no spaces, so we have no target object */
                 paramName = paramStr;
                 paramObj = nil;
             }
 
             /* look up our parameter name */
             info = paramTable_[paramName];
 
             /*
              *   If we didn't find it, and the parameter name contains a
              *   slash ('/'), try reversing the order of the parts before
              *   and after the slash. 
              */
             if (info == nil && rexMatch(patIdSlash, paramName) != nil)
             {
                 /* 
                  *   rebuild the name with the order of the parts
                  *   reversed, and look up the result 
                  */
                 info = paramTable_[rexGroup(2)[3] + '/' + rexGroup(1)[3]];
             }
 
             /*
              *   If we didn't find a match, simply put the entire thing in
              *   the result stream literally, including the braces. 
              */
             if (info == nil)
             {
                 /* 
                  *   We didn't find it, so try treating it as a string
                  *   parameter object.  Try getting the string from the
                  *   action.  
                  */
                 newText = gAction.getMessageParam(paramName);
                 if (dataType(newText) == TypeSString)
                 {
                     /* 
                      *   It's a valid string parameter.  Simply add the
                      *   literal text to the result.  If we're in html
                      *   mode, translate the string to ensure that any
                      *   markup-significant characters are properly quoted
                      *   so that they aren't taken as html themselves.  
                      */
                     result += processResult(newText.htmlify());
                 }
                 else
                 {
                     /* 
                      *   the parameter is completely undefined; simply add
                      *   the original text, including the braces 
                      */
                     result += processResult('{' + paramStr + '}');
                 }
                     
                 /* 
                  *   we're done with this substitution string - go back
                  *   for more 
                  */
                 continue;
             }
 
             /*
              *   If we have no target object specified in the substitution
              *   string, and the parameter name has an associated implicit
              *   target object, use the implied object. 
              */
             if (paramObj == nil && info[3] != nil)
                 paramObj = info[3];
 
             /* 
              *   If we have a target object name, ask the current action
              *   for the target object value.  Otherwise, use the same
              *   target object as the previous expansion.  
              */
             if (paramObj != nil)
             {
                 /* check for a current action */
                 if (gAction != nil)
                 {
                     /* get the target object by name through the action */
                     targetObj = gAction.getMessageParam(paramObj);
                 }
                 else
                 {
                     /* there's no action, so we don't have a value yet */
                     targetObj = nil;
                 }
 
                 /* 
                  *   if we didn't find a value, look up the name in our
                  *   global name table 
                  */
                 if (targetObj == nil)
                 {
                     /* look up the name */
                     targetObj = nameTable_[paramObj];
 
                     /* 
                      *   if we found it, and the result is a function
                      *   pointer or an anonymous function, invoke the
                      *   function to get the result 
                      */
                     if (dataTypeXlat(targetObj) == TypeFuncPtr)
                     {
                         /* evaluate the function */
                         targetObj = (targetObj)();
                     }
                 }
 
                 /* 
                  *   remember this for next time, in case the next
                  *   substitution string doesn't include a target object 
                  */
                 lastTargetObj_ = targetObj;
                 lastParamObj_ = paramObj;
             }
             else
             {
                 /* 
                  *   there's no implied or explicit target - use the same
                  *   one as last time 
                  */
                 targetObj = lastTargetObj_;
                 paramObj = lastParamObj_;
             }
 
             /* 
              *   if the target object wasn't found, treat the whole thing
              *   as a failure - put the entire parameter string back in
              *   the result stream literally 
              */
             if (targetObj == nil)
             {
                 /* add it to the output literally, and go back for more */
                 result += processResult('{' + paramStr + '}');
                 continue;
             }
 
             /* get the property to call on the target */
             prop = getTargetProp(targetObj, paramObj, info);
 
             /* evaluate the parameter's associated property on the target */
             newText = targetObj.(prop);
 
             /* apply the appropriate capitalization to the result */
             if (allCaps)
                 newText = newText.toUpper();
             else if (initCap)
                 newText = newText.substr(1, 1).toUpper() + newText.substr(2);
 
             /* 
              *   append the new text to the output result so far, and
              *   we're finished with this round 
              */
             result += processResult(newText);
         }
 
         /* return the result string */
         return result;
     }
 
     /*
      *   Get the property to invoke on the target object for the given
      *   parameter information entry.  By default, we simply return
      *   info[2], which is the standard property to call on the target.
      *   This can be overridden by the language-specific subclass to
      *   provide a different property if appropriate.
      *   
      *   'targetObj' is the target object, and 'paramObj' is the parameter
      *   name of the target object.  For example, 'paramObj' might be the
      *   string 'dobj' to represent the direct object, in which case
      *   'targetObj' will be the gDobj object.
      *   
      *   The English version, for example, uses this routine to supply a
      *   reflexive instead of the default entry when the target object
      *   matches the subject of the sentence.  
      */
     getTargetProp(targetObj, paramObj, info)
     {
         /* return the standard property mapping from the parameter info */
         return info[2];
     }
 
     /*
      *   Process result text.  This takes some result text that we're
      *   about to add and returns a processed version.  This is called for
      *   all text as we add it to the result string.
      *   
      *   The text we pass to this method has already had all parameter
      *   text fully expanded, so this routine does not need to worry about
      *   { } sequences - all { } sequences will have been removed and
      *   replaced with the corresponding expansion text before this is
      *   called.
      *   
      *   This routine is called piecewise: the routine will be called once
      *   for each parameter replacement text and once for each run of text
      *   between parameters, and is called in the order in which the text
      *   appears in the original string.
      *   
      *   By default we do nothing with the result text; we simply return
      *   the original text unchanged.  The language-specific subclass can
      *   override this as desired to further modify the text for special
      *   language-specific parameterization outside of the { } mechanism.
      *   The subclass can also use this routine to maintain internal state
      *   that depends on sentence structure.  For example, the English
      *   version looks for sentence-ending punctuation so that it can
      *   reset its internal notion of the subject of the sentence when a
      *   sentence appears to be ending.  
      */
     processResult(txt) { return txt; }
 
     /*
      *   "Quote" a message - double each open brace, so that braces in the
      *   message will be taken literally when run through the substitution
      *   replacer.  This can be used when a message is expanded prior to
      *   being displayed to ensure that braces in the result won't be
      *   mistaken as substitution parameters requiring further expansion.
      *   
      *   Note that only open braces need to be quoted, since lone close
      *   braces are ignored in the substitution process.  
      */
     quoteMessage(str)
     {
         return str.findReplace('{', '{{', ReplaceAll);
     }
 
     /*
      *   Internal routine - generate the literal text for the given source
      *   string.  We'll remove any stuttered close braces. 
      */
     genLiteral(str)
     {
         /* replace all '}}' sequences with '}' sequences */
         return str.findReplace('}}', '}', ReplaceAll);
     }
 
     /*
      *   execute pre-initialization 
      */
     execute()
     {
         /* create a lookup table for our parameter names */
         paramTable_ = new LookupTable();
 
         /* add each element of our list to the table */
         foreach (local cur in paramList_)
             paramTable_[cur[1]] = cur;
 
         /* create a lookup table for our global names */
         nameTable_ = new LookupTable();
 
         /* 
          *   Add an entry for 'actor', which resolves to gActor if there is
          *   a gActor when evaluated, or the current player character if
          *   not.  Note that using a function ensures that we evaluate the
          *   current gActor or gPlayerChar each time we need the 'actor'
          *   value.  
          */
         nameTable_['actor'] = {: gActor != nil ? gActor : gPlayerChar };
     }
 
     /*
      *   Our output filter method.  We'll run each string written to the
      *   display through our parameter substitution method.  
      */
     filterText(ostr, txt)
     {
         /* substitute any parameters in the string and return the result */
         return generateMessage(txt);
     }
 
     /*
      *   The most recent target object.  Each time we parse a substitution
      *   string, we'll remember the target object here; when a
      *   substitution string doesn't imply or specify a target object,
      *   we'll use the previous one by default. 
      */
     lastTargetObj_ = nil
 
     /* the parameter name of the last target object ('dobj', 'actor', etc) */
     lastParamObj_ = nil
 
     /* our parameter table - a LookupTable that we set up during preinit */
     paramTable_ = nil
 
     /* our global name table - a LookupTable we set up during preinit */
     nameTable_ = nil
 
     /*
      *   Rewrite the parameter string for any language-specific rules.  By
      *   default, we'll return the original parameter string unchanged;
      *   the language-specific instance can override this to provide any
      *   special syntax extensions to the parameter string syntax desired
      *   by the language-specific library.  The returned string must be in
      *   one of the formats recognized by the generic handler.  
      */
     langRewriteParam(paramStr)
     {
         /* by default, return the original unchanged */
         return paramStr;
     }
 
     /* 
      *   our parameter list - this should be initialized in the
      *   language-specific subclass to a list like this:
      *   
      *   [entry1, entry2, entry3, ...]
      *   
      *   Each entry is a list like this:
      *   
      *   [paramName, &prop, impliedTargetName, <extra>]
      *   
      *   paramName is a string giving the substitution parameter name;
      *   this can be one word or two ('the' or 'the obj', for example).
      *   
      *   prop is a property identifier.  This is the property invoked on
      *   the target object to obtain the substitution text.
      *   
      *   impliedTargetName is a string giving the target object name to
      *   use.  When this is supplied, the paramName is normally used in
      *   message text with no object name.  This should be nil for
      *   parameters that do not imply a particular target.
      *   
      *   <extra> is any number of additional parameters for the
      *   language-specific subclass.  The generic code ignores these extra
      *   parameters, but the langague-specific subclass can use them if it
      *   requires additional information.
      *   
      *   Here's an example:
      *   
      *   paramList_ = [
      *.                ['you', &theDesc, nil, 'actor'],
      *.                ['the obj' &theObjDesc, &itReflexive, nil]
      *.  ]
      *   
      *   The first item specifies a substitution name of 'you', which is
      *   expanded by evaluating the property theDesc on the target object,
      *   and specifies an implied target object of 'actor'.  When this is
      *   expanded, we'll call the current action to get the meaning of
      *   'actor', then evaulate property theDesc on the result.
      *   
      *   The second item specifies a substitution name of 'the obj',
      *   expanded by evaluating property theObjDesc on the target object.
      *   This one doesn't have an implied object, so the target object is
      *   the one explicitly given in the message source text or is the
      *   previous target object if one isn't specified in the message
      *   text.  
      */
     paramList_ = []
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   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. 
                      */
                     ret += gLibMessages.commandResultsPrefix;
 
                     /* 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 */
                     ret += gLibMessages.commandInterruptionPrefix;
                     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. 
                      */
                     ret += gLibMessages.commandResultsSeparator;
 
                     /* 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 
                      */
                     writeThrough(gLibMessages.commandResultsEmpty());
                     break;
 
                 case stateBetweenCommands:
                 case stateInCommand:
                     /* 
                      *   we've written at least one command result, so
                      *   show the after-command separator 
                      */
                     writeThrough(gLibMessages.commandResultsSuffix());
                     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
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   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);
         addOutputFilter(langMessageBuilder);
     }
 
     /* 
      *   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
 ;
 
TADS 3 Library Manual
Generated on 9/8/2006 from TADS version 3.0.11