/Foundation/CPAttributedString.j
http://github.com/cacaodev/cappuccino · Unknown · 927 lines · 770 code · 157 blank · 0 comment · 0 complexity · 7c6e95d39a5a09566a320ba7f53be7c9 MD5 · raw file
- /*
- * 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 "CPArray.j"
- @import "CPDictionary.j"
- @import "CPException.j"
- @import "CPObject.j"
- @import "CPRange.j"
- @import "CPString.j"
- /*!
- @class CPAttributedString
- @ingroup foundation
- @brief A mutable character string with attributes.
- A character string with sets of attributes that apply to single or ranges of
- characters. The attributes are contained within a CPDictionary class.
- Attributes can be any name/value pair. The data type of the value is not
- restricted.
- This class is mutable.
- @note Cocoa developers: in Cappuccino CPAttributedString is mutable. It
- implements functionality from both NSAttributedString and
- NSMutableAttributedString. However, to ease converting of existing
- Objective-C code a CPMutableAttributedString alias to this class exists.
- */
- @implementation CPAttributedString : CPObject
- {
- CPString _string;
- CPArray _rangeEntries;
- }
- // Creating a CPAttributedString Object
- /*!
- Creates an empty attributed string.
- @return a new empty CPAttributedString.
- */
- - (id)init
- {
- return [self initWithString:@"" attributes:nil];
- }
- /*!
- Creates a new attributed string from a character string.
- @param aString is the string to initialise from.
- @return a new CPAttributedString containing the string \c aString.
- */
- - (id)initWithString:(CPString)aString
- {
- return [self initWithString:aString attributes:nil];
- }
- /*!
- Creates a new attributed string from an existing attributed string.
- @param aString is the attributed string to initialise from.
- @return a new CPAttributedString containing the string \c aString.
- */
- - (id)initWithAttributedString:(CPAttributedString)aString
- {
- var string = [self initWithString:@"" attributes:nil];
- [string setAttributedString:aString];
- return string;
- }
- /*!
- Creates a new attributed string from a character string and the specified
- dictionary of attributes.
- @param aString is the attributed string to initialise from.
- @param attributes is a dictionary of string attributes.
- @return a new CPAttributedString containing the string \c aString
- with associated attributes, \c attributes.
- */
- - (id)initWithString:(CPString)aString attributes:(CPDictionary)attributes
- {
- self = [super init];
- if (self)
- {
- if (!attributes)
- attributes = @{};
- _string = "" + aString;
- _rangeEntries = [makeRangeEntry(CPMakeRange(0, _string.length), attributes)];
- }
- return self;
- }
- //Retrieving Character Information
- /*!
- Returns a string containing the receiver's character data without
- attributes.
- @return a string of type CPString.
- */
- - (CPString)string
- {
- return _string;
- }
- /*!
- Returns a string containing the receiver's character data without
- attributes.
- @return a string of type CPString.
- */
- - (CPString)mutableString
- {
- return [self string];
- }
- /*!
- Get the length of the string.
- @return an unsigned integer equivalent to the number of characters in the
- string.
- */
- - (unsigned)length
- {
- return _string.length;
- }
- // private method
- - (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) || (!index && !CPMaxRange(entry.range)))
- return CPOrderedSame;
- else if (CPMaxRange(entry.range) <= index)
- return CPOrderedDescending;
- else
- return CPOrderedAscending;
- };
- return [_rangeEntries indexOfObject:anIndex inSortedRange:nil options:0 usingComparator:sortFunction];
- }
- //Retrieving Attribute Information
- /*!
- Returns a dictionary of attributes for the character at a given index. The
- range in which this character resides in which the attributes are the
- same, can be returned if desired.
- @note there is no guarantee that the range returned is in fact the complete
- range of the particular attributes. To ensure this use
- \c attributesAtIndex:longestEffectiveRange:inRange: instead. Note
- however that it may take significantly longer to execute.
- @param anIndex is an unsigned integer index. It must lie within the bounds
- of the string.
- @param aRange is a reference to a CPRange object
- that is set (upon return) to the range over which the attributes are the
- same as those at index, \c anIndex. If not required pass
- \c nil.
- @return a CPDictionary containing the attributes associated with the
- character at index \c anIndex. Returns an empty dictionary if index
- is out of bounds.
- */
- - (CPDictionary)attributesAtIndex:(CPUInteger)anIndex effectiveRange:(CPRangePointer)aRange
- {
- // find the range entry that contains anIndex.
- var entryIndex = [self _indexOfEntryWithIndex:anIndex];
- if (entryIndex === CPNotFound)
- return @{};
- var matchingRange = _rangeEntries[entryIndex];
- if (aRange)
- {
- aRange.location = matchingRange.range.location;
- aRange.length = matchingRange.range.length;
- }
- return matchingRange.attributes;
- }
- /*!
- Returns a dictionary of all attributes for the character at a given index
- and, by reference, the range over which the attributes apply. This is the
- maximum range both forwards and backwards in the string over which the
- attributes apply, bounded in both directions by the range limit parameter,
- \c rangeLimit.
- @note this method performs a search to find this range which may be
- computationally intensive. Use the \c rangeLimit to limit the
- search space or use \c -attributesAtIndex:effectiveRange: but
- note that it is not guaranteed to return the full range of the current
- character's attributes.
- @param anIndex is the unsigned integer index. It must lie within the bounds
- of the string.
- @param aRange is a reference to a CPRange object that is set (upon return)
- to the range over which the attributes apply.
- @param rangeLimit a range limiting the search for the attributes' applicable
- range.
- @return a CPDictionary containing the attributes associated with the
- character at index \c anIndex. Returns an empty dictionary if index
- is out of bounds.
- */
- - (CPDictionary)attributesAtIndex:(CPUInteger)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
- {
- var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex];
- if (startingEntryIndex === CPNotFound)
- return @{};
- 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;
- }
- /*!
- Returns the specified named attribute for the given character index and, if
- required, the range over which the attribute applies.
- @note there is no guarantee that the range returned is in fact the complete
- range of a particular attribute. To ensure this use
- \c -attribute:atIndex:longestEffectiveRange:inRange: instead but
- note that it may take significantly longer to execute.
- @param attribute the name of the desired attribute.
- @param anIndex is an unsigned integer character index from which to retrieve
- the attribute. It must lie within the bounds of the string.
- @param aRange is a reference to a CPRange object, that is set upon return
- to the range over which the named attribute applies. If not required pass
- \c nil.
- @return the named attribute or \c nil is the attribute does not
- exist.
- */
- - (id)attribute:(CPString)attribute atIndex:(CPUInteger)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];
- }
- /*!
- Returns the specified named attribute for the given character index and the
- range over which the attribute applies. This is the maximum range both
- forwards and backwards in the string over which the attribute applies,
- bounded in both directions by the range limit parameter,
- \c rangeLimit.
- @note this method performs a search to find this range which may be
- computationally intensive. Use the \c rangeLimit to limit the
- search space or use \c -attribute:atIndex:effectiveRange: but
- note that it is not guaranteed to return the full range of the current
- character's named attribute.
- @param attribute the name of the desired attribute.
- @param anIndex is an unsigned integer character index from which to retrieve
- the attribute. It must lie within the bounds of the string.
- @param aRange is a reference to a CPRange object, that is set upon return
- to the range over which the named attribute applies.
- @param rangeLimit a range limiting the search for the attribute's applicable
- range.
- @return the named attribute or \c nil is the attribute does not
- exist.
- */
- - (id)attribute:(CPString)attribute atIndex:(CPUInteger)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
- {
- 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
- /*!
- Compares the receiver's characters and attributes to the specified
- attributed string, \c aString, and tests for equality.
- @param aString the CPAttributedString to compare.
- @return a boolean indicating equality.
- */
- - (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;
- do
- {
- if (CPIntersectionRange(myRange, comparisonRange).length > 0 &&
- ![myAttributes isEqualToDictionary:comparisonAttributes])
- {
- return NO;
- }
- else if (CPMaxRange(myRange) < CPMaxRange(comparisonRange))
- myAttributes = [self attributesAtIndex:CPMaxRange(myRange) effectiveRange:myRange];
- else
- comparisonAttributes = [aString attributesAtIndex:CPMaxRange(comparisonRange) effectiveRange:comparisonRange];
- } while (CPMaxRange(CPUnionRange(myRange, comparisonRange)) < length);
- return YES;
- }
- /*!
- Determine whether the given object is the same as the receiver. If the
- specified object is an attributed string then an attributed string compare
- is performed.
- @param anObject an object to test for equality.
- @return a boolean indicating equality.
- */
- - (BOOL)isEqual:(id)anObject
- {
- if (anObject === self)
- return YES;
- if ([anObject isKindOfClass:[self class]])
- return [self isEqualToAttributedString:anObject];
- return NO;
- }
- //Extracting a Substring
- /*!
- Extracts a substring from the receiver, both characters and attributes,
- within the range given by \c aRange.
- @param aRange the range of the substring to extract.
- @return a CPAttributedString containing the desired substring.
- @exception CPRangeException if the range lies outside the receiver's bounds.
- */
- - (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];
- if (entryIndex === CPNotFound)
- _CPRaiseRangeException(self, _cmd, aRange.location, _string.length);
- var 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
- /*!
- Replaces the characters in the receiver with those of the specified string
- over the range, \c aRange. If the range has a length of 0 then
- the specified string is inserted at the range location. The new characters
- inherit the attributes of the first character in the range that they
- replace or in the case if a 0 range length, the first character before of
- after the insert (after if the insert is at location 0).
- @note the replacement string need not be the same length as the range
- being replaced. The full \c aString is inserted and thus the
- receiver's length changes to match this
- @param aRange the range of characters to replace.
- @param aString the string to replace the specified characters in the
- receiver.
- */
- - (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString
- {
- if (!aString)
- aString = @"";
- var lastValidIndex = MAX(_rangeEntries.length - 1, 0),
- startingIndex = [self _indexOfEntryWithIndex:aRange.location];
- if (startingIndex < 0)
- startingIndex = lastValidIndex;
- var endingIndex = [self _indexOfEntryWithIndex:CPMaxRange(aRange)];
- if (endingIndex < 0)
- endingIndex = lastValidIndex;
- var additionalLength = aString.length - aRange.length,
- patchPosition = startingIndex;
- _string = _string.substring(0, aRange.location) + aString + _string.substring(CPMaxRange(aRange));
- var originalLength = _rangeEntries[patchPosition].range.length;
- if (startingIndex === endingIndex)
- _rangeEntries[patchPosition].range.length += additionalLength;
- else
- {
- if (CPIntersectionRange(_rangeEntries[patchPosition].range, aRange).length < originalLength)
- {
- startingIndex++;
- }
- if (endingIndex > startingIndex)
- {
- var originalOffset= _rangeEntries[startingIndex].range.location,
- offsetFromSplicing = CPMaxRange(_rangeEntries[endingIndex].range) - originalOffset;
- _rangeEntries.splice(startingIndex, endingIndex - startingIndex);
- _rangeEntries[startingIndex].range = CPMakeRange(originalOffset, offsetFromSplicing);
- }
- if (patchPosition !== startingIndex)
- {
- var lhsOffset = aString.length - CPIntersectionRange(_rangeEntries[patchPosition].range, aRange).length;
- _rangeEntries[patchPosition].range.length = originalLength + lhsOffset;
- var rhsOffset = aString.length - CPIntersectionRange(_rangeEntries[startingIndex].range, aRange).length;
- _rangeEntries[startingIndex].range.location += lhsOffset;
- _rangeEntries[startingIndex].range.length += rhsOffset;
- patchPosition = startingIndex;
- } else
- _rangeEntries[patchPosition].range.length += additionalLength;
- }
- for (var patchIndex = patchPosition + 1, l = _rangeEntries.length; patchIndex < l; patchIndex++)
- _rangeEntries[patchIndex].range.location += additionalLength;
- }
- /*!
- Deletes a range of characters and their associated attributes.
- @param aRange a CPRange indicating the range of characters to delete.
- */
- - (void)deleteCharactersInRange:(CPRange)aRange
- {
- [self replaceCharactersInRange:aRange withString:nil];
- }
- //Changing Attributes
- /*!
- Sets the attributes of the specified character range.
- @note This process removes the attributes already associated with the
- character range. If you wish to retain the current attributes use
- \c -addAttributes:range:.
- @param aDictionary a CPDictionary of attributes (names and values) to set
- to.
- @param aRange a CPRange indicating the range of characters to set their
- associated attributes to \c aDictionary.
- */
- - (void)setAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
- {
- var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
- endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
- current = startingEntryIndex;
- if (current < 0)
- current = MAX(_rangeEntries.length - 1, 0);
- if (endingEntryIndex === CPNotFound)
- endingEntryIndex = _rangeEntries.length;
- while (current < endingEntryIndex)
- _rangeEntries[current++].attributes = [aDictionary copy];
- //necessary?
- [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
- }
- /*!
- Add a collection of attributes to the specified character range.
- @note Attributes currently associated with the characters in the range are
- untouched. To remove all previous attributes when adding use
- \c -setAttributes:range:.
- @param aDictionary a CPDictionary of attributes (names and values) to add.
- @param aRange a CPRange indicating the range of characters to add the
- attributes to.
- */
- - (void)addAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
- {
- 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];
- }
- /*!
- Add an attribute with the given name and value to the specified character
- range.
- @note Attributes currently associated with the characters in the range are
- untouched. To remove all previous attributes when adding use
- \c -setAttributes:range:.
- @param anAttribute a CPString of the attribute name.
- @param aValue a value to assign to the attribute. Can be of any type.
- @param aRange a CPRange indicating the range of characters to add the
- attribute too.
- */
- - (void)addAttribute:(CPString)anAttribute value:(id)aValue range:(CPRange)aRange
- {
- [self addAttributes:@{ anAttribute: aValue } range:aRange];
- }
- /*!
- Remove a named attribute from a character range.
- @param anAttribute a CPString specifying the name of the attribute.
- @param aRange a CPRange indicating the range of character from which the
- attribute will be removed.
- */
- - (void)removeAttribute:(CPString)anAttribute range:(CPRange)aRange
- {
- 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 removeObjectForKey:anAttribute];
- //necessary?
- [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
- }
- //Changing Characters and Attributes
- /*!
- Append an attributed string (characters and attributes) on to the end of
- the receiver.
- @param aString a CPAttributedString to append.
- */
- - (void)appendAttributedString:(CPAttributedString)aString
- {
- [self insertAttributedString:aString atIndex:_string.length];
- }
- /*!
- Inserts an attributed string (characters and attributes) at index,
- \c anIndex, into the receiver. The portion of the
- receiver's attributed string from the specified index to the end is shifted
- until after the inserted string.
- @param aString a CPAttributedString to insert.
- @param anIndex the index at which the insert is to occur.
- @exception CPRangeException If the index is out of bounds.
- */
- - (void)insertAttributedString:(CPAttributedString)aString atIndex:(CPUInteger)anIndex
- {
- 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);
- }
- [self _coalesceRangeEntriesFromIndex:MAX(0, entryIndexOfNextEntry - 1) toIndex:MIN(entryIndexOfNextEntry + 1, _rangeEntries.length - 1)];
- }
- /*!
- Replaces characters and attributes in the range \c aRange with
- those of the given attributed string, \c aString.
- @param aRange a CPRange object specifying the range of characters and
- attributes in the object to replace.
- @param aString a CPAttributedString containing the data to be used for
- replacement.
- */
- - (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString
- {
- [self deleteCharactersInRange:aRange];
- [self insertAttributedString:aString atIndex:aRange.location];
- }
- /*!
- Sets the objects characters and attributes to those of \c aString.
- @param aString is a CPAttributedString from which the contents will be
- copied.
- */
- - (void)setAttributedString:(CPAttributedString)aString
- {
- _string = aString._string;
- _rangeEntries = [];
- var i = 0,
- count = aString._rangeEntries.length;
- for (; i < count; i++)
- _rangeEntries.push(copyRangeEntry(aString._rangeEntries[i]));
- }
- //Private methods
- - (Number)_indexOfRangeEntryForIndex:(unsigned)characterIndex splitOnMaxIndex:(BOOL)split
- {
- var index = [self _indexOfEntryWithIndex:characterIndex];
- if (index === CPNotFound)
- return index;
- var rangeEntry = _rangeEntries[index];
- if (rangeEntry.range.location === characterIndex || (CPMaxRange(rangeEntry.range) - 1 === characterIndex && !split))
- return index;
- var newEntries = splitRangeEntryAtIndex(rangeEntry, characterIndex);
- _rangeEntries.splice(index, 1, newEntries[0], newEntries[1]);
- index++;
- return index;
- }
- - (void)_coalesceRangeEntriesFromIndex:(unsigned)start toIndex:(unsigned)end
- {
- var current = start;
- if (end >= _rangeEntries.length)
- end = _rangeEntries.length - 1;
- while (current < end)
- {
- var a = _rangeEntries[current],
- b = _rangeEntries[current + 1];
- if (a && b && [a.attributes isEqualToDictionary:b.attributes])
- {
- a.range.length = CPMaxRange(b.range) - a.range.location;
- _rangeEntries.splice(current + 1, 1);
- end--;
- }
- else
- current++;
- }
- }
- //Grouping Changes
- /*!
- This function is deliberately empty. It is provided to ease code converting
- from Cocoa.
- */
- - (void)beginEditing
- {
- //do nothing (says cocotron and gnustep)
- }
- /*!
- This function is deliberately empty. It is provided to ease code converting
- from Cocoa.
- */
- - (void)endEditing
- {
- //do nothing (says cocotron and gnustep)
- }
- @end
- var CPAttributedStringStringKey = "CPAttributedStringString",
- CPAttributedStringRangesKey = "CPAttributedStringRanges",
- CPAttributedStringAttributesKey = "CPAttributedStringAttributes";
- @implementation CPAttributedString (CPCoding)
- - (id)initWithCoder:(CPCoder)aCoder
- {
- self = [self init];
- if (self)
- {
- _string = [aCoder decodeObjectForKey:CPAttributedStringStringKey];
- var decodedRanges = [aCoder decodeObjectForKey:CPAttributedStringRangesKey],
- decodedAttributes = [aCoder decodeObjectForKey:CPAttributedStringAttributesKey];
- _rangeEntries = [];
- for (var i = 0, l = decodedRanges.length; i < l; i++)
- _rangeEntries.push(makeRangeEntry(decodedRanges[i], decodedAttributes[i]));
- }
- return self;
- }
- - (void)encodeWithCoder:(CPCoder)aCoder
- {
- [aCoder encodeObject:_string forKey:CPAttributedStringStringKey];
- var rangesForEncoding = [],
- dictsForEncoding = [];
- for (var i = 0, l = _rangeEntries.length; i < l; i++)
- {
- rangesForEncoding.push(_rangeEntries[i].range);
- dictsForEncoding.push(_rangeEntries[i].attributes);
- }
- [aCoder encodeObject:rangesForEncoding forKey:CPAttributedStringRangesKey];
- [aCoder encodeObject:dictsForEncoding forKey:CPAttributedStringAttributesKey];
- }
- @end
- /*!
- @class CPMutableAttributedString
- @ingroup compatibility
- This class is just an empty subclass of CPAttributedString.
- CPAttributedString already implements mutable methods and
- this class only exists for source compatibility.
- */
- @implementation CPMutableAttributedString : CPAttributedString
- @end
- var isEqual = function(a, b)
- {
- if (a === b)
- return YES;
- if ([a respondsToSelector:@selector(isEqual:)] && [a isEqual:b])
- return YES;
- return NO;
- };
- var makeRangeEntry = function(/*CPRange*/aRange, /*CPDictionary*/attributes)
- {
- return {range:aRange, attributes:[attributes copy]};
- };
- var copyRangeEntry = function(/*RangeEntry*/aRangeEntry)
- {
- return makeRangeEntry(CPMakeRangeCopy(aRangeEntry.range), [aRangeEntry.attributes copy]);
- };
- var splitRangeEntryAtIndex = function(/*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];
- };