API 0.9.5
Foundation/CPAttributedString.j
Go to the documentation of this file.
00001 /*
00002  * CPAttributedString.j
00003  * Foundation
00004  *
00005  * Created by Ross Boucher.
00006  * Copyright 2008, 280 North, Inc.
00007  *
00008  * This library is free software; you can redistribute it and/or
00009  * modify it under the terms of the GNU Lesser General Public
00010  * License as published by the Free Software Foundation; either
00011  * version 2.1 of the License, or (at your option) any later version.
00012  *
00013  * This library is distributed in the hope that it will be useful,
00014  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00015  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
00016  * Lesser General Public License for more details.
00017  *
00018  * You should have received a copy of the GNU Lesser General Public
00019  * License along with this library; if not, write to the Free Software
00020  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
00021  */
00022 
00023 
00040 @implementation CPAttributedString : CPObject
00041 {
00042     CPString    _string;
00043     CPArray     _rangeEntries;
00044 }
00045 
00046 // Creating a CPAttributedString Object
00052 - (id)initWithString:(CPString)aString
00053 {
00054     return [self initWithString:aString attributes:nil];
00055 }
00056 
00062 - (id)initWithAttributedString:(CPAttributedString)aString
00063 {
00064     var string = [self initWithString:"" attributes:nil];
00065 
00066     [string setAttributedString:aString];
00067 
00068     return string;
00069 }
00070 
00079 - (id)initWithString:(CPString)aString attributes:(CPDictionary)attributes
00080 {
00081     self = [super init];
00082 
00083     if (self)
00084     {
00085         if (!attributes)
00086             attributes = [CPDictionary dictionary];
00087 
00088         _string = ""+aString;
00089         _rangeEntries = [makeRangeEntry(CPMakeRange(0, _string.length), attributes)];
00090     }
00091 
00092     return self;
00093 }
00094 
00095 //Retrieving Character Information
00101 - (CPString)string
00102 {
00103     return _string;
00104 }
00105 
00111 - (CPString)mutableString
00112 {
00113     return [self string];
00114 }
00115 
00121 - (unsigned)length
00122 {
00123     return _string.length;
00124 }
00125 
00126 // private method
00127 - (unsigned)_indexOfEntryWithIndex:(unsigned)anIndex
00128 {
00129     if (anIndex < 0 || anIndex > _string.length || anIndex === undefined)
00130         return CPNotFound;
00131 
00132     //find the range entry that contains anIndex.
00133     var sortFunction = function(index, entry)
00134     {
00135         //index is the character index we're searching for, while range is the actual range entry we're comparing against
00136         if (CPLocationInRange(index, entry.range))
00137             return CPOrderedSame;
00138         else if (CPMaxRange(entry.range) <= index)
00139             return CPOrderedDescending;
00140         else
00141             return CPOrderedAscending;
00142     }
00143 
00144     return [_rangeEntries indexOfObject:anIndex inSortedRange:nil options:0 usingComparator:sortFunction];
00145 }
00146 
00147 //Retrieving Attribute Information
00166 - (CPDictionary)attributesAtIndex:(unsigned)anIndex effectiveRange:(CPRangePointer)aRange
00167 {
00168     //find the range entry that contains anIndex.
00169     var entryIndex = [self _indexOfEntryWithIndex:anIndex];
00170 
00171     if (entryIndex == CPNotFound)
00172         return nil;
00173 
00174     var matchingRange = _rangeEntries[entryIndex];
00175     if (aRange)
00176     {
00177         aRange.location = matchingRange.range.location;
00178         aRange.length = matchingRange.range.length;
00179     }
00180 
00181     return matchingRange.attributes;
00182 }
00183 
00205 - (CPDictionary)attributesAtIndex:(unsigned)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
00206 {
00207     var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex];
00208 
00209     if (startingEntryIndex == CPNotFound)
00210         return nil;
00211 
00212     if (!aRange)
00213         return _rangeEntries[startingEntryIndex].attributes;
00214 
00215     if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit))
00216     {
00217         aRange.location = rangeLimit.location;
00218         aRange.length = rangeLimit.length;
00219 
00220         return _rangeEntries[startingEntryIndex].attributes;
00221     }
00222 
00223     //scan backwards
00224     var nextRangeIndex = startingEntryIndex - 1,
00225         currentEntry = _rangeEntries[startingEntryIndex],
00226         comparisonDict = currentEntry.attributes;
00227 
00228     while (nextRangeIndex >= 0)
00229     {
00230         var nextEntry = _rangeEntries[nextRangeIndex];
00231 
00232         if (CPMaxRange(nextEntry.range) > rangeLimit.location && [nextEntry.attributes isEqualToDictionary:comparisonDict])
00233         {
00234             currentEntry = nextEntry;
00235             nextRangeIndex--;
00236         }
00237         else
00238             break;
00239     }
00240 
00241     aRange.location = MAX(currentEntry.range.location, rangeLimit.location);
00242 
00243     //scan forwards
00244     currentEntry = _rangeEntries[startingEntryIndex];
00245     nextRangeIndex = startingEntryIndex + 1;
00246 
00247     while (nextRangeIndex < _rangeEntries.length)
00248     {
00249         var nextEntry = _rangeEntries[nextRangeIndex];
00250 
00251         if (nextEntry.range.location < CPMaxRange(rangeLimit) && [nextEntry.attributes isEqualToDictionary:comparisonDict])
00252         {
00253             currentEntry = nextEntry;
00254             nextRangeIndex++;
00255         }
00256         else
00257             break;
00258     }
00259 
00260     aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location;
00261 
00262     return comparisonDict;
00263 }
00264 
00281 - (id)attribute:(CPString)attribute atIndex:(unsigned)index effectiveRange:(CPRangePointer)aRange
00282 {
00283     if (!attribute)
00284     {
00285         if (aRange)
00286         {
00287             aRange.location = 0;
00288             aRange.length = _string.length;
00289         }
00290 
00291         return nil;
00292     }
00293 
00294     return [[self attributesAtIndex:index effectiveRange:aRange] valueForKey:attribute];
00295 }
00296 
00318 - (id)attribute:(CPString)attribute atIndex:(unsigned)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
00319 {
00320     //find the range entry that contains anIndex.
00321     var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex];
00322 
00323     if (startingEntryIndex == CPNotFound || !attribute)
00324         return nil;
00325 
00326     if (!aRange)
00327         return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute];
00328 
00329     if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit))
00330     {
00331         aRange.location = rangeLimit.location;
00332         aRange.length = rangeLimit.length;
00333 
00334         return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute];
00335     }
00336 
00337     //scan backwards
00338     var nextRangeIndex = startingEntryIndex - 1,
00339         currentEntry = _rangeEntries[startingEntryIndex],
00340         comparisonAttribute = [currentEntry.attributes objectForKey:attribute];
00341 
00342     while (nextRangeIndex >= 0)
00343     {
00344         var nextEntry = _rangeEntries[nextRangeIndex];
00345 
00346         if (CPMaxRange(nextEntry.range) > rangeLimit.location && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute]))
00347         {
00348             currentEntry = nextEntry;
00349             nextRangeIndex--;
00350         }
00351         else
00352             break;
00353     }
00354 
00355     aRange.location = MAX(currentEntry.range.location, rangeLimit.location);
00356 
00357     //scan forwards
00358     currentEntry = _rangeEntries[startingEntryIndex];
00359     nextRangeIndex = startingEntryIndex + 1;
00360 
00361     while (nextRangeIndex < _rangeEntries.length)
00362     {
00363         var nextEntry = _rangeEntries[nextRangeIndex];
00364 
00365         if (nextEntry.range.location < CPMaxRange(rangeLimit) && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute]))
00366         {
00367             currentEntry = nextEntry;
00368             nextRangeIndex++;
00369         }
00370         else
00371             break;
00372     }
00373 
00374     aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location;
00375 
00376     return comparisonAttribute;
00377 }
00378 
00379 //Comparing Attributed Strings
00386 - (BOOL)isEqualToAttributedString:(CPAttributedString)aString
00387 {
00388     if (!aString)
00389         return NO;
00390 
00391     if (_string != [aString string])
00392         return NO;
00393 
00394     var myRange = CPMakeRange(),
00395         comparisonRange = CPMakeRange(),
00396         myAttributes = [self attributesAtIndex:0 effectiveRange:myRange],
00397         comparisonAttributes = [aString attributesAtIndex:0 effectiveRange:comparisonRange],
00398         length = _string.length;
00399 
00400     while (CPMaxRange(CPUnionRange(myRange, comparisonRange)) < length)
00401     {
00402         if (CPIntersectionRange(myRange, comparisonRange).length > 0 && ![myAttributes isEqualToDictionary:comparisonAttributes])
00403             return NO;
00404         if (CPMaxRange(myRange) < CPMaxRange(comparisonRange))
00405             myAttributes = [self attributesAtIndex:CPMaxRange(myRange) effectiveRange:myRange];
00406         else
00407             comparisonAttributes = [aString attributesAtIndex:CPMaxRange(comparisonRange) effectiveRange:comparisonRange];
00408     }
00409 
00410     return YES;
00411 }
00412 
00420 - (BOOL)isEqual:(id)anObject
00421 {
00422     if (anObject == self)
00423         return YES;
00424 
00425     if ([anObject isKindOfClass:[self class]])
00426         return [self isEqualToAttributedString:anObject];
00427 
00428     return NO;
00429 }
00430 
00431 //Extracting a Substring
00439 - (CPAttributedString)attributedSubstringFromRange:(CPRange)aRange
00440 {
00441     if (!aRange || CPMaxRange(aRange) > _string.length || aRange.location < 0)
00442         [CPException raise:CPRangeException
00443                     reason:"tried to get attributedSubstring for an invalid range: "+(aRange?CPStringFromRange(aRange):"nil")];
00444 
00445     var newString = [[CPAttributedString alloc] initWithString:_string.substring(aRange.location, CPMaxRange(aRange))],
00446         entryIndex = [self _indexOfEntryWithIndex:aRange.location],
00447         currentRangeEntry = _rangeEntries[entryIndex],
00448         lastIndex = CPMaxRange(aRange);
00449 
00450     newString._rangeEntries = [];
00451 
00452     while (currentRangeEntry && CPMaxRange(currentRangeEntry.range) < lastIndex)
00453     {
00454         var newEntry = copyRangeEntry(currentRangeEntry);
00455         newEntry.range.location -= aRange.location;
00456 
00457         if (newEntry.range.location < 0)
00458         {
00459             newEntry.range.length += newEntry.range.location;
00460             newEntry.range.location = 0;
00461         }
00462 
00463         newString._rangeEntries.push(newEntry);
00464         currentRangeEntry = _rangeEntries[++entryIndex];
00465     }
00466 
00467     if (currentRangeEntry)
00468     {
00469         var newRangeEntry = copyRangeEntry(currentRangeEntry);
00470 
00471         newRangeEntry.range.length = CPMaxRange(aRange) - newRangeEntry.range.location;
00472         newRangeEntry.range.location -= aRange.location;
00473 
00474         if (newRangeEntry.range.location < 0)
00475         {
00476             newRangeEntry.range.length += newRangeEntry.range.location;
00477             newRangeEntry.range.location = 0;
00478         }
00479 
00480         newString._rangeEntries.push(newRangeEntry);
00481     }
00482 
00483     return newString;
00484 }
00485 
00486 //Changing Characters
00501 - (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString
00502 {
00503     if (!aString)
00504         aString = "";
00505 
00506     var startingIndex = [self _indexOfEntryWithIndex:aRange.location],
00507         startingRangeEntry = _rangeEntries[startingIndex],
00508         endingIndex = [self _indexOfEntryWithIndex:MAX(CPMaxRange(aRange) - 1, 0)],
00509         endingRangeEntry = _rangeEntries[endingIndex],
00510         additionalLength = aString.length - aRange.length;
00511 
00512     _string = _string.substring(0, aRange.location) + aString + _string.substring(CPMaxRange(aRange));
00513 
00514     if (startingIndex == endingIndex)
00515         startingRangeEntry.range.length += additionalLength;
00516     else
00517     {
00518         endingRangeEntry.range.length = CPMaxRange(endingRangeEntry.range) - CPMaxRange(aRange);
00519         endingRangeEntry.range.location = CPMaxRange(aRange);
00520 
00521         startingRangeEntry.range.length = CPMaxRange(aRange) - startingRangeEntry.range.location;
00522 
00523         _rangeEntries.splice(startingIndex, endingIndex - startingIndex);
00524     }
00525 
00526     endingIndex = startingIndex + 1;
00527 
00528     while (endingIndex < _rangeEntries.length)
00529         _rangeEntries[endingIndex++].range.location += additionalLength;
00530 }
00531 
00536 - (void)deleteCharactersInRange:(CPRange)aRange
00537 {
00538     [self replaceCharactersInRange:aRange withString:nil];
00539 }
00540 
00541 //Changing Attributes
00553 - (void)setAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
00554 {
00555     var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
00556         endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
00557         current = startingEntryIndex;
00558 
00559     if (endingEntryIndex == CPNotFound)
00560         endingEntryIndex = _rangeEntries.length;
00561 
00562     while (current < endingEntryIndex)
00563         _rangeEntries[current++].attributes = [aDictionary copy];
00564 
00565     //necessary?
00566     [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
00567 }
00568 
00579 - (void)addAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
00580 {
00581     var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
00582         endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
00583         current = startingEntryIndex;
00584 
00585     if (endingEntryIndex == CPNotFound)
00586         endingEntryIndex = _rangeEntries.length;
00587 
00588     while (current < endingEntryIndex)
00589     {
00590         var keys = [aDictionary allKeys],
00591             count = [keys count];
00592 
00593         while (count--)
00594             [_rangeEntries[current].attributes setObject:[aDictionary objectForKey:keys[count]] forKey:keys[count]];
00595 
00596         current++;
00597     }
00598 
00599     //necessary?
00600     [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
00601 }
00602 
00615 - (void)addAttribute:(CPString)anAttribute value:(id)aValue range:(CPRange)aRange
00616 {
00617     [self addAttributes:[CPDictionary dictionaryWithObject:aValue forKey:anAttribute] range:aRange];
00618 }
00619 
00626 - (void)removeAttribute:(CPString)anAttribute range:(CPRange)aRange
00627 {
00628     var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
00629         endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
00630         current = startingEntryIndex;
00631 
00632     if (endingEntryIndex == CPNotFound)
00633         endingEntryIndex = _rangeEntries.length;
00634 
00635     while (current < endingEntryIndex)
00636         [_rangeEntries[current++].attributes removeObjectForKey:anAttribute];
00637 
00638     //necessary?
00639     [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
00640 }
00641 
00642 //Changing Characters and Attributes
00648 - (void)appendAttributedString:(CPAttributedString)aString
00649 {
00650     [self insertAttributedString:aString atIndex:_string.length];
00651 }
00652 
00662 - (void)insertAttributedString:(CPAttributedString)aString atIndex:(unsigned)anIndex
00663 {
00664     if (anIndex < 0 || anIndex > [self length])
00665         [CPException raise:CPRangeException reason:"tried to insert attributed string at an invalid index: "+anIndex];
00666 
00667     var entryIndexOfNextEntry = [self _indexOfRangeEntryForIndex:anIndex splitOnMaxIndex:YES],
00668         otherRangeEntries = aString._rangeEntries,
00669         length = [aString length];
00670 
00671     if (entryIndexOfNextEntry == CPNotFound)
00672         entryIndexOfNextEntry = _rangeEntries.length;
00673 
00674     _string = _string.substring(0, anIndex) + aString._string + _string.substring(anIndex);
00675 
00676     var current = entryIndexOfNextEntry;
00677     while (current < _rangeEntries.length)
00678         _rangeEntries[current++].range.location += length;
00679 
00680     var newRangeEntryCount = otherRangeEntries.length,
00681         index = 0;
00682 
00683     while (index < newRangeEntryCount)
00684     {
00685         var entryCopy = copyRangeEntry(otherRangeEntries[index++]);
00686         entryCopy.range.location += anIndex;
00687 
00688         _rangeEntries.splice(entryIndexOfNextEntry - 1 + index, 0, entryCopy);
00689     }
00690 
00691     //necessary?
00692     //[self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:startingEntryIndex+rangeEntries.length];
00693 }
00694 
00703 - (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString
00704 {
00705     [self deleteCharactersInRange:aRange];
00706     [self insertAttributedString:aString atIndex:aRange.location];
00707 }
00708 
00714 - (void)setAttributedString:(CPAttributedString)aString
00715 {
00716     _string = aString._string;
00717     _rangeEntries = [];
00718 
00719     var i = 0,
00720         count = aString._rangeEntries.length;
00721 
00722     for (; i < count; i++)
00723         _rangeEntries.push(copyRangeEntry(aString._rangeEntries[i]));
00724 }
00725 
00726 //Private methods
00727 - (Number)_indexOfRangeEntryForIndex:(unsigned)characterIndex splitOnMaxIndex:(BOOL)split
00728 {
00729     var index = [self _indexOfEntryWithIndex:characterIndex];
00730 
00731     if (index < 0)
00732         return index;
00733 
00734     var rangeEntry = _rangeEntries[index];
00735 
00736     if (rangeEntry.range.location == characterIndex || (CPMaxRange(rangeEntry.range) - 1 == characterIndex && !split))
00737         return index;
00738 
00739     var newEntries = splitRangeEntryAtIndex(rangeEntry, characterIndex);
00740     _rangeEntries.splice(index, 1, newEntries[0], newEntries[1]);
00741     index++;
00742 
00743     return index;
00744 }
00745 
00746 - (void)_coalesceRangeEntriesFromIndex:(unsigned)start toIndex:(unsigned)end
00747 {
00748     var current = start;
00749 
00750     if (end >= _rangeEntries.length)
00751         end = _rangeEntries.length - 1;
00752 
00753     while (current < end)
00754     {
00755         var a = _rangeEntries[current],
00756             b = _rangeEntries[current + 1];
00757 
00758         if ([a.attributes isEqualToDictionary:b.attributes])
00759         {
00760             a.range.length = CPMaxRange(b.range) - a.range.location;
00761             _rangeEntries.splice(current + 1, 1);
00762             end--;
00763         }
00764         else
00765             current++;
00766     }
00767 }
00768 
00769 //Grouping Changes
00774 - (void)beginEditing
00775 {
00776     //do nothing (says cocotron and gnustep)
00777 }
00778 
00783 - (void)endEditing
00784 {
00785     //do nothing (says cocotron and gnustep)
00786 }
00787 
00788 @end
00789 
00798 @implementation CPMutableAttributedString : CPAttributedString
00799 {
00800     id __doxygen__;
00801 }
00802 
00803 @end
00804 
00805 var isEqual = function isEqual(a, b)
00806 {
00807     if (a == b)
00808         return YES;
00809 
00810     if ([a respondsToSelector:@selector(isEqual:)] && [a isEqual:b])
00811         return YES;
00812 
00813     return NO;
00814 }
00815 
00816 var makeRangeEntry = function makeRangeEntry(/*CPRange*/aRange, /*CPDictionary*/attributes)
00817 {
00818     return {range:aRange, attributes:[attributes copy]};
00819 }
00820 
00821 var copyRangeEntry = function copyRangeEntry(/*RangeEntry*/aRangeEntry)
00822 {
00823     return makeRangeEntry(CPCopyRange(aRangeEntry.range), [aRangeEntry.attributes copy]);
00824 }
00825 
00826 var splitRangeEntry = function splitRangeEntryAtIndex(/*RangeEntry*/aRangeEntry, /*unsigned*/anIndex)
00827 {
00828     var newRangeEntry = copyRangeEntry(aRangeEntry),
00829         cachedIndex = CPMaxRange(aRangeEntry.range);
00830 
00831     aRangeEntry.range.length = anIndex - aRangeEntry.range.location;
00832     newRangeEntry.range.location = anIndex;
00833     newRangeEntry.range.length = cachedIndex - anIndex;
00834     newRangeEntry.attributes = [newRangeEntry.attributes copy];
00835 
00836     return [aRangeEntry, newRangeEntry];
00837 }
 All Classes Files Functions Variables Defines