#charset "us-ascii"
#include "advlite.h"
/*
* ***************************************************************************
* output.t
*
* This module forms part of the adv3Lite library (c) 2012-24 Eric Eve, based
* heavily in parts on the equivalent code in adv3 (c) Micheal J. Roberts.
*/
/* ------------------------------------------------------------------------ */
/*
* The standard library output function. We set this up as the default
* display function (for double-quoted strings and for "<< >>"
* embeddings). Code can also call this directly to display items.
*/
say(val)
{
/*
* Use the dmsg() function to pass a string value through the message substitution parameter
* filter and the enumTabObj to translate an enum into its string equivalent, otherwise output
* the value directly.
*/
switch(dataType(val))
{
case TypeSString:
dmsg(val);
break;
case TypeEnum:
local str = enumTabObj.enumTab[val];
if(str)
{
oSay(str);
break;
}
/* Otherwise deliberate fall-through */
default:
oSay(val);
}
// if(dataType(val) == TypeSString)
// {
// dmsg(val);
// }
// else
// oSay(val);
//
}
/*
* Send a value straight to the output stream without any further message
* filtering.
*/
oSay(val)
{
outputManager.curOutputStream.writeToStream(val);
}
/*
* A version of say() that avoids the cquote filter that can make havoc of
* some HTML strings, especially those generated by HRef (the cquote filter
* turns straight quotes into typographical ones, which is undesirable when
* straight quotes are used as part of HTML markup).
*/
htmlSay(val)
{
try
{
/* Deactivate the cquote filter. */
cquoteOutputFilter.deactivate();
/* Display the text */
oSay(val);
}
finally
{
/* reactivate the cquote filter */
cquoteOutputFilter.activate();
}
}
/*
* A version of say() that only produces output if the player can see obj (or,
* optionally, sense obj by some other sense passed as a canXXX method of the
* Query object via the prop parameter) */
senseSay(val, obj, prop = &canSee)
{
if(Q.(prop)(gPlayerChar, obj))
say(val);
}
/* ------------------------------------------------------------------------ */
/*
* Generate a string for showing quoted text. We simply enclose the
* text in a <Q>...</Q> tag sequence and return the result.
*/
withQuotes(txt)
{
return '<q><<txt>></q>';
}
/* ------------------------------------------------------------------------ */
/*
* Output Manager. This object contains global code for displaying text
* on the console.
*
* The output manager is transient because we don't want its state to be
* saved and restored; the output manager state is essentially part of
* the intepreter user interface, which is not affected by save and
* restore.
*/
transient outputManager: object
/*
* Switch to a new active output stream. Returns the previously
* active output stream, so that the caller can easily restore the
* old output stream if the new output stream is to be established
* only for a specific duration.
*/
setOutputStream(ostr)
{
local oldStr;
/* remember the old stream for a moment */
oldStr = curOutputStream;
/* set the new output stream */
curOutputStream = ostr;
/*
* return the old stream, so the caller can restore it later if
* desired
*/
return oldStr;
}
/*
* run the given function, using the given output stream as the
* active default output stream
*/
withOutputStream(ostr, func)
{
/* establish the new stream */
local oldStr = setOutputStream(ostr);
/* make sure we restore the old active stream on the way out */
try
{
/* invoke the callback */
(func)();
}
finally
{
/* restore the old output stream */
setOutputStream(oldStr);
}
}
/* the current output stream - start with the main text stream */
curOutputStream = mainOutputStream
/*
* Is the UI running in HTML mode? This tells us if we have a full
* HTML UI or a text-only UI. Full HTML mode applies if we're
* running on a Multimedia TADS interpreter, or we're using the Web
* UI, which runs in a separate browser and is thus inherently
* HTML-capable.
*
* (The result can't change during a session, since it's a function
* of the game and interpreter capabilities, so we store the result
* on the first evaluation to avoid having to recompute it on each
* query. Since 'self' is a static object, we'll recompute this each
* time we run the program, which is important because we could save
* the game on one interpreter and resume the session on a different
* interpreter with different capabilities.)
*/
htmlMode = (self.htmlMode = checkHtmlMode())
;
/* ------------------------------------------------------------------------ */
/*
* Output Stream. This class provides a stream-oriented interface to
* displaying text on the console. "Stream-oriented" means that we write
* text as a sequential string of characters.
*
* Output streams are always transient, since they track the system user
* interface in the interpreter. The interpreter does not save its UI
* state with a saved position, so objects such as output streams that
* track the UI state should not be saved either.
*/
class OutputStream: PreinitObject
/*
* Write a value to the stream. If the value is a string, we'll
* display the text of the string; if it's nil, we'll ignore it; if
* it's anything else, we'll try to convert it to a string (with the
* toString() function) and display the resulting text.
*/
writeToStream(val)
{
/* if we have any prefix text, output it first */
if(prefix != nil)
{
writeFromStream(prefix);
prefix = nil;
}
/* convert the value to a string */
switch(dataType(val))
{
case TypeSString:
/*
* it's a string - no conversion is needed, but if it's
* empty, it doesn't count as real output (so don't notify
* anyone, and don't set any output flags)
*/
if (val == '')
return;
break;
case TypeNil:
case TypeTrue:
/* nil or true - don't display anything for this */
return;
case TypeInt:
case TypeObject:
/* convert integers and objects to strings */
val = toString(val);
break;
}
/* run it through our output filters */
val = applyFilters(val);
/*
* if, after filtering, we're not writing anything at all,
* there's nothing left to do
*/
if (val == nil || val == '')
return;
/* write the text to our underlying system stream */
writeFromStream(val);
}
/*
* Watch the stream for output. It's sometimes useful to be able to
* call out to some code and determine whether or not the code
* generated any text output. This routine invokes the given
* callback function, monitoring the stream for output; if any
* occurs, we'll return true, otherwise we'll return nil.
*/
watchForOutput(func)
{
local mon;
/* set up a monitor filter on the stream */
addOutputFilter(mon = new MonitorFilter());
/* catch any exceptions so we can remove our filter before leaving */
try
{
/* invoke the callback */
(func)();
/* return the monitor's status, indicating if output occurred */
return mon.outputFlag;
}
catch(ExitSignal ex)
{
return mon.outputFlag;
}
catch(ExitActionSignal ex)
{
return mon.outputFlag;
}
catch(AbortActionSignal ex)
{
return mon.outputFlag;
}
finally
{
/* remove our monitor filter */
removeOutputFilter(mon);
}
}
/*
* Call the given function, capturing all text output to this stream
* in the course of the function call. Return a string containing
* the captured text.
*/
captureOutput(func, [args])
{
/* install a string capture filter */
local filter = new StringCaptureFilter();
addOutputFilter(filter);
/* make sure we don't leave without removing our capturer */
try
{
/* invoke the function */
(func)(args...);
/* return the text that we captured */
return filter.txt_;
}
finally
{
/* we're done with our filter, so remove it */
removeOutputFilter(filter);
}
}
/*
* A Version of captureOutput that ignores an Exit Exception. This can be
* used to attempt to retrieve the string value of an output filter that
* threw an exit exeption.
*/
captureOutputIgnoreExit(func, [args])
{
/* install a string capture filter */
local filter = new StringCaptureFilter();
addOutputFilter(filter);
/* make sure we don't leave without removing our capturer */
try
{
/* invoke the function */
(func)(args...);
/* return the text that we captured */
return filter.txt_;
}
catch(ExitSignal ex)
{
return filter.txt_;
}
catch(ExitActionSignal ex)
{
return filter.txt_;
}
catch(AbortActionSignal ex)
{
return filter.txt_;
}
finally
{
/* we're done with our filter, so remove it */
removeOutputFilter(filter);
}
}
/* my associated input manager, if I have one */
myInputManager = nil
/* dynamic construction */
construct()
{
/*
* Set up filter list. Output streams are always transient, so
* make our filter list transient as well.
*/
filterList_ = new transient Vector(10);
}
/* execute pre-initialization */
execute()
{
/* do the same set-up we would do for dynamic construction */
construct();
}
/*
* Write text out from this stream; this writes to the lower-level
* stream underlying this stream. This routine is intended to be
* called only from within this class.
*
* Each output stream is conceptually "stacked" on top of another,
* lower-level stream. At the bottom of the stack is usually some
* kind of physical device, such as the display, or a file on disk.
*
* This method must be defined in each subclass to write to the
* appropriate underlying stream. Most subclasses are specifically
* designed to sit atop a system-level stream, such as the display
* output stream, so most implementations of this method will call
* directly to a system-level output function.
*/
writeFromStream(txt) { }
/*
* The list of active filters on this stream, in the order in which
* they are to be called. This should normally be initialized to a
* Vector in each instance.
*/
filterList_ = []
/*
* Add an output filter. The argument is an object of class
* OutputFilter, or any object implementing the filterText() method.
*
* Filters are always arranged in a "stack": the last output filter
* added is the first one called during output. This method thus
* adds the new filter at the "top" of the stack.
*/
addOutputFilter(filter)
{
/* add the filter to the end of our list */
filterList_.append(filter);
}
/*
* Add an output filter at a given point in the filter stack: add
* the filter so that it is "below" the given existing filter in the
* stack. This means that the new filter will be called just after
* the existing filter during output.
*
* If 'existingFilter' isn't in the stack of existing filters, we'll
* add the new filter at the "top" of the stack.
*/
addOutputFilterBelow(newFilter, existingFilter)
{
/* find the existing filter in our list */
local idx = filterList_.indexOf(existingFilter);
/*
* If we found the old filter, add the new filter below the
* existing filter in the stack, which is to say just before the
* old filter in our vector of filters (since we call the
* filters in reverse order of the list).
*
* If we didn't find the existing filter, simply add the new
* filter at the top of the stack, by appending the new filter
* at the end of the list.
*/
if (idx != nil)
filterList_.insertAt(idx, newFilter);
else
filterList_.append(newFilter);
}
/*
* Remove an output filter. Since filters are arranged in a stack,
* only the LAST output filter added may be removed. It's an error
* to remove a filter other than the last one.
*/
removeOutputFilter(filter)
{
/* get the filter count */
local len = filterList_.length();
/* make sure it's the last filter */
if (len == 0 || filterList_[len] != filter)
t3DebugTrace(T3DebugBreak);
/* remove the filter from my list */
filterList_.removeElementAt(len);
}
/* call the filters */
applyFilters(val)
{
/*
* Run through the list, applying each filter in turn. We work
* backwards through the list from the last element, because the
* filter list is a stack: the last element added is the topmost
* element of the stack, so it must be called first.
*/
for (local i in filterList_.length()..1 step -1 ; val != nil ; )
val = filterList_[i].filterText(self, val);
/* return the result of all of the filters */
return val;
}
/*
* Apply the current set of text transformation filters to a string.
* This applies only the non-capturing filters; we skip any capture
* filters.
*/
applyTextFilters(val)
{
/* run through the filter stack from top to bottom */
for (local i in filterList_.length()..1 step -1 ; val != nil ; )
{
/* skip capturing filters */
local f = filterList_[i];
if (f.ofKind(CaptureFilter))
continue;
/* apply the filter */
val = f.filterText(self, val);
}
/* return the result */
return val;
}
/*
* Receive notification from the input manager that we have just
* ended reading a line of input from the keyboard.
*/
inputLineEnd()
{
/* an input line ending doesn't look like a paragraph */
justDidPara = nil;
}
/*
* Internal state: we just wrote a paragraph break, and there has
* not yet been any intervening text. By default, we set this to
* true initially, so that we suppress any paragraph breaks at the
* very start of the text.
*/
justDidPara = true
/*
* Internal state: we just wrote a character that suppresses
* paragraph breaks that immediately follow. In this state, we'll
* suppress any paragraph marker that immediately follows, but we
* won't suppress any other characters.
*/
justDidParaSuppressor = nil
/* Text to be output before anything else */
prefix = nil
/* Set the prefix to txt */
setPrefix(txt)
{
prefix = txt;
}
;
/*
* The OutputStream for the main text area.
*
* This object is transient because the output stream state is
* effectively part of the interpreter user interface, which is not
* affected by save and restore.
*/
transient mainOutputStream: OutputStream
/*
* The main text area is the same place where we normally read
* command lines from the keyboard, so associate this output stream
* with the primary input manager.
*/
myInputManager = inputManager
/* the current command transcript */
curTranscript = nil
/* we sit atop the system-level main console output stream */
writeFromStream(txt)
{
/* write the text to the console */
aioSay(txt);
}
;
/* ------------------------------------------------------------------------ */
/*
* Paragraph manager. We filter strings as they're about to be sent to
* the console to convert paragraph markers (represented in the source
* text using the "style tag" format, <.P>) into a configurable display
* rendering.
*
* We also process the zero-spacing paragraph, <.P0>. This doesn't
* generate any output, but otherwise acts like a paragraph break in that
* it suppresses any paragraph breaks that immediately follow.
*
* The special marker <./P0> cancels the effect of a <.P0>. This can be
* used if you want to ensure that a newline or paragraph break is
* displayed, even if a <.P0> was just displayed.
*
* Our special processing ensures that paragraph tags interact with one
* another and with other display elements specially:
*
* - A run of multiple consecutive paragraph tags is treated as a single
* paragraph tag. This property is particularly important because it
* allows code to write out a paragraph marker without having to worry
* about whether preceding code or following code add paragraph markers
* of their own; if redundant markers are found, we'll filter them out
* automatically.
*
* - We can suppress paragraph markers following other specific
* sequences. For example, if the paragraph break is rendered as a blank
* line, we might want to suppress an extra blank line for a paragraph
* break after an explicit blank line.
*
* - We can suppress other specific sequences following a paragraph
* marker. For example, if the paragraph break is rendered as a newline
* plus a tab, we could suppress whitespace following the paragraph
* break.
*
* The paragraph manager should always be instantiated with transient
* instances, because this object's state is effectively part of the
* interpreter user interface, which doesn't participate in save and
* restore.
*/
class ParagraphManager: OutputFilter
/*
* Rendering - this is what we display on the console to represent a
* paragraph break. By default, we'll display a blank line.
*/
renderText = '\b'
/*
* Flag: show or hide paragraph breaks immediately after input. By
* default, we do not show paragraph breaks after an input line.
*/
renderAfterInput = nil
/*
* Preceding suppression. This is a regular expression that we
* match to individual characters. If the character immediately
* preceding a paragraph marker matches this expression, we'll
* suppress the paragraph marker in the output. By default, we'll
* suppress a paragraph break following a blank line, because the
* default rendering would add a redundant blank line.
*/
suppressBefore = static new RexPattern('\b')
/*
* Following suppression. This is a regular expression that we
* match to individual characters. If the character immediately
* following a paragraph marker matches this expression, we'll
* suppress the character. We'll apply this to each character
* following a paragraph marker in turn until we find one that does
* not match; we'll suppress all of the characters that do match.
* By default, we suppress additional blank lines after a paragraph
* break.
*/
suppressAfter = static new RexPattern('[\b\n]')
/* pre-compile some regular expression patterns we use a lot */
leadingMultiPat = static new RexPattern('(<langle><dot>[pP]0?<rangle>)+')
leadingSinglePat = static new RexPattern(
'<langle><dot>([pP]0?|/[pP]0)<rangle>')
/* process a string that's about to be written to the console */
filterText(ostr, txt)
{
local ret;
/* we don't have anything in our translated string yet */
ret = '';
/* keep going until we run out of string to process */
while (txt != '')
{
local len;
local match;
local p0;
local unp0;
/*
* if we just wrote a paragraph break, suppress any
* character that matches 'suppressAfter', and suppress any
* paragraph markers that immediately follow
*/
if (ostr.justDidPara)
{
/* check for any consecutive paragraph markers */
if ((len = rexMatch(leadingMultiPat, txt)) != nil)
{
/* discard the consecutive <.P>'s, and keep going */
txt = txt.substr(len + 1);
continue;
}
/* check for a match to the suppressAfter pattern */
if (rexMatch(suppressAfter, txt) != nil)
{
/* discard the suppressed character and keep going */
txt = txt.substr(2);
continue;
}
}
/*
* we have a character other than a paragraph marker, so we
* didn't just scan a paragraph marker
*/
ostr.justDidPara = nil;
/*
* if we just wrote a suppressBefore character, discard any
* leading paragraph markers
*/
if (ostr.justDidParaSuppressor
&& (len = rexMatch(leadingMultiPat, txt)) != nil)
{
/* remove the paragraph markers */
txt = txt.substr(len + 1);
/*
* even though we're not rendering the paragraph, note
* that a logical paragraph just started
*/
ostr.justDidPara = true;
/* keep going */
continue;
}
/* presume we won't find a <.p0> or <./p0> */
p0 = unp0 = nil;
/* find the next paragraph marker */
match = rexSearch(leadingSinglePat, txt);
if (match == nil)
{
/*
* there are no more paragraph markers - copy the
* remainder of the input string to the output
*/
ret += txt;
txt = '';
/* we just did something other than a paragraph */
ostr.justDidPara = nil;
}
else
{
/* add everything up to the paragraph break to the output */
ret += txt.substr(1, match[1] - 1);
/* get the rest of the string following the paragraph mark */
txt = txt.substr(match[1] + match[2]);
/* note if we found a <.p0> or <./p0> */
p0 = (match[3] is in ('<.p0>', '<.P0>'));
unp0 = (match[3] is in ('<./p0>', '<./P0>'));
/*
* note that we just found a paragraph marker, unless
* this is a <./p0>
*/
ostr.justDidPara = !unp0;
}
/*
* If the last character we copied out is a suppressBefore
* character, note for next time that we have a suppressor
* pending. Likewise, if we found a <.p0> rather than a
* <.p>, this counts as a suppressor.
*/
ostr.justDidParaSuppressor =
(p0 || rexMatch(suppressBefore,
ret.substr(ret.length(), 1)) != nil);
/*
* if we found a paragraph marker, and we didn't find a
* leading suppressor character just before it, add the
* paragraph rendering
*/
if (ostr.justDidPara && !ostr.justDidParaSuppressor)
ret += renderText;
}
/* return the translated string */
return ret;
}
;
/* the paragraph manager for the main output stream */
transient mainParagraphManager: ParagraphManager
;
/* ------------------------------------------------------------------------ */
/*
* Output Filter
*/
class OutputFilter: object
/*
* Apply the filter - this should be overridden in each filter. The
* return value is the result of filtering the string.
*
* 'ostr' is the OutputStream to which the text is being written,
* and 'txt' is the original text to be displayed.
*/
filterText(ostr, txt) { return txt; }
;
/* ------------------------------------------------------------------------ */
/*
* Output monitor filter. This is a filter that leaves the filtered
* text unchanged, but keeps track of whether any text was seen at all.
* Our 'outputFlag' is true if we've seen any output, nil if not.
*/
class MonitorFilter: OutputFilter
/* filter text */
filterText(ostr, val)
{
/* if the value is non-empty, note the output */
if (val != nil && val != '')
outputFlag = true;
/* return the input value unchanged */
return val;
}
/* flag: has any output occurred for this monitor yet? */
outputFlag = nil
;
/* ------------------------------------------------------------------------ */
/*
* Capture Filter. This is an output filter that simply captures all of
* the text sent through the filter, sending nothing out to the
* underlying stream.
*
* The default implementation simply discards the incoming text.
* Subclasses can keep track of the text in memory, in a file, or
* wherever desired.
*/
class CaptureFilter: OutputFilter
/*
* Filter the text. We simply discard the text, passing nothing
* through to the underlying stream.
*/
filterText(ostr, txt)
{
/* leave nothing for the underlying stream */
return nil;
}
;
/*
* "Switchable" capture filter. This filter can have its blocking
* enabled or disabled. When blocking is enabled, we capture
* everything, leaving nothing to the underlying stream; when disabled,
* we pass everything through to the underyling stream unchanged.
*/
class SwitchableCaptureFilter: CaptureFilter
/* filter the text */
filterText(ostr, txt)
{
/*
* if we're blocking output, return nothing to the underlying
* stream; if we're disabled, return the input unchanged
*/
return (isBlocking ? nil : txt);
}
/*
* Blocking enabled: if this is true, we'll capture all text passed
* through us, leaving nothing to the underyling stream. Blocking
* is enabled by default.
*/
isBlocking = true
;
/*
* String capturer. This is an implementation of CaptureFilter that
* saves the captured text to a string.
*/
class StringCaptureFilter: CaptureFilter
/* filter text */
filterText(ostr, txt)
{
/* add the text to my captured text so far */
addText(txt);
}
/* add to my captured text */
addText(txt)
{
/* append the text to my string of captured text */
txt_ += txt;
}
/* my captured text so far */
txt_ = ''
;
/*
* ImplicitAction announcement filter. This is applied just before text from
* an action routine is output to ensure than any pending implicit action
* reports are output before any text from the action routine itself.
*/
class ImplicitActionFilter: OutputFilter
filterText(ostr, txt)
{
/*
* This method should never be called if we don't have a current
* action, but just in case we return the text unchanged if there is
* no gAction.
*/
if(gAction == nil)
return txt;
/*
* If we do have have a gAction, prepend any pending implicit action
* announcements to the text we output.
*/
return gAction.buildImplicitActionAnnouncement(true, !gAction.isImplicit)
+ txt;
}
;
/* ------------------------------------------------------------------------ */
/*
* Style tag. This defines an HTML-like tag that can be used in output
* text to display an author-customizable substitution string.
*
* Each StyleTag object defines the name of the tag, which can be
* invoked in output text using the syntax "<.name>" - we require the
* period after the opening angle-bracket to plainly distinguish the
* sequence as a style tag, not a regular HTML tag.
*
* Each StyleTag also defines the text string that should be substituted
* for each occurrence of the "<.name>" sequence in output text, and,
* optionally, another string that is substituted for occurrences of the
* "closing" version of the tag, invoked with the syntax "<./name>".
*/
class StyleTag: object
/* name of the tag - the tag appears in source text in <.xxx> notation */
tagName = ''
/*
* opening text - this is substituted for each instance of the tag
* without a '/' prefix
*/
openText = ''
/*
* Closing text - this is substituted for each instance of the tag
* with a '/' prefix (<./xxx>). Note that non-container tags don't
* have closing text at all.
*/
closeText = ''
;
/*
* HtmlStyleTag - this is a subclass of StyleTag that provides different
* rendering depending on whether the interpreter is in HTML mode or not.
* In HTML mode, we display our htmlOpenText and htmlCloseText; when not
* in HTML mode, we display our plainOpenText and plainCloseText.
*/
class HtmlStyleTag: StyleTag
openText = (outputManager.htmlMode ? htmlOpenText : plainOpenText)
closeText = (outputManager.htmlMode ? htmlCloseText : plainCloseText)
/* our HTML-mode opening and closing text */
htmlOpenText = ''
htmlCloseText = ''
/* our plain (non-HTML) opening and closing text */
plainOpenText = ''
plainCloseText = ''
;
/*
* Define our default style tags. We name all of these StyleTag objects
* so that authors can easily change the expansion text strings at
* compile-time with the 'modify' syntax, or dynamically at run-time by
* assigning new strings to the appropriate properties of these objects.
*/
/*
* <.roomname> - we use this to display the room's name in the
* description of a room (such as in a LOOK AROUND command, or when
* entering a new location). By default, we display the room name in
* boldface on a line by itself.
*/
roomnameStyleTag: StyleTag 'roomname' '\n<b>' '</b><br>\n';
/* <.roomdesc> - we use this to display a room's long description */
roomdescStyleTag: StyleTag 'roomdesc' '' '';
/* <.roomcontents> - we use this to display a room's contents */
roomcontentsStyleTag: StyleTag 'roomcontents' '' '';
/*
* <.roompara> - we use this to separate paragraphs within a room's long
* description
*/
roomparaStyleTag: StyleTag 'roompara' '<.p>\n';
/*
* <.inputline> - we use this to display the text actually entered by the
* user on a command line. Note that this isn't used for the prompt text
* - it's used only for the command-line text itself.
*/
inputlineStyleTag: HtmlStyleTag 'inputline'
/* in HTML mode, switch in and out of TADS-Input font */
htmlOpenText = '<font face="tads-input">'
htmlCloseText = '</font>'
/* in plain mode, do nothing */
plainOpenText = ''
plainCloseText = ''
;
/*
* <.a> (named in analogy to the HTML <a> tag) - we use this to display
* hyperlinked text. Note that this goes *inside* an HTML <a> tag - this
* doesn't do the actual linking (the true <a> tag does that), but rather
* allows customized text formatting for hyperlinked text.
*/
hyperlinkStyleTag: HtmlStyleTag 'a'
;
/* <.statusroom> - style for the room name in a status line */
statusroomStyleTag: HtmlStyleTag 'statusroom'
htmlOpenText = '<b>'
htmlCloseText = '</b>'
;
/* <.statusscore> - style for the score in a status line */
statusscoreStyleTag: HtmlStyleTag 'statusscore'
htmlOpenText = '<i>'
htmlCloseText = '</i>'
;
/*
* <.parser> - style for messages explicitly from the parser.
*
* By default, we do nothing special with these messages. Many games
* like to use a distinctive notation for parser messages, to make it
* clear that the messages are "meta" text that's not part of the story
* but rather specific to the game mechanics; one common convention is
* to put parser messages in [square brackets].
*
* If the game defines a special appearance for parser messages, for
* consistency it might want to use the same appearance for notification
* messages displayed with the <.notification> tag (see
* notificationStyleTag).
*/
parserStyleTag: StyleTag 'parser'
openText = ''
closeText = ''
;
/*
* <.notification> - style for "notification" messages, such as score
* changes and messages explaining how facilities (footnotes, exit
* lists) work the first time they come up.
*
* By default, we'll put notifications in parentheses. Games that use
* [square brackets] for parser messages (i.e., for the <.parser> tag)
* might want to use the same notation here for consistency.
*/
notificationStyleTag: StyleTag 'notification'
openText = '('
closeText = ')'
;
/*
* <.assume> - style for "assumption" messages, showing an assumption
* the parser is making. This style is used for showing objects used by
* default when not specified in a command, objects that the parser
* chose despite some ambiguity, and implied commands.
*/
assumeStyleTag: StyleTag 'assume'
openText = '('
closeText = ')'
;
/*
* <.announceObj> - style for object announcement messages. The parser
* shows an object announcement for each object when a command is applied
* to multiple objects (TAKE ALL, DROP KEYS AND WALLET). The
* announcement simply shows the object's name and a colon, to let the
* player know that the response text that follows applies to the
* announced object.
*/
announceObjStyleTag: StyleTag 'announceObj'
openText = '<b>'
closeText = '</b>'
;
/* ------------------------------------------------------------------------ */
/*
* "Style tag" filter. This is an output filter that expands our
* special style tags in output text.
*/
styleTagFilter: OutputFilter, PreinitObject
/* pre-compile our frequently-used tag search pattern */
tagPattern = static new RexPattern(
'<nocase><langle>%.(/?[a-z][a-z0-9]*)<rangle>')
/* filter for a style tag */
filterText(ostr, val)
{
local idx;
/* search for our special '<.xxx>' tags, and expand any we find */
idx = rexSearch(tagPattern, val);
while (idx != nil)
{
local xlat;
local afterOfs;
local afterStr;
/* ask the formatter to translate it */
xlat = translateTag(rexGroup(1)[3]);
/* get the part of the string that follows the tag */
afterOfs = idx[1] + idx[2];
afterStr = val.substr(idx[1] + idx[2]);
/*
* if we got a translation, replace it; otherwise, leave the
* original text intact
*/
if (xlat != nil)
{
/* replace the tag with its translation */
val = val.substr(1, idx[1] - 1) + xlat + afterStr;
/*
* figure the offset of the remainder of the string in
* the replaced version of the string - this is the
* length of the original part up to the replacement
* text plus the length of the replacement text
*/
afterOfs = idx[1] + xlat.length();
}
/*
* search for the next tag, considering only the part of
* the string following the replacement text - we do not
* want to re-scan the replacement text for tags
*/
idx = rexSearch(tagPattern, afterStr);
/*
* If we found it, adjust the starting index of the match to
* its position in the actual string. Note that we do this
* by adding the OFFSET of the remainder of the string,
* which is 1 less than its INDEX, because idx[1] is already
* a string index. (An offset is one less than an index
* because the index of the first character is 1.)
*/
if (idx != nil)
idx[1] += afterOfs - 1;
}
/* return the filtered value */
return val;
}
/*
* Translate a tag
*/
translateTag(tag)
{
local isClose;
local styleTag;
/* if it's a close tag, so note and remove the leading slash */
isClose = tag.startsWith('/');
if (isClose)
tag = tag.substr(2);
/* look up the tag object in our table */
styleTag = tagTable[tag];
/*
* if we found it, return the open or close text, as
* appropriate; otherwise return nil
*/
return (styleTag != nil
? (isClose ? styleTag.closeText : styleTag.openText)
: nil);
}
/* preinitialization */
execute()
{
/* create a lookup table for our style table */
tagTable = new LookupTable();
/*
* Populate the table with all of the StyleTag instances. Key
* by tag name, storing the tag object as the value for each
* key. This will let us efficiently look up the StyleTag
* object given a tag name string.
*/
forEachInstance(StyleTag, { tag: tagTable[tag.tagName] = tag });
}
/*
* Our tag translation table. We'll initialize this during preinit
* to a lookup table with all of the defined StyleTag objects.
*/
tagTable = nil
;
/* ------------------------------------------------------------------------ */
/*
* Command Sequencer Filter. This is an output filter that handles the
* special <.commandsep> tag for visual command separation. This tag has
* the form of a style tag, but must be processed specially.
*
* <.commandsep> shows an appropriate separator between commands. Before
* the first command output or after the last command output, this has no
* effect. A run of multiple consecutive <.commandsep> tags is treated
* as a single tag.
*
* Between commands, we show gLibMessages.commandResultsSeparator. After
* an input line and before the first command result text, we show
* gLibMessages.commandResultsPrefix. After the last command result text
* before a new input line, we show gLibMessages.commandResultsSuffix.
* If we read two input lines, and there is no intervening text output at
* all, we show gLibMessages.commandResultsEmpty.
*
* The input manager should write a <.commandbefore> tag whenever it
* starts reading a command line, and a <.commandafter> tag whenever it
* finishes reading a command line.
*/
enum stateReadingCommand, stateBeforeCommand, stateBeforeInterruption,
stateInCommand, stateBetweenCommands, stateWriteThrough,
stateNoCommand;
transient commandSequencer: OutputFilter
/*
* Force the sequencer into mid-command mode. This can be used to
* defeat the resequencing into before-results mode that occurs if
* any interactive command-line input must be read in the course of
* a command's execution.
*/
setCommandMode() { state_ = stateInCommand; }
/*
* Internal routine: write the given text directly through us,
* skipping any filtering we'd otherwise apply.
*/
writeThrough(txt)
{
local oldState;
/* remember our old state */
oldState = state_;
/* set our state to write-through */
state_ = stateWriteThrough;
/* make sure we reset things on the way out */
try
{
/* write the text */
say(txt);
}
finally
{
/* restore our old state */
state_ = oldState;
}
}
/* pre-compile our tag sequence pattern */
patNextTag = static new RexPattern(
'<nocase><langle><dot>'
+ 'command(sep|int|before|after|none|mid)'
+ '<rangle>')
/*
* Apply our filter
*/
filterText(ostr, txt)
{
local ret;
/*
* if we're in write-through mode, simply pass the text through
* unchanged
*/
if (state_ == stateWriteThrough)
return txt;
/* scan for tags */
for (ret = '' ; txt != '' ; )
{
local match;
local cur;
local tag;
/* search for our next special tag sequence */
match = rexSearch(patNextTag, txt);
/* check to see if we found a tag */
if (match == nil)
{
/* no more tags - the rest of the text is plain text */
cur = txt;
txt = '';
tag = nil;
}
else
{
/* found a tag - get the plain text up to the tag */
cur = txt.substr(1, match[1] - 1);
txt = txt.substr(match[1] + match[2]);
/* get the tag name */
tag = rexGroup(1)[3];
}
/* process the plain text up to the tag, if any */
if (cur != '')
{
/* check our state */
switch(state_)
{
case stateReadingCommand:
case stateWriteThrough:
case stateInCommand:
case stateNoCommand:
/* we don't need to add anything in these states */
break;
case stateBeforeCommand:
/*
* We're waiting for the first command output, and we've
* now found it. Write the command results prefix
* separator.
*/
/*
* Command group prefix - this is displayed after a
* command line and before the first command results shown
* after the command line.
*
* By default, we'll show the "zero-space paragraph"
* marker, which acts like a paragraph break in that it
* swallows up immediately following paragraph breaks, but
* doesn't actually add any space. This will ensure that
* we don't add any space between the command input line
* and the next text.
*/
ret += BMsg(command results prefix, '<.p0>');
/* we're now inside some command result text */
state_ = stateInCommand;
break;
case stateBeforeInterruption:
/*
* An editing session has been interrupted, and we're
* showing new output. First, switch to normal
* in-command mode - do this before doing anything
* else, since we might recursively show some more
* text in the course of canceling the input line.
*/
state_ = stateInCommand;
/*
* Now tell the input manager that we're canceling
* the input line that was under construction. Don't
* reset the input editor state, though, since we
* might be able to resume editing the same line
* later.
*/
inputManager.cancelInputInProgress(nil);
/* insert the command interruption prefix */
/*
* Command "interruption" group prefix. This is displayed
* after an interrupted command line - a command line
* editing session that was interrupted by a timeout event
* - just before the text that interrupted the command
* line.
*
* By default, we'll show a paragraph break here, to set
* off the interrupting text from the command line under
* construction.
*/
ret += BMsg(command interuption prefix, '<.p>');
break;
case stateBetweenCommands:
/*
* We've been waiting for a new command to start
* after seeing a <.commandsep> tag. We now have
* some text for the new command, so show a command
* separator.
*/
/*
* Command separator - this is displayed after the results
* from a command when another command is about to be
* executed without any more user input. That is, when a
* command line contains more than one command, this
* message is displayed between each successive command,
* to separate the results visually.
*
* This is not shown before the first command results
* after a command input line, and is not shown after the
* last results before a new input line. Furthermore,
* this is shown only between adjacent commands for which
* output actually occurs; if a series of commands
* executes without any output, we won't show any
* separators between the silent commands.
*
* By default, we'll just start a new paragraph.
*/
ret += BMsg(command results separator, '<.p>');
/* we're now inside some command result text */
state_ = stateInCommand;
break;
}
/* add the plain text */
ret += cur;
}
/* if we found the tag, process it */
switch(tag)
{
case 'none':
/* switching to no-command mode */
state_ = stateNoCommand;
break;
case 'mid':
/* switching back to mid-command mode */
state_ = stateInCommand;
break;
case 'sep':
/* command separation - check our state */
switch(state_)
{
case stateReadingCommand:
case stateBeforeCommand:
case stateBetweenCommands:
case stateWriteThrough:
/* in these states, <.commandsep> has no effect */
break;
case stateInCommand:
/*
* We're inside some command text. <.commandsep>
* tells us that we've reached the end of one
* command's output, so any subsequent output text
* belongs to a new command and thus must be visually
* separated from the preceding text. Don't add any
* separation text yet, because we don't know for
* sure that there will ever be any more output text;
* instead, switch our state to between-commands, so
* that any subsequent text will trigger addition of
* a separator.
*/
state_ = stateBetweenCommands;
break;
}
break;
case 'int':
/*
* we've just interrupted reading a command line, due to
* an expired timeout event - switch to the
* before-interruption state
*/
state_ = stateBeforeInterruption;
break;
case 'before':
/* we're about to start reading a command */
switch (state_)
{
case stateBeforeCommand:
/*
* we've shown nothing since the last command; show
* the empty command separator
*/
/*
* Empty command results - this is shown when we read a
* command line and then go back and read another without
* having displaying anything.
*
* By default, we'll return a message indicating that
* nothing happened.
*/
writeThrough(BMsg(command results empty,
'Nothing obvious {dummy}{happens}.<.p>'));
break;
case stateBetweenCommands:
case stateInCommand:
/*
* we've written at least one command result, so
* show the after-command separator
*/
/*
* Command results suffix - this is displayed just before
* a new command line is about to be read if any command
* results have been shown since the last command line.
*
* By default, we'll show nothing extra.
*/
writeThrough(BMsg(command results suffix, ''));
break;
default:
/* do nothing in other modes */
break;
}
/* switch to reading-command mode */
state_ = stateReadingCommand;
break;
case 'after':
/*
* We've just finished reading a command. If we're
* still in reading-command mode, switch to
* before-command-results mode. Don't switch if we're
* in another state, since we must have switched to
* another state already by a different route, in which
* case we can ignore this notification.
*/
if (state_ == stateReadingCommand)
state_ = stateBeforeCommand;
break;
}
}
/* return the results */
return ret;
}
/* our current state - start out in before-command mode */
state_ = stateBeforeCommand
;
/* ------------------------------------------------------------------------ */
/*
* quoteFilter: this loooks for smart quotes in the output and checks that
* they are balanced.
*
* The problem with the smart quotes <q> </q> is that if one is missing, or a
* spurious one is added, the error is perpetrated throughout the rest of the
* game (or until a compensating error is located). The purpose of
* quoteFilter is (a) to report such errors (to make them easier to fix) and
* (b) to prevent them being propagated beyond a single turn. In the main
* this works by having quoteFilter take over responsibility for turning the
* <q> and </q> tags into the appropriate HTML entities rather than leaving
* it to the HTML rendering engine in the interpreter. The quoteFilter
* OutputFilter keeps its own track of whether a double quote or a single
* quote is rquired next, and resets this count at the start of each turn.
*/
quoteFilter: OutputFilter, InitObject
filterText(ostr, txt)
{
local quoteRes, quoteStr;
do
{
/* Look for <q> and </q> in the output */
quoteRes = rexSearch(quotePat, txt);
/* If we found <q> or </q>...*/
if(quoteRes)
{
/*
* Note whether it was an opening or a closing smart quote we
* found.
*/
quoteStr = quoteRes[3];
/*
* If it was an opening smart quote then replace it with an
* opening double quote mark if we've had a net even number of
* (or zero) opening quote marks this turn, otherwise replace
* it with an opening single quote mark.
*/
if(quoteStr.toLower() == '<q>')
{
txt = txt.findReplace(quoteStr, quoteCount % 2 == 0
? '“' : '‘', ReplaceOnce);
/* Increment our counter of opening quote marks */
quoteCount ++;
}
/* Otherwise it's a closing smart quote */
else
{
/*
* Replace it with a closing double quote mark if we've
* had a net even number of opening quotes so far on this
* turn, otherwise replace it with a closing single quote
* mark.
*/
txt = txt.findReplace(quoteStr, quoteCount % 2 == 1
? '”' : '’', ReplaceOnce);
/* Decrement our net quote count */
quoteCount --;
}
}
} while(quoteRes);
/*
* Return the filtered string, with <q> and </q> replaced with opening
* and closing quote marks.
*/
return txt;
}
/*
* Our quoteCount is the net number of quote marks we've output this turn,
* i.e. the number of opening quote marks less the number of closing quote
* marks.
*/
quoteCount = 0
/* Our rex pattern to match <q> and </q> */
quotePat = static new RexPattern('<NoCase><langle>(q|/q)<rangle>')
/* In Initialize this filter */
execute()
{
/* Add this filter to the main output stream */
mainOutputStream.addOutputFilter(self);
/*
* Set up a new prompt daemon to display a warning message about any
* unmatched quotes and zeroize our quoteCount each turn.
*/
if(defined(PromptDaemon) && new PromptDaemon(self, "eCheck));
}
/*
* Should I show a warning when I find unmatched smart quotes over the
* course of a turn? Displaying such a warning would probably look
* intrusive in a released version, but might well be useful in a
* version sent out to beta-testers (so it shouldn't be tied to a
* version compiled for debugging). The showWarnings flag thus allows
* the warning messages to be turned on and off as desired.
*/
showWarnings = true
/*
* The PromptDaemon set up in our execute() method at Initialization runs
* this method at the end of each turn. It checks to see if the number of
* opening smart quotes over the course of the turn just completed is the
* same as the number of closing smart quotes, and optionally prints a
* warning message if it is not.
*/
quoteCheck()
{
/*
* If we have a non-zero quoteCount at the start of a turn, this means
* that the number of opening quotes output on the previous turn didn't
* match the number of closing quotes, so if our showWarnings property
* is true, display a message calling attention to the unmatched quotes
* (to enable the game author to fix them or beta-testers to report
* them)
*/
if(quoteCount != 0 && showWarnings)
"<FONT COLOR=RED><b>WARNING!!</b></FONT> Unmatched quotes on
this turn; quoteCount = <<quoteCount>>. ";
/*
* In any case we want to zeroize the quoteCount at the start of
* each turn so that the first smart quote we encounter on the
* turn will display correctly no matter what went before.
*/
quoteCount = 0;
}
;
/* ------------------------------------------------------------------------ */
/*
* Log Console output stream. This is a simple wrapper for the system
* log console, which allows console-style output to be captured to a
* file, with full processing (HTML expansion, word wrapping, etc) but
* without displaying anything to the game window.
*
* This class should always be instantiated with transient instances,
* since the underlying system object doesn't participate in save/restore
* operations.
*/
class LogConsole: OutputStream
/*
* Utility method: create a log file, set up to capture all console
* output to the log file, run the given callback function, and then
* close the log file and restore the console output. This can be
* used as a simple means of creating a file that captures the output
* of a command.
*/
captureToFile(filename, charset, width, func)
{
local con;
/* set up a log console to do the capturing */
con = new LogConsole(filename, charset, width);
/* capture to the console and run our command */
outputManager.withOutputStream(con, func);
/* done with the console */
con.closeConsole();
}
/* create a log console */
construct(filename, charset, width)
{
/* inherit base class handling */
inherited();
/* create the system log console object */
handle_ = logConsoleCreate(filename, charset, width);
/* install the standard output filters */
addOutputFilter(typographicalOutputFilter);
addOutputFilter(new transient ParagraphManager());
addOutputFilter(styleTagFilter);
}
/*
* Close the console. This closes the underlying system log console,
* which closes the operating system file. No further text can be
* written to the console after it's closed.
*/
closeConsole()
{
/* close our underlying system console */
logConsoleClose(handle_);
/*
* forget our handle, since it's no longer valid; setting the
* handle to nil will make it more obvious what's going on if
* someone tries to write more text after we've been closed
*/
handle_ = nil;
}
/* low-level stream writer - write to our system log console */
writeFromStream(txt) { logConsoleSay(handle_, txt); }
/* our system log console handle */
handle_ = nil
;
/* ------------------------------------------------------------------------ */
/*
* Output stream window.
*
* This is an abstract base class for UI widgets that have output
* streams, such as Banner Windows and Web UI windows. This base class
* essentially handles the interior of the window, and leaves the details
* of the window's layout in the broader UI to subclasses.
*/
class OutputStreamWindow: object
/*
* Invoke the given callback function, setting the default output
* stream to the window's output stream for the duration of the call.
* This allows invoking any code that writes to the current default
* output stream and displaying the result in the window.
*/
captureOutput(func)
{
/* make my output stream the global default */
local oldStr = outputManager.setOutputStream(outputStream_);
/* make sure we restore the default output stream on the way out */
try
{
/* invoke the callback function */
(func)();
}
finally
{
/* restore the original default output stream */
outputManager.setOutputStream(oldStr);
}
}
/*
* Make my output stream the default in the output manager. Returns
* the previous default output stream; the caller can note the return
* value and use it later to restore the original output stream via a
* call to outputManager.setOutputStream(), if desired.
*/
setOutputStream()
{
/* set my stream as the default */
return outputManager.setOutputStream(outputStream_);
}
/*
* Create our output stream. We'll create the appropriate output
* stream subclass and set it up with our default output filters.
* Subclasses can override this as needed to customize the filters.
*/
createOutputStream()
{
/* create a banner output stream */
outputStream_ = createOutputStreamObj();
/* set up the default filters */
outputStream_.addOutputFilter(typographicalOutputFilter);
outputStream_.addOutputFilter(new transient ParagraphManager());
outputStream_.addOutputFilter(styleTagFilter);
}
/*
* Create the output stream object. Subclasses can override this to
* create the appropriate stream subclass. Note that the stream
* should always be created as a transient object.
*/
createOutputStreamObj() { return new transient OutputStream(); }
/*
* My output stream - this is a transient OutputStream instance.
* Subclasses must create this explicitly by calling
* createOutputStream() when the underlying UI window is first
* created.
*/
outputStream_ = nil
;
/* ------------------------------------------------------------------------ */
/*
* Typographical effects output filter. This filter looks for certain
* sequences in the text and converts them to typographical equivalents.
* Authors could simply write the HTML for the typographical markups in
* the first place, but it's easier to write the typewriter-like
* sequences and let this filter convert to HTML.
*
* We perform the following conversions:
*
* '---' -> &zwnbsp;—
*. '--' -> &zwnbsp;–
*. sentence-ending punctuation -> same +  
*
* Since this routine is called so frequently, we hard-code the
* replacement strings, rather than using properties, for slightly faster
* performance. Since this routine is so simple, games that want to
* customize the replacement style should simply replace this entire
* routine with a new routine that applies the customizations.
*
* Note that we define this filter in the English-specific part of the
* library, because it seems almost certain that each language will want
* to customize it for local conventions.
*/
typographicalOutputFilter: OutputFilter
filterText(ostr, val)
{
/*
* Look for sentence-ending punctuation, and put an 'en' space after each occurrence.
* Recognize ends of sentences even if we have closing quotes, parentheses, or other
* grouping characters following the punctuation. Do this before the hyphen substitutions
* so that we can look for ordinary hyphens rather than all of the expanded versions. We
* don't do this if sentenceSpacer has been overridden to a single normal space.
*/
if(sentenceSpacer != ' ')
val = rexReplace(eosPattern, val, '%1'+sentenceSpacer, ReplaceAll);
/* undo any abbreviations we mistook for sentence endings */
val = rexReplace(abbrevPat, val, '%1. ', ReplaceAll);
/*
* Replace dashes with typographical hyphens. Three hyphens in a
* row become an em-dash, and two in a row become an en-dash.
* Note that we look for the three-hyphen sequence first, because
* if we did it the other way around, we'd incorrectly find the
* first two hyphens of each '---' sequence and replace them with
* an en-dash, causing us to miss the '---' sequences entirely.
*
* We put a no-break marker (\uFEFF) just before each hyphen, and
* an okay-to-break marker (\u200B) just after, to ensure that we
* won't have a line break between the preceding text and the
* hyphen, and to indicate that a line break is specifically
* allowed if needed to the right of the hyphen.
*/
val = val.findReplace(['---', '--'],
['\uFEFF—\u200B', '\uFEFF–\u200B']);
/* return the result */
return val;
}
/*
* The character to use between a full stop and the start of the next sentence. By
* default we use an n-space.
*/
sentenceSpacer = '\u2002'
/*
* The end-of-sentence pattern. This looks a bit complicated, but
* all we're looking for is a period, exclamation point, or question
* mark, optionally followed by any number of closing group marks
* (right parentheses or square brackets, closing HTML tags, or
* double or single quotes in either straight or curly styles), all
* followed by an ordinary space.
*
* If a lower-case letter follows the space, though, we won't
* consider it a sentence ending. This applies most commonly after
* quoted passages ending with what would normally be sentence-ending
* punctuation: "'Who are you?' he asked." In these cases, the
* enclosing sentence isn't ending, so we don't want the extra space.
* We can tell the enclosing sentence isn't ending because a
* non-capital letter follows.
*
* Note that we specifically look only for ordinary spaces. Any
* sentence-ending punctuation that's followed by a quoted space or
* any typographical space overrides this substitution.
*/
eosPattern = static new RexPattern(
'<case>'
+ '('
+ '[.!?]'
+ '('
+ '<rparen|rsquare|dquote|squote|\u2019|\u201D>'
+ '|<langle><^rangle>*<rangle>'
+ ')*'
+ ')'
+ ' +(?![-a-z])'
)
/* pattern for abbreviations that were mistaken for sentence endings */
abbrevPat = static new RexPattern(
'<nocase>%<(' + abbreviations + ')<dot>\u2002')
/*
* Common abbreviations. These are excluded from being treated as
* sentence endings when they appear with a trailing period.
*
* Note that abbrevPat must be rebuilt manually if you change this on
* the fly - abbrevPat is static, so it picks up the initial value of
* this property at start-up, and doesn't re-evaluate it while the
* game is running.
*/
abbreviations = 'mr|mrs|ms|dr|prof'
;
/*
* cquoteOutputFilter; this turns straight quotes into typographical quotes
* (and is based on an extension by Stephen Grsnade).
*/
cquoteOutputFilter: OutputFilter
aggressive = true
// Patterns for our searches
patIsHTMLTag = static new RexPattern('<langle><^rangle>+<squote|dquote><^rangle>*<rangle>')
patIsFormatTag = static new RexPattern('{[^}]+<squote>[^}]*}')
patAggressive = static new RexPattern('(<alphanum|punct>)<squote>')
patIsCont1Tag = static new RexPattern('(<alpha>)<squote>(s|m|d|ve|re|ll)')
patIsCont2Tag = static new RexPattern('(<alpha>)n<squote>t')
patIsPossTag = static new RexPattern('(<alpha>)s<squote>')
filterText(ostr, val) {
local ret;
if(!isActive)
return val;
// Look for an HTML tag. We only need to find the first one,
// because we'll be recursing through the string
ret = rexSearch(patIsHTMLTag, val);
if (ret == nil) {
// Look for a formatting tag
ret = rexSearch(patIsFormatTag, val);
}
// If we got a match either from the HTML or the formatting
// tag, ignore that match recursively; that is, run the output
// filter on the text before and after the match. This is
// assuming that the whole start wasn't prefixed by a backslash
// (since e.g. "\<font face='courier>" isn't really an HTML tag)
if (ret != nil && (ret[1] == 1 ||
val.substr(ret[1] - 1, 1) != '\\')) {
return filterText(ostr, val.substr(1, ret[1] - 1)) + ret[3] +
filterText(ostr, val.substr(ret[1] + ret[2],
val.length() - (ret[1]+ret[2])
+ 1));
}
// Do the appropriate replacements. First, aggressive
if (aggressive) {
val = rexReplace(patAggressive, val, '%1’',
ReplaceAll);
/* Also replace double quotes with curly quotes */
val = val.findReplace([R'(^|%<|<space>)"', R'(<^space>)"'],
['%1“','%1”']);
}
else {
// We recognize the contractions 's, 'm, 'd, 've, 're,
// 'll, and n't, as well as the plural possessive s'.
// (Possessive 's is handled by the contraction.) All
// must be preceeded by a letter.
val = rexReplace(patIsCont1Tag,
val, '%1’%2', ReplaceAll);
val = rexReplace(patIsCont2Tag, val, '%1n’t',
ReplaceAll);
val = rexReplace(patIsPossTag, val, '%1s’',
ReplaceAll);
}
return val;
}
isActive = true
activate() { isActive = true; }
deactivate() { isActive = nil; }
;
/* ------------------------------------------------------------------------ */
/*
* Temporarily override the current narrative tense and invoke a callback
* function.
*/
withTense(usePastTense, callback)
{
/*
* Remember the old value of the usePastTense flag.
*/
local oldUsePastTense = gameMain.usePastTense;
/*
* Set the new value.
*/
gameMain.usePastTense = usePastTense;
/*
* Invoke the callback (remembering the return value) and restore the
* usePastTense flag on our way out.
*/
local ret;
try { ret = callback(); }
finally { gameMain.usePastTense = oldUsePastTense; }
/*
* Return the result.
*/
return ret;
}
/*
* Display msg bypassing all filters except for the massage parameter
* substitutions; these may also be bypassed if the second (optional)
* parameter is nil.
*/
extraReport(msg, expandParam = true)
{
if(expandParam)
msg = buildMessage(nil, msg);
gOutStream.writeFromStream(msg);
}
/*
* Basic conversation manager for use in adv3Liter and adv3Litest(when actor.t is not present) We
* just provide handling for <.reveal> and <.unreveal> in case game code uses them.
*/
conversationManager: OutputFilter, PreinitObject
/*
* Custom extended tags. Games and library extensions can add their
* own tag processing as needed, by using 'modify' to extend this
* object. There are two things you have to do to add your own tags:
*
* First, add a 'customTags' property that defines a regular
* expression for your added tags. This will be incorporated into
* the main pattern we use to look for tags. Simply specify a
* string that lists your tags separated by "|" characters, like
* this:
*
* customTags = 'foo|bar'
*
* Second, define a doCustomTag() method to process the tags. The
* filter routine will call your doCustomTag() method whenever it
* finds one of your custom tags in the output stream.
*/
customTags = nil
doCustomTag(tag, arg) { /* do nothing by default */ }
filterText(ostr, txt)
{
local start;
/* scan for our special tags */
for (start = 1 ; ; )
{
local match;
local arg;
local tag;
local nxtOfs;
local args;
/* scan for the next tag */
match = rexSearch(tagPat, txt, start);
/* if we didn't find it, we're done */
if (match == nil)
break;
/* note the next offset */
nxtOfs = match[1] + match[2];
/* get the argument (the third group from the match) */
arg = rexGroup(3);
if (arg != nil)
arg = arg[3];
/* pick out the tag */
tag = rexGroup(1)[3].toLower();
/* check which tag we have */
switch (tag)
{
case 'reveal':
/* reveal the key by adding it to our database */
args = arg.split('=');
if(args.length > 1)
{
arg = enumTabObj.getEnum(args[2]) ?? args[2];
setRevealed(args[1], arg);
}
else
setRevealed(arg);
break;
/* unreveal the key by removing it from our database */
case 'unreveal':
setUnrevealed(arg);
break;
default:
/* check for an extended tag */
doCustomTag(tag, arg);
break;
}
/* continue the search after this match */
start = nxtOfs;
}
/*
* remove the tags from the text by replacing every occurrence with an empty string, and
* return the result
*/
return rexReplace(tagPat, txt, '', ReplaceAll);
}
tagPat = static new RexPattern(
'<nocase><langle><dot>'
+ '(reveal|unreveal'
+ (customTags != nil ? '|' + customTags : '')
+ ')(<space>+(<^rangle>+))?'
+ '<rangle>')
setRevealed(tag, val?)
{
/* Note that our tag has been revealed */
libGlobal.setRevealed(tag, val);
}
;
Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1