WBStatusComposeTextParser.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. //
  2. // WBStatusComposeTextParser.m
  3. // YYKitExample
  4. //
  5. // Created by ibireme on 15/9/5.
  6. // Copyright (C) 2015 ibireme. All rights reserved.
  7. //
  8. #import "WBStatusComposeTextParser.h"
  9. #import "WBStatusHelper.h"
  10. @implementation WBStatusComposeTextParser
  11. - (instancetype)init {
  12. self = [super init];
  13. _font = [UIFont systemFontOfSize:17];
  14. _textColor = [UIColor colorWithWhite:0.2 alpha:1];
  15. _highlightTextColor = UIColorHex(527ead);
  16. return self;
  17. }
  18. - (BOOL)parseText:(NSMutableAttributedString *)text selectedRange:(NSRangePointer)selectedRange {
  19. text.color = _textColor;
  20. // 此处没有进行优化,性能较低,只是为了功能演示
  21. {
  22. static NSArray *topicExts, *topicExtImages;
  23. static dispatch_once_t onceToken;
  24. dispatch_once(&onceToken, ^{
  25. topicExts = @[ @"[电影]#", @"[图书]#", @"[音乐]#", @"[地点]#", @"[股票]#" ];
  26. topicExtImages = @[
  27. [WBStatusHelper imageNamed:@"timeline_card_small_movie"],
  28. [WBStatusHelper imageNamed:@"timeline_card_small_book"],
  29. [WBStatusHelper imageNamed:@"timeline_card_small_music"],
  30. [WBStatusHelper imageNamed:@"timeline_card_small_location"],
  31. [WBStatusHelper imageNamed:@"timeline_card_small_stock"]
  32. ];
  33. });
  34. NSArray<NSTextCheckingResult *> *topicResults = [[WBStatusHelper regexTopic] matchesInString:text.string options:kNilOptions range:text.rangeOfAll];
  35. NSUInteger clipLength = 0;
  36. for (NSTextCheckingResult *topic in topicResults) {
  37. if (topic.range.location == NSNotFound && topic.range.length <= 1) continue;
  38. NSRange range = topic.range;
  39. range.location -= clipLength;
  40. __block BOOL containsBindingRange = NO;
  41. [text enumerateAttribute:YYTextBindingAttributeName inRange:range options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id value, NSRange range, BOOL *stop) {
  42. if (value) {
  43. containsBindingRange = YES;
  44. *stop = YES;
  45. }
  46. }];
  47. if (containsBindingRange) continue;
  48. BOOL hasExt = NO;
  49. NSString *subText = [text.string substringWithRange:range];
  50. for (NSUInteger i = 0; i < topicExts.count; i++) {
  51. NSString *ext = topicExts[i];
  52. if ([subText hasSuffix:ext] && subText.length > ext.length + 1) {
  53. NSMutableAttributedString *replace = [[NSMutableAttributedString alloc] initWithString:[subText substringWithRange:NSMakeRange(1, subText.length - 1 - ext.length)]];
  54. NSAttributedString *pic = [self _attachmentWithFontSize:_font.pointSize image:topicExtImages[i] shrink:YES];
  55. [replace insertAttributedString:pic atIndex:0];
  56. replace.font = _font;
  57. replace.color = _highlightTextColor;
  58. // original text, used for text copy
  59. YYTextBackedString *backed = [YYTextBackedString stringWithString:subText];
  60. [replace setTextBackedString:backed range:NSMakeRange(0, replace.length)];
  61. [text replaceCharactersInRange:range withAttributedString:replace];
  62. [text setTextBinding:[YYTextBinding bindingWithDeleteConfirm:YES] range:NSMakeRange(range.location, replace.length)];
  63. [text setColor:_highlightTextColor range:NSMakeRange(range.location, replace.length)];
  64. if (selectedRange) {
  65. *selectedRange = [self _replaceTextInRange:range withLength:replace.length selectedRange:*selectedRange];
  66. }
  67. clipLength += range.length - replace.length;
  68. hasExt = YES;
  69. break;
  70. }
  71. }
  72. if (!hasExt) {
  73. [text setColor:_highlightTextColor range:range];
  74. }
  75. }
  76. }
  77. {
  78. NSArray<NSTextCheckingResult *> *atResults = [[WBStatusHelper regexAt] matchesInString:text.string options:kNilOptions range:text.rangeOfAll];
  79. for (NSTextCheckingResult *at in atResults) {
  80. if (at.range.location == NSNotFound && at.range.length <= 1) continue;
  81. __block BOOL containsBindingRange = NO;
  82. [text enumerateAttribute:YYTextBindingAttributeName inRange:at.range options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id value, NSRange range, BOOL *stop) {
  83. if (value) {
  84. containsBindingRange = YES;
  85. *stop = YES;
  86. }
  87. }];
  88. if (containsBindingRange) continue;
  89. [text setColor:_highlightTextColor range:at.range];
  90. }
  91. }
  92. {
  93. NSArray<NSTextCheckingResult *> *emoticonResults = [[WBStatusHelper regexEmoticon] matchesInString:text.string options:kNilOptions range:text.rangeOfAll];
  94. NSUInteger clipLength = 0;
  95. for (NSTextCheckingResult *emo in emoticonResults) {
  96. if (emo.range.location == NSNotFound && emo.range.length <= 1) continue;
  97. NSRange range = emo.range;
  98. range.location -= clipLength;
  99. if ([text attribute:YYTextAttachmentAttributeName atIndex:range.location]) continue;
  100. NSString *emoString = [text.string substringWithRange:range];
  101. NSString *imagePath = [WBStatusHelper emoticonDic][emoString];
  102. UIImage *image = [WBStatusHelper imageWithPath:imagePath];
  103. if (!image) continue;
  104. __block BOOL containsBindingRange = NO;
  105. [text enumerateAttribute:YYTextBindingAttributeName inRange:range options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id value, NSRange range, BOOL *stop) {
  106. if (value) {
  107. containsBindingRange = YES;
  108. *stop = YES;
  109. }
  110. }];
  111. if (containsBindingRange) continue;
  112. YYTextBackedString *backed = [YYTextBackedString stringWithString:emoString];
  113. NSMutableAttributedString *emoText = [NSAttributedString attachmentStringWithEmojiImage:image fontSize:_font.pointSize].mutableCopy;
  114. // original text, used for text copy
  115. [emoText setTextBackedString:backed range:NSMakeRange(0, emoText.length)];
  116. [emoText setTextBinding:[YYTextBinding bindingWithDeleteConfirm:NO] range:NSMakeRange(0, emoText.length)];
  117. [text replaceCharactersInRange:range withAttributedString:emoText];
  118. if (selectedRange) {
  119. *selectedRange = [self _replaceTextInRange:range withLength:emoText.length selectedRange:*selectedRange];
  120. }
  121. clipLength += range.length - emoText.length;
  122. }
  123. }
  124. [text enumerateAttribute:YYTextBindingAttributeName inRange:text.rangeOfAll options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id value, NSRange range, BOOL *stop) {
  125. if (value && range.length > 1) {
  126. [text setColor:_highlightTextColor range:range];
  127. }
  128. }];
  129. text.font = _font;
  130. return YES;
  131. }
  132. // correct the selected range during text replacement
  133. - (NSRange)_replaceTextInRange:(NSRange)range withLength:(NSUInteger)length selectedRange:(NSRange)selectedRange {
  134. // no change
  135. if (range.length == length) return selectedRange;
  136. // right
  137. if (range.location >= selectedRange.location + selectedRange.length) return selectedRange;
  138. // left
  139. if (selectedRange.location >= range.location + range.length) {
  140. selectedRange.location = selectedRange.location + length - range.length;
  141. return selectedRange;
  142. }
  143. // same
  144. if (NSEqualRanges(range, selectedRange)) {
  145. selectedRange.length = length;
  146. return selectedRange;
  147. }
  148. // one edge same
  149. if ((range.location == selectedRange.location && range.length < selectedRange.length) ||
  150. (range.location + range.length == selectedRange.location + selectedRange.length && range.length < selectedRange.length)) {
  151. selectedRange.length = selectedRange.length + length - range.length;
  152. return selectedRange;
  153. }
  154. selectedRange.location = range.location + length;
  155. selectedRange.length = 0;
  156. return selectedRange;
  157. }
  158. - (NSAttributedString *)_attachmentWithFontSize:(CGFloat)fontSize image:(UIImage *)image shrink:(BOOL)shrink {
  159. // CGFloat ascent = YYEmojiGetAscentWithFontSize(fontSize);
  160. // CGFloat descent = YYEmojiGetDescentWithFontSize(fontSize);
  161. // CGRect bounding = YYEmojiGetGlyphBoundingRectWithFontSize(fontSize);
  162. // Heiti SC 字体。。
  163. CGFloat ascent = fontSize * 0.86;
  164. CGFloat descent = fontSize * 0.14;
  165. CGRect bounding = CGRectMake(0, -0.14 * fontSize, fontSize, fontSize);
  166. UIEdgeInsets contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), 0, descent + bounding.origin.y, 0);
  167. YYTextRunDelegate *delegate = [YYTextRunDelegate new];
  168. delegate.ascent = ascent;
  169. delegate.descent = descent;
  170. delegate.width = bounding.size.width;
  171. YYTextAttachment *attachment = [YYTextAttachment new];
  172. attachment.contentMode = UIViewContentModeScaleAspectFit;
  173. attachment.contentInsets = contentInsets;
  174. attachment.content = image;
  175. if (shrink) {
  176. // 缩小~
  177. CGFloat scale = 1 / 10.0;
  178. contentInsets.top += fontSize * scale;
  179. contentInsets.bottom += fontSize * scale;
  180. contentInsets.left += fontSize * scale;
  181. contentInsets.right += fontSize * scale;
  182. contentInsets = UIEdgeInsetPixelFloor(contentInsets);
  183. attachment.contentInsets = contentInsets;
  184. }
  185. NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
  186. [atr setTextAttachment:attachment range:NSMakeRange(0, atr.length)];
  187. CTRunDelegateRef ctDelegate = delegate.CTRunDelegate;
  188. [atr setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)];
  189. if (ctDelegate) CFRelease(ctDelegate);
  190. return atr;
  191. }
  192. @end