From 9cd57b731ab1c666d4a1cb725538fdc137763d12 Mon Sep 17 00:00:00 2001 From: Starla Insigna Date: Sat, 30 Jul 2011 11:19:14 -0400 Subject: Initial commit (version 0.2.1) --- libs/FontLabel/ZAttributedString.m | 597 +++++++++++++++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100755 libs/FontLabel/ZAttributedString.m (limited to 'libs/FontLabel/ZAttributedString.m') diff --git a/libs/FontLabel/ZAttributedString.m b/libs/FontLabel/ZAttributedString.m new file mode 100755 index 0000000..a4163bc --- /dev/null +++ b/libs/FontLabel/ZAttributedString.m @@ -0,0 +1,597 @@ +// +// ZAttributedString.m +// FontLabel +// +// Created by Kevin Ballard on 9/22/09. +// Copyright 2009 Zynga Game Networks. All rights reserved. +// + +#import "ZAttributedString.h" +#import "ZAttributedStringPrivate.h" + +@interface ZAttributedString () +- (NSUInteger)indexOfEffectiveAttributeRunForIndex:(NSUInteger)index; +- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange uniquingOnName:(NSString *)attributeName; +- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange + inRange:(NSRange)rangeLimit uniquingOnName:(NSString *)attributeName; +@end + +@interface ZAttributedString () +@property (nonatomic, readonly) NSArray *attributes; +@end + +@implementation ZAttributedString +@synthesize string = _buffer; +@synthesize attributes = _attributes; + +- (id)initWithAttributedString:(ZAttributedString *)attr { + NSParameterAssert(attr != nil); + if ((self = [super init])) { + _buffer = [attr->_buffer mutableCopy]; + _attributes = [[NSMutableArray alloc] initWithArray:attr->_attributes copyItems:YES]; + } + return self; +} + +- (id)initWithString:(NSString *)str { + return [self initWithString:str attributes:nil]; +} + +- (id)initWithString:(NSString *)str attributes:(NSDictionary *)attributes { + if ((self = [super init])) { + _buffer = [str mutableCopy]; + _attributes = [[NSMutableArray alloc] initWithObjects:[ZAttributeRun attributeRunWithIndex:0 attributes:attributes], nil]; + } + return self; +} + +- (id)init { + return [self initWithString:@"" attributes:nil]; +} + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super init])) { + _buffer = [[decoder decodeObjectForKey:@"buffer"] mutableCopy]; + _attributes = [[decoder decodeObjectForKey:@"attributes"] mutableCopy]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_buffer forKey:@"buffer"]; + [aCoder encodeObject:_attributes forKey:@"attributes"]; +} + +- (id)copyWithZone:(NSZone *)zone { + return [self retain]; +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + return [(ZMutableAttributedString *)[ZMutableAttributedString allocWithZone:zone] initWithAttributedString:self]; +} + +- (NSUInteger)length { + return [_buffer length]; +} + +- (NSString *)description { + NSMutableArray *components = [NSMutableArray arrayWithCapacity:[_attributes count]*2]; + NSRange range = NSMakeRange(0, 0); + for (NSUInteger i = 0; i <= [_attributes count]; i++) { + range.location = NSMaxRange(range); + ZAttributeRun *run; + if (i < [_attributes count]) { + run = [_attributes objectAtIndex:i]; + range.length = run.index - range.location; + } else { + run = nil; + range.length = [_buffer length] - range.location; + } + if (range.length > 0) { + [components addObject:[NSString stringWithFormat:@"\"%@\"", [_buffer substringWithRange:range]]]; + } + if (run != nil) { + NSMutableArray *attrDesc = [NSMutableArray arrayWithCapacity:[run.attributes count]]; + for (id key in run.attributes) { + [attrDesc addObject:[NSString stringWithFormat:@"%@: %@", key, [run.attributes objectForKey:key]]]; + } + [components addObject:[NSString stringWithFormat:@"{%@}", [attrDesc componentsJoinedByString:@", "]]]; + } + } + return [NSString stringWithFormat:@"%@", [components componentsJoinedByString:@" "]]; +} + +- (id)attribute:(NSString *)attributeName atIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange { + NSParameterAssert(attributeName != nil); + return [[self attributesAtIndex:index effectiveRange:aRange uniquingOnName:attributeName] objectForKey:attributeName]; +} + +- (id)attribute:(NSString *)attributeName atIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange inRange:(NSRange)rangeLimit { + NSParameterAssert(attributeName != nil); + return [[self attributesAtIndex:index longestEffectiveRange:aRange inRange:rangeLimit uniquingOnName:attributeName] objectForKey:attributeName]; +} + +- (ZAttributedString *)attributedSubstringFromRange:(NSRange)aRange { + if (NSMaxRange(aRange) > [_buffer length]) { + @throw [NSException exceptionWithName:NSRangeException reason:@"range was outisde of the attributed string" userInfo:nil]; + } + ZMutableAttributedString *newStr = [self mutableCopy]; + if (aRange.location > 0) { + [newStr deleteCharactersInRange:NSMakeRange(0, aRange.location)]; + } + if (NSMaxRange(aRange) < [_buffer length]) { + [newStr deleteCharactersInRange:NSMakeRange(aRange.length, [_buffer length] - NSMaxRange(aRange))]; + } + return [newStr autorelease]; +} + +- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange { + return [NSDictionary dictionaryWithDictionary:[self attributesAtIndex:index effectiveRange:aRange uniquingOnName:nil]]; +} + +- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange inRange:(NSRange)rangeLimit { + return [NSDictionary dictionaryWithDictionary:[self attributesAtIndex:index longestEffectiveRange:aRange inRange:rangeLimit uniquingOnName:nil]]; +} + +#if Z_BLOCKS +// Warning: this code has not been tested. The only guarantee is that it compiles. +- (void)enumerateAttribute:(NSString *)attrName inRange:(NSRange)enumerationRange options:(ZAttributedStringEnumerationOptions)opts + usingBlock:(void (^)(id, NSRange, BOOL*))block { + if (opts & ZAttributedStringEnumerationLongestEffectiveRangeNotRequired) { + [self enumerateAttributesInRange:enumerationRange options:opts usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { + id value = [attrs objectForKey:attrName]; + if (value != nil) { + block(value, range, stop); + } + }]; + } else { + __block id oldValue = nil; + __block NSRange effectiveRange = NSMakeRange(0, 0); + [self enumerateAttributesInRange:enumerationRange options:opts usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { + id value = [attrs objectForKey:attrName]; + if (oldValue == nil) { + oldValue = value; + effectiveRange = range; + } else if (value != nil && [oldValue isEqual:value]) { + // combine the attributes + effectiveRange = NSUnionRange(effectiveRange, range); + } else { + BOOL innerStop = NO; + block(oldValue, effectiveRange, &innerStop); + if (innerStop) { + *stop = YES; + oldValue = nil; + } else { + oldValue = value; + } + } + }]; + if (oldValue != nil) { + BOOL innerStop = NO; // necessary for the block, but unused + block(oldValue, effectiveRange, &innerStop); + } + } +} + +- (void)enumerateAttributesInRange:(NSRange)enumerationRange options:(ZAttributedStringEnumerationOptions)opts + usingBlock:(void (^)(NSDictionary*, NSRange, BOOL*))block { + // copy the attributes so we can mutate the string if necessary during enumeration + // also clip the array during copy to only the subarray of attributes that cover the requested range + NSArray *attrs; + if (NSEqualRanges(enumerationRange, NSMakeRange(0, 0))) { + attrs = [NSArray arrayWithArray:_attributes]; + } else { + // in this binary search, last is the first run after the range + NSUInteger first = 0, last = [_attributes count]; + while (last > first+1) { + NSUInteger pivot = (last + first) / 2; + ZAttributeRun *run = [_attributes objectAtIndex:pivot]; + if (run.index < enumerationRange.location) { + first = pivot; + } else if (run.index >= NSMaxRange(enumerationRange)) { + last = pivot; + } + } + attrs = [_attributes subarrayWithRange:NSMakeRange(first, last-first)]; + } + if (opts & ZAttributedStringEnumerationReverse) { + NSUInteger end = [_buffer length]; + for (ZAttributeRun *run in [attrs reverseObjectEnumerator]) { + BOOL stop = NO; + NSUInteger start = run.index; + // clip to enumerationRange + start = MAX(start, enumerationRange.location); + end = MIN(end, NSMaxRange(enumerationRange)); + block(run.attributes, NSMakeRange(start, end - start), &stop); + if (stop) break; + end = run.index; + } + } else { + NSUInteger start = 0; + ZAttributeRun *run = [attrs objectAtIndex:0]; + NSInteger offset = 0; + NSInteger oldLength = [_buffer length]; + for (NSUInteger i = 1;;i++) { + NSUInteger end; + if (i >= [attrs count]) { + end = oldLength; + } else { + end = [[attrs objectAtIndex:i] index]; + } + BOOL stop = NO; + NSUInteger clippedStart = MAX(start, enumerationRange.location); + NSUInteger clippedEnd = MIN(end, NSMaxRange(enumerationRange)); + block(run.attributes, NSMakeRange(clippedStart + offset, clippedEnd - start), &stop); + if (stop || i >= [attrs count]) break; + start = end; + NSUInteger newLength = [_buffer length]; + offset += (newLength - oldLength); + oldLength = newLength; + } + } +} +#endif + +- (BOOL)isEqualToAttributedString:(ZAttributedString *)otherString { + return ([_buffer isEqualToString:otherString->_buffer] && [_attributes isEqualToArray:otherString->_attributes]); +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[ZAttributedString class]] && [self isEqualToAttributedString:(ZAttributedString *)object]; +} + +#pragma mark - + +- (NSUInteger)indexOfEffectiveAttributeRunForIndex:(NSUInteger)index { + NSUInteger first = 0, last = [_attributes count]; + while (last > first + 1) { + NSUInteger pivot = (last + first) / 2; + ZAttributeRun *run = [_attributes objectAtIndex:pivot]; + if (run.index > index) { + last = pivot; + } else if (run.index < index) { + first = pivot; + } else { + first = pivot; + break; + } + } + return first; +} + +- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange uniquingOnName:(NSString *)attributeName { + if (index >= [_buffer length]) { + @throw [NSException exceptionWithName:NSRangeException reason:@"index beyond range of attributed string" userInfo:nil]; + } + NSUInteger runIndex = [self indexOfEffectiveAttributeRunForIndex:index]; + ZAttributeRun *run = [_attributes objectAtIndex:runIndex]; + if (aRange != NULL) { + aRange->location = run.index; + runIndex++; + if (runIndex < [_attributes count]) { + aRange->length = [[_attributes objectAtIndex:runIndex] index] - aRange->location; + } else { + aRange->length = [_buffer length] - aRange->location; + } + } + return run.attributes; +} +- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange + inRange:(NSRange)rangeLimit uniquingOnName:(NSString *)attributeName { + if (index >= [_buffer length]) { + @throw [NSException exceptionWithName:NSRangeException reason:@"index beyond range of attributed string" userInfo:nil]; + } else if (NSMaxRange(rangeLimit) > [_buffer length]) { + @throw [NSException exceptionWithName:NSRangeException reason:@"rangeLimit beyond range of attributed string" userInfo:nil]; + } + NSUInteger runIndex = [self indexOfEffectiveAttributeRunForIndex:index]; + ZAttributeRun *run = [_attributes objectAtIndex:runIndex]; + if (aRange != NULL) { + if (attributeName != nil) { + id value = [run.attributes objectForKey:attributeName]; + NSUInteger endRunIndex = runIndex+1; + runIndex--; + // search backwards + while (1) { + if (run.index <= rangeLimit.location) { + break; + } + ZAttributeRun *prevRun = [_attributes objectAtIndex:runIndex]; + id prevValue = [prevRun.attributes objectForKey:attributeName]; + if (prevValue == value || (value != nil && [prevValue isEqual:value])) { + runIndex--; + run = prevRun; + } else { + break; + } + } + // search forwards + ZAttributeRun *endRun = nil; + while (endRunIndex < [_attributes count]) { + ZAttributeRun *nextRun = [_attributes objectAtIndex:endRunIndex]; + if (nextRun.index >= NSMaxRange(rangeLimit)) { + endRun = nextRun; + break; + } + id nextValue = [nextRun.attributes objectForKey:attributeName]; + if (nextValue == value || (value != nil && [nextValue isEqual:value])) { + endRunIndex++; + } else { + endRun = nextRun; + break; + } + } + aRange->location = MAX(run.index, rangeLimit.location); + aRange->length = MIN((endRun ? endRun.index : [_buffer length]), NSMaxRange(rangeLimit)) - aRange->location; + } else { + // with no attribute name, we don't need to do any real searching, + // as we already guarantee each run has unique attributes. + // just make sure to clip the range to the rangeLimit + aRange->location = MAX(run.index, rangeLimit.location); + ZAttributeRun *endRun = (runIndex+1 < [_attributes count] ? [_attributes objectAtIndex:runIndex+1] : nil); + aRange->length = MIN((endRun ? endRun.index : [_buffer length]), NSMaxRange(rangeLimit)) - aRange->location; + } + } + return run.attributes; +} + +- (void)dealloc { + [_buffer release]; + [_attributes release]; + [super dealloc]; +} +@end + +@interface ZMutableAttributedString () +- (void)cleanupAttributesInRange:(NSRange)range; +- (NSRange)rangeOfAttributeRunsForRange:(NSRange)range; +- (void)offsetRunsInRange:(NSRange )range byOffset:(NSInteger)offset; +@end + +@implementation ZMutableAttributedString +- (id)copyWithZone:(NSZone *)zone { + return [(ZAttributedString *)[ZAttributedString allocWithZone:zone] initWithAttributedString:self]; +} + +- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range { + range = [self rangeOfAttributeRunsForRange:range]; + for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) { + [run.attributes setObject:value forKey:name]; + } + [self cleanupAttributesInRange:range]; +} + +- (void)addAttributes:(NSDictionary *)attributes range:(NSRange)range { + range = [self rangeOfAttributeRunsForRange:range]; + for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) { + [run.attributes addEntriesFromDictionary:attributes]; + } + [self cleanupAttributesInRange:range]; +} + +- (void)appendAttributedString:(ZAttributedString *)str { + [self insertAttributedString:str atIndex:[_buffer length]]; +} + +- (void)deleteCharactersInRange:(NSRange)range { + NSRange runRange = [self rangeOfAttributeRunsForRange:range]; + [_buffer replaceCharactersInRange:range withString:@""]; + [_attributes removeObjectsInRange:runRange]; + for (NSUInteger i = runRange.location; i < [_attributes count]; i++) { + ZAttributeRun *run = [_attributes objectAtIndex:i]; + ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:(run.index - range.length) attributes:run.attributes]; + [_attributes replaceObjectAtIndex:i withObject:newRun]; + [newRun release]; + } + [self cleanupAttributesInRange:NSMakeRange(runRange.location, 0)]; +} + +- (void)insertAttributedString:(ZAttributedString *)str atIndex:(NSUInteger)idx { + [self replaceCharactersInRange:NSMakeRange(idx, 0) withAttributedString:str]; +} + +- (void)removeAttribute:(NSString *)name range:(NSRange)range { + range = [self rangeOfAttributeRunsForRange:range]; + for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) { + [run.attributes removeObjectForKey:name]; + } + [self cleanupAttributesInRange:range]; +} + +- (void)replaceCharactersInRange:(NSRange)range withAttributedString:(ZAttributedString *)str { + NSRange replaceRange = [self rangeOfAttributeRunsForRange:range]; + NSInteger offset = [str->_buffer length] - range.length; + [_buffer replaceCharactersInRange:range withString:str->_buffer]; + [_attributes replaceObjectsInRange:replaceRange withObjectsFromArray:str->_attributes]; + NSRange newRange = NSMakeRange(replaceRange.location, [str->_attributes count]); + [self offsetRunsInRange:newRange byOffset:range.location]; + [self offsetRunsInRange:NSMakeRange(NSMaxRange(newRange), [_attributes count] - NSMaxRange(newRange)) byOffset:offset]; + [self cleanupAttributesInRange:NSMakeRange(newRange.location, 0)]; + [self cleanupAttributesInRange:NSMakeRange(NSMaxRange(newRange), 0)]; +} + +- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str { + [self replaceCharactersInRange:range withAttributedString:[[[ZAttributedString alloc] initWithString:str] autorelease]]; +} + +- (void)setAttributedString:(ZAttributedString *)str { + [_buffer release], _buffer = [str->_buffer mutableCopy]; + [_attributes release], _attributes = [str->_attributes mutableCopy]; +} + +- (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range { + range = [self rangeOfAttributeRunsForRange:range]; + for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) { + [run.attributes setDictionary:attributes]; + } + [self cleanupAttributesInRange:range]; +} + +#pragma mark - + +// splits the existing runs to provide one or more new runs for the given range +- (NSRange)rangeOfAttributeRunsForRange:(NSRange)range { + NSParameterAssert(NSMaxRange(range) <= [_buffer length]); + + // find (or create) the first run + NSUInteger first = 0; + ZAttributeRun *lastRun = nil; + for (;;first++) { + if (first >= [_attributes count]) { + // we didn't find a run + first = [_attributes count]; + ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:range.location attributes:lastRun.attributes]; + [_attributes addObject:newRun]; + [newRun release]; + break; + } + ZAttributeRun *run = [_attributes objectAtIndex:first]; + if (run.index == range.location) { + break; + } else if (run.index > range.location) { + ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:range.location attributes:lastRun.attributes]; + [_attributes insertObject:newRun atIndex:first]; + [newRun release]; + break; + } + lastRun = run; + } + + if (((ZAttributeRun *)[_attributes lastObject]).index < NSMaxRange(range)) { + NSRange subrange = NSMakeRange(first, [_attributes count] - first); + if (NSMaxRange(range) < [_buffer length]) { + ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:NSMaxRange(range) + attributes:(NSDictionary*)[(ZAttributeRun *)[_attributes lastObject] attributes]]; + [_attributes addObject:newRun]; + [newRun release]; + } + return subrange; + } else { + // find the last run within and the first run after the range + NSUInteger lastIn = first, firstAfter = [_attributes count]-1; + while (firstAfter > lastIn + 1) { + NSUInteger idx = (firstAfter + lastIn) / 2; + ZAttributeRun *run = [_attributes objectAtIndex:idx]; + if (run.index < range.location) { + lastIn = idx; + } else if (run.index > range.location) { + firstAfter = idx; + } else { + // this is definitively the first run after the range + firstAfter = idx; + break; + } + } + if ([[_attributes objectAtIndex:firstAfter] index] > NSMaxRange(range)) { + // the first after is too far after, insert another run! + ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:NSMaxRange(range) + attributes:[(ZAttributeRun *)[_attributes objectAtIndex:firstAfter-1] attributes]]; + [_attributes insertObject:newRun atIndex:firstAfter]; + [newRun release]; + } + return NSMakeRange(lastIn, firstAfter - lastIn); + } +} + +- (void)cleanupAttributesInRange:(NSRange)range { + // expand the range to include one surrounding attribute on each side + if (range.location > 0) { + range.location -= 1; + range.length += 1; + } + if (NSMaxRange(range) < [_attributes count]) { + range.length += 1; + } else { + // make sure the range is capped to the attributes count + range.length = [_attributes count] - range.location; + } + if (range.length == 0) return; + ZAttributeRun *lastRun = [_attributes objectAtIndex:range.location]; + for (NSUInteger i = range.location+1; i < NSMaxRange(range);) { + ZAttributeRun *run = [_attributes objectAtIndex:i]; + if ([lastRun.attributes isEqualToDictionary:run.attributes]) { + [_attributes removeObjectAtIndex:i]; + range.length -= 1; + } else { + lastRun = run; + i++; + } + } +} + +- (void)offsetRunsInRange:(NSRange)range byOffset:(NSInteger)offset { + for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { + ZAttributeRun *run = [_attributes objectAtIndex:i]; + ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:run.index + offset attributes:run.attributes]; + [_attributes replaceObjectAtIndex:i withObject:newRun]; + [newRun release]; + } +} +@end + +@implementation ZAttributeRun +@synthesize index = _index; +@synthesize attributes = _attributes; + ++ (id)attributeRunWithIndex:(NSUInteger)idx attributes:(NSDictionary *)attrs { + return [[[self alloc] initWithIndex:idx attributes:attrs] autorelease]; +} + +- (id)initWithIndex:(NSUInteger)idx attributes:(NSDictionary *)attrs { + NSParameterAssert(idx >= 0); + if ((self = [super init])) { + _index = idx; + if (attrs == nil) { + _attributes = [[NSMutableDictionary alloc] init]; + } else { + _attributes = [attrs mutableCopy]; + } + } + return self; +} + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super init])) { + _index = [[decoder decodeObjectForKey:@"index"] unsignedIntegerValue]; + _attributes = [[decoder decodeObjectForKey:@"attributes"] mutableCopy]; + } + return self; +} + +- (id)init { + return [self initWithIndex:0 attributes:[NSDictionary dictionary]]; +} + +- (id)copyWithZone:(NSZone *)zone { + return [[ZAttributeRun allocWithZone:zone] initWithIndex:_index attributes:_attributes]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:[NSNumber numberWithUnsignedInteger:_index] forKey:@"index"]; + [aCoder encodeObject:_attributes forKey:@"attributes"]; +} + +- (NSString *)description { + NSMutableArray *components = [NSMutableArray arrayWithCapacity:[_attributes count]]; + for (id key in _attributes) { + [components addObject:[NSString stringWithFormat:@"%@=%@", key, [_attributes objectForKey:key]]]; + } + return [NSString stringWithFormat:@"<%@: %p index=%lu attributes={%@}>", + NSStringFromClass([self class]), self, (unsigned long)_index, [components componentsJoinedByString:@" "]]; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[ZAttributeRun class]]) return NO; + ZAttributeRun *other = (ZAttributeRun *)object; + return _index == other->_index && [_attributes isEqualToDictionary:other->_attributes]; +} + +- (void)dealloc { + [_attributes release]; + [super dealloc]; +} +@end + +NSString * const ZFontAttributeName = @"ZFontAttributeName"; +NSString * const ZForegroundColorAttributeName = @"ZForegroundColorAttributeName"; +NSString * const ZBackgroundColorAttributeName = @"ZBackgroundColorAttributeName"; +NSString * const ZUnderlineStyleAttributeName = @"ZUnderlineStyleAttributeName"; -- cgit 1.4.1