├── Resources └── MTDURLPreview.bundle │ ├── image-placeholder.png │ ├── imgur-placeholder.jpg │ ├── reddit-placeholder.png │ ├── image-placeholder@2x.png │ ├── imgur-placeholder@2x.jpg │ └── reddit-placeholder@2x.png ├── Prefix.pch ├── .gitignore ├── MTDURLPreview ├── MTDURLPreviewParser.h ├── MTDURLPreviewCache.h ├── MTDURLPreviewCache.m ├── MTDURLPreview.h ├── MTDURLPreviewView.h ├── MTDURLPreview.m ├── MTDURLPreviewParser.m ├── Parser │ ├── MTDHTMLElement.h │ └── MTDHTMLElement.m └── MTDURLPreviewView.m ├── README.md ├── LICENSE └── MTDURLPreview.xcodeproj └── project.pbxproj /Resources/MTDURLPreview.bundle/image-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myell0w/MTDURLPreview/HEAD/Resources/MTDURLPreview.bundle/image-placeholder.png -------------------------------------------------------------------------------- /Resources/MTDURLPreview.bundle/imgur-placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myell0w/MTDURLPreview/HEAD/Resources/MTDURLPreview.bundle/imgur-placeholder.jpg -------------------------------------------------------------------------------- /Resources/MTDURLPreview.bundle/reddit-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myell0w/MTDURLPreview/HEAD/Resources/MTDURLPreview.bundle/reddit-placeholder.png -------------------------------------------------------------------------------- /Resources/MTDURLPreview.bundle/image-placeholder@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myell0w/MTDURLPreview/HEAD/Resources/MTDURLPreview.bundle/image-placeholder@2x.png -------------------------------------------------------------------------------- /Resources/MTDURLPreview.bundle/imgur-placeholder@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myell0w/MTDURLPreview/HEAD/Resources/MTDURLPreview.bundle/imgur-placeholder@2x.jpg -------------------------------------------------------------------------------- /Resources/MTDURLPreview.bundle/reddit-placeholder@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myell0w/MTDURLPreview/HEAD/Resources/MTDURLPreview.bundle/reddit-placeholder@2x.png -------------------------------------------------------------------------------- /Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'MTDURLPreview' target in the 'MTDURLPreview' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #import 8 | #endif 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | -------------------------------------------------------------------------------- /MTDURLPreview/MTDURLPreviewParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // MTDURLPreviewParser.h 3 | // MTDURLPreview 4 | // 5 | // Created by Matthias Tretter on 09.07.13. 6 | // Copyright (c) 2013 @myell0w. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "MTDURLPreview.h" 11 | 12 | 13 | @interface MTDURLPreviewParser : NSObject 14 | 15 | + (MTDURLPreview *)previewFromHTMLData:(NSData *)data URL:(NSURL *)URL; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /MTDURLPreview/MTDURLPreviewCache.h: -------------------------------------------------------------------------------- 1 | // 2 | // MTDURLPreviewCache.h 3 | // MTDURLPreview 4 | // 5 | // Created by Matthias Tretter on 31.07.13. 6 | // Copyright (c) 2013 @myell0w. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | @class MTDURLPreview; 13 | 14 | 15 | @interface MTDURLPreviewCache : NSCache 16 | 17 | - (MTDURLPreview *)cachedPreviewForURL:(NSURL *)URL; 18 | - (void)cachePreview:(MTDURLPreview *)preview forURL:(NSURL *)URL; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /MTDURLPreview/MTDURLPreviewCache.m: -------------------------------------------------------------------------------- 1 | #import "MTDURLPreviewCache.h" 2 | 3 | 4 | static inline NSString* MTDCacheKeyFromURL(NSURL *URL) { 5 | return URL.absoluteString; 6 | } 7 | 8 | 9 | @implementation MTDURLPreviewCache 10 | 11 | - (instancetype)init { 12 | if ((self = [super init])) { 13 | self.name = @"MTDURLPreviewCache"; 14 | } 15 | 16 | return self; 17 | } 18 | 19 | - (MTDURLPreview *)cachedPreviewForURL:(NSURL *)URL { 20 | NSString *key = MTDCacheKeyFromURL(URL); 21 | return [self objectForKey:key]; 22 | } 23 | 24 | - (void)cachePreview:(MTDURLPreview *)preview forURL:(NSURL *)URL { 25 | if (preview != nil && URL != nil) { 26 | NSString *key = MTDCacheKeyFromURL(URL); 27 | [self setObject:preview forKey:key]; 28 | } 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #MTDURLPreview 2 | 3 | Delight your users with simple URL previews for your iOS app. 4 | 5 | MTDURLPreview parses the HTML content of a given URL and tries to find a suitable image and the title/domain of the URL. 6 | If you have ideas for improvement (e.g. better image heuristic), please hit me up on Twitter. 7 | 8 | ![MTDURLPreview](http://f.cl.ly/items/3q383h2Z3Y101h411R0r/iOS_Simulator_Bildschirmfoto_22.07.2013_12.26.38-2.png) 9 | 10 | ## Credits 11 | 12 | MTDURLPreview was created by [Matthias Tretter](https://github.com/myell0w/) ([@myell0w](http://twitter.com/myell0w)) and extracted from the beautiful reddit client [*Biscuit for reddit*](http://biscuitapp.co). 13 | 14 | ![Biscuit for reddit](http://cdn.maikoapp.com/4u9e/5cu3e/200w.png) 15 | 16 | ## License 17 | 18 | MTDURLPreview is available under the MIT license. See the LICENSE file for more info. 19 | -------------------------------------------------------------------------------- /MTDURLPreview/MTDURLPreview.h: -------------------------------------------------------------------------------- 1 | // 2 | // MTDURLPreview.h 3 | // MTDURLPreview 4 | // 5 | // Created by Matthias Tretter on 09.07.13. 6 | // Copyright (c) 2013 @myell0w. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "MTDURLPreviewView.h" 11 | 12 | 13 | @class MTDURLPreview; 14 | 15 | 16 | typedef void(^mtd_url_preview_block)(MTDURLPreview *preview, NSError *error); 17 | 18 | 19 | @interface MTDURLPreview : NSObject 20 | 21 | @property (nonatomic, readonly) NSString *title; 22 | @property (nonatomic, readonly) NSString *domain; 23 | @property (nonatomic, readonly) NSURL *imageURL; 24 | @property (nonatomic, readonly) NSString *content; 25 | 26 | - (instancetype)initWithTitle:(NSString *)title 27 | domain:(NSString *)domain 28 | imageURL:(NSURL *)imageURL 29 | content:(NSString *)content; 30 | 31 | + (void)loadPreviewWithURL:(NSURL *)URL completion:(mtd_url_preview_block)completion; 32 | + (void)cancelLoadOfPreviewWithURL:(NSURL *)URL; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Matthias Tretter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MTDURLPreview/MTDURLPreviewView.h: -------------------------------------------------------------------------------- 1 | // 2 | // MTDURLPreviewView.h 3 | // MTDURLPreview 4 | // 5 | // Created by Matthias Tretter on 09.07.13. 6 | // Copyright (c) 2013 @myell0w. All rights reserved. 7 | // 8 | 9 | 10 | @class MTDURLPreview; 11 | 12 | 13 | @interface MTDURLPreviewView : UIView 14 | 15 | + (CGFloat)neededHeightForTitle:(NSString *)title 16 | domain:(NSString *)domain 17 | content:(NSString *)content 18 | imageVisible:(BOOL)imageVisible 19 | constrainedToWidth:(CGFloat)width; 20 | 21 | + (void)setTitleFont:(UIFont *)titleFont; 22 | + (void)setDomainFont:(UIFont *)domainFont; 23 | 24 | @property (nonatomic, readonly) UILabel *titleLabel; 25 | @property (nonatomic, readonly) UILabel *domainLabel; 26 | @property (nonatomic, readonly) UILabel *contentLabel; 27 | @property (nonatomic, readonly) UIImageView *imageView; 28 | 29 | @property (nonatomic, strong) UIColor *backgroundColor; 30 | @property (nonatomic, strong) UIColor *textColor; 31 | @property (nonatomic, strong) UIColor *borderColor; 32 | 33 | @end 34 | 35 | 36 | @interface MTDURLPreviewView (MTDModelObject) 37 | 38 | + (CGFloat)neededHeightForURLPreview:(MTDURLPreview *)preview 39 | constrainedToWidth:(CGFloat)width; 40 | 41 | - (void)setFromURLPreview:(MTDURLPreview *)preview; 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /MTDURLPreview/MTDURLPreview.m: -------------------------------------------------------------------------------- 1 | #import "MTDURLPreview.h" 2 | #import "MTDURLPreviewParser.h" 3 | #import "MTDURLPreviewCache.h" 4 | 5 | 6 | static NSMutableSet *canceledURLs = nil; 7 | static dispatch_queue_t mtd_url_preview_queue() { 8 | static dispatch_queue_t queue; 9 | static dispatch_once_t onceToken; 10 | dispatch_once(&onceToken, ^{ 11 | queue = dispatch_queue_create("at.myell0w.url-preview-queue", DISPATCH_QUEUE_CONCURRENT); 12 | }); 13 | 14 | return queue; 15 | } 16 | 17 | static MTDURLPreviewCache* mtd_preview_cache() { 18 | static MTDURLPreviewCache *cache = nil; 19 | static dispatch_once_t onceToken; 20 | dispatch_once(&onceToken, ^{ 21 | cache = [MTDURLPreviewCache new]; 22 | canceledURLs = [NSMutableSet new]; 23 | }); 24 | 25 | return cache; 26 | } 27 | 28 | 29 | @implementation MTDURLPreview 30 | 31 | //////////////////////////////////////////////////////////////////////// 32 | #pragma mark - Lifecycle 33 | //////////////////////////////////////////////////////////////////////// 34 | 35 | - (instancetype)initWithTitle:(NSString *)title 36 | domain:(NSString *)domain 37 | imageURL:(NSURL *)imageURL 38 | content:(NSString *)content { 39 | if ((self = [super init])) { 40 | _title = [title copy]; 41 | _domain = [domain copy]; 42 | _imageURL = imageURL; 43 | _content = [content copy]; 44 | } 45 | 46 | return self; 47 | } 48 | 49 | //////////////////////////////////////////////////////////////////////// 50 | #pragma mark - Class Methods 51 | //////////////////////////////////////////////////////////////////////// 52 | 53 | + (void)loadPreviewWithURL:(NSURL *)URL completion:(mtd_url_preview_block)completion { 54 | NSParameterAssert(URL != nil); 55 | NSParameterAssert(completion != nil); 56 | 57 | if (URL == nil || completion == nil) { 58 | return; 59 | } 60 | 61 | MTDURLPreview *cachedPreview = [mtd_preview_cache() cachedPreviewForURL:URL]; 62 | 63 | if (cachedPreview != nil) { 64 | dispatch_async(dispatch_get_main_queue(), ^{ 65 | completion(cachedPreview, nil); 66 | }); 67 | } else { 68 | [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:URL] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *error) { 69 | dispatch_async(mtd_url_preview_queue(), ^{ 70 | if (responseData != nil) { 71 | MTDURLPreview *preview = [MTDURLPreviewParser previewFromHTMLData:responseData URL:URL]; 72 | 73 | [mtd_preview_cache() cachePreview:preview forURL:URL]; 74 | 75 | if (![canceledURLs containsObject:URL]) { 76 | dispatch_async(dispatch_get_main_queue(), ^{ 77 | completion(preview, nil); 78 | }); 79 | } 80 | } else { 81 | if (![canceledURLs containsObject:URL]) { 82 | dispatch_async(dispatch_get_main_queue(), ^{ 83 | completion(nil, error); 84 | }); 85 | } 86 | } 87 | 88 | [canceledURLs removeObject:URL]; 89 | }); 90 | }]; 91 | } 92 | } 93 | 94 | + (void)cancelLoadOfPreviewWithURL:(NSURL *)URL { 95 | if (URL != nil) { 96 | [canceledURLs addObject:URL]; 97 | } 98 | } 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /MTDURLPreview/MTDURLPreviewParser.m: -------------------------------------------------------------------------------- 1 | #import "MTDURLPreviewParser.h" 2 | #import "MTDHTMLElement.h" 3 | 4 | 5 | 6 | static BOOL MTDStringHasImageExtension(NSString *string) { 7 | static NSSet *imageExtensions = nil; 8 | 9 | static dispatch_once_t onceToken; 10 | dispatch_once(&onceToken, ^{ 11 | imageExtensions = [NSSet setWithObjects:@"tiff", @"tif", @"jpg", @"jpeg", @"png", @"bmp", @"bmpf", @"ico", nil]; 12 | }); 13 | 14 | 15 | NSString *extension = [string.pathExtension lowercaseString]; 16 | NSRange parameterRange = [extension rangeOfString:@"?"]; 17 | 18 | if (parameterRange.location != NSNotFound) { 19 | extension = [extension substringToIndex:parameterRange.location]; 20 | } 21 | 22 | return [imageExtensions containsObject:extension]; 23 | } 24 | 25 | 26 | @implementation MTDURLPreviewParser 27 | 28 | + (MTDURLPreview *)previewFromHTMLData:(NSData *)data URL:(NSURL *)URL { 29 | NSString *title = nil; 30 | NSString *domain = [URL host]; 31 | NSURL *imageURL = nil; 32 | NSString *content = nil; 33 | 34 | // Check for Open Graph Metadata first 35 | NSArray *metaElements = [MTDHTMLElement nodesForXPathQuery:@"//html/head/meta" onHTML:data]; 36 | for (MTDHTMLElement *metaElement in metaElements) { 37 | NSString *property = [metaElement attributeWithName:@"property"]; 38 | 39 | if ([property isEqualToString:@"og:title"]) { 40 | title = [metaElement attributeWithName:@"content"]; 41 | } else if ([property isEqualToString:@"og:image"]) { 42 | NSString *imageAddress = [metaElement attributeWithName:@"content"]; 43 | imageURL = [self sanitizedImageURLWithBaseURL:URL imageAddress:imageAddress]; 44 | } else if ([property isEqualToString:@"og:description"]) { 45 | content = [metaElement attributeWithName:@"content"]; 46 | } 47 | } 48 | 49 | if (title == nil) { 50 | MTDHTMLElement *titleElement = [MTDHTMLElement nodeForXPathQuery:@"//html/head/title" onHTML:data]; 51 | title = titleElement.contentString; 52 | } 53 | 54 | if (imageURL == nil) { 55 | NSArray *imageElements = [MTDHTMLElement nodesForXPathQuery:@"//img" onHTML:data]; 56 | NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"MTDURLPreview" ofType:@"bundle"]; 57 | NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; 58 | NSString *suffix = [UIScreen mainScreen].scale > 1.f ? @"@2x" : @""; 59 | 60 | // special cases 61 | if ([domain rangeOfString:@"imgur"].location != NSNotFound) { 62 | imageURL = [bundle URLForResource:[@"imgur-placeholder" stringByAppendingString:suffix] withExtension:@"jpg"]; 63 | } else if ([domain rangeOfString:@"reddit"].location != NSNotFound) { 64 | imageURL = [bundle URLForResource:[@"reddit-placeholder" stringByAppendingString:suffix] withExtension:@"png"]; 65 | } 66 | 67 | if (imageURL == nil) { 68 | // heuristic: give higher priority to jpg images 69 | for (MTDHTMLElement *element in imageElements) { 70 | NSString *imageAddress = [element attributeWithName:@"src"]; 71 | NSString *lowercaseAddress = [imageAddress lowercaseString]; 72 | 73 | if ([lowercaseAddress hasSuffix:@"jpg"] || [lowercaseAddress hasSuffix:@"jpeg"]) { 74 | imageURL = [self sanitizedImageURLWithBaseURL:URL imageAddress:imageAddress]; 75 | break; 76 | } 77 | } 78 | } 79 | 80 | if (imageURL == nil) { 81 | for (MTDHTMLElement *element in imageElements) { 82 | NSString *imageAddress = [element attributeWithName:@"src"]; 83 | 84 | if (MTDStringHasImageExtension(imageAddress)) { 85 | imageURL = [self sanitizedImageURLWithBaseURL:URL imageAddress:imageAddress]; 86 | break; 87 | } 88 | } 89 | } 90 | } 91 | 92 | if (content == nil) { 93 | MTDHTMLElement *firstPElement = [MTDHTMLElement nodeForXPathQuery:@"//p" onHTML:data]; 94 | content = firstPElement.contentStringByUnifyingSubnodes; 95 | } 96 | 97 | return [[MTDURLPreview alloc] initWithTitle:title 98 | domain:domain 99 | imageURL:imageURL 100 | content:content]; 101 | } 102 | 103 | //////////////////////////////////////////////////////////////////////// 104 | #pragma mark - Private 105 | //////////////////////////////////////////////////////////////////////// 106 | 107 | + (NSURL *)sanitizedImageURLWithBaseURL:(NSURL *)URL imageAddress:(NSString *)imageAddress { 108 | if (imageAddress == nil) { 109 | return nil; 110 | } else if ([imageAddress hasPrefix:@"//"]) { 111 | imageAddress = [imageAddress substringFromIndex:2]; 112 | } else if ([imageAddress hasPrefix:@"/"]) { 113 | imageAddress = [[@"http://" stringByAppendingString:URL.host] stringByAppendingString:imageAddress]; 114 | } 115 | 116 | return [NSURL URLWithString:imageAddress]; 117 | } 118 | 119 | @end 120 | -------------------------------------------------------------------------------- /MTDURLPreview/Parser/MTDHTMLElement.h: -------------------------------------------------------------------------------- 1 | // 2 | // MTDHTMLElement.h 3 | // 4 | // Updated by Matthias Tretter 5 | // Copyright (c) 2012 Matthias Tretter (@myell0w). All rights reserved. 6 | // 7 | // Based on Matt Gallagher's XPathResultNode from cocoawithlove.com, modified for MTDirectionsKit. 8 | // Original LICENSE: 9 | // 10 | // CocoaWithLove 11 | // 12 | // Created by Matt Gallagher on 2011/05/20. 13 | // Copyright 2011 Matt Gallagher. All rights reserved. 14 | // 15 | // This software is provided 'as-is', without any express or implied 16 | // warranty. In no event will the authors be held liable for any damages 17 | // arising from the use of this software. Permission is granted to anyone to 18 | // use this software for any purpose, including commercial applications, and to 19 | // alter it and redistribute it freely, subject to the following restrictions: 20 | // 21 | // 1. The origin of this software must not be misrepresented; you must not 22 | // claim that you wrote the original software. If you use this software 23 | // in a product, an acknowledgment in the product documentation would be 24 | // appreciated but is not required. 25 | // 2. Altered source versions must be plainly marked as such, and must not be 26 | // misrepresented as being the original software. 27 | // 3. This notice may not be removed or altered from any source 28 | // distribution. 29 | // 30 | 31 | 32 | /** 33 | An instance of MTDHTMLElement represents one XML node of a whole XML tree. 34 | It is used to parse XML documents and uses libxml under the hood, which allows 35 | for convenient use of XPath. 36 | */ 37 | @interface MTDHTMLElement : NSObject 38 | 39 | /****************************************** 40 | @name XML Element 41 | ******************************************/ 42 | 43 | /** the tag name of the xml node */ 44 | @property (nonatomic, strong, readonly) NSString *name; 45 | /** all attributes of the node */ 46 | @property (nonatomic, strong, readonly) NSDictionary *attributes; 47 | /** all content sections of the node */ 48 | @property (nonatomic, strong, readonly) NSArray *content; 49 | 50 | /** all child nodes of the current node in the xml-tree */ 51 | @property (nonatomic, readonly) NSArray *childNodes; 52 | /** the content of this node represented as string */ 53 | @property (nonatomic, readonly) NSString *contentString; 54 | /** the content of this node and all childnodes (recursive) as concatenated string */ 55 | @property (nonatomic, readonly) NSString *contentStringByUnifyingSubnodes; 56 | 57 | /****************************************** 58 | @name Lifecycle 59 | ******************************************/ 60 | 61 | /** 62 | Returns an array of all xml nodes matching the given query on the given xml data. 63 | 64 | @param query the xpath query 65 | @param htmlData data representing a xml document 66 | @return array of MTDHTMLElements 67 | @see nodeForXPathQuery:onHTML: 68 | */ 69 | + (NSArray *)nodesForXPathQuery:(NSString *)query onHTML:(NSData *)htmlData; 70 | 71 | /** 72 | Returns an array of all xml nodes matching the given query on the given xml data in the given namespace. 73 | 74 | @param query the xpath query 75 | @param htmlData data representing a xml document 76 | @param namespacePrefix the XML namespace prefix used 77 | @param namespaceURI the URI of the namesapce 78 | @return array of MTDHTMLElements 79 | @see nodeForXPathQuery:onHTML: 80 | */ 81 | + (NSArray *)nodesForXPathQuery:(NSString *)query onHTML:(NSData *)htmlData namespacePrefix:(NSString *)namespacePrefix namespaceURI:(NSString *)namespaceURI; 82 | 83 | /** 84 | Returns the first xml node matching the given query on the given xml data. 85 | 86 | @param query the xpath query 87 | @param htmlData data representing a xml document 88 | @return MTDHTMLElement representing the first node matching 89 | */ 90 | + (instancetype)nodeForXPathQuery:(NSString *)query onHTML:(NSData *)htmlData; 91 | 92 | /** 93 | Returns the first xml node matching the given query on the given xml data in the given namespace. 94 | 95 | @param query the xpath query 96 | @param htmlData data representing a xml document 97 | @param namespacePrefix the XML namespace prefix used 98 | @param namespaceURI the URI of the namesapce 99 | @return MTDHTMLElement representing the first node matching 100 | */ 101 | + (instancetype)nodeForXPathQuery:(NSString *)query onHTML:(NSData *)htmlData namespacePrefix:(NSString *)namespacePrefix namespaceURI:(NSString *)namespaceURI; 102 | 103 | 104 | /****************************************** 105 | @name Queries 106 | ******************************************/ 107 | 108 | /** 109 | Returns the first child node with the given tagname. 110 | 111 | @param name the tag name of the child we want 112 | @return an instance of MTDHTMLElement representing the first found child node, or nil if there was none found 113 | */ 114 | - (MTDHTMLElement *)firstChildNodeWithName:(NSString *)name; 115 | /** 116 | Returns the first child node with the given tagname and attribute value. 117 | 118 | @param name the tag name of the child we want 119 | @param attributeName the name of the attribute to query 120 | @param attributeValue the desired value of the attribute 121 | @return an instance of MTDHTMLElement representing the first found child node, or nil if there was none found 122 | */ 123 | - (MTDHTMLElement *)firstChildNodeWithName:(NSString *)name attribute:(NSString *)attributeName attributeValue:(NSString *)attributeValue; 124 | 125 | /** 126 | Returns an array of child nodes with the given tagname. 127 | 128 | @param name the tag name of the children we want 129 | @return an array of MTDHTMLElements 130 | */ 131 | - (NSArray *)childNodesWithName:(NSString *)name; 132 | 133 | /** 134 | Returns an array of child nodes with the given dot-separated path, e.g. shape.shapePoints.latLng 135 | For all path components except the last one only the first child is traversed, if there are several. 136 | 137 | @param path a dot-separated path of tag names 138 | @return an array of MTDHTMLElements 139 | */ 140 | - (NSArray *)childNodesTraversingFirstChildWithPath:(NSString *)path; 141 | 142 | /** 143 | Returns an array of child nodes with the given dot-separated path, e.g. shape.shapePoints.latLng 144 | For all path components all children are traversed. 145 | 146 | @param path a dot-separated path of tag names 147 | @return an array of MTDHTMLElements 148 | */ 149 | - (NSArray *)childNodesTraversingAllChildrenWithPath:(NSString *)path; 150 | 151 | /** 152 | Returns the attribute value of attribute with the given name. 153 | 154 | @param attributeName the name of the attribute 155 | @return the value of the attribute 156 | */ 157 | - (id)attributeWithName:(NSString *)attributeName; 158 | 159 | @end 160 | -------------------------------------------------------------------------------- /MTDURLPreview/MTDURLPreviewView.m: -------------------------------------------------------------------------------- 1 | #import "MTDURLPreviewView.h" 2 | #import "MTDURLPreview.h" 3 | #import 4 | #import 5 | 6 | 7 | #define kMTDPadding 10.f 8 | #define kMTDImageDimension 60.f 9 | #define kMTDTitleLineBreakMode NSLineBreakByTruncatingTail | NSLineBreakByWordWrapping 10 | 11 | 12 | static UIFont *titleFont = nil; 13 | static UIFont *domainFont = nil; 14 | 15 | 16 | @implementation MTDURLPreviewView 17 | 18 | @synthesize imageView = _imageView; 19 | 20 | //////////////////////////////////////////////////////////////////////// 21 | #pragma mark - Lifecycle 22 | //////////////////////////////////////////////////////////////////////// 23 | 24 | + (void)initialize { 25 | if (self == [MTDURLPreviewView class]) { 26 | titleFont = [UIFont boldSystemFontOfSize:16.f]; 27 | domainFont = [UIFont systemFontOfSize:15.f]; 28 | } 29 | } 30 | 31 | - (id)initWithFrame:(CGRect)frame { 32 | self = [super initWithFrame:frame]; 33 | if (self) { 34 | self.backgroundColor = [UIColor colorWithRed:246.f/255.f green:246.f/255.f blue:246.f/255.f alpha:1.f]; 35 | _textColor = [UIColor colorWithRed:62.f/255.f green:66.f/255.f blue:81.f/255.f alpha:1.f]; 36 | _borderColor = [UIColor colorWithRed:209.f/255.f green:209.f/255.f blue:209.f/255.f alpha:1.f]; 37 | 38 | _titleLabel = [self labelWithFont:titleFont 39 | textColor:_textColor 40 | numberOfLines:0 41 | lineBreakMode:kMTDTitleLineBreakMode]; 42 | [self addSubview:_titleLabel]; 43 | 44 | _domainLabel = [self labelWithFont:domainFont 45 | textColor:_textColor 46 | numberOfLines:1 47 | lineBreakMode:NSLineBreakByTruncatingTail]; 48 | [self addSubview:_domainLabel]; 49 | 50 | 51 | self.layer.borderWidth = 1.f / [UIScreen mainScreen].scale; 52 | self.layer.borderColor = _borderColor.CGColor; 53 | } 54 | 55 | return self; 56 | } 57 | 58 | - (void)dealloc { 59 | // Support for SDWebImage 60 | if ([_imageView respondsToSelector:@selector(sd_cancelCurrentImageLoad)]) { 61 | [_imageView performSelector:@selector(sd_cancelCurrentImageLoad)]; 62 | } 63 | } 64 | 65 | //////////////////////////////////////////////////////////////////////// 66 | #pragma mark - Class Methods 67 | //////////////////////////////////////////////////////////////////////// 68 | 69 | + (CGFloat)neededHeightForTitle:(NSString *)title 70 | domain:(NSString *)domain 71 | content:(NSString *)content 72 | imageVisible:(BOOL)imageVisible 73 | constrainedToWidth:(CGFloat)width { 74 | 75 | CGFloat textX = kMTDPadding; 76 | CGFloat minHeight = 0.f; 77 | 78 | if (imageVisible) { 79 | textX = kMTDPadding + kMTDImageDimension + kMTDPadding; 80 | minHeight = kMTDPadding + 3.f + kMTDImageDimension + kMTDPadding; 81 | } 82 | 83 | CGFloat textWidth = width - textX - kMTDPadding; 84 | CGFloat domainHeight = ceil(domainFont.lineHeight); 85 | CGFloat maxTitleHeight = titleFont.lineHeight * 3.f; 86 | CGSize constraint = CGSizeMake(textWidth, maxTitleHeight); 87 | CGSize sizeTitle = [title sizeWithFont:titleFont constrainedToSize:constraint lineBreakMode:kMTDTitleLineBreakMode]; 88 | 89 | return MAX(minHeight, kMTDPadding + ceil(sizeTitle.height) + domainHeight + kMTDPadding); 90 | } 91 | 92 | + (void)setTitleFont:(UIFont *)titleFont { 93 | titleFont = titleFont; 94 | } 95 | 96 | + (void)setDomainFont:(UIFont *)domainFont { 97 | domainFont = domainFont; 98 | } 99 | 100 | //////////////////////////////////////////////////////////////////////// 101 | #pragma mark - UIView 102 | //////////////////////////////////////////////////////////////////////// 103 | 104 | - (CGSize)sizeThatFits:(CGSize)size { 105 | return [self sizeOfContentsWithSize:size shouldLayout:NO]; 106 | } 107 | 108 | - (void)layoutSubviews { 109 | [super layoutSubviews]; 110 | 111 | [self sizeOfContentsWithSize:self.bounds.size shouldLayout:YES]; 112 | } 113 | 114 | - (void)setBackgroundColor:(UIColor *)backgroundColor { 115 | [super setBackgroundColor:backgroundColor]; 116 | 117 | self.titleLabel.backgroundColor = backgroundColor; 118 | self.domainLabel.backgroundColor = backgroundColor; 119 | self.contentLabel.backgroundColor = backgroundColor; 120 | } 121 | 122 | //////////////////////////////////////////////////////////////////////// 123 | #pragma mark - MTDURLPreviewView+MTDModelObject 124 | //////////////////////////////////////////////////////////////////////// 125 | 126 | + (CGFloat)neededHeightForURLPreview:(MTDURLPreview *)preview constrainedToWidth:(CGFloat)width { 127 | return [self neededHeightForTitle:preview.title 128 | domain:preview.domain 129 | content:preview.content 130 | imageVisible:YES 131 | constrainedToWidth:width]; 132 | } 133 | 134 | - (void)setFromURLPreview:(MTDURLPreview *)preview { 135 | self.titleLabel.text = preview.title; 136 | self.domainLabel.text = preview.domain; 137 | self.contentLabel.text = preview.content; 138 | 139 | UIImage *placeholderImage = [UIImage imageNamed:@"MTDURLPreview.bundle/image-placeholder"]; 140 | self.imageView.image = placeholderImage; 141 | if (preview.imageURL != nil) { 142 | // Support for SDWebImage 143 | if ([self.imageView respondsToSelector:@selector(sd_setImageWithURL:placeholderImage:)]) { 144 | [self.imageView performSelector:@selector(sd_setImageWithURL:placeholderImage:) withObject:preview.imageURL withObject:placeholderImage]; 145 | } 146 | } 147 | 148 | [self sizeToFit]; 149 | [self setNeedsLayout]; 150 | } 151 | 152 | - (void)setTextColor:(UIColor *)textColor { 153 | if (textColor != _textColor) { 154 | _textColor = textColor; 155 | self.titleLabel.textColor = textColor; 156 | self.domainLabel.textColor = textColor; 157 | self.contentLabel.textColor = textColor; 158 | } 159 | } 160 | 161 | - (void)setBorderColor:(UIColor *)borderColor { 162 | if (borderColor != _borderColor) { 163 | _borderColor = borderColor; 164 | self.layer.borderColor = _borderColor.CGColor; 165 | } 166 | } 167 | 168 | //////////////////////////////////////////////////////////////////////// 169 | #pragma mark - Private 170 | //////////////////////////////////////////////////////////////////////// 171 | 172 | - (UIImageView *)imageView { 173 | if (_imageView == nil) { 174 | _imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"MTDURLPreview.bundle/image-placeholder"]]; 175 | _imageView.contentMode = UIViewContentModeScaleAspectFill; 176 | _imageView.clipsToBounds = YES; 177 | [self addSubview:_imageView]; 178 | } 179 | 180 | return _imageView; 181 | } 182 | 183 | - (id)labelWithFont:(UIFont *)font 184 | textColor:(UIColor *)textColor 185 | numberOfLines:(NSUInteger)numberOfLines 186 | lineBreakMode:(NSLineBreakMode)lineBreakMode { 187 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero]; 188 | 189 | label.opaque = YES; 190 | label.font = font; 191 | label.textColor = textColor; 192 | label.highlightedTextColor = textColor; 193 | label.lineBreakMode = lineBreakMode; 194 | label.numberOfLines = numberOfLines; 195 | 196 | return label; 197 | } 198 | 199 | - (CGSize)sizeOfContentsWithSize:(CGSize)size 200 | shouldLayout:(BOOL)shouldLayout { 201 | CGFloat textX = kMTDPadding; 202 | CGFloat minHeight = 0.f; 203 | 204 | if (_imageView != nil) { 205 | textX = kMTDPadding + kMTDImageDimension + kMTDPadding; 206 | minHeight = kMTDPadding + 3.f + kMTDImageDimension + kMTDPadding; 207 | } 208 | 209 | CGFloat textWidth = size.width - textX - kMTDPadding; 210 | CGFloat domainHeight = ceil(self.domainLabel.font.lineHeight); 211 | CGFloat maxTitleHeight = self.titleLabel.font.lineHeight * 3.f; 212 | CGSize constraint = CGSizeMake(textWidth, maxTitleHeight); 213 | CGSize sizeTitle = [self.titleLabel.text sizeWithFont:self.titleLabel.font constrainedToSize:constraint lineBreakMode:self.titleLabel.lineBreakMode]; 214 | sizeTitle = CGSizeMake(ceil(sizeTitle.width), ceil(sizeTitle.height)); 215 | 216 | if (shouldLayout) { 217 | _imageView.frame = CGRectMake(kMTDPadding, kMTDPadding + 3.f, kMTDImageDimension, kMTDImageDimension); 218 | self.titleLabel.frame = CGRectMake(textX, kMTDPadding, textWidth, sizeTitle.height); 219 | self.domainLabel.frame = CGRectMake(textX, CGRectGetMaxY(self.titleLabel.frame), textWidth, domainHeight); 220 | } 221 | 222 | return CGSizeMake(size.width, MAX(minHeight, kMTDPadding + sizeTitle.height + domainHeight + kMTDPadding)); 223 | } 224 | 225 | @end 226 | -------------------------------------------------------------------------------- /MTDURLPreview/Parser/MTDHTMLElement.m: -------------------------------------------------------------------------------- 1 | #import "MTDHTMLElement.h" 2 | #import 3 | #import 4 | #import 5 | #import 6 | 7 | @interface MTDHTMLElement () { 8 | NSMutableDictionary *_attributes; 9 | NSMutableArray *_content; 10 | NSArray *_childNodes; 11 | } 12 | 13 | @property (nonatomic, strong, readwrite) NSString *name; // re-defined as read/write 14 | 15 | @end 16 | 17 | @implementation MTDHTMLElement 18 | 19 | //////////////////////////////////////////////////////////////////////// 20 | #pragma mark - Lifecycle 21 | //////////////////////////////////////////////////////////////////////// 22 | 23 | + (NSArray *)nodesForXPathQuery:(NSString *)query onHTML:(NSData *)htmlData { 24 | return [self nodesForXPathQuery:query onHTML:htmlData namespacePrefix:nil namespaceURI:nil]; 25 | } 26 | 27 | + (NSArray *)nodesForXPathQuery:(NSString *)query onHTML:(NSData *)htmlData namespacePrefix:(NSString *)namespacePrefix namespaceURI:(NSString *)namespaceURI { 28 | xmlDocPtr doc = htmlReadMemory([htmlData bytes], (int)[htmlData length], "", NULL, HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR); 29 | if (doc == NULL) { 30 | return nil; 31 | } 32 | 33 | NSArray *result = [MTDHTMLElement mtd_nodesForXPathQuery:query namespacePrefix:namespacePrefix namespaceURI:namespaceURI libXMLDoc:doc]; 34 | xmlFreeDoc(doc); 35 | 36 | return result; 37 | } 38 | 39 | + (instancetype)nodeForXPathQuery:(NSString *)query onHTML:(NSData *)htmlData { 40 | NSArray *nodes = [self nodesForXPathQuery:query onHTML:htmlData]; 41 | 42 | return nodes.count > 0 ? nodes[0] : nil; 43 | } 44 | 45 | + (instancetype)nodeForXPathQuery:(NSString *)query onHTML:(NSData *)htmlData namespacePrefix:(NSString *)namespacePrefix namespaceURI:(NSString *)namespaceURI { 46 | NSArray *nodes = [self nodesForXPathQuery:query onHTML:htmlData namespacePrefix:namespacePrefix namespaceURI:namespaceURI]; 47 | 48 | return nodes.count > 0 ? nodes[0] : nil; 49 | } 50 | 51 | //////////////////////////////////////////////////////////////////////// 52 | #pragma mark - NSObject 53 | //////////////////////////////////////////////////////////////////////// 54 | 55 | - (NSString *)description { 56 | NSMutableString *description = [NSMutableString string]; 57 | [description appendFormat:@"<%@", self.name]; 58 | 59 | for (NSString *attributeName in self.attributes) { 60 | NSString *attributeValue = self.attributes[attributeName]; 61 | [description appendFormat:@" %@=\"%@\"", attributeName, attributeValue]; 62 | } 63 | 64 | if (self.content.count > 0) { 65 | [description appendString:@">"]; 66 | 67 | for (id object in self.content) { 68 | [description appendString:[object description]]; 69 | } 70 | 71 | [description appendFormat:@"", self.name]; 72 | } else { 73 | [description appendString:@"/>"]; 74 | } 75 | 76 | return description; 77 | } 78 | 79 | //////////////////////////////////////////////////////////////////////// 80 | #pragma mark - MTXPathResultNode 81 | //////////////////////////////////////////////////////////////////////// 82 | 83 | - (NSArray *)childNodes { 84 | if (_childNodes == nil) { 85 | _childNodes = [self.content filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, __unused NSDictionary *bindings) { 86 | return [evaluatedObject isKindOfClass:[MTDHTMLElement class]]; 87 | }]]; 88 | } 89 | 90 | return _childNodes; 91 | } 92 | 93 | - (NSString *)contentString { 94 | for (NSObject *object in self.content) { 95 | if ([object isKindOfClass:[NSString class]]) { 96 | return (NSString *)object; 97 | } 98 | } 99 | 100 | return nil; 101 | } 102 | 103 | - (NSString *)contentStringByUnifyingSubnodes { 104 | NSMutableString *result = nil; 105 | 106 | for (NSObject *object in self.content) { 107 | if ([object isKindOfClass:[NSString class]]) { 108 | if (!result) { 109 | result = [NSMutableString stringWithString:(NSString *)object]; 110 | } else { 111 | [result appendString:(NSString *)object]; 112 | } 113 | } else { 114 | NSString *subnodeResult = [(MTDHTMLElement *)object contentStringByUnifyingSubnodes]; 115 | 116 | if (subnodeResult) { 117 | if (!result) { 118 | result = [NSMutableString stringWithString:subnodeResult]; 119 | } else { 120 | [result appendString:subnodeResult]; 121 | } 122 | } 123 | } 124 | } 125 | 126 | return result; 127 | } 128 | 129 | - (MTDHTMLElement *)firstChildNodeWithName:(NSString *)name { 130 | __block MTDHTMLElement *foundNode = nil; 131 | 132 | [self.childNodes enumerateObjectsUsingBlock:^(MTDHTMLElement *element, __unused NSUInteger idx, BOOL *stop) { 133 | if ([element.name isEqualToString:name]) { 134 | foundNode = element; 135 | *stop = YES; 136 | } 137 | }]; 138 | 139 | return foundNode; 140 | } 141 | 142 | - (MTDHTMLElement *)firstChildNodeWithName:(NSString *)name attribute:(NSString *)attributeName attributeValue:(NSString *)attributeValue { 143 | __block MTDHTMLElement *foundNode = nil; 144 | 145 | [self.childNodes enumerateObjectsUsingBlock:^(MTDHTMLElement *element, __unused NSUInteger idx, BOOL *stop) { 146 | if ([element.name isEqualToString:name] && [[element attributeWithName:attributeName] isEqualToString:attributeValue]) { 147 | foundNode = element; 148 | *stop = YES; 149 | } 150 | }]; 151 | 152 | return foundNode; 153 | } 154 | 155 | - (NSArray *)childNodesWithName:(NSString *)name { 156 | return [self.childNodes filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(MTDHTMLElement *element, __unused NSDictionary *bindings) { 157 | return [element.name isEqualToString:name]; 158 | }]]; 159 | } 160 | 161 | - (NSArray *)childNodesTraversingFirstChildWithPath:(NSString *)path { 162 | NSArray *parts = [path componentsSeparatedByString:@"."]; 163 | __block MTDHTMLElement *element = self; 164 | __block NSArray *childNodes = nil; 165 | 166 | [parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) { 167 | // intermediate path component 168 | if (idx < parts.count - 1) { 169 | element = [element firstChildNodeWithName:part]; 170 | } 171 | 172 | // last path component 173 | else { 174 | childNodes = [element childNodesWithName:part]; 175 | } 176 | }]; 177 | 178 | return childNodes; 179 | } 180 | 181 | - (NSArray *)childNodesTraversingAllChildrenWithPath:(NSString *)path { 182 | NSArray *parts = [path componentsSeparatedByString:@"."]; 183 | __block NSArray *childrenToTraverse = [NSArray arrayWithObject:self]; 184 | __block NSArray *childNodes = nil; 185 | 186 | [parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) { 187 | NSMutableArray *childrenOfThisStep = [NSMutableArray array]; 188 | 189 | for (MTDHTMLElement *element in childrenToTraverse) { 190 | [childrenOfThisStep addObjectsFromArray:[element childNodesWithName:part]]; 191 | } 192 | 193 | // intermediate path component 194 | if (idx < parts.count - 1) { 195 | childrenToTraverse = childrenOfThisStep; 196 | } 197 | 198 | // last path component 199 | else { 200 | childNodes = childrenOfThisStep; 201 | } 202 | }]; 203 | 204 | return childNodes; 205 | } 206 | 207 | - (id)attributeWithName:(NSString *)attributeName { 208 | return [self.attributes objectForKey:attributeName]; 209 | } 210 | 211 | //////////////////////////////////////////////////////////////////////// 212 | #pragma mark - Private 213 | //////////////////////////////////////////////////////////////////////// 214 | 215 | + (MTDHTMLElement *)mtd_nodeFromLibXMLNode:(xmlNodePtr)libXMLNode parentNode:(MTDHTMLElement *)parentNode { 216 | MTDHTMLElement *node = [MTDHTMLElement new]; 217 | 218 | if (libXMLNode->name) { 219 | node.name = @((const char *)libXMLNode->name); 220 | } 221 | 222 | if (libXMLNode->content && libXMLNode->type != XML_DOCUMENT_TYPE_NODE) { 223 | NSString *contentString = [[NSString alloc] initWithCString:(const char *)libXMLNode->content encoding:NSUTF8StringEncoding]; 224 | if (contentString == nil) { 225 | contentString = [[NSString alloc] initWithCString:(const char *)libXMLNode->content encoding:NSASCIIStringEncoding]; 226 | } 227 | 228 | if (contentString != nil && parentNode && (libXMLNode->type == XML_CDATA_SECTION_NODE || libXMLNode->type == XML_TEXT_NODE)) { 229 | if (libXMLNode->type == XML_TEXT_NODE) { 230 | contentString = [contentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 231 | } 232 | 233 | if (!parentNode.content) { 234 | parentNode->_content = [NSMutableArray arrayWithObject:contentString]; 235 | } else { 236 | [parentNode->_content addObject:contentString]; 237 | } 238 | 239 | return nil; 240 | } 241 | } 242 | 243 | xmlAttr *attribute = libXMLNode->properties; 244 | 245 | if (attribute) { 246 | while (attribute) { 247 | NSString *attributeName = nil; 248 | NSString *attributeValue = nil; 249 | 250 | if (attribute->name && attribute->children && attribute->children->type == XML_TEXT_NODE && attribute->children->content) { 251 | attributeName = @((const char *)attribute->name); 252 | attributeValue = @((const char *)attribute->children->content); 253 | 254 | if (attributeName && attributeValue) { 255 | if (!node.attributes) { 256 | node->_attributes = [NSMutableDictionary dictionaryWithObject:attributeValue forKey:attributeName]; 257 | } else { 258 | node->_attributes[attributeName] = attributeValue; 259 | } 260 | } 261 | } 262 | 263 | attribute = attribute->next; 264 | } 265 | } 266 | 267 | xmlNodePtr childLibXMLNode = libXMLNode->children; 268 | 269 | if (childLibXMLNode) { 270 | while (childLibXMLNode) { 271 | MTDHTMLElement *childNode = [MTDHTMLElement mtd_nodeFromLibXMLNode:childLibXMLNode parentNode:node]; 272 | 273 | if (childNode) { 274 | if (!node.content) { 275 | node->_content = [NSMutableArray arrayWithObject:childNode]; 276 | } else { 277 | [node->_content addObject:childNode]; 278 | } 279 | } 280 | 281 | childLibXMLNode = childLibXMLNode->next; 282 | } 283 | } 284 | 285 | return node; 286 | } 287 | 288 | + (NSArray *)mtd_nodesForXPathQuery:(NSString *)query namespacePrefix:(NSString *)namespacePrefix namespaceURI:(NSString *)namespaceURI libXMLDoc:(xmlDocPtr)doc { 289 | xmlXPathContextPtr xpathCtx; 290 | xmlXPathObjectPtr xpathObj; 291 | 292 | xpathCtx = xmlXPathNewContext(doc); 293 | 294 | if (xpathCtx == NULL) { 295 | return nil; 296 | } 297 | 298 | if (namespacePrefix != nil && namespaceURI != nil) { 299 | xmlXPathRegisterNs(xpathCtx, 300 | (xmlChar *)[namespacePrefix cStringUsingEncoding:NSUTF8StringEncoding], 301 | (xmlChar *)[namespaceURI cStringUsingEncoding:NSUTF8StringEncoding]); 302 | } 303 | 304 | xpathObj = xmlXPathEvalExpression((xmlChar *)[query cStringUsingEncoding:NSUTF8StringEncoding], xpathCtx); 305 | 306 | if(xpathObj == NULL) { 307 | xmlXPathFreeContext(xpathCtx); 308 | return nil; 309 | } 310 | 311 | xmlNodeSetPtr nodes = xpathObj->nodesetval; 312 | 313 | if (!nodes) { 314 | xmlXPathFreeObject(xpathObj); 315 | xmlXPathFreeContext(xpathCtx); 316 | return nil; 317 | } 318 | 319 | NSMutableArray *resultNodes = [NSMutableArray array]; 320 | 321 | for (NSInteger i = 0; i < nodes->nodeNr; i++) { 322 | MTDHTMLElement *node = [MTDHTMLElement mtd_nodeFromLibXMLNode:nodes->nodeTab[i] parentNode:nil]; 323 | 324 | if (node) { 325 | [resultNodes addObject:node]; 326 | } 327 | } 328 | 329 | xmlXPathFreeObject(xpathObj); 330 | xmlXPathFreeContext(xpathCtx); 331 | 332 | return resultNodes; 333 | } 334 | 335 | @end 336 | 337 | -------------------------------------------------------------------------------- /MTDURLPreview.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9B7C05F9179D3301009D8A0F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B7C05F8179D3301009D8A0F /* Foundation.framework */; }; 11 | 9B7C05FE179D3301009D8A0F /* MTDURLPreview.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 9B7C05FD179D3301009D8A0F /* MTDURLPreview.h */; }; 12 | 9B7C0600179D3301009D8A0F /* MTDURLPreview.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B7C05FF179D3301009D8A0F /* MTDURLPreview.m */; }; 13 | 9B7C060B179D334D009D8A0F /* MTDURLPreviewParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B7C0608179D334D009D8A0F /* MTDURLPreviewParser.m */; }; 14 | 9B7C060C179D334D009D8A0F /* MTDURLPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B7C060A179D334D009D8A0F /* MTDURLPreviewView.m */; }; 15 | 9B7C0610179D3380009D8A0F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B7C060F179D3380009D8A0F /* UIKit.framework */; }; 16 | 9B7C0614179D33C1009D8A0F /* MTDHTMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B7C0613179D33C1009D8A0F /* MTDHTMLElement.m */; }; 17 | 9BF7A70E17A90FC800819735 /* MTDURLPreviewCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BF7A70D17A90FC700819735 /* MTDURLPreviewCache.m */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXCopyFilesBuildPhase section */ 21 | 9B7C05F3179D3301009D8A0F /* CopyFiles */ = { 22 | isa = PBXCopyFilesBuildPhase; 23 | buildActionMask = 2147483647; 24 | dstPath = "include/${PRODUCT_NAME}"; 25 | dstSubfolderSpec = 16; 26 | files = ( 27 | 9B7C05FE179D3301009D8A0F /* MTDURLPreview.h in CopyFiles */, 28 | ); 29 | runOnlyForDeploymentPostprocessing = 0; 30 | }; 31 | /* End PBXCopyFilesBuildPhase section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 9B7C05F5179D3301009D8A0F /* libMTDURLPreview.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libMTDURLPreview.a; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 9B7C05F8179D3301009D8A0F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 36 | 9B7C05FD179D3301009D8A0F /* MTDURLPreview.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTDURLPreview.h; sourceTree = ""; }; 37 | 9B7C05FF179D3301009D8A0F /* MTDURLPreview.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTDURLPreview.m; sourceTree = ""; }; 38 | 9B7C0606179D3348009D8A0F /* Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = SOURCE_ROOT; }; 39 | 9B7C0607179D334D009D8A0F /* MTDURLPreviewParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDURLPreviewParser.h; sourceTree = ""; }; 40 | 9B7C0608179D334D009D8A0F /* MTDURLPreviewParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDURLPreviewParser.m; sourceTree = ""; }; 41 | 9B7C0609179D334D009D8A0F /* MTDURLPreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDURLPreviewView.h; sourceTree = ""; }; 42 | 9B7C060A179D334D009D8A0F /* MTDURLPreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDURLPreviewView.m; sourceTree = ""; }; 43 | 9B7C060D179D3375009D8A0F /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; }; 44 | 9B7C060F179D3380009D8A0F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 45 | 9B7C0612179D33C1009D8A0F /* MTDHTMLElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDHTMLElement.h; sourceTree = ""; }; 46 | 9B7C0613179D33C1009D8A0F /* MTDHTMLElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDHTMLElement.m; sourceTree = ""; }; 47 | 9B7C0616179D37D7009D8A0F /* MTDURLPreview.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = MTDURLPreview.bundle; sourceTree = ""; }; 48 | 9BF7A70C17A90FC700819735 /* MTDURLPreviewCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDURLPreviewCache.h; sourceTree = ""; }; 49 | 9BF7A70D17A90FC700819735 /* MTDURLPreviewCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDURLPreviewCache.m; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 9B7C05F2179D3301009D8A0F /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | 9B7C0610179D3380009D8A0F /* UIKit.framework in Frameworks */, 58 | 9B7C05F9179D3301009D8A0F /* Foundation.framework in Frameworks */, 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | 9B7C05EC179D3301009D8A0F = { 66 | isa = PBXGroup; 67 | children = ( 68 | 9B7C05FA179D3301009D8A0F /* MTDURLPreview */, 69 | 9B7C0615179D37C2009D8A0F /* Resources */, 70 | 9B7C05F7179D3301009D8A0F /* Frameworks */, 71 | 9B7C05F6179D3301009D8A0F /* Products */, 72 | ); 73 | sourceTree = ""; 74 | }; 75 | 9B7C05F6179D3301009D8A0F /* Products */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 9B7C05F5179D3301009D8A0F /* libMTDURLPreview.a */, 79 | ); 80 | name = Products; 81 | sourceTree = ""; 82 | }; 83 | 9B7C05F7179D3301009D8A0F /* Frameworks */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 9B7C060D179D3375009D8A0F /* libxml2.dylib */, 87 | 9B7C05F8179D3301009D8A0F /* Foundation.framework */, 88 | 9B7C060F179D3380009D8A0F /* UIKit.framework */, 89 | ); 90 | name = Frameworks; 91 | sourceTree = ""; 92 | }; 93 | 9B7C05FA179D3301009D8A0F /* MTDURLPreview */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 9B7C0611179D33C1009D8A0F /* HTML Parser */, 97 | 9B7C05FD179D3301009D8A0F /* MTDURLPreview.h */, 98 | 9B7C05FF179D3301009D8A0F /* MTDURLPreview.m */, 99 | 9B7C0607179D334D009D8A0F /* MTDURLPreviewParser.h */, 100 | 9B7C0608179D334D009D8A0F /* MTDURLPreviewParser.m */, 101 | 9B7C0609179D334D009D8A0F /* MTDURLPreviewView.h */, 102 | 9B7C060A179D334D009D8A0F /* MTDURLPreviewView.m */, 103 | 9BF7A70C17A90FC700819735 /* MTDURLPreviewCache.h */, 104 | 9BF7A70D17A90FC700819735 /* MTDURLPreviewCache.m */, 105 | 9B7C05FB179D3301009D8A0F /* Supporting Files */, 106 | ); 107 | path = MTDURLPreview; 108 | sourceTree = ""; 109 | }; 110 | 9B7C05FB179D3301009D8A0F /* Supporting Files */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 9B7C0606179D3348009D8A0F /* Prefix.pch */, 114 | ); 115 | name = "Supporting Files"; 116 | sourceTree = ""; 117 | }; 118 | 9B7C0611179D33C1009D8A0F /* HTML Parser */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 9B7C0612179D33C1009D8A0F /* MTDHTMLElement.h */, 122 | 9B7C0613179D33C1009D8A0F /* MTDHTMLElement.m */, 123 | ); 124 | name = "HTML Parser"; 125 | path = Parser; 126 | sourceTree = ""; 127 | }; 128 | 9B7C0615179D37C2009D8A0F /* Resources */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 9B7C0616179D37D7009D8A0F /* MTDURLPreview.bundle */, 132 | ); 133 | path = Resources; 134 | sourceTree = ""; 135 | }; 136 | /* End PBXGroup section */ 137 | 138 | /* Begin PBXNativeTarget section */ 139 | 9B7C05F4179D3301009D8A0F /* MTDURLPreview */ = { 140 | isa = PBXNativeTarget; 141 | buildConfigurationList = 9B7C0603179D3301009D8A0F /* Build configuration list for PBXNativeTarget "MTDURLPreview" */; 142 | buildPhases = ( 143 | 9B7C05F1179D3301009D8A0F /* Sources */, 144 | 9B7C05F2179D3301009D8A0F /* Frameworks */, 145 | 9B7C05F3179D3301009D8A0F /* CopyFiles */, 146 | ); 147 | buildRules = ( 148 | ); 149 | dependencies = ( 150 | ); 151 | name = MTDURLPreview; 152 | productName = MTDURLPreview; 153 | productReference = 9B7C05F5179D3301009D8A0F /* libMTDURLPreview.a */; 154 | productType = "com.apple.product-type.library.static"; 155 | }; 156 | /* End PBXNativeTarget section */ 157 | 158 | /* Begin PBXProject section */ 159 | 9B7C05ED179D3301009D8A0F /* Project object */ = { 160 | isa = PBXProject; 161 | attributes = { 162 | LastUpgradeCheck = 0460; 163 | ORGANIZATIONNAME = "@myell0w"; 164 | }; 165 | buildConfigurationList = 9B7C05F0179D3301009D8A0F /* Build configuration list for PBXProject "MTDURLPreview" */; 166 | compatibilityVersion = "Xcode 3.2"; 167 | developmentRegion = English; 168 | hasScannedForEncodings = 0; 169 | knownRegions = ( 170 | en, 171 | ); 172 | mainGroup = 9B7C05EC179D3301009D8A0F; 173 | productRefGroup = 9B7C05F6179D3301009D8A0F /* Products */; 174 | projectDirPath = ""; 175 | projectRoot = ""; 176 | targets = ( 177 | 9B7C05F4179D3301009D8A0F /* MTDURLPreview */, 178 | ); 179 | }; 180 | /* End PBXProject section */ 181 | 182 | /* Begin PBXSourcesBuildPhase section */ 183 | 9B7C05F1179D3301009D8A0F /* Sources */ = { 184 | isa = PBXSourcesBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | 9B7C0600179D3301009D8A0F /* MTDURLPreview.m in Sources */, 188 | 9B7C060B179D334D009D8A0F /* MTDURLPreviewParser.m in Sources */, 189 | 9B7C060C179D334D009D8A0F /* MTDURLPreviewView.m in Sources */, 190 | 9B7C0614179D33C1009D8A0F /* MTDHTMLElement.m in Sources */, 191 | 9BF7A70E17A90FC800819735 /* MTDURLPreviewCache.m in Sources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXSourcesBuildPhase section */ 196 | 197 | /* Begin XCBuildConfiguration section */ 198 | 9B7C0601179D3301009D8A0F /* Debug */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | ALWAYS_SEARCH_USER_PATHS = NO; 202 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 203 | CLANG_CXX_LIBRARY = "libc++"; 204 | CLANG_ENABLE_OBJC_ARC = YES; 205 | CLANG_WARN_CONSTANT_CONVERSION = YES; 206 | CLANG_WARN_EMPTY_BODY = YES; 207 | CLANG_WARN_ENUM_CONVERSION = YES; 208 | CLANG_WARN_INT_CONVERSION = YES; 209 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 210 | COPY_PHASE_STRIP = NO; 211 | GCC_C_LANGUAGE_STANDARD = gnu99; 212 | GCC_DYNAMIC_NO_PIC = NO; 213 | GCC_OPTIMIZATION_LEVEL = 0; 214 | GCC_PREPROCESSOR_DEFINITIONS = ( 215 | "DEBUG=1", 216 | "$(inherited)", 217 | ); 218 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 219 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 220 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 221 | GCC_WARN_UNUSED_VARIABLE = YES; 222 | HEADER_SEARCH_PATHS = ( 223 | "$(SDKROOT)/usr/include/libxml2", 224 | "$(SDK_ROOT)/usr/include/libxml2", 225 | ); 226 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 227 | ONLY_ACTIVE_ARCH = YES; 228 | SDKROOT = iphoneos; 229 | }; 230 | name = Debug; 231 | }; 232 | 9B7C0602179D3301009D8A0F /* Release */ = { 233 | isa = XCBuildConfiguration; 234 | buildSettings = { 235 | ALWAYS_SEARCH_USER_PATHS = NO; 236 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 237 | CLANG_CXX_LIBRARY = "libc++"; 238 | CLANG_ENABLE_OBJC_ARC = YES; 239 | CLANG_WARN_CONSTANT_CONVERSION = YES; 240 | CLANG_WARN_EMPTY_BODY = YES; 241 | CLANG_WARN_ENUM_CONVERSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | COPY_PHASE_STRIP = YES; 245 | GCC_C_LANGUAGE_STANDARD = gnu99; 246 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 247 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 248 | GCC_WARN_UNUSED_VARIABLE = YES; 249 | HEADER_SEARCH_PATHS = ( 250 | "$(SDKROOT)/usr/include/libxml2", 251 | "$(SDK_ROOT)/usr/include/libxml2", 252 | ); 253 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 254 | SDKROOT = iphoneos; 255 | VALIDATE_PRODUCT = YES; 256 | }; 257 | name = Release; 258 | }; 259 | 9B7C0604179D3301009D8A0F /* Debug */ = { 260 | isa = XCBuildConfiguration; 261 | buildSettings = { 262 | DSTROOT = /tmp/MTDURLPreview.dst; 263 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 264 | GCC_PREFIX_HEADER = Prefix.pch; 265 | OTHER_LDFLAGS = "-ObjC"; 266 | PRODUCT_NAME = "$(TARGET_NAME)"; 267 | SKIP_INSTALL = YES; 268 | }; 269 | name = Debug; 270 | }; 271 | 9B7C0605179D3301009D8A0F /* Release */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | DSTROOT = /tmp/MTDURLPreview.dst; 275 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 276 | GCC_PREFIX_HEADER = Prefix.pch; 277 | OTHER_LDFLAGS = "-ObjC"; 278 | PRODUCT_NAME = "$(TARGET_NAME)"; 279 | SKIP_INSTALL = YES; 280 | }; 281 | name = Release; 282 | }; 283 | /* End XCBuildConfiguration section */ 284 | 285 | /* Begin XCConfigurationList section */ 286 | 9B7C05F0179D3301009D8A0F /* Build configuration list for PBXProject "MTDURLPreview" */ = { 287 | isa = XCConfigurationList; 288 | buildConfigurations = ( 289 | 9B7C0601179D3301009D8A0F /* Debug */, 290 | 9B7C0602179D3301009D8A0F /* Release */, 291 | ); 292 | defaultConfigurationIsVisible = 0; 293 | defaultConfigurationName = Release; 294 | }; 295 | 9B7C0603179D3301009D8A0F /* Build configuration list for PBXNativeTarget "MTDURLPreview" */ = { 296 | isa = XCConfigurationList; 297 | buildConfigurations = ( 298 | 9B7C0604179D3301009D8A0F /* Debug */, 299 | 9B7C0605179D3301009D8A0F /* Release */, 300 | ); 301 | defaultConfigurationIsVisible = 0; 302 | defaultConfigurationName = Release; 303 | }; 304 | /* End XCConfigurationList section */ 305 | }; 306 | rootObject = 9B7C05ED179D3301009D8A0F /* Project object */; 307 | } 308 | --------------------------------------------------------------------------------