├── Tests └── HTMLLabelTests │ ├── HTMLLabelTests │ ├── en.lproj │ │ ├── InfoPlist.strings │ │ └── ParsingViewController.xib │ ├── Default.png │ ├── Default@2x.png │ ├── Default-568h@2x.png │ ├── ParsingViewController.h │ ├── AppDelegate.h │ ├── HTMLLabelTests-Prefix.pch │ ├── main.m │ ├── AppDelegate.m │ ├── HTMLLabelTests-Info.plist │ └── ParsingViewController.m │ └── HTMLLabelTests.xcodeproj │ └── project.pbxproj ├── RELEASE NOTES.md ├── HTMLLabel.podspec.json ├── LICENCE.md ├── README.md └── HTMLLabel ├── HTMLLabel.h └── HTMLLabel.m /Tests/HTMLLabelTests/HTMLLabelTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklockwood/HTMLLabel/HEAD/Tests/HTMLLabelTests/HTMLLabelTests/Default.png -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklockwood/HTMLLabel/HEAD/Tests/HTMLLabelTests/HTMLLabelTests/Default@2x.png -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklockwood/HTMLLabel/HEAD/Tests/HTMLLabelTests/HTMLLabelTests/Default-568h@2x.png -------------------------------------------------------------------------------- /RELEASE NOTES.md: -------------------------------------------------------------------------------- 1 | Version 1.1 2 | 3 | - Fixed bug where text size could be set incorrectly on first render 4 | - Added workaround for iOS 8 bug where font was set incorrectly 5 | - Touch events are now forwarded to next responder 6 | - Added support for more entities 7 | 8 | Version 1.0 beta 9 | 10 | - Initial release -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/ParsingViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // FirstViewController.h 3 | // HTMLLabelTests 4 | // 5 | // Created by Nick Lockwood on 19/11/2012. 6 | // Copyright (c) 2012 Charcoal Design. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ParsingViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // HTMLLabelTests 4 | // 5 | // Created by Nick Lockwood on 19/11/2012. 6 | // Copyright (c) 2012 Charcoal Design. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/HTMLLabelTests-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'HTMLLabelTests' target in the 'HTMLLabelTests' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_4_0 8 | #warning "This project uses features only available in iOS SDK 4.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // HTMLLabelTests 4 | // 5 | // Created by Nick Lockwood on 19/11/2012. 6 | // Copyright (c) 2012 Charcoal Design. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "AppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /HTMLLabel.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTMLLabel", 3 | "version": "1.1", 4 | "license": "zlib", 5 | "summary": "HTMLabel is a simple UILabel subclass for displaying basic HTML content (e.g. bold/italic, links, bullet lists) on iOS without the overhead of using a UIWebView.", 6 | "authors": { 7 | "Nick Lockwood": "http://charcoaldesign.co.uk/" 8 | }, 9 | "source": { 10 | "git": "https://github.com/nicklockwood/HTMLLabel.git", 11 | "tag": "1.1" 12 | }, 13 | "homepage": "http://github.com/nicklockwood/HTMLLabel", 14 | "platforms": { 15 | "ios": "4.3" 16 | }, 17 | "source_files": "HTMLLabel", 18 | "requires_arc": true 19 | } 20 | -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // HTMLLabelTests 4 | // 5 | // Created by Nick Lockwood on 19/11/2012. 6 | // Copyright (c) 2012 Charcoal Design. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "ParsingViewController.h" 11 | 12 | 13 | @implementation AppDelegate 14 | 15 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 16 | { 17 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 18 | self.window.rootViewController = [[ParsingViewController alloc] init]; 19 | [self.window makeKeyAndVisible]; 20 | return YES; 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | HTMLLabel 2 | 3 | Copyright (C) 2012 Charcoal Design 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *************** 2 | WARNING: THIS PROJECT IS DEPRECATED 3 | ==================================== 4 | It will not receive any future updates or bug fixes. If you are using it, please migrate to another solution. 5 | *************** 6 | 7 | 8 | Purpose 9 | -------------- 10 | 11 | HTMLabel is a simple UILabel subclass for displaying basic HTML content (e.g. bold/italic, links, bullet lists) on iOS without the overhead of using a UIWebView. 12 | 13 | HTMLLabel is **BETA** software, and as such it should be expected to have bugs. You should also expect undocumented and/or backward-incompatible changes to the interface between now and the 1.0 release. That said, it's been used in a few shipping apps now and should be safe for production use. 14 | 15 | 16 | Installation 17 | -------------- 18 | 19 | To use HTMLLabel in an app, just drag the class files into your project. 20 | 21 | 22 | Usage 23 | --------------- 24 | 25 | Because HTMLLabel is a subclass of UILabel, you can use it in exactly the same way, either in code or Interface Builder. The only difference is that the label text will be treated as HTML. 26 | 27 | You can provide styles in the form of a dictionary of attributes keyed by tag and/or CSS-style class name. Check out the example app for details. 28 | -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/HTMLLabelTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | com.charcoaldesign.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarTintParameters 32 | 33 | UINavigationBar 34 | 35 | Style 36 | UIBarStyleDefault 37 | Translucent 38 | 39 | 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /HTMLLabel/HTMLLabel.h: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLLabel.m 3 | // 4 | // Version 1.1 5 | // 6 | // Created by Nick Lockwood on 18/11/2012. 7 | // Copyright 2012 Charcoal Design 8 | // 9 | // Distributed under the permissive zlib license 10 | // Get the latest version from here: 11 | // 12 | // https://github.com/nicklockwood/HTMLLabel 13 | // 14 | // This software is provided 'as-is', without any express or implied 15 | // warranty. In no event will the authors be held liable for any damages 16 | // arising from the use of this software. 17 | // 18 | // Permission is granted to anyone to use this software for any purpose, 19 | // including commercial applications, and to alter it and redistribute it 20 | // freely, subject to the following restrictions: 21 | // 22 | // 1. The origin of this software must not be misrepresented; you must not 23 | // claim that you wrote the original software. If you use this software 24 | // in a product, an acknowledgment in the product documentation would be 25 | // appreciated but is not required. 26 | // 27 | // 2. Altered source versions must be plainly marked as such, and must not be 28 | // misrepresented as being the original software. 29 | // 30 | // 3. This notice may not be removed or altered from any source distribution. 31 | // 32 | 33 | #import 34 | 35 | 36 | #import 37 | #undef weak_delegate 38 | #if __has_feature(objc_arc_weak) 39 | #define weak_delegate weak 40 | #else 41 | #define weak_delegate unsafe_unretained 42 | #endif 43 | 44 | 45 | UIKIT_EXTERN NSString *const HTMLBold; //bold 46 | UIKIT_EXTERN NSString *const HTMLItalic; //italic 47 | UIKIT_EXTERN NSString *const HTMLUnderline; // underline 48 | UIKIT_EXTERN NSString *const HTMLFont; // font 49 | UIKIT_EXTERN NSString *const HTMLTextSize; // textSize 50 | UIKIT_EXTERN NSString *const HTMLTextColor; // textColor 51 | UIKIT_EXTERN NSString *const HTMLTextAlignment; // textAlignment 52 | 53 | 54 | @interface UIFont (Variants) 55 | 56 | - (UIFont *)boldFontOfSize:(CGFloat)size; 57 | - (UIFont *)italicFontOfSize:(CGFloat)size; 58 | - (UIFont *)boldItalicFontOfSize:(CGFloat)size; 59 | 60 | @end 61 | 62 | 63 | @interface NSString (HTMLRendering) 64 | 65 | - (CGSize)sizeForWidth:(CGFloat)width withHTMLStyles:(NSDictionary *)stylesheet; 66 | - (void)drawInRect:(CGRect)rect withHTMLStyles:(NSDictionary *)stylesheet; 67 | 68 | @end 69 | 70 | 71 | @class HTMLLabel; 72 | 73 | 74 | @protocol HTMLLabelDelegate 75 | @optional 76 | 77 | - (void)HTMLLabel:(HTMLLabel *)label tappedLinkWithURL:(NSURL *)URL bounds:(CGRect)bounds; 78 | - (BOOL)HTMLLabel:(HTMLLabel *)label shouldOpenURL:(NSURL *)URL; 79 | 80 | @end 81 | 82 | 83 | @interface HTMLLabel : UILabel 84 | 85 | @property (nonatomic, weak_delegate) IBOutlet id delegate; 86 | @property (nonatomic, copy) NSDictionary *stylesheet; 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/ParsingViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // FirstViewController.m 3 | // HTMLLabelTests 4 | // 5 | // Created by Nick Lockwood on 19/11/2012. 6 | // Copyright (c) 2012 Charcoal Design. All rights reserved. 7 | // 8 | 9 | #import "ParsingViewController.h" 10 | #import "HTMLLabel.h" 11 | 12 | 13 | @interface ParsingViewController () 14 | 15 | @property (nonatomic, weak) IBOutlet UINavigationBar *navigationBar; 16 | @property (nonatomic, weak) IBOutlet UITextView *inputField; 17 | @property (nonatomic, weak) IBOutlet HTMLLabel *outputField; 18 | @property (nonatomic, weak) IBOutlet UIScrollView *scrollView; 19 | 20 | @end 21 | 22 | 23 | @implementation ParsingViewController 24 | 25 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 26 | { 27 | if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) 28 | { 29 | self.title = @"Parsing"; 30 | self.tabBarItem.image = [UIImage imageNamed:@"tab"]; 31 | } 32 | return self; 33 | } 34 | 35 | - (void)dismissKeyboard 36 | { 37 | [_inputField resignFirstResponder]; 38 | self.navigationBar.topItem.leftBarButtonItem = nil; 39 | } 40 | 41 | - (IBAction)selectInput 42 | { 43 | [[[UIActionSheet alloc] initWithTitle:@"Select input" delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete all input" otherButtonTitles:@"Lorem ipsum", @"Ordered list", @"Unordered list", @"Naked list", @"Links", @"Broken tags", @"Styles", nil] showInView:self.view]; 44 | } 45 | 46 | - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex 47 | { 48 | if (buttonIndex != actionSheet.cancelButtonIndex) 49 | { 50 | _outputField.stylesheet = nil; 51 | if (buttonIndex == actionSheet.destructiveButtonIndex) 52 | { 53 | _inputField.text = nil; 54 | } 55 | else 56 | { 57 | // Reset styles 58 | _outputField.stylesheet = nil; 59 | _outputField.font = [UIFont systemFontOfSize:14.0f]; 60 | 61 | switch (buttonIndex) 62 | { 63 | case 1: 64 | { 65 | _inputField.text = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda."; 66 | break; 67 | } 68 | case 2: 69 | { 70 | _inputField.text = @"Lorem ipsum
  1. dolor sit er elit lamet,
  2. consectetaur cillium adipisicing pecu,
    1. sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua."; 71 | break; 72 | } 73 | case 3: 74 | { 75 | _inputField.text = @"Lorem ipsum
  • dolor sit er elit lamet,
  • consectetaur cillium adipisicing pecu,
    • sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua."; 76 | break; 77 | } 78 | case 4: 79 | { 80 | _inputField.text = @"Lorem ipsum
  • dolor sit er elit lamet,
  • consectetaur cillium adipisicing pecu, sed
  • do eiusmod tempor incididunt
  • ut labore et dolore magna aliqua."; 81 | break; 82 | } 83 | case 5: 84 | { 85 | _inputField.text = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."; 86 | break; 87 | } 88 | case 6: 89 | { 90 | _inputField.text = @"Lorem >ipsum dolor sit < er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."; 91 | break; 92 | } 93 | case 7: 94 | { 95 | _outputField.stylesheet = @{ 96 | @"html": @{HTMLTextSize: @20}, 97 | @"h2": @{HTMLTextAlignment: @(NSTextAlignmentCenter)}, 98 | @"a": @{HTMLFont: @"Georgia", HTMLTextColor: [UIColor redColor]}, 99 | @"a:active": @{HTMLTextColor: [UIColor purpleColor]}, 100 | @".green": @{HTMLTextColor: [UIColor greenColor], HTMLBold: @YES} 101 | }; 102 | 103 | _inputField.text = @"

    Lorem Ipsum

    Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda."; 104 | break; 105 | } 106 | default: 107 | { 108 | break; 109 | } 110 | } 111 | 112 | } 113 | [self update]; 114 | } 115 | } 116 | 117 | - (void)textViewDidBeginEditing:(UITextView *)textView 118 | { 119 | self.navigationBar.topItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dismissKeyboard)]; 120 | } 121 | 122 | - (void)update 123 | { 124 | _outputField.text = _inputField.text; 125 | CGRect frame = _outputField.frame; 126 | frame.size.height = [_outputField sizeThatFits:CGSizeMake(frame.size.width, INFINITY)].height; 127 | _outputField.frame = frame; 128 | _scrollView.contentSize = CGSizeMake(_scrollView.contentSize.width, frame.size.height + 20); 129 | } 130 | 131 | - (void)viewDidLoad 132 | { 133 | [super viewDidLoad]; 134 | 135 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(update) name:UITextViewTextDidChangeNotification object:_inputField]; 136 | 137 | [self update]; 138 | } 139 | 140 | - (void)viewDidUnload 141 | { 142 | [super viewDidUnload]; 143 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:_inputField]; 144 | } 145 | 146 | - (void)dealloc 147 | { 148 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 149 | } 150 | 151 | @end 152 | -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 01B88967165A9D6B0048C30E /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01B88966165A9D6B0048C30E /* UIKit.framework */; }; 11 | 01B88969165A9D6B0048C30E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01B88968165A9D6B0048C30E /* Foundation.framework */; }; 12 | 01B8896B165A9D6B0048C30E /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01B8896A165A9D6B0048C30E /* CoreGraphics.framework */; }; 13 | 01B88971165A9D6B0048C30E /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 01B8896F165A9D6B0048C30E /* InfoPlist.strings */; }; 14 | 01B88973165A9D6B0048C30E /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 01B88972165A9D6B0048C30E /* main.m */; }; 15 | 01B88977165A9D6B0048C30E /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 01B88976165A9D6B0048C30E /* AppDelegate.m */; }; 16 | 01B88979165A9D6B0048C30E /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = 01B88978165A9D6B0048C30E /* Default.png */; }; 17 | 01B8897B165A9D6B0048C30E /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 01B8897A165A9D6B0048C30E /* Default@2x.png */; }; 18 | 01B8897D165A9D6B0048C30E /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 01B8897C165A9D6B0048C30E /* Default-568h@2x.png */; }; 19 | 01B88980165A9D6B0048C30E /* ParsingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 01B8897F165A9D6B0048C30E /* ParsingViewController.m */; }; 20 | 01B8898E165A9D6B0048C30E /* ParsingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 01B8898C165A9D6B0048C30E /* ParsingViewController.xib */; }; 21 | 01DCA0B416CC31E400675C86 /* HTMLLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 01DCA0B316CC31E400675C86 /* HTMLLabel.m */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 01B88962165A9D6B0048C30E /* HTMLLabelTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HTMLLabelTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 01B88966165A9D6B0048C30E /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 27 | 01B88968165A9D6B0048C30E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 28 | 01B8896A165A9D6B0048C30E /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 29 | 01B8896E165A9D6B0048C30E /* HTMLLabelTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "HTMLLabelTests-Info.plist"; sourceTree = ""; }; 30 | 01B88970165A9D6B0048C30E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 31 | 01B88972165A9D6B0048C30E /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 32 | 01B88974165A9D6B0048C30E /* HTMLLabelTests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "HTMLLabelTests-Prefix.pch"; sourceTree = ""; }; 33 | 01B88975165A9D6B0048C30E /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 34 | 01B88976165A9D6B0048C30E /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 35 | 01B88978165A9D6B0048C30E /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; 36 | 01B8897A165A9D6B0048C30E /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; 37 | 01B8897C165A9D6B0048C30E /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 38 | 01B8897E165A9D6B0048C30E /* ParsingViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ParsingViewController.h; sourceTree = ""; }; 39 | 01B8897F165A9D6B0048C30E /* ParsingViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ParsingViewController.m; sourceTree = ""; }; 40 | 01B8898D165A9D6B0048C30E /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/ParsingViewController.xib; sourceTree = ""; }; 41 | 01DCA0B216CC31E400675C86 /* HTMLLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLLabel.h; sourceTree = ""; }; 42 | 01DCA0B316CC31E400675C86 /* HTMLLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLLabel.m; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | 01B8895F165A9D6B0048C30E /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | 01B88967165A9D6B0048C30E /* UIKit.framework in Frameworks */, 51 | 01B88969165A9D6B0048C30E /* Foundation.framework in Frameworks */, 52 | 01B8896B165A9D6B0048C30E /* CoreGraphics.framework in Frameworks */, 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | /* End PBXFrameworksBuildPhase section */ 57 | 58 | /* Begin PBXGroup section */ 59 | 01B88957165A9D6B0048C30E = { 60 | isa = PBXGroup; 61 | children = ( 62 | 01DCA0B116CC31E400675C86 /* HTMLLabel */, 63 | 01B8896C165A9D6B0048C30E /* HTMLLabelTests */, 64 | 01B88965165A9D6B0048C30E /* Frameworks */, 65 | 01B88963165A9D6B0048C30E /* Products */, 66 | ); 67 | sourceTree = ""; 68 | }; 69 | 01B88963165A9D6B0048C30E /* Products */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 01B88962165A9D6B0048C30E /* HTMLLabelTests.app */, 73 | ); 74 | name = Products; 75 | sourceTree = ""; 76 | }; 77 | 01B88965165A9D6B0048C30E /* Frameworks */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 01B88966165A9D6B0048C30E /* UIKit.framework */, 81 | 01B88968165A9D6B0048C30E /* Foundation.framework */, 82 | 01B8896A165A9D6B0048C30E /* CoreGraphics.framework */, 83 | ); 84 | name = Frameworks; 85 | sourceTree = ""; 86 | }; 87 | 01B8896C165A9D6B0048C30E /* HTMLLabelTests */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 01B88998165AA2BB0048C30E /* Nibs */, 91 | 01B88997165AA2AA0048C30E /* Controllers */, 92 | 01B8896D165A9D6B0048C30E /* Supporting Files */, 93 | ); 94 | path = HTMLLabelTests; 95 | sourceTree = ""; 96 | }; 97 | 01B8896D165A9D6B0048C30E /* Supporting Files */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 01B8896E165A9D6B0048C30E /* HTMLLabelTests-Info.plist */, 101 | 01B8896F165A9D6B0048C30E /* InfoPlist.strings */, 102 | 01B88972165A9D6B0048C30E /* main.m */, 103 | 01B88974165A9D6B0048C30E /* HTMLLabelTests-Prefix.pch */, 104 | 01B88978165A9D6B0048C30E /* Default.png */, 105 | 01B8897A165A9D6B0048C30E /* Default@2x.png */, 106 | 01B8897C165A9D6B0048C30E /* Default-568h@2x.png */, 107 | ); 108 | name = "Supporting Files"; 109 | sourceTree = ""; 110 | }; 111 | 01B88997165AA2AA0048C30E /* Controllers */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 01B88975165A9D6B0048C30E /* AppDelegate.h */, 115 | 01B88976165A9D6B0048C30E /* AppDelegate.m */, 116 | 01B8897E165A9D6B0048C30E /* ParsingViewController.h */, 117 | 01B8897F165A9D6B0048C30E /* ParsingViewController.m */, 118 | ); 119 | name = Controllers; 120 | sourceTree = ""; 121 | }; 122 | 01B88998165AA2BB0048C30E /* Nibs */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 01B8898C165A9D6B0048C30E /* ParsingViewController.xib */, 126 | ); 127 | name = Nibs; 128 | sourceTree = ""; 129 | }; 130 | 01DCA0B116CC31E400675C86 /* HTMLLabel */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 01DCA0B216CC31E400675C86 /* HTMLLabel.h */, 134 | 01DCA0B316CC31E400675C86 /* HTMLLabel.m */, 135 | ); 136 | name = HTMLLabel; 137 | path = ../../HTMLLabel; 138 | sourceTree = ""; 139 | }; 140 | /* End PBXGroup section */ 141 | 142 | /* Begin PBXNativeTarget section */ 143 | 01B88961165A9D6B0048C30E /* HTMLLabelTests */ = { 144 | isa = PBXNativeTarget; 145 | buildConfigurationList = 01B88994165A9D6B0048C30E /* Build configuration list for PBXNativeTarget "HTMLLabelTests" */; 146 | buildPhases = ( 147 | 01B8895E165A9D6B0048C30E /* Sources */, 148 | 01B8895F165A9D6B0048C30E /* Frameworks */, 149 | 01B88960165A9D6B0048C30E /* Resources */, 150 | ); 151 | buildRules = ( 152 | ); 153 | dependencies = ( 154 | ); 155 | name = HTMLLabelTests; 156 | productName = HTMLLabelTests; 157 | productReference = 01B88962165A9D6B0048C30E /* HTMLLabelTests.app */; 158 | productType = "com.apple.product-type.application"; 159 | }; 160 | /* End PBXNativeTarget section */ 161 | 162 | /* Begin PBXProject section */ 163 | 01B88959165A9D6B0048C30E /* Project object */ = { 164 | isa = PBXProject; 165 | attributes = { 166 | LastUpgradeCheck = 0460; 167 | ORGANIZATIONNAME = "Charcoal Design"; 168 | }; 169 | buildConfigurationList = 01B8895C165A9D6B0048C30E /* Build configuration list for PBXProject "HTMLLabelTests" */; 170 | compatibilityVersion = "Xcode 3.2"; 171 | developmentRegion = English; 172 | hasScannedForEncodings = 0; 173 | knownRegions = ( 174 | en, 175 | ); 176 | mainGroup = 01B88957165A9D6B0048C30E; 177 | productRefGroup = 01B88963165A9D6B0048C30E /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 01B88961165A9D6B0048C30E /* HTMLLabelTests */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 01B88960165A9D6B0048C30E /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 01B88971165A9D6B0048C30E /* InfoPlist.strings in Resources */, 192 | 01B88979165A9D6B0048C30E /* Default.png in Resources */, 193 | 01B8897B165A9D6B0048C30E /* Default@2x.png in Resources */, 194 | 01B8897D165A9D6B0048C30E /* Default-568h@2x.png in Resources */, 195 | 01B8898E165A9D6B0048C30E /* ParsingViewController.xib in Resources */, 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | /* End PBXResourcesBuildPhase section */ 200 | 201 | /* Begin PBXSourcesBuildPhase section */ 202 | 01B8895E165A9D6B0048C30E /* Sources */ = { 203 | isa = PBXSourcesBuildPhase; 204 | buildActionMask = 2147483647; 205 | files = ( 206 | 01B88973165A9D6B0048C30E /* main.m in Sources */, 207 | 01B88977165A9D6B0048C30E /* AppDelegate.m in Sources */, 208 | 01B88980165A9D6B0048C30E /* ParsingViewController.m in Sources */, 209 | 01DCA0B416CC31E400675C86 /* HTMLLabel.m in Sources */, 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | }; 213 | /* End PBXSourcesBuildPhase section */ 214 | 215 | /* Begin PBXVariantGroup section */ 216 | 01B8896F165A9D6B0048C30E /* InfoPlist.strings */ = { 217 | isa = PBXVariantGroup; 218 | children = ( 219 | 01B88970165A9D6B0048C30E /* en */, 220 | ); 221 | name = InfoPlist.strings; 222 | sourceTree = ""; 223 | }; 224 | 01B8898C165A9D6B0048C30E /* ParsingViewController.xib */ = { 225 | isa = PBXVariantGroup; 226 | children = ( 227 | 01B8898D165A9D6B0048C30E /* en */, 228 | ); 229 | name = ParsingViewController.xib; 230 | sourceTree = ""; 231 | }; 232 | /* End PBXVariantGroup section */ 233 | 234 | /* Begin XCBuildConfiguration section */ 235 | 01B88992165A9D6B0048C30E /* Debug */ = { 236 | isa = XCBuildConfiguration; 237 | buildSettings = { 238 | ALWAYS_SEARCH_USER_PATHS = NO; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_OBJC_ARC = YES; 242 | CLANG_WARN_EMPTY_BODY = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 245 | COPY_PHASE_STRIP = NO; 246 | GCC_C_LANGUAGE_STANDARD = gnu99; 247 | GCC_DYNAMIC_NO_PIC = NO; 248 | GCC_OPTIMIZATION_LEVEL = 0; 249 | GCC_PREPROCESSOR_DEFINITIONS = ( 250 | "DEBUG=1", 251 | "$(inherited)", 252 | ); 253 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 254 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 255 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 256 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 257 | GCC_WARN_UNUSED_VARIABLE = YES; 258 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 259 | ONLY_ACTIVE_ARCH = YES; 260 | RUN_CLANG_STATIC_ANALYZER = YES; 261 | SDKROOT = iphoneos; 262 | WARNING_CFLAGS = "-Wall"; 263 | }; 264 | name = Debug; 265 | }; 266 | 01B88993165A9D6B0048C30E /* Release */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ALWAYS_SEARCH_USER_PATHS = NO; 270 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 271 | CLANG_CXX_LIBRARY = "libc++"; 272 | CLANG_ENABLE_OBJC_ARC = YES; 273 | CLANG_WARN_EMPTY_BODY = YES; 274 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 275 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 276 | COPY_PHASE_STRIP = YES; 277 | GCC_C_LANGUAGE_STANDARD = gnu99; 278 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 279 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 280 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 281 | GCC_WARN_UNUSED_VARIABLE = YES; 282 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 283 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 284 | RUN_CLANG_STATIC_ANALYZER = YES; 285 | SDKROOT = iphoneos; 286 | VALIDATE_PRODUCT = YES; 287 | WARNING_CFLAGS = "-Wall"; 288 | }; 289 | name = Release; 290 | }; 291 | 01B88995165A9D6B0048C30E /* Debug */ = { 292 | isa = XCBuildConfiguration; 293 | buildSettings = { 294 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 295 | GCC_PREFIX_HEADER = "HTMLLabelTests/HTMLLabelTests-Prefix.pch"; 296 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 297 | INFOPLIST_FILE = "HTMLLabelTests/HTMLLabelTests-Info.plist"; 298 | PRODUCT_NAME = "$(TARGET_NAME)"; 299 | RUN_CLANG_STATIC_ANALYZER = YES; 300 | WRAPPER_EXTENSION = app; 301 | }; 302 | name = Debug; 303 | }; 304 | 01B88996165A9D6B0048C30E /* Release */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 308 | GCC_PREFIX_HEADER = "HTMLLabelTests/HTMLLabelTests-Prefix.pch"; 309 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 310 | INFOPLIST_FILE = "HTMLLabelTests/HTMLLabelTests-Info.plist"; 311 | PRODUCT_NAME = "$(TARGET_NAME)"; 312 | RUN_CLANG_STATIC_ANALYZER = YES; 313 | WRAPPER_EXTENSION = app; 314 | }; 315 | name = Release; 316 | }; 317 | /* End XCBuildConfiguration section */ 318 | 319 | /* Begin XCConfigurationList section */ 320 | 01B8895C165A9D6B0048C30E /* Build configuration list for PBXProject "HTMLLabelTests" */ = { 321 | isa = XCConfigurationList; 322 | buildConfigurations = ( 323 | 01B88992165A9D6B0048C30E /* Debug */, 324 | 01B88993165A9D6B0048C30E /* Release */, 325 | ); 326 | defaultConfigurationIsVisible = 0; 327 | defaultConfigurationName = Release; 328 | }; 329 | 01B88994165A9D6B0048C30E /* Build configuration list for PBXNativeTarget "HTMLLabelTests" */ = { 330 | isa = XCConfigurationList; 331 | buildConfigurations = ( 332 | 01B88995165A9D6B0048C30E /* Debug */, 333 | 01B88996165A9D6B0048C30E /* Release */, 334 | ); 335 | defaultConfigurationIsVisible = 0; 336 | defaultConfigurationName = Release; 337 | }; 338 | /* End XCConfigurationList section */ 339 | }; 340 | rootObject = 01B88959165A9D6B0048C30E /* Project object */; 341 | } 342 | -------------------------------------------------------------------------------- /Tests/HTMLLabelTests/HTMLLabelTests/en.lproj/ParsingViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1536 5 | 12C60 6 | 2844 7 | 1187.34 8 | 625.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 1930 12 | 13 | 14 | IBProxyObject 15 | IBUIBarButtonItem 16 | IBUILabel 17 | IBUINavigationBar 18 | IBUINavigationItem 19 | IBUIScrollView 20 | IBUITextView 21 | IBUIView 22 | 23 | 24 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 25 | 26 | 27 | PluginDependencyRecalculationVersion 28 | 29 | 30 | 31 | 32 | IBFilesOwner 33 | IBCocoaTouchFramework 34 | 35 | 36 | IBFirstResponder 37 | IBCocoaTouchFramework 38 | 39 | 40 | 41 | 274 42 | 43 | 44 | 45 | 290 46 | {{0, 44}, {320, 197}} 47 | 48 | 49 | 50 | _NS:9 51 | 52 | 1 53 | MSAxIDEAA 54 | 55 | YES 56 | YES 57 | IBCocoaTouchFramework 58 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. 59 | 60 | 1 61 | IBCocoaTouchFramework 62 | 63 | 64 | 1 65 | 14 66 | 67 | 68 | Helvetica 69 | 14 70 | 16 71 | 72 | 73 | 74 | 75 | 274 76 | 77 | 78 | 79 | 292 80 | {{8, 9.5}, {304, 100}} 81 | 82 | 83 | 84 | _NS:9 85 | NO 86 | YES 87 | 7 88 | IBCocoaTouchFramework 89 | Label 90 | 91 | 0 92 | 93 | 94 | NO 95 | 96 | 97 | {{0, 249}, {320, 299}} 98 | 99 | 100 | 101 | _NS:9 102 | YES 103 | YES 104 | IBCocoaTouchFramework 105 | 106 | 107 | 108 | 290 109 | {320, 44} 110 | 111 | 112 | 113 | _NS:9 114 | IBCocoaTouchFramework 115 | 1 116 | 117 | 118 | 119 | Parsing 120 | 121 | IBCocoaTouchFramework 122 | 1 123 | 124 | 9 125 | 126 | IBCocoaTouchFramework 127 | 128 | 129 | 130 | 131 | {{0, 20}, {320, 548}} 132 | 133 | 134 | 135 | 136 | 3 137 | MQA 138 | 139 | 140 | 141 | IBUIScreenMetrics 142 | 143 | YES 144 | 145 | 146 | 147 | 148 | 149 | {320, 568} 150 | {568, 320} 151 | 152 | 153 | IBCocoaTouchFramework 154 | Retina 4 Full Screen 155 | 2 156 | 157 | IBCocoaTouchFramework 158 | 159 | 160 | 161 | 162 | 163 | 164 | view 165 | 166 | 167 | 168 | 3 169 | 170 | 171 | 172 | inputField 173 | 174 | 175 | 176 | 23 177 | 178 | 179 | 180 | scrollView 181 | 182 | 183 | 184 | 24 185 | 186 | 187 | 188 | outputField 189 | 190 | 191 | 192 | 25 193 | 194 | 195 | 196 | navigationBar 197 | 198 | 199 | 200 | 29 201 | 202 | 203 | 204 | delegate 205 | 206 | 207 | 208 | 26 209 | 210 | 211 | 212 | selectInput 213 | 214 | 215 | 216 | 30 217 | 218 | 219 | 220 | 221 | 222 | 0 223 | 224 | 225 | 226 | 227 | 228 | 1 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | -1 239 | 240 | 241 | File's Owner 242 | 243 | 244 | -2 245 | 246 | 247 | 248 | 249 | 18 250 | 251 | 252 | 253 | 254 | 19 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 20 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 21 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 22 279 | 280 | 281 | 282 | 283 | 27 284 | 285 | 286 | 287 | 288 | 289 | 290 | ParsingViewController 291 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 292 | UIResponder 293 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 294 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 295 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 296 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 297 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 298 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 299 | HTMLLabel 300 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 301 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 302 | 303 | 304 | 305 | 306 | 307 | 30 308 | 309 | 310 | 311 | 312 | HTMLLabel 313 | UILabel 314 | 315 | delegate 316 | id 317 | 318 | 319 | delegate 320 | 321 | delegate 322 | id 323 | 324 | 325 | 326 | IBProjectSource 327 | ./Classes/HTMLLabel.h 328 | 329 | 330 | 331 | ParsingViewController 332 | UIViewController 333 | 334 | UITextView 335 | UINavigationBar 336 | HTMLLabel 337 | UIScrollView 338 | 339 | 340 | 341 | inputField 342 | UITextView 343 | 344 | 345 | navigationBar 346 | UINavigationBar 347 | 348 | 349 | outputField 350 | HTMLLabel 351 | 352 | 353 | scrollView 354 | UIScrollView 355 | 356 | 357 | 358 | IBProjectSource 359 | ./Classes/ParsingViewController.h 360 | 361 | 362 | 363 | 364 | 0 365 | IBCocoaTouchFramework 366 | YES 367 | 3 368 | 1930 369 | 370 | 371 | -------------------------------------------------------------------------------- /HTMLLabel/HTMLLabel.m: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLLabel.m 3 | // 4 | // Version 1.1 5 | // 6 | // Created by Nick Lockwood on 18/11/2012. 7 | // Copyright 2012 Charcoal Design 8 | // 9 | // Distributed under the permissive zlib license 10 | // Get the latest version from here: 11 | // 12 | // https://github.com/nicklockwood/HTMLLabel 13 | // 14 | // This software is provided 'as-is', without any express or implied 15 | // warranty. In no event will the authors be held liable for any damages 16 | // arising from the use of this software. 17 | // 18 | // Permission is granted to anyone to use this software for any purpose, 19 | // including commercial applications, and to alter it and redistribute it 20 | // freely, subject to the following restrictions: 21 | // 22 | // 1. The origin of this software must not be misrepresented; you must not 23 | // claim that you wrote the original software. If you use this software 24 | // in a product, an acknowledgment in the product documentation would be 25 | // appreciated but is not required. 26 | // 27 | // 2. Altered source versions must be plainly marked as such, and must not be 28 | // misrepresented as being the original software. 29 | // 30 | // 3. This notice may not be removed or altered from any source distribution. 31 | // 32 | 33 | #import "HTMLLabel.h" 34 | 35 | 36 | //temporary fix 37 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" 38 | 39 | 40 | NSString *const HTMLBold = @"bold"; 41 | NSString *const HTMLItalic = @"italic"; 42 | NSString *const HTMLUnderline = @"underline"; 43 | NSString *const HTMLFont = @"font"; 44 | NSString *const HTMLTextSize = @"textSize"; 45 | NSString *const HTMLTextColor = @"textColor"; 46 | NSString *const HTMLTextAlignment = @"textAlignment"; 47 | 48 | 49 | #pragma mark - 50 | #pragma mark Fonts 51 | 52 | 53 | @implementation UIFont (Variants) 54 | 55 | - (BOOL)fontWithName:(NSString *)name hasTrait:(NSString *)trait 56 | { 57 | name = [name lowercaseString]; 58 | if ([name rangeOfString:trait].location != NSNotFound) 59 | { 60 | return YES; 61 | } 62 | else if ([trait isEqualToString:@"oblique"]) 63 | { 64 | return [name rangeOfString:@"italic"].location != NSNotFound; 65 | } 66 | return NO; 67 | } 68 | 69 | - (UIFont *)fontWithSize:(CGFloat)fontSize traits:(NSArray *)traits 70 | { 71 | NSMutableArray *blacklist = [@[@"bold", @"oblique", @"light", @"condensed"] mutableCopy]; 72 | for (NSString *trait in traits) 73 | { 74 | //is desired trait 75 | [blacklist removeObject:trait]; 76 | } 77 | for (NSString *trait in [blacklist reverseObjectEnumerator]) 78 | { 79 | //is property of base font 80 | if ([[self.fontName lowercaseString] rangeOfString:trait].location != NSNotFound) 81 | { 82 | [blacklist removeObject:trait]; 83 | } 84 | } 85 | NSString *familyName = [self familyName]; 86 | 87 | //special case due to weirdness with iPhone system font 88 | if ([familyName hasPrefix:@".Helvetica Neue"]) 89 | { 90 | familyName = @"Helvetica Neue"; 91 | } 92 | 93 | for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) 94 | { 95 | BOOL match = YES; 96 | for (NSString *trait in blacklist) 97 | { 98 | if ([self fontWithName:name hasTrait:trait]) 99 | { 100 | match = NO; 101 | break; 102 | } 103 | } 104 | for (NSString *trait in traits) 105 | { 106 | if (![self fontWithName:name hasTrait:trait]) 107 | { 108 | match = NO; 109 | break; 110 | } 111 | } 112 | if (match) 113 | { 114 | return [UIFont fontWithName:name size:fontSize]; 115 | } 116 | } 117 | return [UIFont fontWithName:self.fontName size:fontSize]; 118 | } 119 | 120 | - (UIFont *)boldFontOfSize:(CGFloat)fontSize 121 | { 122 | return [self fontWithSize:fontSize traits:@[@"bold"]]; 123 | } 124 | 125 | - (UIFont *)italicFontOfSize:(CGFloat)fontSize 126 | { 127 | return [self fontWithSize:fontSize traits:@[@"oblique"]]; 128 | } 129 | 130 | - (UIFont *)boldItalicFontOfSize:(CGFloat)fontSize 131 | { 132 | return [self fontWithSize:fontSize traits:@[@"bold", @"oblique"]]; 133 | } 134 | 135 | @end 136 | 137 | 138 | #pragma mark - 139 | #pragma mark HTML token 140 | 141 | 142 | @interface HTMLTokenAttributes : NSObject 143 | 144 | @property (nonatomic, copy) NSString *href; 145 | @property (nonatomic, copy) NSString *tag; 146 | @property (nonatomic, copy) NSArray *classNames; 147 | @property (nonatomic, strong) HTMLTokenAttributes *parent; 148 | @property (nonatomic, assign) BOOL active; 149 | @property (nonatomic, assign) BOOL list; 150 | @property (nonatomic, assign) BOOL bullet; 151 | @property (nonatomic, assign) NSInteger listLevel; 152 | @property (nonatomic, assign) NSInteger nextListIndex; 153 | 154 | @end 155 | 156 | 157 | @class HTMLStyles; 158 | 159 | 160 | @interface HTMLToken : NSObject 161 | 162 | @property (nonatomic, strong) HTMLTokenAttributes *attributes; 163 | @property (nonatomic, copy) NSString *text; 164 | 165 | - (BOOL)isSpace; 166 | - (BOOL)isLinebreak; 167 | - (CGSize)sizeWithStyles:(HTMLStyles *)styles; 168 | - (void)drawInRect:(CGRect)rect withStyles:(HTMLStyles *)styles; 169 | 170 | @end 171 | 172 | 173 | #pragma mark - 174 | #pragma mark HTML styles 175 | 176 | 177 | @interface HTMLStyles : NSObject 178 | 179 | @property (nonatomic, strong, readonly) UIFont *font; 180 | @property (nonatomic, strong, readonly) UIColor *textColor; 181 | @property (nonatomic, assign, readonly) CGFloat textSize; 182 | @property (nonatomic, assign, readonly) NSTextAlignment textAlignment; 183 | @property (nonatomic, assign, readonly, getter = isBold) BOOL bold; 184 | @property (nonatomic, assign, readonly, getter = isItalic) BOOL italic; 185 | @property (nonatomic, assign, readonly, getter = isUnderlined) BOOL underline; 186 | 187 | - (id)initWithDictionary:(NSDictionary *)dict; 188 | - (NSDictionary *)dictionaryRepresentation; 189 | - (HTMLStyles *)stylesByAddingStylesFromDictionary:(NSDictionary *)dict; 190 | - (HTMLStyles *)stylesByAddingStyles:(HTMLStyles *)styles; 191 | 192 | @end 193 | 194 | 195 | @interface HTMLStyles () 196 | 197 | @property (nonatomic, copy) NSDictionary *styles; 198 | 199 | @end 200 | 201 | 202 | @implementation HTMLStyles 203 | 204 | - (id)initWithDictionary:(NSDictionary *)dict 205 | { 206 | if ((self = [super init])) 207 | { 208 | _styles = [dict copy]; 209 | } 210 | return self; 211 | } 212 | 213 | - (UIFont *)font 214 | { 215 | UIFont *font = _styles[HTMLFont]; 216 | if ([font isKindOfClass:[NSString class]]) 217 | { 218 | font = [UIFont fontWithName:(NSString *)font size:17.0f]; 219 | } 220 | NSInteger pointSize = [_styles[HTMLTextSize] floatValue] ?: font.pointSize; 221 | if (self.bold) 222 | { 223 | if (self.italic) 224 | { 225 | font = [font boldItalicFontOfSize:pointSize]; 226 | } 227 | else 228 | { 229 | font = [font boldFontOfSize:pointSize]; 230 | } 231 | } 232 | else if (self.italic) 233 | { 234 | font = [font italicFontOfSize:pointSize]; 235 | } 236 | else 237 | { 238 | font = [UIFont fontWithName:font.fontName size:pointSize]; 239 | } 240 | return font; 241 | } 242 | 243 | - (UIColor *)textColor 244 | { 245 | UIColor *textColor = _styles[HTMLTextColor]; 246 | if ([textColor isKindOfClass:[NSString class]]) 247 | { 248 | SEL selector = NSSelectorFromString(@"colorWithString:"); 249 | if ([UIColor respondsToSelector:selector]) 250 | { 251 | 252 | #pragma GCC diagnostic push 253 | #pragma GCC diagnostic ignored "-Warc-performSelector-leaks" 254 | 255 | textColor = [[UIColor class] performSelector:selector withObject:textColor]; 256 | 257 | #pragma GCC diagnostic pop 258 | 259 | } 260 | else 261 | { 262 | [NSException raise:@"HTMLLabelError" format:@"Setting a color by string requires the ColorUtils library to be included in your project. Get it from here: https://github.com/nicklockwood/ColorUtils"]; 263 | } 264 | } 265 | return textColor; 266 | } 267 | 268 | - (CGFloat)textSize 269 | { 270 | return [_styles[HTMLTextSize] floatValue] ?: [_styles[HTMLFont] pointSize]; 271 | } 272 | 273 | - (NSTextAlignment)textAlignment 274 | { 275 | return [_styles[HTMLTextAlignment] integerValue]; 276 | } 277 | 278 | - (BOOL)isBold 279 | { 280 | return [_styles[HTMLBold] boolValue]; 281 | } 282 | 283 | - (BOOL)isItalic 284 | { 285 | return [_styles[HTMLItalic] boolValue]; 286 | } 287 | 288 | - (BOOL)isUnderlined 289 | { 290 | return [_styles[HTMLUnderline] boolValue]; 291 | } 292 | 293 | - (id)copyWithZone:(NSZone *)zone 294 | { 295 | return self; 296 | } 297 | 298 | - (NSDictionary *)dictionaryRepresentation 299 | { 300 | return [_styles copy]; 301 | } 302 | 303 | - (HTMLStyles *)stylesByAddingStylesFromDictionary:(NSDictionary *)dict 304 | { 305 | NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:_styles]; 306 | [result addEntriesFromDictionary:dict]; 307 | return [[HTMLStyles alloc] initWithDictionary:result]; 308 | } 309 | 310 | - (HTMLStyles *)stylesByAddingStyles:(HTMLStyles *)styles 311 | { 312 | return [self stylesByAddingStylesFromDictionary:[styles dictionaryRepresentation]]; 313 | } 314 | 315 | - (NSString *)description 316 | { 317 | return [[self dictionaryRepresentation] description]; 318 | } 319 | 320 | @end 321 | 322 | 323 | @interface HTMLStyleSelector : NSObject 324 | 325 | @property (nonatomic, copy, readonly) NSString *tag; 326 | @property (nonatomic, copy, readonly) NSArray *classNames; 327 | @property (nonatomic, copy, readonly) NSArray *pseudoSelectors; 328 | 329 | - (id)initWithString:(NSString *)selectorString; 330 | - (NSString *)stringRepresentation; 331 | - (BOOL)matchesTokenAttributes:(HTMLTokenAttributes *)attributes; 332 | 333 | @end 334 | 335 | 336 | @interface HTMLStyleSelector () 337 | 338 | @property (nonatomic, copy) NSString *selectorString; 339 | 340 | @end 341 | 342 | 343 | @implementation HTMLStyleSelector 344 | 345 | - (id)initWithString:(NSString *)selectorString 346 | { 347 | if ((self = [super init])) 348 | { 349 | _selectorString = selectorString; 350 | 351 | //parse pseudo selectors 352 | NSArray *parts = [selectorString componentsSeparatedByString:@":"]; 353 | NSInteger count = [parts count]; 354 | if (count > 0) 355 | { 356 | selectorString = parts[0]; 357 | if (count > 1) 358 | { 359 | _pseudoSelectors = [parts subarrayWithRange:NSMakeRange(1, count - 1)]; 360 | } 361 | } 362 | 363 | //parse class names 364 | parts = [selectorString componentsSeparatedByString:@"."]; 365 | count = [parts count]; 366 | if (count > 0) 367 | { 368 | _tag = parts[0]; 369 | if (count > 1) 370 | { 371 | _classNames = [parts subarrayWithRange:NSMakeRange(1, count - 1)]; 372 | } 373 | } 374 | 375 | //TODO: more 376 | } 377 | return self; 378 | } 379 | 380 | - (id)copyWithZone:(NSZone *)zone 381 | { 382 | return self; 383 | } 384 | 385 | - (NSString *)stringRepresentation 386 | { 387 | return _selectorString; 388 | } 389 | 390 | - (NSString *)description 391 | { 392 | return [self stringRepresentation]; 393 | } 394 | 395 | - (NSUInteger)hash 396 | { 397 | return [[self stringRepresentation] hash]; 398 | } 399 | 400 | - (BOOL)isEqual:(id)object 401 | { 402 | if ([object isKindOfClass:[HTMLStyleSelector class]]) 403 | { 404 | return [[self stringRepresentation] isEqualToString:[object stringRepresentation]]; 405 | } 406 | else if ([object isKindOfClass:[NSString class]]) 407 | { 408 | return [[self stringRepresentation] isEqualToString:object]; 409 | } 410 | return NO; 411 | } 412 | 413 | - (BOOL)matchesTokenAttributes:(HTMLTokenAttributes *)attributes 414 | { 415 | //check tag 416 | if (![_tag length] || [_tag isEqualToString:@"*"] || [attributes.tag isEqualToString:_tag]) 417 | { 418 | //check classes 419 | for (NSString *className in _classNames) 420 | { 421 | if (![attributes.classNames containsObject:className]) 422 | { 423 | return NO; 424 | } 425 | } 426 | for (NSString *pseudo in _pseudoSelectors) 427 | { 428 | if ([pseudo isEqualToString:@"active"] && !attributes.active) 429 | { 430 | return NO; 431 | } 432 | } 433 | return YES; 434 | } 435 | return NO; 436 | } 437 | 438 | @end 439 | 440 | 441 | @interface HTMLStylesheet : NSObject 442 | 443 | @property (nonatomic, copy) NSArray *selectors; 444 | @property (nonatomic, copy) NSDictionary *stylesBySelector; 445 | 446 | @end 447 | 448 | 449 | @implementation HTMLStylesheet 450 | 451 | + (HTMLStylesheet *)defaultStylesheet 452 | { 453 | static HTMLStylesheet *defaultStylesheet = nil; 454 | if (defaultStylesheet == nil) 455 | { 456 | NSDictionary *styles = @{ 457 | @"html": @{HTMLTextColor: [UIColor blackColor], HTMLFont:[UIFont systemFontOfSize:17.0f]}, 458 | @"a": @{HTMLTextColor: [UIColor blueColor], HTMLUnderline: @YES}, 459 | @"a:active": @{HTMLTextColor: [UIColor redColor]}, 460 | @"b,strong,h1,h2,h3,h4,h5,h6": @{HTMLBold: @YES}, 461 | @"i,em": @{HTMLItalic: @YES}, 462 | @"u": @{HTMLUnderline: @YES} 463 | }; 464 | defaultStylesheet = [[HTMLStylesheet alloc] initWithDictionary:styles]; 465 | } 466 | return defaultStylesheet; 467 | } 468 | 469 | + (instancetype)stylesheetWithDictionary:(NSDictionary *)dictionary 470 | { 471 | return [[self alloc] initWithDictionary:dictionary]; 472 | } 473 | 474 | + (NSDictionary *)dictionaryByMergingStyleDictionaries:(NSArray *)dictionaries 475 | { 476 | HTMLStylesheet *stylesheet = [[HTMLStylesheet alloc] init]; 477 | for (NSDictionary *dictionary in dictionaries) 478 | { 479 | [stylesheet addStylesFromDictionary:dictionary]; 480 | } 481 | return [stylesheet dictionaryRepresentation]; 482 | } 483 | 484 | - (void)addStylesFromDictionary:(NSDictionary *)dictionary 485 | { 486 | for (NSString *key in dictionary) 487 | { 488 | [self addStyles:dictionary[key] forSelector:key]; 489 | } 490 | } 491 | 492 | - (void)addStyles:(id)styles forSelector:(id)selector 493 | { 494 | if ([selector isKindOfClass:[NSString class]]) 495 | { 496 | NSArray *selectors = [selector componentsSeparatedByString:@","]; 497 | if ([selectors count] > 1) 498 | { 499 | for (NSString *selector in selectors) 500 | { 501 | [self addStyles:styles forSelector:selector]; 502 | } 503 | return; 504 | } 505 | selector = [[HTMLStyleSelector alloc] initWithString:selector]; 506 | } 507 | HTMLStyles *existingStyles = [_stylesBySelector objectForKey:selector]; 508 | if (existingStyles) 509 | { 510 | if (![styles isKindOfClass:[NSDictionary class]]) 511 | { 512 | styles = [existingStyles stylesByAddingStyles:styles]; 513 | } 514 | else 515 | { 516 | styles = [existingStyles stylesByAddingStylesFromDictionary:styles]; 517 | } 518 | } 519 | else 520 | { 521 | [(NSMutableArray *)_selectors addObject:selector]; 522 | if ([styles isKindOfClass:[NSDictionary class]]) 523 | { 524 | styles = [[HTMLStyles alloc] initWithDictionary:styles]; 525 | } 526 | } 527 | [(NSMutableDictionary *)_stylesBySelector setObject:styles forKey:selector]; 528 | } 529 | 530 | - (id)initWithDictionary:(NSDictionary *)dictionary 531 | { 532 | if ((self = [self init])) 533 | { 534 | [self addStylesFromDictionary:dictionary]; 535 | } 536 | return self; 537 | } 538 | 539 | - (id)init 540 | { 541 | if ((self = [super init])) 542 | { 543 | _selectors = [NSMutableArray array]; 544 | _stylesBySelector = [NSMutableDictionary dictionary]; 545 | } 546 | return self; 547 | } 548 | 549 | - (id)copyWithZone:(NSZone *)zone 550 | { 551 | return self; 552 | } 553 | 554 | - (NSDictionary *)dictionaryRepresentation 555 | { 556 | NSMutableDictionary *stylesheet = [NSMutableDictionary dictionary]; 557 | for (HTMLStyleSelector *selector in _selectors) 558 | { 559 | HTMLStyles *styles = [self stylesForSelector:selector]; 560 | [stylesheet setObject:[styles dictionaryRepresentation] 561 | forKey:[selector stringRepresentation]]; 562 | } 563 | return stylesheet; 564 | } 565 | 566 | - (HTMLStylesheet *)stylesheetByaddingStyles:(NSDictionary *)styles forSelector:(NSString *)selector 567 | { 568 | HTMLStylesheet *stylesheet = [[HTMLStylesheet alloc] initWithDictionary:[self dictionaryRepresentation]]; 569 | [stylesheet addStyles:styles forSelector:selector]; 570 | return stylesheet; 571 | } 572 | 573 | - (HTMLStylesheet *)stylesheetByaddingStylesFromDictionary:(NSDictionary *)dictionary 574 | { 575 | HTMLStylesheet *stylesheet = [[HTMLStylesheet alloc] initWithDictionary:[self dictionaryRepresentation]]; 576 | [stylesheet addStylesFromDictionary:dictionary]; 577 | return stylesheet; 578 | } 579 | 580 | - (HTMLStylesheet *)stylesheetByaddingStyles:(HTMLStylesheet *)styles 581 | { 582 | HTMLStylesheet *stylesheet = [[HTMLStylesheet alloc] initWithDictionary:[self dictionaryRepresentation]]; 583 | [stylesheet addStylesFromDictionary:[styles dictionaryRepresentation]]; 584 | return stylesheet; 585 | } 586 | 587 | - (HTMLStyles *)stylesForSelector:(id)selector 588 | { 589 | return [_stylesBySelector objectForKey:selector]; 590 | } 591 | 592 | - (HTMLStyles *)stylesForToken:(HTMLToken *)token 593 | { 594 | HTMLStyles *allStyles = nil; 595 | HTMLTokenAttributes *attributes = token.attributes; 596 | while (attributes) 597 | { 598 | HTMLStyles *styles = [[HTMLStyles alloc] init]; 599 | for (HTMLStyleSelector *selector in _selectors) 600 | { 601 | if ([selector matchesTokenAttributes:attributes]) 602 | { 603 | styles = [styles stylesByAddingStyles:[self stylesForSelector:selector]]; 604 | } 605 | } 606 | allStyles = [styles stylesByAddingStyles:allStyles]; 607 | attributes = attributes.parent; 608 | } 609 | return allStyles; 610 | } 611 | 612 | - (NSString *)description 613 | { 614 | return [[self dictionaryRepresentation] description]; 615 | } 616 | 617 | @end 618 | 619 | 620 | #pragma mark - 621 | #pragma mark HTML parsing 622 | 623 | 624 | @implementation HTMLTokenAttributes 625 | 626 | - (id)mutableCopyWithZone:(NSZone *)zone 627 | { 628 | HTMLTokenAttributes *copy = [[HTMLTokenAttributes alloc] init]; 629 | copy.href = _href; 630 | copy.tag = _tag; 631 | copy.classNames = _classNames; 632 | copy.list = _list; 633 | copy.listLevel = _listLevel; 634 | copy.nextListIndex = _nextListIndex; 635 | return copy; 636 | } 637 | 638 | @end 639 | 640 | 641 | @implementation HTMLToken 642 | 643 | - (BOOL)isSpace 644 | { 645 | return [_text isEqualToString:@" "]; 646 | } 647 | 648 | - (BOOL)isLinebreak 649 | { 650 | return [_text isEqualToString:@"\n"]; 651 | } 652 | 653 | - (BOOL)isWhitespace 654 | { 655 | return [self isSpace] || [self isLinebreak] || [_text isEqualToString:@"\u00A0"] || _attributes.bullet; 656 | } 657 | 658 | - (void)drawInRect:(CGRect)rect withStyles:(HTMLStyles *)styles 659 | { 660 | //set color 661 | [styles.textColor setFill]; 662 | 663 | //draw text 664 | [_text drawInRect:rect withFont:styles.font]; 665 | 666 | //underline? 667 | if (styles.underline) 668 | { 669 | CGContextRef c = UIGraphicsGetCurrentContext(); 670 | CGContextFillRect(c, CGRectMake(rect.origin.x, rect.origin.y + styles.font.pointSize + 1.0f, rect.size.width, 1.0f)); 671 | } 672 | } 673 | 674 | - (CGSize)sizeWithStyles:(HTMLStyles *)styles 675 | { 676 | return [_text sizeWithFont:styles.font]; 677 | } 678 | 679 | - (NSString *)description 680 | { 681 | return [[super description] stringByAppendingFormat:@"%@", [self isLinebreak]? @"\\n": _text]; 682 | } 683 | 684 | @end 685 | 686 | 687 | @interface HTMLTokenizer : NSObject 688 | 689 | @property (nonatomic, strong) NSMutableArray *tokens; 690 | @property (nonatomic, strong) NSMutableArray *stack; 691 | @property (nonatomic, strong) NSMutableString *text; 692 | @property (nonatomic, strong) NSString *html; 693 | 694 | - (id)initWithHTML:(NSString *)html; 695 | 696 | @end 697 | 698 | 699 | @implementation HTMLTokenizer 700 | 701 | - (NSCache *)cache 702 | { 703 | static NSCache *cache = nil; 704 | if (cache == nil) 705 | { 706 | cache = [[NSCache alloc] init]; 707 | } 708 | return cache; 709 | } 710 | 711 | - (id)initWithHTML:(NSString *)input 712 | { 713 | if ((self = [super init])) 714 | { 715 | if (input) 716 | { 717 | //check cache 718 | if (!(_tokens = [[self cache] objectForKey:input])) 719 | { 720 | _stack = [[NSMutableArray alloc] init]; 721 | _tokens = [[NSMutableArray alloc] init]; 722 | _text = [[NSMutableString alloc] init]; 723 | 724 | NSMutableString *html = [input mutableCopy]; 725 | 726 | //sanitize entities 727 | [self replaceEntities:@{ 728 | @"nbsp":@"\u00A0", @"bull":@"•", @"copy":@"©", @"reg":@"®", @"deg":@"°", 729 | @"ndash":@"–", @"mdash":@"—", @"apos":@"’", @"lsquo":@"‘", @"ldquo":@"“", @"rsquo":@"’", @"rdquo":@"”", 730 | @"cent":@"¢", @"pound":@"£", @"euro":@"€", @"yen":@"¥", @"ntilde":@"\u00F1", @"#39":@"'", 731 | @"frac14":@"¼", @"frac12":@"½", @"frac34":@"¾", @"auml": @"ä", @"ouml":@"ö", @"uuml":@"ü", @"oslash":@"ø", 732 | @"eacute":@"é", @"iacute":@"í", @"oacute":@"ó", @"uacute":@"ú", @"aring":@"å" 733 | } inString:html]; 734 | [self replacePattern:@"&(?!(gt|lt|amp|quot|(#[0-9]+)));" inString:html withPattern:@""]; 735 | [self replacePattern:@"&(?![a-z0-9]+;)" inString:html withPattern:@"&"]; 736 | [self replacePattern:@"<(?![/a-z])" inString:html withPattern:@"<"]; 737 | 738 | //sanitize tags 739 | [self replacePattern:@"([-_a-z]+)=([^\"'][^ >]+)" inString:html withPattern:@"$1=\"$2\""]; 740 | [self replacePattern:@"<(area|base|br|col|command|embed|hr|img|input|link|meta|param|source)(\\s[^>]*)?>" inString:html withPattern:@"<$1/>"]; 741 | 742 | //wrap in html tag 743 | _html = [NSString stringWithFormat:@"%@", html]; 744 | 745 | //parse 746 | NSData *data = [_html dataUsingEncoding:NSUTF8StringEncoding]; 747 | NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; 748 | parser.delegate = self; 749 | [parser parse]; 750 | 751 | //cache result 752 | [[self cache] setObject:_tokens forKey:input]; 753 | } 754 | } 755 | } 756 | return self; 757 | } 758 | 759 | - (void)replacePattern:(NSString *)pattern inString:(NSMutableString *)string withPattern:(NSString *)replacement 760 | { 761 | if (pattern && string) 762 | { 763 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil]; 764 | [regex replaceMatchesInString:string options:0 range:NSMakeRange(0, [string length]) withTemplate:replacement]; 765 | } 766 | } 767 | 768 | - (void)replaceEntities:(NSDictionary *)entitiesAndReplacements inString:(NSMutableString *)string 769 | { 770 | for (NSString *entity in entitiesAndReplacements) 771 | { 772 | [self replacePattern:[NSString stringWithFormat:@"&%@;", entity] 773 | inString:string withPattern:entitiesAndReplacements[entity]]; 774 | } 775 | } 776 | 777 | - (void)addText:(NSString *)text 778 | { 779 | [_text appendString:text]; 780 | } 781 | 782 | - (void)endText 783 | { 784 | //collapse white space 785 | NSString *text = [_text stringByReplacingOccurrencesOfString:@"[\t\n\r ]+" withString:@" " options:NSRegularExpressionSearch range:NSMakeRange(0, [_text length])]; 786 | 787 | //split into words 788 | NSMutableArray *words = [[text componentsSeparatedByString:@" "] mutableCopy]; 789 | 790 | //create tokens 791 | for (int i = 0; i < [words count]; i++) 792 | { 793 | NSString *word = words[i]; 794 | if (i > 0 && ![[_tokens lastObject] isWhitespace]) 795 | { 796 | //space 797 | HTMLToken *token = [[HTMLToken alloc] init]; 798 | token.attributes = [_stack lastObject]; 799 | token.text = @" "; 800 | [_tokens addObject:token]; 801 | } 802 | if ([word length]) 803 | { 804 | //word 805 | HTMLToken *token = [[HTMLToken alloc] init]; 806 | token.attributes = [_stack lastObject]; 807 | token.text = word; 808 | [_tokens addObject:token]; 809 | } 810 | } 811 | 812 | //clear text 813 | [_text setString:@""]; 814 | } 815 | 816 | - (void)addLinebreaks:(NSInteger)count 817 | { 818 | if ([_tokens count] && count) 819 | { 820 | HTMLToken *linebreak = [[HTMLToken alloc] init]; 821 | linebreak.attributes = [_stack lastObject]; 822 | linebreak.text = @"\n"; 823 | 824 | //discard white-space before a line break 825 | if ([[_tokens lastObject] isSpace]) [_tokens removeLastObject]; 826 | 827 | NSInteger last = [_tokens count] - 1; 828 | for (NSInteger i = last; i > last - count; i--) 829 | { 830 | if (i < 0 || ![_tokens[i] isLinebreak]) 831 | { 832 | [_tokens addObject:linebreak]; 833 | } 834 | } 835 | } 836 | } 837 | 838 | - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict 839 | { 840 | [self endText]; 841 | 842 | HTMLTokenAttributes *attributes = [[_stack lastObject] mutableCopy] ?: [[HTMLTokenAttributes alloc] init]; 843 | attributes.parent = [_stack lastObject]; 844 | elementName = [elementName lowercaseString]; 845 | attributes.tag = elementName; 846 | attributes.classNames = [attributeDict[@"class"] componentsSeparatedByString:@" "]; 847 | 848 | if ([elementName isEqualToString:@"a"]) 849 | { 850 | attributes.href = attributeDict[@"href"]; 851 | } 852 | else if ([elementName rangeOfString:@"^h\\d$" options:NSRegularExpressionSearch].length == [elementName length]) 853 | { 854 | [self addLinebreaks:2]; 855 | } 856 | else if ([elementName isEqualToString:@"p"] || [elementName isEqualToString:@"div"]) 857 | { 858 | [self addLinebreaks:2]; 859 | } 860 | else if ([elementName isEqualToString:@"ul"] || [elementName isEqualToString:@"ol"]) 861 | { 862 | [self addLinebreaks:2]; 863 | 864 | attributes.list = YES; 865 | attributes.listLevel ++; 866 | if ([elementName isEqualToString:@"ol"]) attributes.nextListIndex = 1; 867 | } 868 | else if ([elementName isEqualToString:@"li"]) 869 | { 870 | [self addLinebreaks:1]; 871 | 872 | attributes.list = NO; 873 | NSString *bullet = @"•"; 874 | if (attributes.nextListIndex) 875 | { 876 | bullet = [NSString stringWithFormat:@"%@.", @(attributes.nextListIndex)]; 877 | ((HTMLTokenAttributes *)[_stack lastObject]).nextListIndex ++; 878 | } 879 | 880 | //add list bullet 881 | HTMLToken *token = [[HTMLToken alloc] init]; 882 | token.attributes = [[_stack lastObject] mutableCopy]; 883 | token.attributes.parent = [_stack lastObject]; 884 | token.attributes.bullet = YES; 885 | token.text = bullet; 886 | [_tokens addObject:token]; 887 | } 888 | [_stack addObject:attributes]; 889 | } 890 | 891 | - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName 892 | { 893 | [self endText]; 894 | 895 | elementName = [elementName lowercaseString]; 896 | if ([elementName isEqualToString:@"br"]) 897 | { 898 | //discard white-space before a line break 899 | if ([[_tokens lastObject] isSpace]) [_tokens removeLastObject]; 900 | 901 | //this is a non-collapsing break, so we 902 | //won't use the addLinebreaks method 903 | HTMLToken *linebreak = [[HTMLToken alloc] init]; 904 | linebreak.attributes = [_stack lastObject]; 905 | linebreak.text = @"\n"; 906 | [_tokens addObject:linebreak]; 907 | } 908 | else if ([elementName isEqualToString:@"p"] || [elementName isEqualToString:@"div"] || 909 | [elementName rangeOfString:@"^h(\\d$|r)" options:NSRegularExpressionSearch].length == [elementName length]) 910 | { 911 | [self addLinebreaks:2]; 912 | } 913 | else if ([elementName isEqualToString:@"ul"] || [elementName isEqualToString:@"ol"]) 914 | { 915 | [self addLinebreaks:2]; 916 | } 917 | else if ([elementName isEqualToString:@"li"]) 918 | { 919 | [self addLinebreaks:1]; 920 | } 921 | 922 | [_stack removeLastObject]; 923 | } 924 | 925 | - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string 926 | { 927 | [self addText:string]; 928 | } 929 | 930 | - (void)parser:(NSXMLParser *)parser foundCDATA:(NSData *)CDATABlock 931 | { 932 | [self addText:[[NSString alloc] initWithData:CDATABlock encoding:NSUTF8StringEncoding]]; 933 | } 934 | 935 | - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError 936 | { 937 | [self endText]; 938 | 939 | #ifdef DEBUG 940 | 941 | [self addLinebreaks:2]; 942 | [self addText:[parseError localizedDescription]]; 943 | [self endText]; 944 | 945 | NSLog(@"XML parser error: %@", parseError); 946 | 947 | #endif 948 | 949 | } 950 | 951 | @end 952 | 953 | 954 | #pragma mark - 955 | #pragma mark HTML layout 956 | 957 | 958 | @interface HTMLLayout : NSObject 959 | 960 | @property (nonatomic, copy) NSArray *tokens; 961 | @property (nonatomic, copy) HTMLStylesheet *stylesheet; 962 | @property (nonatomic, assign) CGFloat maxWidth; 963 | 964 | //generated properties 965 | @property (nonatomic, strong, readonly) NSMutableArray *frames; 966 | @property (nonatomic, assign, readonly) CGSize size; 967 | 968 | - (void)update; 969 | - (void)drawAtPoint:(CGPoint)point; 970 | - (HTMLToken *)tokenAtPosition:(CGPoint)point; 971 | 972 | @end 973 | 974 | 975 | @interface HTMLLayout () 976 | 977 | @property (nonatomic, assign) CGSize size; 978 | 979 | @end 980 | 981 | 982 | @implementation HTMLLayout 983 | 984 | - (id)init 985 | { 986 | if ((self = [super init])) 987 | { 988 | _stylesheet = [HTMLStylesheet defaultStylesheet]; 989 | } 990 | return self; 991 | } 992 | 993 | - (void)setTokens:(NSArray *)tokens 994 | { 995 | _tokens = [tokens copy]; 996 | [self setNeedsUpdate]; 997 | } 998 | 999 | - (void)setStylesheet:(HTMLStylesheet *)stylesheet 1000 | { 1001 | _stylesheet = [[HTMLStylesheet defaultStylesheet] stylesheetByaddingStyles:stylesheet]; 1002 | [self setNeedsUpdate]; 1003 | } 1004 | 1005 | - (void)setMaxWidth:(CGFloat)maxWidth 1006 | { 1007 | _maxWidth = maxWidth; 1008 | [self setNeedsUpdate]; 1009 | } 1010 | 1011 | - (void)setNeedsUpdate 1012 | { 1013 | _frames = nil; 1014 | } 1015 | 1016 | - (void)update 1017 | { 1018 | _frames = [[NSMutableArray alloc] init]; 1019 | _size = CGSizeZero; 1020 | 1021 | CGPoint position = CGPointZero; 1022 | CGFloat lineHeight = 0.0f; 1023 | NSInteger lineStartIndex = 0; 1024 | BOOL newLine = YES; 1025 | BOOL wrapped = NO; 1026 | 1027 | NSInteger tokenCount = [_tokens count]; 1028 | for (NSInteger i = 0; i < tokenCount; i++) 1029 | { 1030 | HTMLToken *token = _tokens[i]; 1031 | HTMLStyles *styles = [_stylesheet stylesForToken:token]; 1032 | 1033 | CGFloat oneEm = [@"m" sizeWithFont:styles.font].width; 1034 | CGFloat indent = oneEm * 3; 1035 | CGFloat padding = oneEm; 1036 | 1037 | CGSize size = [token sizeWithStyles:styles]; 1038 | if ([token isLinebreak]) 1039 | { 1040 | //newline 1041 | lineHeight = MAX(lineHeight, size.height); 1042 | position = CGPointMake(0.0f, position.y + lineHeight); 1043 | lineHeight = 0.0f; 1044 | newLine = YES; 1045 | 1046 | //calculate frame and update size 1047 | [_frames addObject:[NSValue valueWithCGRect:CGRectZero]]; 1048 | _size.height = position.y; 1049 | } 1050 | else if ([token isSpace]) 1051 | { 1052 | //space 1053 | if (newLine || position.x + size.width > _maxWidth) 1054 | { 1055 | //discard token 1056 | size = CGSizeZero; 1057 | } 1058 | 1059 | //calculate frame 1060 | CGRect frame; 1061 | frame.origin = position; 1062 | frame.origin.x += token.attributes.listLevel * indent; 1063 | frame.size = size; 1064 | [_frames addObject:[NSValue valueWithCGRect:frame]]; 1065 | 1066 | //prepare for next frame 1067 | position.x += size.width; 1068 | lineHeight = MAX(lineHeight, size.height); 1069 | } 1070 | else 1071 | { 1072 | if (newLine) 1073 | { 1074 | //indent list 1075 | position.x = token.attributes.listLevel * indent; 1076 | } 1077 | 1078 | //calculate size 1079 | if (position.x + size.width > _maxWidth) 1080 | { 1081 | if (!newLine) 1082 | { 1083 | //discard previous space token 1084 | if (i > 0 && [_tokens[i-1] isSpace]) 1085 | { 1086 | CGRect frame = [_frames[i-1] CGRectValue]; 1087 | frame.size = CGSizeZero; 1088 | _frames[i-1] = [NSValue valueWithCGRect:frame]; 1089 | } 1090 | 1091 | //new line 1092 | position = CGPointMake(token.attributes.listLevel * indent, position.y + lineHeight); 1093 | lineHeight = 0.0f; 1094 | wrapped = YES; 1095 | } 1096 | else 1097 | { 1098 | //truncate 1099 | size.width = _maxWidth; 1100 | } 1101 | } 1102 | 1103 | //handle bullets 1104 | if (token.attributes.bullet) 1105 | { 1106 | size.width += padding; 1107 | if (token.attributes.listLevel) 1108 | { 1109 | position.x -= size.width; 1110 | } 1111 | } 1112 | 1113 | //calculate frame 1114 | CGRect frame; 1115 | frame.origin = position; 1116 | frame.size = size; 1117 | [_frames addObject:[NSValue valueWithCGRect:frame]]; 1118 | 1119 | //update size 1120 | _size.height = MAX(_size.height, position.y + size.height); 1121 | _size.width = MAX(_size.width, position.x + size.width); 1122 | 1123 | //prepare for next frame 1124 | lineHeight = MAX(lineHeight, size.height); 1125 | position.x += size.width; 1126 | newLine = NO; 1127 | } 1128 | 1129 | if (newLine || wrapped || i == tokenCount - 1) 1130 | { 1131 | //determine if adjustment is needed 1132 | NSInteger lastIndex = (wrapped || newLine)? i - 1: i; 1133 | NSTextAlignment alignment = [_stylesheet stylesForToken:_tokens[lineStartIndex]].textAlignment; 1134 | if (self.maxWidth && alignment != NSTextAlignmentLeft) 1135 | { 1136 | //adjust alignment 1137 | CGRect frame = [_frames[lastIndex] CGRectValue]; 1138 | CGFloat lineWidth = frame.origin.x + frame.size.width - [_frames[lineStartIndex] CGRectValue].origin.x; 1139 | CGFloat offset = 0.0f; 1140 | if (alignment == NSTextAlignmentRight) 1141 | { 1142 | offset = self.maxWidth - lineWidth; 1143 | } 1144 | else if (alignment == NSTextAlignmentCenter) 1145 | { 1146 | offset = (self.maxWidth - lineWidth) / 2; 1147 | } 1148 | else 1149 | { 1150 | //TODO: other alignment options 1151 | } 1152 | 1153 | for (NSInteger j = lineStartIndex; j <= lastIndex; j++) 1154 | { 1155 | CGRect frame = [_frames[j] CGRectValue]; 1156 | frame.origin.x += offset; 1157 | _frames[j] = [NSValue valueWithCGRect:frame]; 1158 | } 1159 | } 1160 | 1161 | //prepare for next line 1162 | if (newLine) 1163 | { 1164 | lineStartIndex = i + 1; 1165 | } 1166 | else if (wrapped) 1167 | { 1168 | lineStartIndex = i; 1169 | wrapped = NO; 1170 | } 1171 | } 1172 | } 1173 | } 1174 | 1175 | - (CGSize)size 1176 | { 1177 | if (!_frames) [self update]; 1178 | return _size; 1179 | } 1180 | 1181 | - (void)drawAtPoint:(CGPoint)point 1182 | { 1183 | if (!_frames) [self update]; 1184 | for (int i = 0 ; i < [_tokens count]; i ++) 1185 | { 1186 | CGRect frame = [_frames[i] CGRectValue]; 1187 | HTMLToken *token = _tokens[i]; 1188 | [token drawInRect:frame withStyles:[_stylesheet stylesForToken:token]]; 1189 | } 1190 | } 1191 | 1192 | - (HTMLToken *)tokenAtPosition:(CGPoint)point 1193 | { 1194 | if (!_frames) [self update]; 1195 | for (int i = 0; i < [_tokens count]; i++) 1196 | { 1197 | if (CGRectContainsPoint([_frames[i] CGRectValue], point)) 1198 | { 1199 | return _tokens[i]; 1200 | } 1201 | } 1202 | return nil; 1203 | } 1204 | 1205 | @end 1206 | 1207 | 1208 | @implementation NSString (HTMLRendering) 1209 | 1210 | - (CGSize)sizeForWidth:(CGFloat)width withHTMLStyles:(NSDictionary *)stylesheet 1211 | { 1212 | HTMLTokenizer *tokenizer = [[HTMLTokenizer alloc] initWithHTML:self]; 1213 | HTMLLayout *layout = [[HTMLLayout alloc] init]; 1214 | layout.tokens = tokenizer.tokens; 1215 | layout.stylesheet = [HTMLStylesheet stylesheetWithDictionary:stylesheet]; 1216 | layout.maxWidth = width; 1217 | return layout.size; 1218 | } 1219 | 1220 | - (void)drawInRect:(CGRect)rect withHTMLStyles:(NSDictionary *)stylesheet 1221 | { 1222 | HTMLTokenizer *tokenizer = [[HTMLTokenizer alloc] init]; 1223 | HTMLLayout *layout = [[HTMLLayout alloc] init]; 1224 | layout.tokens = tokenizer.tokens; 1225 | layout.stylesheet = [HTMLStylesheet stylesheetWithDictionary:stylesheet]; 1226 | layout.maxWidth = rect.size.width; 1227 | 1228 | //TODO: crop to correct height 1229 | [layout drawAtPoint:rect.origin]; 1230 | } 1231 | 1232 | @end 1233 | 1234 | 1235 | #pragma mark - 1236 | #pragma mark HTML label 1237 | 1238 | 1239 | @interface HTMLLabel () 1240 | 1241 | @property (nonatomic, strong) HTMLLayout *layout; 1242 | 1243 | @end 1244 | 1245 | 1246 | @implementation HTMLLabel 1247 | 1248 | - (void)setUp 1249 | { 1250 | _layout = [[HTMLLayout alloc] init]; 1251 | self.stylesheet = nil; 1252 | 1253 | HTMLTokenizer *tokenizer = [[HTMLTokenizer alloc] initWithHTML:self.text]; 1254 | _layout.tokens = tokenizer.tokens; 1255 | [self setNeedsDisplay]; 1256 | } 1257 | 1258 | - (id)initWithFrame:(CGRect)frame 1259 | { 1260 | if ((self = [super initWithFrame:frame])) 1261 | { 1262 | self.userInteractionEnabled = YES; 1263 | [self setUp]; 1264 | } 1265 | return self; 1266 | } 1267 | 1268 | - (id)initWithCoder:(NSCoder *)aDecoder 1269 | { 1270 | if ((self = [super initWithCoder:aDecoder])) 1271 | { 1272 | [self setUp]; 1273 | } 1274 | return self; 1275 | } 1276 | 1277 | - (void)setText:(NSString *)text 1278 | { 1279 | if (![super.text isEqualToString:text]) 1280 | { 1281 | super.text = text; 1282 | HTMLTokenizer *tokenizer = [[HTMLTokenizer alloc] initWithHTML:self.text]; 1283 | _layout.tokens = tokenizer.tokens; 1284 | [self setNeedsDisplay]; 1285 | } 1286 | } 1287 | 1288 | - (void)setFont:(UIFont *)font 1289 | { 1290 | super.font = font; 1291 | self.stylesheet = [HTMLStylesheet dictionaryByMergingStyleDictionaries:@[_stylesheet ?: @{}, @{@"html": @{HTMLFont: self.font ?: [UIFont systemFontOfSize:17.0f]}}]]; 1292 | } 1293 | 1294 | - (void)setTextColor:(UIColor *)textColor 1295 | { 1296 | super.textColor = textColor; 1297 | self.stylesheet = [HTMLStylesheet dictionaryByMergingStyleDictionaries:@[_stylesheet ?: @{}, @{@"html": @{HTMLTextColor: self.textColor ?: [UIColor blackColor]}}]]; 1298 | } 1299 | 1300 | - (void)setTextAlignment:(NSTextAlignment)textAlignment 1301 | { 1302 | super.textAlignment = textAlignment; 1303 | self.stylesheet = [HTMLStylesheet dictionaryByMergingStyleDictionaries:@[_stylesheet ?: @{}, @{@"html": @{HTMLTextAlignment: @(textAlignment)}}]]; 1304 | } 1305 | 1306 | - (void)setStylesheet:(NSDictionary *)stylesheet 1307 | { 1308 | _stylesheet = [HTMLStylesheet dictionaryByMergingStyleDictionaries:@[@{@"html": @{ 1309 | HTMLFont: self.font ?: [UIFont systemFontOfSize:17.0f], 1310 | HTMLTextColor: self.textColor ?: [UIColor blackColor], 1311 | HTMLTextAlignment: @(self.textAlignment) 1312 | }}, stylesheet ?: @{}]]; 1313 | 1314 | _layout.stylesheet = [HTMLStylesheet stylesheetWithDictionary:_stylesheet]; 1315 | 1316 | HTMLStyles *styles = [_layout.stylesheet stylesForSelector:@"html"]; 1317 | super.font = styles.font ?: self.font; 1318 | super.textColor = styles.textColor ?: self.textColor; 1319 | super.textAlignment = styles.textAlignment; 1320 | [self setNeedsDisplay]; 1321 | } 1322 | 1323 | - (CGSize)sizeThatFits:(CGSize)size 1324 | { 1325 | _layout.maxWidth = size.width; 1326 | return _layout.size; 1327 | } 1328 | 1329 | - (void)drawRect:(CGRect)rect 1330 | { 1331 | _layout.maxWidth = self.bounds.size.width; 1332 | [_layout drawAtPoint:CGPointZero]; 1333 | } 1334 | 1335 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 1336 | { 1337 | UITouch *touch = [touches anyObject]; 1338 | [_layout tokenAtPosition:[touch locationInView:self]].attributes.active = YES; 1339 | [self setNeedsDisplay]; 1340 | [self.nextResponder touchesBegan:touches withEvent:event]; 1341 | } 1342 | 1343 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 1344 | { 1345 | [[_layout valueForKeyPath:@"tokens.attributes"] makeObjectsPerformSelector:@selector(setActive:) withObject:0]; 1346 | [self setNeedsDisplay]; 1347 | 1348 | UITouch *touch = [touches anyObject]; 1349 | HTMLToken *token = [_layout tokenAtPosition:[touch locationInView:self]]; 1350 | if (token.attributes.href) 1351 | { 1352 | NSURL *URL = [NSURL URLWithString:token.attributes.href]; 1353 | if ([_delegate respondsToSelector:@selector(HTMLLabel:tappedLinkWithURL:bounds:)]) 1354 | { 1355 | CGRect frame = [_layout.frames[[_layout.tokens indexOfObject:token]] CGRectValue]; 1356 | [_delegate HTMLLabel:self tappedLinkWithURL:URL bounds:frame]; 1357 | } 1358 | BOOL openURL = YES; 1359 | if ([_delegate respondsToSelector:@selector(HTMLLabel:shouldOpenURL:)]) 1360 | { 1361 | openURL = [_delegate HTMLLabel:self shouldOpenURL:URL]; 1362 | } 1363 | if (openURL) [[UIApplication sharedApplication] openURL:URL]; 1364 | } 1365 | [self.nextResponder touchesEnded:touches withEvent:event]; 1366 | } 1367 | 1368 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 1369 | { 1370 | [[_layout valueForKeyPath:@"tokens.attributes"] makeObjectsPerformSelector:@selector(setActive:) withObject:0]; 1371 | [self setNeedsDisplay]; 1372 | [self.nextResponder touchesCancelled:touches withEvent:event]; 1373 | } 1374 | 1375 | - (CGSize)intrinsicContentSize 1376 | { 1377 | _layout.maxWidth = self.bounds.size.width; 1378 | return _layout.size; 1379 | } 1380 | 1381 | 1382 | @end 1383 | --------------------------------------------------------------------------------