From 8fcf7581e6ee74269dcf41cb77165a5301444d6f Mon Sep 17 00:00:00 2001 From: Ross Boucher Date: Sun, 26 Oct 2008 14:37:54 -0700 Subject: [PATCH 1/5] Starting a test class for attributed string. --- Tests/Foundation/CPAttributedStringTest.j | 449 ++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 Tests/Foundation/CPAttributedStringTest.j diff --git a/Tests/Foundation/CPAttributedStringTest.j b/Tests/Foundation/CPAttributedStringTest.j new file mode 100644 index 000000000..d559c6dd4 --- /dev/null +++ b/Tests/Foundation/CPAttributedStringTest.j @@ -0,0 +1,449 @@ +import "CPAttributedString.j" + +@implementation CPAttributedStringTest : OJTestCase + + +- (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]; + + //add a case where we init with a string that actually adds attributes +} + +- (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:@"bar"] === "foo") + 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]]; +} + +- (unsigned)test_indexOfEntryWithIndex +{ + +} + +//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; + } + + var currentRange = _rangeEntries[startingEntryIndex], + currentRangeIndex = startingEntryIndex, + comparisonDict = currentRange.attributes; + + while (CPMaxRange(currentRange) > rangeLimit.location && [currentRange.attributes isEqualToDictionary:comparisonDict]) + currentRange = _rangeEntries[--currentRangeIndex]; + + aRange.location = currentRange.location; + currentRange = _rangeEntries[startingEntryIndex], + currentRangeIndex = startingEntryIndex; + + while (currentRange.location < CPMaxRange(rangeLimit) && [currentRange.attributes isEqualToDictionary:comparisonDict]) + currentRange = _rangeEntries[++currentRangeIndex]; + + aRange.length = CPMaxRange(currentRange) - aRange.location; + + return currentRange.attributes; +} + +- (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)index 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]; + } + + var currentRange = _rangeEntries[startingEntryIndex], + currentRangeIndex = startingEntryIndex, + comparisonAttribute = [currentRange.attributes objectForKey:attribute]; + + while (CPMaxRange(currentRange) > rangeLimit.location) + { + var thisAttribute = [currentRange.attributes objectForKey:attribute]; + + if (isEqual(comparisonAttribute, thisAttribute)) + currentRange = _rangeEntries[--currentRangeIndex]; + else + break; + } + + aRange.location = currentRange.location; + currentRange = _rangeEntries[startingEntryIndex], + currentRangeIndex = startingEntryIndex; + + while (currentRange.location < CPMaxRange(rangeLimit)) + { + var thisAttribute = [currentRange.attributes objectForKey:attribute]; + + if (isEqual(comparisonAttribute, thisAttribute)) + currentRange = _rangeEntries[++currentRangeIndex]; + else + break; + } + + aRange.length = CPMaxRange(currentRange) - 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); + + while (CPMaxRange(currentRangeEntry.range) < lastIndex); + { + newString._rangeEntries.push(copyRangeEntry(currentRangeEntry)); + currentRangeEntry = _rangeEntries[++entryIndex]; + } + + var newRangeEntry = copyRangeEntry(currentRangeEntry); + newString._rangeEntries.push(newRangeEntry); + + newRangeEntry.range.length = CPMaxRange(aRange) - newRangeEntry.range.location; + + return newString; +} + +//Changing Characters +- (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString +{ + [self beginEditing]; + + if (!aString) + aString = ""; + + var startingIndex = [self _indexOfEntryWithIndex:characterIndex], + startingRangeEntry = _rangeEntries[index], + endingIndex = [self _indexOfEntryWithIndex:CPMaxRange(aRange)], + 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); + } + + [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 splitIfNecessary:YES], + endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitIfNecessary:YES] - 1, + current = startingEntryIndex; + + 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 splitIfNecessary:YES], + endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitIfNecessary:YES] - 1, + current = startingEntryIndex; + + while (current < endingEntryIndex) + { + var keys = [aDictionary keys], + 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 beginEditing]; + + var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitIfNecessary:YES], + endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitIfNecessary:YES] - 1, + current = startingEntryIndex; + + while (current < endingEntryIndex) + [_rangeEntries[current++].attributes setObject:aValue forKey:anAttribute]; + + //necessary? + //[self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex]; + + [self endEditing]; +} + +- (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]; +} + +- insertAttributedString:(CPAttributedString)aString atIndex:(CPString)anIndex +{ + [self beginEditing]; + + var startingEntryIndex = [self _indexOfRangeEntryForIndex:anIndex splitIfNecessary:YES], + rangeEntries = aString._rangeEntries, + count = rangeEntries.length; + + _string = _string.substring(0, anIndex) + aString._string + _string.substring(anIndex); + + while (count--) + { + var newEntry = copyRangeEntry(rangeEntries[count]); + + newEntry.range.location += anIndex; + + _rangeEntries.splice(startingEntryIndex, 0, newEntry); + } + + //necessary? + //[self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex]; + + [self endEditing]; +} + +- (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString +{ + [self beginEditing]; + + [self deleteCharactersInRange:aRange]; + [self insertAttributeString:aString atIndex:aRange.location]; + + [self endEditing]; +} + +- (void)setAttributedString:(CPAttributedString)aString +{ + [self beginEditing]; + + _string = aString; + _rangeEntries = []; + + for (var i=0, count = aString._rangeEntries.length; i Date: Sun, 26 Oct 2008 23:06:23 -0700 Subject: [PATCH 2/5] Fix binary search in CPArray to allow searching for 0. --- Foundation/CPArray.j | 4 +- Tests/Foundation/CPAttributedStringTest.j | 449 ---------------------- 2 files changed, 2 insertions(+), 451 deletions(-) delete mode 100644 Tests/Foundation/CPAttributedStringTest.j diff --git a/Foundation/CPArray.j b/Foundation/CPArray.j index 90942d1a6..b486b5a1a 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/Tests/Foundation/CPAttributedStringTest.j b/Tests/Foundation/CPAttributedStringTest.j deleted file mode 100644 index d559c6dd4..000000000 --- a/Tests/Foundation/CPAttributedStringTest.j +++ /dev/null @@ -1,449 +0,0 @@ -import "CPAttributedString.j" - -@implementation CPAttributedStringTest : OJTestCase - - -- (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]; - - //add a case where we init with a string that actually adds attributes -} - -- (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:@"bar"] === "foo") - 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]]; -} - -- (unsigned)test_indexOfEntryWithIndex -{ - -} - -//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; - } - - var currentRange = _rangeEntries[startingEntryIndex], - currentRangeIndex = startingEntryIndex, - comparisonDict = currentRange.attributes; - - while (CPMaxRange(currentRange) > rangeLimit.location && [currentRange.attributes isEqualToDictionary:comparisonDict]) - currentRange = _rangeEntries[--currentRangeIndex]; - - aRange.location = currentRange.location; - currentRange = _rangeEntries[startingEntryIndex], - currentRangeIndex = startingEntryIndex; - - while (currentRange.location < CPMaxRange(rangeLimit) && [currentRange.attributes isEqualToDictionary:comparisonDict]) - currentRange = _rangeEntries[++currentRangeIndex]; - - aRange.length = CPMaxRange(currentRange) - aRange.location; - - return currentRange.attributes; -} - -- (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)index 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]; - } - - var currentRange = _rangeEntries[startingEntryIndex], - currentRangeIndex = startingEntryIndex, - comparisonAttribute = [currentRange.attributes objectForKey:attribute]; - - while (CPMaxRange(currentRange) > rangeLimit.location) - { - var thisAttribute = [currentRange.attributes objectForKey:attribute]; - - if (isEqual(comparisonAttribute, thisAttribute)) - currentRange = _rangeEntries[--currentRangeIndex]; - else - break; - } - - aRange.location = currentRange.location; - currentRange = _rangeEntries[startingEntryIndex], - currentRangeIndex = startingEntryIndex; - - while (currentRange.location < CPMaxRange(rangeLimit)) - { - var thisAttribute = [currentRange.attributes objectForKey:attribute]; - - if (isEqual(comparisonAttribute, thisAttribute)) - currentRange = _rangeEntries[++currentRangeIndex]; - else - break; - } - - aRange.length = CPMaxRange(currentRange) - 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); - - while (CPMaxRange(currentRangeEntry.range) < lastIndex); - { - newString._rangeEntries.push(copyRangeEntry(currentRangeEntry)); - currentRangeEntry = _rangeEntries[++entryIndex]; - } - - var newRangeEntry = copyRangeEntry(currentRangeEntry); - newString._rangeEntries.push(newRangeEntry); - - newRangeEntry.range.length = CPMaxRange(aRange) - newRangeEntry.range.location; - - return newString; -} - -//Changing Characters -- (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString -{ - [self beginEditing]; - - if (!aString) - aString = ""; - - var startingIndex = [self _indexOfEntryWithIndex:characterIndex], - startingRangeEntry = _rangeEntries[index], - endingIndex = [self _indexOfEntryWithIndex:CPMaxRange(aRange)], - 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); - } - - [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 splitIfNecessary:YES], - endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitIfNecessary:YES] - 1, - current = startingEntryIndex; - - 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 splitIfNecessary:YES], - endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitIfNecessary:YES] - 1, - current = startingEntryIndex; - - while (current < endingEntryIndex) - { - var keys = [aDictionary keys], - 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 beginEditing]; - - var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitIfNecessary:YES], - endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitIfNecessary:YES] - 1, - current = startingEntryIndex; - - while (current < endingEntryIndex) - [_rangeEntries[current++].attributes setObject:aValue forKey:anAttribute]; - - //necessary? - //[self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex]; - - [self endEditing]; -} - -- (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]; -} - -- insertAttributedString:(CPAttributedString)aString atIndex:(CPString)anIndex -{ - [self beginEditing]; - - var startingEntryIndex = [self _indexOfRangeEntryForIndex:anIndex splitIfNecessary:YES], - rangeEntries = aString._rangeEntries, - count = rangeEntries.length; - - _string = _string.substring(0, anIndex) + aString._string + _string.substring(anIndex); - - while (count--) - { - var newEntry = copyRangeEntry(rangeEntries[count]); - - newEntry.range.location += anIndex; - - _rangeEntries.splice(startingEntryIndex, 0, newEntry); - } - - //necessary? - //[self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex]; - - [self endEditing]; -} - -- (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString -{ - [self beginEditing]; - - [self deleteCharactersInRange:aRange]; - [self insertAttributeString:aString atIndex:aRange.location]; - - [self endEditing]; -} - -- (void)setAttributedString:(CPAttributedString)aString -{ - [self beginEditing]; - - _string = aString; - _rangeEntries = []; - - for (var i=0, count = aString._rangeEntries.length; i Date: Mon, 27 Oct 2008 19:27:37 -0700 Subject: [PATCH 3/5] Make isEqual: more like Cocoa's version. --- Foundation/CPObject.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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]; } /*! From 102c741ca38d6a541499aae30c61c969c6f1cfd9 Mon Sep 17 00:00:00 2001 From: Ross Boucher Date: Thu, 13 Nov 2008 11:23:00 -0800 Subject: [PATCH 4/5] Initial commit of CPAttributedString, and test cases. All test cases currently pass. --- Foundation/CPAttributedString.j | 593 ++++++++++++++++++++++ Tests/Foundation/CPAttributedStringTest.j | 593 ++++++++++++++++++++++ 2 files changed, 1186 insertions(+) create mode 100644 Foundation/CPAttributedString.j create mode 100644 Tests/Foundation/CPAttributedStringTest.j diff --git a/Foundation/CPAttributedString.j b/Foundation/CPAttributedString.j new file mode 100644 index 000000000..5a24d0ce1 --- /dev/null +++ b/Foundation/CPAttributedString.j @@ -0,0 +1,593 @@ +/* + * 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 + +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/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 From d64e55226d253006443a021a403f00c8ccd3b3e0 Mon Sep 17 00:00:00 2001 From: Ross Boucher Date: Thu, 13 Nov 2008 19:33:48 -0800 Subject: [PATCH 5/5] Add CPMutableAttributedString --- Foundation/CPAttributedString.j | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Foundation/CPAttributedString.j b/Foundation/CPAttributedString.j index 5a24d0ce1..7c0a14055 100644 --- a/Foundation/CPAttributedString.j +++ b/Foundation/CPAttributedString.j @@ -558,6 +558,9 @@ import @end +@implementation CPMutableAttributedString : CPAttributedString {} +@end + var isEqual = function isEqual(a, b) { if (a == b)