MDHTMLLabel.m 77 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066
  1. //
  2. // MDHTMLLabel.m
  3. // MDHTMLLabel
  4. //
  5. // Copyright (c) 2013 Matt Donnelly
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. //
  25. #import "MDHTMLLabel.h"
  26. #define kMDLineBreakWordWrapTextWidthScalingFactor (M_PI / M_E)
  27. static CGFloat const MDFLOAT_MAX = 100000;
  28. const NSTextAlignment MDTextAlignmentLeft = NSTextAlignmentLeft;
  29. const NSTextAlignment MDTextAlignmentCenter = NSTextAlignmentCenter;
  30. const NSTextAlignment MDTextAlignmentRight = NSTextAlignmentRight;
  31. const NSTextAlignment MDTextAlignmentJustified = NSTextAlignmentJustified;
  32. const NSTextAlignment MDTextAlignmentNatural = NSTextAlignmentNatural;
  33. const NSLineBreakMode MDLineBreakByWordWrapping = NSLineBreakByWordWrapping;
  34. const NSLineBreakMode MDLineBreakByCharWrapping = NSLineBreakByCharWrapping;
  35. const NSLineBreakMode MDLineBreakByClipping = NSLineBreakByClipping;
  36. const NSLineBreakMode MDLineBreakByTruncatingHead = NSLineBreakByTruncatingHead;
  37. const NSLineBreakMode MDLineBreakByTruncatingMiddle = NSLineBreakByTruncatingMiddle;
  38. const NSLineBreakMode MDLineBreakByTruncatingTail = NSLineBreakByTruncatingTail;
  39. typedef NSTextAlignment MDTextAlignment;
  40. typedef NSLineBreakMode MDLineBreakMode;
  41. static inline CTTextAlignment CTTextAlignmentFromMDTextAlignment(MDTextAlignment alignment)
  42. {
  43. switch (alignment)
  44. {
  45. case NSTextAlignmentLeft: return kCTLeftTextAlignment;
  46. case NSTextAlignmentCenter: return kCTCenterTextAlignment;
  47. case NSTextAlignmentRight: return kCTRightTextAlignment;
  48. default: return kCTNaturalTextAlignment;
  49. }
  50. }
  51. static inline CTLineBreakMode CTLineBreakModeFromMDLineBreakMode(MDLineBreakMode lineBreakMode)
  52. {
  53. switch (lineBreakMode)
  54. {
  55. case NSLineBreakByWordWrapping: return kCTLineBreakByWordWrapping;
  56. case NSLineBreakByCharWrapping: return kCTLineBreakByCharWrapping;
  57. case NSLineBreakByClipping: return kCTLineBreakByClipping;
  58. case NSLineBreakByTruncatingHead: return kCTLineBreakByTruncatingHead;
  59. case NSLineBreakByTruncatingTail: return kCTLineBreakByTruncatingTail;
  60. case NSLineBreakByTruncatingMiddle: return kCTLineBreakByTruncatingMiddle;
  61. default: return 0;
  62. }
  63. }
  64. static inline CFRange CFRangeFromNSRange(NSRange range)
  65. {
  66. return CFRangeMake(range.location, range.length);
  67. }
  68. static inline NSArray * CGColorComponentsForHex(NSString *hexColor)
  69. {
  70. hexColor = [[hexColor stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
  71. NSRange range;
  72. range.location = 0;
  73. range.length = 2;
  74. NSString *rString = [hexColor substringWithRange:range];
  75. range.location = 2;
  76. NSString *gString = [hexColor substringWithRange:range];
  77. range.location = 4;
  78. NSString *bString = [hexColor substringWithRange:range];
  79. unsigned int r, g, b;
  80. [[NSScanner scannerWithString:rString] scanHexInt:&r];
  81. [[NSScanner scannerWithString:gString] scanHexInt:&g];
  82. [[NSScanner scannerWithString:bString] scanHexInt:&b];
  83. NSArray *components = @[[NSNumber numberWithFloat:((float) r / 255.0f)],
  84. [NSNumber numberWithFloat:((float) g / 255.0f)],
  85. [NSNumber numberWithFloat:((float) b / 255.0f)],
  86. [NSNumber numberWithFloat:1.0]];
  87. return components;
  88. }
  89. static inline CGFLOAT_TYPE CGFloat_ceil(CGFLOAT_TYPE cgfloat)
  90. {
  91. #if defined(__LP64__) && __LP64__
  92. return ceil(cgfloat);
  93. #else
  94. return ceilf(cgfloat);
  95. #endif
  96. }
  97. static inline CGFLOAT_TYPE CGFloat_floor(CGFLOAT_TYPE cgfloat)
  98. {
  99. #if defined(__LP64__) && __LP64__
  100. return floor(cgfloat);
  101. #else
  102. return floorf(cgfloat);
  103. #endif
  104. }
  105. static inline NSAttributedString * NSAttributedStringByScalingFontSize(NSAttributedString *attributedString,
  106. CGFloat scale)
  107. {
  108. NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
  109. [mutableAttributedString enumerateAttribute:(NSString *)kCTFontAttributeName inRange:NSMakeRange(0, [mutableAttributedString length])
  110. options:0
  111. usingBlock:^(id value, NSRange range, BOOL * __unused stop)
  112. {
  113. UIFont *font = (UIFont *)value;
  114. if (font)
  115. {
  116. NSString *fontName;
  117. CGFloat pointSize;
  118. if ([font isKindOfClass:[UIFont class]])
  119. {
  120. fontName = font.fontName;
  121. pointSize = font.pointSize;
  122. }
  123. else
  124. {
  125. fontName = (NSString *)CFBridgingRelease(CTFontCopyName((__bridge CTFontRef)font, kCTFontPostScriptNameKey));
  126. pointSize = CTFontGetSize((__bridge CTFontRef)font);
  127. }
  128. [mutableAttributedString removeAttribute:(NSString *)kCTFontAttributeName range:range];
  129. CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)fontName, CGFloat_floor(pointSize * scale), NULL);
  130. [mutableAttributedString addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)fontRef range:range];
  131. CFRelease(fontRef);
  132. }
  133. }];
  134. return mutableAttributedString;
  135. }
  136. static inline CGSize CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter,
  137. NSAttributedString *attributedString,
  138. CGSize size,
  139. NSUInteger numberOfLines)
  140. {
  141. CFRange rangeToSize = CFRangeMake(0, (CFIndex)attributedString.length);
  142. CGSize constraints = CGSizeMake(size.width, MDFLOAT_MAX);
  143. if (numberOfLines == 1)
  144. {
  145. // If there is one line, the size that fits is the full width of the line
  146. constraints = CGSizeMake(MDFLOAT_MAX, MDFLOAT_MAX);
  147. }
  148. else if (numberOfLines > 0)
  149. {
  150. // If the line count of the label more than 1, limit the range to size to the number of lines that have been set
  151. CGMutablePathRef path = CGPathCreateMutable();
  152. CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, MDFLOAT_MAX));
  153. CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
  154. CFArrayRef lines = CTFrameGetLines(frame);
  155. if (CFArrayGetCount(lines) > 0)
  156. {
  157. NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1;
  158. CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
  159. CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
  160. rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
  161. }
  162. CFRelease(frame);
  163. CFRelease(path);
  164. }
  165. CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL);
  166. return CGSizeMake(CGFloat_ceil(suggestedSize.width), CGFloat_ceil(suggestedSize.height));
  167. }
  168. #pragma mark - MDHTMLComponent
  169. @interface MDHTMLComponent : NSObject
  170. @property (nonatomic, assign) NSInteger componentIndex;
  171. @property (nonatomic, copy) NSString *text;
  172. @property (nonatomic, copy) NSString *htmlTag;
  173. @property (nonatomic) NSMutableDictionary *attributes;
  174. @property (nonatomic, assign) NSInteger position;
  175. - (id)initWithString:(NSString *)string
  176. htmlTag:(NSString *)htmlTag
  177. attributes:(NSMutableDictionary *)attributes;
  178. - (id)initWithTag:(NSString *)htmlTag
  179. position:(NSInteger)position
  180. attributes:(NSMutableDictionary *)attributes;
  181. - (NSRange)range;
  182. @end
  183. @implementation MDHTMLComponent
  184. - (id)initWithString:(NSString *)string
  185. htmlTag:(NSString *)htmlTag
  186. attributes:(NSMutableDictionary *)attributes
  187. {
  188. self = [super init];
  189. if (self)
  190. {
  191. self.text = string;
  192. self.htmlTag = htmlTag;
  193. self.attributes = attributes;
  194. }
  195. return self;
  196. }
  197. - (id)initWithTag:(NSString *)htmlTag
  198. position:(NSInteger)position
  199. attributes:(NSMutableDictionary *)attributes
  200. {
  201. self = [super init];
  202. if (self)
  203. {
  204. self.htmlTag = htmlTag;
  205. self.position = position;
  206. self.attributes = attributes;
  207. }
  208. return self;
  209. }
  210. - (NSRange)range
  211. {
  212. return NSMakeRange(self.position, self.text.length);
  213. }
  214. - (NSString *)description
  215. {
  216. NSMutableString *desc = [NSMutableString string];
  217. [desc appendFormat:@"Text: %@", self.text];
  218. [desc appendFormat:@"\nPosition: %li", (long)_position];
  219. if (self.htmlTag)
  220. {
  221. [desc appendFormat:@"\nHTML Tag: %@", self.htmlTag];
  222. }
  223. if (self.attributes)
  224. {
  225. [desc appendFormat:@"\nAttributes: %@", self.attributes];
  226. }
  227. return desc;
  228. }
  229. @end
  230. #pragma mark - MDHTMLLabel
  231. @interface MDHTMLLabel ()
  232. @property (nonatomic, copy) NSString *plainText;
  233. @property (nonatomic, copy) NSAttributedString *inactiveAttributedText;
  234. @property (nonatomic, assign) BOOL needsFramesetter;
  235. @property (nonatomic, strong) NSDataDetector *dataDetector;
  236. @property (nonatomic, strong) NSMutableArray *links;
  237. @property (nonatomic, strong) NSTextCheckingResult *activeLink;
  238. @property (nonatomic, strong) NSTimer *holdGestureTimer;
  239. @property (nonatomic, strong) NSMutableArray *styleComponents;
  240. @property (nonatomic, strong) NSMutableArray *highlightedStyleComponents;
  241. - (NSString *)detectURLsInText:(NSString *)text;
  242. - (void)extractStyleFromText:(NSString *)text;
  243. - (void)setNeedsFramesetter;
  244. - (NSAttributedString *)applyStylesToString:(NSString *)string;
  245. - (void)applyItalicStyleToText:(CFMutableAttributedStringRef)text
  246. range:(NSRange)range;
  247. - (void)applyBoldStyleToText:(CFMutableAttributedStringRef)text
  248. range:(NSRange)range;
  249. - (void)applyBoldItalicStyleToText:(CFMutableAttributedStringRef)text
  250. range:(NSRange)range;
  251. - (void)applyColor:(id)value
  252. toText:(CFMutableAttributedStringRef)text
  253. range:(NSRange)range;
  254. - (void)applyUnderlineColor:(NSString *)value
  255. toText:(CFMutableAttributedStringRef)text
  256. range:(NSRange)range;
  257. - (void)applyFontAttributes:(NSDictionary *)attributes
  258. toText:(CFMutableAttributedStringRef)text
  259. range:(NSRange)range;
  260. - (void)applyParagraphStyleToText:(CFMutableAttributedStringRef)text
  261. attributes:(NSMutableDictionary *)attributes
  262. range:(NSRange)range;
  263. @end
  264. @implementation MDHTMLLabel
  265. {
  266. @private
  267. NSAttributedString *_htmlAttributedText;
  268. BOOL _needsFramesetter;
  269. CTFramesetterRef _framesetter;
  270. CTFramesetterRef _highlightFramesetter;
  271. }
  272. #pragma mark - Initialization
  273. - (id)init
  274. {
  275. self = [super init];
  276. if (self)
  277. {
  278. [self commonInit];
  279. }
  280. return self;
  281. }
  282. - (id)initWithFrame:(CGRect)frame
  283. {
  284. self = [super initWithFrame:frame];
  285. if (self)
  286. {
  287. [self commonInit];
  288. }
  289. return self;
  290. }
  291. - (void)commonInit
  292. {
  293. self.userInteractionEnabled = YES;
  294. self.multipleTouchEnabled = NO;
  295. self.textInsets = UIEdgeInsetsZero;
  296. self.links = [NSMutableArray array];
  297. self.minimumPressDuration = 0.5;
  298. self.autoDetectUrls = YES;
  299. self.linkAttributes = [NSDictionary dictionary];
  300. self.activeLinkAttributes = [NSDictionary dictionary];
  301. self.inactiveLinkAttributes = [NSDictionary dictionary];
  302. }
  303. - (void)dealloc
  304. {
  305. if (_framesetter)
  306. {
  307. CFRelease(_framesetter);
  308. }
  309. if (_highlightFramesetter)
  310. {
  311. CFRelease(_highlightFramesetter);
  312. }
  313. }
  314. #pragma mark - Accessors
  315. - (void)setText:(NSString *)text
  316. {
  317. self.htmlText = nil;
  318. [super setText:text];
  319. }
  320. - (void)setAttributedText:(NSAttributedString *)attributedText
  321. {
  322. self.htmlText = nil;
  323. [super setAttributedText:attributedText];
  324. }
  325. - (void)setHtmlText:(NSString *)htmlText
  326. {
  327. // if(![[htmlText class] isKindOfClass:[NSString class]])
  328. // htmlText=nil;
  329. if ([_htmlText isEqualToString:htmlText])
  330. {
  331. return;
  332. }
  333. _htmlText = [htmlText copy];
  334. _htmlAttributedText = nil;
  335. if (_htmlText)
  336. {
  337. htmlText = [_htmlText stringByReplacingOccurrencesOfString:@"<br>" withString:@"\n"];
  338. if (self.autoDetectUrls)
  339. {
  340. htmlText = [self detectURLsInText:_htmlText];
  341. }
  342. [self extractStyleFromText:_htmlText];
  343. }
  344. else
  345. {
  346. self.styleComponents = nil;
  347. self.plainText = nil;
  348. self.links = [NSMutableArray array];
  349. }
  350. [self setNeedsFramesetter];
  351. [self setNeedsDisplay];
  352. [self invalidateIntrinsicContentSize];
  353. }
  354. - (NSAttributedString *)htmlAttributedText
  355. {
  356. if (!_htmlAttributedText)
  357. {
  358. self.htmlAttributedText = [self applyStylesToString:_plainText];
  359. }
  360. return _htmlAttributedText;
  361. }
  362. - (void)setHtmlAttributedText:(NSAttributedString *)htmlAttributedText
  363. {
  364. if ([_htmlAttributedText isEqualToAttributedString:htmlAttributedText])
  365. {
  366. return;
  367. }
  368. _htmlAttributedText = [htmlAttributedText copy];
  369. [self setNeedsFramesetter];
  370. [self setNeedsDisplay];
  371. }
  372. - (void)setNeedsFramesetter
  373. {
  374. _needsFramesetter = YES;
  375. }
  376. - (CTFramesetterRef)framesetter
  377. {
  378. if (_needsFramesetter)
  379. {
  380. @synchronized(self)
  381. {
  382. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.htmlAttributedText);
  383. self.framesetter = framesetter;
  384. _needsFramesetter = NO;
  385. if (framesetter)
  386. {
  387. CFRelease(framesetter);
  388. }
  389. }
  390. }
  391. return _framesetter;
  392. }
  393. - (CTFramesetterRef)highlightFramesetter
  394. {
  395. return _highlightFramesetter;
  396. }
  397. - (void)setHighlightFramesetter:(CTFramesetterRef)highlightFramesetter
  398. {
  399. if (highlightFramesetter)
  400. {
  401. CFRetain(highlightFramesetter);
  402. }
  403. if (_highlightFramesetter)
  404. {
  405. CFRelease(_highlightFramesetter);
  406. }
  407. _highlightFramesetter = highlightFramesetter;
  408. }
  409. - (void)setFramesetter:(CTFramesetterRef)framesetter
  410. {
  411. if (framesetter)
  412. {
  413. CFRetain(framesetter);
  414. }
  415. if (_framesetter)
  416. {
  417. CFRelease(_framesetter);
  418. }
  419. _framesetter = framesetter;
  420. }
  421. - (void)setActiveLink:(NSTextCheckingResult *)activeLink
  422. {
  423. _activeLink = activeLink;
  424. if (_activeLink && self.activeLinkAttributes.count > 0)
  425. {
  426. if (!self.inactiveAttributedText)
  427. {
  428. self.inactiveAttributedText = [self.htmlAttributedText copy];
  429. }
  430. NSMutableAttributedString *mutableAttributedString = [self.inactiveAttributedText mutableCopy];
  431. if (NSLocationInRange(NSMaxRange(_activeLink.range), NSMakeRange(0, self.inactiveAttributedText.length + 1)))
  432. {
  433. NSMutableDictionary *mutableActiveLinkAttributes = [self.activeLinkAttributes mutableCopy];
  434. if (!mutableActiveLinkAttributes[(NSString *)kCTForegroundColorAttributeName] && !mutableActiveLinkAttributes[NSForegroundColorAttributeName])
  435. {
  436. mutableActiveLinkAttributes[(NSString *)kCTForegroundColorAttributeName] = [UIColor redColor];
  437. }
  438. [self applyFontAttributes:mutableActiveLinkAttributes
  439. toText:(__bridge CFMutableAttributedStringRef)mutableAttributedString
  440. range:_activeLink.range];
  441. }
  442. self.htmlAttributedText = mutableAttributedString;
  443. [self setNeedsDisplay];
  444. }
  445. else if (self.inactiveAttributedText)
  446. {
  447. self.htmlAttributedText = self.inactiveAttributedText;
  448. self.inactiveAttributedText = nil;
  449. [self setNeedsDisplay];
  450. }
  451. }
  452. #pragma mark - Drawing
  453. - (void)drawTextInRect:(CGRect)rect
  454. {
  455. if (!self.htmlText)
  456. {
  457. return [super drawTextInRect:rect];
  458. }
  459. NSAttributedString *originalAttributedText = nil;
  460. // Adjust the font size to fit width, if necessarry
  461. if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 0)
  462. {
  463. // Use infinite width to find the max width, which will be compared to availableWidth if needed.
  464. CGSize maxSize = (self.numberOfLines > 1) ? CGSizeMake(MDFLOAT_MAX, MDFLOAT_MAX) : CGSizeZero;
  465. CGFloat textWidth = [self sizeThatFits:maxSize].width;
  466. CGFloat availableWidth = self.frame.size.width * self.numberOfLines;
  467. if (self.numberOfLines > 1 && self.lineBreakMode == MDLineBreakByWordWrapping)
  468. {
  469. textWidth *= kMDLineBreakWordWrapTextWidthScalingFactor;
  470. }
  471. if (textWidth > availableWidth && textWidth > 0.0f)
  472. {
  473. originalAttributedText = [self.htmlAttributedText copy];
  474. self.htmlAttributedText = NSAttributedStringByScalingFontSize(self.htmlAttributedText, availableWidth / textWidth);
  475. }
  476. }
  477. CGContextRef c = UIGraphicsGetCurrentContext();
  478. CGContextSaveGState(c);
  479. {
  480. CGContextSetTextMatrix(c, CGAffineTransformIdentity);
  481. // Inverts the CTM to match iOS coordinates (otherwise text draws upside-down; Mac OS's system is different)
  482. CGContextTranslateCTM(c, 0.0f, rect.size.height);
  483. CGContextScaleCTM(c, 1.0f, -1.0f);
  484. CFRange textRange = CFRangeMake(0, (CFIndex)self.htmlAttributedText.length);
  485. // First, get the text rect (which takes vertical centering into account)
  486. CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines];
  487. // CoreText draws it's text aligned to the bottom, so we move the CTM here to take our vertical offsets into account
  488. CGContextTranslateCTM(c, rect.origin.x, rect.size.height - textRect.origin.y - textRect.size.height);
  489. // Second, trace the shadow before the actual text, if we have one
  490. if (self.shadowColor && !self.highlighted)
  491. {
  492. CGContextSetShadowWithColor(c, self.shadowOffset, self.shadowRadius, self.shadowColor.CGColor);
  493. }
  494. else if (self.highlightedShadowColor)
  495. {
  496. CGContextSetShadowWithColor(c, self.highlightedShadowOffset, self.highlightedShadowRadius, self.highlightedShadowColor.CGColor);
  497. }
  498. // Finally, draw the text or highlighted text itself (on top of the shadow, if there is one)
  499. if (self.highlighted && self.highlightedTextColor)
  500. {
  501. NSMutableAttributedString *highlightAttributedString = [self.htmlAttributedText mutableCopy];
  502. [highlightAttributedString addAttribute:(__bridge NSString *)kCTForegroundColorAttributeName
  503. value:(id)self.highlightedTextColor.CGColor
  504. range:NSMakeRange(0, highlightAttributedString.length)];
  505. if (!self.highlightFramesetter)
  506. {
  507. CTFramesetterRef highlightFramesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)highlightAttributedString);
  508. [self setHighlightFramesetter:highlightFramesetter];
  509. CFRelease(highlightFramesetter);
  510. }
  511. [self drawFramesetter:self.highlightFramesetter attributedString:highlightAttributedString textRange:textRange inRect:textRect context:c];
  512. }
  513. else
  514. {
  515. [self drawFramesetter:self.framesetter attributedString:self.htmlAttributedText textRange:textRange inRect:textRect context:c];
  516. }
  517. // If we adjusted the font size, set it back to its original size
  518. if (originalAttributedText)
  519. {
  520. // Use ivar directly to avoid clearing out framesetter and renderedAttributedText
  521. _htmlAttributedText = originalAttributedText;
  522. }
  523. }
  524. CGContextRestoreGState(c);
  525. }
  526. - (void)drawFramesetter:(CTFramesetterRef)framesetter
  527. attributedString:(NSAttributedString *)attributedString
  528. textRange:(CFRange)textRange
  529. inRect:(CGRect)rect
  530. context:(CGContextRef)c
  531. {
  532. CGMutablePathRef path = CGPathCreateMutable();
  533. CGPathAddRect(path, NULL, rect);
  534. CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL);
  535. CFArrayRef lines = CTFrameGetLines(frame);
  536. NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
  537. BOOL truncateLastLine = (self.lineBreakMode == MDLineBreakByTruncatingHead
  538. || self.lineBreakMode == MDLineBreakByTruncatingMiddle
  539. || self.lineBreakMode == MDLineBreakByTruncatingTail);
  540. CGPoint lineOrigins[numberOfLines];
  541. CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
  542. for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++)
  543. {
  544. CGPoint lineOrigin = lineOrigins[lineIndex];
  545. CGContextSetTextPosition(c, lineOrigin.x, lineOrigin.y);
  546. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  547. if (lineIndex == numberOfLines - 1 && truncateLastLine)
  548. {
  549. // Check if the range of text in the last line reaches the end of the full attributed string
  550. CFRange lastLineRange = CTLineGetStringRange(line);
  551. if (!(lastLineRange.length == 0 && lastLineRange.location == 0) && lastLineRange.location + lastLineRange.length < textRange.location + textRange.length)
  552. {
  553. // Get correct truncationType and attribute position
  554. CTLineTruncationType truncationType;
  555. CFIndex truncationAttributePosition = lastLineRange.location;
  556. NSLineBreakMode lineBreakMode = self.lineBreakMode;
  557. // Multiple lines, only use NSLineBreakByTruncatingTail
  558. if (numberOfLines != 1)
  559. {
  560. lineBreakMode = NSLineBreakByTruncatingTail;
  561. }
  562. switch (lineBreakMode)
  563. {
  564. case NSLineBreakByTruncatingHead:
  565. truncationType = kCTLineTruncationStart;
  566. break;
  567. case NSLineBreakByTruncatingMiddle:
  568. truncationType = kCTLineTruncationMiddle;
  569. truncationAttributePosition += (lastLineRange.length / 2);
  570. break;
  571. case NSLineBreakByTruncatingTail:
  572. default:
  573. truncationType = kCTLineTruncationEnd;
  574. truncationAttributePosition += (lastLineRange.length - 1);
  575. break;
  576. }
  577. NSString *truncationTokenString = self.truncationTokenString;
  578. if (!truncationTokenString)
  579. {
  580. truncationTokenString = @"\u2026"; // Unicode Character 'HORIZONTAL ELLIPSIS' (U+2026)
  581. }
  582. NSDictionary *truncationTokenStringAttributes = self.truncationTokenStringAttributes;
  583. if (!truncationTokenStringAttributes)
  584. {
  585. truncationTokenStringAttributes = [attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition
  586. effectiveRange:NULL];
  587. }
  588. NSAttributedString *attributedTokenString = [[NSAttributedString alloc] initWithString:truncationTokenString
  589. attributes:truncationTokenStringAttributes];
  590. CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedTokenString);
  591. // Append truncationToken to the string
  592. // because if string isn't too long, CT wont add the truncationToken on it's own
  593. // There is no change of a double truncationToken because CT only add the token if it removes characters (and the one we add will go first)
  594. NSMutableAttributedString *truncationString = [[attributedString attributedSubstringFromRange:NSMakeRange((NSUInteger)lastLineRange.location,
  595. (NSUInteger)lastLineRange.length)] mutableCopy];
  596. if (lastLineRange.length > 0)
  597. {
  598. // Remove any newline at the end (we don't want newline space between the text and the truncation token). There can only be one, because the second would be on the next line.
  599. unichar lastCharacter = [truncationString.string characterAtIndex:(NSUInteger)(lastLineRange.length - 1)];
  600. if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter])
  601. {
  602. [truncationString deleteCharactersInRange:NSMakeRange((NSUInteger)(lastLineRange.length - 1), 1)];
  603. }
  604. }
  605. [truncationString appendAttributedString:attributedTokenString];
  606. CTLineRef truncationLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationString);
  607. // Truncate the line in case it is too long.
  608. CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken);
  609. if (!truncatedLine)
  610. {
  611. // If the line is not as wide as the truncationToken, truncatedLine is NULL
  612. truncatedLine = CFRetain(truncationToken);
  613. }
  614. // Adjust pen offset for flush depending on text alignment
  615. CGFloat flushFactor = 0.0f;
  616. switch (self.textAlignment)
  617. {
  618. case NSTextAlignmentCenter:
  619. flushFactor = 0.5f;
  620. break;
  621. case NSTextAlignmentRight:
  622. flushFactor = 1.0f;
  623. break;
  624. case NSTextAlignmentLeft:
  625. default:
  626. break;
  627. }
  628. CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(truncatedLine, flushFactor, rect.size.width);
  629. CGContextSetTextPosition(c, penOffset, lineOrigin.y);
  630. CTLineDraw(truncatedLine, c);
  631. CFRelease(truncatedLine);
  632. CFRelease(truncationLine);
  633. CFRelease(truncationToken);
  634. }
  635. else
  636. {
  637. CTLineDraw(line, c);
  638. }
  639. }
  640. else
  641. {
  642. CTLineDraw(line, c);
  643. }
  644. }
  645. CFRelease(frame);
  646. CFRelease(path);
  647. }
  648. #pragma mark - Styling methods
  649. - (NSAttributedString *)applyStylesToString:(NSString *)string
  650. {
  651. if (!string)
  652. {
  653. return nil;
  654. }
  655. // Create attributed string ref for text
  656. CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
  657. CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), (__bridge CFStringRef)string);
  658. // Apply text color to text
  659. CFMutableDictionaryRef styleDict = CFDictionaryCreateMutable(0, 0, 0, 0);
  660. CFDictionaryAddValue(styleDict, kCTForegroundColorAttributeName, self.textColor.CGColor);
  661. CFAttributedStringSetAttributes(attrString, CFRangeMake( 0, CFAttributedStringGetLength(attrString)), styleDict, 0);
  662. CFRelease(styleDict);
  663. // Apply default paragraph text style
  664. [self applyParagraphStyleToText:attrString attributes:nil range:NSMakeRange(0, string.length)];
  665. // Apply font to text
  666. CTFontRef font = CTFontCreateWithName ((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL);
  667. CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTFontAttributeName, font);
  668. CFRelease(font);
  669. NSMutableArray *styleComponents = nil;
  670. if (self.highlighted)
  671. {
  672. styleComponents = self.highlightedStyleComponents;
  673. }
  674. else
  675. {
  676. styleComponents = self.styleComponents;
  677. }
  678. // Loop through each component and apply its style to the text
  679. for (MDHTMLComponent *component in styleComponents)
  680. {
  681. NSInteger index = [styleComponents indexOfObject:component];
  682. component.componentIndex = index;
  683. if (component.range.location == NSNotFound || component.range.length == 0) {
  684. // ignore bad tags, like <b><b>
  685. continue;
  686. }
  687. if ([component.htmlTag caseInsensitiveCompare:@"i"] == NSOrderedSame)
  688. {
  689. [self applyItalicStyleToText:attrString
  690. range:component.range];
  691. }
  692. else if ([component.htmlTag caseInsensitiveCompare:@"b"] == NSOrderedSame
  693. || [component.htmlTag caseInsensitiveCompare:@"strong"] == NSOrderedSame)
  694. {
  695. [self applyBoldStyleToText:attrString
  696. range:component.range];
  697. }
  698. else if ([component.htmlTag caseInsensitiveCompare:@"bi"] == NSOrderedSame)
  699. {
  700. [self applyBoldItalicStyleToText:attrString
  701. range:component.range];
  702. }
  703. else if ([component.htmlTag caseInsensitiveCompare:@"a"] == NSOrderedSame)
  704. {
  705. NSMutableDictionary *mutableLinkAttributes = [self.linkAttributes mutableCopy];
  706. if (!self.linkAttributes[(NSString *)kCTForegroundColorAttributeName] && !mutableLinkAttributes[NSForegroundColorAttributeName])
  707. {
  708. if ([self respondsToSelector:@selector(tintColor)])
  709. {
  710. mutableLinkAttributes[(NSString *)kCTForegroundColorAttributeName] = self.tintColor;
  711. }
  712. else
  713. {
  714. mutableLinkAttributes[(NSString *)kCTForegroundColorAttributeName] = [UIColor blueColor];
  715. }
  716. }
  717. [self applyFontAttributes:mutableLinkAttributes
  718. toText:attrString
  719. range:component.range];
  720. }
  721. else if ([component.htmlTag caseInsensitiveCompare:@"u"] == NSOrderedSame || [component.htmlTag caseInsensitiveCompare:@"uu"] == NSOrderedSame)
  722. {
  723. if ([component.htmlTag caseInsensitiveCompare:@"u"] == NSOrderedSame)
  724. {
  725. CFAttributedStringSetAttribute(attrString, CFRangeFromNSRange(component.range),
  726. kCTUnderlineStyleAttributeName, (__bridge CFNumberRef)[NSNumber numberWithInt:kCTUnderlineStyleSingle]);
  727. }
  728. else if ([component.htmlTag caseInsensitiveCompare:@"uu"] == NSOrderedSame)
  729. {
  730. CFAttributedStringSetAttribute(attrString, CFRangeFromNSRange(component.range),
  731. kCTUnderlineStyleAttributeName, (__bridge CFNumberRef)[NSNumber numberWithInt:kCTUnderlineStyleDouble]);
  732. }
  733. if ([component.attributes objectForKey:(NSString *)kCTForegroundColorAttributeName])
  734. {
  735. id value = [component.attributes objectForKey:(NSString *)kCTForegroundColorAttributeName];
  736. [self applyUnderlineColor:value
  737. toText:attrString
  738. range:component.range];
  739. }
  740. }
  741. else if ([component.htmlTag caseInsensitiveCompare:@"font"] == NSOrderedSame)
  742. {
  743. [self applyFontAttributes:component.attributes
  744. toText:attrString
  745. range:component.range];
  746. }
  747. else if ([component.htmlTag caseInsensitiveCompare:@"p"] == NSOrderedSame)
  748. {
  749. [self applyParagraphStyleToText:attrString
  750. attributes:component.attributes
  751. range:component.range];
  752. }
  753. else if ([component.htmlTag caseInsensitiveCompare:@"center"] == NSOrderedSame)
  754. {
  755. [self applyCenterStyleToText:attrString
  756. attributes:component.attributes
  757. range:component.range];
  758. }
  759. }
  760. NSAttributedString *styledString = [[NSAttributedString alloc] initWithAttributedString:(__bridge NSAttributedString *)attrString];
  761. CFRelease(attrString);
  762. return styledString;
  763. }
  764. - (void)applyParagraphStyleToText:(CFMutableAttributedStringRef)text
  765. attributes:(NSMutableDictionary *)attributes
  766. range:(NSRange)range
  767. {
  768. CFMutableDictionaryRef styleDict = ( CFDictionaryCreateMutable( (0), 0, (0), (0) ) );
  769. CGFloat lineSpacing = self.leading;
  770. CGFloat lineSpacingAdjustment = CGFloat_ceil(self.font.lineHeight - self.font.ascender + self.font.descender);
  771. CGFloat lineHeightMultiple = self.lineHeightMultiple;
  772. CGFloat topMargin = self.textInsets.top;
  773. CGFloat bottomMargin = self.textInsets.bottom;
  774. CGFloat leftMargin = self.textInsets.left;
  775. CGFloat rightMargin = -self.textInsets.right;
  776. CGFloat firstLineIndent = self.firstLineIndent + leftMargin;
  777. CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
  778. if (self.numberOfLines == 1)
  779. {
  780. lineBreakMode = CTLineBreakModeFromMDLineBreakMode(self.lineBreakMode);
  781. }
  782. CTTextAlignment textAlignment = CTTextAlignmentFromMDTextAlignment(self.textAlignment);
  783. for (NSUInteger i = 0; i < attributes.allKeys.count; i++)
  784. {
  785. NSString *key = attributes.allKeys[i];
  786. id value = attributes[key];
  787. if ([key caseInsensitiveCompare:@"align"] == NSOrderedSame)
  788. {
  789. if ([value caseInsensitiveCompare:@"left"] == NSOrderedSame)
  790. {
  791. textAlignment = kCTLeftTextAlignment;
  792. }
  793. else if ([value caseInsensitiveCompare:@"right"] == NSOrderedSame)
  794. {
  795. textAlignment = kCTRightTextAlignment;
  796. }
  797. else if ([value caseInsensitiveCompare:@"justify"] == NSOrderedSame)
  798. {
  799. textAlignment = kCTJustifiedTextAlignment;
  800. }
  801. else if ([value caseInsensitiveCompare:@"center"] == NSOrderedSame)
  802. {
  803. textAlignment = kCTCenterTextAlignment;
  804. }
  805. }
  806. else if ([key caseInsensitiveCompare:@"indent"] == NSOrderedSame)
  807. {
  808. firstLineIndent = [value floatValue];
  809. }
  810. else if ([key caseInsensitiveCompare:@"linebreakmode"] == NSOrderedSame)
  811. {
  812. if ([value caseInsensitiveCompare:@"wordwrap"] == NSOrderedSame)
  813. {
  814. lineBreakMode = kCTLineBreakByWordWrapping;
  815. }
  816. else if ([value caseInsensitiveCompare:@"charwrap"] == NSOrderedSame)
  817. {
  818. lineBreakMode = kCTLineBreakByCharWrapping;
  819. }
  820. else if ([value caseInsensitiveCompare:@"clipping"] == NSOrderedSame)
  821. {
  822. lineBreakMode = kCTLineBreakByClipping;
  823. }
  824. else if ([value caseInsensitiveCompare:@"truncatinghead"] == NSOrderedSame)
  825. {
  826. lineBreakMode = kCTLineBreakByTruncatingHead;
  827. }
  828. else if ([value caseInsensitiveCompare:@"truncatingtail"] == NSOrderedSame)
  829. {
  830. lineBreakMode = kCTLineBreakByTruncatingTail;
  831. }
  832. else if ([value caseInsensitiveCompare:@"truncatingmiddle"] == NSOrderedSame)
  833. {
  834. lineBreakMode = kCTLineBreakByTruncatingMiddle;
  835. }
  836. }
  837. }
  838. CTParagraphStyleSetting settings[] =
  839. {
  840. { kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &textAlignment },
  841. { kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBreakMode },
  842. { kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing },
  843. { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacingAdjustment },
  844. { kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(CGFloat), &lineHeightMultiple },
  845. { kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), &topMargin },
  846. { kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), &bottomMargin },
  847. { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineIndent },
  848. { kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &leftMargin },
  849. { kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), &rightMargin },
  850. };
  851. CTParagraphStyleRef paragraphRef = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(CTParagraphStyleSetting));
  852. CFDictionaryAddValue(styleDict, kCTParagraphStyleAttributeName, paragraphRef);
  853. CFAttributedStringSetAttributes(text, CFRangeFromNSRange(range), styleDict, 0);
  854. CFRelease(paragraphRef);
  855. CFRelease(styleDict);
  856. }
  857. - (void)applyCenterStyleToText:(CFMutableAttributedStringRef)text
  858. attributes:(NSMutableDictionary *)attributes
  859. range:(NSRange)range
  860. {
  861. CFMutableDictionaryRef styleDict = CFDictionaryCreateMutable(0, 0, 0, 0) ;
  862. CGFloat lineSpacing = self.leading;
  863. CGFloat lineSpacingAdjustment = CGFloat_ceil(self.font.lineHeight - self.font.ascender + self.font.descender);
  864. CGFloat lineHeightMultiple = self.lineHeightMultiple;
  865. CGFloat topMargin = self.textInsets.top;
  866. CGFloat bottomMargin = self.textInsets.bottom;
  867. CGFloat leftMargin = self.textInsets.left;
  868. CGFloat rightMargin = -self.textInsets.right;
  869. CGFloat firstLineIndent = self.firstLineIndent + leftMargin;
  870. CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
  871. if (self.numberOfLines == 1)
  872. {
  873. lineBreakMode = CTLineBreakModeFromMDLineBreakMode(self.lineBreakMode);
  874. }
  875. CTTextAlignment textAlignment = kCTCenterTextAlignment;
  876. CTParagraphStyleSetting settings[] =
  877. {
  878. { kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &textAlignment },
  879. { kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBreakMode },
  880. { kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing },
  881. { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacingAdjustment },
  882. { kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(CGFloat), &lineHeightMultiple },
  883. { kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), &topMargin },
  884. { kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), &bottomMargin },
  885. { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineIndent },
  886. { kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &leftMargin },
  887. { kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), &rightMargin },
  888. };
  889. CTParagraphStyleRef paragraphRef = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(CTParagraphStyleSetting));
  890. CFDictionaryAddValue(styleDict, kCTParagraphStyleAttributeName, paragraphRef);
  891. CFAttributedStringSetAttributes( text, CFRangeFromNSRange(range), styleDict, 0 );
  892. CFRelease(paragraphRef);
  893. CFRelease(styleDict);
  894. }
  895. - (void)applyItalicStyleToText:(CFMutableAttributedStringRef)text
  896. range:(NSRange)range
  897. {
  898. CFTypeRef actualFontRef = CFAttributedStringGetAttribute(text, range.location, kCTFontAttributeName, NULL);
  899. CTFontRef italicFontRef = CTFontCreateCopyWithSymbolicTraits(actualFontRef, 0.0, NULL, kCTFontItalicTrait, kCTFontItalicTrait);
  900. BOOL forceCustomFont = self.customItalicFontName;
  901. if (!italicFontRef || forceCustomFont)
  902. {
  903. UIFont *font = [self italicFontOfSize:CTFontGetSize(actualFontRef)];
  904. italicFontRef = CTFontCreateWithName ((__bridge CFStringRef)[font fontName], [font pointSize], NULL);
  905. }
  906. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, italicFontRef);
  907. CFRelease(italicFontRef);
  908. }
  909. - (void)applyFontAttributes:(NSDictionary *)attributes
  910. toText:(CFMutableAttributedStringRef)text
  911. range:(NSRange)range
  912. {
  913. for (NSString *key in attributes.allKeys)
  914. {
  915. id value = attributes[key];
  916. if ([value isKindOfClass:[NSString class]])
  917. {
  918. value = [value stringByReplacingOccurrencesOfString:@"'" withString:@""];
  919. }
  920. if ([key caseInsensitiveCompare:@"face"] == NSOrderedSame)
  921. {
  922. CGFloat size = self.font.pointSize;
  923. if (attributes[@"size"])
  924. {
  925. size = [attributes[@"size"] floatValue];
  926. }
  927. UIFont *font = [UIFont fontWithName:value size:size];
  928. if (font)
  929. {
  930. [attributes setValue:font forKey:(NSString *)kCTFontNameAttribute];
  931. }
  932. }
  933. else if ([key caseInsensitiveCompare:@"size"] == NSOrderedSame && !attributes[@"face"] && !attributes[@"FACE"])
  934. {
  935. CGFloat size = [attributes[@"size"] floatValue];
  936. UIFont *font = [UIFont systemFontOfSize:size];
  937. [attributes setValue:font forKey:(NSString *)kCTFontNameAttribute];
  938. }
  939. else if ([key isEqualToString:(NSString *)kCTParagraphStyleAttributeName])
  940. {
  941. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range),
  942. kCTParagraphStyleAttributeName, (CTParagraphStyleRef)value);
  943. }
  944. else if ([key isEqualToString:NSParagraphStyleAttributeName])
  945. {
  946. NSMutableAttributedString *mutableText = [value mutableCopy];
  947. [mutableText addAttribute:NSParagraphStyleAttributeName value:(NSParagraphStyle *)value range:range];
  948. }
  949. else if ([key isEqualToString:(NSString *)kCTForegroundColorAttributeName]
  950. || [key isEqualToString:NSForegroundColorAttributeName]
  951. || [key caseInsensitiveCompare:@"color"] == NSOrderedSame)
  952. {
  953. [self applyColor:value toText:text range:range];
  954. }
  955. else if ([key isEqualToString:(NSString *)kCTStrokeWidthAttributeName]
  956. || [key isEqualToString:NSStrokeWidthAttributeName])
  957. {
  958. CFAttributedStringSetAttribute(text,
  959. CFRangeFromNSRange(range),
  960. kCTStrokeWidthAttributeName,
  961. (__bridge CFTypeRef)([attributes objectForKey:(NSString *)kCTStrokeWidthAttributeName]));
  962. }
  963. else if ([key isEqualToString:(NSString *)kCTStrokeColorAttributeName]
  964. || [key isEqualToString:NSStrokeColorAttributeName])
  965. {
  966. [self applyStrokeColor:value toText:text range:range];
  967. }
  968. else if ([key isEqualToString:(NSString *)kCTKernAttributeName]
  969. || [key isEqualToString:NSKernAttributeName])
  970. {
  971. CFAttributedStringSetAttribute(text,
  972. CFRangeFromNSRange(range),
  973. kCTKernAttributeName,
  974. (__bridge CFTypeRef)([attributes objectForKey:(NSString *)kCTKernAttributeName]));
  975. }
  976. else if ([key isEqualToString:(NSString *)kCTUnderlineStyleAttributeName]
  977. || [key isEqualToString:NSUnderlineStyleAttributeName])
  978. {
  979. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTUnderlineStyleAttributeName, (__bridge CFNumberRef)value);
  980. }
  981. }
  982. UIFont *font = [attributes objectForKey:(NSString *)kCTFontNameAttribute];
  983. if (font)
  984. {
  985. CTFontRef customFont = CTFontCreateWithName ((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
  986. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, customFont);
  987. CFRelease(customFont);
  988. return;
  989. }
  990. font = [attributes objectForKey:NSFontAttributeName];
  991. if (font)
  992. {
  993. CTFontRef customFont = CTFontCreateWithName ((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
  994. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, customFont);
  995. CFRelease(customFont);
  996. }
  997. else
  998. {
  999. CTFontRef customFont = CTFontCreateWithName ((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL);
  1000. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, customFont);
  1001. CFRelease(customFont);
  1002. }
  1003. }
  1004. - (void)applyBoldStyleToText:(CFMutableAttributedStringRef)text
  1005. range:(NSRange)range
  1006. {
  1007. CFTypeRef actualFontRef = CFAttributedStringGetAttribute(text, range.location, kCTFontAttributeName, NULL);
  1008. CTFontRef boldFontRef = CTFontCreateCopyWithSymbolicTraits(actualFontRef, 0.0, NULL, kCTFontBoldTrait, kCTFontBoldTrait);
  1009. BOOL forceCustomFont = self.customBoldFontName;
  1010. if (!boldFontRef || forceCustomFont)
  1011. {
  1012. // UIFont *font = [UIFont boldSystemFontOfSize:CTFontGetSize(actualFontRef)];
  1013. UIFont *font = [self boldFontOfSize:CTFontGetSize(actualFontRef)];
  1014. boldFontRef = CTFontCreateWithName((__bridge CFStringRef)font.fontName, self.font.pointSize, NULL);
  1015. }
  1016. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, boldFontRef);
  1017. CFRelease(boldFontRef);
  1018. }
  1019. - (void)applyBoldItalicStyleToText:(CFMutableAttributedStringRef)text
  1020. range:(NSRange)range
  1021. {
  1022. CFTypeRef actualFontRef = CFAttributedStringGetAttribute(text, range.location, kCTFontAttributeName, NULL);
  1023. CTFontRef boldItalicFontRef = CTFontCreateCopyWithSymbolicTraits(actualFontRef, 0.0, NULL, kCTFontBoldTrait | kCTFontItalicTrait , kCTFontBoldTrait | kCTFontItalicTrait);
  1024. BOOL forceCustomFont = self.customBoldItalicFontName;
  1025. if (!boldItalicFontRef || forceCustomFont)
  1026. {
  1027. // NSString *fontName = [NSString stringWithFormat:@"%@-BoldOblique", self.font.fontName];
  1028. UIFont *font = [self boldItalicFontOfSize:CTFontGetSize(actualFontRef)];
  1029. boldItalicFontRef = CTFontCreateWithName((__bridge CFStringRef)font.fontName, self.font.pointSize, NULL);
  1030. }
  1031. if (boldItalicFontRef)
  1032. {
  1033. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, boldItalicFontRef);
  1034. CFRelease(boldItalicFontRef);
  1035. }
  1036. }
  1037. - (void)applyColor:(id)value
  1038. toText:(CFMutableAttributedStringRef)text
  1039. range:(NSRange)range
  1040. {
  1041. if ([value isKindOfClass:[UIColor class]])
  1042. {
  1043. UIColor *color = (UIColor *)value;
  1044. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTForegroundColorAttributeName, color.CGColor);
  1045. }
  1046. else if ([value isKindOfClass:[NSString class]])
  1047. {
  1048. if ([value rangeOfString:@"#"].location == 0)
  1049. {
  1050. if ([value rangeOfString:@"#"].location == 0)
  1051. {
  1052. value = [value stringByReplacingOccurrencesOfString:@"#" withString:@""];
  1053. }
  1054. CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
  1055. NSArray *colorComponents = CGColorComponentsForHex(value);
  1056. CGFloat components[] = {[[colorComponents objectAtIndex:0] floatValue],
  1057. [[colorComponents objectAtIndex:1] floatValue],
  1058. [[colorComponents objectAtIndex:2] floatValue],
  1059. [[colorComponents objectAtIndex:3] floatValue]};
  1060. CGColorRef color = CGColorCreate(rgbColorSpace, components);
  1061. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTForegroundColorAttributeName, color);
  1062. CFRelease(color);
  1063. CGColorSpaceRelease(rgbColorSpace);
  1064. }
  1065. }
  1066. }
  1067. - (void)applyStrokeColor:(id)value
  1068. toText:(CFMutableAttributedStringRef)text
  1069. range:(NSRange)range
  1070. {
  1071. if ([value isKindOfClass:[UIColor class]])
  1072. {
  1073. UIColor *color = (UIColor *)value;
  1074. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTStrokeColorAttributeName, color.CGColor);
  1075. }
  1076. else if ([value isKindOfClass:[NSString class]])
  1077. {
  1078. if ([value rangeOfString:@"#"].location == 0)
  1079. {
  1080. value = [value stringByReplacingOccurrencesOfString:@"#" withString:@""];
  1081. }
  1082. CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
  1083. NSArray *colorComponents = CGColorComponentsForHex(value);
  1084. CGFloat components[] = {[[colorComponents objectAtIndex:0] floatValue],
  1085. [[colorComponents objectAtIndex:1] floatValue],
  1086. [[colorComponents objectAtIndex:2] floatValue],
  1087. [[colorComponents objectAtIndex:3] floatValue]};
  1088. CGColorRef color = CGColorCreate(rgbColorSpace, components);
  1089. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTStrokeColorAttributeName, color);
  1090. CFRelease(color);
  1091. CGColorSpaceRelease(rgbColorSpace);
  1092. }
  1093. }
  1094. - (void)applyUnderlineColor:(id)value
  1095. toText:(CFMutableAttributedStringRef)text
  1096. range:(NSRange)range
  1097. {
  1098. if ([value isKindOfClass:[UIColor class]])
  1099. {
  1100. UIColor *color = (UIColor *)value;
  1101. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTForegroundColorAttributeName, color.CGColor);
  1102. }
  1103. else if ([value isKindOfClass:[NSString class]])
  1104. {
  1105. value = [value stringByReplacingOccurrencesOfString:@"'" withString:@""];
  1106. if ([value rangeOfString:@"#"].location==0)
  1107. {
  1108. CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
  1109. value = [value stringByReplacingOccurrencesOfString:@"#" withString:@""];
  1110. NSArray *colorComponents = CGColorComponentsForHex(value);
  1111. CGFloat components[] = {[[colorComponents objectAtIndex:0] floatValue],
  1112. [[colorComponents objectAtIndex:1] floatValue],
  1113. [[colorComponents objectAtIndex:2] floatValue],
  1114. [[colorComponents objectAtIndex:3] floatValue]};
  1115. CGColorRef color = CGColorCreate(rgbColorSpace, components);
  1116. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTUnderlineColorAttributeName, color);
  1117. CGColorRelease(color);
  1118. CGColorSpaceRelease(rgbColorSpace);
  1119. }
  1120. }
  1121. }
  1122. #pragma mark - Parsing methods
  1123. - (void)extractStyleFromText:(NSString *)data
  1124. {
  1125. // clear existing links
  1126. self.links = [NSMutableArray array];
  1127. // Replace html entities
  1128. if (data)
  1129. {
  1130. data = [data stringByReplacingOccurrencesOfString:@"&lt;" withString:@"<"];
  1131. data = [data stringByReplacingOccurrencesOfString:@"&gt;" withString:@">"];
  1132. data = [data stringByReplacingOccurrencesOfString:@"&amp;" withString:@"&"];
  1133. data = [data stringByReplacingOccurrencesOfString:@"&apos;" withString:@"'"];
  1134. data = [data stringByReplacingOccurrencesOfString:@"&quot;" withString:@"\""];
  1135. }
  1136. NSMutableArray *components = [NSMutableArray array];
  1137. NSInteger last_position = 0;
  1138. NSString *text = nil;
  1139. NSString *htmlTag = nil;
  1140. NSScanner *scanner = [NSScanner scannerWithString:data];
  1141. while (!scanner.isAtEnd)
  1142. {
  1143. // Get position of scanner, used to check if <p> tags are at the start of the text
  1144. NSInteger tagStartPosition = scanner.scanLocation;
  1145. // Capture tag text
  1146. [scanner scanUpToString:@"<" intoString:NULL];
  1147. [scanner scanUpToString:@">" intoString:&text];
  1148. NSString *fullTag = [NSString stringWithFormat:@"%@>", text];
  1149. NSInteger position = [data rangeOfString:fullTag].location;
  1150. if (position != NSNotFound)
  1151. {
  1152. // Remove tag from text and replace occurences of paragraph tags
  1153. if ([fullTag rangeOfString:@"<p"].location == 0 && tagStartPosition != 0)
  1154. {
  1155. data = [data stringByReplacingOccurrencesOfString:fullTag
  1156. withString:@"\n"
  1157. options:NSCaseInsensitiveSearch
  1158. range:NSMakeRange(last_position, position + fullTag.length - last_position)];
  1159. }
  1160. else
  1161. {
  1162. data = [data stringByReplacingOccurrencesOfString:fullTag
  1163. withString:@""
  1164. options:NSCaseInsensitiveSearch
  1165. range:NSMakeRange(last_position, position + fullTag.length - last_position)];
  1166. }
  1167. }
  1168. // Found closing tag
  1169. if ([text rangeOfString:@"</"].location == 0)
  1170. {
  1171. // Get just the html tag value
  1172. htmlTag = [text substringFromIndex:2];
  1173. if (position != NSNotFound)
  1174. {
  1175. // Find the the corresponding component for the closing tag
  1176. for (NSInteger i = components.count - 1; i >= 0; i--)
  1177. {
  1178. MDHTMLComponent *component = components[i];
  1179. if (component.text == nil && [component.htmlTag isEqualToString:htmlTag])
  1180. {
  1181. NSString *componentText = [data substringWithRange:NSMakeRange(component.position, position - component.position)];
  1182. component.text = componentText;
  1183. if ([component.htmlTag caseInsensitiveCompare:@"a"] == NSOrderedSame)
  1184. {
  1185. NSTextCheckingResult *result = [NSTextCheckingResult linkCheckingResultWithRange:component.range
  1186. URL:[NSURL URLWithString:component.attributes[@"href"]]];
  1187. [self.links addObject:result];
  1188. }
  1189. break;
  1190. }
  1191. }
  1192. }
  1193. }
  1194. else
  1195. {
  1196. // Get text components without the opening '<'
  1197. NSMutableArray *textComponents = [[[text substringFromIndex:1] componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
  1198. // Capture html tag for later
  1199. htmlTag = textComponents[0];
  1200. if (htmlTag.length > 0) {
  1201. // remove the tag
  1202. [textComponents removeObjectAtIndex:0];
  1203. // remove consecutive spaces
  1204. [textComponents removeObject:@""];
  1205. // clear out spaces around "=" since they cause a crash and make it hard to identify which are keys and which are values
  1206. NSString *cleanText = [textComponents componentsJoinedByString:@" "];
  1207. cleanText = [cleanText stringByReplacingOccurrencesOfString:@"= " withString:@"="];
  1208. cleanText = [cleanText stringByReplacingOccurrencesOfString:@" =" withString:@"="];
  1209. // clean out spaces around value quotes but preserve spaces between pairs
  1210. cleanText = [cleanText stringByReplacingOccurrencesOfString:@" ' " withString:@"' "];
  1211. cleanText = [cleanText stringByReplacingOccurrencesOfString:@"=' " withString:@"='"];
  1212. cleanText = [cleanText stringByReplacingOccurrencesOfString:@" \" " withString:@"\" "];
  1213. cleanText = [cleanText stringByReplacingOccurrencesOfString:@"=\" " withString:@"=\""];
  1214. textComponents = [[cleanText componentsSeparatedByString:@" "] mutableCopy];
  1215. // spaces can still exist inside of values and they will be split, put them back with their key/value pair
  1216. NSUInteger lastPairIndex = 0;
  1217. if (textComponents.count > 1) {
  1218. for (NSUInteger index = 1; index < textComponents.count; index++) {
  1219. NSRange equalRange = [textComponents[index] rangeOfString:@"="];
  1220. if (equalRange.location != NSNotFound) {
  1221. lastPairIndex = index;
  1222. } else {
  1223. textComponents[lastPairIndex] = [textComponents[lastPairIndex] stringByAppendingFormat:@" %@", textComponents[index]];
  1224. textComponents[index] = @"";
  1225. }
  1226. }
  1227. }
  1228. // Capture the tag's attributes
  1229. NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
  1230. for (NSString *pairString in textComponents) {
  1231. if (pairString.length > 0) {
  1232. NSArray *pair = [pairString componentsSeparatedByString:@"="];
  1233. if (pair.count > 0) {
  1234. NSString *key = [pair[0] lowercaseString];
  1235. if (pair.count >= 2) {
  1236. NSString *value = [[pair subarrayWithRange:NSMakeRange(1, [pair count] - 1)] componentsJoinedByString:@"="];
  1237. value = [value stringByReplacingOccurrencesOfString:@"\"" withString:@"" options:NSLiteralSearch range:NSMakeRange(0, 1)];
  1238. value = [value stringByReplacingOccurrencesOfString:@"\"" withString:@"" options:NSLiteralSearch range:NSMakeRange([value length]-1, 1)];
  1239. value = [value stringByReplacingOccurrencesOfString:@"'" withString:@"" options:NSLiteralSearch range:NSMakeRange(0, 1)];
  1240. value = [value stringByReplacingOccurrencesOfString:@"'" withString:@"" options:NSLiteralSearch range:NSMakeRange([value length]-1, 1)];
  1241. value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
  1242. attributes[key] = value;
  1243. } else if (pair.count == 1) {
  1244. attributes[key] = key;
  1245. }
  1246. }
  1247. }
  1248. }
  1249. // Create component from tag and attributes, we'll know the text once we reach the closing tag
  1250. MDHTMLComponent *component = [[MDHTMLComponent alloc] initWithString:nil htmlTag:htmlTag attributes:attributes];
  1251. component.position = position;
  1252. [components addObject:component];
  1253. }
  1254. }
  1255. last_position = position;
  1256. }
  1257. self.styleComponents = components;
  1258. self.plainText = data;
  1259. }
  1260. #pragma mark - UILabel
  1261. - (void)setHighlighted:(BOOL)highlighted
  1262. {
  1263. [super setHighlighted:highlighted];
  1264. [self setNeedsDisplay];
  1265. }
  1266. // Fixes crash when loading from a UIStoryboard
  1267. - (UIColor *)textColor
  1268. {
  1269. UIColor *color = [super textColor];
  1270. if (!color)
  1271. {
  1272. color = [UIColor blackColor];
  1273. }
  1274. return color;
  1275. }
  1276. - (CGRect)textRectForBounds:(CGRect)bounds
  1277. limitedToNumberOfLines:(NSInteger)numberOfLines
  1278. {
  1279. if (!self.htmlText)
  1280. {
  1281. return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
  1282. }
  1283. CGRect textRect = bounds;
  1284. // Calculate height with a minimum of double the font pointSize, to ensure that CTFramesetterSuggestFrameSizeWithConstraints doesn't return CGSizeZero, as it would if textRect height is insufficient.
  1285. textRect.size.height = MAX(self.font.pointSize * 2.0f, bounds.size.height);
  1286. // Adjust the text to be in the center vertically, if the text size is smaller than bounds
  1287. CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, CFRangeMake(0, (CFIndex)self.htmlAttributedText.length), NULL, textRect.size, NULL);
  1288. textSize = CGSizeMake(CGFloat_ceil(textSize.width), CGFloat_ceil(textSize.height)); // Fix for iOS 4, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns fractional sizes
  1289. if (textSize.height < textRect.size.height)
  1290. {
  1291. CGFloat yOffset = 0.0f;
  1292. switch (self.verticalAlignment)
  1293. {
  1294. case MDHTMLLabelVerticalAlignmentCenter:
  1295. yOffset = CGFloat_floor((bounds.size.height - textSize.height) / 2.0f);
  1296. break;
  1297. case MDHTMLLabelVerticalAlignmentBottom:
  1298. yOffset = bounds.size.height - textSize.height;
  1299. break;
  1300. case MDHTMLLabelVerticalAlignmentTop:
  1301. default:
  1302. break;
  1303. }
  1304. textRect.origin.y += yOffset;
  1305. }
  1306. return textRect;
  1307. }
  1308. #pragma mark - UIView
  1309. + (CGFloat)sizeThatFitsHTMLString:(NSString *)htmlString
  1310. withFont:(UIFont *)font
  1311. constraints:(CGSize)size
  1312. limitedToNumberOfLines:(NSUInteger)numberOfLines
  1313. autoDetectUrls:(BOOL)autoDetectUrls
  1314. {
  1315. MDHTMLLabel *label = [[MDHTMLLabel alloc] initWithFrame:CGRectMake(0.0, 0.0, size.width, size.height)];
  1316. label.font = font;
  1317. label.numberOfLines = numberOfLines;
  1318. label.lineBreakMode = NSLineBreakByWordWrapping;
  1319. label.autoDetectUrls = autoDetectUrls;
  1320. label.htmlText = htmlString;
  1321. return [label sizeThatFits:size].height;
  1322. }
  1323. - (CGSize)sizeThatFits:(CGSize)size
  1324. {
  1325. if (!self.htmlAttributedText)
  1326. {
  1327. return [super sizeThatFits:size];
  1328. }
  1329. return CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(self.framesetter, self.htmlAttributedText, size, (NSUInteger)self.numberOfLines);
  1330. }
  1331. - (CGSize)intrinsicContentSize
  1332. {
  1333. // There's an implicit width from the original UILabel implementation
  1334. return [self sizeThatFits:[super intrinsicContentSize]];
  1335. }
  1336. - (void)tintColorDidChange
  1337. {
  1338. BOOL isInactive = (CGColorSpaceGetModel(CGColorGetColorSpace([self.tintColor CGColor])) == kCGColorSpaceModelMonochrome);
  1339. NSMutableDictionary *mutableLinkAttributes = [self.linkAttributes mutableCopy];
  1340. if (!mutableLinkAttributes[(NSString *)kCTForegroundColorAttributeName] && !mutableLinkAttributes[NSForegroundColorAttributeName])
  1341. {
  1342. if ([self respondsToSelector:@selector(tintColor)])
  1343. {
  1344. mutableLinkAttributes[(NSString *)kCTForegroundColorAttributeName] = self.tintColor;
  1345. }
  1346. else
  1347. {
  1348. mutableLinkAttributes[(NSString *)kCTForegroundColorAttributeName] = [UIColor blueColor];
  1349. }
  1350. }
  1351. if (!mutableLinkAttributes[(NSString *)kCTFontAttributeName] && !mutableLinkAttributes[NSFontAttributeName])
  1352. {
  1353. mutableLinkAttributes[(NSString *)kCTFontAttributeName] = self.font;
  1354. }
  1355. NSMutableDictionary *mutableInactiveLinkAttributes = [self.inactiveLinkAttributes mutableCopy];
  1356. if (!mutableInactiveLinkAttributes[(NSString *)kCTForegroundColorAttributeName] && !mutableInactiveLinkAttributes[NSForegroundColorAttributeName])
  1357. {
  1358. mutableInactiveLinkAttributes[(NSString *)kCTForegroundColorAttributeName] = [UIColor grayColor];
  1359. }
  1360. if (!mutableInactiveLinkAttributes[(NSString *)kCTFontAttributeName] && !mutableInactiveLinkAttributes[NSFontAttributeName])
  1361. {
  1362. mutableInactiveLinkAttributes[(NSString *)kCTFontAttributeName] = mutableLinkAttributes[(NSString *)kCTFontAttributeName];
  1363. }
  1364. NSDictionary *attributesToRemove = isInactive ? mutableLinkAttributes : mutableInactiveLinkAttributes;
  1365. NSDictionary *attributesToAdd = isInactive ? mutableInactiveLinkAttributes : mutableLinkAttributes;
  1366. NSMutableAttributedString *mutableAttributedString = [self.htmlAttributedText mutableCopy];
  1367. for (NSTextCheckingResult *result in self.links)
  1368. {
  1369. [attributesToRemove enumerateKeysAndObjectsUsingBlock:^(NSString *name, __unused id value, __unused BOOL *stop)
  1370. {
  1371. [mutableAttributedString removeAttribute:name range:result.range];
  1372. }];
  1373. if (attributesToAdd)
  1374. {
  1375. [mutableAttributedString addAttributes:attributesToAdd range:result.range];
  1376. }
  1377. }
  1378. self.htmlAttributedText = mutableAttributedString;
  1379. [self setNeedsDisplay];
  1380. }
  1381. #pragma mark - Data Detection
  1382. - (NSString *)detectURLsInText:(NSString *)text
  1383. {
  1384. NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:NULL];
  1385. NSUInteger matchDetectorStartLocation = 0;
  1386. NSTextCheckingResult *match = [detector firstMatchInString:text
  1387. options:kNilOptions
  1388. range:NSMakeRange(matchDetectorStartLocation, text.length)];
  1389. while (match != nil && match.range.location != NSNotFound)
  1390. {
  1391. NSUInteger matchLength = match.range.length;
  1392. if (match.resultType == NSTextCheckingTypeLink)
  1393. {
  1394. // if there's no "<a" or "href" before the link regardless of spaces. e.g. this is valid <a href = "SomeURL">
  1395. BOOL insideHref = NO;
  1396. // an href
  1397. // there has to be room for at least "<a href='" before we bother checking this
  1398. if ((match.range.location - matchDetectorStartLocation) >= 8) {
  1399. NSRange prevTextRange = NSMakeRange(matchDetectorStartLocation, (match.range.location - 1) - matchDetectorStartLocation);
  1400. NSRange prevHrefRange = [text rangeOfString:@"href" options:NSCaseInsensitiveSearch | NSBackwardsSearch range:prevTextRange];
  1401. NSRange prevStartATagRange = [text rangeOfString:@"<a" options:NSCaseInsensitiveSearch | NSBackwardsSearch range:prevTextRange];
  1402. NSRange prevEndTagRange = [text rangeOfString:@">" options:NSCaseInsensitiveSearch | NSBackwardsSearch range:prevTextRange];
  1403. insideHref = (prevHrefRange.location != NSNotFound && prevStartATagRange.location != NSNotFound &&
  1404. NSMaxRange(prevStartATagRange) < prevHrefRange.location &&
  1405. (prevEndTagRange.location == NSNotFound || (prevEndTagRange.location != NSNotFound &&
  1406. prevStartATagRange.location >= NSMaxRange(prevEndTagRange))));
  1407. }
  1408. BOOL wrappedInAnchors = [text rangeOfString:@"a>" options:NSCaseInsensitiveSearch | NSBackwardsSearch range:match.range].location != NSNotFound;
  1409. if (!insideHref && !wrappedInAnchors)
  1410. {
  1411. NSString *wrappedURL = [NSString stringWithFormat:@"<a href='%@'>%@</a>", match.URL.absoluteString, match.URL.absoluteString];
  1412. text = [text stringByReplacingCharactersInRange:match.range
  1413. withString:wrappedURL];
  1414. matchLength = wrappedURL.length;
  1415. }
  1416. }
  1417. matchDetectorStartLocation = match.range.location + matchLength;
  1418. match = [detector firstMatchInString:text
  1419. options:kNilOptions
  1420. range:NSMakeRange(matchDetectorStartLocation, text.length - matchDetectorStartLocation)];
  1421. }
  1422. return text;
  1423. }
  1424. - (NSTextCheckingResult *)linkAtCharacterIndex:(CFIndex)idx
  1425. {
  1426. NSEnumerator *enumerator = [self.links reverseObjectEnumerator];
  1427. NSTextCheckingResult *result = nil;
  1428. while ((result = [enumerator nextObject]))
  1429. {
  1430. if (NSLocationInRange((NSUInteger)idx, result.range))
  1431. {
  1432. return result;
  1433. }
  1434. }
  1435. return nil;
  1436. }
  1437. - (NSTextCheckingResult *)linkAtPoint:(CGPoint)p
  1438. {
  1439. CFIndex idx = [self characterIndexAtPoint:p];
  1440. return [self linkAtCharacterIndex:idx];
  1441. }
  1442. - (CFIndex)characterIndexAtPoint:(CGPoint)p
  1443. {
  1444. if (!CGRectContainsPoint(self.bounds, p))
  1445. {
  1446. return NSNotFound;
  1447. }
  1448. CGRect textRect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines];
  1449. if (!CGRectContainsPoint(textRect, p))
  1450. {
  1451. return NSNotFound;
  1452. }
  1453. // Offset tap coordinates by textRect origin to make them relative to the origin of frame
  1454. p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y);
  1455. // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left)
  1456. p = CGPointMake(p.x, textRect.size.height - p.y);
  1457. CGMutablePathRef path = CGPathCreateMutable();
  1458. CGPathAddRect(path, NULL, textRect);
  1459. CTFrameRef frame = CTFramesetterCreateFrame([self framesetter], CFRangeMake(0, (CFIndex)self.htmlAttributedText.length), path, NULL);
  1460. if (frame == NULL)
  1461. {
  1462. CFRelease(path);
  1463. return NSNotFound;
  1464. }
  1465. CFArrayRef lines = CTFrameGetLines(frame);
  1466. NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
  1467. if (numberOfLines == 0)
  1468. {
  1469. CFRelease(frame);
  1470. CFRelease(path);
  1471. return NSNotFound;
  1472. }
  1473. CFIndex idx = NSNotFound;
  1474. CGPoint lineOrigins[numberOfLines];
  1475. CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
  1476. for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++)
  1477. {
  1478. CGPoint lineOrigin = lineOrigins[lineIndex];
  1479. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  1480. // Get bounding information of line
  1481. CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
  1482. CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  1483. CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
  1484. CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);
  1485. // Check if we've already passed the line
  1486. if (p.y > yMax)
  1487. {
  1488. break;
  1489. }
  1490. // Check if the point is within this line vertically
  1491. if (p.y >= yMin)
  1492. {
  1493. // Check if the point is within this line horizontally
  1494. if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
  1495. // Convert CT coordinates to line-relative coordinates
  1496. CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
  1497. idx = CTLineGetStringIndexForPosition(line, relativePoint);
  1498. break;
  1499. }
  1500. }
  1501. }
  1502. CFRelease(frame);
  1503. CFRelease(path);
  1504. return idx;
  1505. }
  1506. #pragma mark - UIResponder
  1507. - (BOOL)canBecomeFirstResponder
  1508. {
  1509. return YES;
  1510. }
  1511. - (BOOL)canPerformAction:(SEL)action
  1512. withSender:(__unused id)sender
  1513. {
  1514. return (action == @selector(copy:));
  1515. }
  1516. - (void)touchesBegan:(NSSet *)touches
  1517. withEvent:(UIEvent *)event
  1518. {
  1519. UITouch *touch = [touches anyObject];
  1520. self.activeLink = [self linkAtPoint:[touch locationInView:self]];
  1521. if (self.activeLink)
  1522. {
  1523. self.holdGestureTimer = [NSTimer scheduledTimerWithTimeInterval:self.minimumPressDuration
  1524. target:self
  1525. selector:@selector(handleDidHoldTouch:)
  1526. userInfo:touch
  1527. repeats:NO];
  1528. }
  1529. else
  1530. {
  1531. [super touchesBegan:touches withEvent:event];
  1532. self.highlighted = YES;
  1533. }
  1534. }
  1535. - (void)touchesMoved:(NSSet *)touches
  1536. withEvent:(UIEvent *)event
  1537. {
  1538. if (self.activeLink)
  1539. {
  1540. UITouch *touch = [touches anyObject];
  1541. if (self.activeLink != [self linkAtPoint:[touch locationInView:self]])
  1542. {
  1543. self.activeLink = nil;
  1544. [self.holdGestureTimer invalidate];
  1545. }
  1546. }
  1547. else
  1548. {
  1549. [super touchesMoved:touches withEvent:event];
  1550. }
  1551. }
  1552. - (void)touchesEnded:(NSSet *)touches
  1553. withEvent:(UIEvent *)event
  1554. {
  1555. self.highlighted = NO;
  1556. if (self.activeLink)
  1557. {
  1558. NSTextCheckingResult *result = self.activeLink;
  1559. self.activeLink = nil;
  1560. [self.holdGestureTimer invalidate];
  1561. if ([self.delegate respondsToSelector:@selector(HTMLLabel:didSelectLinkWithURL:)])
  1562. {
  1563. [self.delegate HTMLLabel:self didSelectLinkWithURL:result.URL];
  1564. return;
  1565. }
  1566. }
  1567. else
  1568. {
  1569. [super touchesEnded:touches withEvent:event];
  1570. }
  1571. }
  1572. - (void)touchesCancelled:(NSSet *)touches
  1573. withEvent:(UIEvent *)event
  1574. {
  1575. if (self.activeLink)
  1576. {
  1577. self.activeLink = nil;
  1578. }
  1579. else
  1580. {
  1581. [super touchesCancelled:touches withEvent:event];
  1582. }
  1583. }
  1584. - (void)handleDidHoldTouch:(NSTimer *)timer
  1585. {
  1586. self.highlighted = NO;
  1587. [self.holdGestureTimer invalidate];
  1588. if ([self.delegate respondsToSelector:@selector(HTMLLabel:didHoldLinkWithURL:)])
  1589. {
  1590. NSTextCheckingResult *result = self.activeLink;
  1591. self.activeLink = nil;
  1592. [self.delegate HTMLLabel:self didHoldLinkWithURL:result.URL];
  1593. }
  1594. }
  1595. #pragma mark - UIResponderStandardEditActions
  1596. - (void)copy:(id)sender
  1597. {
  1598. if (self.htmlText)
  1599. {
  1600. [[UIPasteboard generalPasteboard] setString:self.plainText];
  1601. }
  1602. else
  1603. {
  1604. [super copy:sender];
  1605. }
  1606. }
  1607. #pragma mark - NSCoding
  1608. - (void)encodeWithCoder:(NSCoder *)coder
  1609. {
  1610. [super encodeWithCoder:coder];
  1611. [coder encodeObject:self.htmlText forKey:NSStringFromSelector(@selector(links))];
  1612. [coder encodeObject:self.links forKey:NSStringFromSelector(@selector(links))];
  1613. [coder encodeObject:self.linkAttributes forKey:NSStringFromSelector(@selector(linkAttributes))];
  1614. [coder encodeObject:self.activeLinkAttributes forKey:NSStringFromSelector(@selector(activeLinkAttributes))];
  1615. [coder encodeObject:self.inactiveLinkAttributes forKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
  1616. [coder encodeDouble:self.minimumPressDuration forKey:NSStringFromSelector(@selector(minimumPressDuration))];
  1617. [coder encodeDouble:self.shadowRadius forKey:NSStringFromSelector(@selector(shadowRadius))];
  1618. [coder encodeDouble:self.highlightedShadowRadius forKey:NSStringFromSelector(@selector(highlightedShadowRadius))];
  1619. [coder encodeCGSize:self.highlightedShadowOffset forKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
  1620. [coder encodeObject:self.highlightedShadowColor forKey:NSStringFromSelector(@selector(highlightedShadowColor))];
  1621. [coder encodeDouble:self.firstLineIndent forKey:NSStringFromSelector(@selector(firstLineIndent))];
  1622. [coder encodeDouble:self.leading forKey:NSStringFromSelector(@selector(leading))];
  1623. [coder encodeDouble:self.lineHeightMultiple forKey:NSStringFromSelector(@selector(lineHeightMultiple))];
  1624. [coder encodeUIEdgeInsets:self.textInsets forKey:NSStringFromSelector(@selector(textInsets))];
  1625. [coder encodeInteger:self.verticalAlignment forKey:NSStringFromSelector(@selector(verticalAlignment))];
  1626. [coder encodeObject:self.truncationTokenString forKey:NSStringFromSelector(@selector(truncationTokenString))];
  1627. [coder encodeObject:self.truncationTokenStringAttributes forKey:NSStringFromSelector(@selector(truncationTokenStringAttributes))];
  1628. }
  1629. - (id)initWithCoder:(NSCoder *)coder
  1630. {
  1631. self = [super initWithCoder:coder];
  1632. if (self)
  1633. {
  1634. [self commonInit];
  1635. if ([coder containsValueForKey:NSStringFromSelector(@selector(htmlText))])
  1636. {
  1637. self.htmlText = [coder decodeObjectForKey:NSStringFromSelector(@selector(htmlText))];
  1638. }
  1639. if ([coder containsValueForKey:NSStringFromSelector(@selector(links))])
  1640. {
  1641. self.links = [coder decodeObjectForKey:NSStringFromSelector(@selector(links))];
  1642. }
  1643. if ([coder containsValueForKey:NSStringFromSelector(@selector(linkAttributes))])
  1644. {
  1645. self.linkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkAttributes))];
  1646. }
  1647. if ([coder containsValueForKey:NSStringFromSelector(@selector(activeLinkAttributes))])
  1648. {
  1649. self.activeLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(activeLinkAttributes))];
  1650. }
  1651. if ([coder containsValueForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))])
  1652. {
  1653. self.inactiveLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
  1654. }
  1655. if ([coder containsValueForKey:NSStringFromSelector(@selector(minimumPressDuration))])
  1656. {
  1657. self.minimumPressDuration = [coder decodeDoubleForKey:NSStringFromSelector(@selector(minimumPressDuration))];
  1658. }
  1659. if ([coder containsValueForKey:NSStringFromSelector(@selector(shadowRadius))])
  1660. {
  1661. self.shadowRadius = [coder decodeDoubleForKey:NSStringFromSelector(@selector(shadowRadius))];
  1662. }
  1663. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowRadius))])
  1664. {
  1665. self.highlightedShadowRadius = [coder decodeDoubleForKey:NSStringFromSelector(@selector(highlightedShadowRadius))];
  1666. }
  1667. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowOffset))])
  1668. {
  1669. self.highlightedShadowOffset = [coder decodeCGSizeForKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
  1670. }
  1671. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowColor))])
  1672. {
  1673. self.highlightedShadowColor = [coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowColor))];
  1674. }
  1675. if ([coder containsValueForKey:NSStringFromSelector(@selector(firstLineIndent))])
  1676. {
  1677. self.firstLineIndent = [coder decodeDoubleForKey:NSStringFromSelector(@selector(firstLineHeadIndent))];
  1678. }
  1679. if ([coder containsValueForKey:NSStringFromSelector(@selector(leading))])
  1680. {
  1681. self.leading = [coder decodeDoubleForKey:NSStringFromSelector(@selector(leading))];
  1682. }
  1683. if ([coder containsValueForKey:NSStringFromSelector(@selector(lineHeightMultiple))])
  1684. {
  1685. self.lineHeightMultiple = [coder decodeDoubleForKey:NSStringFromSelector(@selector(lineHeightMultiple))];
  1686. }
  1687. if ([coder containsValueForKey:NSStringFromSelector(@selector(textInsets))])
  1688. {
  1689. self.textInsets = [coder decodeUIEdgeInsetsForKey:NSStringFromSelector(@selector(textInsets))];
  1690. }
  1691. if ([coder containsValueForKey:NSStringFromSelector(@selector(verticalAlignment))])
  1692. {
  1693. self.verticalAlignment = [coder decodeIntegerForKey:NSStringFromSelector(@selector(verticalAlignment))];
  1694. }
  1695. if ([coder containsValueForKey:NSStringFromSelector(@selector(truncationTokenString))])
  1696. {
  1697. self.truncationTokenString = [coder decodeObjectForKey:NSStringFromSelector(@selector(truncationTokenString))];
  1698. }
  1699. if ([coder containsValueForKey:NSStringFromSelector(@selector(truncationTokenStringAttributes))])
  1700. {
  1701. self.truncationTokenStringAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(truncationTokenStringAttributes))];
  1702. }
  1703. }
  1704. return self;
  1705. }
  1706. #pragma mark - Custom fonts
  1707. - (UIFont *)boldFontOfSize:(CGFloat)size {
  1708. if (self.customBoldFontName) {
  1709. return [UIFont fontWithName:self.customBoldFontName size:size];
  1710. }
  1711. return [UIFont boldSystemFontOfSize:size];
  1712. }
  1713. - (UIFont *)italicFontOfSize:(CGFloat)size {
  1714. if (self.customItalicFontName) {
  1715. return [UIFont fontWithName:self.customItalicFontName size:size];
  1716. }
  1717. return [UIFont italicSystemFontOfSize:size];
  1718. }
  1719. - (UIFont *)boldItalicFontOfSize:(CGFloat)size {
  1720. if (self.customBoldItalicFontName) {
  1721. return [UIFont fontWithName:self.customBoldItalicFontName size:size];
  1722. }
  1723. return [UIFont fontWithName:[NSString stringWithFormat:@"%@-BoldOblique", self.font.fontName] size:size];
  1724. }
  1725. @end