# input.t | documentation

    #charset "us-ascii"

    /*
     *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved.
     *
     *   TADS 3 Library: input
     *
     *   This modules defines functions and objects related to reading input
     *   from the player.
     */

    #include "adv3.h"

    /* ------------------------------------------------------------------------ */
    /*
     *   Keyboard input parameter definition.
     */
    class InputDef: object
        /*
         *   The prompt function.  This is a function pointer (which is
         *   frequently given as an anonymous function) or nil; if it's nil,
         *   we won't show any prompt at all, otherwise we'll call the
         *   function pointer to display a prompt as needed.
         */
        promptFunc = nil

        /*
         *   Allow real-time events.  If this is true, we'll allow real-time
         *   events to interrupt the input; if it's nil, we'll freeze the
         *   real-time clock while reading input.
         */
        allowRealTime = nil

        /*
         *   Begin the input style.  This should do anything required to set
         *   the font to the desired attributes for the input text.  By
         *   default, we'll simply display <.inputline> to set up the default
         *   input style.
         */
        beginInputFont() { "<.inputline>"; }

        /*
         *   End the input style.  By default, we'll close the <.inputline>
         *   that we opened in beginInputFont().
         */
        endInputFont() { "<./inputline>"; }
    ;

    /*
     *   Basic keyboard input parameter definition.  This class defines
     *   keyboard input parameters with the real-time status and prompt
     *   function specified via the constructor.
     */
    class BasicInputDef: InputDef
        construct(allowRealTime, promptFunc)
        {
            self.allowRealTime = allowRealTime;
            self.promptFunc = promptFunc;
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Keyboard input manager.
     */
    inputManager: PostRestoreObject
        /*
         *   Read a line of input from the keyboard.
         *
         *   If allowRealTime is true, we'll execute any real-time events that
         *   are already due to run, and then we'll allow the input to be
         *   interrupted by real-time events, if interrupted input is
         *   supported on the local platform.  Otherwise, we will not process
         *   any real-time events.
         *
         *   promptFunc is a callback function to invoke to display the
         *   prompt.  This is provided as a callback so that we can re-display
         *   the prompt as necessary after real-time event interruptions.
         *   Note that if real-time interruption is not to be allowed, the
         *   caller can simply display the prompt before calling this routine
         *   rather than passing in a prompt callback, if desired.
         *
         *   If we're in HTML mode, this will switch into the 'tads-input'
         *   font while reading the line, so this routine should be used
         *   wherever possible rather than calling inputLine() or
         *   inputLineTimeout() directly.
         */
        getInputLine(allowRealTime, promptFunc)
        {
            /* read input using a basic InputDef for the given parameters */
            return getInputLineExt(new BasicInputDef(allowRealTime, promptFunc));
        }

        /*
         *   Read a line of input from the keyboard - extended interface,
         *   using the InputDef object to define the input parameters.
         *   'defObj' is an instance of class InputDef, defining how we're to
         *   handle the input.
         */
        getInputLineExt(defObj)
        {
            /* make sure the command transcript is flushed */
            if (gTranscript != nil)
                gTranscript.flushForInput();

            /*
             *   If a previous input was in progress, cancel it - this must be
             *   a recursive entry from a real-time event that's interrupting
             *   the enclosing input attempt. Simply cancel out the enclosing
             *   read attempt entirely in this case; if and when we return to
             *   the enclosing reader, that reader will start over with a
             *   fresh read attempt at that point.
             */
            cancelInputInProgress(true);

            /*
             *   Keep going until we finish reading the command.  We might
             *   have to try several times, because our attempts might be
             *   interrupted by real-time events.
             */
            for (;;)
            {
                local result;
                local timeout;
                local t0;

                /* note the starting time, in case we want to freeze the clock */
                t0 = realTimeManager.getElapsedTime();

                /* process real-time events, if possible */
                timeout = processRealTimeEvents(defObj.allowRealTime);

                /* show the prompt and any pre-input codes */
                inputLineBegin(defObj);

            getInput:
                /*
                 *   Read the input.  (Note that if our timeout is nil, this
                 *   will simply act like the ordinary untimed inputLine.)
                 */
                result = aioInputLineTimeout(timeout);

                /*
                 *   If we're not allowing real-time event processing, freeze
                 *   the clock during the read - set the elapsed game
                 *   real-time clock back to the value it had on entry, so
                 *   that the input effectively consumes no real time.
                 */
                if (!defObj.allowRealTime)
                    realTimeManager.setElapsedTime(t0);

                /* check the event code from the result list */
                switch(result[1])
                {
                case InEvtNoTimeout:
                    /*
                     *   the platform doesn't support timeouts - note it for
                     *   future reference so that we don't ask for input with
                     *   timeout again, then go back to try the input again
                     *   without a timeout
                     */
                    noInputTimeout = true;
                    timeout = nil;
                    goto getInput;

                case InEvtLine:
                    /* we've finished the current line - end input mode */
                    inputLineEnd();

                    /* return the line of text we got */
                    return result[2];

                case InEvtTimeout:
                    /*
                     *   We got a timeout without finishing the input line.
                     *   This means that we've reached the time when the next
                     *   real-time event is ready to execute.  Simply continue
                     *   looping; we'll process all real-time events that are
                     *   ready to go, then we'll resume reading the command.
                     *
                     *   Before we proceed, though, notify the command
                     *   sequencer (via the command-interrupt pseudo-tag) that
                     *   we're at the start of output text after an interrupted
                     *   command line input
                     */
                    "<.commandint>";
                    break;

                case InEvtEof:
                    /*
                     *   End of file - this indicates that the user has closed
                     *   down the application, or that the keyboard has become
                     *   unreadable due to a hardware or OS error.
                     *
                     *   Write a blank line to the display in an attempt to
                     *   flush any partially-entered command line text, then
                     *   throw an error to signal the EOF condition.
                     */
                    "\b";
                    throw new EndOfFileException();

                case InEvtEndQuietScript:
                    /*
                     *   End of "quiet" script - this indicates that we've
                     *   been reading input from a script file, but we've now
                     *   reached the end of that file and are about to return
                     *   to reading from the keyboard.
                     *
                     *   "Quiet script" mode causes all output to be hidden
                     *   while the script is being processed.  This means that
                     *   we won't have displayed a prompt for the current
                     *   line, or updated the status line.  We'll
                     *   automatically display a new prompt when we loop back
                     *   for another line of input, but we have to mark the
                     *   current input line as actually ended now for that to
                     *   happen.
                     */
                    inputLineInProgress = nil;
                    inProgressDefObj = nil;

                    /*
                     *   update the status line, since the quiet script mode
                     *   will have suppressed all status line updates while we
                     *   were reading the script, and thus the last update
                     *   before this prompt won't have been shown
                     */
                    statusLine.showStatusLine();

                    /* back for more */
                    break;

                case 'newGuest':
                    /*
                     *   Synthetic "new guest" event from the Web UI.  This
                     *   indicates that a new user has joined the session.  The
                     *   parameter is the new user's screen name.  Announce the
                     *   new user's arrival as a real-time event, and go back
                     *   to reading input.
                     */
                    "<.commandint>";
                    libMessages.webNewUser(result[2]);
                    break;

                case 'logError':
                    /*
                     *   Synthetic "log error" event from the Web UI.  The UI
                     *   posts this type of an event when an error occurs in an
                     *   asynchronous task, where it's not possible to display
                     *   an error message directly.
                     */
                    "<.commandint>\b<<result[2]>>\b";
                    break;
                }
            }
        }

        /*
         *   Pause for a MORE prompt.  If freezeRealTime is true, we'll stop
         *   the real-time clock; otherwise we'll let it keep running.  Even if
         *   we don't freeze the clock, we won't actually process any real-time
         *   events while waiting: the point of the MORE prompt is to allow the
         *   player to read and acknowledge the on-screen display before
         *   showing anything new, so we don't want to allow any output to
         *   result from real-time events that occur while waiting for user
         *   input.  If any real-time events come due while we're waiting,
         *   we'll process them when we're done.
         *
         *   In order to ensure that the display makes sense to the user, we
         *   flush any captured input in the transcript before pausing.  We
         *   re-activate transcript capture after the pause if it was active
         *   before.  Note that in some cases, this could affect the overall
         *   output for the command, since some commands wait until the very
         *   end of the command to go back and process the entire transcript
         *   for the command.  Since we interrupt the transcript, flushing any
         *   output that occurred before the pause, a command that goes back
         *   over its entire output stream at the end of the turn won't be able
         *   to see or modify any of the output that occurred prior to the
         *   pause, since we will have flushed the output to that point.
         */
        pauseForMore(freezeRealTime)
        {
            local t0;
            local wasTranscriptActive = nil;

            /*
             *   flush any command transcript and turn off transcript capture,
             *   so that we show any pent-up reports before pausing for the
             *   MORE prompt
             */
            if (gTranscript != nil)
                wasTranscriptActive = gTranscript.flushForInput();

            /*
             *   cancel any pending input - we must be interrupting the
             *   pending input with a real-time event
             */
            cancelInputInProgress(true);

            /* note the starting time, in case we want to freeze the clock */
            t0 = realTimeManager.getElapsedTime();

            /* run the MORE prompt */
            aioMorePrompt();

            /* if the transcript was previously active, re-activate it */
            if (wasTranscriptActive)
                gTranscript.activate();

            /*
             *   if the caller wanted us to freeze the clock, restore the
             *   elapsed game real time to what it was when we started, so
             *   that the time the player took to acknowledge the MORE prompt
             *   won't count against the elapsed game time; otherwise, process
             *   any real-time events that came due while we were waiting
             */
            if (freezeRealTime)
            {
                /* time was frozen - restore the original elapsed time */
                realTimeManager.setElapsedTime(t0);
            }
            else
            {
                /*
                 *   time wasn't frozen - check for any events that have come
                 *   due since we started waiting, and process them
                 *   immediately
                 */
                processRealTimeEvents(true);
            }
        }

        /*
         *   Ask for an input file.  Freezes the real-time event clock for the
         *   duration of reading the event.
         */
        getInputFile(prompt, dialogType, fileType, flags)
        {
            /*
             *   note the game elapsed time before we start - we want to
             *   freeze the real-time clock while we're waiting for the user
             *   to respond, since this system verb exists outside of the
             *   usual time flow of the game
             */
            local origElapsedTime = realTimeManager.getElapsedTime();

            /* ask for a file */
            local result = aioInputFile(prompt, dialogType, fileType, flags);

            /*
             *   restore the game real-time counter to what it was before we
             *   started the interactive response
             */
            realTimeManager.setElapsedTime(origElapsedTime);

            /* return the result from inputFile */
            return result;
        }

        /*
         *   Ask for input through a dialog.  Freezes the real-time clock for
         *   the duration of the dialog display.  The arguments are the same as
         *   for the built-in inputDialog() function.
         */
        getInputDialog(icon, prompt, buttons, defaultButton, cancelButton)
        {
            /*
             *   note the current elapsed game real time, so we can restore it
             *   after the dialog is done
             */
            local origElapsedTime = realTimeManager.getElapsedTime();

            /* show the dialog */
            local result = aioInputDialog(icon, prompt, buttons,
                                          defaultButton, cancelButton);

            /*
             *   restore the real-time counter, so that the time spent in the
             *   dialog doesn't count
             */
            realTimeManager.setElapsedTime(origElapsedTime);

            /* return the dialog result */
            return result;
        }

        /*
         *   Read a keystroke, processing real-time events while waiting, if
         *   desired.  'allowRealTime' and 'promptFunc' work the same way they
         *   do with getInputLine().
         */
        getKey(allowRealTime, promptFunc)
        {
            local evt;

            /* get an event */
            evt = getEventOrKey(allowRealTime, promptFunc, true);

            /*
             *   the only event that getEventOrKey will return is a keystroke,
             *   so return the keystroke from the event record
             */
            return evt[2];
        }

        /*
         *   Read an event, processing real-time events while waiting, if
         *   desired.  'allowRealTime' and 'promptFunc' work the same way they
         *   do with getInputLine().
         */
        getEvent(allowRealTime, promptFunc)
        {
            /* read and return an event */
            return getEventOrKey(allowRealTime, promptFunc, nil);
        }

        /*
         *   Read an event or keystroke.  'allowRealTime' and 'promptFunc' work
         *   the same way they do in getInputLine().  If 'keyOnly' is true,
         *   then we're only interested in keystroke events, and we'll ignore
         *   any other events entered.
         *
         *   Note that this routine is not generally called directly; callers
         *   should usually call the convenience routines getKey() or
         *   getEvent(), as needed.
         */
        getEventOrKey(allowRealTime, promptFunc, keyOnly)
        {
            /* make sure the command transcript is flushed */
            if (gTranscript != nil)
                gTranscript.flushForInput();

            /*
             *   Cancel any in-progress input.  If there's an in-progress
             *   input, a real-time event must be interrupting the input,
             *   which is recursively invoking us to start a new input.
             */
            cancelInputInProgress(true);

            /* keep going until we get a keystroke or other event */
            for (;;)
            {
                local result;
                local timeout;
                local t0;

                /* note the starting time, in case we want to freeze the clock */
                t0 = realTimeManager.getElapsedTime();

                /* process real-time events, if possible */
                timeout = processRealTimeEvents(allowRealTime);

                /* show the prompt and any pre-input codes */
                inputEventBegin(promptFunc);

            getInput:
                /*
                 *   Read the input.  (Note that if our timeout is nil, this
                 *   will simply act like the ordinary untimed inputLine.)
                 */
                result = aioInputEvent(timeout);

                /*
                 *   If we're not allowing real-time event processing, freeze
                 *   the clock during the read - set the elapsed game
                 *   real-time clock back to the value it had on entry, so
                 *   that the input effectively consumes no real time.
                 */
                if (!allowRealTime)
                    realTimeManager.setElapsedTime(t0);

                /* check the event code from the result list */
                switch(result[1])
                {
                case InEvtNoTimeout:
                    /*
                     *   the platform doesn't support timeouts - note it for
                     *   future reference so that we don't ask for input with
                     *   timeout again, then go back to try the input again
                     *   without a timeout
                     */
                    noInputTimeout = true;
                    timeout = nil;
                    goto getInput;

                case InEvtTimeout:
                    /*
                     *   We got a timeout without finishing the input line.
                     *   This means that we've reached the time when the next
                     *   real-time event is ready to execute.  Simply continue
                     *   looping; we'll process all real-time events that are
                     *   ready to go, then we'll restart the event wait.
                     */
                    break;

                case InEvtEof:
                    /*
                     *   End of file - this indicates that the user has closed
                     *   down the application, or that the keyboard has become
                     *   unreadable due to a hardware or OS error.
                     *
                     *   Write a blank line to the display in an attempt to
                     *   flush any partially-entered command line text, then
                     *   throw an error to signal the EOF condition.
                     */
                    "\b";
                    throw new EndOfFileException();

                case InEvtKey:
                    /* keystroke - finish the input and return the event */
                    inputEventEnd();
                    return result;

                case InEvtHref:
                    /*
                     *   Hyperlink activation - if we're allowed to return
                     *   events other than keystrokes, finish the input and
                     *   return the event; otherwise, ignore the event and keep
                     *   looping.
                     */
                    if (!keyOnly)
                    {
                        inputEventEnd();
                        return result;
                    }
                    break;

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

        /*
         *   Cancel input in progress.
         *
         *   If 'reset' is true, we'll clear any input state saved from the
         *   interrupted in-progress editing session; otherwise, we'll retain
         *   the saved editing state for restoration on the next input.
         *
         *   This MUST be called before calling tadsSay().  Games should
         *   generally never call tadsSay() directly (call the library
         *   function say() instead), so in most cases authors will not need
         *   to worry about calling this on output.
         *
         *   This MUST ALSO be called before performing any keyboard input.
         *   Callers using inputManager methods for keyboard operations won't
         *   have to worry about this, because the inputManager methods call
         *   this routine when necessary.
         */
        cancelInputInProgress(reset)
        {
            /* cancel the interpreter's internal input state */
            aioInputLineCancel(reset);

            /* if we were editing a command line, terminate the editing session */
            if (inputLineInProgress)
            {
                /* do our normal after-input work */
                inputLineEnd();
            }

            /* if we were waiting for event input, note that we are no longer */
            if (inputEventInProgress)
            {
                /* do our normal after-input work */
                inputEventEnd();
            }
        }

        /*
         *   Process any real-time events that are ready to run, and return the
         *   timeout until the next real-time event.
         *
         *   If allowRealTime is nil, we won't process real-time events at all;
         *   we'll merely return nil for the timeout to indicate to the caller
         *   that any user input interaction about to be attempted should wait
         *   indefinitely.
         */
        processRealTimeEvents(allowRealTime)
        {
            local timeout;

            /* presume we will not use a timeout */
            timeout = nil;

            /* process real-time events, if allowed */
            if (allowRealTime)
            {
                local tNext;

                /*
                 *   Process any real-time events that are currently ready to
                 *   execute, and note the amount of time until the next
                 *   real-time event is ready.
                 */
                tNext = realTimeManager.executeEvents();

                /*
                 *   If there's an event pending, note the interval between the
                 *   current time and the event's scheduled time - this will
                 *   give us the maximum amount of time we want to wait for the
                 *   user to edit the command line before interrupting to
                 *   execute the pending event.  Ignore this if the platform
                 *   doesn't support timeouts to begin with.
                 */
                if (tNext != nil && !noInputTimeout)
                    timeout = tNext - realTimeManager.getElapsedTime();
            }

            /* return the timeout until the next real-time event */
            return timeout;
        }

        /*
         *   Begin reading key/event input.  We'll cancel any report gatherer
         *   so that prompt text shows immediately, and show the prompt if
         *   desired.
         */
        inputEventBegin(promptFunc)
        {
            /* if we're not continuing previous input, show the prompt */
            if (!inputEventInProgress)
            {
                inputBegin(promptFunc);

                /* note that we're in input mode */
                inputEventInProgress = true;
            }
        }

        /*
         *   End keystroke/event input.
         */
        inputEventEnd()
        {
            /* if input is in progress, terminate it */
            if (inputEventInProgress)
            {
                /* note that we're no longer reading an event */
                inputEventInProgress = nil;
            }
        }

        /*
         *   Begin command line editing.  If we're in HTML mode, we'll show
         *   the appropriate codes to establish the input font.
         */
        inputLineBegin(defObj)
        {
            /* notify the command sequencer that we're reading a command */
            "<.commandbefore>";

            /* if we're not resuming a session, set up a new session */
            if (!inputLineInProgress)
            {
                /* begin input */
                inputBegin(defObj.promptFunc);

                /* switch to input font */
                defObj.beginInputFont();

                /* note that we're in input mode */
                inputLineInProgress = true;

                /* remember the parameter object for this input */
                inProgressDefObj = defObj;
            }
        }

        /*
         *   End command line editing.  If we're in HTML mode, we'll show the
         *   appropriate codes to close the input font.
         */
        inputLineEnd()
        {
            /* if input is in progress, terminate it */
            if (inputLineInProgress)
            {
                /* note that we're no longer reading a line of input */
                inputLineInProgress = nil;

                /* end input font mode */
                inProgressDefObj.endInputFont();

                /* notify the command sequencer that we're done reading */
                "<.commandafter>";

                /*
                 *   tell the main text area's output stream that we just
                 *   ended an input line
                 */
                mainOutputStream.inputLineEnd();

                /* forget the parameter object for the input */
                inProgressDefObj = nil;
            }
        }

        /*
         *   Begin generic input.  Cancels command report list capture, and
         *   shows the prompt if given.
         */
        inputBegin(promptFunc)
        {
            /*
             *   Turn off command transcript capture, if it's active.  Once
             *   we're soliciting input interactively, we can no longer
             *   usefully capture the text output of commands, but this is fine
             *   because we must be doing something for which capture isn't
             *   important anyway.  Reporting capture is used for things like
             *   selecting the kind of result to show, which clearly isn't a
             *   factor for actions involving interactive input.
             */
            if (gTranscript != nil)
                gTranscript.flushForInput();

            /* if we have a prompt, display it */
            if (promptFunc != nil)
                (promptFunc)();
        }

        /* receive post-restore notification */
        execute()
        {
            /*
             *   Reset the inputLine state.  If we had any previously
             *   interrupted input from the current interpreter session, forget
             *   it by canceling and resetting the input line.  If we had an
             *   interrupted line in the session being restored, forget about
             *   that, too.
             */
            aioInputLineCancel(true);
            inputLineInProgress = nil;
            inputEventInProgress = nil;

            /*
             *   Clear the inputLineTimeout disabling flag - we might be
             *   restoring the game on a different platform from the one where
             *   the game started, so we might be able to use timed command
             *   line input even if we didn't when we started the game.  By
             *   clearing this flag, we'll check again to see if we can
             *   perform timed input; if we can't, we'll just set the flag
             *   again, so there will be no harm done.
             */
            noInputTimeout = nil;
        }

        /*
         *   Flag: command line input is in progress.  If this is set, it means
         *   that we interrupted command-line editing by a timeout, so we
         *   should not show a prompt the next time we go back to the keyboard
         *   for input.
         */
        inputLineInProgress = nil

        /* the InputDef object for the input in progress */
        inProgressDefObj = nil

        /* flag: keystroke/event input is in progress */
        inputEventInProgress = nil

        /*
         *   Flag: inputLine does not support timeouts on the current platform.
         *   We set this when we get an InEvtNoTimeout return code from
         *   inputLineTimeout, so that we'll know not to try calling again with
         *   a timeout.  This applies to the current interpreter only, so we
         *   must ignore any value restored from a previously saved game, since
         *   the game might have been saved on a different platform.
         *
         *   Note that if this value is nil, it means only that we've never
         *   seen an InEvtNoTimeout return code from inputLineEvent - it does
         *   NOT mean that timeouts are supported locally.
         *
         *   We assume that the input functions are uniform in their treatment
         *   of timeouts; that is, we assume that if inputLineTimeout supports
         *   timeout, then so does inputEvent, and that if one doesn't support
         *   timeout, the other won't either.
         */
        noInputTimeout = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Read a command line from the player.  Displays the main command
     *   prompt and returns a line of input.
     *
     *   We process any pending real-time events before reading the command.
     *   If the local platform supports real-time command-line interruptions,
     *   we'll continue processing real-time events as they occur in the
     *   course of command editing.
     */
    readMainCommand(which)
    {
        local str;

        /* execute any pre-command-prompt daemons */
        eventManager.executePrompt();

        /*
         *   Read a line of input, allowing real-time event processing, and
         *   return the line of text we read.  Use the appropriate main
         *   command prompt for the given prompt mode.
         */
        str = inputManager.getInputLine(
            true, {: gLibMessages.mainCommandPrompt(which) });

        /* return the string we read */
        return str;
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   End-of-file exception - this is thrown when readMainCommand()
     *   encounters end of file reading the console input.
     */
    class EndOfFileException: Exception
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   'Quitting' exception.  This isn't an error - it merely indicates that
     *   the user has explicitly asked to quit the game.
     */
    class QuittingException: Exception
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Base class for command input string preparsers.
     *
     *   Preparsers must be registered in order to run.  During
     *   preinitialization, we will automatically register any existing
     *   preparser objects; preparsers that are created dynamically during
     *   execution must be registered explicitly, which can be accomplished by
     *   inheriting the default constructor from this class.
     */
    class StringPreParser: PreinitObject
        /*
         *   My execution order number.  When multiple preparsers are
         *   registered, we'll run the preparsers in ascending order of this
         *   value (i.e., smallest runOrder goes first).
         */
        runOrder = 100

        /*
         *   Do our parsing.  Each instance should override this method to
         *   define the parsing that it does.
         *
         *   'str' is the string to parse, and 'which' is the rmcXxx enum
         *   giving the type of command we're working with.
         *
         *   This method returns a string or nil.  If the method returns a
         *   string, the caller will forget the original string and work from
         *   here on out with the new version returned; this allows the method
         *   to rewrite the original input as desired.  If the method returns
         *   nil, it means that the string has been fully handled and that
         *   further parsing of the same string is not desired.
         */
        doParsing(str, which)
        {
            /* return the original string unchanged */
            return str;
        }

        /*
         *   construction - when we dynamically create a preparser, register
         *   it by default
         */
        construct()
        {
            /* register the preparser */
            StringPreParser.registerPreParser(self);
        }

        /* run pre-initialization */
        execute()
        {
            /* register the preparser if it's not already registered */
            StringPreParser.registerPreParser(self);
        }

        /* register a preparser */
        registerPreParser(pp)
        {
            /* if the preparser isn't already in our list, add it */
            if (regList.indexOf(pp) == nil)
            {
                /* append this new item to the list */
                regList.append(pp);

                /* the list is no longer sorted */
                regListSorted = nil;
            }
        }

        /*
         *   Class method - Run all preparsers.  Returns the result of
         *   successively calling each preparser on the given string.
         */
        runAll(str, which)
        {
            /*
             *   if the list of preparsers isn't sorted, sort it in ascending
             *   order of execution order number
             */
            if (!regListSorted)
            {
                /* sort the list */
                regList.sort(SortAsc, {x, y: x.runOrder - y.runOrder});

                /* the list is now sorted */
                regListSorted = true;
            }

            /* run each preparser */
            foreach (local cur in regList)
            {
                /* run this preparser */
                str = cur.doParsing(str, which);

                /*
                 *   if the result is nil, it means that the string has been
                 *   fully handled, so we need not run any further preparsing
                 */
                if (str == nil)
                    return nil;
            }

            /* return the result of the series of preparsing steps */
            return str;
        }

        /* class property containing the list of registered parsers */
        regList = static new Vector(10)

        /* class property - the registration list has been sorted */
        regListSorted = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   The "comment" pre-parser.  If the command line starts with a special
     *   prefix string (by default, "*", but this can be changed via our
     *   commentPrefix property), this pre-parser intercepts the command,
     *   treating it as a comment from the player and otherwise ignoring the
     *   entire input line.  The main purpose is to give players a way to put
     *   comments into recorded transcripts, as notes to themselves when later
     *   reviewing the transcripts or as notes to the author when submitting
     *   play-testing feedback.
     */
    commentPreParser: StringPreParser
        doParsing(str, which)
        {
            /* get the amount of leading whitespace, so we can ignore it */
            local sp = rexMatch(leadPat, str);

            /*
             *   if the command line starts with the comment prefix, treat it
             *   as a comment
             */
            if (str.substr(sp + 1, commentPrefix.length()) == commentPrefix)
            {
                /*
                 *   It's a comment.
                 *
                 *   If a transcript is being recorded, simply acknowledge the
                 *   comment; if not, acknowledge it, but with a warning that
                 *   the comment isn't being saved anywhere
                 */
                if (scriptStatus.scriptFile != nil)
                    gLibMessages.noteWithScript;
                else if (warningCount++ == 0)
                    gLibMessages.noteWithoutScriptWarning;
                else
                    gLibMessages.noteWithoutScript;

                /*
                 *   Otherwise completely ignore the command line.  To do this,
                 *   simply return nil: this tells the parser that the command
                 *   has been fully handled by the preparser.
                 */
                return nil;
            }
            else
            {
                /* it's not a command - return the string unchanged */
                return str;
            }
        }

        /*
         *   The comment prefix.  You can change this to any character, or to
         *   any sequence of characters (longer sequences, such as '//', will
         *   work fine).  If a command line starts with this exact string (or
         *   starts with whitespace followed by this string), we'll consider
         *   the line to be a comment.
         */
        commentPrefix = '*'

        /*
         *   The leading-whitespace pattern.  We skip any text that matches
         *   this pattern at the start of a command line before looking for the
         *   comment prefix.
         *
         *   If you don't want to allow leading whitespace before the comment
         *   prefix, you can simply change this to '' - a pattern consisting of
         *   an empty string always matches zero characters, so it will prevent
         *   us from skipping any leading charactres in the player's input.
         */
        leadPat = static new RexPattern('<space>*')

        /* warning count for entering comments without SCRIPT in effect */
        warningCount = 0

        /*
         *   Use a lower execution order than the default, so that we run
         *   before most other pre-parsers.  Most other pre-parsers are written
         *   to handle actual commands, so it's usually just a waste of time to
         *   have them look at comments at all - and can occasionally be
         *   problematic, since the free-form text of a comment could confuse a
         *   pre-parser that's expecting a more conventional command format.
         *   When the comment pre-parser detects a comment, it halts any
         *   further processing of the command - so by running ahead of other
         *   pre-parsers, we'll effectively bypass other pre-parsers when we
         *   detect a comment.
         */
        runOrder = 50
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Read a line of text and return the token list and the original text.
     *   We keep going until a non-empty line of text is read.
     *
     *   'which' is one of the rmcXxx enum values specifying what kind of
     *   command line we're reading.
     *
     *   The return value is a list of two elements.  The first element is the
     *   string entered, and the second element is the token list.
     */
    readMainCommandTokens(which)
    {
        local str;
        local toks;

        /* keep going until we get a non-empty command line */
        for (;;)
        {
            /* read a command line */
            str = readMainCommand(which);

            /* run any preparsing desired on the string */
            str = StringPreParser.runAll(str, which);

            /*
             *   if preparsing returned nil, it means that the preparser fully
             *   handled the string - simply return nil to tell the caller
             *   that its work is done
             */
            if (str == nil)
                return nil;

            try
            {
                /* tokenize the command string */
                toks = cmdTokenizer.tokenize(str);
            }
            catch (TokErrorNoMatch tokExc)
            {
                /*
                 *   Invalid tokens in the response - complain about it.  Flag
                 *   the error as being in the first character of the
                 *   remaining string, since that's the character for which we
                 *   could find no match.
                 */
                gLibMessages.invalidCommandToken(tokExc.curChar_.htmlify());

                /* go back for another input line */
                continue;
            }

            /* if we got a non-empty token list, return it */
            if (toks.length() != 0)
                return [str, toks];

            /* show the empty-command reply */
            gLibMessages.emptyCommandResponse();
        }
    }

TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3