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