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