├── .gitignore ├── LICENSE ├── README.md ├── RSXML.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcbaselines │ └── 84F22C161B52DDEA000060CE.xcbaseline │ ├── 8756A8AF-348E-4D58-81F2-569F71868FB1.plist │ ├── B9957C87-0F96-4C9B-864E-DF5BD201D1DC.plist │ └── Info.plist ├── RSXML ├── FeedParser.h ├── Info.plist ├── NSString+RSXML.h ├── NSString+RSXML.m ├── RSAtomParser.h ├── RSAtomParser.m ├── RSDateParser.h ├── RSDateParser.m ├── RSFeedParser.h ├── RSFeedParser.m ├── RSHTMLLinkParser.h ├── RSHTMLLinkParser.m ├── RSHTMLMetadata.h ├── RSHTMLMetadata.m ├── RSHTMLMetadataParser.h ├── RSHTMLMetadataParser.m ├── RSOPMLAttributes.h ├── RSOPMLAttributes.m ├── RSOPMLDocument.h ├── RSOPMLDocument.m ├── RSOPMLFeedSpecifier.h ├── RSOPMLFeedSpecifier.m ├── RSOPMLItem.h ├── RSOPMLItem.m ├── RSOPMLParser.h ├── RSOPMLParser.m ├── RSParsedArticle.h ├── RSParsedArticle.m ├── RSParsedFeed.h ├── RSParsedFeed.m ├── RSRSSParser.h ├── RSRSSParser.m ├── RSSAXHTMLParser.h ├── RSSAXHTMLParser.m ├── RSSAXParser.h ├── RSSAXParser.m ├── RSXML.h ├── RSXMLData.h ├── RSXMLData.m ├── RSXMLError.h ├── RSXMLError.m ├── RSXMLInternal.h └── RSXMLInternal.m ├── RSXMLTests ├── Info.plist ├── RSDateParserTests.m ├── RSEntityTests.m ├── RSHTMLTests.m ├── RSOPMLTests.m ├── RSXMLTests.m └── Resources │ ├── DaringFireball.html │ ├── DaringFireball.rss │ ├── EMarley.rss │ ├── KatieFloyd.rss │ ├── OneFootTsunami.atom │ ├── Subs.opml │ ├── TimerSearch.txt │ ├── furbo.html │ ├── inessential.html │ ├── manton.rss │ ├── scriptingNews.rss │ └── sixcolors.html └── RSXMLiOS └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/screenshots 54 | 55 | #Code Injection 56 | # 57 | # After new code Injection tools there's a generated folder /iOSInjectionProject 58 | # https://github.com/johnno1962/injectionforxcode 59 | 60 | iOSInjectionProject/ 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 brentsimmons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSXML 2 | 3 | This is utility code for parsing XML and HTML using libXML2’s SAX parser. 4 | 5 | It builds two framework targets: one for Mac, one for iOS. It does not depend on any other third-party frameworks. The code is Objective-C with ARC. 6 | 7 | #### The gist 8 | 9 | To parse XML, create an `RSSAXParserDelegate`. (There are examples in the framework that you can crib from.) 10 | 11 | To parse HTML, create an `RSSAXHTMLParserDelegate`. (There are examples for this too.) 12 | 13 | #### Goodies and Extras 14 | 15 | There are three XML parsers included, for OPML, RSS, and Atom. To parse OPML, see `RSOPMLParser`. To parse RSS and Atom, see `RSFeedParser`. 16 | 17 | These parsers may or may not be complete enough for your needs. You could, in theory, start writing an RSS reader just with these. (And, if you want to, go for it, with my blessing.) 18 | 19 | There are two HTML parsers included. `RSHTMLMetadataParser` pulls metadata from the head section of an HTML document. `RSHTMLLinkParser` pulls all the links (anchors, <a href=…> tags) from an HTML document. 20 | 21 | Other possibly interesting things: 22 | 23 | `RSDateParser` makes it easy to parse dates in the formats found in various types of feeds. 24 | 25 | `NSString+RSXML` decodes HTML entities. 26 | 27 | Also note: there are some unit tests. 28 | 29 | #### Why use libXML2’s SAX API? 30 | 31 | SAX is kind of a pain because of all the state you have to manage. But it’s fastest and uses the least amount of memory. 32 | 33 | An alternative is to use `NSXMLParser`, which is event-driven like SAX. However, RSXML was written to avoid allocating Objective-C objects except when absolutely needed. You’ll note use of things like `memcp` and `strncmp`. 34 | 35 | Normally I avoid this kind of thing *strenuously*. I prefer to work at the highest level possible. 36 | 37 | But my more-than-a-decade of experience parsing XML has led me to this solution, which — last time I checked, which was, admittedly, a few years ago — was not only fastest but also uses the least memory. (The two things are related, of course: creating objects is bad for performance, so this code attempts to do the minimum possible.) 38 | 39 | All that low-level stuff is encapsulated, however. If you parse a feed, for instance, the caller gets an `RSParsedFeed` which contains `RSParsedArticle`s, and they’re standard Objective-C objects. It’s only inside your `RSSAXParserDelegate` and `RSSAXHTMLParserDelegate` where you’ll need to deal with C. -------------------------------------------------------------------------------- /RSXML.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RSXML.xcodeproj/xcshareddata/xcbaselines/84F22C161B52DDEA000060CE.xcbaseline/8756A8AF-348E-4D58-81F2-569F71868FB1.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | RSHTMLTests 8 | 9 | testSixcolorsPerformance 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.0022932 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /RSXML.xcodeproj/xcshareddata/xcbaselines/84F22C161B52DDEA000060CE.xcbaseline/B9957C87-0F96-4C9B-864E-DF5BD201D1DC.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | RSHTMLTests 8 | 9 | testDaringFireballPerformance 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.001 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | testFurboPerformance 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 0.00055962 25 | baselineIntegrationDisplayName 26 | Local Baseline 27 | 28 | 29 | testInessentialPerformance 30 | 31 | com.apple.XCTPerformanceMetric_WallClockTime 32 | 33 | baselineAverage 34 | 0.00027723 35 | baselineIntegrationDisplayName 36 | Local Baseline 37 | 38 | 39 | testSixcolorsPerformance 40 | 41 | com.apple.XCTPerformanceMetric_WallClockTime 42 | 43 | baselineAverage 44 | 0.00085183 45 | baselineIntegrationDisplayName 46 | Local Baseline 47 | 48 | 49 | 50 | RSOPMLTests 51 | 52 | testBigSubsPerformance 53 | 54 | com.apple.XCTPerformanceMetric_WallClockTime 55 | 56 | baselineAverage 57 | 0.026 58 | baselineIntegrationDisplayName 59 | Local Baseline 60 | 61 | 62 | 63 | RSXMLTests 64 | 65 | testDaringFireballPerformance 66 | 67 | com.apple.XCTPerformanceMetric_WallClockTime 68 | 69 | baselineAverage 70 | 0.02 71 | baselineIntegrationDisplayName 72 | Local Baseline 73 | 74 | 75 | testMantonPerformance 76 | 77 | com.apple.XCTPerformanceMetric_WallClockTime 78 | 79 | baselineAverage 80 | 0.003 81 | baselineIntegrationDisplayName 82 | Local Baseline 83 | 84 | 85 | testOFTPerformance 86 | 87 | com.apple.XCTPerformanceMetric_WallClockTime 88 | 89 | baselineAverage 90 | 0.007 91 | baselineIntegrationDisplayName 92 | Local Baseline 93 | 94 | 95 | testScriptingNewsPerformance 96 | 97 | com.apple.XCTPerformanceMetric_WallClockTime 98 | 99 | baselineAverage 100 | 0.008 101 | baselineIntegrationDisplayName 102 | Local Baseline 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /RSXML.xcodeproj/xcshareddata/xcbaselines/84F22C161B52DDEA000060CE.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 8756A8AF-348E-4D58-81F2-569F71868FB1 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i5 17 | cpuSpeedInMHz 18 | 1800 19 | logicalCPUCoresPerPackage 20 | 4 21 | modelCode 22 | MacBookAir5,2 23 | physicalCPUCoresPerPackage 24 | 2 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | 31 | B9957C87-0F96-4C9B-864E-DF5BD201D1DC 32 | 33 | localComputer 34 | 35 | busSpeedInMHz 36 | 100 37 | cpuCount 38 | 1 39 | cpuKind 40 | Intel Core i7 41 | cpuSpeedInMHz 42 | 3400 43 | logicalCPUCoresPerPackage 44 | 8 45 | modelCode 46 | iMac13,2 47 | physicalCPUCoresPerPackage 48 | 4 49 | platformIdentifier 50 | com.apple.platform.macosx 51 | 52 | targetArchitecture 53 | x86_64 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /RSXML/FeedParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // FeedParser.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 7/12/15. 6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @class RSParsedFeed; 12 | @class RSXMLData; 13 | 14 | 15 | @protocol FeedParser 16 | 17 | + (BOOL)canParseFeed:(RSXMLData * _Nonnull)xmlData; 18 | 19 | - (nonnull instancetype)initWithXMLData:(RSXMLData * _Nonnull)xmlData; 20 | 21 | - (nullable RSParsedFeed *)parseFeed:(NSError * _Nullable * _Nullable)error; 22 | 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /RSXML/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2015 Ranchero Software, LLC. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /RSXML/NSString+RSXML.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+RSXML.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 9/25/15. 6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @interface NSString (RSXML) 12 | 13 | - (NSString *)rs_stringByDecodingHTMLEntities; 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /RSXML/NSString+RSXML.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+RSXML.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 9/25/15. 6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import "NSString+RSXML.h" 10 | 11 | 12 | @interface NSScanner (RSXML) 13 | 14 | - (BOOL)rs_scanEntityValue:(NSString * _Nullable * _Nullable)decodedEntity; 15 | 16 | @end 17 | 18 | 19 | @implementation NSString (RSXML) 20 | 21 | - (NSString *)rs_stringByDecodingHTMLEntities { 22 | 23 | @autoreleasepool { 24 | 25 | NSScanner *scanner = [[NSScanner alloc] initWithString:self]; 26 | scanner.charactersToBeSkipped = nil; 27 | NSMutableString *result = [[NSMutableString alloc] init]; 28 | 29 | while (true) { 30 | 31 | NSString *scannedString = nil; 32 | if ([scanner scanUpToString:@"&" intoString:&scannedString]) { 33 | [result appendString:scannedString]; 34 | } 35 | if (scanner.isAtEnd) { 36 | break; 37 | } 38 | NSUInteger savedScanLocation = scanner.scanLocation; 39 | 40 | NSString *decodedEntity = nil; 41 | if ([scanner rs_scanEntityValue:&decodedEntity]) { 42 | [result appendString:decodedEntity]; 43 | } 44 | else { 45 | [result appendString:@"&"]; 46 | scanner.scanLocation = savedScanLocation + 1; 47 | } 48 | 49 | if (scanner.isAtEnd) { 50 | break; 51 | } 52 | } 53 | 54 | if ([self isEqualToString:result]) { 55 | return self; 56 | } 57 | return [result copy]; 58 | } 59 | } 60 | 61 | 62 | static NSDictionary *RSEntitiesDictionary(void); 63 | static NSString *RSXMLStringWithValue(unichar value); 64 | 65 | - (NSString * _Nullable)rs_stringByDecodingEntity { 66 | 67 | // self may or may not have outer & and ; characters. 68 | 69 | NSMutableString *s = [self mutableCopy]; 70 | 71 | if ([s hasPrefix:@"&"]) { 72 | [s deleteCharactersInRange:NSMakeRange(0, 1)]; 73 | } 74 | if ([s hasSuffix:@";"]) { 75 | [s deleteCharactersInRange:NSMakeRange(s.length - 1, 1)]; 76 | } 77 | 78 | NSDictionary *entitiesDictionary = RSEntitiesDictionary(); 79 | 80 | NSString *decodedEntity = entitiesDictionary[self]; 81 | if (decodedEntity) { 82 | return decodedEntity; 83 | } 84 | 85 | if ([s hasPrefix:@"#x"]) { // Hex 86 | NSScanner *scanner = [[NSScanner alloc] initWithString:s]; 87 | scanner.charactersToBeSkipped = [NSCharacterSet characterSetWithCharactersInString:@"#x"]; 88 | unsigned int hexValue = 0; 89 | if ([scanner scanHexInt:&hexValue]) { 90 | return RSXMLStringWithValue((unichar)hexValue); 91 | } 92 | return nil; 93 | } 94 | 95 | else if ([s hasPrefix:@"#"]) { 96 | [s deleteCharactersInRange:NSMakeRange(0, 1)]; 97 | NSInteger value = s.integerValue; 98 | if (value < 1) { 99 | return nil; 100 | } 101 | return RSXMLStringWithValue((unichar)value); 102 | } 103 | 104 | return nil; 105 | } 106 | 107 | @end 108 | 109 | @implementation NSScanner (RSXML) 110 | 111 | - (BOOL)rs_scanEntityValue:(NSString * _Nullable * _Nullable)decodedEntity { 112 | 113 | NSString *s = self.string; 114 | NSUInteger initialScanLocation = self.scanLocation; 115 | static NSUInteger maxEntityLength = 20; // It’s probably smaller, but this is just for sanity. 116 | 117 | while (true) { 118 | 119 | unichar ch = [s characterAtIndex:self.scanLocation]; 120 | if ([NSCharacterSet.whitespaceAndNewlineCharacterSet characterIsMember:ch]) { 121 | break; 122 | } 123 | if (ch == ';') { 124 | if (!decodedEntity) { 125 | return YES; 126 | } 127 | NSString *rawEntity = [s substringWithRange:NSMakeRange(initialScanLocation + 1, (self.scanLocation - initialScanLocation) - 1)]; 128 | *decodedEntity = [rawEntity rs_stringByDecodingEntity]; 129 | self.scanLocation = self.scanLocation + 1; 130 | return *decodedEntity != nil; 131 | } 132 | 133 | self.scanLocation = self.scanLocation + 1; 134 | if (self.scanLocation - initialScanLocation > maxEntityLength) { 135 | break; 136 | } 137 | if (self.isAtEnd) { 138 | break; 139 | } 140 | } 141 | 142 | return NO; 143 | } 144 | 145 | @end 146 | 147 | static NSString *RSXMLStringWithValue(unichar value) { 148 | 149 | return [[NSString alloc] initWithFormat:@"%C", value]; 150 | } 151 | 152 | static NSDictionary *RSEntitiesDictionary(void) { 153 | 154 | static NSDictionary *entitiesDictionary = nil; 155 | 156 | static dispatch_once_t onceToken; 157 | dispatch_once(&onceToken, ^{ 158 | 159 | entitiesDictionary = 160 | @{@"#034": @"\"", 161 | @"#038": @"&", 162 | @"#38": @"&", 163 | @"#039": @"'", 164 | @"#145": @"‘", 165 | @"#146": @"’", 166 | @"#147": @"“", 167 | @"#148": @"”", 168 | @"#149": @"•", 169 | @"#150": @"-", 170 | @"#151": @"—", 171 | @"#153": @"™", 172 | @"#160": RSXMLStringWithValue(160), 173 | @"#161": @"¡", 174 | @"#162": @"¢", 175 | @"#163": @"£", 176 | @"#164": @"?", 177 | @"#165": @"¥", 178 | @"#166": @"?", 179 | @"#167": @"§", 180 | @"#168": @"¨", 181 | @"#169": @"©", 182 | @"#170": @"©", 183 | @"#171": @"«", 184 | @"#172": @"¬", 185 | @"#173": @"¬", 186 | @"#174": @"®", 187 | @"#175": @"¯", 188 | @"#176": @"°", 189 | @"#177": @"±", 190 | @"#178": @" ", 191 | @"#179": @" ", 192 | @"#180": @"´", 193 | @"#181": @"µ", 194 | @"#182": @"µ", 195 | @"#183": @"·", 196 | @"#184": @"¸", 197 | @"#185": @" ", 198 | @"#186": @"º", 199 | @"#187": @"»", 200 | @"#188": @"1/4", 201 | @"#189": @"1/2", 202 | @"#190": @"1/2", 203 | @"#191": @"¿", 204 | @"#192": @"À", 205 | @"#193": @"Á", 206 | @"#194": @"Â", 207 | @"#195": @"Ã", 208 | @"#196": @"Ä", 209 | @"#197": @"Å", 210 | @"#198": @"Æ", 211 | @"#199": @"Ç", 212 | @"#200": @"È", 213 | @"#201": @"É", 214 | @"#202": @"Ê", 215 | @"#203": @"Ë", 216 | @"#204": @"Ì", 217 | @"#205": @"Í", 218 | @"#206": @"Î", 219 | @"#207": @"Ï", 220 | @"#208": @"?", 221 | @"#209": @"Ñ", 222 | @"#210": @"Ò", 223 | @"#211": @"Ó", 224 | @"#212": @"Ô", 225 | @"#213": @"Õ", 226 | @"#214": @"Ö", 227 | @"#215": @"x", 228 | @"#216": @"Ø", 229 | @"#217": @"Ù", 230 | @"#218": @"Ú", 231 | @"#219": @"Û", 232 | @"#220": @"Ü", 233 | @"#221": @"Y", 234 | @"#222": @"?", 235 | @"#223": @"ß", 236 | @"#224": @"à", 237 | @"#225": @"á", 238 | @"#226": @"â", 239 | @"#227": @"ã", 240 | @"#228": @"ä", 241 | @"#229": @"å", 242 | @"#230": @"æ", 243 | @"#231": @"ç", 244 | @"#232": @"è", 245 | @"#233": @"é", 246 | @"#234": @"ê", 247 | @"#235": @"ë", 248 | @"#236": @"ì", 249 | @"#237": @"í", 250 | @"#238": @"î", 251 | @"#239": @"ï", 252 | @"#240": @"?", 253 | @"#241": @"ñ", 254 | @"#242": @"ò", 255 | @"#243": @"ó", 256 | @"#244": @"ô", 257 | @"#245": @"õ", 258 | @"#246": @"ö", 259 | @"#247": @"÷", 260 | @"#248": @"ø", 261 | @"#249": @"ù", 262 | @"#250": @"ú", 263 | @"#251": @"û", 264 | @"#252": @"ü", 265 | @"#253": @"y", 266 | @"#254": @"?", 267 | @"#255": @"ÿ", 268 | @"#32": @" ", 269 | @"#34": @"\"", 270 | @"#39": @"", 271 | @"#8194": @" ", 272 | @"#8195": @" ", 273 | @"#8211": @"-", 274 | @"#8212": @"—", 275 | @"#8216": @"‘", 276 | @"#8217": @"’", 277 | @"#8220": @"“", 278 | @"#8221": @"”", 279 | @"#8230": @"…", 280 | @"#8617": RSXMLStringWithValue(8617), 281 | @"AElig": @"Æ", 282 | @"Aacute": @"Á", 283 | @"Acirc": @"Â", 284 | @"Agrave": @"À", 285 | @"Aring": @"Å", 286 | @"Atilde": @"Ã", 287 | @"Auml": @"Ä", 288 | @"Ccedil": @"Ç", 289 | @"Dstrok": @"?", 290 | @"ETH": @"?", 291 | @"Eacute": @"É", 292 | @"Ecirc": @"Ê", 293 | @"Egrave": @"È", 294 | @"Euml": @"Ë", 295 | @"Iacute": @"Í", 296 | @"Icirc": @"Î", 297 | @"Igrave": @"Ì", 298 | @"Iuml": @"Ï", 299 | @"Ntilde": @"Ñ", 300 | @"Oacute": @"Ó", 301 | @"Ocirc": @"Ô", 302 | @"Ograve": @"Ò", 303 | @"Oslash": @"Ø", 304 | @"Otilde": @"Õ", 305 | @"Ouml": @"Ö", 306 | @"Pi": @"Π", 307 | @"THORN": @"?", 308 | @"Uacute": @"Ú", 309 | @"Ucirc": @"Û", 310 | @"Ugrave": @"Ù", 311 | @"Uuml": @"Ü", 312 | @"Yacute": @"Y", 313 | @"aacute": @"á", 314 | @"acirc": @"â", 315 | @"acute": @"´", 316 | @"aelig": @"æ", 317 | @"agrave": @"à", 318 | @"amp": @"&", 319 | @"apos": @"'", 320 | @"aring": @"å", 321 | @"atilde": @"ã", 322 | @"auml": @"ä", 323 | @"brkbar": @"?", 324 | @"brvbar": @"?", 325 | @"ccedil": @"ç", 326 | @"cedil": @"¸", 327 | @"cent": @"¢", 328 | @"copy": @"©", 329 | @"curren": @"?", 330 | @"deg": @"°", 331 | @"die": @"?", 332 | @"divide": @"÷", 333 | @"eacute": @"é", 334 | @"ecirc": @"ê", 335 | @"egrave": @"è", 336 | @"eth": @"?", 337 | @"euml": @"ë", 338 | @"euro": @"€", 339 | @"frac12": @"1/2", 340 | @"frac14": @"1/4", 341 | @"frac34": @"3/4", 342 | @"gt": @">", 343 | @"hearts": @"♥", 344 | @"hellip": @"…", 345 | @"iacute": @"í", 346 | @"icirc": @"î", 347 | @"iexcl": @"¡", 348 | @"igrave": @"ì", 349 | @"iquest": @"¿", 350 | @"iuml": @"ï", 351 | @"laquo": @"«", 352 | @"ldquo": @"“", 353 | @"lsquo": @"‘", 354 | @"lt": @"<", 355 | @"macr": @"¯", 356 | @"mdash": @"—", 357 | @"micro": @"µ", 358 | @"middot": @"·", 359 | @"ndash": @"-", 360 | @"not": @"¬", 361 | @"ntilde": @"ñ", 362 | @"oacute": @"ó", 363 | @"ocirc": @"ô", 364 | @"ograve": @"ò", 365 | @"ordf": @"ª", 366 | @"ordm": @"º", 367 | @"oslash": @"ø", 368 | @"otilde": @"õ", 369 | @"ouml": @"ö", 370 | @"para": @"¶", 371 | @"pi": @"π", 372 | @"plusmn": @"±", 373 | @"pound": @"£", 374 | @"quot": @"\"", 375 | @"raquo": @"»", 376 | @"rdquo": @"”", 377 | @"reg": @"®", 378 | @"rsquo": @"’", 379 | @"sect": @"§", 380 | @"shy": @" ", 381 | @"sup1": @" ", 382 | @"sup2": @" ", 383 | @"sup3": @" ", 384 | @"szlig": @"ß", 385 | @"thorn": @"?", 386 | @"times": @"x", 387 | @"trade": @"™", 388 | @"uacute": @"ú", 389 | @"ucirc": @"û", 390 | @"ugrave": @"ù", 391 | @"uml": @"¨", 392 | @"uuml": @"ü", 393 | @"yacute": @"y", 394 | @"yen": @"¥", 395 | @"yuml": @"ÿ", 396 | @"infin": @"∞", 397 | @"nbsp": RSXMLStringWithValue(160), 398 | @"#x21A9": RSXMLStringWithValue(8617), 399 | @"#xFE0E": RSXMLStringWithValue(65038), 400 | @"#x2019": RSXMLStringWithValue(8217), 401 | @"#x2026": RSXMLStringWithValue(8230), 402 | @"#x201C": RSXMLStringWithValue(8220), 403 | @"#x201D": RSXMLStringWithValue(8221), 404 | @"#x2014": RSXMLStringWithValue(8212)}; 405 | }); 406 | 407 | return entitiesDictionary; 408 | } 409 | -------------------------------------------------------------------------------- /RSXML/RSAtomParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSAtomParser.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 1/15/15. 6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved. 7 | // 8 | 9 | #import "FeedParser.h" 10 | 11 | @interface RSAtomParser : NSObject 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /RSXML/RSAtomParser.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSAtomParser.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 1/15/15. 6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RSAtomParser.h" 11 | #import "RSSAXParser.h" 12 | #import "FeedParser.h" 13 | #import "RSParsedFeed.h" 14 | #import "RSParsedArticle.h" 15 | #import "RSXMLData.h" 16 | #import "NSString+RSXML.h" 17 | #import "RSDateParser.h" 18 | 19 | 20 | @interface RSAtomParser () 21 | 22 | @property (nonatomic) NSData *feedData; 23 | @property (nonatomic) NSString *urlString; 24 | @property (nonatomic) BOOL endFeedFound; 25 | @property (nonatomic) BOOL parsingXHTML; 26 | @property (nonatomic) BOOL parsingSource; 27 | @property (nonatomic) BOOL parsingArticle; 28 | @property (nonatomic) BOOL parsingAuthor; 29 | @property (nonatomic) NSMutableArray *attributesStack; 30 | @property (nonatomic, readonly) NSDictionary *currentAttributes; 31 | @property (nonatomic) NSMutableString *xhtmlString; 32 | @property (nonatomic) NSString *link; 33 | @property (nonatomic) NSString *title; 34 | @property (nonatomic) NSMutableArray *articles; 35 | @property (nonatomic) NSDate *dateParsed; 36 | @property (nonatomic) RSSAXParser *parser; 37 | @property (nonatomic, readonly) RSParsedArticle *currentArticle; 38 | @property (nonatomic, readonly) NSDate *currentDate; 39 | 40 | @end 41 | 42 | 43 | @implementation RSAtomParser 44 | 45 | #pragma mark - Class Methods 46 | 47 | + (BOOL)canParseFeed:(RSXMLData *)xmlData { 48 | 49 | // Checking for ' entryRange.location) { 79 | return NO; // Wrong order. 80 | } 81 | } 82 | 83 | return YES; 84 | } 85 | 86 | 87 | #pragma mark - Init 88 | 89 | - (instancetype)initWithXMLData:(RSXMLData *)xmlData { 90 | 91 | self = [super init]; 92 | if (!self) { 93 | return nil; 94 | } 95 | 96 | _feedData = xmlData.data; 97 | _urlString = xmlData.urlString; 98 | _parser = [[RSSAXParser alloc] initWithDelegate:self]; 99 | _attributesStack = [NSMutableArray new]; 100 | _articles = [NSMutableArray new]; 101 | 102 | return self; 103 | } 104 | 105 | 106 | #pragma mark - API 107 | 108 | - (RSParsedFeed *)parseFeed:(NSError **)error { 109 | 110 | [self parse]; 111 | 112 | RSParsedFeed *parsedFeed = [[RSParsedFeed alloc] initWithURLString:self.urlString title:self.title link:self.link articles:self.articles]; 113 | 114 | return parsedFeed; 115 | } 116 | 117 | 118 | #pragma mark - Constants 119 | 120 | static NSString *kTypeKey = @"type"; 121 | static NSString *kXHTMLType = @"xhtml"; 122 | static NSString *kRelKey = @"rel"; 123 | static NSString *kAlternateValue = @"alternate"; 124 | static NSString *kHrefKey = @"href"; 125 | static NSString *kXMLKey = @"xml"; 126 | static NSString *kBaseKey = @"base"; 127 | static NSString *kLangKey = @"lang"; 128 | static NSString *kXMLBaseKey = @"xml:base"; 129 | static NSString *kXMLLangKey = @"xml:lang"; 130 | static NSString *kTextHTMLValue = @"text/html"; 131 | static NSString *kRelatedValue = @"related"; 132 | static NSString *kShortURLValue = @"shorturl"; 133 | static NSString *kHTMLValue = @"html"; 134 | static NSString *kEnValue = @"en"; 135 | static NSString *kTextValue = @"text"; 136 | static NSString *kSelfValue = @"self"; 137 | 138 | static const char *kID = "id"; 139 | static const NSInteger kIDLength = 3; 140 | 141 | static const char *kTitle = "title"; 142 | static const NSInteger kTitleLength = 6; 143 | 144 | static const char *kContent = "content"; 145 | static const NSInteger kContentLength = 8; 146 | 147 | static const char *kSummary = "summary"; 148 | static const NSInteger kSummaryLength = 8; 149 | 150 | static const char *kLink = "link"; 151 | static const NSInteger kLinkLength = 5; 152 | 153 | static const char *kPublished = "published"; 154 | static const NSInteger kPublishedLength = 10; 155 | 156 | static const char *kUpdated = "updated"; 157 | static const NSInteger kUpdatedLength = 8; 158 | 159 | static const char *kAuthor = "author"; 160 | static const NSInteger kAuthorLength = 7; 161 | 162 | static const char *kEntry = "entry"; 163 | static const NSInteger kEntryLength = 6; 164 | 165 | static const char *kSource = "source"; 166 | static const NSInteger kSourceLength = 7; 167 | 168 | static const char *kFeed = "feed"; 169 | static const NSInteger kFeedLength = 5; 170 | 171 | static const char *kType = "type"; 172 | static const NSInteger kTypeLength = 5; 173 | 174 | static const char *kRel = "rel"; 175 | static const NSInteger kRelLength = 4; 176 | 177 | static const char *kAlternate = "alternate"; 178 | static const NSInteger kAlternateLength = 10; 179 | 180 | static const char *kHref = "href"; 181 | static const NSInteger kHrefLength = 5; 182 | 183 | static const char *kXML = "xml"; 184 | static const NSInteger kXMLLength = 4; 185 | 186 | static const char *kBase = "base"; 187 | static const NSInteger kBaseLength = 5; 188 | 189 | static const char *kLang = "lang"; 190 | static const NSInteger kLangLength = 5; 191 | 192 | static const char *kTextHTML = "text/html"; 193 | static const NSInteger kTextHTMLLength = 10; 194 | 195 | static const char *kRelated = "related"; 196 | static const NSInteger kRelatedLength = 8; 197 | 198 | static const char *kShortURL = "shorturl"; 199 | static const NSInteger kShortURLLength = 9; 200 | 201 | static const char *kHTML = "html"; 202 | static const NSInteger kHTMLLength = 5; 203 | 204 | static const char *kEn = "en"; 205 | static const NSInteger kEnLength = 3; 206 | 207 | static const char *kText = "text"; 208 | static const NSInteger kTextLength = 5; 209 | 210 | static const char *kSelf = "self"; 211 | static const NSInteger kSelfLength = 5; 212 | 213 | 214 | #pragma mark - Parsing 215 | 216 | - (void)parse { 217 | 218 | self.dateParsed = [NSDate date]; 219 | 220 | @autoreleasepool { 221 | [self.parser parseData:self.feedData]; 222 | [self.parser finishParsing]; 223 | } 224 | 225 | // Optimization: make articles do calculations on this background thread. 226 | [self.articles makeObjectsPerformSelector:@selector(calculateArticleID)]; 227 | } 228 | 229 | 230 | - (void)addArticle { 231 | 232 | RSParsedArticle *article = [[RSParsedArticle alloc] initWithFeedURL:self.urlString]; 233 | article.dateParsed = self.dateParsed; 234 | 235 | [self.articles addObject:article]; 236 | } 237 | 238 | 239 | - (RSParsedArticle *)currentArticle { 240 | 241 | return self.articles.lastObject; 242 | } 243 | 244 | 245 | - (NSDictionary *)currentAttributes { 246 | 247 | return self.attributesStack.lastObject; 248 | } 249 | 250 | 251 | - (NSDate *)currentDate { 252 | 253 | return RSDateWithBytes(self.parser.currentCharacters.bytes, self.parser.currentCharacters.length); 254 | } 255 | 256 | 257 | - (void)addFeedLink { 258 | 259 | if (self.link && self.link.length > 0) { 260 | return; 261 | } 262 | 263 | NSString *related = self.currentAttributes[kRelKey]; 264 | if (related == kAlternateValue) { 265 | self.link = self.currentAttributes[kHrefKey]; 266 | } 267 | } 268 | 269 | 270 | - (void)addFeedTitle { 271 | 272 | if (self.title.length < 1) { 273 | self.title = self.parser.currentStringWithTrimmedWhitespace; 274 | } 275 | } 276 | 277 | - (void)addLink { 278 | 279 | NSString *urlString = self.currentAttributes[kHrefKey]; 280 | if (urlString.length < 1) { 281 | return; 282 | } 283 | 284 | NSString *rel = self.currentAttributes[kRelKey]; 285 | if (rel.length < 1) { 286 | rel = kAlternateValue; 287 | } 288 | 289 | if (rel == kAlternateValue) { 290 | if (!self.currentArticle.link) { 291 | self.currentArticle.link = urlString; 292 | } 293 | } 294 | else if (rel == kRelatedValue) { 295 | if (!self.currentArticle.permalink) { 296 | self.currentArticle.permalink = urlString; 297 | } 298 | } 299 | } 300 | 301 | 302 | - (void)addContent { 303 | 304 | self.currentArticle.body = [self currentStringWithHTMLEntitiesDecoded]; 305 | } 306 | 307 | 308 | - (void)addSummary { 309 | 310 | if (!self.currentArticle.body) { 311 | self.currentArticle.body = [self currentStringWithHTMLEntitiesDecoded]; 312 | } 313 | } 314 | 315 | 316 | - (NSString *)currentStringWithHTMLEntitiesDecoded { 317 | 318 | return [self.parser.currentStringWithTrimmedWhitespace rs_stringByDecodingHTMLEntities]; 319 | } 320 | 321 | 322 | - (void)addArticleElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix { 323 | 324 | if (prefix) { 325 | return; 326 | } 327 | 328 | if (RSSAXEqualTags(localName, kID, kIDLength)) { 329 | self.currentArticle.guid = self.parser.currentStringWithTrimmedWhitespace; 330 | } 331 | 332 | else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) { 333 | self.currentArticle.title = [self currentStringWithHTMLEntitiesDecoded]; 334 | } 335 | 336 | else if (RSSAXEqualTags(localName, kContent, kContentLength)) { 337 | [self addContent]; 338 | } 339 | 340 | else if (RSSAXEqualTags(localName, kSummary, kSummaryLength)) { 341 | [self addSummary]; 342 | } 343 | 344 | else if (RSSAXEqualTags(localName, kLink, kLinkLength)) { 345 | [self addLink]; 346 | } 347 | 348 | else if (RSSAXEqualTags(localName, kPublished, kPublishedLength)) { 349 | self.currentArticle.datePublished = self.currentDate; 350 | } 351 | 352 | else if (RSSAXEqualTags(localName, kUpdated, kUpdatedLength)) { 353 | self.currentArticle.dateModified = self.currentDate; 354 | } 355 | } 356 | 357 | 358 | - (void)addXHTMLTag:(const xmlChar *)localName { 359 | 360 | if (!localName) { 361 | return; 362 | } 363 | 364 | [self.xhtmlString appendString:@"<"]; 365 | [self.xhtmlString appendString:[NSString stringWithUTF8String:(const char *)localName]]; 366 | 367 | if (self.currentAttributes.count < 1) { 368 | [self.xhtmlString appendString:@">"]; 369 | return; 370 | } 371 | 372 | for (NSString *oneKey in self.currentAttributes) { 373 | 374 | [self.xhtmlString appendString:@" "]; 375 | 376 | NSString *oneValue = self.currentAttributes[oneKey]; 377 | [self.xhtmlString appendString:oneKey]; 378 | 379 | [self.xhtmlString appendString:@"=\""]; 380 | 381 | oneValue = [oneValue stringByReplacingOccurrencesOfString:@"\"" withString:@"""]; 382 | [self.xhtmlString appendString:oneValue]; 383 | 384 | [self.xhtmlString appendString:@"\""]; 385 | } 386 | 387 | [self.xhtmlString appendString:@">"]; 388 | } 389 | 390 | 391 | #pragma mark - RSSAXParserDelegate 392 | 393 | - (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes { 394 | 395 | if (self.endFeedFound) { 396 | return; 397 | } 398 | 399 | NSDictionary *xmlAttributes = [self.parser attributesDictionary:attributes numberOfAttributes:numberOfAttributes]; 400 | if (!xmlAttributes) { 401 | xmlAttributes = [NSDictionary dictionary]; 402 | } 403 | [self.attributesStack addObject:xmlAttributes]; 404 | 405 | if (self.parsingXHTML) { 406 | [self addXHTMLTag:localName]; 407 | return; 408 | } 409 | 410 | if (RSSAXEqualTags(localName, kEntry, kEntryLength)) { 411 | self.parsingArticle = YES; 412 | [self addArticle]; 413 | return; 414 | } 415 | 416 | if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) { 417 | self.parsingAuthor = YES; 418 | return; 419 | } 420 | 421 | if (RSSAXEqualTags(localName, kSource, kSourceLength)) { 422 | self.parsingSource = YES; 423 | return; 424 | } 425 | 426 | BOOL isContentTag = RSSAXEqualTags(localName, kContent, kContentLength); 427 | BOOL isSummaryTag = RSSAXEqualTags(localName, kSummary, kSummaryLength); 428 | if (self.parsingArticle && (isContentTag || isSummaryTag)) { 429 | 430 | NSString *contentType = xmlAttributes[kTypeKey]; 431 | if ([contentType isEqualToString:kXHTMLType]) { 432 | self.parsingXHTML = YES; 433 | self.xhtmlString = [NSMutableString stringWithString:@""]; 434 | return; 435 | } 436 | } 437 | 438 | if (!self.parsingArticle && RSSAXEqualTags(localName, kLink, kLinkLength)) { 439 | [self addFeedLink]; 440 | return; 441 | } 442 | 443 | [self.parser beginStoringCharacters]; 444 | } 445 | 446 | 447 | - (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri { 448 | 449 | if (RSSAXEqualTags(localName, kFeed, kFeedLength)) { 450 | self.endFeedFound = YES; 451 | return; 452 | } 453 | 454 | if (self.endFeedFound) { 455 | return; 456 | } 457 | 458 | if (self.parsingXHTML) { 459 | 460 | BOOL isContentTag = RSSAXEqualTags(localName, kContent, kContentLength); 461 | BOOL isSummaryTag = RSSAXEqualTags(localName, kSummary, kSummaryLength); 462 | 463 | if (self.parsingArticle && (isContentTag || isSummaryTag)) { 464 | 465 | if (isContentTag) { 466 | self.currentArticle.body = [self.xhtmlString copy]; 467 | } 468 | 469 | else if (isSummaryTag) { 470 | if (self.currentArticle.body.length < 1) { 471 | self.currentArticle.body = [self.xhtmlString copy]; 472 | } 473 | } 474 | } 475 | 476 | if (isContentTag || isSummaryTag) { 477 | self.parsingXHTML = NO; 478 | } 479 | 480 | [self.xhtmlString appendString:@""]; 483 | } 484 | 485 | else if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) { 486 | self.parsingAuthor = NO; 487 | } 488 | 489 | else if (RSSAXEqualTags(localName, kEntry, kEntryLength)) { 490 | self.parsingArticle = NO; 491 | } 492 | 493 | else if (self.parsingArticle && !self.parsingSource) { 494 | [self addArticleElement:localName prefix:prefix]; 495 | } 496 | 497 | else if (RSSAXEqualTags(localName, kSource, kSourceLength)) { 498 | self.parsingSource = NO; 499 | } 500 | 501 | else if (!self.parsingArticle && !self.parsingSource && RSSAXEqualTags(localName, kTitle, kTitleLength)) { 502 | [self addFeedTitle]; 503 | } 504 | [self.attributesStack removeLastObject]; 505 | } 506 | 507 | 508 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix { 509 | 510 | if (prefix && RSSAXEqualTags(prefix, kXML, kXMLLength)) { 511 | 512 | if (RSSAXEqualTags(name, kBase, kBaseLength)) { 513 | return kXMLBaseKey; 514 | } 515 | if (RSSAXEqualTags(name, kLang, kLangLength)) { 516 | return kXMLLangKey; 517 | } 518 | } 519 | 520 | if (prefix) { 521 | return nil; 522 | } 523 | 524 | if (RSSAXEqualTags(name, kRel, kRelLength)) { 525 | return kRelKey; 526 | } 527 | 528 | if (RSSAXEqualTags(name, kType, kTypeLength)) { 529 | return kTypeKey; 530 | } 531 | 532 | if (RSSAXEqualTags(name, kHref, kHrefLength)) { 533 | return kHrefKey; 534 | } 535 | 536 | if (RSSAXEqualTags(name, kAlternate, kAlternateLength)) { 537 | return kAlternateValue; 538 | } 539 | 540 | return nil; 541 | } 542 | 543 | 544 | static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) { 545 | 546 | return memcmp(bytes1, bytes2, length) == 0; 547 | } 548 | 549 | 550 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length { 551 | 552 | static const NSUInteger alternateLength = kAlternateLength - 1; 553 | static const NSUInteger textHTMLLength = kTextHTMLLength - 1; 554 | static const NSUInteger relatedLength = kRelatedLength - 1; 555 | static const NSUInteger shortURLLength = kShortURLLength - 1; 556 | static const NSUInteger htmlLength = kHTMLLength - 1; 557 | static const NSUInteger enLength = kEnLength - 1; 558 | static const NSUInteger textLength = kTextLength - 1; 559 | static const NSUInteger selfLength = kSelfLength - 1; 560 | 561 | if (length == alternateLength && equalBytes(bytes, kAlternate, alternateLength)) { 562 | return kAlternateValue; 563 | } 564 | 565 | if (length == textHTMLLength && equalBytes(bytes, kTextHTML, textHTMLLength)) { 566 | return kTextHTMLValue; 567 | } 568 | 569 | if (length == relatedLength && equalBytes(bytes, kRelated, relatedLength)) { 570 | return kRelatedValue; 571 | } 572 | 573 | if (length == shortURLLength && equalBytes(bytes, kShortURL, shortURLLength)) { 574 | return kShortURLValue; 575 | } 576 | 577 | if (length == htmlLength && equalBytes(bytes, kHTML, htmlLength)) { 578 | return kHTMLValue; 579 | } 580 | 581 | if (length == enLength && equalBytes(bytes, kEn, enLength)) { 582 | return kEnValue; 583 | } 584 | 585 | if (length == textLength && equalBytes(bytes, kText, textLength)) { 586 | return kTextValue; 587 | } 588 | 589 | if (length == selfLength && equalBytes(bytes, kSelf, selfLength)) { 590 | return kSelfValue; 591 | } 592 | 593 | return nil; 594 | } 595 | 596 | 597 | - (void)saxParser:(RSSAXParser *)SAXParser XMLCharactersFound:(const unsigned char *)characters length:(NSUInteger)length { 598 | 599 | if (self.parsingXHTML) { 600 | [self.xhtmlString appendString:[[NSString alloc] initWithBytesNoCopy:(void *)characters length:length encoding:NSUTF8StringEncoding freeWhenDone:NO]]; 601 | } 602 | } 603 | 604 | @end 605 | -------------------------------------------------------------------------------- /RSXML/RSDateParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSDateParser.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 3/25/15. 6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | 12 | /*Common web dates -- RFC 822 and 8601 -- are handled here: 13 | the formats you find in JSON and XML feeds. 14 | 15 | Any of these may return nil. They may also return garbage, given bad input.*/ 16 | 17 | 18 | NSDate *RSDateWithString(NSString *dateString); 19 | 20 | /*If you're using a SAX parser, you have the bytes and don't need to convert to a string first. 21 | It's faster and uses less memory. 22 | (Assumes bytes are UTF-8 or ASCII. If you're using the libxml SAX parser, this will work.)*/ 23 | 24 | NSDate *RSDateWithBytes(const char *bytes, NSUInteger numberOfBytes); 25 | 26 | -------------------------------------------------------------------------------- /RSXML/RSDateParser.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSDateParser.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 3/25/15. 6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RSDateParser.h" 11 | 12 | typedef struct { 13 | const char *abbreviation; 14 | const NSInteger offsetHours; 15 | const NSInteger offsetMinutes; 16 | } RSTimeZoneAbbreviationAndOffset; 17 | 18 | 19 | #define kNumberOfTimeZones 96 20 | 21 | static const RSTimeZoneAbbreviationAndOffset timeZoneTable[kNumberOfTimeZones] = { 22 | {"GMT", 0, 0}, //Most common at top, for performance 23 | {"PDT", -7, 0}, {"PST", -8, 0}, {"EST", -5, 0}, {"EDT", -4, 0}, 24 | {"MDT", -6, 0}, {"MST", -7, 0}, {"CST", -6, 0}, {"CDT", -5, 0}, 25 | {"ACT", -8, 0}, {"AFT", 4, 30}, {"AMT", 4, 0}, {"ART", -3, 0}, 26 | {"AST", 3, 0}, {"AZT", 4, 0}, {"BIT", -12, 0}, {"BDT", 8, 0}, 27 | {"ACST", 9, 30}, {"AEST", 10, 0}, {"AKST", -9, 0}, {"AMST", 5, 0}, 28 | {"AWST", 8, 0}, {"AZOST", -1, 0}, {"BIOT", 6, 0}, {"BRT", -3, 0}, 29 | {"BST", 6, 0}, {"BTT", 6, 0}, {"CAT", 2, 0}, {"CCT", 6, 30}, 30 | {"CET", 1, 0}, {"CEST", 2, 0}, {"CHAST", 12, 45}, {"ChST", 10, 0}, 31 | {"CIST", -8, 0}, {"CKT", -10, 0}, {"CLT", -4, 0}, {"CLST", -3, 0}, 32 | {"COT", -5, 0}, {"COST", -4, 0}, {"CVT", -1, 0}, {"CXT", 7, 0}, 33 | {"EAST", -6, 0}, {"EAT", 3, 0}, {"ECT", -4, 0}, {"EEST", 3, 0}, 34 | {"EET", 2, 0}, {"FJT", 12, 0}, {"FKST", -4, 0}, {"GALT", -6, 0}, 35 | {"GET", 4, 0}, {"GFT", -3, 0}, {"GILT", 7, 0}, {"GIT", -9, 0}, 36 | {"GST", -2, 0}, {"GYT", -4, 0}, {"HAST", -10, 0}, {"HKT", 8, 0}, 37 | {"HMT", 5, 0}, {"IRKT", 8, 0}, {"IRST", 3, 30}, {"IST", 2, 0}, 38 | {"JST", 9, 0}, {"KRAT", 7, 0}, {"KST", 9, 0}, {"LHST", 10, 30}, 39 | {"LINT", 14, 0}, {"MAGT", 11, 0}, {"MIT", -9, 30}, {"MSK", 3, 0}, 40 | {"MUT", 4, 0}, {"NDT", -2, 30}, {"NFT", 11, 30}, {"NPT", 5, 45}, 41 | {"NT", -3, 30}, {"OMST", 6, 0}, {"PETT", 12, 0}, {"PHOT", 13, 0}, 42 | {"PKT", 5, 0}, {"RET", 4, 0}, {"SAMT", 4, 0}, {"SAST", 2, 0}, 43 | {"SBT", 11, 0}, {"SCT", 4, 0}, {"SLT", 5, 30}, {"SST", 8, 0}, 44 | {"TAHT", -10, 0}, {"THA", 7, 0}, {"UYT", -3, 0}, {"UYST", -2, 0}, 45 | {"VET", -4, 30}, {"VLAT", 10, 0}, {"WAT", 1, 0}, {"WET", 0, 0}, 46 | {"WEST", 1, 0}, {"YAKT", 9, 0}, {"YEKT", 5, 0} 47 | }; /*See http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations for list*/ 48 | 49 | 50 | 51 | #pragma mark - Parser 52 | 53 | enum { 54 | RSJanuary = 1, 55 | RSFebruary, 56 | RSMarch, 57 | RSApril, 58 | RSMay, 59 | RSJune, 60 | RSJuly, 61 | RSAugust, 62 | RSSeptember, 63 | RSOctober, 64 | RSNovember, 65 | RSDecember 66 | }; 67 | 68 | static NSInteger nextMonthValue(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex, NSUInteger *finalIndex) { 69 | 70 | /*Months are 1-based -- January is 1, Dec is 12. 71 | Lots of short-circuits here. Not strict. GIGO.*/ 72 | 73 | NSUInteger i;// = startingIndex; 74 | NSUInteger numberOfAlphaCharactersFound = 0; 75 | char monthCharacters[3] = {0, 0, 0}; 76 | 77 | for (i = startingIndex; i < numberOfBytes; i++) { 78 | 79 | *finalIndex = i; 80 | char character = bytes[i]; 81 | 82 | BOOL isAlphaCharacter = (BOOL)isalpha(character); 83 | if (!isAlphaCharacter && numberOfAlphaCharactersFound < 1) 84 | continue; 85 | if (!isAlphaCharacter && numberOfAlphaCharactersFound > 0) 86 | break; 87 | 88 | numberOfAlphaCharactersFound++; 89 | if (numberOfAlphaCharactersFound == 1) { 90 | if (character == 'F' || character == 'f') 91 | return RSFebruary; 92 | if (character == 'S' || character == 's') 93 | return RSSeptember; 94 | if (character == 'O' || character == 'o') 95 | return RSOctober; 96 | if (character == 'N' || character == 'n') 97 | return RSNovember; 98 | if (character == 'D' || character == 'd') 99 | return RSDecember; 100 | } 101 | 102 | monthCharacters[numberOfAlphaCharactersFound - 1] = character; 103 | if (numberOfAlphaCharactersFound >=3) 104 | break; 105 | } 106 | 107 | if (numberOfAlphaCharactersFound < 2) 108 | return NSNotFound; 109 | 110 | if (monthCharacters[0] == 'J' || monthCharacters[0] == 'j') { //Jan, Jun, Jul 111 | if (monthCharacters[1] == 'a' || monthCharacters[i] == 'A') 112 | return RSJanuary; 113 | if (monthCharacters[1] == 'u' || monthCharacters[1] == 'U') { 114 | if (monthCharacters[2] == 'n' || monthCharacters[2] == 'N') 115 | return RSJune; 116 | return RSJuly; 117 | } 118 | return RSJanuary; 119 | } 120 | 121 | if (monthCharacters[0] == 'M' || monthCharacters[0] == 'm') { //March, May 122 | if (monthCharacters[2] == 'y' || monthCharacters[2] == 'Y') 123 | return RSMay; 124 | return RSMarch; 125 | } 126 | 127 | if (monthCharacters[0] == 'A' || monthCharacters[0] == 'a') { //April, August 128 | if (monthCharacters[1] == 'u' || monthCharacters[1] == 'U') 129 | return RSAugust; 130 | return RSApril; 131 | } 132 | 133 | return RSJanuary; //should never get here 134 | } 135 | 136 | 137 | static NSInteger nextNumericValue(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex, NSUInteger maximumNumberOfDigits, NSUInteger *finalIndex) { 138 | 139 | /*maximumNumberOfDigits has a maximum limit of 4 (for time zone offsets and years). 140 | *finalIndex will be the index of the last character looked at.*/ 141 | 142 | if (maximumNumberOfDigits > 4) 143 | maximumNumberOfDigits = 4; 144 | 145 | NSUInteger i = 0; 146 | NSUInteger numberOfDigitsFound = 0; 147 | NSInteger digits[4] = {0, 0, 0, 0}; 148 | 149 | for (i = startingIndex; i < numberOfBytes; i++) { 150 | *finalIndex = i; 151 | BOOL isDigit = (BOOL)isdigit(bytes[i]); 152 | if (!isDigit && numberOfDigitsFound < 1) 153 | continue; 154 | if (!isDigit && numberOfDigitsFound > 0) 155 | break; 156 | digits[numberOfDigitsFound] = bytes[i] - 48; // '0' is 48 157 | numberOfDigitsFound++; 158 | if (numberOfDigitsFound >= maximumNumberOfDigits) 159 | break; 160 | } 161 | 162 | if (numberOfDigitsFound < 1) 163 | return NSNotFound; 164 | if (numberOfDigitsFound == 1) 165 | return digits[0]; 166 | if (numberOfDigitsFound == 2) 167 | return (digits[0] * 10) + digits[1]; 168 | if (numberOfDigitsFound == 3) 169 | return (digits[0] * 100) + (digits[1] * 10) + digits[2]; 170 | return (digits[0] * 1000) + (digits[1] * 100) + (digits[2] * 10) + digits[3]; 171 | } 172 | 173 | 174 | static BOOL hasAtLeastOneAlphaCharacter(const char *s) { 175 | 176 | NSUInteger length = strlen(s); 177 | NSUInteger i = 0; 178 | 179 | for (i = 0; i < length; i++) { 180 | if (isalpha(s[i])) 181 | return YES; 182 | } 183 | 184 | return NO; 185 | } 186 | 187 | 188 | #pragma mark - Time Zones and offsets 189 | 190 | static NSInteger offsetInSecondsForTimeZoneAbbreviation(const char *abbreviation) { 191 | 192 | /*Linear search should be fine. It's a C array, and short (under 100 items). 193 | Most common time zones are at the beginning of the array. (We can tweak this as needed.)*/ 194 | 195 | NSUInteger i; 196 | 197 | for (i = 0; i < kNumberOfTimeZones; i++) { 198 | 199 | RSTimeZoneAbbreviationAndOffset zone = timeZoneTable[i]; 200 | if (strcmp(abbreviation, zone.abbreviation) == 0) { 201 | if (zone.offsetHours < 0) 202 | return (zone.offsetHours * 60 * 60) - (zone.offsetMinutes * 60); 203 | return (zone.offsetHours * 60 * 60) + (zone.offsetMinutes * 60); 204 | } 205 | } 206 | 207 | return 0; 208 | } 209 | 210 | 211 | static NSInteger offsetInSecondsForOffsetCharacters(const char *timeZoneCharacters) { 212 | 213 | BOOL isPlus = timeZoneCharacters[0] == '+'; 214 | NSUInteger finalIndex = 0; 215 | NSInteger hours = nextNumericValue(timeZoneCharacters, strlen(timeZoneCharacters), 0, 2, &finalIndex); 216 | NSInteger minutes = nextNumericValue(timeZoneCharacters, strlen(timeZoneCharacters), finalIndex + 1, 2, &finalIndex); 217 | 218 | if (hours == NSNotFound) 219 | hours = 0; 220 | if (minutes == NSNotFound) 221 | minutes = 0; 222 | if (hours == 0 && minutes == 0) 223 | return 0; 224 | 225 | NSInteger seconds = (hours * 60 * 60) + (minutes * 60); 226 | if (!isPlus) 227 | seconds = 0 - seconds; 228 | return seconds; 229 | } 230 | 231 | 232 | static const char *rs_GMT = "GMT"; 233 | static const char *rs_UTC = "UTC"; 234 | 235 | static NSInteger parsedTimeZoneOffset(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex) { 236 | 237 | /*Examples: GMT Z +0000 -0000 +07:00 -0700 PDT EST 238 | Parse into char[5] -- drop any colon characters. If numeric, calculate seconds from GMT. 239 | If alpha, special-case GMT and Z, otherwise look up in time zone list to get offset.*/ 240 | 241 | char timeZoneCharacters[6] = {0, 0, 0, 0, 0, 0}; //nil-terminated last character 242 | NSUInteger i = 0; 243 | NSUInteger numberOfCharactersFound = 0; 244 | 245 | for (i = startingIndex; i < numberOfBytes; i++) { 246 | char ch = bytes[i]; 247 | if (ch == ':' || ch == ' ') 248 | continue; 249 | if (isdigit(ch) || isalpha(ch) || ch == '+' || ch == '-') { 250 | numberOfCharactersFound++; 251 | timeZoneCharacters[numberOfCharactersFound - 1] = ch; 252 | } 253 | if (numberOfCharactersFound >= 5) 254 | break; 255 | } 256 | 257 | if (numberOfCharactersFound < 1 || timeZoneCharacters[0] == 'Z' || timeZoneCharacters[0] == 'z') 258 | return 0; 259 | if (strcasestr(timeZoneCharacters, rs_GMT) != nil || strcasestr(timeZoneCharacters, rs_UTC)) 260 | return 0; 261 | 262 | if (hasAtLeastOneAlphaCharacter(timeZoneCharacters)) 263 | return offsetInSecondsForTimeZoneAbbreviation(timeZoneCharacters); 264 | return offsetInSecondsForOffsetCharacters(timeZoneCharacters); 265 | } 266 | 267 | 268 | #pragma mark - Date Creation 269 | 270 | static NSDate *dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(NSInteger year, NSInteger month, NSInteger day, NSInteger hour, NSInteger minute, NSInteger second, NSInteger milliseconds, NSInteger timeZoneOffset) { 271 | 272 | struct tm timeInfo; 273 | timeInfo.tm_sec = (int)second; 274 | timeInfo.tm_min = (int)minute; 275 | timeInfo.tm_hour = (int)hour; 276 | timeInfo.tm_mday = (int)day; 277 | timeInfo.tm_mon = (int)(month - 1); //It's 1-based coming in 278 | timeInfo.tm_year = (int)(year - 1900); //see time.h -- it's years since 1900 279 | timeInfo.tm_wday = -1; 280 | timeInfo.tm_yday = -1; 281 | timeInfo.tm_isdst = -1; 282 | timeInfo.tm_gmtoff = 0;//[timeZone secondsFromGMT]; 283 | timeInfo.tm_zone = nil; 284 | 285 | NSTimeInterval rawTime = (NSTimeInterval)(timegm(&timeInfo) - timeZoneOffset); //timegm instead of mktime (which uses local time zone) 286 | if (rawTime == (time_t)ULONG_MAX) { 287 | 288 | /*NSCalendar is super-amazingly-slow (which is partly why RSDateParser exists), so this is used only when the date is far enough in the future (19 January 2038 03:14:08Z on 32-bit systems) that timegm fails. If profiling says that this is a performance issue, then you've got a weird app that needs to work with dates far in the future.*/ 289 | 290 | NSDateComponents *dateComponents = [NSDateComponents new]; 291 | 292 | dateComponents.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:timeZoneOffset]; 293 | dateComponents.year = year; 294 | dateComponents.month = month; 295 | dateComponents.day = day; 296 | dateComponents.hour = hour; 297 | dateComponents.minute = minute; 298 | dateComponents.second = second + (milliseconds / 1000); 299 | 300 | return [[NSCalendar autoupdatingCurrentCalendar] dateFromComponents:dateComponents]; 301 | } 302 | 303 | if (milliseconds > 0) { 304 | rawTime += ((float)milliseconds / 1000.0f); 305 | } 306 | 307 | return [NSDate dateWithTimeIntervalSince1970:rawTime]; 308 | } 309 | 310 | 311 | #pragma mark - Standard Formats 312 | 313 | static NSDate *RSParsePubDateWithBytes(const char *bytes, NSUInteger numberOfBytes) { 314 | 315 | /*@"EEE',' dd MMM yyyy HH':'mm':'ss ZZZ" 316 | @"EEE, dd MMM yyyy HH:mm:ss zzz" 317 | @"dd MMM yyyy HH:mm zzz" 318 | @"dd MMM yyyy HH:mm ZZZ" 319 | @"EEE, dd MMM yyyy" 320 | @"EEE, dd MMM yyyy HH:mm zzz" 321 | etc.*/ 322 | 323 | NSUInteger finalIndex = 0; 324 | NSInteger day = 1; 325 | NSInteger month = RSJanuary; 326 | NSInteger year = 1970; 327 | NSInteger hour = 0; 328 | NSInteger minute = 0; 329 | NSInteger second = 0; 330 | NSInteger timeZoneOffset = 0; 331 | 332 | day = nextNumericValue(bytes, numberOfBytes, 0, 2, &finalIndex); 333 | if (day < 1 || day == NSNotFound) 334 | day = 1; 335 | 336 | month = nextMonthValue(bytes, numberOfBytes, finalIndex + 1, &finalIndex); 337 | year = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 4, &finalIndex); 338 | hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex); 339 | if (hour == NSNotFound) 340 | hour = 0; 341 | 342 | minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex); 343 | if (minute == NSNotFound) 344 | minute = 0; 345 | 346 | NSUInteger currentIndex = finalIndex + 1; 347 | 348 | BOOL hasSeconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == ':'); 349 | if (hasSeconds) 350 | second = nextNumericValue(bytes, numberOfBytes, currentIndex, 2, &finalIndex); 351 | 352 | currentIndex = finalIndex + 1; 353 | BOOL hasTimeZone = (currentIndex < numberOfBytes) && (bytes[currentIndex] == ' '); 354 | if (hasTimeZone) 355 | timeZoneOffset = parsedTimeZoneOffset(bytes, numberOfBytes, currentIndex); 356 | 357 | return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(year, month, day, hour, minute, second, 0, timeZoneOffset); 358 | } 359 | 360 | 361 | static NSDate *RSParseW3CWithBytes(const char *bytes, NSUInteger numberOfBytes) { 362 | 363 | /*@"yyyy'-'MM'-'dd'T'HH':'mm':'ss" 364 | @"yyyy-MM-dd'T'HH:mm:sszzz" 365 | @"yyyy-MM-dd'T'HH:mm:ss'.'SSSzzz" 366 | etc.*/ 367 | 368 | NSUInteger finalIndex = 0; 369 | NSInteger day = 1; 370 | NSInteger month = RSJanuary; 371 | NSInteger year = 1970; 372 | NSInteger hour = 0; 373 | NSInteger minute = 0; 374 | NSInteger second = 0; 375 | NSInteger milliseconds = 0; 376 | NSInteger timeZoneOffset = 0; 377 | 378 | year = nextNumericValue(bytes, numberOfBytes, 0, 4, &finalIndex); 379 | month = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex); 380 | day = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex); 381 | hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex); 382 | minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex); 383 | second = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex); 384 | 385 | NSUInteger currentIndex = finalIndex + 1; 386 | BOOL hasMilliseconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == '.'); 387 | if (hasMilliseconds) { 388 | milliseconds = nextNumericValue(bytes, numberOfBytes, currentIndex, 3, &finalIndex); 389 | currentIndex = finalIndex + 1; 390 | } 391 | 392 | timeZoneOffset = parsedTimeZoneOffset(bytes, numberOfBytes, currentIndex); 393 | 394 | return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(year, month, day, hour, minute, second, milliseconds, timeZoneOffset); 395 | } 396 | 397 | 398 | static BOOL dateIsPubDate(const char *bytes, NSUInteger numberOfBytes) { 399 | 400 | NSUInteger i = 0; 401 | 402 | for (i = 0; i < numberOfBytes; i++) { 403 | if (bytes[i] == ' ' || bytes[i] == ',') 404 | return YES; 405 | } 406 | 407 | return NO; 408 | } 409 | 410 | 411 | static BOOL numberOfBytesIsOutsideReasonableRange(NSUInteger numberOfBytes) { 412 | return numberOfBytes < 6 || numberOfBytes > 50; 413 | } 414 | 415 | 416 | #pragma mark - API 417 | 418 | NSDate *RSDateWithBytes(const char *bytes, NSUInteger numberOfBytes) { 419 | 420 | if (numberOfBytesIsOutsideReasonableRange(numberOfBytes)) 421 | return nil; 422 | 423 | if (dateIsPubDate(bytes, numberOfBytes)) 424 | return RSParsePubDateWithBytes(bytes, numberOfBytes); 425 | 426 | return RSParseW3CWithBytes(bytes, numberOfBytes); 427 | } 428 | 429 | 430 | NSDate *RSDateWithString(NSString *dateString) { 431 | 432 | const char *utf8String = [dateString UTF8String]; 433 | return RSDateWithBytes(utf8String, strlen(utf8String)); 434 | } 435 | 436 | -------------------------------------------------------------------------------- /RSXML/RSFeedParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSFeedParser.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 1/4/15. 6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved. 7 | // 8 | 9 | #import "FeedParser.h" 10 | 11 | // If you have a feed and don’t know or care what it is (RSS or Atom), 12 | // then call RSParseFeed or RSParseFeedSync. 13 | 14 | @class RSXMLData; 15 | @class RSParsedFeed; 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | BOOL RSCanParseFeed(RSXMLData *xmlData); 20 | 21 | 22 | typedef void (^RSParsedFeedBlock)(RSParsedFeed * _Nullable parsedFeed, NSError * _Nullable error); 23 | 24 | // callback is called on main queue. 25 | void RSParseFeed(RSXMLData *xmlData, RSParsedFeedBlock callback); 26 | RSParsedFeed * _Nullable RSParseFeedSync(RSXMLData *xmlData, NSError * _Nullable * _Nullable error); 27 | 28 | NS_ASSUME_NONNULL_END 29 | -------------------------------------------------------------------------------- /RSXML/RSFeedParser.m: -------------------------------------------------------------------------------- 1 | // 2 | // FeedParser.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 1/4/15. 6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved. 7 | // 8 | 9 | #import "RSFeedParser.h" 10 | #import "FeedParser.h" 11 | #import "RSXMLData.h" 12 | #import "RSRSSParser.h" 13 | #import "RSAtomParser.h" 14 | 15 | static NSArray *parserClasses(void) { 16 | 17 | static NSArray *gParserClasses = nil; 18 | 19 | static dispatch_once_t onceToken; 20 | dispatch_once(&onceToken, ^{ 21 | 22 | gParserClasses = @[[RSRSSParser class], [RSAtomParser class]]; 23 | }); 24 | 25 | return gParserClasses; 26 | } 27 | 28 | static BOOL feedMayBeParseable(RSXMLData *xmlData) { 29 | 30 | /*Sanity checks.*/ 31 | 32 | if (!xmlData.data) { 33 | return NO; 34 | } 35 | 36 | /*TODO: check size, type, etc.*/ 37 | 38 | return YES; 39 | } 40 | 41 | static BOOL optimisticCanParseRSSData(const char *bytes, NSUInteger numberOfBytes); 42 | static BOOL optimisticCanParseAtomData(const char *bytes, NSUInteger numberOfBytes); 43 | static BOOL optimisticCanParseRDF(const char *bytes, NSUInteger numberOfBytes); 44 | static BOOL dataIsProbablyHTML(const char *bytes, NSUInteger numberOfBytes); 45 | static BOOL dataIsSomeWeirdException(const char *bytes, NSUInteger numberOfBytes); 46 | static BOOL dataHasLeftCaret(const char *bytes, NSUInteger numberOfBytes); 47 | 48 | static const NSUInteger maxNumberOfBytesToSearch = 4096; 49 | static const NSUInteger minNumberOfBytesToSearch = 20; 50 | 51 | static Class parserClassForXMLData(RSXMLData *xmlData) { 52 | 53 | if (!feedMayBeParseable(xmlData)) { 54 | return nil; 55 | } 56 | 57 | // TODO: check for things like images and movies and return nil. 58 | 59 | const char *bytes = xmlData.data.bytes; 60 | NSUInteger numberOfBytes = xmlData.data.length; 61 | 62 | if (numberOfBytes > minNumberOfBytesToSearch) { 63 | 64 | if (numberOfBytes > maxNumberOfBytesToSearch) { 65 | numberOfBytes = maxNumberOfBytesToSearch; 66 | } 67 | 68 | if (!dataHasLeftCaret(bytes, numberOfBytes)) { 69 | return nil; 70 | } 71 | 72 | if (optimisticCanParseRSSData(bytes, numberOfBytes)) { 73 | return [RSRSSParser class]; 74 | } 75 | if (optimisticCanParseAtomData(bytes, numberOfBytes)) { 76 | return [RSAtomParser class]; 77 | } 78 | 79 | if (optimisticCanParseRDF(bytes, numberOfBytes)) { 80 | return nil; //TODO: parse RDF feeds 81 | } 82 | 83 | if (dataIsProbablyHTML(bytes, numberOfBytes)) { 84 | return nil; 85 | } 86 | if (dataIsSomeWeirdException(bytes, numberOfBytes)) { 87 | return nil; 88 | } 89 | } 90 | 91 | for (Class parserClass in parserClasses()) { 92 | if ([parserClass canParseFeed:xmlData]) { 93 | return [[parserClass alloc] initWithXMLData:xmlData]; 94 | } 95 | } 96 | 97 | return nil; 98 | } 99 | 100 | static id parserForXMLData(RSXMLData *xmlData) { 101 | 102 | Class parserClass = parserClassForXMLData(xmlData); 103 | if (!parserClass) { 104 | return nil; 105 | } 106 | return [[parserClass alloc] initWithXMLData:xmlData]; 107 | } 108 | 109 | static BOOL canParseXMLData(RSXMLData *xmlData) { 110 | 111 | return parserClassForXMLData(xmlData) != nil; 112 | } 113 | 114 | static BOOL didFindString(const char *string, const char *bytes, NSUInteger numberOfBytes) { 115 | 116 | char *foundString = strnstr(bytes, string, numberOfBytes); 117 | return foundString != NULL; 118 | } 119 | 120 | static BOOL dataHasLeftCaret(const char *bytes, NSUInteger numberOfBytes) { 121 | 122 | return didFindString("<", bytes, numberOfBytes); 123 | } 124 | 125 | static BOOL dataIsProbablyHTML(const char *bytes, NSUInteger numberOfBytes) { 126 | 127 | // Won’t catch every single case, which is fine. 128 | 129 | if (didFindString(" 0) { 156 | [self.itemStack removeLastObject]; 157 | } 158 | } 159 | 160 | 161 | - (RSOPMLItem *)currentItem { 162 | 163 | return self.itemStack.lastObject; 164 | } 165 | 166 | 167 | #pragma mark - RSSAXParserDelegate 168 | 169 | static const char *kOutline = "outline"; 170 | static const char kOutlineLength = 8; 171 | 172 | - (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes { 173 | 174 | if (!RSSAXEqualTags(localName, kOutline, kOutlineLength)) { 175 | return; 176 | } 177 | 178 | RSOPMLItem *item = [RSOPMLItem new]; 179 | item.attributes = [SAXParser attributesDictionary:attributes numberOfAttributes:numberOfAttributes]; 180 | 181 | [[self currentItem] addChild:item]; 182 | [self pushItem:item]; 183 | } 184 | 185 | 186 | - (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri { 187 | 188 | if (RSSAXEqualTags(localName, kOutline, kOutlineLength)) { 189 | [self popItem]; 190 | } 191 | } 192 | 193 | 194 | static const char *kText = "text"; 195 | static const NSInteger kTextLength = 5; 196 | 197 | static const char *kTitle = "title"; 198 | static const NSInteger kTitleLength = 6; 199 | 200 | static const char *kDescription = "description"; 201 | static const NSInteger kDescriptionLength = 12; 202 | 203 | static const char *kType = "type"; 204 | static const NSInteger kTypeLength = 5; 205 | 206 | static const char *kVersion = "version"; 207 | static const NSInteger kVersionLength = 8; 208 | 209 | static const char *kHTMLURL = "htmlUrl"; 210 | static const NSInteger kHTMLURLLength = 8; 211 | 212 | static const char *kXMLURL = "xmlUrl"; 213 | static const NSInteger kXMLURLLength = 7; 214 | 215 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix { 216 | 217 | if (prefix) { 218 | return nil; 219 | } 220 | 221 | size_t nameLength = strlen((const char *)name); 222 | 223 | if (nameLength == kTextLength - 1) { 224 | if (RSSAXEqualTags(name, kText, kTextLength)) { 225 | return OPMLTextKey; 226 | } 227 | if (RSSAXEqualTags(name, kType, kTypeLength)) { 228 | return OPMLTypeKey; 229 | } 230 | } 231 | 232 | else if (nameLength == kTitleLength - 1) { 233 | if (RSSAXEqualTags(name, kTitle, kTitleLength)) { 234 | return OPMLTitleKey; 235 | } 236 | } 237 | 238 | else if (nameLength == kXMLURLLength - 1) { 239 | if (RSSAXEqualTags(name, kXMLURL, kXMLURLLength)) { 240 | return OPMLXMLURLKey; 241 | } 242 | } 243 | 244 | else if (nameLength == kVersionLength - 1) { 245 | if (RSSAXEqualTags(name, kVersion, kVersionLength)) { 246 | return OPMLVersionKey; 247 | } 248 | if (RSSAXEqualTags(name, kHTMLURL, kHTMLURLLength)) { 249 | return OPMLHMTLURLKey; 250 | } 251 | } 252 | 253 | else if (nameLength == kDescriptionLength - 1) { 254 | if (RSSAXEqualTags(name, kDescription, kDescriptionLength)) { 255 | return OPMLDescriptionKey; 256 | } 257 | } 258 | 259 | return nil; 260 | } 261 | 262 | 263 | static const char *kRSSUppercase = "RSS"; 264 | static const char *kRSSLowercase = "rss"; 265 | static const NSUInteger kRSSLength = 3; 266 | static NSString *RSSUppercaseValue = @"RSS"; 267 | static NSString *RSSLowercaseValue = @"rss"; 268 | static NSString *emptyString = @""; 269 | 270 | static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) { 271 | 272 | return memcmp(bytes1, bytes2, length) == 0; 273 | } 274 | 275 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length { 276 | 277 | 278 | if (length < 1) { 279 | return emptyString; 280 | } 281 | 282 | if (length == kRSSLength) { 283 | 284 | if (equalBytes(bytes, kRSSUppercase, kRSSLength)) { 285 | return RSSUppercaseValue; 286 | } 287 | else if (equalBytes(bytes, kRSSLowercase, kRSSLength)) { 288 | return RSSLowercaseValue; 289 | } 290 | 291 | } 292 | 293 | return nil; 294 | } 295 | 296 | 297 | @end 298 | -------------------------------------------------------------------------------- /RSXML/RSParsedArticle.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSParsedArticle.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 12/6/14. 6 | // Copyright (c) 2014 Ranchero Software LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | 12 | @interface RSParsedArticle : NSObject 13 | 14 | - (nonnull instancetype)initWithFeedURL:(NSString * _Nonnull)feedURL; 15 | 16 | @property (nonatomic, readonly, nonnull) NSString *feedURL; 17 | @property (nonatomic, nonnull) NSString *articleID; //Calculated. Don't get until other properties have been set. 18 | 19 | @property (nonatomic, nullable) NSString *guid; 20 | @property (nonatomic, nullable) NSString *title; 21 | @property (nonatomic, nullable) NSString *body; 22 | @property (nonatomic, nullable) NSString *link; 23 | @property (nonatomic, nullable) NSString *permalink; 24 | @property (nonatomic, nullable) NSString *author; 25 | @property (nonatomic, nullable) NSDate *datePublished; 26 | @property (nonatomic, nullable) NSDate *dateModified; 27 | @property (nonatomic, nonnull) NSDate *dateParsed; 28 | 29 | - (void)calculateArticleID; // Optimization. Call after all properties have been set. Call on a background thread. 30 | 31 | @end 32 | 33 | -------------------------------------------------------------------------------- /RSXML/RSParsedArticle.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSParsedArticle.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 12/6/14. 6 | // Copyright (c) 2014 Ranchero Software LLC. All rights reserved. 7 | // 8 | 9 | #import "RSParsedArticle.h" 10 | #import "RSXMLInternal.h" 11 | 12 | 13 | @implementation RSParsedArticle 14 | 15 | 16 | #pragma mark - Init 17 | 18 | - (instancetype)initWithFeedURL:(NSString *)feedURL { 19 | 20 | NSParameterAssert(feedURL != nil); 21 | 22 | self = [super init]; 23 | if (!self) { 24 | return nil; 25 | } 26 | 27 | _feedURL = feedURL; 28 | _dateParsed = [NSDate date]; 29 | 30 | return self; 31 | } 32 | 33 | 34 | #pragma mark - Accessors 35 | 36 | - (NSString *)articleID { 37 | 38 | if (!_articleID) { 39 | _articleID = self.calculatedUniqueID; 40 | } 41 | 42 | return _articleID; 43 | } 44 | 45 | 46 | - (NSString *)calculatedUniqueID { 47 | 48 | /*guid+feedID, or a combination of properties when no guid. Then hash the result. 49 | In general, feeds should have guids. When they don't, re-runs are very likely, 50 | because there's no other 100% reliable way to determine identity.*/ 51 | 52 | NSMutableString *s = [NSMutableString stringWithString:@""]; 53 | 54 | NSString *datePublishedTimeStampString = nil; 55 | if (self.datePublished) { 56 | datePublishedTimeStampString = [NSString stringWithFormat:@"%.0f", self.datePublished.timeIntervalSince1970]; 57 | } 58 | 59 | if (!RSXMLStringIsEmpty(self.guid)) { 60 | [s appendString:self.guid]; 61 | } 62 | 63 | else if (!RSXMLStringIsEmpty(self.link) && self.datePublished != nil) { 64 | [s appendString:self.link]; 65 | [s appendString:datePublishedTimeStampString]; 66 | } 67 | 68 | else if (!RSXMLStringIsEmpty(self.title) && self.datePublished != nil) { 69 | [s appendString:self.title]; 70 | [s appendString:datePublishedTimeStampString]; 71 | } 72 | 73 | else if (self.datePublished != nil) { 74 | [s appendString:datePublishedTimeStampString]; 75 | } 76 | 77 | else if (!RSXMLStringIsEmpty(self.link)) { 78 | [s appendString:self.link]; 79 | } 80 | 81 | else if (!RSXMLStringIsEmpty(self.title)) { 82 | [s appendString:self.title]; 83 | } 84 | 85 | else if (!RSXMLStringIsEmpty(self.body)) { 86 | [s appendString:self.body]; 87 | } 88 | 89 | NSAssert(!RSXMLStringIsEmpty(self.feedURL), nil); 90 | [s appendString:self.feedURL]; 91 | 92 | return [s rsxml_md5HashString]; 93 | } 94 | 95 | - (void)calculateArticleID { 96 | 97 | (void)self.articleID; 98 | } 99 | 100 | @end 101 | 102 | -------------------------------------------------------------------------------- /RSXML/RSParsedFeed.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSParsedFeed.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 7/12/15. 6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @class RSParsedArticle; 12 | 13 | @interface RSParsedFeed : NSObject 14 | 15 | - (nonnull instancetype)initWithURLString:(NSString * _Nonnull)urlString title:(NSString * _Nullable)title link:(NSString * _Nullable)link articles:(NSArray * _Nonnull)articles; 16 | 17 | @property (nonatomic, readonly, nonnull) NSString *urlString; 18 | @property (nonatomic, readonly, nullable) NSString *title; 19 | @property (nonatomic, readonly, nullable) NSString *link; 20 | @property (nonatomic, readonly, nonnull) NSSet *articles; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /RSXML/RSParsedFeed.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSParsedFeed.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 7/12/15. 6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import "RSParsedFeed.h" 10 | 11 | @implementation RSParsedFeed 12 | 13 | - (instancetype)initWithURLString:(NSString *)urlString title:(NSString *)title link:(NSString *)link articles:(NSSet *)articles { 14 | 15 | self = [super init]; 16 | if (!self) { 17 | return nil; 18 | } 19 | 20 | _urlString = urlString; 21 | _title = title; 22 | _link = link; 23 | _articles = articles; 24 | 25 | return self; 26 | } 27 | 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /RSXML/RSRSSParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSRSSParser.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 1/6/15. 6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved. 7 | // 8 | 9 | #import "FeedParser.h" 10 | 11 | @interface RSRSSParser : NSObject 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /RSXML/RSRSSParser.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSRSSParser.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 1/6/15. 6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RSRSSParser.h" 11 | #import "RSSAXParser.h" 12 | #import "RSParsedFeed.h" 13 | #import "RSParsedArticle.h" 14 | #import "RSXMLData.h" 15 | #import "RSXMLInternal.h" 16 | #import "NSString+RSXML.h" 17 | #import "RSDateParser.h" 18 | 19 | 20 | @interface RSRSSParser () 21 | 22 | @property (nonatomic) NSData *feedData; 23 | @property (nonatomic) NSString *urlString; 24 | @property (nonatomic) NSDictionary *currentAttributes; 25 | @property (nonatomic) RSSAXParser *parser; 26 | @property (nonatomic) NSMutableArray *articles; 27 | @property (nonatomic) BOOL parsingArticle; 28 | @property (nonatomic, readonly) RSParsedArticle *currentArticle; 29 | @property (nonatomic) BOOL parsingChannelImage; 30 | @property (nonatomic, readonly) NSDate *currentDate; 31 | @property (nonatomic) BOOL endRSSFound; 32 | @property (nonatomic) NSString *link; 33 | @property (nonatomic) NSString *title; 34 | @property (nonatomic) NSDate *dateParsed; 35 | 36 | @end 37 | 38 | 39 | @implementation RSRSSParser 40 | 41 | #pragma mark - Class Methods 42 | 43 | + (BOOL)canParseFeed:(RSXMLData *)xmlData { 44 | 45 | // Checking for '' within first n characters should do it. 46 | // TODO: handle RSS 1.0 47 | 48 | @autoreleasepool { 49 | 50 | NSData *feedData = xmlData.data; 51 | NSString *s = [[NSString alloc] initWithBytesNoCopy:(void *)feedData.bytes length:feedData.length encoding:NSUTF8StringEncoding freeWhenDone:NO]; 52 | if (!s) { 53 | s = [[NSString alloc] initWithData:feedData encoding:NSUTF8StringEncoding]; 54 | } 55 | if (!s) { 56 | s = [[NSString alloc] initWithData:feedData encoding:NSUnicodeStringEncoding]; 57 | } 58 | if (!s) { 59 | return NO; 60 | } 61 | 62 | static const NSInteger numberOfCharactersToSearch = 4096; 63 | NSRange rangeToSearch = NSMakeRange(0, numberOfCharactersToSearch); 64 | if (s.length < numberOfCharactersToSearch) { 65 | rangeToSearch.length = s.length; 66 | } 67 | 68 | NSRange rssRange = [s rangeOfString:@"" options:NSLiteralSearch range:rangeToSearch]; 70 | if (rssRange.length < 1 || channelRange.length < 1) { 71 | return NO; 72 | } 73 | 74 | if (rssRange.location > channelRange.location) { 75 | return NO; // Wrong order. 76 | } 77 | } 78 | 79 | return YES; 80 | } 81 | 82 | 83 | #pragma mark - Init 84 | 85 | - (instancetype)initWithXMLData:(RSXMLData *)xmlData { 86 | 87 | self = [super init]; 88 | if (!self) { 89 | return nil; 90 | } 91 | 92 | _feedData = xmlData.data; 93 | _urlString = xmlData.urlString; 94 | _parser = [[RSSAXParser alloc] initWithDelegate:self]; 95 | _articles = [NSMutableArray new]; 96 | 97 | return self; 98 | } 99 | 100 | 101 | #pragma mark - API 102 | 103 | - (RSParsedFeed *)parseFeed:(NSError **)error { 104 | 105 | [self parse]; 106 | 107 | RSParsedFeed *parsedFeed = [[RSParsedFeed alloc] initWithURLString:self.urlString title:self.title link:self.link articles:self.articles]; 108 | 109 | return parsedFeed; 110 | } 111 | 112 | 113 | #pragma mark - Constants 114 | 115 | static NSString *kIsPermaLinkKey = @"isPermaLink"; 116 | static NSString *kURLKey = @"url"; 117 | static NSString *kLengthKey = @"length"; 118 | static NSString *kTypeKey = @"type"; 119 | static NSString *kFalseValue = @"false"; 120 | static NSString *kTrueValue = @"true"; 121 | static NSString *kContentEncodedKey = @"content:encoded"; 122 | static NSString *kDCDateKey = @"dc:date"; 123 | static NSString *kDCCreatorKey = @"dc:creator"; 124 | static NSString *kRDFAboutKey = @"rdf:about"; 125 | 126 | static const char *kItem = "item"; 127 | static const NSInteger kItemLength = 5; 128 | 129 | static const char *kImage = "image"; 130 | static const NSInteger kImageLength = 6; 131 | 132 | static const char *kLink = "link"; 133 | static const NSInteger kLinkLength = 5; 134 | 135 | static const char *kTitle = "title"; 136 | static const NSInteger kTitleLength = 6; 137 | 138 | static const char *kDC = "dc"; 139 | static const NSInteger kDCLength = 3; 140 | 141 | static const char *kCreator = "creator"; 142 | static const NSInteger kCreatorLength = 8; 143 | 144 | static const char *kDate = "date"; 145 | static const NSInteger kDateLength = 5; 146 | 147 | static const char *kContent = "content"; 148 | static const NSInteger kContentLength = 8; 149 | 150 | static const char *kEncoded = "encoded"; 151 | static const NSInteger kEncodedLength = 8; 152 | 153 | static const char *kGuid = "guid"; 154 | static const NSInteger kGuidLength = 5; 155 | 156 | static const char *kPubDate = "pubDate"; 157 | static const NSInteger kPubDateLength = 8; 158 | 159 | static const char *kAuthor = "author"; 160 | static const NSInteger kAuthorLength = 7; 161 | 162 | static const char *kDescription = "description"; 163 | static const NSInteger kDescriptionLength = 12; 164 | 165 | static const char *kRSS = "rss"; 166 | static const NSInteger kRSSLength = 4; 167 | 168 | static const char *kURL = "url"; 169 | static const NSInteger kURLLength = 4; 170 | 171 | static const char *kLength = "length"; 172 | static const NSInteger kLengthLength = 7; 173 | 174 | static const char *kType = "type"; 175 | static const NSInteger kTypeLength = 5; 176 | 177 | static const char *kIsPermaLink = "isPermaLink"; 178 | static const NSInteger kIsPermaLinkLength = 12; 179 | 180 | static const char *kRDF = "rdf"; 181 | static const NSInteger kRDFlength = 4; 182 | 183 | static const char *kAbout = "about"; 184 | static const NSInteger kAboutLength = 6; 185 | 186 | static const char *kFalse = "false"; 187 | static const NSInteger kFalseLength = 6; 188 | 189 | static const char *kTrue = "true"; 190 | static const NSInteger kTrueLength = 5; 191 | 192 | 193 | #pragma mark - Parsing 194 | 195 | - (void)parse { 196 | 197 | self.dateParsed = [NSDate date]; 198 | 199 | @autoreleasepool { 200 | [self.parser parseData:self.feedData]; 201 | [self.parser finishParsing]; 202 | } 203 | 204 | // Optimization: make articles do calculations on this background thread. 205 | [self.articles makeObjectsPerformSelector:@selector(calculateArticleID)]; 206 | } 207 | 208 | 209 | - (void)addArticle { 210 | 211 | RSParsedArticle *article = [[RSParsedArticle alloc] initWithFeedURL:self.urlString]; 212 | article.dateParsed = self.dateParsed; 213 | 214 | [self.articles addObject:article]; 215 | } 216 | 217 | 218 | - (RSParsedArticle *)currentArticle { 219 | 220 | return self.articles.lastObject; 221 | } 222 | 223 | 224 | - (void)addFeedElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix { 225 | 226 | if (prefix != NULL) { 227 | return; 228 | } 229 | 230 | if (RSSAXEqualTags(localName, kLink, kLinkLength)) { 231 | if (!self.link) { 232 | self.link = self.parser.currentStringWithTrimmedWhitespace; 233 | } 234 | } 235 | 236 | else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) { 237 | self.title = self.parser.currentStringWithTrimmedWhitespace; 238 | } 239 | } 240 | 241 | 242 | - (void)addDCElement:(const xmlChar *)localName { 243 | 244 | if (RSSAXEqualTags(localName, kCreator, kCreatorLength)) { 245 | 246 | self.currentArticle.author = self.parser.currentStringWithTrimmedWhitespace; 247 | } 248 | else if (RSSAXEqualTags(localName, kDate, kDateLength)) { 249 | 250 | self.currentArticle.datePublished = self.currentDate; 251 | } 252 | } 253 | 254 | 255 | - (void)addGuid { 256 | 257 | self.currentArticle.guid = self.parser.currentStringWithTrimmedWhitespace; 258 | 259 | NSString *isPermaLinkValue = [self.currentAttributes rsxml_objectForCaseInsensitiveKey:@"ispermalink"]; 260 | if (!isPermaLinkValue || ![isPermaLinkValue isEqualToString:@"false"]) { 261 | self.currentArticle.permalink = [self urlString:self.currentArticle.guid]; 262 | } 263 | } 264 | 265 | 266 | - (NSString *)urlString:(NSString *)s { 267 | 268 | /*Resolve against home page URL (if available) or feed URL.*/ 269 | 270 | if ([[s lowercaseString] hasPrefix:@"http"]) { 271 | return s; 272 | } 273 | 274 | if (!self.link) { 275 | //TODO: get feed URL and use that to resolve URL.*/ 276 | return s; 277 | } 278 | 279 | NSURL *baseURL = [NSURL URLWithString:self.link]; 280 | if (!baseURL) { 281 | return s; 282 | } 283 | 284 | NSURL *resolvedURL = [NSURL URLWithString:s relativeToURL:baseURL]; 285 | if (resolvedURL.absoluteString) { 286 | return resolvedURL.absoluteString; 287 | } 288 | 289 | return s; 290 | } 291 | 292 | 293 | - (NSString *)currentStringWithHTMLEntitiesDecoded { 294 | 295 | return [self.parser.currentStringWithTrimmedWhitespace rs_stringByDecodingHTMLEntities]; 296 | } 297 | 298 | - (void)addArticleElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix { 299 | 300 | if (RSSAXEqualTags(prefix, kDC, kDCLength)) { 301 | 302 | [self addDCElement:localName]; 303 | return; 304 | } 305 | 306 | if (RSSAXEqualTags(prefix, kContent, kContentLength) && RSSAXEqualTags(localName, kEncoded, kEncodedLength)) { 307 | 308 | self.currentArticle.body = [self currentStringWithHTMLEntitiesDecoded]; 309 | return; 310 | } 311 | 312 | if (prefix != NULL) { 313 | return; 314 | } 315 | 316 | if (RSSAXEqualTags(localName, kGuid, kGuidLength)) { 317 | [self addGuid]; 318 | } 319 | else if (RSSAXEqualTags(localName, kPubDate, kPubDateLength)) { 320 | self.currentArticle.datePublished = self.currentDate; 321 | } 322 | else if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) { 323 | self.currentArticle.author = self.parser.currentStringWithTrimmedWhitespace; 324 | } 325 | else if (RSSAXEqualTags(localName, kLink, kLinkLength)) { 326 | self.currentArticle.link = [self urlString:self.parser.currentStringWithTrimmedWhitespace]; 327 | } 328 | else if (RSSAXEqualTags(localName, kDescription, kDescriptionLength)) { 329 | 330 | if (!self.currentArticle.body) { 331 | self.currentArticle.body = [self currentStringWithHTMLEntitiesDecoded]; 332 | } 333 | } 334 | else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) { 335 | self.currentArticle.title = [self currentStringWithHTMLEntitiesDecoded]; 336 | } 337 | } 338 | 339 | 340 | - (NSDate *)currentDate { 341 | 342 | return RSDateWithBytes(self.parser.currentCharacters.bytes, self.parser.currentCharacters.length); 343 | } 344 | 345 | 346 | #pragma mark - RSSAXParserDelegate 347 | 348 | - (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes { 349 | 350 | if (self.endRSSFound) { 351 | return; 352 | } 353 | 354 | NSDictionary *xmlAttributes = nil; 355 | if (RSSAXEqualTags(localName, kItem, kItemLength) || RSSAXEqualTags(localName, kGuid, kGuidLength)) { 356 | xmlAttributes = [self.parser attributesDictionary:attributes numberOfAttributes:numberOfAttributes]; 357 | } 358 | if (self.currentAttributes != xmlAttributes) { 359 | self.currentAttributes = xmlAttributes; 360 | } 361 | 362 | if (!prefix && RSSAXEqualTags(localName, kItem, kItemLength)) { 363 | 364 | [self addArticle]; 365 | self.parsingArticle = YES; 366 | 367 | if (xmlAttributes && xmlAttributes[kRDFAboutKey]) { /*RSS 1.0 guid*/ 368 | self.currentArticle.guid = xmlAttributes[kRDFAboutKey]; 369 | self.currentArticle.permalink = self.currentArticle.guid; 370 | } 371 | } 372 | 373 | else if (!prefix && RSSAXEqualTags(localName, kImage, kImageLength)) { 374 | self.parsingChannelImage = YES; 375 | } 376 | 377 | if (!self.parsingChannelImage) { 378 | [self.parser beginStoringCharacters]; 379 | } 380 | } 381 | 382 | 383 | - (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri { 384 | 385 | if (self.endRSSFound) { 386 | return; 387 | } 388 | 389 | if (RSSAXEqualTags(localName, kRSS, kRSSLength)) { 390 | self.endRSSFound = YES; 391 | } 392 | 393 | else if (RSSAXEqualTags(localName, kImage, kImageLength)) { 394 | self.parsingChannelImage = NO; 395 | } 396 | 397 | else if (RSSAXEqualTags(localName, kItem, kItemLength)) { 398 | self.parsingArticle = NO; 399 | } 400 | 401 | else if (self.parsingArticle) { 402 | [self addArticleElement:localName prefix:prefix]; 403 | } 404 | 405 | else if (!self.parsingChannelImage) { 406 | [self addFeedElement:localName prefix:prefix]; 407 | } 408 | } 409 | 410 | 411 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix { 412 | 413 | if (RSSAXEqualTags(prefix, kRDF, kRDFlength)) { 414 | 415 | if (RSSAXEqualTags(name, kAbout, kAboutLength)) { 416 | return kRDFAboutKey; 417 | } 418 | 419 | return nil; 420 | } 421 | 422 | if (prefix) { 423 | return nil; 424 | } 425 | 426 | if (RSSAXEqualTags(name, kIsPermaLink, kIsPermaLinkLength)) { 427 | return kIsPermaLinkKey; 428 | } 429 | 430 | if (RSSAXEqualTags(name, kURL, kURLLength)) { 431 | return kURLKey; 432 | } 433 | 434 | if (RSSAXEqualTags(name, kLength, kLengthLength)) { 435 | return kLengthKey; 436 | } 437 | 438 | if (RSSAXEqualTags(name, kType, kTypeLength)) { 439 | return kTypeKey; 440 | } 441 | 442 | return nil; 443 | } 444 | 445 | 446 | static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) { 447 | 448 | return memcmp(bytes1, bytes2, length) == 0; 449 | } 450 | 451 | 452 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length { 453 | 454 | static const NSUInteger falseLength = kFalseLength - 1; 455 | static const NSUInteger trueLength = kTrueLength - 1; 456 | 457 | if (length == falseLength && equalBytes(bytes, kFalse, falseLength)) { 458 | return kFalseValue; 459 | } 460 | 461 | if (length == trueLength && equalBytes(bytes, kTrue, trueLength)) { 462 | return kTrueValue; 463 | } 464 | 465 | return nil; 466 | } 467 | 468 | 469 | @end 470 | -------------------------------------------------------------------------------- /RSXML/RSSAXHTMLParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSSAXHTMLParser.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 3/6/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @class RSSAXHTMLParser; 12 | 13 | @protocol RSSAXHTMLParserDelegate 14 | 15 | @optional 16 | 17 | - (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLStartElement:(const unsigned char *)localName attributes:(const unsigned char **)attributes; 18 | 19 | - (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLEndElement:(const unsigned char *)localName; 20 | 21 | - (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLCharactersFound:(const unsigned char *)characters length:(NSUInteger)length; 22 | 23 | - (void)saxParserDidReachEndOfDocument:(RSSAXHTMLParser *)SAXParser; // If canceled, may not get called (but might). 24 | 25 | @end 26 | 27 | 28 | @interface RSSAXHTMLParser : NSObject 29 | 30 | 31 | - (instancetype)initWithDelegate:(id)delegate; 32 | 33 | - (void)parseData:(NSData *)data; 34 | - (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes; 35 | - (void)finishParsing; 36 | - (void)cancel; 37 | 38 | @property (nonatomic, strong, readonly) NSData *currentCharacters; // nil if not storing characters. UTF-8 encoded. 39 | @property (nonatomic, strong, readonly) NSString *currentString; // Convenience to get string version of currentCharacters. 40 | @property (nonatomic, strong, readonly) NSString *currentStringWithTrimmedWhitespace; 41 | 42 | - (void)beginStoringCharacters; // Delegate can call from XMLStartElement. Characters will be available in XMLEndElement as currentCharacters property. Storing characters is stopped after each XMLEndElement. 43 | 44 | // Delegate can call from within XMLStartElement. 45 | 46 | - (NSDictionary *)attributesDictionary:(const unsigned char **)attributes; 47 | 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /RSXML/RSSAXHTMLParser.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSSAXHTMLParser.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 3/6/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import "RSSAXHTMLParser.h" 10 | #import "RSSAXParser.h" 11 | #import 12 | #import 13 | #import 14 | #import "RSXMLInternal.h" 15 | 16 | 17 | @interface RSSAXHTMLParser () 18 | 19 | @property (nonatomic) id delegate; 20 | @property (nonatomic, assign) htmlParserCtxtPtr context; 21 | @property (nonatomic, assign) BOOL storingCharacters; 22 | @property (nonatomic) NSMutableData *characters; 23 | @property (nonatomic) BOOL delegateRespondsToStartElementMethod; 24 | @property (nonatomic) BOOL delegateRespondsToEndElementMethod; 25 | @property (nonatomic) BOOL delegateRespondsToCharactersFoundMethod; 26 | @property (nonatomic) BOOL delegateRespondsToEndOfDocumentMethod; 27 | 28 | @end 29 | 30 | 31 | @implementation RSSAXHTMLParser 32 | 33 | 34 | + (void)initialize { 35 | 36 | RSSAXInitLibXMLParser(); 37 | } 38 | 39 | 40 | #pragma mark - Init 41 | 42 | - (instancetype)initWithDelegate:(id)delegate { 43 | 44 | self = [super init]; 45 | if (self == nil) 46 | return nil; 47 | 48 | _delegate = delegate; 49 | 50 | if ([_delegate respondsToSelector:@selector(saxParser:XMLStartElement:attributes:)]) { 51 | _delegateRespondsToStartElementMethod = YES; 52 | } 53 | if ([_delegate respondsToSelector:@selector(saxParser:XMLEndElement:)]) { 54 | _delegateRespondsToEndElementMethod = YES; 55 | } 56 | if ([_delegate respondsToSelector:@selector(saxParser:XMLCharactersFound:length:)]) { 57 | _delegateRespondsToCharactersFoundMethod = YES; 58 | } 59 | if ([_delegate respondsToSelector:@selector(saxParserDidReachEndOfDocument:)]) { 60 | _delegateRespondsToEndOfDocumentMethod = YES; 61 | } 62 | 63 | return self; 64 | } 65 | 66 | 67 | #pragma mark - Dealloc 68 | 69 | - (void)dealloc { 70 | 71 | if (_context != nil) { 72 | htmlFreeParserCtxt(_context); 73 | _context = nil; 74 | } 75 | _delegate = nil; 76 | } 77 | 78 | 79 | #pragma mark - API 80 | 81 | static xmlSAXHandler saxHandlerStruct; 82 | 83 | - (void)parseData:(NSData *)data { 84 | 85 | [self parseBytes:data.bytes numberOfBytes:data.length]; 86 | } 87 | 88 | 89 | - (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes { 90 | 91 | if (self.context == nil) { 92 | 93 | xmlCharEncoding characterEncoding = xmlDetectCharEncoding(bytes, (int)numberOfBytes); 94 | self.context = htmlCreatePushParserCtxt(&saxHandlerStruct, (__bridge void *)self, nil, 0, nil, characterEncoding); 95 | htmlCtxtUseOptions(self.context, XML_PARSE_RECOVER | XML_PARSE_NONET | HTML_PARSE_COMPACT); 96 | } 97 | 98 | @autoreleasepool { 99 | htmlParseChunk(self.context, (const char *)bytes, (int)numberOfBytes, 0); 100 | } 101 | } 102 | 103 | 104 | - (void)finishParsing { 105 | 106 | NSAssert(self.context != nil, nil); 107 | if (self.context == nil) 108 | return; 109 | 110 | @autoreleasepool { 111 | htmlParseChunk(self.context, nil, 0, 1); 112 | htmlFreeParserCtxt(self.context); 113 | self.context = nil; 114 | self.characters = nil; 115 | } 116 | } 117 | 118 | 119 | - (void)cancel { 120 | 121 | @autoreleasepool { 122 | xmlStopParser(self.context); 123 | } 124 | } 125 | 126 | 127 | 128 | - (void)beginStoringCharacters { 129 | self.storingCharacters = YES; 130 | self.characters = [NSMutableData new]; 131 | } 132 | 133 | 134 | - (void)endStoringCharacters { 135 | self.storingCharacters = NO; 136 | self.characters = nil; 137 | } 138 | 139 | 140 | - (NSData *)currentCharacters { 141 | 142 | if (!self.storingCharacters) { 143 | return nil; 144 | } 145 | 146 | return self.characters; 147 | } 148 | 149 | 150 | - (NSString *)currentString { 151 | 152 | NSData *d = self.currentCharacters; 153 | if (RSXMLIsEmpty(d)) { 154 | return nil; 155 | } 156 | 157 | return [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding]; 158 | } 159 | 160 | 161 | - (NSString *)currentStringWithTrimmedWhitespace { 162 | 163 | return [self.currentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 164 | } 165 | 166 | 167 | #pragma mark - Attributes Dictionary 168 | 169 | - (NSDictionary *)attributesDictionary:(const xmlChar **)attributes { 170 | 171 | if (!attributes) { 172 | return nil; 173 | } 174 | 175 | NSMutableDictionary *d = [NSMutableDictionary new]; 176 | 177 | NSInteger ix = 0; 178 | NSString *currentKey = nil; 179 | while (true) { 180 | 181 | const xmlChar *oneAttribute = attributes[ix]; 182 | ix++; 183 | 184 | if (!currentKey && !oneAttribute) { 185 | break; 186 | } 187 | 188 | if (!currentKey) { 189 | currentKey = [NSString stringWithUTF8String:(const char *)oneAttribute]; 190 | } 191 | else { 192 | NSString *value = nil; 193 | if (oneAttribute) { 194 | value = [NSString stringWithUTF8String:(const char *)oneAttribute]; 195 | } 196 | 197 | d[currentKey] = value ? value : @""; 198 | currentKey = nil; 199 | } 200 | } 201 | 202 | return [d copy]; 203 | } 204 | 205 | 206 | #pragma mark - Callbacks 207 | 208 | - (void)xmlEndDocument { 209 | 210 | @autoreleasepool { 211 | if (self.delegateRespondsToEndOfDocumentMethod) { 212 | [self.delegate saxParserDidReachEndOfDocument:self]; 213 | } 214 | 215 | [self endStoringCharacters]; 216 | } 217 | } 218 | 219 | 220 | - (void)xmlCharactersFound:(const xmlChar *)ch length:(NSUInteger)length { 221 | 222 | @autoreleasepool { 223 | if (self.storingCharacters) { 224 | [self.characters appendBytes:(const void *)ch length:length]; 225 | } 226 | 227 | if (self.delegateRespondsToCharactersFoundMethod) { 228 | [self.delegate saxParser:self XMLCharactersFound:ch length:length]; 229 | } 230 | } 231 | } 232 | 233 | 234 | - (void)xmlStartElement:(const xmlChar *)localName attributes:(const xmlChar **)attributes { 235 | 236 | @autoreleasepool { 237 | if (self.delegateRespondsToStartElementMethod) { 238 | 239 | [self.delegate saxParser:self XMLStartElement:localName attributes:attributes]; 240 | } 241 | } 242 | } 243 | 244 | 245 | - (void)xmlEndElement:(const xmlChar *)localName { 246 | 247 | @autoreleasepool { 248 | if (self.delegateRespondsToEndElementMethod) { 249 | [self.delegate saxParser:self XMLEndElement:localName]; 250 | } 251 | 252 | [self endStoringCharacters]; 253 | } 254 | } 255 | 256 | 257 | @end 258 | 259 | 260 | static void startElementSAX(void *context, const xmlChar *localname, const xmlChar **attributes) { 261 | 262 | [(__bridge RSSAXHTMLParser *)context xmlStartElement:localname attributes:attributes]; 263 | } 264 | 265 | 266 | static void endElementSAX(void *context, const xmlChar *localname) { 267 | [(__bridge RSSAXHTMLParser *)context xmlEndElement:localname]; 268 | } 269 | 270 | 271 | static void charactersFoundSAX(void *context, const xmlChar *ch, int len) { 272 | [(__bridge RSSAXHTMLParser *)context xmlCharactersFound:ch length:(NSUInteger)len]; 273 | } 274 | 275 | 276 | static void endDocumentSAX(void *context) { 277 | [(__bridge RSSAXHTMLParser *)context xmlEndDocument]; 278 | } 279 | 280 | 281 | static htmlSAXHandler saxHandlerStruct = { 282 | nil, /* internalSubset */ 283 | nil, /* isStandalone */ 284 | nil, /* hasInternalSubset */ 285 | nil, /* hasExternalSubset */ 286 | nil, /* resolveEntity */ 287 | nil, /* getEntity */ 288 | nil, /* entityDecl */ 289 | nil, /* notationDecl */ 290 | nil, /* attributeDecl */ 291 | nil, /* elementDecl */ 292 | nil, /* unparsedEntityDecl */ 293 | nil, /* setDocumentLocator */ 294 | nil, /* startDocument */ 295 | endDocumentSAX, /* endDocument */ 296 | startElementSAX, /* startElement*/ 297 | endElementSAX, /* endElement */ 298 | nil, /* reference */ 299 | charactersFoundSAX, /* characters */ 300 | nil, /* ignorableWhitespace */ 301 | nil, /* processingInstruction */ 302 | nil, /* comment */ 303 | nil, /* warning */ 304 | nil, /* error */ 305 | nil, /* fatalError //: unused error() get all the errors */ 306 | nil, /* getParameterEntity */ 307 | nil, /* cdataBlock */ 308 | nil, /* externalSubset */ 309 | XML_SAX2_MAGIC, 310 | nil, 311 | nil, /* startElementNs */ 312 | nil, /* endElementNs */ 313 | nil /* serror */ 314 | }; 315 | 316 | -------------------------------------------------------------------------------- /RSXML/RSSAXParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSSAXParser.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 3/25/15. 6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | /*Thread-safe, not re-entrant. 12 | 13 | Calls to the delegate will happen on the same thread where the parser runs. 14 | 15 | This is a low-level streaming XML parser, a thin wrapper for libxml2's SAX parser. It doesn't do much Foundation-ifying quite on purpose -- because the goal is performance and low memory use. 16 | 17 | This class is not meant to be sub-classed. Use the delegate methods. 18 | */ 19 | 20 | 21 | @class RSSAXParser; 22 | 23 | @protocol RSSAXParserDelegate 24 | 25 | @optional 26 | 27 | - (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const unsigned char *)localName prefix:(const unsigned char *)prefix uri:(const unsigned char *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const unsigned char **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const unsigned char **)attributes; 28 | 29 | - (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const unsigned char *)localName prefix:(const unsigned char *)prefix uri:(const unsigned char *)uri; 30 | 31 | - (void)saxParser:(RSSAXParser *)SAXParser XMLCharactersFound:(const unsigned char *)characters length:(NSUInteger)length; 32 | 33 | - (void)saxParserDidReachEndOfDocument:(RSSAXParser *)SAXParser; /*If canceled, may not get called (but might).*/ 34 | 35 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const unsigned char *)name prefix:(const unsigned char *)prefix; /*Okay to return nil. Prefix may be nil.*/ 36 | 37 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length; 38 | 39 | @end 40 | 41 | 42 | void RSSAXInitLibXMLParser(void); // Needed by RSSAXHTMLParser. 43 | 44 | /*For use by delegate.*/ 45 | 46 | BOOL RSSAXEqualTags(const unsigned char *localName, const char *tag, NSInteger tagLength); 47 | 48 | 49 | @interface RSSAXParser : NSObject 50 | 51 | - (instancetype)initWithDelegate:(id)delegate; 52 | 53 | - (void)parseData:(NSData *)data; 54 | - (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes; 55 | - (void)finishParsing; 56 | - (void)cancel; 57 | 58 | @property (nonatomic, strong, readonly) NSData *currentCharacters; /*nil if not storing characters. UTF-8 encoded.*/ 59 | @property (nonatomic, strong, readonly) NSString *currentString; /*Convenience to get string version of currentCharacters.*/ 60 | @property (nonatomic, strong, readonly) NSString *currentStringWithTrimmedWhitespace; 61 | 62 | - (void)beginStoringCharacters; /*Delegate can call from XMLStartElement. Characters will be available in XMLEndElement as currentCharacters property. Storing characters is stopped after each XMLEndElement.*/ 63 | 64 | /*Delegate can call from within XMLStartElement. Returns nil if numberOfAttributes < 1.*/ 65 | 66 | - (NSDictionary *)attributesDictionary:(const unsigned char **)attributes numberOfAttributes:(NSInteger)numberOfAttributes; 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /RSXML/RSSAXParser.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSSAXParser.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 3/25/15. 6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "RSSAXParser.h" 12 | #import "RSXMLInternal.h" 13 | 14 | 15 | @interface RSSAXParser () 16 | 17 | @property (nonatomic, weak) id delegate; 18 | @property (nonatomic, assign) xmlParserCtxtPtr context; 19 | @property (nonatomic, assign) BOOL storingCharacters; 20 | @property (nonatomic) NSMutableData *characters; 21 | @property (nonatomic) BOOL delegateRespondsToInternedStringMethod; 22 | @property (nonatomic) BOOL delegateRespondsToInternedStringForValueMethod; 23 | @property (nonatomic) BOOL delegateRespondsToStartElementMethod; 24 | @property (nonatomic) BOOL delegateRespondsToEndElementMethod; 25 | @property (nonatomic) BOOL delegateRespondsToCharactersFoundMethod; 26 | @property (nonatomic) BOOL delegateRespondsToEndOfDocumentMethod; 27 | 28 | @end 29 | 30 | 31 | @implementation RSSAXParser 32 | 33 | + (void)initialize { 34 | 35 | RSSAXInitLibXMLParser(); 36 | } 37 | 38 | 39 | #pragma mark - Init 40 | 41 | - (instancetype)initWithDelegate:(id)delegate { 42 | 43 | self = [super init]; 44 | if (self == nil) 45 | return nil; 46 | 47 | _delegate = delegate; 48 | 49 | if ([_delegate respondsToSelector:@selector(saxParser:internedStringForName:prefix:)]) { 50 | _delegateRespondsToInternedStringMethod = YES; 51 | } 52 | if ([_delegate respondsToSelector:@selector(saxParser:internedStringForValue:length:)]) { 53 | _delegateRespondsToInternedStringForValueMethod = YES; 54 | } 55 | if ([_delegate respondsToSelector:@selector(saxParser:XMLStartElement:prefix:uri:numberOfNamespaces:namespaces:numberOfAttributes:numberDefaulted:attributes:)]) { 56 | _delegateRespondsToStartElementMethod = YES; 57 | } 58 | if ([_delegate respondsToSelector:@selector(saxParser:XMLEndElement:prefix:uri:)]) { 59 | _delegateRespondsToEndElementMethod = YES; 60 | } 61 | if ([_delegate respondsToSelector:@selector(saxParser:XMLCharactersFound:length:)]) { 62 | _delegateRespondsToCharactersFoundMethod = YES; 63 | } 64 | if ([_delegate respondsToSelector:@selector(saxParserDidReachEndOfDocument:)]) { 65 | _delegateRespondsToEndOfDocumentMethod = YES; 66 | } 67 | 68 | return self; 69 | } 70 | 71 | 72 | #pragma mark - Dealloc 73 | 74 | - (void)dealloc { 75 | if (_context != nil) { 76 | xmlFreeParserCtxt(_context); 77 | _context = nil; 78 | } 79 | _delegate = nil; 80 | } 81 | 82 | 83 | #pragma mark - API 84 | 85 | static xmlSAXHandler saxHandlerStruct; 86 | 87 | - (void)parseData:(NSData *)data { 88 | 89 | [self parseBytes:data.bytes numberOfBytes:data.length]; 90 | } 91 | 92 | 93 | - (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes { 94 | 95 | if (self.context == nil) { 96 | 97 | self.context = xmlCreatePushParserCtxt(&saxHandlerStruct, (__bridge void *)self, nil, 0, nil); 98 | xmlCtxtUseOptions(self.context, XML_PARSE_RECOVER | XML_PARSE_NOENT); 99 | } 100 | 101 | @autoreleasepool { 102 | xmlParseChunk(self.context, (const char *)bytes, (int)numberOfBytes, 0); 103 | } 104 | } 105 | 106 | 107 | - (void)finishParsing { 108 | 109 | NSAssert(self.context != nil, nil); 110 | if (self.context == nil) 111 | return; 112 | 113 | @autoreleasepool { 114 | xmlParseChunk(self.context, nil, 0, 1); 115 | xmlFreeParserCtxt(self.context); 116 | self.context = nil; 117 | self.characters = nil; 118 | } 119 | } 120 | 121 | 122 | - (void)cancel { 123 | 124 | @autoreleasepool { 125 | xmlStopParser(self.context); 126 | } 127 | } 128 | 129 | 130 | - (void)beginStoringCharacters { 131 | self.storingCharacters = YES; 132 | self.characters = [NSMutableData new]; 133 | } 134 | 135 | 136 | - (void)endStoringCharacters { 137 | self.storingCharacters = NO; 138 | self.characters = nil; 139 | } 140 | 141 | 142 | - (NSData *)currentCharacters { 143 | 144 | if (!self.storingCharacters) { 145 | return nil; 146 | } 147 | 148 | return self.characters; 149 | } 150 | 151 | 152 | - (NSString *)currentString { 153 | 154 | NSData *d = self.currentCharacters; 155 | if (RSXMLIsEmpty(d)) { 156 | return nil; 157 | } 158 | 159 | return [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding]; 160 | } 161 | 162 | 163 | - (NSString *)currentStringWithTrimmedWhitespace { 164 | 165 | return [self.currentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 166 | } 167 | 168 | 169 | #pragma mark - Attributes Dictionary 170 | 171 | - (NSDictionary *)attributesDictionary:(const xmlChar **)attributes numberOfAttributes:(NSInteger)numberOfAttributes { 172 | 173 | if (numberOfAttributes < 1 || !attributes) { 174 | return nil; 175 | } 176 | 177 | NSMutableDictionary *d = [NSMutableDictionary new]; 178 | 179 | @autoreleasepool { 180 | NSInteger i = 0, j = 0; 181 | for (i = 0, j = 0; i < numberOfAttributes; i++, j+=5) { 182 | 183 | NSUInteger lenValue = (NSUInteger)(attributes[j + 4] - attributes[j + 3]); 184 | NSString *value = nil; 185 | 186 | if (self.delegateRespondsToInternedStringForValueMethod) { 187 | value = [self.delegate saxParser:self internedStringForValue:(const void *)attributes[j + 3] length:lenValue]; 188 | } 189 | if (!value) { 190 | value = [[NSString alloc] initWithBytes:(const void *)attributes[j + 3] length:lenValue encoding:NSUTF8StringEncoding]; 191 | } 192 | 193 | NSString *attributeName = nil; 194 | 195 | if (self.delegateRespondsToInternedStringMethod) { 196 | attributeName = [self.delegate saxParser:self internedStringForName:(const xmlChar *)attributes[j] prefix:(const xmlChar *)attributes[j + 1]]; 197 | } 198 | 199 | if (!attributeName) { 200 | attributeName = [NSString stringWithUTF8String:(const char *)attributes[j]]; 201 | if (attributes[j + 1]) { 202 | NSString *attributePrefix = [NSString stringWithUTF8String:(const char *)attributes[j + 1]]; 203 | attributeName = [NSString stringWithFormat:@"%@:%@", attributePrefix, attributeName]; 204 | } 205 | } 206 | 207 | if (value && attributeName) { 208 | d[attributeName] = value; 209 | } 210 | } 211 | } 212 | 213 | return d; 214 | } 215 | 216 | 217 | #pragma mark - Equal Tags 218 | 219 | BOOL RSSAXEqualTags(const xmlChar *localName, const char *tag, NSInteger tagLength) { 220 | 221 | if (!localName) { 222 | return NO; 223 | } 224 | return !strncmp((const char *)localName, tag, (size_t)tagLength); 225 | } 226 | 227 | 228 | #pragma mark - Callbacks 229 | 230 | - (void)xmlEndDocument { 231 | 232 | @autoreleasepool { 233 | if (self.delegateRespondsToEndOfDocumentMethod) { 234 | [self.delegate saxParserDidReachEndOfDocument:self]; 235 | } 236 | 237 | [self endStoringCharacters]; 238 | } 239 | } 240 | 241 | 242 | - (void)xmlCharactersFound:(const xmlChar *)ch length:(NSUInteger)length { 243 | 244 | @autoreleasepool { 245 | if (self.storingCharacters) { 246 | [self.characters appendBytes:(const void *)ch length:length]; 247 | } 248 | 249 | if (self.delegateRespondsToCharactersFoundMethod) { 250 | [self.delegate saxParser:self XMLCharactersFound:ch length:length]; 251 | } 252 | } 253 | } 254 | 255 | 256 | - (void)xmlStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(int)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(int)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes { 257 | 258 | @autoreleasepool { 259 | if (self.delegateRespondsToStartElementMethod) { 260 | 261 | [self.delegate saxParser:self XMLStartElement:localName prefix:prefix uri:uri numberOfNamespaces:numberOfNamespaces namespaces:namespaces numberOfAttributes:numberOfAttributes numberDefaulted:numberDefaulted attributes:attributes]; 262 | } 263 | } 264 | } 265 | 266 | 267 | - (void)xmlEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri { 268 | 269 | @autoreleasepool { 270 | if (self.delegateRespondsToEndElementMethod) { 271 | [self.delegate saxParser:self XMLEndElement:localName prefix:prefix uri:uri]; 272 | } 273 | 274 | [self endStoringCharacters]; 275 | } 276 | } 277 | 278 | 279 | @end 280 | 281 | 282 | static void startElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes) { 283 | 284 | [(__bridge RSSAXParser *)context xmlStartElement:localname prefix:prefix uri:URI numberOfNamespaces:nb_namespaces namespaces:namespaces numberOfAttributes:nb_attributes numberDefaulted:nb_defaulted attributes:attributes]; 285 | } 286 | 287 | 288 | static void endElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI) { 289 | [(__bridge RSSAXParser *)context xmlEndElement:localname prefix:prefix uri:URI]; 290 | } 291 | 292 | 293 | static void charactersFoundSAX(void *context, const xmlChar *ch, int len) { 294 | [(__bridge RSSAXParser *)context xmlCharactersFound:ch length:(NSUInteger)len]; 295 | } 296 | 297 | 298 | static void endDocumentSAX(void *context) { 299 | [(__bridge RSSAXParser *)context xmlEndDocument]; 300 | } 301 | 302 | 303 | static xmlSAXHandler saxHandlerStruct = { 304 | nil, /* internalSubset */ 305 | nil, /* isStandalone */ 306 | nil, /* hasInternalSubset */ 307 | nil, /* hasExternalSubset */ 308 | nil, /* resolveEntity */ 309 | nil, /* getEntity */ 310 | nil, /* entityDecl */ 311 | nil, /* notationDecl */ 312 | nil, /* attributeDecl */ 313 | nil, /* elementDecl */ 314 | nil, /* unparsedEntityDecl */ 315 | nil, /* setDocumentLocator */ 316 | nil, /* startDocument */ 317 | endDocumentSAX, /* endDocument */ 318 | nil, /* startElement*/ 319 | nil, /* endElement */ 320 | nil, /* reference */ 321 | charactersFoundSAX, /* characters */ 322 | nil, /* ignorableWhitespace */ 323 | nil, /* processingInstruction */ 324 | nil, /* comment */ 325 | nil, /* warning */ 326 | nil, /* error */ 327 | nil, /* fatalError //: unused error() get all the errors */ 328 | nil, /* getParameterEntity */ 329 | nil, /* cdataBlock */ 330 | nil, /* externalSubset */ 331 | XML_SAX2_MAGIC, 332 | nil, 333 | startElementSAX, /* startElementNs */ 334 | endElementSAX, /* endElementNs */ 335 | nil /* serror */ 336 | }; 337 | 338 | 339 | void RSSAXInitLibXMLParser(void) { 340 | 341 | static dispatch_once_t onceToken; 342 | dispatch_once(&onceToken, ^{ 343 | xmlInitParser(); 344 | }); 345 | } 346 | 347 | -------------------------------------------------------------------------------- /RSXML/RSXML.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSXML.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 7/12/15. 6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | 12 | #import 13 | #import 14 | 15 | #import 16 | #import 17 | #import 18 | #import 19 | #import 20 | #import 21 | 22 | #import 23 | #import 24 | #import 25 | #import 26 | #import 27 | 28 | #import 29 | 30 | #import 31 | #import 32 | 33 | // HTML 34 | 35 | #import 36 | 37 | #import 38 | #import 39 | #import 40 | -------------------------------------------------------------------------------- /RSXML/RSXMLData.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSXMLData.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 8/24/15. 6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface RSXMLData : NSObject 14 | 15 | - (instancetype)initWithData:(NSData *)data urlString:(NSString *)urlString; 16 | 17 | @property (nonatomic, readonly) NSData *data; 18 | @property (nonatomic, readonly) NSString *urlString; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /RSXML/RSXMLData.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSXMLData.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 8/24/15. 6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import "RSXMLData.h" 10 | 11 | @implementation RSXMLData 12 | 13 | 14 | - (instancetype)initWithData:(NSData *)data urlString:(NSString *)urlString { 15 | 16 | self = [super init]; 17 | if (!self) { 18 | return nil; 19 | } 20 | 21 | _data = data; 22 | _urlString = urlString; 23 | 24 | return self; 25 | } 26 | 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /RSXML/RSXMLError.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSXMLError.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 2/28/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | extern NSString *RSXMLErrorDomain; 12 | 13 | 14 | typedef NS_ENUM(NSInteger, RSXMLErrorCode) { 15 | RSXMLErrorCodeDataIsWrongFormat = 1024 16 | }; 17 | 18 | 19 | NSError *RSOPMLWrongFormatError(NSString *fileName); 20 | -------------------------------------------------------------------------------- /RSXML/RSXMLError.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSXMLError.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 2/28/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import "RSXMLError.h" 10 | 11 | NSString *RSXMLErrorDomain = @"com.ranchero.RSXML"; 12 | 13 | NSError *RSOPMLWrongFormatError(NSString *fileName) { 14 | 15 | NSString *localizedDescriptionFormatString = NSLocalizedString(@"The file ‘%@’ can’t be parsed because it’s not an OPML file.", @"OPML wrong format"); 16 | NSString *localizedDescription = [NSString stringWithFormat:localizedDescriptionFormatString, fileName]; 17 | 18 | NSString *localizedFailureString = NSLocalizedString(@"The file is not an OPML file.", @"OPML wrong format"); 19 | NSDictionary *userInfo = @{NSLocalizedDescriptionKey: localizedDescription, NSLocalizedFailureReasonErrorKey: localizedFailureString}; 20 | 21 | return [[NSError alloc] initWithDomain:RSXMLErrorDomain code:RSXMLErrorCodeDataIsWrongFormat userInfo:userInfo]; 22 | } 23 | -------------------------------------------------------------------------------- /RSXML/RSXMLInternal.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSXMLInternal.h 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 12/26/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | BOOL RSXMLIsEmpty(id _Nullable obj); 14 | BOOL RSXMLStringIsEmpty(NSString * _Nullable s); 15 | 16 | 17 | @interface NSString (RSXMLInternal) 18 | 19 | - (NSString *)rsxml_md5HashString; 20 | 21 | @end 22 | 23 | 24 | @interface NSDictionary (RSXMLInternal) 25 | 26 | - (nullable id)rsxml_objectForCaseInsensitiveKey:(NSString *)key; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | 32 | -------------------------------------------------------------------------------- /RSXML/RSXMLInternal.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSXMLInternal.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 12/26/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RSXMLInternal.h" 11 | 12 | 13 | static BOOL RSXMLIsNil(id obj) { 14 | 15 | return obj == nil || obj == [NSNull null]; 16 | } 17 | 18 | BOOL RSXMLIsEmpty(id obj) { 19 | 20 | if (RSXMLIsNil(obj)) { 21 | return YES; 22 | } 23 | 24 | if ([obj respondsToSelector:@selector(count)]) { 25 | return [obj count] < 1; 26 | } 27 | 28 | if ([obj respondsToSelector:@selector(length)]) { 29 | return [obj length] < 1; 30 | } 31 | 32 | return NO; /*Shouldn't get here very often.*/ 33 | } 34 | 35 | BOOL RSXMLStringIsEmpty(NSString *s) { 36 | 37 | return RSXMLIsNil(s) || s.length < 1; 38 | } 39 | 40 | 41 | @implementation NSString (RSXMLInternal) 42 | 43 | - (NSData *)rsxml_md5Hash { 44 | 45 | NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding]; 46 | unsigned char hash[CC_MD5_DIGEST_LENGTH]; 47 | CC_MD5(data.bytes, (CC_LONG)data.length, hash); 48 | 49 | return [NSData dataWithBytes:(const void *)hash length:CC_MD5_DIGEST_LENGTH]; 50 | } 51 | 52 | - (NSString *)rsxml_md5HashString { 53 | 54 | NSData *md5Data = [self rsxml_md5Hash]; 55 | const Byte *bytes = md5Data.bytes; 56 | return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]]; 57 | } 58 | 59 | @end 60 | 61 | 62 | @implementation NSDictionary (RSXMLInternal) 63 | 64 | 65 | - (nullable id)rsxml_objectForCaseInsensitiveKey:(NSString *)key { 66 | 67 | id obj = self[key]; 68 | if (obj) { 69 | return obj; 70 | } 71 | 72 | for (NSString *oneKey in self.allKeys) { 73 | 74 | if ([oneKey isKindOfClass:[NSString class]] && [key caseInsensitiveCompare:oneKey] == NSOrderedSame) { 75 | return self[oneKey]; 76 | } 77 | } 78 | 79 | return nil; 80 | } 81 | 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /RSXMLTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /RSXMLTests/RSDateParserTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSDateParserTests.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 12/26/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | @import RSXML; 11 | 12 | @interface RSDateParserTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation RSDateParserTests 17 | 18 | static NSDate *dateWithValues(NSInteger year, NSInteger month, NSInteger day, NSInteger hour, NSInteger minute, NSInteger second) { 19 | 20 | NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; 21 | dateComponents.calendar = NSCalendar.currentCalendar; 22 | dateComponents.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; 23 | [dateComponents setValue:year forComponent:NSCalendarUnitYear]; 24 | [dateComponents setValue:month forComponent:NSCalendarUnitMonth]; 25 | [dateComponents setValue:day forComponent:NSCalendarUnitDay]; 26 | [dateComponents setValue:hour forComponent:NSCalendarUnitHour]; 27 | [dateComponents setValue:minute forComponent:NSCalendarUnitMinute]; 28 | [dateComponents setValue:second forComponent:NSCalendarUnitSecond]; 29 | 30 | return dateComponents.date; 31 | } 32 | 33 | - (void)testDateWithString { 34 | 35 | NSDate *expectedDateResult = dateWithValues(2010, 5, 28, 21, 3, 38); 36 | XCTAssertNotNil(expectedDateResult); 37 | 38 | NSDate *d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 +0000"); 39 | XCTAssertEqualObjects(d, expectedDateResult); 40 | 41 | d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 +00:00"); 42 | XCTAssertEqualObjects(d, expectedDateResult); 43 | 44 | d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 -00:00"); 45 | XCTAssertEqualObjects(d, expectedDateResult); 46 | 47 | d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 -0000"); 48 | XCTAssertEqualObjects(d, expectedDateResult); 49 | 50 | d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 GMT"); 51 | XCTAssertEqualObjects(d, expectedDateResult); 52 | 53 | d = RSDateWithString(@"2010-05-28T21:03:38+00:00"); 54 | XCTAssertEqualObjects(d, expectedDateResult); 55 | 56 | d = RSDateWithString(@"2010-05-28T21:03:38+0000"); 57 | XCTAssertEqualObjects(d, expectedDateResult); 58 | 59 | d = RSDateWithString(@"2010-05-28T21:03:38-0000"); 60 | XCTAssertEqualObjects(d, expectedDateResult); 61 | 62 | d = RSDateWithString(@"2010-05-28T21:03:38-00:00"); 63 | XCTAssertEqualObjects(d, expectedDateResult); 64 | 65 | d = RSDateWithString(@"2010-05-28T21:03:38Z"); 66 | XCTAssertEqualObjects(d, expectedDateResult); 67 | 68 | expectedDateResult = dateWithValues(2010, 7, 13, 17, 6, 40); 69 | d = RSDateWithString(@"2010-07-13T17:06:40+00:00"); 70 | XCTAssertEqualObjects(d, expectedDateResult); 71 | 72 | expectedDateResult = dateWithValues(2010, 4, 30, 12, 0, 0); 73 | d = RSDateWithString(@"30 Apr 2010 5:00 PDT"); 74 | XCTAssertEqualObjects(d, expectedDateResult); 75 | 76 | expectedDateResult = dateWithValues(2010, 5, 21, 21, 22, 53); 77 | d = RSDateWithString(@"21 May 2010 21:22:53 GMT"); 78 | XCTAssertEqualObjects(d, expectedDateResult); 79 | 80 | expectedDateResult = dateWithValues(2010, 6, 9, 5, 0, 0); 81 | d = RSDateWithString(@"Wed, 09 Jun 2010 00:00 EST"); 82 | XCTAssertEqualObjects(d, expectedDateResult); 83 | 84 | expectedDateResult = dateWithValues(2010, 6, 23, 3, 43, 50); 85 | d = RSDateWithString(@"Wed, 23 Jun 2010 03:43:50 Z"); 86 | XCTAssertEqualObjects(d, expectedDateResult); 87 | 88 | expectedDateResult = dateWithValues(2010, 6, 22, 3, 57, 49); 89 | d = RSDateWithString(@"2010-06-22T03:57:49+00:00"); 90 | XCTAssertEqualObjects(d, expectedDateResult); 91 | 92 | expectedDateResult = dateWithValues(2010, 11, 17, 13, 40, 07); 93 | d = RSDateWithString(@"2010-11-17T08:40:07-05:00"); 94 | XCTAssertEqualObjects(d, expectedDateResult); 95 | } 96 | 97 | 98 | @end 99 | -------------------------------------------------------------------------------- /RSXMLTests/RSEntityTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSEntityTests.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 12/26/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | @import RSXML; 11 | 12 | @interface RSEntityTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation RSEntityTests 17 | 18 | 19 | - (void)testInnerAmpersand { 20 | 21 | NSString *expectedResult = @"A&P"; 22 | 23 | NSString *result = [@"A&P" rs_stringByDecodingHTMLEntities]; 24 | XCTAssertEqualObjects(result, expectedResult); 25 | 26 | result = [@"A&P" rs_stringByDecodingHTMLEntities]; 27 | XCTAssertEqualObjects(result, expectedResult); 28 | 29 | result = [@"A&P" rs_stringByDecodingHTMLEntities]; 30 | XCTAssertEqualObjects(result, expectedResult); 31 | 32 | } 33 | 34 | - (void)testSingleEntity { 35 | 36 | NSString *result = [@"∞" rs_stringByDecodingHTMLEntities]; 37 | XCTAssertEqualObjects(result, @"∞"); 38 | 39 | result = [@"&" rs_stringByDecodingHTMLEntities]; 40 | XCTAssertEqualObjects(result, @"&"); 41 | 42 | result = [@"’" rs_stringByDecodingHTMLEntities]; 43 | XCTAssertEqualObjects(result, @"’"); 44 | } 45 | 46 | - (void)testNotEntities { 47 | 48 | NSString *s = @"&&\t\nFoo & Bar &0; Baz & 1238 4948 More things &foobar;&"; 49 | NSString *result = [s rs_stringByDecodingHTMLEntities]; 50 | XCTAssertEqualObjects(result, s); 51 | } 52 | 53 | - (void)testURLs { 54 | 55 | NSString *urlString = @"http://www.nytimes.com/2015/09/05/us/at-west-point-annual-pillow-fight-becomes-weaponized.html?mwrsm=Email&_r=1&pagewanted=all"; 56 | NSString *expectedResult = @"http://www.nytimes.com/2015/09/05/us/at-west-point-annual-pillow-fight-becomes-weaponized.html?mwrsm=Email&_r=1&pagewanted=all"; 57 | 58 | NSString *result = [urlString rs_stringByDecodingHTMLEntities]; 59 | XCTAssertEqualObjects(result, expectedResult); 60 | } 61 | 62 | - (void)testEntityPlusWhitespace { 63 | 64 | NSString *s = @"∞ Permalink"; 65 | NSString *expectedResult = @"∞ Permalink"; 66 | 67 | NSString *result = [s rs_stringByDecodingHTMLEntities]; 68 | XCTAssertEqualObjects(result, expectedResult); 69 | } 70 | 71 | - (void)testNonBreakingSpace { 72 | 73 | NSString *s = @"   -- just some spaces"; 74 | NSString *expectedResult = [NSString stringWithFormat:@"%C%C -- just some spaces", 160, 160]; 75 | 76 | NSString *result = [s rs_stringByDecodingHTMLEntities]; 77 | XCTAssertEqualObjects(result, expectedResult); 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /RSXMLTests/RSHTMLTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSHTMLTests.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 3/5/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import RSXML; 10 | #import 11 | 12 | @interface RSHTMLTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation RSHTMLTests 17 | 18 | 19 | + (RSXMLData *)xmlData:(NSString *)title urlString:(NSString *)urlString { 20 | 21 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:title ofType:@"html"]; 22 | NSData *d = [[NSData alloc] initWithContentsOfFile:s]; 23 | return [[RSXMLData alloc] initWithData:d urlString:urlString]; 24 | } 25 | 26 | 27 | + (RSXMLData *)daringFireballData { 28 | 29 | static RSXMLData *xmlData = nil; 30 | 31 | static dispatch_once_t onceToken; 32 | dispatch_once(&onceToken, ^{ 33 | xmlData = [self xmlData:@"DaringFireball" urlString:@"http://daringfireball.net/"]; 34 | }); 35 | 36 | return xmlData; 37 | } 38 | 39 | 40 | + (RSXMLData *)furboData { 41 | 42 | static RSXMLData *xmlData = nil; 43 | 44 | static dispatch_once_t onceToken; 45 | dispatch_once(&onceToken, ^{ 46 | xmlData = [self xmlData:@"furbo" urlString:@"http://furbo.org/"]; 47 | }); 48 | 49 | return xmlData; 50 | } 51 | 52 | 53 | + (RSXMLData *)inessentialData { 54 | 55 | static RSXMLData *xmlData = nil; 56 | 57 | static dispatch_once_t onceToken; 58 | dispatch_once(&onceToken, ^{ 59 | xmlData = [self xmlData:@"inessential" urlString:@"http://inessential.com/"]; 60 | }); 61 | 62 | return xmlData; 63 | } 64 | 65 | 66 | + (RSXMLData *)sixcolorsData { 67 | 68 | static RSXMLData *xmlData = nil; 69 | 70 | static dispatch_once_t onceToken; 71 | dispatch_once(&onceToken, ^{ 72 | xmlData = [self xmlData:@"sixcolors" urlString:@"https://sixcolors.com/"]; 73 | }); 74 | 75 | return xmlData; 76 | } 77 | 78 | 79 | - (void)testDaringFireball { 80 | 81 | RSXMLData *xmlData = [[self class] daringFireballData]; 82 | RSHTMLMetadata *metadata = [RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData]; 83 | 84 | XCTAssertEqualObjects(metadata.faviconLink, @"http://daringfireball.net/graphics/favicon.ico?v=005"); 85 | 86 | XCTAssertTrue(metadata.feedLinks.count == 1); 87 | RSHTMLMetadataFeedLink *feedLink = metadata.feedLinks[0]; 88 | XCTAssertNil(feedLink.title); 89 | XCTAssertEqualObjects(feedLink.type, @"application/atom+xml"); 90 | XCTAssertEqualObjects(feedLink.urlString, @"http://daringfireball.net/feeds/main"); 91 | } 92 | 93 | 94 | - (void)testDaringFireballPerformance { 95 | 96 | RSXMLData *xmlData = [[self class] daringFireballData]; 97 | 98 | [self measureBlock:^{ 99 | (void)[RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData]; 100 | }]; 101 | } 102 | 103 | - (void)testFurbo { 104 | 105 | RSXMLData *xmlData = [[self class] furboData]; 106 | RSHTMLMetadata *metadata = [RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData]; 107 | 108 | XCTAssertEqualObjects(metadata.faviconLink, @"http://furbo.org/favicon.ico"); 109 | 110 | XCTAssertTrue(metadata.feedLinks.count == 1); 111 | RSHTMLMetadataFeedLink *feedLink = metadata.feedLinks[0]; 112 | XCTAssertEqualObjects(feedLink.title, @"Iconfactory News Feed"); 113 | XCTAssertEqualObjects(feedLink.type, @"application/rss+xml"); 114 | } 115 | 116 | 117 | - (void)testFurboPerformance { 118 | 119 | RSXMLData *xmlData = [[self class] furboData]; 120 | 121 | [self measureBlock:^{ 122 | (void)[RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData]; 123 | }]; 124 | } 125 | 126 | 127 | - (void)testInessential { 128 | 129 | RSXMLData *xmlData = [[self class] inessentialData]; 130 | RSHTMLMetadata *metadata = [RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData]; 131 | 132 | XCTAssertNil(metadata.faviconLink); 133 | 134 | XCTAssertTrue(metadata.feedLinks.count == 1); 135 | RSHTMLMetadataFeedLink *feedLink = metadata.feedLinks[0]; 136 | XCTAssertEqualObjects(feedLink.title, @"RSS"); 137 | XCTAssertEqualObjects(feedLink.type, @"application/rss+xml"); 138 | XCTAssertEqualObjects(feedLink.urlString, @"http://inessential.com/xml/rss.xml"); 139 | 140 | XCTAssertEqual(metadata.appleTouchIcons.count, 0); 141 | } 142 | 143 | 144 | - (void)testInessentialPerformance { 145 | 146 | RSXMLData *xmlData = [[self class] inessentialData]; 147 | 148 | [self measureBlock:^{ 149 | (void)[RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData]; 150 | }]; 151 | } 152 | 153 | 154 | - (void)testSixcolors { 155 | 156 | RSXMLData *xmlData = [[self class] sixcolorsData]; 157 | RSHTMLMetadata *metadata = [RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData]; 158 | 159 | XCTAssertEqualObjects(metadata.faviconLink, @"https://sixcolors.com/images/favicon.ico"); 160 | 161 | XCTAssertTrue(metadata.feedLinks.count == 1); 162 | RSHTMLMetadataFeedLink *feedLink = metadata.feedLinks[0]; 163 | XCTAssertEqualObjects(feedLink.title, @"RSS"); 164 | XCTAssertEqualObjects(feedLink.type, @"application/rss+xml"); 165 | XCTAssertEqualObjects(feedLink.urlString, @"http://feedpress.me/sixcolors"); 166 | 167 | XCTAssertEqual(metadata.appleTouchIcons.count, 6); 168 | RSHTMLMetadataAppleTouchIcon *icon = metadata.appleTouchIcons[3]; 169 | XCTAssertEqualObjects(icon.rel, @"apple-touch-icon"); 170 | XCTAssertEqualObjects(icon.sizes, @"120x120"); 171 | XCTAssertEqualObjects(icon.urlString, @"https://sixcolors.com/apple-touch-icon-120.png"); 172 | } 173 | 174 | 175 | - (void)testSixcolorsPerformance { 176 | 177 | RSXMLData *xmlData = [[self class] sixcolorsData]; 178 | 179 | [self measureBlock:^{ 180 | (void)[RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData]; 181 | }]; 182 | } 183 | 184 | #pragma mark - Links 185 | 186 | - (void)testSixColorsLinks { 187 | 188 | RSXMLData *xmlData = [[self class] sixcolorsData]; 189 | NSArray *links = [RSHTMLLinkParser htmlLinksWithData:xmlData]; 190 | 191 | NSString *linkToFind = @"https://www.theincomparable.com/theincomparable/290/index.php"; 192 | NSString *textToFind = @"this week’s episode of The Incomparable"; 193 | 194 | BOOL found = NO; 195 | for (RSHTMLLink *oneLink in links) { 196 | 197 | if ([oneLink.urlString isEqualToString:linkToFind] && [oneLink.text isEqualToString:textToFind]) { 198 | found = YES; 199 | break; 200 | } 201 | } 202 | 203 | XCTAssertTrue(found, @"Expected link should have been found."); 204 | XCTAssertEqual(links.count, 131, @"Expected 131 links."); 205 | } 206 | 207 | 208 | - (void)testSixColorsLinksPerformance { 209 | 210 | RSXMLData *xmlData = [[self class] sixcolorsData]; 211 | 212 | [self measureBlock:^{ 213 | (void)[RSHTMLLinkParser htmlLinksWithData:xmlData]; 214 | }]; 215 | } 216 | 217 | @end 218 | 219 | -------------------------------------------------------------------------------- /RSXMLTests/RSOPMLTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSOPMLTests.m 3 | // RSXML 4 | // 5 | // Created by Brent Simmons on 2/28/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | @import RSXML; 11 | 12 | @interface RSOPMLTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation RSOPMLTests 17 | 18 | + (RSXMLData *)subsData { 19 | 20 | static RSXMLData *xmlData = nil; 21 | 22 | static dispatch_once_t onceToken; 23 | dispatch_once(&onceToken, ^{ 24 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"Subs" ofType:@"opml"]; 25 | NSData *d = [[NSData alloc] initWithContentsOfFile:s]; 26 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://example.org/"]; 27 | }); 28 | 29 | return xmlData; 30 | } 31 | 32 | - (void)testNotOPML { 33 | 34 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"DaringFireball" ofType:@"rss"]; 35 | NSData *d = [[NSData alloc] initWithContentsOfFile:s]; 36 | RSXMLData *xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://example.org/"]; 37 | RSOPMLParser *parser = [[RSOPMLParser alloc] initWithXMLData:xmlData]; 38 | XCTAssertNotNil(parser.error); 39 | 40 | d = [[NSData alloc] initWithContentsOfFile:@"/System/Library/Kernels/kernel"]; 41 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"/System/Library/Kernels/kernel"]; 42 | parser = [[RSOPMLParser alloc] initWithXMLData:xmlData]; 43 | XCTAssertNotNil(parser.error); 44 | } 45 | 46 | 47 | - (void)testSubsPerformance { 48 | 49 | RSXMLData *xmlData = [[self class] subsData]; 50 | 51 | [self measureBlock:^{ 52 | (void)[[RSOPMLParser alloc] initWithXMLData:xmlData]; 53 | }]; 54 | } 55 | 56 | 57 | - (void)testSubsStructure { 58 | 59 | RSXMLData *xmlData = [[self class] subsData]; 60 | 61 | RSOPMLParser *parser = [[RSOPMLParser alloc] initWithXMLData:xmlData]; 62 | XCTAssertNotNil(parser); 63 | 64 | RSOPMLDocument *document = parser.OPMLDocument; 65 | XCTAssertNotNil(document); 66 | 67 | [self checkStructureForOPMLItem:document]; 68 | } 69 | 70 | - (void)checkStructureForOPMLItem:(RSOPMLItem *)item { 71 | 72 | RSOPMLFeedSpecifier *feedSpecifier = item.OPMLFeedSpecifier; 73 | 74 | if (![item isKindOfClass:[RSOPMLDocument class]]) { 75 | XCTAssertNotNil(item.attributes.opml_text); 76 | } 77 | 78 | // If it has no children, it should have a feed specifier. The converse is also true. 79 | BOOL isFolder = (item.children.count > 0); 80 | if (!isFolder && [item.attributes.opml_title isEqualToString:@"Skip"]) { 81 | isFolder = YES; 82 | } 83 | 84 | if (!isFolder) { 85 | XCTAssertNotNil(feedSpecifier.title); 86 | XCTAssertNotNil(feedSpecifier.feedURL); 87 | } 88 | else { 89 | XCTAssertNil(feedSpecifier); 90 | if (![item isKindOfClass:[RSOPMLDocument class]]) { 91 | XCTAssertNotNil(item.attributes.opml_title); 92 | } 93 | } 94 | 95 | if (item.children.count > 0) { 96 | for (RSOPMLItem *oneItem in item.children) { 97 | [self checkStructureForOPMLItem:oneItem]; 98 | } 99 | } 100 | } 101 | 102 | 103 | @end 104 | -------------------------------------------------------------------------------- /RSXMLTests/RSXMLTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSXMLTests.m 3 | // RSXMLTests 4 | // 5 | // Created by Brent Simmons on 7/12/15. 6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | @import RSXML; 11 | 12 | @interface RSXMLTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation RSXMLTests 17 | 18 | + (RSXMLData *)oftData { 19 | 20 | static RSXMLData *xmlData = nil; 21 | 22 | static dispatch_once_t onceToken; 23 | dispatch_once(&onceToken, ^{ 24 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"OneFootTsunami" ofType:@"atom"]; 25 | NSData *d = [[NSData alloc] initWithContentsOfFile:s]; 26 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://onefoottsunami.com/"]; 27 | }); 28 | 29 | return xmlData; 30 | } 31 | 32 | 33 | + (RSXMLData *)scriptingNewsData { 34 | 35 | static RSXMLData *xmlData = nil; 36 | 37 | static dispatch_once_t onceToken; 38 | dispatch_once(&onceToken, ^{ 39 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"scriptingNews" ofType:@"rss"]; 40 | NSData *d = [[NSData alloc] initWithContentsOfFile:s]; 41 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://scripting.com/"]; 42 | }); 43 | 44 | return xmlData; 45 | } 46 | 47 | 48 | + (RSXMLData *)mantonData { 49 | 50 | static RSXMLData *xmlData = nil; 51 | 52 | static dispatch_once_t onceToken; 53 | dispatch_once(&onceToken, ^{ 54 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"manton" ofType:@"rss"]; 55 | NSData *d = [[NSData alloc] initWithContentsOfFile:s]; 56 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://manton.org/"]; 57 | }); 58 | 59 | return xmlData; 60 | } 61 | 62 | 63 | + (RSXMLData *)daringFireballData { 64 | 65 | static RSXMLData *xmlData = nil; 66 | 67 | static dispatch_once_t onceToken; 68 | dispatch_once(&onceToken, ^{ 69 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"DaringFireball" ofType:@"rss"]; 70 | NSData *d = [[NSData alloc] initWithContentsOfFile:s]; 71 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://daringfireball.net/"]; 72 | }); 73 | 74 | return xmlData; 75 | } 76 | 77 | 78 | + (RSXMLData *)katieFloydData { 79 | 80 | static RSXMLData *xmlData = nil; 81 | 82 | static dispatch_once_t onceToken; 83 | dispatch_once(&onceToken, ^{ 84 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"KatieFloyd" ofType:@"rss"]; 85 | NSData *d = [[NSData alloc] initWithContentsOfFile:s]; 86 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://katiefloyd.com/"]; 87 | }); 88 | 89 | return xmlData; 90 | } 91 | 92 | 93 | + (RSXMLData *)eMarleyData { 94 | 95 | static RSXMLData *xmlData = nil; 96 | 97 | static dispatch_once_t onceToken; 98 | dispatch_once(&onceToken, ^{ 99 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"EMarley" ofType:@"rss"]; 100 | NSData *d = [[NSData alloc] initWithContentsOfFile:s]; 101 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"https://medium.com/@emarley"]; 102 | }); 103 | 104 | return xmlData; 105 | } 106 | 107 | 108 | - (void)testOneFootTsunami { 109 | 110 | NSError *error = nil; 111 | RSXMLData *xmlData = [[self class] oftData]; 112 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error); 113 | NSLog(@"parsedFeed: %@", parsedFeed); 114 | } 115 | 116 | 117 | - (void)testOFTPerformance { 118 | 119 | RSXMLData *xmlData = [[self class] oftData]; 120 | 121 | [self measureBlock:^{ 122 | NSError *error = nil; 123 | RSParseFeedSync(xmlData, &error); 124 | }]; 125 | } 126 | 127 | 128 | - (void)testScriptingNews { 129 | 130 | NSError *error = nil; 131 | RSXMLData *xmlData = [[self class] scriptingNewsData]; 132 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error); 133 | NSLog(@"parsedFeed: %@", parsedFeed); 134 | } 135 | 136 | 137 | - (void)testManton { 138 | 139 | NSError *error = nil; 140 | RSXMLData *xmlData = [[self class] mantonData]; 141 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error); 142 | NSLog(@"parsedFeed: %@", parsedFeed); 143 | } 144 | 145 | 146 | - (void)testKatieFloyd { 147 | 148 | NSError *error = nil; 149 | RSXMLData *xmlData = [[self class] katieFloydData]; 150 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error); 151 | XCTAssertEqualObjects(parsedFeed.title, @"Katie Floyd"); 152 | } 153 | 154 | 155 | - (void)testEMarley { 156 | 157 | NSError *error = nil; 158 | RSXMLData *xmlData = [[self class] eMarleyData]; 159 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error); 160 | XCTAssertEqualObjects(parsedFeed.title, @"Stories by Liz Marley on Medium"); 161 | XCTAssertEqual(parsedFeed.articles.count, 10); 162 | } 163 | 164 | 165 | - (void)testScriptingNewsPerformance { 166 | 167 | RSXMLData *xmlData = [[self class] scriptingNewsData]; 168 | 169 | [self measureBlock:^{ 170 | NSError *error = nil; 171 | RSParseFeedSync(xmlData, &error); 172 | }]; 173 | 174 | } 175 | 176 | 177 | - (void)testMantonPerformance { 178 | 179 | RSXMLData *xmlData = [[self class] mantonData]; 180 | 181 | [self measureBlock:^{ 182 | NSError *error = nil; 183 | RSParseFeedSync(xmlData, &error); 184 | }]; 185 | 186 | } 187 | 188 | 189 | - (void)testDaringFireballPerformance { 190 | 191 | RSXMLData *xmlData = [[self class] daringFireballData]; 192 | 193 | [self measureBlock:^{ 194 | NSError *error = nil; 195 | RSParseFeedSync(xmlData, &error); 196 | }]; 197 | } 198 | 199 | 200 | - (void)testCanParseFeedPerformance { 201 | 202 | RSXMLData *xmlData = [[self class] daringFireballData]; 203 | // 0.379 204 | [self measureBlock:^{ 205 | for (NSInteger i = 0; i < 100; i++) { 206 | RSCanParseFeed(xmlData); 207 | } 208 | }]; 209 | } 210 | 211 | @end 212 | -------------------------------------------------------------------------------- /RSXMLTests/Resources/EMarley.rss: -------------------------------------------------------------------------------- 1 | 2 | 3 | <![CDATA[Stories by Liz Marley on Medium]]> 4 | 5 | https://medium.com/@emarley?source=rss-b4981c59ffa5------2 6 | 7 | https://d262ilb51hltx0.cloudfront.net/fit/c/150/150/0*I9s5OlzJw_En0NzC.jpg 8 | Stories by Liz Marley on Medium 9 | https://medium.com/@emarley?source=rss-b4981c59ffa5------2 10 | 11 | Medium 12 | Sun, 28 Aug 2016 17:27:51 GMT 13 | 14 | 15 | 16 | 17 | <![CDATA[UI Automation & screenshots]]> 18 |

Here’s a partial collection of links from my talk today…

]]>
19 | https://medium.com/@emarley/ui-automation-screenshots-c44a41af38d1?source=rss-b4981c59ffa5------2 20 | https://medium.com/p/c44a41af38d1 21 | 22 | Sat, 07 May 2016 23:53:30 GMT 23 |
24 | 25 | <![CDATA[They didn’t.]]> 26 |

“The [software developer tool] team clearly doesn’t use [that tool] themselves.”

]]>
27 | https://medium.com/@emarley/they-didn-t-3a4dab489f45?source=rss-b4981c59ffa5------2 28 | https://medium.com/p/3a4dab489f45 29 | 30 | Sat, 09 Jan 2016 15:29:25 GMT 31 |
32 | 33 | <![CDATA[Side quest: Drawing]]> 34 |

]]>
35 | https://medium.com/@emarley/side-quest-drawing-b959ded1a1a4?source=rss-b4981c59ffa5------2 36 | https://medium.com/p/b959ded1a1a4 37 | 38 | Wed, 09 Dec 2015 03:37:35 GMT 39 |
40 | 41 | <![CDATA[And if I somehow lose the iPad Pro, I can find that with Find My iPhone.]]> 42 | ]]> 43 | https://medium.com/@emarley/and-if-i-somehow-lose-the-ipad-pro-i-can-find-that-with-find-my-iphone-e9aa43486521?source=rss-b4981c59ffa5------2 44 | https://medium.com/p/e9aa43486521 45 | 46 | Mon, 23 Nov 2015 19:38:20 GMT 47 | 48 | 49 | <![CDATA[Though not as much more weight as you might expect.]]> 50 | ]]> 51 | https://medium.com/@emarley/though-not-as-much-more-weight-as-you-might-expect-7b33fe989f6e?source=rss-b4981c59ffa5------2 52 | https://medium.com/p/7b33fe989f6e 53 | 54 | Mon, 23 Nov 2015 19:37:38 GMT 55 | 56 | 57 | <![CDATA[I avoided art classes in high school and college because I was afraid they would hurt my GPA.]]> 58 | ]]> 59 | https://medium.com/@emarley/i-avoided-art-classes-in-high-school-and-college-because-i-was-afraid-they-would-hurt-my-gpa-ab916601f2ad?source=rss-b4981c59ffa5------2 60 | https://medium.com/p/ab916601f2ad 61 | 62 | Mon, 23 Nov 2015 19:37:13 GMT 63 | 64 | 65 | <![CDATA[Finding Value]]> 66 |

I lose things a lot. Sometimes they’re just misplaced, sometimes gone forever. I don’t know if I have ever run out of ink in a pen—there’s…

]]>
67 | https://medium.com/@emarley/finding-value-20a90bf5ebf?source=rss-b4981c59ffa5------2 68 | https://medium.com/p/20a90bf5ebf 69 | 70 | Mon, 23 Nov 2015 19:34:18 GMT 71 |
72 | 73 | <![CDATA[Replaying this post in my head last night, I regret this word.]]> 74 |

Keyboard shortcuts, and other little details may be programmatically simple to set up, but they are still an important part of an app’s…

]]>
75 | https://medium.com/@emarley/replaying-this-post-in-my-head-last-night-i-regret-this-word-d8ed0b43f0f9?source=rss-b4981c59ffa5------2 76 | https://medium.com/p/d8ed0b43f0f9 77 | 78 | Tue, 10 Nov 2015 18:08:19 GMT 79 |
80 | 81 | <![CDATA[Betterment]]> 82 |

I moved from Senior Test Pilot to Software Engineer last month.

]]>
83 | https://medium.com/@emarley/betterment-e0ef45fcd284?source=rss-b4981c59ffa5------2 84 | https://medium.com/p/e0ef45fcd284 85 | 86 | Tue, 10 Nov 2015 02:17:46 GMT 87 |
88 | 89 | <![CDATA[This is a test.]]> 90 |

This is only a test.

]]>
91 | https://medium.com/@emarley/this-is-a-test-6ab141a1c5b5?source=rss-b4981c59ffa5------2 92 | https://medium.com/p/6ab141a1c5b5 93 | 94 | Sun, 20 Sep 2015 07:00:44 GMT 95 |
96 |
97 |
-------------------------------------------------------------------------------- /RSXMLTests/Resources/TimerSearch.txt: -------------------------------------------------------------------------------- 1 | class SomeViewController: NSViewController { 2 | 3 | @IBOutlet weak var textField: NSTextField 4 | private var NSTimer: fetchDataTimer? 5 | private var currentText: String? { 6 | didSet { 7 | invalidateTimer() 8 | if currentText.length > 3 { 9 | restartTimer() 10 | } 11 | } 12 | } 13 | 14 | func textDidChange(notification: NSNotification) { 15 | 16 | currentText = textField.stringValue 17 | } 18 | 19 | func invalidateTimer() { 20 | 21 | if let timer = timer { 22 | if timer.isValid { 23 | timer.invalidate() 24 | } 25 | self.timer = nil 26 | } 27 | } 28 | 29 | 30 | 31 | } -------------------------------------------------------------------------------- /RSXMLTests/Resources/inessential.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | inessential: weblog 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 |
17 | 18 |
19 |

Lizcast

20 |

The Omni Group’s Liz Marley, who recently transitioned from testing to engineering, appears on the NSNorth 2016 podcast. She talks about…

21 | 22 |

…challenges in engineering school, working with office cats, making the transition from software engineering to testing to developing and how knitting, like code, has the ultimate undo.

23 | 24 |

Knitting is serious (though not somber) business here at Omni.

25 | 26 |
27 |

OmniOutliner 4.5

28 |

OmniOutliner 4.5 is up on Omni’s site, and should be in the Mac App Store within days.

29 | 30 |

With this release — see the release notes — I helped work on, of all things, printing bugs and features. This is the first time in my entire career where I worked on printing support that was more than just the most basic possible thing.

31 | 32 |

And that sounds weird for the year 2016, I realize. But here’s the thing: working on printing support is far from glamorous. You wouldn’t call it fun. But the people who need these features really do need them, and it’s a matter of respect for OmniOutliner users that we do a great job even with printing.

33 | 34 |

But I sure am glad to get it finished and shipping. And I’m proud of the work we did — more proud than I expected to be. It’s solid, and I think the people who print from OmniOutliner will be very pleased.

35 | 36 |

Now we’re on to other new features, including editing Markdown documents with OmniOutliner.

37 | 38 |
39 |

OmniDev

40 |

Omni is hiring a Mac/iOS developer!

41 | 42 |

We’re also hiring a web developer, graphic designer, and phone support humans.

43 | 44 |

I’ll let you try out my new beanbag chair.

45 | 46 |
47 |

OmniJobs

48 |

We’re hiring a senior front-end web developer, a graphic designer, and support humans.

49 | 50 |

You should apply.

51 | 52 |
53 |

It Will Be Trump

54 |

The South Carolina primary is where the establishment fixes the errors of Iowa and New Hampshire. It’s Lee Atwater’s firewall.

55 | 56 |

When Buchanan threatens Dole, South Carolina shuts it down. When McCain threatens Bush, South Carolina applies the kibosh.

57 | 58 |

But is there any hope that it will function that way this time?

59 | 60 |

I don’t think so. The establishment candidates are Bush, Rubio, and Kasich. They don’t have a shot. Nor does Cruz. Trump wins South Carolina.

61 | 62 |

If that’s true, then it’s all over. If South Carolina fails — if the very primary that’s designed to toss the ball back to the establishment fails — then there’s no hope at all.

63 | 64 |

Cruz will go on to win a few states, most notably Texas. But otherwise it’s going to be Trump. He’ll get the delegates he needs, and that will be that.

65 | 66 |
67 |

Origin of Good (and Bad) Hair Day

68 |

When I was in middle school in the late ’70s I struggled to get my hair to feather properly. It just didn’t want to do it.

69 | 70 |

Like many kids that age I was newly conscious of my appearance — and I naïvely thought that well-feathered hair was a necessary (though not sufficient) key to fitting in. (Which was probably true, by the way.)

71 | 72 |

Every morning I would find that my hair behaved, at least somewhat, or it didn’t. So I categorized each day as a “good hair day” and a “bad hair day.”

73 | 74 |

I told my friends about this categorization — including a neighborhood girl named Sarah. She ended up telling other kids at school.

75 | 76 |

And pretty soon those kids, even kids I didn’t really know, would stop me in the halls or at lunch and say, “Hey Brent — good hair day or bad hair day?” Not meanly. Teasingly. It was funny.

77 | 78 |

Years later I started hearing the phrase on TV, and I was surprised that my little middle-school thing had spread and become part of the culture.

79 | 80 |

* * *

81 | 82 | 83 |

Of course, it’s also possible that I picked it up from Jane Pauley. But for all these years I’ve believed — no joke — that it was me, that it was my phrase. Maybe Jane Pauley got it (indirectly) from me.

84 | 85 |

It’s highly unlikely — of course, I know this — that I’m the originator. But still, it had to be someone, right?

86 | 87 |

(Not necessarily. It’s kind of obvious and could have had many originators.)

88 | 89 |

* * *

90 | 91 | 92 |

I stopped categorizing good and bad hair days by the time I got to high school. And these days I’m just glad that I still have some hair.

93 | 94 |
95 |

River5

96 |

River5 is Dave Winer’s river-of-news RSS aggregator.

97 | 98 |

It’s a Node app. You can run it on a public machine and access it anywhere, or run it on your desktop and just read your news there.

99 | 100 |
101 |

Stop Watch

102 |

Some time last week my iPhone started prompting me frequently to re-enter my iCloud password. And then my Watch started doing the same, about once a minute — with a little tap on the wrist each time.

103 | 104 |

Obviously I did re-enter my password — and have done so a dozen or so times now — but it doesn’t seem to matter.

105 | 106 |

So I stopped wearing my Watch and have switched to a mid-sixties Hamilton that my Dad gave me. (He had gotten it as a high school graduation present.)

107 | 108 |

I’m no watch aficionado — but I do appreciate a good and attractive watch (which this is), and I appreciate even more an old watch that’s a family thing.

109 | 110 |

Here’s the thing, though: the Apple Watch contains a hundred miracles of engineering and design, surely, but serious problems with software and services can turn even the most incredible hardware into something you just sit on your desk and ignore.

111 | 112 |
113 |

On Sanders Governing

114 |

The Atlantic, Norm Ornstein:

115 | 116 |

But is there any real evidence that there is a hidden “sleeper cell” of potential voters who are waiting for the signal to emerge and transform the electorate? No.

117 | 118 |

Pure candidates on both sides of the spectrum often claim that their purity will bring in the checked-out voters, because they’re just waiting for a real conservative or a real liberal.

119 | 120 |

It’s an enduring fairy tale with terrible consequences. To put faith in it is to lose to the other party.

121 | 122 |
123 |

CocoaConf Podcast with Me

124 |

Cesare Rocchi interviewed me for the latest CocoaConf Podcast on life before the App Store.

125 | 126 |

There was a life, by the way. It was fun! We could release software any time we wanted to.

127 | 128 |
129 |

Archive

130 | 131 |
132 | 133 |
134 |
135 | Ads via The Deck 136 | 145 |
146 |
147 | 148 | 149 | 164 | 165 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /RSXMLTests/Resources/manton.rss: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | Manton Reece 12 | 13 | http://www.manton.org 14 | 15 | Fri, 25 Sep 2015 14:26:40 +0000 16 | en-US 17 | hourly 18 | 1 19 | http://wordpress.org/?v=4.2.5 20 | 21 | 22 | http://www.manton.org/2015/09/3071.html 23 | http://www.manton.org/2015/09/3071.html#comments 24 | Fri, 25 Sep 2015 14:26:40 +0000 25 | 26 | 27 | 28 | http://www.manton.org/?p=3071 29 | 30 | This week’s Core Intuition is out with a discussion about new and old iPhones, the latest rumors about an Apple Car, and a follow-up on WebKit for Apple TV.

31 | ]]>
32 | http://www.manton.org/2015/09/3071.html/feed 33 | 0 34 |
35 | 36 | 37 | http://www.manton.org/2015/09/3069.html 38 | http://www.manton.org/2015/09/3069.html#comments 39 | Fri, 25 Sep 2015 00:38:25 +0000 40 | 41 | 42 | 43 | http://www.manton.org/?p=3069 44 | 45 | I probably shouldn’t have started installing watchOS 2.0 right before needing to leave the house. Taking… for… ev… er.

46 | ]]>
47 | http://www.manton.org/2015/09/3069.html/feed 48 | 0 49 |
50 | 51 | 52 | http://www.manton.org/2015/09/3067.html 53 | http://www.manton.org/2015/09/3067.html#comments 54 | Thu, 24 Sep 2015 15:51:51 +0000 55 | 56 | 57 | 58 | http://www.manton.org/?p=3067 59 | 60 | Looking forward to NSDrinking tonight, 8pm at Radio Coffee & Beer.

61 | ]]>
62 | http://www.manton.org/2015/09/3067.html/feed 63 | 0 64 |
65 | 66 | Instagram hits 400 million users 67 | http://www.manton.org/2015/09/instagram-hits-400-million-users.html 68 | http://www.manton.org/2015/09/instagram-hits-400-million-users.html#comments 69 | Wed, 23 Sep 2015 14:34:12 +0000 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | http://www.manton.org/?p=3064 78 | 79 | From Graham Spencer at MacStories, commenting on the latest Instagram numbers and that the service is only 5 years old:

80 |

81 | “But I was really surprised to remember that Facebook acquired Instagram in April 2012, when Instagram had ‘only’ 40 million users. If I recall correctly, a lot of people thought Facebook was crazy to buy Instagram for $1 billion. Well, I think Facebook got the last laugh on that one, and as Forbes points out, Instagram now has more monthly active users than Twitter (316 million).” 82 |

83 |

Impressive growth, but it fits. Instagram has crafted a user experience that encourages thoughtful posts and never feels overwhelming in the way a Twitter or Facebook timeline can be. If Instagram was a paid product, I bet Instagram’s churn rate would be the lowest of any of the big social networks. They did it with a small team and weren’t afraid to grow slowly.

84 | ]]>
85 | http://www.manton.org/2015/09/instagram-hits-400-million-users.html/feed 86 | 0 87 |
88 | 89 | Complete mirror of this blog 90 | http://www.manton.org/2015/09/complete-mirror-of-this-blog.html 91 | http://www.manton.org/2015/09/complete-mirror-of-this-blog.html#comments 92 | Sun, 20 Sep 2015 19:00:33 +0000 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | http://www.manton.org/?p=3061 102 | 103 | I’ve been blogging here for 13 years. If you take any random post from that first year, the majority of the links to other web sites are broken. The default outcome for any site that isn’t maintained — including the one you’re reading right now — is for it to vanish. Permanence doesn’t exist on the web.

104 |

We can solve this, but it will take time. For now I think mirroring our writing is a great solution, to guard against domain names expiring and other inevitable failures. But where to mirror to?

105 |

Only 2 companies keep coming to mind: WordPress.com and GitHub. I believe both will last for decades, maybe even 100 years, and both embrace the open web in a way that most other centralized web sites do not.

106 |

Even though I self-host this weblog on WordPress, I’ve chosen to mirror to GitHub because of their focus on simple, static publishing via GitHub Pages. It has the best chance of running for a long time without intervention.

107 |

I exported all of manton.org with the httrack command-line tool and checked it into GitHub, with a CNAME for mirror.manton.org. It works perfectly. I still need to automate this process so that it updates regularly, but I’m very happy to finally have a complete mirror for the first time.

108 | ]]>
109 | http://www.manton.org/2015/09/complete-mirror-of-this-blog.html/feed 110 | 0 111 |
112 | 113 | Steve Jobs and ET 114 | http://www.manton.org/2015/09/steve-jobs-and-et.html 115 | http://www.manton.org/2015/09/steve-jobs-and-et.html#comments 116 | Sat, 19 Sep 2015 23:00:36 +0000 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | http://www.manton.org/?p=3059 126 | 127 | I watched two documentaries last week. The first was “Steve Jobs: The Man in the Machine”, which I somewhat regret paying $7 to rent. It had its moments, but also seemed to become more negative and dramatic the longer it went on. I guess we should all hope to be so lucky and famous to have people try to bring out the best and worst of us.

128 |

The second documentary I watched was “Atari: Game Over”, which was free on Netflix. It was great, interspersing a history of the rise and fall of Atari with the effort to dig up the ET game cartridges supposedly buried in New Mexico. Highly recommended.

129 | ]]>
130 | http://www.manton.org/2015/09/steve-jobs-and-et.html/feed 131 | 0 132 |
133 | 134 | Peace, indies, and the App Store 135 | http://www.manton.org/2015/09/peace-indies-and-the-app-store.html 136 | http://www.manton.org/2015/09/peace-indies-and-the-app-store.html#comments 137 | Sat, 19 Sep 2015 15:53:31 +0000 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | http://www.manton.org/?p=3056 147 | 148 | You’ve probably heard that Marco Arment has pulled his content-blocking app Peace from the App Store. The app was extremely successful:

149 |

150 | “As I write this, Peace has been the number one paid app in the U.S. App Store for about 36 hours. It’s a massive achievement that should be the highlight of my professional career. If Overcast even broke the top 100, I’d be over the moon.” 151 |

152 |

I’ve seen some comments asking why he didn’t think to do this sooner, before he even shipped the app. But we are just now starting to understand the impact of ad blockers in iOS 9. I don’t think it’s an exaggeration to say that the web is different than it was a few days ago, and so our choices — and Marco’s — are different too. As I mentioned yesterday, content blockers are one facet of an overall shake-up for the web.

153 |

Brent Simmons writes that only indies can do what Marco did. Marco must have left a lot of money on the table with this decision. It will always look like the right call to me when someone goes with their gut feeling and not with profit.

154 | ]]>
155 | http://www.manton.org/2015/09/peace-indies-and-the-app-store.html/feed 156 | 0 157 |
158 | 159 | Core Intuition and ATP this week 160 | http://www.manton.org/2015/09/core-intuition-and-atp-this-week.html 161 | http://www.manton.org/2015/09/core-intuition-and-atp-this-week.html#comments 162 | Fri, 18 Sep 2015 20:25:57 +0000 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | http://www.manton.org/?p=3051 171 | 172 | On this week’s Core Intuition, Daniel and I spend the whole show talking about the Apple TV. The first half is about the Apple TV dev kit lottery, and the second half is about whether we need the web on our TVs.

173 |

There’s also a good discussion on the Accidental Tech Podcast about this. Here’s an Overcast link about halfway into the episode.

174 | ]]>
175 | http://www.manton.org/2015/09/core-intuition-and-atp-this-week.html/feed 176 | 0 177 |
178 | 179 | Wrap-up thoughts on the TV web 180 | http://www.manton.org/2015/09/wrap-up-thoughts-on-the-tv-web.html 181 | http://www.manton.org/2015/09/wrap-up-thoughts-on-the-tv-web.html#comments 182 | Fri, 18 Sep 2015 14:58:39 +0000 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | http://www.manton.org/?p=3048 192 | 193 | I’m going to mostly let John Gruber have the last word on the Apple TV vs. the web debate, because I could write about this every day and my readers would run away before I run out of material. I’m glad John addressed the Mac vs. the command-line argument, though, because it didn’t seem quite right to me either. He says:

194 |

195 | “The difference is that the command-line-less Mac was intended to replace command-line-based computers. The GUI relegated the command-line interface to a permanent tiny niche. Apple TV and Apple Watch aren’t like that at all — they’re not meant to replace any device you already use to access the open web.” 196 |

197 |

This is the most hopeful part of the Apple ecosystem as it relates to the web. Apple’s other platforms really do have a great web experience. Remember when web sites were faster and worked better on a PC than a Mac? If anything, the opposite is true now.

198 |

One of the themes I keep hearing is that a “web browser” on a TV will make for a poor user experience, so don’t bother. I tried to correct that misunderstanding in this post; it’s not about standalone Safari, it’s about web technologies that could be used in native apps. But ignoring that, I think everyone too easily forgets what the mobile web was like before the iPhone.

199 |

Steve Jobs, from the original iPhone introduction:

200 |

201 | “We wanted the best web browser in the world on our phone. Not a baby web browser or a WAP browser — a real browser. […] It is the first fully usable HTML browser on a phone.” 202 |

203 |

That was a breakthrough. I believe the same evolution is possible on tvOS — to include parts of the open web and do it with a great user experience. You can start by weaving it together inside native apps. (I filed a bug with Apple yesterday with a suggestion. It was marked as a duplicate.)

204 |

The web is at a fascinating, pivotal time right now. It has been shaken up by centralized publishing, closed platforms, and now content blockers. Users no longer value the concepts that made Web 2.0 special. The web can still have a strong future, but we have to try something, and we have to try it on every platform we can.

205 | ]]>
206 | http://www.manton.org/2015/09/wrap-up-thoughts-on-the-tv-web.html/feed 207 | 0 208 |
209 | 210 | 211 | http://www.manton.org/2015/09/3046.html 212 | http://www.manton.org/2015/09/3046.html#comments 213 | Fri, 18 Sep 2015 13:43:21 +0000 214 | 215 | 216 | 217 | http://www.manton.org/?p=3046 218 | 219 | Expecting two packages today: the new Apple TV, and my new iPhone 5S (32 GB, space gray).

220 | ]]>
221 | http://www.manton.org/2015/09/3046.html/feed 222 | 0 223 |
224 |
225 |
226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /RSXMLiOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | RSXML 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------