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

#import "XTOutputFormatter.h"
#import "XTTextRecolorationTemplate.h"
#import "XTTextView.h"
#import "XTHtmlLinebreakHandler2.h"
#import "XTFormattedOutputElement.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 "XTMainTextHandler.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 "XTTextRecolorationHelper.h"


@interface XTOutputFormatter ()

@property XTHtmlLinebreakHandler2 *linebreakHandler2;
@property BOOL afterBlockLevelSpacing;

@property XTPendingWhitespaceQueue *pendingWhitespaceQueue;

@property BOOL shouldWriteWhitespace;
	//TODO rename?

@property NSArray<XTRecalcTabStopCommand *> *tabStopsNeedingRecalc;

@property XTTabStopModel *tabStopModel;
@property CGFloat pendingParagraphSpacingBefore;
@property XTTextAlignMode pTagTextAlignMode;

@property NSInteger tagBannerDepth;

@property NSArray *emptyArray;

@property XTFontManager *fontManager;

@property XTPrefs *prefs;

@property XTHtmlTagSpecialSpace *htmlSpecialSpaceForOneSpace;

@property XTTextRecolorationTemplate *recolorationTemplate;
@property XTTextRecolorationHelper *recolorationHelper;

@property XTHtmlColor *bodyBackgroundColor;
@property XTHtmlColor *bodyTextColor;
@property XTHtmlColor *bodyInputColor;
@property XTHtmlColor *bodyLinkColor;

@property XTHtmlColor *lastUsedTextHtmlColor;
@property XTHtmlColor *lastUsedInputHtmlColor;
@property NSNumber *lastUsedColorsForBodyTag;

@property XTHtmlColor *gridModeForegroundColor;
@property XTHtmlColor *gridModeBackgroundColor;
@property XTHtmlColor *gridModeScreenColor;

@end


@implementation XTOutputFormatter

static XTLogger* logger;

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

@synthesize gridModeForegroundColor = _gridModeForegroundColor;
@synthesize gridModeBackgroundColor = _gridModeBackgroundColor;
@synthesize gridModeScreenColor = _gridModeScreenColor;

+ (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];
		_emptyArray = [NSArray array];
		_isForBanner = NO;
		_isForTagBanner = NO;
		_isForT3 = NO;
		_tabStopModel = [XTTabStopModel new];
		_htmlSpecialSpaceForOneSpace = [XTHtmlTagSpecialSpace tagWithChar:' '];
		
		_formattingSpecForHtmlTag = [XTFormattingSpecificationForHtmlTag new];
		
		_preMode = NO;
		_tagBannerDepth = 0;
		_pendingParagraphSpacingBefore = 0.0;
		
		_needsRecalcAllTabStops = NO;
		
		_recolorationTemplate = [XTTextRecolorationTemplate forOutputFormatter:self];
		
		_bodyBackgroundColor = nil;
		_bodyTextColor = nil;
		_bodyInputColor = nil;
		_bodyLinkColor = nil;

		_lastUsedTextHtmlColor = nil;
		_lastUsedInputHtmlColor = nil;
		_lastUsedColorsForBodyTag = nil;

		_gridModeForegroundColor = nil;
		_gridModeBackgroundColor = nil;
		_gridModeScreenColor = nil;

		[self resetFlags];
 	}
    return self;
}

- (void)setTextView:(XTTextView *)textView
{
	_textView = textView;
	_recolorationHelper = [XTTextRecolorationHelper forTextStorage:textView.textStorage isForBanner:self.isForBanner];
}

- (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;
	[_tabStopModel reset];
	_tabStopModel = nil;
	_emptyArray = nil;
	_fontManager = nil;
	_prefs = nil;
	_htmlSpecialSpaceForOneSpace = nil;
}

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

	[self resetNonHtmlModesFlags];
	[self resetHtmlModesFlags];
	[self.linebreakHandler2 resetForNextCommand];
	[self.tabStopModel reset];

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

- (NSString *)stringForSpecialSpaceWhenMonofont:(XTHtmlTagSpecialSpace *)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;
}

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

//TODO !!! mv:
- (void)removeTextBackgroundColorForNewlines:(NSMutableAttributedString *)mutAttrString
{
	NSString *string = mutAttrString.string;
	NSUInteger stringLen = string.length;
	
	for (NSUInteger idx = 0; idx < stringLen; idx++) {
		unichar ch = [string characterAtIndex:idx];
		if (ch == '\n') {
			NSRange range = NSMakeRange(idx, 1);
			[mutAttrString removeAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND range:range];
			[mutAttrString removeAttribute:NSBackgroundColorAttributeName range:range];
		}
	}
}

//------

- (NSDictionary *)getTextAttributesDictionaryForOutput
{
	XT_DEF_SELNAME;

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

	NSArray *tabStops = [self 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 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
{
	NSTextStorage *textStorage = [self.textView textStorage];
	NSString *string = [textStorage string];
	NSRange rangeOfLastPara = [XTStringUtils findRangeOfLastParagraph:string];
	
	[textStorage addAttribute:NSParagraphStyleAttributeName value:pgStyle range:rangeOfLastPara];
}

//TODO !!! rm? can we replace by getParagraphStyleAtStartOfOngoingParagraph?
- (NSMutableParagraphStyle *)getParagraphStyleAtStartOfLastParagraph
{
	XT_DEF_SELNAME;

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

	NSRange rangeOfLastPara = [XTStringUtils findRangeOfLastParagraph:string];
	if (rangeOfLastPara.length == 0) {
		XT_ERROR_0(@"rangeOfLastPara.length == 0 -- this should not happen");
	}
	
	NSUInteger indexStartOfLastPara = rangeOfLastPara.location;
	NSMutableParagraphStyle *res = [XTStringUtils mutableParagraphStyleFor:textStorage atIndex:indexStartOfLastPara];
	
	return res;
}

- (NSMutableParagraphStyle *)getParagraphStyleAtStartOfOngoingParagraph
{
	//XT_DEF_SELNAME;
	
	NSTextStorage *textStorage = [self.textView textStorage];
	NSString *string = [textStorage string];
	
	NSRange range = [XTStringUtils findRangeOfOngoingParagraph:string];

	NSMutableParagraphStyle *res = nil;
	if (range.length >= 1) {
		NSUInteger indexStart = range.location;
		res = [XTStringUtils mutableParagraphStyleFor:textStorage atIndex:indexStart];
	}
	
	return res;
}

- (NSDictionary *)getTextTemporaryAttributesDictionaryForOutput
{
	//XT_DEF_SELNAME;
	
	NSMutableDictionary *dict = nil;
	
	XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
	
	//TODO !!! refactor - this is badly written:
	if (formattingSpec.activeTagA != nil) {
		dict = [NSMutableDictionary dictionary];
		
		XTOutputTextColorResult *colorResult = [self getLinkColorResult];
		dict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = colorResult;
		
		NSColor *color = nil;
		if ([self allowGameToSetColors]) {
			color = colorResult.htmlColor.color;
		}
		if (color == nil) {
			color = self.prefs.linksTextColor.value;
		}
		dict[NSForegroundColorAttributeName] = color;
		
		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];
		}
		
		if ([formattingSpec.activeTagA hasAttribute:@"plain"]) { // ...
			// "plain" link - set temp attrs so it looks like regular text
			
			dict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK] = [NSNumber numberWithBool:YES];

			XTOutputTextColorResult *outputTextColorResult = [self getOutputTextColor];
			if ([self allowGameToSetColors]) {
				dict[NSForegroundColorAttributeName] = outputTextColorResult.htmlColor.color;
			} else {
				dict[NSForegroundColorAttributeName] = [self getPrefsOutputTextColor];
			}
			dict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = outputTextColorResult;

			if (! formattingSpec.underline) {
				dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleNone];
			}
			[dict removeObjectForKey:XT_OUTPUT_FORMATTER_ATTR_UNDERLINE_FROM_PREFS];
		}
	}
	
	return dict;
}

- (XTOutputTextColorResult *)getLinkColorResult
{
	XTColorSource colorSource;
	XTHtmlColor *htmlColor;
	
	if (self.bodyLinkColor != nil) {
		colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_BODY;
		htmlColor = [XTHtmlColor forAttributeValue:self.bodyLinkColor.attrValue];
	} else {
		colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
		htmlColor = [XTHtmlColor forAttributeValueLink];
	}
	
	XTOutputTextColorResult *res = [XTOutputTextColorResult forHtmlColor:htmlColor colorSource:colorSource];
	
	return res;
}

- (NSMutableDictionary *)getTextAttributesDictionaryCommonForOutput
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
	
	dict[NSFontAttributeName] = [self getCurrentFontForOutput];
		//TODO (wasteful) not here - overridden for input and grid banners
	
	XTOutputTextColorResult *outputTextColorResult = [self getOutputTextColor];
	if ([self allowGameToSetColors]) {
		dict[NSForegroundColorAttributeName] = outputTextColorResult.htmlColor.color;
	} else {
		dict[NSForegroundColorAttributeName] = [self getPrefsOutputTextColor];
	}
	dict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = outputTextColorResult;
	
	XTOutputTextColorResult *outputBackgroundColorResult = [self getOutputBackgroundColor];
	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;
}

//TODO !!! adapt: a mess - streamline
- (XTOutputTextColorResult *)getOutputTextColor
{
	XTHtmlColor *htmlColor = nil;
	XTColorSource colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
	BOOL bannerTextColorFromMainAreaPrefs = NO;
	
	if (self.isForGridBanner) {
		if (self.gridModeForegroundColor != nil) {
			if (self.gridModeForegroundColor.color != nil) {
				htmlColor = self.gridModeForegroundColor;
				colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_FONT;
			} else {
				NSColor *color;
				if ([self allowGameToSetColors]) {
					color = self.prefs.outputAreaTextColor.value; // yup, that's what mjr terp and qtads do
				} else {
					color = [self getPrefsOutputTextColor];
				}
				htmlColor = [XTHtmlColor forNSColor:color];
				colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
				bannerTextColorFromMainAreaPrefs = YES;
			}
		} else {
			htmlColor = [self getGridModeDefaultTextColor];
			colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_FONT;
		}
	}
	
	if (htmlColor == nil) {
		XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
		if (formattingSpec.fontColor != nil) {
			htmlColor = formattingSpec.fontColor;
			colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_FONT;
			if (htmlColor.color == nil) {
				htmlColor = [XTHtmlColor forAttributeValue:@"black"];
			}
		}
	}
	
	if (htmlColor == nil) {
		if (self.bodyTextColor != nil) {
			htmlColor = self.bodyTextColor;
			colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_BODY;
		}
		if (htmlColor.color == nil) {
			if (self.isForBanner && ! [self isForT2PlainTextBanner] && ! self.isForT3) {
				htmlColor = [XTHtmlColor forAttributeValue:@"black"];
				colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_BODY;
			} else {
				NSColor *color;
				if (self.isForBanner && self.isForT3) {
					if ([self allowGameToSetColors]) {
						color = self.prefs.outputAreaTextColor.value; // yup, that's what mjr terp and qtads do
					} else {
						color = [self getPrefsOutputTextColor];
					}
					bannerTextColorFromMainAreaPrefs = YES;
				} else {
					color = [self getPrefsOutputTextColor];
				}
				htmlColor = [XTHtmlColor forNSColor:color];
				colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
			}
		}
	}
	
	XTOutputTextColorResult *res = [XTOutputTextColorResult forHtmlColor:htmlColor
															 colorSource:colorSource
										bannerTextColorFromMainAreaPrefs:bannerTextColorFromMainAreaPrefs];
	return res;
}

- (XTHtmlColor *)getGridModeDefaultTextColor
{
	XTHtmlColor *res = [XTHtmlColor forAttributeValue:@"black"];
	return res;
}

//TODO !!! rm - call helper
- (NSColor *)getPrefsOutputTextColor
{
	NSColor *res;
	if (self.isForBanner) {
		res = self.prefs.statusLineTextColor.value;
	} else {
		res = self.prefs.outputAreaTextColor.value;
	}
	return res;
}

- (XTOutputTextColorResult *)getOutputBackgroundColor // for text
{
	XTHtmlColor *htmlColor = nil;
	XTColorSource colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
	
	if (self.gridModeBackgroundColor != nil) {
		htmlColor = self.gridModeBackgroundColor;
		colorSource = XT_COLOR_SOURCE_BACKGROUND_FROM_FONT;
	} else {
		XTFormattingSpecification *formattingSpec = self.formattingSpecForHtmlTag.formattingSpec;
		if (formattingSpec.backgroundColor.color != nil) {
			htmlColor = formattingSpec.backgroundColor;
			colorSource = XT_COLOR_SOURCE_BACKGROUND_FROM_FONT;
		}
	}
	
	XTOutputTextColorResult *res = [XTOutputTextColorResult forHtmlColor:htmlColor colorSource:colorSource];
	return res;
}

//TODO !!! adapt: simplify logic!!!
- (NSColor *)getOutputBackgroundColorForTextView
{
	NSColor *res;
	BOOL allowGameToSetColors = [self allowGameToSetColors];
	
	if (self.isForGridBanner) {
		if (self.gridModeScreenColor != nil) {
			if (allowGameToSetColors) {
				res = self.gridModeScreenColor.color;
			} else {
				res = [self getPrefsOutputBackgroundColorForTextView];
			}
		} else {
			res = [self getOutputBackgroundColorForTextBannerTextView];
		}
		return res;
	}
	
	if (allowGameToSetColors && self.bodyBackgroundColor.color != nil) {
		res = self.bodyBackgroundColor.color;
	} else if (allowGameToSetColors && self.isForBanner /*isForTagBanner*/ && ! self.isForT3) {
		res = [self getOutputBackgroundColorForTextBannerTextView];
	} else {
		if (self.isForBanner && self.isForT3 && allowGameToSetColors) {
			res = self.prefs.outputAreaBackgroundColor.value; // yup, that's what mjr terp and qtads do
		} else {
			res = [self getPrefsOutputBackgroundColorForTextView];
		}
	}
	
	return res;
}

//TODO !!! rename getPrefs...
- (NSColor *)getOutputBackgroundColorForTextBannerTextView
{
	NSColor *res;
	if ([self allowGameToSetColors]) {
		res = self.prefs.outputAreaBackgroundColor.value; // yup, main area's screen color - this is what mjr's terp does
	} else {
		res = [self getPrefsOutputBackgroundColorForTextView];
	}
	return res;
}

- (NSColor *)getPrefsOutputBackgroundColorForTextView
{
	NSColor *res;
	if (self.isForBanner) {
		res = self.prefs.statusLineBackgroundColor.value;
	} else {
		res = self.prefs.outputAreaBackgroundColor.value;
	}
	return res;
}

- (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.bodyInputColor.color != nil) {
		htmlColor = self.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];
	//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 //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;
}

//TODO !!! refactor
- (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;
}

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

//TODO mv
- (XTTextTab *)createTabStopAtRhsOfViewForOptTextTableBlock:(XTTextTableBlock *)textTableBlock
{
	[self.textView ensureLayoutForTextContainer];
	
	NSTextAlignment alignment = NSTextAlignmentRight;
	CGFloat locRhsOfView;
	
	if (textTableBlock != nil) {
		XTRect *contentRect = [textTableBlock getlatestContentRect];
		if (contentRect == nil) {
			//XT_WARN_1(@"contentRect == nil for textTableBlock %@", textTableBlock);
			locRhsOfView = 20;
			// temporary, placeholder value - otherwise it would be 0, which causes a problem with attr.string width measurements
			// gets recalc'd later
			//XT_WARN_1(@"created tab at pos %lf (contentRect == nil)", tabLoc);
		} else {
			CGFloat contentRectWidth = contentRect.rect.size.width;
			locRhsOfView = contentRectWidth;
		}

	} else {
		locRhsOfView = [self.textView findCoordOfRhsOfView];
	}

	NSDictionary<NSString *,id> *customOptions = [NSDictionary dictionaryWithObject:XT_TAB_TYPE_RHS_OF_VIEW forKey:XT_TAB_TYPE_KEY];
	XTTextTab *tab = [XTTextTab withTextAlignment:alignment location:locRhsOfView options:nil customOptions:customOptions];
	
	//XT_DEF_SELNAME;
	//XT_WARN_1(@"loc=%lf", locRhsOfView);

	_needsRecalcAllTabStops = YES;
	
	return tab;
}

- (XTTextTab *)createTabStopAtRhsOfView
{
	XTTextTab *res = [self createTabStopAtRhsOfViewForOptTextTableBlock:nil];
	return res;
}

//TODO mv
- (XTTextTab *)createTabStopAtHalfwayToRhsOfViewFromLoc:(CGFloat)fromLoc textTableBlock:(XTTextTableBlock *)textTableBlock
{
	//XT_DEF_SELNAME;
	
	[self.textView ensureLayoutForTextContainer];
	
	NSTextAlignment alignment = NSTextAlignmentCenter;
	CGFloat tabLoc;
	
	if (textTableBlock != nil) {
		XTRect *contentRect = [textTableBlock getlatestContentRect];
		if (contentRect == nil) {
			//XT_WARN_1(@"contentRect == nil for textTableBlock %@", textTableBlock);
			tabLoc = 20;
				// temporary, placeholder value - otherwise it would be 0, which causes a problem with attr.string width measurements.
				// gets recalc'd later
			//XT_WARN_1(@"created tab at pos %lf (contentRect == nil)", tabLoc);
		} else {
			CGFloat contentRectWidth = contentRect.rect.size.width;
			if ((contentRectWidth - fromLoc) < 0.0) {
				XT_DEF_SELNAME;
				XT_WARN_0(@"(contentRectWidth - fromLoc) < 0.0");
			}
			tabLoc = fromLoc + ((contentRectWidth - fromLoc) / 2.0);
			tabLoc = floor(tabLoc);
			//XT_WARN_2(@"created tab at pos %lf (contentRect width %lf)", tabLoc, contentRect.rect.size.width);
			
			if (tabLoc >= contentRectWidth) {
				tabLoc = contentRectWidth - 1;
			}
		}
	} else {
		tabLoc = [self.textView findCoordOfTabAtHalfwayToRhsOfViewFromLoc:fromLoc];
	}
	
	NSDictionary<NSString *,id> *customOptions = [NSDictionary dictionaryWithObject:XT_TAB_TYPE_HALFWAY_TO_RHS_OF_VIEW forKey:XT_TAB_TYPE_KEY];
	XTTextTab *tab = [XTTextTab withTextAlignment:alignment location:tabLoc options:nil customOptions:customOptions];

	_needsRecalcAllTabStops = YES;
	
	//XT_WARN_1(@"--> tabLoc=%lf", tabLoc);

	return tab;
}

//TODO mv
- (XTTextTab *)createTabFromModelEntry:(XTTabStopModelEntry *)modelEntry
						   minLocation:(CGFloat)minLocation
							  resizing:(BOOL)resizing
					 optTextTableBlock:(XTTextTableBlock *)textTableBlock
{
	//XT_DEF_SELNAME;
	//XT_TRACE_3(@"modelEntry.ident=%@ modelEntry.position=%f minLocation=%f", modelEntry.ident, modelEntry.position.doubleValue, minLocation);
	
	[self.textView ensureLayoutForTextContainer];
	
	NSTextAlignment alignment = modelEntry.nsTextAlignment;
	NSDictionary<NSString *,id> *options = modelEntry.nsTextAlignmentOptions; // NB! r/o property (not a member field)
	NSDictionary<NSString *,id> *customOptions = [modelEntry getCustomOptions];
	//...clear options entry "xtads.tabModelEntry" ???

	CGFloat pos = modelEntry.position.doubleValue;

	pos -= modelEntry.positionAdjustmentForDecimalPoint;
	if (pos < 0.0) {
		pos = 0.0;
	}
	
	if (! resizing) {
		//TODO !!! exp rm:
		//pos += 0.01;
	} else {
		int brkpt = 1;
	}
	
	if (pos < minLocation) {
		// make sure new tab isn't at or before minLocation (typically == insertion pos.)
		pos = minLocation + 0.01;
	}
	
	//TODO !!! adapt: refactor vs other similar cases
	CGFloat viewWidth;
	if (textTableBlock != nil) {
		XTRect *contentRect = [textTableBlock getlatestContentRect];
		if (contentRect == nil) {
			//XT_WARN_1(@"contentRect == nil for textTableBlock %@", textTableBlock);
			viewWidth = 20;
			// temporary, placeholder value - otherwise it would be 0, which causes a problem with attr.string width measurements
			// gets recalc'd later
			//XT_WARN_1(@"created tab at pos %lf (contentRect == nil)", tabLoc);
		} else {
			CGFloat contentRectWidth = contentRect.rect.size.width;
			viewWidth = contentRectWidth;
		}
	} else {
		viewWidth = [self.textView findCoordOfRhsOfView];
	}
	
	if (pos >= viewWidth) {
		if (resizing) {
			int brkpt = 1;
		} else {
			pos = minLocation + 0.01;
		}
	}
	
	//TODO !!! rm if not needed
	if (pos == 0.0) {
		pos = 0.01;
	}
	
	if (pos >= viewWidth) {
		int brkpt = 1;
	}

	XTTextTab *tab = [XTTextTab withTextAlignment:alignment location:pos options:options customOptions:customOptions];
	
	//XT_WARN_4(@"modelEntry.ident=%@ modelEntry.position=%f minLocation=%f -> %@", modelEntry.ident, modelEntry.position.doubleValue, minLocation, tab);

	return tab;
}

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

- (XTTextTab *)createTabStopFor:(NSAttributedString *)attrString atNextMultipleOf:(NSNumber *)multiple fromLocation:(CGFloat)location
{
	//XT_DEF_SELNAME;
	
	[self.textView ensureLayoutForTextContainer];
	
	CGFloat enSpaceWidth = [self getHtmlModeEnSpaceTabStopColumnWidthInPointsFor:attrString];
	
	CGFloat locNewTab = [self.tabStopModel findPositionOfNextTabWithMultiple:multiple
																fromPosition:location
																enSpaceWidth:enSpaceWidth];
	
	NSMutableDictionary<NSString *,id> *customOptions = [NSMutableDictionary dictionaryWithCapacity:2];
	[customOptions setObject:XT_TAB_TYPE_MULTIPLE forKey:XT_TAB_TYPE_KEY];
	[customOptions setObject:multiple forKey:XT_TAB_MULTIPLE_KEY];
	
	XTTextTab *textTab = [XTTextTab withTextAlignment:NSTextAlignmentLeft location:locNewTab options:nil customOptions:customOptions];
	//XT_WARN_3(@"fromLocation=%lf multiple=%lu --> loc=%lf", location, multiple.unsignedIntegerValue, locNewTab);
	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)getHtmlModeEnSpaceTabStopColumnWidthInPointsFor:(NSAttributedString *)attrString
{
	NSDictionary *dict = [attrString attributesAtIndex:0 effectiveRange:nil];
	NSFont *font = dict[NSFontAttributeName];
	
	NSString *enSpaceString;
	CGFloat scale;
	if (font.fixedPitch) {
		enSpaceString = EFFECTIVE_EN_SPACE_FOR_TABS_MONO_FONT;
		scale = 0.85;
	} else {
		enSpaceString = EFFECTIVE_EN_SPACE_FOR_TABS_PROP_FONT;
		scale = 1.0;
	}
	
	NSAttributedString *attrStringEnSpace = [[NSAttributedString alloc] initWithString:enSpaceString attributes:dict];
	
	NSSize size = [XTFontUtils requiredRectForText:attrStringEnSpace forViewOfSize:NSMakeSize(10000, 10000) suppressCenterAndRightTabs:YES];
	
	CGFloat res = size.width;
	res = res * scale;
	return res;

	/* pb-10 
	NSFont *outputFont = [self getCurrentFontForOutput];
	CGFloat fontSize = outputFont.pointSize;
	CGFloat res = ceil(fontSize / 2.0);

	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.tabStopModel findTabWithId: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.tabStopModel 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.tabStopModel addTabStopWithId: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.tabStopModel findTabWithId: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 = [self tableCellForAttrString:mutAttrString atIndex:0];
			NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
			if ([tagTab hasAttribute:@"align"] || [tagTab 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
			}
			XTTextTab *newTabStop = [self 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 paragraphStyleHasTabStopAtRhsOfWindow:pgStyle]) {
			
			CGFloat position = [self findXCoordOfInsertionPointForTabs2];
			XTTextTab *newTabStop = [self 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 textStorageEndsWithARegularSpace];
				//TODO !!! can mv into stringForTabIndent:
			replacement = [self 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.tabStopModel alignmentFromString:align];
		
		if (alignEnum == XTTABSTOP_ALIGN_RIGHT) {

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

			if (! [self paragraphStyleHasTabStopAtRhsOfWindow:pgStyle]) {
				
				NSMutableArray *tabStops = [NSMutableArray arrayWithArray:pgStyle.tabStops];
				XTTextTableBlock *textTableBlock = [self 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 = [self 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;
}

- (BOOL)textStorageEndsWithARegularSpace
{
	//TODO !!! check if last space was from tab#indent
	BOOL res = NO;
	NSTextStorage *textStorage = self.textView.textStorage;
	NSString *textStorageString = textStorage.string;
	if (textStorageString.length >= 1) {
		unichar lastCh = [XTStringUtils lastChar:textStorageString];
		if (lastCh == ' ') {
			NSUInteger lastChIdx = textStorageString.length - 1;
			NSRange range = NSMakeRange(lastChIdx, 1);
			NSAttributedString *lastChAttrString = [textStorage attributedSubstringFromRange:range];
			if (! [XTMutableAttributedStringHelper isFromExpandedTabTagWithIndent:lastChAttrString]) {
				res = YES;
			}
		}
	}
	return res;
}

- (NSString *)stringForTabIndent:(NSUInteger)indent foldLeadingSpace:(BOOL)foldLeadingSpace
{
	NSMutableString *mutString = [NSMutableString stringWithCapacity:(indent * 2)];
	for (NSInteger idx = 0; idx < indent; idx++) {
		[mutString appendString:EFFECTIVE_EN_SPACE_FOR_TABS_PROP_FONT];
		//[mutString appendString:UNICHAR_EN_SPACE]; // too narrow. 2 regular spaces looks like on qtads.
	}
	NSString *res = mutString;
	if (foldLeadingSpace) {
		if (mutString.length >= 1) {
			res = [XTStringUtils withoutLastChar:mutString];
		}
	}
	return res;
}
							
 - (BOOL)paragraphStyleHasTabStopAtRhsOfWindow:(NSParagraphStyle *)pgStyle
 {
	BOOL res = NO;
	NSArray *tabStops = pgStyle.tabStops;
	if (tabStops.count >= 1) {
		NSTextTab *lastTabStop = tabStops[tabStops.count - 1];
		if ([lastTabStop isKindOfClass:[XTTextTab class]]) {
			XTTextTab *castLastTabStop = (XTTextTab *)lastTabStop;
			res = [XTTabStopUtils tabStopIsAtRhsOfView:castLastTabStop];
		}
	}
	return res;
 }

- (void)prepareForRecalcAllOfTabStops
{
	//XT_WARN_ENTRY;
	
	NSTextStorage *textStorage = [self.textView textStorage];
	NSRange range = NSMakeRange(0, textStorage.length);
	[self prepareForRecalcAllOfTabStopsInRange:range];
}

- (void)prepareForRecalcAllOfTabStopsInRange:(NSRange)range
{
	//XT_WARN_ENTRY;
	
	if (self.tabStopsNeedingRecalc.count >= 1) {
		return;
	}
	
	NSTextStorage *textStorage = [self.textView textStorage];
	self.tabStopsNeedingRecalc = [XTTabStopUtils limitTabStopsSensitiveToResizing:textStorage range:range];
}

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

	//XTTimer *timer = [XTTimer fromNow];
	
	NSTextStorage *textStorage = [self.textView textStorage];
	NSString *textStorageString = textStorage.string;
	
	[self.textView ensureLayoutForTextContainer];
	
	for (XTRecalcTabStopCommand *recalcTabStopCommand in self.tabStopsNeedingRecalc) {
		NSUInteger tabIdxInTextStorage = recalcTabStopCommand.idxInTextStorage;
		NSUInteger tabStopInParaIndex = recalcTabStopCommand.tabIndexInParagraph;
		
		NSRange paraRange = [XTStringUtils rangeOfParagraphIn:textStorageString atLoc:tabIdxInTextStorage];
		//NSAttributedString *attrStringForParaRange = [textStorage attributedSubstringFromRange:paraRange];
		NSMutableParagraphStyle *oldParaStyle = [textStorage attribute:NSParagraphStyleAttributeName atIndex:tabIdxInTextStorage effectiveRange:nil];
		
		if (oldParaStyle == nil) {
			//XT_WARN_0(@"oldParaStyle == nil");
			// This can happen sometimes. Not a problem.
			int brkpt = 1;
		} else {
			NSArray *oldTabStops = oldParaStyle.tabStops;
			NSMutableParagraphStyle *newParaStyle = [oldParaStyle mutableCopy];
			NSMutableArray *newTabStops = [oldTabStops mutableCopy];
			NSTextTab *oldTabStop = oldTabStops[tabStopInParaIndex];

			if ([oldTabStop isKindOfClass:[XTTextTab class]]) {
				XTTextTab *castOldTabStop = (XTTextTab *)oldTabStop;
				NSInteger posBeforeTab = ((NSInteger)tabIdxInTextStorage) - 1;
				XTTextTab *newTabStop = [self recalcTabStop:castOldTabStop fromInsertionLoc:posBeforeTab];
				
				newTabStops[tabStopInParaIndex] = newTabStop;
				newParaStyle.tabStops = newTabStops;
				[textStorage addAttribute:NSParagraphStyleAttributeName value:newParaStyle range:paraRange];
			}
			
			//XTTimer *timerLayout = [XTTimer fromNow];
			//TODO !!! adapt? [self.textView ensureLayoutForTextContainer];
		}
		
		if (recalcTabStopCommand.reinsertTabForPlaceholder) {
			[XTTabStopUtils restoreTabStopSensitiveToResizing:textStorage atPos:tabIdxInTextStorage];
		}
	}

	self.tabStopsNeedingRecalc = nil;

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

	//XT_WARN_0(@"exit")
	
	_needsRecalcAllTabStops = NO;
}

- (XTTextTab *)recalcTabStop:(XTTextTab *)oldTabStop fromInsertionLoc:(NSUInteger)insLoc
{
	XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	XTTextTab *newTabStop = nil;
	
	if ([XTTabStopUtils tabStopIsAtRhsOfView:oldTabStop]) {
		NSRange range = NSMakeRange(insLoc + 1, 1); // +1 to get to tab itself -- insLoc is idx _before_ tab char
		XTTextTableBlock *textTableBlock = [self tableCellForTextInRange:range];
		newTabStop = [self createTabStopAtRhsOfViewForOptTextTableBlock:textTableBlock];

	} else if ([XTTabStopUtils tabStopIsAtHalfwayToRhsOfView:oldTabStop]) {
		NSUInteger tabStopLoc = insLoc + 1;  // +1 to get to tab itself -- insLoc is idx _before_ tab char
		NSRange rangeOfTabStop = NSMakeRange(tabStopLoc, 1);
		XTTextTableBlock *textTableBlock = [self tableCellForTextInRange:rangeOfTabStop];

		NSTextStorage *textStorage = [self.textView textStorage];
		NSString *string = [textStorage string];
		NSRange rangeOfParagraph = [XTStringUtils rangeOfParagraphIn:string atLoc:tabStopLoc];
		NSUInteger numCharsUpToTabInParagraph = tabStopLoc - rangeOfParagraph.location;
		NSRange rangeOfParagraphUpToTab = NSMakeRange(rangeOfParagraph.location, numCharsUpToTabInParagraph);
		NSAttributedString *attrAtringParagraphUpToTab = [textStorage attributedSubstringFromRange:rangeOfParagraphUpToTab];
			//TODO !!! rm? should not be necessary now
		attrAtringParagraphUpToTab = [XTStringUtils withoutTrailingNewline:attrAtringParagraphUpToTab]; // trailing newline makes width of para == width of viewSize
		NSSize viewSize = NSMakeSize(10000.0, 10000.0); //TODO !!! use tTB's size?
		NSSize requiredSizeForParagraph = [XTFontUtils requiredRectForText:attrAtringParagraphUpToTab
															 forViewOfSize:viewSize
												suppressCenterAndRightTabs:NO];
		CGFloat xCoord = requiredSizeForParagraph.width;
		
		newTabStop = [self createTabStopAtHalfwayToRhsOfViewFromLoc:xCoord textTableBlock:textTableBlock];

	} else if ([XTTabStopUtils tabStopIsToId:oldTabStop]) {
		NSRange range = NSMakeRange(insLoc + 1, 1); // +1 to get to tab itself -- insLoc is idx _before_ tab char
		XTTextTableBlock *textTableBlock = [self tableCellForTextInRange:range];
		XTTabStopModelEntry *modelTabStop = (XTTabStopModelEntry *)[oldTabStop getCustomOptionForKey:XT_TAB_MODEL_ENTRY_KEY];
		newTabStop = [self createTabFromModelEntry:modelTabStop
									   minLocation:oldTabStop.location
										  resizing:YES
								 optTextTableBlock:textTableBlock];
		
	} else if ([XTTabStopUtils tabStopIsAtMultiple:oldTabStop]) {
		newTabStop = oldTabStop;
		//XT_WARN_1(@"newTabStop.location=%lf", newTabStop.location);

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

- (XTTextTableBlock *)tableCellForTextInRange:(NSRange)range
{
	NSParagraphStyle *paragraphStyle = [self paragraphStyleOfRange:range];
	XTTextTableBlock *res = [self tableCellForParagraphStyle:paragraphStyle];
	return res;
}

- (XTTextTableBlock *)tableCellForAttrString:(NSAttributedString *)attrString atIndex:(NSUInteger)idx
{
	NSParagraphStyle *paragraphStyle = [attrString attribute:NSParagraphStyleAttributeName atIndex:idx effectiveRange:nil];
	XTTextTableBlock *res = [self tableCellForParagraphStyle:paragraphStyle];
	return res;
}

- (XTTextTableBlock *)tableCellForParagraphStyle:(NSParagraphStyle *)paragraphStyle
{
	XTTextTableBlock *res = nil;

	//TODO !!! when nested tables...
	NSArray<NSTextBlock *> *textBlockArray = paragraphStyle.textBlocks;
	if ((textBlockArray != nil) && (textBlockArray.count >= 1)) {
		NSTextBlock *tableCell = textBlockArray[0];
		if ([tableCell isKindOfClass:[XTTextTableBlock class]]) {
			res = (XTTextTableBlock *)tableCell;
		}
	}

	return res;
}

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

- (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 = @"–";
	
	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];
	
	//TODO !!! reconsider...
	self.formattingSpecForHtmlTag.formattingSpec.textAlignMode = XT_TEXT_ALIGN_LEFT;
	
	[self.linebreakHandler2 handleText:horRuler]; // so we're "in text"
	
	NSMutableArray *res = [NSMutableArray array];

	[res addObjectsFromArray:[self makeRegularOutputElements:horRuler]];
	
	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 *)handleHtmlTagTableClose:(XTHtmlTagTable *)tag
{
	[self clearPendingWhitespace];
	self.shouldWriteWhitespace = NO;

	[self.linebreakHandler2 handleTableClose];

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

	return self.emptyArray;
}

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

- (void)updateCursorColor
{
	NSColor *textColor;
	if (! [self allowGameToSetColors]) {
		textColor = self.prefs.outputAreaTextColor.value;
	} else {
		XTOutputTextColorResult *textColorResult = [self getOutputTextColor];
		textColor = textColorResult.htmlColor.color;
	}
	if (textColor != nil) {
		[self.textView setInsertionPointColor:textColor];
	}
}

//TODO !!! mv:
- (void)applyBodyTextAndInputColorsForceApply:(BOOL)forceApply
{
	//XT_DEF_SELNAME;

	XTHtmlColor *textHtmlColor = self.bodyTextColor;
	XTHtmlColor *inputHtmlColor = self.bodyInputColor;
	[self applyTextHtmlColor:textHtmlColor inputHtmlColor:inputHtmlColor forBodyTag:YES forceApply:forceApply];
}

- (void)applyBodyLinkColor
{
	[self.recolorationTemplate recolorForAttribute4:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT callback:^(NSRange range, id attrValue) {
		
		NSDictionary *tempAttrDict = attrValue;
		NSMutableDictionary *newTempAttrDict = [NSMutableDictionary dictionaryWithDictionary:tempAttrDict];
		
		NSNumber *isPlainLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK];
		BOOL isPlainLink = isPlainLinkObj.boolValue;
		
		//TODO !!! adapt: recalc dict[NSUnderlineStyleAttributeName]
			// sep. method?
			// when? non-plain link without underline from fmt spec? XT_OUTPUT_FORMATTER_ATTR_UNDERLINE_FROM_PREFS
				
		NSColor *color = nil;
		XTOutputTextColorResult *colorResult = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT]; //TODO !!! adapt: what is the point of this?!
		if (! isPlainLink) {
			colorResult = [self getLinkColorResult];
			if ([self allowGameToSetColors]) {
				color = colorResult.htmlColor.color;
			}
			if (color == nil) {
				color = self.prefs.linksTextColor.value;
			}
		} else { // ...
			//TODO !!! refactor vs. formatter l. 974 getOutputTextColor {
			XTHtmlColor *htmlColor = nil;
			XTColorSource colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;

			if (self.bodyTextColor != nil) {
				htmlColor = self.bodyTextColor;
				colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_BODY;
			}
			if (htmlColor.color == nil) {
				if (self.isForBanner && ! [self isForT2PlainTextBanner]) {
					htmlColor = [XTHtmlColor forAttributeValue:@"black"];
					colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_BODY;
				} else {
					NSColor *color = [self getPrefsOutputTextColor];
					htmlColor = [XTHtmlColor forNSColor:color];
					colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
				}
			}
			colorResult = [XTOutputTextColorResult forHtmlColor:htmlColor colorSource:colorSource];
			// } refactor...
			
			if ([self allowGameToSetColors]) {
				color = colorResult.htmlColor.color;
			} else {
				color = [self getPrefsOutputTextColor]; //TODO !!! adapt: why not ...link color? -- *plain* link??
			}
		}
		newTempAttrDict[NSForegroundColorAttributeName] = color;
		newTempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = colorResult;
		
		[self.textStorage addAttribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT value:newTempAttrDict range:range];
		[self.textView.layoutManager addTemporaryAttributes:newTempAttrDict forCharacterRange:range];
	}];
}

- (void)updateLinkColors
{
	[self.recolorationTemplate recolorForAttribute4:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT callback:^(NSRange range, id attrValue) {
		
		NSDictionary *tempAttrDict = attrValue;
		NSMutableDictionary *newTempAttrDict = [NSMutableDictionary dictionaryWithDictionary:tempAttrDict];
		
		//TODO !!! adapt: recalc dict[NSUnderlineStyleAttributeName]
			// sep. method?
			// when? non-plain link without underline from fmt spec? XT_OUTPUT_FORMATTER_ATTR_UNDERLINE_FROM_PREFS
				
		NSColor *color = nil;
		XTOutputTextColorResult *colorResult = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT];
		
		//TODO !!! refactor vs getTextTemporaryAttributesDictionaryForOutput ?
		if ([self allowGameToSetColors]) {
			color = colorResult.htmlColor.color;
		}
		if (color == nil) {
			NSNumber *isPlainLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK];
			BOOL isPlainLink = isPlainLinkObj.boolValue;

			if (! isPlainLink) {
				color = self.prefs.linksTextColor.value;
			} else {
				color = [self.textStorage attribute:NSForegroundColorAttributeName atIndex:range.location effectiveRange:nil];
			}
		}
		
		newTempAttrDict[NSForegroundColorAttributeName] = color;
		
		[self.textStorage addAttribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT value:newTempAttrDict range:range];
		[self.textView.layoutManager addTemporaryAttributes:newTempAttrDict forCharacterRange:range];
	}];
}

//TODO !!! adapt: call when changing underline links option
- (void)updateLinksUnderline
{
	NSNumber *underlineStyleSingle = [NSNumber numberWithInt:NSUnderlineStyleSingle];
	NSNumber *underlineStyleNone = [NSNumber numberWithInt:NSUnderlineStyleNone];
	BOOL underlineLinks = self.prefs.linksUnderline.value.boolValue;
	
	[self.recolorationTemplate recolorForAttribute4:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT callback:^(NSRange range, id attrValue) {
		
		//TODO !!! refactor: make method in recolorationhelper:
		NSDictionary *tempAttrDict = attrValue;
		
		NSNumber *underlineIsFromPrefsObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_UNDERLINE_FROM_PREFS];
		BOOL underlineIsFromPrefs = underlineIsFromPrefsObj.boolValue;

		if (underlineIsFromPrefs) {
			NSMutableDictionary *newTempAttrDict = [NSMutableDictionary dictionaryWithDictionary:tempAttrDict];

			if (underlineLinks) {
				newTempAttrDict[NSUnderlineStyleAttributeName] = underlineStyleSingle;
			} else {
				newTempAttrDict[NSUnderlineStyleAttributeName] = underlineStyleNone;
			}
							
			[self.textStorage addAttribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT value:newTempAttrDict range:range];
			[self.textView.layoutManager addTemporaryAttributes:newTempAttrDict forCharacterRange:range];
		}
	}];
}

- (void)updateTableColors
{
	[self.recolorationTemplate recolorForAttribute4:NSParagraphStyleAttributeName callback:^(NSRange range, id attrValue) {
		
		//TODO !!! refactor: make method in recolorationhelper:
		NSMutableParagraphStyle *mutPgStyle = attrValue;
		NSArray<XTTextTableBlock *> *tableBlockArray = mutPgStyle.textBlocks;
		if (tableBlockArray != nil) {
			for (XTTextTableBlock *tableBlock in tableBlockArray) {
				XTTextTable *table = (XTTextTable *)tableBlock.table;
				[table updateBackgroundColor];
				[tableBlock updateBackgroundColor];
			}
		}
	}];
}

//TODO !!! mv:
- (void)applyPrefsTextAndInputColors
{
	NSColor *textColor = [self getPrefsOutputTextColor];
	XTHtmlColor *textHtmlColor = [XTHtmlColor forNSColor:textColor];
	
	NSColor *inputColor = self.prefs.inputTextColor.value;
	XTHtmlColor *inputHtmlColor = [XTHtmlColor forNSColor:inputColor];
	
	[self applyTextHtmlColor:textHtmlColor inputHtmlColor:inputHtmlColor forBodyTag:NO forceApply:NO];
}

- (void)applyTextAndInputColorsForAllowGameToSetColors
{
	if ([self allowGameToSetColors]) {
		[self.recolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.recolorationHelper restoreTextColorsSetByGameForRange:range colorResult:colorResult colorSource:colorSource];
		}];
		[self.recolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.recolorationHelper restoreTextBackgroundColorsSetByGameForRange:range colorResult:colorResult colorSource:colorSource];
		}];
	} else {
		[self.recolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.recolorationHelper removeTextColorsSetByGameForRange:range colorResult:colorResult colorSource:colorSource];
		}];
		[self.recolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.recolorationHelper removeTextBackgroundColorsSetByGameForRange:range colorResult:colorResult colorSource:colorSource];
		}];
	}
}

//TODO !!! mv:
- (void)applyTextHtmlColor:(XTHtmlColor *)textHtmlColor
			inputHtmlColor:(XTHtmlColor *)inputHtmlColor
				forBodyTag:(BOOL)forBodyTag
				forceApply:(BOOL)forceApply
{
	if (! forceApply) {
		if (! [self shouldApplyTextHtmlColor:textHtmlColor inputHtmlColor:inputHtmlColor forBodyTag:forBodyTag]) {
			return;
		}
	}
	
	BOOL allowGameToSetColors = [self allowGameToSetColors];
	
	[self.recolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
		[self.recolorationHelper applyTextHtmlColor:range
										colorResult:colorResult
										colorSource:colorSource
									  textHtmlColor:textHtmlColor
									 inputHtmlColor:inputHtmlColor
										 forBodyTag:forBodyTag
										 forceApply:forceApply
							   allowGameToSetColors:allowGameToSetColors];
	}];
	
	self.lastUsedTextHtmlColor = textHtmlColor;
	self.lastUsedInputHtmlColor = inputHtmlColor;
	self.lastUsedColorsForBodyTag = [NSNumber numberWithBool:forBodyTag];

	if ((! forBodyTag) || forceApply) {
		// prefs changed
		[self.recolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.recolorationHelper applyTextHtmlBackgroundColor:range
													  colorResult:colorResult
													  colorSource:colorSource
													   forceApply:forceApply
											 allowGameToSetColors:allowGameToSetColors];
		}];
	}
}

//TODO !!! mv:
- (BOOL)shouldApplyTextHtmlColor:(XTHtmlColor *)textHtmlColor inputHtmlColor:(XTHtmlColor *)inputHtmlColor forBodyTag:(BOOL)forBodyTag
{
	if (! forBodyTag) {
		// prefs changed
		return YES;
	}
	
	BOOL newTextHtmlColor = ! [XTHtmlColor safeCompare:textHtmlColor to:self.lastUsedTextHtmlColor];
	if (newTextHtmlColor) {
		return YES;
	}
	
	if (! self.isForBanner) {
		BOOL newInputHtmlColor = ! [XTHtmlColor safeCompare:inputHtmlColor to:self.lastUsedInputHtmlColor];
		if (newInputHtmlColor) {
			return YES;
		}
	}
	
	BOOL newForBodyTag = (self.lastUsedColorsForBodyTag == nil || self.lastUsedColorsForBodyTag.boolValue != forBodyTag);
	if (newForBodyTag) {
		return YES;
	}

	return NO;
}

- (void)setGridModeForegroundColor:(XTHtmlColor *)color
{
	_gridModeForegroundColor = color;
}

- (XTHtmlColor *)gridModeForegroundColor
{
	return _gridModeForegroundColor;
}

- (void)setGridModeBackgroundColor:(XTHtmlColor *)color
{
	_gridModeBackgroundColor = color;
}

- (XTHtmlColor *)gridModeBackgroundColor
{
	return _gridModeBackgroundColor;
}

- (void)setGridModeScreenColor:(XTHtmlColor *)color
{
	_gridModeScreenColor = color;
	[self refreshGridModeScreenColor];
}

- (XTHtmlColor *)gridModeScreenColor
{
	return _gridModeScreenColor;
}

//TODO !!! adaot: inline in only caller
- (void)refreshGridModeScreenColor
{
	self.textView.backgroundColor = [self getOutputBackgroundColorForTextView];
}

- (void)resetBodyColors
{
	self.bodyBackgroundColor = nil;
	self.textView.backgroundColor = [self getOutputBackgroundColorForTextView];
	self.bodyTextColor = nil;
	self.bodyInputColor = nil;
	self.bodyLinkColor = nil;
}

- (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 = [self stringForSpecialSpaceWhenMonofont:specialSpace];
			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];
}

@end
