├── .gitignore ├── Classes ├── Categories │ ├── NSString+Tags.h │ ├── NSDate+TimeAgo.h │ ├── NSDictionary+Parameters.h │ ├── NSString+URLEncoding.h │ ├── NSString+RemoveSuffix.h │ ├── NSURL+Parameters.h │ ├── NSString+RemoveSuffix.m │ ├── NSString+URLEncoding.m │ ├── NSString+Tags.m │ ├── NSDictionary+Parameters.m │ ├── NSObject+PerformSelector.h │ ├── NSDate+TimeAgo.m │ ├── NSString+Entities.h │ ├── NSURL+Parameters.m │ ├── NSString+Entities.m │ └── NSObject+PerformSelector.m ├── HNKit │ ├── HNAnonymousSession.h │ ├── HNAPIRequestParser.h │ ├── HNNetworkActivityController.h │ ├── HNAPIRequest.h │ ├── HNSessionController.h │ ├── HNObjectCache.h │ ├── HNContainer.h │ ├── HNShared.h │ ├── HNAPISearch.h │ ├── HNUser.h │ ├── HNKit.h │ ├── HNAnonymousSession.m │ ├── HNAPISubmission.h │ ├── HNSessionAuthenticator.h │ ├── HNSubmission.m │ ├── HNObjectBodyRenderer.h │ ├── HNSubmission.h │ ├── HNEntryList.h │ ├── HNSession.h │ ├── HNNetworkActivityController.m │ ├── HNEntry.h │ ├── HNUser.m │ ├── HNContainer.m │ ├── HNEntryList.m │ ├── HNObject.h │ ├── HNSession.m │ ├── HNAPIRequest.m │ ├── HNEntry.m │ ├── HNSessionController.m │ ├── HNAPISearch.m │ ├── HNSessionAuthenticator.m │ ├── HNObjectCache.m │ ├── HNAPISubmission.m │ ├── HNObject.m │ ├── HNObjectBodyRenderer.m │ └── HNAPIRequestParser.m └── XML │ ├── XMLDocument.h │ ├── XMLElement.h │ ├── XMLDocument.m │ └── XMLElement.m ├── README.md ├── LICENSE └── HNKit.xcodeproj └── project.pbxproj /.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 | -------------------------------------------------------------------------------- /Classes/Categories/NSString+Tags.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Tags.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/13/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString (Tags) 12 | 13 | - (NSString *)stringByRemovingHTMLTags; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Classes/Categories/NSDate+TimeAgo.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDate+TimeAgo.m 3 | // HNKit 4 | // 5 | // By Rudy Jahchan 6 | // https://gist.github.com/rudyjahchan/1660134 7 | // No copyright - released into public domain 8 | // 9 | 10 | #import 11 | 12 | @interface NSDate (TimeAgo) 13 | 14 | - (NSString *) timeAgoInWords; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Classes/Categories/NSDictionary+Parameters.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+Parameters.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/13/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSDictionary (Parameters) 12 | 13 | - (NSString *)queryString; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Classes/Categories/NSString+URLEncoding.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+URLEncoding.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/10/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString (URLEncoding) 12 | 13 | - (NSString *)stringByURLEncodingString; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Classes/Categories/NSString+RemoveSuffix.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+RemoveSuffix.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/5/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString (RemoveSuffix) 12 | 13 | - (NSString *)stringByRemovingSuffix:(NSString *)suffix; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Classes/Categories/NSURL+Parameters.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+Parameters.h 3 | // Telekinesis 4 | // 5 | // Created by Nicholas Jitkoff on 6/14/07. 6 | // Copyright 2007 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSURL (Parameters) 12 | - (NSArray *)parameterArray; 13 | - (NSDictionary *)parameterDictionary; 14 | @end 15 | -------------------------------------------------------------------------------- /Classes/HNKit/HNAnonymousSession.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNAnonymousSession.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 4/13/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNSession.h" 11 | 12 | @interface HNAnonymousSession : HNSession 13 | 14 | @end 15 | 16 | @interface HNSession (HNAnonymousSession) 17 | 18 | @property (nonatomic, readonly) BOOL isAnonymous; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Classes/HNKit/HNAPIRequestParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNAPIRequestParser.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/12/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | 11 | @interface HNAPIRequestParser : NSObject 12 | 13 | - (NSDictionary *)parseWithString:(NSString *)string; 14 | - (BOOL)stringIsProcrastinationError:(NSString *)string; 15 | - (BOOL)stringIsExpiredError:(NSString *)string; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Classes/Categories/NSString+RemoveSuffix.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+RemoveSuffix.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/5/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "NSString+RemoveSuffix.h" 10 | 11 | @implementation NSString (RemoveSuffix) 12 | 13 | - (NSString *)stringByRemovingSuffix:(NSString *)s { 14 | if([self hasSuffix:s]) 15 | self = [self substringToIndex:[self length] - [s length]]; 16 | return self; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## HNKit 2 | 3 | HNKit is a Cocoa framework for interacting with [Hacker News](http://news.ycombinator.com), extracted from the [news:yc](http://newsyc.me) project. It supports both iOS and OS X. 4 | 5 | There isn't any documentation yet, but you can look at the [news:yc code](http://github.com/Xuzz/newsyc) for examples or just read the headers. Feel free to contact me if you have any questions. 6 | 7 | ### Apps using HNKit 8 | 9 | - [news:yc](http://newsyc.me/) 10 | - *Maybe yours next? Submit a pull request so I can add it!* 11 | 12 | ### License 13 | 14 | As with news:yc, HNKit is BSD licensed. 15 | -------------------------------------------------------------------------------- /Classes/Categories/NSString+URLEncoding.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+URLEncoding.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/10/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "NSString+URLEncoding.h" 12 | 13 | @implementation NSString (URLEncoding) 14 | 15 | - (NSString *)stringByURLEncodingString { 16 | CFStringRef encoded = CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef) self, NULL, (CFStringRef) @"!*'\"();:@&=+$,/?%#[]% ", kCFStringEncodingUTF8); 17 | return [(NSString *) encoded autorelease]; 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Classes/HNKit/HNNetworkActivityController.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNNetworkActivityController.h 3 | // HNKit 4 | // 5 | // Created by Grant Paul on 3/3/13. 6 | // Copyright (c) 2013 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface HNNetworkActivityController : NSObject 12 | 13 | + (void (^)())newtorkActivityBeganBlock; 14 | + (void)setNetworkActivityBeganBlock:(void (^)())block; 15 | 16 | + (void (^)())newtorkActivityEndedBlock; 17 | + (void)setNetworkActivityEndedBlock:(void (^)())block; 18 | 19 | + (void)networkActivityBegan; 20 | + (void)networkActivityEnded; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Classes/HNKit/HNAPIRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNAPIRequest.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/4/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | @interface HNAPIRequest : NSObject { 10 | HNSession *session; 11 | 12 | id target; 13 | SEL action; 14 | NSMutableData *received; 15 | NSURLConnection *connection; 16 | NSString *path; 17 | } 18 | 19 | - (HNAPIRequest *)initWithSession:(HNSession *)session_ target:(id)target_ action:(SEL)action_; 20 | - (void)performRequestWithPath:(NSString *)path parameters:(NSDictionary *)parameters; 21 | - (void)cancelRequest; 22 | - (BOOL)isLoading; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /Classes/HNKit/HNSessionController.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNSessionController.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 1/8/13. 6 | // 7 | // 8 | 9 | #import "HNKit.h" 10 | 11 | extern NSString *kHNSessionControllerSessionsChangedNotification; 12 | 13 | @class HNSession; 14 | 15 | @interface HNSessionController : NSObject { 16 | NSMutableArray *sessions; 17 | } 18 | 19 | + (id)sessionController; 20 | 21 | @property (nonatomic, copy, readonly) NSArray *sessions; 22 | @property (nonatomic, retain) HNSession *recentSession; 23 | 24 | - (NSInteger)numberOfSessions; 25 | 26 | - (void)addSession:(HNSession *)session; 27 | - (void)removeSession:(HNSession *)session; 28 | - (void)moveSession:(HNSession *)session toIndex:(NSInteger)index; 29 | 30 | - (void)refresh; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /Classes/HNKit/HNObjectCache.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNObjectCache.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 7/15/12. 6 | // 7 | // 8 | 9 | #import "HNObject.h" 10 | 11 | @interface HNObjectCache : NSObject { 12 | HNSession *session; 13 | NSMutableDictionary *cacheDictionary; 14 | } 15 | 16 | - (id)initWithSession:(HNSession *)session_; 17 | 18 | - (BOOL)cacheHasObject:(HNObject *)object; 19 | - (void)addObjectToCache:(HNObject *)object_; 20 | - (HNObject *)objectFromCacheWithClass:(Class)cls_ identifier:(id)identifier_ infoDictionary:(NSDictionary *)info; 21 | 22 | - (void)createPersistentCache; 23 | - (void)clearPersistentCache; 24 | - (void)updateObjectFromPersistentCache:(HNObject *)object; 25 | - (void)savePersistentCacheDictionary:(NSDictionary *)dict forObject:(HNObject *)object; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Classes/XML/XMLDocument.h: -------------------------------------------------------------------------------- 1 | // 2 | // XMLDocument.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/10/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | #import "XMLElement.h" 16 | 17 | @interface XMLDocument : NSObject { 18 | xmlDocPtr document; 19 | } 20 | 21 | - (id)initWithHTMLData:(NSData *)data_; 22 | - (id)initWithXMLData:(NSData *)data_; 23 | - (NSArray *)elementsMatchingPath:(NSString *)query relativeToElement:(XMLElement *)element; 24 | - (NSArray *)elementsMatchingPath:(NSString *)xpath; 25 | - (XMLElement *)firstElementMatchingPath:(NSString *)xpath; 26 | - (xmlDocPtr)document; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Classes/Categories/NSString+Tags.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Tags.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/13/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "NSString+Tags.h" 10 | 11 | @implementation NSString (Tags) 12 | 13 | - (NSString *)stringByRemovingHTMLTags { 14 | NSString *html = self; 15 | NSScanner *scanner = [NSScanner scannerWithString:html]; 16 | NSString *text = nil; 17 | 18 | while ([scanner isAtEnd] == NO) { 19 | [scanner scanUpToString:@"<" intoString:NULL]; 20 | [scanner scanUpToString:@">" intoString:&text]; 21 | 22 | html = [html stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"%@>", text] withString:@" "]; 23 | } 24 | 25 | return html; 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Classes/HNKit/HNContainer.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNContainer.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 2/25/12. 6 | // Copyright (c) 2012 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNShared.h" 10 | #import "HNObject.h" 11 | 12 | #define kHNContainerLoadingStateLoadingMore 0x00010000 13 | 14 | @interface HNContainer : HNObject { 15 | NSArray *entries; 16 | 17 | HNMoreToken moreToken; 18 | HNAPIRequest *moreRequest; 19 | NSArray *pendingMoreEntries; 20 | } 21 | 22 | @property (nonatomic, retain) NSArray *entries; 23 | @property (nonatomic, copy) HNMoreToken moreToken; 24 | 25 | - (void)beginLoadingMore; 26 | - (BOOL)isLoadingMore; 27 | - (void)cancelLoadingMore; 28 | 29 | - (void)loadMoreFromDictionary:(NSDictionary *)dictionary complete:(BOOL)complete; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Classes/HNKit/HNShared.h: -------------------------------------------------------------------------------- 1 | 2 | #import "NSURL+Parameters.h" 3 | #import "NSObject+PerformSelector.h" 4 | #import "NSString+RemoveSuffix.h" 5 | 6 | 7 | #define HNKIT_RENDERING_ENABLED 8 | 9 | 10 | #define kHNWebsiteHost @"news.ycombinator.com" 11 | #define kHNFAQURL [NSURL URLWithString:@"http://ycombinator.com/newsfaq.html"] 12 | #define kHNWebsiteURL [NSURL URLWithString:[NSString stringWithFormat:@"https://%@/", kHNWebsiteHost]] 13 | 14 | #define kHNSearchBaseURL @"https://hn.algolia.com/api/v1/search?%@" 15 | #define kHNSearchParamsInteresting @"query=%@" 16 | #define kHNSearchParamsRecent @"query=%@" 17 | 18 | typedef enum { 19 | kHNSearchTypeInteresting, 20 | kHNSearchTypeRecent 21 | } HNSearchType; 22 | 23 | 24 | typedef enum { 25 | kHNVoteDirectionDown, 26 | kHNVoteDirectionUp 27 | } HNVoteDirection; 28 | 29 | 30 | typedef NSString *HNSessionToken; 31 | 32 | typedef NSString *HNMoreToken; 33 | -------------------------------------------------------------------------------- /Classes/HNKit/HNAPISearch.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNAPISearch.h 3 | // newsyc 4 | // 5 | // Created by Quin Hoxie on 6/2/11. 6 | // 7 | 8 | #import "HNKit.h" 9 | 10 | #import "NSString+URLEncoding.h" 11 | 12 | @interface HNAPISearch : NSObject { 13 | HNSession *session; 14 | 15 | NSMutableData *responseData; 16 | NSMutableArray *entries; 17 | HNSearchType searchType; 18 | 19 | NSDateFormatter *dateFormatter; 20 | } 21 | 22 | @property (nonatomic, retain, readonly) HNSession *session; 23 | @property (nonatomic, retain) NSMutableArray *entries; 24 | @property (nonatomic, retain) NSMutableData *responseData; 25 | @property (nonatomic, assign) HNSearchType searchType; 26 | @property (nonatomic, retain) NSDateFormatter *dateFormatter; 27 | 28 | - (id)initWithSession:(HNSession *)session_; 29 | 30 | - (void)handleResponse; 31 | - (NSDictionary *)itemFromRaw:(NSDictionary *)rawDictionary; 32 | - (void)performSearch:(NSString *)searchQuery; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /Classes/HNKit/HNUser.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNUser.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/4/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "HNObject.h" 12 | 13 | #ifdef HNKIT_RENDERING_ENABLED 14 | @class HNObjectBodyRenderer; 15 | #endif 16 | 17 | @interface HNUser : HNObject { 18 | NSInteger karma; 19 | float average; 20 | NSString *created; 21 | NSString *about; 22 | #ifdef HNKIT_RENDERING_ENABLED 23 | HNObjectBodyRenderer *renderer; 24 | #endif 25 | } 26 | 27 | @property (nonatomic, assign) NSInteger karma; 28 | @property (nonatomic, assign) float average; 29 | @property (nonatomic, copy) NSString *created; 30 | @property (nonatomic, copy) NSString *about; 31 | #ifdef HNKIT_RENDERING_ENABLED 32 | @property (nonatomic, readonly) HNObjectBodyRenderer *renderer; 33 | #endif 34 | 35 | + (id)session:(HNSession *)session userWithIdentifier:(id)identifier_; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /Classes/HNKit/HNKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNKit.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/4/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | // This is the main header file for the HNKit "framework", 10 | // which has the singular purpose of scraping Hacker News. 11 | // 12 | // HNKit depends on the files in the "XML" directory, as 13 | // well as few files in the "Categories" folder. 14 | 15 | #import "HNShared.h" 16 | 17 | // data 18 | #import "HNObject.h" 19 | #import "HNUser.h" 20 | #import "HNContainer.h" 21 | #import "HNEntry.h" 22 | #import "HNEntryList.h" 23 | 24 | // sessions 25 | #import "HNSession.h" 26 | #import "HNAnonymousSession.h" 27 | #import "HNSessionController.h" 28 | #import "HNSessionAuthenticator.h" 29 | #import "HNSubmission.h" 30 | 31 | // internal 32 | #import "HNAPIRequest.h" 33 | #import "HNAPIRequestParser.h" 34 | #import "HNAPISubmission.h" 35 | 36 | #ifdef HNKIT_RENDERING_ENABLED 37 | // rendering 38 | #import "HNObjectBodyRenderer.h" 39 | #endif 40 | -------------------------------------------------------------------------------- /Classes/HNKit/HNAnonymousSession.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNAnonymousSession.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 4/13/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNAnonymousSession.h" 11 | #import "HNSubmission.h" 12 | 13 | @implementation HNSession (HNAnonymousSession) 14 | 15 | - (BOOL)isAnonymous { 16 | return NO; 17 | } 18 | 19 | @end 20 | 21 | @implementation HNAnonymousSession 22 | 23 | - (id)init { 24 | if ((self = [super initWithUsername:nil password:nil token:nil])) { 25 | } 26 | 27 | return self; 28 | } 29 | 30 | - (void)performSubmission:(HNSubmission *)submission target:(id)target action:(SEL)action { 31 | [target performSelector:action withObject:nil withObject:[NSNumber numberWithBool:NO]]; 32 | } 33 | 34 | - (BOOL)isAnonymous { 35 | return YES; 36 | } 37 | 38 | - (NSString *)identifier { 39 | return @"HNKit-HNAnonymousSession"; 40 | } 41 | 42 | - (void)reloadToken { 43 | // do nothing 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Classes/Categories/NSDictionary+Parameters.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+Parameters.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/13/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "NSDictionary+Parameters.h" 10 | #import "NSString+URLEncoding.h" 11 | 12 | @implementation NSDictionary (Parameters) 13 | 14 | - (NSString *)queryString { 15 | NSMutableArray *parameters = [NSMutableArray array]; 16 | for (NSString *key in [self allKeys]) { 17 | NSString *escapedk = [key stringByURLEncodingString]; 18 | NSString *v = [[self objectForKey:key] isKindOfClass:[NSString class]] ? [self objectForKey:key] : [[self objectForKey:key] stringValue]; 19 | NSString *escapedv = [v stringByURLEncodingString]; 20 | [parameters addObject:[NSString stringWithFormat:@"%@=%@", escapedk, escapedv]]; 21 | } 22 | 23 | NSString *query = [parameters count] > 0 ? @"?" : @""; 24 | query = [query stringByAppendingString:[parameters componentsJoinedByString:@"&"]]; 25 | return query; 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Classes/XML/XMLElement.h: -------------------------------------------------------------------------------- 1 | // 2 | // XMLElement.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/10/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | 17 | @class XMLDocument; 18 | @interface XMLElement : NSObject { 19 | xmlNodePtr node; 20 | XMLDocument *document; 21 | 22 | NSArray *cachedChildren; 23 | NSDictionary *cachedAttributes; 24 | NSString *cachedContent; 25 | } 26 | 27 | - (id)initWithNode:(xmlNodePtr)node_ inDocument:(XMLDocument *)document_; 28 | - (NSString *)content; 29 | - (NSString *)tagName; 30 | - (NSArray *)children; 31 | - (NSDictionary *)attributes; 32 | - (NSString *)attributeWithName:(NSString *)name; 33 | - (BOOL)isTextNode; 34 | - (xmlNodePtr)node; 35 | 36 | - (NSArray *)elementsMatchingPath:(NSString *)xpath; 37 | - (XMLElement *)firstElementMatchingPath:(NSString *)xpath; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Classes/HNKit/HNAPISubmission.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNAPISubmission.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/30/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | @protocol HNAPISubmissionDelegate; 10 | 11 | typedef enum { 12 | kHNAPISubmissionLoadingStateReady, 13 | kHNAPISubmissionLoadingStateFormTokens, 14 | kHNAPISubmissionLoadingStateFormSubmit 15 | } HNAPISubmissionLoadingState; 16 | 17 | @class HNSubmission; 18 | @interface HNAPISubmission : NSObject { 19 | HNSession *session; 20 | HNSubmission *submission; 21 | 22 | HNAPISubmissionLoadingState loadingState; 23 | NSMutableData *received; 24 | NSURLConnection *connection; 25 | } 26 | 27 | @property (nonatomic, readonly, retain) HNSubmission *submission; 28 | 29 | - (id)initWithSession:(HNSession *)session_ submission:(HNSubmission *)submission_; 30 | - (void)performSubmission; 31 | - (BOOL)isLoading; 32 | 33 | @end 34 | 35 | @protocol HNAPISubmissionDelegate 36 | @optional 37 | 38 | - (void)submissionCompletedSuccessfully:(BOOL)successfully withError:(NSError *)error; 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /Classes/HNKit/HNSessionAuthenticator.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNSessionAuthenticator.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/21/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNSession.h" 11 | 12 | @protocol HNSessionAuthenticatorDelegate; 13 | 14 | @interface HNSessionAuthenticator : NSObject { 15 | NSURLConnection *connection; 16 | 17 | __weak id delegate; 18 | NSString *username; 19 | NSString *password; 20 | } 21 | 22 | @property (nonatomic, assign) id delegate; 23 | 24 | - (id)initWithUsername:(NSString *)username password:(NSString *)password; 25 | - (void)beginAuthenticationRequest; 26 | 27 | @end 28 | 29 | @protocol HNSessionAuthenticatorDelegate 30 | 31 | // Success: we got a token. 32 | - (void)sessionAuthenticator:(HNSessionAuthenticator *)authenticator didRecieveToken:(HNSessionToken)token; 33 | // Failure: username or password invalid or network error. 34 | - (void)sessionAuthenticatorDidRecieveFailure:(HNSessionAuthenticator *)authenticator; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /Classes/HNKit/HNSubmission.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNSubmission.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/30/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNSubmission.h" 10 | 11 | @implementation HNSubmission 12 | @synthesize type, target, destination, title, body, direction; 13 | 14 | - (id)initWithSubmissionType:(HNSubmissionType)type_ { 15 | if ((self = [super init])) { 16 | type = type_; 17 | } 18 | 19 | return self; 20 | } 21 | 22 | - (void)submissionCompletedSuccessfully:(BOOL)successfully withError:(NSError *)error { 23 | if (successfully) { 24 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNSubmissionSuccessNotification object:self]; 25 | } else { 26 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNSubmissionFailureNotification object:self userInfo:[NSDictionary dictionaryWithObject:error forKey:@"error"]]; 27 | } 28 | } 29 | 30 | - (void)dealloc { 31 | [target release]; 32 | [destination release]; 33 | [body release]; 34 | [title release]; 35 | 36 | [super dealloc]; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Classes/HNKit/HNObjectBodyRenderer.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNObjectBodyRenderer.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 2/26/12. 6 | // Copyright (c) 2012 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNShared.h" 10 | 11 | #ifdef HNKIT_RENDERING_ENABLED 12 | 13 | #import 14 | 15 | @class HNObject; 16 | 17 | @interface HNObjectBodyRenderer : NSObject { 18 | HNObject *object; 19 | 20 | NSAttributedString *attributed; 21 | CTFramesetterRef framesetter; 22 | } 23 | 24 | @property (nonatomic, readonly, assign) HNObject *object; 25 | 26 | @property (nonatomic, readonly, copy) NSString *HTMLString; 27 | @property (nonatomic, readonly, copy) NSAttributedString *attributedString; 28 | @property (nonatomic, readonly, copy) NSString *string; 29 | 30 | + (CGFloat)defaultFontSize; 31 | + (void)setDefaultFontSize:(CGFloat)size; 32 | 33 | - (id)initWithObject:(HNObject *)object; 34 | 35 | - (CGSize)sizeForWidth:(CGFloat)width; 36 | - (void)renderInContext:(CGContextRef)context rect:(CGRect)rect; 37 | - (NSURL *)linkURLAtPoint:(CGPoint)point forWidth:(CGFloat)width rects:(NSSet **)rects; 38 | 39 | @end 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /Classes/HNKit/HNSubmission.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNSubmission.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/30/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNAPISubmission.h" 11 | 12 | #define kHNSubmissionSuccessNotification @"kHNSubmissionSuccessNotification" 13 | #define kHNSubmissionFailureNotification @"kHNSubmissionFailureNotification" 14 | 15 | typedef enum { 16 | kHNSubmissionTypeSubmission, 17 | kHNSubmissionTypeVote, 18 | kHNSubmissionTypeFlag, 19 | kHNSubmissionTypeReply 20 | } HNSubmissionType; 21 | 22 | @class HNEntry; 23 | @interface HNSubmission : NSObject { 24 | HNSubmissionType type; 25 | HNEntry *target; 26 | 27 | NSURL *destination; 28 | NSString *title; 29 | NSString *body; 30 | 31 | HNVoteDirection direction; 32 | } 33 | 34 | @property (nonatomic, readonly) HNSubmissionType type; 35 | @property (nonatomic, retain) HNEntry *target; 36 | @property (nonatomic, copy) NSURL *destination; 37 | @property (nonatomic, copy) NSString *body; 38 | @property (nonatomic, copy) NSString *title; 39 | @property (nonatomic, assign) HNVoteDirection direction; 40 | 41 | - (HNSubmission *)initWithSubmissionType:(HNSubmissionType)type_; 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /Classes/HNKit/HNEntryList.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNEntryList.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 8/12/11. 6 | // Copyright (c) 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNShared.h" 10 | #import "HNContainer.h" 11 | #import "HNUser.h" 12 | 13 | typedef NSString *HNEntryListIdentifier; 14 | 15 | #define kHNEntryListIdentifierSubmissions @"news" 16 | #define kHNEntryListIdentifierNewSubmissions @"newest" 17 | #define kHNEntryListIdentifierBestSubmissions @"best" 18 | #define kHNEntryListIdentifierActiveSubmissions @"active" 19 | #define kHNEntryListIdentifierClassicSubmissions @"classic" 20 | #define kHNEntryListIdentifierAskSubmissions @"ask" 21 | #define kHNEntryListIdentifierBestComments @"bestcomments" 22 | #define kHNEntryListIdentifierNewComments @"newcomments" 23 | #define kHNEntryListIdentifierUserSubmissions @"submitted" 24 | #define kHNEntryListIdentifierUserComments @"threads" 25 | #define kHNEntryListIdentifierSaved @"saved" 26 | 27 | @interface HNEntryList : HNContainer { 28 | HNUser *user; 29 | } 30 | 31 | @property (nonatomic, retain, readonly) HNUser *user; 32 | 33 | + (id)session:(HNSession *)session entryListWithIdentifier:(HNEntryListIdentifier)identifier_ user:(HNUser *)user_; 34 | + (id)session:(HNSession *)session entryListWithIdentifier:(HNEntryListIdentifier)identifier_; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /Classes/HNKit/HNSession.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNSession.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/13/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNSessionAuthenticator.h" 11 | 12 | @class HNUser, HNEntry, HNSubmission, HNObjectCache; 13 | @interface HNSession : NSObject { 14 | BOOL loaded; 15 | HNSessionToken token; 16 | HNUser *user; 17 | NSString *password; 18 | HNSessionAuthenticator *authenticator; 19 | HNObjectCache *cache; 20 | 21 | NSDictionary *pool; 22 | } 23 | 24 | @property (nonatomic, retain) NSString *password; 25 | @property (nonatomic, retain) HNUser *user; 26 | @property (nonatomic, copy) NSString *token; 27 | @property (nonatomic, assign, getter=isLoaded) BOOL loaded; 28 | @property (nonatomic, retain, readonly) NSString *identifier; 29 | @property (nonatomic, retain, readonly) HNObjectCache *cache; 30 | 31 | - (id)initWithUsername:(NSString *)username password:(NSString *)password token:(HNSessionToken)token; 32 | - (id)initWithSessionDictionary:(NSDictionary *)sessionDictionary; 33 | 34 | - (NSDictionary *)sessionDictionary; 35 | 36 | - (void)performSubmission:(HNSubmission *)submission; 37 | - (void)reloadToken; 38 | 39 | - (void)addCookiesToRequest:(NSMutableURLRequest *)request; 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /Classes/HNKit/HNNetworkActivityController.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNNetworkActivityController.m 3 | // HNKit 4 | // 5 | // Created by Grant Paul on 3/3/13. 6 | // Copyright (c) 2013 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNNetworkActivityController.h" 10 | 11 | @implementation HNNetworkActivityController 12 | 13 | static void (^networkActivityBeganBlock)(); 14 | static void (^networkActivityEndedBlock)(); 15 | 16 | + (void (^)())newtorkActivityBeganBlock { 17 | return networkActivityBeganBlock; 18 | } 19 | 20 | + (void)setNetworkActivityBeganBlock:(void (^)())block { 21 | if (networkActivityBeganBlock != block) { 22 | [networkActivityBeganBlock release]; 23 | networkActivityBeganBlock = [block copy]; 24 | } 25 | } 26 | 27 | + (void (^)())newtorkActivityEndedBlock { 28 | return networkActivityEndedBlock; 29 | } 30 | 31 | + (void)setNetworkActivityEndedBlock:(void (^)())block { 32 | if (networkActivityEndedBlock != block) { 33 | [networkActivityEndedBlock release]; 34 | networkActivityEndedBlock = [block copy]; 35 | } 36 | } 37 | 38 | + (void)networkActivityBegan { 39 | if (networkActivityBeganBlock != NULL) { 40 | networkActivityBeganBlock(); 41 | } 42 | } 43 | 44 | + (void)networkActivityEnded { 45 | if (networkActivityEndedBlock != NULL) { 46 | networkActivityEndedBlock(); 47 | } 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /Classes/Categories/NSObject+PerformSelector.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2009-2011 Facebook 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #import 18 | 19 | @interface NSObject (PerformSelector) 20 | 21 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3; 22 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3 withObject:(id)p4; 23 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3 withObject:(id)p4 withObject:(id)p5; 24 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3 withObject:(id)p4 withObject:(id)p5 withObject:(id)p6; 25 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3 withObject:(id)p4 withObject:(id)p5 withObject:(id)p6 withObject:(id)p7; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Classes/HNKit/HNEntry.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNEntry.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/4/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNShared.h" 10 | #import "HNContainer.h" 11 | 12 | #ifdef HNKIT_RENDERING_ENABLED 13 | @class HNObjectBodyRenderer; 14 | #endif 15 | 16 | @class HNUser; 17 | @interface HNEntry : HNContainer { 18 | NSInteger points; 19 | NSInteger children; 20 | HNUser *submitter; 21 | NSString *body; 22 | NSString *posted; 23 | HNEntry *parent; 24 | HNEntry *submission; 25 | NSURL *destination; 26 | NSString *title; 27 | #ifdef HNKIT_RENDERING_ENABLED 28 | HNObjectBodyRenderer *renderer; 29 | #endif 30 | } 31 | 32 | @property (nonatomic, assign) NSInteger points; 33 | @property (nonatomic, assign) NSInteger children; 34 | @property (nonatomic, retain) HNUser *submitter; 35 | @property (nonatomic, copy) NSString *body; 36 | @property (nonatomic, retain) NSString *posted; 37 | @property (nonatomic, retain) HNEntry *parent; 38 | @property (nonatomic, retain) HNEntry *submission; 39 | @property (nonatomic, copy) NSURL *destination; 40 | @property (nonatomic, copy) NSString *title; 41 | #ifdef HNKIT_RENDERING_ENABLED 42 | @property (nonatomic, readonly) HNObjectBodyRenderer *renderer; 43 | #endif 44 | 45 | - (BOOL)isComment; 46 | - (BOOL)isSubmission; 47 | 48 | + (id)session:(HNSession *)session entryWithIdentifier:(id)identifier_; 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /Classes/Categories/NSDate+TimeAgo.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDate+TimeAgo.m 3 | // HNKit 4 | // 5 | // By Rudy Jahchan 6 | // https://gist.github.com/rudyjahchan/1660134 7 | // No copyright - released into public domain 8 | // 9 | 10 | #import "NSDate+TimeAgo.h" 11 | 12 | @implementation NSDate (TimeAgo) 13 | 14 | - (NSString *)timeAgoInWords { 15 | double seconds = [self timeIntervalSinceNow]; 16 | seconds = seconds * -1; 17 | 18 | if (seconds < 1) { 19 | return @"now"; 20 | } else if (seconds < 60) { 21 | return @"less than a minute ago"; 22 | } else { 23 | NSUInteger difference = 0; 24 | BOOL pluralize = NO; 25 | NSString *unit = @""; 26 | 27 | if (seconds < 3600) { 28 | difference = round(seconds / 60); 29 | unit = @"minute"; 30 | } else if (seconds < 86400) { 31 | difference = round(seconds / 3600); 32 | unit = @"hour"; 33 | } else if (seconds < 604800) { 34 | difference = round(seconds / 86400); 35 | unit = @"day"; 36 | } else if (seconds < 2592000) { 37 | difference = round(seconds / 604800); 38 | unit = @"week"; 39 | } else if (seconds < 31557600) { 40 | difference = round(seconds / 2592000); 41 | unit = @"month"; 42 | } else { 43 | difference = round(seconds / 31557600); 44 | unit = @"year"; 45 | } 46 | 47 | if (difference > 1) { 48 | pluralize = YES; 49 | } 50 | 51 | return [NSString stringWithFormat:@"%d %@%@ ago", 52 | difference, 53 | unit, 54 | (pluralize ? @"s" : @"")]; 55 | } 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2014, Xuzz Productions, LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | Neither the name of the Xuzz Productions, LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | 11 | -------------------------------------------------------------------------------- /Classes/Categories/NSString+Entities.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+HTML.m 3 | // MWFeedParser 4 | // 5 | // Copyright (c) 2010 Michael Waterfall 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // 1. The above copyright notice and this permission notice shall be included 15 | // in all copies or substantial portions of the Software. 16 | // 17 | // 2. This Software cannot be used to archive or collect data such as (but not 18 | // limited to) that of events, news, experiences and activities, for the 19 | // purpose of any concept relating to diary/journal keeping. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | // 29 | 30 | #import 31 | 32 | @interface NSString (Entities) 33 | 34 | - (NSString *)stringByDecodingHTMLEntities; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /Classes/Categories/NSURL+Parameters.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+Parameters.m 3 | // Telekinesis 4 | // 5 | // Created by Nicholas Jitkoff on 6/14/07. 6 | // Copyright 2007 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "NSURL+Parameters.h" 10 | 11 | 12 | @implementation NSURL (Parameters) 13 | 14 | - (NSArray *)parameterArray { 15 | if (![self query]) return nil; 16 | NSScanner *scanner = [NSScanner scannerWithString:[self query]]; 17 | if (!scanner) return nil; 18 | 19 | NSMutableArray *array = [NSMutableArray array]; 20 | 21 | NSString *key; 22 | NSString *val; 23 | while (![scanner isAtEnd]) { 24 | if (![scanner scanUpToString:@"=" intoString:&key]) key = nil; 25 | [scanner scanString:@"=" intoString:nil]; 26 | if (![scanner scanUpToString:@"&" intoString:&val]) val = nil; 27 | [scanner scanString:@"&" intoString:nil]; 28 | 29 | key = [key stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 30 | val = [val stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 31 | 32 | if (key) [array addObject:[NSDictionary dictionaryWithObjectsAndKeys: 33 | key, @"key", val, @"value", nil]]; 34 | } 35 | return array; 36 | } 37 | 38 | - (NSDictionary *)parameterDictionary { 39 | if (![self query]) return nil; 40 | NSArray *parameterArray = [self parameterArray]; 41 | 42 | NSArray *keys = [parameterArray valueForKey:@"key"]; 43 | NSArray *values = [parameterArray valueForKey:@"value"]; 44 | NSDictionary *dictionary = [NSDictionary dictionaryWithObjects:values forKeys:keys]; 45 | return dictionary; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /Classes/HNKit/HNUser.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNUser.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/4/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "NSURL+Parameters.h" 10 | 11 | #import "HNKit.h" 12 | #import "HNUser.h" 13 | 14 | @implementation HNUser 15 | @synthesize karma, average, created, about; 16 | 17 | #ifdef HNKIT_RENDERING_ENABLED 18 | @synthesize renderer; 19 | 20 | - (HNObjectBodyRenderer *)renderer { 21 | if (renderer != nil) return renderer; 22 | 23 | renderer = [[HNObjectBodyRenderer alloc] initWithObject:self]; 24 | return renderer; 25 | } 26 | #endif 27 | 28 | + (id)identifierForURL:(NSURL *)url_ { 29 | if (![self isValidURL:url_]) return NO; 30 | 31 | NSDictionary *parameters = [url_ parameterDictionary]; 32 | return [parameters objectForKey:@"id"]; 33 | } 34 | 35 | + (NSString *)pathForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 36 | return @"user"; 37 | } 38 | 39 | + (NSDictionary *)parametersForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 40 | if (identifier_ != nil) return [NSDictionary dictionaryWithObject:identifier_ forKey:@"id"]; 41 | else return [NSDictionary dictionary]; 42 | } 43 | 44 | + (id)session:(HNSession *)session userWithIdentifier:(id)identifier_ { 45 | return [self session:session objectWithIdentifier:identifier_]; 46 | } 47 | 48 | - (void)loadFromDictionary:(NSDictionary *)dictionary complete:(BOOL)complete { 49 | [self setAbout:[dictionary objectForKey:@"about"]]; 50 | [self setKarma:[[dictionary objectForKey:@"karma"] intValue]]; 51 | [self setAverage:[[dictionary objectForKey:@"average"] floatValue]]; 52 | [self setCreated:[[dictionary objectForKey:@"created"] stringByRemovingSuffix:@" ago"]]; 53 | 54 | [super loadFromDictionary:dictionary complete:complete]; 55 | } 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /Classes/XML/XMLDocument.m: -------------------------------------------------------------------------------- 1 | // 2 | // XMLDocument.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/10/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | // Lazy-loading-ish wrapper for libxml2. 10 | // Not the most amazing stuff, but hey, it works fine. 11 | // (Somewhat inspired by "Hpple", but rewritten.) 12 | 13 | #import "XMLDocument.h" 14 | #import "XMLElement.h" 15 | 16 | @implementation XMLDocument 17 | 18 | - (void)dealloc { 19 | xmlFreeDoc(document); 20 | [super dealloc]; 21 | } 22 | 23 | - (id)initWithData:(NSData *)data isXML:(BOOL)xml { 24 | if ((self = [super init])) { 25 | document = (xml ? xmlReadMemory : htmlReadMemory)([data bytes], [data length], "", NULL, xml ? XML_PARSE_RECOVER : HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR); 26 | 27 | if (document == NULL) { 28 | [self autorelease]; 29 | return nil; 30 | } 31 | } 32 | 33 | return self; 34 | } 35 | 36 | - (xmlDocPtr)document { 37 | return document; 38 | } 39 | 40 | - (id)initWithXMLData:(NSData *)data_ { 41 | return [self initWithData:data_ isXML:YES]; 42 | } 43 | 44 | - (id)initWithHTMLData:(NSData *)data_ { 45 | return [self initWithData:data_ isXML:NO]; 46 | } 47 | 48 | - (NSArray *)elementsMatchingPath:(NSString *)query relativeToElement:(XMLElement *)element { 49 | xmlXPathContextPtr xpathCtx; 50 | xmlXPathObjectPtr xpathObj; 51 | 52 | xpathCtx = xmlXPathNewContext(document); 53 | if (xpathCtx == NULL) return nil; 54 | 55 | xpathCtx->node = [element node]; 56 | 57 | xpathObj = xmlXPathEvalExpression((xmlChar *) [query cStringUsingEncoding:NSUTF8StringEncoding], xpathCtx); 58 | if (xpathObj == NULL) return nil; 59 | 60 | xmlNodeSetPtr nodes = xpathObj->nodesetval; 61 | if (nodes == NULL) return nil; 62 | 63 | NSMutableArray *result = [NSMutableArray array]; 64 | for (NSInteger i = 0; i < nodes->nodeNr; i++) { 65 | XMLElement *element = [[XMLElement alloc] initWithNode:nodes->nodeTab[i] inDocument:self]; 66 | [result addObject:[element autorelease]]; 67 | } 68 | 69 | xmlXPathFreeObject(xpathObj); 70 | xmlXPathFreeContext(xpathCtx); 71 | 72 | return result; 73 | } 74 | 75 | - (NSArray *)elementsMatchingPath:(NSString *)xpath { 76 | return [self elementsMatchingPath:xpath relativeToElement:nil]; 77 | } 78 | 79 | - (XMLElement *)firstElementMatchingPath:(NSString *)xpath { 80 | NSArray *elements = [self elementsMatchingPath:xpath]; 81 | 82 | if ([elements count] >= 1) { 83 | return [elements objectAtIndex:0]; 84 | } else { 85 | return nil; 86 | } 87 | } 88 | 89 | @end 90 | -------------------------------------------------------------------------------- /Classes/HNKit/HNContainer.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNContainer.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 2/25/12. 6 | // Copyright (c) 2012 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNContainer.h" 10 | #import "HNAPIRequest.h" 11 | #import "HNEntry.h" 12 | 13 | #import "NSURL+Parameters.h" 14 | 15 | @implementation HNContainer 16 | @synthesize entries, moreToken; 17 | 18 | - (void)loadFromDictionary:(NSDictionary *)dictionary complete:(BOOL)complete { 19 | [self setMoreToken:[dictionary objectForKey:@"more"]]; 20 | 21 | [super loadFromDictionary:dictionary complete:complete]; 22 | } 23 | 24 | - (void)loadMoreFromDictionary:(NSDictionary *)dictionary complete:(BOOL)complete { 25 | pendingMoreEntries = [[self entries] retain]; 26 | [self loadFromDictionary:dictionary complete:complete]; 27 | [pendingMoreEntries release]; 28 | pendingMoreEntries = nil; 29 | } 30 | 31 | - (BOOL)isLoadingMore { 32 | return [self hasLoadingState:kHNContainerLoadingStateLoadingMore]; 33 | } 34 | 35 | - (void)beginLoadingMore { 36 | if ([self isLoadingMore] || moreToken == nil || ![self isLoaded]) return; 37 | 38 | [self addLoadingState:kHNContainerLoadingStateLoadingMore]; 39 | 40 | NSURL *moreURL = [NSURL URLWithString:moreToken]; 41 | 42 | NSString *path = [moreURL path]; 43 | if ([path hasPrefix:@"/"]) path = [path substringFromIndex:[@"/" length]]; 44 | NSDictionary *parameters = [moreURL parameterDictionary]; 45 | if (parameters == nil) parameters = [NSDictionary dictionary]; 46 | 47 | moreRequest = [[HNAPIRequest alloc] initWithSession:session target:self action:@selector(moreRequest:completedWithResponse:error:)]; 48 | [moreRequest performRequestWithPath:path parameters:parameters]; 49 | } 50 | 51 | - (void)cancelLoadingMore { 52 | [self clearLoadingState:kHNContainerLoadingStateLoadingMore]; 53 | [moreRequest cancelRequest]; 54 | [moreRequest release]; 55 | moreRequest = nil; 56 | } 57 | 58 | - (void)beginLoadingWithState:(HNObjectLoadingState)state_ { 59 | [self cancelLoadingMore]; 60 | [super beginLoadingWithState:state_]; 61 | } 62 | 63 | - (void)moreRequest:(HNAPIRequest *)request completedWithResponse:(NSDictionary *)response error:(NSError *)error { 64 | [self clearLoadingState:kHNContainerLoadingStateLoadingMore]; 65 | [moreRequest release]; 66 | moreRequest = nil; 67 | 68 | if (error == nil) { 69 | [self loadMoreFromDictionary:response complete:YES]; 70 | 71 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNObjectFinishedLoadingNotification object:self]; 72 | } else { 73 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNObjectFailedLoadingNotification object:self]; 74 | } 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /Classes/HNKit/HNEntryList.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNEntryList.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 8/12/11. 6 | // Copyright (c) 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNEntryList.h" 10 | #import "HNEntry.h" 11 | 12 | @interface HNEntryList () 13 | 14 | @property (nonatomic, retain) HNUser *user; 15 | 16 | @end 17 | 18 | @implementation HNEntryList 19 | @synthesize user; 20 | 21 | + (NSDictionary *)infoDictionaryForURL:(NSURL *)url_ { 22 | if (![self isValidURL:url_]) return nil; 23 | 24 | NSDictionary *parameters = [url_ parameterDictionary]; 25 | if ([parameters objectForKey:@"id"] != nil) return [NSDictionary dictionaryWithObject:[parameters objectForKey:@"id"] forKey:@"user"]; 26 | else return nil; 27 | } 28 | 29 | + (id)identifierForURL:(NSURL *)url_ { 30 | if (![self isValidURL:url_]) return nil; 31 | 32 | NSString *path = [url_ path]; 33 | if ([path hasSuffix:@"/"]) path = [path substringToIndex:[path length] - 2]; 34 | 35 | return path; 36 | } 37 | 38 | + (NSString *)pathForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 39 | return identifier_; 40 | } 41 | 42 | + (NSDictionary *)parametersForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 43 | if (info != nil && [info objectForKey:@"user"] != nil) { 44 | return [NSDictionary dictionaryWithObject:[info objectForKey:@"user"] forKey:@"id"]; 45 | } else { 46 | return [NSDictionary dictionary]; 47 | } 48 | } 49 | 50 | + (id)session:(HNSession *)session entryListWithIdentifier:(HNEntryListIdentifier)identifier_ user:(HNUser *)user_ { 51 | NSDictionary *info = nil; 52 | if (user_ != nil) info = [NSDictionary dictionaryWithObject:[user_ identifier] forKey:@"user"]; 53 | 54 | return [self session:session objectWithIdentifier:identifier_ infoDictionary:info]; 55 | } 56 | 57 | + (id)session:(HNSession *)session entryListWithIdentifier:(HNEntryListIdentifier)identifier_ { 58 | return [self session:session entryListWithIdentifier:identifier_ user:nil]; 59 | } 60 | 61 | - (void)loadInfoDictionary:(NSDictionary *)info { 62 | if (info != nil) { 63 | NSString *identifier_ = [info objectForKey:@"user"]; 64 | [self setUser:[HNUser session:session userWithIdentifier:identifier_]]; 65 | } 66 | } 67 | 68 | - (NSDictionary *)infoDictionary { 69 | if (user != nil) { 70 | return [NSDictionary dictionaryWithObject:[user identifier] forKey:@"user"]; 71 | } else { 72 | return [super infoDictionary]; 73 | } 74 | } 75 | 76 | - (void)loadFromDictionary:(NSDictionary *)response complete:(BOOL)complete { 77 | NSMutableArray *children = [NSMutableArray array]; 78 | 79 | for (NSDictionary *entryDictionary in [response objectForKey:@"children"]) { 80 | HNEntry *entry = [HNEntry session:session entryWithIdentifier:[entryDictionary objectForKey:@"identifier"]]; 81 | [entry loadFromDictionary:entryDictionary complete:NO]; 82 | [children addObject:entry]; 83 | } 84 | 85 | NSArray *allEntries = [(pendingMoreEntries ? : [NSArray array]) arrayByAddingObjectsFromArray:children]; 86 | [self setEntries:allEntries]; 87 | 88 | [super loadFromDictionary:response complete:complete]; 89 | } 90 | 91 | @end 92 | -------------------------------------------------------------------------------- /Classes/HNKit/HNObject.h: -------------------------------------------------------------------------------- 1 | // 2 | // HNObject.h 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/4/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNShared.h" 10 | 11 | typedef enum { 12 | kHNObjectLoadingStateNotLoaded = 0, 13 | kHNObjectLoadingStateUnloaded = 0, 14 | kHNObjectLoadingStateLoadingInitial = 1 << 0, 15 | kHNObjectLoadingStateLoadingReload = 1 << 1, 16 | kHNObjectLoadingStateLoadingOther = 1 << 2, 17 | kHNObjectLoadingStateLoadingAny = (kHNObjectLoadingStateLoadingInitial | kHNObjectLoadingStateLoadingReload | kHNObjectLoadingStateLoadingOther), 18 | kHNObjectLoadingStateCustom = 0xFFFF0000, /* mask */ 19 | kHNObjectLoadingStateLoaded = 1 << 15, 20 | } HNObjectLoadingState; 21 | 22 | #define kHNObjectStartedLoadingNotification @"HNObjectStartedLoading" 23 | #define kHNObjectFinishedLoadingNotification @"HNObjectFinishedLoading" 24 | #define kHNObjectFailedLoadingNotification @"HNObjectFailedLoading" 25 | #define kHNObjectLoadingStateChangedNotification @"HNObjectLoadingStateChanged" 26 | #define kHNObjectLoadingStateChangedNotificationErrorKey @"HNObjectLoadingStateChangedNotificationError" 27 | 28 | @class HNAPIRequest, HNSession; 29 | @interface HNObject : NSObject { 30 | HNSession *session; 31 | id identifier; 32 | NSURL *url; 33 | 34 | HNObjectLoadingState loadingState; 35 | 36 | HNAPIRequest *apiRequest; 37 | } 38 | 39 | @property (nonatomic, assign, readonly) HNSession *session; 40 | @property (nonatomic, copy) id identifier; 41 | @property (nonatomic, copy) NSURL *URL; 42 | @property (nonatomic, readonly) HNObjectLoadingState loadingState; 43 | 44 | + (BOOL)isValidURL:(NSURL *)url_; 45 | + (NSDictionary *)infoDictionaryForURL:(NSURL *)url_; 46 | + (id)identifierForURL:(NSURL *)url_; 47 | 48 | + (NSString *)pathForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info; 49 | + (NSDictionary *)parametersForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info; 50 | + (NSURL *)generateURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info; 51 | 52 | // These methods don't necessarily create a new instance if it's already in the 53 | // cache. The cache's keyed on (class, identifier, info) tuples inside HNObject. 54 | + (id)session:(HNSession *)session objectWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info URL:(NSURL *)url_; 55 | + (id)session:(HNSession *)session objectWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info; 56 | + (id)session:(HNSession *)session objectWithIdentifier:(id)identifier_; 57 | + (id)session:(HNSession *)session objectWithURL:(NSURL *)url_; 58 | 59 | - (NSDictionary *)infoDictionary; 60 | 61 | - (void)loadFromDictionary:(NSDictionary *)dictionary complete:(BOOL)complete; 62 | 63 | - (void)setIsLoaded:(BOOL)loaded; 64 | - (BOOL)isLoaded; 65 | 66 | - (BOOL)isLoading; 67 | 68 | - (void)cancelLoading; 69 | - (void)beginLoading; 70 | 71 | @end 72 | 73 | @interface HNObject (Subclassing) 74 | 75 | @property (nonatomic, copy) NSDictionary *contentsDictionary; 76 | 77 | - (void)beginLoadingWithState:(HNObjectLoadingState)state_; 78 | - (void)addLoadingState:(HNObjectLoadingState)state_; 79 | - (void)clearLoadingState:(HNObjectLoadingState)state_; 80 | - (BOOL)hasLoadingState:(HNObjectLoadingState)state_; 81 | 82 | - (void)loadInfoDictionary:(NSDictionary *)info; 83 | 84 | @end 85 | 86 | -------------------------------------------------------------------------------- /Classes/HNKit/HNSession.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNSession.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/13/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNSession.h" 11 | #import "HNObjectCache.h" 12 | #import "HNAPISubmission.h" 13 | #import "HNSubmission.h" 14 | #import "HNAnonymousSession.h" 15 | 16 | @implementation HNSession 17 | @synthesize user, token, loaded, password, cache; 18 | 19 | - (id)initWithUsername:(NSString *)username password:(NSString *)password_ token:(NSString *)token_ { 20 | if ((self = [super init])) { 21 | cache = [[HNObjectCache alloc] initWithSession:self]; 22 | 23 | if (username != nil) { 24 | HNUser *user_ = [HNUser session:self userWithIdentifier:username]; 25 | [self setUser:user_]; 26 | } 27 | 28 | [self setToken:token_]; 29 | [self setPassword:password_]; 30 | 31 | [self setLoaded:YES]; 32 | 33 | [cache createPersistentCache]; 34 | } 35 | 36 | return self; 37 | } 38 | 39 | - (id)initWithSessionDictionary:(NSDictionary *)sessionDictionary { 40 | HNSessionToken token_ = (HNSessionToken) [sessionDictionary objectForKey:@"HNKit:HNSession:Token"]; 41 | NSString *password_ = [sessionDictionary objectForKey:@"HNKit:HNSession:Password"]; 42 | NSString *name_ = [sessionDictionary objectForKey:@"HNKit:HNSession:Identifier"]; 43 | 44 | return [self initWithUsername:name_ password:password_ token:token_]; 45 | } 46 | 47 | - (NSDictionary *)sessionDictionary { 48 | return [NSDictionary dictionaryWithObjectsAndKeys: 49 | token, @"HNKit:HNSession:Token", 50 | password, @"HNKit:HNSession:Password", 51 | [self identifier], @"HNKit:HNSession:Identifier", 52 | nil]; 53 | } 54 | 55 | - (NSString *)identifier { 56 | return [[self user] identifier]; 57 | } 58 | 59 | - (void)dealloc { 60 | [cache release]; 61 | 62 | [super dealloc]; 63 | } 64 | 65 | - (void)sessionAuthenticatorDidRecieveFailure:(HNSessionAuthenticator *)authenticator_ { 66 | [authenticator autorelease]; 67 | authenticator = nil; 68 | } 69 | 70 | - (void)sessionAuthenticator:(HNSessionAuthenticator *)authenticator_ didRecieveToken:(HNSessionToken)token_ { 71 | [authenticator autorelease]; 72 | authenticator = nil; 73 | 74 | [self setToken:token_]; 75 | } 76 | 77 | - (void)reloadToken { 78 | // XXX: maybe this should return an error code 79 | if (authenticator != nil) return; 80 | 81 | authenticator = [[HNSessionAuthenticator alloc] initWithUsername:[user identifier] password:password]; 82 | [authenticator setDelegate:self]; 83 | [authenticator beginAuthenticationRequest]; 84 | } 85 | 86 | - (void)performSubmission:(HNSubmission *)submission { 87 | HNAPISubmission *api = [[HNAPISubmission alloc] initWithSession:self submission:submission]; 88 | [api performSubmission]; 89 | [api autorelease]; 90 | } 91 | 92 | - (void)addCookiesToRequest:(NSMutableURLRequest *)request { 93 | if (token == nil) return; 94 | 95 | NSDictionary *properties = [NSDictionary dictionaryWithObjectsAndKeys: 96 | kHNWebsiteHost, NSHTTPCookieDomain, 97 | @"/", NSHTTPCookiePath, 98 | @"user", NSHTTPCookieName, 99 | (NSString *) token, NSHTTPCookieValue, 100 | nil]; 101 | NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:properties]; 102 | NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:[NSArray arrayWithObject:cookie]]; 103 | [request setAllHTTPHeaderFields:headers]; 104 | } 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /Classes/HNKit/HNAPIRequest.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNAPIRequest.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/4/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNAPIRequest.h" 11 | 12 | #import "NSDictionary+Parameters.h" 13 | 14 | #import "HNNetworkActivityController.h" 15 | 16 | @implementation HNAPIRequest 17 | 18 | - (HNAPIRequest *)initWithSession:(HNSession *)session_ target:(id)target_ action:(SEL)action_ { 19 | if ((self = [super init])) { 20 | session = session_; 21 | target = target_; 22 | action = action_; 23 | } 24 | 25 | return self; 26 | } 27 | 28 | - (void)connection:(NSURLConnection *)connection_ didReceiveData:(NSData *)data { 29 | [received appendData:data]; 30 | } 31 | 32 | - (void)connection:(NSURLConnection *)connection_ didFailWithError:(NSError *)error { 33 | [HNNetworkActivityController networkActivityEnded]; 34 | [connection release]; 35 | connection = nil; 36 | 37 | [target performSelector:action withObject:self withObject:nil withObject:error]; 38 | } 39 | 40 | - (void)completeSuccessfullyWithResult:(NSDictionary *)result { 41 | [target performSelector:action withObject:self withObject:result withObject:nil]; 42 | } 43 | 44 | - (void)completeUnsuccessfullyWithError:(NSError *)error { 45 | [target performSelector:action withObject:self withObject:nil withObject:error]; 46 | } 47 | 48 | - (void)parseInBackground { 49 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 50 | 51 | NSString *resp = [[NSString alloc] initWithData:received encoding:NSUTF8StringEncoding]; 52 | 53 | NSDictionary *result = nil; 54 | 55 | if ([resp length] > 0) { 56 | HNAPIRequestParser *parser = [[HNAPIRequestParser alloc] init]; 57 | 58 | @try { 59 | if (![parser stringIsProcrastinationError:resp] && ![parser stringIsExpiredError:resp]) { 60 | result = [parser parseWithString:resp]; 61 | } 62 | } @catch (NSException *e) { 63 | NSLog(@"HNAPIRequest: Exception parsing page at /%@ with reason \"%@\".", path, [e reason]); 64 | } 65 | 66 | [parser release]; 67 | } 68 | 69 | if (result != nil) { 70 | [self performSelectorOnMainThread:@selector(completeSuccessfullyWithResult:) withObject:result waitUntilDone:YES]; 71 | } else { 72 | NSError *error = [NSError errorWithDomain:@"error" code:100 userInfo:[NSDictionary dictionaryWithObject:@"Error scraping." forKey:NSLocalizedDescriptionKey]]; 73 | [self performSelectorOnMainThread:@selector(completeUnsuccessfullyWithError:) withObject:error waitUntilDone:YES]; 74 | } 75 | 76 | [resp release]; 77 | [received release]; 78 | received = nil; 79 | 80 | [pool release]; 81 | } 82 | 83 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection_ { 84 | [HNNetworkActivityController networkActivityEnded]; 85 | [connection release]; 86 | connection = nil; 87 | 88 | [self performSelectorInBackground:@selector(parseInBackground) withObject:nil]; 89 | } 90 | 91 | - (void)performRequestWithPath:(NSString *)path_ parameters:(NSDictionary *)parameters { 92 | path = [path_ copy]; 93 | received = [[NSMutableData alloc] init]; 94 | 95 | NSString *combined = [path stringByAppendingString:[parameters queryString]]; 96 | NSURL *url = [NSURL URLWithString:combined relativeToURL:kHNWebsiteURL]; 97 | 98 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 99 | [session addCookiesToRequest:request]; 100 | 101 | connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; 102 | [connection start]; 103 | 104 | [HNNetworkActivityController networkActivityBegan]; 105 | } 106 | 107 | - (BOOL)isLoading { 108 | return connection != nil; 109 | } 110 | 111 | - (void)cancelRequest { 112 | if (connection != nil) { 113 | [HNNetworkActivityController networkActivityEnded]; 114 | [connection cancel]; 115 | [connection release]; 116 | connection = nil; 117 | } 118 | } 119 | 120 | - (void)dealloc { 121 | [connection release]; 122 | [received release]; 123 | [path release]; 124 | 125 | [super dealloc]; 126 | } 127 | 128 | @end 129 | -------------------------------------------------------------------------------- /Classes/Categories/NSString+Entities.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+HTML.m 3 | // MWFeedParser 4 | // 5 | // Copyright (c) 2010 Michael Waterfall 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // 1. The above copyright notice and this permission notice shall be included 15 | // in all copies or substantial portions of the Software. 16 | // 17 | // 2. This Software cannot be used to archive or collect data such as (but not 18 | // limited to) that of events, news, experiences and activities, for the 19 | // purpose of any concept relating to diary/journal keeping. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | // 29 | 30 | #import "NSString+Entities.h" 31 | 32 | 33 | @implementation NSString (Entities) 34 | 35 | - (NSString *)stringByDecodingHTMLEntities { 36 | NSUInteger myLength = [self length]; 37 | NSUInteger ampIndex = [self rangeOfString:@"&" options:NSLiteralSearch].location; 38 | 39 | if (ampIndex == NSNotFound) return self; 40 | 41 | NSMutableString *result = [NSMutableString stringWithCapacity:(myLength * 1.25f)]; 42 | 43 | // First iteration doesn't need to scan to & since we did that already, but for code simplicity's sake we'll do it again with the scanner. 44 | NSScanner *scanner = [NSScanner scannerWithString:self]; 45 | [scanner setCharactersToBeSkipped:nil]; 46 | 47 | NSCharacterSet *boundaryCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@" \t\n\r;"]; 48 | 49 | do { 50 | NSString *nonEntityString; 51 | if ([scanner scanUpToString:@"&" intoString:&nonEntityString]) { 52 | [result appendString:nonEntityString]; 53 | } 54 | 55 | if ([scanner isAtEnd]) goto finish; 56 | 57 | if ([scanner scanString:@"&" intoString:NULL]) { 58 | [result appendString:@"&"]; 59 | } else if ([scanner scanString:@"'" intoString:NULL]) { 60 | [result appendString:@"'"]; 61 | } else if ([scanner scanString:@""" intoString:NULL]) { 62 | [result appendString:@"\""]; 63 | } else if ([scanner scanString:@"<" intoString:NULL]) { 64 | [result appendString:@"<"]; 65 | } else if ([scanner scanString:@">" intoString:NULL]) { 66 | [result appendString:@">"]; 67 | } else if ([scanner scanString:@"&#" intoString:NULL]) { 68 | BOOL gotNumber; 69 | unsigned charCode; 70 | NSString *xForHex = @""; 71 | 72 | // Is it hex or decimal? 73 | if ([scanner scanString:@"x" intoString:&xForHex]) { 74 | gotNumber = [scanner scanHexInt:&charCode]; 75 | } 76 | else { 77 | gotNumber = [scanner scanInt:(int*)&charCode]; 78 | } 79 | 80 | if (gotNumber) { 81 | [result appendFormat:@"%u", charCode]; 82 | [scanner scanString:@";" intoString:NULL]; 83 | } else { 84 | NSString *unknownEntity = nil; 85 | [scanner scanUpToCharactersFromSet:boundaryCharacterSet intoString:&unknownEntity]; 86 | [result appendFormat:@"&#%@%@", xForHex, unknownEntity]; 87 | } 88 | } else { 89 | NSString *amp; 90 | 91 | [scanner scanString:@"&" intoString:&]; //an isolated & symbol 92 | [result appendString:amp]; 93 | } 94 | } while (![scanner isAtEnd]); 95 | 96 | finish: 97 | return result; 98 | } 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /Classes/HNKit/HNEntry.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNEntry.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/4/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "NSURL+Parameters.h" 10 | 11 | #import "HNKit.h" 12 | #import "HNEntry.h" 13 | 14 | #ifdef HNKIT_RENDERING_ENABLED 15 | #import "HNObjectBodyRenderer.h" 16 | #endif 17 | 18 | @implementation HNEntry 19 | @synthesize points, children, submitter, body, posted, parent, submission, title, destination; 20 | 21 | #ifdef HNKIT_RENDERING_ENABLED 22 | @synthesize renderer; 23 | 24 | - (HNObjectBodyRenderer *)renderer { 25 | if (renderer != nil) return renderer; 26 | 27 | renderer = [[HNObjectBodyRenderer alloc] initWithObject:self]; 28 | return renderer; 29 | } 30 | #endif 31 | 32 | + (id)identifierForURL:(NSURL *)url_ { 33 | if (![self isValidURL:url_]) return NO; 34 | 35 | NSDictionary *parameters = [url_ parameterDictionary]; 36 | return [NSNumber numberWithInt:[[parameters objectForKey:@"id"] intValue]]; 37 | } 38 | 39 | + (NSString *)pathForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 40 | return @"item"; 41 | } 42 | 43 | + (NSDictionary *)parametersForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 44 | return [NSDictionary dictionaryWithObject:identifier_ forKey:@"id"]; 45 | } 46 | 47 | + (id)session:(HNSession *)session entryWithIdentifier:(id)identifier_ { 48 | return [self session:session objectWithIdentifier:identifier_]; 49 | } 50 | 51 | - (BOOL)isComment { 52 | return ![self isSubmission]; 53 | } 54 | 55 | - (BOOL)isSubmission { 56 | // Checking submission rather than something like title since this will be set 57 | // even when the entry hasn't been loaded. 58 | return [self submission] == nil; 59 | } 60 | 61 | - (void)loadFromDictionary:(NSDictionary *)response complete:(BOOL)complete { 62 | if ([response objectForKey:@"submission"]) { 63 | [self setSubmission:[HNEntry session:session entryWithIdentifier:[response objectForKey:@"submission"]]]; 64 | } 65 | 66 | if ([response objectForKey:@"parent"]) { 67 | [self setParent:[HNEntry session:session entryWithIdentifier:[response objectForKey:@"parent"]]]; 68 | 69 | // Set the submission property on the parent, as long as that's not the submission itself 70 | // (we want all submission objects to have a submission property value of nil) 71 | if (![[[self parent] identifier] isEqual:[[self submission] identifier]]) { 72 | [[self parent] setSubmission:[self submission]]; 73 | } 74 | } 75 | 76 | if ([response objectForKey:@"url"] != nil) [self setDestination:[NSURL URLWithString:[response objectForKey:@"url"]]]; 77 | if ([response objectForKey:@"user"] != nil) [self setSubmitter:[HNUser session:session userWithIdentifier:[response objectForKey:@"user"]]]; 78 | if ([response objectForKey:@"body"] != nil) [self setBody:[response objectForKey:@"body"]]; 79 | if ([response objectForKey:@"date"] != nil) [self setPosted:[response objectForKey:@"date"]]; 80 | if ([response objectForKey:@"title"] != nil) [self setTitle:[response objectForKey:@"title"]]; 81 | if ([response objectForKey:@"points"] != nil) [self setPoints:[[response objectForKey:@"points"] intValue]]; 82 | 83 | if ([response objectForKey:@"children"] != nil) { 84 | NSMutableArray *comments = [NSMutableArray array]; 85 | 86 | for (NSDictionary *child in [response objectForKey:@"children"]) { 87 | HNEntry *childEntry = [HNEntry session:session entryWithIdentifier:[child objectForKey:@"identifier"]]; 88 | 89 | [childEntry setParent:self]; 90 | [childEntry setSubmission:[self submission] ?: self]; 91 | 92 | BOOL complete = ([child objectForKey:@"children"] != nil); 93 | [childEntry loadFromDictionary:child complete:complete]; 94 | 95 | [comments addObject:childEntry]; 96 | } 97 | 98 | NSArray *allEntries = [(pendingMoreEntries ? : [NSArray array]) arrayByAddingObjectsFromArray:comments]; 99 | [self setEntries:allEntries]; 100 | } 101 | 102 | if ([response objectForKey:@"numchildren"] != nil) { 103 | NSInteger count = [[response objectForKey:@"numchildren"] intValue]; 104 | [self setChildren:count]; 105 | } else { 106 | NSInteger count = [[self entries] count]; 107 | 108 | for (HNEntry *child in [self entries]) { 109 | count += [child children]; 110 | } 111 | 112 | [self setChildren:count]; 113 | } 114 | 115 | [super loadFromDictionary:response complete:complete]; 116 | } 117 | 118 | @end 119 | -------------------------------------------------------------------------------- /Classes/XML/XMLElement.m: -------------------------------------------------------------------------------- 1 | // 2 | // XMLElement.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/10/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "XMLElement.h" 10 | #import "XMLDocument.h" 11 | 12 | static int XMLElementOutputWriteCallback(void *context, const char *buffer, int len) { 13 | NSMutableData *data = context; 14 | [data appendBytes:buffer length:len]; 15 | return len; 16 | } 17 | 18 | static int XMLElementOutputCloseCallback(void *context) { 19 | NSMutableData *data = context; 20 | [data release]; 21 | return 0; 22 | } 23 | 24 | @implementation XMLElement 25 | 26 | - (void)dealloc { 27 | [cachedChildren release]; 28 | [cachedAttributes release]; 29 | [cachedContent release]; 30 | [document release]; 31 | 32 | [super dealloc]; 33 | } 34 | 35 | - (id)initWithNode:(xmlNodePtr)node_ inDocument:(XMLDocument *)document_ { 36 | if ((self = [super init])) { 37 | node = node_; 38 | document = [document_ retain]; 39 | } 40 | 41 | return self; 42 | } 43 | 44 | - (NSUInteger)hash { 45 | return (NSUInteger) node; 46 | } 47 | 48 | - (BOOL)isEqual:(id)object { 49 | if ([object isKindOfClass:[XMLElement class]]) { 50 | XMLElement *other = (XMLElement *)object; 51 | return [other node] == node; 52 | } else { 53 | return NO; 54 | } 55 | } 56 | 57 | - (NSString *)content { 58 | if (cachedContent != nil) return cachedContent; 59 | 60 | NSMutableString *content = [[NSMutableString string] retain]; 61 | 62 | if (![self isTextNode]) { 63 | xmlNodePtr children = node->children; 64 | 65 | while (children) { 66 | NSMutableData *data = [[NSMutableData alloc] init]; 67 | xmlOutputBufferPtr buffer = xmlOutputBufferCreateIO(XMLElementOutputWriteCallback, XMLElementOutputCloseCallback, data, NULL); 68 | xmlNodeDumpOutput(buffer, [document document], children, 0, 0, "utf-8"); 69 | xmlOutputBufferFlush(buffer); 70 | [content appendString:[[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]]; 71 | xmlOutputBufferClose(buffer); 72 | 73 | children = children->next; 74 | } 75 | } else { 76 | xmlChar *nodeContent = xmlNodeGetContent(node); 77 | [content appendString:[NSString stringWithUTF8String:(char *) nodeContent]]; 78 | xmlFree(nodeContent); 79 | } 80 | 81 | cachedContent = content; 82 | return cachedContent; 83 | } 84 | 85 | 86 | - (NSString *)tagName { 87 | if ([self isTextNode]) return nil; 88 | 89 | char *nodeName = (char *) node->name; 90 | if (nodeName == NULL) nodeName = ""; 91 | 92 | NSString *name = [NSString stringWithUTF8String:nodeName]; 93 | return name; 94 | } 95 | 96 | - (NSArray *)children { 97 | if (cachedChildren != nil) return cachedChildren; 98 | 99 | xmlNodePtr list = node->children; 100 | NSMutableArray *children = [NSMutableArray array]; 101 | 102 | while (list) { 103 | XMLElement *element = [[XMLElement alloc] initWithNode:list inDocument:document]; 104 | [children addObject:[element autorelease]]; 105 | 106 | list = list->next; 107 | } 108 | 109 | cachedChildren = [children retain]; 110 | return cachedChildren; 111 | } 112 | 113 | - (NSDictionary *)attributes { 114 | if (cachedAttributes != nil) return cachedAttributes; 115 | 116 | NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; 117 | xmlAttrPtr list = node->properties; 118 | 119 | while (list) { 120 | NSString *name = nil, *value = nil; 121 | 122 | name = [NSString stringWithCString:(const char *) list->name encoding:NSUTF8StringEncoding]; 123 | if (list->children != NULL && list->children->content != NULL) { 124 | value = [NSString stringWithCString:(const char *) list->children->content encoding:NSUTF8StringEncoding]; 125 | } 126 | 127 | if (name != nil && value != nil) { 128 | [attributes setObject:value forKey:name]; 129 | } 130 | 131 | list = list->next; 132 | } 133 | 134 | cachedAttributes = [attributes retain]; 135 | return cachedAttributes; 136 | } 137 | 138 | - (NSString *)attributeWithName:(NSString *)name { 139 | return [[self attributes] objectForKey:name]; 140 | } 141 | 142 | - (BOOL)isTextNode { 143 | return node->type == XML_TEXT_NODE; 144 | } 145 | 146 | - (xmlNodePtr)node { 147 | return node; 148 | } 149 | 150 | - (NSArray *)elementsMatchingPath:(NSString *)xpath { 151 | return [document elementsMatchingPath:xpath relativeToElement:self]; 152 | } 153 | 154 | - (XMLElement *)firstElementMatchingPath:(NSString *)xpath { 155 | NSArray *elements = [self elementsMatchingPath:xpath]; 156 | 157 | if ([elements count] >= 1) { 158 | return [elements objectAtIndex:0]; 159 | } else { 160 | return nil; 161 | } 162 | } 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /Classes/HNKit/HNSessionController.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNSessionController.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 1/8/13. 6 | // 7 | // 8 | 9 | #import "HNSessionController.h" 10 | #import "HNSession.h" 11 | #import "HNAnonymousSession.h" 12 | #import "HNObjectCache.h" 13 | 14 | NSString *kHNSessionControllerSessionsChangedNotification = @"HNSessionControllerSessionsChangedNotification"; 15 | 16 | @implementation HNSessionController 17 | @synthesize recentSession; 18 | 19 | + (id)sessionController { 20 | static HNSessionController *sessionController = nil; 21 | 22 | static dispatch_once_t sessionControllerToken; 23 | dispatch_once(&sessionControllerToken, ^{ 24 | sessionController = [[HNSessionController alloc] init]; 25 | }); 26 | 27 | return sessionController; 28 | } 29 | 30 | - (id)init { 31 | if ((self = [super init])) { 32 | sessions = [[NSMutableArray alloc] init]; 33 | 34 | // FIXME: This should use the Keychain, not NSUserDefaults. 35 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 36 | 37 | NSArray *allSessionsArray = [userDefaults objectForKey:@"HNKit:HNSessionController:Sessions"]; 38 | NSString *recentIdentifier = [userDefaults objectForKey:@"HNKit:HNSessionController:RecentSession"]; 39 | 40 | for (NSDictionary *sessionDictionary in allSessionsArray) { 41 | HNSession *session = [[HNSession alloc] initWithSessionDictionary:sessionDictionary]; 42 | [sessions addObject:session]; 43 | [session release]; 44 | 45 | if ([recentIdentifier isEqualToString:[session identifier]]) { 46 | [self setRecentSession:session]; 47 | } 48 | } 49 | 50 | HNSessionToken token = (HNSessionToken) [userDefaults objectForKey:@"HNKit:SessionToken"]; 51 | NSString *password = [userDefaults objectForKey:@"HNKit:SessionPassword"]; 52 | NSString *name = [userDefaults objectForKey:@"HNKit:SessionName"]; 53 | 54 | if (token != nil && password != nil && name != nil) { 55 | // Restore old-style single-account sessions. 56 | HNSession *session = [[HNSession alloc] initWithUsername:name password:password token:token]; 57 | [self addSession:session]; 58 | [self setRecentSession:session]; 59 | [session release]; 60 | 61 | [userDefaults removeObjectForKey:@"HNKit:SessionToken"]; 62 | [userDefaults removeObjectForKey:@"HNKit:SessionPassword"]; 63 | [userDefaults removeObjectForKey:@"HNKit:SessionName"]; 64 | } 65 | } 66 | 67 | return self; 68 | } 69 | 70 | - (NSArray *)sessions { 71 | return [[sessions copy] autorelease]; 72 | } 73 | 74 | - (void)refresh { 75 | for (HNSession *session in sessions) { 76 | [session reloadToken]; 77 | } 78 | } 79 | 80 | - (NSInteger)numberOfSessions { 81 | return [sessions count]; 82 | } 83 | 84 | - (void)setRecentSession:(HNSession *)recentSession_ { 85 | // This wouldn't even make sense. 86 | if ([recentSession_ isAnonymous]) return; 87 | 88 | [recentSession release]; 89 | recentSession = [recentSession_ retain]; 90 | 91 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 92 | [userDefaults setObject:[recentSession identifier] forKey:@"HNKit:HNSessionController:RecentSession"]; 93 | [userDefaults synchronize]; 94 | } 95 | 96 | - (void)saveSessions { 97 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 98 | NSMutableArray *allSessionsArray = [NSMutableArray array]; 99 | 100 | for (HNSession *session in sessions) { 101 | NSDictionary *sessionDictionary = [session sessionDictionary]; 102 | [allSessionsArray addObject:sessionDictionary]; 103 | } 104 | 105 | [userDefaults setObject:allSessionsArray forKey:@"HNKit:HNSessionController:Sessions"]; 106 | [userDefaults synchronize]; 107 | } 108 | 109 | - (void)addSession:(HNSession *)session { 110 | NSAssert(![session isAnonymous], @"Sessions must not be anonymous."); 111 | 112 | [sessions addObject:session]; 113 | [self saveSessions]; 114 | 115 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNSessionControllerSessionsChangedNotification object:self]; 116 | } 117 | 118 | - (void)removeSession:(HNSession *)session { 119 | [[session cache] clearPersistentCache]; 120 | 121 | [sessions removeObject:session]; 122 | [self saveSessions]; 123 | 124 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNSessionControllerSessionsChangedNotification object:self]; 125 | } 126 | 127 | - (void)moveSession:(HNSession *)session toIndex:(NSInteger)index { 128 | NSInteger from = [sessions indexOfObject:session]; 129 | 130 | [session retain]; 131 | [sessions removeObjectAtIndex:from]; 132 | 133 | if (index >= [sessions count]) { 134 | [sessions addObject:session]; 135 | } else { 136 | [sessions insertObject:session atIndex:index]; 137 | } 138 | 139 | [session release]; 140 | 141 | [self saveSessions]; 142 | 143 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNSessionControllerSessionsChangedNotification object:self]; 144 | } 145 | 146 | @end 147 | -------------------------------------------------------------------------------- /Classes/HNKit/HNAPISearch.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNAPISearch.m 3 | // newsyc 4 | // 5 | // Created by Quin Hoxie on 6/2/11. 6 | // 7 | 8 | #import "HNAPISearch.h" 9 | #import "HNKit.h" 10 | #import "NSDate+TimeAgo.h" 11 | 12 | @class HNEntry; 13 | 14 | @interface HNAPISearch () 15 | @end 16 | 17 | @implementation HNAPISearch 18 | 19 | @synthesize entries; 20 | @synthesize responseData; 21 | @synthesize searchType; 22 | @synthesize session; 23 | @synthesize dateFormatter; 24 | 25 | - (id)initWithSession:(HNSession *)session_ { 26 | if (self = [super init]) { 27 | session = session_; 28 | [self setSearchType:kHNSearchTypeInteresting]; 29 | } 30 | 31 | return self; 32 | } 33 | 34 | #pragma mark NSURLConnectionDelegate 35 | 36 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { 37 | [responseData setLength:0]; 38 | } 39 | 40 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 41 | if (responseData == nil) { 42 | responseData = [[NSMutableData alloc] init]; 43 | } 44 | 45 | [responseData appendData:data]; 46 | } 47 | 48 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { 49 | [connection release]; 50 | [responseData release]; 51 | responseData = nil; 52 | 53 | NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; 54 | [notificationCenter postNotificationName:@"searchDone" object:nil userInfo:nil]; 55 | } 56 | 57 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection { 58 | [connection release]; 59 | 60 | [self handleResponse]; 61 | } 62 | 63 | - (void)handleResponse { 64 | id responseJSON = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:NULL]; 65 | NSArray *rawResults = [[NSArray alloc] initWithArray:[responseJSON objectForKey:@"hits"]]; 66 | 67 | self.entries = [NSMutableArray array]; 68 | 69 | for (NSDictionary *result in rawResults) { 70 | NSDictionary *item = [self itemFromRaw:result]; 71 | HNEntry *entry = [HNEntry session:session entryWithIdentifier:[item objectForKey:@"identifier"]]; 72 | 73 | [entry loadFromDictionary:item complete:NO]; 74 | [entries addObject:entry]; 75 | } 76 | 77 | [rawResults release]; 78 | rawResults = nil; 79 | [responseData release]; 80 | responseData = nil; 81 | 82 | NSDictionary *dictToBePassed = [NSDictionary dictionaryWithObject:entries forKey:@"array"]; 83 | NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; 84 | [notificationCenter postNotificationName:@"searchDone" object:nil userInfo:dictToBePassed]; 85 | } 86 | 87 | - (NSDictionary *)itemFromRaw:(NSDictionary *)rawDictionary { 88 | NSMutableDictionary *item = [NSMutableDictionary dictionary]; 89 | NSNumber *points = nil; 90 | NSNumber *comments = nil; 91 | NSString *title = nil; 92 | NSString *user = nil; 93 | NSNumber *identifier = nil; 94 | NSString *body = nil; 95 | NSString *date = nil; 96 | NSString *url = nil; 97 | 98 | points = [rawDictionary valueForKey:@"points"]; 99 | comments = [rawDictionary valueForKey:@"num_comments"]; 100 | title = [rawDictionary valueForKey:@"title"]; 101 | user = [rawDictionary valueForKey:@"author"]; 102 | identifier = [rawDictionary valueForKey:@"objectID"]; 103 | body = [rawDictionary valueForKey:@"comment_text"]; 104 | date = [rawDictionary valueForKey:@"created_at"]; 105 | url = [rawDictionary valueForKey:@"url"]; 106 | 107 | if (self.dateFormatter == nil) { 108 | self.dateFormatter = [[NSDateFormatter alloc] init]; 109 | [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; 110 | [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z"]; 111 | } 112 | NSDate *parsedDate = [dateFormatter dateFromString:date]; 113 | NSString *timeAgo = [parsedDate timeAgoInWords]; 114 | 115 | if (!timeAgo) { 116 | timeAgo = @""; 117 | } 118 | 119 | if ((NSNull *)user != [NSNull null]) [item setObject:user forKey:@"user"]; 120 | if ((NSNull *)points != [NSNull null]) [item setObject:points forKey:@"points"]; 121 | if ((NSNull *)title != [NSNull null]) [item setObject:title forKey:@"title"]; 122 | if ((NSNull *)comments != [NSNull null]) [item setObject:comments forKey:@"numchildren"]; 123 | if ((NSNull *)url != [NSNull null]) [item setObject:url forKey:@"url"]; 124 | if ((NSNull *)timeAgo != [NSNull null]) [item setObject:timeAgo forKey:@"date"]; 125 | if ((NSNull *)body != [NSNull null]) [item setObject:body forKey:@"body"]; 126 | if ((NSNull *)identifier != [NSNull null]) [item setObject:identifier forKey:@"identifier"]; 127 | return item; 128 | } 129 | 130 | - (void)performSearch:(NSString *)searchQuery { 131 | NSString *paramsString = nil; 132 | NSString *encodedQuery = [searchQuery stringByURLEncodingString]; 133 | if (searchType == kHNSearchTypeInteresting) { 134 | paramsString = [NSString stringWithFormat:kHNSearchParamsInteresting, encodedQuery]; 135 | } else { 136 | paramsString = [NSString stringWithFormat:kHNSearchParamsRecent, encodedQuery]; 137 | } 138 | 139 | NSString *urlString = [NSString stringWithFormat:kHNSearchBaseURL, paramsString]; 140 | NSURL *url = [NSURL URLWithString:urlString]; 141 | 142 | NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url]; 143 | NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; 144 | [connection start]; 145 | [request release]; 146 | 147 | searchQuery = nil; 148 | } 149 | 150 | - (void)dealloc { 151 | [responseData release]; 152 | [entries release]; 153 | [dateFormatter release]; 154 | 155 | [super dealloc]; 156 | } 157 | 158 | @end 159 | -------------------------------------------------------------------------------- /Classes/Categories/NSObject+PerformSelector.m: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2009-2011 Facebook 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #import "NSObject+PerformSelector.h" 18 | 19 | @implementation NSObject (PerformSelector) 20 | 21 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3 { 22 | NSMethodSignature *sig = [self methodSignatureForSelector:selector]; 23 | if (sig) { 24 | NSInvocation* invo = [NSInvocation invocationWithMethodSignature:sig]; 25 | [invo setTarget:self]; 26 | [invo setSelector:selector]; 27 | [invo setArgument:&p1 atIndex:2]; 28 | [invo setArgument:&p2 atIndex:3]; 29 | [invo setArgument:&p3 atIndex:4]; 30 | [invo invoke]; 31 | if (sig.methodReturnLength) { 32 | id anObject; 33 | [invo getReturnValue:&anObject]; 34 | return anObject; 35 | 36 | } else { 37 | return nil; 38 | } 39 | 40 | } else { 41 | return nil; 42 | } 43 | } 44 | 45 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3 46 | withObject:(id)p4 { 47 | NSMethodSignature *sig = [self methodSignatureForSelector:selector]; 48 | if (sig) { 49 | NSInvocation* invo = [NSInvocation invocationWithMethodSignature:sig]; 50 | [invo setTarget:self]; 51 | [invo setSelector:selector]; 52 | [invo setArgument:&p1 atIndex:2]; 53 | [invo setArgument:&p2 atIndex:3]; 54 | [invo setArgument:&p3 atIndex:4]; 55 | [invo setArgument:&p4 atIndex:5]; 56 | [invo invoke]; 57 | if (sig.methodReturnLength) { 58 | id anObject; 59 | [invo getReturnValue:&anObject]; 60 | return anObject; 61 | 62 | } else { 63 | return nil; 64 | } 65 | 66 | } else { 67 | return nil; 68 | } 69 | } 70 | 71 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3 72 | withObject:(id)p4 withObject:(id)p5 { 73 | NSMethodSignature *sig = [self methodSignatureForSelector:selector]; 74 | if (sig) { 75 | NSInvocation* invo = [NSInvocation invocationWithMethodSignature:sig]; 76 | [invo setTarget:self]; 77 | [invo setSelector:selector]; 78 | [invo setArgument:&p1 atIndex:2]; 79 | [invo setArgument:&p2 atIndex:3]; 80 | [invo setArgument:&p3 atIndex:4]; 81 | [invo setArgument:&p4 atIndex:5]; 82 | [invo setArgument:&p5 atIndex:6]; 83 | [invo invoke]; 84 | if (sig.methodReturnLength) { 85 | id anObject; 86 | [invo getReturnValue:&anObject]; 87 | return anObject; 88 | 89 | } else { 90 | return nil; 91 | } 92 | 93 | } else { 94 | return nil; 95 | } 96 | } 97 | 98 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3 99 | withObject:(id)p4 withObject:(id)p5 withObject:(id)p6 { 100 | NSMethodSignature *sig = [self methodSignatureForSelector:selector]; 101 | if (sig) { 102 | NSInvocation* invo = [NSInvocation invocationWithMethodSignature:sig]; 103 | [invo setTarget:self]; 104 | [invo setSelector:selector]; 105 | [invo setArgument:&p1 atIndex:2]; 106 | [invo setArgument:&p2 atIndex:3]; 107 | [invo setArgument:&p3 atIndex:4]; 108 | [invo setArgument:&p4 atIndex:5]; 109 | [invo setArgument:&p5 atIndex:6]; 110 | [invo setArgument:&p6 atIndex:7]; 111 | [invo invoke]; 112 | if (sig.methodReturnLength) { 113 | id anObject; 114 | [invo getReturnValue:&anObject]; 115 | return anObject; 116 | 117 | } else { 118 | return nil; 119 | } 120 | 121 | } else { 122 | return nil; 123 | } 124 | } 125 | 126 | - (id)performSelector:(SEL)selector withObject:(id)p1 withObject:(id)p2 withObject:(id)p3 127 | withObject:(id)p4 withObject:(id)p5 withObject:(id)p6 withObject:(id)p7 { 128 | NSMethodSignature *sig = [self methodSignatureForSelector:selector]; 129 | if (sig) { 130 | NSInvocation* invo = [NSInvocation invocationWithMethodSignature:sig]; 131 | [invo setTarget:self]; 132 | [invo setSelector:selector]; 133 | [invo setArgument:&p1 atIndex:2]; 134 | [invo setArgument:&p2 atIndex:3]; 135 | [invo setArgument:&p3 atIndex:4]; 136 | [invo setArgument:&p4 atIndex:5]; 137 | [invo setArgument:&p5 atIndex:6]; 138 | [invo setArgument:&p6 atIndex:7]; 139 | [invo setArgument:&p7 atIndex:8]; 140 | [invo invoke]; 141 | if (sig.methodReturnLength) { 142 | id anObject; 143 | [invo getReturnValue:&anObject]; 144 | return anObject; 145 | 146 | } else { 147 | return nil; 148 | } 149 | 150 | } else { 151 | return nil; 152 | } 153 | } 154 | 155 | @end 156 | -------------------------------------------------------------------------------- /Classes/HNKit/HNSessionAuthenticator.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNSessionAuthenticator.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/21/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNSessionAuthenticator.h" 11 | #import "HNNetworkActivityController.h" 12 | 13 | #import "XMLDocument.h" 14 | 15 | #import "NSDictionary+Parameters.h" 16 | 17 | @implementation HNSessionAuthenticator 18 | @synthesize delegate; 19 | 20 | - (void)dealloc { 21 | if (connection != nil) [connection cancel]; 22 | [connection release]; 23 | [username release]; 24 | [password release]; 25 | 26 | [super dealloc]; 27 | } 28 | 29 | - (id)initWithUsername:(NSString *)username_ password:(NSString *)password_ { 30 | if ((self = [super init])) { 31 | password = [password_ copy]; 32 | username = [username_ copy]; 33 | } 34 | 35 | return self; 36 | } 37 | 38 | - (void)_failAuthentication { 39 | if ([delegate respondsToSelector:@selector(sessionAuthenticatorDidRecieveFailure:)]) { 40 | [delegate sessionAuthenticatorDidRecieveFailure:self]; 41 | } 42 | } 43 | 44 | - (void)_completeAuthenticationWithToken:(HNSessionToken)token { 45 | if ([delegate respondsToSelector:@selector(sessionAuthenticator:didRecieveToken:)]) { 46 | [delegate sessionAuthenticator:self didRecieveToken:token]; 47 | } 48 | } 49 | 50 | - (void)_clearConnection { 51 | if (connection != nil) { 52 | [HNNetworkActivityController networkActivityEnded]; 53 | [connection release]; 54 | connection = nil; 55 | } 56 | } 57 | 58 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection { 59 | [self _failAuthentication]; 60 | [self _clearConnection]; 61 | } 62 | 63 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { 64 | [self _failAuthentication]; 65 | [self _clearConnection]; 66 | } 67 | 68 | - (NSURLRequest *)connection:(NSURLConnection *)connection_ willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response { 69 | if (response == nil) { 70 | // not from a server redirect 71 | return request; 72 | } 73 | 74 | if (response != nil && [response isKindOfClass:[NSHTTPURLResponse class]]) { 75 | NSHTTPURLResponse *http = (NSHTTPURLResponse *) response; 76 | 77 | NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[http allHeaderFields] forURL:[http URL]]; 78 | for (NSHTTPCookie *cookie in cookies) { 79 | if ([[cookie name] isEqual:@"user"]) { 80 | [self _completeAuthenticationWithToken:(HNSessionToken) [cookie value]]; 81 | 82 | [connection cancel]; 83 | [self _clearConnection]; 84 | 85 | return nil; 86 | } 87 | } 88 | } 89 | 90 | [self _failAuthentication]; 91 | [connection cancel]; 92 | [self _clearConnection]; 93 | 94 | return nil; 95 | } 96 | 97 | - (NSString *)_generateLoginPageURL { 98 | NSData *data = [NSData dataWithContentsOfURL:kHNWebsiteURL]; 99 | XMLDocument *document = [[[XMLDocument alloc] initWithHTMLData:data] autorelease]; 100 | if (document == nil) return nil; 101 | 102 | XMLElement *element = [document firstElementMatchingPath:@"//span[@class='pagetop']//a[text()='login']"]; 103 | return [element attributeWithName:@"href"]; 104 | } 105 | 106 | - (NSString *)_loginURLFromLoginDocument:(XMLDocument *)loginDocument { 107 | XMLElement *element = [loginDocument firstElementMatchingPath:@"//form"]; 108 | return [element attributeWithName:@"action"]; 109 | } 110 | 111 | - (void)_sendAuthenticationRequest:(NSURLRequest *)request { 112 | connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; 113 | [connection start]; 114 | [HNNetworkActivityController networkActivityBegan]; 115 | } 116 | 117 | - (void)_performAuthentication { 118 | NSString *loginurl = nil; 119 | NSString *submiturl = nil; 120 | 121 | loginurl = [self _generateLoginPageURL]; 122 | 123 | if (loginurl != nil) { 124 | NSURL *url = [[NSURL URLWithString:loginurl relativeToURL:kHNWebsiteURL] absoluteURL]; 125 | NSData *data = [NSData dataWithContentsOfURL:url]; 126 | XMLDocument *document = [[XMLDocument alloc] initWithHTMLData:data]; 127 | 128 | if (document != nil) { 129 | submiturl = [self _loginURLFromLoginDocument:document]; 130 | } 131 | 132 | [document release]; 133 | } 134 | 135 | if (loginurl == nil || submiturl == nil) { 136 | [self performSelectorOnMainThread:@selector(_failAuthentication) withObject:nil waitUntilDone:YES]; 137 | return; 138 | } 139 | 140 | NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys: 141 | username, @"acct", 142 | password, @"pw", 143 | nil]; 144 | 145 | NSURL *submitURL = [[NSURL URLWithString:submiturl relativeToURL:kHNWebsiteURL] absoluteURL]; 146 | 147 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:submitURL]; 148 | [request setHTTPMethod:@"POST"]; 149 | [request setHTTPShouldHandleCookies:NO]; 150 | 151 | // Take the slice [1:] so avoid the question mark that doesn't make sense in POST requests. 152 | // XXX: is that an issue with this category in general? 153 | [request setHTTPBody:[[[query queryString] substringFromIndex:1] dataUsingEncoding:NSUTF8StringEncoding]]; 154 | 155 | // The NSURLRequest object must be created on the main thread, or else it 156 | // will be destroyed when this thread exits (now), which is not what we want. 157 | [self performSelectorOnMainThread:@selector(_sendAuthenticationRequest:) withObject:request waitUntilDone:YES]; 158 | } 159 | 160 | - (void)_authenticateWrapper { 161 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 162 | [self _performAuthentication]; 163 | [pool release]; 164 | } 165 | 166 | - (void)beginAuthenticationRequest { 167 | [NSThread detachNewThreadSelector:@selector(_authenticateWrapper) toTarget:self withObject:nil]; 168 | } 169 | 170 | @end 171 | -------------------------------------------------------------------------------- /Classes/HNKit/HNObjectCache.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNObjectCache.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 7/15/12. 6 | // 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNObjectCache.h" 11 | #import "HNSession.h" 12 | 13 | #import "NSDictionary+Parameters.h" 14 | #import "NSString+URLEncoding.h" 15 | #import "NSString+Entities.h" 16 | #import "NSString+Tags.h" 17 | 18 | @interface HNObjectCacheKey : NSObject { 19 | Class cls; 20 | id identifier; 21 | NSDictionary *info; 22 | } 23 | 24 | @end 25 | 26 | @implementation HNObjectCacheKey 27 | 28 | #pragma mark - Lifecycle 29 | 30 | - (id)initWithClass:(Class)cls_ identifier:(id)identifier_ infoDictionary:(NSDictionary *)info_ { 31 | if ((self = [super init])) { 32 | cls = cls_; 33 | identifier = [identifier_ copy]; 34 | info = [info_ copy]; 35 | } 36 | 37 | return self; 38 | } 39 | 40 | - (void)dealloc { 41 | [identifier release]; 42 | [info release]; 43 | 44 | [super dealloc]; 45 | } 46 | 47 | + (id)objectCacheWithClass:(Class)cls_ identifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 48 | return [[[self alloc] initWithClass:cls_ identifier:identifier_ infoDictionary:info] autorelease]; 49 | } 50 | 51 | + (id)objectCacheForObject:(HNObject *)object { 52 | return [self objectCacheWithClass:[object class] identifier:[object identifier] infoDictionary:[object infoDictionary]]; 53 | } 54 | 55 | + (NSString *)persistentCacheIdentiferForClass:(Class)cls_ identifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 56 | NSString *infoString = @""; 57 | 58 | if (info != nil) { 59 | infoString = [@"-" stringByAppendingString:[[info queryString] stringByURLEncodingString]]; 60 | } 61 | 62 | return [NSString stringWithFormat:@"%@-%@%@", cls_, identifier_, infoString]; 63 | } 64 | 65 | - (NSString *)persistentCacheIdentifier { 66 | return [[self class] persistentCacheIdentiferForClass:cls identifier:identifier infoDictionary:info]; 67 | } 68 | 69 | #pragma mark - Properties 70 | 71 | - (Class)objectClass { 72 | return cls; 73 | } 74 | 75 | - (id)objectIdentifier { 76 | return identifier; 77 | } 78 | 79 | - (NSDictionary *)objectInfoDictionary { 80 | return info; 81 | } 82 | 83 | #pragma mark - NSCopying 84 | 85 | - (id)copyWithZone:(NSZone *)zone { 86 | return [[[self class] allocWithZone:zone] initWithClass:cls identifier:identifier infoDictionary:info]; 87 | } 88 | 89 | #pragma mark - NSObject 90 | 91 | - (BOOL)isEqual:(id)object_ { 92 | BOOL classes = cls == [object_ objectClass]; 93 | BOOL identifiers = [identifier isEqual:[object_ objectIdentifier]]; 94 | BOOL infos = [info isEqualToDictionary:[object_ objectInfoDictionary]] || (info == nil && [object_ objectInfoDictionary] == nil); 95 | 96 | return classes && identifiers && infos; 97 | } 98 | 99 | - (NSUInteger)hash { 100 | return [cls hash] ^ [identifier hash] ^ [info hash]; 101 | } 102 | 103 | - (NSString *)description { 104 | return [NSString stringWithFormat:@"<%@:%p class=%@ identifier=%@ info=%p>", [self class], self, cls, identifier, info]; 105 | } 106 | 107 | @end 108 | 109 | @implementation HNObjectCache 110 | 111 | - (NSMutableDictionary *)cacheDictionary { 112 | return cacheDictionary; 113 | } 114 | 115 | - (NSString *)persistentCachePath { 116 | NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); 117 | 118 | if ([cachePaths count] > 0) { 119 | NSString *cachePath = [cachePaths objectAtIndex:0]; 120 | NSString *objectPath = [cachePath stringByAppendingPathComponent:@"HNObjectCache"]; 121 | objectPath = [objectPath stringByAppendingPathComponent:[session identifier]]; 122 | return objectPath; 123 | } else { 124 | return nil; 125 | } 126 | } 127 | 128 | - (void)clearPersistentCache { 129 | NSString *cachePath = [self persistentCachePath]; 130 | 131 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 132 | NSFileManager *fileManager = [NSFileManager defaultManager]; 133 | [fileManager removeItemAtPath:cachePath error:NULL]; 134 | }); 135 | } 136 | 137 | - (NSString *)persistentCachePathForKey:(HNObjectCacheKey *)key { 138 | NSString *cachePath = [self persistentCachePath]; 139 | NSString *keyPath = [key persistentCacheIdentifier]; 140 | NSString *objectPath = [cachePath stringByAppendingPathComponent:keyPath]; 141 | 142 | return objectPath; 143 | } 144 | 145 | - (BOOL)persistentCacheHasObjectForKey:(HNObjectCacheKey *)key { 146 | NSString *path = [self persistentCachePathForKey:key]; 147 | NSFileManager *fileManager = [NSFileManager defaultManager]; 148 | return [fileManager fileExistsAtPath:path]; 149 | } 150 | 151 | - (NSDictionary *)persistentCacheDictionaryForKey:(HNObjectCacheKey *)key { 152 | NSString *path = [self persistentCachePathForKey:key]; 153 | return [NSDictionary dictionaryWithContentsOfFile:path]; 154 | } 155 | 156 | - (void)updateObjectFromPersistentCache:(HNObject *)object { 157 | HNObjectCacheKey *key = [HNObjectCacheKey objectCacheForObject:object]; 158 | 159 | if ([self persistentCacheHasObjectForKey:key] && ![object isLoaded]) { 160 | NSDictionary *cachedDictionary = [self persistentCacheDictionaryForKey:key]; 161 | [object loadFromDictionary:cachedDictionary complete:YES]; 162 | } 163 | } 164 | 165 | - (void)savePersistentCacheDictionary:(NSDictionary *)dict forObject:(HNObject *)object { 166 | HNObjectCacheKey *key = [HNObjectCacheKey objectCacheForObject:object]; 167 | NSString *path = [self persistentCachePathForKey:key]; 168 | 169 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 170 | [dict writeToFile:path atomically:YES]; 171 | }); 172 | } 173 | 174 | - (void)createPersistentCache { 175 | NSFileManager *fileManager = [NSFileManager defaultManager]; 176 | [fileManager createDirectoryAtPath:[self persistentCachePath] withIntermediateDirectories:YES attributes:nil error:NULL]; 177 | } 178 | 179 | - (id)initWithSession:(HNSession *)session_ { 180 | if ((self = [super init])) { 181 | session = session_; 182 | 183 | cacheDictionary = [[NSMutableDictionary alloc] init]; 184 | } 185 | 186 | return self; 187 | } 188 | 189 | - (void)dealloc { 190 | [cacheDictionary release]; 191 | 192 | [super dealloc]; 193 | } 194 | 195 | - (HNObject *)objectFromCacheWithKey:(HNObjectCacheKey *)key { 196 | NSMutableDictionary *cache = [self cacheDictionary]; 197 | HNObject *object = [cache objectForKey:key]; 198 | return object; 199 | } 200 | 201 | - (HNObject *)objectFromCacheWithClass:(Class)cls_ identifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 202 | HNObjectCacheKey *key = [HNObjectCacheKey objectCacheWithClass:cls_ identifier:identifier_ infoDictionary:info]; 203 | return [self objectFromCacheWithKey:key]; 204 | } 205 | 206 | - (BOOL)cacheHasObject:(HNObject *)object { 207 | HNObjectCacheKey *key = [HNObjectCacheKey objectCacheForObject:object]; 208 | return ([self objectFromCacheWithKey:key] != nil); 209 | } 210 | 211 | - (void)addObjectToCache:(HNObject *)object { 212 | HNObjectCacheKey *key = [HNObjectCacheKey objectCacheForObject:object]; 213 | 214 | NSMutableDictionary *cache = [self cacheDictionary]; 215 | [cache setObject:object forKey:key]; 216 | } 217 | 218 | @end 219 | -------------------------------------------------------------------------------- /Classes/HNKit/HNAPISubmission.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNAPISubmission.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/30/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNKit.h" 10 | #import "HNAPISubmission.h" 11 | #import "HNNetworkActivityController.h" 12 | 13 | #import "XMLDocument.h" 14 | 15 | #import "NSDictionary+Parameters.h" 16 | 17 | @implementation HNAPISubmission 18 | @synthesize submission; 19 | 20 | - (void)dealloc { 21 | [submission release]; 22 | 23 | [super dealloc]; 24 | } 25 | 26 | - (id)initWithSession:(HNSession *)session_ submission:(HNSubmission *)submission_ { 27 | if ((self = [super init])) { 28 | session = session_; 29 | submission = [submission_ retain]; 30 | loadingState = kHNAPISubmissionLoadingStateReady; 31 | } 32 | 33 | return self; 34 | } 35 | 36 | - (void)_completedSuccessfully:(BOOL)successfully withError:(NSError *)error { 37 | loadingState = kHNAPISubmissionLoadingStateReady; 38 | 39 | if ([submission respondsToSelector:@selector(submissionCompletedSuccessfully:withError:)]) 40 | [submission submissionCompletedSuccessfully:successfully withError:error]; 41 | } 42 | 43 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection_ { 44 | [HNNetworkActivityController networkActivityEnded]; 45 | 46 | NSString *result = [[[NSString alloc] initWithData:received encoding:NSUTF8StringEncoding] autorelease]; 47 | [received release]; 48 | received = nil; 49 | [connection release]; 50 | connection = nil; 51 | 52 | if (loadingState == kHNAPISubmissionLoadingStateFormTokens) { 53 | loadingState = kHNAPISubmissionLoadingStateFormSubmit; 54 | 55 | XMLDocument *document = [[XMLDocument alloc] initWithHTMLData:[result dataUsingEncoding:NSUTF8StringEncoding]]; 56 | [document autorelease]; 57 | 58 | NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] init] autorelease]; 59 | [session addCookiesToRequest:request]; 60 | 61 | if ([submission type] == kHNSubmissionTypeSubmission) { 62 | XMLElement *element = [document firstElementMatchingPath:@"//input[@name='fnid']"]; 63 | 64 | NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys: 65 | [element attributeWithName:@"value"], @"fnid", 66 | [submission title] ?: @"", @"t", 67 | [[submission destination] absoluteString] ?: @"", @"u", 68 | [submission body] ?: @"", @"x", 69 | nil]; 70 | 71 | [request setURL:[[NSURL URLWithString:@"/r" relativeToURL:kHNWebsiteURL] absoluteURL]]; 72 | [request setHTTPMethod:@"POST"]; 73 | [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; 74 | [request setHTTPBody:[[[query queryString] substringFromIndex:1] dataUsingEncoding:NSUTF8StringEncoding]]; 75 | } else if ([submission type] == kHNSubmissionTypeVote) { 76 | NSString *dir = [submission direction] == kHNVoteDirectionUp ? @"up" : @"down"; 77 | NSString *query = [NSString stringWithFormat:@"//a[@id='%@_%@']", dir, [[submission target] identifier]]; 78 | XMLElement *element = [document firstElementMatchingPath:query]; 79 | 80 | if (element == nil) { 81 | NSError *error = [NSError errorWithDomain:@"" code:0 userInfo:[NSDictionary dictionaryWithObject:@"Voting not allowed." forKey:NSLocalizedDescriptionKey]]; 82 | [self _completedSuccessfully:NO withError:error]; 83 | return; 84 | } else { 85 | NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/%@", kHNWebsiteHost, [element attributeWithName:@"href"]]]; 86 | [request setURL:url]; 87 | } 88 | } else if ([submission type] == kHNSubmissionTypeReply) { 89 | XMLElement *element = [document firstElementMatchingPath:@"//input[@name='fnid']"]; 90 | 91 | if (element == nil) { 92 | NSError *error = [NSError errorWithDomain:@"" code:0 userInfo:[NSDictionary dictionaryWithObject:@"Replying not allowed." forKey:NSLocalizedDescriptionKey]]; 93 | [self _completedSuccessfully:NO withError:error]; 94 | return; 95 | } else { 96 | NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys: 97 | [element attributeWithName:@"value"], @"fnid", 98 | [submission body], @"text", 99 | nil]; 100 | 101 | [request setURL:[[NSURL URLWithString:@"/r" relativeToURL:kHNWebsiteURL] absoluteURL]]; 102 | [request setHTTPMethod:@"POST"]; 103 | [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; 104 | [request setHTTPBody:[[[query queryString] substringFromIndex:1] dataUsingEncoding:NSUTF8StringEncoding]]; 105 | } 106 | } else if ([submission type] == kHNSubmissionTypeFlag) { 107 | XMLElement *element = [document firstElementMatchingPath:@"//a[text()='flag' and starts-with(@href,'/r?fnid=')]"]; 108 | 109 | if (element == nil) { 110 | NSError *error = [NSError errorWithDomain:@"" code:0 userInfo:[NSDictionary dictionaryWithObject:@"Flagging not allowed." forKey:NSLocalizedDescriptionKey]]; 111 | [self _completedSuccessfully:NO withError:error]; 112 | return; 113 | } else { 114 | NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@%@", kHNWebsiteHost, [element attributeWithName:@"href"]]]; 115 | [request setURL:url]; 116 | } 117 | } 118 | 119 | connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; 120 | [connection start]; 121 | 122 | [HNNetworkActivityController networkActivityBegan]; 123 | } else if (loadingState == kHNAPISubmissionLoadingStateFormSubmit) { 124 | [self _completedSuccessfully:YES withError:nil]; 125 | } 126 | } 127 | 128 | - (void)connection:(NSURLConnection *)connection_ didFailWithError:(NSError *)error { 129 | [HNNetworkActivityController networkActivityEnded]; 130 | 131 | [received release]; 132 | received = nil; 133 | 134 | [self _completedSuccessfully:NO withError:error]; 135 | } 136 | 137 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 138 | [received appendData:data]; 139 | } 140 | 141 | - (void)performSubmission { 142 | received = [[NSMutableData alloc] init]; 143 | 144 | loadingState = kHNAPISubmissionLoadingStateFormTokens; 145 | 146 | NSURL *url = nil; 147 | 148 | if ([submission type] == kHNSubmissionTypeSubmission) { 149 | NSString *base = [NSString stringWithFormat:@"http://%@/%@", kHNWebsiteHost, @"submit"]; 150 | url = [NSURL URLWithString:base]; 151 | } else { 152 | url = [[submission target] URL]; 153 | } 154 | 155 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 156 | [session addCookiesToRequest:request]; 157 | 158 | connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; 159 | [connection start]; 160 | 161 | [HNNetworkActivityController networkActivityBegan]; 162 | } 163 | 164 | - (BOOL)isLoading { 165 | return loadingState != kHNAPISubmissionLoadingStateReady; 166 | } 167 | 168 | @end 169 | -------------------------------------------------------------------------------- /Classes/HNKit/HNObject.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNObject.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 3/4/11. 6 | // Copyright 2011 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "NSURL+Parameters.h" 10 | #import "NSDictionary+Parameters.h" 11 | 12 | #import "HNKit.h" 13 | #import "HNObject.h" 14 | #import "HNSession.h" 15 | #import "HNObjectCache.h" 16 | 17 | @interface HNObject () 18 | 19 | @property (nonatomic, assign, readwrite) HNSession *session; 20 | 21 | @end 22 | 23 | @implementation HNObject 24 | @synthesize identifier=identifier, URL=url, session=session; 25 | @synthesize loadingState; 26 | 27 | + (BOOL)isValidURL:(NSURL *)url_ { 28 | if (url_ == nil) return NO; 29 | if (![[url_ scheme] isEqualToString:@"http"] && ![[url_ scheme] isEqualToString:@"https"]) return NO; 30 | if (![[url_ host] isEqualToString:kHNWebsiteHost]) return NO; 31 | 32 | return YES; 33 | } 34 | 35 | + (NSDictionary *)infoDictionaryForURL:(NSURL *)url_ { 36 | return nil; 37 | } 38 | 39 | + (id)identifierForURL:(NSURL *)url_ { 40 | return nil; 41 | } 42 | 43 | + (NSString *)pathForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 44 | return nil; 45 | } 46 | 47 | + (NSDictionary *)parametersForURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 48 | return nil; 49 | } 50 | 51 | + (NSURL *)generateURLWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 52 | NSDictionary *parameters = [self parametersForURLWithIdentifier:identifier_ infoDictionary:info]; 53 | NSString *path = [self pathForURLWithIdentifier:identifier_ infoDictionary:info]; 54 | NSString *combined = [path stringByAppendingString:[parameters queryString]]; 55 | return [[NSURL URLWithString:combined relativeToURL:kHNWebsiteURL] absoluteURL]; 56 | } 57 | 58 | + (id)session:(HNSession *)session objectWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info URL:(NSURL *)url_ { 59 | HNObjectCache *cache = [session cache]; 60 | HNObject *object = [cache objectFromCacheWithClass:self identifier:identifier_ infoDictionary:info]; 61 | 62 | if (object == nil) { 63 | object = [[[self alloc] init] autorelease]; 64 | [object setSession:session]; 65 | } 66 | 67 | if (url_ != nil) { 68 | [object setURL:url_]; 69 | [object setIdentifier:identifier_]; 70 | [object loadInfoDictionary:info]; 71 | 72 | if (![cache cacheHasObject:object]) { 73 | [cache addObjectToCache:object]; 74 | } 75 | } 76 | 77 | return object; 78 | } 79 | 80 | + (id)session:(HNSession *)session objectWithIdentifier:(id)identifier_ infoDictionary:(NSDictionary *)info { 81 | return [self session:session objectWithIdentifier:identifier_ infoDictionary:info URL:[[self class] generateURLWithIdentifier:identifier_ infoDictionary:info]]; 82 | } 83 | 84 | + (id)session:(HNSession *)session objectWithIdentifier:(id)identifier_ { 85 | return [self session:session objectWithIdentifier:identifier_ infoDictionary:nil]; 86 | } 87 | 88 | + (id)session:(HNSession *)session objectWithURL:(NSURL *)url_ { 89 | id identifier_ = [self identifierForURL:url_]; 90 | NSDictionary *info = [self infoDictionaryForURL:url_]; 91 | return [self session:session objectWithIdentifier:identifier_ infoDictionary:info URL:url_]; 92 | } 93 | 94 | + (NSString *)pathWithIdentifier:(id)identifier { 95 | return nil; 96 | } 97 | 98 | - (NSDictionary *)infoDictionary { 99 | return nil; 100 | } 101 | 102 | - (void)loadInfoDictionary:(NSDictionary *)info { 103 | return; 104 | } 105 | 106 | - (NSString *)description { 107 | NSString *other = nil; 108 | 109 | if (![self isLoaded]) { 110 | other = @"[not loaded]"; 111 | } 112 | 113 | NSString *identifier_ = [NSString stringWithFormat:@" identifier=%@", identifier]; 114 | if (identifier == nil) identifier_ = @""; 115 | 116 | return [NSString stringWithFormat:@"<%@:%p %@ %@>", [self class], self, identifier_, other]; 117 | } 118 | 119 | #pragma mark - Loading 120 | 121 | - (void)clearLoadingState:(HNObjectLoadingState)state_ { 122 | loadingState &= ~state_; 123 | 124 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNObjectLoadingStateChangedNotification object:self]; 125 | } 126 | 127 | - (void)addLoadingState:(HNObjectLoadingState)state_ { 128 | loadingState |= state_; 129 | 130 | if ((state_ & kHNObjectLoadingStateLoaded) > 0) { 131 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNObjectFinishedLoadingNotification object:self]; 132 | } else if ((state_ & kHNObjectLoadingStateLoadingAny) > 0) { 133 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNObjectStartedLoadingNotification object:self]; 134 | } 135 | 136 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNObjectLoadingStateChangedNotification object:self]; 137 | } 138 | 139 | - (BOOL)hasLoadingState:(HNObjectLoadingState)state_ { 140 | return ([self loadingState] & state_) > 0; 141 | } 142 | 143 | - (BOOL)isLoaded { 144 | return [self hasLoadingState:kHNObjectLoadingStateLoaded]; 145 | } 146 | 147 | - (void)setIsLoaded:(BOOL)loaded { 148 | if (loaded) { 149 | // XXX: do we really want to do this here? 150 | if ([self isLoading]) [self cancelLoading]; 151 | 152 | [self clearLoadingState:kHNObjectLoadingStateUnloaded]; 153 | [self clearLoadingState:kHNObjectLoadingStateNotLoaded]; 154 | [self addLoadingState:kHNObjectLoadingStateLoaded]; 155 | } else { 156 | [self clearLoadingState:kHNObjectLoadingStateLoaded]; 157 | [self addLoadingState:kHNObjectLoadingStateUnloaded]; 158 | } 159 | } 160 | 161 | - (BOOL)isLoading { 162 | return [self hasLoadingState:kHNObjectLoadingStateLoadingAny]; 163 | } 164 | 165 | - (void)loadFromDictionary:(NSDictionary *)dictionary complete:(BOOL)complete { 166 | if (complete) { 167 | HNObjectCache *cache = [session cache]; 168 | [cache savePersistentCacheDictionary:dictionary forObject:self]; 169 | [self setIsLoaded:YES]; 170 | } 171 | } 172 | 173 | - (void)_clearRequest { 174 | if ([apiRequest isLoading]) { 175 | [apiRequest cancelRequest]; 176 | } 177 | 178 | [apiRequest release]; 179 | apiRequest = nil; 180 | 181 | [self clearLoadingState:kHNObjectLoadingStateLoadingAny]; 182 | } 183 | 184 | - (void)request:(HNAPIRequest *)request completedWithResponse:(NSDictionary *)response error:(NSError *)error { 185 | if (error == nil) { 186 | [self loadFromDictionary:response complete:YES]; 187 | } 188 | 189 | // note: don't move this downwards, bad things will happen 190 | [self _clearRequest]; 191 | 192 | if (error != nil) { 193 | [[NSNotificationCenter defaultCenter] postNotificationName:kHNObjectFailedLoadingNotification object:self]; 194 | } 195 | } 196 | 197 | - (void)beginLoadingWithState:(HNObjectLoadingState)state_ { 198 | HNObjectCache *cache = [session cache]; 199 | [cache updateObjectFromPersistentCache:self]; 200 | 201 | [self addLoadingState:state_]; 202 | 203 | NSDictionary *info = [self infoDictionary]; 204 | NSDictionary *parameters = [[self class] parametersForURLWithIdentifier:identifier infoDictionary:info]; 205 | NSString *path = [[self class] pathForURLWithIdentifier:identifier infoDictionary:info]; 206 | 207 | apiRequest = [[HNAPIRequest alloc] initWithSession:session target:self action:@selector(request:completedWithResponse:error:)]; 208 | [apiRequest performRequestWithPath:path parameters:parameters]; 209 | } 210 | 211 | - (void)cancelLoading { 212 | [self _clearRequest]; 213 | } 214 | 215 | - (void)beginLoading { 216 | // Loading multiple times at once just makes no sense. 217 | if ([self isLoading]) return; 218 | 219 | if ([self isLoaded]) { 220 | [self beginLoadingWithState:kHNObjectLoadingStateLoadingReload]; 221 | } else { 222 | [self beginLoadingWithState:kHNObjectLoadingStateLoadingInitial]; 223 | } 224 | } 225 | 226 | @end 227 | -------------------------------------------------------------------------------- /Classes/HNKit/HNObjectBodyRenderer.m: -------------------------------------------------------------------------------- 1 | // 2 | // HNObjectBodyRenderer.m 3 | // newsyc 4 | // 5 | // Created by Grant Paul on 2/26/12. 6 | // Copyright (c) 2012 Xuzz Productions, LLC. All rights reserved. 7 | // 8 | 9 | #import "HNShared.h" 10 | 11 | #ifdef HNKIT_RENDERING_ENABLED 12 | 13 | #import "HNObjectBodyRenderer.h" 14 | 15 | #import "HNObject.h" 16 | #import "HNEntry.h" 17 | #import "HNUser.h" 18 | 19 | #import "XMLDocument.h" 20 | #import "XMLElement.h" 21 | 22 | #ifndef IS_MAC_OS_X 23 | #import 24 | #else 25 | #import 26 | #endif 27 | 28 | static const CGFloat HNObjectBodyRendererLinkColor[3] = {9.0 / 255.0, 33 / 255.0, 234 / 255.0}; 29 | 30 | static CGFloat defaultFontSize = 13.0f; 31 | 32 | @implementation HNObjectBodyRenderer 33 | @synthesize object; 34 | 35 | + (CGFloat)defaultFontSize { 36 | return defaultFontSize; 37 | } 38 | 39 | + (void)setDefaultFontSize:(CGFloat)size { 40 | defaultFontSize = size; 41 | } 42 | 43 | #ifndef IS_MAC_OS_X 44 | - (CTFontRef)fontForFont:(UIFont *)font { 45 | #else 46 | - (CTFontRef)fontForFont:(NSFont *)font { 47 | #endif 48 | static NSCache *fontCache = nil; 49 | if (fontCache == nil) fontCache = [[NSCache alloc] init]; 50 | 51 | // This is okay as we only setup the font based on the name and size anyway. 52 | NSArray *key = [NSArray arrayWithObjects:[font fontName], [NSNumber numberWithFloat:[font pointSize]], nil]; 53 | 54 | if ([fontCache objectForKey:font]) { 55 | return (CTFontRef) [fontCache objectForKey:key]; 56 | } else { 57 | CTFontRef ref = CTFontCreateWithName((CFStringRef) [font fontName], [font pointSize], NULL); 58 | [fontCache setObject:(id) ref forKey:key]; 59 | return ref; 60 | } 61 | } 62 | 63 | #ifndef IS_MAC_OS_X 64 | - (UIColor *)colorFromHexString:(NSString *)hex { 65 | #else 66 | - (NSColor *)colorFromHexString:(NSString *)hex { 67 | #endif 68 | if ([hex hasPrefix:@"#"]) hex = [hex substringFromIndex:1]; 69 | 70 | NSScanner *scanner = [NSScanner scannerWithString:hex]; 71 | [scanner setCharactersToBeSkipped:[NSCharacterSet symbolCharacterSet]]; 72 | 73 | NSUInteger color; 74 | [scanner scanHexInt:&color]; 75 | 76 | CGFloat red = ((color & 0xFF0000) >> 16) / 255.0f; 77 | CGFloat green = ((color & 0x00FF00) >> 8) / 255.0f; 78 | CGFloat blue = ((color & 0x0000FF) >> 0) / 255.0f; 79 | 80 | #ifndef IS_MAC_OS_X 81 | return [UIColor colorWithRed:red green:green blue:blue alpha:1.0f]; 82 | #else 83 | return [NSColor colorWithCalibratedRed:red green:green blue:blue alpha:1.0f]; 84 | #endif 85 | } 86 | 87 | - (NSString *)text { 88 | if ([object isKindOfClass:[HNEntry class]]) { 89 | HNEntry *entry = (HNEntry *) object; 90 | return [entry body]; 91 | } else if ([object isKindOfClass:[HNUser class]]) { 92 | HNUser *user = (HNUser *) object; 93 | NSString *body = [user about]; 94 | 95 | body = [body stringByReplacingOccurrencesOfString:@"

" withString:@"\n\n"]; 96 | 97 | __block NSInteger lastIndex = 0; 98 | NSMutableString *markupBody = [NSMutableString string]; 99 | 100 | NSDataDetector *dataDetector = [NSDataDetector dataDetectorWithTypes:(NSTextCheckingTypes)NSTextCheckingTypeLink error:NULL]; 101 | [dataDetector enumerateMatchesInString:body options:0 range:NSMakeRange(0, [body length]) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { 102 | NSRange range = [result range]; 103 | NSURL *url = [result URL]; 104 | 105 | if ([[url scheme] isEqualToString:@"mailto"]) { 106 | // XXX: support mailto: URLs 107 | url = nil; 108 | } 109 | 110 | [markupBody appendString:[body substringWithRange:NSMakeRange(lastIndex, range.location - lastIndex)]]; 111 | 112 | if (url != nil) { 113 | NSString *addressString = [url absoluteString]; 114 | [markupBody appendString:[NSString stringWithFormat:@"", addressString]]; 115 | } 116 | 117 | [markupBody appendString:[body substringWithRange:range]]; 118 | 119 | if (url != nil) { 120 | [markupBody appendString:@""]; 121 | } 122 | 123 | lastIndex = range.location + range.length; 124 | }]; 125 | 126 | [markupBody appendString:[body substringFromIndex:lastIndex]]; 127 | 128 | [markupBody replaceOccurrencesOfString:@"\n" withString:@"
" options:0 range:NSMakeRange(0, [markupBody length])]; 129 | 130 | return markupBody; 131 | } 132 | 133 | return nil; 134 | } 135 | 136 | - (NSAttributedString *)createAttributedString { 137 | NSString *body = [self text]; 138 | if ([body length] == 0) return [[[NSAttributedString alloc] init] autorelease]; 139 | 140 | CGFloat fontSize = [[self class] defaultFontSize]; 141 | 142 | #ifndef IS_MAC_OS_X 143 | CTFontRef fontBody = [self fontForFont:[UIFont systemFontOfSize:fontSize]]; 144 | CTFontRef fontCode = [self fontForFont:[UIFont fontWithName:@"Courier New" size:fontSize]]; 145 | CTFontRef fontItalic = [self fontForFont:[UIFont italicSystemFontOfSize:fontSize]]; 146 | CTFontRef fontSmallParagraphBreak = [self fontForFont:[UIFont systemFontOfSize:floorf(fontSize * 2.0f / 3.0f)]]; 147 | 148 | CGColorRef colorBody = [[UIColor blackColor] CGColor]; 149 | CGColorRef colorLink = [[UIColor colorWithRed:HNObjectBodyRendererLinkColor[0] green:HNObjectBodyRendererLinkColor[1] blue:HNObjectBodyRendererLinkColor[2] alpha:1.0] CGColor]; 150 | #else 151 | CTFontRef fontBody = [self fontForFont:[NSFont systemFontOfSize:fontSize]]; 152 | CTFontRef fontCode = [self fontForFont:[NSFont fontWithName:@"Courier New" size:fontSize]]; 153 | CTFontRef fontItalic = [self fontForFont:[NSFont italicSystemFontOfSize:fontSize]]; 154 | CTFontRef fontSmallParagraphBreak = [self fontForFont:[NSFont systemFontOfSize:floorf(fontSize * 2.0f / 3.0f)]]; 155 | 156 | CGColorRef colorBody = [[NSColor blackColor] CGColor]; 157 | CGColorRef colorLink = [[NSColor colorWithCalibratedRed:HNObjectBodyRendererLinkColor[0] green:HNObjectBodyRendererLinkColor[1] blue:HNObjectBodyRendererLinkColor[2] alpha:1.0] CGColor]; 158 | #endif 159 | 160 | __block NSMutableAttributedString *bodyAttributed = [[NSMutableAttributedString alloc] init]; 161 | __block NSMutableDictionary *currentAttributes = [NSMutableDictionary dictionary]; 162 | 163 | BOOL(^hasContent)() = ^BOOL { 164 | return [[[bodyAttributed string] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] != 0; 165 | }; 166 | 167 | void(^formatBody)(NSMutableDictionary *, XMLElement *) = ^(NSMutableDictionary *attributes, XMLElement *element) { 168 | [attributes setObject:(id) colorBody forKey:(NSString *) kCTForegroundColorAttributeName]; 169 | [attributes setObject:(id) fontBody forKey:(NSString *) kCTFontAttributeName]; 170 | }; 171 | 172 | void(^formatFont)(NSMutableDictionary *, XMLElement *) = ^(NSMutableDictionary *attributes, XMLElement *element) { 173 | NSString *colorText = [element attributeWithName:@"color"]; 174 | CGColorRef color = [[self colorFromHexString:colorText] CGColor]; 175 | 176 | [attributes setObject:(id) color forKey:(NSString *) kCTForegroundColorAttributeName]; 177 | }; 178 | 179 | void(^formatCode)(NSMutableDictionary *, XMLElement *) = ^(NSMutableDictionary *attributes, XMLElement *element) { 180 | [attributes setObject:(id) fontCode forKey:(NSString *) kCTFontAttributeName]; 181 | [attributes setObject:(id) kCFBooleanTrue forKey:@"PreserveWhitepace"]; 182 | }; 183 | 184 | void(^formatItalic)(NSMutableDictionary *, XMLElement *) = ^(NSMutableDictionary *attributes, XMLElement *element) { 185 | [attributes setObject:(id) fontItalic forKey:(NSString *) kCTFontAttributeName]; 186 | }; 187 | 188 | void(^formatLink)(NSMutableDictionary *, XMLElement *) = ^(NSMutableDictionary *attributes, XMLElement *element) { 189 | NSString *href = [element attributeWithName:@"href"]; 190 | 191 | [attributes setObject:(id) colorLink forKey:(NSString *) kCTForegroundColorAttributeName]; 192 | if (href != nil) [attributes setObject:href forKey:@"LinkDestination"]; 193 | [attributes setObject:[NSNumber numberWithInt:arc4random()] forKey:@"LinkIdentifier"]; 194 | }; 195 | 196 | void(^formatParagraph)(NSMutableDictionary *, XMLElement *) = ^(NSMutableDictionary *attributes, XMLElement *element) { 197 | if (!hasContent()) return; 198 | 199 | NSAttributedString *newlineString = [[NSAttributedString alloc] initWithString:@"\n" attributes:attributes]; 200 | [bodyAttributed appendAttributedString:[newlineString autorelease]]; 201 | 202 | NSMutableDictionary *blankLineAttributes = [[attributes mutableCopy] autorelease]; 203 | [blankLineAttributes setObject:(id) fontSmallParagraphBreak forKey:(NSString *) kCTFontAttributeName]; 204 | NSAttributedString *blankLineString = [[NSAttributedString alloc] initWithString:@"\n" attributes:blankLineAttributes]; 205 | [bodyAttributed appendAttributedString:[blankLineString autorelease]]; 206 | }; 207 | 208 | void (^formatNewline)(NSMutableDictionary *, XMLElement *) = ^(NSMutableDictionary *attributes, XMLElement *element) { 209 | if (!hasContent()) return; 210 | 211 | NSAttributedString *childString = [[NSAttributedString alloc] initWithString:@"\n" attributes:nil]; 212 | [bodyAttributed appendAttributedString:[childString autorelease]]; 213 | }; 214 | 215 | NSDictionary *tagActions = [NSDictionary dictionaryWithObjectsAndKeys: 216 | [[formatParagraph copy] autorelease], @"p", 217 | [[formatCode copy] autorelease], @"pre", 218 | [[formatItalic copy] autorelease], @"i", 219 | [[formatLink copy] autorelease], @"a", 220 | [[formatFont copy] autorelease], @"font", 221 | [[formatNewline copy] autorelease], @"br", 222 | [[formatBody copy] autorelease], @"body", 223 | nil]; 224 | 225 | __block void(^formatChildren)(XMLElement *) = ^(XMLElement *element) { 226 | for (XMLElement *child in [element children]) { 227 | if (![child isTextNode]) { 228 | NSMutableDictionary *savedAttributes = [[currentAttributes mutableCopy] autorelease]; 229 | 230 | NSAttributedString *(^formatAction)(NSMutableDictionary *, XMLElement *element) = [tagActions objectForKey:[child tagName]]; 231 | if (formatAction != NULL) formatAction(currentAttributes, child); 232 | 233 | formatChildren(child); 234 | 235 | currentAttributes = savedAttributes; 236 | } else { 237 | NSString *content = [child content]; 238 | 239 | // strip out whitespace not in

 when 
240 |                 if (![[currentAttributes objectForKey:@"PreserveWhitepace"] boolValue]) {
241 |                     while ([content rangeOfString:@"  "].location != NSNotFound) {
242 |                         content = [content stringByReplacingOccurrencesOfString:@"  " withString:@" "];
243 |                     }
244 |                     
245 |                     content = [content stringByReplacingOccurrencesOfString:@"\n" withString:@""];
246 |                 } else {
247 |                     NSString *prefix = @"  ";
248 |                     
249 |                     if ([content hasPrefix:prefix]) {
250 |                         content = [content substringFromIndex:[prefix length]];
251 |                     }
252 |                     
253 |                     content = [content stringByReplacingOccurrencesOfString:[@"\n" stringByAppendingString:prefix] withString:@"\n"];
254 |                     
255 |                     if (hasContent()) {
256 |                         content = [content stringByAppendingString:@"\n"];
257 |                     }
258 |                 }
259 |                 
260 |                 NSAttributedString *childString = [[NSAttributedString alloc] initWithString:content attributes:currentAttributes];
261 |                 [bodyAttributed appendAttributedString:[childString autorelease]];
262 |             }
263 |         }
264 |     };
265 |     
266 |     // ensure body has a root element
267 |     body = [NSString stringWithFormat:@"%@", body];
268 |     
269 |     XMLDocument *xml = [[XMLDocument alloc] initWithHTMLData:[body dataUsingEncoding:NSUTF8StringEncoding]];
270 |     formatChildren([xml firstElementMatchingPath:@"/"]);
271 |     [xml release];
272 |     
273 |     /*CFRelease(fontBody);
274 |     CFRelease(fontCode);
275 |     CFRelease(fontItalic);*/
276 |     
277 |     return [bodyAttributed autorelease];
278 | }
279 | 
280 | - (CGSize)sizeForWidth:(CGFloat)width {
281 |     CGSize size = CGSizeZero;
282 |     size.width = width;
283 |     size.height = CGFLOAT_MAX;
284 |     
285 |     size = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, [attributed length]), NULL, size, NULL);
286 |     return size;
287 | }
288 | 
289 | - (NSURL *)linkURLAtPoint:(CGPoint)point forWidth:(CGFloat)width rects:(NSSet **)rects {
290 |     CGSize size = [self sizeForWidth:width];
291 |     CGRect rect = CGRectMake(0, 0, size.width, size.height);
292 |     
293 |     // flip it into CoreText coordinates
294 |     point.y = size.height - point.y;
295 |     
296 |     CGMutablePathRef path = CGPathCreateMutable();
297 |     CGPathAddRect(path, NULL, rect);
298 |     
299 |     CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);    
300 |     NSArray *lines = (NSArray *) CTFrameGetLines(frame);
301 |     
302 |     CGPoint *origins = (CGPoint *) calloc(sizeof(CGPoint), [lines count]);
303 |     CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
304 |     
305 |     CGRect (^computeLineRect)(CTLineRef, NSInteger) = ^CGRect (CTLineRef line, NSInteger index) {
306 |         CGRect lineRect;
307 |         lineRect.origin.x = 0;
308 |         lineRect.origin.y = origins[index].y;
309 |         
310 |         CGFloat ascent, descent, leading;
311 |         lineRect.size.width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
312 |         lineRect.size.height = ascent + descent;
313 |         
314 |         return lineRect;
315 |     };
316 |     
317 |     CGRect (^computeRunRect)(CTRunRef, CTLineRef, CGRect) = ^CGRect (CTRunRef run, CTLineRef line, CGRect lineRect) {                    
318 |         CGRect runRect;
319 |         CGFloat ascent, descent;
320 |         runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
321 |         runRect.size.height = ascent + descent;
322 |         runRect.origin.x = lineRect.origin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
323 |         runRect.origin.y = lineRect.origin.y - descent;
324 |         
325 |         return runRect;
326 |     };
327 |     
328 |     for (NSInteger i = 0; i < [lines count]; i++) {
329 |         CTLineRef line = (CTLineRef) [lines objectAtIndex:i];
330 |         CGRect lineBounds = computeLineRect(line, i);
331 |         
332 |         CGFloat ascent, descent, leading;
333 |         CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
334 |                 
335 |         // if the bottom of the line is less than the point
336 |         if (lineBounds.origin.y - descent < point.y) {
337 |             NSArray *runs = (NSArray *) CTLineGetGlyphRuns(line);
338 |                         
339 |             for (NSInteger j = 0; j < [runs count]; j++) {
340 |                 CTRunRef run = (CTRunRef) [runs objectAtIndex:j];
341 |                 CGRect runBounds = computeRunRect(run, line, lineBounds);
342 |                 
343 |                 if (runBounds.origin.x + runBounds.size.width > point.x) {
344 |                     NSDictionary *attributes = (NSDictionary *) CTRunGetAttributes(run);
345 |                     NSURL *url = [NSURL URLWithString:[attributes objectForKey:@"LinkDestination"]];
346 |                     NSNumber *linkIdentifier = [attributes objectForKey:@"LinkIdentifier"];
347 |                     
348 |                     if (linkIdentifier != nil && rects != NULL) {
349 |                         NSMutableSet *runRects = [NSMutableSet set];
350 |                         
351 |                         for (NSInteger k = 0; k < [lines count]; k++) {
352 |                             CTLineRef line = (CTLineRef) [lines objectAtIndex:k];
353 |                             NSArray *runs = (NSArray *) CTLineGetGlyphRuns(line);
354 | 
355 |                             for (NSInteger l = 0; l < [runs count]; l++) {
356 |                                 CTRunRef run = (CTRunRef) [runs objectAtIndex:l];
357 |                                 NSDictionary *attributes = (NSDictionary *) CTRunGetAttributes(run);
358 | 
359 |                                 NSNumber *runIdentifier = [attributes objectForKey:@"LinkIdentifier"];
360 |                                 if ([runIdentifier isEqual:linkIdentifier]) {
361 |                                     CGRect lineRect = computeLineRect(line, k);
362 |                                     CGRect runRect = computeRunRect(run, line, lineRect);
363 |                                     
364 |                                     // flip it back into the top-left coordinate system
365 |                                     runRect.origin.y = size.height - (runRect.origin.y + runRect.size.height);
366 |                                     
367 |                                     NSValue *rectValue = [NSValue valueWithCGRect:runRect];
368 |                                     [runRects addObject:rectValue];
369 |                                 }
370 |                             }
371 |                         }
372 |                         
373 |                         *rects = (NSSet *) runRects;
374 |                     }
375 |                     
376 |                     free(origins);
377 |                     CFRelease(frame);
378 |                     CFRelease(path);
379 |                     return url;
380 |                 }
381 |             }
382 |             
383 |             free(origins);
384 |             CFRelease(frame);
385 |             CFRelease(path);
386 |             return nil;
387 |         }
388 |     }
389 |     
390 |     free(origins);
391 |     CFRelease(frame);
392 |     CFRelease(path);
393 |     return nil;
394 | }
395 | 
396 | - (void)renderInContext:(CGContextRef)context rect:(CGRect)rect {
397 |     CGContextSaveGState(context);
398 |     
399 |     CGContextSetTextMatrix(context, CGAffineTransformIdentity);
400 |     CGContextTranslateCTM(context, rect.origin.x, rect.origin.y);
401 |     CGContextScaleCTM(context, 1.0f, -1.0f);
402 |     CGContextTranslateCTM(context, -rect.origin.x, -(rect.origin.y + rect.size.height));
403 |     
404 |     CGMutablePathRef path = CGPathCreateMutable();
405 |     CGPathAddRect(path, NULL, rect);
406 |     
407 |     CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);    
408 |     CTFrameDraw(frame, context);
409 |     
410 |     CGPathRelease(path);  
411 |     CFRelease(frame);
412 |     
413 |     CGContextRestoreGState(context);  
414 | }
415 | 
416 | - (void)prepare {
417 |     if (framesetter != NULL) CFRelease(framesetter);
418 |     [attributed release];
419 |     
420 |     attributed = [[self createAttributedString] retain];
421 |     framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) attributed);
422 | }
423 | 
424 | - (NSString *)string {
425 |     return [attributed string];
426 | }
427 | 
428 | - (NSString *)HTMLString {
429 |     return [[[self text] copy] autorelease];
430 | }
431 | 
432 | - (NSAttributedString *)attributedString {
433 |     return [[attributed copy] autorelease];
434 | }
435 | 
436 | - (id)initWithObject:(HNObject *)object_ {
437 |     if ((self = [super init])) {
438 |         object = object_;
439 |         [self prepare];
440 |         
441 |         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepare) name:kHNObjectLoadingStateChangedNotification object:object];
442 |     }
443 |     
444 |     return self;
445 | }
446 | 
447 | - (void)dealloc {
448 |     CFRelease(framesetter);
449 |     [attributed release];
450 |     
451 |     [[NSNotificationCenter defaultCenter] removeObserver:self];
452 |     
453 |     [super dealloc];
454 | }
455 | 
456 | @end
457 | 
458 | #endif
459 | 


--------------------------------------------------------------------------------
/Classes/HNKit/HNAPIRequestParser.m:
--------------------------------------------------------------------------------
  1 | //
  2 | //  HNAPIRequestParser.m
  3 | //  newsyc
  4 | //
  5 | //  Created by Grant Paul on 3/12/11.
  6 | //  Copyright 2011 Xuzz Productions, LLC. All rights reserved.
  7 | //
  8 | 
  9 | #import "HNKit.h"
 10 | #import "HNAPIRequestParser.h"
 11 | #import "XMLDocument.h"
 12 | #import "XMLElement.h"
 13 | #import "NSString+Tags.h"
 14 | 
 15 | typedef enum {
 16 |     kHNPageLayoutTypeUnknown,
 17 |     kHNPageLayoutTypeEnclosed, //  inside [3]
 18 |     kHNPageLayoutTypeHeaderFooter, // 
[1:2] inside [3] 19 | kHNPageLayoutTypeExposed // [3:] 20 | } HNPageLayoutType; 21 | 22 | @implementation HNAPIRequestParser 23 | 24 | - (BOOL)stringIsProcrastinationError:(NSString *)string { 25 | XMLDocument *document = [[XMLDocument alloc] initWithHTMLData:[string dataUsingEncoding:NSUTF8StringEncoding]]; 26 | XMLElement *procrastElement = [document firstElementMatchingPath:@"//body/center/table/tr/td/b"]; 27 | NSString *procrastMessage = [procrastElement content]; 28 | 29 | BOOL procrast = [procrastMessage isEqualToString:@"Get back to work!"]; 30 | 31 | [document release]; 32 | return procrast; 33 | } 34 | 35 | - (BOOL)stringIsExpiredError:(NSString *)string { 36 | XMLDocument *document = [[XMLDocument alloc] initWithHTMLData:[string dataUsingEncoding:NSUTF8StringEncoding]]; 37 | XMLElement *procrastElement = [document firstElementMatchingPath:@"//body/p"]; 38 | NSString *procrastMessage = [procrastElement content]; 39 | 40 | BOOL expired = [procrastMessage isEqualToString:@"Unknown or expired link."]; 41 | 42 | [document release]; 43 | return expired; 44 | } 45 | 46 | - (NSDictionary *)parseUserProfileWithString:(NSString *)string { 47 | NSScanner *scanner = [NSScanner scannerWithString:string]; 48 | NSString *key = nil, *value = nil; 49 | 50 | NSString *start = @""; 53 | 54 | NSMutableDictionary *result = [NSMutableDictionary dictionary]; 55 | 56 | while ([scanner isAtEnd] == NO) { 57 | [scanner scanUpToString:start intoString:NULL]; 58 | [scanner scanUpToString:mid intoString:&key]; 59 | [scanner scanUpToString:end intoString:&value]; 60 | if ([key hasPrefix:start]) key = [key substringFromIndex:[start length]]; 61 | if ([value hasPrefix:mid]) value = [value substringFromIndex:[mid length]]; 62 | 63 | if ([key isEqual:@"about"]) { 64 | // XXX: hacky method to extract text from a textarea 65 | if ([value rangeOfString:@""].location != NSNotFound) { 69 | tempValue = [tempValue substringFromIndex:[tempValue rangeOfString:@">"].location + [tempValue rangeOfString:@">"].length]; 70 | if ([tempValue rangeOfString:@"\n"].location == 0) { 71 | tempValue = [tempValue substringFromIndex:[tempValue rangeOfString:@"\n"].location + [tempValue rangeOfString:@"\n"].length]; 72 | } 73 | 74 | if ([tempValue rangeOfString:@""].location != NSNotFound) { 75 | value = [tempValue substringToIndex:[tempValue rangeOfString:@""].location]; 76 | } 77 | } 78 | } 79 | 80 | [result setObject:value forKey:@"about"]; 81 | } else if ([key isEqual:@"karma"]) { 82 | [result setObject:value forKey:@"karma"]; 83 | } else if ([key isEqual:@"avg"]) { 84 | [result setObject:value forKey:@"average"]; 85 | } else if ([key isEqual:@"created"]) { 86 | [result setObject:value forKey:@"created"]; 87 | } 88 | } 89 | 90 | return result; 91 | } 92 | 93 | - (XMLElement *)headerRowForDocument:(XMLDocument *)document { 94 | return [document firstElementMatchingPath:@"//body/center/table/tr[//span[@class='pagetop']]"]; 95 | } 96 | 97 | - (XMLElement *)headerCellForDocument:(XMLDocument *)document { 98 | XMLElement *header = [self headerCellForDocument:document]; 99 | XMLElement *headerCell = [[header children] lastObject]; 100 | return headerCell; 101 | } 102 | 103 | - (NSArray *)mainRowsForDocument:(XMLDocument *)document { 104 | NSArray *elements = [document elementsMatchingPath:@"//body/center/table/tr"]; 105 | return elements; 106 | } 107 | 108 | - (NSInteger)bodyIndexForDocument:(XMLDocument *)document { 109 | NSArray *elements = [self mainRowsForDocument:document]; 110 | XMLElement *header = [self headerRowForDocument:document]; 111 | NSInteger index = [elements indexOfObject:header] + 3; 112 | return index; 113 | } 114 | 115 | - (XMLElement *)bodyRowElementForDocument:(XMLDocument *)document { 116 | NSArray *elements = [self mainRowsForDocument:document]; 117 | NSInteger index = [self bodyIndexForDocument:document]; 118 | XMLElement *body = [elements objectAtIndex:(index - 1)]; 119 | return body; 120 | } 121 | 122 | - (XMLElement *)bodyCellElementForDocument:(XMLDocument *)document { 123 | XMLElement *body = [self bodyRowElementForDocument:document]; 124 | XMLElement *cell = [[body children] lastObject]; 125 | return cell; 126 | } 127 | 128 | - (HNPageLayoutType)pageLayoutTypeForDocument:(XMLDocument *)document { 129 | NSArray *elements = [self mainRowsForDocument:document]; 130 | 131 | if ([elements count] >= 4) { 132 | XMLElement *td = [self bodyCellElementForDocument:document]; 133 | 134 | if (td != nil) { 135 | if ([[td children] count] == 0) { 136 | return kHNPageLayoutTypeExposed; 137 | } else { 138 | NSArray *tables = [td elementsMatchingPath:@"table"]; 139 | NSArray *linebreaks = [td elementsMatchingPath:@"br"]; 140 | 141 | // XXX: This is horrible hack. 142 | // This is because when there is only a header, make sure 143 | // that the header isn't considered to be replying to itself 144 | // There are two
tags in that case, so use those to guess. 145 | if ([tables count] >= 2 || [linebreaks count] == 2) { 146 | return kHNPageLayoutTypeHeaderFooter; 147 | } else if ([tables count] == 1) { 148 | return kHNPageLayoutTypeEnclosed; 149 | } 150 | } 151 | } 152 | } 153 | 154 | return kHNPageLayoutTypeUnknown; 155 | } 156 | 157 | - (BOOL)rootElementIsSubmission:(XMLDocument *)document { 158 | XMLElement *bodyCell = [self bodyCellElementForDocument:document]; 159 | return [bodyCell firstElementMatchingPath:@"table//td[@class='title']"] != nil; 160 | } 161 | 162 | - (XMLElement *)rootElementForDocument:(XMLDocument *)document pageLayoutType:(HNPageLayoutType)type { 163 | if (type == kHNPageLayoutTypeHeaderFooter) { 164 | XMLElement *bodyCell = [self bodyCellElementForDocument:document]; 165 | return [bodyCell firstElementMatchingPath:@"table[1]"]; 166 | } else { 167 | return nil; 168 | } 169 | } 170 | 171 | - (NSArray *)contentRowsForDocument:(XMLDocument *)document pageLayoutType:(HNPageLayoutType)type { 172 | if (type == kHNPageLayoutTypeEnclosed) { 173 | XMLElement *bodyCell = [self bodyCellElementForDocument:document]; 174 | NSArray *elements = [bodyCell elementsMatchingPath:@"table/tr"]; 175 | return elements; 176 | } else if (type == kHNPageLayoutTypeExposed) { 177 | NSArray *elements = [self mainRowsForDocument:document]; 178 | NSInteger index = [self bodyIndexForDocument:document]; 179 | return [elements subarrayWithRange:NSMakeRange(index, [elements count] - index)]; 180 | } else if (type == kHNPageLayoutTypeHeaderFooter) { 181 | XMLElement *bodyCell = [self bodyCellElementForDocument:document]; 182 | NSArray *elements = [bodyCell elementsMatchingPath:@"table[2]/tr"]; 183 | return elements; 184 | } else { 185 | return nil; 186 | } 187 | } 188 | 189 | - (NSDictionary *)parseSubmissionWithElements:(NSArray *)elements { 190 | XMLElement *first = [elements objectAtIndex:0]; 191 | XMLElement *second = [elements objectAtIndex:1]; 192 | XMLElement *fourth = nil; 193 | if ([elements count] >= 4) fourth = [elements objectAtIndex:3]; 194 | 195 | // These have a number of edge cases (e.g. "discuss"), 196 | // so use sane default values in case of one of those. 197 | NSNumber *points = [NSNumber numberWithInt:0]; 198 | NSNumber *comments = [NSNumber numberWithInt:0]; 199 | 200 | NSString *title = nil; 201 | NSString *user = nil; 202 | NSNumber *identifier = nil; 203 | NSString *body = nil; 204 | NSString *date = nil; 205 | NSString *href = nil; 206 | HNMoreToken more = nil; 207 | 208 | for (XMLElement *element in [first children]) { 209 | if ([[element attributeWithName:@"class"] isEqual:@"title"]) { 210 | for (XMLElement *element2 in [element children]) { 211 | if ([[element2 tagName] isEqual:@"a"] && ![[element2 content] isEqual:@"scribd"]) { 212 | title = [element2 content]; 213 | href = [element2 attributeWithName:@"href"]; 214 | 215 | // In "ask HN" posts, we need to extract the id (and fix the URL) here. 216 | if ([href hasPrefix:@"item?id="]) { 217 | identifier = [NSNumber numberWithInt:[[href substringFromIndex:[@"item?id=" length]] intValue]]; 218 | href = nil; 219 | } 220 | } 221 | } 222 | } 223 | } 224 | 225 | for (XMLElement *element in [second children]) { 226 | if ([[element attributeWithName:@"class"] isEqual:@"subtext"]) { 227 | NSString *content = [element content]; 228 | 229 | // XXX: is there any better way of doing this? 230 | NSInteger start = [content rangeOfString:@" "].location; 231 | if (start != NSNotFound) content = [content substringFromIndex:start + [@" " length]]; 232 | NSInteger end = [content rangeOfString:@" ago"].location; 233 | if (end != NSNotFound) date = [[content substringToIndex:end] stringByRemovingHTMLTags]; 234 | 235 | for (XMLElement *element2 in [element children]) { 236 | NSString *content = [element2 content]; 237 | NSString *tag = [element2 tagName]; 238 | 239 | if ([tag isEqual:@"a"]) { 240 | if ([[element2 attributeWithName:@"href"] hasPrefix:@"user?id="]) { 241 | user = [content stringByRemovingHTMLTags]; 242 | user = [user stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 243 | } else if ([[element2 attributeWithName:@"href"] hasPrefix:@"item?id="]) { 244 | NSInteger end = [content rangeOfString:@" "].location; 245 | if (end != NSNotFound) comments = [NSNumber numberWithInt:[[content substringToIndex:end] intValue]]; 246 | identifier = [NSNumber numberWithInt:[[[element2 attributeWithName:@"href"] substringFromIndex:[@"item?id=" length]] intValue]]; 247 | } 248 | } else if ([tag isEqual:@"span"]) { 249 | NSInteger end = [content rangeOfString:@" "].location; 250 | if (end != NSNotFound) points = [NSNumber numberWithInt:[[content substringToIndex:end] intValue]]; 251 | } 252 | } 253 | } else if ([[element attributeWithName:@"class"] isEqual:@"title"] && [[[[element content] stringByRemovingHTMLTags] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] isEqualToString:@"More"]) { 254 | for (XMLElement *element2 in [element children]) { 255 | if ([[element2 tagName] isEqualToString:@"a"]) { 256 | more = [element2 attributeWithName:@"href"]; 257 | } 258 | } 259 | } 260 | } 261 | 262 | for (XMLElement *element in [fourth children]) { 263 | if ([[element tagName] isEqual:@"td"]) { 264 | BOOL isReplyForm = NO; 265 | NSString *content = [element content]; 266 | 267 | for (XMLElement *element2 in [element children]) { 268 | if ([[element2 tagName] isEqual:@"form"]) { 269 | isReplyForm = YES; 270 | break; 271 | } 272 | } 273 | 274 | if ([content length] > 0 && !isReplyForm) { 275 | body = content; 276 | } 277 | } 278 | } 279 | 280 | if (more != nil) { 281 | return [NSDictionary dictionaryWithObject:more forKey:@"more"]; 282 | } else if (user != nil && title != nil && identifier != nil) { 283 | // XXX: better sanity checks? 284 | NSMutableDictionary *item = [NSMutableDictionary dictionary]; 285 | [item setObject:user forKey:@"user"]; 286 | [item setObject:points forKey:@"points"]; 287 | [item setObject:title forKey:@"title"]; 288 | [item setObject:comments forKey:@"numchildren"]; 289 | if (href != nil) [item setObject:href forKey:@"url"]; 290 | [item setObject:date forKey:@"date"]; 291 | if (body != nil) [item setObject:body forKey:@"body"]; 292 | if (more != nil) [item setObject:more forKey:@"more"]; 293 | [item setObject:identifier forKey:@"identifier"]; 294 | return item; 295 | } else { 296 | NSLog(@"Bug: Ignoring unparsable submission."); 297 | return nil; 298 | } 299 | } 300 | 301 | - (NSDictionary *)parseCommentWithElement:(XMLElement *)comment { 302 | NSNumber *depth = nil; 303 | NSNumber *points = [NSNumber numberWithInt:0]; 304 | NSString *body = nil; 305 | NSString *user = nil; 306 | NSNumber *identifier = nil; 307 | NSString *date = nil; 308 | NSNumber *parent = nil; 309 | NSNumber *submission = nil; 310 | NSString *more = nil; 311 | 312 | for (XMLElement *element in [comment children]) { 313 | if ([[element tagName] isEqual:@"tr"]) { 314 | comment = element; 315 | break; 316 | } 317 | } 318 | 319 | for (XMLElement *element in [comment children]) { 320 | if ([[element attributeWithName:@"class"] isEqual:@"title"] && [[[[element content] stringByRemovingHTMLTags] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] isEqualToString:@"More"]) { 321 | for (XMLElement *element2 in [element children]) { 322 | if ([[element2 tagName] isEqualToString:@"a"]) { 323 | more = [element2 attributeWithName:@"href"]; 324 | } 325 | } 326 | } else if ([[element tagName] isEqual:@"td"]) { 327 | for (XMLElement *element2 in [element children]) { 328 | if ([[element2 tagName] isEqual:@"table"]) { 329 | for (XMLElement *element3 in [element2 children]) { 330 | if ([[element3 tagName] isEqual:@"tr"]) { 331 | comment = element3; 332 | goto found; 333 | } 334 | } 335 | } 336 | } 337 | } 338 | } found:; 339 | 340 | for (XMLElement *element in [comment children]) { 341 | if ([[element attributeWithName:@"class"] isEqual:@"default"]) { 342 | for (XMLElement *element2 in [element children]) { 343 | if ([[element2 tagName] isEqual:@"div"]) { 344 | for (XMLElement *element3 in [element2 children]) { 345 | if ([[element3 attributeWithName:@"class"] isEqual:@"comhead"]) { 346 | NSString *content = [element3 content]; 347 | 348 | // XXX: is there any better way of doing this? 349 | NSInteger start = [content rangeOfString:@" "].location; 350 | if (start != NSNotFound) content = [content substringFromIndex:start + [@" " length]]; 351 | NSInteger end = [content rangeOfString:@" ago"].location; 352 | if (end != NSNotFound) date = [[content substringToIndex:end] stringByRemovingHTMLTags]; 353 | 354 | for (XMLElement *element4 in [element3 children]) { 355 | NSString *content = [element4 content]; 356 | NSString *tag = [element4 tagName]; 357 | 358 | if ([tag isEqual:@"a"]) { 359 | NSString *href = [element4 attributeWithName:@"href"]; 360 | 361 | if ([href hasPrefix:@"user?id="]) { 362 | user = [content stringByRemovingHTMLTags]; 363 | user = [user stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 364 | } else if ([href hasPrefix:@"item?id="]) { 365 | identifier = [NSNumber numberWithInt:[[href substringFromIndex:[@"item?id=" length]] intValue]]; 366 | } else if ([href hasPrefix:@"item?id="] && [content isEqual:@"parent"]) { 367 | parent = [NSNumber numberWithInt:[[href substringFromIndex:[@"item?id=" length]] intValue]]; 368 | } else if ([href hasPrefix:@"item?id="]) { 369 | submission = [NSNumber numberWithInt:[[href substringFromIndex:[@"item?id=" length]] intValue]]; 370 | } 371 | } else if ([tag isEqual:@"span"]) { 372 | NSInteger end = [content rangeOfString:@" "].location; 373 | if (end != NSNotFound) points = [NSNumber numberWithInt:[[content substringToIndex:end] intValue]]; 374 | } 375 | } 376 | } 377 | } 378 | } else if ([[element2 attributeWithName:@"class"] isEqual:@"comment"]) { 379 | // XXX: strip out _reply_ link (or "----") at the bottom (when logged in), if necessary? 380 | body = [element2 content]; 381 | } 382 | } 383 | } else if ([[element attributeWithName:@"class"] isEqual:@"title"] && [[[[element content] stringByRemovingHTMLTags] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] isEqualToString:@"More"]) { 384 | for (XMLElement *element2 in [element children]) { 385 | if ([[element2 tagName] isEqualToString:@"a"]) { 386 | more = [element2 attributeWithName:@"href"]; 387 | } 388 | } 389 | } else { 390 | for (XMLElement *element2 in [element children]) { 391 | if ([[element2 tagName] isEqual:@"img"] && [[element2 attributeWithName:@"src"] hasSuffix:@"s.gif"]) { 392 | // Yes, really: HN uses a 1x1 gif to indent comments. It's like 1999 all over again. :( 393 | NSInteger width = [[element2 attributeWithName:@"width"] intValue]; 394 | // Each comment is "indented" by setting the width to "depth * 40", so divide to get the depth. 395 | depth = [NSNumber numberWithInt:(width / 40)]; 396 | } 397 | } 398 | } 399 | } 400 | 401 | if (user == nil && [body isEqual:@"[deleted]"]) { 402 | // XXX: handle deleted comments 403 | NSLog(@"Bug: Ignoring deleted comment."); 404 | return nil; 405 | } 406 | 407 | if (more != nil) { 408 | return [NSDictionary dictionaryWithObject:more forKey:@"more"]; 409 | } else if (user != nil && identifier != nil) { 410 | // XXX: should this be more strict about what's a valid comment? 411 | NSMutableDictionary *item = [NSMutableDictionary dictionary]; 412 | [item setObject:user forKey:@"user"]; 413 | if (body != nil) [item setObject:body forKey:@"body"]; 414 | if (date != nil) [item setObject:date forKey:@"date"]; 415 | if (points != nil) [item setObject:points forKey:@"points"]; 416 | if (depth != nil) [item setObject:[NSMutableArray array] forKey:@"children"]; 417 | if (depth != nil) [item setObject:depth forKey:@"depth"]; 418 | if (parent != nil) [item setObject:parent forKey:@"parent"]; 419 | if (submission != nil) [item setObject:submission forKey:@"submission"]; 420 | 421 | [item setObject:identifier forKey:@"identifier"]; 422 | 423 | return item; 424 | } else { 425 | NSLog(@"Bug: Unable to parse comment."); 426 | return nil; 427 | } 428 | } 429 | 430 | - (NSDictionary *)parseCommentTreeInDocument:(XMLDocument *)document { 431 | HNPageLayoutType type = [self pageLayoutTypeForDocument:document]; 432 | 433 | XMLElement *rootElement = [self rootElementForDocument:document pageLayoutType:type]; 434 | NSMutableDictionary *root = nil; 435 | if (rootElement != nil) { 436 | NSDictionary *item = nil; 437 | if ([self rootElementIsSubmission:document]) 438 | item = [self parseSubmissionWithElements:[rootElement children]]; 439 | else 440 | item = [self parseCommentWithElement:[[rootElement children] objectAtIndex:0]]; 441 | root = [[item mutableCopy] autorelease]; 442 | } 443 | if (root == nil) root = [NSMutableDictionary dictionary]; 444 | [root setObject:[NSMutableArray array] forKey:@"children"]; 445 | 446 | NSArray *comments = [self contentRowsForDocument:document pageLayoutType:type]; 447 | NSMutableArray *lasts = [NSMutableArray array]; 448 | [lasts addObject:root]; 449 | 450 | NSString *moreToken = nil; 451 | 452 | for (NSInteger i = 0; i < [comments count]; i++) { 453 | XMLElement *element = [comments objectAtIndex:i]; 454 | if ([[element content] length] == 0) continue; 455 | NSDictionary *comment = [self parseCommentWithElement:element]; 456 | if (comment == nil) continue; 457 | 458 | if ([comment objectForKey:@"more"] != nil) { 459 | moreToken = [comment objectForKey:@"more"]; 460 | continue; 461 | } 462 | 463 | NSDictionary *parent = nil; 464 | NSNumber *depth = [comment objectForKey:@"depth"]; 465 | 466 | if (depth != nil) { 467 | if ([depth intValue] >= [lasts count]) continue; 468 | if ([lasts count] >= [depth intValue]) 469 | [lasts removeObjectsInRange:NSMakeRange([depth intValue] + 1, [lasts count] - [depth intValue] - 1)]; 470 | parent = [lasts lastObject]; 471 | [lasts addObject:comment]; 472 | } else { 473 | parent = root; 474 | } 475 | 476 | NSMutableArray *children = [parent objectForKey:@"children"]; 477 | [children addObject:comment]; 478 | } 479 | 480 | if (moreToken != nil) [root setObject:moreToken forKey:@"more"]; 481 | 482 | return root; 483 | } 484 | 485 | - (NSDictionary *)parseSubmissionsInDocument:(XMLDocument *)document { 486 | HNPageLayoutType type = [self pageLayoutTypeForDocument:document]; 487 | NSArray *submissions = [self contentRowsForDocument:document pageLayoutType:type]; 488 | 489 | NSMutableArray *result = [NSMutableArray array]; 490 | NSString *moreToken = nil; 491 | 492 | // Three rows are used per submission. 493 | for (NSInteger i = 0; i < [submissions count]; i += 3) { 494 | XMLElement *first = [submissions objectAtIndex:i]; 495 | XMLElement *second = i + 1 < [submissions count] ? [submissions objectAtIndex:i + 1] : nil; 496 | XMLElement *third = i + 2 < [submissions count] ? [submissions objectAtIndex:i + 2] : nil; 497 | 498 | NSDictionary *submission = [self parseSubmissionWithElements:[NSArray arrayWithObjects:first, second, third, nil]]; 499 | if (submission != nil) { 500 | if ([submission objectForKey:@"more"] != nil) { 501 | moreToken = [submission objectForKey:@"more"]; 502 | } else { 503 | [result addObject:submission]; 504 | } 505 | } 506 | } 507 | 508 | NSMutableDictionary *item = [NSMutableDictionary dictionary]; 509 | [item setObject:result forKey:@"children"]; 510 | if (moreToken != nil) [item setObject:moreToken forKey:@"more"]; 511 | return item; 512 | } 513 | 514 | - (NSDictionary *)parseWithString:(NSString *)string { 515 | NSDictionary *result = nil; 516 | XMLDocument *document = [[XMLDocument alloc] initWithHTMLData:[string dataUsingEncoding:NSUTF8StringEncoding]]; 517 | 518 | // XXX: these are quite random and not perfect 519 | XMLElement *bodyCell = [self bodyCellElementForDocument:document]; 520 | XMLElement *userLabel = [bodyCell firstElementMatchingPath:@"form/table/tr/td"]; 521 | XMLElement *commentSpan = [document firstElementMatchingPath:@"//span[@class='comment']"]; 522 | HNPageLayoutType pageLayoutType = [self pageLayoutTypeForDocument:document]; 523 | 524 | if (userLabel != nil && [[userLabel content] hasPrefix:@"user:"]) { 525 | result = [self parseUserProfileWithString:string]; 526 | } else if (commentSpan != nil || pageLayoutType == kHNPageLayoutTypeHeaderFooter) { 527 | result = [self parseCommentTreeInDocument:document]; 528 | } else { 529 | result = [self parseSubmissionsInDocument:document]; 530 | } 531 | 532 | [document release]; 533 | return result; 534 | } 535 | 536 | - (void)dealloc { 537 | [super dealloc]; 538 | } 539 | 540 | @end 541 | -------------------------------------------------------------------------------- /HNKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7D0116CE17E7C17C00BA5044 /* NSDate+TimeAgo.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D0116CD17E7C17C00BA5044 /* NSDate+TimeAgo.m */; }; 11 | 7D563E5416E4501E00BDD2DB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D563E5316E4501E00BDD2DB /* Foundation.framework */; }; 12 | 7D563F9C16E452B800BDD2DB /* NSObject+PerformSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F6F16E452B800BDD2DB /* NSObject+PerformSelector.m */; }; 13 | 7D563F9D16E452B800BDD2DB /* NSString+RemoveSuffix.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F7116E452B800BDD2DB /* NSString+RemoveSuffix.m */; }; 14 | 7D563F9E16E452B800BDD2DB /* NSURL+Parameters.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F7316E452B800BDD2DB /* NSURL+Parameters.m */; }; 15 | 7D563F9F16E452B800BDD2DB /* HNAnonymousSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F7616E452B800BDD2DB /* HNAnonymousSession.m */; }; 16 | 7D563FA016E452B800BDD2DB /* HNAPIRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F7816E452B800BDD2DB /* HNAPIRequest.m */; }; 17 | 7D563FA116E452B800BDD2DB /* HNAPIRequestParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F7A16E452B800BDD2DB /* HNAPIRequestParser.m */; }; 18 | 7D563FA216E452B800BDD2DB /* HNAPISearch.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F7C16E452B800BDD2DB /* HNAPISearch.m */; }; 19 | 7D563FA316E452B800BDD2DB /* HNAPISubmission.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F7E16E452B800BDD2DB /* HNAPISubmission.m */; }; 20 | 7D563FA416E452B800BDD2DB /* HNContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F8016E452B800BDD2DB /* HNContainer.m */; }; 21 | 7D563FA516E452B800BDD2DB /* HNEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F8216E452B800BDD2DB /* HNEntry.m */; }; 22 | 7D563FA616E452B800BDD2DB /* HNEntryList.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F8416E452B800BDD2DB /* HNEntryList.m */; }; 23 | 7D563FA716E452B800BDD2DB /* HNObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F8716E452B800BDD2DB /* HNObject.m */; }; 24 | 7D563FA816E452B800BDD2DB /* HNObjectBodyRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F8916E452B800BDD2DB /* HNObjectBodyRenderer.m */; }; 25 | 7D563FA916E452B800BDD2DB /* HNObjectCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F8B16E452B800BDD2DB /* HNObjectCache.m */; }; 26 | 7D563FAA16E452B800BDD2DB /* HNSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F8D16E452B800BDD2DB /* HNSession.m */; }; 27 | 7D563FAB16E452B800BDD2DB /* HNSessionAuthenticator.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F8F16E452B800BDD2DB /* HNSessionAuthenticator.m */; }; 28 | 7D563FAC16E452B800BDD2DB /* HNSessionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F9116E452B800BDD2DB /* HNSessionController.m */; }; 29 | 7D563FAD16E452B800BDD2DB /* HNSubmission.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F9416E452B800BDD2DB /* HNSubmission.m */; }; 30 | 7D563FAE16E452B800BDD2DB /* HNUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F9616E452B800BDD2DB /* HNUser.m */; }; 31 | 7D563FAF16E452B800BDD2DB /* XMLDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F9916E452B800BDD2DB /* XMLDocument.m */; }; 32 | 7D563FB016E452B800BDD2DB /* XMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563F9B16E452B800BDD2DB /* XMLElement.m */; }; 33 | 7D563FB316E4531700BDD2DB /* NSString+URLEncoding.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563FB216E4531700BDD2DB /* NSString+URLEncoding.m */; }; 34 | 7D563FB616E4532B00BDD2DB /* NSDictionary+Parameters.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563FB516E4532B00BDD2DB /* NSDictionary+Parameters.m */; }; 35 | 7D563FBE16E453E000BDD2DB /* NSString+Entities.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563FBB16E453E000BDD2DB /* NSString+Entities.m */; }; 36 | 7D563FBF16E453E000BDD2DB /* NSString+Tags.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563FBD16E453E000BDD2DB /* NSString+Tags.m */; }; 37 | 7D563FCE16E454C400BDD2DB /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D563FCD16E454C400BDD2DB /* CoreText.framework */; }; 38 | 7D563FD016E454C900BDD2DB /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D563FCF16E454C900BDD2DB /* libxml2.dylib */; }; 39 | 7D563FD216E455A900BDD2DB /* HNKit.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F8516E452B800BDD2DB /* HNKit.h */; }; 40 | 7D563FD316E455C900BDD2DB /* HNAnonymousSession.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F7516E452B800BDD2DB /* HNAnonymousSession.h */; }; 41 | 7D563FD416E455C900BDD2DB /* HNAPIRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F7716E452B800BDD2DB /* HNAPIRequest.h */; }; 42 | 7D563FD516E455C900BDD2DB /* HNAPISearch.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F7B16E452B800BDD2DB /* HNAPISearch.h */; }; 43 | 7D563FD616E455C900BDD2DB /* HNAPISubmission.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F7D16E452B800BDD2DB /* HNAPISubmission.h */; }; 44 | 7D563FD716E455C900BDD2DB /* HNContainer.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F7F16E452B800BDD2DB /* HNContainer.h */; }; 45 | 7D563FD816E455C900BDD2DB /* HNEntry.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F8116E452B800BDD2DB /* HNEntry.h */; }; 46 | 7D563FD916E455C900BDD2DB /* HNEntryList.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F8316E452B800BDD2DB /* HNEntryList.h */; }; 47 | 7D563FDA16E455C900BDD2DB /* HNObject.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F8616E452B800BDD2DB /* HNObject.h */; }; 48 | 7D563FDB16E455C900BDD2DB /* HNObjectBodyRenderer.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F8816E452B800BDD2DB /* HNObjectBodyRenderer.h */; }; 49 | 7D563FDC16E455C900BDD2DB /* HNSession.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F8C16E452B800BDD2DB /* HNSession.h */; }; 50 | 7D563FDD16E455C900BDD2DB /* HNSessionAuthenticator.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F8E16E452B800BDD2DB /* HNSessionAuthenticator.h */; }; 51 | 7D563FDE16E455C900BDD2DB /* HNSessionController.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F9016E452B800BDD2DB /* HNSessionController.h */; }; 52 | 7D563FDF16E455C900BDD2DB /* HNSubmission.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F9316E452B800BDD2DB /* HNSubmission.h */; }; 53 | 7D563FE016E455C900BDD2DB /* HNUser.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F9516E452B800BDD2DB /* HNUser.h */; }; 54 | 7D563FE116E4562100BDD2DB /* HNShared.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F9216E452B800BDD2DB /* HNShared.h */; }; 55 | 7D563FE216E4564100BDD2DB /* HNAPIRequestParser.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F7916E452B800BDD2DB /* HNAPIRequestParser.h */; }; 56 | 7D563FE316E4564100BDD2DB /* HNObjectCache.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F8A16E452B800BDD2DB /* HNObjectCache.h */; }; 57 | 7D563FE716E457E900BDD2DB /* HNNetworkActivityController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D563FE616E457E900BDD2DB /* HNNetworkActivityController.m */; }; 58 | 7D563FE816E4596400BDD2DB /* NSString+Tags.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563FBC16E453E000BDD2DB /* NSString+Tags.h */; }; 59 | 7D563FE916E4596400BDD2DB /* NSString+Entities.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563FB716E4533800BDD2DB /* NSString+Entities.h */; }; 60 | 7D563FEA16E4596400BDD2DB /* NSDictionary+Parameters.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563FB416E4532B00BDD2DB /* NSDictionary+Parameters.h */; }; 61 | 7D563FEB16E4596400BDD2DB /* NSString+URLEncoding.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563FB116E4531700BDD2DB /* NSString+URLEncoding.h */; }; 62 | 7D563FEC16E4596400BDD2DB /* NSObject+PerformSelector.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F6E16E452B800BDD2DB /* NSObject+PerformSelector.h */; }; 63 | 7D563FED16E4596400BDD2DB /* NSString+RemoveSuffix.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F7016E452B800BDD2DB /* NSString+RemoveSuffix.h */; }; 64 | 7D563FEE16E4596400BDD2DB /* NSURL+Parameters.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563F7216E452B800BDD2DB /* NSURL+Parameters.h */; }; 65 | 7D563FEF16E4596400BDD2DB /* HNNetworkActivityController.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D563FE516E457E900BDD2DB /* HNNetworkActivityController.h */; }; 66 | /* End PBXBuildFile section */ 67 | 68 | /* Begin PBXCopyFilesBuildPhase section */ 69 | 7D563E4E16E4501E00BDD2DB /* CopyFiles */ = { 70 | isa = PBXCopyFilesBuildPhase; 71 | buildActionMask = 2147483647; 72 | dstPath = "include/${PRODUCT_NAME}"; 73 | dstSubfolderSpec = 16; 74 | files = ( 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | 7D563FD116E4559000BDD2DB /* CopyFiles */ = { 79 | isa = PBXCopyFilesBuildPhase; 80 | buildActionMask = 2147483647; 81 | dstPath = "include/${PRODUCT_NAME}"; 82 | dstSubfolderSpec = 16; 83 | files = ( 84 | 7D563FE816E4596400BDD2DB /* NSString+Tags.h in CopyFiles */, 85 | 7D563FE916E4596400BDD2DB /* NSString+Entities.h in CopyFiles */, 86 | 7D563FEA16E4596400BDD2DB /* NSDictionary+Parameters.h in CopyFiles */, 87 | 7D563FEB16E4596400BDD2DB /* NSString+URLEncoding.h in CopyFiles */, 88 | 7D563FEC16E4596400BDD2DB /* NSObject+PerformSelector.h in CopyFiles */, 89 | 7D563FED16E4596400BDD2DB /* NSString+RemoveSuffix.h in CopyFiles */, 90 | 7D563FEE16E4596400BDD2DB /* NSURL+Parameters.h in CopyFiles */, 91 | 7D563FEF16E4596400BDD2DB /* HNNetworkActivityController.h in CopyFiles */, 92 | 7D563FE216E4564100BDD2DB /* HNAPIRequestParser.h in CopyFiles */, 93 | 7D563FE316E4564100BDD2DB /* HNObjectCache.h in CopyFiles */, 94 | 7D563FE116E4562100BDD2DB /* HNShared.h in CopyFiles */, 95 | 7D563FD316E455C900BDD2DB /* HNAnonymousSession.h in CopyFiles */, 96 | 7D563FD416E455C900BDD2DB /* HNAPIRequest.h in CopyFiles */, 97 | 7D563FD516E455C900BDD2DB /* HNAPISearch.h in CopyFiles */, 98 | 7D563FD616E455C900BDD2DB /* HNAPISubmission.h in CopyFiles */, 99 | 7D563FD716E455C900BDD2DB /* HNContainer.h in CopyFiles */, 100 | 7D563FD816E455C900BDD2DB /* HNEntry.h in CopyFiles */, 101 | 7D563FD916E455C900BDD2DB /* HNEntryList.h in CopyFiles */, 102 | 7D563FDA16E455C900BDD2DB /* HNObject.h in CopyFiles */, 103 | 7D563FDB16E455C900BDD2DB /* HNObjectBodyRenderer.h in CopyFiles */, 104 | 7D563FDC16E455C900BDD2DB /* HNSession.h in CopyFiles */, 105 | 7D563FDD16E455C900BDD2DB /* HNSessionAuthenticator.h in CopyFiles */, 106 | 7D563FDE16E455C900BDD2DB /* HNSessionController.h in CopyFiles */, 107 | 7D563FDF16E455C900BDD2DB /* HNSubmission.h in CopyFiles */, 108 | 7D563FE016E455C900BDD2DB /* HNUser.h in CopyFiles */, 109 | 7D563FD216E455A900BDD2DB /* HNKit.h in CopyFiles */, 110 | ); 111 | runOnlyForDeploymentPostprocessing = 0; 112 | }; 113 | /* End PBXCopyFilesBuildPhase section */ 114 | 115 | /* Begin PBXFileReference section */ 116 | 7D0116CC17E7C17C00BA5044 /* NSDate+TimeAgo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+TimeAgo.h"; sourceTree = ""; }; 117 | 7D0116CD17E7C17C00BA5044 /* NSDate+TimeAgo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+TimeAgo.m"; sourceTree = ""; }; 118 | 7D563E5016E4501E00BDD2DB /* libHNKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libHNKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; 119 | 7D563E5316E4501E00BDD2DB /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 120 | 7D563F6E16E452B800BDD2DB /* NSObject+PerformSelector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+PerformSelector.h"; sourceTree = ""; }; 121 | 7D563F6F16E452B800BDD2DB /* NSObject+PerformSelector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+PerformSelector.m"; sourceTree = ""; }; 122 | 7D563F7016E452B800BDD2DB /* NSString+RemoveSuffix.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+RemoveSuffix.h"; sourceTree = ""; }; 123 | 7D563F7116E452B800BDD2DB /* NSString+RemoveSuffix.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+RemoveSuffix.m"; sourceTree = ""; }; 124 | 7D563F7216E452B800BDD2DB /* NSURL+Parameters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURL+Parameters.h"; sourceTree = ""; }; 125 | 7D563F7316E452B800BDD2DB /* NSURL+Parameters.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURL+Parameters.m"; sourceTree = ""; }; 126 | 7D563F7516E452B800BDD2DB /* HNAnonymousSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNAnonymousSession.h; sourceTree = ""; }; 127 | 7D563F7616E452B800BDD2DB /* HNAnonymousSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNAnonymousSession.m; sourceTree = ""; }; 128 | 7D563F7716E452B800BDD2DB /* HNAPIRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNAPIRequest.h; sourceTree = ""; }; 129 | 7D563F7816E452B800BDD2DB /* HNAPIRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNAPIRequest.m; sourceTree = ""; }; 130 | 7D563F7916E452B800BDD2DB /* HNAPIRequestParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNAPIRequestParser.h; sourceTree = ""; }; 131 | 7D563F7A16E452B800BDD2DB /* HNAPIRequestParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNAPIRequestParser.m; sourceTree = ""; }; 132 | 7D563F7B16E452B800BDD2DB /* HNAPISearch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNAPISearch.h; sourceTree = ""; }; 133 | 7D563F7C16E452B800BDD2DB /* HNAPISearch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNAPISearch.m; sourceTree = ""; }; 134 | 7D563F7D16E452B800BDD2DB /* HNAPISubmission.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNAPISubmission.h; sourceTree = ""; }; 135 | 7D563F7E16E452B800BDD2DB /* HNAPISubmission.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNAPISubmission.m; sourceTree = ""; }; 136 | 7D563F7F16E452B800BDD2DB /* HNContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNContainer.h; sourceTree = ""; }; 137 | 7D563F8016E452B800BDD2DB /* HNContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNContainer.m; sourceTree = ""; }; 138 | 7D563F8116E452B800BDD2DB /* HNEntry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNEntry.h; sourceTree = ""; }; 139 | 7D563F8216E452B800BDD2DB /* HNEntry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNEntry.m; sourceTree = ""; }; 140 | 7D563F8316E452B800BDD2DB /* HNEntryList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNEntryList.h; sourceTree = ""; }; 141 | 7D563F8416E452B800BDD2DB /* HNEntryList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNEntryList.m; sourceTree = ""; }; 142 | 7D563F8516E452B800BDD2DB /* HNKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNKit.h; sourceTree = ""; }; 143 | 7D563F8616E452B800BDD2DB /* HNObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNObject.h; sourceTree = ""; }; 144 | 7D563F8716E452B800BDD2DB /* HNObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNObject.m; sourceTree = ""; }; 145 | 7D563F8816E452B800BDD2DB /* HNObjectBodyRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNObjectBodyRenderer.h; sourceTree = ""; }; 146 | 7D563F8916E452B800BDD2DB /* HNObjectBodyRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNObjectBodyRenderer.m; sourceTree = ""; }; 147 | 7D563F8A16E452B800BDD2DB /* HNObjectCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNObjectCache.h; sourceTree = ""; }; 148 | 7D563F8B16E452B800BDD2DB /* HNObjectCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNObjectCache.m; sourceTree = ""; }; 149 | 7D563F8C16E452B800BDD2DB /* HNSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNSession.h; sourceTree = ""; }; 150 | 7D563F8D16E452B800BDD2DB /* HNSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNSession.m; sourceTree = ""; }; 151 | 7D563F8E16E452B800BDD2DB /* HNSessionAuthenticator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNSessionAuthenticator.h; sourceTree = ""; }; 152 | 7D563F8F16E452B800BDD2DB /* HNSessionAuthenticator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNSessionAuthenticator.m; sourceTree = ""; }; 153 | 7D563F9016E452B800BDD2DB /* HNSessionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNSessionController.h; sourceTree = ""; }; 154 | 7D563F9116E452B800BDD2DB /* HNSessionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNSessionController.m; sourceTree = ""; }; 155 | 7D563F9216E452B800BDD2DB /* HNShared.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNShared.h; sourceTree = ""; }; 156 | 7D563F9316E452B800BDD2DB /* HNSubmission.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNSubmission.h; sourceTree = ""; }; 157 | 7D563F9416E452B800BDD2DB /* HNSubmission.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNSubmission.m; sourceTree = ""; }; 158 | 7D563F9516E452B800BDD2DB /* HNUser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNUser.h; sourceTree = ""; }; 159 | 7D563F9616E452B800BDD2DB /* HNUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNUser.m; sourceTree = ""; }; 160 | 7D563F9816E452B800BDD2DB /* XMLDocument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMLDocument.h; sourceTree = ""; }; 161 | 7D563F9916E452B800BDD2DB /* XMLDocument.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMLDocument.m; sourceTree = ""; }; 162 | 7D563F9A16E452B800BDD2DB /* XMLElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMLElement.h; sourceTree = ""; }; 163 | 7D563F9B16E452B800BDD2DB /* XMLElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMLElement.m; sourceTree = ""; }; 164 | 7D563FB116E4531700BDD2DB /* NSString+URLEncoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+URLEncoding.h"; sourceTree = ""; }; 165 | 7D563FB216E4531700BDD2DB /* NSString+URLEncoding.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+URLEncoding.m"; sourceTree = ""; }; 166 | 7D563FB416E4532B00BDD2DB /* NSDictionary+Parameters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Parameters.h"; sourceTree = ""; }; 167 | 7D563FB516E4532B00BDD2DB /* NSDictionary+Parameters.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Parameters.m"; sourceTree = ""; }; 168 | 7D563FB716E4533800BDD2DB /* NSString+Entities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Entities.h"; sourceTree = ""; }; 169 | 7D563FBB16E453E000BDD2DB /* NSString+Entities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Entities.m"; sourceTree = ""; }; 170 | 7D563FBC16E453E000BDD2DB /* NSString+Tags.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Tags.h"; sourceTree = ""; }; 171 | 7D563FBD16E453E000BDD2DB /* NSString+Tags.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Tags.m"; sourceTree = ""; }; 172 | 7D563FCD16E454C400BDD2DB /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; 173 | 7D563FCF16E454C900BDD2DB /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; }; 174 | 7D563FE516E457E900BDD2DB /* HNNetworkActivityController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNNetworkActivityController.h; sourceTree = ""; }; 175 | 7D563FE616E457E900BDD2DB /* HNNetworkActivityController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNNetworkActivityController.m; sourceTree = ""; }; 176 | /* End PBXFileReference section */ 177 | 178 | /* Begin PBXFrameworksBuildPhase section */ 179 | 7D563E4D16E4501E00BDD2DB /* Frameworks */ = { 180 | isa = PBXFrameworksBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | 7D563FD016E454C900BDD2DB /* libxml2.dylib in Frameworks */, 184 | 7D563FCE16E454C400BDD2DB /* CoreText.framework in Frameworks */, 185 | 7D563E5416E4501E00BDD2DB /* Foundation.framework in Frameworks */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | /* End PBXFrameworksBuildPhase section */ 190 | 191 | /* Begin PBXGroup section */ 192 | 7D563E4716E4501E00BDD2DB = { 193 | isa = PBXGroup; 194 | children = ( 195 | 7D563F6C16E452B800BDD2DB /* Classes */, 196 | 7D563E5216E4501E00BDD2DB /* Frameworks */, 197 | 7D563E5116E4501E00BDD2DB /* Products */, 198 | ); 199 | sourceTree = ""; 200 | }; 201 | 7D563E5116E4501E00BDD2DB /* Products */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 7D563E5016E4501E00BDD2DB /* libHNKit.a */, 205 | ); 206 | name = Products; 207 | sourceTree = ""; 208 | }; 209 | 7D563E5216E4501E00BDD2DB /* Frameworks */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | 7D563FCF16E454C900BDD2DB /* libxml2.dylib */, 213 | 7D563FCD16E454C400BDD2DB /* CoreText.framework */, 214 | 7D563E5316E4501E00BDD2DB /* Foundation.framework */, 215 | ); 216 | name = Frameworks; 217 | sourceTree = ""; 218 | }; 219 | 7D563F6C16E452B800BDD2DB /* Classes */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 7D563F6D16E452B800BDD2DB /* Categories */, 223 | 7D563F9716E452B800BDD2DB /* XML */, 224 | 7D563F7416E452B800BDD2DB /* HNKit */, 225 | ); 226 | path = Classes; 227 | sourceTree = ""; 228 | }; 229 | 7D563F6D16E452B800BDD2DB /* Categories */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | 7D563FBC16E453E000BDD2DB /* NSString+Tags.h */, 233 | 7D563FBD16E453E000BDD2DB /* NSString+Tags.m */, 234 | 7D563FB716E4533800BDD2DB /* NSString+Entities.h */, 235 | 7D563FBB16E453E000BDD2DB /* NSString+Entities.m */, 236 | 7D563FB416E4532B00BDD2DB /* NSDictionary+Parameters.h */, 237 | 7D563FB516E4532B00BDD2DB /* NSDictionary+Parameters.m */, 238 | 7D563FB116E4531700BDD2DB /* NSString+URLEncoding.h */, 239 | 7D563FB216E4531700BDD2DB /* NSString+URLEncoding.m */, 240 | 7D563F6E16E452B800BDD2DB /* NSObject+PerformSelector.h */, 241 | 7D563F6F16E452B800BDD2DB /* NSObject+PerformSelector.m */, 242 | 7D563F7016E452B800BDD2DB /* NSString+RemoveSuffix.h */, 243 | 7D563F7116E452B800BDD2DB /* NSString+RemoveSuffix.m */, 244 | 7D563F7216E452B800BDD2DB /* NSURL+Parameters.h */, 245 | 7D563F7316E452B800BDD2DB /* NSURL+Parameters.m */, 246 | 7D0116CC17E7C17C00BA5044 /* NSDate+TimeAgo.h */, 247 | 7D0116CD17E7C17C00BA5044 /* NSDate+TimeAgo.m */, 248 | ); 249 | path = Categories; 250 | sourceTree = ""; 251 | }; 252 | 7D563F7416E452B800BDD2DB /* HNKit */ = { 253 | isa = PBXGroup; 254 | children = ( 255 | 7D563F8516E452B800BDD2DB /* HNKit.h */, 256 | 7D563F9216E452B800BDD2DB /* HNShared.h */, 257 | 7D563FE516E457E900BDD2DB /* HNNetworkActivityController.h */, 258 | 7D563FE616E457E900BDD2DB /* HNNetworkActivityController.m */, 259 | 7D563F7516E452B800BDD2DB /* HNAnonymousSession.h */, 260 | 7D563F7616E452B800BDD2DB /* HNAnonymousSession.m */, 261 | 7D563F7716E452B800BDD2DB /* HNAPIRequest.h */, 262 | 7D563F7816E452B800BDD2DB /* HNAPIRequest.m */, 263 | 7D563F7916E452B800BDD2DB /* HNAPIRequestParser.h */, 264 | 7D563F7A16E452B800BDD2DB /* HNAPIRequestParser.m */, 265 | 7D563F7B16E452B800BDD2DB /* HNAPISearch.h */, 266 | 7D563F7C16E452B800BDD2DB /* HNAPISearch.m */, 267 | 7D563F7D16E452B800BDD2DB /* HNAPISubmission.h */, 268 | 7D563F7E16E452B800BDD2DB /* HNAPISubmission.m */, 269 | 7D563F7F16E452B800BDD2DB /* HNContainer.h */, 270 | 7D563F8016E452B800BDD2DB /* HNContainer.m */, 271 | 7D563F8116E452B800BDD2DB /* HNEntry.h */, 272 | 7D563F8216E452B800BDD2DB /* HNEntry.m */, 273 | 7D563F8316E452B800BDD2DB /* HNEntryList.h */, 274 | 7D563F8416E452B800BDD2DB /* HNEntryList.m */, 275 | 7D563F8616E452B800BDD2DB /* HNObject.h */, 276 | 7D563F8716E452B800BDD2DB /* HNObject.m */, 277 | 7D563F8816E452B800BDD2DB /* HNObjectBodyRenderer.h */, 278 | 7D563F8916E452B800BDD2DB /* HNObjectBodyRenderer.m */, 279 | 7D563F8A16E452B800BDD2DB /* HNObjectCache.h */, 280 | 7D563F8B16E452B800BDD2DB /* HNObjectCache.m */, 281 | 7D563F8C16E452B800BDD2DB /* HNSession.h */, 282 | 7D563F8D16E452B800BDD2DB /* HNSession.m */, 283 | 7D563F8E16E452B800BDD2DB /* HNSessionAuthenticator.h */, 284 | 7D563F8F16E452B800BDD2DB /* HNSessionAuthenticator.m */, 285 | 7D563F9016E452B800BDD2DB /* HNSessionController.h */, 286 | 7D563F9116E452B800BDD2DB /* HNSessionController.m */, 287 | 7D563F9316E452B800BDD2DB /* HNSubmission.h */, 288 | 7D563F9416E452B800BDD2DB /* HNSubmission.m */, 289 | 7D563F9516E452B800BDD2DB /* HNUser.h */, 290 | 7D563F9616E452B800BDD2DB /* HNUser.m */, 291 | ); 292 | path = HNKit; 293 | sourceTree = ""; 294 | }; 295 | 7D563F9716E452B800BDD2DB /* XML */ = { 296 | isa = PBXGroup; 297 | children = ( 298 | 7D563F9816E452B800BDD2DB /* XMLDocument.h */, 299 | 7D563F9916E452B800BDD2DB /* XMLDocument.m */, 300 | 7D563F9A16E452B800BDD2DB /* XMLElement.h */, 301 | 7D563F9B16E452B800BDD2DB /* XMLElement.m */, 302 | ); 303 | path = XML; 304 | sourceTree = ""; 305 | }; 306 | /* End PBXGroup section */ 307 | 308 | /* Begin PBXNativeTarget section */ 309 | 7D563E4F16E4501E00BDD2DB /* HNKit */ = { 310 | isa = PBXNativeTarget; 311 | buildConfigurationList = 7D563E5E16E4501E00BDD2DB /* Build configuration list for PBXNativeTarget "HNKit" */; 312 | buildPhases = ( 313 | 7D563E4C16E4501E00BDD2DB /* Sources */, 314 | 7D563E4D16E4501E00BDD2DB /* Frameworks */, 315 | 7D563E4E16E4501E00BDD2DB /* CopyFiles */, 316 | 7D563FD116E4559000BDD2DB /* CopyFiles */, 317 | ); 318 | buildRules = ( 319 | ); 320 | dependencies = ( 321 | ); 322 | name = HNKit; 323 | productName = HNKit; 324 | productReference = 7D563E5016E4501E00BDD2DB /* libHNKit.a */; 325 | productType = "com.apple.product-type.library.static"; 326 | }; 327 | /* End PBXNativeTarget section */ 328 | 329 | /* Begin PBXProject section */ 330 | 7D563E4816E4501E00BDD2DB /* Project object */ = { 331 | isa = PBXProject; 332 | attributes = { 333 | LastUpgradeCheck = 0460; 334 | ORGANIZATIONNAME = "Xuzz Productions, LLC"; 335 | }; 336 | buildConfigurationList = 7D563E4B16E4501E00BDD2DB /* Build configuration list for PBXProject "HNKit" */; 337 | compatibilityVersion = "Xcode 3.2"; 338 | developmentRegion = English; 339 | hasScannedForEncodings = 0; 340 | knownRegions = ( 341 | en, 342 | ); 343 | mainGroup = 7D563E4716E4501E00BDD2DB; 344 | productRefGroup = 7D563E5116E4501E00BDD2DB /* Products */; 345 | projectDirPath = ""; 346 | projectRoot = ""; 347 | targets = ( 348 | 7D563E4F16E4501E00BDD2DB /* HNKit */, 349 | ); 350 | }; 351 | /* End PBXProject section */ 352 | 353 | /* Begin PBXSourcesBuildPhase section */ 354 | 7D563E4C16E4501E00BDD2DB /* Sources */ = { 355 | isa = PBXSourcesBuildPhase; 356 | buildActionMask = 2147483647; 357 | files = ( 358 | 7D563F9C16E452B800BDD2DB /* NSObject+PerformSelector.m in Sources */, 359 | 7D563F9D16E452B800BDD2DB /* NSString+RemoveSuffix.m in Sources */, 360 | 7D563F9E16E452B800BDD2DB /* NSURL+Parameters.m in Sources */, 361 | 7D563F9F16E452B800BDD2DB /* HNAnonymousSession.m in Sources */, 362 | 7D563FA016E452B800BDD2DB /* HNAPIRequest.m in Sources */, 363 | 7D0116CE17E7C17C00BA5044 /* NSDate+TimeAgo.m in Sources */, 364 | 7D563FA116E452B800BDD2DB /* HNAPIRequestParser.m in Sources */, 365 | 7D563FA216E452B800BDD2DB /* HNAPISearch.m in Sources */, 366 | 7D563FA316E452B800BDD2DB /* HNAPISubmission.m in Sources */, 367 | 7D563FA416E452B800BDD2DB /* HNContainer.m in Sources */, 368 | 7D563FA516E452B800BDD2DB /* HNEntry.m in Sources */, 369 | 7D563FA616E452B800BDD2DB /* HNEntryList.m in Sources */, 370 | 7D563FA716E452B800BDD2DB /* HNObject.m in Sources */, 371 | 7D563FA816E452B800BDD2DB /* HNObjectBodyRenderer.m in Sources */, 372 | 7D563FA916E452B800BDD2DB /* HNObjectCache.m in Sources */, 373 | 7D563FAA16E452B800BDD2DB /* HNSession.m in Sources */, 374 | 7D563FAB16E452B800BDD2DB /* HNSessionAuthenticator.m in Sources */, 375 | 7D563FAC16E452B800BDD2DB /* HNSessionController.m in Sources */, 376 | 7D563FAD16E452B800BDD2DB /* HNSubmission.m in Sources */, 377 | 7D563FAE16E452B800BDD2DB /* HNUser.m in Sources */, 378 | 7D563FAF16E452B800BDD2DB /* XMLDocument.m in Sources */, 379 | 7D563FB016E452B800BDD2DB /* XMLElement.m in Sources */, 380 | 7D563FB316E4531700BDD2DB /* NSString+URLEncoding.m in Sources */, 381 | 7D563FB616E4532B00BDD2DB /* NSDictionary+Parameters.m in Sources */, 382 | 7D563FBE16E453E000BDD2DB /* NSString+Entities.m in Sources */, 383 | 7D563FBF16E453E000BDD2DB /* NSString+Tags.m in Sources */, 384 | 7D563FE716E457E900BDD2DB /* HNNetworkActivityController.m in Sources */, 385 | ); 386 | runOnlyForDeploymentPostprocessing = 0; 387 | }; 388 | /* End PBXSourcesBuildPhase section */ 389 | 390 | /* Begin XCBuildConfiguration section */ 391 | 7D563E5C16E4501E00BDD2DB /* Debug */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ALWAYS_SEARCH_USER_PATHS = NO; 395 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 396 | CLANG_CXX_LIBRARY = "libc++"; 397 | CLANG_WARN_CONSTANT_CONVERSION = YES; 398 | CLANG_WARN_EMPTY_BODY = YES; 399 | CLANG_WARN_ENUM_CONVERSION = YES; 400 | CLANG_WARN_INT_CONVERSION = YES; 401 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 402 | COPY_PHASE_STRIP = NO; 403 | GCC_C_LANGUAGE_STANDARD = gnu99; 404 | GCC_DYNAMIC_NO_PIC = NO; 405 | GCC_OPTIMIZATION_LEVEL = 0; 406 | GCC_PREPROCESSOR_DEFINITIONS = ( 407 | "DEBUG=1", 408 | "$(inherited)", 409 | ); 410 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 411 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 412 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 413 | GCC_WARN_UNUSED_VARIABLE = YES; 414 | HEADER_SEARCH_PATHS = /usr/include/libxml2; 415 | IPHONEOS_DEPLOYMENT_TARGET = 5.0; 416 | ONLY_ACTIVE_ARCH = NO; 417 | SDKROOT = iphoneos; 418 | }; 419 | name = Debug; 420 | }; 421 | 7D563E5D16E4501E00BDD2DB /* Release */ = { 422 | isa = XCBuildConfiguration; 423 | buildSettings = { 424 | ALWAYS_SEARCH_USER_PATHS = NO; 425 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 426 | CLANG_CXX_LIBRARY = "libc++"; 427 | CLANG_WARN_CONSTANT_CONVERSION = YES; 428 | CLANG_WARN_EMPTY_BODY = YES; 429 | CLANG_WARN_ENUM_CONVERSION = YES; 430 | CLANG_WARN_INT_CONVERSION = YES; 431 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 432 | COPY_PHASE_STRIP = YES; 433 | GCC_C_LANGUAGE_STANDARD = gnu99; 434 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 435 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 436 | GCC_WARN_UNUSED_VARIABLE = YES; 437 | HEADER_SEARCH_PATHS = /usr/include/libxml2; 438 | IPHONEOS_DEPLOYMENT_TARGET = 5.0; 439 | SDKROOT = iphoneos; 440 | VALIDATE_PRODUCT = YES; 441 | }; 442 | name = Release; 443 | }; 444 | 7D563E5F16E4501E00BDD2DB /* Debug */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | DSTROOT = /tmp/HNKit.dst; 448 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 449 | ONLY_ACTIVE_ARCH = NO; 450 | OTHER_LDFLAGS = "-ObjC"; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | SKIP_INSTALL = YES; 453 | }; 454 | name = Debug; 455 | }; 456 | 7D563E6016E4501E00BDD2DB /* Release */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | DSTROOT = /tmp/HNKit.dst; 460 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 461 | ONLY_ACTIVE_ARCH = NO; 462 | OTHER_LDFLAGS = "-ObjC"; 463 | PRODUCT_NAME = "$(TARGET_NAME)"; 464 | SKIP_INSTALL = YES; 465 | }; 466 | name = Release; 467 | }; 468 | /* End XCBuildConfiguration section */ 469 | 470 | /* Begin XCConfigurationList section */ 471 | 7D563E4B16E4501E00BDD2DB /* Build configuration list for PBXProject "HNKit" */ = { 472 | isa = XCConfigurationList; 473 | buildConfigurations = ( 474 | 7D563E5C16E4501E00BDD2DB /* Debug */, 475 | 7D563E5D16E4501E00BDD2DB /* Release */, 476 | ); 477 | defaultConfigurationIsVisible = 0; 478 | defaultConfigurationName = Release; 479 | }; 480 | 7D563E5E16E4501E00BDD2DB /* Build configuration list for PBXNativeTarget "HNKit" */ = { 481 | isa = XCConfigurationList; 482 | buildConfigurations = ( 483 | 7D563E5F16E4501E00BDD2DB /* Debug */, 484 | 7D563E6016E4501E00BDD2DB /* Release */, 485 | ); 486 | defaultConfigurationIsVisible = 0; 487 | defaultConfigurationName = Release; 488 | }; 489 | /* End XCConfigurationList section */ 490 | }; 491 | rootObject = 7D563E4816E4501E00BDD2DB /* Project object */; 492 | } 493 | --------------------------------------------------------------------------------
"; 51 | NSString *mid = @":"; 52 | NSString *end = @"