diff --git a/Foundation/CPArray.j b/Foundation/CPArray.j index ee3503cd4..381e25de6 100755 --- a/Foundation/CPArray.j +++ b/Foundation/CPArray.j @@ -461,9 +461,9 @@ */ - (unsigned)indexOfObject:(id)anObject sortedByFunction:(Function)aFunction context:(id)aContext { - if (!aFunction || !anObject) + if (!aFunction || anObject === undefined) return CPNotFound; - + var mid, c, first = 0, last = length - 1; while (first <= last) { diff --git a/Foundation/CPAttributedString.j b/Foundation/CPAttributedString.j new file mode 100644 index 000000000..7c0a14055 --- /dev/null +++ b/Foundation/CPAttributedString.j @@ -0,0 +1,596 @@ +/* + * CPAttributedString.j + * Foundation + * + * Created by Ross Boucher. + * Copyright 2008, 280 North, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +import +import + +@implementation CPAttributedString : CPObject +{ + CPString _string; + CPArray _rangeEntries; +} + +//Creating an NSAttributedString Object +- (id)initWithString:(CPString)aString +{ + return [self initWithString:aString attributes:nil]; +} + +- (id)initWithAttributedString:(CPAttributedString)aString +{ + var string = [self initWithString:"" attributes:nil]; + + [string setAttributedString:aString]; + + return string; +} + +- (id)initWithString:(CPString)aString attributes:(CPDictionary)attributes +{ + self = [super init]; + + if (!attributes) + attributes = [CPDictionary dictionary]; + + _string = ""+aString; + _rangeEntries = [makeRangeEntry(CPMakeRange(0, _string.length), attributes)]; + + return self; +} + +//Retrieving Character Information +- (CPString)string +{ + return _string; +} + +- (CPString)mutableString +{ + return [self string]; +} + +- (unsigned)length +{ + return _string.length; +} + +- (unsigned)_indexOfEntryWithIndex:(unsigned)anIndex +{ + if (anIndex < 0 || anIndex > _string.length || anIndex === undefined) + return CPNotFound; + + //find the range entry that contains anIndex. + var sortFunction = function(index, entry) + { + //index is the character index we're searching for, while range is the actual range entry we're comparing against + if (CPLocationInRange(index, entry.range)) + return CPOrderedSame; + else if (CPMaxRange(entry.range) <= index) + return CPOrderedDescending; + else + return CPOrderedAscending; + } + + return [_rangeEntries indexOfObject:anIndex sortedByFunction:sortFunction]; +} + +//Retrieving Attribute Information +- (CPDictionary)attributesAtIndex:(unsigned)anIndex effectiveRange:(CPRangePointer)aRange +{ + //find the range entry that contains anIndex. + var entryIndex = [self _indexOfEntryWithIndex:anIndex]; + + if (entryIndex == CPNotFound) + return nil; + + var matchingRange = _rangeEntries[entryIndex]; + if (aRange) + { + aRange.location = matchingRange.range.location; + aRange.length = matchingRange.range.length; + } + + return matchingRange.attributes; +} + +- (CPDictionary)attributesAtIndex:(unsigned)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit +{ + var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex]; + + if (startingEntryIndex == CPNotFound) + return nil; + + if (!aRange) + return _rangeEntries[startingEntryIndex].attributes; + + if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit)) + { + aRange.location = rangeLimit.location; + aRange.length = rangeLimit.length; + + return _rangeEntries[startingEntryIndex].attributes; + } + + //scan backwards + var nextRangeIndex = startingEntryIndex - 1, + currentEntry = _rangeEntries[startingEntryIndex], + comparisonDict = currentEntry.attributes; + + while (nextRangeIndex >= 0) + { + var nextEntry = _rangeEntries[nextRangeIndex]; + + if (CPMaxRange(nextEntry.range) > rangeLimit.location && [nextEntry.attributes isEqualToDictionary:comparisonDict]) + { + currentEntry = nextEntry; + nextRangeIndex--; + } + else + break; + } + + aRange.location = MAX(currentEntry.range.location, rangeLimit.location); + + //scan forwards + currentEntry = _rangeEntries[startingEntryIndex]; + nextRangeIndex = startingEntryIndex + 1; + + while (nextRangeIndex < _rangeEntries.length) + { + var nextEntry = _rangeEntries[nextRangeIndex]; + + if (nextEntry.range.location < CPMaxRange(rangeLimit) && [nextEntry.attributes isEqualToDictionary:comparisonDict]) + { + currentEntry = nextEntry; + nextRangeIndex++; + } + else + break; + } + + aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location; + + return comparisonDict; +} + +- (id)attribute:(CPString)attribute atIndex:(unsigned)index effectiveRange:(CPRangePointer)aRange +{ + if (!attribute) + { + if (aRange) + { + aRange.location = 0; + aRange.length = _string.length; + } + + return nil; + } + + return [[self attributesAtIndex:index effectiveRange:aRange] valueForKey:attribute]; +} + +- (id)attribute:(CPString)attribute atIndex:(unsigned)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit +{ + //find the range entry that contains anIndex. + var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex]; + + if (startingEntryIndex == CPNotFound || !attribute) + return nil; + + if (!aRange) + return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute]; + + if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit)) + { + aRange.location = rangeLimit.location; + aRange.length = rangeLimit.length; + + return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute]; + } + + //scan backwards + var nextRangeIndex = startingEntryIndex - 1, + currentEntry = _rangeEntries[startingEntryIndex], + comparisonAttribute = [currentEntry.attributes objectForKey:attribute]; + + while (nextRangeIndex >= 0) + { + var nextEntry = _rangeEntries[nextRangeIndex]; + + if (CPMaxRange(nextEntry.range) > rangeLimit.location && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute])) + { + currentEntry = nextEntry; + nextRangeIndex--; + } + else + break; + } + + aRange.location = MAX(currentEntry.range.location, rangeLimit.location); + + //scan forwards + currentEntry = _rangeEntries[startingEntryIndex]; + nextRangeIndex = startingEntryIndex + 1; + + while (nextRangeIndex < _rangeEntries.length) + { + var nextEntry = _rangeEntries[nextRangeIndex]; + + if (nextEntry.range.location < CPMaxRange(rangeLimit) && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute])) + { + currentEntry = nextEntry; + nextRangeIndex++; + } + else + break; + } + + aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location; + + return comparisonAttribute; +} + +//Comparing Attributed Strings +- (BOOL)isEqualToAttributedString:(CPAttributedString)aString +{ + if(!aString) + return NO; + + if(_string != [aString string]) + return NO; + + var myRange = CPMakeRange(), + comparisonRange = CPMakeRange(), + myAttributes = [self attributesAtIndex:0 effectiveRange:myRange], + comparisonAttributes = [aString attributesAtIndex:0 effectiveRange:comparisonRange], + length = _string.length; + + while (CPMaxRange(CPUnionRange(myRange, comparisonRange)) < length) + { + if (CPIntersectionRange(myRange, comparisonRange).length > 0 && ![myAttributes isEqualToDictionary:comparisonAttributes]) + return NO; + if (CPMaxRange(myRange) < CPMaxRange(comparisonRange)) + myAttributes = [self attributesAtIndex:CPMaxRange(myRange) effectiveRange:myRange]; + else + comparisonAttributes = [aString attributesAtIndex:CPMaxRange(comparisonRange) effectiveRange:comparisonRange]; + } + + return YES; +} + +- (BOOL)isEqual:(id)anObject +{ + if (anObject == self) + return YES; + + if ([anObject isKindOfClass:[self class]]) + return [self isEqualToAttributedString:anObject]; + + return NO; +} + +//Extracting a Substring +- (CPAttributedString)attributedSubstringFromRange:(CPRange)aRange +{ + if (!aRange || CPMaxRange(aRange) > _string.length || aRange.location < 0) + [CPException raise:CPRangeException + reason:"tried to get attributedSubstring for an invalid range: "+(aRange?CPStringFromRange(aRange):"nil")]; + + var newString = [[CPAttributedString alloc] initWithString:_string.substring(aRange.location, CPMaxRange(aRange))], + entryIndex = [self _indexOfEntryWithIndex:aRange.location], + currentRangeEntry = _rangeEntries[entryIndex], + lastIndex = CPMaxRange(aRange); + + newString._rangeEntries = []; + + while (currentRangeEntry && CPMaxRange(currentRangeEntry.range) < lastIndex) + { + var newEntry = copyRangeEntry(currentRangeEntry); + newEntry.range.location -= aRange.location; + + if (newEntry.range.location < 0) + { + newEntry.range.length += newEntry.range.location; + newEntry.range.location = 0; + } + + newString._rangeEntries.push(newEntry); + currentRangeEntry = _rangeEntries[++entryIndex]; + } + + if (currentRangeEntry) + { + var newRangeEntry = copyRangeEntry(currentRangeEntry); + + newRangeEntry.range.length = CPMaxRange(aRange) - newRangeEntry.range.location; + newRangeEntry.range.location -= aRange.location; + + if (newRangeEntry.range.location < 0) + { + newRangeEntry.range.length += newRangeEntry.range.location; + newRangeEntry.range.location = 0; + } + + newString._rangeEntries.push(newRangeEntry); + } + + return newString; +} + +//Changing Characters +- (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString +{ + [self beginEditing]; + + if (!aString) + aString = ""; + + var startingIndex = [self _indexOfEntryWithIndex:aRange.location], + startingRangeEntry = _rangeEntries[startingIndex], + endingIndex = [self _indexOfEntryWithIndex:CPMaxRange(aRange)-1], + endingRangeEntry = _rangeEntries[endingIndex], + additionalLength = aString.length - aRange.length; + + _string = _string.substring(0, aRange.location) + aString + _string.substring(CPMaxRange(aRange)); + + if (startingIndex == endingIndex) + startingRangeEntry.range.length += additionalLength; + else + { + endingRangeEntry.range.length = CPMaxRange(endingRangeEntry.range) - CPMaxRange(aRange); + endingRangeEntry.range.location = CPMaxRange(aRange); + + startingRangeEntry.range.length = CPMaxRange(aRange) - startingRangeEntry.range.location; + + _rangeEntries.splice(startingIndex, endingIndex - startingIndex); + } + + endingIndex = startingIndex + 1; + + while(endingIndex < _rangeEntries.length) + _rangeEntries[endingIndex++].range.location+=additionalLength; + + [self endEditing]; +} + +- (void)deleteCharactersInRange:(CPRange)aRange +{ + [self replaceCharactersInRange:aRange withString:nil]; +} + +//Changing Attributes +- (void)setAttributes:(CPDictionary)aDictionary range:(CPRange)aRange +{ + [self beginEditing]; + + var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES], + endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES], + current = startingEntryIndex; + + if (endingEntryIndex == CPNotFound) + endingEntryIndex = _rangeEntries.length; + + while (current < endingEntryIndex) + _rangeEntries[current++].attributes = [aDictionary copy]; + + //necessary? + [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex]; + + [self endEditing]; +} + +- (void)addAttributes:(CPDictionary)aDictionary range:(CPRange)aRange +{ + [self beginEditing]; + + var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES], + endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES], + current = startingEntryIndex; + + if (endingEntryIndex == CPNotFound) + endingEntryIndex = _rangeEntries.length; + + while (current < endingEntryIndex) + { + var keys = [aDictionary allKeys], + count = [keys count]; + + while (count--) + [_rangeEntries[current].attributes setObject:[aDictionary objectForKey:keys[count]] forKey:keys[count]]; + + current++; + } + + //necessary? + [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex]; + + [self endEditing]; +} + +- (void)addAttribute:(CPString)anAttribute value:(id)aValue range:(CPRange)aRange +{ + [self addAttributes:[CPDictionary dictionaryWithObject:aValue forKey:anAttribute] range:aRange]; +} + +- (void)removeAttribute:(CPString)anAttribute range:(CPRange)aRange +{ + [self addAttribute:anAttribute value:nil range:aRange]; +} + +//Changing Characters and Attributes +- (void)appendAttributedString:(CPAttributedString)aString +{ + [self insertAttributedString:aString atIndex:_string.length]; +} + +- (void)insertAttributedString:(CPAttributedString)aString atIndex:(CPString)anIndex +{ + [self beginEditing]; + + if (anIndex < 0 || anIndex > [self length]) + [CPException raise:CPRangeException reason:"tried to insert attributed string at an invalid index: "+anIndex]; + + var entryIndexOfNextEntry = [self _indexOfRangeEntryForIndex:anIndex splitOnMaxIndex:YES], + otherRangeEntries = aString._rangeEntries, + length = [aString length]; + + if (entryIndexOfNextEntry == CPNotFound) + entryIndexOfNextEntry = _rangeEntries.length; + + _string = _string.substring(0, anIndex) + aString._string + _string.substring(anIndex); + + var current = entryIndexOfNextEntry; + while (current < _rangeEntries.length) + _rangeEntries[current++].range.location += length; + + var newRangeEntryCount = otherRangeEntries.length, + index = 0; + + while (index < newRangeEntryCount) + { + var entryCopy = copyRangeEntry(otherRangeEntries[index++]); + entryCopy.range.location += anIndex; + + _rangeEntries.splice(entryIndexOfNextEntry-1+index, 0, entryCopy); + } + + //necessary? + //[self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:startingEntryIndex+rangeEntries.length]; + + [self endEditing]; +} + +- (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString +{ + [self beginEditing]; + + [self deleteCharactersInRange:aRange]; + [self insertAttributedString:aString atIndex:aRange.location]; + + [self endEditing]; +} + +- (void)setAttributedString:(CPAttributedString)aString +{ + [self beginEditing]; + + _string = aString._string; + _rangeEntries = []; + + for (var i=0, count = aString._rangeEntries.length; i= _rangeEntries.length) + end = _rangeEntries.length -1; + + while (current < end) + { + var a = _rangeEntries[current], + b = _rangeEntries[current+1]; + + if ([a.attributes isEqualToDictionary:b.attributes]) + { + a.range.length = CPMaxRange(b.range) - a.range.location; + _rangeEntries.splice(current+1, 1); + end--; + } + else + current++; + } +} + +//Grouping Changes +- (void)beginEditing +{ + //do nothing (says cocotron and gnustep) +} + +- (void)endEditing +{ + //do nothing (says cocotron and gnustep) +} + +@end + +@implementation CPMutableAttributedString : CPAttributedString {} +@end + +var isEqual = function isEqual(a, b) +{ + if (a == b) + return YES; + + if ([a respondsToSelector:@selector(isEqual:)] && [a isEqual:b]) + return YES; + + return NO; +} + +var makeRangeEntry = function makeRangeEntry(/*CPRange*/aRange, /*CPDictionary*/attributes) +{ + return {range:aRange, attributes:[attributes copy]}; +} + +var copyRangeEntry = function copyRangeEntry(/*RangeEntry*/aRangeEntry) +{ + return makeRangeEntry(CPCopyRange(aRangeEntry.range), [aRangeEntry.attributes copy]); +} + +var splitRangeEntry = function splitRangeEntryAtIndex(/*RangeEntry*/aRangeEntry, /*unsigned*/anIndex) +{ + var newRangeEntry = copyRangeEntry(aRangeEntry), + cachedIndex = CPMaxRange(aRangeEntry.range); + + aRangeEntry.range.length = anIndex - aRangeEntry.range.location; + newRangeEntry.range.location = anIndex; + newRangeEntry.range.length = cachedIndex - anIndex; + newRangeEntry.attributes = [newRangeEntry.attributes copy]; + + return [aRangeEntry, newRangeEntry]; +} \ No newline at end of file diff --git a/Foundation/CPObject.j b/Foundation/CPObject.j index e7d5e914f..e498847cb 100644 --- a/Foundation/CPObject.j +++ b/Foundation/CPObject.j @@ -423,7 +423,7 @@ */ - (BOOL)isEqual:(id)anObject { - return self === anObject; + return self === anObject || [self hash] === [anObject hash]; } /*! diff --git a/Tests/Foundation/CPAttributedStringTest.j b/Tests/Foundation/CPAttributedStringTest.j new file mode 100644 index 000000000..96f7046eb --- /dev/null +++ b/Tests/Foundation/CPAttributedStringTest.j @@ -0,0 +1,593 @@ + +import + +var sharedObject = [CPObject new]; + +@implementation CPAttributedStringTest : OJTestCase + +- (CPAttributedString)stringForTesting +{ + var string = [[CPAttributedString alloc] initWithString:"The quick brown fox jumped over the lazy dog."]; + + string._rangeEntries = []; + + string._rangeEntries.push({ + range:CPMakeRange(0, 9), + attributes:[CPDictionary dictionaryWithObjects:[1, "bar", sharedObject, 20] forKeys:["a", "b", "c", "d"]] + }); + string._rangeEntries.push({ + range:CPMakeRange(9, 11), + attributes:[CPDictionary dictionaryWithObjects:[2, "baz", sharedObject] forKeys:["a", "b", "c"]] + }); + string._rangeEntries.push({ + range:CPMakeRange(20, 12), + attributes:[CPDictionary dictionaryWithObjects:[2, "baz", "astring", [CPNull null]] forKeys:["a", "b", "c", "d"]] + }); + string._rangeEntries.push({ + range:CPMakeRange(32, 13), + attributes:[CPDictionary dictionaryWithObjects:[37, "baz", 1, 20, 55, 43] forKeys:["a", "b", "c", "d", "e", "f"]] + }); + + return string; +} + +- (id)testInitWithString +{ + var string = [[CPAttributedString alloc] initWithString:@"hi there"]; + + [self assertTrue:([string string] === @"hi there") + message:"testInitWithString: expected:" + @"hi there" + " actual:" + [string string]]; +} +- (id)testInitWithAttributedString +{ + var string = [[CPAttributedString alloc] initWithString:@"hi there"], + attributedString = [[CPAttributedString alloc] initWithAttributedString:string]; + + [self assertTrue:([string isEqualToAttributedString:attributedString]) + message:"testInitWithAttributedString: expected:" + string + " actual:" + attributedString]; + + //TODO add a case where we init with a string that actually adds attributes + string = [[CPAttributedString alloc] initWithAttributedString:[self stringForTesting]]; + + [self assertTrue:([string isEqualToAttributedString:[self stringForTesting]]) + message:"testInitWithAttributedString: expected:" + [self stringForTesting] + " actual:" + string]; +} + +- (id)testIinitWithString_attributes +{ + var string = [[CPAttributedString alloc] initWithString:@"hi there" attributes:[CPDictionary dictionary]]; + + [self assertTrue:([string string] === @"hi there") + message:"testIinitWithString_attributes: expected:" + @"hi there" + " actual:" + [string string]]; + + var string = [[CPAttributedString alloc] initWithString:@"hi there" attributes:[CPDictionary dictionaryWithObjects:[1, "bar"] forKeys:["number", "foo"]]]; + + [self assertTrue:([[string attributesAtIndex:0 effectiveRange:nil] objectForKey:@"number"] === 1) + message:"testIinitWithString_attributes: value for key 'number' expected:" + 1 + " actual:" + [[string attributesAtIndex:0 effectiveRange:nil] objectForKey:@"number"]]; + + [self assertTrue:([[string attributesAtIndex:0 effectiveRange:nil] objectForKey:@"foo"] === "bar") + message:"testIinitWithString_attributes: value for key 'foo' expected:" + "bar" + " actual:" + [[string attributesAtIndex:0 effectiveRange:nil] objectForKey:@"foo"]]; +} + +//Retrieving Character Information +- (CPString)testString +{ + var string = [[CPAttributedString alloc] initWithString:@"hi there"]; + + [self assertTrue:([string string] === string._string) + message:"testString: expected:" + @"hi there" + " actual:" + [string string]]; +} + +- (unsigned)testLength +{ + var string = [[CPAttributedString alloc] initWithString:@"hi there"]; + + [self assertTrue:([string length] === 8) + message:"testLength: expected:" + 8 + " actual:" + [string length]]; + + [self assertTrue:([[self stringForTesting] length] === 45) + message:"testLength: expected:" + 45 + " actual:" + [[self stringForTesting] length]]; +} + +- (unsigned)test_indexOfEntryWithIndex +{ + var string = [self stringForTesting]; + + [self assertTrue:[string _indexOfEntryWithIndex:0]===0 message:@"expecting index 0, was:"+[string _indexOfEntryWithIndex:0]]; + [self assertTrue:[string _indexOfEntryWithIndex:10]===1 message:@"expecting index 1, was:"+[string _indexOfEntryWithIndex:10]]; + [self assertTrue:[string _indexOfEntryWithIndex:30]===2 message:@"expecting index 2, was:"+[string _indexOfEntryWithIndex:30]]; + [self assertTrue:[string _indexOfEntryWithIndex:35]===3 message:@"expecting index 3, was:"+[string _indexOfEntryWithIndex:35]]; + [self assertTrue:[string _indexOfEntryWithIndex:8]===0 message:@"expecting index 0, was:"+[string _indexOfEntryWithIndex:8]]; + [self assertTrue:[string _indexOfEntryWithIndex:9]===1 message:@"expecting index 1, was:"+[string _indexOfEntryWithIndex:9]]; + [self assertTrue:[string _indexOfEntryWithIndex:20]===2 message:@"expecting index 2, was:"+[string _indexOfEntryWithIndex:20]]; + [self assertTrue:[string _indexOfEntryWithIndex:32]===3 message:@"expecting index 3, was:"+[string _indexOfEntryWithIndex:32]]; +} + +//Retrieving Attribute Information +- (CPDictionary)testAttributesAtIndexEffectiveRange +{ + var string = [self stringForTesting], + expectedValues = {a:1, b:"bar", c:sharedObject, d:20}; + + testAttributesAtIndexWithValues(string, 1, expectedValues, self); + + expectedValues = {a:37, b:"baz", f:43} + + testAttributesAtIndexWithValues(string, 33, expectedValues, self); +} + +//- (CPDictionary)attributesAtIndex:(unsigned)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit +- (CPDictionary)testAttributesAtIndexLongestEffectiveRangeInRange +{ + var string = [self stringForTesting]; + + string._rangeEntries[3].range.length = 10; + string._rangeEntries.push({range:CPMakeRange(42, 3), attributes:[string._rangeEntries[3].attributes copy]}); + + var range = CPMakeRange(0, 0), + attributes = [string attributesAtIndex:35 longestEffectiveRange:range inRange:CPMakeRange(0, 43)]; + + [self assertTrue:CPMaxRange(range)==43 message:@"expecting attributes to be valuable at range 43, was: "+CPMaxRange(range)]; + [self assertTrue:range.location==32 message:@"expecting attributes to be valuable at range 32, was: "+range.location]; + + [self assertTrue:[attributes objectForKey:"a"]===37 message:@"expecting 'a' to be '1', was: "+[attributes objectForKey:"a"]]; + [self assertTrue:[attributes objectForKey:"b"]==="baz" message:@"expecting 'b' to be 'baz', was: "+[attributes objectForKey:"b"]]; + [self assertTrue:[attributes objectForKey:"f"]===43 message:@"expecting 'f' to be 43, was: "+[attributes objectForKey:"f"]]; +} + +//- (id)attribute:(CPString)attribute atIndex:(unsigned)index effectiveRange:(CPRangePointer)aRange +- (id)testAttributeAtIndexEffectiveRange +{ + var string = [self stringForTesting]; + + testAttributeAtIndexWithValue(string, 1, "a", 1, self); + testAttributeAtIndexWithValue(string, 20, "d", [CPNull null], self); +} + +//- (id)attribute:(CPString)attribute atIndex:(unsigned)index longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit +- (id)testAttributeAtIndexLongestEffectiveRangeInRange +{ + var string = [self stringForTesting]; + + var range = CPMakeRange(0, 0), + attribute = [string attribute:"b" atIndex:35 longestEffectiveRange:range inRange:CPMakeRange(0, 43)]; + + [self assertTrue:CPMaxRange(range)==43 message:@"expecting attributes to be valuable at range 43, was: "+CPMaxRange(range)]; + [self assertTrue:range.location==9 message:@"expecting attributes to be valuable at range 9, was: "+range.location]; + + [self assertTrue:attribute==="baz" message:@"expecting 'b' to be 'baz', was: "+attribute]; +} + +//Comparing Attributed Strings +- (BOOL)testIsEqualToAttributedString +{ + [self assertTrue:[[self stringForTesting] isEqualToAttributedString:[self stringForTesting]] message:"expected stringForTesting to equal itself, but it didn't"]; + + [self assertFalse:[[self stringForTesting] isEqualToAttributedString:[[CPAttributedString alloc] initWithString:@"HELLO!"]] message:"Expected stringForTesting to not equal 'HELLO!', but it did"]; +} + +- (BOOL)testIsEqual +{ + [self assertTrue:[[self stringForTesting] isEqual:[self stringForTesting]] message:"expected stringForTesting to equal itself, but it didn't"]; + + [self assertFalse:[[self stringForTesting] isEqual:[[CPAttributedString alloc] initWithString:@"HELLO!"]] message:"Expected stringForTesting to not equal 'HELLO!', but it did"]; + + var a = [self stringForTesting]; + + [self assertTrue:[a isEqual:a] message:"expected a to equal itself, but it didn't"]; + + [self assertFalse:[a isEqual:@"HELLO!"] message:"Expected a to not equal 'HELLO!', but it did"]; +} + +//Extracting a Substring +//- (CPAttributedString)attributedSubstringFromRange:(CPRange)aRange +- (CPAttributedString)testAttributedSubstringFromRange +{ + var a = [[self stringForTesting] attributedSubstringFromRange:CPMakeRange(0, 9)], + expectedValues = {a:1, b:"bar", c:sharedObject, d:20}; + + testAttributesAtIndexWithValues(a, 0, expectedValues, self); + + var b = [[self stringForTesting] attributedSubstringFromRange:CPMakeRange(0, 10)], + expectedValues = {a:1, b:"bar", c:sharedObject, d:20}; + + testAttributesAtIndexWithValues(b, 0, expectedValues, self); + + var c = [[self stringForTesting] attributedSubstringFromRange:CPMakeRange(0, 45)]; + [self assertTrue:[c isEqual:[self stringForTesting]] message:"expected c to equal itself, but it didn't"]; + + var d = [[self stringForTesting] attributedSubstringFromRange:CPMakeRange(9, 11)], + expectedValues = {a:2, b:"baz", c:sharedObject}; + + testAttributesAtIndexWithValues(d, 0, expectedValues, self); + + var e = [[self stringForTesting] attributedSubstringFromRange:CPMakeRange(8, 30)], + expectedValues = {a:2, b:"baz", c:"astring", d:[CPNull null]}; + + testAttributesAtIndexWithValues(e, 13, expectedValues, self); + + [self assertTrue:CPEqualRanges(e._rangeEntries[0].range, CPMakeRange(0, 1)) message:"expected range to be {0, 1}, was "+CPStringFromRange(e._rangeEntries[0].range)]; +} + +//Changing Characters +//- (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString +- (void)testReplaceCharactersInRangeWithString +{ + var string = [self stringForTesting]; + + [string replaceCharactersInRange:CPMakeRange(10, 5) withString:"firetruck red"]; + + [self assertTrue:[string string]==="The quick firetruck red fox jumped over the lazy dog." message:"replacing 'brown' with 'firetruck red' produced: "+[string string].substr(10, 5)]; + + testAttributesAtIndexWithValues(string, 21, {a:2, b:"baz", c:sharedObject}, self); + testAttributesAtIndexWithValues(string, 40, {a:37, b:"baz", c:1, d:20, e:55, f:43}, self); + testAttributesAtIndexWithValues(string, [string length] - 1, {a:37, b:"baz", c:1, d:20, e:55, f:43}, self); +} + +//- (void)deleteCharactersInRange:(CPRange)aRange +- (void)testDeleteCharactersInRange +{ + var string = [self stringForTesting]; + + [string deleteCharactersInRange:CPMakeRange(10, 5)]; + + [self assertTrue:[string string]==="The quick fox jumped over the lazy dog." message:"replacing 'brown' with '' produced: "+[string string]]; + + testAttributesAtIndexWithValues(string, 10, {a:2, b:"baz", c:sharedObject}, self); + testAttributesAtIndexWithValues(string, 27, {a:37, b:"baz", c:1, d:20, e:55, f:43}, self); + testAttributesAtIndexWithValues(string, [string length] - 1, {a:37, b:"baz", c:1, d:20, e:55, f:43}, self); + + [string deleteCharactersInRange:CPMakeRange(0, [string length])]; + + [self assertTrue:[string isEqual:[[CPAttributedString alloc] initWithString:""]] message:"emptry string was not equal: "+[string string]]; + + var string = [self stringForTesting]; + + //this deletes an exact rangeEntry + [string deleteCharactersInRange:CPMakeRange(9, 11)]; + + testAttributesAtIndexWithValues(string, 9, {a:2, b:"baz", c:"astring", d:[CPNull null]}, self); +} + +//Private methods +//- (void)_indexOfRangeEntryForIndex:(unsigned)characterIndex splitOnMaxIndex:(BOOL)split +- (void)testIndexOfRangeEntryForIndexSplitOnMaxIndex +{ + var string = [self stringForTesting]; + + var index = [string _indexOfRangeEntryForIndex:4 splitOnMaxIndex:YES]; + + [self assertTrue:index==1 message:"index of character 4 should have been 1, was: "+index]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[0].range, CPMakeRange(0, 4)) message:"range 0 should be {0, 4}; was "+CPStringFromRange(string._rangeEntries[0].range)]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[1].range, CPMakeRange(4, 5)) message:"range 1 should be {4, 5}; was "+CPStringFromRange(string._rangeEntries[1].range)]; + + [self assertTrue:[string isEqual:[self stringForTesting]] message:"should be equal to template attributed string but wasn't"]; + + string = [self stringForTesting]; + + index = [string _indexOfRangeEntryForIndex:8 splitOnMaxIndex:YES]; + + [self assertTrue:index==1 message:"index of character 8 should have been 1, was: "+index]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[0].range, CPMakeRange(0, 8)) message:"range 0 should be {0, 8}; was "+CPStringFromRange(string._rangeEntries[0].range)]; + + index = [string _indexOfRangeEntryForIndex:9 splitOnMaxIndex:YES]; + + [self assertTrue:index==2 message:"index of character 9 should have been 2, was: "+index]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[0].range, CPMakeRange(0, 8)) message:"range 0 should be {0, 8}; was "+CPStringFromRange(string._rangeEntries[0].range)]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[1].range, CPMakeRange(8, 1)) message:"range 1 should be {8, 1}; was "+CPStringFromRange(string._rangeEntries[1].range)]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[2].range, CPMakeRange(9, 11)) message:"range 2 should be {9, 11}; was "+CPStringFromRange(string._rangeEntries[2].range)]; + + string = [self stringForTesting]; + + index = [string _indexOfRangeEntryForIndex:44 splitOnMaxIndex:YES]; + + [self assertTrue:index==4 message:"index of character 44 should have been 4, was: "+index]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[index].range, CPMakeRange(44, 1)) message:"range 3 should be {44, 1}; was "+CPStringFromRange(string._rangeEntries[index].range)]; + + index = [string _indexOfRangeEntryForIndex:43 splitOnMaxIndex:YES]; + + [self assertTrue:index==4 message:"index of character 43 should have been 4, was: "+index]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[index-1].range, CPMakeRange(32, 11)) message:"range 3 should be {32, 11}; was "+CPStringFromRange(string._rangeEntries[index-1].range)]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[index].range, CPMakeRange(43, 1)) message:"range 4 should be {43, 1}; was "+CPStringFromRange(string._rangeEntries[index].range)]; +} + +//- (void)_coalesceRangeEntriesFromIndex:(unsigned)start toIndex:(unsigned)end +- (void)testCoalesceRangeEntriesFromIndexToIndex +{ + var templateString = [[CPAttributedString alloc] initWithString:"The quick brown fox jumped over the lazy dog."]; + + templateString._rangeEntries = []; + + templateString._rangeEntries.push({ + range:CPMakeRange(0, 4), + attributes:[CPDictionary dictionaryWithObjects:[1, "bar", sharedObject, 20] forKeys:["a", "b", "c", "d"]] + }); + templateString._rangeEntries.push({ + range:CPMakeRange(4, 5), + attributes:[CPDictionary dictionaryWithObjects:[1, "bar", sharedObject, 20] forKeys:["a", "b", "c", "d"]] + }); + templateString._rangeEntries.push({ + range:CPMakeRange(9, 10), + attributes:[CPDictionary dictionaryWithObjects:[2, "baz", sharedObject] forKeys:["a", "b", "c"]] + }); + templateString._rangeEntries.push({ + range:CPMakeRange(19, 1), + attributes:[CPDictionary dictionaryWithObjects:[2, "baz", sharedObject] forKeys:["a", "b", "c"]] + }); + templateString._rangeEntries.push({ + range:CPMakeRange(20, 12), + attributes:[CPDictionary dictionaryWithObjects:[2, "baz", "astring", [CPNull null]] forKeys:["a", "b", "c", "d"]] + }); + templateString._rangeEntries.push({ + range:CPMakeRange(32, 1), + attributes:[CPDictionary dictionaryWithObjects:[37, "baz", 1, 20, 55, 43] forKeys:["a", "b", "c", "d", "e", "f"]] + }); + templateString._rangeEntries.push({ + range:CPMakeRange(33, 10), + attributes:[CPDictionary dictionaryWithObjects:[37, "baz", 1, 20, 55, 43] forKeys:["a", "b", "c", "d", "e", "f"]] + }); + templateString._rangeEntries.push({ + range:CPMakeRange(43, 2), + attributes:[CPDictionary dictionaryWithObjects:[37, "baz", 1, 20, 55, 43] forKeys:["a", "b", "c", "d", "e", "f"]] + }); + + var string = [[CPAttributedString alloc] initWithAttributedString:templateString]; + + [string _coalesceRangeEntriesFromIndex:0 toIndex:string._rangeEntries.length-1]; + + [self assertTrue:[string isEqual:templateString] message:"string should equal template string but does not."]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[0].range, CPMakeRange(0, 9)) message:"range 0 should be {0, 9}; was "+CPStringFromRange(string._rangeEntries[0].range)]; + [self assertTrue:CPEqualRanges(string._rangeEntries[1].range, CPMakeRange(9, 11)) message:"range 1 should be {9, 11}; was "+CPStringFromRange(string._rangeEntries[1].range)]; + [self assertTrue:CPEqualRanges(string._rangeEntries[2].range, CPMakeRange(20, 12)) message:"range 2 should be {20, 12}; was "+CPStringFromRange(string._rangeEntries[2].range)]; + [self assertTrue:CPEqualRanges(string._rangeEntries[3].range, CPMakeRange(32, 13)) message:"range 3 should be {32, 13}; was "+CPStringFromRange(string._rangeEntries[3].range)]; + + var string = [[CPAttributedString alloc] initWithAttributedString:templateString]; + + [string _coalesceRangeEntriesFromIndex:4 toIndex:7]; + + [self assertTrue:[string isEqual:templateString] message:"string should equal template string but does not."]; + + [self assertTrue:CPEqualRanges(string._rangeEntries[0].range, CPMakeRange(0, 4)) message:"range 0 should be {0, 4}; was "+CPStringFromRange(string._rangeEntries[0].range)]; + [self assertTrue:CPEqualRanges(string._rangeEntries[1].range, CPMakeRange(4, 5)) message:"range 1 should be {4, 5}; was "+CPStringFromRange(string._rangeEntries[1].range)]; + [self assertTrue:CPEqualRanges(string._rangeEntries[2].range, CPMakeRange(9, 10)) message:"range 2 should be {9, 10}; was "+CPStringFromRange(string._rangeEntries[2].range)]; + [self assertTrue:CPEqualRanges(string._rangeEntries[3].range, CPMakeRange(19, 1)) message:"range 3 should be {19, 1}; was "+CPStringFromRange(string._rangeEntries[3].range)]; + [self assertTrue:CPEqualRanges(string._rangeEntries[4].range, CPMakeRange(20, 12)) message:"range 4 should be {20, 12}; was "+CPStringFromRange(string._rangeEntries[4].range)]; + [self assertTrue:CPEqualRanges(string._rangeEntries[5].range, CPMakeRange(32, 13)) message:"range 5 should be {32, 13}; was "+CPStringFromRange(string._rangeEntries[5].range)]; +} + +//Changing Attributes +//- (void)setAttributes:(CPDictionary)aDictionary range:(CPRange)aRange +- (void)testSetAttributesRange +{ + var string = [self stringForTesting]; + + [string setAttributes:[CPDictionary dictionary] range:CPMakeRange(0, [string length])]; + + testAttributesAtIndexWithValues(string, 10, {a:undefined, b:undefined, c:undefined, d:undefined, e:undefined, f:undefined}, self); + + string = [self stringForTesting]; + + [string setAttributes:[CPDictionary dictionaryWithObjects:[1, 2, 3, 4, 5] forKeys:["a", "b", "c", "d", "e"]] range:CPMakeRange(0, [string length])]; + + testAttributesAtIndexWithValues(string, 10, {a:1, b:2, c:3, d:4, e:5}, self); + + var range = CPMakeRange(0, 0); + + [string setAttributes:[CPDictionary dictionaryWithObject:"FOO" forKey:"BAR"] range:CPMakeRange(15, 20)]; + + var value = [string attribute:"BAR" atIndex:16 longestEffectiveRange:range inRange:CPMakeRange(0, 45)]; + + [self assertTrue:value==="FOO" message:"expected value to be 'FOO', was: "+value]; + [self assertTrue:CPEqualRanges(range, CPMakeRange(15, 20)) message:"expected key to be valid across {15, 20}, was: "+CPStringFromRange(range)]; + + var index = range.location; + while(index < CPMaxRange(range)) + [self assertTrue:[string attribute:"BAR" atIndex:index++ effectiveRange:nil]=="FOO" message:"incorrect value for key BAR"]; + + string = [self stringForTesting]; + + [string addAttributes:[CPDictionary dictionaryWithObject:"FOO" forKey:"BAR"] range:CPMakeRange(0, 45)]; + + var value = [string attribute:"BAR" atIndex:16 longestEffectiveRange:range inRange:CPMakeRange(0, 45)]; + + [self assertTrue:value==="FOO" message:"expected value to be 'FOO', was: "+value]; + [self assertTrue:CPEqualRanges(range, CPMakeRange(0, 45)) message:"expected key to be valid across {0, 45}, was: "+CPStringFromRange(range)]; + + var index = range.location; + while(index < CPMaxRange(range)) + [self assertTrue:[string attribute:"BAR" atIndex:index++ effectiveRange:nil]=="FOO" message:"incorrect value for key BAR"]; +} + +//- (void)addAttributes:(CPDictionary)aDictionary range:(CPRange)aRange +- (void)testAddAttributesRange +{ + var string = [self stringForTesting], + range = CPMakeRange(0, 0); + + [string addAttributes:[CPDictionary dictionaryWithObject:"FOO" forKey:"BAR"] range:CPMakeRange(15, 20)]; + + var value = [string attribute:"BAR" atIndex:16 longestEffectiveRange:range inRange:CPMakeRange(0, 45)]; + + [self assertTrue:value==="FOO" message:"expected value to be 'FOO', was: "+value]; + [self assertTrue:CPEqualRanges(range, CPMakeRange(15, 20)) message:"expected key to be valid across {15, 20}, was: "+CPStringFromRange(range)]; + + var index = range.location; + while(index < CPMaxRange(range)) + [self assertTrue:[string attribute:"BAR" atIndex:index++ effectiveRange:nil]=="FOO" message:"incorrect value for key BAR"]; + + string = [self stringForTesting]; + + [string addAttributes:[CPDictionary dictionaryWithObjects:[1, 2, 3, 4, 5] forKeys:["a", "b", "c", "d", "e"]] range:CPMakeRange(0, [string length])]; + + testAttributesAtIndexWithValues(string, 10, {a:1, b:2, c:3, d:4, e:5}, self); + + string = [self stringForTesting]; + + [string addAttributes:[CPDictionary dictionaryWithObject:"FOO" forKey:"BAR"] range:CPMakeRange(0, 45)]; + + var value = [string attribute:"BAR" atIndex:16 longestEffectiveRange:range inRange:CPMakeRange(0, 45)]; + + [self assertTrue:value==="FOO" message:"expected value to be 'FOO', was: "+value]; + [self assertTrue:CPEqualRanges(range, CPMakeRange(0, 45)) message:"expected key to be valid across {0, 45}, was: "+CPStringFromRange(range)]; + + var index = range.location; + while(index < CPMaxRange(range)) + [self assertTrue:[string attribute:"BAR" atIndex:index++ effectiveRange:nil]=="FOO" message:"incorrect value for key BAR"]; +} + +//- (void)addAttribute:(CPString)anAttribute value:(id)aValue range:(CPRange)aRange +- (void)testAddAttributeValueRange +{ + var string = [self stringForTesting]; + + [string addAttribute:"duck" value:"goose" range:CPMakeRange(10, 30)]; + + testAttributeAtIndexWithValue(string, 10, "duck", "goose", self); + testAttributeAtIndexWithValue(string, 30, "duck", "goose", self); + testAttributeAtIndexWithValue(string, 40, "duck", undefined, self); + +} + +//- (void)removeAttribute:(CPString)anAttribute range:(CPRange)aRange +- (void)testRemoveAttributeRange +{ + var string = [self stringForTesting]; + + [string removeAttribute:"a" range:CPMakeRange(10, 30)]; + + testAttributeAtIndexWithValue(string, 10, "a", null, self); + testAttributeAtIndexWithValue(string, 30, "a", null, self); + testAttributeAtIndexWithValue(string, 40, "a", 37, self); +} + +//Changing Characters and Attributes +//- (void)appendAttributedString:(CPAttributedString)aString +- (void)testAppendAttributedString +{ + var string = [self stringForTesting], + addOn = [string attributedSubstringFromRange:CPMakeRange(41, 3)]; + + [string appendAttributedString:addOn]; + [string appendAttributedString:addOn]; + + [self assertTrue:[string string]==="The quick brown fox jumped over the lazy dog.dogdog" message:"wrong string after appending"]; + + var range = CPMakeRange(0, 0), + value = [string attribute:"c" atIndex:47 longestEffectiveRange:range inRange:CPMakeRange(0, [string length])]; + + [self assertTrue:value===1 message:"expected value to be '1', was: "+value]; + [self assertTrue:CPEqualRanges(range, CPMakeRange(32, 13+3+3)) message:"expected key to be valid across {32, 19}, was: "+CPStringFromRange(range)]; +} + +//- insertAttributedString:(CPAttributedString)aString atIndex:(CPString)anIndex +- (void)testInsertAttributedStringAtIndex +{ + var string = [self stringForTesting], + addOn = [[self stringForTesting] attributedSubstringFromRange:CPMakeRange(41, 3)]; + + [string insertAttributedString:addOn atIndex:[string length]]; + + testAttributesAtIndexWithValues(string, 46, {a:37, b:"baz", c:1, d:20, e:55, f:43}, self); + + string = [self stringForTesting]; + + addOn = [[self stringForTesting] attributedSubstringFromRange:CPMakeRange(41, 3)]; + + [string insertAttributedString:addOn atIndex:9]; + + testAttributesAtIndexWithValues(string, 9, {a:37, b:"baz", c:1, d:20, e:55, f:43}, self); + + addOn = [[self stringForTesting] attributedSubstringFromRange:CPMakeRange(41, 3)]; + + [string insertAttributedString:addOn atIndex:8]; + + testAttributesAtIndexWithValues(string, 8, {a:37, b:"baz", c:1, d:20, e:55, f:43}, self); + + var range = CPMakeRange(0, 0), + value = [string attribute:"a" atIndex:9 longestEffectiveRange:range inRange:CPMakeRange(0, 45)]; + + [self assertTrue:value===37 message:"expected value to be '37', was: "+value]; + [self assertTrue:CPEqualRanges(range, CPMakeRange(8, 3)) message:"expected key to be valid across {8, 3}, was: "+CPStringFromRange(range)]; + + value = [string attribute:"a" atIndex:11 longestEffectiveRange:range inRange:CPMakeRange(0, 45)]; + + [self assertTrue:value===1 message:"expected value to be '37', was: "+value]; + [self assertTrue:CPEqualRanges(range, CPMakeRange(11, 1)) message:"expected key to be valid across {11, 1}, was: "+CPStringFromRange(range)]; +} + +//- (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString +- (void)testReplaceCharactersInRangeWithAttributedString +{ + var string = [self stringForTesting], + addOn = [string attributedSubstringFromRange:CPMakeRange(41, 3)]; + + [string replaceCharactersInRange:CPMakeRange(41, 3) withAttributedString:addOn]; + + [self assertTrue:[string string]===[[self stringForTesting] string] message:"wrong string after replacing."]; + + var range = CPMakeRange(0, 0), + value = [string attribute:"c" atIndex:42 longestEffectiveRange:range inRange:CPMakeRange(0, [string length])]; + + [self assertTrue:value===1 message:"expected value to be '1', was: "+value]; + [self assertTrue:CPEqualRanges(range, CPMakeRange(32, 13)) message:"expected key to be valid across {32, 13}, was: "+CPStringFromRange(range)]; +} + +//- (void)setAttributedString:(CPAttributedString)aString +- (void)testSetAttributedString +{ + var string = [[CPAttributedString alloc] initWithString:"HELLO THERE"]; + [string setAttributedString:[self stringForTesting]]; + + [self assertTrue:[[self stringForTesting] isEqual:string] message:"setAttributedString should have made strings equal, but they were not"]; +} + +@end + +function testAttributesAtIndexWithValues(aString, anIndex, values, aSelf) +{ + var range = CPMakeRange(0, 0), + attributes = [aString attributesAtIndex:anIndex effectiveRange:range]; + + for(key in values) + [aSelf assertTrue:[attributes objectForKey:key] === values[key] message: "expecting '"+key+"' to be '"+values[key]+"', was '"+[attributes objectForKey:key]]; + + var index = range.location; + while(index < CPMaxRange(range)) + { + attributes = [aString attributesAtIndex:index++ effectiveRange:nil]; + + for(key in values) + [aSelf assertTrue:[attributes objectForKey:key] === values[key] message: "expecting '"+key+"' in loop to be '"+values[key]+"', was '"+[attributes objectForKey:key]]; + } +} + +function testAttributeAtIndexWithValue(aString, anIndex, aKey, aValue, aSelf) +{ + var range = CPMakeRange(0, 0), + attribute = [aString attribute:aKey atIndex:anIndex effectiveRange:range]; + + [aSelf assertTrue: attribute === aValue message: "expecting '"+aKey+"' to be '"+aValue+"', was '"+attribute]; + + var index = range.location; + while(index < CPMaxRange(range)) + { + attribute = [aString attribute:aKey atIndex:index++ effectiveRange:nil]; + + [aSelf assertTrue: attribute === aValue message: "expecting '"+aKey+"' to be '"+aValue+"', was '"+attribute]; + } +} + +function printRangeEntry(entry) +{ + print("range: "+CPStringFromRange(entry.range)+" "+[entry.attributes description]); +} \ No newline at end of file