output.t | documentation |
#charset "us-ascii" /* * Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved. * * TADS 3 Library - output formatting * * This module defines the framework for displaying output text. */ /* include the library header */ #include "adv3.h" /* ------------------------------------------------------------------------ */ /* * The standard library output function. We set this up as the default * display function (for double-quoted strings and for "<< >>" * embeddings). Code can also call this directly to display items. */ say(val) { /* send output to the active output stream */ outputManager.curOutputStream.writeToStream(val); } /* ------------------------------------------------------------------------ */ /* * 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) { /* convert the value to a string */ switch(dataType(val)) { case TypeSString: /* * it's a string - no conversion is needed, but if it's * empty, it doesn't count as real output (so don't notify * anyone, and don't set any output flags) */ if (val == '') return; break; case TypeNil: /* nil - don't display anything for this */ return; case TypeInt: case TypeObject: /* convert integers and objects to strings */ val = toString(val); break; } /* run it through our output filters */ val = applyFilters(val); /* * if, after filtering, we're not writing anything at all, * there's nothing left to do */ if (val == nil || val == '') return; /* write the text to our underlying system stream */ writeFromStream(val); } /* * Watch the stream for output. It's sometimes useful to be able to * call out to some code and determine whether or not the code * generated any text output. This routine invokes the given * callback function, monitoring the stream for output; if any * occurs, we'll return true, otherwise we'll return nil. */ watchForOutput(func) { local mon; /* set up a monitor filter on the stream */ addOutputFilter(mon = new MonitorFilter()); /* catch any exceptions so we can remove our filter before leaving */ try { /* invoke the callback */ (func)(); /* return the monitor's status, indicating if output occurred */ return mon.outputFlag; } finally { /* remove our monitor filter */ removeOutputFilter(mon); } } /* * Call the given function, capturing all text output to this stream * in the course of the function call. Return a string containing * the captured text. */ captureOutput(func, [args]) { /* 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); } } /* 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 ; /* * 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) { /* if an input event was interrupted, cancel the event */ inputManager.inputEventEnd(); /* 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_ = '' ; /* ------------------------------------------------------------------------ */ /* * 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' '' ''; /* * <.roompara> - we use this to separate paragraphs within a room's long * description */ roomparaStyleTag: StyleTag 'roompara' '<.p>\n'; /* * <.inputline> - we use this to display the text actually entered by the * user on a command line. Note that this isn't used for the prompt text * - it's used only for the command-line text itself. */ inputlineStyleTag: HtmlStyleTag 'inputline' /* in HTML mode, switch in and out of TADS-Input font */ htmlOpenText = '<font face="tads-input">' htmlCloseText = '</font>' /* in plain mode, do nothing */ plainOpenText = '' plainCloseText = '' ; /* * <.a> (named in analogy to the HTML <a> tag) - we use this to display * hyperlinked text. Note that this goes *inside* an HTML <a> tag - this * doesn't do the actual linking (the true <a> tag does that), but rather * allows customized text formatting for hyperlinked text. */ hyperlinkStyleTag: HtmlStyleTag 'a' ; /* <.statusroom> - style for the room name in a status line */ statusroomStyleTag: HtmlStyleTag 'statusroom' htmlOpenText = '<b>' htmlCloseText = '</b>' ; /* <.statusscore> - style for the score in a status line */ statusscoreStyleTag: HtmlStyleTag 'statusscore' htmlOpenText = '<i>' htmlCloseText = '</i>' ; /* * <.parser> - style for messages explicitly from the parser. * * By default, we do nothing special with these messages. Many games * like to use a distinctive notation for parser messages, to make it * clear that the messages are "meta" text that's not part of the story * but rather specific to the game mechanics; one common convention is * to put parser messages in [square brackets]. * * If the game defines a special appearance for parser messages, for * consistency it might want to use the same appearance for notification * messages displayed with the <.notification> tag (see * notificationStyleTag). */ parserStyleTag: StyleTag 'parser' openText = '' closeText = '' ; /* * <.notification> - style for "notification" messages, such as score * changes and messages explaining how facilities (footnotes, exit * lists) work the first time they come up. * * By default, we'll put notifications in parentheses. Games that use * [square brackets] for parser messages (i.e., for the <.parser> tag) * might want to use the same notation here for consistency. */ notificationStyleTag: StyleTag 'notification' openText = '(' closeText = ')' ; /* * <.assume> - style for "assumption" messages, showing an assumption * the parser is making. This style is used for showing objects used by * default when not specified in a command, objects that the parser * chose despite some ambiguity, and implied commands. */ assumeStyleTag: StyleTag 'assume' openText = '(' closeText = ')' ; /* * <.announceObj> - style for object announcement messages. The parser * shows an object announcement for each object when a command is applied * to multiple objects (TAKE ALL, DROP KEYS AND WALLET). The * announcement simply shows the object's name and a colon, to let the * player know that the response text that follows applies to the * announced object. */ announceObjStyleTag: StyleTag 'announceObj' openText = '<b>' closeText = '</b>' ; /* ------------------------------------------------------------------------ */ /* * "Style tag" filter. This is an output filter that expands our * special style tags in output text. */ styleTagFilter: OutputFilter, PreinitObject /* pre-compile our frequently-used tag search pattern */ tagPattern = static new RexPattern( '<nocase><langle>%.(/?[a-z][a-z0-9]*)<rangle>') /* filter for a style tag */ filterText(ostr, val) { local idx; /* search for our special '<.xxx>' tags, and expand any we find */ idx = rexSearch(tagPattern, val); while (idx != nil) { local xlat; local afterOfs; local afterStr; /* ask the formatter to translate it */ xlat = translateTag(rexGroup(1)[3]); /* get the part of the string that follows the tag */ afterOfs = idx[1] + idx[2]; afterStr = val.substr(idx[1] + idx[2]); /* * if we got a translation, replace it; otherwise, leave the * original text intact */ if (xlat != nil) { /* replace the tag with its translation */ val = val.substr(1, idx[1] - 1) + xlat + afterStr; /* * figure the offset of the remainder of the string in * the replaced version of the string - this is the * length of the original part up to the replacement * text plus the length of the replacement text */ afterOfs = idx[1] + xlat.length(); } /* * search for the next tag, considering only the part of * the string following the replacement text - we do not * want to re-scan the replacement text for tags */ idx = rexSearch(tagPattern, afterStr); /* * If we found it, adjust the starting index of the match to * its position in the actual string. Note that we do this * by adding the OFFSET of the remainder of the string, * which is 1 less than its INDEX, because idx[1] is already * a string index. (An offset is one less than an index * because the index of the first character is 1.) */ if (idx != nil) idx[1] += afterOfs - 1; } /* return the filtered value */ return val; } /* * Translate a tag */ translateTag(tag) { local isClose; local styleTag; /* if it's a close tag, so note and remove the leading slash */ isClose = tag.startsWith('/'); if (isClose) tag = tag.substr(2); /* look up the tag object in our table */ styleTag = tagTable[tag]; /* * if we found it, return the open or close text, as * appropriate; otherwise return nil */ return (styleTag != nil ? (isClose ? styleTag.closeText : styleTag.openText) : nil); } /* preinitialization */ execute() { /* create a lookup table for our style table */ tagTable = new LookupTable(); /* * Populate the table with all of the StyleTag instances. Key * by tag name, storing the tag object as the value for each * key. This will let us efficiently look up the StyleTag * object given a tag name string. */ forEachInstance(StyleTag, { tag: tagTable[tag.tagName] = tag }); } /* * Our tag translation table. We'll initialize this during preinit * to a lookup table with all of the defined StyleTag objects. */ tagTable = nil ; /* ------------------------------------------------------------------------ */ /* * MessageBuilder - this object provides a general text substitution * mechanism. Text to be substituted is enclosed in {curly braces}. * Within the braces, we have the substitution parameter name, which can * be in the following formats: * * id *. id obj *. id1/id2 obj *. id1 obj/id2 * * The ID string gives the type of substitution to perform. The ID's * all come from a table, which is specified by the language-specific * subclass, so the ID's can vary by language (to allow for natural * template-style parameter names for each language). If the ID is in * two pieces (id1 and id2), we concatenate the two pieces together with * a slash between to form the name we seek in the table - so {the/he * dobj} and {the dobj/he} are equivalent, and both look up the * identifier 'the/he'. If a two-part identifier is given, and the * identifier isn't found in the table, we'll try looking it up with the * parts reversed: if we see {he/the dobj}, we'll first try finding * 'he/the', and if that fails we'll look for 'the/he'. * * If 'obj' is present, it specificies the target object providing the * text to be substitutued; this is a string passed to the current * Action, and is usually something like 'actor', 'dobj', or 'iobj'. * * One instance of this class, called langMessageBuilder, should be * created by the language-specific library. */ class MessageBuilder: OutputFilter, PreinitObject /* pre-compile some regular expressions we use a lot */ patUpper = static new RexPattern('<upper>') patAllCaps = static new RexPattern('<upper><upper>') patIdObjSlashId = static new RexPattern( '(<^space|/>+)<space>+(<^space|/>+)(/<^space|/>+)') patIdObj = static new RexPattern('(<^space>+)<space>+(<^space>+)') patIdSlash = static new RexPattern('([^/]+)/([^/]+)') /* * Given a source string with substitution parameters, generate the * expanded message with the appropriate text in place of the * parameters. */ generateMessage(orig) { local result; /* we have nothing in the result string so far */ result = ''; /* keep going until we run out of substitution parameters */ for (;;) { local idx; local paramStr; local paramName; local paramObj; local info; local initCap, allCaps; local targetObj; local newText; local prop; /* get the position of the next brace */ idx = orig.find('{'); /* * if there are no braces, the rest of the string is simply * literal text; add the entire remainder of the string to * the result, and we're done */ if (idx == nil) { result += processResult(genLiteral(orig)); break; } /* add everything up to the brace to the result string */ result += processResult(genLiteral(orig.substr(1, idx - 1))); /* * lop off everything up to and including the brace from the * source string, since we're done with the part up to the * brace now */ orig = orig.substr(idx + 1); /* * if the brace was the last thing in the source string, or * it's a stuttered brace, add it literally to the result */ if (orig.length() == 0) { /* * nothing follows - add a literal brace to the result, * and we're done */ result += processResult('{'); break; } else if (orig.substr(1, 1) == '{') { /* * it's a stuttered brace - add a literal brace to the * result */ result += processResult('{'); /* remove the second brace from the source */ orig = orig.substr(2); /* we're finished processing this brace - go back for more */ continue; } /* find the closing brace */ idx = orig.find('}'); /* * if there is no closing brace, include the brace and * whatever follows as literal text */ if (idx == nil) { /* add the literal brace to the result */ result += processResult('{'); /* we're done with the brace - go back for more */ continue; } /* * Pull out everything up to the brace as the parameter * text. */ paramStr = orig.substr(1, idx - 1); /* assume for now that we will have no parameter object */ paramObj = nil; /* * drop everything up to and including the closing brace, * since we've pulled it out for processing now */ orig = orig.substr(idx + 1); /* * Note the capitalization of the first two letters. If * they're both lower-case, we won't adjust the case of the * substitution text at all. If the first is a capital and * the second isn't, we'll capitalize the first letter of * the replacement text. If they're both capitals, we'll * capitalize the entire replacement text. */ initCap = (rexMatch(patUpper, paramStr) != nil); allCaps = (rexMatch(patAllCaps, paramStr) != nil); /* lower-case the entire parameter string for matching */ local origParamStr = paramStr; paramStr = paramStr.toLower(); /* perform any language-specific rewriting on the string */ paramStr = langRewriteParam(paramStr); /* * Figure out which format we have. The allowable formats * are: * * id * obj *. id obj *. id1/id2 *. id1/id2 obj *. id1 obj/id2 */ if (rexMatch(patIdObjSlashId, paramStr) != nil) { /* we have the id1 obj/id2 format */ paramName = rexGroup(1)[3] + rexGroup(3)[3]; paramObj = rexGroup(2)[3]; } else if (rexMatch(patIdObj, paramStr) != nil) { /* we have 'id obj' or 'id1/id2 obj' */ paramName = rexGroup(1)[3]; paramObj = rexGroup(2)[3]; } else { /* we have no spaces, so we have no target object */ paramName = paramStr; paramObj = nil; } /* look up our parameter name */ info = paramTable_[paramName]; /* * If we didn't find it, and the parameter name contains a * slash ('/'), try reversing the order of the parts before * and after the slash. */ if (info == nil && rexMatch(patIdSlash, paramName) != nil) { /* * rebuild the name with the order of the parts * reversed, and look up the result */ info = paramTable_[rexGroup(2)[3] + '/' + rexGroup(1)[3]]; } /* * If we didn't find a match, simply put the entire thing in * the result stream literally, including the braces. */ if (info == nil) { /* * We didn't find it, so try treating it as a string * parameter object. Try getting the string from the * action. */ newText = (gAction != nil ? gAction.getMessageParam(paramName) : nil); /* check what we found */ if (dataType(newText) == TypeSString) { /* * It's a valid string parameter. Simply add the * literal text to the result. If we're in html * mode, translate the string to ensure that any * markup-significant characters are properly quoted * so that they aren't taken as html themselves. */ result += processResult(newText.htmlify()); } else { /* * the parameter is completely undefined; simply add * the original text, including the braces */ result += processResult('{<<origParamStr>>}'); } /* * we're done with this substitution string - go back * for more */ continue; } /* * If we have no target object specified in the substitution * string, and the parameter name has an associated implicit * target object, use the implied object. */ if (paramObj == nil && info[3] != nil) paramObj = info[3]; /* * If we have a target object name, ask the current action * for the target object value. Otherwise, use the same * target object as the previous expansion. */ if (paramObj != nil) { /* check for a current action */ if (gAction != nil) { /* get the target object by name through the action */ targetObj = gAction.getMessageParam(paramObj); } else { /* there's no action, so we don't have a value yet */ targetObj = nil; } /* * if we didn't find a value, look up the name in our * global name table */ if (targetObj == nil) { /* look up the name */ targetObj = nameTable_[paramObj]; /* * if we found it, and the result is a function * pointer or an anonymous function, invoke the * function to get the result */ if (dataTypeXlat(targetObj) == TypeFuncPtr) { /* evaluate the function */ targetObj = (targetObj)(); } } /* * remember this for next time, in case the next * substitution string doesn't include a target object */ lastTargetObj_ = targetObj; lastParamObj_ = paramObj; } else { /* * there's no implied or explicit target - use the same * one as last time */ targetObj = lastTargetObj_; paramObj = lastParamObj_; } /* * if the target object wasn't found, treat the whole thing * as a failure - put the entire parameter string back in * the result stream literally */ if (targetObj == nil) { /* add it to the output literally, and go back for more */ result += processResult('{' + paramStr + '}'); continue; } /* get the property to call on the target */ prop = getTargetProp(targetObj, paramObj, info); /* evaluate the parameter's associated property on the target */ newText = targetObj.(prop); /* apply the appropriate capitalization to the result */ if (allCaps) newText = newText.toUpper(); else if (initCap) newText = newText.substr(1, 1).toUpper() + newText.substr(2); /* * append the new text to the output result so far, and * we're finished with this round */ result += processResult(newText); } /* return the result string */ return result; } /* * Get the property to invoke on the target object for the given * parameter information entry. By default, we simply return * info[2], which is the standard property to call on the target. * This can be overridden by the language-specific subclass to * provide a different property if appropriate. * * 'targetObj' is the target object, and 'paramObj' is the parameter * name of the target object. For example, 'paramObj' might be the * string 'dobj' to represent the direct object, in which case * 'targetObj' will be the gDobj object. * * The English version, for example, uses this routine to supply a * reflexive instead of the default entry when the target object * matches the subject of the sentence. */ getTargetProp(targetObj, paramObj, info) { /* return the standard property mapping from the parameter info */ return info[2]; } /* * Process result text. This takes some result text that we're * about to add and returns a processed version. This is called for * all text as we add it to the result string. * * The text we pass to this method has already had all parameter * text fully expanded, so this routine does not need to worry about * { } sequences - all { } sequences will have been removed and * replaced with the corresponding expansion text before this is * called. * * This routine is called piecewise: the routine will be called once * for each parameter replacement text and once for each run of text * between parameters, and is called in the order in which the text * appears in the original string. * * By default we do nothing with the result text; we simply return * the original text unchanged. The language-specific subclass can * override this as desired to further modify the text for special * language-specific parameterization outside of the { } mechanism. * The subclass can also use this routine to maintain internal state * that depends on sentence structure. For example, the English * version looks for sentence-ending punctuation so that it can * reset its internal notion of the subject of the sentence when a * sentence appears to be ending. */ processResult(txt) { return txt; } /* * "Quote" a message - double each open or close brace, so that braces in * the message will be taken literally when run through the substitution * replacer. This can be used when a message is expanded prior to being * displayed to ensure that braces in the result won't be mistaken as * substitution parameters requiring further expansion. * * Note that only open braces need to be quoted, since lone close braces * are ignored in the substitution process. */ quoteMessage(str) { return str.findReplace(['{', '}'], [', '], ReplaceAll); } /* * Internal routine - generate the literal text for the given source * string. We'll remove any stuttered close braces. */ genLiteral(str) { /* replace all '}}' sequences with '}' sequences */ return str.findReplace('}}', '}', ReplaceAll); } /* * execute pre-initialization */ execute() { /* create a lookup table for our parameter names */ paramTable_ = new LookupTable(); /* add each element of our list to the table */ foreach (local cur in paramList_) paramTable_[cur[1]] = cur; /* create a lookup table for our global names */ nameTable_ = new LookupTable(); /* * Add an entry for 'actor', which resolves to gActor if there is * a gActor when evaluated, or the current player character if * not. Note that using a function ensures that we evaluate the * current gActor or gPlayerChar each time we need the 'actor' * value. */ nameTable_['actor'] = {: gActor != nil ? gActor : gPlayerChar }; } /* * Our output filter method. We'll run each string written to the * display through our parameter substitution method. */ filterText(ostr, txt) { /* substitute any parameters in the string and return the result */ return generateMessage(txt); } /* * The most recent target object. Each time we parse a substitution * string, we'll remember the target object here; when a * substitution string doesn't imply or specify a target object, * we'll use the previous one by default. */ lastTargetObj_ = nil /* the parameter name of the last target object ('dobj', 'actor', etc) */ lastParamObj_ = nil /* our parameter table - a LookupTable that we set up during preinit */ paramTable_ = nil /* our global name table - a LookupTable we set up during preinit */ nameTable_ = nil /* * Rewrite the parameter string for any language-specific rules. By * default, we'll return the original parameter string unchanged; * the language-specific instance can override this to provide any * special syntax extensions to the parameter string syntax desired * by the language-specific library. The returned string must be in * one of the formats recognized by the generic handler. */ langRewriteParam(paramStr) { /* by default, return the original unchanged */ return paramStr; } /* * our parameter list - this should be initialized in the * language-specific subclass to a list like this: * * [entry1, entry2, entry3, ...] * * Each entry is a list like this: * * [paramName, &prop, impliedTargetName, <extra>] * * paramName is a string giving the substitution parameter name; * this can be one word or two ('the' or 'the obj', for example). * * prop is a property identifier. This is the property invoked on * the target object to obtain the substitution text. * * impliedTargetName is a string giving the target object name to * use. When this is supplied, the paramName is normally used in * message text with no object name. This should be nil for * parameters that do not imply a particular target. * * <extra> is any number of additional parameters for the * language-specific subclass. The generic code ignores these extra * parameters, but the langague-specific subclass can use them if it * requires additional information. * * Here's an example: * * paramList_ = [ *. ['you', &theDesc, nil, 'actor'], *. ['the obj' &theObjDesc, &itReflexive, nil] *. ] * * The first item specifies a substitution name of 'you', which is * expanded by evaluating the property theDesc on the target object, * and specifies an implied target object of 'actor'. When this is * expanded, we'll call the current action to get the meaning of * 'actor', then evaulate property theDesc on the result. * * The second item specifies a substitution name of 'the obj', * expanded by evaluating property theObjDesc on the target object. * This one doesn't have an implied object, so the target object is * the one explicitly given in the message source text or is the * previous target object if one isn't specified in the message * text. */ paramList_ = [] ; /* ------------------------------------------------------------------------ */ /* * Command Sequencer Filter. This is an output filter that handles the * special <.commandsep> tag for visual command separation. This tag has * the form of a style tag, but must be processed specially. * * <.commandsep> shows an appropriate separator between commands. Before * the first command output or after the last command output, this has no * effect. A run of multiple consecutive <.commandsep> tags is treated * as a single tag. * * Between commands, we show gLibMessages.commandResultsSeparator. After * an input line and before the first command result text, we show * gLibMessages.commandResultsPrefix. After the last command result text * before a new input line, we show gLibMessages.commandResultsSuffix. * If we read two input lines, and there is no intervening text output at * all, we show gLibMessages.commandResultsEmpty. * * The input manager should write a <.commandbefore> tag whenever it * starts reading a command line, and a <.commandafter> tag whenever it * finishes reading a command line. */ enum stateReadingCommand, stateBeforeCommand, stateBeforeInterruption, stateInCommand, stateBetweenCommands, stateWriteThrough, stateNoCommand; transient commandSequencer: OutputFilter /* * Force the sequencer into mid-command mode. This can be used to * defeat the resequencing into before-results mode that occurs if * any interactive command-line input must be read in the course of * a command's execution. */ setCommandMode() { state_ = stateInCommand; } /* * Internal routine: write the given text directly through us, * skipping any filtering we'd otherwise apply. */ writeThrough(txt) { local oldState; /* remember our old state */ oldState = state_; /* set our state to write-through */ state_ = stateWriteThrough; /* make sure we reset things on the way out */ try { /* write the text */ say(txt); } finally { /* restore our old state */ state_ = oldState; } } /* pre-compile our tag sequence pattern */ patNextTag = static new RexPattern( '<nocase><langle><dot>' + 'command(sep|int|before|after|none|mid)' + '<rangle>') /* * Apply our filter */ filterText(ostr, txt) { local ret; /* * if we're in write-through mode, simply pass the text through * unchanged */ if (state_ == stateWriteThrough) return txt; /* scan for tags */ for (ret = '' ; txt != '' ; ) { local match; local cur; local tag; /* search for our next special tag sequence */ match = rexSearch(patNextTag, txt); /* check to see if we found a tag */ if (match == nil) { /* no more tags - the rest of the text is plain text */ cur = txt; txt = ''; tag = nil; } else { /* found a tag - get the plain text up to the tag */ cur = txt.substr(1, match[1] - 1); txt = txt.substr(match[1] + match[2]); /* get the tag name */ tag = rexGroup(1)[3]; } /* process the plain text up to the tag, if any */ if (cur != '') { /* check our state */ switch(state_) { case stateReadingCommand: case stateWriteThrough: case stateInCommand: case stateNoCommand: /* we don't need to add anything in these states */ break; case stateBeforeCommand: /* * We're waiting for the first command output, and * we've now found it. Write the command results * prefix separator. */ ret += gLibMessages.commandResultsPrefix; /* we're now inside some command result text */ state_ = stateInCommand; break; case stateBeforeInterruption: /* * An editing session has been interrupted, and we're * showing new output. First, switch to normal * in-command mode - do this before doing anything * else, since we might recursively show some more * text in the course of canceling the input line. */ state_ = stateInCommand; /* * Now tell the input manager that we're canceling * the input line that was under construction. Don't * reset the input editor state, though, since we * might be able to resume editing the same line * later. */ inputManager.cancelInputInProgress(nil); /* insert the command interruption prefix */ ret += gLibMessages.commandInterruptionPrefix; break; case stateBetweenCommands: /* * We've been waiting for a new command to start * after seeing a <.commandsep> tag. We now have * some text for the new command, so show a command * separator. */ ret += gLibMessages.commandResultsSeparator; /* we're now inside some command result text */ state_ = stateInCommand; break; } /* add the plain text */ ret += cur; } /* if we found the tag, process it */ switch(tag) { case 'none': /* switching to no-command mode */ state_ = stateNoCommand; break; case 'mid': /* switching back to mid-command mode */ state_ = stateInCommand; break; case 'sep': /* command separation - check our state */ switch(state_) { case stateReadingCommand: case stateBeforeCommand: case stateBetweenCommands: case stateWriteThrough: /* in these states, <.commandsep> has no effect */ break; case stateInCommand: /* * We're inside some command text. <.commandsep> * tells us that we've reached the end of one * command's output, so any subsequent output text * belongs to a new command and thus must be visually * separated from the preceding text. Don't add any * separation text yet, because we don't know for * sure that there will ever be any more output text; * instead, switch our state to between-commands, so * that any subsequent text will trigger addition of * a separator. */ state_ = stateBetweenCommands; break; } break; case 'int': /* * we've just interrupted reading a command line, due to * an expired timeout event - switch to the * before-interruption state */ state_ = stateBeforeInterruption; break; case 'before': /* we're about to start reading a command */ switch (state_) { case stateBeforeCommand: /* * we've shown nothing since the last command; show * the empty command separator */ writeThrough(gLibMessages.commandResultsEmpty()); break; case stateBetweenCommands: case stateInCommand: /* * we've written at least one command result, so * show the after-command separator */ writeThrough(gLibMessages.commandResultsSuffix()); break; default: /* do nothing in other modes */ break; } /* switch to reading-command mode */ state_ = stateReadingCommand; break; case 'after': /* * We've just finished reading a command. If we're * still in reading-command mode, switch to * before-command-results mode. Don't switch if we're * in another state, since we must have switched to * another state already by a different route, in which * case we can ignore this notification. */ if (state_ == stateReadingCommand) state_ = stateBeforeCommand; break; } } /* return the results */ return ret; } /* our current state - start out in before-command mode */ state_ = stateBeforeCommand ; /* ------------------------------------------------------------------------ */ /* * Log Console output stream. This is a simple wrapper for the system * log console, which allows console-style output to be captured to a * file, with full processing (HTML expansion, word wrapping, etc) but * without displaying anything to the game window. * * This class should always be instantiated with transient instances, * since the underlying system object doesn't participate in save/restore * operations. */ class LogConsole: OutputStream /* * Utility method: create a log file, set up to capture all console * output to the log file, run the given callback function, and then * close the log file and restore the console output. This can be * used as a simple means of creating a file that captures the output * of a command. */ captureToFile(filename, charset, width, func) { local con; /* set up a log console to do the capturing */ con = new LogConsole(filename, charset, width); /* capture to the console and run our command */ outputManager.withOutputStream(con, func); /* done with the console */ con.closeConsole(); } /* create a log console */ construct(filename, charset, width) { /* inherit base class handling */ inherited(); /* create the system log console object */ handle_ = logConsoleCreate(filename, charset, width); /* install the standard output filters */ addOutputFilter(typographicalOutputFilter); addOutputFilter(new transient ParagraphManager()); addOutputFilter(styleTagFilter); addOutputFilter(langMessageBuilder); } /* * Close the console. This closes the underlying system log console, * which closes the operating system file. No further text can be * written to the console after it's closed. */ closeConsole() { /* close our underlying system console */ logConsoleClose(handle_); /* * forget our handle, since it's no longer valid; setting the * handle to nil will make it more obvious what's going on if * someone tries to write more text after we've been closed */ handle_ = nil; } /* low-level stream writer - write to our system log console */ writeFromStream(txt) { logConsoleSay(handle_, txt); } /* our system log console handle */ handle_ = nil ; /* ------------------------------------------------------------------------ */ /* * 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); outputStream_.addOutputFilter(langMessageBuilder); } /* * 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 ;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3
Generated on 5/16/2013 from TADS version 3.1.3