MDHTMLLabel.m 77 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071
  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 kCTTextAlignmentLeft;
  46. case NSTextAlignmentCenter: return kCTTextAlignmentCenter;
  47. case NSTextAlignmentRight: return kCTTextAlignmentRight;
  48. default: return kCTTextAlignmentNatural;
  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 = kCTTextAlignmentLeft;
  792. }
  793. else if ([value caseInsensitiveCompare:@"right"] == NSOrderedSame)
  794. {
  795. textAlignment = kCTTextAlignmentRight;
  796. }
  797. else if ([value caseInsensitiveCompare:@"justify"] == NSOrderedSame)
  798. {
  799. textAlignment = kCTTextAlignmentJustified;
  800. }
  801. else if ([value caseInsensitiveCompare:@"center"] == NSOrderedSame)
  802. {
  803. textAlignment = kCTTextAlignmentCenter;
  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. { kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing },
  844. { kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing },
  845. { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacingAdjustment },
  846. { kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(CGFloat), &lineHeightMultiple },
  847. { kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), &topMargin },
  848. { kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), &bottomMargin },
  849. { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineIndent },
  850. { kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &leftMargin },
  851. { kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), &rightMargin },
  852. };
  853. CTParagraphStyleRef paragraphRef = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(CTParagraphStyleSetting));
  854. CFDictionaryAddValue(styleDict, kCTParagraphStyleAttributeName, paragraphRef);
  855. CFAttributedStringSetAttributes(text, CFRangeFromNSRange(range), styleDict, 0);
  856. CFRelease(paragraphRef);
  857. CFRelease(styleDict);
  858. }
  859. - (void)applyCenterStyleToText:(CFMutableAttributedStringRef)text
  860. attributes:(NSMutableDictionary *)attributes
  861. range:(NSRange)range
  862. {
  863. CFMutableDictionaryRef styleDict = CFDictionaryCreateMutable(0, 0, 0, 0) ;
  864. CGFloat lineSpacing = self.leading;
  865. CGFloat lineSpacingAdjustment = CGFloat_ceil(self.font.lineHeight - self.font.ascender + self.font.descender);
  866. CGFloat lineHeightMultiple = self.lineHeightMultiple;
  867. CGFloat topMargin = self.textInsets.top;
  868. CGFloat bottomMargin = self.textInsets.bottom;
  869. CGFloat leftMargin = self.textInsets.left;
  870. CGFloat rightMargin = -self.textInsets.right;
  871. CGFloat firstLineIndent = self.firstLineIndent + leftMargin;
  872. CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
  873. if (self.numberOfLines == 1)
  874. {
  875. lineBreakMode = CTLineBreakModeFromMDLineBreakMode(self.lineBreakMode);
  876. }
  877. CTTextAlignment textAlignment = kCTTextAlignmentCenter;
  878. CTParagraphStyleSetting settings[] =
  879. {
  880. { kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &textAlignment },
  881. { kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBreakMode },
  882. // { kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing },
  883. { kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing },
  884. { kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing },
  885. { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacingAdjustment },
  886. { kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(CGFloat), &lineHeightMultiple },
  887. { kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), &topMargin },
  888. { kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), &bottomMargin },
  889. { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineIndent },
  890. { kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &leftMargin },
  891. { kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), &rightMargin },
  892. };
  893. CTParagraphStyleRef paragraphRef = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(CTParagraphStyleSetting));
  894. CFDictionaryAddValue(styleDict, kCTParagraphStyleAttributeName, paragraphRef);
  895. CFAttributedStringSetAttributes( text, CFRangeFromNSRange(range), styleDict, 0 );
  896. CFRelease(paragraphRef);
  897. CFRelease(styleDict);
  898. }
  899. - (void)applyItalicStyleToText:(CFMutableAttributedStringRef)text
  900. range:(NSRange)range
  901. {
  902. CFTypeRef actualFontRef = CFAttributedStringGetAttribute(text, range.location, kCTFontAttributeName, NULL);
  903. CTFontRef italicFontRef = CTFontCreateCopyWithSymbolicTraits(actualFontRef, 0.0, NULL, kCTFontItalicTrait, kCTFontItalicTrait);
  904. BOOL forceCustomFont = self.customItalicFontName;
  905. if (!italicFontRef || forceCustomFont)
  906. {
  907. UIFont *font = [self italicFontOfSize:CTFontGetSize(actualFontRef)];
  908. italicFontRef = CTFontCreateWithName ((__bridge CFStringRef)[font fontName], [font pointSize], NULL);
  909. }
  910. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, italicFontRef);
  911. CFRelease(italicFontRef);
  912. }
  913. - (void)applyFontAttributes:(NSDictionary *)attributes
  914. toText:(CFMutableAttributedStringRef)text
  915. range:(NSRange)range
  916. {
  917. for (NSString *key in attributes.allKeys)
  918. {
  919. id value = attributes[key];
  920. if ([value isKindOfClass:[NSString class]])
  921. {
  922. value = [value stringByReplacingOccurrencesOfString:@"'" withString:@""];
  923. }
  924. if ([key caseInsensitiveCompare:@"face"] == NSOrderedSame)
  925. {
  926. CGFloat size = self.font.pointSize;
  927. if (attributes[@"size"])
  928. {
  929. size = [attributes[@"size"] floatValue];
  930. }
  931. UIFont *font = [UIFont fontWithName:value size:size];
  932. if (font)
  933. {
  934. [attributes setValue:font forKey:(NSString *)kCTFontNameAttribute];
  935. }
  936. }
  937. else if ([key caseInsensitiveCompare:@"size"] == NSOrderedSame && !attributes[@"face"] && !attributes[@"FACE"])
  938. {
  939. CGFloat size = [attributes[@"size"] floatValue];
  940. UIFont *font = [UIFont systemFontOfSize:size];
  941. [attributes setValue:font forKey:(NSString *)kCTFontNameAttribute];
  942. }
  943. else if ([key isEqualToString:(NSString *)kCTParagraphStyleAttributeName])
  944. {
  945. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range),
  946. kCTParagraphStyleAttributeName, (CTParagraphStyleRef)value);
  947. }
  948. else if ([key isEqualToString:NSParagraphStyleAttributeName])
  949. {
  950. NSMutableAttributedString *mutableText = [value mutableCopy];
  951. [mutableText addAttribute:NSParagraphStyleAttributeName value:(NSParagraphStyle *)value range:range];
  952. }
  953. else if ([key isEqualToString:(NSString *)kCTForegroundColorAttributeName]
  954. || [key isEqualToString:NSForegroundColorAttributeName]
  955. || [key caseInsensitiveCompare:@"color"] == NSOrderedSame)
  956. {
  957. [self applyColor:value toText:text range:range];
  958. }
  959. else if ([key isEqualToString:(NSString *)kCTStrokeWidthAttributeName]
  960. || [key isEqualToString:NSStrokeWidthAttributeName])
  961. {
  962. CFAttributedStringSetAttribute(text,
  963. CFRangeFromNSRange(range),
  964. kCTStrokeWidthAttributeName,
  965. (__bridge CFTypeRef)([attributes objectForKey:(NSString *)kCTStrokeWidthAttributeName]));
  966. }
  967. else if ([key isEqualToString:(NSString *)kCTStrokeColorAttributeName]
  968. || [key isEqualToString:NSStrokeColorAttributeName])
  969. {
  970. [self applyStrokeColor:value toText:text range:range];
  971. }
  972. else if ([key isEqualToString:(NSString *)kCTKernAttributeName]
  973. || [key isEqualToString:NSKernAttributeName])
  974. {
  975. CFAttributedStringSetAttribute(text,
  976. CFRangeFromNSRange(range),
  977. kCTKernAttributeName,
  978. (__bridge CFTypeRef)([attributes objectForKey:(NSString *)kCTKernAttributeName]));
  979. }
  980. else if ([key isEqualToString:(NSString *)kCTUnderlineStyleAttributeName]
  981. || [key isEqualToString:NSUnderlineStyleAttributeName])
  982. {
  983. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTUnderlineStyleAttributeName, (__bridge CFNumberRef)value);
  984. }
  985. }
  986. UIFont *font = [attributes objectForKey:(NSString *)kCTFontNameAttribute];
  987. if (font)
  988. {
  989. CTFontRef customFont = CTFontCreateWithName ((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
  990. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, customFont);
  991. CFRelease(customFont);
  992. return;
  993. }
  994. font = [attributes objectForKey:NSFontAttributeName];
  995. if (font)
  996. {
  997. CTFontRef customFont = CTFontCreateWithName ((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
  998. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, customFont);
  999. CFRelease(customFont);
  1000. }
  1001. else
  1002. {
  1003. CTFontRef customFont = CTFontCreateWithName ((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL);
  1004. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, customFont);
  1005. CFRelease(customFont);
  1006. }
  1007. }
  1008. - (void)applyBoldStyleToText:(CFMutableAttributedStringRef)text
  1009. range:(NSRange)range
  1010. {
  1011. CFTypeRef actualFontRef = CFAttributedStringGetAttribute(text, range.location, kCTFontAttributeName, NULL);
  1012. CTFontRef boldFontRef = CTFontCreateCopyWithSymbolicTraits(actualFontRef, 0.0, NULL, kCTFontBoldTrait, kCTFontBoldTrait);
  1013. BOOL forceCustomFont = self.customBoldFontName;
  1014. if (!boldFontRef || forceCustomFont)
  1015. {
  1016. // UIFont *font = [UIFont boldSystemFontOfSize:CTFontGetSize(actualFontRef)];
  1017. UIFont *font = [self boldFontOfSize:CTFontGetSize(actualFontRef)];
  1018. boldFontRef = CTFontCreateWithName((__bridge CFStringRef)font.fontName, self.font.pointSize, NULL);
  1019. }
  1020. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, boldFontRef);
  1021. CFRelease(boldFontRef);
  1022. }
  1023. - (void)applyBoldItalicStyleToText:(CFMutableAttributedStringRef)text
  1024. range:(NSRange)range
  1025. {
  1026. CFTypeRef actualFontRef = CFAttributedStringGetAttribute(text, range.location, kCTFontAttributeName, NULL);
  1027. CTFontRef boldItalicFontRef = CTFontCreateCopyWithSymbolicTraits(actualFontRef, 0.0, NULL, kCTFontBoldTrait | kCTFontItalicTrait , kCTFontBoldTrait | kCTFontItalicTrait);
  1028. BOOL forceCustomFont = self.customBoldItalicFontName;
  1029. if (!boldItalicFontRef || forceCustomFont)
  1030. {
  1031. // NSString *fontName = [NSString stringWithFormat:@"%@-BoldOblique", self.font.fontName];
  1032. UIFont *font = [self boldItalicFontOfSize:CTFontGetSize(actualFontRef)];
  1033. boldItalicFontRef = CTFontCreateWithName((__bridge CFStringRef)font.fontName, self.font.pointSize, NULL);
  1034. }
  1035. if (boldItalicFontRef)
  1036. {
  1037. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTFontAttributeName, boldItalicFontRef);
  1038. CFRelease(boldItalicFontRef);
  1039. }
  1040. }
  1041. - (void)applyColor:(id)value
  1042. toText:(CFMutableAttributedStringRef)text
  1043. range:(NSRange)range
  1044. {
  1045. if ([value isKindOfClass:[UIColor class]])
  1046. {
  1047. UIColor *color = (UIColor *)value;
  1048. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTForegroundColorAttributeName, color.CGColor);
  1049. }
  1050. else if ([value isKindOfClass:[NSString class]])
  1051. {
  1052. if ([value rangeOfString:@"#"].location == 0)
  1053. {
  1054. if ([value rangeOfString:@"#"].location == 0)
  1055. {
  1056. value = [value stringByReplacingOccurrencesOfString:@"#" withString:@""];
  1057. }
  1058. CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
  1059. NSArray *colorComponents = CGColorComponentsForHex(value);
  1060. CGFloat components[] = {[[colorComponents objectAtIndex:0] floatValue],
  1061. [[colorComponents objectAtIndex:1] floatValue],
  1062. [[colorComponents objectAtIndex:2] floatValue],
  1063. [[colorComponents objectAtIndex:3] floatValue]};
  1064. CGColorRef color = CGColorCreate(rgbColorSpace, components);
  1065. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTForegroundColorAttributeName, color);
  1066. CFRelease(color);
  1067. CGColorSpaceRelease(rgbColorSpace);
  1068. }
  1069. }
  1070. }
  1071. - (void)applyStrokeColor:(id)value
  1072. toText:(CFMutableAttributedStringRef)text
  1073. range:(NSRange)range
  1074. {
  1075. if ([value isKindOfClass:[UIColor class]])
  1076. {
  1077. UIColor *color = (UIColor *)value;
  1078. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTStrokeColorAttributeName, color.CGColor);
  1079. }
  1080. else if ([value isKindOfClass:[NSString class]])
  1081. {
  1082. if ([value rangeOfString:@"#"].location == 0)
  1083. {
  1084. value = [value stringByReplacingOccurrencesOfString:@"#" withString:@""];
  1085. }
  1086. CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
  1087. NSArray *colorComponents = CGColorComponentsForHex(value);
  1088. CGFloat components[] = {[[colorComponents objectAtIndex:0] floatValue],
  1089. [[colorComponents objectAtIndex:1] floatValue],
  1090. [[colorComponents objectAtIndex:2] floatValue],
  1091. [[colorComponents objectAtIndex:3] floatValue]};
  1092. CGColorRef color = CGColorCreate(rgbColorSpace, components);
  1093. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTStrokeColorAttributeName, color);
  1094. CFRelease(color);
  1095. CGColorSpaceRelease(rgbColorSpace);
  1096. }
  1097. }
  1098. - (void)applyUnderlineColor:(id)value
  1099. toText:(CFMutableAttributedStringRef)text
  1100. range:(NSRange)range
  1101. {
  1102. if ([value isKindOfClass:[UIColor class]])
  1103. {
  1104. UIColor *color = (UIColor *)value;
  1105. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTForegroundColorAttributeName, color.CGColor);
  1106. }
  1107. else if ([value isKindOfClass:[NSString class]])
  1108. {
  1109. value = [value stringByReplacingOccurrencesOfString:@"'" withString:@""];
  1110. if ([value rangeOfString:@"#"].location==0)
  1111. {
  1112. CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
  1113. value = [value stringByReplacingOccurrencesOfString:@"#" withString:@""];
  1114. NSArray *colorComponents = CGColorComponentsForHex(value);
  1115. CGFloat components[] = {[[colorComponents objectAtIndex:0] floatValue],
  1116. [[colorComponents objectAtIndex:1] floatValue],
  1117. [[colorComponents objectAtIndex:2] floatValue],
  1118. [[colorComponents objectAtIndex:3] floatValue]};
  1119. CGColorRef color = CGColorCreate(rgbColorSpace, components);
  1120. CFAttributedStringSetAttribute(text, CFRangeFromNSRange(range), kCTUnderlineColorAttributeName, color);
  1121. CGColorRelease(color);
  1122. CGColorSpaceRelease(rgbColorSpace);
  1123. }
  1124. }
  1125. }
  1126. #pragma mark - Parsing methods
  1127. - (void)extractStyleFromText:(NSString *)data
  1128. {
  1129. // clear existing links
  1130. self.links = [NSMutableArray array];
  1131. // Replace html entities
  1132. if (data)
  1133. {
  1134. data = [data stringByReplacingOccurrencesOfString:@"&lt;" withString:@"<"];
  1135. data = [data stringByReplacingOccurrencesOfString:@"&gt;" withString:@">"];
  1136. data = [data stringByReplacingOccurrencesOfString:@"&amp;" withString:@"&"];
  1137. data = [data stringByReplacingOccurrencesOfString:@"&apos;" withString:@"'"];
  1138. data = [data stringByReplacingOccurrencesOfString:@"&quot;" withString:@"\""];
  1139. }
  1140. NSMutableArray *components = [NSMutableArray array];
  1141. NSInteger last_position = 0;
  1142. NSString *text = nil;
  1143. NSString *htmlTag = nil;
  1144. NSScanner *scanner = [NSScanner scannerWithString:data];
  1145. while (!scanner.isAtEnd)
  1146. {
  1147. // Get position of scanner, used to check if <p> tags are at the start of the text
  1148. NSInteger tagStartPosition = scanner.scanLocation;
  1149. // Capture tag text
  1150. [scanner scanUpToString:@"<" intoString:NULL];
  1151. [scanner scanUpToString:@">" intoString:&text];
  1152. NSString *fullTag = [NSString stringWithFormat:@"%@>", text];
  1153. NSInteger position = [data rangeOfString:fullTag].location;
  1154. if (position != NSNotFound)
  1155. {
  1156. // Remove tag from text and replace occurences of paragraph tags
  1157. if ([fullTag rangeOfString:@"<p"].location == 0 && tagStartPosition != 0)
  1158. {
  1159. data = [data stringByReplacingOccurrencesOfString:fullTag
  1160. withString:@"\n"
  1161. options:NSCaseInsensitiveSearch
  1162. range:NSMakeRange(last_position, position + fullTag.length - last_position)];
  1163. }
  1164. else
  1165. {
  1166. data = [data stringByReplacingOccurrencesOfString:fullTag
  1167. withString:@""
  1168. options:NSCaseInsensitiveSearch
  1169. range:NSMakeRange(last_position, position + fullTag.length - last_position)];
  1170. }
  1171. }
  1172. // Found closing tag
  1173. if ([text rangeOfString:@"</"].location == 0)
  1174. {
  1175. // Get just the html tag value
  1176. htmlTag = [text substringFromIndex:2];
  1177. if (position != NSNotFound)
  1178. {
  1179. // Find the the corresponding component for the closing tag
  1180. for (NSInteger i = components.count - 1; i >= 0; i--)
  1181. {
  1182. MDHTMLComponent *component = components[i];
  1183. if (component.text == nil && [component.htmlTag isEqualToString:htmlTag])
  1184. {
  1185. NSString *componentText = [data substringWithRange:NSMakeRange(component.position, position - component.position)];
  1186. component.text = componentText;
  1187. if ([component.htmlTag caseInsensitiveCompare:@"a"] == NSOrderedSame)
  1188. {
  1189. NSTextCheckingResult *result = [NSTextCheckingResult linkCheckingResultWithRange:component.range
  1190. URL:[NSURL URLWithString:component.attributes[@"href"]]];
  1191. [self.links addObject:result];
  1192. }
  1193. break;
  1194. }
  1195. }
  1196. }
  1197. }
  1198. else
  1199. {
  1200. // Get text components without the opening '<'
  1201. NSMutableArray *textComponents = [[[text substringFromIndex:1] componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
  1202. // Capture html tag for later
  1203. htmlTag = textComponents[0];
  1204. if (htmlTag.length > 0) {
  1205. // remove the tag
  1206. [textComponents removeObjectAtIndex:0];
  1207. // remove consecutive spaces
  1208. [textComponents removeObject:@""];
  1209. // clear out spaces around "=" since they cause a crash and make it hard to identify which are keys and which are values
  1210. NSString *cleanText = [textComponents componentsJoinedByString:@" "];
  1211. cleanText = [cleanText stringByReplacingOccurrencesOfString:@"= " withString:@"="];
  1212. cleanText = [cleanText stringByReplacingOccurrencesOfString:@" =" withString:@"="];
  1213. // clean out spaces around value quotes but preserve spaces between pairs
  1214. cleanText = [cleanText stringByReplacingOccurrencesOfString:@" ' " withString:@"' "];
  1215. cleanText = [cleanText stringByReplacingOccurrencesOfString:@"=' " withString:@"='"];
  1216. cleanText = [cleanText stringByReplacingOccurrencesOfString:@" \" " withString:@"\" "];
  1217. cleanText = [cleanText stringByReplacingOccurrencesOfString:@"=\" " withString:@"=\""];
  1218. textComponents = [[cleanText componentsSeparatedByString:@" "] mutableCopy];
  1219. // spaces can still exist inside of values and they will be split, put them back with their key/value pair
  1220. NSUInteger lastPairIndex = 0;
  1221. if (textComponents.count > 1) {
  1222. for (NSUInteger index = 1; index < textComponents.count; index++) {
  1223. NSRange equalRange = [textComponents[index] rangeOfString:@"="];
  1224. if (equalRange.location != NSNotFound) {
  1225. lastPairIndex = index;
  1226. } else {
  1227. textComponents[lastPairIndex] = [textComponents[lastPairIndex] stringByAppendingFormat:@" %@", textComponents[index]];
  1228. textComponents[index] = @"";
  1229. }
  1230. }
  1231. }
  1232. // Capture the tag's attributes
  1233. NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
  1234. for (NSString *pairString in textComponents) {
  1235. if (pairString.length > 0) {
  1236. NSArray *pair = [pairString componentsSeparatedByString:@"="];
  1237. if (pair.count > 0) {
  1238. NSString *key = [pair[0] lowercaseString];
  1239. if (pair.count >= 2) {
  1240. NSString *value = [[pair subarrayWithRange:NSMakeRange(1, [pair count] - 1)] componentsJoinedByString:@"="];
  1241. value = [value stringByReplacingOccurrencesOfString:@"\"" withString:@"" options:NSLiteralSearch range:NSMakeRange(0, 1)];
  1242. value = [value stringByReplacingOccurrencesOfString:@"\"" withString:@"" options:NSLiteralSearch range:NSMakeRange([value length]-1, 1)];
  1243. value = [value stringByReplacingOccurrencesOfString:@"'" withString:@"" options:NSLiteralSearch range:NSMakeRange(0, 1)];
  1244. value = [value stringByReplacingOccurrencesOfString:@"'" withString:@"" options:NSLiteralSearch range:NSMakeRange([value length]-1, 1)];
  1245. value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
  1246. attributes[key] = value;
  1247. } else if (pair.count == 1) {
  1248. attributes[key] = key;
  1249. }
  1250. }
  1251. }
  1252. }
  1253. // Create component from tag and attributes, we'll know the text once we reach the closing tag
  1254. MDHTMLComponent *component = [[MDHTMLComponent alloc] initWithString:nil htmlTag:htmlTag attributes:attributes];
  1255. component.position = position;
  1256. [components addObject:component];
  1257. }
  1258. }
  1259. last_position = position;
  1260. }
  1261. self.styleComponents = components;
  1262. self.plainText = data;
  1263. }
  1264. #pragma mark - UILabel
  1265. - (void)setHighlighted:(BOOL)highlighted
  1266. {
  1267. [super setHighlighted:highlighted];
  1268. [self setNeedsDisplay];
  1269. }
  1270. // Fixes crash when loading from a UIStoryboard
  1271. - (UIColor *)textColor
  1272. {
  1273. UIColor *color = [super textColor];
  1274. if (!color)
  1275. {
  1276. color = [UIColor blackColor];
  1277. }
  1278. return color;
  1279. }
  1280. - (CGRect)textRectForBounds:(CGRect)bounds
  1281. limitedToNumberOfLines:(NSInteger)numberOfLines
  1282. {
  1283. if (!self.htmlText)
  1284. {
  1285. return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
  1286. }
  1287. CGRect textRect = bounds;
  1288. // 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.
  1289. textRect.size.height = MAX(self.font.pointSize * 2.0f, bounds.size.height);
  1290. // Adjust the text to be in the center vertically, if the text size is smaller than bounds
  1291. CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, CFRangeMake(0, (CFIndex)self.htmlAttributedText.length), NULL, textRect.size, NULL);
  1292. textSize = CGSizeMake(CGFloat_ceil(textSize.width), CGFloat_ceil(textSize.height)); // Fix for iOS 4, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns fractional sizes
  1293. if (textSize.height < textRect.size.height)
  1294. {
  1295. CGFloat yOffset = 0.0f;
  1296. switch (self.verticalAlignment)
  1297. {
  1298. case MDHTMLLabelVerticalAlignmentCenter:
  1299. yOffset = CGFloat_floor((bounds.size.height - textSize.height) / 2.0f);
  1300. break;
  1301. case MDHTMLLabelVerticalAlignmentBottom:
  1302. yOffset = bounds.size.height - textSize.height;
  1303. break;
  1304. case MDHTMLLabelVerticalAlignmentTop:
  1305. default:
  1306. break;
  1307. }
  1308. textRect.origin.y += yOffset;
  1309. }
  1310. return textRect;
  1311. }
  1312. #pragma mark - UIView
  1313. + (CGFloat)sizeThatFitsHTMLString:(NSString *)htmlString
  1314. withFont:(UIFont *)font
  1315. constraints:(CGSize)size
  1316. limitedToNumberOfLines:(NSUInteger)numberOfLines
  1317. autoDetectUrls:(BOOL)autoDetectUrls
  1318. {
  1319. MDHTMLLabel *label = [[MDHTMLLabel alloc] initWithFrame:CGRectMake(0.0, 0.0, size.width, size.height)];
  1320. label.font = font;
  1321. label.numberOfLines = numberOfLines;
  1322. label.lineBreakMode = NSLineBreakByWordWrapping;
  1323. label.autoDetectUrls = autoDetectUrls;
  1324. label.htmlText = htmlString;
  1325. return [label sizeThatFits:size].height;
  1326. }
  1327. - (CGSize)sizeThatFits:(CGSize)size
  1328. {
  1329. if (!self.htmlAttributedText)
  1330. {
  1331. return [super sizeThatFits:size];
  1332. }
  1333. return CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(self.framesetter, self.htmlAttributedText, size, (NSUInteger)self.numberOfLines);
  1334. }
  1335. - (CGSize)intrinsicContentSize
  1336. {
  1337. // There's an implicit width from the original UILabel implementation
  1338. return [self sizeThatFits:[super intrinsicContentSize]];
  1339. }
  1340. - (void)tintColorDidChange
  1341. {
  1342. BOOL isInactive = (CGColorSpaceGetModel(CGColorGetColorSpace([self.tintColor CGColor])) == kCGColorSpaceModelMonochrome);
  1343. NSMutableDictionary *mutableLinkAttributes = [self.linkAttributes mutableCopy];
  1344. if (!mutableLinkAttributes[(NSString *)kCTForegroundColorAttributeName] && !mutableLinkAttributes[NSForegroundColorAttributeName])
  1345. {
  1346. if ([self respondsToSelector:@selector(tintColor)])
  1347. {
  1348. mutableLinkAttributes[(NSString *)kCTForegroundColorAttributeName] = self.tintColor;
  1349. }
  1350. else
  1351. {
  1352. mutableLinkAttributes[(NSString *)kCTForegroundColorAttributeName] = [UIColor blueColor];
  1353. }
  1354. }
  1355. if (!mutableLinkAttributes[(NSString *)kCTFontAttributeName] && !mutableLinkAttributes[NSFontAttributeName])
  1356. {
  1357. mutableLinkAttributes[(NSString *)kCTFontAttributeName] = self.font;
  1358. }
  1359. NSMutableDictionary *mutableInactiveLinkAttributes = [self.inactiveLinkAttributes mutableCopy];
  1360. if (!mutableInactiveLinkAttributes[(NSString *)kCTForegroundColorAttributeName] && !mutableInactiveLinkAttributes[NSForegroundColorAttributeName])
  1361. {
  1362. mutableInactiveLinkAttributes[(NSString *)kCTForegroundColorAttributeName] = [UIColor grayColor];
  1363. }
  1364. if (!mutableInactiveLinkAttributes[(NSString *)kCTFontAttributeName] && !mutableInactiveLinkAttributes[NSFontAttributeName])
  1365. {
  1366. mutableInactiveLinkAttributes[(NSString *)kCTFontAttributeName] = mutableLinkAttributes[(NSString *)kCTFontAttributeName];
  1367. }
  1368. NSDictionary *attributesToRemove = isInactive ? mutableLinkAttributes : mutableInactiveLinkAttributes;
  1369. NSDictionary *attributesToAdd = isInactive ? mutableInactiveLinkAttributes : mutableLinkAttributes;
  1370. NSMutableAttributedString *mutableAttributedString = [self.htmlAttributedText mutableCopy];
  1371. for (NSTextCheckingResult *result in self.links)
  1372. {
  1373. [attributesToRemove enumerateKeysAndObjectsUsingBlock:^(NSString *name, __unused id value, __unused BOOL *stop)
  1374. {
  1375. [mutableAttributedString removeAttribute:name range:result.range];
  1376. }];
  1377. if (attributesToAdd)
  1378. {
  1379. [mutableAttributedString addAttributes:attributesToAdd range:result.range];
  1380. }
  1381. }
  1382. self.htmlAttributedText = mutableAttributedString;
  1383. [self setNeedsDisplay];
  1384. }
  1385. #pragma mark - Data Detection
  1386. - (NSString *)detectURLsInText:(NSString *)text
  1387. {
  1388. NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:NULL];
  1389. NSUInteger matchDetectorStartLocation = 0;
  1390. NSTextCheckingResult *match = [detector firstMatchInString:text
  1391. options:kNilOptions
  1392. range:NSMakeRange(matchDetectorStartLocation, text.length)];
  1393. while (match != nil && match.range.location != NSNotFound)
  1394. {
  1395. NSUInteger matchLength = match.range.length;
  1396. if (match.resultType == NSTextCheckingTypeLink)
  1397. {
  1398. // if there's no "<a" or "href" before the link regardless of spaces. e.g. this is valid <a href = "SomeURL">
  1399. BOOL insideHref = NO;
  1400. // an href
  1401. // there has to be room for at least "<a href='" before we bother checking this
  1402. if ((match.range.location - matchDetectorStartLocation) >= 8) {
  1403. NSRange prevTextRange = NSMakeRange(matchDetectorStartLocation, (match.range.location - 1) - matchDetectorStartLocation);
  1404. NSRange prevHrefRange = [text rangeOfString:@"href" options:NSCaseInsensitiveSearch | NSBackwardsSearch range:prevTextRange];
  1405. NSRange prevStartATagRange = [text rangeOfString:@"<a" options:NSCaseInsensitiveSearch | NSBackwardsSearch range:prevTextRange];
  1406. NSRange prevEndTagRange = [text rangeOfString:@">" options:NSCaseInsensitiveSearch | NSBackwardsSearch range:prevTextRange];
  1407. insideHref = (prevHrefRange.location != NSNotFound && prevStartATagRange.location != NSNotFound &&
  1408. NSMaxRange(prevStartATagRange) < prevHrefRange.location &&
  1409. (prevEndTagRange.location == NSNotFound || (prevEndTagRange.location != NSNotFound &&
  1410. prevStartATagRange.location >= NSMaxRange(prevEndTagRange))));
  1411. }
  1412. BOOL wrappedInAnchors = [text rangeOfString:@"a>" options:NSCaseInsensitiveSearch | NSBackwardsSearch range:match.range].location != NSNotFound;
  1413. if (!insideHref && !wrappedInAnchors)
  1414. {
  1415. NSString *wrappedURL = [NSString stringWithFormat:@"<a href='%@'>%@</a>", match.URL.absoluteString, match.URL.absoluteString];
  1416. text = [text stringByReplacingCharactersInRange:match.range
  1417. withString:wrappedURL];
  1418. matchLength = wrappedURL.length;
  1419. }
  1420. }
  1421. matchDetectorStartLocation = match.range.location + matchLength;
  1422. match = [detector firstMatchInString:text
  1423. options:kNilOptions
  1424. range:NSMakeRange(matchDetectorStartLocation, text.length - matchDetectorStartLocation)];
  1425. }
  1426. return text;
  1427. }
  1428. - (NSTextCheckingResult *)linkAtCharacterIndex:(CFIndex)idx
  1429. {
  1430. NSEnumerator *enumerator = [self.links reverseObjectEnumerator];
  1431. NSTextCheckingResult *result = nil;
  1432. while ((result = [enumerator nextObject]))
  1433. {
  1434. if (NSLocationInRange((NSUInteger)idx, result.range))
  1435. {
  1436. return result;
  1437. }
  1438. }
  1439. return nil;
  1440. }
  1441. - (NSTextCheckingResult *)linkAtPoint:(CGPoint)p
  1442. {
  1443. CFIndex idx = [self characterIndexAtPoint:p];
  1444. return [self linkAtCharacterIndex:idx];
  1445. }
  1446. - (CFIndex)characterIndexAtPoint:(CGPoint)p
  1447. {
  1448. if (!CGRectContainsPoint(self.bounds, p))
  1449. {
  1450. return NSNotFound;
  1451. }
  1452. CGRect textRect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines];
  1453. if (!CGRectContainsPoint(textRect, p))
  1454. {
  1455. return NSNotFound;
  1456. }
  1457. // Offset tap coordinates by textRect origin to make them relative to the origin of frame
  1458. p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y);
  1459. // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left)
  1460. p = CGPointMake(p.x, textRect.size.height - p.y);
  1461. CGMutablePathRef path = CGPathCreateMutable();
  1462. CGPathAddRect(path, NULL, textRect);
  1463. CTFrameRef frame = CTFramesetterCreateFrame([self framesetter], CFRangeMake(0, (CFIndex)self.htmlAttributedText.length), path, NULL);
  1464. if (frame == NULL)
  1465. {
  1466. CFRelease(path);
  1467. return NSNotFound;
  1468. }
  1469. CFArrayRef lines = CTFrameGetLines(frame);
  1470. NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
  1471. if (numberOfLines == 0)
  1472. {
  1473. CFRelease(frame);
  1474. CFRelease(path);
  1475. return NSNotFound;
  1476. }
  1477. CFIndex idx = NSNotFound;
  1478. CGPoint lineOrigins[numberOfLines];
  1479. CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
  1480. for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++)
  1481. {
  1482. CGPoint lineOrigin = lineOrigins[lineIndex];
  1483. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  1484. // Get bounding information of line
  1485. CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
  1486. CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  1487. CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
  1488. CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);
  1489. // Check if we've already passed the line
  1490. if (p.y > yMax)
  1491. {
  1492. break;
  1493. }
  1494. // Check if the point is within this line vertically
  1495. if (p.y >= yMin)
  1496. {
  1497. // Check if the point is within this line horizontally
  1498. if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
  1499. // Convert CT coordinates to line-relative coordinates
  1500. CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
  1501. idx = CTLineGetStringIndexForPosition(line, relativePoint);
  1502. break;
  1503. }
  1504. }
  1505. }
  1506. CFRelease(frame);
  1507. CFRelease(path);
  1508. return idx;
  1509. }
  1510. #pragma mark - UIResponder
  1511. - (BOOL)canBecomeFirstResponder
  1512. {
  1513. return YES;
  1514. }
  1515. - (BOOL)canPerformAction:(SEL)action
  1516. withSender:(__unused id)sender
  1517. {
  1518. return (action == @selector(copy:));
  1519. }
  1520. - (void)touchesBegan:(NSSet *)touches
  1521. withEvent:(UIEvent *)event
  1522. {
  1523. UITouch *touch = [touches anyObject];
  1524. self.activeLink = [self linkAtPoint:[touch locationInView:self]];
  1525. if (self.activeLink)
  1526. {
  1527. self.holdGestureTimer = [NSTimer scheduledTimerWithTimeInterval:self.minimumPressDuration
  1528. target:self
  1529. selector:@selector(handleDidHoldTouch:)
  1530. userInfo:touch
  1531. repeats:NO];
  1532. }
  1533. else
  1534. {
  1535. [super touchesBegan:touches withEvent:event];
  1536. self.highlighted = YES;
  1537. }
  1538. }
  1539. - (void)touchesMoved:(NSSet *)touches
  1540. withEvent:(UIEvent *)event
  1541. {
  1542. if (self.activeLink)
  1543. {
  1544. UITouch *touch = [touches anyObject];
  1545. if (self.activeLink != [self linkAtPoint:[touch locationInView:self]])
  1546. {
  1547. self.activeLink = nil;
  1548. [self.holdGestureTimer invalidate];
  1549. }
  1550. }
  1551. else
  1552. {
  1553. [super touchesMoved:touches withEvent:event];
  1554. }
  1555. }
  1556. - (void)touchesEnded:(NSSet *)touches
  1557. withEvent:(UIEvent *)event
  1558. {
  1559. self.highlighted = NO;
  1560. if (self.activeLink)
  1561. {
  1562. NSTextCheckingResult *result = self.activeLink;
  1563. self.activeLink = nil;
  1564. [self.holdGestureTimer invalidate];
  1565. if ([self.delegate respondsToSelector:@selector(HTMLLabel:didSelectLinkWithURL:)])
  1566. {
  1567. [self.delegate HTMLLabel:self didSelectLinkWithURL:result.URL];
  1568. return;
  1569. }
  1570. }
  1571. else
  1572. {
  1573. [super touchesEnded:touches withEvent:event];
  1574. }
  1575. }
  1576. - (void)touchesCancelled:(NSSet *)touches
  1577. withEvent:(UIEvent *)event
  1578. {
  1579. if (self.activeLink)
  1580. {
  1581. self.activeLink = nil;
  1582. }
  1583. else
  1584. {
  1585. [super touchesCancelled:touches withEvent:event];
  1586. }
  1587. }
  1588. - (void)handleDidHoldTouch:(NSTimer *)timer
  1589. {
  1590. self.highlighted = NO;
  1591. [self.holdGestureTimer invalidate];
  1592. if ([self.delegate respondsToSelector:@selector(HTMLLabel:didHoldLinkWithURL:)])
  1593. {
  1594. NSTextCheckingResult *result = self.activeLink;
  1595. self.activeLink = nil;
  1596. [self.delegate HTMLLabel:self didHoldLinkWithURL:result.URL];
  1597. }
  1598. }
  1599. #pragma mark - UIResponderStandardEditActions
  1600. - (void)copy:(id)sender
  1601. {
  1602. if (self.htmlText)
  1603. {
  1604. [[UIPasteboard generalPasteboard] setString:self.plainText];
  1605. }
  1606. else
  1607. {
  1608. [super copy:sender];
  1609. }
  1610. }
  1611. #pragma mark - NSCoding
  1612. - (void)encodeWithCoder:(NSCoder *)coder
  1613. {
  1614. [super encodeWithCoder:coder];
  1615. [coder encodeObject:self.htmlText forKey:NSStringFromSelector(@selector(links))];
  1616. [coder encodeObject:self.links forKey:NSStringFromSelector(@selector(links))];
  1617. [coder encodeObject:self.linkAttributes forKey:NSStringFromSelector(@selector(linkAttributes))];
  1618. [coder encodeObject:self.activeLinkAttributes forKey:NSStringFromSelector(@selector(activeLinkAttributes))];
  1619. [coder encodeObject:self.inactiveLinkAttributes forKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
  1620. [coder encodeDouble:self.minimumPressDuration forKey:NSStringFromSelector(@selector(minimumPressDuration))];
  1621. [coder encodeDouble:self.shadowRadius forKey:NSStringFromSelector(@selector(shadowRadius))];
  1622. [coder encodeDouble:self.highlightedShadowRadius forKey:NSStringFromSelector(@selector(highlightedShadowRadius))];
  1623. [coder encodeCGSize:self.highlightedShadowOffset forKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
  1624. [coder encodeObject:self.highlightedShadowColor forKey:NSStringFromSelector(@selector(highlightedShadowColor))];
  1625. [coder encodeDouble:self.firstLineIndent forKey:NSStringFromSelector(@selector(firstLineIndent))];
  1626. [coder encodeDouble:self.leading forKey:NSStringFromSelector(@selector(leading))];
  1627. [coder encodeDouble:self.lineHeightMultiple forKey:NSStringFromSelector(@selector(lineHeightMultiple))];
  1628. [coder encodeUIEdgeInsets:self.textInsets forKey:NSStringFromSelector(@selector(textInsets))];
  1629. [coder encodeInteger:self.verticalAlignment forKey:NSStringFromSelector(@selector(verticalAlignment))];
  1630. [coder encodeObject:self.truncationTokenString forKey:NSStringFromSelector(@selector(truncationTokenString))];
  1631. [coder encodeObject:self.truncationTokenStringAttributes forKey:NSStringFromSelector(@selector(truncationTokenStringAttributes))];
  1632. }
  1633. - (id)initWithCoder:(NSCoder *)coder
  1634. {
  1635. self = [super initWithCoder:coder];
  1636. if (self)
  1637. {
  1638. [self commonInit];
  1639. if ([coder containsValueForKey:NSStringFromSelector(@selector(htmlText))])
  1640. {
  1641. self.htmlText = [coder decodeObjectForKey:NSStringFromSelector(@selector(htmlText))];
  1642. }
  1643. if ([coder containsValueForKey:NSStringFromSelector(@selector(links))])
  1644. {
  1645. self.links = [coder decodeObjectForKey:NSStringFromSelector(@selector(links))];
  1646. }
  1647. if ([coder containsValueForKey:NSStringFromSelector(@selector(linkAttributes))])
  1648. {
  1649. self.linkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkAttributes))];
  1650. }
  1651. if ([coder containsValueForKey:NSStringFromSelector(@selector(activeLinkAttributes))])
  1652. {
  1653. self.activeLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(activeLinkAttributes))];
  1654. }
  1655. if ([coder containsValueForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))])
  1656. {
  1657. self.inactiveLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
  1658. }
  1659. if ([coder containsValueForKey:NSStringFromSelector(@selector(minimumPressDuration))])
  1660. {
  1661. self.minimumPressDuration = [coder decodeDoubleForKey:NSStringFromSelector(@selector(minimumPressDuration))];
  1662. }
  1663. if ([coder containsValueForKey:NSStringFromSelector(@selector(shadowRadius))])
  1664. {
  1665. self.shadowRadius = [coder decodeDoubleForKey:NSStringFromSelector(@selector(shadowRadius))];
  1666. }
  1667. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowRadius))])
  1668. {
  1669. self.highlightedShadowRadius = [coder decodeDoubleForKey:NSStringFromSelector(@selector(highlightedShadowRadius))];
  1670. }
  1671. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowOffset))])
  1672. {
  1673. self.highlightedShadowOffset = [coder decodeCGSizeForKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
  1674. }
  1675. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowColor))])
  1676. {
  1677. self.highlightedShadowColor = [coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowColor))];
  1678. }
  1679. if ([coder containsValueForKey:NSStringFromSelector(@selector(firstLineIndent))])
  1680. {
  1681. self.firstLineIndent = [coder decodeDoubleForKey:NSStringFromSelector(@selector(firstLineHeadIndent))];
  1682. }
  1683. if ([coder containsValueForKey:NSStringFromSelector(@selector(leading))])
  1684. {
  1685. self.leading = [coder decodeDoubleForKey:NSStringFromSelector(@selector(leading))];
  1686. }
  1687. if ([coder containsValueForKey:NSStringFromSelector(@selector(lineHeightMultiple))])
  1688. {
  1689. self.lineHeightMultiple = [coder decodeDoubleForKey:NSStringFromSelector(@selector(lineHeightMultiple))];
  1690. }
  1691. if ([coder containsValueForKey:NSStringFromSelector(@selector(textInsets))])
  1692. {
  1693. self.textInsets = [coder decodeUIEdgeInsetsForKey:NSStringFromSelector(@selector(textInsets))];
  1694. }
  1695. if ([coder containsValueForKey:NSStringFromSelector(@selector(verticalAlignment))])
  1696. {
  1697. self.verticalAlignment = [coder decodeIntegerForKey:NSStringFromSelector(@selector(verticalAlignment))];
  1698. }
  1699. if ([coder containsValueForKey:NSStringFromSelector(@selector(truncationTokenString))])
  1700. {
  1701. self.truncationTokenString = [coder decodeObjectForKey:NSStringFromSelector(@selector(truncationTokenString))];
  1702. }
  1703. if ([coder containsValueForKey:NSStringFromSelector(@selector(truncationTokenStringAttributes))])
  1704. {
  1705. self.truncationTokenStringAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(truncationTokenStringAttributes))];
  1706. }
  1707. }
  1708. return self;
  1709. }
  1710. #pragma mark - Custom fonts
  1711. - (UIFont *)boldFontOfSize:(CGFloat)size {
  1712. if (self.customBoldFontName) {
  1713. return [UIFont fontWithName:self.customBoldFontName size:size];
  1714. }
  1715. return [UIFont boldSystemFontOfSize:size];
  1716. }
  1717. - (UIFont *)italicFontOfSize:(CGFloat)size {
  1718. if (self.customItalicFontName) {
  1719. return [UIFont fontWithName:self.customItalicFontName size:size];
  1720. }
  1721. return [UIFont italicSystemFontOfSize:size];
  1722. }
  1723. - (UIFont *)boldItalicFontOfSize:(CGFloat)size {
  1724. if (self.customBoldItalicFontName) {
  1725. return [UIFont fontWithName:self.customBoldItalicFontName size:size];
  1726. }
  1727. return [UIFont fontWithName:[NSString stringWithFormat:@"%@-BoldOblique", self.font.fontName] size:size];
  1728. }
  1729. @end