├── .gitignore ├── IDNFeedParser.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── IDNFeedParser ├── AppDelegate.h ├── AppDelegate.m ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── FeedInfosController.h ├── FeedInfosController.m ├── FeedItemsController.h ├── FeedItemsController.m ├── IDNFeedParser │ ├── GTMNSString+HTML.h │ ├── GTMNSString+HTML.m │ ├── IDNFeedParser.h │ ├── IDNFeedParser.m │ ├── NSDate+InternetDateTime.h │ ├── NSDate+InternetDateTime.m │ ├── NSString+HTML.h │ └── NSString+HTML.m ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── UIViewController+IDNPrompt.h ├── UIViewController+IDNPrompt.m ├── WebViewController.h ├── WebViewController.m └── main.m ├── IDNFeedParserTests ├── IDNFeedParserTests.m └── Info.plist └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | !.gitattributes 4 | !.env.example 5 | !.project 6 | !.cproject 7 | !.gitkeep 8 | !.gitkeeper 9 | !.htaccess 10 | *~ 11 | *.swp 12 | *.bak 13 | build/ 14 | Debug/ 15 | Release/ 16 | ipch/ 17 | *.ncb 18 | Thumbs.db 19 | *.mode1v3 20 | *.DS_Store 21 | *.vspscc 22 | *.user 23 | *.sdf 24 | *.filters 25 | *.opensdf 26 | *.suo 27 | ~$*.xls* 28 | ~$*.doc* 29 | xcuserdata/ 30 | xcshareddata/ 31 | nbproject/ 32 | nbproject/private/ 33 | -------------------------------------------------------------------------------- /IDNFeedParser.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 94CD91501B3C6E5C00823D12 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD914F1B3C6E5C00823D12 /* main.m */; }; 11 | 94CD91531B3C6E5C00823D12 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD91521B3C6E5C00823D12 /* AppDelegate.m */; }; 12 | 94CD91561B3C6E5C00823D12 /* FeedItemsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD91551B3C6E5C00823D12 /* FeedItemsController.m */; }; 13 | 94CD91591B3C6E5C00823D12 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 94CD91571B3C6E5C00823D12 /* Main.storyboard */; }; 14 | 94CD915B1B3C6E5C00823D12 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 94CD915A1B3C6E5C00823D12 /* Images.xcassets */; }; 15 | 94CD915E1B3C6E5C00823D12 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 94CD915C1B3C6E5C00823D12 /* LaunchScreen.xib */; }; 16 | 94CD916A1B3C6E5C00823D12 /* IDNFeedParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD91691B3C6E5C00823D12 /* IDNFeedParserTests.m */; }; 17 | 94CD917C1B3C6EF700823D12 /* GTMNSString+HTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD91751B3C6EF700823D12 /* GTMNSString+HTML.m */; }; 18 | 94CD917D1B3C6EF700823D12 /* IDNFeedParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD91771B3C6EF700823D12 /* IDNFeedParser.m */; }; 19 | 94CD917E1B3C6EF700823D12 /* NSDate+InternetDateTime.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD91791B3C6EF700823D12 /* NSDate+InternetDateTime.m */; }; 20 | 94CD917F1B3C6EF700823D12 /* NSString+HTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD917B1B3C6EF700823D12 /* NSString+HTML.m */; }; 21 | 94CD91811B3DC1D500823D12 /* FeedInfosController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD91801B3DC1D500823D12 /* FeedInfosController.m */; }; 22 | 94CD91851B3DC4FE00823D12 /* UIViewController+IDNPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD91841B3DC4FE00823D12 /* UIViewController+IDNPrompt.m */; }; 23 | 94CD91891B3DCAEF00823D12 /* WebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CD91881B3DCAEF00823D12 /* WebViewController.m */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | 94CD91641B3C6E5C00823D12 /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = 94CD91421B3C6E5C00823D12 /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = 94CD91491B3C6E5C00823D12; 32 | remoteInfo = IDNFeedParser; 33 | }; 34 | /* End PBXContainerItemProxy section */ 35 | 36 | /* Begin PBXFileReference section */ 37 | 94CD914A1B3C6E5C00823D12 /* IDNFeedParser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IDNFeedParser.app; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | 94CD914E1B3C6E5C00823D12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | 94CD914F1B3C6E5C00823D12 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 40 | 94CD91511B3C6E5C00823D12 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 41 | 94CD91521B3C6E5C00823D12 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 42 | 94CD91541B3C6E5C00823D12 /* FeedItemsController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FeedItemsController.h; sourceTree = ""; }; 43 | 94CD91551B3C6E5C00823D12 /* FeedItemsController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FeedItemsController.m; sourceTree = ""; }; 44 | 94CD91581B3C6E5C00823D12 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 45 | 94CD915A1B3C6E5C00823D12 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 46 | 94CD915D1B3C6E5C00823D12 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 47 | 94CD91631B3C6E5C00823D12 /* IDNFeedParserTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IDNFeedParserTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 94CD91681B3C6E5C00823D12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | 94CD91691B3C6E5C00823D12 /* IDNFeedParserTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IDNFeedParserTests.m; sourceTree = ""; }; 50 | 94CD91741B3C6EF700823D12 /* GTMNSString+HTML.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSString+HTML.h"; sourceTree = ""; }; 51 | 94CD91751B3C6EF700823D12 /* GTMNSString+HTML.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSString+HTML.m"; sourceTree = ""; }; 52 | 94CD91761B3C6EF700823D12 /* IDNFeedParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IDNFeedParser.h; sourceTree = ""; }; 53 | 94CD91771B3C6EF700823D12 /* IDNFeedParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IDNFeedParser.m; sourceTree = ""; }; 54 | 94CD91781B3C6EF700823D12 /* NSDate+InternetDateTime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+InternetDateTime.h"; sourceTree = ""; }; 55 | 94CD91791B3C6EF700823D12 /* NSDate+InternetDateTime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+InternetDateTime.m"; sourceTree = ""; }; 56 | 94CD917A1B3C6EF700823D12 /* NSString+HTML.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+HTML.h"; sourceTree = ""; }; 57 | 94CD917B1B3C6EF700823D12 /* NSString+HTML.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+HTML.m"; sourceTree = ""; }; 58 | 94CD91801B3DC1D500823D12 /* FeedInfosController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedInfosController.m; sourceTree = ""; }; 59 | 94CD91821B3DC1E300823D12 /* FeedInfosController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeedInfosController.h; sourceTree = ""; }; 60 | 94CD91831B3DC4FE00823D12 /* UIViewController+IDNPrompt.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+IDNPrompt.h"; sourceTree = ""; }; 61 | 94CD91841B3DC4FE00823D12 /* UIViewController+IDNPrompt.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+IDNPrompt.m"; sourceTree = ""; }; 62 | 94CD91871B3DCAEF00823D12 /* WebViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebViewController.h; sourceTree = ""; }; 63 | 94CD91881B3DCAEF00823D12 /* WebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebViewController.m; sourceTree = ""; }; 64 | /* End PBXFileReference section */ 65 | 66 | /* Begin PBXFrameworksBuildPhase section */ 67 | 94CD91471B3C6E5C00823D12 /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | 94CD91601B3C6E5C00823D12 /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | /* End PBXFrameworksBuildPhase section */ 82 | 83 | /* Begin PBXGroup section */ 84 | 94CD91411B3C6E5C00823D12 = { 85 | isa = PBXGroup; 86 | children = ( 87 | 94CD914C1B3C6E5C00823D12 /* IDNFeedParser */, 88 | 94CD91661B3C6E5C00823D12 /* IDNFeedParserTests */, 89 | 94CD914B1B3C6E5C00823D12 /* Products */, 90 | ); 91 | sourceTree = ""; 92 | }; 93 | 94CD914B1B3C6E5C00823D12 /* Products */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 94CD914A1B3C6E5C00823D12 /* IDNFeedParser.app */, 97 | 94CD91631B3C6E5C00823D12 /* IDNFeedParserTests.xctest */, 98 | ); 99 | name = Products; 100 | sourceTree = ""; 101 | }; 102 | 94CD914C1B3C6E5C00823D12 /* IDNFeedParser */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 94CD91861B3DC51600823D12 /* IDNKit */, 106 | 94CD91731B3C6EF700823D12 /* IDNFeedParser */, 107 | 94CD91511B3C6E5C00823D12 /* AppDelegate.h */, 108 | 94CD91521B3C6E5C00823D12 /* AppDelegate.m */, 109 | 94CD91821B3DC1E300823D12 /* FeedInfosController.h */, 110 | 94CD91801B3DC1D500823D12 /* FeedInfosController.m */, 111 | 94CD91541B3C6E5C00823D12 /* FeedItemsController.h */, 112 | 94CD91551B3C6E5C00823D12 /* FeedItemsController.m */, 113 | 94CD91871B3DCAEF00823D12 /* WebViewController.h */, 114 | 94CD91881B3DCAEF00823D12 /* WebViewController.m */, 115 | 94CD91571B3C6E5C00823D12 /* Main.storyboard */, 116 | 94CD915A1B3C6E5C00823D12 /* Images.xcassets */, 117 | 94CD915C1B3C6E5C00823D12 /* LaunchScreen.xib */, 118 | 94CD914D1B3C6E5C00823D12 /* Supporting Files */, 119 | ); 120 | path = IDNFeedParser; 121 | sourceTree = ""; 122 | }; 123 | 94CD914D1B3C6E5C00823D12 /* Supporting Files */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 94CD914E1B3C6E5C00823D12 /* Info.plist */, 127 | 94CD914F1B3C6E5C00823D12 /* main.m */, 128 | ); 129 | name = "Supporting Files"; 130 | sourceTree = ""; 131 | }; 132 | 94CD91661B3C6E5C00823D12 /* IDNFeedParserTests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 94CD91691B3C6E5C00823D12 /* IDNFeedParserTests.m */, 136 | 94CD91671B3C6E5C00823D12 /* Supporting Files */, 137 | ); 138 | path = IDNFeedParserTests; 139 | sourceTree = ""; 140 | }; 141 | 94CD91671B3C6E5C00823D12 /* Supporting Files */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 94CD91681B3C6E5C00823D12 /* Info.plist */, 145 | ); 146 | name = "Supporting Files"; 147 | sourceTree = ""; 148 | }; 149 | 94CD91731B3C6EF700823D12 /* IDNFeedParser */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 94CD91761B3C6EF700823D12 /* IDNFeedParser.h */, 153 | 94CD91771B3C6EF700823D12 /* IDNFeedParser.m */, 154 | 94CD91741B3C6EF700823D12 /* GTMNSString+HTML.h */, 155 | 94CD91751B3C6EF700823D12 /* GTMNSString+HTML.m */, 156 | 94CD91781B3C6EF700823D12 /* NSDate+InternetDateTime.h */, 157 | 94CD91791B3C6EF700823D12 /* NSDate+InternetDateTime.m */, 158 | 94CD917A1B3C6EF700823D12 /* NSString+HTML.h */, 159 | 94CD917B1B3C6EF700823D12 /* NSString+HTML.m */, 160 | ); 161 | path = IDNFeedParser; 162 | sourceTree = ""; 163 | }; 164 | 94CD91861B3DC51600823D12 /* IDNKit */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 94CD91831B3DC4FE00823D12 /* UIViewController+IDNPrompt.h */, 168 | 94CD91841B3DC4FE00823D12 /* UIViewController+IDNPrompt.m */, 169 | ); 170 | name = IDNKit; 171 | sourceTree = ""; 172 | }; 173 | /* End PBXGroup section */ 174 | 175 | /* Begin PBXNativeTarget section */ 176 | 94CD91491B3C6E5C00823D12 /* IDNFeedParser */ = { 177 | isa = PBXNativeTarget; 178 | buildConfigurationList = 94CD916D1B3C6E5C00823D12 /* Build configuration list for PBXNativeTarget "IDNFeedParser" */; 179 | buildPhases = ( 180 | 94CD91461B3C6E5C00823D12 /* Sources */, 181 | 94CD91471B3C6E5C00823D12 /* Frameworks */, 182 | 94CD91481B3C6E5C00823D12 /* Resources */, 183 | ); 184 | buildRules = ( 185 | ); 186 | dependencies = ( 187 | ); 188 | name = IDNFeedParser; 189 | productName = IDNFeedParser; 190 | productReference = 94CD914A1B3C6E5C00823D12 /* IDNFeedParser.app */; 191 | productType = "com.apple.product-type.application"; 192 | }; 193 | 94CD91621B3C6E5C00823D12 /* IDNFeedParserTests */ = { 194 | isa = PBXNativeTarget; 195 | buildConfigurationList = 94CD91701B3C6E5C00823D12 /* Build configuration list for PBXNativeTarget "IDNFeedParserTests" */; 196 | buildPhases = ( 197 | 94CD915F1B3C6E5C00823D12 /* Sources */, 198 | 94CD91601B3C6E5C00823D12 /* Frameworks */, 199 | 94CD91611B3C6E5C00823D12 /* Resources */, 200 | ); 201 | buildRules = ( 202 | ); 203 | dependencies = ( 204 | 94CD91651B3C6E5C00823D12 /* PBXTargetDependency */, 205 | ); 206 | name = IDNFeedParserTests; 207 | productName = IDNFeedParserTests; 208 | productReference = 94CD91631B3C6E5C00823D12 /* IDNFeedParserTests.xctest */; 209 | productType = "com.apple.product-type.bundle.unit-test"; 210 | }; 211 | /* End PBXNativeTarget section */ 212 | 213 | /* Begin PBXProject section */ 214 | 94CD91421B3C6E5C00823D12 /* Project object */ = { 215 | isa = PBXProject; 216 | attributes = { 217 | LastUpgradeCheck = 0630; 218 | ORGANIZATIONNAME = iosdev.net; 219 | TargetAttributes = { 220 | 94CD91491B3C6E5C00823D12 = { 221 | CreatedOnToolsVersion = 6.3.1; 222 | }; 223 | 94CD91621B3C6E5C00823D12 = { 224 | CreatedOnToolsVersion = 6.3.1; 225 | TestTargetID = 94CD91491B3C6E5C00823D12; 226 | }; 227 | }; 228 | }; 229 | buildConfigurationList = 94CD91451B3C6E5C00823D12 /* Build configuration list for PBXProject "IDNFeedParser" */; 230 | compatibilityVersion = "Xcode 3.2"; 231 | developmentRegion = English; 232 | hasScannedForEncodings = 0; 233 | knownRegions = ( 234 | en, 235 | Base, 236 | ); 237 | mainGroup = 94CD91411B3C6E5C00823D12; 238 | productRefGroup = 94CD914B1B3C6E5C00823D12 /* Products */; 239 | projectDirPath = ""; 240 | projectRoot = ""; 241 | targets = ( 242 | 94CD91491B3C6E5C00823D12 /* IDNFeedParser */, 243 | 94CD91621B3C6E5C00823D12 /* IDNFeedParserTests */, 244 | ); 245 | }; 246 | /* End PBXProject section */ 247 | 248 | /* Begin PBXResourcesBuildPhase section */ 249 | 94CD91481B3C6E5C00823D12 /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | 94CD91591B3C6E5C00823D12 /* Main.storyboard in Resources */, 254 | 94CD915E1B3C6E5C00823D12 /* LaunchScreen.xib in Resources */, 255 | 94CD915B1B3C6E5C00823D12 /* Images.xcassets in Resources */, 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | 94CD91611B3C6E5C00823D12 /* Resources */ = { 260 | isa = PBXResourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | /* End PBXResourcesBuildPhase section */ 267 | 268 | /* Begin PBXSourcesBuildPhase section */ 269 | 94CD91461B3C6E5C00823D12 /* Sources */ = { 270 | isa = PBXSourcesBuildPhase; 271 | buildActionMask = 2147483647; 272 | files = ( 273 | 94CD91561B3C6E5C00823D12 /* FeedItemsController.m in Sources */, 274 | 94CD91891B3DCAEF00823D12 /* WebViewController.m in Sources */, 275 | 94CD917C1B3C6EF700823D12 /* GTMNSString+HTML.m in Sources */, 276 | 94CD91531B3C6E5C00823D12 /* AppDelegate.m in Sources */, 277 | 94CD917F1B3C6EF700823D12 /* NSString+HTML.m in Sources */, 278 | 94CD91811B3DC1D500823D12 /* FeedInfosController.m in Sources */, 279 | 94CD91501B3C6E5C00823D12 /* main.m in Sources */, 280 | 94CD91851B3DC4FE00823D12 /* UIViewController+IDNPrompt.m in Sources */, 281 | 94CD917E1B3C6EF700823D12 /* NSDate+InternetDateTime.m in Sources */, 282 | 94CD917D1B3C6EF700823D12 /* IDNFeedParser.m in Sources */, 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | }; 286 | 94CD915F1B3C6E5C00823D12 /* Sources */ = { 287 | isa = PBXSourcesBuildPhase; 288 | buildActionMask = 2147483647; 289 | files = ( 290 | 94CD916A1B3C6E5C00823D12 /* IDNFeedParserTests.m in Sources */, 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | /* End PBXSourcesBuildPhase section */ 295 | 296 | /* Begin PBXTargetDependency section */ 297 | 94CD91651B3C6E5C00823D12 /* PBXTargetDependency */ = { 298 | isa = PBXTargetDependency; 299 | target = 94CD91491B3C6E5C00823D12 /* IDNFeedParser */; 300 | targetProxy = 94CD91641B3C6E5C00823D12 /* PBXContainerItemProxy */; 301 | }; 302 | /* End PBXTargetDependency section */ 303 | 304 | /* Begin PBXVariantGroup section */ 305 | 94CD91571B3C6E5C00823D12 /* Main.storyboard */ = { 306 | isa = PBXVariantGroup; 307 | children = ( 308 | 94CD91581B3C6E5C00823D12 /* Base */, 309 | ); 310 | name = Main.storyboard; 311 | sourceTree = ""; 312 | }; 313 | 94CD915C1B3C6E5C00823D12 /* LaunchScreen.xib */ = { 314 | isa = PBXVariantGroup; 315 | children = ( 316 | 94CD915D1B3C6E5C00823D12 /* Base */, 317 | ); 318 | name = LaunchScreen.xib; 319 | sourceTree = ""; 320 | }; 321 | /* End PBXVariantGroup section */ 322 | 323 | /* Begin XCBuildConfiguration section */ 324 | 94CD916B1B3C6E5C00823D12 /* Debug */ = { 325 | isa = XCBuildConfiguration; 326 | buildSettings = { 327 | ALWAYS_SEARCH_USER_PATHS = NO; 328 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 329 | CLANG_CXX_LIBRARY = "libc++"; 330 | CLANG_ENABLE_MODULES = YES; 331 | CLANG_ENABLE_OBJC_ARC = YES; 332 | CLANG_WARN_BOOL_CONVERSION = YES; 333 | CLANG_WARN_CONSTANT_CONVERSION = YES; 334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 335 | CLANG_WARN_EMPTY_BODY = YES; 336 | CLANG_WARN_ENUM_CONVERSION = YES; 337 | CLANG_WARN_INT_CONVERSION = YES; 338 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 339 | CLANG_WARN_UNREACHABLE_CODE = YES; 340 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 341 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 342 | COPY_PHASE_STRIP = NO; 343 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 344 | ENABLE_STRICT_OBJC_MSGSEND = YES; 345 | GCC_C_LANGUAGE_STANDARD = gnu99; 346 | GCC_DYNAMIC_NO_PIC = NO; 347 | GCC_NO_COMMON_BLOCKS = YES; 348 | GCC_OPTIMIZATION_LEVEL = 0; 349 | GCC_PREPROCESSOR_DEFINITIONS = ( 350 | "DEBUG=1", 351 | "$(inherited)", 352 | ); 353 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 354 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 355 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 356 | GCC_WARN_UNDECLARED_SELECTOR = YES; 357 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 358 | GCC_WARN_UNUSED_FUNCTION = YES; 359 | GCC_WARN_UNUSED_VARIABLE = YES; 360 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 361 | MTL_ENABLE_DEBUG_INFO = YES; 362 | ONLY_ACTIVE_ARCH = YES; 363 | SDKROOT = iphoneos; 364 | }; 365 | name = Debug; 366 | }; 367 | 94CD916C1B3C6E5C00823D12 /* Release */ = { 368 | isa = XCBuildConfiguration; 369 | buildSettings = { 370 | ALWAYS_SEARCH_USER_PATHS = NO; 371 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 372 | CLANG_CXX_LIBRARY = "libc++"; 373 | CLANG_ENABLE_MODULES = YES; 374 | CLANG_ENABLE_OBJC_ARC = YES; 375 | CLANG_WARN_BOOL_CONVERSION = YES; 376 | CLANG_WARN_CONSTANT_CONVERSION = YES; 377 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 378 | CLANG_WARN_EMPTY_BODY = YES; 379 | CLANG_WARN_ENUM_CONVERSION = YES; 380 | CLANG_WARN_INT_CONVERSION = YES; 381 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 382 | CLANG_WARN_UNREACHABLE_CODE = YES; 383 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 384 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 385 | COPY_PHASE_STRIP = NO; 386 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 387 | ENABLE_NS_ASSERTIONS = NO; 388 | ENABLE_STRICT_OBJC_MSGSEND = YES; 389 | GCC_C_LANGUAGE_STANDARD = gnu99; 390 | GCC_NO_COMMON_BLOCKS = YES; 391 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 392 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 393 | GCC_WARN_UNDECLARED_SELECTOR = YES; 394 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 395 | GCC_WARN_UNUSED_FUNCTION = YES; 396 | GCC_WARN_UNUSED_VARIABLE = YES; 397 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 398 | MTL_ENABLE_DEBUG_INFO = NO; 399 | SDKROOT = iphoneos; 400 | VALIDATE_PRODUCT = YES; 401 | }; 402 | name = Release; 403 | }; 404 | 94CD916E1B3C6E5C00823D12 /* Debug */ = { 405 | isa = XCBuildConfiguration; 406 | buildSettings = { 407 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 408 | INFOPLIST_FILE = IDNFeedParser/Info.plist; 409 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 410 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 411 | PRODUCT_NAME = "$(TARGET_NAME)"; 412 | }; 413 | name = Debug; 414 | }; 415 | 94CD916F1B3C6E5C00823D12 /* Release */ = { 416 | isa = XCBuildConfiguration; 417 | buildSettings = { 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | INFOPLIST_FILE = IDNFeedParser/Info.plist; 420 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 421 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 422 | PRODUCT_NAME = "$(TARGET_NAME)"; 423 | }; 424 | name = Release; 425 | }; 426 | 94CD91711B3C6E5C00823D12 /* Debug */ = { 427 | isa = XCBuildConfiguration; 428 | buildSettings = { 429 | BUNDLE_LOADER = "$(TEST_HOST)"; 430 | FRAMEWORK_SEARCH_PATHS = ( 431 | "$(SDKROOT)/Developer/Library/Frameworks", 432 | "$(inherited)", 433 | ); 434 | GCC_PREPROCESSOR_DEFINITIONS = ( 435 | "DEBUG=1", 436 | "$(inherited)", 437 | ); 438 | INFOPLIST_FILE = IDNFeedParserTests/Info.plist; 439 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 440 | PRODUCT_NAME = "$(TARGET_NAME)"; 441 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IDNFeedParser.app/IDNFeedParser"; 442 | }; 443 | name = Debug; 444 | }; 445 | 94CD91721B3C6E5C00823D12 /* Release */ = { 446 | isa = XCBuildConfiguration; 447 | buildSettings = { 448 | BUNDLE_LOADER = "$(TEST_HOST)"; 449 | FRAMEWORK_SEARCH_PATHS = ( 450 | "$(SDKROOT)/Developer/Library/Frameworks", 451 | "$(inherited)", 452 | ); 453 | INFOPLIST_FILE = IDNFeedParserTests/Info.plist; 454 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 455 | PRODUCT_NAME = "$(TARGET_NAME)"; 456 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IDNFeedParser.app/IDNFeedParser"; 457 | }; 458 | name = Release; 459 | }; 460 | /* End XCBuildConfiguration section */ 461 | 462 | /* Begin XCConfigurationList section */ 463 | 94CD91451B3C6E5C00823D12 /* Build configuration list for PBXProject "IDNFeedParser" */ = { 464 | isa = XCConfigurationList; 465 | buildConfigurations = ( 466 | 94CD916B1B3C6E5C00823D12 /* Debug */, 467 | 94CD916C1B3C6E5C00823D12 /* Release */, 468 | ); 469 | defaultConfigurationIsVisible = 0; 470 | defaultConfigurationName = Release; 471 | }; 472 | 94CD916D1B3C6E5C00823D12 /* Build configuration list for PBXNativeTarget "IDNFeedParser" */ = { 473 | isa = XCConfigurationList; 474 | buildConfigurations = ( 475 | 94CD916E1B3C6E5C00823D12 /* Debug */, 476 | 94CD916F1B3C6E5C00823D12 /* Release */, 477 | ); 478 | defaultConfigurationIsVisible = 0; 479 | defaultConfigurationName = Release; 480 | }; 481 | 94CD91701B3C6E5C00823D12 /* Build configuration list for PBXNativeTarget "IDNFeedParserTests" */ = { 482 | isa = XCConfigurationList; 483 | buildConfigurations = ( 484 | 94CD91711B3C6E5C00823D12 /* Debug */, 485 | 94CD91721B3C6E5C00823D12 /* Release */, 486 | ); 487 | defaultConfigurationIsVisible = 0; 488 | defaultConfigurationName = Release; 489 | }; 490 | /* End XCConfigurationList section */ 491 | }; 492 | rootObject = 94CD91421B3C6E5C00823D12 /* Project object */; 493 | } 494 | -------------------------------------------------------------------------------- /IDNFeedParser.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /IDNFeedParser/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // IDNFeedParser 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /IDNFeedParser/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // IDNFeedParser 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | - (void)applicationWillResignActive:(UIApplication *)application { 24 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 25 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 26 | } 27 | 28 | - (void)applicationDidEnterBackground:(UIApplication *)application { 29 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 31 | } 32 | 33 | - (void)applicationWillEnterForeground:(UIApplication *)application { 34 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 35 | } 36 | 37 | - (void)applicationDidBecomeActive:(UIApplication *)application { 38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 39 | } 40 | 41 | - (void)applicationWillTerminate:(UIApplication *)application { 42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /IDNFeedParser/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /IDNFeedParser/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /IDNFeedParser/FeedInfosController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // IDNFeedParser 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface FeedInfosController : UITableViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /IDNFeedParser/FeedInfosController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // IDNFeedParser 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import "FeedInfosController.h" 10 | #import "IDNFeedParser.h" 11 | #import "UIViewController+IDNPrompt.h" 12 | #import "FeedItemsController.h" 13 | 14 | @interface FeedInfosController () 15 | 16 | @property(nonatomic,strong) NSMutableArray* feedInfos; 17 | 18 | @end 19 | 20 | @implementation FeedInfosController 21 | 22 | - (void)viewDidLoad { 23 | [super viewDidLoad]; 24 | self.title = @"订阅"; 25 | 26 | self.feedInfos = [NSMutableArray new]; 27 | 28 | [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"]; 29 | 30 | [self.navigationController prompting:@"正在读取RSS源信息"]; 31 | // 在后台线程下载解析RSS 32 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 33 | 34 | // 获取RSS源信息 35 | // IDNFeedInfo* info = [IDNFeedParser feedInfoWithUrl:@"http://www.zhihu.com/rss"]; 36 | IDNFeedInfo* info = [IDNFeedParser feedInfoWithUrl:@"http://news.baidu.com/n?cmd=1&class=civilnews&tn=rss"]; 37 | 38 | if(info==nil) //失败 39 | [self.navigationController prompt:@"读取RSS源信息失败" duration:2]; 40 | else //成功 41 | { 42 | [self.navigationController stopPrompt]; 43 | 44 | // 解析完成后在主线程更新显示 45 | dispatch_async(dispatch_get_main_queue(), ^{ 46 | [self addFeedInfo:info]; 47 | }); 48 | } 49 | }); 50 | } 51 | 52 | - (void)addFeedInfo:(IDNFeedInfo*)feedInfo 53 | { 54 | [self.feedInfos addObject:feedInfo]; 55 | [self.tableView reloadData]; 56 | } 57 | 58 | #pragma mark table view 59 | 60 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 61 | { 62 | return self.feedInfos.count; 63 | } 64 | 65 | - (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 66 | { 67 | UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath]; 68 | 69 | IDNFeedInfo* info = self.feedInfos[indexPath.row]; 70 | cell.textLabel.text = info.title; 71 | 72 | return cell; 73 | } 74 | 75 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 76 | { 77 | IDNFeedInfo* info = self.feedInfos[indexPath.row]; 78 | 79 | FeedItemsController* c = [FeedItemsController new]; 80 | [self.navigationController pushViewController:c animated:YES]; 81 | c.title = info.title; 82 | c.feedInfo = info; 83 | } 84 | 85 | @end 86 | -------------------------------------------------------------------------------- /IDNFeedParser/FeedItemsController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // IDNFeedParser 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "IDNFeedParser.h" 11 | 12 | @interface FeedItemsController : UITableViewController 13 | 14 | @property(nonatomic,strong) IDNFeedInfo* feedInfo; 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /IDNFeedParser/FeedItemsController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // IDNFeedParser 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import "FeedItemsController.h" 10 | #import "WebViewController.h" 11 | #import "UIViewController+IDNPrompt.h" 12 | 13 | @interface FeedItemsController () 14 | 15 | @property(nonatomic,strong) NSArray* feedItems; 16 | 17 | @end 18 | 19 | @implementation FeedItemsController 20 | 21 | - (void)viewDidLoad { 22 | [super viewDidLoad]; 23 | 24 | [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"]; 25 | 26 | } 27 | 28 | - (void)setFeedInfo:(IDNFeedInfo *)feedInfo 29 | { 30 | [self view]; //强制loadView 31 | 32 | _feedInfo = feedInfo; 33 | 34 | [self.navigationController prompting:@"正在获取文章列表"]; 35 | 36 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 37 | 38 | // 获取文章列表 39 | NSArray* items = [IDNFeedParser feedItemsWithUrl:feedInfo.url]; 40 | 41 | if(items==nil) //失败 42 | [self.navigationController prompt:@"获取文章列表失败" duration:2]; 43 | else //成功 44 | { 45 | [self.navigationController stopPrompt]; 46 | 47 | // 解析完成后在主线程更新显示 48 | dispatch_async(dispatch_get_main_queue(), ^{ 49 | self.feedItems = items; 50 | }); 51 | } 52 | }); 53 | } 54 | - (void)setFeedItems:(NSArray *)feedItems 55 | { 56 | _feedItems = feedItems; 57 | [self.tableView reloadData]; 58 | } 59 | 60 | #pragma mark table view 61 | 62 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 63 | { 64 | return self.feedItems.count; 65 | } 66 | 67 | - (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 68 | { 69 | UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath]; 70 | 71 | IDNFeedItem* item = self.feedItems[indexPath.row]; 72 | cell.textLabel.text = item.title; 73 | 74 | return cell; 75 | } 76 | 77 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 78 | { 79 | IDNFeedItem* item = self.feedItems[indexPath.row]; 80 | 81 | WebViewController* web = [WebViewController new]; 82 | web.url = item.link; 83 | web.title = item.title; 84 | [self.navigationController pushViewController:web animated:YES]; 85 | } 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /IDNFeedParser/IDNFeedParser/GTMNSString+HTML.h: -------------------------------------------------------------------------------- 1 | // 2 | // GTMNSString+HTML.h 3 | // Dealing with NSStrings that contain HTML 4 | // 5 | // Copyright 2006-2008 Google Inc. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 8 | // use this file except in compliance with the License. You may obtain a copy 9 | // of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | // License for the specific language governing permissions and limitations under 17 | // the License. 18 | // 19 | 20 | #import 21 | 22 | /// Utilities for NSStrings containing HTML 23 | @interface NSString (GTMNSStringHTMLAdditions) 24 | 25 | /// Get a string where internal characters that need escaping for HTML are escaped 26 | // 27 | /// For example, '&' become '&'. This will only cover characters from table 28 | /// A.2.2 of http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters 29 | /// which is what you want for a unicode encoded webpage. If you have a ascii 30 | /// or non-encoded webpage, please use stringByEscapingAsciiHTML which will 31 | /// encode all characters. 32 | /// 33 | /// For obvious reasons this call is only safe once. 34 | // 35 | // Returns: 36 | // Autoreleased NSString 37 | // 38 | - (NSString *)gtm_stringByEscapingForHTML; 39 | 40 | /// Get a string where internal characters that need escaping for HTML are escaped 41 | // 42 | /// For example, '&' become '&' 43 | /// All non-mapped characters (unicode that don't have a &keyword; mapping) 44 | /// will be converted to the appropriate &#xxx; value. If your webpage is 45 | /// unicode encoded (UTF16 or UTF8) use stringByEscapingHTML instead as it is 46 | /// faster, and produces less bloated and more readable HTML (as long as you 47 | /// are using a unicode compliant HTML reader). 48 | /// 49 | /// For obvious reasons this call is only safe once. 50 | // 51 | // Returns: 52 | // Autoreleased NSString 53 | // 54 | - (NSString *)gtm_stringByEscapingForAsciiHTML; 55 | 56 | /// Get a string where internal characters that are escaped for HTML are unescaped 57 | // 58 | /// For example, '&' becomes '&' 59 | /// Handles and 2 cases as well 60 | /// 61 | // Returns: 62 | // Autoreleased NSString 63 | // 64 | - (NSString *)gtm_stringByUnescapingFromHTML; 65 | 66 | @end -------------------------------------------------------------------------------- /IDNFeedParser/IDNFeedParser/GTMNSString+HTML.m: -------------------------------------------------------------------------------- 1 | // 2 | // GTMNSString+HTML.m 3 | // Dealing with NSStrings that contain HTML 4 | // 5 | // Copyright 2006-2008 Google Inc. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 8 | // use this file except in compliance with the License. You may obtain a copy 9 | // of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | // License for the specific language governing permissions and limitations under 17 | // the License. 18 | // 19 | 20 | //#import "GTMDefines.h" 21 | #import "GTMNSString+HTML.h" 22 | 23 | typedef struct { 24 | __unsafe_unretained NSString *escapeSequence; 25 | unichar uchar; 26 | } HTMLEscapeMap; 27 | 28 | // Taken from http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters 29 | // Ordered by uchar lowest to highest for bsearching 30 | static HTMLEscapeMap gAsciiHTMLEscapeMap[] = { 31 | // A.2.2. Special characters 32 | { @""", 34 }, 33 | { @"&", 38 }, 34 | { @"'", 39 }, 35 | { @"<", 60 }, 36 | { @">", 62 }, 37 | 38 | // A.2.1. Latin-1 characters 39 | { @" ", 160 }, 40 | { @"¡", 161 }, 41 | { @"¢", 162 }, 42 | { @"£", 163 }, 43 | { @"¤", 164 }, 44 | { @"¥", 165 }, 45 | { @"¦", 166 }, 46 | { @"§", 167 }, 47 | { @"¨", 168 }, 48 | { @"©", 169 }, 49 | { @"ª", 170 }, 50 | { @"«", 171 }, 51 | { @"¬", 172 }, 52 | { @"­", 173 }, 53 | { @"®", 174 }, 54 | { @"¯", 175 }, 55 | { @"°", 176 }, 56 | { @"±", 177 }, 57 | { @"²", 178 }, 58 | { @"³", 179 }, 59 | { @"´", 180 }, 60 | { @"µ", 181 }, 61 | { @"¶", 182 }, 62 | { @"·", 183 }, 63 | { @"¸", 184 }, 64 | { @"¹", 185 }, 65 | { @"º", 186 }, 66 | { @"»", 187 }, 67 | { @"¼", 188 }, 68 | { @"½", 189 }, 69 | { @"¾", 190 }, 70 | { @"¿", 191 }, 71 | { @"À", 192 }, 72 | { @"Á", 193 }, 73 | { @"Â", 194 }, 74 | { @"Ã", 195 }, 75 | { @"Ä", 196 }, 76 | { @"Å", 197 }, 77 | { @"Æ", 198 }, 78 | { @"Ç", 199 }, 79 | { @"È", 200 }, 80 | { @"É", 201 }, 81 | { @"Ê", 202 }, 82 | { @"Ë", 203 }, 83 | { @"Ì", 204 }, 84 | { @"Í", 205 }, 85 | { @"Î", 206 }, 86 | { @"Ï", 207 }, 87 | { @"Ð", 208 }, 88 | { @"Ñ", 209 }, 89 | { @"Ò", 210 }, 90 | { @"Ó", 211 }, 91 | { @"Ô", 212 }, 92 | { @"Õ", 213 }, 93 | { @"Ö", 214 }, 94 | { @"×", 215 }, 95 | { @"Ø", 216 }, 96 | { @"Ù", 217 }, 97 | { @"Ú", 218 }, 98 | { @"Û", 219 }, 99 | { @"Ü", 220 }, 100 | { @"Ý", 221 }, 101 | { @"Þ", 222 }, 102 | { @"ß", 223 }, 103 | { @"à", 224 }, 104 | { @"á", 225 }, 105 | { @"â", 226 }, 106 | { @"ã", 227 }, 107 | { @"ä", 228 }, 108 | { @"å", 229 }, 109 | { @"æ", 230 }, 110 | { @"ç", 231 }, 111 | { @"è", 232 }, 112 | { @"é", 233 }, 113 | { @"ê", 234 }, 114 | { @"ë", 235 }, 115 | { @"ì", 236 }, 116 | { @"í", 237 }, 117 | { @"î", 238 }, 118 | { @"ï", 239 }, 119 | { @"ð", 240 }, 120 | { @"ñ", 241 }, 121 | { @"ò", 242 }, 122 | { @"ó", 243 }, 123 | { @"ô", 244 }, 124 | { @"õ", 245 }, 125 | { @"ö", 246 }, 126 | { @"÷", 247 }, 127 | { @"ø", 248 }, 128 | { @"ù", 249 }, 129 | { @"ú", 250 }, 130 | { @"û", 251 }, 131 | { @"ü", 252 }, 132 | { @"ý", 253 }, 133 | { @"þ", 254 }, 134 | { @"ÿ", 255 }, 135 | 136 | // A.2.2. Special characters cont'd 137 | { @"Œ", 338 }, 138 | { @"œ", 339 }, 139 | { @"Š", 352 }, 140 | { @"š", 353 }, 141 | { @"Ÿ", 376 }, 142 | 143 | // A.2.3. Symbols 144 | { @"ƒ", 402 }, 145 | 146 | // A.2.2. Special characters cont'd 147 | { @"ˆ", 710 }, 148 | { @"˜", 732 }, 149 | 150 | // A.2.3. Symbols cont'd 151 | { @"Α", 913 }, 152 | { @"Β", 914 }, 153 | { @"Γ", 915 }, 154 | { @"Δ", 916 }, 155 | { @"Ε", 917 }, 156 | { @"Ζ", 918 }, 157 | { @"Η", 919 }, 158 | { @"Θ", 920 }, 159 | { @"Ι", 921 }, 160 | { @"Κ", 922 }, 161 | { @"Λ", 923 }, 162 | { @"Μ", 924 }, 163 | { @"Ν", 925 }, 164 | { @"Ξ", 926 }, 165 | { @"Ο", 927 }, 166 | { @"Π", 928 }, 167 | { @"Ρ", 929 }, 168 | { @"Σ", 931 }, 169 | { @"Τ", 932 }, 170 | { @"Υ", 933 }, 171 | { @"Φ", 934 }, 172 | { @"Χ", 935 }, 173 | { @"Ψ", 936 }, 174 | { @"Ω", 937 }, 175 | { @"α", 945 }, 176 | { @"β", 946 }, 177 | { @"γ", 947 }, 178 | { @"δ", 948 }, 179 | { @"ε", 949 }, 180 | { @"ζ", 950 }, 181 | { @"η", 951 }, 182 | { @"θ", 952 }, 183 | { @"ι", 953 }, 184 | { @"κ", 954 }, 185 | { @"λ", 955 }, 186 | { @"μ", 956 }, 187 | { @"ν", 957 }, 188 | { @"ξ", 958 }, 189 | { @"ο", 959 }, 190 | { @"π", 960 }, 191 | { @"ρ", 961 }, 192 | { @"ς", 962 }, 193 | { @"σ", 963 }, 194 | { @"τ", 964 }, 195 | { @"υ", 965 }, 196 | { @"φ", 966 }, 197 | { @"χ", 967 }, 198 | { @"ψ", 968 }, 199 | { @"ω", 969 }, 200 | { @"ϑ", 977 }, 201 | { @"ϒ", 978 }, 202 | { @"ϖ", 982 }, 203 | 204 | // A.2.2. Special characters cont'd 205 | { @" ", 8194 }, 206 | { @" ", 8195 }, 207 | { @" ", 8201 }, 208 | { @"‌", 8204 }, 209 | { @"‍", 8205 }, 210 | { @"‎", 8206 }, 211 | { @"‏", 8207 }, 212 | { @"–", 8211 }, 213 | { @"—", 8212 }, 214 | { @"‘", 8216 }, 215 | { @"’", 8217 }, 216 | { @"‚", 8218 }, 217 | { @"“", 8220 }, 218 | { @"”", 8221 }, 219 | { @"„", 8222 }, 220 | { @"†", 8224 }, 221 | { @"‡", 8225 }, 222 | // A.2.3. Symbols cont'd 223 | { @"•", 8226 }, 224 | { @"…", 8230 }, 225 | 226 | // A.2.2. Special characters cont'd 227 | { @"‰", 8240 }, 228 | 229 | // A.2.3. Symbols cont'd 230 | { @"′", 8242 }, 231 | { @"″", 8243 }, 232 | 233 | // A.2.2. Special characters cont'd 234 | { @"‹", 8249 }, 235 | { @"›", 8250 }, 236 | 237 | // A.2.3. Symbols cont'd 238 | { @"‾", 8254 }, 239 | { @"⁄", 8260 }, 240 | 241 | // A.2.2. Special characters cont'd 242 | { @"€", 8364 }, 243 | 244 | // A.2.3. Symbols cont'd 245 | { @"ℑ", 8465 }, 246 | { @"℘", 8472 }, 247 | { @"ℜ", 8476 }, 248 | { @"™", 8482 }, 249 | { @"ℵ", 8501 }, 250 | { @"←", 8592 }, 251 | { @"↑", 8593 }, 252 | { @"→", 8594 }, 253 | { @"↓", 8595 }, 254 | { @"↔", 8596 }, 255 | { @"↵", 8629 }, 256 | { @"⇐", 8656 }, 257 | { @"⇑", 8657 }, 258 | { @"⇒", 8658 }, 259 | { @"⇓", 8659 }, 260 | { @"⇔", 8660 }, 261 | { @"∀", 8704 }, 262 | { @"∂", 8706 }, 263 | { @"∃", 8707 }, 264 | { @"∅", 8709 }, 265 | { @"∇", 8711 }, 266 | { @"∈", 8712 }, 267 | { @"∉", 8713 }, 268 | { @"∋", 8715 }, 269 | { @"∏", 8719 }, 270 | { @"∑", 8721 }, 271 | { @"−", 8722 }, 272 | { @"∗", 8727 }, 273 | { @"√", 8730 }, 274 | { @"∝", 8733 }, 275 | { @"∞", 8734 }, 276 | { @"∠", 8736 }, 277 | { @"∧", 8743 }, 278 | { @"∨", 8744 }, 279 | { @"∩", 8745 }, 280 | { @"∪", 8746 }, 281 | { @"∫", 8747 }, 282 | { @"∴", 8756 }, 283 | { @"∼", 8764 }, 284 | { @"≅", 8773 }, 285 | { @"≈", 8776 }, 286 | { @"≠", 8800 }, 287 | { @"≡", 8801 }, 288 | { @"≤", 8804 }, 289 | { @"≥", 8805 }, 290 | { @"⊂", 8834 }, 291 | { @"⊃", 8835 }, 292 | { @"⊄", 8836 }, 293 | { @"⊆", 8838 }, 294 | { @"⊇", 8839 }, 295 | { @"⊕", 8853 }, 296 | { @"⊗", 8855 }, 297 | { @"⊥", 8869 }, 298 | { @"⋅", 8901 }, 299 | { @"⌈", 8968 }, 300 | { @"⌉", 8969 }, 301 | { @"⌊", 8970 }, 302 | { @"⌋", 8971 }, 303 | { @"⟨", 9001 }, 304 | { @"⟩", 9002 }, 305 | { @"◊", 9674 }, 306 | { @"♠", 9824 }, 307 | { @"♣", 9827 }, 308 | { @"♥", 9829 }, 309 | { @"♦", 9830 } 310 | }; 311 | 312 | // Taken from http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters 313 | // This is table A.2.2 Special Characters 314 | static HTMLEscapeMap gUnicodeHTMLEscapeMap[] = { 315 | // C0 Controls and Basic Latin 316 | { @""", 34 }, 317 | { @"&", 38 }, 318 | { @"'", 39 }, 319 | { @"<", 60 }, 320 | { @">", 62 }, 321 | 322 | // Latin Extended-A 323 | { @"Œ", 338 }, 324 | { @"œ", 339 }, 325 | { @"Š", 352 }, 326 | { @"š", 353 }, 327 | { @"Ÿ", 376 }, 328 | 329 | // Spacing Modifier Letters 330 | { @"ˆ", 710 }, 331 | { @"˜", 732 }, 332 | 333 | // General Punctuation 334 | { @" ", 8194 }, 335 | { @" ", 8195 }, 336 | { @" ", 8201 }, 337 | { @"‌", 8204 }, 338 | { @"‍", 8205 }, 339 | { @"‎", 8206 }, 340 | { @"‏", 8207 }, 341 | { @"–", 8211 }, 342 | { @"—", 8212 }, 343 | { @"‘", 8216 }, 344 | { @"’", 8217 }, 345 | { @"‚", 8218 }, 346 | { @"“", 8220 }, 347 | { @"”", 8221 }, 348 | { @"„", 8222 }, 349 | { @"†", 8224 }, 350 | { @"‡", 8225 }, 351 | { @"‰", 8240 }, 352 | { @"‹", 8249 }, 353 | { @"›", 8250 }, 354 | { @"€", 8364 }, 355 | }; 356 | 357 | 358 | // Utility function for Bsearching table above 359 | static int EscapeMapCompare(const void *ucharVoid, const void *mapVoid) { 360 | const unichar *uchar = (const unichar*)ucharVoid; 361 | const HTMLEscapeMap *map = (const HTMLEscapeMap*)mapVoid; 362 | int val; 363 | if (*uchar > map->uchar) { 364 | val = 1; 365 | } else if (*uchar < map->uchar) { 366 | val = -1; 367 | } else { 368 | val = 0; 369 | } 370 | return val; 371 | } 372 | 373 | @implementation NSString (GTMNSStringHTMLAdditions) 374 | 375 | - (NSString *)gtm_stringByEscapingHTMLUsingTable:(HTMLEscapeMap*)table 376 | ofSize:(NSUInteger)size 377 | escapingUnicode:(BOOL)escapeUnicode { 378 | NSUInteger length = [self length]; 379 | if (!length) { 380 | return self; 381 | } 382 | 383 | NSMutableString *finalString = [NSMutableString string]; 384 | NSMutableData *data2 = [NSMutableData dataWithCapacity:sizeof(unichar) * length]; 385 | 386 | // this block is common between GTMNSString+HTML and GTMNSString+XML but 387 | // it's so short that it isn't really worth trying to share. 388 | const unichar *buffer = CFStringGetCharactersPtr((CFStringRef)self); 389 | if (!buffer) { 390 | // We want this buffer to be autoreleased. 391 | NSMutableData *data = [NSMutableData dataWithLength:length * sizeof(UniChar)]; 392 | if (!data) { 393 | // COV_NF_START - Memory fail case 394 | // _GTMDevLog(@"couldn't alloc buffer"); 395 | return nil; 396 | // COV_NF_END 397 | } 398 | [self getCharacters:[data mutableBytes]]; 399 | buffer = [data bytes]; 400 | } 401 | 402 | if (!buffer || !data2) { 403 | // COV_NF_START 404 | // _GTMDevLog(@"Unable to allocate buffer or data2"); 405 | return nil; 406 | // COV_NF_END 407 | } 408 | 409 | unichar *buffer2 = (unichar *)[data2 mutableBytes]; 410 | 411 | NSUInteger buffer2Length = 0; 412 | 413 | for (NSUInteger i = 0; i < length; ++i) { 414 | HTMLEscapeMap *val = bsearch(&buffer[i], table, 415 | size / sizeof(HTMLEscapeMap), 416 | sizeof(HTMLEscapeMap), EscapeMapCompare); 417 | if (val || (escapeUnicode && buffer[i] > 127)) { 418 | if (buffer2Length) { 419 | CFStringAppendCharacters((CFMutableStringRef)finalString, 420 | buffer2, 421 | buffer2Length); 422 | buffer2Length = 0; 423 | } 424 | if (val) { 425 | [finalString appendString:val->escapeSequence]; 426 | } 427 | else { 428 | // _GTMDevAssert(escapeUnicode && buffer[i] > 127, @"Illegal Character"); 429 | [finalString appendFormat:@"&#%d;", buffer[i]]; 430 | } 431 | } else { 432 | buffer2[buffer2Length] = buffer[i]; 433 | buffer2Length += 1; 434 | } 435 | } 436 | if (buffer2Length) { 437 | CFStringAppendCharacters((CFMutableStringRef)finalString, 438 | buffer2, 439 | buffer2Length); 440 | } 441 | return finalString; 442 | } 443 | 444 | - (NSString *)gtm_stringByEscapingForHTML { 445 | return [self gtm_stringByEscapingHTMLUsingTable:gUnicodeHTMLEscapeMap 446 | ofSize:sizeof(gUnicodeHTMLEscapeMap) 447 | escapingUnicode:NO]; 448 | } // gtm_stringByEscapingHTML 449 | 450 | - (NSString *)gtm_stringByEscapingForAsciiHTML { 451 | return [self gtm_stringByEscapingHTMLUsingTable:gAsciiHTMLEscapeMap 452 | ofSize:sizeof(gAsciiHTMLEscapeMap) 453 | escapingUnicode:YES]; 454 | } // gtm_stringByEscapingAsciiHTML 455 | 456 | - (NSString *)gtm_stringByUnescapingFromHTML { 457 | NSRange range = NSMakeRange(0, [self length]); 458 | NSRange subrange = [self rangeOfString:@"&" options:NSBackwardsSearch range:range]; 459 | 460 | // if no ampersands, we've got a quick way out 461 | if (subrange.length == 0) return self; 462 | NSMutableString *finalString = [NSMutableString stringWithString:self]; 463 | do { 464 | NSRange semiColonRange = NSMakeRange(subrange.location, NSMaxRange(range) - subrange.location); 465 | semiColonRange = [self rangeOfString:@";" options:0 range:semiColonRange]; 466 | range = NSMakeRange(0, subrange.location); 467 | // if we don't find a semicolon in the range, we don't have a sequence 468 | if (semiColonRange.location == NSNotFound) { 469 | continue; 470 | } 471 | NSRange escapeRange = NSMakeRange(subrange.location, semiColonRange.location - subrange.location + 1); 472 | NSString *escapeString = [self substringWithRange:escapeRange]; 473 | NSUInteger length = [escapeString length]; 474 | // a squence must be longer than 3 (<) and less than 11 (ϑ) 475 | if (length > 3 && length < 11) { 476 | if ([escapeString characterAtIndex:1] == '#') { 477 | unichar char2 = [escapeString characterAtIndex:2]; 478 | if (char2 == 'x' || char2 == 'X') { 479 | // Hex escape squences £ 480 | NSString *hexSequence = [escapeString substringWithRange:NSMakeRange(3, length - 4)]; 481 | NSScanner *scanner = [NSScanner scannerWithString:hexSequence]; 482 | unsigned value; 483 | if ([scanner scanHexInt:&value] && 484 | value < USHRT_MAX && 485 | value > 0 486 | && [scanner scanLocation] == length - 4) { 487 | unichar uchar = value; 488 | NSString *charString = [NSString stringWithCharacters:&uchar length:1]; 489 | [finalString replaceCharactersInRange:escapeRange withString:charString]; 490 | } 491 | 492 | } else { 493 | // Decimal Sequences { 494 | NSString *numberSequence = [escapeString substringWithRange:NSMakeRange(2, length - 3)]; 495 | NSScanner *scanner = [NSScanner scannerWithString:numberSequence]; 496 | int value; 497 | if ([scanner scanInt:&value] && 498 | value < USHRT_MAX && 499 | value > 0 500 | && [scanner scanLocation] == length - 3) { 501 | unichar uchar = value; 502 | NSString *charString = [NSString stringWithCharacters:&uchar length:1]; 503 | [finalString replaceCharactersInRange:escapeRange withString:charString]; 504 | } 505 | } 506 | } else { 507 | // "standard" sequences 508 | for (unsigned i = 0; i < sizeof(gAsciiHTMLEscapeMap) / sizeof(HTMLEscapeMap); ++i) { 509 | if ([escapeString isEqualToString:gAsciiHTMLEscapeMap[i].escapeSequence]) { 510 | [finalString replaceCharactersInRange:escapeRange withString:[NSString stringWithCharacters:&gAsciiHTMLEscapeMap[i].uchar length:1]]; 511 | break; 512 | } 513 | } 514 | } 515 | } 516 | } while ((subrange = [self rangeOfString:@"&" options:NSBackwardsSearch range:range]).length != 0); 517 | return finalString; 518 | } // gtm_stringByUnescapingHTML 519 | 520 | 521 | 522 | @end -------------------------------------------------------------------------------- /IDNFeedParser/IDNFeedParser/IDNFeedParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // IDNFeedParser.h 3 | // IDNFramework 4 | // 5 | // Created by photondragon on 15/6/24. 6 | // Copyright (c) 2015年 iosdev.net. 7 | // 8 | 9 | /* 10 | ### *IDNFeedParser* 一个使用非常方便的Rss解析器 11 | 12 | 在[MWFeedParser](https://github.com/mwaterfall/MWFeedParser)的基础上修改。之所以要重新封装,是因为MWFeedParser的接口有点复杂,使用起来不太方便。 13 | 14 | 重新封装后的IDNFeedParser,**只需要一两行代码就可以完成RSS解析**,不用再写那么多delegate方法了。 15 | 16 | 主要做了以下改动: 17 | 18 | 1. 简化接口,去掉异步操作和delegate,只留下同步操作; 19 | 1. 把下载与解析分开(解耦),方便对RSS数据进行缓存。 20 | 1. 识别RSS频道的图像信息,为FeedInfo增加了image属性 21 | 1. 自动分析文章摘要信息,为FeedItem增加了image属性 22 | 1. 修复编码为中文时(如GB2313),识别出来的信息为乱码的问题 23 | 24 | 这个库使用起来非常简单,只要把IDNFeedParser目录下的源文件拷贝到你的项目中,然后`#import "IDNFeedParser.h"`就可以使用了 25 | 26 | 简单用法: 27 | 28 | // 获取RSS源信息 29 | IDNFeedInfo* info = [IDNFeedParser feedInfoWithUrl:@"http://www.zhihu.com/rss"]; 30 | // 获取文章列表 31 | NSArray* items = [IDNFeedParser feedItemsWithUrl:@"http://www.zhihu.com/rss"]; 32 | 33 | 下载和解析分离的用法: 34 | 35 | NSString* rssUrl = @"http://www.zhihu.com/rss"; 36 | // 获rss原始Data 37 | NSData* rssData = [IDNFeedParser dataFromUrl:rssUrl]; 38 | 39 | // 解析RSS源信息 40 | IDNFeedInfo* info = [IDNFeedParser feedInfoWithData:rssData fromUrl:rssUrl]; 41 | 42 | // 获取文章列表 43 | NSArray* items = [IDNFeedParser feedItemsWithData:rssData fromUrl:rssUrl]; 44 | 45 | 如果要在后台线程解析RSS,用GCD可以很方便地实现: 46 | 47 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 48 | 49 | // 获取文章列表 50 | NSArray* items = [IDNFeedParser feedItemsWithUrl:feedInfo.url]; 51 | 52 | if(items==nil) //失败 53 | NSLog(@"获取文章列表失败"); 54 | else //成功 55 | { 56 | // 解析完成后在主线程更新显示 57 | dispatch_async(dispatch_get_main_queue(), ^{ 58 | [self showFeedItems:items]; 59 | }); 60 | } 61 | }); 62 | 63 | #### 为什么接口只提供同步方法,不提供异步方法和delegate? 64 | 65 | 因为同步方法非常灵活,可以很方便地结合GCG或者NSOperation使用以实现异步操作; 66 | 而且很多开发者都有自己的多线程组件,如果用delegate方式返回数据,反而很难用。 67 | 最重要的是好处还是接口一目了然,**简单易用**! 68 | */ 69 | 70 | // Copyright (c) 2010 Michael Waterfall 71 | // 72 | // Permission is hereby granted, free of charge, to any person obtaining a copy 73 | // of this software and associated documentation files (the "Software"), to deal 74 | // in the Software without restriction, including without limitation the rights 75 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 76 | // copies of the Software, and to permit persons to whom the Software is 77 | // furnished to do so, subject to the following conditions: 78 | // 79 | // 1. The above copyright notice and this permission notice shall be included 80 | // in all copies or substantial portions of the Software. 81 | // 82 | // 2. This Software cannot be used to archive or collect data such as (but not 83 | // limited to) that of events, news, experiences and activities, for the 84 | // purpose of any concept relating to diary/journal keeping. 85 | // 86 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 87 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 88 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 89 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 90 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 91 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 92 | // THE SOFTWARE. 93 | // 94 | 95 | #import 96 | 97 | @class IDNFeedInfo,IDNFeedItem; 98 | 99 | //RSS解析器 100 | @interface IDNFeedParser : NSObject 101 | 102 | /// 获取RSS源信息。出错返回nil 103 | + (IDNFeedInfo*)feedInfoWithUrl:(NSString*)url; 104 | /// 获取RSS文章列表。出错返回nil 105 | + (NSArray*)feedItemsWithUrl:(NSString*)url; // 返回IDNFeedItem对象的数组 106 | 107 | /** 108 | 从指定URL下载RSS数据,如果不是UTF8编码,会转换为UTF8编码的数据。 109 | 内部调用了+[IDNFeedParser utf8DataFromData:textEncodingName:] 110 | */ 111 | + (NSData*)dataFromUrl:(NSString*)url; 112 | /// 获取RSS源信息。出错返回nil 113 | + (IDNFeedInfo*)feedInfoWithData:(NSData *)data fromUrl:(NSString*)url; 114 | /// 获取RSS文章列表。出错返回nil 115 | + (NSArray*)feedItemsWithData:(NSData*)data fromUrl:(NSString*)url; // 返回IDNFeedItem对象的数组 116 | 117 | /** 118 | 辅助方法 119 | 将网络上下载的原始RSS数据转为utf8编码的数据 120 | @param data 待转换的RSS xml数据 121 | @param textEncodingName data的字符编码,来自NSURLResponse.textEncodingName属性 122 | */ 123 | + (NSData*)utf8DataFromData:(NSData*)data textEncodingName:(NSString*)textEncodingName; 124 | 125 | @end 126 | 127 | /// RSS源信息 128 | @interface IDNFeedInfo : NSObject 129 | 130 | 131 | @property(nonatomic,copy) NSString* title; 132 | @property(nonatomic,copy) NSString* link; 133 | @property(nonatomic,copy) NSString* summary; 134 | @property(nonatomic,copy) NSString* url; 135 | @property(nonatomic,copy) NSString* image; 136 | 137 | @end 138 | 139 | /// RSS文章信息 140 | @interface IDNFeedItem : NSObject 141 | 142 | 143 | @property (nonatomic, copy) NSString* identifier; 144 | @property (nonatomic, copy) NSString* title; 145 | @property (nonatomic, copy) NSString* link; 146 | @property (nonatomic, copy) NSString* image; 147 | @property (nonatomic, copy) NSDate* date; 148 | @property (nonatomic, copy) NSDate* updated; 149 | @property (nonatomic, copy) NSString* summary; 150 | @property (nonatomic, copy) NSString* content; 151 | @property (nonatomic, copy) NSString* author; 152 | @property (nonatomic, copy) NSArray* enclosures; 153 | 154 | @end 155 | 156 | -------------------------------------------------------------------------------- /IDNFeedParser/IDNFeedParser/IDNFeedParser.m: -------------------------------------------------------------------------------- 1 | // 2 | // IDNFeedParser.m 3 | // IDNFramework 4 | // 5 | // Created by photondragon on 15/6/24. 6 | // Copyright (c) 2015年 iosdev.net. 7 | // 8 | 9 | #import "IDNFeedParser.h" 10 | #import "NSString+HTML.h" 11 | #import "NSDate+InternetDateTime.h" 12 | 13 | // Debug Logging 14 | #if 0 // Set to 1 to enable debug logging 15 | #define MWLog(x, ...) NSLog(x, ## __VA_ARGS__); 16 | #else 17 | #define MWLog(x, ...) 18 | #endif 19 | 20 | // NSXMLParser Logging 21 | #if 0 // Set to 1 to enable XML parsing logs 22 | #define MWXMLLog(x, ...) NSLog(x, ## __VA_ARGS__); 23 | #else 24 | #define MWXMLLog(x, ...) 25 | #endif 26 | 27 | // Empty XHTML elements ( in http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd ) 28 | #define ELEMENT_IS_EMPTY(e) ([e isEqualToString:@"br"] || [e isEqualToString:@"img"] || [e isEqualToString:@"input"] || \ 29 | [e isEqualToString:@"hr"] || [e isEqualToString:@"link"] || [e isEqualToString:@"base"] || \ 30 | [e isEqualToString:@"basefont"] || [e isEqualToString:@"frame"] || [e isEqualToString:@"meta"] || \ 31 | [e isEqualToString:@"area"] || [e isEqualToString:@"col"] || [e isEqualToString:@"param"]) 32 | 33 | #define MWErrorDomain @"MWFeedParser" 34 | #define MWErrorCodeNotInitiated 1 /* MWFeedParser not initialised correctly */ 35 | #define MWErrorCodeConnectionFailed 2 /* Connection to the URL failed */ 36 | #define MWErrorCodeFeedParsingError 3 /* NSXMLParser encountered a parsing error */ 37 | #define MWErrorCodeFeedValidationError 4 /* NSXMLParser encountered a validation error */ 38 | #define MWErrorCodeGeneral 5 /* MWFeedParser general error */ 39 | 40 | typedef enum { ParseTypeFull, ParseTypeItemsOnly, ParseTypeInfoOnly } ParseType; 41 | typedef enum { FeedTypeUnknown, FeedTypeRSS, FeedTypeRSS1, FeedTypeAtom } FeedType; 42 | 43 | @interface IDNFeedParser() 44 | 45 | @property(nonatomic,strong,readonly) NSString* standardUrl; //标准化URL 46 | @property(nonatomic,strong) IDNFeedInfo* feedInfo; 47 | @property(nonatomic,strong) NSArray* feedItems; 48 | @property(nonatomic) ParseType feedParseType; 49 | @end 50 | 51 | @implementation IDNFeedParser 52 | { 53 | NSString *asyncTextEncodingName; 54 | 55 | NSXMLParser *feedParser; 56 | FeedType feedType; 57 | NSDateFormatter *dateFormatterRFC822, *dateFormatterRFC3339; 58 | 59 | BOOL aborted; //是否中止。只有当feedParseType==ParseTypeInfoOnly时,才会中止 60 | NSError* error; //解析过程中产生的错误。如果非nil,则解析一定会中止 61 | 62 | BOOL hasEncounteredItems; // Whether the parser has started parsing items 63 | 64 | // Parsing of XML structure as content 65 | NSString *pathOfElementWithXHTMLType; // Hold the path of the element who's type="xhtml" so we can stop parsing when it's ended 66 | BOOL parseStructureAsContent; // For atom feeds when element type="xhtml" 67 | 68 | // Parsing Data 69 | NSString *currentPath; 70 | NSMutableString *currentText; 71 | NSDictionary *currentElementAttributes; 72 | IDNFeedInfo *parsingInfo; 73 | IDNFeedItem *parsingItem; 74 | NSMutableArray* itemsContainer; 75 | } 76 | 77 | #pragma mark NSObject 78 | 79 | - (instancetype)init 80 | { 81 | if ((self = [super init])) { 82 | 83 | // Defaults 84 | _feedParseType = ParseTypeFull; 85 | 86 | // Date Formatters 87 | // Good info on internet dates here: http://developer.apple.com/iphone/library/qa/qa2010/qa1480.html 88 | NSLocale *en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; 89 | dateFormatterRFC822 = [[NSDateFormatter alloc] init]; 90 | dateFormatterRFC3339 = [[NSDateFormatter alloc] init]; 91 | [dateFormatterRFC822 setLocale:en_US_POSIX]; 92 | [dateFormatterRFC3339 setLocale:en_US_POSIX]; 93 | [dateFormatterRFC822 setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; 94 | [dateFormatterRFC3339 setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; 95 | 96 | } 97 | return self; 98 | } 99 | 100 | #pragma mark Parsing 101 | 102 | // Reset data variables before processing 103 | // Exclude parse state variables as they are needed after parse 104 | - (void)reset { 105 | feedType = FeedTypeUnknown; 106 | currentPath = @"/"; 107 | currentText = [[NSMutableString alloc] init]; 108 | parsingItem = nil; 109 | parsingInfo = nil; 110 | currentElementAttributes = nil; 111 | parseStructureAsContent = NO; 112 | pathOfElementWithXHTMLType = nil; 113 | hasEncounteredItems = NO; 114 | } 115 | 116 | + (NSData*)dataFromUrl:(NSString*)url 117 | { 118 | NSURL* nsurl = [NSURL URLWithString:url]; 119 | if(nsurl==nil) 120 | return nil; 121 | 122 | NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsurl]; 123 | request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData; 124 | request.timeoutInterval = 30.0; 125 | request.HTTPMethod = @"GET"; 126 | NSError* netError = nil; 127 | NSHTTPURLResponse* response; 128 | NSData* data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&netError]; 129 | if(data==nil) 130 | { 131 | return nil; 132 | } 133 | data = [IDNFeedParser utf8DataFromData:data textEncodingName:response.textEncodingName]; 134 | return data; 135 | } 136 | 137 | + (IDNFeedInfo*)feedInfoWithUrl:(NSString*)url 138 | { 139 | NSData* data = [self dataFromUrl:url]; 140 | return [self feedInfoWithData:data fromUrl:url]; 141 | } 142 | + (IDNFeedInfo*)feedInfoWithData:(NSData *)data fromUrl:(NSString*)url 143 | { 144 | if(data==nil) 145 | return nil; 146 | 147 | IDNFeedParser* parser = [IDNFeedParser new]; 148 | parser.feedParseType = ParseTypeInfoOnly; 149 | [parser parseData:data fromUrl:url]; 150 | return parser.feedInfo; 151 | } 152 | 153 | + (NSArray*)feedItemsWithUrl:(NSString*)url 154 | { 155 | NSData* data = [self dataFromUrl:url]; 156 | return [self feedItemsWithData:data fromUrl:url]; 157 | } 158 | + (NSArray*)feedItemsWithData:(NSData*)data fromUrl:(NSString*)url; // 返回IDNFeedItem对象的数组 159 | { 160 | if(data==nil) 161 | return nil; 162 | 163 | IDNFeedParser* parser = [IDNFeedParser new]; 164 | parser.feedParseType = ParseTypeItemsOnly; 165 | [parser parseData:data fromUrl:url]; 166 | return parser.feedItems; 167 | } 168 | 169 | + (NSData*)utf8DataFromData:(NSData*)data textEncodingName:(NSString*)textEncodingName 170 | { 171 | if(textEncodingName==nil) //检测xml文件头中包含的字符编码。例如: 172 | { 173 | NSData* dataHeader; 174 | if(data.length>1024) 175 | dataHeader = [data subdataWithRange:NSMakeRange(0, 1024)]; 176 | else 177 | dataHeader = data; 178 | NSString* dataHeaderString = [[NSString alloc] initWithData:dataHeader encoding:NSMacOSRomanStringEncoding]; 179 | if ([dataHeaderString hasPrefix:@""]; 181 | if (a.location != NSNotFound) { 182 | NSString *xmlHeader = [dataHeaderString substringToIndex:a.location]; 183 | NSRange b = [xmlHeader rangeOfString:@"encoding=\""]; 184 | if (b.location != NSNotFound) { 185 | NSUInteger s = b.location+b.length; 186 | NSRange c = [xmlHeader rangeOfString:@"\"" options:0 range:NSMakeRange(s, [xmlHeader length] - s)]; 187 | if (c.location != NSNotFound) { 188 | textEncodingName = [xmlHeader substringWithRange:NSMakeRange(b.location+b.length,c.location-b.location-b.length)]; 189 | } 190 | } 191 | } 192 | } 193 | } 194 | // Check whether it's UTF-8 195 | if (![[textEncodingName lowercaseString] isEqualToString:@"utf-8"]) { 196 | 197 | // Not UTF-8 so convert 198 | MWLog(@"MWFeedParser: XML document was not UTF-8 so we're converting it"); 199 | NSString *string = nil; 200 | 201 | // Attempt to detect encoding from response header 202 | NSStringEncoding nsEncoding = 0; 203 | 204 | if (textEncodingName) { 205 | if([textEncodingName rangeOfString:@"gb2312" options:NSCaseInsensitiveSearch].location!=NSNotFound) 206 | { 207 | nsEncoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000); 208 | } 209 | else if([textEncodingName rangeOfString:@"gb18030" options:NSCaseInsensitiveSearch].location!=NSNotFound) 210 | { 211 | nsEncoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000); 212 | } 213 | else if([textEncodingName rangeOfString:@"gbk" options:NSCaseInsensitiveSearch].location!=NSNotFound) 214 | { 215 | nsEncoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGBK_95); 216 | } 217 | else 218 | { 219 | CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)textEncodingName); 220 | if (cfEncoding != kCFStringEncodingInvalidId) 221 | nsEncoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); 222 | } 223 | 224 | if (nsEncoding != 0) 225 | string = [[NSString alloc] initWithData:data encoding:nsEncoding]; 226 | } 227 | 228 | // If that failed then make our own attempts 229 | if (!string) { 230 | // http://www.mikeash.com/pyblog/friday-qa-2010-02-19-character-encodings.html 231 | string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 232 | if (!string) string = [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding]; 233 | if (!string) string = [[NSString alloc] initWithData:data encoding:NSMacOSRomanStringEncoding]; 234 | } 235 | 236 | // Nil data 237 | data = nil; 238 | 239 | // Parse 240 | if (string) { 241 | 242 | // Set XML encoding to UTF-8 243 | if ([string hasPrefix:@""]; 245 | if (a.location != NSNotFound) { 246 | NSString *xmlDec = [string substringToIndex:a.location]; 247 | if ([xmlDec rangeOfString:@"encoding=\"UTF-8\"" 248 | options:NSCaseInsensitiveSearch].location == NSNotFound) { 249 | NSRange b = [xmlDec rangeOfString:@"encoding=\""]; 250 | if (b.location != NSNotFound) { 251 | NSUInteger s = b.location+b.length; 252 | NSRange c = [xmlDec rangeOfString:@"\"" options:0 range:NSMakeRange(s, [xmlDec length] - s)]; 253 | if (c.location != NSNotFound) { 254 | NSString *temp = [string stringByReplacingCharactersInRange:NSMakeRange(b.location,c.location+c.length-b.location) 255 | withString:@"encoding=\"UTF-8\""]; 256 | string = temp; 257 | } 258 | } 259 | } 260 | } 261 | } 262 | 263 | // Convert string to UTF-8 data 264 | if (string) { 265 | data = [string dataUsingEncoding:NSUTF8StringEncoding]; 266 | } 267 | } 268 | } 269 | return data; 270 | } 271 | 272 | - (void)parseData:(NSData *)data fromUrl:(NSString*)url 273 | { 274 | if(data.length==0 || url.length==0) 275 | return; 276 | if(feedParser)//正在解析 277 | return; 278 | 279 | [self reset]; 280 | error = nil; 281 | aborted = NO; 282 | 283 | if(data==nil) 284 | { 285 | [self parsingFailedWithErrorCode:MWErrorCodeFeedParsingError andDescription:@"Error with feed encoding"]; 286 | return; 287 | } 288 | 289 | feedParser = [[NSXMLParser alloc] initWithData:data]; 290 | if(feedParser==nil) 291 | { 292 | [self parsingFailedWithErrorCode:MWErrorCodeFeedParsingError andDescription:@"Feed not a valid XML document"]; 293 | return; 294 | } 295 | 296 | self.standardUrl = url; 297 | parsingInfo = [[IDNFeedInfo alloc] init]; 298 | parsingInfo.url = self.standardUrl; 299 | 300 | itemsContainer = [NSMutableArray new]; 301 | 302 | feedParser.delegate = self; 303 | [feedParser setShouldProcessNamespaces:YES]; 304 | [feedParser parse]; 305 | feedParser = nil; // Release after parse 306 | 307 | self.feedInfo = parsingInfo; 308 | if(itemsContainer.count>0) 309 | { 310 | if(error) 311 | NSLog(@"解析出了%d个items,但是遇到了错误:\n%@", (int)itemsContainer.count, error); 312 | for(IDNFeedItem* item in itemsContainer) 313 | { 314 | item.image = [IDNFeedParser findImageUrlInContent:item.summary]; 315 | } 316 | self.feedItems = [itemsContainer copy]; 317 | } 318 | else 319 | { 320 | if(error) 321 | self.feedItems = nil; 322 | } 323 | itemsContainer = nil; 324 | } 325 | 326 | static NSRegularExpression* regex = nil; 327 | 328 | + (NSString*)findImageUrlInContent:(NSString*)content 329 | { 330 | if(content.length==0) 331 | return nil; 332 | 333 | if(regex==nil) 334 | regex = [NSRegularExpression regularExpressionWithPattern:@"()" options:0 error:nil]; 335 | 336 | NSTextCheckingResult *match = [regex firstMatchInString:content options:0 range:NSMakeRange(0, [content length])]; 337 | 338 | if (match == NULL ) 339 | return nil; 340 | return [content substringWithRange:[match rangeAtIndex:2]]; 341 | } 342 | 343 | // If an error occurs, create NSError and inform delegate 344 | - (void)parsingFailedWithErrorCode:(int)code andDescription:(NSString *)description { 345 | if(error) //如果有错,则解析已经中止 346 | return; 347 | error = [NSError errorWithDomain:MWErrorDomain code:code 348 | userInfo:[NSDictionary dictionaryWithObject:description 349 | forKey:NSLocalizedDescriptionKey]]; 350 | MWLog(@"%@", error); 351 | 352 | if (feedParser) 353 | [feedParser abortParsing]; 354 | 355 | [self reset]; 356 | } 357 | 358 | #pragma mark XML Parsing 359 | 360 | - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI 361 | qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict { 362 | MWXMLLog(@"NSXMLParser: didStartElement: %@", qualifiedName); 363 | @autoreleasepool { 364 | 365 | // Adjust path 366 | currentPath = [currentPath stringByAppendingPathComponent:qualifiedName]; 367 | currentElementAttributes = attributeDict; 368 | 369 | // Parse content as structure (Atom feeds with element type="xhtml") 370 | // - Use elementName not qualifiedName to ignore XML namespaces for XHTML entities 371 | if (parseStructureAsContent) { 372 | 373 | // Open XHTML tag 374 | [currentText appendFormat:@"<%@", elementName]; 375 | 376 | // Add attributes 377 | for (NSString *key in attributeDict) { 378 | [currentText appendFormat:@" %@=\"%@\"", key, 379 | [[attributeDict objectForKey:key] stringByEncodingHTMLEntities]]; 380 | } 381 | 382 | // End tag or close 383 | if (ELEMENT_IS_EMPTY(elementName)) { 384 | [currentText appendString:@" />"]; 385 | } else { 386 | [currentText appendString:@">"]; 387 | } 388 | 389 | // Dont continue 390 | return; 391 | 392 | } 393 | 394 | // Reset 395 | [currentText setString:@""]; 396 | 397 | // Determine feed type 398 | if (feedType == FeedTypeUnknown) { 399 | if ([qualifiedName isEqualToString:@"rss"]) feedType = FeedTypeRSS; 400 | else if ([qualifiedName isEqualToString:@"rdf:RDF"]) feedType = FeedTypeRSS1; 401 | else if ([qualifiedName isEqualToString:@"feed"]) feedType = FeedTypeAtom; 402 | else { 403 | 404 | // Invalid format so fail 405 | [self parsingFailedWithErrorCode:MWErrorCodeFeedParsingError 406 | andDescription:@"XML document is not a valid web feed document."]; 407 | 408 | } 409 | return; 410 | } 411 | 412 | // Entering new feed element 413 | if (_feedParseType != ParseTypeItemsOnly) { 414 | if ((feedType == FeedTypeRSS && [currentPath isEqualToString:@"/rss/channel"]) || 415 | (feedType == FeedTypeRSS1 && [currentPath isEqualToString:@"/rdf:RDF/channel"]) || 416 | (feedType == FeedTypeAtom && [currentPath isEqualToString:@"/feed"])) { 417 | return; 418 | } 419 | } 420 | 421 | // Entering new item element 422 | if ((feedType == FeedTypeRSS && [currentPath isEqualToString:@"/rss/channel/item"]) || 423 | (feedType == FeedTypeRSS1 && [currentPath isEqualToString:@"/rdf:RDF/item"]) || 424 | (feedType == FeedTypeAtom && [currentPath isEqualToString:@"/feed/entry"])) { 425 | 426 | // Send off feed info to delegate 427 | if (!hasEncounteredItems) { 428 | hasEncounteredItems = YES; 429 | if (_feedParseType != ParseTypeItemsOnly) { // Check whether to ignore feed info 430 | 431 | // Dispatch feed info to delegate 432 | [self dispatchFeedInfoToDelegate]; 433 | 434 | // Stop parsing if only requiring meta data 435 | if (_feedParseType == ParseTypeInfoOnly) { 436 | 437 | // Debug log 438 | MWLog(@"MWFeedParser: Parse type is ParseTypeInfoOnly so finishing here"); 439 | 440 | // Finish 441 | aborted = YES; 442 | [parser abortParsing]; 443 | return; 444 | 445 | } 446 | 447 | } else { 448 | 449 | // Ignoring feed info so debug log 450 | MWLog(@"MWFeedParser: Parse type is ParseTypeItemsOnly so ignoring feed info"); 451 | 452 | } 453 | } 454 | 455 | // New item 456 | parsingItem = [[IDNFeedItem alloc] init]; 457 | 458 | // Return 459 | return; 460 | } 461 | 462 | // Check if entering into an Atom content tag with type "xhtml" 463 | // If type is "xhtml" then it can contain child elements and structure needs 464 | // to be parsed as content 465 | // See: http://www.atomenabled.org/developers/syndication/atom-format-spec.php#rfc.section.3.1.1 466 | if (feedType == FeedTypeAtom) { 467 | 468 | // Check type attribute 469 | NSString *typeAttribute = [attributeDict objectForKey:@"type"]; 470 | if (typeAttribute && [typeAttribute isEqualToString:@"xhtml"]) { 471 | 472 | // Start parsing structure as content 473 | parseStructureAsContent = YES; 474 | 475 | // Remember path so we can stop parsing structure when element ends 476 | pathOfElementWithXHTMLType = currentPath; 477 | 478 | } 479 | 480 | } 481 | 482 | } 483 | 484 | } 485 | 486 | - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName 487 | namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { 488 | MWXMLLog(@"NSXMLParser: didEndElement: %@", qName); 489 | @autoreleasepool { 490 | 491 | // Parse content as structure (Atom feeds with element type="xhtml") 492 | // - Use elementName not qualifiedName to ignore XML namespaces for XHTML entities 493 | if (parseStructureAsContent) { 494 | 495 | // Check for finishing parsing structure as content 496 | if (currentPath.length > pathOfElementWithXHTMLType.length) { 497 | 498 | // Close XHTML tag unless it is an empty element 499 | if (!ELEMENT_IS_EMPTY(elementName)) [currentText appendFormat:@"", elementName]; 500 | 501 | // Adjust path & don't continue 502 | currentPath = [currentPath stringByDeletingLastPathComponent]; 503 | 504 | // Return 505 | return; 506 | 507 | } 508 | 509 | // Finish 510 | parseStructureAsContent = NO; 511 | pathOfElementWithXHTMLType = nil; 512 | 513 | // Continue... 514 | 515 | } 516 | 517 | // Store data 518 | BOOL processed = NO; 519 | if (currentText) { 520 | 521 | // Remove newlines and whitespace from currentText 522 | NSString *processedText = [currentText stringByRemovingNewLinesAndWhitespace]; 523 | 524 | // Process 525 | switch (feedType) { 526 | case FeedTypeRSS: { 527 | 528 | // Specifications 529 | // http://cyber.law.harvard.edu/rss/index.html 530 | // http://web.resource.org/rss/1.0/modules/dc/ Dublin core also seems to be used for RSS 2 as well 531 | 532 | // Item 533 | if (!processed) { 534 | if ([currentPath isEqualToString:@"/rss/channel/item/title"]) { if (processedText.length > 0) parsingItem.title = processedText; processed = YES; } 535 | else if ([currentPath isEqualToString:@"/rss/channel/item/link"]) { if (processedText.length > 0) parsingItem.link = processedText; processed = YES; } 536 | else if ([currentPath isEqualToString:@"/rss/channel/item/author"]) { if (processedText.length > 0) parsingItem.author = processedText; processed = YES; } 537 | else if ([currentPath isEqualToString:@"/rss/channel/item/dc:creator"]) { if (processedText.length > 0) parsingItem.author = processedText; processed = YES; } 538 | else if ([currentPath isEqualToString:@"/rss/channel/item/guid"]) { if (processedText.length > 0) parsingItem.identifier = processedText; processed = YES; } 539 | else if ([currentPath isEqualToString:@"/rss/channel/item/description"]) { if (processedText.length > 0) parsingItem.summary = processedText; processed = YES; } 540 | else if ([currentPath isEqualToString:@"/rss/channel/item/content:encoded"]) { if (processedText.length > 0) parsingItem.content = processedText; processed = YES; } 541 | else if ([currentPath isEqualToString:@"/rss/channel/item/pubDate"]) { if (processedText.length > 0) parsingItem.date = [NSDate dateFromInternetDateTimeString:processedText formatHint:DateFormatHintRFC822]; processed = YES; } 542 | else if ([currentPath isEqualToString:@"/rss/channel/item/enclosure"]) { [self createEnclosureFromAttributes:currentElementAttributes andAddToItem:parsingItem]; processed = YES; } 543 | else if ([currentPath isEqualToString:@"/rss/channel/item/dc:date"]) { if (processedText.length > 0) parsingItem.date = [NSDate dateFromInternetDateTimeString:processedText formatHint:DateFormatHintRFC3339]; processed = YES; } 544 | } 545 | 546 | // Info 547 | if (!processed && _feedParseType != ParseTypeItemsOnly) { 548 | if ([currentPath isEqualToString:@"/rss/channel/title"]) { if (processedText.length > 0) parsingInfo.title = processedText; processed = YES; } 549 | else if ([currentPath isEqualToString:@"/rss/channel/description"]) { if (processedText.length > 0) parsingInfo.summary = processedText; processed = YES; } 550 | else if ([currentPath isEqualToString:@"/rss/channel/link"]) { if (processedText.length > 0) parsingInfo.link = processedText; processed = YES; } 551 | else if ([currentPath isEqualToString:@"/rss/channel/image/url"]) { if (processedText.length > 0) parsingInfo.image = processedText; processed = YES; } 552 | } 553 | 554 | break; 555 | } 556 | case FeedTypeRSS1: { 557 | 558 | // Specifications 559 | // http://web.resource.org/rss/1.0/spec 560 | // http://web.resource.org/rss/1.0/modules/dc/ 561 | 562 | // Item 563 | if (!processed) { 564 | if ([currentPath isEqualToString:@"/rdf:RDF/item/title"]) { if (processedText.length > 0) parsingItem.title = processedText; processed = YES; } 565 | else if ([currentPath isEqualToString:@"/rdf:RDF/item/link"]) { if (processedText.length > 0) parsingItem.link = processedText; processed = YES; } 566 | else if ([currentPath isEqualToString:@"/rdf:RDF/item/description"]) { if (processedText.length > 0) parsingItem.summary = processedText; processed = YES; } 567 | else if ([currentPath isEqualToString:@"/rdf:RDF/item/content:encoded"]) { if (processedText.length > 0) parsingItem.content = processedText; processed = YES; } 568 | else if ([currentPath isEqualToString:@"/rdf:RDF/item/dc:identifier"]) { if (processedText.length > 0) parsingItem.identifier = processedText; processed = YES; } 569 | else if ([currentPath isEqualToString:@"/rdf:RDF/item/dc:creator"]) { if (processedText.length > 0) parsingItem.author = processedText; processed = YES; } 570 | else if ([currentPath isEqualToString:@"/rdf:RDF/item/dc:date"]) { if (processedText.length > 0) parsingItem.date = [NSDate dateFromInternetDateTimeString:processedText formatHint:DateFormatHintRFC3339]; processed = YES; } 571 | else if ([currentPath isEqualToString:@"/rdf:RDF/item/enc:enclosure"]) { [self createEnclosureFromAttributes:currentElementAttributes andAddToItem:parsingItem]; processed = YES; } 572 | } 573 | 574 | // Info 575 | if (!processed && _feedParseType != ParseTypeItemsOnly) { 576 | if ([currentPath isEqualToString:@"/rdf:RDF/channel/title"]) { if (processedText.length > 0) parsingInfo.title = processedText; processed = YES; } 577 | else if ([currentPath isEqualToString:@"/rdf:RDF/channel/description"]) { if (processedText.length > 0) parsingInfo.summary = processedText; processed = YES; } 578 | else if ([currentPath isEqualToString:@"/rdf:RDF/channel/link"]) { if (processedText.length > 0) parsingInfo.link = processedText; processed = YES; } 579 | } 580 | 581 | break; 582 | } 583 | case FeedTypeAtom: { 584 | 585 | // Specifications 586 | // http://www.ietf.org/rfc/rfc4287.txt 587 | // http://www.intertwingly.net/wiki/pie/DublinCore 588 | 589 | // Item 590 | if (!processed) { 591 | if ([currentPath isEqualToString:@"/feed/entry/title"]) { if (processedText.length > 0) parsingItem.title = processedText; processed = YES; } 592 | else if ([currentPath isEqualToString:@"/feed/entry/link"]) { [self processAtomLink:currentElementAttributes andAddToMWObject:parsingItem]; processed = YES; } 593 | else if ([currentPath isEqualToString:@"/feed/entry/id"]) { if (processedText.length > 0) parsingItem.identifier = processedText; processed = YES; } 594 | else if ([currentPath isEqualToString:@"/feed/entry/summary"]) { if (processedText.length > 0) parsingItem.summary = processedText; processed = YES; } 595 | else if ([currentPath isEqualToString:@"/feed/entry/content"]) { if (processedText.length > 0) parsingItem.content = processedText; processed = YES; } 596 | else if ([currentPath isEqualToString:@"/feed/entry/author/name"]) { if (processedText.length > 0) parsingItem.author = processedText; processed = YES; } 597 | else if ([currentPath isEqualToString:@"/feed/entry/dc:creator"]) { if (processedText.length > 0) parsingItem.author = processedText; processed = YES; } 598 | else if ([currentPath isEqualToString:@"/feed/entry/published"]) { if (processedText.length > 0) parsingItem.date = [NSDate dateFromInternetDateTimeString:processedText formatHint:DateFormatHintRFC3339]; processed = YES; } 599 | else if ([currentPath isEqualToString:@"/feed/entry/updated"]) { if (processedText.length > 0) parsingItem.updated = [NSDate dateFromInternetDateTimeString:processedText formatHint:DateFormatHintRFC3339]; processed = YES; } 600 | } 601 | 602 | // Info 603 | if (!processed && _feedParseType != ParseTypeItemsOnly) { 604 | if ([currentPath isEqualToString:@"/feed/title"]) { if (processedText.length > 0) parsingInfo.title = processedText; processed = YES; } 605 | else if ([currentPath isEqualToString:@"/feed/description"]) { if (processedText.length > 0) parsingInfo.summary = processedText; processed = YES; } 606 | else if ([currentPath isEqualToString:@"/feed/link"]) { [self processAtomLink:currentElementAttributes andAddToMWObject:parsingInfo]; processed = YES;} 607 | } 608 | 609 | break; 610 | } 611 | default: break; 612 | } 613 | } 614 | 615 | // Adjust path 616 | currentPath = [currentPath stringByDeletingLastPathComponent]; 617 | 618 | // If end of an item then tell delegate 619 | if (!processed) { 620 | if (((feedType == FeedTypeRSS || feedType == FeedTypeRSS1) && [qName isEqualToString:@"item"]) || 621 | (feedType == FeedTypeAtom && [qName isEqualToString:@"entry"])) { 622 | 623 | // Dispatch item to delegate 624 | [self dispatchFeedItem]; 625 | 626 | } 627 | } 628 | 629 | // Check if the document has finished parsing and send off info if needed (i.e. there were no items) 630 | if (!processed) { 631 | if ((feedType == FeedTypeRSS && [qName isEqualToString:@"rss"]) || 632 | (feedType == FeedTypeRSS1 && [qName isEqualToString:@"rdf:RDF"]) || 633 | (feedType == FeedTypeAtom && [qName isEqualToString:@"feed"])) { 634 | 635 | // Document ending so if we havent sent off feed info yet, do so 636 | if (parsingInfo && _feedParseType != ParseTypeItemsOnly) 637 | [self dispatchFeedInfoToDelegate]; 638 | } 639 | } 640 | } 641 | } 642 | 643 | - (void)parser:(NSXMLParser *)parser foundCDATA:(NSData *)CDATABlock { 644 | MWXMLLog(@"NSXMLParser: foundCDATA (%d bytes)", CDATABlock.length); 645 | 646 | // Remember characters 647 | NSString *string = nil; 648 | @try { 649 | 650 | // Try decoding with NSUTF8StringEncoding & NSISOLatin1StringEncoding 651 | string = [[NSString alloc] initWithData:CDATABlock encoding:NSUTF8StringEncoding]; 652 | if (!string) string = [[NSString alloc] initWithData:CDATABlock encoding:NSISOLatin1StringEncoding]; 653 | 654 | // Add - No need to encode as CDATA should not be encoded as it's ignored by the parser 655 | if (string) [currentText appendString:string]; 656 | 657 | } @catch (NSException * e) { 658 | } @finally { 659 | string = nil; 660 | } 661 | 662 | } 663 | 664 | - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string 665 | { 666 | MWXMLLog(@"NSXMLParser: foundCharacters: %@", string); 667 | // Remember characters 668 | if (!parseStructureAsContent) { 669 | // Add characters normally 670 | [currentText appendString:string]; 671 | } else { 672 | // If parsing structure as content then we should encode characters 673 | [currentText appendString:[string stringByEncodingHTMLEntities]]; 674 | } 675 | 676 | } 677 | 678 | // Call if parsing error occured or parse was aborted 679 | - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError { 680 | MWXMLLog(@"NSXMLParser: parseErrorOccurred: %@", parseError); 681 | // Fail with error 682 | if (aborted==NO) 683 | { 684 | // This method is called when legimitaly aboring the parser so ignore if this is the case 685 | [self parsingFailedWithErrorCode:MWErrorCodeFeedParsingError andDescription:[parseError localizedDescription]]; 686 | } 687 | 688 | } 689 | 690 | - (void)parser:(NSXMLParser *)parser validationErrorOccurred:(NSError *)validError { 691 | MWXMLLog(@"NSXMLParser: validationErrorOccurred: %@", validError); 692 | 693 | // Fail with error 694 | [self parsingFailedWithErrorCode:MWErrorCodeFeedValidationError andDescription:[validError localizedDescription]]; 695 | 696 | } 697 | 698 | #pragma mark - 699 | #pragma mark Send Items to Delegate 700 | 701 | - (void)dispatchFeedInfoToDelegate { 702 | if (parsingInfo) { 703 | // Debug log 704 | MWLog(@"MWFeedParser: Feed info for \"%@\" successfully parsed", info.title); 705 | } 706 | } 707 | 708 | - (void)dispatchFeedItem { 709 | if (parsingItem) 710 | { 711 | // Process before hand 712 | if (!parsingItem.summary) { parsingItem.summary = parsingItem.content; parsingItem.content = nil; } 713 | if (!parsingItem.date && parsingItem.updated) { parsingItem.date = parsingItem.updated; } 714 | 715 | // Debug log 716 | MWLog(@"MWFeedParser: Feed item \"%@\" successfully parsed", item.title); 717 | 718 | // Finish 719 | [itemsContainer addObject:parsingItem]; 720 | parsingItem = nil; 721 | } 722 | } 723 | 724 | #pragma mark - 725 | #pragma mark Helpers & Properties 726 | 727 | // Set URL to parse and removing feed: uri scheme info 728 | // http://en.wikipedia.org/wiki/Feed:_URI_scheme 729 | - (void)setStandardUrl:(NSString *)standardUrl 730 | { 731 | NSURL* url = [NSURL URLWithString:(NSString *)standardUrl].standardizedURL; 732 | if ([url.scheme isEqualToString:@"feed"]) 733 | { 734 | // Remove feed URL scheme 735 | url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", 736 | ([url.resourceSpecifier hasPrefix:@"//"] ? @"http:" : @""), 737 | url.resourceSpecifier]]; 738 | } 739 | _standardUrl = url.absoluteString; 740 | } 741 | 742 | #pragma mark - 743 | #pragma mark Misc 744 | 745 | // Create an enclosure NSDictionary from enclosure (or link) attributes 746 | - (BOOL)createEnclosureFromAttributes:(NSDictionary *)attributes andAddToItem:(IDNFeedItem *)currentItem { 747 | 748 | // Create enclosure 749 | NSDictionary *enclosure = nil; 750 | NSString *encURL = nil, *encType = nil; 751 | NSNumber *encLength = nil; 752 | if (attributes) { 753 | switch (feedType) { 754 | case FeedTypeRSS: { // http://cyber.law.harvard.edu/rss/rss.html#ltenclosuregtSubelementOfLtitemgt 755 | // 756 | encURL = [attributes objectForKey:@"url"]; 757 | encType = [attributes objectForKey:@"type"]; 758 | encLength = [NSNumber numberWithLongLong:[((NSString *)[attributes objectForKey:@"length"]) longLongValue]]; 759 | break; 760 | } 761 | case FeedTypeRSS1: { // http://www.xs4all.nl/~foz/mod_enclosure.html 762 | // 763 | encURL = [attributes objectForKey:@"rdf:resource"]; 764 | encType = [attributes objectForKey:@"enc:type"]; 765 | encLength = [NSNumber numberWithLongLong:[((NSString *)[attributes objectForKey:@"enc:length"]) longLongValue]]; 766 | break; 767 | } 768 | case FeedTypeAtom: { // http://www.atomenabled.org/developers/syndication/atom-format-spec.php#rel_attribute 769 | // 10 | 11 | // Formatting hints 12 | typedef enum { 13 | DateFormatHintNone, 14 | DateFormatHintRFC822, 15 | DateFormatHintRFC3339 16 | } DateFormatHint; 17 | 18 | // A category to parse internet date & time strings 19 | @interface NSDate (InternetDateTime) 20 | 21 | // Get date from RFC3339 or RFC822 string 22 | // - A format/specification hint can be used to speed up, 23 | // otherwise both will be attempted in order to get a date 24 | + (NSDate *)dateFromInternetDateTimeString:(NSString *)dateString 25 | formatHint:(DateFormatHint)hint; 26 | 27 | // Get date from a string using a specific date specification 28 | + (NSDate *)dateFromRFC3339String:(NSString *)dateString; 29 | + (NSDate *)dateFromRFC822String:(NSString *)dateString; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /IDNFeedParser/IDNFeedParser/NSDate+InternetDateTime.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDate+InternetDateTime.m 3 | // MWFeedParser 4 | // 5 | // Created by Michael Waterfall on 07/10/2010. 6 | // Copyright 2010 Michael Waterfall. All rights reserved. 7 | // 8 | 9 | #import "NSDate+InternetDateTime.h" 10 | 11 | // Always keep the formatter around as they're expensive to instantiate 12 | static NSDateFormatter *_internetDateTimeFormatter = nil; 13 | 14 | // Good info on internet dates here: 15 | // http://developer.apple.com/iphone/library/qa/qa2010/qa1480.html 16 | @implementation NSDate (InternetDateTime) 17 | 18 | // Instantiate single date formatter 19 | + (NSDateFormatter *)internetDateTimeFormatter { 20 | @synchronized(self) { 21 | if (!_internetDateTimeFormatter) { 22 | NSLocale *en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; 23 | _internetDateTimeFormatter = [[NSDateFormatter alloc] init]; 24 | [_internetDateTimeFormatter setLocale:en_US_POSIX]; 25 | [_internetDateTimeFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; 26 | } 27 | } 28 | return _internetDateTimeFormatter; 29 | } 30 | 31 | // Get a date from a string - hint can be used to speed up 32 | + (NSDate *)dateFromInternetDateTimeString:(NSString *)dateString formatHint:(DateFormatHint)hint { 33 | // Keep dateString around a while (for thread-safety) 34 | NSDate *date = nil; 35 | if (dateString) { 36 | if (hint != DateFormatHintRFC3339) { 37 | // Try RFC822 first 38 | date = [NSDate dateFromRFC822String:dateString]; 39 | if (!date) date = [NSDate dateFromRFC3339String:dateString]; 40 | } else { 41 | // Try RFC3339 first 42 | date = [NSDate dateFromRFC3339String:dateString]; 43 | if (!date) date = [NSDate dateFromRFC822String:dateString]; 44 | } 45 | } 46 | // Finished with date string 47 | return date; 48 | } 49 | 50 | // See http://www.faqs.org/rfcs/rfc822.html 51 | + (NSDate *)dateFromRFC822String:(NSString *)dateString { 52 | // Keep dateString around a while (for thread-safety) 53 | NSDate *date = nil; 54 | if (dateString) { 55 | NSDateFormatter *dateFormatter = [NSDate internetDateTimeFormatter]; 56 | @synchronized(dateFormatter) { 57 | 58 | // Process 59 | NSString *RFC822String = [[NSString stringWithString:dateString] uppercaseString]; 60 | if ([RFC822String rangeOfString:@","].location != NSNotFound) { 61 | if (!date) { // Sun, 19 May 2002 15:21:36 GMT 62 | [dateFormatter setDateFormat:@"EEE, d MMM yyyy HH:mm:ss zzz"]; 63 | date = [dateFormatter dateFromString:RFC822String]; 64 | } 65 | if (!date) { // Sun, 19 May 2002 15:21 GMT 66 | [dateFormatter setDateFormat:@"EEE, d MMM yyyy HH:mm zzz"]; 67 | date = [dateFormatter dateFromString:RFC822String]; 68 | } 69 | if (!date) { // Sun, 19 May 2002 15:21:36 70 | [dateFormatter setDateFormat:@"EEE, d MMM yyyy HH:mm:ss"]; 71 | date = [dateFormatter dateFromString:RFC822String]; 72 | } 73 | if (!date) { // Sun, 19 May 2002 15:21 74 | [dateFormatter setDateFormat:@"EEE, d MMM yyyy HH:mm"]; 75 | date = [dateFormatter dateFromString:RFC822String]; 76 | } 77 | } else { 78 | if (!date) { // 19 May 2002 15:21:36 GMT 79 | [dateFormatter setDateFormat:@"d MMM yyyy HH:mm:ss zzz"]; 80 | date = [dateFormatter dateFromString:RFC822String]; 81 | } 82 | if (!date) { // 19 May 2002 15:21 GMT 83 | [dateFormatter setDateFormat:@"d MMM yyyy HH:mm zzz"]; 84 | date = [dateFormatter dateFromString:RFC822String]; 85 | } 86 | if (!date) { // 19 May 2002 15:21:36 87 | [dateFormatter setDateFormat:@"d MMM yyyy HH:mm:ss"]; 88 | date = [dateFormatter dateFromString:RFC822String]; 89 | } 90 | if (!date) { // 19 May 2002 15:21 91 | [dateFormatter setDateFormat:@"d MMM yyyy HH:mm"]; 92 | date = [dateFormatter dateFromString:RFC822String]; 93 | } 94 | } 95 | if (!date) NSLog(@"Could not parse RFC822 date: \"%@\" Possible invalid format.", dateString); 96 | 97 | } 98 | } 99 | // Finished with date string 100 | return date; 101 | } 102 | 103 | // See http://www.faqs.org/rfcs/rfc3339.html 104 | + (NSDate *)dateFromRFC3339String:(NSString *)dateString { 105 | // Keep dateString around a while (for thread-safety) 106 | NSDate *date = nil; 107 | if (dateString) { 108 | NSDateFormatter *dateFormatter = [NSDate internetDateTimeFormatter]; 109 | @synchronized(dateFormatter) { 110 | 111 | // Process date 112 | NSString *RFC3339String = [[NSString stringWithString:dateString] uppercaseString]; 113 | RFC3339String = [RFC3339String stringByReplacingOccurrencesOfString:@"Z" withString:@"-0000"]; 114 | // Remove colon in timezone as it breaks NSDateFormatter in iOS 4+. 115 | // - see https://devforums.apple.com/thread/45837 116 | if (RFC3339String.length > 20) { 117 | RFC3339String = [RFC3339String stringByReplacingOccurrencesOfString:@":" 118 | withString:@"" 119 | options:0 120 | range:NSMakeRange(20, RFC3339String.length-20)]; 121 | } 122 | if (!date) { // 1996-12-19T16:39:57-0800 123 | [dateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZ"]; 124 | date = [dateFormatter dateFromString:RFC3339String]; 125 | } 126 | if (!date) { // 1937-01-01T12:00:27.87+0020 127 | [dateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSSZZZ"]; 128 | date = [dateFormatter dateFromString:RFC3339String]; 129 | } 130 | if (!date) { // 1937-01-01T12:00:27 131 | [dateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss"]; 132 | date = [dateFormatter dateFromString:RFC3339String]; 133 | } 134 | if (!date) NSLog(@"Could not parse RFC3339 date: \"%@\" Possible invalid format.", dateString); 135 | 136 | } 137 | } 138 | // Finished with date string 139 | return date; 140 | } 141 | 142 | @end 143 | -------------------------------------------------------------------------------- /IDNFeedParser/IDNFeedParser/NSString+HTML.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+HTML.h 3 | // MWFeedParser 4 | // 5 | // Copyright (c) 2010 Michael Waterfall 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // 1. The above copyright notice and this permission notice shall be included 15 | // in all copies or substantial portions of the Software. 16 | // 17 | // 2. This Software cannot be used to archive or collect data such as (but not 18 | // limited to) that of events, news, experiences and activities, for the 19 | // purpose of any concept relating to diary/journal keeping. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | // THE SOFTWARE. 28 | // 29 | 30 | #import 31 | 32 | // Dependant upon GTMNSString+HTML 33 | 34 | @interface NSString (HTML) 35 | 36 | // Strips HTML tags & comments, removes extra whitespace and decodes HTML character entities. 37 | - (NSString *)stringByConvertingHTMLToPlainText; 38 | 39 | // Decode all HTML entities using GTM. 40 | - (NSString *)stringByDecodingHTMLEntities; 41 | 42 | // Encode all HTML entities using GTM. 43 | - (NSString *)stringByEncodingHTMLEntities; 44 | 45 | // Minimal unicode encoding will only cover characters from table 46 | // A.2.2 of http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters 47 | // which is what you want for a unicode encoded webpage. 48 | - (NSString *)stringByEncodingHTMLEntities:(BOOL)isUnicode; 49 | 50 | // Replace newlines with
tags. 51 | - (NSString *)stringWithNewLinesAsBRs; 52 | 53 | // Remove newlines and white space from string. 54 | - (NSString *)stringByRemovingNewLinesAndWhitespace; 55 | 56 | // Wrap plain URLs in ... 57 | // - Ignores URLs inside tags (any URL beginning with =") 58 | // - HTTP & HTTPS schemes only 59 | // - Only works in iOS 4+ as we use NSRegularExpression (returns self if not supported so be careful with NSMutableStrings) 60 | // - Expression: (?" intoString:NULL]; 68 | [scanner scanString:@"-->" intoString:NULL]; 69 | 70 | } else if ([scanner scanString:@"script" intoString:NULL]) { 71 | 72 | // Script tag where things don't need escaping! 73 | [scanner scanUpToString:@"" intoString:NULL]; 74 | [scanner scanString:@"" intoString:NULL]; 75 | 76 | } else { 77 | 78 | // Tag - remove and replace with space unless it's 79 | // a closing inline tag then dont replace with a space 80 | if ([scanner scanString:@"/" intoString:NULL]) { 81 | 82 | // Closing tag - replace with space unless it's inline 83 | tagName = nil; dontReplaceTagWithSpace = NO; 84 | if ([scanner scanCharactersFromSet:tagNameCharacters intoString:&tagName]) { 85 | tagName = [tagName lowercaseString]; 86 | dontReplaceTagWithSpace = ([tagName isEqualToString:@"a"] || 87 | [tagName isEqualToString:@"b"] || 88 | [tagName isEqualToString:@"i"] || 89 | [tagName isEqualToString:@"q"] || 90 | [tagName isEqualToString:@"span"] || 91 | [tagName isEqualToString:@"em"] || 92 | [tagName isEqualToString:@"strong"] || 93 | [tagName isEqualToString:@"cite"] || 94 | [tagName isEqualToString:@"abbr"] || 95 | [tagName isEqualToString:@"acronym"] || 96 | [tagName isEqualToString:@"label"]); 97 | } 98 | 99 | // Replace tag with string unless it was an inline 100 | if (!dontReplaceTagWithSpace && result.length > 0 && ![scanner isAtEnd]) [result appendString:@" "]; 101 | 102 | } 103 | 104 | // Scan past tag 105 | [scanner scanUpToString:@">" intoString:NULL]; 106 | [scanner scanString:@">" intoString:NULL]; 107 | 108 | } 109 | 110 | } else { 111 | 112 | // Stopped at whitespace - replace all whitespace and newlines with a space 113 | if ([scanner scanCharactersFromSet:newLineAndWhitespaceCharacters intoString:NULL]) { 114 | if (result.length > 0 && ![scanner isAtEnd]) [result appendString:@" "]; // Dont append space to beginning or end of result 115 | } 116 | 117 | } 118 | 119 | } while (![scanner isAtEnd]); 120 | 121 | // Cleanup 122 | 123 | // Decode HTML entities and return 124 | NSString *retString = [result stringByDecodingHTMLEntities]; 125 | 126 | // Return 127 | return retString; 128 | 129 | } 130 | } 131 | 132 | - (NSString *)stringByDecodingHTMLEntities { 133 | // Can return self so create new string if we're a mutable string 134 | return [NSString stringWithString:[self gtm_stringByUnescapingFromHTML]]; 135 | } 136 | 137 | 138 | - (NSString *)stringByEncodingHTMLEntities { 139 | // Can return self so create new string if we're a mutable string 140 | return [NSString stringWithString:[self gtm_stringByEscapingForAsciiHTML]]; 141 | } 142 | 143 | - (NSString *)stringByEncodingHTMLEntities:(BOOL)isUnicode { 144 | // Can return self so create new string if we're a mutable string 145 | return [NSString stringWithString:(isUnicode ? [self gtm_stringByEscapingForHTML] : [self gtm_stringByEscapingForAsciiHTML])]; 146 | } 147 | 148 | - (NSString *)stringWithNewLinesAsBRs { 149 | @autoreleasepool { 150 | 151 | // Strange New lines: 152 | // Next Line, U+0085 153 | // Form Feed, U+000C 154 | // Line Separator, U+2028 155 | // Paragraph Separator, U+2029 156 | 157 | // Scanner 158 | NSScanner *scanner = [[NSScanner alloc] initWithString:self]; 159 | [scanner setCharactersToBeSkipped:nil]; 160 | NSMutableString *result = [[NSMutableString alloc] init]; 161 | NSString *temp; 162 | NSCharacterSet *newLineCharacters = [NSCharacterSet characterSetWithCharactersInString: 163 | [NSString stringWithFormat:@"\n\r%C%C%C%C", (unichar)0x0085, (unichar)0x000C, (unichar)0x2028, (unichar)0x2029]]; 164 | // Scan 165 | do { 166 | 167 | // Get non new line characters 168 | temp = nil; 169 | [scanner scanUpToCharactersFromSet:newLineCharacters intoString:&temp]; 170 | if (temp) [result appendString:temp]; 171 | temp = nil; 172 | 173 | // Add
s 174 | if ([scanner scanString:@"\r\n" intoString:nil]) { 175 | 176 | // Combine \r\n into just 1
177 | [result appendString:@"
"]; 178 | 179 | } else if ([scanner scanCharactersFromSet:newLineCharacters intoString:&temp]) { 180 | 181 | // Scan other new line characters and add
s 182 | if (temp) { 183 | for (NSUInteger i = 0; i < temp.length; i++) { 184 | [result appendString:@"
"]; 185 | } 186 | } 187 | 188 | } 189 | 190 | } while (![scanner isAtEnd]); 191 | 192 | // Cleanup & return 193 | NSString *retString = [NSString stringWithString:result]; 194 | 195 | // Return 196 | return retString; 197 | 198 | } 199 | } 200 | 201 | - (NSString *)stringByRemovingNewLinesAndWhitespace { 202 | @autoreleasepool { 203 | 204 | // Strange New lines: 205 | // Next Line, U+0085 206 | // Form Feed, U+000C 207 | // Line Separator, U+2028 208 | // Paragraph Separator, U+2029 209 | 210 | // Scanner 211 | NSScanner *scanner = [[NSScanner alloc] initWithString:self]; 212 | [scanner setCharactersToBeSkipped:nil]; 213 | NSMutableString *result = [[NSMutableString alloc] init]; 214 | NSString *temp; 215 | NSCharacterSet *newLineAndWhitespaceCharacters = [NSCharacterSet characterSetWithCharactersInString: 216 | [NSString stringWithFormat:@" \t\n\r%C%C%C%C", (unichar)0x0085, (unichar)0x000C, (unichar)0x2028, (unichar)0x2029]]; 217 | // Scan 218 | while (![scanner isAtEnd]) { 219 | 220 | // Get non new line or whitespace characters 221 | temp = nil; 222 | [scanner scanUpToCharactersFromSet:newLineAndWhitespaceCharacters intoString:&temp]; 223 | if (temp) [result appendString:temp]; 224 | 225 | // Replace with a space 226 | if ([scanner scanCharactersFromSet:newLineAndWhitespaceCharacters intoString:NULL]) { 227 | if (result.length > 0 && ![scanner isAtEnd]) // Dont append space to beginning or end of result 228 | [result appendString:@" "]; 229 | } 230 | 231 | } 232 | 233 | // Cleanup 234 | 235 | // Return 236 | NSString *retString = [NSString stringWithString:result]; 237 | 238 | // Return 239 | return retString; 240 | } 241 | } 242 | 243 | - (NSString *)stringByLinkifyingURLs { 244 | if (!NSClassFromString(@"NSRegularExpression")) return self; 245 | @autoreleasepool { 246 | NSString *pattern = @"(?$1"]; 250 | return modifiedString; 251 | } 252 | } 253 | 254 | - (NSString *)stringByStrippingTags { 255 | @autoreleasepool { 256 | 257 | // Find first & and short-cut if we can 258 | NSUInteger ampIndex = [self rangeOfString:@"<" options:NSLiteralSearch].location; 259 | if (ampIndex == NSNotFound) { 260 | return [NSString stringWithString:self]; // return copy of string as no tags found 261 | } 262 | 263 | // Scan and find all tags 264 | NSScanner *scanner = [NSScanner scannerWithString:self]; 265 | [scanner setCharactersToBeSkipped:nil]; 266 | NSMutableSet *tags = [[NSMutableSet alloc] init]; 267 | NSString *tag; 268 | do { 269 | 270 | // Scan up to < 271 | tag = nil; 272 | [scanner scanUpToString:@"<" intoString:NULL]; 273 | [scanner scanUpToString:@">" intoString:&tag]; 274 | 275 | // Add to set 276 | if (tag) { 277 | NSString *t = [[NSString alloc] initWithFormat:@"%@>", tag]; 278 | [tags addObject:t]; 279 | } 280 | 281 | } while (![scanner isAtEnd]); 282 | 283 | // Strings 284 | NSMutableString *result = [[NSMutableString alloc] initWithString:self]; 285 | NSString *finalString; 286 | 287 | // Replace tags 288 | NSString *replacement; 289 | for (NSString *t in tags) { 290 | 291 | // Replace tag with space unless it's an inline element 292 | replacement = @" "; 293 | if ([t isEqualToString:@""] || 294 | [t isEqualToString:@""] || 295 | [t isEqualToString:@""] || 296 | [t isEqualToString:@""] || 297 | [t isEqualToString:@""] || 298 | [t isEqualToString:@""] || 299 | [t isEqualToString:@""] || 300 | [t isEqualToString:@""]) { 301 | replacement = @""; 302 | } 303 | 304 | // Replace 305 | [result replaceOccurrencesOfString:t 306 | withString:replacement 307 | options:NSLiteralSearch 308 | range:NSMakeRange(0, result.length)]; 309 | } 310 | 311 | // Remove multi-spaces and line breaks 312 | finalString = [result stringByRemovingNewLinesAndWhitespace]; 313 | 314 | // Cleanup 315 | 316 | // Return 317 | return finalString; 318 | 319 | } 320 | } 321 | 322 | @end 323 | -------------------------------------------------------------------------------- /IDNFeedParser/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /IDNFeedParser/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | net.iosdev.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | NSAppTransportSecurity 40 | 41 | NSAllowsArbitraryLoads 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /IDNFeedParser/UIViewController+IDNPrompt.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+IDNPrompt.h 3 | // IDNFramework 4 | // 5 | // Created by photondragon on 15/5/17. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | /** 12 | 在controller.view上居中显示提示内容,比如“正在加载”、“操作成功”之类的。 13 | */ 14 | @interface UIViewController(IDNPrompt) 15 | 16 | @property(nonatomic,readonly) BOOL prompting;//是否正在显示提示框 17 | 18 | // 在controller.view中加入提示框,显示文本提示,位于原有内容之上,原内容可点击。在duration秒后提示框自动消失。如果duration<=0,duration=2.0 19 | - (void)prompt:(NSString*)text duration:(NSTimeInterval)duration; 20 | 21 | // blockTouches==TRUE表示不允许用户点击提示框下面的内容。当提示框自动消失时,会调用finishedHandler 22 | - (void)prompt:(NSString*)text duration:(NSTimeInterval)duration blockTouches:(BOOL)blockTouches finishedHandle:(void(^)())finishedHandler; 23 | 24 | // 在controller.view中加入提示框(默认会有0.3s的延迟),显示旋转菊花加文本提示,原有内容不可点击。提示框不会自动消失,需要手动取消显示。 25 | - (void)prompting:(NSString*)text; 26 | 27 | // 取消显示提示框 28 | - (void)stopPrompt; 29 | 30 | @end -------------------------------------------------------------------------------- /IDNFeedParser/UIViewController+IDNPrompt.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+IDNPrompt.m 3 | // IDNFramework 4 | // 5 | // Created by photondragon on 15/5/17. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import "UIViewController+IDNPrompt.h" 10 | //#import "IDNActivityIndicator.h" 11 | 12 | //#define ActivityIndicator IDNActivityIndicator 13 | //#define ActivityIndicatorColor [UIColor colorWithRed:21/255.0 green:138/255.0 blue:228/255.0 alpha:1] 14 | #define ActivityIndicator UIActivityIndicatorView 15 | #define ActivityIndicatorColor nil 16 | 17 | #define PromptFrameWidth 110 18 | #define PromptFrameHeight 90 19 | #define LoadingIndicatorLength 30 20 | 21 | @interface UIViewControllerPromptView : UIView 22 | @property(nonatomic,weak) UIView* frameView; 23 | @property(nonatomic,weak) UILabel* labelPrompt; 24 | @property(nonatomic,strong) ActivityIndicator* loadingIndicator; 25 | - (void)showLoadingIndicator;//显示旋转菊花 26 | @end 27 | @implementation UIViewControllerPromptView 28 | 29 | - (instancetype)init 30 | { 31 | self = [super init]; 32 | if (self) { 33 | //框 34 | UIView* frameView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, PromptFrameWidth, PromptFrameHeight)];//这里设置frame是为了后面设置labelPrompt.autoresizingMask方便 35 | frameView.translatesAutoresizingMaskIntoConstraints = NO; 36 | frameView.layer.cornerRadius = 6; 37 | frameView.layer.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.5].CGColor; 38 | [self addSubview:frameView]; 39 | self.frameView = frameView; 40 | 41 | //居中显示框 42 | [frameView addConstraint:[NSLayoutConstraint constraintWithItem:frameView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:PromptFrameWidth]]; 43 | [frameView addConstraint:[NSLayoutConstraint constraintWithItem:frameView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:PromptFrameHeight]]; 44 | [self addConstraint:[NSLayoutConstraint constraintWithItem:frameView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0]]; 45 | [self addConstraint:[NSLayoutConstraint constraintWithItem:frameView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:-32]]; 46 | 47 | 48 | UILabel* labelPrompt = [[UILabel alloc] initWithFrame:CGRectMake(8, 4, PromptFrameWidth-16, PromptFrameHeight-8)]; 49 | labelPrompt.numberOfLines = 4; 50 | labelPrompt.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 51 | labelPrompt.font = [UIFont boldSystemFontOfSize:14]; 52 | labelPrompt.textColor = [UIColor whiteColor];//colorWithRed:0.2 green:0.2 blue:0.2 alpha:1]; 53 | labelPrompt.textAlignment = NSTextAlignmentCenter; 54 | [frameView addSubview:labelPrompt]; 55 | self.labelPrompt = labelPrompt; 56 | } 57 | return self; 58 | } 59 | 60 | - (ActivityIndicator*)loadingIndicator 61 | { 62 | if(_loadingIndicator==nil) 63 | { 64 | _loadingIndicator = [[ActivityIndicator alloc] initWithFrame:CGRectMake(PromptFrameWidth/2.0-15, 16, LoadingIndicatorLength, LoadingIndicatorLength)]; 65 | _loadingIndicator.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 66 | _loadingIndicator.color = ActivityIndicatorColor; 67 | } 68 | return _loadingIndicator; 69 | } 70 | - (void)showLoadingIndicator 71 | { 72 | self.labelPrompt.frame = CGRectMake(8, 8+LoadingIndicatorLength+4, PromptFrameWidth-16, PromptFrameHeight-LoadingIndicatorLength-8-4-4); 73 | [self.frameView addSubview:self.loadingIndicator]; 74 | // [self.loadingIndicator startAnimating]; 75 | } 76 | - (void)willMoveToWindow:(UIWindow *)newWindow 77 | { 78 | if(_loadingIndicator && _loadingIndicator.superview) 79 | { 80 | if(newWindow==nil) 81 | [self.loadingIndicator stopAnimating]; 82 | else 83 | [self.loadingIndicator startAnimating]; 84 | } 85 | } 86 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 87 | } 88 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ 89 | } 90 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ 91 | } 92 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{ 93 | } 94 | @end 95 | 96 | @implementation UIViewController(Prompt) 97 | 98 | + (NSMutableDictionary*)dictionaryOfPromptInfos 99 | { 100 | static NSMutableDictionary* dic = nil; 101 | if(dic==nil) 102 | { 103 | dic = [NSMutableDictionary dictionary]; 104 | } 105 | return dic; 106 | } 107 | 108 | - (NSMutableDictionary*)promptInfo 109 | { 110 | NSValue* controllerKey = [NSValue valueWithNonretainedObject:self]; 111 | NSMutableDictionary* dicPromptInfos = [UIViewController dictionaryOfPromptInfos]; 112 | NSMutableDictionary* dicPromptInfo = dicPromptInfos[controllerKey]; 113 | return dicPromptInfo; 114 | } 115 | 116 | - (BOOL)prompting 117 | { 118 | if([self promptInfo]) 119 | return TRUE; 120 | return FALSE; 121 | } 122 | 123 | - (void)prompt:(NSString*)text duration:(NSTimeInterval)duration 124 | { 125 | [self prompt:text duration:duration blockTouches:NO finishedHandle:nil]; 126 | } 127 | - (void)prompt:(NSString*)text duration:(NSTimeInterval)duration blockTouches:(BOOL)blockTouches finishedHandle:(void(^)())finishedHandler 128 | { 129 | if(duration<=0) 130 | duration = 2.0; 131 | 132 | [self promptWithText:text delay:0 duration:duration blockTouches:blockTouches showLoading:NO finishedHandle:finishedHandler]; 133 | } 134 | 135 | - (void)prompting:(NSString*)text 136 | { 137 | [self promptWithText:text delay:0.3 duration:0 blockTouches:YES showLoading:YES finishedHandle:nil]; 138 | } 139 | 140 | // duration==0表示不自动消失。showLoading是否显示旋转菊花。 141 | - (void)promptWithText:(NSString*)text delay:(NSTimeInterval)delay duration:(NSTimeInterval)duration blockTouches:(BOOL)blockTouches showLoading:(BOOL)showLoading finishedHandle:(void(^)())finishedHandler 142 | { 143 | [self stopPrompt]; 144 | 145 | if(delay<0) 146 | delay = 0; 147 | if(duration<0) 148 | duration = 2.0; 149 | 150 | UIViewControllerPromptView* view = [[UIViewControllerPromptView alloc] init]; 151 | view.userInteractionEnabled = blockTouches;//屏蔽下层Touches 152 | if(showLoading) 153 | [view showLoadingIndicator]; 154 | view.labelPrompt.text = text; 155 | view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 156 | 157 | NSMutableDictionary* dicPromptInfo = [NSMutableDictionary dictionaryWithDictionary:@{@"view":view, 158 | @"delay":@(delay), 159 | @"duration":@(duration), 160 | }]; 161 | if(text) 162 | dicPromptInfo[@"text"] = text; 163 | if(finishedHandler) 164 | dicPromptInfo[@"finishedHandler"] = finishedHandler; 165 | NSMutableDictionary* dicPromptInfos = [UIViewController dictionaryOfPromptInfos]; 166 | dicPromptInfos[[NSValue valueWithNonretainedObject:self]] = dicPromptInfo; 167 | 168 | [self performSelector:@selector(addPromptView) withObject:nil afterDelay:delay]; 169 | } 170 | 171 | - (void)addPromptView 172 | { 173 | NSValue* controllerKey = [NSValue valueWithNonretainedObject:self]; 174 | NSMutableDictionary* dicPromptInfos = [UIViewController dictionaryOfPromptInfos]; 175 | NSMutableDictionary* dicPromptInfo = dicPromptInfos[controllerKey]; 176 | if(dicPromptInfo==nil) 177 | return; 178 | UIView* view = dicPromptInfo[@"view"]; 179 | CGRect frame = self.view.frame; 180 | frame.origin = CGPointZero; 181 | view.frame = frame; 182 | [self.view addSubview:view]; 183 | 184 | NSTimeInterval duration = [dicPromptInfo[@"duration"] doubleValue]; 185 | if(duration>0) 186 | [self performSelector:@selector(removePromptView) withObject:nil afterDelay:duration]; 187 | } 188 | 189 | - (void)stopPrompt 190 | { 191 | [self.class cancelPreviousPerformRequestsWithTarget:self selector:@selector(addPromptView) object:nil]; 192 | [self.class cancelPreviousPerformRequestsWithTarget:self selector:@selector(removePromptView) object:nil]; 193 | 194 | [self performSelectorOnMainThread:@selector(removePromptView) withObject:nil waitUntilDone:YES];//立即执行 195 | } 196 | 197 | - (void)removePromptView 198 | { 199 | NSValue* controllerKey = [NSValue valueWithNonretainedObject:self]; 200 | NSMutableDictionary* dicPromptInfos = [UIViewController dictionaryOfPromptInfos]; 201 | NSMutableDictionary* dicPromptInfo = dicPromptInfos[controllerKey]; 202 | if(dicPromptInfo==nil) 203 | return; 204 | UIView* view = dicPromptInfo[@"view"]; 205 | [view removeFromSuperview]; 206 | [dicPromptInfos removeObjectForKey:controllerKey]; 207 | void (^finishedHandler)(); 208 | finishedHandler = dicPromptInfo[@"finishedHandler"]; 209 | if(finishedHandler) 210 | finishedHandler(); 211 | } 212 | 213 | @end 214 | -------------------------------------------------------------------------------- /IDNFeedParser/WebViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewController.h 3 | // IDNFeedParser 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface WebViewController : UIViewController 12 | 13 | @property(nonatomic,strong) NSString* url; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /IDNFeedParser/WebViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewController.m 3 | // IDNFeedParser 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import "WebViewController.h" 10 | #import "UIViewController+IDNPrompt.h" 11 | 12 | @interface WebViewController () 13 | 14 | 15 | @property(nonatomic,strong) UIWebView* webView; 16 | 17 | @end 18 | 19 | @implementation WebViewController 20 | 21 | - (void)loadView 22 | { 23 | _webView = [[UIWebView alloc] init]; 24 | _webView.delegate = self; 25 | self.view = _webView; 26 | } 27 | 28 | - (void)viewDidLoad { 29 | [super viewDidLoad]; 30 | // Do any additional setup after loading the view. 31 | } 32 | 33 | - (void)setUrl:(NSString *)url 34 | { 35 | _url = url; 36 | 37 | [self view]; 38 | 39 | if(url) 40 | { 41 | NSURL* nsurl = [NSURL URLWithString:url]; 42 | NSURLRequest* request = [NSURLRequest requestWithURL:nsurl]; 43 | [self.webView loadRequest:request]; 44 | } 45 | else 46 | [self.webView loadRequest:nil]; 47 | } 48 | 49 | #pragma mark UIWebViewDelegate 50 | 51 | - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 52 | { 53 | return YES; 54 | } 55 | - (void)webViewDidStartLoad:(UIWebView *)webView 56 | { 57 | [self prompting:@"正在加载"]; 58 | } 59 | - (void)webViewDidFinishLoad:(UIWebView *)webView 60 | { 61 | [self stopPrompt]; 62 | } 63 | - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error 64 | { 65 | [self prompt:[NSString stringWithFormat:@"加载失败\n%@", error.localizedDescription] duration:2]; 66 | } 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /IDNFeedParser/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // IDNFeedParser 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /IDNFeedParserTests/IDNFeedParserTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // IDNFeedParserTests.m 3 | // IDNFeedParserTests 4 | // 5 | // Created by photondragon on 15/6/26. 6 | // Copyright (c) 2015年 iosdev.net. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "IDNFeedParser.h" 12 | 13 | @interface IDNFeedParserTests : XCTestCase 14 | 15 | @end 16 | 17 | @implementation IDNFeedParserTests 18 | 19 | - (void)setUp { 20 | [super setUp]; 21 | // Put setup code here. This method is called before the invocation of each test method in the class. 22 | } 23 | 24 | - (void)tearDown { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | [super tearDown]; 27 | } 28 | 29 | - (void)testFeedParser { 30 | // 获取RSS源信息 31 | IDNFeedInfo* info = [IDNFeedParser feedInfoWithUrl:@"http://www.zhihu.com/rss"]; 32 | XCTAssertNotNil(info, @"解析IDNFeedInfo失败"); 33 | // 获取文章列表 34 | NSArray* items = [IDNFeedParser feedItemsWithUrl:@"http://www.zhihu.com/rss"]; 35 | XCTAssertNotEqual(items.count, 0, @"解析IDNFeedItems失败"); 36 | } 37 | 38 | - (void)testFeedParserTwoStep { 39 | NSString* rssUrl = @"http://www.zhihu.com/rss"; 40 | // 获取rss原始Data 41 | NSData* rssData = [IDNFeedParser dataFromUrl:rssUrl]; 42 | 43 | // 解析RSS源信息 44 | IDNFeedInfo* info = [IDNFeedParser feedInfoWithData:rssData fromUrl:rssUrl]; 45 | XCTAssertNotNil(info, @"解析IDNFeedInfo失败"); 46 | 47 | // 获取文章列表 48 | NSArray* items = [IDNFeedParser feedItemsWithData:rssData fromUrl:rssUrl]; 49 | XCTAssertNotEqual(items.count, 0, @"解析IDNFeedItems失败"); 50 | } 51 | 52 | //- (void)testPerformanceExample { 53 | // // This is an example of a performance test case. 54 | // [self measureBlock:^{ 55 | // // Put the code you want to measure the time of here. 56 | // }]; 57 | //} 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /IDNFeedParserTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | net.iosdev.$(PRODUCT_NAME:rfc1034identifier) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### *IDNFeedParser* 一个简单易用的Rss解析库(Objective-C) 2 | 3 | 在[MWFeedParser](https://github.com/mwaterfall/MWFeedParser)的基础上修改。之所以要重新封装,是因为MWFeedParser的接口有点复杂,使用起来不太方便,而且中文编码解析会乱码。 4 | 5 | 重新封装后的IDNFeedParser,**只需要一两行代码就可以完成RSS解析**,不用再写那么多delegate方法了。 6 | 7 | 主要做了以下改动: 8 | 9 | 1. 简化接口,去掉异步操作和delegate,只留下同步操作; 10 | 1. 把下载与解析分开(解耦),方便对RSS数据进行缓存。 11 | 1. 识别RSS频道的图像信息,为FeedInfo增加了image属性 12 | 1. 自动分析文章摘要信息,为FeedItem增加了image属性 13 | 1. 修复编码为中文时(如GB2313),识别出来的信息为乱码的问题 14 | 15 | 这个库使用起来非常简单,只要把IDNFeedParser目录下的源文件拷贝到你的项目中,然后`#import "IDNFeedParser.h"`就可以使用了 16 | 17 | 简单用法: 18 | 19 | // 获取RSS源信息 20 | IDNFeedInfo* info = [IDNFeedParser feedInfoWithUrl:@"http://www.zhihu.com/rss"]; 21 | // 获取文章列表 22 | NSArray* items = [IDNFeedParser feedItemsWithUrl:@"http://www.zhihu.com/rss"]; 23 | 24 | 下载和解析分离的用法: 25 | 26 | NSString* rssUrl = @"http://www.zhihu.com/rss"; 27 | // 下载rss原始Data 28 | NSData* rssData = [IDNFeedParser dataFromUrl:rssUrl]; 29 | 30 | // 解析RSS源信息 31 | IDNFeedInfo* info = [IDNFeedParser feedInfoWithData:rssData fromUrl:rssUrl]; 32 | 33 | // 获取文章列表 34 | NSArray* items = [IDNFeedParser feedItemsWithData:rssData fromUrl:rssUrl]; 35 | 36 | 如果要在后台线程解析RSS,用GCD可以很方便地实现: 37 | 38 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 39 | 40 | // 获取文章列表 41 | NSArray* items = [IDNFeedParser feedItemsWithUrl:feedInfo.url]; 42 | 43 | if(items==nil) //失败 44 | NSLog(@"获取文章列表失败"); 45 | else //成功 46 | { 47 | // 解析完成后在主线程更新显示 48 | dispatch_async(dispatch_get_main_queue(), ^{ 49 | [self showFeedItems:items]; 50 | }); 51 | } 52 | }); 53 | 54 | #### 为什么接口只提供同步方法,不提供异步方法和delegate? 55 | 56 | 因为同步方法非常灵活,可以很方便地结合GCG或者NSOperation使用以实现异步操作; 57 | 而且很多开发者都有自己的多线程组件,如果用delegate方式返回数据,反而很难用。 58 | 最重要的是好处还是接口一目了然,**简单易用**! 59 | --------------------------------------------------------------------------------