![]() |
API 0.9.5
|
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 }