API  0.9.7
 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
51 - (id)init
52 {
53  return [self initWithString:@"" attributes:nil];
54 }
55 
61 - (id)initWithString:(CPString)aString
62 {
63  return [self initWithString:aString attributes:nil];
64 }
65 
71 - (id)initWithAttributedString:(CPAttributedString)aString
72 {
73  var string = [self initWithString:@"" attributes:nil];
74 
75  [string setAttributedString:aString];
76 
77  return string;
78 }
79 
88 - (id)initWithString:(CPString)aString attributes:(CPDictionary)attributes
89 {
90  self = [super init];
91 
92  if (self)
93  {
94  if (!attributes)
95  attributes = @{};
96 
97  _string = "" + aString;
98  _rangeEntries = [makeRangeEntry(CPMakeRange(0, _string.length), attributes)];
99  }
100 
101  return self;
102 }
103 
104 //Retrieving Character Information
110 - (CPString)string
111 {
112  return _string;
113 }
114 
120 - (CPString)mutableString
121 {
122  return [self string];
123 }
124 
130 - (unsigned)length
131 {
132  return _string.length;
133 }
134 
135 // private method
136 - (unsigned)_indexOfEntryWithIndex:(unsigned)anIndex
137 {
138  if (anIndex < 0 || anIndex > _string.length || anIndex === undefined)
139  return CPNotFound;
140 
141  // find the range entry that contains anIndex.
142  var sortFunction = function(index, entry)
143  {
144  // index is the character index we're searching for,
145  // while range is the actual range entry we're comparing against
146  if (CPLocationInRange(index, entry.range))
147  return CPOrderedSame;
148  else if (CPMaxRange(entry.range) <= index)
149  return CPOrderedDescending;
150  else
151  return CPOrderedAscending;
152  };
153 
154  return [_rangeEntries indexOfObject:anIndex inSortedRange:nil options:0 usingComparator:sortFunction];
155 }
156 
157 //Retrieving Attribute Information
176 - (CPDictionary)attributesAtIndex:(CPUInteger)anIndex effectiveRange:(CPRangePointer)aRange
177 {
178  // find the range entry that contains anIndex.
179  var entryIndex = [self _indexOfEntryWithIndex:anIndex];
180 
181  if (entryIndex === CPNotFound)
182  return @{};
183 
184  var matchingRange = _rangeEntries[entryIndex];
185 
186  if (aRange)
187  {
188  aRange.location = matchingRange.range.location;
189  aRange.length = matchingRange.range.length;
190  }
191 
192  return matchingRange.attributes;
193 }
194 
216 - (CPDictionary)attributesAtIndex:(CPUInteger)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
217 {
218  var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex];
219 
220  if (startingEntryIndex === CPNotFound)
221  return @{};
222 
223  if (!aRange)
224  return _rangeEntries[startingEntryIndex].attributes;
225 
226  if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit))
227  {
228  aRange.location = rangeLimit.location;
229  aRange.length = rangeLimit.length;
230 
231  return _rangeEntries[startingEntryIndex].attributes;
232  }
233 
234  // scan backwards
235  var nextRangeIndex = startingEntryIndex - 1,
236  currentEntry = _rangeEntries[startingEntryIndex],
237  comparisonDict = currentEntry.attributes;
238 
239  while (nextRangeIndex >= 0)
240  {
241  var nextEntry = _rangeEntries[nextRangeIndex];
242 
243  if (CPMaxRange(nextEntry.range) > rangeLimit.location && [nextEntry.attributes isEqualToDictionary:comparisonDict])
244  {
245  currentEntry = nextEntry;
246  nextRangeIndex--;
247  }
248  else
249  break;
250  }
251 
252  aRange.location = MAX(currentEntry.range.location, rangeLimit.location);
253 
254  // scan forwards
255  currentEntry = _rangeEntries[startingEntryIndex];
256  nextRangeIndex = startingEntryIndex + 1;
257 
258  while (nextRangeIndex < _rangeEntries.length)
259  {
260  var nextEntry = _rangeEntries[nextRangeIndex];
261 
262  if (nextEntry.range.location < CPMaxRange(rangeLimit) && [nextEntry.attributes isEqualToDictionary:comparisonDict])
263  {
264  currentEntry = nextEntry;
265  nextRangeIndex++;
266  }
267  else
268  break;
269  }
270 
271  aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location;
272 
273  return comparisonDict;
274 }
275 
292 - (id)attribute:(CPString)attribute atIndex:(CPUInteger)index effectiveRange:(CPRangePointer)aRange
293 {
294  if (!attribute)
295  {
296  if (aRange)
297  {
298  aRange.location = 0;
299  aRange.length = _string.length;
300  }
301 
302  return nil;
303  }
304 
305  return [[self attributesAtIndex:index effectiveRange:aRange] valueForKey:attribute];
306 }
307 
329 - (id)attribute:(CPString)attribute atIndex:(CPUInteger)anIndex longestEffectiveRange:(CPRangePointer)aRange inRange:(CPRange)rangeLimit
330 {
331  var startingEntryIndex = [self _indexOfEntryWithIndex:anIndex];
332 
333  if (startingEntryIndex === CPNotFound || !attribute)
334  return nil;
335 
336  if (!aRange)
337  return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute];
338 
339  if (CPRangeInRange(_rangeEntries[startingEntryIndex].range, rangeLimit))
340  {
341  aRange.location = rangeLimit.location;
342  aRange.length = rangeLimit.length;
343 
344  return [_rangeEntries[startingEntryIndex].attributes objectForKey:attribute];
345  }
346 
347  // scan backwards
348  var nextRangeIndex = startingEntryIndex - 1,
349  currentEntry = _rangeEntries[startingEntryIndex],
350  comparisonAttribute = [currentEntry.attributes objectForKey:attribute];
351 
352  while (nextRangeIndex >= 0)
353  {
354  var nextEntry = _rangeEntries[nextRangeIndex];
355 
356  if (CPMaxRange(nextEntry.range) > rangeLimit.location && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute]))
357  {
358  currentEntry = nextEntry;
359  nextRangeIndex--;
360  }
361  else
362  break;
363  }
364 
365  aRange.location = MAX(currentEntry.range.location, rangeLimit.location);
366 
367  // scan forwards
368  currentEntry = _rangeEntries[startingEntryIndex];
369  nextRangeIndex = startingEntryIndex + 1;
370 
371  while (nextRangeIndex < _rangeEntries.length)
372  {
373  var nextEntry = _rangeEntries[nextRangeIndex];
374 
375  if (nextEntry.range.location < CPMaxRange(rangeLimit) && isEqual(comparisonAttribute, [nextEntry.attributes objectForKey:attribute]))
376  {
377  currentEntry = nextEntry;
378  nextRangeIndex++;
379  }
380  else
381  break;
382  }
383 
384  aRange.length = MIN(CPMaxRange(currentEntry.range), CPMaxRange(rangeLimit)) - aRange.location;
385 
386  return comparisonAttribute;
387 }
388 
389 //Comparing Attributed Strings
396 - (BOOL)isEqualToAttributedString:(CPAttributedString)aString
397 {
398  if (!aString)
399  return NO;
400 
401  if (_string !== [aString string])
402  return NO;
403 
404  var myRange = CPMakeRange(),
405  comparisonRange = CPMakeRange(),
406  myAttributes = [self attributesAtIndex:0 effectiveRange:myRange],
407  comparisonAttributes = [aString attributesAtIndex:0 effectiveRange:comparisonRange],
408  length = _string.length;
409 
410  while (CPMaxRange(CPUnionRange(myRange, comparisonRange)) < length)
411  {
412  if (CPIntersectionRange(myRange, comparisonRange).length > 0 &&
413  ![myAttributes isEqualToDictionary:comparisonAttributes])
414  {
415  return NO;
416  }
417  else if (CPMaxRange(myRange) < CPMaxRange(comparisonRange))
418  myAttributes = [self attributesAtIndex:CPMaxRange(myRange) effectiveRange:myRange];
419  else
420  comparisonAttributes = [aString attributesAtIndex:CPMaxRange(comparisonRange) effectiveRange:comparisonRange];
421  }
422 
423  return YES;
424 }
425 
433 - (BOOL)isEqual:(id)anObject
434 {
435  if (anObject === self)
436  return YES;
437 
438  if ([anObject isKindOfClass:[self class]])
439  return [self isEqualToAttributedString:anObject];
440 
441  return NO;
442 }
443 
444 //Extracting a Substring
452 - (CPAttributedString)attributedSubstringFromRange:(CPRange)aRange
453 {
454  if (!aRange || CPMaxRange(aRange) > _string.length || aRange.location < 0)
455  [CPException raise:CPRangeException
456  reason:"tried to get attributedSubstring for an invalid range: "+(aRange?CPStringFromRange(aRange):"nil")];
457 
458  var newString = [[CPAttributedString alloc] initWithString:_string.substring(aRange.location, CPMaxRange(aRange))],
459  entryIndex = [self _indexOfEntryWithIndex:aRange.location];
460 
461  if (entryIndex === CPNotFound)
462  _CPRaiseRangeException(self, _cmd, aRange.location, _string.length);
463 
464  var currentRangeEntry = _rangeEntries[entryIndex],
465  lastIndex = CPMaxRange(aRange);
466 
467  newString._rangeEntries = [];
468 
469  while (currentRangeEntry && CPMaxRange(currentRangeEntry.range) < lastIndex)
470  {
471  var newEntry = copyRangeEntry(currentRangeEntry);
472  newEntry.range.location -= aRange.location;
473 
474  if (newEntry.range.location < 0)
475  {
476  newEntry.range.length += newEntry.range.location;
477  newEntry.range.location = 0;
478  }
479 
480  newString._rangeEntries.push(newEntry);
481  currentRangeEntry = _rangeEntries[++entryIndex];
482  }
483 
484  if (currentRangeEntry)
485  {
486  var newRangeEntry = copyRangeEntry(currentRangeEntry);
487 
488  newRangeEntry.range.length = CPMaxRange(aRange) - newRangeEntry.range.location;
489  newRangeEntry.range.location -= aRange.location;
490 
491  if (newRangeEntry.range.location < 0)
492  {
493  newRangeEntry.range.length += newRangeEntry.range.location;
494  newRangeEntry.range.location = 0;
495  }
496 
497  newString._rangeEntries.push(newRangeEntry);
498  }
499 
500  return newString;
501 }
502 
503 //Changing Characters
518 - (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString
519 {
520  if (!aString)
521  aString = @"";
522 
523  var startingIndex = [self _indexOfEntryWithIndex:aRange.location];
524 
525  if (startingIndex === CPNotFound)
526  _CPRaiseRangeException(self, _cmd, aRange.location, _string.length);
527 
528  var startingRangeEntry = _rangeEntries[startingIndex],
529  endingIndex = [self _indexOfEntryWithIndex:MAX(CPMaxRange(aRange) - 1, 0)];
530 
531  if (endingIndex === CPNotFound)
532  _CPRaiseRangeException(self, _cmd, MAX(CPMaxRange(aRange) - 1, 0), _string.length);
533 
534  var endingRangeEntry = _rangeEntries[endingIndex],
535  additionalLength = aString.length - aRange.length;
536 
537  _string = _string.substring(0, aRange.location) + aString + _string.substring(CPMaxRange(aRange));
538 
539  if (startingIndex === endingIndex)
540  startingRangeEntry.range.length += additionalLength;
541  else
542  {
543  endingRangeEntry.range.length = CPMaxRange(endingRangeEntry.range) - CPMaxRange(aRange);
544  endingRangeEntry.range.location = CPMaxRange(aRange);
545 
546  startingRangeEntry.range.length = CPMaxRange(aRange) - startingRangeEntry.range.location;
547 
548  _rangeEntries.splice(startingIndex, endingIndex - startingIndex);
549  }
550 
551  endingIndex = startingIndex + 1;
552 
553  while (endingIndex < _rangeEntries.length)
554  _rangeEntries[endingIndex++].range.location += additionalLength;
555 }
556 
561 - (void)deleteCharactersInRange:(CPRange)aRange
562 {
563  [self replaceCharactersInRange:aRange withString:nil];
564 }
565 
566 //Changing Attributes
578 - (void)setAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
579 {
580  var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
581  endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
582  current = startingEntryIndex;
583 
584  if (endingEntryIndex === CPNotFound)
585  endingEntryIndex = _rangeEntries.length;
586 
587  while (current < endingEntryIndex)
588  _rangeEntries[current++].attributes = [aDictionary copy];
589 
590  //necessary?
591  [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
592 }
593 
604 - (void)addAttributes:(CPDictionary)aDictionary range:(CPRange)aRange
605 {
606  var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
607  endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
608  current = startingEntryIndex;
609 
610  if (endingEntryIndex === CPNotFound)
611  endingEntryIndex = _rangeEntries.length;
612 
613  while (current < endingEntryIndex)
614  {
615  var keys = [aDictionary allKeys],
616  count = [keys count];
617 
618  while (count--)
619  [_rangeEntries[current].attributes setObject:[aDictionary objectForKey:keys[count]] forKey:keys[count]];
620 
621  current++;
622  }
623 
624  //necessary?
625  [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
626 }
627 
640 - (void)addAttribute:(CPString)anAttribute value:(id)aValue range:(CPRange)aRange
641 {
642  [self addAttributes:@{ anAttribute: aValue } range:aRange];
643 }
644 
651 - (void)removeAttribute:(CPString)anAttribute range:(CPRange)aRange
652 {
653  var startingEntryIndex = [self _indexOfRangeEntryForIndex:aRange.location splitOnMaxIndex:YES],
654  endingEntryIndex = [self _indexOfRangeEntryForIndex:CPMaxRange(aRange) splitOnMaxIndex:YES],
655  current = startingEntryIndex;
656 
657  if (endingEntryIndex === CPNotFound)
658  endingEntryIndex = _rangeEntries.length;
659 
660  while (current < endingEntryIndex)
661  [_rangeEntries[current++].attributes removeObjectForKey:anAttribute];
662 
663  //necessary?
664  [self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:endingEntryIndex];
665 }
666 
667 //Changing Characters and Attributes
673 - (void)appendAttributedString:(CPAttributedString)aString
674 {
675  [self insertAttributedString:aString atIndex:_string.length];
676 }
677 
687 - (void)insertAttributedString:(CPAttributedString)aString atIndex:(CPUInteger)anIndex
688 {
689  if (anIndex < 0 || anIndex > [self length])
690  [CPException raise:CPRangeException reason:"tried to insert attributed string at an invalid index: "+anIndex];
691 
692  var entryIndexOfNextEntry = [self _indexOfRangeEntryForIndex:anIndex splitOnMaxIndex:YES],
693  otherRangeEntries = aString._rangeEntries,
694  length = [aString length];
695 
696  if (entryIndexOfNextEntry === CPNotFound)
697  entryIndexOfNextEntry = _rangeEntries.length;
698 
699  _string = _string.substring(0, anIndex) + aString._string + _string.substring(anIndex);
700 
701  var current = entryIndexOfNextEntry;
702  while (current < _rangeEntries.length)
703  _rangeEntries[current++].range.location += length;
704 
705  var newRangeEntryCount = otherRangeEntries.length,
706  index = 0;
707 
708  while (index < newRangeEntryCount)
709  {
710  var entryCopy = copyRangeEntry(otherRangeEntries[index++]);
711  entryCopy.range.location += anIndex;
712 
713  _rangeEntries.splice(entryIndexOfNextEntry - 1 + index, 0, entryCopy);
714  }
715 
716  //necessary?
717  //[self _coalesceRangeEntriesFromIndex:startingEntryIndex toIndex:startingEntryIndex+rangeEntries.length];
718 }
719 
728 - (void)replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPAttributedString)aString
729 {
730  [self deleteCharactersInRange:aRange];
731  [self insertAttributedString:aString atIndex:aRange.location];
732 }
733 
739 - (void)setAttributedString:(CPAttributedString)aString
740 {
741  _string = aString._string;
742  _rangeEntries = [];
743 
744  var i = 0,
745  count = aString._rangeEntries.length;
746 
747  for (; i < count; i++)
748  _rangeEntries.push(copyRangeEntry(aString._rangeEntries[i]));
749 }
750 
751 //Private methods
752 - (Number)_indexOfRangeEntryForIndex:(unsigned)characterIndex splitOnMaxIndex:(BOOL)split
753 {
754  var index = [self _indexOfEntryWithIndex:characterIndex];
755 
756  if (index === CPNotFound)
757  return index;
758 
759  var rangeEntry = _rangeEntries[index];
760 
761  if (rangeEntry.range.location === characterIndex || (CPMaxRange(rangeEntry.range) - 1 === characterIndex && !split))
762  return index;
763 
764  var newEntries = splitRangeEntryAtIndex(rangeEntry, characterIndex);
765  _rangeEntries.splice(index, 1, newEntries[0], newEntries[1]);
766  index++;
767 
768  return index;
769 }
770 
771 - (void)_coalesceRangeEntriesFromIndex:(unsigned)start toIndex:(unsigned)end
772 {
773  var current = start;
774 
775  if (end >= _rangeEntries.length)
776  end = _rangeEntries.length - 1;
777 
778  while (current < end)
779  {
780  var a = _rangeEntries[current],
781  b = _rangeEntries[current + 1];
782 
783  if ([a.attributes isEqualToDictionary:b.attributes])
784  {
785  a.range.length = CPMaxRange(b.range) - a.range.location;
786  _rangeEntries.splice(current + 1, 1);
787  end--;
788  }
789  else
790  current++;
791  }
792 }
793 
794 //Grouping Changes
799 - (void)beginEditing
800 {
801  //do nothing (says cocotron and gnustep)
802 }
803 
808 - (void)endEditing
809 {
810  //do nothing (says cocotron and gnustep)
811 }
812 
813 @end
814 
824 {
825  id __doxygen__;
826 }
827 
828 @end
829 
830 var isEqual = function(a, b)
831 {
832  if (a === b)
833  return YES;
834 
835  if ([a respondsToSelector:@selector(isEqual:)] && [a isEqual:b])
836  return YES;
837 
838  return NO;
839 };
840 
841 var makeRangeEntry = function(/*CPRange*/aRange, /*CPDictionary*/attributes)
842 {
843  return {range:aRange, attributes:[attributes copy]};
844 };
845 
846 var copyRangeEntry = function(/*RangeEntry*/aRangeEntry)
847 {
848  return makeRangeEntry(CPMakeRangeCopy(aRangeEntry.range), [aRangeEntry.attributes copy]);
849 };
850 
851 var splitRangeEntryAtIndex = function(/*RangeEntry*/aRangeEntry, /*unsigned*/anIndex)
852 {
853  var newRangeEntry = copyRangeEntry(aRangeEntry),
854  cachedIndex = CPMaxRange(aRangeEntry.range);
855 
856  aRangeEntry.range.length = anIndex - aRangeEntry.range.location;
857  newRangeEntry.range.location = anIndex;
858  newRangeEntry.range.length = cachedIndex - anIndex;
859  newRangeEntry.attributes = [newRangeEntry.attributes copy];
860 
861  return [aRangeEntry, newRangeEntry];
862 };