//
//  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 "XTHtmlTagContainer.h"
#import "XTHtmlTagContainer_private.h"
#import "XTHtmlTagA.h"
#import "XTHtmlTagHr.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 "XTHtmlTagTab.h"
#import "XTHtmlTagBr.h"
#import "XTHtmlTagP.h"
#import "XTHtmlTagTitle.h"
#import "XTHtmlTagFont.h"
#import "XTHtmlTagPre.h"
#import "XTHtmlTagImg.h"
#import "XTHtmlTagBody.h"
#import "XTHtmlTagTable.h"
#import "XTHtmlTagTr.h"
#import "XTHtmlTagTh.h"
#import "XTHtmlTagTd.h"
#import "XTHtmlTagT2TradStatusLine.h"
#import "XTHtmlTagWhitespace.h"
#import "XTHtmlTagQuotedSpace.h"
#import "XTHtmlTagSpecialSpace.h"
#import "XTHtmlTagNonbreakingSpace.h"
#import "XTHtmlTagText.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 "XTNotifications.h"
#import "XTTextAlignMode.h"
#import "XTCharConstants.h"
#import "XTTextTableBlock.h"
#import "XTTableColumnWidthTracker.h"
#import "XTMutableAttributedStringHelper.h"
#import "XTFormattingSpecificationForHtmlTag.h"
#import "XTHtmlUtils.h"
#import "XTRect.h"
#import "XTTabStopUtils.h"
#import "XTRequiredRectForTextCache.h"
#import "XTTextTab.h"
#import "XTOutputTextColorResult.h"
#import "XTColorUtils.h"
#import "XTColorationHelper.h"
#import "XTTabStopHelper.h"


@interface XTOutputFormatter ()

@property XTHtmlLinebreakHandler2 *linebreakHandler2;
@property BOOL afterBlockLevelSpacing;

@property XTPendingWhitespaceQueue *pendingWhitespaceQueue;

@property BOOL shouldWriteWhitespace;
	//TODO rename?

@property CGFloat pendingParagraphSpacingBefore;
@property XTTextAlignMode pTagTextAlignMode;

@property NSInteger tagBannerDepth;
@property NSInteger tableDepth;

@property NSArray *emptyArray;

@property XTFontManager *fontManager;
@property XTPrefs *prefs;
@property XTTabStopHelper *tabStopHelper;

@property XTHtmlTagSpecialSpace *htmlSpecialSpaceForOneSpace;

@end


@implementation XTOutputFormatter

static XTLogger* logger;

@synthesize textView = _textView;
@synthesize htmlMode = _htmlMode;
@synthesize formattingSpecForHtmlTag = _formattingSpecForHtmlTag;
@synthesize preMode = _preMode;
@synthesize isForTagBanner = _isForTagBanner;
@synthesize needsRecalcAllTabStops = _needsRecalcAllTabStops;

+ (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];
		_pTagTextAlignMode = XT_TEXT_ALIGN_LEFT;
		
		_prefs = [XTPrefs prefs];
		_tabStopHelper = [XTTabStopHelper new];
		
		_emptyArray = [NSArray array];
		_isForBanner = NO;
		_isForTagBanner = NO;
		_isForT3 = NO;
		_htmlSpecialSpaceForOneSpace = [XTHtmlTagSpecialSpace tagWithChar:' '];
		
		_formattingSpecForHtmlTag = [XTFormattingSpecificationForHtmlTag new];
		
		_preMode = NO;
		_tagBannerDepth = 0;
		_tableDepth = 0;
		_pendingParagraphSpacingBefore = 0.0;
		
		_needsRecalcAllTabStops = NO;
		
		[self resetFlags];
 	}
    return self;
}

- (void)setTextView:(XTTextView *)textView
{
	_textView = textView;
	self.tabStopHelper.textView = textView;
}

- (XTTextView *)textView
{
	return _textView;
}

- (NSTextStorage *)textStorage
{
	NSTextStorage *textStorage = [self.textView textStorage];
	return textStorage;
}

- (BOOL)isForTagBanner
{
	return _isForTagBanner;
}

- (void)setIsForTagBanner:(BOOL)isForTagBanner
{
	_isForTagBanner = isForTagBanner;
	if (isForTagBanner) {
		self.tagBannerDepth = 1;
	} else {
		self.tagBannerDepth = 0;
	}
}

- (void)setHtmlMode:(BOOL)htmlMode
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"%d", htmlMode);
	
	_htmlMode = htmlMode;
}

- (BOOL)htmlMode {
	return _htmlMode;
}

- (void)teardown
{
	_textView = nil;
	_linebreakHandler2 = nil;
	[_pendingWhitespaceQueue reset];
	_pendingWhitespaceQueue = nil;
	[self.tabStopHelper resetTabStopModel];
	_emptyArray = nil;
	_fontManager = nil;
	_prefs = nil;
	_htmlSpecialSpaceForOneSpace = nil;
}

- (void)resetFlags
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	[self resetNonHtmlModesFlags];
	[self resetHtmlModesFlags];
	[self.linebreakHandler2 resetForNextCommand];
	[self.tabStopHelper resetTabStopModel];

	self.formattingSpecForHtmlTag = [XTFormattingSpecificationForHtmlTag new];
}

- (void)resetForNextCommand
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	[self resetNonHtmlModesFlags];
	[self.linebreakHandler2 resetForNextCommand];
	[self.formattingSpecForHtmlTag.formattingSpec resetForNextCommand];
}

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

- (void)resetHtmlModesFlags
{
	//TODO !!! curr font spec?
	_preMode = NO;
	_tagBannerDepth = 0;
	_tableDepth = 0;
	_pendingParagraphSpacingBefore = 0.0;
}

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

//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;
}

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

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

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

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

- (BOOL)willProcessTag:(XTHtmlTag *)tag
{
	BOOL res =
		[tag isKindOfClass:[XTHtmlTagBanner class]] ||
		(((! self.isForT3) || (! self.isForTagBanner)));
	return res;
}

- (NSArray *)handleBlockLevelTagEntry:(XTHtmlTag *)tag
{
	NSMutableArray *resTemp = [NSMutableArray array];
	
	if (! [self shouldPrintOutputText]) {
		return resTemp;
	}
	
	//TODO must clear self.afterBlockLevelSpacing
	// - for non block level tag

	/*TODO adapt?... not here, in caller? is InlineTag event useful anymore?
	if (! tag.isBlockLevel) {
		if (! [tag isKindOfClass:[XTHtmlTagTab class]]) {
			if (! [tag isKindOfClass:[XTHtmlTagBr class]]) {
				[self.pendingWhitespaceQueue appendInlineTag];
			}
		}
	}*/
	
	//TODO try and combine with lbh2
	if ([tag needsBlockLevelSpacingBefore])	{
		if (! tag.closing) {
			if (! self.afterBlockLevelSpacing) {
				NSArray<NSMutableAttributedString *> *mutAttrStringArray = [self makeAttributedStringForOutput:@"\n"];
				for (NSMutableAttributedString *mutAttrString in mutAttrStringArray) {
					[resTemp addObject:[XTFormattedOutputElement regularOutputElement:mutAttrString]];
				}
				self.afterBlockLevelSpacing = YES;
			}
		} else {
			//XT_DEF_SELNAME;
			//XT_WARN_0(@"tag.closing == YES");
			//int brkpt = 1;
		}
	}
	NSString *resPrefix = [self.linebreakHandler2 handleBlockLevelNewline:tag];
	if (resPrefix != nil) {
		NSArray<NSMutableAttributedString *> *mutAttrStringArray = [self makeAttributedStringForOutput:resPrefix];
		for (NSMutableAttributedString *mutAttrString in mutAttrStringArray) {
			[resTemp addObject:[XTFormattedOutputElement regularOutputElement:mutAttrString]];
		}
	}

	[self handleWhitespaceBeforeOrAfterBlockLevelTag:resTemp];
	
	return resTemp;
}

//TODO !!! mv
- (void)handleWhitespaceBeforeOrAfterBlockLevelTag:(NSMutableArray *)res
{
	self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
	if (! self.shouldWriteWhitespace) {
		//XT_WARN_ENTRY;
		[self clearPendingWhitespace]; // must be done sync'ly!
		[res addObject:[XTFormattedOutputElement clearWhitespaceBeforeOrAfterBlockLevelTagOutputElement]];
	}
}

- (void)clearAfterBlockLevelSpacing
{
	self.afterBlockLevelSpacing = NO;
}

- (void)resetTagBannerDepth
{
	self.tagBannerDepth = 0;
}

- (NSArray *)handleBlockLevelTagExit:(XTHtmlTag *)tag
{
	NSMutableArray *res = [NSMutableArray array];

	if (! [self shouldPrintOutputText]) {
		return res;
	}

	NSString *resSuffix = [self.linebreakHandler2 handleBlockLevelNewline:tag];
	if (resSuffix != nil) {
		NSArray<NSMutableAttributedString *> *mutAttrStringArray = [self makeAttributedStringForOutput:resSuffix];
		for (NSMutableAttributedString *mutAttrString in mutAttrStringArray) {
			[res addObject:[XTFormattedOutputElement regularOutputElement:mutAttrString]];
		}
	}

	[self handleWhitespaceBeforeOrAfterBlockLevelTag:res];
		//TODO try and combine with lbh2
	if ([tag needsBlockLevelSpacingAfter])	{
		if (! self.afterBlockLevelSpacing) {
			NSArray<NSMutableAttributedString *> *mutAttrStringArray = [self makeAttributedStringForOutput:@"\n"];
			for (NSMutableAttributedString *mutAttrString in mutAttrStringArray) {
				[res addObject:[XTFormattedOutputElement regularOutputElement:mutAttrString]];
			}
			self.afterBlockLevelSpacing = YES;
		}
	}

	return res;
}

- (NSArray *)handleRegularText:(NSString *)string
{
	//XT_DEF_SELNAME;
	//XT_WARN_2(@"\"%@\" htmlMode=%d", string, self.htmlMode);

	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];
		
		NSArray<XTFormattedOutputElement *>* array = [self makeRegularOutputElements:string];
		for (XTFormattedOutputElement *fmtOutputElt in array) {
			[res addObject:fmtOutputElt];
			NSAttributedString *attrString = fmtOutputElt.attributedString;
			if (attrString != nil && attrString.length >= 1) {
				[self.linebreakHandler2 handleText:attrString.string];
			}
		}
		
	} else {
		if (self.receivingGameTitle) {
			NSArray *tempRes = [self makeArrayWithGameTitleElement:string];
			[res addObjectsFromArray:tempRes];
		} else if (! [self shouldPrintOutputText]) {
			// nothing
			int brkpt = 1;
		} 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];
					[res addObjectsFromArray:[self makeRegularOutputElements:s]];
					self.shouldWriteWhitespace = YES;
				}
			}
		}
	}
	
	return res;
}

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

- (void)addPendingWhitespace:(XTHtmlTagWhitespace *)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
		NSArray<NSMutableAttributedString *> *mutAttrStringArray = [self makeAttributedStringForOutput:wsOneCh];
		for (NSMutableAttributedString *mutAttrString in mutAttrStringArray) {
			[self.pendingWhitespaceQueue append:mutAttrString];
		}
	}
}

- (void)addPendingSpecialSpace:(XTHtmlTagSpecialSpace *)specialSpace
{
	NSString *string = [NSString stringWithFormat:@"%C", specialSpace.ch];
	NSArray<NSMutableAttributedString *> *mutAttrStringArray = [self makeAttributedStringForOutput:string];
	for (NSMutableAttributedString *mutAttrString in mutAttrStringArray) {
		[self.pendingWhitespaceQueue append:mutAttrString];
	}
}

- (NSArray<NSAttributedString *> *)createTabStringForIndent:(NSNumber *)indent
{
	NSArray<NSAttributedString *> *res = nil;
	
	if (indent != nil) {
		NSInteger indentInt = indent.integerValue;
		if (indentInt >= 1) {
			//TODO? doesn't behave exactly like HTML TADS, but very nearly
			NSMutableString *mutString = [NSMutableString stringWithCapacity:indentInt];
			for (NSInteger idx = 0; idx < indentInt; idx++) {
				[mutString appendString:EFFECTIVE_EN_SPACE_FOR_TABS_PROP_FONT];
				//[mutString appendString:UNICHAR_EN_SPACE]; // too narrow. 2 regular spaces looks like on qtads.
			}
			NSArray<NSMutableAttributedString *> *mutAttrStringArray = [self makeAttributedStringForOutput:mutString];
			res = mutAttrStringArray;
		}
	}
	
	return res;
}

- (NSArray<NSAttributedString *>*)createTabStringFor:(XTHtmlTagTab *)tag
{
	NSArray<NSMutableAttributedString *> *res = [self makeAttributedStringForOutput:@"\t"];
	[XTMutableAttributedStringHelper addTabTag:tag toString:[res lastObject]];
	return res;
}

- (NSArray<NSAttributedString *>*)createTabStringForId:(XTHtmlTagTab *)tag
{
	NSArray<NSMutableAttributedString *> *res = [self makeAttributedStringForOutput:ZERO_WIDTH_SPACE];
	[XTMutableAttributedStringHelper addTabTag:tag toString:[res lastObject]];
	return res;
}

- (NSArray<NSAttributedString *>*)createAttributedStringForChar:(unichar)ch
{
	NSString *string = [NSString stringWithFormat:@"%C", ch];
	NSArray<NSMutableAttributedString *> *res = [self makeAttributedStringForOutput:string];
	return res;
}

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

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

- (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 /*TODO rm || [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 arrayWithArray:[self makeRegularOutputElements:string]];
	return res;
}

//------

- (NSArray<XTFormattedOutputElement *>*)makeRegularOutputElements:(NSString *)string
{
	NSArray<NSMutableAttributedString *> *mutAttrStringArray;
	if (self.isForGridBanner) {
		NSMutableAttributedString *mutAttrString = [self makeAttributedStringForGridBanner:string];
		mutAttrStringArray = [NSArray arrayWithObject:mutAttrString];
	} else {
		mutAttrStringArray = [self makeAttributedStringForOutput:string];
	}
	
	NSMutableArray<XTFormattedOutputElement *> *res = [NSMutableArray arrayWithCapacity:mutAttrStringArray.count];
	for (NSMutableAttributedString *mutAttrString in mutAttrStringArray) {
		XTFormattedOutputElement *outputElement = [XTFormattedOutputElement regularOutputElement:mutAttrString];
		[res addObject:outputElement];
	}
	return res;
}

- (NSArray<NSMutableAttributedString *>*)makeAttributedStringForOutput:(NSString *)string
{
	if (! [self shouldPrintOutputText]) {
		return self.emptyArray;
	}
	
	NSDictionary *dict = [self getTextAttributesDictionaryForOutput];

	if ([string isEqualToString:@"\n"]) {
		XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
		if (formattingSpec.inListItem) {
			formattingSpec.inListItemAfterFirstParagraph = YES;
		}
	}
	
	NSMutableArray<NSAttributedString *> *mutRes = [NSMutableArray arrayWithCapacity:2];
	
	if ([self.linebreakHandler2 isAtStartOfLine]) {
		if (! [string isEqualToString:ZERO_WIDTH_SPACE]) {
			//TODO !!! skip if "\n"?
			if (! [string isEqualToString:@"\n"]) {
				NSMutableAttributedString *zwspAttrString = [[NSMutableAttributedString alloc] initWithString:ZERO_WIDTH_SPACE
																							   attributes:dict];
				[mutRes addObject:zwspAttrString];
			}
		} else {
			int brkpt = 1;
		}
	}
	
   	NSMutableAttributedString *mutAttrString = [[NSMutableAttributedString alloc] initWithString:string
																				   attributes:dict];
	if (dict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND] != nil) {
		[self removeTextBackgroundColorForNewlines:mutAttrString];
	}
	[mutRes addObject:mutAttrString];

	return [NSArray arrayWithArray:mutRes];
}

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

- (NSMutableAttributedString *)makeAttributedStringForGridBanner:(NSString *)string
{
	NSDictionary *attrs = [self getTextAttributesDictionaryForGridBanner];
	NSMutableAttributedString *mutAttrString = [[NSMutableAttributedString alloc] initWithString:string
																				   attributes:attrs];
	
	if (attrs[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND] != nil) {
		[self removeTextBackgroundColorForNewlines:mutAttrString];
	}
	
	return mutAttrString;
}

- (void)removeTextBackgroundColorForNewlines:(NSMutableAttributedString *)mutAttrString
{
	[XTStringUtils removeAttributeForNewlines:mutAttrString attrName:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND];
	[XTStringUtils removeAttributeForNewlines:mutAttrString attrName:NSBackgroundColorAttributeName];
}

//------

- (NSDictionary *)getTextAttributesDictionaryForOutput
{
	XT_DEF_SELNAME;

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

	NSArray *tabStops = [self.tabStopHelper getDefaultTabStops];
	[pgStyle setTabStops:tabStops];
	
	XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
	
	NSTextAlignment alignment = NSTextAlignmentLeft;
	
	if (formattingSpec.textAlignMode == XT_TEXT_ALIGN_LEFT) {
		alignment = NSTextAlignmentLeft;
	} else if (formattingSpec.textAlignMode == XT_TEXT_ALIGN_CENTER) {
		alignment = NSTextAlignmentCenter;
	} else if (formattingSpec.textAlignMode == XT_TEXT_ALIGN_RIGHT) {
		alignment = NSTextAlignmentRight;
	} else if (formattingSpec.textAlignMode == XT_TEXT_ALIGN_JUSTIFY) {
		alignment = NSTextAlignmentJustified;
	} else if (formattingSpec.textAlignMode == XT_TEXT_ALIGN_UNSPECIFIED) {
		alignment = NSTextAlignmentLeft;
	} else {
		XT_ERROR_1(@"unknown textAlignMode %d", formattingSpec.textAlignMode);
	}
	
	[pgStyle setAlignment:alignment];
	
	//TODO all "indenting" tags should contribute to actual indent size
	
	if (formattingSpec.blockquoteLevel >= 1) {

		CGFloat firstLineHeadIndent = [self getPlainTextModeTabStopColumnWidthInPoints];
		CGFloat indent = firstLineHeadIndent * formattingSpec.blockquoteLevel * 1.35;
		[pgStyle setFirstLineHeadIndent:indent];
		[pgStyle setHeadIndent:indent];
		
	} else if (formattingSpec.listLevel >= 1) {
		
		if (formattingSpec.inListItem) {
		
			CGFloat listLevelFactor;
			if (formattingSpec.listLevel == 1) {
				listLevelFactor = 1.0;
			} else {
				listLevelFactor = 1.35;
			}
			CGFloat firstLineHeadIndent;
			CGFloat headIndent;
			if (formattingSpec.inListItemAfterFirstParagraph) {
				// 2nd+ para in a list item should have both headIndent and firstLineHeadIndent == headIndent of 1st para
				firstLineHeadIndent = formattingSpec.listLevel * listLevelFactor * [self getPlainTextModeTabStopColumnWidthInPoints] +
										[self getListItemPrefixColumnWidthInPoints];
				headIndent = firstLineHeadIndent;
			} else {
				firstLineHeadIndent = formattingSpec.listLevel * listLevelFactor * [self getPlainTextModeTabStopColumnWidthInPoints];
				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);
			XTTextTab *newTabStop = [self.tabStopHelper createTabStopAtPosition:posNewTab];
			[tabStops addObject:newTabStop];
			pgStyle.tabStops = tabStops;
		}
	}

	if (formattingSpec.underline) {
		dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:1];
	}
	
	NSTextTableBlock *textTableCell = formattingSpec.activeTextTableCell;
	if (textTableCell != nil) {
		[pgStyle setTextBlocks:[NSArray arrayWithObjects:textTableCell, nil]];
			//TODO !!! must support nested tables
	}
	
	if (self.pendingParagraphSpacingBefore >= 1.0) {
		pgStyle.paragraphSpacingBefore = self.pendingParagraphSpacingBefore;
	}
	self.pendingParagraphSpacingBefore = 0.0;
	
	XTHtmlTagA *activeTagA = formattingSpec.activeTagA;
	if (activeTagA != nil) {
		NSString *href = [activeTagA attributeAsString:@"href"];
		if (href == nil) {
			href = @"";
		}
		dict[NSLinkAttributeName] = href;
		if ([activeTagA hasAttribute:@"append"]) {
			dict[XT_OUTPUT_FORMATTER_ATTR_CMDLINK_APPEND] = @"true";
		}
		if ([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
{
	[XTStringUtils applyParagraphStyle:pgStyle toLastParagraphOf:self.textView.textStorage];
}

- (NSMutableParagraphStyle *)getParagraphStyleAtStartOfOngoingParagraph
{
	NSMutableParagraphStyle *res = [XTStringUtils getParagraphStyleAtStartOfOngoingParagraphOf:self.textView.textStorage];
	return res;
}

- (NSDictionary *)getTextTemporaryAttributesDictionaryForOutput
{
	//XT_DEF_SELNAME;
	
	NSMutableDictionary *dict = nil;
	
	XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
	if (formattingSpec.activeTagA == nil) {
		return dict;
	}
		
	dict = [NSMutableDictionary dictionary];

	BOOL regularLink = ! [formattingSpec.activeTagA hasAttribute:@"plain"];
	
	if (regularLink) {
		NSColor *color = nil;
		XTOutputTextColorResult *colorResult = [self.colorationHelper getLinkColorResult];
		if ([self allowGameToSetColors]) {
			color = colorResult.htmlColor.color;
		}
		if (color == nil) {
			color = self.prefs.linksTextColor.value;
		}
		dict[NSForegroundColorAttributeName] = color;
		dict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = colorResult;
		
		if (! formattingSpec.underline) {
			if (! self.prefs.linksUnderline.value.boolValue) {
				dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleNone];
			}
			dict[XT_OUTPUT_FORMATTER_ATTR_UNDERLINE_FROM_PREFS] = [NSNumber numberWithBool:YES];
		}
		
	} else {
		// if "plain" link, set temp attrs so the link looks like regular text:

		dict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK] = [NSNumber numberWithBool:YES];
		XTOutputTextColorResult *colorResult = [self.colorationHelper getOutputTextColor:self.formattingSpecForHtmlTag];
		NSColor *color = nil;
		if ([self allowGameToSetColors]) {
			color = colorResult.htmlColor.color;
		} else {
			color = [self.colorationHelper getPrefsOutputTextColor];
		}
		dict[NSForegroundColorAttributeName] = color;
		dict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = colorResult;

		if (! formattingSpec.underline) {
			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
	
	XTOutputTextColorResult *outputTextColorResult = [self.colorationHelper getOutputTextColor:self.formattingSpecForHtmlTag];
	if ([self allowGameToSetColors]) {
		dict[NSForegroundColorAttributeName] = outputTextColorResult.htmlColor.color;
	} else {
		dict[NSForegroundColorAttributeName] = [self.colorationHelper getPrefsOutputTextColor];
	}
	dict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = outputTextColorResult;
	
	XTOutputTextColorResult *outputBackgroundColorResult = [self.colorationHelper getOutputBackgroundColor:self.formattingSpecForHtmlTag];
	if (outputBackgroundColorResult != nil) {
		if ([self allowGameToSetColors]) {
			dict[NSBackgroundColorAttributeName] = outputBackgroundColorResult.htmlColor.color;
		}
		dict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND] = outputBackgroundColorResult;
	}
	
	NSMutableParagraphStyle *pgStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
	dict[NSParagraphStyleAttributeName] = pgStyle;
	
	return dict;
}

- (NSDictionary *)getTextAttributesDictionaryForInput
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
	
	BOOL allowGameToSetColors = [self allowGameToSetColors];
	
	dict[NSFontAttributeName] = [self getCurrentFontForInput];
	
	XTHtmlColor *htmlColor = nil;
	XTColorSource colorSource = XT_COLOR_SOURCE_UNSPECIFIED;
	if (self.colorationHelper.bodyInputColor.color != nil) {
		htmlColor = self.colorationHelper.bodyInputColor;
		colorSource = XT_COLOR_SOURCE_INPUT_FROM_BODY;
	} else {
		htmlColor = [XTHtmlColor forNSColor:self.prefs.inputTextColor.value];
		colorSource = XT_COLOR_SOURCE_INPUT_FROM_PREFS;
	}
	
	XTOutputTextColorResult *colorResult = [XTOutputTextColorResult forHtmlColor:htmlColor colorSource:colorSource];
	dict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = colorResult;

	NSColor *color;
	if (allowGameToSetColors) {
		color = htmlColor.color;
	} else {
		color = self.prefs.inputTextColor.value;
	}
	dict[NSForegroundColorAttributeName] = color;

	return dict;
}

- (NSDictionary *)getTextAttributesDictionaryForGridBanner
{
	//XT_DEF_SELNAME;
	
	NSMutableDictionary *dict = [self getTextAttributesDictionaryCommonForOutput];
	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 //TODO !!! needed? if not, rm from method signature
										   htmlSize:htmlFontSize
											   bold:NO
											italics:NO];
	return res;
}

- (NSFont *)getCurrentFontForOutput
{
	XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
	
	BOOL bold = (formattingSpec.bold || formattingSpec.t2Hilite || formattingSpec.strong);
	BOOL italics = (formattingSpec.italic || formattingSpec.t2Italics || formattingSpec.cite || formattingSpec.em);
	
	NSString *parameterizedFontName;
	if (formattingSpec.tt || self.preMode || formattingSpec.code) {
		parameterizedFontName = [self.fontManager xtadsFixedWidthParameterizedFontName];
	} else {
		parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
	}
	
	NSArray *fontNames;
	if (formattingSpec.htmlFontFaceList != nil && formattingSpec.htmlFontFaceList.count >= 1) {
		fontNames = formattingSpec.htmlFontFaceList;
	} else {
		fontNames = [NSArray arrayWithObject:parameterizedFontName];
	}

	NSFont *res = [self.fontManager getFontWithName:fontNames
										  pointSize:nil
										   htmlSize:[NSNumber numberWithUnsignedInteger:formattingSpec.htmlSize]
											   bold:bold
											italics:italics];
	
	return res;
}

- (NSFont *)getCurrentFontForInput
{
	XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
	
	BOOL bold = (formattingSpec.bold || formattingSpec.t2Hilite);
	BOOL italics = formattingSpec.italic;

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

- (BOOL)allowGameToSetColors
{
	BOOL res;
	
	if ([self isForT2PlainTextBanner]) {
		res = NO;
	} else {
		NSNumber *valueObj = self.prefs.allowGamesToSetColors.value;
		res = (valueObj == nil || valueObj.boolValue);
	}
	
	return res;
}

- (BOOL)isForT2PlainTextBanner
{
	BOOL res = self.isForBanner && (! self.isForT3) && (! self.htmlMode);
	return res;
}

- (XTTextTab *)createTabStopAtRhsOfViewForOptTextTableBlock:(XTTextTableBlock *)textTableBlock
{
	XTTextTab *tab = [self.tabStopHelper createTabStopAtRhsOfViewForOptTextTableBlock:textTableBlock];
	_needsRecalcAllTabStops = YES;
	return tab;
}

- (XTTextTab *)createTabStopAtHalfwayToRhsOfViewFromLoc:(CGFloat)fromLoc textTableBlock:(XTTextTableBlock *)textTableBlock
{
	//XT_DEF_SELNAME;
	
	XTTextTab *tab = [self.tabStopHelper createTabStopAtHalfwayToRhsOfViewFromLoc:fromLoc textTableBlock:textTableBlock];
	_needsRecalcAllTabStops = YES;
	//XT_WARN_1(@"--> tabLoc=%lf", tabLoc);
	return tab;
}

- (CGFloat)getPlainTextModeTabStopColumnWidthInPoints
{
	NSFont *font = [self getCurrentFontForOutput];
	CGFloat res = [self.tabStopHelper getPlainTextModeTabStopColumnWidthInPoints:font];
	return res;
}

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

//TODO mv down?
- (BOOL)isInTabOppressingTag
{
	BOOL res = self.formattingSpecForHtmlTag.formattingSpec.isInHeader;
	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 addObjectsFromArray:[self makeRegularOutputElements:quote]];
	
	return res;
}

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

		res = [self handleHtmlTagTabWhenNotOppressed:tag];

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

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

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

		res = [self handleHtmlTagTabWhenNotOppressed:tag];

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

	return res;
}

//TODO !!! adapt: this doc is outdated/wrong
/* <tab> is handled in several steps:
 	1. Render any pending whitespace, so that the layout machinery can have coordinates.
 	   Such whitespace is returned by this method.
	2. Create the tab stop string itself.
 	   This is done in createTabStringWithoutTabAttr:ongoingParagraphStyleHasRightAlignedTab:, triggered by
 	   the extra XTFormattedOutputElement we append to the return array from this method.
 	3. Calculate the tab position.
       Done in applyTagTab: called from BaseTH expandTabsAndAppendToTextStorage:
*/
- (NSArray *)handleHtmlTagTabWhenNotOppressed:(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 createTabStringWithoutTabAttr:ongoingParagraphStyleHasRightAlignedTab:
	NSArray<NSAttributedString *> *attrStringArrayForTab = [self createTabStringWithoutTabAttr:tag];
	NSMutableAttributedString *mutAttrStringForTab = [XTStringUtils concatenateAttributedStringArray:attrStringArrayForTab];
	XTFormattedOutputElement *fmtElt = [XTFormattedOutputElement tabElement:tag attributedString:mutAttrStringForTab];
	[tempRes addObject:fmtElt];
	
	NSArray *res = tempRes;
	return res;
}

//TODO? too long - break into several funcs
- (NSArray<NSAttributedString *>*)createTabStringWithoutTabAttr:(XTHtmlTagTab *)tag;
{
	XT_DEF_SELNAME;
	
	NSMutableArray<NSAttributedString *> *res = [NSMutableArray arrayWithCapacity:3];
	
	if ([tag hasAttribute:@"id"]) {
		[res addObjectsFromArray:[self createTabStringForId:tag]];
		
	} else if ([tag hasAttribute:@"to"]) {
		
		NSString *to = [tag attributeAsString:@"to"];
		XTTabStopModelEntry *modelTabStop = [self.tabStopHelper findModelTabWithId:to];
		if (modelTabStop == nil) {
			// toId is unknown - at least add a space
			[res addObjectsFromArray:[self createAttributedStringForChar:self.htmlSpecialSpaceForOneSpace.ch]];
			self.shouldWriteWhitespace = YES;
		} else {
			[res addObjectsFromArray:[self createTabStringFor:tag]];
		}
		
		[self.linebreakHandler2 handleTagTab];

	} else if ([tag hasAttribute:@"multiple"]) {
		
		XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
		if (! formattingSpec.hasRightAlignedTab) {
			[res addObjectsFromArray:[self createTabStringFor:tag]];
		} else {
			NSNumber *multiple = [tag attributeAsNumber:@"multiple"];
			NSArray<NSAttributedString *> *array = [self createTabStringForIndent:multiple];
			if (array != nil) {
				[res addObjectsFromArray:array];
			}
		}
		
		[self.linebreakHandler2 handleTagTab];

	} else if ([tag hasAttribute:@"indent"]) {
		
		NSNumber *indent = [tag attributeAsNumber:@"indent"];
		if (indent != nil) {
			if (indent.integerValue >= 1) {
				[res addObjectsFromArray:[self createTabStringFor:tag]];
				
				[self.linebreakHandler2 handleTagTab];
				self.shouldWriteWhitespace = NO; //TODO exp for tab test game indent_2
			}
		}

	} else if ([tag hasAttribute:@"align"]) {
		
		NSString *align = [tag attributeAsString:@"align"];
		XTTabStopAlignment alignEnum = [self.tabStopHelper alignmentFromString:align];
		
		if (alignEnum == XTTABSTOP_ALIGN_RIGHT) {
			XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
			if (! formattingSpec.hasRightAlignedTab) {
				[res addObjectsFromArray:[self createTabStringFor:tag]];
				[self.linebreakHandler2 handleTagTab];
			}
			formattingSpec.hasRightAlignedTab = YES;
		} else if (alignEnum == XTTABSTOP_ALIGN_CENTER) {
			[res addObjectsFromArray:[self createTabStringFor:tag]];
			[self.linebreakHandler2 handleTagTab];
		} else if (alignEnum == XTTABSTOP_ALIGN_LEFT) {
			// nothing
		} else {
			XT_ERROR_1(@"Unexpected tab alignment: %d", alignEnum);
		}
		
	} else {
		XT_ERROR_0(@"not handling attribute id, to, multiple or align");
	}
	
	return res;
}

//TODO!!! too long - break into several funcs
- (NSMutableAttributedString *)applyTagTab:(XTHtmlTagTab *)tagTab
				forMutableAttributedString:(NSMutableAttributedString *)mutAttrString
{
	XT_DEF_SELNAME;
	
	NSMutableAttributedString *res = mutAttrString;
	
	[self.textView ensureLayoutForTextContainer];
	
	NSString *align = [tagTab attributeAsString:@"align"];
	NSString *decimal = [tagTab attributeAsString:@"dp"];
	
	if ([tagTab hasAttribute:@"id"]) {

		NSString *id = [tagTab attributeAsString:@"id"];
		
		CGFloat position = [self findXCoordOfInsertionPointForTabs2];
		
		[self.tabStopHelper addModelTabStopWithId:id position:position alignment:align decimalChar:decimal];
		
		//XT_WARN_2(@"defined tab id=%@ position=%lf", id, position);

	} else if ([tagTab hasAttribute:@"to"]) {
		
		NSString *to = [tagTab attributeAsString:@"to"];
		XTTabStopModelEntry *modelTabStop = [self.tabStopHelper findModelTabWithId:to];
		if (modelTabStop == nil) {
			XT_ERROR_1(@"modelTabStop == nil for to=%@", to);
		
		} else {
			CGFloat position = [self findXCoordOfInsertionPointForTabs2];
			
			NSMutableParagraphStyle *pgStyle = [self getParagraphStyleAtStartOfOngoingParagraph];
			BOOL hasOngoingParagraph = (pgStyle != nil);
			if (! hasOngoingParagraph) {
				pgStyle = [XTStringUtils mutableParagraphStyleFor:mutAttrString];
			}

			XTTextTableBlock *textTableBlock = [XTStringUtils tableCellForAttrString:mutAttrString atIndex:0];
			NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
			if ([tagTab hasAttribute:@"align"] || [tagTab hasAttribute:@"dp"]) {
				XTTabStopModelEntry *overriddenModelTabStop = [self.tabStopHelper createOverridenModelTabStop: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
			}
			XTTextTab *newTabStop = [self.tabStopHelper createTabFromModelEntry:usedModelTabStop
																	minLocation:position
																	   resizing:NO
															  optTextTableBlock:textTableBlock];
			//XT_WARN_2(@"created tab to=%@ position=%lf", to, newTabStop.location);
			[tabStops addObject:newTabStop];
			pgStyle.tabStops = tabStops;
			
			if (! hasOngoingParagraph) {
				[XTStringUtils applyParagraphStyle:pgStyle toAttrString:mutAttrString];
				//XT_WARN_0(@"! hasOngoingParagraph");
			} else {
				[self applyParagraphStyleToLastParagraph:pgStyle];
				//XT_WARN_0(@"hasOngoingParagraph");
			}

			//XT_WARN_1(@"applied tab to=%@", to);
		}

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

		NSNumber *multiple = [tagTab attributeAsNumber:@"multiple"];

		NSMutableParagraphStyle *pgStyle = [self getParagraphStyleAtStartOfOngoingParagraph];
		BOOL hasOngoingParagraph = (pgStyle != nil);
		if (! hasOngoingParagraph) {
			pgStyle = [XTStringUtils mutableParagraphStyleFor:mutAttrString];
		}

		if (! [self.tabStopHelper paragraphStyleHasTabStopAtRhsOfWindow:pgStyle]) {
			
			CGFloat position = [self findXCoordOfInsertionPointForTabs2];
			XTTextTab *newTabStop = [self.tabStopHelper createTabStopFor:mutAttrString
														atNextMultipleOf:multiple
															fromLocation:position];
			NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
			[tabStops addObject:newTabStop];
			pgStyle.tabStops = tabStops;
			
			if (! hasOngoingParagraph) {
				[XTStringUtils applyParagraphStyle:pgStyle toAttrString:mutAttrString];
			} else {
				[self applyParagraphStyleToLastParagraph:pgStyle];
			}
		}

	} else if ([tagTab hasAttribute:@"indent"]) {
		
		NSNumber *indentObj = [tagTab attributeAsNumber:@"indent"];
		NSUInteger indent = [indentObj unsignedIntegerValue];
		BOOL foldLeadingSpace = NO;
		NSString *replacement = @"";

		if (indent >= 1) {
			foldLeadingSpace = [self.tabStopHelper textStorageEndsWithARegularSpace];
				//TODO !!! can mv into stringForTabIndent:
			replacement = [self.tabStopHelper stringForTabIndent:indent foldLeadingSpace:foldLeadingSpace];
		}
		//TODO !!! adapt: check if mutAttrString != "\t"
		res = [[NSMutableAttributedString alloc] initWithAttributedString:mutAttrString];
		NSRange range = NSMakeRange(0, mutAttrString.length);
		[res replaceCharactersInRange:range withString:replacement];
		
	} else if ([tagTab hasAttribute:@"align"]) {
		
		XTTabStopAlignment alignEnum = [self.tabStopHelper alignmentFromString:align];
		
		if (alignEnum == XTTABSTOP_ALIGN_RIGHT) {

			NSMutableParagraphStyle *pgStyle = [self getParagraphStyleAtStartOfOngoingParagraph];
			BOOL hasOngoingParagraph = (pgStyle != nil);
			if (! hasOngoingParagraph) {
				pgStyle = [XTStringUtils mutableParagraphStyleFor:mutAttrString];
			}

			if (! [self.tabStopHelper paragraphStyleHasTabStopAtRhsOfWindow:pgStyle]) {
				
				NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
				XTTextTableBlock *textTableBlock = [XTStringUtils tableCellForAttrString:mutAttrString atIndex:0];

				XTTextTab *newTabStop = [self createTabStopAtRhsOfViewForOptTextTableBlock:textTableBlock];
				[tabStops addObject:newTabStop];
				pgStyle.tabStops = tabStops;
				
				if (! hasOngoingParagraph) {
					[XTStringUtils applyParagraphStyle:pgStyle toAttrString:mutAttrString];
				} else {
					[self applyParagraphStyleToLastParagraph:pgStyle];
				}
			}
			
		} else if (alignEnum == XTTABSTOP_ALIGN_CENTER) {
			
			NSMutableParagraphStyle *pgStyle = [self getParagraphStyleAtStartOfOngoingParagraph];
			BOOL hasOngoingParagraph = (pgStyle != nil);
			if (! hasOngoingParagraph) {
				pgStyle = [XTStringUtils mutableParagraphStyleFor:mutAttrString];
			}
			
			NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
			CGFloat fromLoc = [self findXCoordOfInsertionPointForTabs2];
			XTTextTableBlock *textTableBlock = [XTStringUtils tableCellForAttrString:mutAttrString atIndex:0];
			XTTextTab *newTabStop = [self createTabStopAtHalfwayToRhsOfViewFromLoc:fromLoc textTableBlock:textTableBlock];
			[tabStops addObject:newTabStop];
			pgStyle.tabStops = tabStops;
			
			if (! hasOngoingParagraph) {
				[XTStringUtils applyParagraphStyle:pgStyle toAttrString:mutAttrString];
			} else {
				[self applyParagraphStyleToLastParagraph:pgStyle];
			}
			
		} else {
			XT_ERROR_1(@"Unexpected tab alignment: %d", alignEnum);
		}
	
	} else {
		XT_ERROR_0(@"not handling attribute id, to, multiple or align");
	}
	
	return res;
}

- (void)prepareForRecalcAllOfTabStops
{
	[self.tabStopHelper prepareForRecalcAllOfTabStops];
}

- (void)prepareForRecalcAllOfTabStopsInRange:(NSRange)range
{
	[self.tabStopHelper prepareForRecalcAllOfTabStopsInRange:range];
}

- (void)recalcAllTabStops
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"entry")

	[self.tabStopHelper recalcAllTabStops];
	_needsRecalcAllTabStops = NO;
}

- (XTTextTableBlock *)tableCellForTextInRange:(NSRange)range
{
	XTTextTableBlock *res = [XTStringUtils tableCellForAttrString:self.textView.textStorage range:range];
	return res;
}

- (CGFloat)findXCoordOfInsertionPointForTabs2
{
	NSTextStorage *textStorage = self.textView.textStorage;
	NSRange rangeOfLastParagraph = [XTStringUtils findRangeOfOngoingParagraph:textStorage.string];
	CGFloat res = 0.0;
	
	if (rangeOfLastParagraph.length >= 1) {
		NSAttributedString *lastParagraph = [textStorage attributedSubstringFromRange:rangeOfLastParagraph];
		NSMutableAttributedString *mutLastParagraph = [[NSMutableAttributedString alloc] initWithAttributedString:lastParagraph];

		NSMutableParagraphStyle *mutParagraphStyle = [XTStringUtils mutableParagraphStyleFor:mutLastParagraph];
		mutParagraphStyle.textBlocks = self.emptyArray;

		[XTStringUtils applyParagraphStyle:mutParagraphStyle toAttrString:mutLastParagraph];
		
		NSAttributedString *measuredLastParagraph = [XTStringUtils withoutTrailingNewline:mutLastParagraph];
		NSSize size = [XTFontUtils sizeOfAttrString:measuredLastParagraph];
		res = size.width; //TODO !!! +1 ? round off?
		res += mutParagraphStyle.headIndent;
	}
	
	return res;
}

//TODO  mv
- (CGFloat)findXCoordAfterLoc:(NSUInteger)loc
{
	//XT_DEF_SELNAME;
	
	NSRange range = NSMakeRange(loc, 1);
	//NSString *tsString = self.textView.textStorage.string;
	//NSString *string = [tsString substringWithRange:range];
	
	CGFloat resTemp = [self.textView findEndXCoordOfTextInRange:range];
	CGFloat res = [self adjustTextXCoordforInsetAndPadding:resTemp];
	
	//XT_WARN_5(@"range=(%lu,%lu) \"%@\"  --> %lf (%lf)", range.location, range.length, string, res, resTemp);
	return res;
}

//TODO  mv
- (CGFloat)findXCoordBeforeLoc:(NSUInteger)loc
{
	//XT_DEF_SELNAME;
	
	NSRange range = NSMakeRange(loc, 1);
	//NSString *tsString = self.textView.textStorage.string;
	//NSString *string = [tsString substringWithRange:range];
	
	CGFloat resTemp = [self.textView findStartXCoordOfTextInRange:range];
	CGFloat res = [self adjustTextXCoordforInsetAndPadding:resTemp];
	
	//XT_WARN_5(@"range=(%lu,%lu) \"%@\"  --> %lf (%lf)", range.location, range.length, string, res, resTemp);
	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 make block level?!
- (NSArray *)handleHtmlTagBr:(XTHtmlTagBr *)tag
{
	NSMutableArray *res = [NSMutableArray arrayWithCapacity:2];
	
	if (! [self shouldPrintOutputText]) {
		return res;
	}

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

	NSString *s = [self.linebreakHandler2 handleTagBr:height];
	[self handleWhitespaceBeforeOrAfterBlockLevelTag:res];
	
	if (s != nil && s.length >= 1) {
		NSArray<NSMutableAttributedString *> *mutAttrStringArray = [self makeAttributedStringForOutput:s];
		for (NSMutableAttributedString *mutAttrString in mutAttrStringArray) {
			[res addObject:[XTFormattedOutputElement regularOutputElement:mutAttrString]];
		}
	}
	
	// We're starting a new paragraph, so:
	self.formattingSpecForHtmlTag.formattingSpec.hasRightAlignedTab = NO;

	return res;
}

- (NSArray *)handleHtmlTagPOpen:(XTHtmlTagP *)tag
{
	[self.linebreakHandler2 handleTagPOpen];

	XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
	
	self.pTagTextAlignMode = formattingSpec.textAlignMode;
	formattingSpec.textAlignMode = [tag getTextAlignModeFrom:formattingSpec.textAlignMode];

	// We're starting a new paragraph, so:
	self.formattingSpecForHtmlTag.formattingSpec.hasRightAlignedTab = NO;

	return self.emptyArray;
}

- (NSArray *)handleHtmlTagPClose:(XTHtmlTagP *)tag
{
	XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
	
	formattingSpec.textAlignMode = self.pTagTextAlignMode; //TODO !!! rm if not needed 
	self.pTagTextAlignMode = XT_TEXT_ALIGN_LEFT;
	
	// We're starting a new paragraph, so:
	self.formattingSpecForHtmlTag.formattingSpec.hasRightAlignedTab = NO;

	return _emptyArray;
}

- (NSArray *)handleHtmlTagTitleOpen:(XTHtmlTagTitle *)tag
{
	self.receivingGameTitle = YES;
	NSArray *res = [self makeArrayWithGameTitleElement:@"{{clear}}"];
		//TODO hack
	return res;
}

- (NSArray *)handleHtmlTagTitleClose
{
	self.receivingGameTitle = NO;
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagHr:(XTHtmlTagHr *)tag
{
	NSString *horRulerOneChar = @"–";
	
	NSUInteger charsInHorRuler;;
	BOOL inTableCell = (self.formattingSpecForHtmlTag.formattingSpec.activeTextTableCell != nil);
	if (inTableCell) {
		charsInHorRuler = 5; // cheapo copout for when in table cell
	} else {
		NSFont *font = [self getCurrentFontForOutput];
		CGFloat widthOfHorRulerOneChar = [XTFontUtils defaultWidthOfString:horRulerOneChar inFont:font];
		CGFloat widthOfTextView = [self.textView findTotalWidthAdjustedForInset];
		charsInHorRuler = floor(widthOfTextView / widthOfHorRulerOneChar);
		if (charsInHorRuler < 1) {
			// for the sake of general formatting logic
			charsInHorRuler = 1;
		}
	}
	
	NSString *horRuler = [XTStringUtils stringOf:charsInHorRuler string:horRulerOneChar];
	
	XTTextAlignMode oldTextAlignMode = self.formattingSpecForHtmlTag.formattingSpec.textAlignMode;
	self.formattingSpecForHtmlTag.formattingSpec.textAlignMode = XT_TEXT_ALIGN_CENTER;
	
	[self.linebreakHandler2 handleText:horRuler]; // so we're "in text"
	
	NSMutableArray *res = [NSMutableArray array];

	[res addObjectsFromArray:[self makeRegularOutputElements:horRuler]];
	
	self.formattingSpecForHtmlTag.formattingSpec.textAlignMode = oldTextAlignMode;
	
	return res;
}

- (NSArray *)handleHtmlTagBannerOpen:(XTHtmlTagBanner *)tag
{
	XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	if (! [tag isForRemovingBanners]) {
		self.tagBannerDepth += 1;
	}

	if (self.isForT3) {
		XT_WARN_0(@"<banner> tag and contents ignored for T3 game");
		return self.emptyArray;
	}

	if (self.isForTagBanner) {
		XT_WARN_0(@"nested <banner> tag and contents ignored");
		return self.emptyArray;
	}

	if (self.tagBannerDepth >= 2) {
		// This should never happen
		XT_ERROR_0(@"nested <banner> tag and contents ignored");
		return self.emptyArray;
	}

	XTFormattedOutputElement *e = [XTFormattedOutputElement bannerStartElement:tag];
	NSArray *res = [NSArray arrayWithObject:e];
	return res;
}

- (NSArray *)handleHtmlTagBannerClose
{
	XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	NSInteger oldTagBannerDepth = self.tagBannerDepth;
	self.tagBannerDepth -= 1;
	if (self.tagBannerDepth < 0) {
		self.tagBannerDepth = 0;
	}

	if (self.isForT3) {
		XT_WARN_0(@"<banner> tag and contents ignored for T3 game");
		return self.emptyArray;
	}

	if (! self.isForTagBanner) {
		XT_WARN_0("extraneous </banner> ignored")
		return self.emptyArray;
	}

	if (oldTagBannerDepth >= 2) {
		XT_WARN_0("nested </banner> ignored")
		return self.emptyArray;
	}

	XTFormattedOutputElement *e = [XTFormattedOutputElement bannerEndElement];
	NSArray *res = [NSArray arrayWithObject:e];
	return res;
}

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

- (NSArray *)handleHtmlTagLiOpen:(XTHtmlTagLi *)tag
{
	NSArray *res = self.emptyArray;
	XTHtmlTagListContainer *listContainer = [XTHtmlUtils listContainerFor:tag];
	
	if ([listContainer isKindOfClass:[XTHtmlTagOl class]]) {

		XTHtmlTagOl *tagOl = (XTHtmlTagOl *)listContainer;
		
		NSString *s = [NSString stringWithFormat:@"%lu.\t", [tagOl getItemIndex]];
		[self.linebreakHandler2 handleText:s];
		res = [self makeArrayWithRegularOutputElement:s];

		[tagOl incItemIndex];

	} else if ([listContainer isKindOfClass:[XTHtmlTagUl class]]) {
		
		NSString *bulletStr = [self liBullet:YES];
		NSString *tabAndBulletStr = [NSString stringWithFormat:@"%@\t", bulletStr];
		[self.linebreakHandler2 handleText:tabAndBulletStr];
		res = [self makeArrayWithRegularOutputElement:tabAndBulletStr];

	} else {

		NSString *s = [self liBullet:NO];
		[self.linebreakHandler2 handleText:s];
		res = [self makeArrayWithRegularOutputElement:s];
		
		// No tab stop
	}
	
	return res;
}

- (NSArray *)handleHtmlTagLiClose:(XTHtmlTagLi *)tag
{
	return self.emptyArray;
}

- (NSString *)liBullet:(BOOL)hasProperContainer
{
	if (hasProperContainer) {
		//TODO !!! adapt: finish - dep on level
		return @"\u2022";
	} else {
		// No support for nesting here
		return @"\u2022   ";
	}
}

- (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 *)handleHtmlTagTableOpen:(XTHtmlTagTable *)tag
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	self.tableDepth += 1;
	if (self.tableDepth >= 2) {
		return self.emptyArray;
	}
	self.tableDepth = 1;

	XTFormattedOutputElement *fmtElt = [XTFormattedOutputElement tableStartElement];
	NSArray *res = [NSArray arrayWithObject:fmtElt];
	return res;
}

- (NSArray *)handleHtmlTagTableClose:(XTHtmlTagTable *)tag
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	self.tableDepth -= 1;
	if (self.tableDepth >= 1) {
		return self.emptyArray;
	}
	self.tableDepth = 0;

	[self clearPendingWhitespace];
	self.shouldWriteWhitespace = NO;

	[self.linebreakHandler2 handleTableClose];

	if (tag.tableBorderSize >= 1) {
		self.pendingParagraphSpacingBefore = (CGFloat)tag.tableBorderSize;
	}

	XTFormattedOutputElement *fmtElt = [XTFormattedOutputElement tableEndElement];
	NSArray *res = [NSArray arrayWithObject:fmtElt];
	return res;
}

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

- (NSArray *)handleHtmlTagTdOpen
{
	[self clearPendingWhitespace];
	self.shouldWriteWhitespace = NO;

	[self.linebreakHandler2 handleTdOpen];
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTdClose:(XTHtmlTagTd *)tag
{
	[self clearPendingWhitespace];

	// Each table cell must be a paragraph:
	NSString *s = [self.linebreakHandler2 handleTdClose];
	NSArray *res;
	if (s == nil) {
		res = self.emptyArray;
	} else {
		res = [self makeArrayWithRegularOutputElement:s];
	}
	return res;
}

- (NSArray *)handleHtmlTagPreOpen:(XTHtmlTagPre *)tag
{
	self.preMode = YES;
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagPreClose
{
	self.preMode = NO;
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagImg:(XTHtmlTagImg *)tag
{
	NSMutableArray *res = [NSMutableArray array];
	
	XTHtmlTagWhitespace *whitespace = [XTHtmlTagWhitespace tagWithText:@" "];
	NSArray *formattedWhitespaceArray = [self handleHtmlTagWhitespace:whitespace]; //TODO !!! probably needs redoing
	[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 handleHtmlTagWhitespace:whitespace];  //TODO !!! probably needs redoing
			[res addObjectsFromArray:formattedWhitespaceArray];
		}
	}

	return res;
}

- (NSArray *)handleHtmlTagBody:(XTHtmlTagBody *)tag
{
	if (! [self shouldPrintOutputText]) {
		return self.emptyArray;
	}

	XTFormattedOutputElement *fmtElement = [XTFormattedOutputElement bodyElement:tag];
	NSArray *res = [NSArray arrayWithObject:fmtElement];
	return res;
}

- (void)executeTagBody:(XTHtmlTagBody *)tag
{
	NSString *bgColorAttr = [tag attributeAsString:@"bgcolor"];
	if (bgColorAttr != nil) {
		XTHtmlColor *backgroundHtmlColor = [XTHtmlColor forAttributeValue:bgColorAttr];
		if (backgroundHtmlColor.color != nil) {
			self.colorationHelper.bodyBackgroundColor = backgroundHtmlColor;
			if ([self allowGameToSetColors]) {
				self.textView.backgroundColor = self.colorationHelper.bodyBackgroundColor.color;
			} else {
				self.textView.backgroundColor = [self.colorationHelper getPrefsOutputBackgroundColorForTextView];
			}
		} else {
			self.colorationHelper.bodyBackgroundColor = nil;
			self.textView.backgroundColor = [self.colorationHelper getPrefsOutputBackgroundColorForTextBannerTextView];
		}
	} else {
		self.colorationHelper.bodyBackgroundColor = nil;
		self.textView.backgroundColor = [self.colorationHelper getPrefsOutputBackgroundColorForTextBannerTextView];
	}
	
	NSString *textColorAttr = [tag attributeAsString:@"text"];
	if (textColorAttr != nil) {
		XTHtmlColor *textHtmlColor = [XTHtmlColor forAttributeValue:textColorAttr];
		self.colorationHelper.bodyTextColor = textHtmlColor;
	} else {
		self.colorationHelper.bodyTextColor = nil;
	}
	
	NSString *inputColorAttr = [tag attributeAsString:@"input"];
	if (inputColorAttr != nil) {
		XTHtmlColor *inputHtmlColor = [XTHtmlColor forAttributeValue:inputColorAttr];
		self.colorationHelper.bodyInputColor = inputHtmlColor;
	} else {
		self.colorationHelper.bodyInputColor = nil;
		if (self.colorationHelper.bodyTextColor != nil) { //TODO !!! adapt: wtf?
			NSColor *resolvedTextColor = self.colorationHelper.bodyTextColor.color;
			if (resolvedTextColor != nil) {
				self.colorationHelper.bodyInputColor = self.colorationHelper.bodyTextColor;
			} else {
				int brkpt = 1;
			}
		}
	}
	
	[self.colorationHelper applyBodyTextAndInputColorsForceApply:NO];
	
	NSString *linkColorAttr = [tag attributeAsString:@"link"];
	if (linkColorAttr != nil) {
		XTHtmlColor *linkHtmlColor = [XTHtmlColor forAttributeValue:linkColorAttr];
		self.colorationHelper.bodyLinkColor = linkHtmlColor;
	} else {
		self.colorationHelper.bodyLinkColor = nil;
	}
	[self.colorationHelper applyBodyLinkColor];
	
	[self updateCursorColor];
}

- (void)updateCursorColor
{
	[self.colorationHelper updateCursorColor:self.formattingSpecForHtmlTag];
}

- (NSArray *)handleHtmlTagText:(XTHtmlTagText *)tag
{
	return [self handleRegularText:tag.text];
}

- (NSArray *)handleHtmlTagWhitespace:(XTHtmlTagWhitespace *)tag
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"shouldWriteWhitespace=%d", self.shouldWriteWhitespace);
	
	NSArray *res = nil;
	
	if (self.receivingGameTitle) {
		res = [self makeArrayWithGameTitleElement:@" "];

	} else if (! [self shouldPrintOutputText]) {
		// nothing
		
	} else {
		if (self.preMode) {
			res = [self makeArrayWithRegularOutputElement:tag.text];
		} else {
			[self addPendingWhitespace:tag];
		}
	}
	
	return res;
}

//TODO fold into special spec handling below?
- (NSArray *)handleHtmlTagQuotedSpace:(XTHtmlTagQuotedSpace *)quotedSpace
{
	XT_TRACE_ENTRY;
	
	NSArray *res = nil;
	
	if (self.receivingGameTitle) {
		res = [self makeArrayWithGameTitleElement:@" "];
	} else if (! [self shouldPrintOutputText]) {
		// nothing
	} else {
		if (self.preMode) {
			res = [self makeArrayWithRegularOutputElement:@" "];
		} else {
			XTHtmlTagWhitespace *whitespace = [XTHtmlTagWhitespace tagWithText:[quotedSpace asString]];
			[self addPendingWhitespace:whitespace];
			self.shouldWriteWhitespace = YES;
		}
	}
	
	return res;
}

- (NSArray *)handleHtmlTagSpecialSpace:(XTHtmlTagSpecialSpace *)specialSpace
{
	XT_TRACE_ENTRY;
	
	NSArray *res = nil;
	
	if (self.receivingGameTitle) {
		res = [self makeArrayWithGameTitleElement:@" "];
	} else if (! [self shouldPrintOutputText]) {
		// nothing
	} else {
		if (self.preMode) {
			NSString *str = [XTStringUtils stringForSpecialSpaceWhenMonoFont:specialSpace.ch];
			if (str.length >= 1) {
				res = [self makeArrayWithRegularOutputElement:str];
			}
		} else {
			[self addPendingSpecialSpace:specialSpace];
			self.shouldWriteWhitespace = YES;
		}
	}
	
	return res;
}

- (NSArray *)handleHtmlTagNonbreakingSpace:(XTHtmlTagNonbreakingSpace *)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 shouldPrintOutputText]) {
		// 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 *)handleHtmlTagBannerClear:(XTHtmlTagBannerClear *)tag
{
	XTFormattedOutputElement *outputElement = [XTFormattedOutputElement bannerClearElement];
	NSArray *res = [NSArray arrayWithObject:outputElement];
	return res;
}

- (BOOL)shouldPrintOutputText
{
	XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
	
	BOOL res = (! formattingSpec.inAboutBox) && (! [self suppressContentInTagBanner]);
	return res;
}

- (XTHtmlLinebreakHandler2State)getLineBreakHandlerState
{
	return self.linebreakHandler2.state;
}

- (void)setLineBreakHandlerState:(XTHtmlLinebreakHandler2State)lbh2State
{
	[self.linebreakHandler2 setState:lbh2State];
}

- (NSArray *)cancelOngoingTable
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	XTFormattingSpecificationForHtmlTag *formattingSpecForHtmlTag = self.formattingSpecForHtmlTag;

	NSArray *res;
	BOOL inTableCell = (formattingSpecForHtmlTag.formattingSpec.activeTextTableCell != nil) && (formattingSpecForHtmlTag.formattingSpec.activeTextTable != nil);
	if (inTableCell) {
		// end the ongoing cell's paragraph
		res = [self handleHtmlTagTdClose:nil];
	} else {
		res = self.emptyArray;
	}
	
	XTHtmlTagContainer *tagTable = (XTHtmlTagTable *)[XTHtmlUtils findAncestorTagContainerOfClass:[XTHtmlTagTable class]
																			 forDescendantTag:formattingSpecForHtmlTag.tagContainer
																			includeDescendant:YES];
	if (tagTable != nil) {
		[self recursiveCancelActiveTableState:tagTable];
	}
	
	return res;
}

- (void)recursiveCancelActiveTableState:(XTHtmlTagContainer *)tagContainer
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	XTFormattingSpecification *formattingSpec = tagContainer.formattingSpecForHtmlTag.formattingSpec;
	formattingSpec.activeTextTable = nil;
	formattingSpec.activeTextTableCell = nil;
	
	Class tagContainerClass = [XTHtmlTagContainer class];

	NSArray *contents = tagContainer.contents;
	for (XTHtmlTag *tag in contents) {
		if ([tag isKindOfClass:tagContainerClass]) {
			XTHtmlTagContainer *castTag = (XTHtmlTagContainer *)tag;
			[self recursiveCancelActiveTableState:castTag];
		}
	}
}

@end
