//
// XTHtmlTag.m
// TadsTerp
//
// Created by Rune Berg on 29/03/14.
// Copyright (c) 2014 Rune Berg. All rights reserved.
//
#import "XTHtmlTag.h"
#import "XTHtmlTag_private.h"
#import "XTLogger.h"
#import "XTConverter.h"
#import "XTOutputFormatterProtocol.h"
#import "XTStringUtils.h"
#import "XTOutputTextParserProtocol.h"
#import "XTOutputTextParserHtml.h"
#import "XTBaseTextHandler.h"
#import "XTAllocDeallocCounter.h"
#import "XTPair.h"
@interface XTHtmlTag ()
@end
@implementation XTHtmlTag
static XTLogger* logger;
static XTConverter *converter;
static NSUInteger nextUnqueId = 1;
@synthesize uniqueId = _uniqueId;
OVERRIDE_ALLOC_FOR_COUNTER
OVERRIDE_DEALLOC_FOR_COUNTER
+ (NSString *)name
{
return @"OOPS! XTHtmlTag name not overridden";
}
+ (NSArray*)nameSynonyms
{
// tags generally don't have synonyms:
return [NSArray array];
}
+ (BOOL)standalone
{
return YES;
}
- (BOOL)blockLevel
{
return NO;
}
- (BOOL)blockLevelSpacingBefore
{
return NO;
}
- (BOOL)blockLevelSpacingAfter
{
return NO;
}
+ (BOOL)forT2
{
return YES;
}
+ (BOOL)forT3
{
return YES;
}
//TODO various methods should test for nil self.attributes
+ (void)initialize
{
logger = [XTLogger loggerForClass:[XTHtmlTag class]];
//TODO find concrete class?
converter = [XTConverter converter];
}
- (id)init
{
self = [super init];
if (self) {
_attributesDict = [NSMutableDictionary dictionary];
_attributesArray = [NSMutableArray array];
_hasFormatted = NO;
_uniqueId = nextUnqueId++;
}
return self;
}
- (NSString *)name
{
NSString *res = [[self class] name];
return res;
}
- (BOOL)isBlockLevel
{
BOOL res = [self blockLevel];
return res;
}
- (XTHtmlTagContainer *)getContainer
{
return self.container;
}
- (void)removeFromContainer
{
//XT_DEF_SELNAME;
//XT_WARN_1(@"%@", self.name);
if (self.container == nil) {
XT_DEF_SELNAME;
XT_ERROR_1(@"%@ has no container", self.name);
}
[self.container removeFromContents:self];
self.container = nil;
}
- (XTHtmlTagContainer *)findCorrectContainer:(XTHtmlTagContainer *)defaultContainer
{
return defaultContainer;
}
- (void)onParsing:(NSObject *)parser
{
[parser appendTagToCurrentContainer:self];
}
- (void)onEndTag:(NSObject *)parser
{
/* process the normal end tag */
[parser endNormalTag:self];
}
- (BOOL)isReadyToFormat
{
//TODO !!! real impl, overrides
return YES;
}
- (void)assertHasContainer
{
if (self.container == nil) {
XT_DEF_SELNAME;
XT_ERROR_0(@"self.container == nil");
}
}
// see htmltags.cpp, CHtmlTag::get_next_fmt_tag
- (XTHtmlTag *)getNextTagToFormat:(NSObject *)formatter
textHandler:(XTBaseTextHandler *)textHandler
shouldFormat:(BOOL)shouldFormat
{
[self assertHasContainer];
/* if I have a sibling, it's the next tag */
if (self.nextSibling != nil) {
return self.nextSibling;
}
/*
* That's the end of the contents list for my container - tell the
* container we're leaving, and move on to its next sibling. If we
* don't have a container, there are no more tags left to format.
*/
if (self.container != nil) {
XTHtmlTag *next;
XTHtmlTagContainer *container;
/*
* Find the container's next sibling. If the container itself
* doesn't have a next sibling, find its container's next
* sibling, and so on until we find a sibling or run out of
* containers.
*/
for (container = self.container; container != nil; container = container.container) {
[container assertHasContainer];
/* get the container's next sibling, and stop if it has one */
next = container.nextSibling;
if (next != nil) {
break;
}
}
/*
* If there is indeed another tag to format, or the tag is
* marked as "closed", tell our container that we're done
* formatting it. If there isn't anything left, and the tag
* hasn't been closed yet, do NOT leave the container yet --
* more document parsing may occur that adds more items to the
* current container, in which case we'll still want the current
* container's settings in effect when we come back to do more
* formatting. Hence, only exit the container if we have more
* work to do immediately, in which case we know that we'll
* never add anything more to our container. Note that we need
* to inform as many containers as we're actually exiting, if
* we're exiting more than one level, so iterate up the
* containment tree until we find a container with a next
* sibling.
*/
for (container = self.container; container != nil; container = container.container) {
[container assertHasContainer];
/*
* if this one isn't closed yet, and there's not another
* sibling, it's still open
*/
if (next == nil && ! container.closed) {
//TODO !!! might cause a problem for tags we don't format until closed:
// Remove some of container's current children - they've already been formatted and should not be formatted again:
[container removeChildrenUntilFirstOpenContainer];
break;
}
/* tell this one we're exiting it */
if ([formatter willProcessTag:container]) {
[container formatExit:formatter textHandler:textHandler];
}
[container assertHasContainer];
/* stop if this one has a sibling */
if (container.nextSibling != nil) {
break;
}
}
/* return the next tag after our container */
return next;
}
/*
* there's nothing after us, and we have no container, so this is
* the end of the line
*/
return nil;
}
- (void)preFormatForBlockLevel:(NSObject *)formatter
textHandler:(XTBaseTextHandler *)textHandler
{
if (self.isBlockLevel) {
NSArray *formattedElements = [formatter handleBlockLevelTagEntry:self];
[textHandler receiveFormattedElements:formattedElements];
}
}
- (void)checkNotHasFormatted
{
if (self.hasFormatted) {
XT_DEF_SELNAME;
XT_ERROR_2(@"hasFormatted=YES <%@> (uid=%lu)", self.name, self.uniqueId);
}
}
- (void)format:(NSObject *)formatter
textHandler:(XTBaseTextHandler *)textHandler
{
XT_DEF_SELNAME;
NSString *actualClassName = [self.class name];
XT_WARN_1(@"not overridden for class %@", actualClassName);
// ... and do nothing
}
- (void)noteHasFormatted
{
self.hasFormatted = YES;
}
- (void)postFormatForBlockLevel:(NSObject *)formatter
textHandler:(XTBaseTextHandler *)textHandler
{
if (self.isBlockLevel) {
NSArray *formattedElements = [formatter handleBlockLevelTagExit:self];
[textHandler receiveFormattedElements:formattedElements];
}
}
- (XTFormattingSpecification *)makeFormattingSpecificationFrom:(XTFormattingSpecification *)formattingSpec
{
XTFormattingSpecification *res = [XTFormattingSpecification specificationFrom:formattingSpec];
return res;
}
- (XTTextAlignMode)getTextAlignModeFrom:(XTTextAlignMode)currentTextAlignMode
{
return currentTextAlignMode;
}
//TODO rm if not needed - try to eliminate
- (XTTextAlignMode)getTextAlignMode:(XTTextAlignMode)currentTextAlignMode
defaultToLeftAligned:(BOOL)defaultToLeftAligned
{
XTTextAlignMode res;
if (defaultToLeftAligned) {
res = [self getTextAlignModeWithDefault:XT_TEXT_ALIGN_LEFT];
} else {
res = [self getTextAlignModeWithDefault:currentTextAlignMode];
}
return res;
}
- (XTTextAlignMode)getTextAlignModeWithDefault:(XTTextAlignMode)defaultTextAlignMode
{
XT_DEF_SELNAME;
XTTextAlignMode res;
NSString *attrNameAlign = @"align";
if (! [self hasAttribute:attrNameAlign]) {
res = defaultTextAlignMode;
} else if ([self hasAttribute:attrNameAlign withCaseInsensitiveValue:@"right"]) {
res = XT_TEXT_ALIGN_RIGHT;
} else if ([self hasAttribute:attrNameAlign withCaseInsensitiveValue:@"center"]) {
res = XT_TEXT_ALIGN_CENTER;
} else if ([self hasAttribute:attrNameAlign withCaseInsensitiveValue:@"left"]) {
res = XT_TEXT_ALIGN_LEFT;
} else if ([self hasAttribute:attrNameAlign withCaseInsensitiveValue:@"justify"]) {
res = XT_TEXT_ALIGN_JUSTIFY;
} else {
NSString *alignAttr = [self attributeAsString:@"align"];
XT_ERROR_1(@"unknown align attr value \"%@\"", alignAttr)
res = XT_TEXT_ALIGN_LEFT;
}
return res;
}
- (XTTextVerticalAlignMode)getTableVerticalAlignModeWithDefault:(XTTextVerticalAlignMode)defaultMode
{
XT_DEF_SELNAME;
NSString *attrName = @"valign";
XTTextVerticalAlignMode res;
if (! [self hasAttribute:attrName]) {
res = defaultMode;
} else if ([self hasAttribute:attrName withCaseInsensitiveValue:@"top"]) {
res = XT_TEXT_VERTICAL_ALIGN_TOP;
} else if ([self hasAttribute:attrName withCaseInsensitiveValue:@"middle"]) {
res = XT_TEXT_VERTICAL_ALIGN_MIDDLE;
} else if ([self hasAttribute:attrName withCaseInsensitiveValue:@"bottom"]) {
res = XT_TEXT_VERTICAL_ALIGN_BOTTOM;
} else {
NSString *attrValue = [self attributeAsString:attrName];
XT_ERROR_1(@"unknown valign attr value \"%@\"", attrValue)
res = defaultMode;
}
return res;
}
- (void)addAttribute:(NSString *)attributeName value:(NSString *)value
{
[self.attributesDict setObject:value forKey:attributeName];
XTPair *attrNameAndValue = [XTPair pairWithFirstObject:attributeName secondObject:value];
[self.attributesArray addObject:attrNameAndValue];
}
- (void)replaceAttributes:(NSArray *)attributesArray
{
self.attributesArray = [NSMutableArray arrayWithArray:attributesArray];
self.attributesDict = [NSMutableDictionary dictionary];
for (XTPair *attrNameAndValue in attributesArray) {
NSString *attrName = (NSString *)attrNameAndValue.firstObject;
NSObject *attrValue = attrNameAndValue.secondObject;
if (attrValue == nil) {
attrValue = [NSNull null];
}
self.attributesDict[attrName] = attrValue;
}
}
- (BOOL)hasAttribute:(NSString *)attributeName
{
BOOL res = ([self attributeAsString:attributeName] != nil);
return res;
}
- (BOOL)attributeNameAndValue:(XTPair *)attrNameAndValue
hasName:(NSString *)name
{
NSString *nameLowercase = [name lowercaseString];
NSString *attrName = (NSString *)attrNameAndValue.firstObject;
attrName = [attrName lowercaseString];
BOOL res = [attrName isEqual:nameLowercase];
return res;
}
- (BOOL)attributeNameAndValue:(XTPair *)attrNameAndValue
hasName:(NSString *)name
withCaseInsensitiveValue:(NSString *)value
{
NSString *nameLowercase = [name lowercaseString];
NSString *attrName = (NSString *)attrNameAndValue.firstObject;
attrName = [attrName lowercaseString];
BOOL res = NO;
if ([attrName isEqual:nameLowercase]) {
NSString *valueLowerCase = [value lowercaseString];
NSString *attrValue = (NSString *)attrNameAndValue.secondObject;
attrValue = [attrValue lowercaseString];
res = [attrValue isEqual:valueLowerCase];
}
return res;
}
- (BOOL)attributeNameAndValue:(XTPair *)attrNameAndValue
hasName:(NSString *)name
withCaseSensitiveValue:(NSString *)value
{
NSString *nameLowercase = [name lowercaseString];
NSString *attrName = (NSString *)attrNameAndValue.firstObject;
attrName = [attrName lowercaseString];
BOOL res = NO;
if ([attrName isEqual:nameLowercase]) {
NSString *attrValue = (NSString *)attrNameAndValue.secondObject;
res = [attrValue isEqual:value];
}
return res;
}
- (BOOL)hasAttribute:(NSString *)attributeName withCaseInsensitiveValue:(NSString *)value
{
BOOL res = NO;
NSString *actualValue = [self attributeAsString:attributeName];
if (actualValue != nil) {
actualValue = [actualValue lowercaseString];
value = [value lowercaseString];
if ([actualValue isEqualToString:value]) {
res = YES;
}
}
return res;
}
- (NSString *)attributeAsString:(NSString *)attributeName
{
NSString *attributeValue = self.attributesDict[[attributeName lowercaseString]];
return attributeValue;
}
- (BOOL)attribute:(NSString *)attributeName asUint:(NSUInteger*)uint
{
id attributeValue = self.attributesDict[[attributeName lowercaseString]];
//TODO trim
BOOL res = NO;
if (attributeValue != nil && attributeValue != [NSNull null]) {
NSString *attributeValueStr = attributeValue;
NSUInteger tempUint;
if ([converter toUInteger:attributeValueStr uinteger:&tempUint]) {
*uint = tempUint;
res = YES;
}
}
return res;
}
- (BOOL)attribute:(NSString *)attributeName asOptionalSign:(NSInteger*)sign andUint:(NSUInteger*)uint
{
id attributeValue = self.attributesDict[[attributeName lowercaseString]];
//TODO trim
BOOL res = NO;
if (attributeValue != nil && attributeValue != [NSNull null]) {
NSString *attributeValueStr = attributeValue;
if (attributeValueStr.length >= 1) {
NSInteger tempSign = 0;
unichar ch = [attributeValueStr characterAtIndex:0];
if (ch == '-') {
tempSign = -1;
} else if (ch == '+') {
tempSign = 1;
}
NSUInteger uintStartIndex = (tempSign == 0 ? 0 : 1);
NSString *uintStr = [attributeValueStr substringFromIndex:uintStartIndex];
NSUInteger tempUint;
if ([converter toUInteger:uintStr uinteger:&tempUint]) {
*sign = tempSign;
*uint = tempUint;
res = YES;
}
}
}
return res;
}
//TODO rewrite like above, to communicate error
//TODO and use that
- (NSUInteger)attributeAsUInt:(NSString *)attributeName
{
NSUInteger res = 0;
id attributeValue = self.attributesDict[[attributeName lowercaseString]];
//TODO trim
BOOL ok = YES;
if (attributeValue != nil && attributeValue != [NSNull null]) {
NSString *attributeValueNumPrefix = [XTStringUtils numericPrefix:attributeValue];
if (! [converter toUInteger:attributeValueNumPrefix uinteger:&res]) {
ok = NO;
}
}
if (! ok) {
XT_DEF_SELNAME;
XT_ERROR_1(@"failed for value \"%@\"", attributeValue);
//TODO emit some kind of error to caller?
}
return res;
}
- (NSNumber *)attributeAsNumber:(NSString *)attributeName
{
NSNumber *res = nil;
NSString *attributeValue = [self trimmedAttributeValue:attributeName];
if (attributeValue != nil) {
NSInteger intVal;
if ([converter toInteger:attributeValue integer:&intVal]) {
res = [NSNumber numberWithInteger:intVal];
} else {
XT_DEF_SELNAME;
XT_ERROR_1(@"failed for value \"%@\"", attributeValue);
}
}
return res;
}
- (NSNumber *)attributeAsNumberWithOptionalSuffix:(NSString *)attributeName
{
NSNumber *res = nil;
NSString *attributeValue = [self trimmedAttributeValue:attributeName];
if (attributeValue != nil) {
NSInteger intVal;
if ([converter toIntegerWithOptionalSuffix:attributeValue integer:&intVal]) {
res = [NSNumber numberWithInteger:intVal];
} else {
XT_DEF_SELNAME;
XT_ERROR_1(@"failed for value \"%@\"", attributeValue);
}
}
return res;
}
- (NSNumber *)attributeAsPercentage:(NSString *)attributeName
{
NSNumber *res = nil;
NSString *attributeValue = [self trimmedAttributeValue:attributeName];
if (attributeValue != nil) {
if ([XTStringUtils string:attributeValue endsWithChar:'%']) {
NSString *prefix = [XTStringUtils withoutLastChar:attributeValue];
prefix = [XTStringUtils trimLeadingAndTrailingWhitespace:prefix];
NSInteger intVal;
if ([converter toInteger:prefix integer:&intVal]) {
res = [NSNumber numberWithInteger:intVal];
} else {
XT_DEF_SELNAME;
XT_ERROR_1(@"failed for value \"%@\"", attributeValue);
}
}
}
return res;
}
//TODO unit test
- (NSArray *)attributeAsCommaSeparatedStrings:(NSString *)attributeName
{
NSMutableArray *res = [NSMutableArray array];
NSString *attributeValue = self.attributesDict[[attributeName lowercaseString]];
if (attributeValue != nil && attributeValue.length >= 1) {
NSArray *rawStrings = [attributeValue componentsSeparatedByString:@","];
for (NSString *rawS in rawStrings) {
NSString *trimmedS = [XTStringUtils trimLeadingAndTrailingWhitespace:rawS];
[res addObject:trimmedS];
}
}
return res;
}
/*TODO keep?
- (NSInteger)attributeAsInt:(NSString *)attributeName
{
NSInteger res = 0;
id attributeValue = self.attributes[[attributeName lowercaseString]];
//TODO trim
BOOL ok = YES;
if (attributeValue != nil && attributeValue != [NSNull null]) {
if (! [converter toInteger:attributeValue integer:&res]) {
ok = NO;
}
}
if (! ok) {
XT_DEF_SELNAME;
XT_ERROR_1(@"attributeAsInt failed for value \"%@\"", attributeValue);
//TODO emit some kind of error to caller
}
return res;
}*/
//TODO use!
- (NSString *)trimmedAttributeValue:(NSString *)attributeName
{
NSString *res = nil;
id attributeValue = self.attributesDict[[attributeName lowercaseString]];
if (attributeValue != nil && attributeValue != [NSNull null]) {
res = [XTStringUtils trimLeadingAndTrailingWhitespace:(NSString *)attributeValue];
}
return res;
}
- (BOOL)isStandalone
{
BOOL res = [[self class] standalone];
return res;
}
- (BOOL)needsBlockLevelSpacingBefore
{
BOOL res = [self blockLevelSpacingBefore];
return res;
}
- (BOOL)needsBlockLevelSpacingAfter
{
BOOL res = [self blockLevelSpacingAfter];
return res;
}
- (NSUInteger)getListNestingLevel
{
NSUInteger res;
if (self.container == nil) {
res = 0;
} else {
res = [self.container getListNestingLevel];
}
return res;
}
- (NSUInteger)getBlockquoteNestingLevel
{
NSUInteger res;
if (self.container == nil) {
res = 0;
} else {
res = [self.container getBlockquoteNestingLevel];
}
return res;
}
- (BOOL)isForT2
{
BOOL res = [[self class] forT2];
return res;
}
- (BOOL)isForT3
{
BOOL res = [[self class] forT3];
return res;
}
- (BOOL)isSameClassAs:(XTHtmlTag *)other
{
BOOL res = [[self class] isEqualTo:[other class]];
return res;
}
- (NSString *)debugString
{
NSString *closingSlash = (self.closing ? @"/" : @"");
NSMutableString *res = [NSMutableString stringWithFormat:@"<%@%@", closingSlash, [[self class] name]];
//TODO make work:
for (NSString *name in self.attributesDict.allKeys) {
id value = self.attributesDict[name];
if ([value isKindOfClass:[NSNull class]]) {
[res appendFormat:@" %@", name];
} else {
[res appendFormat:@" %@=\"%@\"", name, (NSString *)value];
}
}
[res appendString:@">"];
return res;
}
- (NSUInteger)recursivelyCountChildren
{
return 1;
}
@end