//
//  XTOutputFormatter.m
//  XTads
//
//  Created by Rune Berg on 09/07/14.
//  Copyright (c) 2014 Rune Berg. All rights reserved.
//

#import "XTOutputFormatter.h"
#import "XTTextView.h"
#import "XTHtmlLinebreakHandler2.h"
#import "XTFormattedOutputElement.h"
#import "XTHtmlTagA.h"
#import "XTHtmlTagHr.h"
#import "XTHtmlTagBlockQuote.h"
#import "XTHtmlTagB.h"
#import "XTHtmlTagI.h"
#import "XTHtmlTagU.h"
#import "XTHtmlTagSmall.h"
#import "XTHtmlTagBig.h"
#import "XTHtmlTagQ.h"
#import "XTHtmlTagAboutBox.h"
#import "XTHtmlTagBanner.h"
#import "XTHtmlTagCenter.h"
#import "XTHtmlTagH1.h"
#import "XTHtmlTagH2.h"
#import "XTHtmlTagH3.h"
#import "XTHtmlTagH4.h"
#import "XTHtmlTagOl.h"
#import "XTHtmlTagUl.h"
#import "XTHtmlTagLi.h"
#import "XTHtmlTagNoop.h"
#import "XTHtmlTagQuestionMarkT2.h"
#import "XTHtmlTagQuestionMarkT3.h"
#import "XTHtmlTagTt.h"
#import "XTHtmlTagTab.h"
#import "XTHtmlTagDiv.h"
#import "XTHtmlTagBr.h"
#import "XTHtmlTagP.h"
#import "XTHtmlTagTitle.h"
#import "XTHtmlTagCite.h"
#import "XTHtmlTagFont.h"
#import "XTHtmlTagPre.h"
#import "XTHtmlTagImg.h"
#import "XTHtmlTagTable.h"
#import "XTHtmlTagTr.h"
#import "XTHtmlTagTh.h"
#import "XTHtmlTagTd.h"
#import "XTHtmlTagT2Hilite.h"
#import "XTHtmlTagT2Italics.h"
#import "XTHtmlTagT2TradStatusLine.h"
#import "XTHtmlWhitespace.h"
#import "XTHtmlQuotedSpace.h"
#import "XTHtmlSpecialSpace.h"
#import "XTHtmlNonbreakingSpace.h"
#import "XTSpecialAction.h"
#import "XTLogger.h"
#import "XTFontManager.h"
#import "XTPendingWhitespaceQueue.h"
#import "XTPrefs.h"
#import "XTStringUtils.h"
#import "XTFontUtils.h"
#import "XTTabStopModel.h"
#import "XTTabStopModelEntry.h"
#import "XTAllocDeallocCounter.h"
#import "XTTimer.h"
#import "XTMainTextHandler.h"


@interface XTOutputFormatter ()

#define UNICHAR_EN_SPACE 8194

typedef NS_ENUM(NSInteger, XTTextAlignMode) {
	XT_TEXT_ALIGN_LEFT = 1,
	XT_TEXT_ALIGN_CENTER = 2,
	XT_TEXT_ALIGN_RIGHT = 3,
	XT_TEXT_ALIGN_UNSPECIFIED = 99
};

@property XTHtmlLinebreakHandler2 *linebreakHandler2;
@property BOOL afterBlockLevelSpacing;

@property BOOL hiliteMode;
@property BOOL boldFaceMode;
@property BOOL italicsMode;
@property BOOL underlineMode;
@property BOOL smallMode;
@property BOOL bigMode;
@property BOOL ttMode;
@property BOOL preMode;
@property XTTextAlignMode textAlignMode;
@property BOOL aboutBoxMode;
@property BOOL orderedListMode;
@property BOOL unorderedListMode;
@property NSUInteger blockquoteLevel;
@property NSUInteger orderedListIndex;
@property BOOL h1Mode;
@property BOOL h2Mode;
@property BOOL h3Mode;
@property BOOL h4Mode;
@property XTPendingWhitespaceQueue *pendingWhitespaceQueue;
@property BOOL shouldWriteWhitespace;
	//TODO rename?
@property XTTabStopModel *tabStopModel;

@property XTHtmlTagA *activeTagA;

@property NSNumber *htmlFontSize; // 1..7, 3 being "default"
@property NSArray *htmlFontFaceList;
@property NSString *htmlFontColor; //TODO temp stop-gap fix for hint menus

@property NSArray *emptyArray;

@property XTFontManager *fontManager;

@property XTPrefs *prefs;

@property XTHtmlSpecialSpace *htmlSpecialSpaceForOneSpace;

@end


@implementation XTOutputFormatter

static XTLogger* logger;

static NSString * const tableCellSeparator = @"   ";
static const NSString *zeroWidthSpace = @"\u200B"; // non-printing

#define UNICHAR_EN_SPACE 8194

+ (void)initialize
{
	logger = [XTLogger loggerForClass:[XTOutputFormatter class]];
}

OVERRIDE_ALLOC_FOR_COUNTER

OVERRIDE_DEALLOC_FOR_COUNTER

- (id)init
{
    self = [super init];
    if (self) {
		_linebreakHandler2 = [XTHtmlLinebreakHandler2 new];
		_fontManager = [XTFontManager fontManager];
		_pendingWhitespaceQueue = [XTPendingWhitespaceQueue new];
		_prefs = [XTPrefs prefs];
		_emptyArray = [NSArray array];
		_isForBanner = NO;
		_isForT3 = NO;
		_tabStopModel = [XTTabStopModel new];
		_htmlSpecialSpaceForOneSpace = [XTHtmlSpecialSpace specialSpaceWithChar:' '];
		
		[self resetFlags];

		[_prefs startObservingChangesToAll:self];
 	}
    return self;
}

- (void)teardown
{
	//XT_DEF_SELNAME;
	//XT_TRACE_1(@"%@", self);
	[_prefs stopObservingChangesToAll:self];

	_textView = nil;
	_linebreakHandler2 = nil;
	[_pendingWhitespaceQueue reset];
	_pendingWhitespaceQueue = nil;
	[_tabStopModel reset];
	_tabStopModel = nil;
	_activeTagA = nil;
	_htmlFontFaceList = nil;
	_htmlFontColor = nil;
	_emptyArray = nil;
	_fontManager = nil;
	_prefs = nil;
	_htmlSpecialSpaceForOneSpace = nil;
}

- (void)observeValueForKeyPath:(NSString *)keyPath
					  ofObject:(id)object
						change:(NSDictionary *)change
					   context:(void *)context
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"keyPath=\"%@\"", keyPath);
	
	if (object == self.prefs) {
		[self syncWithPrefs];
	} else {
		XT_TRACE_0(@"%@ OTHER");
	}
}

- (void)syncWithPrefs
{
}

- (void)resetFlags
{
	[self resetNonHtmlModesFlags];
	[self resetHtmlModesFlags];
	[self.linebreakHandler2 resetForNextCommand];
	[self.tabStopModel reset];
}

- (void)resetForNextCommand
{
	[self resetNonHtmlModesFlags];
	[self.linebreakHandler2 resetForNextCommand];
}

- (void)resetNonHtmlModesFlags
{
	_receivingGameTitle = NO;
	_afterBlockLevelSpacing = NO;
	[_pendingWhitespaceQueue reset];
	_shouldWriteWhitespace = NO;
	_aboutBoxMode = NO;
	_activeTagA = nil;
}

- (void)resetHtmlModesFlags
{
	_boldFaceMode = NO;
	_italicsMode = NO;
	_underlineMode = NO;
	_smallMode = NO;
	_bigMode = NO;
	_ttMode = NO;
	_preMode = NO;
	_h1Mode = NO;
	_h2Mode = NO;
	_h3Mode = NO;
	_h4Mode = NO;
	_textAlignMode = XT_TEXT_ALIGN_LEFT;
	_htmlFontSize = nil;
	_htmlFontFaceList = nil;
	_htmlFontColor = nil;
	_orderedListMode = NO;
	_unorderedListMode = NO;
	_tagBannerDepth = 0;
	_orderedListIndex = 0;
	_blockquoteLevel = 0;
	_activeTagA = nil;
}

- (BOOL)tagBannerMode
{
	return (_tagBannerDepth >= 1);
}

- (BOOL)suppressContentInTagBanner
{
	NSUInteger minTagBannerDepthForSuppression = (self.isForT3 ? 1 : 2);
	BOOL res = (self.tagBannerDepth >= minTagBannerDepthForSuppression);
	return res;
}

- (NSArray *)formatElement:(id)elt
{
	XT_DEF_SELNAME;
	
	NSArray *eltRes = nil;
	
	if ([elt isKindOfClass:[NSString class]]) {
		eltRes = [self handleRegularText:(NSString *)elt];
		
	} else if ([elt isKindOfClass:[XTHtmlTag class]]) {
		eltRes = [self handleHtmlTag:(XTHtmlTag *)elt];
		
	} else if ([elt isKindOfClass:[XTHtmlWhitespace class]]) {
		eltRes = [self handleWhitespace:(XTHtmlWhitespace *)elt];
		
	} else if ([elt isKindOfClass:[XTHtmlQuotedSpace class]]) {
		eltRes = [self handleQuotedSpace:(XTHtmlQuotedSpace *)elt];
		
	} else if ([elt isKindOfClass:[XTHtmlSpecialSpace class]]) {
		eltRes = [self handleSpecialSpace:(XTHtmlSpecialSpace *)elt];
		
	} else if ([elt isKindOfClass:[XTHtmlNonbreakingSpace class]]) {
		eltRes = [self handleNonbreakingSpace:(XTHtmlNonbreakingSpace *)elt];
		
	} else if ([elt isKindOfClass:[XTSpecialAction class]]) {
		eltRes = [self handleSpecialAction:(XTSpecialAction *)elt];
		
	} else {
		NSString *className;
		if (elt == nil) {
			className = @"nil";
		} else {
			className = NSStringFromClass([elt class]);
		}
		XT_ERROR_1(@"got element of unknown class \"%@\"", className);
	}
	
	return eltRes;
}

//TODO why isn't this called for banners?
- (NSArray *)flushPendingWhitespace
{
	XT_DEF_SELNAME;

	NSMutableArray *res = [self makeArrayWithPendingWhitespace];
	[self clearPendingWhitespace];
	
	XT_TRACE_1(@"res.count=%lu", res.count);
	return res;
}

- (NSAttributedString *)formatOutputText:(NSString *)string
{
	return [self makeAttributedStringForOutput:string];
}

- (NSAttributedString *)formatInputText:(NSString *)string
{
	return [self makeAttributedStringForInput:string];
}

- (NSAttributedString *)formatStringForGridBanner:(NSString *)string
{
	return [self makeAttributedStringForGridBanner:string];
}

//-------------------------------------------------------------------------------------------

- (NSArray *)handleHtmlTag:(XTHtmlTag *)tag
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"\"%@\"", [tag debugString]);
	
	NSArray *res = nil;
	
	if (tag != nil) {
		
		BOOL executeTag =
			[tag isKindOfClass:[XTHtmlTagAboutBox class]] ||
			[tag isKindOfClass:[XTHtmlTagBanner class]] ||
			((! self.aboutBoxMode) && ((! self.isForT3) || (! self.tagBannerMode)));
		
		//TODO display of empty eg <h1>
		//	"Text1<h1></h1>Text2<h2></h2>Text3<h3></h3>Text4<h4></h4>Text5";
		//		qtads / chrome print block sep. such as empty line for each <hN></hN>
		//  "Text1<h1></h1><h2></h2><h3></h3><h4></h4>Text5"
		//		qtads / chrome print ONE block sep. such as empty line for entire <h1></h1><h2></h2><h3></h3><h4></h4>
		// "Text1<h1></h1><h2> </h2><h3></h3><h4></h4>Text5" -- NOTE space in h2
		//		qtads prints extra nl, chrome does not
		
		BOOL wasOutsideListModes = (! self.orderedListMode && ! self.unorderedListMode);
		
		if (executeTag) {
			NSMutableArray *resTemp = [NSMutableArray array];
			
			//TODO must clear self.afterBlockLevelSpacing
			// - for non block level tag
			
			if (! tag.isBlockLevel) {
				if (! [tag isKindOfClass:[XTHtmlTagTab class]]) {
					if (! [tag isKindOfClass:[XTHtmlTagBr class]]) {
						[self.pendingWhitespaceQueue appendInlineTag];
					}
				}
			}
			
			if (tag.isBlockLevel) {
				
				if (tag.isStandalone || ! tag.closing) {
					//TODO try and combine with lbh2
					if (tag.needsBlockLevelSpacingBefore)	{
						if (! self.afterBlockLevelSpacing) {
							NSMutableAttributedString *blockLevelSpacing = [self makeAttributedStringForOutput:@"\n"];
							[resTemp addObject:[XTFormattedOutputElement regularOutputElement:blockLevelSpacing]];
							self.afterBlockLevelSpacing = YES;
						}
					}
					NSString *resPrefix = [self.linebreakHandler2 handleBlockLevelNewline:tag];
						//TODO check behaviour for <p>
					if (resPrefix != nil) {
						NSMutableAttributedString *resPrefixAttrString = [self makeAttributedStringForOutput:resPrefix];
						[resTemp addObject:[XTFormattedOutputElement regularOutputElement:resPrefixAttrString]];
					}
					//TODO refactor... (several places)
					self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
					if (! self.shouldWriteWhitespace) {
						[self clearTextModelTrailingWhitespaceInLastParagraph];
						[self clearPendingWhitespace];
					}
					// ...
				}
			}
			
			NSArray *resMain = [tag dispatchToFormatter:self];
			
			[resTemp addObjectsFromArray:resMain];
			for (XTFormattedOutputElement *elt in resMain) {
				self.afterBlockLevelSpacing = NO;
			}
			
			if (tag.isBlockLevel) {
				if (tag.isStandalone || tag.closing) {
					BOOL dontExecuteTag = ([tag isKindOfClass:[XTHtmlTagUl class]] || [tag isKindOfClass:[XTHtmlTagOl class]]) &&
											wasOutsideListModes;
						//TODO hack...find general sol'n:
					if (! dontExecuteTag) {
						NSString *resSuffix = [self.linebreakHandler2 handleBlockLevelNewline:tag];
						if (resSuffix != nil) {
							NSMutableAttributedString *resSuffixAttrString = [self makeAttributedStringForOutput:resSuffix];
							[resTemp addObject:[XTFormattedOutputElement regularOutputElement:resSuffixAttrString]];
						}
					}
					self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
					if (! self.shouldWriteWhitespace) {
						[self clearTextModelTrailingWhitespaceInLastParagraph];
						[self clearPendingWhitespace];
					}
					if (! dontExecuteTag) {
						//TODO try and combine with lbh2
						if (tag.needsBlockLevelSpacingAfter)	{
							if (! self.afterBlockLevelSpacing) {
								NSMutableAttributedString *blockLevelSpacing = [self makeAttributedStringForOutput:@"\n"];
								[resTemp addObject:[XTFormattedOutputElement regularOutputElement:blockLevelSpacing]];
								self.afterBlockLevelSpacing = YES;
							}
						}
					}
				}
			}
			
			res = [NSArray arrayWithArray:resTemp];
		}
	}
	
	return res;
}

- (NSArray *)handleRegularText:(NSString *)string
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"\"%@\"", string);

	self.afterBlockLevelSpacing = NO;
	
	NSMutableArray *res = [NSMutableArray array];
	
	if (! self.htmlMode) {
		// Handle pending ws for plain-text mode is due to tab handling
		res = [self makeArrayWithPendingWhitespace];
		[self clearPendingWhitespace];
		[res addObject:[self makeRegularOutputElement:string]];
		
	} else {
		if (self.receivingGameTitle) {
			NSArray *tempRes = [self makeArrayWithGameTitleElement:string];
			[res addObjectsFromArray:tempRes];
		} else if (self.aboutBoxMode || [self suppressContentInTagBanner]) {
			// nothing
		} else {
			res = [self makeArrayWithPendingWhitespace];
			[self clearPendingWhitespace];
			NSArray *stringsSepdByNewline = [string componentsSeparatedByString:@"\n"];
				//TODO mv to parser
			for (NSString *s in stringsSepdByNewline) {
				if (s.length >= 1) {
					[self.linebreakHandler2 handleText:s];
					///NSArray *tempRes = [self makeArrayWithRegularOutputElement:s];
					[res addObject:[self makeRegularOutputElement:s]];
					self.shouldWriteWhitespace = YES;
				}
			}
		}
	}
	
	return res;
}

- (NSArray *)handleWhitespace:(XTHtmlWhitespace *)whitespace
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"shouldWriteWhitespace=%d", self.shouldWriteWhitespace);
	
	NSArray *res = nil;
	
	if (self.receivingGameTitle) {
		res = [self makeArrayWithGameTitleElement:@" "];
		
	} else if (self.aboutBoxMode || [self suppressContentInTagBanner]) {
		// nothing (<banner> in T3 games should be ignored)
		
	} else {
		if (self.preMode) {
			res = [self makeArrayWithRegularOutputElement:whitespace.text];
		} else {
			[self addPendingWhitespace:whitespace];
		}
	}
	
	return res;
}

//TODO fold into special spec handling below
- (NSArray *)handleQuotedSpace:(XTHtmlQuotedSpace *)quotedSpace
{
	XT_TRACE_ENTRY;

	NSArray *res = nil;
	
	if (self.receivingGameTitle) {
		res = [self makeArrayWithGameTitleElement:@" "];
	} else if (self.aboutBoxMode || [self suppressContentInTagBanner]) {
		// nothing
	} else {
		if (self.preMode) {
			res = [self makeArrayWithRegularOutputElement:@" "];
		} else {
			XTHtmlWhitespace *whitespace = [XTHtmlWhitespace whitespaceWithText:[quotedSpace asString]];
			[self addPendingWhitespace:whitespace];
			self.shouldWriteWhitespace = YES;
		}
	}
	
	return res;
}

- (NSArray *)handleSpecialSpace:(XTHtmlSpecialSpace *)specialSpace
{
	XT_TRACE_ENTRY;
	
	NSArray *res = nil;
	
	if (self.receivingGameTitle) {
		res = [self makeArrayWithGameTitleElement:@" "];
	} else if (self.aboutBoxMode || [self suppressContentInTagBanner]) {
		// nothing
	} else {
		if (self.preMode) {
			NSString *str = [self stringForSpecialSpaceWhenMonofont:specialSpace];
			if (str.length >= 1) {
				res = [self makeArrayWithRegularOutputElement:str];
			}
		} else {
			[self addPendingSpecialSpace:specialSpace];
			self.shouldWriteWhitespace = YES;
		}
	}
	
	return res;
}

- (NSString *)stringForSpecialSpaceWhenMonofont:(XTHtmlSpecialSpace *)specialSpace
{
	// See http://www.tads.org/t3doc/doc/sysman/fmt.htm
	
	NSString *res;
	
	switch (specialSpace.ch) {
		case 0x2003: // emsp
			res = @"   ";
			break;
		case 0x2002: // ensp
			// fall through
		case 0x2004: // tpmsp
			res = @"  ";
			break;
		case 0x2008: // puncsp
			// fall through
		case 0x200A: // hairsp
			res = @"";
			break;
		default:
			res = @" ";
			break;
	}
	
	return res;
}

- (NSArray *)handleNonbreakingSpace:(XTHtmlNonbreakingSpace *)nonbreakingSpace
{
	XT_TRACE_ENTRY;

	NSMutableArray *res = [NSMutableArray array];
	
	//TODO check logic vs reg'l text case
	if (self.receivingGameTitle) {
		NSArray *tempRes = [self makeArrayWithGameTitleElement:@" "];
		[res addObjectsFromArray:tempRes];
	} else if (self.aboutBoxMode || [self suppressContentInTagBanner]) {
		// nothing
	} else {
		res = [self makeArrayWithPendingWhitespace];
		[self clearPendingWhitespace];
		//TODO use premade objs instead of "[self makeArrayWith" for common cases
		if (self.preMode) {
			NSArray *tempRes = [self makeArrayWithRegularOutputElement:@" "];
			[res addObjectsFromArray:tempRes];
		} else {
			//NSArray *tempRes = [self makeArrayWithRegularOutputElement:@"\u00A0"]; // the actual 0nbsp char caused unwanted indent when at start of line
			NSArray *tempRes = [self makeArrayWithRegularOutputElement:@" "];
				//TODO make pre-made obj
			[res addObjectsFromArray:tempRes];
		}
	}
	
	return res;
}

- (NSArray *)handleSpecialAction:(XTSpecialAction *)specialAction
{
	NSArray *res = [self makeArrayWithSpecialActionElement:specialAction];
	return res;
}


//------- TODO ---------

- (void)addPendingWhitespace:(XTHtmlWhitespace *)whitespace
{
	//TODO tabs?
	NSString *wsText = whitespace.text;
	for (NSUInteger i = 0; i < wsText.length; i++) {
		unichar ch = [wsText characterAtIndex:i];
		if (ch == '\n') {
			// Handle newline as regular space.
			// (More convenient to convert this here than in pending-ws-queue.)
			ch = ' ';
		}
		NSString *wsOneCh = [NSString stringWithFormat:@"%C", ch];
			//TODO these can be precomputed
		NSAttributedString *attrString = [self makeAttributedStringForOutput:wsOneCh];
		[self.pendingWhitespaceQueue append:attrString];
	}
}

- (void)addPendingSpecialSpace:(XTHtmlSpecialSpace *)specialSpace
{
	NSString *string = [NSString stringWithFormat:@"%C", specialSpace.ch];
	NSAttributedString *attrString = [self makeAttributedStringForOutput:string];
	[self.pendingWhitespaceQueue append:attrString];
}

- (void)addPendingTabWithIndent:(NSNumber *)indent
{
	if (indent != nil) {
		NSInteger indentInt = indent.integerValue;
		if (indentInt >= 1) {
			XTHtmlSpecialSpace *specialSpace = [XTHtmlSpecialSpace specialSpaceWithChar:UNICHAR_EN_SPACE];
				//TODO? doesn't behave exactly like HTML TADS, but very nearly
			for (NSInteger i = 0; i < indentInt; i++) {
				[self addPendingSpecialSpace:specialSpace];
			}
		}
	}
}

- (void)addPendingTab
{
	NSAttributedString *attrString = [self makeAttributedStringForOutput:@"\t"];
		//TODO ? just make it an ordinary attr string?
	[self.pendingWhitespaceQueue appendTab:attrString];
}

- (void)clearPendingWhitespace
{
	[self.pendingWhitespaceQueue reset];
}

- (void)clearTextModelTrailingWhitespaceInLastParagraph
{
	NSTextStorage *textStorage = self.textView.textStorage;
	NSString *textStorageString = textStorage.string;
	NSRange rangeOfTrailingWhitespace = [XTStringUtils findRangeOfTrailingWhitespaceInLastParagraph:textStorageString];
	if (rangeOfTrailingWhitespace.location != NSNotFound && rangeOfTrailingWhitespace.length >= 1) {
		[textStorage deleteCharactersInRange:rangeOfTrailingWhitespace];
	}
}

//-------------

- (NSMutableArray *)makeArrayWithPendingWhitespace
{
	NSMutableArray *res = [self makeArrayWithPendingWhitespace:NO];
	return res;
}

- (NSMutableArray *)makeArrayWithPendingWhitespace:(BOOL)force
{
	XT_DEF_SELNAME;
	
	NSMutableArray *res = [NSMutableArray arrayWithCapacity:30];
	
	if (force || self.shouldWriteWhitespace || [self.pendingWhitespaceQueue containsTabs]) {
		NSMutableArray *tempRes = [self.pendingWhitespaceQueue combinedWhitespace];
		//TODO ? return one attr str?
		for (NSMutableAttributedString *attrStr in tempRes) {
			[res addObject:[XTFormattedOutputElement regularOutputElement:attrStr]];
		}
	}
	
	XT_TRACE_1(@"-> with res.count %d", res.count);
	return res;
}
	
- (NSArray *)makeArrayWithGameTitleElement:(NSString *)string
{
	XTFormattedOutputElement *outputElement;
	NSMutableAttributedString *attrString = [[NSMutableAttributedString new] initWithString:string];
		//TODO why mutable?
	outputElement = [XTFormattedOutputElement gameTitleElement:attrString];
	NSArray *res = [NSArray arrayWithObject:outputElement];
	return res;
}

- (NSArray *)makeArrayWithRegularOutputElement:(NSString *)string
{
	NSArray *res = [NSArray arrayWithObject:[self makeRegularOutputElement:string]];
	return res;
}

- (NSArray *)makeArrayWithSpecialActionElement:(XTSpecialAction *)specialAction
{
	XTFormattedOutputElement *outputElement = [XTFormattedOutputElement specialActionElement:specialAction];
	NSArray *res = [NSArray arrayWithObject:outputElement];
	return res;
}

- (NSArray *)makeArrayWithStatusLineModeSElement:(NSUInteger)statusLineMode
{
	XTFormattedOutputElement *outputElement;
	if (statusLineMode == STATUS_LINE_MODE_STATUS) {
		outputElement = [XTFormattedOutputElement statusLineModeStart];
	} else if (statusLineMode == STATUS_LINE_MODE_MAIN) {
		outputElement = [XTFormattedOutputElement statusLineModeEnd];
	} else if (statusLineMode == STATUS_LINE_MODE_SUPPRESS) {
		outputElement = [XTFormattedOutputElement statusLineModeSuppress];
	}
	NSArray *res = [NSArray arrayWithObject:outputElement];
	return res;
}

//------

- (XTFormattedOutputElement *)makeRegularOutputElement:(NSString *)string
{
	NSMutableAttributedString *attrString = [self makeAttributedStringForOutput:string];
	XTFormattedOutputElement *outputElement = [XTFormattedOutputElement regularOutputElement:attrString];
	return outputElement;
}

- (NSMutableAttributedString *)makeAttributedStringForOutput:(NSString *)string
{
   NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:string
																				  attributes:[self getTextAttributesDictionaryForOutput]];
   return attrString;
}

- (NSMutableAttributedString *)makeAttributedStringForInput:(NSString *)string
{
	NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:string
																				   attributes:[self getTextAttributesDictionaryForInput]];
	return attrString;
}

- (NSMutableAttributedString *)makeAttributedStringForGridBanner:(NSString *)string
{
	NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:string
																				   attributes:[self getTextAttributesDictionaryForGridBanner]];
	return attrString;
}

//------

- (NSDictionary *)getTextAttributesDictionaryForOutput
{
	XT_DEF_SELNAME;

	NSMutableDictionary *dict = [self getTextAttributesDictionaryCommonForOutput];
	NSMutableParagraphStyle *pgStyle = dict[NSParagraphStyleAttributeName];

	NSArray *tabStops = [self getDefaultTabStops];
	[pgStyle setTabStops:tabStops];
	
	NSTextAlignment alignment = NSLeftTextAlignment;
	
	if (self.textAlignMode == XT_TEXT_ALIGN_LEFT) {
		alignment = NSLeftTextAlignment;
	} else if (self.textAlignMode == XT_TEXT_ALIGN_CENTER) {
		alignment = NSCenterTextAlignment;
	} else if (self.textAlignMode == XT_TEXT_ALIGN_RIGHT) {
		alignment = NSRightTextAlignment;
	} else {
		XT_ERROR_1(@"unknown textAlignMode %d", self.textAlignMode);
	}
	
	[pgStyle setAlignment:alignment];
	
	//TODO all "indenting" tags should contribute to actual indent size
	
	if (self.blockquoteLevel >= 1) {

		CGFloat firstLineHeadIndent = [self getPlainTextModeTabStopColumnWidthInPoints];
		//CGFloat headIndent = firstLineHeadIndent;
		CGFloat indent = firstLineHeadIndent * self.blockquoteLevel;
		[pgStyle setFirstLineHeadIndent:indent];
		[pgStyle setHeadIndent:indent];
		
	} else if (self.unorderedListMode || self.orderedListMode) {
		
		CGFloat firstLineHeadIndent = [self getPlainTextModeTabStopColumnWidthInPoints];
		CGFloat headIndent = firstLineHeadIndent + [self getListItemPrefixColumnWidthInPoints];
		[pgStyle setFirstLineHeadIndent:firstLineHeadIndent];
		[pgStyle setHeadIndent:headIndent];

		NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
		//CGFloat position = [self findXCoordOfInsertionPointForTabs];
		CGFloat posNewTab = headIndent; //position + (pgStyle.headIndent - pgStyle.firstLineHeadIndent);
		NSTextTab *newTabStop = [self createTabStopAtPosition:posNewTab];
		[tabStops addObject:newTabStop];
		pgStyle.tabStops = tabStops;
	}

	if (self.underlineMode) {
		dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:1];
	}
	
	if (self.activeTagA != nil) {
		NSString *href = [self.activeTagA attributeAsString:@"href"];
		if (href == nil) {
			href = @"";
		}
		dict[NSLinkAttributeName] = href;
		if ([self.activeTagA hasAttribute:@"append"]) {
			dict[XT_OUTPUT_FORMATTER_ATTR_CMDLINK_APPEND] = @"true";
		}
		if ([self.activeTagA hasAttribute:@"noenter"]) {
			dict[XT_OUTPUT_FORMATTER_ATTR_CMDLINK_NOENTER] = @"true";
		}
	}

	NSDictionary *temporaryAttrsDict = [self getTextTemporaryAttributesDictionaryForOutput];
	dict[XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT] = temporaryAttrsDict;
	
	return dict;
}

- (void)applyParagraphStyleToLastParagraph:(NSMutableParagraphStyle *)pgStyle
{
	NSTextStorage *textStorage = [self.textView textStorage];
	NSString *string = [textStorage string];
	NSRange rangeOfLastPara = [XTStringUtils findRangeOfLastParagraph:string];
	
	[textStorage addAttribute:NSParagraphStyleAttributeName value:pgStyle range:rangeOfLastPara];
}

- (NSMutableParagraphStyle *)getParagraphStyleAtStartOfLastParagraph
{
	XT_DEF_SELNAME;

	NSTextStorage *textStorage = [self.textView textStorage];
	NSString *string = [textStorage string];

	NSRange rangeOfLastPara = [XTStringUtils findRangeOfLastParagraph:string];
	if (rangeOfLastPara.length == 0) {
		// At start of paragraph, so insert some non-printing text that we can add a tab stop to the new paragraph
		
		//TODO clean up, make sep func, refactor wrt that in outTextHandler:
		NSArray *formattedOutputElements = [self formatElement:zeroWidthSpace];
		XTFormattedOutputElement *elt = [formattedOutputElements objectAtIndex:0];
		NSAttributedString *attrStr = elt.attributedString;

		[textStorage appendAttributedString:attrStr];
		
		rangeOfLastPara.length = 1;

		if (rangeOfLastPara.location == NSNotFound) {
			// shouldn't happen
			XT_ERROR_0(@"rangeOfLastPara.location == NSNotFound");
		}
	}
	
	NSUInteger indexStartOfLastPara = rangeOfLastPara.location;
	
	NSDictionary *attrsLastPara = [textStorage attributesAtIndex:indexStartOfLastPara effectiveRange:nil];
	
	NSMutableParagraphStyle *pgStyleLastPara = (NSMutableParagraphStyle *) [attrsLastPara objectForKey:NSParagraphStyleAttributeName];

	NSMutableParagraphStyle *res = [pgStyleLastPara mutableCopy];
	return res;
}

- (NSDictionary *)getTextTemporaryAttributesDictionaryForOutput
{
	//XT_DEF_SELNAME;
	
	NSMutableDictionary *dict = nil;
	
	if (self.activeTagA != nil) {
		dict = [NSMutableDictionary dictionary];
		dict[NSForegroundColorAttributeName] = self.prefs.linksTextColor;
		if (! self.underlineMode) {
			if (! self.prefs.linksUnderline.boolValue) {
				dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleNone];
			}
		}
		if ([self.activeTagA hasAttribute:@"plain"]) {
			// "plain" link - set temp attrs so it looks like regular text
			dict[NSForegroundColorAttributeName] = [self getOutputTextColor];
			if (! self.underlineMode) {
				dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleNone];
			}
		}
	}
	
	return dict;
}

- (NSMutableDictionary *)getTextAttributesDictionaryCommonForOutput
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
	
	dict[NSFontAttributeName] = [self getCurrentFontForOutput];
		//TODO (wasteful) not here - overridden for input and grid banners
	
	dict[NSForegroundColorAttributeName] = [self getOutputTextColor];
		//TODO reconsider wrt prefs
	
	NSMutableParagraphStyle *pgStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
	dict[NSParagraphStyleAttributeName] = pgStyle;
	
	return dict;
}

- (NSColor *)getOutputTextColor
{
	NSColor *res;

	if (self.isForBanner) {
		res = self.prefs.statusLineTextColor;
	} else {
		res = self.prefs.outputAreaTextColor;
	}

	//TODO exp stop-gap fix for hint menus:
	if (self.htmlFontColor != nil) {
		if ([self.htmlFontColor isEqualToString:@"statusbg"] || [self.htmlFontColor isEqualToString:@"bgcolor"]) {
			if (self.isForBanner) {
				res = self.prefs.statusLineBackgroundColor;
			} else {
				res = self.prefs.outputAreaBackgroundColor;
			}
		}
	}
	
	return res;

	//TODO when game can set
}

- (NSDictionary *)getTextAttributesDictionaryForInput
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
	
	dict[NSFontAttributeName] = [self getCurrentFontForInput];
	dict[NSForegroundColorAttributeName] = self.prefs.inputTextColor;

	return dict;
}

- (NSDictionary *)getTextAttributesDictionaryForGridBanner
{
	//XT_DEF_SELNAME;
	
	NSMutableDictionary *dict = [self getTextAttributesDictionaryCommonForOutput];
	//TODO color
	dict[NSFontAttributeName] = [self getCurrentFontForGridBanner];
	
	return dict;
}

- (NSFont *)getCurrentFontForGridBanner
{
	NSString *parameterizedFontName = [self.fontManager xtadsFixedWidthParameterizedFontName];
	XTParameterizedFont *parameterizedFont = [self.fontManager getParameterizedFontWithName:parameterizedFontName];
	CGFloat defaultPointSize = parameterizedFont.size;
	NSNumber *pointSize = [NSNumber numberWithFloat:defaultPointSize];
	NSNumber *htmlFontSize = [NSNumber numberWithInteger:3]; // default - don't scale up/down
	NSArray *fontNames = [NSArray arrayWithObject:parameterizedFontName];

	NSFont *res = [self.fontManager getFontWithName:fontNames
										  pointSize:pointSize
										   htmlSize:htmlFontSize
											   bold:NO
											italics:NO
											  small:NO
												big:NO];
	return res;
}

- (NSFont *)getCurrentFontForOutput
{
	BOOL bold = (self.boldFaceMode | self.hiliteMode | self.h1Mode | self.h2Mode | self.h3Mode | self.h4Mode);
	BOOL italics = self.italicsMode;
	
	NSString *parameterizedFontName;
	if (self.ttMode || self.preMode) {
		parameterizedFontName = [self.fontManager xtadsFixedWidthParameterizedFontName];
	} else {
		parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
	}
	
	NSArray *fontNames;
	if (self.htmlFontFaceList != nil && self.htmlFontFaceList.count >= 1) {
		fontNames = self.htmlFontFaceList;
	} else {
		fontNames = [NSArray arrayWithObject:parameterizedFontName];
	}

	XTParameterizedFont *parameterizedFont = [self.fontManager getParameterizedFontWithName:parameterizedFontName];
	CGFloat defaultPointSize = parameterizedFont.size;

	NSNumber *pointSize = nil;
	if (self.h1Mode) {
		pointSize = [NSNumber numberWithFloat:(defaultPointSize + 14.0)];
	} else if (self.h2Mode) {
		pointSize = [NSNumber numberWithFloat:(defaultPointSize + 9.0)];
	} else if (self.h3Mode) {
		pointSize = [NSNumber numberWithFloat:(defaultPointSize + 4.0)];
	} else if (self.h4Mode) {
		pointSize = [NSNumber numberWithFloat:(defaultPointSize + 2.0)];
	}

	NSFont *res = [self.fontManager getFontWithName:fontNames
										  pointSize:pointSize
										   htmlSize:self.htmlFontSize
											   bold:bold
											italics:italics
											  small:self.smallMode
												big:self.bigMode];
	
	return res;
}

- (NSFont *)getCurrentFontForInput
{
	BOOL bold = (self.boldFaceMode | self.hiliteMode); // | self.h1Mode | self.h2Mode | self.h3Mode | self.h4Mode);
	BOOL italics = self.italicsMode;

	XTPrefs *prefs = [XTPrefs prefs];
	NSString *parameterizedFontName;
	
	if (prefs.inputFontIsSameAsDefaultFont.boolValue) {
		parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
	} else if (prefs.inputFontUsedEvenIfNotRequestedByGame.boolValue) {
		parameterizedFontName = [self.fontManager tadsInputParameterizedFontName];
	} else {
		//TODO how to handle?
		parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
	}
	
	NSArray *fontNames;
	if (self.htmlFontFaceList != nil && self.htmlFontFaceList.count >= 1) {
		fontNames = self.htmlFontFaceList;
	} else {
		fontNames = [NSArray arrayWithObject:parameterizedFontName];
	}
	
	NSFont *res = [self.fontManager getFontWithName:fontNames
										  pointSize:nil
										   htmlSize:self.htmlFontSize
											   bold:bold
											italics:italics
											  small:self.smallMode
												big:self.bigMode];
	return res;
}

- (NSArray *)getDefaultTabStops
{
	return self.emptyArray;
}

//TODO mv
- (NSTextTab *)createTabStopAtRhsOfView
{
	CGFloat locRhsOfView = [self.textView findCoordOfTabAtRhsOfView];
	NSTextAlignment alignment = NSRightTextAlignment;
	NSDictionary<NSString *,id> *options = [NSDictionary dictionaryWithObject:XT_TAB_TYPE_RHS_OF_VIEW forKey:XT_TAB_TYPE_KEY];
	NSTextTab *tab = [[NSTextTab alloc] initWithTextAlignment:alignment location:locRhsOfView options:options];
	return tab;
}

//TODO mv
- (NSTextTab *)createTabStopAtHalfwayToRhsOfViewFromLoc:(CGFloat)fromLoc
{
	NSTextAlignment alignment = NSCenterTextAlignment;
	CGFloat tabLoc = [self.textView findCoordOfTabAtHalfwayToRhsOfViewFromLoc:fromLoc];
	NSDictionary<NSString *,id> *options = [NSDictionary dictionaryWithObject:XT_TAB_TYPE_HALFWAY_TO_RHS_OF_VIEW forKey:XT_TAB_TYPE_KEY];
	NSTextTab *tab = [[NSTextTab alloc] initWithTextAlignment:alignment location:tabLoc options:options];
	return tab;
}

//TODO mv
- (NSTextTab *)createTabFromModelEntry:(XTTabStopModelEntry *)modelEntry
						   minLocation:(CGFloat)minLocation
{
	//XT_DEF_SELNAME;
	//XT_TRACE_2(@"modelEntry.position=%f minLocation=%f", modelEntry.position.doubleValue, minLocation);
	
	NSTextAlignment alignment = modelEntry.nsTextAlignment;
	NSDictionary<NSString *,id> *options = modelEntry.nsTextAlignmentOptions;

	CGFloat pos = modelEntry.position.doubleValue;

	pos -= modelEntry.positionAdjustmentForDecimalPoint;
	if (pos < 0.0) {
		pos = 0.0;
	}
	
	if (pos <= minLocation) {
		// make sure new tab isn't at or before minLocation (typically == insertion pos.)
		pos = minLocation + 0.01;
	}
	
	CGFloat viewWidth = [self.textView findCoordOfTabAtRhsOfView];
	if (pos >= viewWidth) {
		pos = minLocation + 0.01;
	}

	NSTextTab *tab = [[NSTextTab alloc] initWithTextAlignment:alignment location:pos options:options];
	return tab;
}

- (NSTextTab *)createTabStopAtPosition:(CGFloat)posNewTab
{
	NSMutableDictionary<NSString *,id> *options = [NSMutableDictionary dictionaryWithCapacity:2];
	[options setObject:XT_TAB_TYPE_FIXED_POS forKey:XT_TAB_TYPE_KEY];
	
	NSTextTab *textTab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft location:posNewTab options:options];
	return textTab;
}

- (NSTextTab *)createTabStopAtNextMultipleOf:(NSNumber *)multiple fromLocation:(CGFloat)location
{
	CGFloat enSpaceWidth = [self getHtmlModeEnSpaceTabStopColumnWidthInPoints];
	CGFloat locNewTab = [self.tabStopModel findPositionOfNextTabWithMultiple:multiple
																fromPosition:location
																enSpaceWidth:enSpaceWidth];
	
	NSMutableDictionary<NSString *,id> *options = [NSMutableDictionary dictionaryWithCapacity:2];
	[options setObject:XT_TAB_TYPE_MULTIPLE forKey:XT_TAB_TYPE_KEY];
	[options setObject:multiple forKey:XT_TAB_MULTIPLE_KEY];
	
	NSTextTab *textTab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft location:locNewTab options:options];
	return textTab;
}

- (CGFloat)getPlainTextModeTabStopColumnWidthInPoints
{
	NSFont *outputFont = [self getCurrentFontForOutput];
	CGFloat fontSize = outputFont.pointSize;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0) + 1.0) * 4.0;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0)) * 4.0;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0) + 1.0) * 3.0;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0)) * 4.0;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0) + 2.0) * 3.0;
	CGFloat columnWidthInPoints = ceil(fontSize / 2.0);
	columnWidthInPoints *= 4.0;
	columnWidthInPoints *= 0.9;
		//TODO make factor dep on fontSize?
	columnWidthInPoints = ceil(columnWidthInPoints);

	//TODO user option?
	//- En (typography), a unit of width in typography, equivalent to half the height of a given font. (see also en dash)

	return columnWidthInPoints;
}

- (CGFloat)getHtmlModeEnSpaceTabStopColumnWidthInPoints
{
	NSFont *outputFont = [self getCurrentFontForOutput];
	CGFloat fontSize = outputFont.pointSize;
	CGFloat columnWidthInPoints = ceil(fontSize / 2.0);
	
	return columnWidthInPoints;
}

- (CGFloat)getListItemPrefixColumnWidthInPoints
{
	CGFloat res = [self getPlainTextModeTabStopColumnWidthInPoints] * 0.7;
	return res;
}

//TODO mv down?
- (BOOL)isInTabOppressingTag
{
	BOOL res = (self.h1Mode || self.h2Mode || self.h3Mode || self.h4Mode);
	return res;
}

- (BOOL)isInListMode
{
	BOOL res = (self.orderedListMode || self.unorderedListMode);
	return res;
}

//-------------------------------------------------------------------------------------------

#pragma mark XTOutputFormatterProtocol

- (NSArray *)handleHtmlTagQ:(XTHtmlTagQ *)tag
{
	NSString *quote = @"\"";
	//TODO? diff char for open / close?
	
	NSMutableArray *res = [self makeArrayWithPendingWhitespace];
	[self clearPendingWhitespace];

	[self.linebreakHandler2 handleText:quote];
	
	[res addObject:[self makeRegularOutputElement:quote]];
	
	return res;
}

- (NSArray *)handleHtmlTagTab:(XTHtmlTagTab *)tag
{
	//XT_DEF_SELNAME;
	
	NSArray *res;
	
	if ([tag hasAttribute:@"id"]) {

		res = [self prepareForHtmlTagTabDelayed:tag];

	} else if ([self isInTabOppressingTag]) {
	
		res = self.emptyArray;
		
	} else if ([tag hasAttribute:@"to"]) {
		
		res = [self prepareForHtmlTagTabDelayed:tag];

	} else if ([tag hasAttribute:@"indent"]) {
		
		res = [self prepareForHtmlTagTabDelayed:tag];

	} else if ([tag hasAttribute:@"multiple"]) {

		res = [self prepareForHtmlTagTabDelayed:tag];

	} else if ([tag hasAttribute:@"align"]) {
		
		res = [self prepareForHtmlTagTabDelayed:tag];
	}

	return res;
}

- (NSArray *)prepareForHtmlTagTabDelayed:(XTHtmlTagTab *)tag
{
	// process pending whitespace so that we have the correct insertion position...
	NSMutableArray *tempRes = [self makeArrayWithPendingWhitespace];
	[self clearPendingWhitespace];
	
	// ...when this element gets handled by handleHtmlTagTabDelayed below:
	XTFormattedOutputElement *fmtElt = [XTFormattedOutputElement tabElement:tag];
	[tempRes addObject:fmtElt];
	
	NSArray *res = tempRes;
	return res;
}

//TODO? too long - break into several funcs
- (NSArray *)handleHtmlTagTabDelayed:(XTHtmlTagTab *)tag
{
	XT_DEF_SELNAME;
	
	NSString *id = [tag attributeAsString:@"id"];
	NSString *to = [tag attributeAsString:@"to"];
	NSString *align = [tag attributeAsString:@"align"];
	NSString *decimal = [tag attributeAsString:@"dp"];
	NSNumber *multiple = [tag attributeAsNumber:@"multiple"];
	
	NSArray *res;

	if ([tag hasAttribute:@"id"]) {

		CGFloat position = [self findXCoordOfInsertionPointForTabs];
		[self.tabStopModel addTabStopWithId:id position:position alignment:align decimalChar:decimal];
		res = self.emptyArray;

	} else if ([tag hasAttribute:@"to"]) {
		
		[self.linebreakHandler2 handleTagTab];
		
		XTTabStopModelEntry *modelTabStop = [self.tabStopModel findTabWithId:to];
		if (modelTabStop == nil) {
			// toId is unknown - at least add a space
			[self addPendingSpecialSpace:self.htmlSpecialSpaceForOneSpace];
			self.shouldWriteWhitespace = YES;
		} else {
			CGFloat position = [self findXCoordOfInsertionPointForTabs];
			NSMutableParagraphStyle *pgStyle = [self getParagraphStyleAtStartOfLastParagraph];
			NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
			if ([tag hasAttribute:@"align"] || [tag hasAttribute:@"dp"]) {
				XTTabStopModelEntry *overriddenModelTabStop = [self.tabStopModel createOverridenTabStop:modelTabStop align:align decimalChar:decimal];
				modelTabStop = overriddenModelTabStop;
			}
			XTTabStopModelEntry *usedModelTabStop = [modelTabStop copy];
			if (usedModelTabStop.decimalPointAligned) {
				NSFont *font = [self getCurrentFontForOutput];
				NSString *decPtChar = usedModelTabStop.effectiveDecimalChar;
				usedModelTabStop.positionAdjustmentForDecimalPoint = [XTFontUtils defaultWidthOfString:decPtChar inFont:font];
					// needed for tab recalc
			}
			NSTextTab *newTabStop = [self createTabFromModelEntry:usedModelTabStop minLocation:position];
			[tabStops addObject:newTabStop];
			pgStyle.tabStops = tabStops;
			[self applyParagraphStyleToLastParagraph:pgStyle];
			[self addPendingTab];
		}
		res = self.emptyArray;
		
	} else if ([tag hasAttribute:@"multiple"]) {

		[self.linebreakHandler2 handleTagTab];

		NSMutableParagraphStyle *pgStyle = [self getParagraphStyleAtStartOfLastParagraph];
		
		if (! [self paragraphStyleHasTabStopAtRhsOfWindow:pgStyle]) {
			
			CGFloat position = [self findXCoordOfInsertionPointForTabs];
			NSTextTab *newTabStop = [self createTabStopAtNextMultipleOf:multiple fromLocation:position];
			NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
			[tabStops addObject:newTabStop];
			pgStyle.tabStops = tabStops;
			[self applyParagraphStyleToLastParagraph:pgStyle];
			[self addPendingTab];
			
		} else {
			
			[self addPendingTabWithIndent:multiple];
		}

	} else if ([tag hasAttribute:@"indent"]) {
		
		[self.linebreakHandler2 handleTagTab];
		
		 NSNumber *indent = [tag attributeAsNumber:@"indent"];
		 if (indent != nil) {
			if (indent.integerValue >= 1) {
				[self addPendingTabWithIndent:indent];
				self.shouldWriteWhitespace = YES;
			}
		 }
		
	} else if ([tag hasAttribute:@"align"]) {
		
		XTTabStopAlignment alignEnum = [self.tabStopModel alignmentFromString:align];
		
		if (alignEnum == XTTABSTOP_ALIGN_RIGHT) {

			NSMutableParagraphStyle *pgStyle = [self getParagraphStyleAtStartOfLastParagraph];

			if (! [self paragraphStyleHasTabStopAtRhsOfWindow:pgStyle]) {
			
				[self.linebreakHandler2 handleTagTab];
				
				NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
				NSTextTab *newTabStop = [self createTabStopAtRhsOfView];
				[tabStops addObject:newTabStop];
				pgStyle.tabStops = tabStops;
				[self applyParagraphStyleToLastParagraph:pgStyle];
				
				[self addPendingTab];
			}
			
		} else if (alignEnum == XTTABSTOP_ALIGN_CENTER) {
			
			NSMutableParagraphStyle *pgStyle = [self getParagraphStyleAtStartOfLastParagraph];
			
			[self.linebreakHandler2 handleTagTab];
			
			NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
			CGFloat fromLoc = [self findXCoordOfInsertionPointForTabs];
			NSTextTab *newTabStop = [self createTabStopAtHalfwayToRhsOfViewFromLoc:fromLoc];
			[tabStops addObject:newTabStop];
			pgStyle.tabStops = tabStops;
			[self applyParagraphStyleToLastParagraph:pgStyle];
			
			[self addPendingTab];
			
		} else {
			XT_ERROR_1(@"Unexpected tab alignment: %d", alignEnum);
		}
		
		res = self.emptyArray;
	
	} else {
		
		XT_ERROR_0(@"not handling attribute id, to, multiple or align");
		res = self.emptyArray;
	}
		
	return res;
}

 - (BOOL)paragraphStyleHasTabStopAtRhsOfWindow:(NSParagraphStyle *)pgStyle
 {
	BOOL res = NO;
	NSArray *tabStops = pgStyle.tabStops;
	if (tabStops.count >= 1) {
		NSTextTab *lastTabStop = tabStops[tabStops.count - 1];
		res = [self.textView tabStopIsAtRhsOfView:lastTabStop];
	}
	return res;
 }

- (void)recalcAllTabStops
{
	XT_DEF_SELNAME;

	//XTTimer *timer = [XTTimer fromNow];
	
	NSTextStorage *textStorage = [self.textView textStorage];
	NSUInteger textStorageLen = textStorage.length;
	NSString *textStorageString = textStorage.string;
	NSString *sep = @"\t"; // tab char

	NSUInteger countTabstopsRecalcd = 0;

	NSUInteger idx = 0;
	const NSUInteger recalcLenLimit = 9000;
	if (textStorageLen > recalcLenLimit) {
		// Limit amount of buffered text to recalc. It gets exponentially more time-consuming :-(
		// ex: textStorageLen = 10000 -> idx should start at 1000 (10000 - 9000)
		idx = textStorageLen - recalcLenLimit;
	}
	const NSUInteger idxStart = idx;
	
	[self.textView ensureLayoutForTextContainer];
	
	while (idx < textStorageLen) {
		//XT_WARN_1(@"para idx=%lu", idx);
		NSRange paraRange = [XTStringUtils rangeOfNextParagraphIn:textStorageString fromLoc:idx];
		
		NSMutableParagraphStyle *oldParaStyle = [textStorage attribute:NSParagraphStyleAttributeName atIndex:idx effectiveRange:nil];
		if (oldParaStyle == nil) {
			//XT_WARN_0(@"oldParaStyle == nil");
			// This can happen sometimes. Not a problem.
		} else {
			// These are set when/if needed
			NSArray *oldTabStops = nil;
			NSMutableParagraphStyle *newParaStyle = nil;
			NSMutableArray *newTabStops = nil;
			
			NSAttributedString *paraAttrStr = [textStorage attributedSubstringFromRange:paraRange];
			NSArray *partsArray = [XTStringUtils splitAttributedString:paraAttrStr bySeparator:sep];
			
			NSUInteger insLoc = paraRange.location;
			NSUInteger tabStopIdx = 0;

			for (NSAttributedString *part in partsArray) {
				if ([part.string isEqualToString:sep]) {
					if (tabStopIdx == 0) {
						oldTabStops = oldParaStyle.tabStops;
						newParaStyle = [oldParaStyle mutableCopy];
						newTabStops = [oldTabStops mutableCopy];
					}
					
					if (tabStopIdx >= oldTabStops.count) {
						XT_ERROR_0(@"tabStopIdx >= oldTabStops.count");
					}
					
					NSTextTab *oldTabStop = oldTabStops[tabStopIdx];
					NSUInteger insLocBeforeTab = insLoc - 1;

					NSTextTab *newTabStop = [self recalcTabStop:oldTabStop fromInsertionLoc:insLocBeforeTab];
					newTabStops[tabStopIdx] = newTabStop;
					tabStopIdx += 1;

					newParaStyle.tabStops = newTabStops;
					[textStorage addAttribute:NSParagraphStyleAttributeName value:newParaStyle range:paraRange];
					
					//XTTimer *timerLayout = [XTTimer fromNow];
					[self.textView ensureLayoutForTextContainer];
					//if (! self.isForBanner) {
					//	XT_WARN_1(@"ensureLayout took %lf secs", [timerLayout timeElapsed]);
					//}
					
					countTabstopsRecalcd += 1;
				}
				insLoc += part.length;
			}
		}
		idx += paraRange.length;
	}

	//if (! self.isForBanner) {
	//	XT_WARN_4(@"took %lf secs for %lu chars, %lu tabstops, idxStart=%lu", [timer timeElapsed], textStorageLen, countTabstopsRecalcd, idxStart);
	//}
}

- (NSTextTab *)recalcTabStop:(NSTextTab *)oldTabStop fromInsertionLoc:(NSUInteger)insLoc
{
	XT_DEF_SELNAME;

	NSTextTab *newTabStop = nil;
	
	if ([self.textView tabStopIsAtRhsOfView:oldTabStop]) {
		newTabStop = [self createTabStopAtRhsOfView];

	} else if ([self.textView tabStopIsAtHalfwayToRhsOfView:oldTabStop]) {
		CGFloat xCoord = [self findXCoordAfterLoc:insLoc];
		newTabStop = [self createTabStopAtHalfwayToRhsOfViewFromLoc:xCoord];

	} else if ([self.textView tabStopIsToId:oldTabStop]) {
		XTTabStopModelEntry *modelTabStop = [oldTabStop.options objectForKey:XT_TAB_MODEL_ENTRY_KEY];
		CGFloat xCoord = [self findXCoordAfterLoc:insLoc];
		newTabStop = [self createTabFromModelEntry:modelTabStop minLocation:xCoord];

	} else if ([self.textView tabStopIsAtMultiple:oldTabStop]) {

		NSNumber *multiple = [oldTabStop.options objectForKey:XT_TAB_MULTIPLE_KEY];
		CGFloat xCoord = [self findXCoordAfterLoc:insLoc];
		newTabStop = [self createTabStopAtNextMultipleOf:multiple fromLocation:xCoord];

	} else if ([self.textView tabStopIsAtFixedPos:oldTabStop]) {
		// used for list items
	
		newTabStop = oldTabStop;
		
	} else {
		XT_ERROR_0(@"Unexpected tab type");
	}
	
	return newTabStop;
}

//TODO mv
- (NSParagraphStyle *)paragraphStyleOfRange:(NSRange)range
{
	NSTextStorage *ts = [self.textView textStorage];
	NSParagraphStyle *res = nil;
	
	if (range.location != NSNotFound && range.length >= 1) {
		// There is some text in current paragraph
		NSDictionary *attrs = [ts attributesAtIndex:range.location effectiveRange:nil];
		res = [attrs objectForKey:NSParagraphStyleAttributeName];
	}

	return res;
}

//TODO  mv
- (CGFloat)findXCoordOfInsertionPointForTabs
{
	[self.textView setInsertionPointAtEndOfText];
		// Because text changes in other (banner) text views can mess with our text view's ins pos.
	
	CGFloat res = [self.textView findXCoordOfInsertionPointRaw];
	res = [self adjustTextXCoordforInsetAndPadding:res];
	return res;
}

//TODO  mv
- (CGFloat)findXCoordAfterLoc:(NSUInteger)loc
{
	NSRange range = NSMakeRange(loc, 1);
	CGFloat res = [self.textView findEndXCoordOfTextInRange:range];
	res = [self adjustTextXCoordforInsetAndPadding:res];
	return res;
}

//TODO  mv
- (CGFloat)adjustTextXCoordforInsetAndPadding:(CGFloat)textXCoord
{
	CGFloat res = textXCoord;
	res -= self.textView.leftRightInset;
	res -= self.textView.textContainer.lineFragmentPadding;
	if (res < 0.0) {
		res = 0.0;
	}
	return res;
}

//TODO unit test newline gen:
- (NSArray *)handleHtmlTagBlockQuote:(XTHtmlTagBlockQuote *)tag
{
	//TODO consider interaction / flags with h*, list modes, etc.

	if (! tag.closing) {
		self.blockquoteLevel += 1;
	} else {
		if (self.blockquoteLevel >= 1) {
			self.blockquoteLevel -= 1;
		}
	}
	
	return self.emptyArray;
}

//TODO make block level?!
- (NSArray *)handleHtmlTagBr:(XTHtmlTagBr *)tag
{
	NSMutableArray *res = [NSMutableArray arrayWithCapacity:2];

	if (tag.closing) {
		return res;
	}
	
	NSInteger height = -1;
	if ([tag hasAttribute:@"height"]) {
	 	height = [tag attributeAsUInt:@"height"];
	}

	NSString *s = [self.linebreakHandler2 handleTagBr:height];
	self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
	if (! self.shouldWriteWhitespace) {
		[self clearTextModelTrailingWhitespaceInLastParagraph];
		[self clearPendingWhitespace];
	}
	
	if (s != nil && s.length >= 1) {
		NSMutableAttributedString *attrString = [self makeAttributedStringForOutput:s];
		[res addObject:[XTFormattedOutputElement regularOutputElement:attrString]];
	}

	return res;
}

- (NSArray *)handleHtmlTagDiv:(XTHtmlTagDiv *)tag
{
	[self setTextAlignModeFromAlignAttribute:tag defaultToLeftAligned:YES];
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagP:(XTHtmlTagP *)tag
{
	[self setTextAlignModeFromAlignAttribute:tag defaultToLeftAligned:NO];

	return self.emptyArray;
}

- (void)setTextAlignModeFromAlignAttribute:(XTHtmlTag *)tag
					  defaultToLeftAligned:(BOOL)defaultToLeftAligned
{
	XT_DEF_SELNAME;

	if (! tag.closing) {
		NSString *attrNameAlign = @"align";
		if (! [tag hasAttribute:attrNameAlign]) {
			if (defaultToLeftAligned) {
				self.textAlignMode = XT_TEXT_ALIGN_LEFT;
			}
		} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"right"]) {
			self.textAlignMode = XT_TEXT_ALIGN_RIGHT;
		} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"center"]) {
			self.textAlignMode = XT_TEXT_ALIGN_CENTER;
		} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"left"]) {
			self.textAlignMode = XT_TEXT_ALIGN_LEFT;
		} else if ([tag hasAttribute:attrNameAlign]) {
			NSString *alignAttr = [tag attributeAsString:@"align"];
			XT_ERROR_1(@"unknown align attr \"%@\"", alignAttr)
			self.textAlignMode = XT_TEXT_ALIGN_LEFT;
		}
	} else {
		self.textAlignMode = XT_TEXT_ALIGN_LEFT;
	}
}

- (NSArray *)handleHtmlTagTitle:(XTHtmlTagTitle *)tag
{
	NSArray *res;
	
	if (! tag.closing) {
		//[stream startTitle];
		self.receivingGameTitle = YES;
		//self.gameTitle = [NSMutableString stringWithString:@""];
		res = [self makeArrayWithGameTitleElement:@"{{clear}}"];
			//TODO hack
	} else {
		//[stream endTitle];
		self.receivingGameTitle = NO;
		res = self.emptyArray;
	}
	
	return res;
}

- (NSArray *)handleHtmlTagHr:(XTHtmlTagHr *)tag
{
	NSString *horRulerOneChar = @"–";
	
	NSFont *font = [self getCurrentFontForOutput];
	CGFloat widthOfHorRulerOneChar = [XTFontUtils defaultWidthOfString:horRulerOneChar inFont:font];
	CGFloat widthOfTextView = [self.textView findTotalWidthAdjustedForInset];
	NSUInteger charsInHorRuler = floor(widthOfTextView / widthOfHorRulerOneChar);
	if (charsInHorRuler < 1) {
		// for the sake of general formatting logic
		charsInHorRuler = 1;
	}
	
	NSString *horRuler = [XTStringUtils stringOf:charsInHorRuler string:horRulerOneChar];
	
	self.textAlignMode = XT_TEXT_ALIGN_LEFT;
	
	[self.linebreakHandler2 handleText:horRuler]; // so we're "in text"
	
	NSMutableArray *res = [NSMutableArray array];

	[res addObject:[self makeRegularOutputElement:horRuler]];
	
	return res;
}

- (NSArray *)handleHtmlTagA:(XTHtmlTagA *)tag
{
	if (! tag.closing) {
		self.activeTagA = tag;
	} else {
		self.activeTagA = nil;
	}
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagB:(XTHtmlTagB *)tag
{
	self.boldFaceMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagI:(XTHtmlTagI *)tag
{
	self.italicsMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagU:(XTHtmlTagU *)tag
{
	self.underlineMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagSmall:(XTHtmlTagSmall *)tag
{
	self.smallMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagBig:(XTHtmlTagBig *)tag
{
	self.bigMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagAboutBox:(XTHtmlTagAboutBox *)tag
{
	self.aboutBoxMode = (! tag.closing);
	
	return self.emptyArray;
}

//TODO unit test
- (NSArray *)handleHtmlTagBanner:(XTHtmlTagBanner *)tag
{
	XT_DEF_SELNAME;

	if (self.isForT3) {
		XT_WARN_0(@"<banner> tag and contents ignored for T3 game");
	}
	
	XTFormattedOutputElement *e = nil;
	if (! tag.closing) {
		self.tagBannerDepth += 1;
		if (! self.isForBanner) {
			if (! self.isForT3) {
				e = [XTFormattedOutputElement bannerStartElement:tag];
			}
		} else {
			if (self.tagBannerDepth >= 2) {
				XT_WARN_0("nested <banner> ignored")
			}
		}
	} else {
		NSInteger oldTagBannerDepth = self.tagBannerDepth;
		self.tagBannerDepth -= 1;
		if (self.tagBannerDepth < 0) {
			self.tagBannerDepth = 0;
		}
		if (! self.isForBanner) {
			XT_WARN_0("extraneous </banner> ignored")
		} else {
			if (oldTagBannerDepth == 1) {
				if (! self.isForT3) {
					e = [XTFormattedOutputElement bannerEndElement];
				}
			} else {
				XT_WARN_0("extraneous </banner> ignored")
			}
		}
	}

	NSArray *res;
	if (e != nil) {
		res = [NSArray arrayWithObject:e];
	} else {
		res = self.emptyArray;
	}
	return res;
}

- (NSArray *)handleHtmlTagCenter:(XTHtmlTagCenter *)tag
{
	self.textAlignMode = (tag.closing ? XT_TEXT_ALIGN_LEFT : XT_TEXT_ALIGN_CENTER);
	
	return self.emptyArray;
		//TODO might be at cmd input, so should really mv cursor to left of line - return "non-printing space"?
}

- (NSArray *)handleHtmlTagH1:(XTHtmlTagH1 *)tag
{
	self.h1Mode = (! [self isInListMode] && ! tag.closing);
	[self setTextAlignModeFromAlignAttribute:tag defaultToLeftAligned:NO];

	[self clearListModes];
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagH2:(XTHtmlTagH2 *)tag
{
	self.h2Mode = (! [self isInListMode] && ! tag.closing);
	[self setTextAlignModeFromAlignAttribute:tag defaultToLeftAligned:NO];

	[self clearListModes];
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagH3:(XTHtmlTagH3 *)tag
{
	self.h3Mode = (! [self isInListMode] && ! tag.closing);
	[self setTextAlignModeFromAlignAttribute:tag defaultToLeftAligned:NO];
	
	[self clearListModes];
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagH4:(XTHtmlTagH4 *)tag
{
	self.h4Mode = (! [self isInListMode] && ! tag.closing);
	[self setTextAlignModeFromAlignAttribute:tag defaultToLeftAligned:NO];
	
	[self clearListModes];
	
	return self.emptyArray;
}

//TODO mv
- (void)clearListModes
{
	//TODO find a more systematic solution for this whole business...
	self.unorderedListMode = NO;
	self.orderedListMode = NO;
}

- (NSArray *)handleHtmlTagOl:(XTHtmlTagOl *)tag
{
	self.orderedListMode = (! tag.closing);
	self.orderedListIndex = 0;
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagUl:(XTHtmlTagUl *)tag
{
	self.unorderedListMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagLi:(XTHtmlTagLi *)tag
{
	NSArray *res = nil;
	
	if (self.orderedListMode) {
		if (! tag.closing) {
			self.orderedListIndex += 1;

			NSString *s = [NSString stringWithFormat:@"%lu.", self.orderedListIndex];
			res = [self makeArrayWithRegularOutputElement:s];

			[self addPendingTab];
			
		} else {
			//s = [self.linebreakHandler2 handleEndLi]; TODO rm handleEndLi
		}

	} else if (self.unorderedListMode) {
		if (! tag.closing) {
			res = [self makeArrayWithRegularOutputElement:@"\u2022"];
		
			[self addPendingTab];

		} else {
			//s = [self.linebreakHandler2 handleEndLi];
		}

	} else {
		NSString *s = nil;
		if (! tag.closing) {
			s = [NSString stringWithFormat:@" "];
		} else {
			//s = [self.linebreakHandler2 handleEndLi];
		}
		if (s != nil) {
			res = [self makeArrayWithRegularOutputElement:s];
		}
	}
	
	if (res == nil) {
		res = self.emptyArray;
	}

	return res;
}

- (NSArray *)handleHtmlTagNoop:(XTHtmlTagNoop *)tag
{
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagQuestionMarkT2:(XTHtmlTagQuestionMarkT2 *)tag
{
	/* TODO handle:
	 *   Write out the special <?T2> HTML sequence, in case we're on an HTML
	 *   system.  This tells the HTML parser to use the parsing rules for
	 *   TADS 2 callers.
	 */
	 //outformat("\\H+<?T2>\\H-");
	
	 return self.emptyArray;
}

- (NSArray *)handleHtmlTagQuestionMarkT3:(XTHtmlTagQuestionMarkT3 *)tag
{
	/* TODO handle, but for T3:
	 *   Write out the special <?T2> HTML sequence, in case we're on an HTML
	 *   system.  This tells the HTML parser to use the parsing rules for
	 *   TADS 2 callers.
	 */
	//outformat("\\H+<?T2>\\H-");
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTt:(XTHtmlTagTt *)tag
{
	self.ttMode = (! tag.closing);
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTable:(XTHtmlTagTable *)tag
{
	self.textAlignMode = XT_TEXT_ALIGN_LEFT;
	//TODO hacky? clear more flags?
	//TODO also do such clearing for other blk lvl tags? ul ol ...?
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTr:(XTHtmlTagTr *)tag
{
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTh:(XTHtmlTagTh *)tag
{
	//TODO pass thru some "filter" wrt. markup ws?
	
	NSArray *res;
	if (tag.closing) {
		res = [self makeArrayWithRegularOutputElement:tableCellSeparator];
			//TODO tab?
	} else {
		res = self.emptyArray;
	}
	
	return res;
}

- (NSArray *)handleHtmlTagTd:(XTHtmlTagTd *)tag
{
	//TODO pass thru some "filter" wrt. markup ws?
	
	NSArray *res;
	if (tag.closing) {
		//TODO exp!
		XTHtmlWhitespace *ws = [XTHtmlWhitespace whitespaceWithText:tableCellSeparator];
		[self addPendingWhitespace:ws];
		//TODO exp! rm'd: res = [self makeArrayWithRegularOutputElement:tableCellSeparator];
		//TODO tab?
	} else {
		res = self.emptyArray;
	}
	
	return res;
}

- (NSArray *)handleHtmlTagCite:(XTHtmlTagCite *)tag
{
	self.italicsMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagFont:(XTHtmlTagFont *)tag
{
	BOOL allow = self.prefs.allowGamesToSetFonts.boolValue;
	
	if (allow && ! tag.closing) {
		
		NSInteger sign;
		NSUInteger tempHtmlFontSize;
		NSInteger htmlFontSize = 3; // default
		
		// size: A number from 1 to 7 that defines the size of the text. Browser default is 3.
		
		if ([tag attribute:@"size" asOptionalSign:&sign andUint:&tempHtmlFontSize]) {
			// Note_ some of the special case handling below might look odd,
			// but it's there to mimic HTML TADS behaviour
			if (sign == 0) {
				if (tempHtmlFontSize >= 1 && tempHtmlFontSize <= 7) {
					htmlFontSize = tempHtmlFontSize;
				}
			} else if (sign < 0) {
				if (tempHtmlFontSize > 2 || tempHtmlFontSize == 0) {
					tempHtmlFontSize = 2;
				}
				htmlFontSize += (sign * tempHtmlFontSize);
			} else {
				if (tempHtmlFontSize == 0) {
					tempHtmlFontSize = 3;
				} else if (tempHtmlFontSize > 4) {
					tempHtmlFontSize = 4;
				}
				htmlFontSize += (sign * tempHtmlFontSize);
			}
			self.htmlFontSize = [NSNumber numberWithUnsignedInteger:htmlFontSize];
			
		}

		NSArray *htmlFontFaceList = [tag attributeAsCommaSeparatedStrings:@"face"];
		if (htmlFontFaceList != nil && htmlFontFaceList.count >= 1) {
			self.htmlFontFaceList = htmlFontFaceList;
		}

		//TODO handle attr COLOR (dep on prefs)
		
		//TODO exp stop-gap fix to handle hint menus:
		NSString *color = [tag attributeAsString:@"color"];
		if (color != nil) {
			color = [color lowercaseString];
			self.htmlFontColor = color;
		}
		
	} else {

		self.htmlFontSize = nil;
		self.htmlFontFaceList = nil;
		self.htmlFontColor = nil;
	}
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagPre:(XTHtmlTagPre *)tag
{
	self.preMode = (! tag.closing);

	return self.emptyArray;
}

- (NSArray *)handleHtmlTagImg:(XTHtmlTagImg *)tag
{
	NSMutableArray *res = [NSMutableArray array];
	
	XTHtmlWhitespace *whitespace = [XTHtmlWhitespace whitespaceWithText:@" "];
	NSArray *formattedWhitespaceArray = [self handleWhitespace:whitespace];
	[res addObjectsFromArray:formattedWhitespaceArray];
	
	if ([tag hasAttribute:@"alt"]) {
		NSString *altText = [tag attributeAsString:@"alt"];
		if (altText != nil && altText.length >= 1) {
			//NSString *s = [NSString stringWithFormat:@"[Image \"%@\" not shown]\n", altText];
			NSArray *formattedAltTextArray = [self handleRegularText:altText];
			[res addObjectsFromArray:formattedAltTextArray];
			formattedWhitespaceArray = [self handleWhitespace:whitespace];
			[res addObjectsFromArray:formattedWhitespaceArray];
		}
	}

	return res;
}

- (NSArray *)handleHtmlTagTads2Hilite:(XTHtmlTagT2Hilite *)tag
{
	self.hiliteMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTads2Italics:(XTHtmlTagT2Italics *)tag
{
	self.italicsMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTads2TradStatusLine:(XTHtmlTagT2TradStatusLine *)tag
{
	XT_DEF_SELNAME;
	
	NSUInteger statusLineMode = [tag attributeAsUInt:@"mode"];
	return [self makeArrayWithStatusLineModeSElement:statusLineMode];
}

@end
