API  0.9.6
 All Classes Files Functions Variables Macros Groups Pages
CPAttributedString.j
Go to the documentation of this 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 
40 @implementation CPAttributedString : CPObject
41 {
42  CPString _string;
43  CPArray _rangeEntries;
44 }
45 
46 // Creating a CPAttributedString Object
52 - (id)initWithString:(CPString)aString
53 {
54  return [self initWithString:aString attributes:nil];
55 }
56 
62 - (id)initWithAttributedString:(CPAttributedString)aString
63 {
64  var string = [self initWithString:"" attributes:nil];
65 
66  [string setAttributedString:aString];
67 
68  return string;
69 }
70 
79 - (id)initWithString:(CPString)aString attributes:(CPDictionary)attributes
80 {
81  self = [super init];
82 
83  if (self)
84  {
85  if (!attributes)
86  attributes = [CPDictionary dictionary];
87 
88  _string = ""+aString;
89  _rangeEntries = [makeRangeEntry(CPMakeRange(0, _string.length), attributes)];
90  }
91 
92  return self;
93 }
94 
95 //Retrieving Character Information
101 - (CPString)string
102 {
103  return _string;
104 }
105 
111 - (CPString)mutableString
112 {
113  return [self string];
114 }
115 
121 - (unsigned)length
122 {
123  return _string.length;
124 }
125 
126 // private method
127 - (unsigned)_indexOfEntryWithIndex:(unsigned)anIndex
128 {
129  if (anIndex < 0 || anIndex > _string.length || anIndex === undefined)
130  return CPNotFound;
131 
132  //find the range entry that contains anIndex.
133  var sortFunction = function(index, entry)
134  {
135  //index is the character index we're searching for, while range is the actual range entry we're comparing against
136  if (CPLocationInRange(index, entry.range))
137  return CPOrderedSame;
138  else if (CPMaxRange(entry.range) <= index)
139  return CPOrderedDescending;
140  else
141  return CPOrderedAscending;
142  };
143 
144  return [_rangeEntries indexOfObject:anIndex inSortedRange:nil options:0 usingComparator:sortFunction];
145 }
146 
147 //Retrieving Attribute Information
166 - (CPDictionary)attributesAtIndex:(unsigned)anIndex effectiveRange:(CPRangePointer)aRange
167 {
168  //find the range entry that contains anIndex.
169  var entryIndex = [self _indexOfEntryWithIndex:anIndex];
170 
171  if (entryIndex == CPNotFound)
172  return nil;
173 
174  var matchingRange = _rangeEntries[entryIndex];
175  if (aRange)
176  {
177  aRange.location = matchingRange.range.location;
178  aRange.length = matchingRange.range.length;
179  }
180 
181  return matchingRange.attributes;
182 }
183 
205 - (CPDictionary)attributesAtIndex:(unsigned)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
206 {
207  var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex];
208 
209  if (startingEntryIndex == CPNotFound)
210  return nil;
211 
212  if (!aRange)
213  return _rangeEntries[startingEntryIndex].attributes;
214 
215  if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit))
216  {
217  aRange.location = rangeLimit.location;
218  aRange.length = rangeLimit.length;
219 
220  return _rangeEntries[startingEntryIndex].attributes;
221  }
222 
223  //scan backwards
224  var nextRangeIndex = startingEntryIndex - 1,
225  currentEntry = _rangeEntries[startingEntryIndex],
226  comparisonDict = currentEntry.attributes;
227 
228  while (nextRangeIndex >= 0)
229  {
230  var nextEntry = _rangeEntries[nextRangeIndex];
231 
232  if (CPMaxRange(nextEntry.range) > rangeLimit.location && [nextEntry.attributes isEqualToDictionary:comparisonDict])
233  {
234  currentEntry = nextEntry;
235  nextRangeIndex--;
236  }
237  else
238  break;
239  }
240 
241  aRange.location = MAX(currentEntry.range.location, rangeLimit.location);
242 
243  //scan forwards
244  currentEntry = _rangeEntries[startingEntryIndex];
245  nextRangeIndex = startingEntryIndex + 1;
246 
247  while (nextRangeIndex < _rangeEntries.length)
248  {
249  var nextEntry = _rangeEntries[nextRangeIndex];
250 
251  if (nextEntry.range.location < CPMaxRange(rangeLimit) && [nextEntry.attributes isEqualToDictionary:comparisonDict])
252  {
253  currentEntry = nextEntry;
254  nextRangeIndex++;
255  }
256  else
257  break;
258  }
259 
260  aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location;
261 
262  return comparisonDict;
263 }
264 
281 - (id)attribute:(CPString)attribute atIndex:(unsigned)index effectiveRange:(CPRangePointer)aRange
282 {
283  if (!attribute)
284  {
285  if (aRange)
286  {
287  aRange.location = 0;
288  aRange.length = _string.length;
289  }
290 
291  return nil;
292  }
293 
294  return [[self attributesAtIndex:index effectiveRange:aRange] valueForKey:attribute];
295 }
296 
318 - (id)attribute:(CPString)attribute atIndex:(unsigned)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
319 {
320  //find the range entry that contains anIndex.
321  var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex];
322 
323  if (startingEntryIndex == CPNotFound || !attribute)
324  return nil;
325 
326  if (!aRange)
327  return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute];
328 
329  if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit))
330  {
331  aRange.location = rangeLimit.location;
332  aRange.length = rangeLimit.length;
333 
334  return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute];
335  }
336 
337  //scan backwards
338  var nextRangeIndex = startingEntryIndex - 1,
339  currentEntry = _rangeEntries[startingEntryIndex],
340  comparisonAttribute = [currentEntry.attributes objectForKey:attribute];
341 
342  while (nextRangeIndex >= 0)
343  {
344  var nextEntry = _rangeEntries[nextRangeIndex];
345 
346  if (CPMaxRange(nextEntry.range) > rangeLimit.location && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute]))
347  {
348  currentEntry = nextEntry;
349  nextRangeIndex--;
350  }
351  else
352  break;
353  }
354 
355  aRange.location = MAX(currentEntry.range.location, rangeLimit.location);
356 
357  //scan forwards
358  currentEntry = _rangeEntries[startingEntryIndex];
359  nextRangeIndex = startingEntryIndex + 1;
360 
361  while (nextRangeIndex < _rangeEntries.length)
362  {
363  var nextEntry = _rangeEntries[nextRangeIndex];
364 
365  if (nextEntry.range.location < CPMaxRange(rangeLimit) && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute]))
366  {
367  currentEntry = nextEntry;
368  nextRangeIndex++;
369  }
370  else
371  break;
372  }
373 
374  aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location;
375 
376  return comparisonAttribute;
377 }
378 
379 //Comparing Attributed Strings
386 - (BOOL)isEqualToAttributedString:(CPAttributedString)aString
387 {
388  if (!aString)
389  return NO;
390 
391  if (_string != [aString string])
392  return NO;
393 
394  var myRange = CPMakeRange(),
395  comparisonRange = CPMakeRange(),
396  myAttributes = [self attributesAtIndex:0 effectiveRange:myRange],
397  comparisonAttributes = [aString attributesAtIndex:0 effectiveRange:comparisonRange],
398  length = _string.length;
399 
400  while (CPMaxRange(CPUnionRange(myRange, comparisonRange)) < length)
401  {
402  if (CPIntersectionRange(myRange, comparisonRange).length > 0 && ![myAttributes isEqualToDictionary:comparisonAttributes])
403  return NO;
404  if (CPMaxRange(myRange) < CPMaxRange(comparisonRange))
405  myAttributes = [self attributesAtIndex:CPMaxRange(myRange) effectiveRange:myRange];
406  else
407  comparisonAttributes = [aString attributesAtIndex:CPMaxRange(comparisonRange) effectiveRange:comparisonRange];
408  }
409 
410  return YES;
411 }
412 
420 - (BOOL)isEqual:(id)anObject
421 {
422  if (anObject == self)
423  return YES;
424 
425  if ([anObject isKindOfClass:[self class]])
426  return [self isEqualToAttributedString:anObject];
427 
428  return NO;
429 }
430 
431 //Extracting a Substring
439 - (CPAttributedString)attributedSubstringFromRange:(CPRange)aRange
440 {
441  if (!aRange || CPMaxRange(aRange) > _string.length || aRange.location < 0)
442  [CPException raise:CPRangeException
443  reason:"tried to get attributedSubstring for an invalid range: "+(aRange?CPStringFromRange(aRange):"nil")];
444 
445  var newString = [[CPAttributedString alloc] initWithString:_string.substring(aRange.location, CPMaxRange(aRange))],
446  entryIndex = [self _indexOfEntryWithIndex:aRange.location],
447  currentRangeEntry = _rangeEntries[entryIndex],
448  lastIndex = CPMaxRange(aRange);
449 
450  newString._rangeEntries = [];
451 
452  while (currentRangeEntry && CPMaxRange(currentRangeEntry.range) < lastIndex)
453  {
454  var newEntry = copyRangeEntry(currentRangeEntry);
455  newEntry.range.location -= aRange.location;
456 
457  if (newEntry.range.location < 0)
458  {
459  newEntry.range.length += newEntry.range.location;
460  newEntry.range.location = 0;
461  }
462 
463  newString._rangeEntries.push(newEntry);
464  currentRangeEntry = _rangeEntries[++entryIndex];
465  }
466 
467  if (currentRangeEntry)
468  {
469  var newRangeEntry = copyRangeEntry(currentRangeEntry);
470 
471  newRangeEntry.range.length = CPMaxRange(aRange) - newRangeEntry.range.location;
472  newRangeEntry.range.location -= aRange.location;
473 
474  if (newRangeEntry.range.location < 0)
475  {
476  newRangeEntry.range.length += newRangeEntry.range.location;
477  newRangeEntry.range.location = 0;
478  }
479 
480  newString._rangeEntries.push(newRangeEntry);
481  }
482 
483  return newString;
484 }
485 
486 //Changing Characters
501 - (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString
502 {
503  if (!aString)
504  aString = "";
505 
506  var startingIndex = [self _indexOfEntryWithIndex:aRange.location],
507  startingRangeEntry = _rangeEntries[startingIndex],
508  endingIndex = [self _indexOfEntryWithIndex:MAX(CPMaxRange(aRange) - 1, 0)],
509  endingRangeEntry = _rangeEntries[endingIndex],
510  additionalLength = aString.length - aRange.length;
511 
512  _string = _string.substring(0, aRange.location) + aString + _string.substring(CPMaxRange(aRange));
513 
514  if (startingIndex == endingIndex)
515  startingRangeEntry.range.length += additionalLength;
516  else
517  {
518  endingRangeEntry.range.length = CPMaxRange(endingRangeEntry.range) - CPMaxRange(aRange);
519  endingRangeEntry.range.location = CPMaxRange(aRange);
520 
521  startingRangeEntry.range.length = CPMaxRange(aRange) - startingRangeEntry.range.location;
522 
523  _rangeEntries.splice(startingIndex, endingIndex - startingIndex);
524  }
525 
526  endingIndex = startingIndex + 1;
527 
528  while (endingIndex < _rangeEntries.length)
529  _rangeEntries[endingIndex++].range.location += additionalLength;
530 }
531 
536 - (void)deleteCharactersInRange:(CPRange)aRange
537 {
538  [self replaceCharactersInRange:aRange withString:nil];
539 }
540 
541 //Changing Attributes
553 - (void)setAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
554 {
555  var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
556  endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
557  current = startingEntryIndex;
558 
559  if (endingEntryIndex == CPNotFound)
560  endingEntryIndex = _rangeEntries.length;
561 
562  while (current < endingEntryIndex)
563  _rangeEntries[current++].attributes = [aDictionary copy];
564 
565  //necessary?
566  [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
567 }
568 
579 - (void)addAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
580 {
581  var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
582  endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
583  current = startingEntryIndex;
584 
585  if (endingEntryIndex == CPNotFound)
586  endingEntryIndex = _rangeEntries.length;
587 
588  while (current < endingEntryIndex)
589  {
590  var keys = [aDictionary allKeys],
591  count = [keys count];
592 
593  while (count--)
594  [_rangeEntries[current].attributes setObject:[aDictionary objectForKey:keys[count]] forKey:keys[count]];
595 
596  current++;
597  }
598 
599  //necessary?
600  [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
601 }
602 
615 - (void)addAttribute:(CPString)anAttribute value:(id)aValue range:(CPRange)aRange
616 {
617  [self addAttributes:[CPDictionary dictionaryWithObject:aValue forKey:anAttribute] range:aRange];
618 }
619 
626 - (void)removeAttribute:(CPString)anAttribute range:(CPRange)aRange
627 {
628  var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
629  endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
630  current = startingEntryIndex;
631 
632  if (endingEntryIndex == CPNotFound)
633  endingEntryIndex = _rangeEntries.length;
634 
635  while (current < endingEntryIndex)
636  [_rangeEntries[current++].attributes removeObjectForKey:anAttribute];
637 
638  //necessary?
639  [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
640 }
641 
642 //Changing Characters and Attributes
648 - (void)appendAttributedString:(CPAttributedString)aString
649 {
650  [self insertAttributedString:aString atIndex:_string.length];
651 }
652 
662 - (void)insertAttributedString:(CPAttributedString)aString atIndex:(unsigned)anIndex
663 {
664  if (anIndex < 0 || anIndex > [self length])
665  [CPException raise:CPRangeException reason:"tried to insert attributed string at an invalid index: "+anIndex];
666 
667  var entryIndexOfNextEntry = [self _indexOfRangeEntryForIndex:anIndex splitOnMaxIndex:YES],
668  otherRangeEntries = aString._rangeEntries,
669  length = [aString length];
670 
671  if (entryIndexOfNextEntry == CPNotFound)
672  entryIndexOfNextEntry = _rangeEntries.length;
673 
674  _string = _string.substring(0, anIndex) + aString._string + _string.substring(anIndex);
675 
676  var current = entryIndexOfNextEntry;
677  while (current < _rangeEntries.length)
678  _rangeEntries[current++].range.location += length;
679 
680  var newRangeEntryCount = otherRangeEntries.length,
681  index = 0;
682 
683  while (index < newRangeEntryCount)
684  {
685  var entryCopy = copyRangeEntry(otherRangeEntries[index++]);
686  entryCopy.range.location += anIndex;
687 
688  _rangeEntries.splice(entryIndexOfNextEntry - 1 + index, 0, entryCopy);
689  }
690 
691  //necessary?
692  //[self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:startingEntryIndex+rangeEntries.length];
693 }
694 
703 - (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString
704 {
705  [self deleteCharactersInRange:aRange];
706  [self insertAttributedString:aString atIndex:aRange.location];
707 }
708 
714 - (void)setAttributedString:(CPAttributedString)aString
715 {
716  _string = aString._string;
717  _rangeEntries = [];
718 
719  var i = 0,
720  count = aString._rangeEntries.length;
721 
722  for (; i < count; i++)
723  _rangeEntries.push(copyRangeEntry(aString._rangeEntries[i]));
724 }
725 
726 //Private methods
727 - (Number)_indexOfRangeEntryForIndex:(unsigned)characterIndex splitOnMaxIndex:(BOOL)split
728 {
729  var index = [self _indexOfEntryWithIndex:characterIndex];
730 
731  if (index < 0)
732  return index;
733 
734  var rangeEntry = _rangeEntries[index];
735 
736  if (rangeEntry.range.location == characterIndex || (CPMaxRange(rangeEntry.range) - 1 == characterIndex && !split))
737  return index;
738 
739  var newEntries = splitRangeEntryAtIndex(rangeEntry, characterIndex);
740  _rangeEntries.splice(index, 1, newEntries[0], newEntries[1]);
741  index++;
742 
743  return index;
744 }
745 
746 - (void)_coalesceRangeEntriesFromIndex:(unsigned)start toIndex:(unsigned)end
747 {
748  var current = start;
749 
750  if (end >= _rangeEntries.length)
751  end = _rangeEntries.length - 1;
752 
753  while (current < end)
754  {
755  var a = _rangeEntries[current],
756  b = _rangeEntries[current + 1];
757 
758  if ([a.attributes isEqualToDictionary:b.attributes])
759  {
760  a.range.length = CPMaxRange(b.range) - a.range.location;
761  _rangeEntries.splice(current + 1, 1);
762  end--;
763  }
764  else
765  current++;
766  }
767 }
768 
769 //Grouping Changes
774 - (void)beginEditing
775 {
776  //do nothing (says cocotron and gnustep)
777 }
778 
783 - (void)endEditing
784 {
785  //do nothing (says cocotron and gnustep)
786 }
787 
788 @end
789 
799 {
800  id __doxygen__;
801 }
802 
803 @end
804 
805 var isEqual = function isEqual(a, b)
806 {
807  if (a == b)
808  return YES;
809 
810  if ([a respondsToSelector:@selector(isEqual:)] && [a isEqual:b])
811  return YES;
812 
813  return NO;
814 };
815 
816 var makeRangeEntry = function makeRangeEntry(/*CPRange*/aRange, /*CPDictionary*/attributes)
817 {
818  return {range:aRange, attributes:[attributes copy]};
819 };
820 
821 var copyRangeEntry = function copyRangeEntry(/*RangeEntry*/aRangeEntry)
822 {
823  return makeRangeEntry(CPMakeRangeCopy(aRangeEntry.range), [aRangeEntry.attributes copy]);
824 };
825 
826 var splitRangeEntry = function splitRangeEntryAtIndex(/*RangeEntry*/aRangeEntry, /*unsigned*/anIndex)
827 {
828  var newRangeEntry = copyRangeEntry(aRangeEntry),
829  cachedIndex = CPMaxRange(aRangeEntry.range);
830 
831  aRangeEntry.range.length = anIndex - aRangeEntry.range.location;
832  newRangeEntry.range.location = anIndex;
833  newRangeEntry.range.length = cachedIndex - anIndex;
834  newRangeEntry.attributes = [newRangeEntry.attributes copy];
835 
836  return [aRangeEntry, newRangeEntry];
837 };