├── 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 dolor sit er elit lamet, consectetaur cillium adipisicing pecu, 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 |
--------------------------------------------------------------------------------