PageRenderTime 35ms CodeModel.GetById 2ms app.highlight 15ms RepoModel.GetById 2ms app.codeStats 0ms

/Foundation/CPAttributedString.j

http://github.com/cacaodev/cappuccino
Unknown | 927 lines | 770 code | 157 blank | 0 comment | 0 complexity | 7c6e95d39a5a09566a320ba7f53be7c9 MD5 | raw file
  1/*
  2 * CPAttributedString.j
  3 * Foundation
  4 *
  5 * Created by Ross Boucher.
  6 * Copyright 2008, 280 North, Inc.
  7 *
  8 * This library is free software; you can redistribute it and/or
  9 * modify it under the terms of the GNU Lesser General Public
 10 * License as published by the Free Software Foundation; either
 11 * version 2.1 of the License, or (at your option) any later version.
 12 *
 13 * This library is distributed in the hope that it will be useful,
 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 16 * Lesser General Public License for more details.
 17 *
 18 * You should have received a copy of the GNU Lesser General Public
 19 * License along with this library; if not, write to the Free Software
 20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 21 */
 22
 23@import "CPArray.j"
 24@import "CPDictionary.j"
 25@import "CPException.j"
 26@import "CPObject.j"
 27@import "CPRange.j"
 28@import "CPString.j"
 29
 30/*!
 31    @class CPAttributedString
 32    @ingroup foundation
 33    @brief A mutable character string with attributes.
 34
 35    A character string with sets of attributes that apply to single or ranges of
 36    characters. The attributes are contained within a CPDictionary class.
 37    Attributes can be any name/value pair. The data type of the value is not
 38    restricted.
 39    This class is mutable.
 40
 41    @note Cocoa developers: in Cappuccino CPAttributedString is mutable. It
 42    implements functionality from both NSAttributedString and
 43    NSMutableAttributedString. However, to ease converting of existing
 44    Objective-C code a CPMutableAttributedString alias to this class exists.
 45*/
 46@implementation CPAttributedString : CPObject
 47{
 48    CPString    _string;
 49    CPArray     _rangeEntries;
 50}
 51
 52// Creating a CPAttributedString Object
 53/*!
 54    Creates an empty attributed string.
 55    @return a new empty CPAttributedString.
 56*/
 57- (id)init
 58{
 59    return [self initWithString:@"" attributes:nil];
 60}
 61
 62/*!
 63    Creates a new attributed string from a character string.
 64    @param aString is the string to initialise from.
 65    @return a new CPAttributedString containing the string \c aString.
 66*/
 67- (id)initWithString:(CPString)aString
 68{
 69    return [self initWithString:aString attributes:nil];
 70}
 71
 72/*!
 73    Creates a new attributed string from an existing attributed string.
 74    @param aString is the attributed string to initialise from.
 75    @return a new CPAttributedString containing the string \c aString.
 76*/
 77- (id)initWithAttributedString:(CPAttributedString)aString
 78{
 79    var string = [self initWithString:@"" attributes:nil];
 80
 81    [string setAttributedString:aString];
 82
 83    return string;
 84}
 85
 86/*!
 87    Creates a new attributed string from a character string and the specified
 88    dictionary of attributes.
 89    @param aString is the attributed string to initialise from.
 90    @param attributes is a dictionary of string attributes.
 91    @return a new CPAttributedString containing the string \c aString
 92    with associated attributes, \c attributes.
 93*/
 94- (id)initWithString:(CPString)aString attributes:(CPDictionary)attributes
 95{
 96    self = [super init];
 97
 98    if (self)
 99    {
100        if (!attributes)
101            attributes = @{};
102
103        _string = "" + aString;
104        _rangeEntries = [makeRangeEntry(CPMakeRange(0, _string.length), attributes)];
105    }
106
107    return self;
108}
109
110//Retrieving Character Information
111/*!
112    Returns a string containing the receiver's character data without
113    attributes.
114    @return a string of type CPString.
115*/
116- (CPString)string
117{
118    return _string;
119}
120
121/*!
122    Returns a string containing the receiver's character data without
123    attributes.
124    @return a string of type CPString.
125*/
126- (CPString)mutableString
127{
128    return [self string];
129}
130
131/*!
132    Get the length of the string.
133    @return an unsigned integer equivalent to the number of characters in the
134    string.
135*/
136- (unsigned)length
137{
138    return _string.length;
139}
140
141// private method
142- (unsigned)_indexOfEntryWithIndex:(unsigned)anIndex
143{
144    if (anIndex < 0 || anIndex > _string.length || anIndex === undefined)
145        return CPNotFound;
146
147    // find the range entry that contains anIndex.
148    var sortFunction = function(index, entry)
149    {
150        // index is the character index we're searching for,
151        // while range is the actual range entry we're comparing against
152        if (CPLocationInRange(index, entry.range) || (!index && !CPMaxRange(entry.range)))
153            return CPOrderedSame;
154        else if (CPMaxRange(entry.range) <= index)
155            return CPOrderedDescending;
156        else
157            return CPOrderedAscending;
158    };
159
160    return [_rangeEntries indexOfObject:anIndex inSortedRange:nil options:0 usingComparator:sortFunction];
161}
162
163//Retrieving Attribute Information
164/*!
165    Returns a dictionary of attributes for the character at a given index. The
166    range in which this character resides in which the attributes are the
167    same, can be returned if desired.
168    @note there is no guarantee that the range returned is in fact the complete
169    range of the particular attributes. To ensure this use
170    \c attributesAtIndex:longestEffectiveRange:inRange: instead. Note
171    however that it may take significantly longer to execute.
172    @param anIndex is an unsigned integer index. It must lie within the bounds
173    of the string.
174    @param aRange is a reference to a CPRange object
175    that is set (upon return) to the range over which the attributes are the
176    same as those at index, \c anIndex. If not required pass
177    \c nil.
178    @return a CPDictionary containing the attributes associated with the
179    character at index \c anIndex. Returns an empty dictionary if index
180    is out of bounds.
181*/
182- (CPDictionary)attributesAtIndex:(CPUInteger)anIndex effectiveRange:(CPRangePointer)aRange
183{
184    // find the range entry that contains anIndex.
185    var entryIndex = [self _indexOfEntryWithIndex:anIndex];
186
187    if (entryIndex === CPNotFound)
188        return @{};
189
190    var matchingRange = _rangeEntries[entryIndex];
191
192    if (aRange)
193    {
194        aRange.location = matchingRange.range.location;
195        aRange.length = matchingRange.range.length;
196    }
197
198    return matchingRange.attributes;
199}
200
201/*!
202    Returns a dictionary of all attributes for the character at a given index
203    and, by reference, the range over which the attributes apply. This is the
204    maximum range both forwards and backwards in the string over which the
205    attributes apply, bounded in both directions by the range limit parameter,
206    \c rangeLimit.
207    @note this method performs a search to find this range which may be
208    computationally intensive. Use the \c rangeLimit to limit the
209    search space or use \c -attributesAtIndex:effectiveRange: but
210    note that it is not guaranteed to return the full range of the current
211    character's attributes.
212    @param anIndex is the unsigned integer index. It must lie within the bounds
213    of the string.
214    @param aRange is a reference to a CPRange object that is set (upon return)
215    to the range over which the attributes apply.
216    @param rangeLimit a range limiting the search for the attributes' applicable
217    range.
218    @return a CPDictionary containing the attributes associated with the
219    character at index \c anIndex. Returns an empty dictionary if index
220    is out of bounds.
221*/
222- (CPDictionary)attributesAtIndex:(CPUInteger)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
223{
224    var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex];
225
226    if (startingEntryIndex === CPNotFound)
227        return @{};
228
229    if (!aRange)
230        return _rangeEntries[startingEntryIndex].attributes;
231
232    if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit))
233    {
234        aRange.location = rangeLimit.location;
235        aRange.length = rangeLimit.length;
236
237        return _rangeEntries[startingEntryIndex].attributes;
238    }
239
240    // scan backwards
241    var nextRangeIndex = startingEntryIndex - 1,
242        currentEntry = _rangeEntries[startingEntryIndex],
243        comparisonDict = currentEntry.attributes;
244
245    while (nextRangeIndex >= 0)
246    {
247        var nextEntry = _rangeEntries[nextRangeIndex];
248
249        if (CPMaxRange(nextEntry.range) > rangeLimit.location && [nextEntry.attributes isEqualToDictionary:comparisonDict])
250        {
251            currentEntry = nextEntry;
252            nextRangeIndex--;
253        }
254        else
255            break;
256    }
257
258    aRange.location = MAX(currentEntry.range.location, rangeLimit.location);
259
260    // scan forwards
261    currentEntry = _rangeEntries[startingEntryIndex];
262    nextRangeIndex = startingEntryIndex + 1;
263
264    while (nextRangeIndex < _rangeEntries.length)
265    {
266        var nextEntry = _rangeEntries[nextRangeIndex];
267
268        if (nextEntry.range.location < CPMaxRange(rangeLimit) && [nextEntry.attributes isEqualToDictionary:comparisonDict])
269        {
270            currentEntry = nextEntry;
271            nextRangeIndex++;
272        }
273        else
274            break;
275    }
276
277    aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location;
278
279    return comparisonDict;
280}
281
282/*!
283    Returns the specified named attribute for the given character index and, if
284    required, the range over which the attribute applies.
285    @note there is no guarantee that the range returned is in fact the complete
286    range of a particular attribute. To ensure this use
287    \c -attribute:atIndex:longestEffectiveRange:inRange: instead but
288    note that it may take significantly longer to execute.
289    @param attribute the name of the desired attribute.
290    @param anIndex is an unsigned integer character index from which to retrieve
291    the attribute. It must lie within the bounds of the string.
292    @param aRange is a reference to a CPRange object, that is set upon return
293    to the range over which the named attribute applies.  If not required pass
294    \c nil.
295    @return the named attribute or \c nil is the attribute does not
296    exist.
297*/
298- (id)attribute:(CPString)attribute atIndex:(CPUInteger)index effectiveRange:(CPRangePointer)aRange
299{
300    if (!attribute)
301    {
302        if (aRange)
303        {
304            aRange.location = 0;
305            aRange.length = _string.length;
306        }
307
308        return nil;
309    }
310
311    return [[self attributesAtIndex:index effectiveRange:aRange] valueForKey:attribute];
312}
313
314/*!
315    Returns the specified named attribute for the given character index and the
316    range over which the attribute applies. This is the maximum range both
317    forwards and backwards in the string over which the attribute applies,
318    bounded in both directions by the range limit parameter,
319    \c rangeLimit.
320    @note this method performs a search to find this range which may be
321    computationally intensive. Use the \c rangeLimit to limit the
322    search space or use \c -attribute:atIndex:effectiveRange: but
323    note that it is not guaranteed to return the full range of the current
324    character's named attribute.
325    @param attribute the name of the desired attribute.
326    @param anIndex is an unsigned integer character index from which to retrieve
327    the attribute. It must lie within the bounds of the string.
328    @param aRange  is a reference to a CPRange object, that is set upon return
329    to the range over which the named attribute applies.
330    @param rangeLimit a range limiting the search for the attribute's applicable
331    range.
332    @return the named attribute or \c nil is the attribute does not
333    exist.
334*/
335- (id)attribute:(CPString)attribute atIndex:(CPUInteger)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
336{
337    var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex];
338
339    if (startingEntryIndex === CPNotFound || !attribute)
340        return nil;
341
342    if (!aRange)
343        return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute];
344
345    if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit))
346    {
347        aRange.location = rangeLimit.location;
348        aRange.length = rangeLimit.length;
349
350        return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute];
351    }
352
353    // scan backwards
354    var nextRangeIndex = startingEntryIndex - 1,
355        currentEntry = _rangeEntries[startingEntryIndex],
356        comparisonAttribute = [currentEntry.attributes objectForKey:attribute];
357
358    while (nextRangeIndex >= 0)
359    {
360        var nextEntry = _rangeEntries[nextRangeIndex];
361
362        if (CPMaxRange(nextEntry.range) > rangeLimit.location && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute]))
363        {
364            currentEntry = nextEntry;
365            nextRangeIndex--;
366        }
367        else
368            break;
369    }
370
371    aRange.location = MAX(currentEntry.range.location, rangeLimit.location);
372
373    // scan forwards
374    currentEntry = _rangeEntries[startingEntryIndex];
375    nextRangeIndex = startingEntryIndex + 1;
376
377    while (nextRangeIndex < _rangeEntries.length)
378    {
379        var nextEntry = _rangeEntries[nextRangeIndex];
380
381        if (nextEntry.range.location < CPMaxRange(rangeLimit) && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute]))
382        {
383            currentEntry = nextEntry;
384            nextRangeIndex++;
385        }
386        else
387            break;
388    }
389
390    aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location;
391
392    return comparisonAttribute;
393}
394
395//Comparing Attributed Strings
396/*!
397    Compares the receiver's characters and attributes to the specified
398    attributed string, \c aString, and tests for equality.
399    @param aString the CPAttributedString to compare.
400    @return a boolean indicating equality.
401*/
402- (BOOL)isEqualToAttributedString:(CPAttributedString)aString
403{
404    if (!aString)
405        return NO;
406
407    if (_string !== [aString string])
408        return NO;
409
410    var myRange = CPMakeRange(),
411        comparisonRange = CPMakeRange(),
412        myAttributes = [self attributesAtIndex:0 effectiveRange:myRange],
413        comparisonAttributes = [aString attributesAtIndex:0 effectiveRange:comparisonRange],
414        length = _string.length;
415
416    do
417    {
418        if (CPIntersectionRange(myRange, comparisonRange).length > 0 &&
419            ![myAttributes isEqualToDictionary:comparisonAttributes])
420        {
421            return NO;
422        }
423        else if (CPMaxRange(myRange) < CPMaxRange(comparisonRange))
424            myAttributes = [self attributesAtIndex:CPMaxRange(myRange) effectiveRange:myRange];
425        else
426            comparisonAttributes = [aString attributesAtIndex:CPMaxRange(comparisonRange) effectiveRange:comparisonRange];
427    } while (CPMaxRange(CPUnionRange(myRange, comparisonRange)) < length);
428
429    return YES;
430}
431
432/*!
433    Determine whether the given object is the same as the receiver. If the
434    specified object is an attributed string then an attributed string compare
435    is performed.
436    @param anObject an object to test for equality.
437    @return a boolean indicating equality.
438*/
439- (BOOL)isEqual:(id)anObject
440{
441    if (anObject === self)
442        return YES;
443
444    if ([anObject isKindOfClass:[self class]])
445        return [self isEqualToAttributedString:anObject];
446
447    return NO;
448}
449
450//Extracting a Substring
451/*!
452    Extracts a substring from the receiver, both characters and attributes,
453    within the range given by \c aRange.
454    @param aRange the range of the substring to extract.
455    @return a CPAttributedString containing the desired substring.
456    @exception CPRangeException if the range lies outside the receiver's bounds.
457*/
458- (CPAttributedString)attributedSubstringFromRange:(CPRange)aRange
459{
460    if (!aRange || CPMaxRange(aRange) > _string.length || aRange.location < 0)
461        [CPException raise:CPRangeException
462                    reason:"tried to get attributedSubstring for an invalid range: "+(aRange?CPStringFromRange(aRange):"nil")];
463
464    var newString = [[CPAttributedString alloc] initWithString:_string.substring(aRange.location, CPMaxRange(aRange))],
465        entryIndex = [self _indexOfEntryWithIndex:aRange.location];
466
467    if (entryIndex === CPNotFound)
468        _CPRaiseRangeException(self, _cmd, aRange.location, _string.length);
469
470    var currentRangeEntry = _rangeEntries[entryIndex],
471        lastIndex = CPMaxRange(aRange);
472
473    newString._rangeEntries = [];
474
475    while (currentRangeEntry && CPMaxRange(currentRangeEntry.range) < lastIndex)
476    {
477        var newEntry = copyRangeEntry(currentRangeEntry);
478        newEntry.range.location -= aRange.location;
479
480        if (newEntry.range.location < 0)
481        {
482            newEntry.range.length += newEntry.range.location;
483            newEntry.range.location = 0;
484        }
485
486        newString._rangeEntries.push(newEntry);
487        currentRangeEntry = _rangeEntries[++entryIndex];
488    }
489
490    if (currentRangeEntry)
491    {
492        var newRangeEntry = copyRangeEntry(currentRangeEntry);
493
494        newRangeEntry.range.length = CPMaxRange(aRange) - newRangeEntry.range.location;
495        newRangeEntry.range.location -= aRange.location;
496
497        if (newRangeEntry.range.location < 0)
498        {
499            newRangeEntry.range.length += newRangeEntry.range.location;
500            newRangeEntry.range.location = 0;
501        }
502
503        newString._rangeEntries.push(newRangeEntry);
504    }
505
506    return newString;
507}
508
509//Changing Characters
510/*!
511    Replaces the characters in the receiver with those of the specified string
512    over the range, \c aRange. If the range has a length of 0 then
513    the specified string is inserted at the range location. The new characters
514    inherit the attributes of the first character in the range that they
515    replace or in the case if a 0 range length, the first character before of
516    after the insert (after if the insert is at location 0).
517    @note the replacement string need not be the same length as the range
518    being replaced. The full \c aString is inserted and thus the
519    receiver's length changes to match this
520    @param aRange the range of characters to replace.
521    @param aString the string to replace the specified characters in the
522    receiver.
523*/
524- (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString
525{
526    if (!aString)
527        aString = @"";
528
529    var lastValidIndex = MAX(_rangeEntries.length - 1, 0),
530        startingIndex = [self _indexOfEntryWithIndex:aRange.location];
531
532    if (startingIndex < 0)
533        startingIndex = lastValidIndex;
534
535    var endingIndex = [self _indexOfEntryWithIndex:CPMaxRange(aRange)];
536
537    if (endingIndex < 0)
538        endingIndex = lastValidIndex;
539
540    var additionalLength = aString.length - aRange.length,
541        patchPosition = startingIndex;
542
543    _string = _string.substring(0, aRange.location) + aString + _string.substring(CPMaxRange(aRange));
544    var originalLength = _rangeEntries[patchPosition].range.length;
545
546    if (startingIndex === endingIndex)
547        _rangeEntries[patchPosition].range.length += additionalLength;
548    else
549    {
550        if (CPIntersectionRange(_rangeEntries[patchPosition].range, aRange).length < originalLength)
551        {
552            startingIndex++;
553        }
554
555        if (endingIndex > startingIndex)
556        {
557            var originalOffset= _rangeEntries[startingIndex].range.location,
558                offsetFromSplicing = CPMaxRange(_rangeEntries[endingIndex].range) - originalOffset;
559            _rangeEntries.splice(startingIndex, endingIndex - startingIndex);
560            _rangeEntries[startingIndex].range = CPMakeRange(originalOffset, offsetFromSplicing);
561        }
562
563        if (patchPosition !== startingIndex)
564        {
565            var lhsOffset = aString.length - CPIntersectionRange(_rangeEntries[patchPosition].range, aRange).length;
566            _rangeEntries[patchPosition].range.length = originalLength + lhsOffset;
567            var rhsOffset = aString.length - CPIntersectionRange(_rangeEntries[startingIndex].range, aRange).length;
568            _rangeEntries[startingIndex].range.location += lhsOffset;
569            _rangeEntries[startingIndex].range.length += rhsOffset;
570            patchPosition = startingIndex;
571        } else
572            _rangeEntries[patchPosition].range.length += additionalLength;
573    }
574
575    for (var patchIndex = patchPosition + 1, l = _rangeEntries.length; patchIndex < l; patchIndex++)
576        _rangeEntries[patchIndex].range.location += additionalLength;
577}
578
579/*!
580    Deletes a range of characters and their associated attributes.
581    @param aRange a CPRange indicating the range of characters to delete.
582*/
583- (void)deleteCharactersInRange:(CPRange)aRange
584{
585    [self replaceCharactersInRange:aRange withString:nil];
586}
587
588//Changing Attributes
589/*!
590    Sets the attributes of the specified character range.
591
592    @note This process removes the attributes already associated with the
593    character range. If you wish to retain the current attributes use
594    \c -addAttributes:range:.
595    @param aDictionary a CPDictionary of attributes (names and values) to set
596    to.
597    @param aRange a CPRange indicating the range of characters to set their
598    associated attributes to \c aDictionary.
599*/
600- (void)setAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
601{
602    var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
603        endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
604        current = startingEntryIndex;
605
606    if (current < 0)
607        current = MAX(_rangeEntries.length - 1, 0);
608
609    if (endingEntryIndex === CPNotFound)
610        endingEntryIndex = _rangeEntries.length;
611
612    while (current < endingEntryIndex)
613        _rangeEntries[current++].attributes = [aDictionary copy];
614
615    //necessary?
616    [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
617}
618
619/*!
620    Add a collection of attributes to the specified character range.
621
622    @note Attributes currently associated with the characters in the range are
623    untouched. To remove all previous attributes when adding use
624    \c -setAttributes:range:.
625    @param aDictionary a CPDictionary of attributes (names and values) to add.
626    @param aRange a CPRange indicating the range of characters to add the
627    attributes to.
628*/
629- (void)addAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
630{
631    var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
632        endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
633        current = startingEntryIndex;
634
635    if (endingEntryIndex === CPNotFound)
636        endingEntryIndex = _rangeEntries.length;
637
638    while (current < endingEntryIndex)
639    {
640        var keys = [aDictionary allKeys],
641            count = [keys count];
642
643        while (count--)
644            [_rangeEntries[current].attributes setObject:[aDictionary objectForKey:keys[count]] forKey:keys[count]];
645
646        current++;
647    }
648
649    //necessary?
650    [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
651}
652
653/*!
654    Add an attribute with the given name and value to the specified character
655    range.
656
657    @note Attributes currently associated with the characters in the range are
658    untouched. To remove all previous attributes when adding use
659    \c -setAttributes:range:.
660    @param anAttribute a CPString of the attribute name.
661    @param aValue a value to assign to the attribute. Can be of any type.
662    @param aRange a CPRange indicating the range of characters to add the
663    attribute too.
664*/
665- (void)addAttribute:(CPString)anAttribute value:(id)aValue range:(CPRange)aRange
666{
667    [self addAttributes:@{ anAttribute: aValue } range:aRange];
668}
669
670/*!
671    Remove a named attribute from a character range.
672    @param anAttribute a CPString specifying the name of the attribute.
673    @param aRange a CPRange indicating the range of character from which the
674    attribute will be removed.
675*/
676- (void)removeAttribute:(CPString)anAttribute range:(CPRange)aRange
677{
678    var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
679        endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
680        current = startingEntryIndex;
681
682    if (endingEntryIndex === CPNotFound)
683        endingEntryIndex = _rangeEntries.length;
684
685    while (current < endingEntryIndex)
686        [_rangeEntries[current++].attributes removeObjectForKey:anAttribute];
687
688    //necessary?
689    [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
690}
691
692//Changing Characters and Attributes
693/*!
694    Append an attributed string (characters and attributes) on to the end of
695    the receiver.
696    @param aString a CPAttributedString to append.
697*/
698- (void)appendAttributedString:(CPAttributedString)aString
699{
700    [self insertAttributedString:aString atIndex:_string.length];
701}
702
703/*!
704    Inserts an attributed string (characters and attributes) at index,
705    \c anIndex, into the receiver. The portion of the
706    receiver's attributed string from the specified index to the end is shifted
707    until after the inserted string.
708    @param aString a CPAttributedString to insert.
709    @param anIndex the index at which the insert is to occur.
710    @exception CPRangeException If the index is out of bounds.
711*/
712- (void)insertAttributedString:(CPAttributedString)aString atIndex:(CPUInteger)anIndex
713{
714    if (anIndex < 0 || anIndex > [self length])
715        [CPException raise:CPRangeException reason:"tried to insert attributed string at an invalid index: " + anIndex];
716
717    var entryIndexOfNextEntry = [self _indexOfRangeEntryForIndex:anIndex splitOnMaxIndex:YES],
718        otherRangeEntries = aString._rangeEntries,
719        length = [aString length];
720
721    if (entryIndexOfNextEntry === CPNotFound)
722        entryIndexOfNextEntry = _rangeEntries.length;
723
724    _string = _string.substring(0, anIndex) + aString._string + _string.substring(anIndex);
725
726    var current = entryIndexOfNextEntry;
727    while (current < _rangeEntries.length)
728        _rangeEntries[current++].range.location += length;
729
730    var newRangeEntryCount = otherRangeEntries.length,
731        index = 0;
732
733    while (index < newRangeEntryCount)
734    {
735        var entryCopy = copyRangeEntry(otherRangeEntries[index++]);
736        entryCopy.range.location += anIndex;
737
738        _rangeEntries.splice(entryIndexOfNextEntry - 1 + index, 0, entryCopy);
739    }
740
741    [self _coalesceRangeEntriesFromIndex:MAX(0, entryIndexOfNextEntry - 1) toIndex:MIN(entryIndexOfNextEntry + 1, _rangeEntries.length - 1)];
742}
743
744/*!
745    Replaces characters and attributes in the range \c aRange with
746    those of the given attributed string, \c aString.
747    @param aRange a CPRange object specifying the range of characters and
748    attributes in the object to replace.
749    @param aString a CPAttributedString containing the data to be used for
750    replacement.
751*/
752- (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString
753{
754    [self deleteCharactersInRange:aRange];
755    [self insertAttributedString:aString atIndex:aRange.location];
756}
757
758/*!
759    Sets the objects characters and attributes to those of \c aString.
760    @param aString is a CPAttributedString from which the contents will be
761    copied.
762*/
763- (void)setAttributedString:(CPAttributedString)aString
764{
765    _string = aString._string;
766    _rangeEntries = [];
767
768    var i = 0,
769        count = aString._rangeEntries.length;
770
771    for (; i < count; i++)
772        _rangeEntries.push(copyRangeEntry(aString._rangeEntries[i]));
773}
774
775//Private methods
776- (Number)_indexOfRangeEntryForIndex:(unsigned)characterIndex splitOnMaxIndex:(BOOL)split
777{
778    var index = [self _indexOfEntryWithIndex:characterIndex];
779
780    if (index === CPNotFound)
781        return index;
782
783    var rangeEntry = _rangeEntries[index];
784
785    if (rangeEntry.range.location === characterIndex || (CPMaxRange(rangeEntry.range) - 1 === characterIndex && !split))
786        return index;
787
788    var newEntries = splitRangeEntryAtIndex(rangeEntry, characterIndex);
789    _rangeEntries.splice(index, 1, newEntries[0], newEntries[1]);
790    index++;
791
792    return index;
793}
794
795- (void)_coalesceRangeEntriesFromIndex:(unsigned)start toIndex:(unsigned)end
796{
797    var current = start;
798
799    if (end >= _rangeEntries.length)
800        end = _rangeEntries.length - 1;
801
802    while (current < end)
803    {
804        var a = _rangeEntries[current],
805            b = _rangeEntries[current + 1];
806
807        if (a && b && [a.attributes isEqualToDictionary:b.attributes])
808        {
809            a.range.length = CPMaxRange(b.range) - a.range.location;
810            _rangeEntries.splice(current + 1, 1);
811            end--;
812        }
813        else
814            current++;
815    }
816}
817
818//Grouping Changes
819/*!
820    This function is deliberately empty. It is provided to ease code converting
821    from Cocoa.
822*/
823- (void)beginEditing
824{
825    //do nothing (says cocotron and gnustep)
826}
827
828/*!
829    This function is deliberately empty. It is provided to ease code converting
830    from Cocoa.
831*/
832- (void)endEditing
833{
834    //do nothing (says cocotron and gnustep)
835}
836
837@end
838
839var CPAttributedStringStringKey     = "CPAttributedStringString",
840    CPAttributedStringRangesKey     = "CPAttributedStringRanges",
841    CPAttributedStringAttributesKey = "CPAttributedStringAttributes";
842
843@implementation CPAttributedString (CPCoding)
844
845- (id)initWithCoder:(CPCoder)aCoder
846{
847    self = [self init];
848
849    if (self)
850    {
851        _string = [aCoder decodeObjectForKey:CPAttributedStringStringKey];
852        var decodedRanges = [aCoder decodeObjectForKey:CPAttributedStringRangesKey],
853            decodedAttributes = [aCoder decodeObjectForKey:CPAttributedStringAttributesKey];
854
855        _rangeEntries = [];
856
857        for (var i = 0, l = decodedRanges.length; i < l; i++)
858            _rangeEntries.push(makeRangeEntry(decodedRanges[i], decodedAttributes[i]));
859    }
860
861    return self;
862}
863
864- (void)encodeWithCoder:(CPCoder)aCoder
865{
866    [aCoder encodeObject:_string forKey:CPAttributedStringStringKey];
867
868    var rangesForEncoding = [],
869        dictsForEncoding = [];
870
871    for (var i = 0, l = _rangeEntries.length; i < l; i++)
872    {
873        rangesForEncoding.push(_rangeEntries[i].range);
874        dictsForEncoding.push(_rangeEntries[i].attributes);
875    }
876
877    [aCoder encodeObject:rangesForEncoding forKey:CPAttributedStringRangesKey];
878    [aCoder encodeObject:dictsForEncoding forKey:CPAttributedStringAttributesKey];
879}
880
881@end
882
883/*!
884    @class CPMutableAttributedString
885    @ingroup compatibility
886
887    This class is just an empty subclass of CPAttributedString.
888    CPAttributedString already implements mutable methods and
889    this class only exists for source compatibility.
890*/
891@implementation CPMutableAttributedString : CPAttributedString
892
893@end
894
895var isEqual = function(a, b)
896{
897    if (a === b)
898        return YES;
899
900    if ([a respondsToSelector:@selector(isEqual:)] && [a isEqual:b])
901        return YES;
902
903    return NO;
904};
905
906var makeRangeEntry = function(/*CPRange*/aRange, /*CPDictionary*/attributes)
907{
908    return {range:aRange, attributes:[attributes copy]};
909};
910
911var copyRangeEntry = function(/*RangeEntry*/aRangeEntry)
912{
913    return makeRangeEntry(CPMakeRangeCopy(aRangeEntry.range), [aRangeEntry.attributes copy]);
914};
915
916var splitRangeEntryAtIndex = function(/*RangeEntry*/aRangeEntry, /*unsigned*/anIndex)
917{
918    var newRangeEntry = copyRangeEntry(aRangeEntry),
919        cachedIndex = CPMaxRange(aRangeEntry.range);
920
921    aRangeEntry.range.length = anIndex - aRangeEntry.range.location;
922    newRangeEntry.range.location = anIndex;
923    newRangeEntry.range.length = cachedIndex - anIndex;
924    newRangeEntry.attributes = [newRangeEntry.attributes copy];
925
926    return [aRangeEntry, newRangeEntry];
927};