NSString+TUIEmoji.m 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. //
  2. // NSString+TUIEmoji.m
  3. // TUIChat
  4. //
  5. // Created by harvy on 2021/11/15.
  6. // Copyright © 2023 Tencent. All rights reserved.
  7. //
  8. #import "NSString+TUIEmoji.h"
  9. #import "TIMConfig.h"
  10. @implementation NSString (TUIEmoji)
  11. + (NSString *)getRegex_emoji {
  12. NSString *regex_emoji = @"\\[[a-zA-Z0-9_\\u4e00-\\u9fa5]+\\]"; // match emoji
  13. return regex_emoji;
  14. }
  15. - (NSString *)getLocalizableStringWithFaceContent {
  16. NSString *content = self;
  17. NSString *regex_emoji = [self.class getRegex_emoji]; // match emoji
  18. NSError *error = nil;
  19. NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regex_emoji options:NSRegularExpressionCaseInsensitive error:&error];
  20. if (re) {
  21. NSArray *resultArray = [re matchesInString:content options:0 range:NSMakeRange(0, content.length)];
  22. TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
  23. NSMutableArray *waitingReplaceM = [NSMutableArray array];
  24. for (NSTextCheckingResult *match in resultArray) {
  25. NSRange range = [match range];
  26. NSString *subStr = [content substringWithRange:range];
  27. for (TUIFaceCellData *face in group.faces) {
  28. if ([face.name isEqualToString:subStr]) {
  29. [waitingReplaceM
  30. addObject:@{@"range" : NSStringFromRange(range), @"localizableStr" : face.localizableName.length ? face.localizableName : face.name}];
  31. break;
  32. }
  33. }
  34. }
  35. if (waitingReplaceM.count) {
  36. /**
  37. * Replace from back to front, otherwise it will cause positional problems
  38. */
  39. for (int i = (int)waitingReplaceM.count - 1; i >= 0; i--) {
  40. NSRange range = NSRangeFromString(waitingReplaceM[i][@"range"]);
  41. NSString *localizableStr = waitingReplaceM[i][@"localizableStr"];
  42. content = [content stringByReplacingCharactersInRange:range withString:localizableStr];
  43. }
  44. }
  45. }
  46. return content;
  47. }
  48. - (NSString *)getInternationalStringWithfaceContent {
  49. NSString *content = self;
  50. NSString *regex_emoji = [self.class getRegex_emoji];
  51. NSError *error = nil;
  52. NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regex_emoji options:NSRegularExpressionCaseInsensitive error:&error];
  53. if (re) {
  54. NSMutableDictionary *faceDict = [NSMutableDictionary dictionary];
  55. TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
  56. for (TUIFaceCellData *face in group.faces) {
  57. NSString *key = face.localizableName ?: face.name;
  58. NSString *value = face.name ?: @"";
  59. faceDict[key] = value;
  60. }
  61. NSArray *resultArray = [re matchesInString:content options:0 range:NSMakeRange(0, content.length)];
  62. NSMutableArray *waitingReplaceM = [NSMutableArray array];
  63. for (NSTextCheckingResult *match in resultArray) {
  64. NSRange range = [match range];
  65. NSString *subStr = [content substringWithRange:range];
  66. [waitingReplaceM addObject:@{@"range" : NSStringFromRange(range), @"localizableStr" : faceDict[subStr] ?: subStr}];
  67. }
  68. if (waitingReplaceM.count != 0) {
  69. /**
  70. * Replace from back to front, otherwise it will cause positional problems
  71. */
  72. for (int i = (int)waitingReplaceM.count - 1; i >= 0; i--) {
  73. NSRange range = NSRangeFromString(waitingReplaceM[i][@"range"]);
  74. NSString *localizableStr = waitingReplaceM[i][@"localizableStr"];
  75. content = [content stringByReplacingCharactersInRange:range withString:localizableStr];
  76. }
  77. }
  78. }
  79. return content;
  80. }
  81. - (NSMutableAttributedString *)getFormatEmojiStringWithFont:(UIFont *)textFont
  82. emojiLocations:(nullable NSMutableArray<NSDictionary<NSValue *, NSAttributedString *> *> *)emojiLocations {
  83. /**
  84. * First determine whether the text exists
  85. */
  86. if (self.length == 0) {
  87. NSLog(@"getFormatEmojiStringWithFont failed , current text is nil");
  88. return [[NSMutableAttributedString alloc] initWithString:@""];
  89. }
  90. /**
  91. * 1. Create a mutable attributed string
  92. */
  93. NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:self];
  94. if ([TIMConfig defaultConfig].faceGroups.count == 0) {
  95. [attributeString addAttribute:NSFontAttributeName value:textFont range:NSMakeRange(0, attributeString.length)];
  96. return attributeString;
  97. }
  98. /**
  99. * 2.Match strings with regular expressions
  100. */
  101. NSError *error = nil;
  102. static NSRegularExpression *re = nil;
  103. if (re == nil) {
  104. NSString *regex_emoji = [self.class getRegex_emoji];
  105. re = [NSRegularExpression regularExpressionWithPattern:regex_emoji options:NSRegularExpressionCaseInsensitive error:&error];
  106. }
  107. if (!re) {
  108. NSLog(@"%@", [error localizedDescription]);
  109. return attributeString;
  110. }
  111. NSArray *resultArray = [re matchesInString:self options:0 range:NSMakeRange(0, self.length)];
  112. TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
  113. /**
  114. * 3.Getting all emotes and locations
  115. * - Used to store the dictionary, the dictionary stores the image and the corresponding location of the image
  116. */
  117. NSMutableArray *imageArray = [NSMutableArray arrayWithCapacity:resultArray.count];
  118. /**
  119. * Replace the image with the corresponding image according to the matching range
  120. */
  121. for (NSTextCheckingResult *match in resultArray) {
  122. /**
  123. * Get the range in the array element
  124. */
  125. NSRange range = [match range];
  126. /**
  127. * Get the corresponding value in the original string
  128. */
  129. NSString *subStr = [self substringWithRange:range];
  130. for (TUIFaceCellData *face in group.faces) {
  131. if ([face.name isEqualToString:subStr]) {
  132. /**
  133. * - Create a new NSTextAttachment to store our image
  134. */
  135. TUIEmojiTextAttachment *emojiTextAttachment = [[TUIEmojiTextAttachment alloc] init];
  136. emojiTextAttachment.faceCellData = face;
  137. NSString *localizableFaceName = face.name;
  138. // Set tag and image
  139. emojiTextAttachment.emojiTag = localizableFaceName;
  140. emojiTextAttachment.image = [[TUIImageCache sharedInstance] getFaceFromCache:face.path];
  141. // Set emoji size
  142. emojiTextAttachment.emojiSize = kTIMDefaultEmojiSize;
  143. NSAttributedString *str = [NSAttributedString attributedStringWithAttachment:emojiTextAttachment];
  144. /**
  145. * - Convert attachments to mutable strings to replace emoji text in source strings
  146. */
  147. NSAttributedString *imageStr = [NSAttributedString attributedStringWithAttachment:emojiTextAttachment];
  148. /**
  149. * - Save the picture and the corresponding position of the picture into the dictionary
  150. */
  151. NSMutableDictionary *imageDic = [NSMutableDictionary dictionaryWithCapacity:2];
  152. [imageDic setObject:imageStr forKey:@"image"];
  153. [imageDic setObject:[NSValue valueWithRange:range] forKey:@"range"];
  154. /**
  155. * - Store dictionary in array
  156. */
  157. [imageArray addObject:imageDic];
  158. break;
  159. }
  160. }
  161. }
  162. /**
  163. * 4.Replace from back to front, otherwise it will cause positional problems
  164. */
  165. NSMutableArray *locations = [NSMutableArray array];
  166. for (int i = (int)imageArray.count - 1; i >= 0; i--) {
  167. NSRange originRange;
  168. [imageArray[i][@"range"] getValue:&originRange];
  169. /**
  170. * Store location information
  171. */
  172. NSAttributedString *originStr = [attributeString attributedSubstringFromRange:originRange];
  173. NSAttributedString *currentStr = imageArray[i][@"image"];
  174. [locations insertObject:@[ [NSValue valueWithRange:originRange], originStr, currentStr ] atIndex:0];
  175. // Replace
  176. [attributeString replaceCharactersInRange:originRange withAttributedString:currentStr];
  177. }
  178. /**
  179. * 5.Getting the position information of the converted string of emoji
  180. */
  181. NSInteger offsetLocation = 0;
  182. for (NSArray *obj in locations) {
  183. NSArray *location = (NSArray *)obj;
  184. NSRange originRange = [(NSValue *)location[0] rangeValue];
  185. NSAttributedString *originStr = location[1];
  186. NSAttributedString *currentStr = location[2];
  187. NSRange currentRange;
  188. currentRange.location = originRange.location + offsetLocation;
  189. currentRange.length = currentStr.length;
  190. offsetLocation += currentStr.length - originStr.length;
  191. [emojiLocations addObject:@{[NSValue valueWithRange:currentRange] : originStr}];
  192. }
  193. NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
  194. paragraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
  195. [attributeString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, attributeString.length)];
  196. [attributeString addAttribute:NSFontAttributeName value:textFont range:NSMakeRange(0, attributeString.length)];
  197. return attributeString;
  198. }
  199. - (NSString *)getEmojiImagePath {
  200. TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
  201. NSString *loaclName = [self getLocalizableStringWithFaceContent];
  202. for (TUIFaceCellData *face in group.faces) {
  203. if ([face.localizableName isEqualToString:loaclName]) {
  204. return face.path;
  205. }
  206. }
  207. return nil;
  208. }
  209. - (UIImage *)getEmojiImage {
  210. TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
  211. for (TUIFaceCellData *face in group.faces) {
  212. if ([face.name isEqualToString:self]) {
  213. return [[TUIImageCache sharedInstance] getFaceFromCache:face.path];
  214. }
  215. }
  216. return nil;
  217. }
  218. - (NSMutableAttributedString *)getAdvancedFormatEmojiStringWithFont:(UIFont *)textFont
  219. textColor:(UIColor *)textColor
  220. emojiLocations:(nullable NSMutableArray<NSDictionary<NSValue *, NSAttributedString *> *> *)emojiLocations {
  221. if (self.length == 0) {
  222. NSLog(@"getAdvancedFormatEmojiStringWithFont failed , current text is nil");
  223. return [[NSMutableAttributedString alloc] initWithString:@""];
  224. }
  225. NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:self];
  226. if ([TIMConfig defaultConfig].faceGroups.count == 0) {
  227. [attributeString addAttribute:NSFontAttributeName value:textFont range:NSMakeRange(0, attributeString.length)];
  228. return attributeString;
  229. }
  230. NSString *regex_emoji = [self.class getRegex_emoji];
  231. NSError *error = nil;
  232. NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regex_emoji options:NSRegularExpressionCaseInsensitive error:&error];
  233. if (error) {
  234. NSLog(@"%@", [error localizedDescription]);
  235. return attributeString;
  236. }
  237. NSArray *resultArray = [re matchesInString:self options:0 range:NSMakeRange(0, self.length)];
  238. TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
  239. NSMutableArray *imageArray = [NSMutableArray arrayWithCapacity:resultArray.count];
  240. for (NSTextCheckingResult *match in resultArray) {
  241. NSRange range = [match range];
  242. NSString *subStr = [self substringWithRange:range];
  243. for (TUIFaceCellData *face in group.faces) {
  244. if ([face.name isEqualToString:subStr] || [face.localizableName isEqualToString:subStr]) {
  245. TUIEmojiTextAttachment *emojiTextAttachment = [[TUIEmojiTextAttachment alloc] init];
  246. emojiTextAttachment.faceCellData = face;
  247. // Set tag and image
  248. emojiTextAttachment.emojiTag = face.name;
  249. emojiTextAttachment.image = [[TUIImageCache sharedInstance] getFaceFromCache:face.path];
  250. // Set emoji size
  251. emojiTextAttachment.emojiSize = kTIMDefaultEmojiSize;
  252. NSAttributedString *imageStr = [NSAttributedString attributedStringWithAttachment:emojiTextAttachment];
  253. NSMutableDictionary *imageDic = [NSMutableDictionary dictionaryWithCapacity:2];
  254. [imageDic setObject:imageStr forKey:@"image"];
  255. [imageDic setObject:[NSValue valueWithRange:range] forKey:@"range"];
  256. [imageArray addObject:imageDic];
  257. break;
  258. }
  259. }
  260. }
  261. NSMutableArray *locations = [NSMutableArray array];
  262. for (int i = (int)imageArray.count - 1; i >= 0; i--) {
  263. NSRange originRange;
  264. [imageArray[i][@"range"] getValue:&originRange];
  265. NSAttributedString *originStr = [attributeString attributedSubstringFromRange:originRange];
  266. NSAttributedString *currentStr = imageArray[i][@"image"];
  267. [locations insertObject:@[ [NSValue valueWithRange:originRange], originStr, currentStr ] atIndex:0];
  268. [attributeString replaceCharactersInRange:originRange withAttributedString:currentStr];
  269. }
  270. NSInteger offsetLocation = 0;
  271. for (NSArray *obj in locations) {
  272. NSArray *location = (NSArray *)obj;
  273. NSRange originRange = [(NSValue *)location[0] rangeValue];
  274. NSAttributedString *originStr = location[1];
  275. NSAttributedString *currentStr = location[2];
  276. NSRange currentRange;
  277. currentRange.location = originRange.location + offsetLocation;
  278. currentRange.length = currentStr.length;
  279. offsetLocation += currentStr.length - originStr.length;
  280. [emojiLocations addObject:@{[NSValue valueWithRange:currentRange] : originStr}];
  281. }
  282. [attributeString addAttribute:NSFontAttributeName value:textFont range:NSMakeRange(0, attributeString.length)];
  283. [attributeString addAttribute:NSForegroundColorAttributeName value:textColor range:NSMakeRange(0, attributeString.length)];
  284. return attributeString;
  285. }
  286. /**
  287. * Steps:
  288. * 1. Match @user infos in string.
  289. * 2. Split origin string into array(A) by @user info's ranges.
  290. * 3. Iterate the array(A) to match emoji one by one.
  291. * 4. Add all parsed elements(emoji, @user, pure text) into result.
  292. * 5. Process the text and textIndex by the way.
  293. * 6. Encapsulate all arrays in a dict and return it.
  294. */
  295. - (NSDictionary *)splitTextByEmojiAndAtUsers:(NSArray *_Nullable)users {
  296. if (self.length == 0) {
  297. return nil;
  298. }
  299. NSMutableArray *result = [NSMutableArray new];
  300. /// Find @user info's ranges in string.
  301. NSMutableArray *atUsers = [NSMutableArray new];
  302. for (NSString *user in users) {
  303. /// Add an whitespace after the user's name due to the special format of @ content.
  304. NSString *atUser = [NSString stringWithFormat:@"@%@ ", user];
  305. [atUsers addObject:atUser];
  306. }
  307. NSArray *atUserRanges = [self rangeOfAtUsers:atUsers inString:self];
  308. /// Split text using @user info's ranges.
  309. NSArray *splitResult = [self splitArrayWithRanges:atUserRanges inString:self];
  310. NSMutableArray *splitArrayByAtUser = splitResult.firstObject;
  311. NSSet *atUserIndex = splitResult.lastObject;
  312. /// Iterate the split array after finding @user, aimed to match emoji.
  313. NSInteger k = -1;
  314. NSMutableArray *textIndexArray = [NSMutableArray new];
  315. for (int i = 0; i < splitArrayByAtUser.count; i++) {
  316. NSString *str = splitArrayByAtUser[i];
  317. if ([atUserIndex containsObject:@(i)]) {
  318. /// str is @user info.
  319. [result addObject:str];
  320. k += 1;
  321. } else {
  322. /// str is not @user info, try to parse emoji in the same way as above.
  323. NSArray *emojiRanges = [self matchTextByEmoji:str];
  324. splitResult = [self splitArrayWithRanges:emojiRanges inString:str];
  325. NSMutableArray *splitArrayByEmoji = splitResult.firstObject;
  326. NSSet *emojiIndex = splitResult.lastObject;
  327. for (int j = 0; j < splitArrayByEmoji.count; j++) {
  328. NSString *tmp = splitArrayByEmoji[j];
  329. [result addObject:tmp];
  330. k += 1;
  331. if (![emojiIndex containsObject:@(j)]) {
  332. /// str is text.
  333. [textIndexArray addObject:@(k)];
  334. }
  335. }
  336. }
  337. }
  338. NSMutableArray *textArray = [NSMutableArray new];
  339. for (NSNumber *n in textIndexArray) {
  340. [textArray addObject:result[[n integerValue]]];
  341. }
  342. NSDictionary *dict = @{kSplitStringResultKey : result, kSplitStringTextKey : textArray, kSplitStringTextIndexKey : textIndexArray};
  343. return dict;
  344. }
  345. /// Find all ranges of @user in string.
  346. - (NSArray *)rangeOfAtUsers:(NSArray *)atUsers inString:(NSString *)string {
  347. /// Find all positions of character "@".
  348. NSString *tmp = nil;
  349. NSMutableIndexSet *atIndex = [NSMutableIndexSet new];
  350. for (int i = 0; i < [string length]; i++) {
  351. tmp = [string substringWithRange:NSMakeRange(i, 1)];
  352. if ([tmp isEqualToString:@"@"]) {
  353. [atIndex addIndex:i];
  354. }
  355. }
  356. /// Match @user with "@" position.
  357. NSMutableArray *result = [NSMutableArray new];
  358. for (NSString *user in atUsers) {
  359. [atIndex enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
  360. if (string.length >= user.length && idx <= string.length - user.length) {
  361. NSRange range = NSMakeRange(idx, user.length);
  362. if ([[string substringWithRange:range] isEqualToString:user]) {
  363. [result addObject:[NSValue valueWithRange:range]];
  364. [atIndex removeIndex:idx];
  365. *stop = YES;
  366. }
  367. }
  368. }];
  369. }
  370. return result;
  371. }
  372. /// Split string into multi substrings by given ranges.
  373. /// Return value's structure is [result, indexes], in which indexs means position of content within ranges located in result after spliting.
  374. - (NSArray *)splitArrayWithRanges:(NSArray *)ranges inString:(NSString *)string {
  375. if (ranges.count == 0) {
  376. return @[ @[ string ], @[] ];
  377. }
  378. if (string.length == 0) {
  379. return nil;
  380. }
  381. /// Ascending sort.
  382. NSArray *sortedRanges = [ranges sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
  383. NSRange range1 = [obj1 rangeValue];
  384. NSRange range2 = [obj2 rangeValue];
  385. if (range1.location < range2.location) {
  386. return (NSComparisonResult)NSOrderedAscending;
  387. } else if (range1.location > range2.location) {
  388. return (NSComparisonResult)NSOrderedDescending;
  389. } else {
  390. return (NSComparisonResult)NSOrderedSame;
  391. }
  392. }];
  393. NSMutableArray *result = [NSMutableArray new];
  394. NSMutableSet *indexes = [NSMutableSet new];
  395. NSInteger prev = 0;
  396. NSInteger i = 0;
  397. NSInteger j = -1;
  398. while (i < sortedRanges.count) {
  399. NSRange cur = [sortedRanges[i] rangeValue];
  400. NSString *str = nil;
  401. if (cur.location > prev) {
  402. /// Add the str in [prev, cur.location).
  403. str = [string substringWithRange:NSMakeRange(prev, cur.location - prev)];
  404. [result addObject:str];
  405. j += 1;
  406. }
  407. /// Add the str in cur range.
  408. str = [string substringWithRange:cur];
  409. [result addObject:str];
  410. j += 1;
  411. [indexes addObject:@(j)];
  412. /// Update prev to support calculation of next round.
  413. prev = cur.location + cur.length;
  414. /// Text exists after the last emoji.
  415. if (i == sortedRanges.count - 1 && prev < string.length - 1) {
  416. NSString *last = [string substringWithRange:NSMakeRange(prev, string.length - prev)];
  417. [result addObject:last];
  418. }
  419. i++;
  420. }
  421. return @[ result, indexes ];
  422. }
  423. /// Match text by emoji, return the matched ranges
  424. - (NSArray *)matchTextByEmoji:(NSString *)text {
  425. NSMutableArray *result = [NSMutableArray new];
  426. /// TUIKit qq emoji.
  427. NSString *regexOfCustomEmoji = [self.class getRegex_emoji];
  428. NSError *error = nil;
  429. NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regexOfCustomEmoji options:NSRegularExpressionCaseInsensitive error:&error];
  430. if (error) {
  431. NSLog(@"re match custom emoji failed, error: %@", [error localizedDescription]);
  432. return nil;
  433. }
  434. NSArray *matchResult = [re matchesInString:text options:0 range:NSMakeRange(0, text.length)];
  435. for (NSTextCheckingResult *match in matchResult) {
  436. NSString *substring = [text substringWithRange:match.range];
  437. TUIFaceGroup *group = [TIMConfig defaultConfig].faceGroups[0];
  438. for (TUIFaceCellData *face in group.faces) {
  439. if ([face.name isEqualToString:substring] || [face.localizableName isEqualToString:substring]) {
  440. [result addObject:[NSValue valueWithRange:match.range]];
  441. break;
  442. }
  443. }
  444. }
  445. /// Unicode emoji.
  446. NSString *regexOfUnicodeEmoji = [NSString unicodeEmojiReString];
  447. re = [NSRegularExpression regularExpressionWithPattern:regexOfUnicodeEmoji options:NSRegularExpressionCaseInsensitive error:&error];
  448. if (error) {
  449. NSLog(@"re match universal emoji failed, error: %@", [error localizedDescription]);
  450. return [result copy];
  451. }
  452. matchResult = [re matchesInString:text options:0 range:NSMakeRange(0, text.length)];
  453. for (NSTextCheckingResult *match in matchResult) {
  454. [result addObject:[NSValue valueWithRange:match.range]];
  455. }
  456. return [result copy];
  457. }
  458. + (NSString *)replacedStringWithArray:(NSArray *)array index:(NSArray *)indexArray replaceDict:(NSDictionary *)replaceDict {
  459. if (replaceDict == nil) {
  460. return nil;
  461. }
  462. NSMutableArray *mutableArray = [array mutableCopy];
  463. for (NSNumber *value in indexArray) {
  464. NSInteger i = [value integerValue];
  465. if (i < 0 || i > mutableArray.count - 1) {
  466. continue;
  467. }
  468. if (replaceDict[mutableArray[i]]) {
  469. mutableArray[i] = replaceDict[mutableArray[i]];
  470. }
  471. }
  472. return [mutableArray componentsJoinedByString:@""];
  473. }
  474. /**
  475. * Regex of unicode emoji, refer to https://unicode.org/reports/tr51/#EBNF_and_Regex
  476. * Regex exression is like:
  477. \p{ri} \p{ri}
  478. | \p{Emoji}
  479. ( \p{EMod}
  480. | \x{FE0F} \x{20E3}?
  481. | [\x{E0020}-\x{E007E}]+ \x{E007F}
  482. )?
  483. (\x{200D}
  484. ( \p{ri} \p{ri}
  485. | \p{Emoji}
  486. ( \p{EMod}
  487. | \x{FE0F} \x{20E3}?
  488. | [\x{E0020}-\x{E007E}]+ \x{E007F}
  489. )?
  490. )
  491. )*
  492. */
  493. + (NSString *)unicodeEmojiReString {
  494. NSString *ri = @"[\U0001F1E6-\U0001F1FF]";
  495. /// \u0023(#), \u002A(*), \u0030(keycap 0), \u0039(keycap 9), \u00A9(©), \u00AE(®) couldn't be added to NSString directly, need to transform a little bit.
  496. NSString *unsupport = [NSString stringWithFormat:@"%C|%C|[%C-%C]|", 0x0023, 0x002A, 0x0030, 0x0039];
  497. NSString *support =
  498. @"\U000000A9|\U000000AE|\u203C|\u2049|\u2122|\u2139|[\u2194-\u2199]|[\u21A9-\u21AA]|[\u231A-\u231B]|\u2328|\u23CF|[\u23E9-\u23EF]|[\u23F0-\u23F3]|["
  499. @"\u23F8-\u23FA]|\u24C2|[\u25AA-\u25AB]|\u25B6|\u25C0|[\u25FB-\u25FE]|[\u2600-\u2604]|\u260E|\u2611|[\u2614-\u2615]|\u2618|\u261D|\u2620|[\u2622-"
  500. @"\u2623]|\u2626|\u262A|[\u262E-\u262F]|[\u2638-\u263A]|\u2640|\u2642|[\u2648-\u264F]|[\u2650-\u2653]|\u265F|\u2660|\u2663|[\u2665-\u2666]|\u2668|"
  501. @"\u267B|[\u267E-\u267F]|[\u2692-\u2697]|\u2699|[\u269B-\u269C]|[\u26A0-\u26A1]|\u26A7|[\u26AA-\u26AB]|[\u26B0-\u26B1]|[\u26BD-\u26BE]|[\u26C4-\u26C5]|"
  502. @"\u26C8|[\u26CE-\u26CF]|\u26D1|[\u26D3-\u26D4]|[\u26E9-\u26EA]|[\u26F0-\u26F5]|[\u26F7-\u26FA]|\u26FD|\u2702|\u2705|[\u2708-\u270D]|\u270F|\u2712|"
  503. @"\u2714|\u2716|\u271D|\u2721|\u2728|[\u2733-\u2734]|\u2744|\u2747|\u274C|\u274E|[\u2753-\u2755]|\u2757|[\u2763-\u2764]|[\u2795-\u2797]|\u27A1|\u27B0|"
  504. @"\u27BF|[\u2934-\u2935]|[\u2B05-\u2B07]|[\u2B1B-\u2B1C]|\u2B50|\u2B55|\u3030|\u303D|\u3297|\u3299|\U0001F004|\U0001F0CF|[\U0001F170-\U0001F171]|["
  505. @"\U0001F17E-\U0001F17F]|\U0001F18E|[\U0001F191-\U0001F19A]|[\U0001F1E6-\U0001F1FF]|[\U0001F201-\U0001F202]|\U0001F21A|\U0001F22F|[\U0001F232-"
  506. @"\U0001F23A]|[\U0001F250-\U0001F251]|[\U0001F300-\U0001F30F]|[\U0001F310-\U0001F31F]|[\U0001F320-\U0001F321]|[\U0001F324-\U0001F32F]|[\U0001F330-"
  507. @"\U0001F33F]|[\U0001F340-\U0001F34F]|[\U0001F350-\U0001F35F]|[\U0001F360-\U0001F36F]|[\U0001F370-\U0001F37F]|[\U0001F380-\U0001F38F]|[\U0001F390-"
  508. @"\U0001F393]|[\U0001F396-\U0001F397]|[\U0001F399-\U0001F39B]|[\U0001F39E-\U0001F39F]|[\U0001F3A0-\U0001F3AF]|[\U0001F3B0-\U0001F3BF]|[\U0001F3C0-"
  509. @"\U0001F3CF]|[\U0001F3D0-\U0001F3DF]|[\U0001F3E0-\U0001F3EF]|\U0001F3F0|[\U0001F3F3-\U0001F3F5]|[\U0001F3F7-\U0001F3FF]|[\U0001F400-\U0001F40F]|["
  510. @"\U0001F410-\U0001F41F]|[\U0001F420-\U0001F42F]|[\U0001F430-\U0001F43F]|[\U0001F440-\U0001F44F]|[\U0001F450-\U0001F45F]|[\U0001F460-\U0001F46F]|["
  511. @"\U0001F470-\U0001F47F]|[\U0001F480-\U0001F48F]|[\U0001F490-\U0001F49F]|[\U0001F4A0-\U0001F4AF]|[\U0001F4B0-\U0001F4BF]|[\U0001F4C0-\U0001F4CF]|["
  512. @"\U0001F4D0-\U0001F4DF]|[\U0001F4E0-\U0001F4EF]|[\U0001F4F0-\U0001F4FF]|[\U0001F500-\U0001F50F]|[\U0001F510-\U0001F51F]|[\U0001F520-\U0001F52F]|["
  513. @"\U0001F530-\U0001F53D]|[\U0001F549-\U0001F54E]|[\U0001F550-\U0001F55F]|[\U0001F560-\U0001F567]|\U0001F56F|\U0001F570|[\U0001F573-\U0001F57A]|"
  514. @"\U0001F587|[\U0001F58A-\U0001F58D]|\U0001F590|[\U0001F595-\U0001F596]|[\U0001F5A4-\U0001F5A5]|\U0001F5A8|[\U0001F5B1-\U0001F5B2]|\U0001F5BC|["
  515. @"\U0001F5C2-\U0001F5C4]|[\U0001F5D1-\U0001F5D3]|[\U0001F5DC-\U0001F5DE]|\U0001F5E1|\U0001F5E3|\U0001F5E8|\U0001F5EF|\U0001F5F3|[\U0001F5FA-\U0001F5FF]"
  516. @"|[\U0001F600-\U0001F60F]|[\U0001F610-\U0001F61F]|[\U0001F620-\U0001F62F]|[\U0001F630-\U0001F63F]|[\U0001F640-\U0001F64F]|[\U0001F650-\U0001F65F]|["
  517. @"\U0001F660-\U0001F66F]|[\U0001F670-\U0001F67F]|[\U0001F680-\U0001F68F]|[\U0001F690-\U0001F69F]|[\U0001F6A0-\U0001F6AF]|[\U0001F6B0-\U0001F6BF]|["
  518. @"\U0001F6C0-\U0001F6C5]|[\U0001F6CB-\U0001F6CF]|[\U0001F6D0-\U0001F6D2]|[\U0001F6D5-\U0001F6D7]|[\U0001F6DD-\U0001F6DF]|[\U0001F6E0-\U0001F6E5]|"
  519. @"\U0001F6E9|[\U0001F6EB-\U0001F6EC]|\U0001F6F0|[\U0001F6F3-\U0001F6FC]|[\U0001F7E0-\U0001F7EB]|\U0001F7F0|[\U0001F90C-\U0001F90F]|[\U0001F910-"
  520. @"\U0001F91F]|[\U0001F920-\U0001F92F]|[\U0001F930-\U0001F93A]|[\U0001F93C-\U0001F93F]|[\U0001F940-\U0001F945]|[\U0001F947-\U0001F94C]|[\U0001F94D-"
  521. @"\U0001F94F]|[\U0001F950-\U0001F95F]|[\U0001F960-\U0001F96F]|[\U0001F970-\U0001F97F]|[\U0001F980-\U0001F98F]|[\U0001F990-\U0001F99F]|[\U0001F9A0-"
  522. @"\U0001F9AF]|[\U0001F9B0-\U0001F9BF]|[\U0001F9C0-\U0001F9CF]|[\U0001F9D0-\U0001F9DF]|[\U0001F9E0-\U0001F9EF]|[\U0001F9F0-\U0001F9FF]|[\U0001FA70-"
  523. @"\U0001FA74]|[\U0001FA78-\U0001FA7C]|[\U0001FA80-\U0001FA86]|[\U0001FA90-\U0001FA9F]|[\U0001FAA0-\U0001FAAC]|[\U0001FAB0-\U0001FABA]|[\U0001FAC0-"
  524. @"\U0001FAC5]|[\U0001FAD0-\U0001FAD9]|[\U0001FAE0-\U0001FAE7]|[\U0001FAF0-\U0001FAF6]";
  525. NSString *emoji = [NSString stringWithFormat:@"[%@%@]", unsupport, support];
  526. /// Construct regex of emoji by the rules above.
  527. NSString *eMod = @"[\U0001F3FB-\U0001F3FF]";
  528. NSString *variationSelector = @"\uFE0F";
  529. NSString *keycap = @"\u20E3";
  530. NSString *tags = @"[\U000E0020-\U000E007E]";
  531. NSString *termTag = @"\U000E007F";
  532. NSString *zwj = @"\u200D";
  533. NSString *riSequence = [NSString stringWithFormat:@"[%@][%@]", ri, ri];
  534. NSString *element = [NSString stringWithFormat:@"[%@]([%@]|%@%@?|[%@]+%@)?", emoji, eMod, variationSelector, keycap, tags, termTag];
  535. NSString *regexEmoji = [NSString stringWithFormat:@"%@|%@(%@(%@|%@))*", riSequence, element, zwj, riSequence, element];
  536. return regexEmoji;
  537. }
  538. @end
  539. @implementation NSAttributedString (EmojiExtension)
  540. - (NSString *)tui_getPlainString {
  541. NSMutableString *plainString = [NSMutableString stringWithString:self.string];
  542. __block NSUInteger base = 0;
  543. [self enumerateAttribute:NSAttachmentAttributeName
  544. inRange:NSMakeRange(0, self.length)
  545. options:0
  546. usingBlock:^(id value, NSRange range, BOOL *stop) {
  547. if (value && [value isKindOfClass:[TUIEmojiTextAttachment class]]) {
  548. [plainString replaceCharactersInRange:NSMakeRange(range.location + base, range.length)
  549. withString:((TUIEmojiTextAttachment *)value).emojiTag];
  550. base += ((TUIEmojiTextAttachment *)value).emojiTag.length - 1;
  551. }
  552. }];
  553. return plainString;
  554. }
  555. @end