//
// XTOutputFormatter.m
// XTads
//
// Created by Rune Berg on 09/07/14.
// Copyright (c) 2014 Rune Berg. All rights reserved.
//
#import "XTOutputFormatter.h"
#import "XTHtmlLinebreakHandler2.h"
#import "XTFormattedOutputElement.h"
#import "XTHtmlTagA.h"
#import "XTHtmlTagHr.h"
#import "XTHtmlTagBlockQuote.h"
#import "XTHtmlTagB.h"
#import "XTHtmlTagI.h"
#import "XTHtmlTagU.h"
#import "XTHtmlTagQ.h"
#import "XTHtmlTagAboutBox.h"
#import "XTHtmlTagBanner.h"
#import "XTHtmlTagCenter.h"
#import "XTHtmlTagH1.h"
#import "XTHtmlTagH2.h"
#import "XTHtmlTagH3.h"
#import "XTHtmlTagH4.h"
#import "XTHtmlTagOl.h"
#import "XTHtmlTagUl.h"
#import "XTHtmlTagLi.h"
#import "XTHtmlTagNoop.h"
#import "XTHtmlTagQuestionMarkT2.h"
#import "XTHtmlTagQuestionMarkT3.h"
#import "XTHtmlTagTt.h"
#import "XTHtmlTagTab.h"
#import "XTHtmlTagDiv.h"
#import "XTHtmlTagBr.h"
#import "XTHtmlTagP.h"
#import "XTHtmlTagTitle.h"
#import "XTHtmlTagCite.h"
#import "XTHtmlTagFont.h"
#import "XTHtmlTagPre.h"
#import "XTHtmlTagImg.h"
#import "XTHtmlTagTable.h"
#import "XTHtmlTagTr.h"
#import "XTHtmlTagTh.h"
#import "XTHtmlTagTd.h"
#import "XTHtmlTagT2Hilite.h"
#import "XTHtmlWhitespace.h"
#import "XTHtmlQuotedSpace.h"
#import "XTHtmlNonbreakingSpace.h"
#import "XTLogger.h"
#import "XTFontManager.h"
#import "XTPrefs.h"
#import "XTStringUtils.h"
@interface XTOutputFormatter ()
typedef NS_ENUM(NSInteger, XTTextAlignMode) {
XT_TEXT_ALIGN_LEFT = 1,
XT_TEXT_ALIGN_CENTER = 2,
XT_TEXT_ALIGN_RIGHT = 3,
XT_TEXT_ALIGN_UNSPECIFIED = 99
};
@property XTHtmlLinebreakHandler2 *linebreakHandler2;
@property BOOL afterBlockLevelSpacing;
@property BOOL hiliteMode;
@property BOOL boldFaceMode;
@property BOOL italicsMode;
@property BOOL underlineMode;
@property BOOL ttMode;
@property BOOL preMode;
@property XTTextAlignMode textAlignMode;
//@property XTTextAlignMode tabAlignMode;
@property BOOL aboutBoxMode;
@property BOOL bannerMode;
@property BOOL orderedListMode;
@property BOOL unorderedListMode;
@property NSUInteger blockquoteLevel;
@property NSUInteger orderedListIndex;
@property BOOL h1Mode;
@property BOOL h2Mode;
@property BOOL h3Mode;
@property BOOL h4Mode;
@property BOOL receivingGameTitle;
@property BOOL shouldWriteWhitespace;
@property BOOL shouldWriteQuotedSpace;
@property BOOL lastElementFormattedEndedInWhitespace;
@property XTHtmlTagA *activeTagA;
@property NSNumber *htmlFontSize; // 1..7, 3 being "default"
@property NSArray *htmlFontFaceList;
@property NSArray *defaultTabStops;
@property NSArray *listItemPrefixTabStops;
@property NSArray *emptyArray;
@property XTFontManager *fontManager;
@property XTPrefs *prefs;
@end
@implementation XTOutputFormatter
static XTLogger* logger;
static NSString * const tabString = @" \t"; // include a space to ensure _some_ visual spacing
static NSString * const tableCellSeparator = @" ";
+ (void)initialize
{
logger = [XTLogger loggerForClass:[XTOutputFormatter class]];
}
- (id)init
{
self = [super init];
if (self) {
_linebreakHandler2 = [XTHtmlLinebreakHandler2 new];
_fontManager = [XTFontManager fontManager];
_prefs = [XTPrefs prefs];
_emptyArray = [NSArray array];
_isForBanner = NO;
[self resetFlags];
}
return self;
}
- (void)resetFlags
{
[self resetNonHtmlModesFlags];
[self resetHtmlModesFlags];
[self.linebreakHandler2 resetForNextCommand];
}
- (void)resetForNextCommand
{
[self resetNonHtmlModesFlags];
[self.linebreakHandler2 resetForNextCommand];
}
- (void)resetNonHtmlModesFlags
{
_receivingGameTitle = NO;
_afterBlockLevelSpacing = NO;
_shouldWriteWhitespace = NO;
_shouldWriteQuotedSpace = YES;
_aboutBoxMode = NO;
_bannerMode = NO;
_activeTagA = nil;
_lastElementFormattedEndedInWhitespace = NO;
}
- (void)resetHtmlModesFlags
{
_boldFaceMode = NO;
_italicsMode = NO;
_underlineMode = NO;
_ttMode = NO;
_preMode = NO;
_h1Mode = NO;
_h2Mode = NO;
_h3Mode = NO;
_h4Mode = NO;
_textAlignMode = XT_TEXT_ALIGN_LEFT;
//_tabAlignMode = XT_TEXT_ALIGN_UNSPECIFIED;
_htmlFontSize = nil;
_htmlFontFaceList = nil;
_orderedListMode = NO;
_unorderedListMode = NO;
_orderedListIndex = 0;
_blockquoteLevel = 0;
_activeTagA = nil;
}
- (NSArray *)formatElement:(id)elt
{
XT_DEF_SELNAME;
NSArray *eltRes = nil;
if ([elt isKindOfClass:[NSString class]]) {
eltRes = [self handleRegularText:(NSString *)elt];
} else if ([elt isKindOfClass:[XTHtmlTag class]]) {
eltRes = [self handleHtmlTag:(XTHtmlTag *)elt];
} else if ([elt isKindOfClass:[XTHtmlWhitespace class]]) {
eltRes = [self handleWhitespace:(XTHtmlWhitespace *)elt];
} else if ([elt isKindOfClass:[XTHtmlQuotedSpace class]]) {
eltRes = [self handleQuotedSpace:(XTHtmlQuotedSpace *)elt];
} else if ([elt isKindOfClass:[XTHtmlNonbreakingSpace class]]) {
eltRes = [self handleNonbreakingSpace:(XTHtmlNonbreakingSpace *)elt];
} else {
NSString *className;
if (elt == nil) {
className = @"nil";
} else {
className = NSStringFromClass([elt class]);
}
XT_ERROR_1(@"got element of unknown class \"%@\"", className);
}
self.lastElementFormattedEndedInWhitespace = NO;
if (eltRes.count >= 1) {
id lastEltId = [eltRes lastObject];
if ([lastEltId isKindOfClass:[XTFormattedOutputElement class]]) {
XTFormattedOutputElement *lastElt = (XTFormattedOutputElement *)lastEltId;
if ([lastElt isRegularOutputElement]) {
NSString *s = [lastElt.attributedString string];
if ([s hasSuffix:@" "]) {
self.lastElementFormattedEndedInWhitespace = YES;
}
}
}
}
return eltRes;
}
- (NSAttributedString *)formatInputText:(NSString *)string
{
return [self makeAttributedStringForInput:string];
}
//-------------------------------------------------------------------------------------------
- (NSArray *)handleHtmlTag:(XTHtmlTag *)tag
{
XT_DEF_SELNAME;
XT_TRACE_1(@"\"%@\"", [tag debugString]);
NSArray *res = nil;
if (tag != nil) {
BOOL executeTag =
[tag isKindOfClass:[XTHtmlTagAboutBox class]] ||
[tag isKindOfClass:[XTHtmlTagBanner class]] ||
(! self.aboutBoxMode && !self.bannerMode);
//TODO display of empty eg
// "Text1Text2Text3Text4Text5";
// qtads / chrome print block sep. such as empty line for each
// "Text1Text5"
// qtads / chrome print ONE block sep. such as empty line for entire
// "Text1
Text5" -- NOTE space in h2
// qtads prints extra nl, chrome does not
BOOL wasOutsideListModes = (! self.orderedListMode && ! self.unorderedListMode);
if (executeTag) {
NSMutableArray *resTemp = [NSMutableArray array];
//TODO must clear self.afterBlockLevelSpacing
// - for non block level tag
if (tag.isBlockLevel) {
//self.tabAlignMode = XT_TEXT_ALIGN_UNSPECIFIED;
if (tag.isStandalone || ! tag.closing) {
//TODO hack...find general sol'n:
if ([tag isKindOfClass:[XTHtmlTagLi class]] && wasOutsideListModes) {
// nothing
} else {
[resTemp addObject:[XTFormattedOutputElement removeTabsToStartOfLineElement]];
}
// ... hack
//TODO exp...
//TODO try and combine with lbh2
if (tag.needsBlockLevelSpacing) {
if (! self.afterBlockLevelSpacing) {
NSMutableAttributedString *blockLevelSpacing = [self makeAttributedStringForOutput:@"\n"];
[resTemp addObject:[XTFormattedOutputElement regularOutputElement:blockLevelSpacing]];
self.afterBlockLevelSpacing = YES;
}
}
//TODO ...exp
NSString *resPrefix = [self.linebreakHandler2 handleBlockLevelNewline:tag];
//TODO check behaviour for
if (resPrefix != nil) {
NSMutableAttributedString *resPrefixAttrString = [self makeAttributedStringForOutput:resPrefix];
[resTemp addObject:[XTFormattedOutputElement regularOutputElement:resPrefixAttrString]];
}
self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
}
}
NSArray *resMain = [tag dispatchToFormatter:self];
[resTemp addObjectsFromArray:resMain];
//TODO exp...
BOOL clearAfterBlockLevelSpacing = NO;
for (XTFormattedOutputElement *elt in resMain) {
if (! [elt isRemoveTabsToStartOfLineElement]) {
clearAfterBlockLevelSpacing = YES;
}
}
if (clearAfterBlockLevelSpacing) {
self.afterBlockLevelSpacing = NO;
}
//TODO ...exp
if (tag.isBlockLevel) {
if (tag.isStandalone || tag.closing) {
//[resTemp addObject:[XTFormattedOutputElement removeTabsToStartOfLineElement]];
//TODO exp rm'd
BOOL dontExecuteTag = ([tag isKindOfClass:[XTHtmlTagUl class]] || [tag isKindOfClass:[XTHtmlTagOl class]]) &&
wasOutsideListModes;
//TODO hack...find general sol'n:
if (! dontExecuteTag) {
NSString *resSuffix = [self.linebreakHandler2 handleBlockLevelNewline:tag];
if (resSuffix != nil) {
NSMutableAttributedString *resSuffixAttrString = [self makeAttributedStringForOutput:resSuffix];
[resTemp addObject:[XTFormattedOutputElement regularOutputElement:resSuffixAttrString]];
}
}
self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
if (! dontExecuteTag) {
//TODO exp...
//TODO try and combine with lbh2
if (tag.needsBlockLevelSpacing) {
if (! self.afterBlockLevelSpacing) {
NSMutableAttributedString *blockLevelSpacing = [self makeAttributedStringForOutput:@"\n"];
[resTemp addObject:[XTFormattedOutputElement regularOutputElement:blockLevelSpacing]];
self.afterBlockLevelSpacing = YES;
}
}
//TODO ...exp
}
}
}
res = [NSArray arrayWithArray:resTemp];
}
self.shouldWriteQuotedSpace = YES;
}
return res;
}
- (NSArray *)handleRegularText:(NSString *)string
{
XT_DEF_SELNAME;
XT_TRACE_1(@"\"%@\"", string);
//if (self.isForBanner) {
// int brkpt = 1;
//}
self.afterBlockLevelSpacing = NO;
NSArray *res = nil;
if (! self.htmlMode) {
res = [self makeArrayWithRegularOutputElement:string];
//TODO self.shouldWriteWhitespace = YES;
} else {
if (self.receivingGameTitle) {
res = [self makeArrayWithGameTitleElement:string];
} else if (self.aboutBoxMode || self.bannerMode) {
// nothing
} else {
NSArray *stringsSepdByNewline = [string componentsSeparatedByString:@"\n"];
//TODO mv to parser
for (NSString *s in stringsSepdByNewline) {
if (s.length >= 1) {
[self.linebreakHandler2 handleText:s];
res = [self makeArrayWithRegularOutputElement:s];
self.shouldWriteWhitespace = YES;
self.shouldWriteQuotedSpace = YES;
}
}
}
}
return res;
}
- (NSArray *)handleWhitespace:(XTHtmlWhitespace *)whitespace
{
XT_DEF_SELNAME;
XT_TRACE_1(@"shouldWriteWhitespace=%d", self.shouldWriteWhitespace);
//TODO handle adjacency to quoted apace
//TODO check html mode?
//if (self.isForBanner) {
// int brkpt = 1;
//}
NSArray *res = nil;
if (self.receivingGameTitle) {
res = [self makeArrayWithGameTitleElement:@" "];
} else {
//TODO check about box mode / banner mode
if (self.preMode) {
res = [self makeArrayWithRegularOutputElement:whitespace.text];
} else {
if (self.shouldWriteWhitespace) {
NSString *ws = @" ";
[self.linebreakHandler2 handleText:ws];
res = [self makeArrayWithRegularOutputElement:ws];
self.shouldWriteWhitespace = NO;
self.shouldWriteQuotedSpace = NO;
//TODO use FSM for this stuff!!
}
}
}
return res;
}
- (NSArray *)handleQuotedSpace:(XTHtmlQuotedSpace *)quotedSpace
{
XT_TRACE_ENTRY;
//TODO unit test
//TODO handle adjacency to space
// sqs -> " "
// sqqs -> " "
// qq -> " "
//TODO also test wrt. text, tabs and tags
//TODO check html mode?
NSArray *res = nil;
if (self.receivingGameTitle) {
res = [self makeArrayWithGameTitleElement:@" "];
} else {
//TODO check about box mode / banner mode
if (self.preMode) {
res = [self makeArrayWithRegularOutputElement:@" "];
} else {
if (self.shouldWriteQuotedSpace) {
NSString *ws = @" ";
[self.linebreakHandler2 handleText:ws];
res = [self makeArrayWithRegularOutputElement:ws];
self.shouldWriteWhitespace = NO;
}
self.shouldWriteQuotedSpace = YES;
}
}
return res;
}
- (NSArray *)handleNonbreakingSpace:(XTHtmlNonbreakingSpace *)nonbreakingSpace
{
XT_TRACE_ENTRY;
//TODO? nbsp variations: https://en.wikipedia.org/wiki/Non-breaking_space - repl. by simlr width breaking ones
NSArray *res = nil;
if (self.receivingGameTitle) {
res = [self makeArrayWithGameTitleElement:@" "];
} else {
//TODO check about box mode / banner mode
//TODO use premade objs instead of "[self makeArrayWith" for common cases
if (self.preMode) {
res = [self makeArrayWithRegularOutputElement:@" "];
} else {
if (self.lastElementFormattedEndedInWhitespace) {
// to avoid unwanted indents at beginning of line
res = [self makeArrayWithRegularOutputElement:@" "];
} else {
res = [self makeArrayWithRegularOutputElement:@"\u00A0"];
}
}
}
return res;
}
//------- TODO ---------
- (NSArray *)makeArrayWithGameTitleElement:(NSString *)string
{
XTFormattedOutputElement *outputElement;
NSMutableAttributedString *attrString = [[NSMutableAttributedString new] initWithString:string];
outputElement = [XTFormattedOutputElement gameTitleElement:attrString];
NSArray *res = [NSArray arrayWithObject:outputElement];
return res;
}
- (NSArray *)makeArrayWithRegularOutputElement:(NSString *)string
{
NSArray *res = [NSArray arrayWithObject:[self makeRegularOutputElement:string]];
return res;
}
- (NSArray *)makeArrayWithListItemPrefixElement:(NSString *)string
{
NSArray *res = [NSArray arrayWithObject:[self makeListItemPrefixElement:string]];
return res;
}
//------
- (XTFormattedOutputElement *)makeRegularOutputElement:(NSString *)string
{
//TODO flags... fonts...
NSMutableAttributedString *attrString = [self makeAttributedStringForOutput:string];
XTFormattedOutputElement *outputElement = [XTFormattedOutputElement regularOutputElement:attrString];
return outputElement;
}
- (NSMutableAttributedString *)makeAttributedStringForOutput:(NSString *)string
{
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:string
attributes:[self getTextAttributesDictionaryForOutput]];
return attrString;
}
- (XTFormattedOutputElement *)makeListItemPrefixElement:(NSString *)string
{
//TODO flags... fonts...
NSMutableAttributedString *attrString = [self makeAttributedStringForListItemPrefix:string];
XTFormattedOutputElement *outputElement = [XTFormattedOutputElement regularOutputElement:attrString];
return outputElement;
}
- (NSMutableAttributedString *)makeAttributedStringForListItemPrefix:(NSString *)string
{
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:string
attributes:[self getTextAttributesDictionaryForListItemPrefix]];
return attrString;
}
- (NSMutableAttributedString *)makeAttributedStringForInput:(NSString *)string
{
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:string
attributes:[self getTextAttributesDictionaryForInput]];
return attrString;
}
//------
- (NSDictionary *)getTextAttributesDictionaryForOutput
{
XT_DEF_SELNAME;
NSMutableDictionary *dict = [self getTextAttributesDictionaryCommon];
NSMutableParagraphStyle *pgStyle = dict[NSParagraphStyleAttributeName];
[pgStyle setTabStops:[self getDefaultTabStops:pgStyle]];
NSTextAlignment alignment = NSLeftTextAlignment;
if (self.textAlignMode == XT_TEXT_ALIGN_LEFT) {
alignment = NSLeftTextAlignment;
} else if (self.textAlignMode == XT_TEXT_ALIGN_CENTER) {
alignment = NSCenterTextAlignment;
} else if (self.textAlignMode == XT_TEXT_ALIGN_RIGHT) {
alignment = NSRightTextAlignment;
} else {
XT_ERROR_1(@"unknown textAlignMode %d", self.textAlignMode);
}
//TODO test:
//TODO doesn't work for "Text1Right?
Text2
Text3";
// !!! "
Right?" works - i.e. just after newline
// cannot mix alignments in same pg - seems we must use right-aligned tabs...
/*
idea sketch:
add enough tab chars to bring use to The Right-aligned One
take into account window width < combined width of tabs
...but how to define The Right-aligned One?
*/
/*
switch (self.tabAlignMode) {
case XT_TEXT_ALIGN_UNSPECIFIED:
// Nothing - don't override textAlignMode
break;
case XT_TEXT_ALIGN_LEFT:
alignment = NSLeftTextAlignment;
dict[XT_OUTPUT_FORMATTER_ATTR_TAB_ALIGNMENT_SET] = @"true";
break;
case XT_TEXT_ALIGN_CENTER:
alignment = NSCenterTextAlignment;
dict[XT_OUTPUT_FORMATTER_ATTR_TAB_ALIGNMENT_SET] = @"true";
break;
case XT_TEXT_ALIGN_RIGHT:
alignment = NSRightTextAlignment;
dict[XT_OUTPUT_FORMATTER_ATTR_TAB_ALIGNMENT_SET] = @"true";
break;
default:
XT_ERROR_1(@"unknown tabAlignMode %d", self.tabAlignMode);
break;
}*/
[pgStyle setAlignment:alignment];
//TODO all "indenting" tags should contribute to actual indent size
CGFloat firstLineHeadIndent = [self getTabStopColumnWidthInPoints];
CGFloat headIndent = firstLineHeadIndent;
if (self.blockquoteLevel >= 1) {
CGFloat indent = firstLineHeadIndent * self.blockquoteLevel;
[pgStyle setFirstLineHeadIndent:indent];
[pgStyle setHeadIndent:indent];
} else if (self.unorderedListMode) {
[pgStyle setFirstLineHeadIndent:firstLineHeadIndent];
[pgStyle setHeadIndent:headIndent];
} else if (self.orderedListMode) {
[pgStyle setFirstLineHeadIndent:firstLineHeadIndent];
[pgStyle setHeadIndent:headIndent];
}
if (self.underlineMode) {
dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:1];
}
/*
//TODO exp!
//TODO test collapsing behaviour -- doesn't seem to work :-(
if (self.h1Mode) {
CGFloat spacingBefore = 12.0; //TODO dep on font size
CGFloat spacingAfter = 12.0; //TODO dep on font size
[pgStyle setParagraphSpacingBefore:spacingBefore];
[pgStyle setParagraphSpacing:spacingAfter];
}
//TODO other h* etc.
*/
if (self.activeTagA != nil) {
NSString *href = [self.activeTagA attributeAsString:@"href"];
if (href == nil) {
href = @"";
}
dict[NSLinkAttributeName] = href;
if ([self.activeTagA hasAttribute:@"append"]) {
dict[XT_OUTPUT_FORMATTER_ATTR_CMDLINK_APPEND] = @"true";
}
if ([self.activeTagA hasAttribute:@"noenter"]) {
dict[XT_OUTPUT_FORMATTER_ATTR_CMDLINK_NOENTER] = @"true";
}
}
NSDictionary *temporaryAttrsDict = [self getTextTemporaryAttributesDictionaryForOutput];
dict[XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT] = temporaryAttrsDict;
return dict;
}
- (NSDictionary *)getTextAttributesDictionaryForListItemPrefix
{
NSMutableDictionary *dict = [self getTextAttributesDictionaryCommon];
NSMutableParagraphStyle *pgStyle = dict[NSParagraphStyleAttributeName];
[pgStyle setTabStops:[self getListItemPrefixTabStops:pgStyle]];
CGFloat firstLineHeadIndent = [self getTabStopColumnWidthInPoints];
[pgStyle setFirstLineHeadIndent:firstLineHeadIndent];
CGFloat headIndent = firstLineHeadIndent + [self getListItemPrefixColumnWidthInPoints];
[pgStyle setHeadIndent:headIndent];
return dict;
}
- (NSDictionary *)getTextTemporaryAttributesDictionaryForOutput
{
XT_DEF_SELNAME;
NSMutableDictionary *dict = nil;
if (self.activeTagA != nil) {
dict = [NSMutableDictionary dictionary];
dict[NSForegroundColorAttributeName] = self.prefs.linksTextColor;
if (! self.underlineMode) {
if (! self.prefs.linksUnderline.boolValue) {
dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleNone];
}
}
if ([self.activeTagA hasAttribute:@"plain"]) {
// "plain" link - set temp attrs so it looks like regular text
dict[NSForegroundColorAttributeName] = [self getOutputTextColor];
if (! self.underlineMode) {
dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleNone];
}
}
}
return dict;
}
//TODO for output only - rename
- (NSMutableDictionary *)getTextAttributesDictionaryCommon
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[NSFontAttributeName] = [self getCurrentFontForOutput];
dict[NSForegroundColorAttributeName] = [self getOutputTextColor];
//TODO reconsider wrt prefs
NSMutableParagraphStyle *pgStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
dict[NSParagraphStyleAttributeName] = pgStyle;
return dict;
}
- (NSColor *)getOutputTextColor
{
NSColor *res;
if (self.isForBanner) {
res = self.prefs.statusLineTextColor;
} else {
res = self.prefs.outputAreaTextColor;
}
return res;
//TODO when game can set
}
//TODO make less wastefull!!
//TODO used?
- (CGFloat)getOutputTextWidth:(NSString *)string
{
// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Tasks/StringHeight.html
NSFont *myFont = [self getCurrentFontForOutput];
float myWidth = 1000; //TODO really should be from NSTextView...
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:string];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize: NSMakeSize(myWidth, FLT_MAX)];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
[textStorage addAttribute:NSFontAttributeName value:myFont range:NSMakeRange(0, [textStorage length])];
[textContainer setLineFragmentPadding:0.0];
(void) [layoutManager glyphRangeForTextContainer:textContainer];
return [layoutManager usedRectForTextContainer:textContainer].size.width;
}
- (NSDictionary *)getTextAttributesDictionaryForInput
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[NSFontAttributeName] = [self getCurrentFontForInput];
dict[NSForegroundColorAttributeName] = self.prefs.inputTextColor;
return dict;
}
- (NSFont *)getCurrentFontForOutput
{
//TODO adapt for prefs' banner font
BOOL bold = (self.boldFaceMode | self.hiliteMode | self.h1Mode | self.h2Mode | self.h3Mode | self.h4Mode);
BOOL italics = self.italicsMode;
NSString *parameterizedFontName;
if (self.ttMode || self.preMode) {
parameterizedFontName = [self.fontManager xtadsFixedWidthParameterizedFontName];
} else {
parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
}
NSArray *fontNames;
if (self.htmlFontFaceList != nil && self.htmlFontFaceList.count >= 1) {
fontNames = self.htmlFontFaceList;
} else {
fontNames = [NSArray arrayWithObject:parameterizedFontName];
}
XTParameterizedFont *parameterizedFont = [self.fontManager getParameterizedFontWithName:parameterizedFontName];
CGFloat defaultPointSize = parameterizedFont.size;
NSNumber *pointSize = nil;
if (self.h1Mode) {
pointSize = [NSNumber numberWithFloat:(defaultPointSize + 14.0)];
} else if (self.h2Mode) {
pointSize = [NSNumber numberWithFloat:(defaultPointSize + 9.0)];
} else if (self.h3Mode) {
pointSize = [NSNumber numberWithFloat:(defaultPointSize + 4.0)];
} else if (self.h4Mode) {
pointSize = [NSNumber numberWithFloat:(defaultPointSize + 2.0)];
}
NSFont *res = [self.fontManager getFontWithName:fontNames
pointSize:pointSize
htmlSize:self.htmlFontSize
bold:bold
italics:italics];
return res;
}
- (NSFont *)getCurrentFontForInput
{
// Remember, this is only used for programmatically added input text
XTPrefs *prefs = [XTPrefs prefs];
NSString *parameterizedFontName;
if (prefs.inputFontIsSameAsDefaultFont.boolValue) {
parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
} if (prefs.inputFontUsedEvenIfNotRequestedByGame.boolValue) {
parameterizedFontName = [self.fontManager tadsInputParameterizedFontName];
} else {
//TODO how to handle?
parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
}
NSArray *fontNames = [NSArray arrayWithObject:parameterizedFontName];
NSFont *res = [self.fontManager getFontWithName:fontNames
pointSize:nil
htmlSize:nil
bold:NO
italics:NO];
//TODO bold/italics should be determined by param font?
return res;
}
- (NSArray *)getDefaultTabStops:(NSMutableParagraphStyle *)pgStyle
{
if (self.defaultTabStops == nil) {
NSUInteger numTabStops = 6; //TODO: 30;
NSMutableArray *tempTabStops = [NSMutableArray arrayWithCapacity:numTabStops];
CGFloat columnWidthInPoints = [self getTabStopColumnWidthInPoints];
for(NSInteger tabCounter = 0; tabCounter < numTabStops; tabCounter++) {
NSTextTab * aTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:((tabCounter + 1) * columnWidthInPoints)];
[tempTabStops addObject:aTab];
}
//TODO exp:
NSTextTab * aTab = [[NSTextTab alloc] initWithType:NSRightTabStopType
location:(15 * columnWidthInPoints)];
[tempTabStops addObject:aTab];
self.defaultTabStops = tempTabStops;
}
return self.defaultTabStops;
}
//TODO what about tabs in list item text itself?
//TODO clean up / cache
- (NSArray *)getListItemPrefixTabStops:(NSMutableParagraphStyle *)pgStyle
{
if (self.listItemPrefixTabStops == nil) {
NSUInteger numTabStops = 2;
NSMutableArray *tempTabStops = [NSMutableArray arrayWithCapacity:numTabStops];
CGFloat prefixStartLocInPoints = [self getTabStopColumnWidthInPoints];
NSTextTab * prefixStartTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:prefixStartLocInPoints];
[tempTabStops addObject:prefixStartTab];
CGFloat textStartLocInPoints = prefixStartLocInPoints + [self getListItemPrefixColumnWidthInPoints];
NSTextTab * textStartTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:textStartLocInPoints];
[tempTabStops addObject:textStartTab];
self.listItemPrefixTabStops = tempTabStops;
}
return self.listItemPrefixTabStops;
}
- (CGFloat)getTabStopColumnWidthInPoints
{
//TODO wip!!!
float columnWidthInPoints = 32; // ((self.fontSize / 2) + 1) * 4;
//TODO real value. dep on relevant font size?
//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)getListItemPrefixColumnWidthInPoints
{
CGFloat res = [self getTabStopColumnWidthInPoints] * 0.7;
return res;
}
//TODO mv down?
- (BOOL)isInTabOppressingTag
{
BOOL res = (self.h1Mode || self.h2Mode || self.h3Mode || self.h4Mode);
return res;
}
- (BOOL)isInListMode
{
BOOL res = (self.orderedListMode || self.unorderedListMode);
return res;
}
//-------------------------------------------------------------------------------------------
#pragma mark XTOutputFormatterProtocol
- (NSArray *)handleHtmlTagQ:(XTHtmlTagQ *)tag
{
NSString *quote = @"\"";
//TODO diff char for open / close?
[self.linebreakHandler2 handleText:quote];
return [self makeArrayWithRegularOutputElement:quote];
}
- (NSArray *)handleHtmlTagTab:(XTHtmlTagTab *)tag
{
XT_DEF_SELNAME;
//TODO completely overhault handling of this tab
if ([tag hasAttribute:@"id"]) {
// a tab definition, so no output
return self.emptyArray;
}
//TODO test:
/*
NSString *attrNameAlign = @"align";
if ([tag hasAttribute:attrNameAlign]) {
if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"right"]) {
self.tabAlignMode = XT_TEXT_ALIGN_RIGHT;
} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"center"]) {
self.tabAlignMode = XT_TEXT_ALIGN_CENTER;
} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"left"]) {
self.tabAlignMode = XT_TEXT_ALIGN_LEFT;
} else {
NSString *alignAttr = [tag attributeAsString:attrNameAlign];
XT_ERROR_1(@"unknown align attr \"%@\"", alignAttr)
self.tabAlignMode = XT_TEXT_ALIGN_UNSPECIFIED;
}
//TODO tabAlignMode should only lasts until next linebreak
return self.emptyArray;
}*/
NSUInteger multiple = 1;
//TODO -1 ?
if ([tag hasAttribute:@"multiple"]) {
multiple = [tag attributeAsUInt:@"multiple"];
}
if (! [self isInTabOppressingTag]) {
self.shouldWriteWhitespace = NO;
[self.linebreakHandler2 handleTagTab];
return [self makeArrayWithRegularOutputElement:tabString];
} else {
return self.emptyArray;
}
}
//TODO unit test newline gen:
- (NSArray *)handleHtmlTagBlockQuote:(XTHtmlTagBlockQuote *)tag
{
//TODO consider interaction / flags with h*, list modes, etc.
if (! tag.closing) {
self.blockquoteLevel += 1;
} else {
if (self.blockquoteLevel >= 1) {
self.blockquoteLevel -= 1;
}
}
return self.emptyArray;
}
//TODO make block level?
- (NSArray *)handleHtmlTagBr:(XTHtmlTagBr *)tag
{
NSInteger height = -1;
if ([tag hasAttribute:@"height"]) {
height = [tag attributeAsUInt:@"height"];
}
NSString *s = [self.linebreakHandler2 handleTagBr:height];
self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
//self.tabAlignMode = XT_TEXT_ALIGN_UNSPECIFIED;
//TODO too coarse-grained?
NSMutableArray *res = [NSMutableArray arrayWithCapacity:2];
[res addObject:[XTFormattedOutputElement removeTabsToStartOfLineElement]];
if (s != nil && s.length >= 1) {
NSMutableAttributedString *attrString = [self makeAttributedStringForOutput:s];
[res addObject:[XTFormattedOutputElement regularOutputElement:attrString]];
}
return res;
}
- (NSArray *)handleHtmlTagDiv:(XTHtmlTagDiv *)tag
{
[self setTextAlignModeFromAlignAttribute:tag];
return self.emptyArray;
}
- (NSArray *)handleHtmlTagP:(XTHtmlTagP *)tag
{
[self setTextAlignModeFromAlignAttribute:tag];
return self.emptyArray;
}
- (void)setTextAlignModeFromAlignAttribute:(XTHtmlTag *)tag
{
XT_DEF_SELNAME;
if (! tag.closing) {
NSString *attrNameAlign = @"align";
if (! [tag hasAttribute:attrNameAlign]) {
self.textAlignMode = XT_TEXT_ALIGN_LEFT;
} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"right"]) {
self.textAlignMode = XT_TEXT_ALIGN_RIGHT;
} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"center"]) {
self.textAlignMode = XT_TEXT_ALIGN_CENTER;
} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"left"]) {
self.textAlignMode = XT_TEXT_ALIGN_LEFT;
} else if ([tag hasAttribute:attrNameAlign]) {
NSString *alignAttr = [tag attributeAsString:@"align"];
XT_ERROR_1(@"unknown align attr \"%@\"", alignAttr)
self.textAlignMode = XT_TEXT_ALIGN_LEFT;
}
} else {
self.textAlignMode = XT_TEXT_ALIGN_LEFT;
}
}
- (NSArray *)handleHtmlTagTitle:(XTHtmlTagTitle *)tag
{
NSArray *res;
if (! tag.closing) {
//[stream startTitle];
self.receivingGameTitle = YES;
//self.gameTitle = [NSMutableString stringWithString:@""];
res = [self makeArrayWithGameTitleElement:@"{{clear}}"];
//TODO hack
} else {
//[stream endTitle];
self.receivingGameTitle = NO;
res = self.emptyArray;
}
return res;
}
- (NSArray *)handleHtmlTagHr:(XTHtmlTagHr *)tag
{
//TODO ideally: adjust length acc to window width
//TODO ...or at least centre a fixed text string
self.textAlignMode = XT_TEXT_ALIGN_LEFT;
NSString *s = @"–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––";
[self.linebreakHandler2 handleText:s]; // so we're "in text"
NSMutableArray *res = [NSMutableArray array];
[res addObject:[self makeRegularOutputElement:s]];
//TODO exp rm: [res addObject:[self makeRegularOutputElement:@"\n"]];
return res;
}
- (NSArray *)handleHtmlTagA:(XTHtmlTagA *)tag
{
if (! tag.closing) {
self.activeTagA = tag;
} else {
self.activeTagA = nil;
}
return self.emptyArray;
}
- (NSArray *)handleHtmlTagB:(XTHtmlTagB *)tag
{
self.boldFaceMode = (! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagI:(XTHtmlTagI *)tag
{
self.italicsMode = (! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagU:(XTHtmlTagU *)tag
{
self.underlineMode = (! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagAboutBox:(XTHtmlTagAboutBox *)tag
{
/*TODO the presence/absence of the below code doesn't seem to matter, all of a sudden ?!?! BUG NOT SHOWING!
//TODO exp!!! ... blighted isle ting! space bug
[self.linebreakHandler2 resetForNextCommand];
//TODO really: [self.linebreakHandler2 handleTagAboutBox:tag]
self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
// ...exp
*/
self.aboutBoxMode = (! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagBanner:(XTHtmlTagBanner *)tag;
{
// "Common Ground", "help" calls this
//TODO temp comt'd out - 1893 startup menu not showing
//TODO ignore for id=StatusLine ?
//[stream setBannerMode:(! self.closing)];
return self.emptyArray;
}
- (NSArray *)handleHtmlTagCenter:(XTHtmlTagCenter *)tag
{
self.textAlignMode = (tag.closing ? XT_TEXT_ALIGN_LEFT : XT_TEXT_ALIGN_CENTER);
return self.emptyArray;
//TODO might be at cmd input, so should really mv cursor to left of line - return "non-printing space"?
}
- (NSArray *)handleHtmlTagH1:(XTHtmlTagH1 *)tag
{
self.h1Mode = (! [self isInListMode] && ! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagH2:(XTHtmlTagH2 *)tag
{
self.h2Mode = (! [self isInListMode] && ! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagH3:(XTHtmlTagH3 *)tag
{
self.h3Mode = (! [self isInListMode] && ! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagH4:(XTHtmlTagH4 *)tag
{
self.h4Mode = (! [self isInListMode] && ! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagOl:(XTHtmlTagOl *)tag
{
self.orderedListMode = (! tag.closing);
self.orderedListIndex = 0;
return self.emptyArray;
}
- (NSArray *)handleHtmlTagUl:(XTHtmlTagUl *)tag
{
self.unorderedListMode = (! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagLi:(XTHtmlTagLi *)tag
{
NSArray *res = nil;
//TODO simplify
if (self.orderedListMode) {
NSString *s = nil;
if (! tag.closing) {
self.orderedListIndex += 1;
s = [NSString stringWithFormat:@"%lu.\t", self.orderedListIndex];
} else {
//s = [self.linebreakHandler2 handleEndLi]; TODO rm handleEndLi
}
if (s != nil) {
res = [self makeArrayWithListItemPrefixElement:s];
}
} else if (self.unorderedListMode) {
NSString *s = nil;
if (! tag.closing) {
s = [NSString stringWithFormat:@"\u2022\t"];
} else {
//s = [self.linebreakHandler2 handleEndLi];
}
if (s != nil) {
res = [self makeArrayWithListItemPrefixElement:s];
}
}
if (res == nil) {
res = self.emptyArray;
}
return res;
}
- (NSArray *)handleHtmlTagNoop:(XTHtmlTagNoop *)tag
{
return self.emptyArray;
}
- (NSArray *)handleHtmlTagQuestionMarkT2:(XTHtmlTagQuestionMarkT2 *)tag
{
/* TODO handle:
* Write out the special 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+\\H-");
return self.emptyArray;
}
- (NSArray *)handleHtmlTagQuestionMarkT3:(XTHtmlTagQuestionMarkT3 *)tag
{
/* TODO handle, but for T3:
* Write out the special 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+\\H-");
return self.emptyArray;
}
- (NSArray *)handleHtmlTagTt:(XTHtmlTagTt *)tag
{
self.ttMode = (! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagTable:(XTHtmlTagTable *)tag
{
self.textAlignMode = XT_TEXT_ALIGN_LEFT;
//TODO hacky? clear more flags?
//TODO also do such clearing for other blk lvl tags? ul ol ...?
return self.emptyArray;
}
- (NSArray *)handleHtmlTagTr:(XTHtmlTagTr *)tag
{
return self.emptyArray;
}
- (NSArray *)handleHtmlTagTh:(XTHtmlTagTh *)tag
{
//TODO pass thru some "filter" wrt. markup ws?
NSArray *res;
if (tag.closing) {
res = [self makeArrayWithRegularOutputElement:tableCellSeparator];
//TODO tab?
} else {
res = self.emptyArray;
}
return res;
}
- (NSArray *)handleHtmlTagTd:(XTHtmlTagTd *)tag
{
//TODO pass thru some "filter" wrt. markup ws?
NSArray *res;
if (tag.closing) {
res = [self makeArrayWithRegularOutputElement:tableCellSeparator];
//TODO tab?
} else {
res = self.emptyArray;
}
return res;
}
- (NSArray *)handleHtmlTagCite:(XTHtmlTagCite *)tag
{
self.italicsMode = (! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagFont:(XTHtmlTagFont *)tag
{
BOOL allow = self.prefs.allowGamesToSetFonts.boolValue;
if (allow && ! tag.closing) {
NSInteger sign;
NSUInteger tempHtmlFontSize;
NSInteger htmlFontSize = 3; // default
// size: A number from 1 to 7 that defines the size of the text. Browser default is 3.
if ([tag attribute:@"size" asOptionalSign:&sign andUint:&tempHtmlFontSize]) {
if (sign == 0) {
htmlFontSize = tempHtmlFontSize;
} else {
htmlFontSize += (sign * tempHtmlFontSize);
}
if (htmlFontSize < 1) {
htmlFontSize = 1;
} else if (htmlFontSize > 7) {
htmlFontSize = 7;
}
self.htmlFontSize = [NSNumber numberWithUnsignedInteger:htmlFontSize];
}
NSArray *htmlFontFaceList = [tag attributeAsCommaSeparatedStrings:@"face"];
if (htmlFontFaceList != nil && htmlFontFaceList.count >= 1) {
self.htmlFontFaceList = htmlFontFaceList;
}
//TODO handle attr COLOR (dep on prefs)
} else {
self.htmlFontSize = nil;
self.htmlFontFaceList = nil;
}
return self.emptyArray;
}
- (NSArray *)handleHtmlTagPre:(XTHtmlTagPre *)tag
{
self.preMode = (! tag.closing);
return self.emptyArray;
}
- (NSArray *)handleHtmlTagImg:(XTHtmlTagImg *)tag
{
NSArray *res = nil;
if ([tag hasAttribute:@"alt"]) {
NSString *altText = [tag attributeAsString:@"alt"];
if (altText != nil && altText.length >= 1) {
//NSString *s = [NSString stringWithFormat:@"[Image \"%@\" not shown]\n\n", altText];
NSString *s = [NSString stringWithFormat:@"[Image \"%@\" not shown]\n", altText];
res = [self makeArrayWithRegularOutputElement:s];
}
}
if (res == nil) {
res = self.emptyArray;
}
return res;
}
- (NSArray *)handleHtmlTagTads2Hilite:(XTHtmlTagT2Hilite *)tag
{
self.hiliteMode = (! tag.closing);
return self.emptyArray;
}
@end