Sharing a method of auto-indentation for TADS 3 / adv3

Hello again, TADS users…
Frequently when I work on some mechanism for my game that is applicable to the adv3 library at large, I just feel like sharing the info once I’m finished.

(If there’s already an obvious way of accomplishing this, and I’m reinventing the wheel here, let me know!)

Indentation was a major headache for me when starting out in TADS 3, because I personally didn’t care for the style where all new lines and paragraphs started flush with the left side of the text field. I went a long time littering my code with thousands of quoted spaces ("\ \ Text…"; I thought I read that \t’s were discouraged because different systems/fonts could display them differently), but it was really cumbersome, and besides that there are hundreds of library messages and default reports that don’t start with the same indentation.
I tampered with several methods that partially limited how many \ \ 's I had to type, but many attempts left a lot of unsightly double-blank lines due to invisible tags and separators. Only recently I found out a way to consistently indent everything in the game, without having to use any quoted spaces to start a new text block anywhere.

If you look at the mainParagraphManager (one of the filters that all output runs through before being displayed), you would almost think the whole issue could be wrapped up by changing renderText = '\b' to renderText = '\b\ \ ' (or however many spaces you like: I decided on two, because I thought it looked reasonable for both multi-paragraph text blocks and one-liners). But, this has the unfortunate effect of indenting things that you may not want indented, such as the command prompt, room titles, perhaps implicit announcements or parser messages.

My solution involved adding two lines to the filterText method of mainParagraphManager, and creating a custom indent-suppressor tag, along with a custom output filter to remove these tags after the paragraph manager is finished with them. The tag, if you like consistency with the paragraph markers, could be <.i0>. If you simply add these tags to a few certain spots, then every other desc, action report, fail report, etc. in the whole game will be indented consistently without having to type \ \ 's or \t’s (one caveat noted in a moment). Here is all the library-modifying code involved:

	/* this is all copy-paste except for the two marked lines and the indent property */
modify ParagraphManager	
	indent = '\ \ '				
	filterText(ostr, txt) 		
    	{ local ret = '';
         while (txt != '') {
            local len, match, p0, unp0;
             if (ostr.justDidPara) {
                if ((len = rexMatch(leadingMultiPat, txt)) != nil) {
                    txt = txt.substr(len + 1); continue; }
                if (rexMatch(suppressAfter, txt) != nil) {
                    txt = txt.substr(2); continue; }
                if(!txt.startsWith('\<')) 		//
                	ret += indent; }			// We're looking for <.i0> tags...
                								// You could type out the whole indent suppressor here, but
                								// most other style tags should be processed before this (my
                								// game still found a <title> tag though, hence I left it as
                								// is).            						
            									// Don't indent unless we didn't start with a tag
            ostr.justDidPara = nil;
            if (ostr.justDidParaSuppressor  && (len = rexMatch(leadingMultiPat, txt)) != nil) {
                txt = txt.substr(len + 1);
                ostr.justDidPara = true; 	continue;  }
            p0 = unp0 = nil;
            match = rexSearch(leadingSinglePat, txt);
            if (match == nil) {
                ret += txt;
                txt = '';
                ostr.justDidPara = nil; }
            else {
                ret += txt.substr(1, match[1] - 1);
                txt = txt.substr(match[1] + match[2]);
                p0 = (match[3] is in ('<.p0>', '<.P0>'));
                unp0 = (match[3] is in ('<./p0>', '<./P0>'));
                ostr.justDidPara = !unp0; }
            ostr.justDidParaSuppressor = (p0 || rexMatch(suppressBefore,ret.substr(ret.length(), 1)) != nil);
            if (ostr.justDidPara && !ostr.justDidParaSuppressor)
                ret += renderText; 		}
        return ret; }
;

iFilter: OutputFilter				// INDENTATION SUPPRESSOR TAG FILTER
	pat = R'<langle><dot>i0<rangle>'	// 	<.i0>
		/* simply erase all tags, since we will run after the paragraph manager is finished */
	filterText(ostr,txt) { return rexReplace(pat,txt,''); }
;

PreinitObject		/* install the suppressor filter to run after the paragraph manager*/
	execute {  mainOutputStream.addOutputFilterBelow(iFilter,mainParagraphManager); } 
	execBeforeMe = [adv3LibPreinit] 
;

That’s the mandatory part, then you will almost certainly want to suppress an indent before the command prompt, which looks like this:

modify libMessages
	mainCommandPrompt(which) { "\b<.i0>&gt; "; }
;

I also used the tag in openText of roomnameStyleTag, assumeStyleTag, and parserStyleTag.

Now, this all being said, there is still a common situation that requires a manual indent: multi-paragraph text blocks that are not separated by a blank line (<.p>). Here, you have to manually type the \n\ \ (or however many spaces you’re using), or if you like, make another tag such as <.i>. I personally used an ugly shortcut because I find both the backslash key and the <.i> combination to be cumbersome to type. My text blocks are simply offset with a triple comma, which a filter will turn into \n\ \ later.

shortcutFilter: OutputFilter
	pattern = R',,,|<langle><dot>i<rangle>'
	replacement = '\n\ \ '
	filterText(ostr,txt) { return rexReplace(pattern,txt,replacement); }
;
/* add this line to the PreinitObject, which makes sure the shortcuts are processed before
 * any other filter 
 
mainOutputStream.addOutputFilter(shortcutFilter);
*/

I tentatively modified the methods below to automatically insert <.p> markers, so I don’t have to in my game text. Time will tell if I need to make any exceptions (effectively letting separate reports add on to each other rather than each being set off with a blank line)…

-TravelWithMessage.showTravelDesc : for all travelDescs
-BasicLocation.travelerArriving : for all enteringRoom text
-eventManager.executeList (right before cur.executeEvent) : for all daemon/fuse text

Note: if you want something like a parenthesized implicit announcement or ambiguous announcement to be followed with \n instead of a blank line, the paragraph manager will not automatically indent the line that follows. Any single- or double-quoted string that ends in \n, but also wishes whatever may be printed subsquently to be indented, must end with \n\ \ or equivalent.

I know there’s not an overwhelming amount of TADS activity going on right now, but I hope eventually somebody might be glad to use this!

For the good of the TADS community,

John

2 Likes