├── .gitignore
├── LICENSE
├── README.md
├── RSXML.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcbaselines
│ └── 84F22C161B52DDEA000060CE.xcbaseline
│ ├── 8756A8AF-348E-4D58-81F2-569F71868FB1.plist
│ ├── B9957C87-0F96-4C9B-864E-DF5BD201D1DC.plist
│ └── Info.plist
├── RSXML
├── FeedParser.h
├── Info.plist
├── NSString+RSXML.h
├── NSString+RSXML.m
├── RSAtomParser.h
├── RSAtomParser.m
├── RSDateParser.h
├── RSDateParser.m
├── RSFeedParser.h
├── RSFeedParser.m
├── RSHTMLLinkParser.h
├── RSHTMLLinkParser.m
├── RSHTMLMetadata.h
├── RSHTMLMetadata.m
├── RSHTMLMetadataParser.h
├── RSHTMLMetadataParser.m
├── RSOPMLAttributes.h
├── RSOPMLAttributes.m
├── RSOPMLDocument.h
├── RSOPMLDocument.m
├── RSOPMLFeedSpecifier.h
├── RSOPMLFeedSpecifier.m
├── RSOPMLItem.h
├── RSOPMLItem.m
├── RSOPMLParser.h
├── RSOPMLParser.m
├── RSParsedArticle.h
├── RSParsedArticle.m
├── RSParsedFeed.h
├── RSParsedFeed.m
├── RSRSSParser.h
├── RSRSSParser.m
├── RSSAXHTMLParser.h
├── RSSAXHTMLParser.m
├── RSSAXParser.h
├── RSSAXParser.m
├── RSXML.h
├── RSXMLData.h
├── RSXMLData.m
├── RSXMLError.h
├── RSXMLError.m
├── RSXMLInternal.h
└── RSXMLInternal.m
├── RSXMLTests
├── Info.plist
├── RSDateParserTests.m
├── RSEntityTests.m
├── RSHTMLTests.m
├── RSOPMLTests.m
├── RSXMLTests.m
└── Resources
│ ├── DaringFireball.html
│ ├── DaringFireball.rss
│ ├── EMarley.rss
│ ├── KatieFloyd.rss
│ ├── OneFootTsunami.atom
│ ├── Subs.opml
│ ├── TimerSearch.txt
│ ├── furbo.html
│ ├── inessential.html
│ ├── manton.rss
│ ├── scriptingNews.rss
│ └── sixcolors.html
└── RSXMLiOS
└── Info.plist
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xcuserstate
23 |
24 | ## Obj-C/Swift specific
25 | *.hmap
26 | *.ipa
27 | *.dSYM.zip
28 | *.dSYM
29 |
30 | # CocoaPods
31 | #
32 | # We recommend against adding the Pods directory to your .gitignore. However
33 | # you should judge for yourself, the pros and cons are mentioned at:
34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
35 | #
36 | # Pods/
37 |
38 | # Carthage
39 | #
40 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
41 | # Carthage/Checkouts
42 |
43 | Carthage/Build
44 |
45 | # fastlane
46 | #
47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
48 | # screenshots whenever they are needed.
49 | # For more information about the recommended setup visit:
50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
51 |
52 | fastlane/report.xml
53 | fastlane/screenshots
54 |
55 | #Code Injection
56 | #
57 | # After new code Injection tools there's a generated folder /iOSInjectionProject
58 | # https://github.com/johnno1962/injectionforxcode
59 |
60 | iOSInjectionProject/
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 brentsimmons
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RSXML
2 |
3 | This is utility code for parsing XML and HTML using libXML2’s SAX parser.
4 |
5 | It builds two framework targets: one for Mac, one for iOS. It does not depend on any other third-party frameworks. The code is Objective-C with ARC.
6 |
7 | #### The gist
8 |
9 | To parse XML, create an `RSSAXParserDelegate`. (There are examples in the framework that you can crib from.)
10 |
11 | To parse HTML, create an `RSSAXHTMLParserDelegate`. (There are examples for this too.)
12 |
13 | #### Goodies and Extras
14 |
15 | There are three XML parsers included, for OPML, RSS, and Atom. To parse OPML, see `RSOPMLParser`. To parse RSS and Atom, see `RSFeedParser`.
16 |
17 | These parsers may or may not be complete enough for your needs. You could, in theory, start writing an RSS reader just with these. (And, if you want to, go for it, with my blessing.)
18 |
19 | There are two HTML parsers included. `RSHTMLMetadataParser` pulls metadata from the head section of an HTML document. `RSHTMLLinkParser` pulls all the links (anchors, <a href=…> tags) from an HTML document.
20 |
21 | Other possibly interesting things:
22 |
23 | `RSDateParser` makes it easy to parse dates in the formats found in various types of feeds.
24 |
25 | `NSString+RSXML` decodes HTML entities.
26 |
27 | Also note: there are some unit tests.
28 |
29 | #### Why use libXML2’s SAX API?
30 |
31 | SAX is kind of a pain because of all the state you have to manage. But it’s fastest and uses the least amount of memory.
32 |
33 | An alternative is to use `NSXMLParser`, which is event-driven like SAX. However, RSXML was written to avoid allocating Objective-C objects except when absolutely needed. You’ll note use of things like `memcp` and `strncmp`.
34 |
35 | Normally I avoid this kind of thing *strenuously*. I prefer to work at the highest level possible.
36 |
37 | But my more-than-a-decade of experience parsing XML has led me to this solution, which — last time I checked, which was, admittedly, a few years ago — was not only fastest but also uses the least memory. (The two things are related, of course: creating objects is bad for performance, so this code attempts to do the minimum possible.)
38 |
39 | All that low-level stuff is encapsulated, however. If you parse a feed, for instance, the caller gets an `RSParsedFeed` which contains `RSParsedArticle`s, and they’re standard Objective-C objects. It’s only inside your `RSSAXParserDelegate` and `RSSAXHTMLParserDelegate` where you’ll need to deal with C.
--------------------------------------------------------------------------------
/RSXML.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/RSXML.xcodeproj/xcshareddata/xcbaselines/84F22C161B52DDEA000060CE.xcbaseline/8756A8AF-348E-4D58-81F2-569F71868FB1.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | classNames
6 |
7 | RSHTMLTests
8 |
9 | testSixcolorsPerformance
10 |
11 | com.apple.XCTPerformanceMetric_WallClockTime
12 |
13 | baselineAverage
14 | 0.0022932
15 | baselineIntegrationDisplayName
16 | Local Baseline
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/RSXML.xcodeproj/xcshareddata/xcbaselines/84F22C161B52DDEA000060CE.xcbaseline/B9957C87-0F96-4C9B-864E-DF5BD201D1DC.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | classNames
6 |
7 | RSHTMLTests
8 |
9 | testDaringFireballPerformance
10 |
11 | com.apple.XCTPerformanceMetric_WallClockTime
12 |
13 | baselineAverage
14 | 0.001
15 | baselineIntegrationDisplayName
16 | Local Baseline
17 |
18 |
19 | testFurboPerformance
20 |
21 | com.apple.XCTPerformanceMetric_WallClockTime
22 |
23 | baselineAverage
24 | 0.00055962
25 | baselineIntegrationDisplayName
26 | Local Baseline
27 |
28 |
29 | testInessentialPerformance
30 |
31 | com.apple.XCTPerformanceMetric_WallClockTime
32 |
33 | baselineAverage
34 | 0.00027723
35 | baselineIntegrationDisplayName
36 | Local Baseline
37 |
38 |
39 | testSixcolorsPerformance
40 |
41 | com.apple.XCTPerformanceMetric_WallClockTime
42 |
43 | baselineAverage
44 | 0.00085183
45 | baselineIntegrationDisplayName
46 | Local Baseline
47 |
48 |
49 |
50 | RSOPMLTests
51 |
52 | testBigSubsPerformance
53 |
54 | com.apple.XCTPerformanceMetric_WallClockTime
55 |
56 | baselineAverage
57 | 0.026
58 | baselineIntegrationDisplayName
59 | Local Baseline
60 |
61 |
62 |
63 | RSXMLTests
64 |
65 | testDaringFireballPerformance
66 |
67 | com.apple.XCTPerformanceMetric_WallClockTime
68 |
69 | baselineAverage
70 | 0.02
71 | baselineIntegrationDisplayName
72 | Local Baseline
73 |
74 |
75 | testMantonPerformance
76 |
77 | com.apple.XCTPerformanceMetric_WallClockTime
78 |
79 | baselineAverage
80 | 0.003
81 | baselineIntegrationDisplayName
82 | Local Baseline
83 |
84 |
85 | testOFTPerformance
86 |
87 | com.apple.XCTPerformanceMetric_WallClockTime
88 |
89 | baselineAverage
90 | 0.007
91 | baselineIntegrationDisplayName
92 | Local Baseline
93 |
94 |
95 | testScriptingNewsPerformance
96 |
97 | com.apple.XCTPerformanceMetric_WallClockTime
98 |
99 | baselineAverage
100 | 0.008
101 | baselineIntegrationDisplayName
102 | Local Baseline
103 |
104 |
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/RSXML.xcodeproj/xcshareddata/xcbaselines/84F22C161B52DDEA000060CE.xcbaseline/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | runDestinationsByUUID
6 |
7 | 8756A8AF-348E-4D58-81F2-569F71868FB1
8 |
9 | localComputer
10 |
11 | busSpeedInMHz
12 | 100
13 | cpuCount
14 | 1
15 | cpuKind
16 | Intel Core i5
17 | cpuSpeedInMHz
18 | 1800
19 | logicalCPUCoresPerPackage
20 | 4
21 | modelCode
22 | MacBookAir5,2
23 | physicalCPUCoresPerPackage
24 | 2
25 | platformIdentifier
26 | com.apple.platform.macosx
27 |
28 | targetArchitecture
29 | x86_64
30 |
31 | B9957C87-0F96-4C9B-864E-DF5BD201D1DC
32 |
33 | localComputer
34 |
35 | busSpeedInMHz
36 | 100
37 | cpuCount
38 | 1
39 | cpuKind
40 | Intel Core i7
41 | cpuSpeedInMHz
42 | 3400
43 | logicalCPUCoresPerPackage
44 | 8
45 | modelCode
46 | iMac13,2
47 | physicalCPUCoresPerPackage
48 | 4
49 | platformIdentifier
50 | com.apple.platform.macosx
51 |
52 | targetArchitecture
53 | x86_64
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/RSXML/FeedParser.h:
--------------------------------------------------------------------------------
1 | //
2 | // FeedParser.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 7/12/15.
6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | @class RSParsedFeed;
12 | @class RSXMLData;
13 |
14 |
15 | @protocol FeedParser
16 |
17 | + (BOOL)canParseFeed:(RSXMLData * _Nonnull)xmlData;
18 |
19 | - (nonnull instancetype)initWithXMLData:(RSXMLData * _Nonnull)xmlData;
20 |
21 | - (nullable RSParsedFeed *)parseFeed:(NSError * _Nullable * _Nullable)error;
22 |
23 |
24 | @end
25 |
--------------------------------------------------------------------------------
/RSXML/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSHumanReadableCopyright
24 | Copyright © 2015 Ranchero Software, LLC. All rights reserved.
25 | NSPrincipalClass
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/RSXML/NSString+RSXML.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSString+RSXML.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 9/25/15.
6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | @interface NSString (RSXML)
12 |
13 | - (NSString *)rs_stringByDecodingHTMLEntities;
14 |
15 | @end
16 |
17 |
--------------------------------------------------------------------------------
/RSXML/NSString+RSXML.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSString+RSXML.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 9/25/15.
6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import "NSString+RSXML.h"
10 |
11 |
12 | @interface NSScanner (RSXML)
13 |
14 | - (BOOL)rs_scanEntityValue:(NSString * _Nullable * _Nullable)decodedEntity;
15 |
16 | @end
17 |
18 |
19 | @implementation NSString (RSXML)
20 |
21 | - (NSString *)rs_stringByDecodingHTMLEntities {
22 |
23 | @autoreleasepool {
24 |
25 | NSScanner *scanner = [[NSScanner alloc] initWithString:self];
26 | scanner.charactersToBeSkipped = nil;
27 | NSMutableString *result = [[NSMutableString alloc] init];
28 |
29 | while (true) {
30 |
31 | NSString *scannedString = nil;
32 | if ([scanner scanUpToString:@"&" intoString:&scannedString]) {
33 | [result appendString:scannedString];
34 | }
35 | if (scanner.isAtEnd) {
36 | break;
37 | }
38 | NSUInteger savedScanLocation = scanner.scanLocation;
39 |
40 | NSString *decodedEntity = nil;
41 | if ([scanner rs_scanEntityValue:&decodedEntity]) {
42 | [result appendString:decodedEntity];
43 | }
44 | else {
45 | [result appendString:@"&"];
46 | scanner.scanLocation = savedScanLocation + 1;
47 | }
48 |
49 | if (scanner.isAtEnd) {
50 | break;
51 | }
52 | }
53 |
54 | if ([self isEqualToString:result]) {
55 | return self;
56 | }
57 | return [result copy];
58 | }
59 | }
60 |
61 |
62 | static NSDictionary *RSEntitiesDictionary(void);
63 | static NSString *RSXMLStringWithValue(unichar value);
64 |
65 | - (NSString * _Nullable)rs_stringByDecodingEntity {
66 |
67 | // self may or may not have outer & and ; characters.
68 |
69 | NSMutableString *s = [self mutableCopy];
70 |
71 | if ([s hasPrefix:@"&"]) {
72 | [s deleteCharactersInRange:NSMakeRange(0, 1)];
73 | }
74 | if ([s hasSuffix:@";"]) {
75 | [s deleteCharactersInRange:NSMakeRange(s.length - 1, 1)];
76 | }
77 |
78 | NSDictionary *entitiesDictionary = RSEntitiesDictionary();
79 |
80 | NSString *decodedEntity = entitiesDictionary[self];
81 | if (decodedEntity) {
82 | return decodedEntity;
83 | }
84 |
85 | if ([s hasPrefix:@"#x"]) { // Hex
86 | NSScanner *scanner = [[NSScanner alloc] initWithString:s];
87 | scanner.charactersToBeSkipped = [NSCharacterSet characterSetWithCharactersInString:@"#x"];
88 | unsigned int hexValue = 0;
89 | if ([scanner scanHexInt:&hexValue]) {
90 | return RSXMLStringWithValue((unichar)hexValue);
91 | }
92 | return nil;
93 | }
94 |
95 | else if ([s hasPrefix:@"#"]) {
96 | [s deleteCharactersInRange:NSMakeRange(0, 1)];
97 | NSInteger value = s.integerValue;
98 | if (value < 1) {
99 | return nil;
100 | }
101 | return RSXMLStringWithValue((unichar)value);
102 | }
103 |
104 | return nil;
105 | }
106 |
107 | @end
108 |
109 | @implementation NSScanner (RSXML)
110 |
111 | - (BOOL)rs_scanEntityValue:(NSString * _Nullable * _Nullable)decodedEntity {
112 |
113 | NSString *s = self.string;
114 | NSUInteger initialScanLocation = self.scanLocation;
115 | static NSUInteger maxEntityLength = 20; // It’s probably smaller, but this is just for sanity.
116 |
117 | while (true) {
118 |
119 | unichar ch = [s characterAtIndex:self.scanLocation];
120 | if ([NSCharacterSet.whitespaceAndNewlineCharacterSet characterIsMember:ch]) {
121 | break;
122 | }
123 | if (ch == ';') {
124 | if (!decodedEntity) {
125 | return YES;
126 | }
127 | NSString *rawEntity = [s substringWithRange:NSMakeRange(initialScanLocation + 1, (self.scanLocation - initialScanLocation) - 1)];
128 | *decodedEntity = [rawEntity rs_stringByDecodingEntity];
129 | self.scanLocation = self.scanLocation + 1;
130 | return *decodedEntity != nil;
131 | }
132 |
133 | self.scanLocation = self.scanLocation + 1;
134 | if (self.scanLocation - initialScanLocation > maxEntityLength) {
135 | break;
136 | }
137 | if (self.isAtEnd) {
138 | break;
139 | }
140 | }
141 |
142 | return NO;
143 | }
144 |
145 | @end
146 |
147 | static NSString *RSXMLStringWithValue(unichar value) {
148 |
149 | return [[NSString alloc] initWithFormat:@"%C", value];
150 | }
151 |
152 | static NSDictionary *RSEntitiesDictionary(void) {
153 |
154 | static NSDictionary *entitiesDictionary = nil;
155 |
156 | static dispatch_once_t onceToken;
157 | dispatch_once(&onceToken, ^{
158 |
159 | entitiesDictionary =
160 | @{@"#034": @"\"",
161 | @"#038": @"&",
162 | @"#38": @"&",
163 | @"#039": @"'",
164 | @"#145": @"‘",
165 | @"#146": @"’",
166 | @"#147": @"“",
167 | @"#148": @"”",
168 | @"#149": @"•",
169 | @"#150": @"-",
170 | @"#151": @"—",
171 | @"#153": @"™",
172 | @"#160": RSXMLStringWithValue(160),
173 | @"#161": @"¡",
174 | @"#162": @"¢",
175 | @"#163": @"£",
176 | @"#164": @"?",
177 | @"#165": @"¥",
178 | @"#166": @"?",
179 | @"#167": @"§",
180 | @"#168": @"¨",
181 | @"#169": @"©",
182 | @"#170": @"©",
183 | @"#171": @"«",
184 | @"#172": @"¬",
185 | @"#173": @"¬",
186 | @"#174": @"®",
187 | @"#175": @"¯",
188 | @"#176": @"°",
189 | @"#177": @"±",
190 | @"#178": @" ",
191 | @"#179": @" ",
192 | @"#180": @"´",
193 | @"#181": @"µ",
194 | @"#182": @"µ",
195 | @"#183": @"·",
196 | @"#184": @"¸",
197 | @"#185": @" ",
198 | @"#186": @"º",
199 | @"#187": @"»",
200 | @"#188": @"1/4",
201 | @"#189": @"1/2",
202 | @"#190": @"1/2",
203 | @"#191": @"¿",
204 | @"#192": @"À",
205 | @"#193": @"Á",
206 | @"#194": @"Â",
207 | @"#195": @"Ã",
208 | @"#196": @"Ä",
209 | @"#197": @"Å",
210 | @"#198": @"Æ",
211 | @"#199": @"Ç",
212 | @"#200": @"È",
213 | @"#201": @"É",
214 | @"#202": @"Ê",
215 | @"#203": @"Ë",
216 | @"#204": @"Ì",
217 | @"#205": @"Í",
218 | @"#206": @"Î",
219 | @"#207": @"Ï",
220 | @"#208": @"?",
221 | @"#209": @"Ñ",
222 | @"#210": @"Ò",
223 | @"#211": @"Ó",
224 | @"#212": @"Ô",
225 | @"#213": @"Õ",
226 | @"#214": @"Ö",
227 | @"#215": @"x",
228 | @"#216": @"Ø",
229 | @"#217": @"Ù",
230 | @"#218": @"Ú",
231 | @"#219": @"Û",
232 | @"#220": @"Ü",
233 | @"#221": @"Y",
234 | @"#222": @"?",
235 | @"#223": @"ß",
236 | @"#224": @"à",
237 | @"#225": @"á",
238 | @"#226": @"â",
239 | @"#227": @"ã",
240 | @"#228": @"ä",
241 | @"#229": @"å",
242 | @"#230": @"æ",
243 | @"#231": @"ç",
244 | @"#232": @"è",
245 | @"#233": @"é",
246 | @"#234": @"ê",
247 | @"#235": @"ë",
248 | @"#236": @"ì",
249 | @"#237": @"í",
250 | @"#238": @"î",
251 | @"#239": @"ï",
252 | @"#240": @"?",
253 | @"#241": @"ñ",
254 | @"#242": @"ò",
255 | @"#243": @"ó",
256 | @"#244": @"ô",
257 | @"#245": @"õ",
258 | @"#246": @"ö",
259 | @"#247": @"÷",
260 | @"#248": @"ø",
261 | @"#249": @"ù",
262 | @"#250": @"ú",
263 | @"#251": @"û",
264 | @"#252": @"ü",
265 | @"#253": @"y",
266 | @"#254": @"?",
267 | @"#255": @"ÿ",
268 | @"#32": @" ",
269 | @"#34": @"\"",
270 | @"#39": @"",
271 | @"#8194": @" ",
272 | @"#8195": @" ",
273 | @"#8211": @"-",
274 | @"#8212": @"—",
275 | @"#8216": @"‘",
276 | @"#8217": @"’",
277 | @"#8220": @"“",
278 | @"#8221": @"”",
279 | @"#8230": @"…",
280 | @"#8617": RSXMLStringWithValue(8617),
281 | @"AElig": @"Æ",
282 | @"Aacute": @"Á",
283 | @"Acirc": @"Â",
284 | @"Agrave": @"À",
285 | @"Aring": @"Å",
286 | @"Atilde": @"Ã",
287 | @"Auml": @"Ä",
288 | @"Ccedil": @"Ç",
289 | @"Dstrok": @"?",
290 | @"ETH": @"?",
291 | @"Eacute": @"É",
292 | @"Ecirc": @"Ê",
293 | @"Egrave": @"È",
294 | @"Euml": @"Ë",
295 | @"Iacute": @"Í",
296 | @"Icirc": @"Î",
297 | @"Igrave": @"Ì",
298 | @"Iuml": @"Ï",
299 | @"Ntilde": @"Ñ",
300 | @"Oacute": @"Ó",
301 | @"Ocirc": @"Ô",
302 | @"Ograve": @"Ò",
303 | @"Oslash": @"Ø",
304 | @"Otilde": @"Õ",
305 | @"Ouml": @"Ö",
306 | @"Pi": @"Π",
307 | @"THORN": @"?",
308 | @"Uacute": @"Ú",
309 | @"Ucirc": @"Û",
310 | @"Ugrave": @"Ù",
311 | @"Uuml": @"Ü",
312 | @"Yacute": @"Y",
313 | @"aacute": @"á",
314 | @"acirc": @"â",
315 | @"acute": @"´",
316 | @"aelig": @"æ",
317 | @"agrave": @"à",
318 | @"amp": @"&",
319 | @"apos": @"'",
320 | @"aring": @"å",
321 | @"atilde": @"ã",
322 | @"auml": @"ä",
323 | @"brkbar": @"?",
324 | @"brvbar": @"?",
325 | @"ccedil": @"ç",
326 | @"cedil": @"¸",
327 | @"cent": @"¢",
328 | @"copy": @"©",
329 | @"curren": @"?",
330 | @"deg": @"°",
331 | @"die": @"?",
332 | @"divide": @"÷",
333 | @"eacute": @"é",
334 | @"ecirc": @"ê",
335 | @"egrave": @"è",
336 | @"eth": @"?",
337 | @"euml": @"ë",
338 | @"euro": @"€",
339 | @"frac12": @"1/2",
340 | @"frac14": @"1/4",
341 | @"frac34": @"3/4",
342 | @"gt": @">",
343 | @"hearts": @"♥",
344 | @"hellip": @"…",
345 | @"iacute": @"í",
346 | @"icirc": @"î",
347 | @"iexcl": @"¡",
348 | @"igrave": @"ì",
349 | @"iquest": @"¿",
350 | @"iuml": @"ï",
351 | @"laquo": @"«",
352 | @"ldquo": @"“",
353 | @"lsquo": @"‘",
354 | @"lt": @"<",
355 | @"macr": @"¯",
356 | @"mdash": @"—",
357 | @"micro": @"µ",
358 | @"middot": @"·",
359 | @"ndash": @"-",
360 | @"not": @"¬",
361 | @"ntilde": @"ñ",
362 | @"oacute": @"ó",
363 | @"ocirc": @"ô",
364 | @"ograve": @"ò",
365 | @"ordf": @"ª",
366 | @"ordm": @"º",
367 | @"oslash": @"ø",
368 | @"otilde": @"õ",
369 | @"ouml": @"ö",
370 | @"para": @"¶",
371 | @"pi": @"π",
372 | @"plusmn": @"±",
373 | @"pound": @"£",
374 | @"quot": @"\"",
375 | @"raquo": @"»",
376 | @"rdquo": @"”",
377 | @"reg": @"®",
378 | @"rsquo": @"’",
379 | @"sect": @"§",
380 | @"shy": @" ",
381 | @"sup1": @" ",
382 | @"sup2": @" ",
383 | @"sup3": @" ",
384 | @"szlig": @"ß",
385 | @"thorn": @"?",
386 | @"times": @"x",
387 | @"trade": @"™",
388 | @"uacute": @"ú",
389 | @"ucirc": @"û",
390 | @"ugrave": @"ù",
391 | @"uml": @"¨",
392 | @"uuml": @"ü",
393 | @"yacute": @"y",
394 | @"yen": @"¥",
395 | @"yuml": @"ÿ",
396 | @"infin": @"∞",
397 | @"nbsp": RSXMLStringWithValue(160),
398 | @"#x21A9": RSXMLStringWithValue(8617),
399 | @"#xFE0E": RSXMLStringWithValue(65038),
400 | @"#x2019": RSXMLStringWithValue(8217),
401 | @"#x2026": RSXMLStringWithValue(8230),
402 | @"#x201C": RSXMLStringWithValue(8220),
403 | @"#x201D": RSXMLStringWithValue(8221),
404 | @"#x2014": RSXMLStringWithValue(8212)};
405 | });
406 |
407 | return entitiesDictionary;
408 | }
409 |
--------------------------------------------------------------------------------
/RSXML/RSAtomParser.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSAtomParser.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 1/15/15.
6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
7 | //
8 |
9 | #import "FeedParser.h"
10 |
11 | @interface RSAtomParser : NSObject
12 |
13 | @end
14 |
--------------------------------------------------------------------------------
/RSXML/RSAtomParser.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSAtomParser.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 1/15/15.
6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "RSAtomParser.h"
11 | #import "RSSAXParser.h"
12 | #import "FeedParser.h"
13 | #import "RSParsedFeed.h"
14 | #import "RSParsedArticle.h"
15 | #import "RSXMLData.h"
16 | #import "NSString+RSXML.h"
17 | #import "RSDateParser.h"
18 |
19 |
20 | @interface RSAtomParser ()
21 |
22 | @property (nonatomic) NSData *feedData;
23 | @property (nonatomic) NSString *urlString;
24 | @property (nonatomic) BOOL endFeedFound;
25 | @property (nonatomic) BOOL parsingXHTML;
26 | @property (nonatomic) BOOL parsingSource;
27 | @property (nonatomic) BOOL parsingArticle;
28 | @property (nonatomic) BOOL parsingAuthor;
29 | @property (nonatomic) NSMutableArray *attributesStack;
30 | @property (nonatomic, readonly) NSDictionary *currentAttributes;
31 | @property (nonatomic) NSMutableString *xhtmlString;
32 | @property (nonatomic) NSString *link;
33 | @property (nonatomic) NSString *title;
34 | @property (nonatomic) NSMutableArray *articles;
35 | @property (nonatomic) NSDate *dateParsed;
36 | @property (nonatomic) RSSAXParser *parser;
37 | @property (nonatomic, readonly) RSParsedArticle *currentArticle;
38 | @property (nonatomic, readonly) NSDate *currentDate;
39 |
40 | @end
41 |
42 |
43 | @implementation RSAtomParser
44 |
45 | #pragma mark - Class Methods
46 |
47 | + (BOOL)canParseFeed:(RSXMLData *)xmlData {
48 |
49 | // Checking for ' entryRange.location) {
79 | return NO; // Wrong order.
80 | }
81 | }
82 |
83 | return YES;
84 | }
85 |
86 |
87 | #pragma mark - Init
88 |
89 | - (instancetype)initWithXMLData:(RSXMLData *)xmlData {
90 |
91 | self = [super init];
92 | if (!self) {
93 | return nil;
94 | }
95 |
96 | _feedData = xmlData.data;
97 | _urlString = xmlData.urlString;
98 | _parser = [[RSSAXParser alloc] initWithDelegate:self];
99 | _attributesStack = [NSMutableArray new];
100 | _articles = [NSMutableArray new];
101 |
102 | return self;
103 | }
104 |
105 |
106 | #pragma mark - API
107 |
108 | - (RSParsedFeed *)parseFeed:(NSError **)error {
109 |
110 | [self parse];
111 |
112 | RSParsedFeed *parsedFeed = [[RSParsedFeed alloc] initWithURLString:self.urlString title:self.title link:self.link articles:self.articles];
113 |
114 | return parsedFeed;
115 | }
116 |
117 |
118 | #pragma mark - Constants
119 |
120 | static NSString *kTypeKey = @"type";
121 | static NSString *kXHTMLType = @"xhtml";
122 | static NSString *kRelKey = @"rel";
123 | static NSString *kAlternateValue = @"alternate";
124 | static NSString *kHrefKey = @"href";
125 | static NSString *kXMLKey = @"xml";
126 | static NSString *kBaseKey = @"base";
127 | static NSString *kLangKey = @"lang";
128 | static NSString *kXMLBaseKey = @"xml:base";
129 | static NSString *kXMLLangKey = @"xml:lang";
130 | static NSString *kTextHTMLValue = @"text/html";
131 | static NSString *kRelatedValue = @"related";
132 | static NSString *kShortURLValue = @"shorturl";
133 | static NSString *kHTMLValue = @"html";
134 | static NSString *kEnValue = @"en";
135 | static NSString *kTextValue = @"text";
136 | static NSString *kSelfValue = @"self";
137 |
138 | static const char *kID = "id";
139 | static const NSInteger kIDLength = 3;
140 |
141 | static const char *kTitle = "title";
142 | static const NSInteger kTitleLength = 6;
143 |
144 | static const char *kContent = "content";
145 | static const NSInteger kContentLength = 8;
146 |
147 | static const char *kSummary = "summary";
148 | static const NSInteger kSummaryLength = 8;
149 |
150 | static const char *kLink = "link";
151 | static const NSInteger kLinkLength = 5;
152 |
153 | static const char *kPublished = "published";
154 | static const NSInteger kPublishedLength = 10;
155 |
156 | static const char *kUpdated = "updated";
157 | static const NSInteger kUpdatedLength = 8;
158 |
159 | static const char *kAuthor = "author";
160 | static const NSInteger kAuthorLength = 7;
161 |
162 | static const char *kEntry = "entry";
163 | static const NSInteger kEntryLength = 6;
164 |
165 | static const char *kSource = "source";
166 | static const NSInteger kSourceLength = 7;
167 |
168 | static const char *kFeed = "feed";
169 | static const NSInteger kFeedLength = 5;
170 |
171 | static const char *kType = "type";
172 | static const NSInteger kTypeLength = 5;
173 |
174 | static const char *kRel = "rel";
175 | static const NSInteger kRelLength = 4;
176 |
177 | static const char *kAlternate = "alternate";
178 | static const NSInteger kAlternateLength = 10;
179 |
180 | static const char *kHref = "href";
181 | static const NSInteger kHrefLength = 5;
182 |
183 | static const char *kXML = "xml";
184 | static const NSInteger kXMLLength = 4;
185 |
186 | static const char *kBase = "base";
187 | static const NSInteger kBaseLength = 5;
188 |
189 | static const char *kLang = "lang";
190 | static const NSInteger kLangLength = 5;
191 |
192 | static const char *kTextHTML = "text/html";
193 | static const NSInteger kTextHTMLLength = 10;
194 |
195 | static const char *kRelated = "related";
196 | static const NSInteger kRelatedLength = 8;
197 |
198 | static const char *kShortURL = "shorturl";
199 | static const NSInteger kShortURLLength = 9;
200 |
201 | static const char *kHTML = "html";
202 | static const NSInteger kHTMLLength = 5;
203 |
204 | static const char *kEn = "en";
205 | static const NSInteger kEnLength = 3;
206 |
207 | static const char *kText = "text";
208 | static const NSInteger kTextLength = 5;
209 |
210 | static const char *kSelf = "self";
211 | static const NSInteger kSelfLength = 5;
212 |
213 |
214 | #pragma mark - Parsing
215 |
216 | - (void)parse {
217 |
218 | self.dateParsed = [NSDate date];
219 |
220 | @autoreleasepool {
221 | [self.parser parseData:self.feedData];
222 | [self.parser finishParsing];
223 | }
224 |
225 | // Optimization: make articles do calculations on this background thread.
226 | [self.articles makeObjectsPerformSelector:@selector(calculateArticleID)];
227 | }
228 |
229 |
230 | - (void)addArticle {
231 |
232 | RSParsedArticle *article = [[RSParsedArticle alloc] initWithFeedURL:self.urlString];
233 | article.dateParsed = self.dateParsed;
234 |
235 | [self.articles addObject:article];
236 | }
237 |
238 |
239 | - (RSParsedArticle *)currentArticle {
240 |
241 | return self.articles.lastObject;
242 | }
243 |
244 |
245 | - (NSDictionary *)currentAttributes {
246 |
247 | return self.attributesStack.lastObject;
248 | }
249 |
250 |
251 | - (NSDate *)currentDate {
252 |
253 | return RSDateWithBytes(self.parser.currentCharacters.bytes, self.parser.currentCharacters.length);
254 | }
255 |
256 |
257 | - (void)addFeedLink {
258 |
259 | if (self.link && self.link.length > 0) {
260 | return;
261 | }
262 |
263 | NSString *related = self.currentAttributes[kRelKey];
264 | if (related == kAlternateValue) {
265 | self.link = self.currentAttributes[kHrefKey];
266 | }
267 | }
268 |
269 |
270 | - (void)addFeedTitle {
271 |
272 | if (self.title.length < 1) {
273 | self.title = self.parser.currentStringWithTrimmedWhitespace;
274 | }
275 | }
276 |
277 | - (void)addLink {
278 |
279 | NSString *urlString = self.currentAttributes[kHrefKey];
280 | if (urlString.length < 1) {
281 | return;
282 | }
283 |
284 | NSString *rel = self.currentAttributes[kRelKey];
285 | if (rel.length < 1) {
286 | rel = kAlternateValue;
287 | }
288 |
289 | if (rel == kAlternateValue) {
290 | if (!self.currentArticle.link) {
291 | self.currentArticle.link = urlString;
292 | }
293 | }
294 | else if (rel == kRelatedValue) {
295 | if (!self.currentArticle.permalink) {
296 | self.currentArticle.permalink = urlString;
297 | }
298 | }
299 | }
300 |
301 |
302 | - (void)addContent {
303 |
304 | self.currentArticle.body = [self currentStringWithHTMLEntitiesDecoded];
305 | }
306 |
307 |
308 | - (void)addSummary {
309 |
310 | if (!self.currentArticle.body) {
311 | self.currentArticle.body = [self currentStringWithHTMLEntitiesDecoded];
312 | }
313 | }
314 |
315 |
316 | - (NSString *)currentStringWithHTMLEntitiesDecoded {
317 |
318 | return [self.parser.currentStringWithTrimmedWhitespace rs_stringByDecodingHTMLEntities];
319 | }
320 |
321 |
322 | - (void)addArticleElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix {
323 |
324 | if (prefix) {
325 | return;
326 | }
327 |
328 | if (RSSAXEqualTags(localName, kID, kIDLength)) {
329 | self.currentArticle.guid = self.parser.currentStringWithTrimmedWhitespace;
330 | }
331 |
332 | else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) {
333 | self.currentArticle.title = [self currentStringWithHTMLEntitiesDecoded];
334 | }
335 |
336 | else if (RSSAXEqualTags(localName, kContent, kContentLength)) {
337 | [self addContent];
338 | }
339 |
340 | else if (RSSAXEqualTags(localName, kSummary, kSummaryLength)) {
341 | [self addSummary];
342 | }
343 |
344 | else if (RSSAXEqualTags(localName, kLink, kLinkLength)) {
345 | [self addLink];
346 | }
347 |
348 | else if (RSSAXEqualTags(localName, kPublished, kPublishedLength)) {
349 | self.currentArticle.datePublished = self.currentDate;
350 | }
351 |
352 | else if (RSSAXEqualTags(localName, kUpdated, kUpdatedLength)) {
353 | self.currentArticle.dateModified = self.currentDate;
354 | }
355 | }
356 |
357 |
358 | - (void)addXHTMLTag:(const xmlChar *)localName {
359 |
360 | if (!localName) {
361 | return;
362 | }
363 |
364 | [self.xhtmlString appendString:@"<"];
365 | [self.xhtmlString appendString:[NSString stringWithUTF8String:(const char *)localName]];
366 |
367 | if (self.currentAttributes.count < 1) {
368 | [self.xhtmlString appendString:@">"];
369 | return;
370 | }
371 |
372 | for (NSString *oneKey in self.currentAttributes) {
373 |
374 | [self.xhtmlString appendString:@" "];
375 |
376 | NSString *oneValue = self.currentAttributes[oneKey];
377 | [self.xhtmlString appendString:oneKey];
378 |
379 | [self.xhtmlString appendString:@"=\""];
380 |
381 | oneValue = [oneValue stringByReplacingOccurrencesOfString:@"\"" withString:@"""];
382 | [self.xhtmlString appendString:oneValue];
383 |
384 | [self.xhtmlString appendString:@"\""];
385 | }
386 |
387 | [self.xhtmlString appendString:@">"];
388 | }
389 |
390 |
391 | #pragma mark - RSSAXParserDelegate
392 |
393 | - (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes {
394 |
395 | if (self.endFeedFound) {
396 | return;
397 | }
398 |
399 | NSDictionary *xmlAttributes = [self.parser attributesDictionary:attributes numberOfAttributes:numberOfAttributes];
400 | if (!xmlAttributes) {
401 | xmlAttributes = [NSDictionary dictionary];
402 | }
403 | [self.attributesStack addObject:xmlAttributes];
404 |
405 | if (self.parsingXHTML) {
406 | [self addXHTMLTag:localName];
407 | return;
408 | }
409 |
410 | if (RSSAXEqualTags(localName, kEntry, kEntryLength)) {
411 | self.parsingArticle = YES;
412 | [self addArticle];
413 | return;
414 | }
415 |
416 | if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) {
417 | self.parsingAuthor = YES;
418 | return;
419 | }
420 |
421 | if (RSSAXEqualTags(localName, kSource, kSourceLength)) {
422 | self.parsingSource = YES;
423 | return;
424 | }
425 |
426 | BOOL isContentTag = RSSAXEqualTags(localName, kContent, kContentLength);
427 | BOOL isSummaryTag = RSSAXEqualTags(localName, kSummary, kSummaryLength);
428 | if (self.parsingArticle && (isContentTag || isSummaryTag)) {
429 |
430 | NSString *contentType = xmlAttributes[kTypeKey];
431 | if ([contentType isEqualToString:kXHTMLType]) {
432 | self.parsingXHTML = YES;
433 | self.xhtmlString = [NSMutableString stringWithString:@""];
434 | return;
435 | }
436 | }
437 |
438 | if (!self.parsingArticle && RSSAXEqualTags(localName, kLink, kLinkLength)) {
439 | [self addFeedLink];
440 | return;
441 | }
442 |
443 | [self.parser beginStoringCharacters];
444 | }
445 |
446 |
447 | - (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri {
448 |
449 | if (RSSAXEqualTags(localName, kFeed, kFeedLength)) {
450 | self.endFeedFound = YES;
451 | return;
452 | }
453 |
454 | if (self.endFeedFound) {
455 | return;
456 | }
457 |
458 | if (self.parsingXHTML) {
459 |
460 | BOOL isContentTag = RSSAXEqualTags(localName, kContent, kContentLength);
461 | BOOL isSummaryTag = RSSAXEqualTags(localName, kSummary, kSummaryLength);
462 |
463 | if (self.parsingArticle && (isContentTag || isSummaryTag)) {
464 |
465 | if (isContentTag) {
466 | self.currentArticle.body = [self.xhtmlString copy];
467 | }
468 |
469 | else if (isSummaryTag) {
470 | if (self.currentArticle.body.length < 1) {
471 | self.currentArticle.body = [self.xhtmlString copy];
472 | }
473 | }
474 | }
475 |
476 | if (isContentTag || isSummaryTag) {
477 | self.parsingXHTML = NO;
478 | }
479 |
480 | [self.xhtmlString appendString:@""];
481 | [self.xhtmlString appendString:[NSString stringWithUTF8String:(const char *)localName]];
482 | [self.xhtmlString appendString:@">"];
483 | }
484 |
485 | else if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) {
486 | self.parsingAuthor = NO;
487 | }
488 |
489 | else if (RSSAXEqualTags(localName, kEntry, kEntryLength)) {
490 | self.parsingArticle = NO;
491 | }
492 |
493 | else if (self.parsingArticle && !self.parsingSource) {
494 | [self addArticleElement:localName prefix:prefix];
495 | }
496 |
497 | else if (RSSAXEqualTags(localName, kSource, kSourceLength)) {
498 | self.parsingSource = NO;
499 | }
500 |
501 | else if (!self.parsingArticle && !self.parsingSource && RSSAXEqualTags(localName, kTitle, kTitleLength)) {
502 | [self addFeedTitle];
503 | }
504 | [self.attributesStack removeLastObject];
505 | }
506 |
507 |
508 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix {
509 |
510 | if (prefix && RSSAXEqualTags(prefix, kXML, kXMLLength)) {
511 |
512 | if (RSSAXEqualTags(name, kBase, kBaseLength)) {
513 | return kXMLBaseKey;
514 | }
515 | if (RSSAXEqualTags(name, kLang, kLangLength)) {
516 | return kXMLLangKey;
517 | }
518 | }
519 |
520 | if (prefix) {
521 | return nil;
522 | }
523 |
524 | if (RSSAXEqualTags(name, kRel, kRelLength)) {
525 | return kRelKey;
526 | }
527 |
528 | if (RSSAXEqualTags(name, kType, kTypeLength)) {
529 | return kTypeKey;
530 | }
531 |
532 | if (RSSAXEqualTags(name, kHref, kHrefLength)) {
533 | return kHrefKey;
534 | }
535 |
536 | if (RSSAXEqualTags(name, kAlternate, kAlternateLength)) {
537 | return kAlternateValue;
538 | }
539 |
540 | return nil;
541 | }
542 |
543 |
544 | static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
545 |
546 | return memcmp(bytes1, bytes2, length) == 0;
547 | }
548 |
549 |
550 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length {
551 |
552 | static const NSUInteger alternateLength = kAlternateLength - 1;
553 | static const NSUInteger textHTMLLength = kTextHTMLLength - 1;
554 | static const NSUInteger relatedLength = kRelatedLength - 1;
555 | static const NSUInteger shortURLLength = kShortURLLength - 1;
556 | static const NSUInteger htmlLength = kHTMLLength - 1;
557 | static const NSUInteger enLength = kEnLength - 1;
558 | static const NSUInteger textLength = kTextLength - 1;
559 | static const NSUInteger selfLength = kSelfLength - 1;
560 |
561 | if (length == alternateLength && equalBytes(bytes, kAlternate, alternateLength)) {
562 | return kAlternateValue;
563 | }
564 |
565 | if (length == textHTMLLength && equalBytes(bytes, kTextHTML, textHTMLLength)) {
566 | return kTextHTMLValue;
567 | }
568 |
569 | if (length == relatedLength && equalBytes(bytes, kRelated, relatedLength)) {
570 | return kRelatedValue;
571 | }
572 |
573 | if (length == shortURLLength && equalBytes(bytes, kShortURL, shortURLLength)) {
574 | return kShortURLValue;
575 | }
576 |
577 | if (length == htmlLength && equalBytes(bytes, kHTML, htmlLength)) {
578 | return kHTMLValue;
579 | }
580 |
581 | if (length == enLength && equalBytes(bytes, kEn, enLength)) {
582 | return kEnValue;
583 | }
584 |
585 | if (length == textLength && equalBytes(bytes, kText, textLength)) {
586 | return kTextValue;
587 | }
588 |
589 | if (length == selfLength && equalBytes(bytes, kSelf, selfLength)) {
590 | return kSelfValue;
591 | }
592 |
593 | return nil;
594 | }
595 |
596 |
597 | - (void)saxParser:(RSSAXParser *)SAXParser XMLCharactersFound:(const unsigned char *)characters length:(NSUInteger)length {
598 |
599 | if (self.parsingXHTML) {
600 | [self.xhtmlString appendString:[[NSString alloc] initWithBytesNoCopy:(void *)characters length:length encoding:NSUTF8StringEncoding freeWhenDone:NO]];
601 | }
602 | }
603 |
604 | @end
605 |
--------------------------------------------------------------------------------
/RSXML/RSDateParser.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSDateParser.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 3/25/15.
6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 |
12 | /*Common web dates -- RFC 822 and 8601 -- are handled here:
13 | the formats you find in JSON and XML feeds.
14 |
15 | Any of these may return nil. They may also return garbage, given bad input.*/
16 |
17 |
18 | NSDate *RSDateWithString(NSString *dateString);
19 |
20 | /*If you're using a SAX parser, you have the bytes and don't need to convert to a string first.
21 | It's faster and uses less memory.
22 | (Assumes bytes are UTF-8 or ASCII. If you're using the libxml SAX parser, this will work.)*/
23 |
24 | NSDate *RSDateWithBytes(const char *bytes, NSUInteger numberOfBytes);
25 |
26 |
--------------------------------------------------------------------------------
/RSXML/RSDateParser.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSDateParser.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 3/25/15.
6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "RSDateParser.h"
11 |
12 | typedef struct {
13 | const char *abbreviation;
14 | const NSInteger offsetHours;
15 | const NSInteger offsetMinutes;
16 | } RSTimeZoneAbbreviationAndOffset;
17 |
18 |
19 | #define kNumberOfTimeZones 96
20 |
21 | static const RSTimeZoneAbbreviationAndOffset timeZoneTable[kNumberOfTimeZones] = {
22 | {"GMT", 0, 0}, //Most common at top, for performance
23 | {"PDT", -7, 0}, {"PST", -8, 0}, {"EST", -5, 0}, {"EDT", -4, 0},
24 | {"MDT", -6, 0}, {"MST", -7, 0}, {"CST", -6, 0}, {"CDT", -5, 0},
25 | {"ACT", -8, 0}, {"AFT", 4, 30}, {"AMT", 4, 0}, {"ART", -3, 0},
26 | {"AST", 3, 0}, {"AZT", 4, 0}, {"BIT", -12, 0}, {"BDT", 8, 0},
27 | {"ACST", 9, 30}, {"AEST", 10, 0}, {"AKST", -9, 0}, {"AMST", 5, 0},
28 | {"AWST", 8, 0}, {"AZOST", -1, 0}, {"BIOT", 6, 0}, {"BRT", -3, 0},
29 | {"BST", 6, 0}, {"BTT", 6, 0}, {"CAT", 2, 0}, {"CCT", 6, 30},
30 | {"CET", 1, 0}, {"CEST", 2, 0}, {"CHAST", 12, 45}, {"ChST", 10, 0},
31 | {"CIST", -8, 0}, {"CKT", -10, 0}, {"CLT", -4, 0}, {"CLST", -3, 0},
32 | {"COT", -5, 0}, {"COST", -4, 0}, {"CVT", -1, 0}, {"CXT", 7, 0},
33 | {"EAST", -6, 0}, {"EAT", 3, 0}, {"ECT", -4, 0}, {"EEST", 3, 0},
34 | {"EET", 2, 0}, {"FJT", 12, 0}, {"FKST", -4, 0}, {"GALT", -6, 0},
35 | {"GET", 4, 0}, {"GFT", -3, 0}, {"GILT", 7, 0}, {"GIT", -9, 0},
36 | {"GST", -2, 0}, {"GYT", -4, 0}, {"HAST", -10, 0}, {"HKT", 8, 0},
37 | {"HMT", 5, 0}, {"IRKT", 8, 0}, {"IRST", 3, 30}, {"IST", 2, 0},
38 | {"JST", 9, 0}, {"KRAT", 7, 0}, {"KST", 9, 0}, {"LHST", 10, 30},
39 | {"LINT", 14, 0}, {"MAGT", 11, 0}, {"MIT", -9, 30}, {"MSK", 3, 0},
40 | {"MUT", 4, 0}, {"NDT", -2, 30}, {"NFT", 11, 30}, {"NPT", 5, 45},
41 | {"NT", -3, 30}, {"OMST", 6, 0}, {"PETT", 12, 0}, {"PHOT", 13, 0},
42 | {"PKT", 5, 0}, {"RET", 4, 0}, {"SAMT", 4, 0}, {"SAST", 2, 0},
43 | {"SBT", 11, 0}, {"SCT", 4, 0}, {"SLT", 5, 30}, {"SST", 8, 0},
44 | {"TAHT", -10, 0}, {"THA", 7, 0}, {"UYT", -3, 0}, {"UYST", -2, 0},
45 | {"VET", -4, 30}, {"VLAT", 10, 0}, {"WAT", 1, 0}, {"WET", 0, 0},
46 | {"WEST", 1, 0}, {"YAKT", 9, 0}, {"YEKT", 5, 0}
47 | }; /*See http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations for list*/
48 |
49 |
50 |
51 | #pragma mark - Parser
52 |
53 | enum {
54 | RSJanuary = 1,
55 | RSFebruary,
56 | RSMarch,
57 | RSApril,
58 | RSMay,
59 | RSJune,
60 | RSJuly,
61 | RSAugust,
62 | RSSeptember,
63 | RSOctober,
64 | RSNovember,
65 | RSDecember
66 | };
67 |
68 | static NSInteger nextMonthValue(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex, NSUInteger *finalIndex) {
69 |
70 | /*Months are 1-based -- January is 1, Dec is 12.
71 | Lots of short-circuits here. Not strict. GIGO.*/
72 |
73 | NSUInteger i;// = startingIndex;
74 | NSUInteger numberOfAlphaCharactersFound = 0;
75 | char monthCharacters[3] = {0, 0, 0};
76 |
77 | for (i = startingIndex; i < numberOfBytes; i++) {
78 |
79 | *finalIndex = i;
80 | char character = bytes[i];
81 |
82 | BOOL isAlphaCharacter = (BOOL)isalpha(character);
83 | if (!isAlphaCharacter && numberOfAlphaCharactersFound < 1)
84 | continue;
85 | if (!isAlphaCharacter && numberOfAlphaCharactersFound > 0)
86 | break;
87 |
88 | numberOfAlphaCharactersFound++;
89 | if (numberOfAlphaCharactersFound == 1) {
90 | if (character == 'F' || character == 'f')
91 | return RSFebruary;
92 | if (character == 'S' || character == 's')
93 | return RSSeptember;
94 | if (character == 'O' || character == 'o')
95 | return RSOctober;
96 | if (character == 'N' || character == 'n')
97 | return RSNovember;
98 | if (character == 'D' || character == 'd')
99 | return RSDecember;
100 | }
101 |
102 | monthCharacters[numberOfAlphaCharactersFound - 1] = character;
103 | if (numberOfAlphaCharactersFound >=3)
104 | break;
105 | }
106 |
107 | if (numberOfAlphaCharactersFound < 2)
108 | return NSNotFound;
109 |
110 | if (monthCharacters[0] == 'J' || monthCharacters[0] == 'j') { //Jan, Jun, Jul
111 | if (monthCharacters[1] == 'a' || monthCharacters[i] == 'A')
112 | return RSJanuary;
113 | if (monthCharacters[1] == 'u' || monthCharacters[1] == 'U') {
114 | if (monthCharacters[2] == 'n' || monthCharacters[2] == 'N')
115 | return RSJune;
116 | return RSJuly;
117 | }
118 | return RSJanuary;
119 | }
120 |
121 | if (monthCharacters[0] == 'M' || monthCharacters[0] == 'm') { //March, May
122 | if (monthCharacters[2] == 'y' || monthCharacters[2] == 'Y')
123 | return RSMay;
124 | return RSMarch;
125 | }
126 |
127 | if (monthCharacters[0] == 'A' || monthCharacters[0] == 'a') { //April, August
128 | if (monthCharacters[1] == 'u' || monthCharacters[1] == 'U')
129 | return RSAugust;
130 | return RSApril;
131 | }
132 |
133 | return RSJanuary; //should never get here
134 | }
135 |
136 |
137 | static NSInteger nextNumericValue(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex, NSUInteger maximumNumberOfDigits, NSUInteger *finalIndex) {
138 |
139 | /*maximumNumberOfDigits has a maximum limit of 4 (for time zone offsets and years).
140 | *finalIndex will be the index of the last character looked at.*/
141 |
142 | if (maximumNumberOfDigits > 4)
143 | maximumNumberOfDigits = 4;
144 |
145 | NSUInteger i = 0;
146 | NSUInteger numberOfDigitsFound = 0;
147 | NSInteger digits[4] = {0, 0, 0, 0};
148 |
149 | for (i = startingIndex; i < numberOfBytes; i++) {
150 | *finalIndex = i;
151 | BOOL isDigit = (BOOL)isdigit(bytes[i]);
152 | if (!isDigit && numberOfDigitsFound < 1)
153 | continue;
154 | if (!isDigit && numberOfDigitsFound > 0)
155 | break;
156 | digits[numberOfDigitsFound] = bytes[i] - 48; // '0' is 48
157 | numberOfDigitsFound++;
158 | if (numberOfDigitsFound >= maximumNumberOfDigits)
159 | break;
160 | }
161 |
162 | if (numberOfDigitsFound < 1)
163 | return NSNotFound;
164 | if (numberOfDigitsFound == 1)
165 | return digits[0];
166 | if (numberOfDigitsFound == 2)
167 | return (digits[0] * 10) + digits[1];
168 | if (numberOfDigitsFound == 3)
169 | return (digits[0] * 100) + (digits[1] * 10) + digits[2];
170 | return (digits[0] * 1000) + (digits[1] * 100) + (digits[2] * 10) + digits[3];
171 | }
172 |
173 |
174 | static BOOL hasAtLeastOneAlphaCharacter(const char *s) {
175 |
176 | NSUInteger length = strlen(s);
177 | NSUInteger i = 0;
178 |
179 | for (i = 0; i < length; i++) {
180 | if (isalpha(s[i]))
181 | return YES;
182 | }
183 |
184 | return NO;
185 | }
186 |
187 |
188 | #pragma mark - Time Zones and offsets
189 |
190 | static NSInteger offsetInSecondsForTimeZoneAbbreviation(const char *abbreviation) {
191 |
192 | /*Linear search should be fine. It's a C array, and short (under 100 items).
193 | Most common time zones are at the beginning of the array. (We can tweak this as needed.)*/
194 |
195 | NSUInteger i;
196 |
197 | for (i = 0; i < kNumberOfTimeZones; i++) {
198 |
199 | RSTimeZoneAbbreviationAndOffset zone = timeZoneTable[i];
200 | if (strcmp(abbreviation, zone.abbreviation) == 0) {
201 | if (zone.offsetHours < 0)
202 | return (zone.offsetHours * 60 * 60) - (zone.offsetMinutes * 60);
203 | return (zone.offsetHours * 60 * 60) + (zone.offsetMinutes * 60);
204 | }
205 | }
206 |
207 | return 0;
208 | }
209 |
210 |
211 | static NSInteger offsetInSecondsForOffsetCharacters(const char *timeZoneCharacters) {
212 |
213 | BOOL isPlus = timeZoneCharacters[0] == '+';
214 | NSUInteger finalIndex = 0;
215 | NSInteger hours = nextNumericValue(timeZoneCharacters, strlen(timeZoneCharacters), 0, 2, &finalIndex);
216 | NSInteger minutes = nextNumericValue(timeZoneCharacters, strlen(timeZoneCharacters), finalIndex + 1, 2, &finalIndex);
217 |
218 | if (hours == NSNotFound)
219 | hours = 0;
220 | if (minutes == NSNotFound)
221 | minutes = 0;
222 | if (hours == 0 && minutes == 0)
223 | return 0;
224 |
225 | NSInteger seconds = (hours * 60 * 60) + (minutes * 60);
226 | if (!isPlus)
227 | seconds = 0 - seconds;
228 | return seconds;
229 | }
230 |
231 |
232 | static const char *rs_GMT = "GMT";
233 | static const char *rs_UTC = "UTC";
234 |
235 | static NSInteger parsedTimeZoneOffset(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex) {
236 |
237 | /*Examples: GMT Z +0000 -0000 +07:00 -0700 PDT EST
238 | Parse into char[5] -- drop any colon characters. If numeric, calculate seconds from GMT.
239 | If alpha, special-case GMT and Z, otherwise look up in time zone list to get offset.*/
240 |
241 | char timeZoneCharacters[6] = {0, 0, 0, 0, 0, 0}; //nil-terminated last character
242 | NSUInteger i = 0;
243 | NSUInteger numberOfCharactersFound = 0;
244 |
245 | for (i = startingIndex; i < numberOfBytes; i++) {
246 | char ch = bytes[i];
247 | if (ch == ':' || ch == ' ')
248 | continue;
249 | if (isdigit(ch) || isalpha(ch) || ch == '+' || ch == '-') {
250 | numberOfCharactersFound++;
251 | timeZoneCharacters[numberOfCharactersFound - 1] = ch;
252 | }
253 | if (numberOfCharactersFound >= 5)
254 | break;
255 | }
256 |
257 | if (numberOfCharactersFound < 1 || timeZoneCharacters[0] == 'Z' || timeZoneCharacters[0] == 'z')
258 | return 0;
259 | if (strcasestr(timeZoneCharacters, rs_GMT) != nil || strcasestr(timeZoneCharacters, rs_UTC))
260 | return 0;
261 |
262 | if (hasAtLeastOneAlphaCharacter(timeZoneCharacters))
263 | return offsetInSecondsForTimeZoneAbbreviation(timeZoneCharacters);
264 | return offsetInSecondsForOffsetCharacters(timeZoneCharacters);
265 | }
266 |
267 |
268 | #pragma mark - Date Creation
269 |
270 | static NSDate *dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(NSInteger year, NSInteger month, NSInteger day, NSInteger hour, NSInteger minute, NSInteger second, NSInteger milliseconds, NSInteger timeZoneOffset) {
271 |
272 | struct tm timeInfo;
273 | timeInfo.tm_sec = (int)second;
274 | timeInfo.tm_min = (int)minute;
275 | timeInfo.tm_hour = (int)hour;
276 | timeInfo.tm_mday = (int)day;
277 | timeInfo.tm_mon = (int)(month - 1); //It's 1-based coming in
278 | timeInfo.tm_year = (int)(year - 1900); //see time.h -- it's years since 1900
279 | timeInfo.tm_wday = -1;
280 | timeInfo.tm_yday = -1;
281 | timeInfo.tm_isdst = -1;
282 | timeInfo.tm_gmtoff = 0;//[timeZone secondsFromGMT];
283 | timeInfo.tm_zone = nil;
284 |
285 | NSTimeInterval rawTime = (NSTimeInterval)(timegm(&timeInfo) - timeZoneOffset); //timegm instead of mktime (which uses local time zone)
286 | if (rawTime == (time_t)ULONG_MAX) {
287 |
288 | /*NSCalendar is super-amazingly-slow (which is partly why RSDateParser exists), so this is used only when the date is far enough in the future (19 January 2038 03:14:08Z on 32-bit systems) that timegm fails. If profiling says that this is a performance issue, then you've got a weird app that needs to work with dates far in the future.*/
289 |
290 | NSDateComponents *dateComponents = [NSDateComponents new];
291 |
292 | dateComponents.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:timeZoneOffset];
293 | dateComponents.year = year;
294 | dateComponents.month = month;
295 | dateComponents.day = day;
296 | dateComponents.hour = hour;
297 | dateComponents.minute = minute;
298 | dateComponents.second = second + (milliseconds / 1000);
299 |
300 | return [[NSCalendar autoupdatingCurrentCalendar] dateFromComponents:dateComponents];
301 | }
302 |
303 | if (milliseconds > 0) {
304 | rawTime += ((float)milliseconds / 1000.0f);
305 | }
306 |
307 | return [NSDate dateWithTimeIntervalSince1970:rawTime];
308 | }
309 |
310 |
311 | #pragma mark - Standard Formats
312 |
313 | static NSDate *RSParsePubDateWithBytes(const char *bytes, NSUInteger numberOfBytes) {
314 |
315 | /*@"EEE',' dd MMM yyyy HH':'mm':'ss ZZZ"
316 | @"EEE, dd MMM yyyy HH:mm:ss zzz"
317 | @"dd MMM yyyy HH:mm zzz"
318 | @"dd MMM yyyy HH:mm ZZZ"
319 | @"EEE, dd MMM yyyy"
320 | @"EEE, dd MMM yyyy HH:mm zzz"
321 | etc.*/
322 |
323 | NSUInteger finalIndex = 0;
324 | NSInteger day = 1;
325 | NSInteger month = RSJanuary;
326 | NSInteger year = 1970;
327 | NSInteger hour = 0;
328 | NSInteger minute = 0;
329 | NSInteger second = 0;
330 | NSInteger timeZoneOffset = 0;
331 |
332 | day = nextNumericValue(bytes, numberOfBytes, 0, 2, &finalIndex);
333 | if (day < 1 || day == NSNotFound)
334 | day = 1;
335 |
336 | month = nextMonthValue(bytes, numberOfBytes, finalIndex + 1, &finalIndex);
337 | year = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 4, &finalIndex);
338 | hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
339 | if (hour == NSNotFound)
340 | hour = 0;
341 |
342 | minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
343 | if (minute == NSNotFound)
344 | minute = 0;
345 |
346 | NSUInteger currentIndex = finalIndex + 1;
347 |
348 | BOOL hasSeconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == ':');
349 | if (hasSeconds)
350 | second = nextNumericValue(bytes, numberOfBytes, currentIndex, 2, &finalIndex);
351 |
352 | currentIndex = finalIndex + 1;
353 | BOOL hasTimeZone = (currentIndex < numberOfBytes) && (bytes[currentIndex] == ' ');
354 | if (hasTimeZone)
355 | timeZoneOffset = parsedTimeZoneOffset(bytes, numberOfBytes, currentIndex);
356 |
357 | return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(year, month, day, hour, minute, second, 0, timeZoneOffset);
358 | }
359 |
360 |
361 | static NSDate *RSParseW3CWithBytes(const char *bytes, NSUInteger numberOfBytes) {
362 |
363 | /*@"yyyy'-'MM'-'dd'T'HH':'mm':'ss"
364 | @"yyyy-MM-dd'T'HH:mm:sszzz"
365 | @"yyyy-MM-dd'T'HH:mm:ss'.'SSSzzz"
366 | etc.*/
367 |
368 | NSUInteger finalIndex = 0;
369 | NSInteger day = 1;
370 | NSInteger month = RSJanuary;
371 | NSInteger year = 1970;
372 | NSInteger hour = 0;
373 | NSInteger minute = 0;
374 | NSInteger second = 0;
375 | NSInteger milliseconds = 0;
376 | NSInteger timeZoneOffset = 0;
377 |
378 | year = nextNumericValue(bytes, numberOfBytes, 0, 4, &finalIndex);
379 | month = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
380 | day = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
381 | hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
382 | minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
383 | second = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
384 |
385 | NSUInteger currentIndex = finalIndex + 1;
386 | BOOL hasMilliseconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == '.');
387 | if (hasMilliseconds) {
388 | milliseconds = nextNumericValue(bytes, numberOfBytes, currentIndex, 3, &finalIndex);
389 | currentIndex = finalIndex + 1;
390 | }
391 |
392 | timeZoneOffset = parsedTimeZoneOffset(bytes, numberOfBytes, currentIndex);
393 |
394 | return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(year, month, day, hour, minute, second, milliseconds, timeZoneOffset);
395 | }
396 |
397 |
398 | static BOOL dateIsPubDate(const char *bytes, NSUInteger numberOfBytes) {
399 |
400 | NSUInteger i = 0;
401 |
402 | for (i = 0; i < numberOfBytes; i++) {
403 | if (bytes[i] == ' ' || bytes[i] == ',')
404 | return YES;
405 | }
406 |
407 | return NO;
408 | }
409 |
410 |
411 | static BOOL numberOfBytesIsOutsideReasonableRange(NSUInteger numberOfBytes) {
412 | return numberOfBytes < 6 || numberOfBytes > 50;
413 | }
414 |
415 |
416 | #pragma mark - API
417 |
418 | NSDate *RSDateWithBytes(const char *bytes, NSUInteger numberOfBytes) {
419 |
420 | if (numberOfBytesIsOutsideReasonableRange(numberOfBytes))
421 | return nil;
422 |
423 | if (dateIsPubDate(bytes, numberOfBytes))
424 | return RSParsePubDateWithBytes(bytes, numberOfBytes);
425 |
426 | return RSParseW3CWithBytes(bytes, numberOfBytes);
427 | }
428 |
429 |
430 | NSDate *RSDateWithString(NSString *dateString) {
431 |
432 | const char *utf8String = [dateString UTF8String];
433 | return RSDateWithBytes(utf8String, strlen(utf8String));
434 | }
435 |
436 |
--------------------------------------------------------------------------------
/RSXML/RSFeedParser.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSFeedParser.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 1/4/15.
6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
7 | //
8 |
9 | #import "FeedParser.h"
10 |
11 | // If you have a feed and don’t know or care what it is (RSS or Atom),
12 | // then call RSParseFeed or RSParseFeedSync.
13 |
14 | @class RSXMLData;
15 | @class RSParsedFeed;
16 |
17 | NS_ASSUME_NONNULL_BEGIN
18 |
19 | BOOL RSCanParseFeed(RSXMLData *xmlData);
20 |
21 |
22 | typedef void (^RSParsedFeedBlock)(RSParsedFeed * _Nullable parsedFeed, NSError * _Nullable error);
23 |
24 | // callback is called on main queue.
25 | void RSParseFeed(RSXMLData *xmlData, RSParsedFeedBlock callback);
26 | RSParsedFeed * _Nullable RSParseFeedSync(RSXMLData *xmlData, NSError * _Nullable * _Nullable error);
27 |
28 | NS_ASSUME_NONNULL_END
29 |
--------------------------------------------------------------------------------
/RSXML/RSFeedParser.m:
--------------------------------------------------------------------------------
1 | //
2 | // FeedParser.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 1/4/15.
6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
7 | //
8 |
9 | #import "RSFeedParser.h"
10 | #import "FeedParser.h"
11 | #import "RSXMLData.h"
12 | #import "RSRSSParser.h"
13 | #import "RSAtomParser.h"
14 |
15 | static NSArray *parserClasses(void) {
16 |
17 | static NSArray *gParserClasses = nil;
18 |
19 | static dispatch_once_t onceToken;
20 | dispatch_once(&onceToken, ^{
21 |
22 | gParserClasses = @[[RSRSSParser class], [RSAtomParser class]];
23 | });
24 |
25 | return gParserClasses;
26 | }
27 |
28 | static BOOL feedMayBeParseable(RSXMLData *xmlData) {
29 |
30 | /*Sanity checks.*/
31 |
32 | if (!xmlData.data) {
33 | return NO;
34 | }
35 |
36 | /*TODO: check size, type, etc.*/
37 |
38 | return YES;
39 | }
40 |
41 | static BOOL optimisticCanParseRSSData(const char *bytes, NSUInteger numberOfBytes);
42 | static BOOL optimisticCanParseAtomData(const char *bytes, NSUInteger numberOfBytes);
43 | static BOOL optimisticCanParseRDF(const char *bytes, NSUInteger numberOfBytes);
44 | static BOOL dataIsProbablyHTML(const char *bytes, NSUInteger numberOfBytes);
45 | static BOOL dataIsSomeWeirdException(const char *bytes, NSUInteger numberOfBytes);
46 | static BOOL dataHasLeftCaret(const char *bytes, NSUInteger numberOfBytes);
47 |
48 | static const NSUInteger maxNumberOfBytesToSearch = 4096;
49 | static const NSUInteger minNumberOfBytesToSearch = 20;
50 |
51 | static Class parserClassForXMLData(RSXMLData *xmlData) {
52 |
53 | if (!feedMayBeParseable(xmlData)) {
54 | return nil;
55 | }
56 |
57 | // TODO: check for things like images and movies and return nil.
58 |
59 | const char *bytes = xmlData.data.bytes;
60 | NSUInteger numberOfBytes = xmlData.data.length;
61 |
62 | if (numberOfBytes > minNumberOfBytesToSearch) {
63 |
64 | if (numberOfBytes > maxNumberOfBytesToSearch) {
65 | numberOfBytes = maxNumberOfBytesToSearch;
66 | }
67 |
68 | if (!dataHasLeftCaret(bytes, numberOfBytes)) {
69 | return nil;
70 | }
71 |
72 | if (optimisticCanParseRSSData(bytes, numberOfBytes)) {
73 | return [RSRSSParser class];
74 | }
75 | if (optimisticCanParseAtomData(bytes, numberOfBytes)) {
76 | return [RSAtomParser class];
77 | }
78 |
79 | if (optimisticCanParseRDF(bytes, numberOfBytes)) {
80 | return nil; //TODO: parse RDF feeds
81 | }
82 |
83 | if (dataIsProbablyHTML(bytes, numberOfBytes)) {
84 | return nil;
85 | }
86 | if (dataIsSomeWeirdException(bytes, numberOfBytes)) {
87 | return nil;
88 | }
89 | }
90 |
91 | for (Class parserClass in parserClasses()) {
92 | if ([parserClass canParseFeed:xmlData]) {
93 | return [[parserClass alloc] initWithXMLData:xmlData];
94 | }
95 | }
96 |
97 | return nil;
98 | }
99 |
100 | static id parserForXMLData(RSXMLData *xmlData) {
101 |
102 | Class parserClass = parserClassForXMLData(xmlData);
103 | if (!parserClass) {
104 | return nil;
105 | }
106 | return [[parserClass alloc] initWithXMLData:xmlData];
107 | }
108 |
109 | static BOOL canParseXMLData(RSXMLData *xmlData) {
110 |
111 | return parserClassForXMLData(xmlData) != nil;
112 | }
113 |
114 | static BOOL didFindString(const char *string, const char *bytes, NSUInteger numberOfBytes) {
115 |
116 | char *foundString = strnstr(bytes, string, numberOfBytes);
117 | return foundString != NULL;
118 | }
119 |
120 | static BOOL dataHasLeftCaret(const char *bytes, NSUInteger numberOfBytes) {
121 |
122 | return didFindString("<", bytes, numberOfBytes);
123 | }
124 |
125 | static BOOL dataIsProbablyHTML(const char *bytes, NSUInteger numberOfBytes) {
126 |
127 | // Won’t catch every single case, which is fine.
128 |
129 | if (didFindString(" 0) {
156 | [self.itemStack removeLastObject];
157 | }
158 | }
159 |
160 |
161 | - (RSOPMLItem *)currentItem {
162 |
163 | return self.itemStack.lastObject;
164 | }
165 |
166 |
167 | #pragma mark - RSSAXParserDelegate
168 |
169 | static const char *kOutline = "outline";
170 | static const char kOutlineLength = 8;
171 |
172 | - (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes {
173 |
174 | if (!RSSAXEqualTags(localName, kOutline, kOutlineLength)) {
175 | return;
176 | }
177 |
178 | RSOPMLItem *item = [RSOPMLItem new];
179 | item.attributes = [SAXParser attributesDictionary:attributes numberOfAttributes:numberOfAttributes];
180 |
181 | [[self currentItem] addChild:item];
182 | [self pushItem:item];
183 | }
184 |
185 |
186 | - (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri {
187 |
188 | if (RSSAXEqualTags(localName, kOutline, kOutlineLength)) {
189 | [self popItem];
190 | }
191 | }
192 |
193 |
194 | static const char *kText = "text";
195 | static const NSInteger kTextLength = 5;
196 |
197 | static const char *kTitle = "title";
198 | static const NSInteger kTitleLength = 6;
199 |
200 | static const char *kDescription = "description";
201 | static const NSInteger kDescriptionLength = 12;
202 |
203 | static const char *kType = "type";
204 | static const NSInteger kTypeLength = 5;
205 |
206 | static const char *kVersion = "version";
207 | static const NSInteger kVersionLength = 8;
208 |
209 | static const char *kHTMLURL = "htmlUrl";
210 | static const NSInteger kHTMLURLLength = 8;
211 |
212 | static const char *kXMLURL = "xmlUrl";
213 | static const NSInteger kXMLURLLength = 7;
214 |
215 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix {
216 |
217 | if (prefix) {
218 | return nil;
219 | }
220 |
221 | size_t nameLength = strlen((const char *)name);
222 |
223 | if (nameLength == kTextLength - 1) {
224 | if (RSSAXEqualTags(name, kText, kTextLength)) {
225 | return OPMLTextKey;
226 | }
227 | if (RSSAXEqualTags(name, kType, kTypeLength)) {
228 | return OPMLTypeKey;
229 | }
230 | }
231 |
232 | else if (nameLength == kTitleLength - 1) {
233 | if (RSSAXEqualTags(name, kTitle, kTitleLength)) {
234 | return OPMLTitleKey;
235 | }
236 | }
237 |
238 | else if (nameLength == kXMLURLLength - 1) {
239 | if (RSSAXEqualTags(name, kXMLURL, kXMLURLLength)) {
240 | return OPMLXMLURLKey;
241 | }
242 | }
243 |
244 | else if (nameLength == kVersionLength - 1) {
245 | if (RSSAXEqualTags(name, kVersion, kVersionLength)) {
246 | return OPMLVersionKey;
247 | }
248 | if (RSSAXEqualTags(name, kHTMLURL, kHTMLURLLength)) {
249 | return OPMLHMTLURLKey;
250 | }
251 | }
252 |
253 | else if (nameLength == kDescriptionLength - 1) {
254 | if (RSSAXEqualTags(name, kDescription, kDescriptionLength)) {
255 | return OPMLDescriptionKey;
256 | }
257 | }
258 |
259 | return nil;
260 | }
261 |
262 |
263 | static const char *kRSSUppercase = "RSS";
264 | static const char *kRSSLowercase = "rss";
265 | static const NSUInteger kRSSLength = 3;
266 | static NSString *RSSUppercaseValue = @"RSS";
267 | static NSString *RSSLowercaseValue = @"rss";
268 | static NSString *emptyString = @"";
269 |
270 | static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
271 |
272 | return memcmp(bytes1, bytes2, length) == 0;
273 | }
274 |
275 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length {
276 |
277 |
278 | if (length < 1) {
279 | return emptyString;
280 | }
281 |
282 | if (length == kRSSLength) {
283 |
284 | if (equalBytes(bytes, kRSSUppercase, kRSSLength)) {
285 | return RSSUppercaseValue;
286 | }
287 | else if (equalBytes(bytes, kRSSLowercase, kRSSLength)) {
288 | return RSSLowercaseValue;
289 | }
290 |
291 | }
292 |
293 | return nil;
294 | }
295 |
296 |
297 | @end
298 |
--------------------------------------------------------------------------------
/RSXML/RSParsedArticle.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSParsedArticle.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 12/6/14.
6 | // Copyright (c) 2014 Ranchero Software LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 |
12 | @interface RSParsedArticle : NSObject
13 |
14 | - (nonnull instancetype)initWithFeedURL:(NSString * _Nonnull)feedURL;
15 |
16 | @property (nonatomic, readonly, nonnull) NSString *feedURL;
17 | @property (nonatomic, nonnull) NSString *articleID; //Calculated. Don't get until other properties have been set.
18 |
19 | @property (nonatomic, nullable) NSString *guid;
20 | @property (nonatomic, nullable) NSString *title;
21 | @property (nonatomic, nullable) NSString *body;
22 | @property (nonatomic, nullable) NSString *link;
23 | @property (nonatomic, nullable) NSString *permalink;
24 | @property (nonatomic, nullable) NSString *author;
25 | @property (nonatomic, nullable) NSDate *datePublished;
26 | @property (nonatomic, nullable) NSDate *dateModified;
27 | @property (nonatomic, nonnull) NSDate *dateParsed;
28 |
29 | - (void)calculateArticleID; // Optimization. Call after all properties have been set. Call on a background thread.
30 |
31 | @end
32 |
33 |
--------------------------------------------------------------------------------
/RSXML/RSParsedArticle.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSParsedArticle.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 12/6/14.
6 | // Copyright (c) 2014 Ranchero Software LLC. All rights reserved.
7 | //
8 |
9 | #import "RSParsedArticle.h"
10 | #import "RSXMLInternal.h"
11 |
12 |
13 | @implementation RSParsedArticle
14 |
15 |
16 | #pragma mark - Init
17 |
18 | - (instancetype)initWithFeedURL:(NSString *)feedURL {
19 |
20 | NSParameterAssert(feedURL != nil);
21 |
22 | self = [super init];
23 | if (!self) {
24 | return nil;
25 | }
26 |
27 | _feedURL = feedURL;
28 | _dateParsed = [NSDate date];
29 |
30 | return self;
31 | }
32 |
33 |
34 | #pragma mark - Accessors
35 |
36 | - (NSString *)articleID {
37 |
38 | if (!_articleID) {
39 | _articleID = self.calculatedUniqueID;
40 | }
41 |
42 | return _articleID;
43 | }
44 |
45 |
46 | - (NSString *)calculatedUniqueID {
47 |
48 | /*guid+feedID, or a combination of properties when no guid. Then hash the result.
49 | In general, feeds should have guids. When they don't, re-runs are very likely,
50 | because there's no other 100% reliable way to determine identity.*/
51 |
52 | NSMutableString *s = [NSMutableString stringWithString:@""];
53 |
54 | NSString *datePublishedTimeStampString = nil;
55 | if (self.datePublished) {
56 | datePublishedTimeStampString = [NSString stringWithFormat:@"%.0f", self.datePublished.timeIntervalSince1970];
57 | }
58 |
59 | if (!RSXMLStringIsEmpty(self.guid)) {
60 | [s appendString:self.guid];
61 | }
62 |
63 | else if (!RSXMLStringIsEmpty(self.link) && self.datePublished != nil) {
64 | [s appendString:self.link];
65 | [s appendString:datePublishedTimeStampString];
66 | }
67 |
68 | else if (!RSXMLStringIsEmpty(self.title) && self.datePublished != nil) {
69 | [s appendString:self.title];
70 | [s appendString:datePublishedTimeStampString];
71 | }
72 |
73 | else if (self.datePublished != nil) {
74 | [s appendString:datePublishedTimeStampString];
75 | }
76 |
77 | else if (!RSXMLStringIsEmpty(self.link)) {
78 | [s appendString:self.link];
79 | }
80 |
81 | else if (!RSXMLStringIsEmpty(self.title)) {
82 | [s appendString:self.title];
83 | }
84 |
85 | else if (!RSXMLStringIsEmpty(self.body)) {
86 | [s appendString:self.body];
87 | }
88 |
89 | NSAssert(!RSXMLStringIsEmpty(self.feedURL), nil);
90 | [s appendString:self.feedURL];
91 |
92 | return [s rsxml_md5HashString];
93 | }
94 |
95 | - (void)calculateArticleID {
96 |
97 | (void)self.articleID;
98 | }
99 |
100 | @end
101 |
102 |
--------------------------------------------------------------------------------
/RSXML/RSParsedFeed.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSParsedFeed.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 7/12/15.
6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | @class RSParsedArticle;
12 |
13 | @interface RSParsedFeed : NSObject
14 |
15 | - (nonnull instancetype)initWithURLString:(NSString * _Nonnull)urlString title:(NSString * _Nullable)title link:(NSString * _Nullable)link articles:(NSArray * _Nonnull)articles;
16 |
17 | @property (nonatomic, readonly, nonnull) NSString *urlString;
18 | @property (nonatomic, readonly, nullable) NSString *title;
19 | @property (nonatomic, readonly, nullable) NSString *link;
20 | @property (nonatomic, readonly, nonnull) NSSet *articles;
21 |
22 | @end
23 |
--------------------------------------------------------------------------------
/RSXML/RSParsedFeed.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSParsedFeed.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 7/12/15.
6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import "RSParsedFeed.h"
10 |
11 | @implementation RSParsedFeed
12 |
13 | - (instancetype)initWithURLString:(NSString *)urlString title:(NSString *)title link:(NSString *)link articles:(NSSet *)articles {
14 |
15 | self = [super init];
16 | if (!self) {
17 | return nil;
18 | }
19 |
20 | _urlString = urlString;
21 | _title = title;
22 | _link = link;
23 | _articles = articles;
24 |
25 | return self;
26 | }
27 |
28 |
29 | @end
30 |
--------------------------------------------------------------------------------
/RSXML/RSRSSParser.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSRSSParser.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 1/6/15.
6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
7 | //
8 |
9 | #import "FeedParser.h"
10 |
11 | @interface RSRSSParser : NSObject
12 |
13 | @end
14 |
--------------------------------------------------------------------------------
/RSXML/RSRSSParser.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSRSSParser.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 1/6/15.
6 | // Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "RSRSSParser.h"
11 | #import "RSSAXParser.h"
12 | #import "RSParsedFeed.h"
13 | #import "RSParsedArticle.h"
14 | #import "RSXMLData.h"
15 | #import "RSXMLInternal.h"
16 | #import "NSString+RSXML.h"
17 | #import "RSDateParser.h"
18 |
19 |
20 | @interface RSRSSParser ()
21 |
22 | @property (nonatomic) NSData *feedData;
23 | @property (nonatomic) NSString *urlString;
24 | @property (nonatomic) NSDictionary *currentAttributes;
25 | @property (nonatomic) RSSAXParser *parser;
26 | @property (nonatomic) NSMutableArray *articles;
27 | @property (nonatomic) BOOL parsingArticle;
28 | @property (nonatomic, readonly) RSParsedArticle *currentArticle;
29 | @property (nonatomic) BOOL parsingChannelImage;
30 | @property (nonatomic, readonly) NSDate *currentDate;
31 | @property (nonatomic) BOOL endRSSFound;
32 | @property (nonatomic) NSString *link;
33 | @property (nonatomic) NSString *title;
34 | @property (nonatomic) NSDate *dateParsed;
35 |
36 | @end
37 |
38 |
39 | @implementation RSRSSParser
40 |
41 | #pragma mark - Class Methods
42 |
43 | + (BOOL)canParseFeed:(RSXMLData *)xmlData {
44 |
45 | // Checking for '' within first n characters should do it.
46 | // TODO: handle RSS 1.0
47 |
48 | @autoreleasepool {
49 |
50 | NSData *feedData = xmlData.data;
51 | NSString *s = [[NSString alloc] initWithBytesNoCopy:(void *)feedData.bytes length:feedData.length encoding:NSUTF8StringEncoding freeWhenDone:NO];
52 | if (!s) {
53 | s = [[NSString alloc] initWithData:feedData encoding:NSUTF8StringEncoding];
54 | }
55 | if (!s) {
56 | s = [[NSString alloc] initWithData:feedData encoding:NSUnicodeStringEncoding];
57 | }
58 | if (!s) {
59 | return NO;
60 | }
61 |
62 | static const NSInteger numberOfCharactersToSearch = 4096;
63 | NSRange rangeToSearch = NSMakeRange(0, numberOfCharactersToSearch);
64 | if (s.length < numberOfCharactersToSearch) {
65 | rangeToSearch.length = s.length;
66 | }
67 |
68 | NSRange rssRange = [s rangeOfString:@"" options:NSLiteralSearch range:rangeToSearch];
70 | if (rssRange.length < 1 || channelRange.length < 1) {
71 | return NO;
72 | }
73 |
74 | if (rssRange.location > channelRange.location) {
75 | return NO; // Wrong order.
76 | }
77 | }
78 |
79 | return YES;
80 | }
81 |
82 |
83 | #pragma mark - Init
84 |
85 | - (instancetype)initWithXMLData:(RSXMLData *)xmlData {
86 |
87 | self = [super init];
88 | if (!self) {
89 | return nil;
90 | }
91 |
92 | _feedData = xmlData.data;
93 | _urlString = xmlData.urlString;
94 | _parser = [[RSSAXParser alloc] initWithDelegate:self];
95 | _articles = [NSMutableArray new];
96 |
97 | return self;
98 | }
99 |
100 |
101 | #pragma mark - API
102 |
103 | - (RSParsedFeed *)parseFeed:(NSError **)error {
104 |
105 | [self parse];
106 |
107 | RSParsedFeed *parsedFeed = [[RSParsedFeed alloc] initWithURLString:self.urlString title:self.title link:self.link articles:self.articles];
108 |
109 | return parsedFeed;
110 | }
111 |
112 |
113 | #pragma mark - Constants
114 |
115 | static NSString *kIsPermaLinkKey = @"isPermaLink";
116 | static NSString *kURLKey = @"url";
117 | static NSString *kLengthKey = @"length";
118 | static NSString *kTypeKey = @"type";
119 | static NSString *kFalseValue = @"false";
120 | static NSString *kTrueValue = @"true";
121 | static NSString *kContentEncodedKey = @"content:encoded";
122 | static NSString *kDCDateKey = @"dc:date";
123 | static NSString *kDCCreatorKey = @"dc:creator";
124 | static NSString *kRDFAboutKey = @"rdf:about";
125 |
126 | static const char *kItem = "item";
127 | static const NSInteger kItemLength = 5;
128 |
129 | static const char *kImage = "image";
130 | static const NSInteger kImageLength = 6;
131 |
132 | static const char *kLink = "link";
133 | static const NSInteger kLinkLength = 5;
134 |
135 | static const char *kTitle = "title";
136 | static const NSInteger kTitleLength = 6;
137 |
138 | static const char *kDC = "dc";
139 | static const NSInteger kDCLength = 3;
140 |
141 | static const char *kCreator = "creator";
142 | static const NSInteger kCreatorLength = 8;
143 |
144 | static const char *kDate = "date";
145 | static const NSInteger kDateLength = 5;
146 |
147 | static const char *kContent = "content";
148 | static const NSInteger kContentLength = 8;
149 |
150 | static const char *kEncoded = "encoded";
151 | static const NSInteger kEncodedLength = 8;
152 |
153 | static const char *kGuid = "guid";
154 | static const NSInteger kGuidLength = 5;
155 |
156 | static const char *kPubDate = "pubDate";
157 | static const NSInteger kPubDateLength = 8;
158 |
159 | static const char *kAuthor = "author";
160 | static const NSInteger kAuthorLength = 7;
161 |
162 | static const char *kDescription = "description";
163 | static const NSInteger kDescriptionLength = 12;
164 |
165 | static const char *kRSS = "rss";
166 | static const NSInteger kRSSLength = 4;
167 |
168 | static const char *kURL = "url";
169 | static const NSInteger kURLLength = 4;
170 |
171 | static const char *kLength = "length";
172 | static const NSInteger kLengthLength = 7;
173 |
174 | static const char *kType = "type";
175 | static const NSInteger kTypeLength = 5;
176 |
177 | static const char *kIsPermaLink = "isPermaLink";
178 | static const NSInteger kIsPermaLinkLength = 12;
179 |
180 | static const char *kRDF = "rdf";
181 | static const NSInteger kRDFlength = 4;
182 |
183 | static const char *kAbout = "about";
184 | static const NSInteger kAboutLength = 6;
185 |
186 | static const char *kFalse = "false";
187 | static const NSInteger kFalseLength = 6;
188 |
189 | static const char *kTrue = "true";
190 | static const NSInteger kTrueLength = 5;
191 |
192 |
193 | #pragma mark - Parsing
194 |
195 | - (void)parse {
196 |
197 | self.dateParsed = [NSDate date];
198 |
199 | @autoreleasepool {
200 | [self.parser parseData:self.feedData];
201 | [self.parser finishParsing];
202 | }
203 |
204 | // Optimization: make articles do calculations on this background thread.
205 | [self.articles makeObjectsPerformSelector:@selector(calculateArticleID)];
206 | }
207 |
208 |
209 | - (void)addArticle {
210 |
211 | RSParsedArticle *article = [[RSParsedArticle alloc] initWithFeedURL:self.urlString];
212 | article.dateParsed = self.dateParsed;
213 |
214 | [self.articles addObject:article];
215 | }
216 |
217 |
218 | - (RSParsedArticle *)currentArticle {
219 |
220 | return self.articles.lastObject;
221 | }
222 |
223 |
224 | - (void)addFeedElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix {
225 |
226 | if (prefix != NULL) {
227 | return;
228 | }
229 |
230 | if (RSSAXEqualTags(localName, kLink, kLinkLength)) {
231 | if (!self.link) {
232 | self.link = self.parser.currentStringWithTrimmedWhitespace;
233 | }
234 | }
235 |
236 | else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) {
237 | self.title = self.parser.currentStringWithTrimmedWhitespace;
238 | }
239 | }
240 |
241 |
242 | - (void)addDCElement:(const xmlChar *)localName {
243 |
244 | if (RSSAXEqualTags(localName, kCreator, kCreatorLength)) {
245 |
246 | self.currentArticle.author = self.parser.currentStringWithTrimmedWhitespace;
247 | }
248 | else if (RSSAXEqualTags(localName, kDate, kDateLength)) {
249 |
250 | self.currentArticle.datePublished = self.currentDate;
251 | }
252 | }
253 |
254 |
255 | - (void)addGuid {
256 |
257 | self.currentArticle.guid = self.parser.currentStringWithTrimmedWhitespace;
258 |
259 | NSString *isPermaLinkValue = [self.currentAttributes rsxml_objectForCaseInsensitiveKey:@"ispermalink"];
260 | if (!isPermaLinkValue || ![isPermaLinkValue isEqualToString:@"false"]) {
261 | self.currentArticle.permalink = [self urlString:self.currentArticle.guid];
262 | }
263 | }
264 |
265 |
266 | - (NSString *)urlString:(NSString *)s {
267 |
268 | /*Resolve against home page URL (if available) or feed URL.*/
269 |
270 | if ([[s lowercaseString] hasPrefix:@"http"]) {
271 | return s;
272 | }
273 |
274 | if (!self.link) {
275 | //TODO: get feed URL and use that to resolve URL.*/
276 | return s;
277 | }
278 |
279 | NSURL *baseURL = [NSURL URLWithString:self.link];
280 | if (!baseURL) {
281 | return s;
282 | }
283 |
284 | NSURL *resolvedURL = [NSURL URLWithString:s relativeToURL:baseURL];
285 | if (resolvedURL.absoluteString) {
286 | return resolvedURL.absoluteString;
287 | }
288 |
289 | return s;
290 | }
291 |
292 |
293 | - (NSString *)currentStringWithHTMLEntitiesDecoded {
294 |
295 | return [self.parser.currentStringWithTrimmedWhitespace rs_stringByDecodingHTMLEntities];
296 | }
297 |
298 | - (void)addArticleElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix {
299 |
300 | if (RSSAXEqualTags(prefix, kDC, kDCLength)) {
301 |
302 | [self addDCElement:localName];
303 | return;
304 | }
305 |
306 | if (RSSAXEqualTags(prefix, kContent, kContentLength) && RSSAXEqualTags(localName, kEncoded, kEncodedLength)) {
307 |
308 | self.currentArticle.body = [self currentStringWithHTMLEntitiesDecoded];
309 | return;
310 | }
311 |
312 | if (prefix != NULL) {
313 | return;
314 | }
315 |
316 | if (RSSAXEqualTags(localName, kGuid, kGuidLength)) {
317 | [self addGuid];
318 | }
319 | else if (RSSAXEqualTags(localName, kPubDate, kPubDateLength)) {
320 | self.currentArticle.datePublished = self.currentDate;
321 | }
322 | else if (RSSAXEqualTags(localName, kAuthor, kAuthorLength)) {
323 | self.currentArticle.author = self.parser.currentStringWithTrimmedWhitespace;
324 | }
325 | else if (RSSAXEqualTags(localName, kLink, kLinkLength)) {
326 | self.currentArticle.link = [self urlString:self.parser.currentStringWithTrimmedWhitespace];
327 | }
328 | else if (RSSAXEqualTags(localName, kDescription, kDescriptionLength)) {
329 |
330 | if (!self.currentArticle.body) {
331 | self.currentArticle.body = [self currentStringWithHTMLEntitiesDecoded];
332 | }
333 | }
334 | else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) {
335 | self.currentArticle.title = [self currentStringWithHTMLEntitiesDecoded];
336 | }
337 | }
338 |
339 |
340 | - (NSDate *)currentDate {
341 |
342 | return RSDateWithBytes(self.parser.currentCharacters.bytes, self.parser.currentCharacters.length);
343 | }
344 |
345 |
346 | #pragma mark - RSSAXParserDelegate
347 |
348 | - (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes {
349 |
350 | if (self.endRSSFound) {
351 | return;
352 | }
353 |
354 | NSDictionary *xmlAttributes = nil;
355 | if (RSSAXEqualTags(localName, kItem, kItemLength) || RSSAXEqualTags(localName, kGuid, kGuidLength)) {
356 | xmlAttributes = [self.parser attributesDictionary:attributes numberOfAttributes:numberOfAttributes];
357 | }
358 | if (self.currentAttributes != xmlAttributes) {
359 | self.currentAttributes = xmlAttributes;
360 | }
361 |
362 | if (!prefix && RSSAXEqualTags(localName, kItem, kItemLength)) {
363 |
364 | [self addArticle];
365 | self.parsingArticle = YES;
366 |
367 | if (xmlAttributes && xmlAttributes[kRDFAboutKey]) { /*RSS 1.0 guid*/
368 | self.currentArticle.guid = xmlAttributes[kRDFAboutKey];
369 | self.currentArticle.permalink = self.currentArticle.guid;
370 | }
371 | }
372 |
373 | else if (!prefix && RSSAXEqualTags(localName, kImage, kImageLength)) {
374 | self.parsingChannelImage = YES;
375 | }
376 |
377 | if (!self.parsingChannelImage) {
378 | [self.parser beginStoringCharacters];
379 | }
380 | }
381 |
382 |
383 | - (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri {
384 |
385 | if (self.endRSSFound) {
386 | return;
387 | }
388 |
389 | if (RSSAXEqualTags(localName, kRSS, kRSSLength)) {
390 | self.endRSSFound = YES;
391 | }
392 |
393 | else if (RSSAXEqualTags(localName, kImage, kImageLength)) {
394 | self.parsingChannelImage = NO;
395 | }
396 |
397 | else if (RSSAXEqualTags(localName, kItem, kItemLength)) {
398 | self.parsingArticle = NO;
399 | }
400 |
401 | else if (self.parsingArticle) {
402 | [self addArticleElement:localName prefix:prefix];
403 | }
404 |
405 | else if (!self.parsingChannelImage) {
406 | [self addFeedElement:localName prefix:prefix];
407 | }
408 | }
409 |
410 |
411 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix {
412 |
413 | if (RSSAXEqualTags(prefix, kRDF, kRDFlength)) {
414 |
415 | if (RSSAXEqualTags(name, kAbout, kAboutLength)) {
416 | return kRDFAboutKey;
417 | }
418 |
419 | return nil;
420 | }
421 |
422 | if (prefix) {
423 | return nil;
424 | }
425 |
426 | if (RSSAXEqualTags(name, kIsPermaLink, kIsPermaLinkLength)) {
427 | return kIsPermaLinkKey;
428 | }
429 |
430 | if (RSSAXEqualTags(name, kURL, kURLLength)) {
431 | return kURLKey;
432 | }
433 |
434 | if (RSSAXEqualTags(name, kLength, kLengthLength)) {
435 | return kLengthKey;
436 | }
437 |
438 | if (RSSAXEqualTags(name, kType, kTypeLength)) {
439 | return kTypeKey;
440 | }
441 |
442 | return nil;
443 | }
444 |
445 |
446 | static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
447 |
448 | return memcmp(bytes1, bytes2, length) == 0;
449 | }
450 |
451 |
452 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length {
453 |
454 | static const NSUInteger falseLength = kFalseLength - 1;
455 | static const NSUInteger trueLength = kTrueLength - 1;
456 |
457 | if (length == falseLength && equalBytes(bytes, kFalse, falseLength)) {
458 | return kFalseValue;
459 | }
460 |
461 | if (length == trueLength && equalBytes(bytes, kTrue, trueLength)) {
462 | return kTrueValue;
463 | }
464 |
465 | return nil;
466 | }
467 |
468 |
469 | @end
470 |
--------------------------------------------------------------------------------
/RSXML/RSSAXHTMLParser.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSSAXHTMLParser.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 3/6/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | @class RSSAXHTMLParser;
12 |
13 | @protocol RSSAXHTMLParserDelegate
14 |
15 | @optional
16 |
17 | - (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLStartElement:(const unsigned char *)localName attributes:(const unsigned char **)attributes;
18 |
19 | - (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLEndElement:(const unsigned char *)localName;
20 |
21 | - (void)saxParser:(RSSAXHTMLParser *)SAXParser XMLCharactersFound:(const unsigned char *)characters length:(NSUInteger)length;
22 |
23 | - (void)saxParserDidReachEndOfDocument:(RSSAXHTMLParser *)SAXParser; // If canceled, may not get called (but might).
24 |
25 | @end
26 |
27 |
28 | @interface RSSAXHTMLParser : NSObject
29 |
30 |
31 | - (instancetype)initWithDelegate:(id)delegate;
32 |
33 | - (void)parseData:(NSData *)data;
34 | - (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes;
35 | - (void)finishParsing;
36 | - (void)cancel;
37 |
38 | @property (nonatomic, strong, readonly) NSData *currentCharacters; // nil if not storing characters. UTF-8 encoded.
39 | @property (nonatomic, strong, readonly) NSString *currentString; // Convenience to get string version of currentCharacters.
40 | @property (nonatomic, strong, readonly) NSString *currentStringWithTrimmedWhitespace;
41 |
42 | - (void)beginStoringCharacters; // Delegate can call from XMLStartElement. Characters will be available in XMLEndElement as currentCharacters property. Storing characters is stopped after each XMLEndElement.
43 |
44 | // Delegate can call from within XMLStartElement.
45 |
46 | - (NSDictionary *)attributesDictionary:(const unsigned char **)attributes;
47 |
48 |
49 | @end
50 |
--------------------------------------------------------------------------------
/RSXML/RSSAXHTMLParser.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSSAXHTMLParser.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 3/6/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import "RSSAXHTMLParser.h"
10 | #import "RSSAXParser.h"
11 | #import
12 | #import
13 | #import
14 | #import "RSXMLInternal.h"
15 |
16 |
17 | @interface RSSAXHTMLParser ()
18 |
19 | @property (nonatomic) id delegate;
20 | @property (nonatomic, assign) htmlParserCtxtPtr context;
21 | @property (nonatomic, assign) BOOL storingCharacters;
22 | @property (nonatomic) NSMutableData *characters;
23 | @property (nonatomic) BOOL delegateRespondsToStartElementMethod;
24 | @property (nonatomic) BOOL delegateRespondsToEndElementMethod;
25 | @property (nonatomic) BOOL delegateRespondsToCharactersFoundMethod;
26 | @property (nonatomic) BOOL delegateRespondsToEndOfDocumentMethod;
27 |
28 | @end
29 |
30 |
31 | @implementation RSSAXHTMLParser
32 |
33 |
34 | + (void)initialize {
35 |
36 | RSSAXInitLibXMLParser();
37 | }
38 |
39 |
40 | #pragma mark - Init
41 |
42 | - (instancetype)initWithDelegate:(id)delegate {
43 |
44 | self = [super init];
45 | if (self == nil)
46 | return nil;
47 |
48 | _delegate = delegate;
49 |
50 | if ([_delegate respondsToSelector:@selector(saxParser:XMLStartElement:attributes:)]) {
51 | _delegateRespondsToStartElementMethod = YES;
52 | }
53 | if ([_delegate respondsToSelector:@selector(saxParser:XMLEndElement:)]) {
54 | _delegateRespondsToEndElementMethod = YES;
55 | }
56 | if ([_delegate respondsToSelector:@selector(saxParser:XMLCharactersFound:length:)]) {
57 | _delegateRespondsToCharactersFoundMethod = YES;
58 | }
59 | if ([_delegate respondsToSelector:@selector(saxParserDidReachEndOfDocument:)]) {
60 | _delegateRespondsToEndOfDocumentMethod = YES;
61 | }
62 |
63 | return self;
64 | }
65 |
66 |
67 | #pragma mark - Dealloc
68 |
69 | - (void)dealloc {
70 |
71 | if (_context != nil) {
72 | htmlFreeParserCtxt(_context);
73 | _context = nil;
74 | }
75 | _delegate = nil;
76 | }
77 |
78 |
79 | #pragma mark - API
80 |
81 | static xmlSAXHandler saxHandlerStruct;
82 |
83 | - (void)parseData:(NSData *)data {
84 |
85 | [self parseBytes:data.bytes numberOfBytes:data.length];
86 | }
87 |
88 |
89 | - (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes {
90 |
91 | if (self.context == nil) {
92 |
93 | xmlCharEncoding characterEncoding = xmlDetectCharEncoding(bytes, (int)numberOfBytes);
94 | self.context = htmlCreatePushParserCtxt(&saxHandlerStruct, (__bridge void *)self, nil, 0, nil, characterEncoding);
95 | htmlCtxtUseOptions(self.context, XML_PARSE_RECOVER | XML_PARSE_NONET | HTML_PARSE_COMPACT);
96 | }
97 |
98 | @autoreleasepool {
99 | htmlParseChunk(self.context, (const char *)bytes, (int)numberOfBytes, 0);
100 | }
101 | }
102 |
103 |
104 | - (void)finishParsing {
105 |
106 | NSAssert(self.context != nil, nil);
107 | if (self.context == nil)
108 | return;
109 |
110 | @autoreleasepool {
111 | htmlParseChunk(self.context, nil, 0, 1);
112 | htmlFreeParserCtxt(self.context);
113 | self.context = nil;
114 | self.characters = nil;
115 | }
116 | }
117 |
118 |
119 | - (void)cancel {
120 |
121 | @autoreleasepool {
122 | xmlStopParser(self.context);
123 | }
124 | }
125 |
126 |
127 |
128 | - (void)beginStoringCharacters {
129 | self.storingCharacters = YES;
130 | self.characters = [NSMutableData new];
131 | }
132 |
133 |
134 | - (void)endStoringCharacters {
135 | self.storingCharacters = NO;
136 | self.characters = nil;
137 | }
138 |
139 |
140 | - (NSData *)currentCharacters {
141 |
142 | if (!self.storingCharacters) {
143 | return nil;
144 | }
145 |
146 | return self.characters;
147 | }
148 |
149 |
150 | - (NSString *)currentString {
151 |
152 | NSData *d = self.currentCharacters;
153 | if (RSXMLIsEmpty(d)) {
154 | return nil;
155 | }
156 |
157 | return [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
158 | }
159 |
160 |
161 | - (NSString *)currentStringWithTrimmedWhitespace {
162 |
163 | return [self.currentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
164 | }
165 |
166 |
167 | #pragma mark - Attributes Dictionary
168 |
169 | - (NSDictionary *)attributesDictionary:(const xmlChar **)attributes {
170 |
171 | if (!attributes) {
172 | return nil;
173 | }
174 |
175 | NSMutableDictionary *d = [NSMutableDictionary new];
176 |
177 | NSInteger ix = 0;
178 | NSString *currentKey = nil;
179 | while (true) {
180 |
181 | const xmlChar *oneAttribute = attributes[ix];
182 | ix++;
183 |
184 | if (!currentKey && !oneAttribute) {
185 | break;
186 | }
187 |
188 | if (!currentKey) {
189 | currentKey = [NSString stringWithUTF8String:(const char *)oneAttribute];
190 | }
191 | else {
192 | NSString *value = nil;
193 | if (oneAttribute) {
194 | value = [NSString stringWithUTF8String:(const char *)oneAttribute];
195 | }
196 |
197 | d[currentKey] = value ? value : @"";
198 | currentKey = nil;
199 | }
200 | }
201 |
202 | return [d copy];
203 | }
204 |
205 |
206 | #pragma mark - Callbacks
207 |
208 | - (void)xmlEndDocument {
209 |
210 | @autoreleasepool {
211 | if (self.delegateRespondsToEndOfDocumentMethod) {
212 | [self.delegate saxParserDidReachEndOfDocument:self];
213 | }
214 |
215 | [self endStoringCharacters];
216 | }
217 | }
218 |
219 |
220 | - (void)xmlCharactersFound:(const xmlChar *)ch length:(NSUInteger)length {
221 |
222 | @autoreleasepool {
223 | if (self.storingCharacters) {
224 | [self.characters appendBytes:(const void *)ch length:length];
225 | }
226 |
227 | if (self.delegateRespondsToCharactersFoundMethod) {
228 | [self.delegate saxParser:self XMLCharactersFound:ch length:length];
229 | }
230 | }
231 | }
232 |
233 |
234 | - (void)xmlStartElement:(const xmlChar *)localName attributes:(const xmlChar **)attributes {
235 |
236 | @autoreleasepool {
237 | if (self.delegateRespondsToStartElementMethod) {
238 |
239 | [self.delegate saxParser:self XMLStartElement:localName attributes:attributes];
240 | }
241 | }
242 | }
243 |
244 |
245 | - (void)xmlEndElement:(const xmlChar *)localName {
246 |
247 | @autoreleasepool {
248 | if (self.delegateRespondsToEndElementMethod) {
249 | [self.delegate saxParser:self XMLEndElement:localName];
250 | }
251 |
252 | [self endStoringCharacters];
253 | }
254 | }
255 |
256 |
257 | @end
258 |
259 |
260 | static void startElementSAX(void *context, const xmlChar *localname, const xmlChar **attributes) {
261 |
262 | [(__bridge RSSAXHTMLParser *)context xmlStartElement:localname attributes:attributes];
263 | }
264 |
265 |
266 | static void endElementSAX(void *context, const xmlChar *localname) {
267 | [(__bridge RSSAXHTMLParser *)context xmlEndElement:localname];
268 | }
269 |
270 |
271 | static void charactersFoundSAX(void *context, const xmlChar *ch, int len) {
272 | [(__bridge RSSAXHTMLParser *)context xmlCharactersFound:ch length:(NSUInteger)len];
273 | }
274 |
275 |
276 | static void endDocumentSAX(void *context) {
277 | [(__bridge RSSAXHTMLParser *)context xmlEndDocument];
278 | }
279 |
280 |
281 | static htmlSAXHandler saxHandlerStruct = {
282 | nil, /* internalSubset */
283 | nil, /* isStandalone */
284 | nil, /* hasInternalSubset */
285 | nil, /* hasExternalSubset */
286 | nil, /* resolveEntity */
287 | nil, /* getEntity */
288 | nil, /* entityDecl */
289 | nil, /* notationDecl */
290 | nil, /* attributeDecl */
291 | nil, /* elementDecl */
292 | nil, /* unparsedEntityDecl */
293 | nil, /* setDocumentLocator */
294 | nil, /* startDocument */
295 | endDocumentSAX, /* endDocument */
296 | startElementSAX, /* startElement*/
297 | endElementSAX, /* endElement */
298 | nil, /* reference */
299 | charactersFoundSAX, /* characters */
300 | nil, /* ignorableWhitespace */
301 | nil, /* processingInstruction */
302 | nil, /* comment */
303 | nil, /* warning */
304 | nil, /* error */
305 | nil, /* fatalError //: unused error() get all the errors */
306 | nil, /* getParameterEntity */
307 | nil, /* cdataBlock */
308 | nil, /* externalSubset */
309 | XML_SAX2_MAGIC,
310 | nil,
311 | nil, /* startElementNs */
312 | nil, /* endElementNs */
313 | nil /* serror */
314 | };
315 |
316 |
--------------------------------------------------------------------------------
/RSXML/RSSAXParser.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSSAXParser.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 3/25/15.
6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | /*Thread-safe, not re-entrant.
12 |
13 | Calls to the delegate will happen on the same thread where the parser runs.
14 |
15 | This is a low-level streaming XML parser, a thin wrapper for libxml2's SAX parser. It doesn't do much Foundation-ifying quite on purpose -- because the goal is performance and low memory use.
16 |
17 | This class is not meant to be sub-classed. Use the delegate methods.
18 | */
19 |
20 |
21 | @class RSSAXParser;
22 |
23 | @protocol RSSAXParserDelegate
24 |
25 | @optional
26 |
27 | - (void)saxParser:(RSSAXParser *)SAXParser XMLStartElement:(const unsigned char *)localName prefix:(const unsigned char *)prefix uri:(const unsigned char *)uri numberOfNamespaces:(NSInteger)numberOfNamespaces namespaces:(const unsigned char **)namespaces numberOfAttributes:(NSInteger)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const unsigned char **)attributes;
28 |
29 | - (void)saxParser:(RSSAXParser *)SAXParser XMLEndElement:(const unsigned char *)localName prefix:(const unsigned char *)prefix uri:(const unsigned char *)uri;
30 |
31 | - (void)saxParser:(RSSAXParser *)SAXParser XMLCharactersFound:(const unsigned char *)characters length:(NSUInteger)length;
32 |
33 | - (void)saxParserDidReachEndOfDocument:(RSSAXParser *)SAXParser; /*If canceled, may not get called (but might).*/
34 |
35 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const unsigned char *)name prefix:(const unsigned char *)prefix; /*Okay to return nil. Prefix may be nil.*/
36 |
37 | - (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length;
38 |
39 | @end
40 |
41 |
42 | void RSSAXInitLibXMLParser(void); // Needed by RSSAXHTMLParser.
43 |
44 | /*For use by delegate.*/
45 |
46 | BOOL RSSAXEqualTags(const unsigned char *localName, const char *tag, NSInteger tagLength);
47 |
48 |
49 | @interface RSSAXParser : NSObject
50 |
51 | - (instancetype)initWithDelegate:(id)delegate;
52 |
53 | - (void)parseData:(NSData *)data;
54 | - (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes;
55 | - (void)finishParsing;
56 | - (void)cancel;
57 |
58 | @property (nonatomic, strong, readonly) NSData *currentCharacters; /*nil if not storing characters. UTF-8 encoded.*/
59 | @property (nonatomic, strong, readonly) NSString *currentString; /*Convenience to get string version of currentCharacters.*/
60 | @property (nonatomic, strong, readonly) NSString *currentStringWithTrimmedWhitespace;
61 |
62 | - (void)beginStoringCharacters; /*Delegate can call from XMLStartElement. Characters will be available in XMLEndElement as currentCharacters property. Storing characters is stopped after each XMLEndElement.*/
63 |
64 | /*Delegate can call from within XMLStartElement. Returns nil if numberOfAttributes < 1.*/
65 |
66 | - (NSDictionary *)attributesDictionary:(const unsigned char **)attributes numberOfAttributes:(NSInteger)numberOfAttributes;
67 |
68 | @end
69 |
--------------------------------------------------------------------------------
/RSXML/RSSAXParser.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSSAXParser.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 3/25/15.
6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import
10 | #import
11 | #import "RSSAXParser.h"
12 | #import "RSXMLInternal.h"
13 |
14 |
15 | @interface RSSAXParser ()
16 |
17 | @property (nonatomic, weak) id delegate;
18 | @property (nonatomic, assign) xmlParserCtxtPtr context;
19 | @property (nonatomic, assign) BOOL storingCharacters;
20 | @property (nonatomic) NSMutableData *characters;
21 | @property (nonatomic) BOOL delegateRespondsToInternedStringMethod;
22 | @property (nonatomic) BOOL delegateRespondsToInternedStringForValueMethod;
23 | @property (nonatomic) BOOL delegateRespondsToStartElementMethod;
24 | @property (nonatomic) BOOL delegateRespondsToEndElementMethod;
25 | @property (nonatomic) BOOL delegateRespondsToCharactersFoundMethod;
26 | @property (nonatomic) BOOL delegateRespondsToEndOfDocumentMethod;
27 |
28 | @end
29 |
30 |
31 | @implementation RSSAXParser
32 |
33 | + (void)initialize {
34 |
35 | RSSAXInitLibXMLParser();
36 | }
37 |
38 |
39 | #pragma mark - Init
40 |
41 | - (instancetype)initWithDelegate:(id)delegate {
42 |
43 | self = [super init];
44 | if (self == nil)
45 | return nil;
46 |
47 | _delegate = delegate;
48 |
49 | if ([_delegate respondsToSelector:@selector(saxParser:internedStringForName:prefix:)]) {
50 | _delegateRespondsToInternedStringMethod = YES;
51 | }
52 | if ([_delegate respondsToSelector:@selector(saxParser:internedStringForValue:length:)]) {
53 | _delegateRespondsToInternedStringForValueMethod = YES;
54 | }
55 | if ([_delegate respondsToSelector:@selector(saxParser:XMLStartElement:prefix:uri:numberOfNamespaces:namespaces:numberOfAttributes:numberDefaulted:attributes:)]) {
56 | _delegateRespondsToStartElementMethod = YES;
57 | }
58 | if ([_delegate respondsToSelector:@selector(saxParser:XMLEndElement:prefix:uri:)]) {
59 | _delegateRespondsToEndElementMethod = YES;
60 | }
61 | if ([_delegate respondsToSelector:@selector(saxParser:XMLCharactersFound:length:)]) {
62 | _delegateRespondsToCharactersFoundMethod = YES;
63 | }
64 | if ([_delegate respondsToSelector:@selector(saxParserDidReachEndOfDocument:)]) {
65 | _delegateRespondsToEndOfDocumentMethod = YES;
66 | }
67 |
68 | return self;
69 | }
70 |
71 |
72 | #pragma mark - Dealloc
73 |
74 | - (void)dealloc {
75 | if (_context != nil) {
76 | xmlFreeParserCtxt(_context);
77 | _context = nil;
78 | }
79 | _delegate = nil;
80 | }
81 |
82 |
83 | #pragma mark - API
84 |
85 | static xmlSAXHandler saxHandlerStruct;
86 |
87 | - (void)parseData:(NSData *)data {
88 |
89 | [self parseBytes:data.bytes numberOfBytes:data.length];
90 | }
91 |
92 |
93 | - (void)parseBytes:(const void *)bytes numberOfBytes:(NSUInteger)numberOfBytes {
94 |
95 | if (self.context == nil) {
96 |
97 | self.context = xmlCreatePushParserCtxt(&saxHandlerStruct, (__bridge void *)self, nil, 0, nil);
98 | xmlCtxtUseOptions(self.context, XML_PARSE_RECOVER | XML_PARSE_NOENT);
99 | }
100 |
101 | @autoreleasepool {
102 | xmlParseChunk(self.context, (const char *)bytes, (int)numberOfBytes, 0);
103 | }
104 | }
105 |
106 |
107 | - (void)finishParsing {
108 |
109 | NSAssert(self.context != nil, nil);
110 | if (self.context == nil)
111 | return;
112 |
113 | @autoreleasepool {
114 | xmlParseChunk(self.context, nil, 0, 1);
115 | xmlFreeParserCtxt(self.context);
116 | self.context = nil;
117 | self.characters = nil;
118 | }
119 | }
120 |
121 |
122 | - (void)cancel {
123 |
124 | @autoreleasepool {
125 | xmlStopParser(self.context);
126 | }
127 | }
128 |
129 |
130 | - (void)beginStoringCharacters {
131 | self.storingCharacters = YES;
132 | self.characters = [NSMutableData new];
133 | }
134 |
135 |
136 | - (void)endStoringCharacters {
137 | self.storingCharacters = NO;
138 | self.characters = nil;
139 | }
140 |
141 |
142 | - (NSData *)currentCharacters {
143 |
144 | if (!self.storingCharacters) {
145 | return nil;
146 | }
147 |
148 | return self.characters;
149 | }
150 |
151 |
152 | - (NSString *)currentString {
153 |
154 | NSData *d = self.currentCharacters;
155 | if (RSXMLIsEmpty(d)) {
156 | return nil;
157 | }
158 |
159 | return [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
160 | }
161 |
162 |
163 | - (NSString *)currentStringWithTrimmedWhitespace {
164 |
165 | return [self.currentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
166 | }
167 |
168 |
169 | #pragma mark - Attributes Dictionary
170 |
171 | - (NSDictionary *)attributesDictionary:(const xmlChar **)attributes numberOfAttributes:(NSInteger)numberOfAttributes {
172 |
173 | if (numberOfAttributes < 1 || !attributes) {
174 | return nil;
175 | }
176 |
177 | NSMutableDictionary *d = [NSMutableDictionary new];
178 |
179 | @autoreleasepool {
180 | NSInteger i = 0, j = 0;
181 | for (i = 0, j = 0; i < numberOfAttributes; i++, j+=5) {
182 |
183 | NSUInteger lenValue = (NSUInteger)(attributes[j + 4] - attributes[j + 3]);
184 | NSString *value = nil;
185 |
186 | if (self.delegateRespondsToInternedStringForValueMethod) {
187 | value = [self.delegate saxParser:self internedStringForValue:(const void *)attributes[j + 3] length:lenValue];
188 | }
189 | if (!value) {
190 | value = [[NSString alloc] initWithBytes:(const void *)attributes[j + 3] length:lenValue encoding:NSUTF8StringEncoding];
191 | }
192 |
193 | NSString *attributeName = nil;
194 |
195 | if (self.delegateRespondsToInternedStringMethod) {
196 | attributeName = [self.delegate saxParser:self internedStringForName:(const xmlChar *)attributes[j] prefix:(const xmlChar *)attributes[j + 1]];
197 | }
198 |
199 | if (!attributeName) {
200 | attributeName = [NSString stringWithUTF8String:(const char *)attributes[j]];
201 | if (attributes[j + 1]) {
202 | NSString *attributePrefix = [NSString stringWithUTF8String:(const char *)attributes[j + 1]];
203 | attributeName = [NSString stringWithFormat:@"%@:%@", attributePrefix, attributeName];
204 | }
205 | }
206 |
207 | if (value && attributeName) {
208 | d[attributeName] = value;
209 | }
210 | }
211 | }
212 |
213 | return d;
214 | }
215 |
216 |
217 | #pragma mark - Equal Tags
218 |
219 | BOOL RSSAXEqualTags(const xmlChar *localName, const char *tag, NSInteger tagLength) {
220 |
221 | if (!localName) {
222 | return NO;
223 | }
224 | return !strncmp((const char *)localName, tag, (size_t)tagLength);
225 | }
226 |
227 |
228 | #pragma mark - Callbacks
229 |
230 | - (void)xmlEndDocument {
231 |
232 | @autoreleasepool {
233 | if (self.delegateRespondsToEndOfDocumentMethod) {
234 | [self.delegate saxParserDidReachEndOfDocument:self];
235 | }
236 |
237 | [self endStoringCharacters];
238 | }
239 | }
240 |
241 |
242 | - (void)xmlCharactersFound:(const xmlChar *)ch length:(NSUInteger)length {
243 |
244 | @autoreleasepool {
245 | if (self.storingCharacters) {
246 | [self.characters appendBytes:(const void *)ch length:length];
247 | }
248 |
249 | if (self.delegateRespondsToCharactersFoundMethod) {
250 | [self.delegate saxParser:self XMLCharactersFound:ch length:length];
251 | }
252 | }
253 | }
254 |
255 |
256 | - (void)xmlStartElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri numberOfNamespaces:(int)numberOfNamespaces namespaces:(const xmlChar **)namespaces numberOfAttributes:(int)numberOfAttributes numberDefaulted:(int)numberDefaulted attributes:(const xmlChar **)attributes {
257 |
258 | @autoreleasepool {
259 | if (self.delegateRespondsToStartElementMethod) {
260 |
261 | [self.delegate saxParser:self XMLStartElement:localName prefix:prefix uri:uri numberOfNamespaces:numberOfNamespaces namespaces:namespaces numberOfAttributes:numberOfAttributes numberDefaulted:numberDefaulted attributes:attributes];
262 | }
263 | }
264 | }
265 |
266 |
267 | - (void)xmlEndElement:(const xmlChar *)localName prefix:(const xmlChar *)prefix uri:(const xmlChar *)uri {
268 |
269 | @autoreleasepool {
270 | if (self.delegateRespondsToEndElementMethod) {
271 | [self.delegate saxParser:self XMLEndElement:localName prefix:prefix uri:uri];
272 | }
273 |
274 | [self endStoringCharacters];
275 | }
276 | }
277 |
278 |
279 | @end
280 |
281 |
282 | static void startElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes) {
283 |
284 | [(__bridge RSSAXParser *)context xmlStartElement:localname prefix:prefix uri:URI numberOfNamespaces:nb_namespaces namespaces:namespaces numberOfAttributes:nb_attributes numberDefaulted:nb_defaulted attributes:attributes];
285 | }
286 |
287 |
288 | static void endElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI) {
289 | [(__bridge RSSAXParser *)context xmlEndElement:localname prefix:prefix uri:URI];
290 | }
291 |
292 |
293 | static void charactersFoundSAX(void *context, const xmlChar *ch, int len) {
294 | [(__bridge RSSAXParser *)context xmlCharactersFound:ch length:(NSUInteger)len];
295 | }
296 |
297 |
298 | static void endDocumentSAX(void *context) {
299 | [(__bridge RSSAXParser *)context xmlEndDocument];
300 | }
301 |
302 |
303 | static xmlSAXHandler saxHandlerStruct = {
304 | nil, /* internalSubset */
305 | nil, /* isStandalone */
306 | nil, /* hasInternalSubset */
307 | nil, /* hasExternalSubset */
308 | nil, /* resolveEntity */
309 | nil, /* getEntity */
310 | nil, /* entityDecl */
311 | nil, /* notationDecl */
312 | nil, /* attributeDecl */
313 | nil, /* elementDecl */
314 | nil, /* unparsedEntityDecl */
315 | nil, /* setDocumentLocator */
316 | nil, /* startDocument */
317 | endDocumentSAX, /* endDocument */
318 | nil, /* startElement*/
319 | nil, /* endElement */
320 | nil, /* reference */
321 | charactersFoundSAX, /* characters */
322 | nil, /* ignorableWhitespace */
323 | nil, /* processingInstruction */
324 | nil, /* comment */
325 | nil, /* warning */
326 | nil, /* error */
327 | nil, /* fatalError //: unused error() get all the errors */
328 | nil, /* getParameterEntity */
329 | nil, /* cdataBlock */
330 | nil, /* externalSubset */
331 | XML_SAX2_MAGIC,
332 | nil,
333 | startElementSAX, /* startElementNs */
334 | endElementSAX, /* endElementNs */
335 | nil /* serror */
336 | };
337 |
338 |
339 | void RSSAXInitLibXMLParser(void) {
340 |
341 | static dispatch_once_t onceToken;
342 | dispatch_once(&onceToken, ^{
343 | xmlInitParser();
344 | });
345 | }
346 |
347 |
--------------------------------------------------------------------------------
/RSXML/RSXML.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSXML.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 7/12/15.
6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 |
12 | #import
13 | #import
14 |
15 | #import
16 | #import
17 | #import
18 | #import
19 | #import
20 | #import
21 |
22 | #import
23 | #import
24 | #import
25 | #import
26 | #import
27 |
28 | #import
29 |
30 | #import
31 | #import
32 |
33 | // HTML
34 |
35 | #import
36 |
37 | #import
38 | #import
39 | #import
40 |
--------------------------------------------------------------------------------
/RSXML/RSXMLData.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSXMLData.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 8/24/15.
6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | @interface RSXMLData : NSObject
14 |
15 | - (instancetype)initWithData:(NSData *)data urlString:(NSString *)urlString;
16 |
17 | @property (nonatomic, readonly) NSData *data;
18 | @property (nonatomic, readonly) NSString *urlString;
19 |
20 | @end
21 |
22 | NS_ASSUME_NONNULL_END
23 |
--------------------------------------------------------------------------------
/RSXML/RSXMLData.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSXMLData.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 8/24/15.
6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import "RSXMLData.h"
10 |
11 | @implementation RSXMLData
12 |
13 |
14 | - (instancetype)initWithData:(NSData *)data urlString:(NSString *)urlString {
15 |
16 | self = [super init];
17 | if (!self) {
18 | return nil;
19 | }
20 |
21 | _data = data;
22 | _urlString = urlString;
23 |
24 | return self;
25 | }
26 |
27 |
28 | @end
29 |
--------------------------------------------------------------------------------
/RSXML/RSXMLError.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSXMLError.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 2/28/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | extern NSString *RSXMLErrorDomain;
12 |
13 |
14 | typedef NS_ENUM(NSInteger, RSXMLErrorCode) {
15 | RSXMLErrorCodeDataIsWrongFormat = 1024
16 | };
17 |
18 |
19 | NSError *RSOPMLWrongFormatError(NSString *fileName);
20 |
--------------------------------------------------------------------------------
/RSXML/RSXMLError.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSXMLError.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 2/28/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import "RSXMLError.h"
10 |
11 | NSString *RSXMLErrorDomain = @"com.ranchero.RSXML";
12 |
13 | NSError *RSOPMLWrongFormatError(NSString *fileName) {
14 |
15 | NSString *localizedDescriptionFormatString = NSLocalizedString(@"The file ‘%@’ can’t be parsed because it’s not an OPML file.", @"OPML wrong format");
16 | NSString *localizedDescription = [NSString stringWithFormat:localizedDescriptionFormatString, fileName];
17 |
18 | NSString *localizedFailureString = NSLocalizedString(@"The file is not an OPML file.", @"OPML wrong format");
19 | NSDictionary *userInfo = @{NSLocalizedDescriptionKey: localizedDescription, NSLocalizedFailureReasonErrorKey: localizedFailureString};
20 |
21 | return [[NSError alloc] initWithDomain:RSXMLErrorDomain code:RSXMLErrorCodeDataIsWrongFormat userInfo:userInfo];
22 | }
23 |
--------------------------------------------------------------------------------
/RSXML/RSXMLInternal.h:
--------------------------------------------------------------------------------
1 | //
2 | // RSXMLInternal.h
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 12/26/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | BOOL RSXMLIsEmpty(id _Nullable obj);
14 | BOOL RSXMLStringIsEmpty(NSString * _Nullable s);
15 |
16 |
17 | @interface NSString (RSXMLInternal)
18 |
19 | - (NSString *)rsxml_md5HashString;
20 |
21 | @end
22 |
23 |
24 | @interface NSDictionary (RSXMLInternal)
25 |
26 | - (nullable id)rsxml_objectForCaseInsensitiveKey:(NSString *)key;
27 |
28 | @end
29 |
30 | NS_ASSUME_NONNULL_END
31 |
32 |
--------------------------------------------------------------------------------
/RSXML/RSXMLInternal.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSXMLInternal.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 12/26/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "RSXMLInternal.h"
11 |
12 |
13 | static BOOL RSXMLIsNil(id obj) {
14 |
15 | return obj == nil || obj == [NSNull null];
16 | }
17 |
18 | BOOL RSXMLIsEmpty(id obj) {
19 |
20 | if (RSXMLIsNil(obj)) {
21 | return YES;
22 | }
23 |
24 | if ([obj respondsToSelector:@selector(count)]) {
25 | return [obj count] < 1;
26 | }
27 |
28 | if ([obj respondsToSelector:@selector(length)]) {
29 | return [obj length] < 1;
30 | }
31 |
32 | return NO; /*Shouldn't get here very often.*/
33 | }
34 |
35 | BOOL RSXMLStringIsEmpty(NSString *s) {
36 |
37 | return RSXMLIsNil(s) || s.length < 1;
38 | }
39 |
40 |
41 | @implementation NSString (RSXMLInternal)
42 |
43 | - (NSData *)rsxml_md5Hash {
44 |
45 | NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding];
46 | unsigned char hash[CC_MD5_DIGEST_LENGTH];
47 | CC_MD5(data.bytes, (CC_LONG)data.length, hash);
48 |
49 | return [NSData dataWithBytes:(const void *)hash length:CC_MD5_DIGEST_LENGTH];
50 | }
51 |
52 | - (NSString *)rsxml_md5HashString {
53 |
54 | NSData *md5Data = [self rsxml_md5Hash];
55 | const Byte *bytes = md5Data.bytes;
56 | return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]];
57 | }
58 |
59 | @end
60 |
61 |
62 | @implementation NSDictionary (RSXMLInternal)
63 |
64 |
65 | - (nullable id)rsxml_objectForCaseInsensitiveKey:(NSString *)key {
66 |
67 | id obj = self[key];
68 | if (obj) {
69 | return obj;
70 | }
71 |
72 | for (NSString *oneKey in self.allKeys) {
73 |
74 | if ([oneKey isKindOfClass:[NSString class]] && [key caseInsensitiveCompare:oneKey] == NSOrderedSame) {
75 | return self[oneKey];
76 | }
77 | }
78 |
79 | return nil;
80 | }
81 |
82 |
83 | @end
84 |
--------------------------------------------------------------------------------
/RSXMLTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/RSXMLTests/RSDateParserTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSDateParserTests.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 12/26/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import
10 | @import RSXML;
11 |
12 | @interface RSDateParserTests : XCTestCase
13 |
14 | @end
15 |
16 | @implementation RSDateParserTests
17 |
18 | static NSDate *dateWithValues(NSInteger year, NSInteger month, NSInteger day, NSInteger hour, NSInteger minute, NSInteger second) {
19 |
20 | NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
21 | dateComponents.calendar = NSCalendar.currentCalendar;
22 | dateComponents.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
23 | [dateComponents setValue:year forComponent:NSCalendarUnitYear];
24 | [dateComponents setValue:month forComponent:NSCalendarUnitMonth];
25 | [dateComponents setValue:day forComponent:NSCalendarUnitDay];
26 | [dateComponents setValue:hour forComponent:NSCalendarUnitHour];
27 | [dateComponents setValue:minute forComponent:NSCalendarUnitMinute];
28 | [dateComponents setValue:second forComponent:NSCalendarUnitSecond];
29 |
30 | return dateComponents.date;
31 | }
32 |
33 | - (void)testDateWithString {
34 |
35 | NSDate *expectedDateResult = dateWithValues(2010, 5, 28, 21, 3, 38);
36 | XCTAssertNotNil(expectedDateResult);
37 |
38 | NSDate *d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 +0000");
39 | XCTAssertEqualObjects(d, expectedDateResult);
40 |
41 | d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 +00:00");
42 | XCTAssertEqualObjects(d, expectedDateResult);
43 |
44 | d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 -00:00");
45 | XCTAssertEqualObjects(d, expectedDateResult);
46 |
47 | d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 -0000");
48 | XCTAssertEqualObjects(d, expectedDateResult);
49 |
50 | d = RSDateWithString(@"Fri, 28 May 2010 21:03:38 GMT");
51 | XCTAssertEqualObjects(d, expectedDateResult);
52 |
53 | d = RSDateWithString(@"2010-05-28T21:03:38+00:00");
54 | XCTAssertEqualObjects(d, expectedDateResult);
55 |
56 | d = RSDateWithString(@"2010-05-28T21:03:38+0000");
57 | XCTAssertEqualObjects(d, expectedDateResult);
58 |
59 | d = RSDateWithString(@"2010-05-28T21:03:38-0000");
60 | XCTAssertEqualObjects(d, expectedDateResult);
61 |
62 | d = RSDateWithString(@"2010-05-28T21:03:38-00:00");
63 | XCTAssertEqualObjects(d, expectedDateResult);
64 |
65 | d = RSDateWithString(@"2010-05-28T21:03:38Z");
66 | XCTAssertEqualObjects(d, expectedDateResult);
67 |
68 | expectedDateResult = dateWithValues(2010, 7, 13, 17, 6, 40);
69 | d = RSDateWithString(@"2010-07-13T17:06:40+00:00");
70 | XCTAssertEqualObjects(d, expectedDateResult);
71 |
72 | expectedDateResult = dateWithValues(2010, 4, 30, 12, 0, 0);
73 | d = RSDateWithString(@"30 Apr 2010 5:00 PDT");
74 | XCTAssertEqualObjects(d, expectedDateResult);
75 |
76 | expectedDateResult = dateWithValues(2010, 5, 21, 21, 22, 53);
77 | d = RSDateWithString(@"21 May 2010 21:22:53 GMT");
78 | XCTAssertEqualObjects(d, expectedDateResult);
79 |
80 | expectedDateResult = dateWithValues(2010, 6, 9, 5, 0, 0);
81 | d = RSDateWithString(@"Wed, 09 Jun 2010 00:00 EST");
82 | XCTAssertEqualObjects(d, expectedDateResult);
83 |
84 | expectedDateResult = dateWithValues(2010, 6, 23, 3, 43, 50);
85 | d = RSDateWithString(@"Wed, 23 Jun 2010 03:43:50 Z");
86 | XCTAssertEqualObjects(d, expectedDateResult);
87 |
88 | expectedDateResult = dateWithValues(2010, 6, 22, 3, 57, 49);
89 | d = RSDateWithString(@"2010-06-22T03:57:49+00:00");
90 | XCTAssertEqualObjects(d, expectedDateResult);
91 |
92 | expectedDateResult = dateWithValues(2010, 11, 17, 13, 40, 07);
93 | d = RSDateWithString(@"2010-11-17T08:40:07-05:00");
94 | XCTAssertEqualObjects(d, expectedDateResult);
95 | }
96 |
97 |
98 | @end
99 |
--------------------------------------------------------------------------------
/RSXMLTests/RSEntityTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSEntityTests.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 12/26/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import
10 | @import RSXML;
11 |
12 | @interface RSEntityTests : XCTestCase
13 |
14 | @end
15 |
16 | @implementation RSEntityTests
17 |
18 |
19 | - (void)testInnerAmpersand {
20 |
21 | NSString *expectedResult = @"A&P";
22 |
23 | NSString *result = [@"A&P" rs_stringByDecodingHTMLEntities];
24 | XCTAssertEqualObjects(result, expectedResult);
25 |
26 | result = [@"A&P" rs_stringByDecodingHTMLEntities];
27 | XCTAssertEqualObjects(result, expectedResult);
28 |
29 | result = [@"A&P" rs_stringByDecodingHTMLEntities];
30 | XCTAssertEqualObjects(result, expectedResult);
31 |
32 | }
33 |
34 | - (void)testSingleEntity {
35 |
36 | NSString *result = [@"∞" rs_stringByDecodingHTMLEntities];
37 | XCTAssertEqualObjects(result, @"∞");
38 |
39 | result = [@"&" rs_stringByDecodingHTMLEntities];
40 | XCTAssertEqualObjects(result, @"&");
41 |
42 | result = [@"’" rs_stringByDecodingHTMLEntities];
43 | XCTAssertEqualObjects(result, @"’");
44 | }
45 |
46 | - (void)testNotEntities {
47 |
48 | NSString *s = @"&&\t\nFoo & Bar &0; Baz & 1238 4948 More things &foobar;&";
49 | NSString *result = [s rs_stringByDecodingHTMLEntities];
50 | XCTAssertEqualObjects(result, s);
51 | }
52 |
53 | - (void)testURLs {
54 |
55 | NSString *urlString = @"http://www.nytimes.com/2015/09/05/us/at-west-point-annual-pillow-fight-becomes-weaponized.html?mwrsm=Email&_r=1&pagewanted=all";
56 | NSString *expectedResult = @"http://www.nytimes.com/2015/09/05/us/at-west-point-annual-pillow-fight-becomes-weaponized.html?mwrsm=Email&_r=1&pagewanted=all";
57 |
58 | NSString *result = [urlString rs_stringByDecodingHTMLEntities];
59 | XCTAssertEqualObjects(result, expectedResult);
60 | }
61 |
62 | - (void)testEntityPlusWhitespace {
63 |
64 | NSString *s = @"∞ Permalink";
65 | NSString *expectedResult = @"∞ Permalink";
66 |
67 | NSString *result = [s rs_stringByDecodingHTMLEntities];
68 | XCTAssertEqualObjects(result, expectedResult);
69 | }
70 |
71 | - (void)testNonBreakingSpace {
72 |
73 | NSString *s = @" -- just some spaces";
74 | NSString *expectedResult = [NSString stringWithFormat:@"%C%C -- just some spaces", 160, 160];
75 |
76 | NSString *result = [s rs_stringByDecodingHTMLEntities];
77 | XCTAssertEqualObjects(result, expectedResult);
78 | }
79 |
80 | @end
81 |
--------------------------------------------------------------------------------
/RSXMLTests/RSHTMLTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSHTMLTests.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 3/5/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | @import RSXML;
10 | #import
11 |
12 | @interface RSHTMLTests : XCTestCase
13 |
14 | @end
15 |
16 | @implementation RSHTMLTests
17 |
18 |
19 | + (RSXMLData *)xmlData:(NSString *)title urlString:(NSString *)urlString {
20 |
21 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:title ofType:@"html"];
22 | NSData *d = [[NSData alloc] initWithContentsOfFile:s];
23 | return [[RSXMLData alloc] initWithData:d urlString:urlString];
24 | }
25 |
26 |
27 | + (RSXMLData *)daringFireballData {
28 |
29 | static RSXMLData *xmlData = nil;
30 |
31 | static dispatch_once_t onceToken;
32 | dispatch_once(&onceToken, ^{
33 | xmlData = [self xmlData:@"DaringFireball" urlString:@"http://daringfireball.net/"];
34 | });
35 |
36 | return xmlData;
37 | }
38 |
39 |
40 | + (RSXMLData *)furboData {
41 |
42 | static RSXMLData *xmlData = nil;
43 |
44 | static dispatch_once_t onceToken;
45 | dispatch_once(&onceToken, ^{
46 | xmlData = [self xmlData:@"furbo" urlString:@"http://furbo.org/"];
47 | });
48 |
49 | return xmlData;
50 | }
51 |
52 |
53 | + (RSXMLData *)inessentialData {
54 |
55 | static RSXMLData *xmlData = nil;
56 |
57 | static dispatch_once_t onceToken;
58 | dispatch_once(&onceToken, ^{
59 | xmlData = [self xmlData:@"inessential" urlString:@"http://inessential.com/"];
60 | });
61 |
62 | return xmlData;
63 | }
64 |
65 |
66 | + (RSXMLData *)sixcolorsData {
67 |
68 | static RSXMLData *xmlData = nil;
69 |
70 | static dispatch_once_t onceToken;
71 | dispatch_once(&onceToken, ^{
72 | xmlData = [self xmlData:@"sixcolors" urlString:@"https://sixcolors.com/"];
73 | });
74 |
75 | return xmlData;
76 | }
77 |
78 |
79 | - (void)testDaringFireball {
80 |
81 | RSXMLData *xmlData = [[self class] daringFireballData];
82 | RSHTMLMetadata *metadata = [RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData];
83 |
84 | XCTAssertEqualObjects(metadata.faviconLink, @"http://daringfireball.net/graphics/favicon.ico?v=005");
85 |
86 | XCTAssertTrue(metadata.feedLinks.count == 1);
87 | RSHTMLMetadataFeedLink *feedLink = metadata.feedLinks[0];
88 | XCTAssertNil(feedLink.title);
89 | XCTAssertEqualObjects(feedLink.type, @"application/atom+xml");
90 | XCTAssertEqualObjects(feedLink.urlString, @"http://daringfireball.net/feeds/main");
91 | }
92 |
93 |
94 | - (void)testDaringFireballPerformance {
95 |
96 | RSXMLData *xmlData = [[self class] daringFireballData];
97 |
98 | [self measureBlock:^{
99 | (void)[RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData];
100 | }];
101 | }
102 |
103 | - (void)testFurbo {
104 |
105 | RSXMLData *xmlData = [[self class] furboData];
106 | RSHTMLMetadata *metadata = [RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData];
107 |
108 | XCTAssertEqualObjects(metadata.faviconLink, @"http://furbo.org/favicon.ico");
109 |
110 | XCTAssertTrue(metadata.feedLinks.count == 1);
111 | RSHTMLMetadataFeedLink *feedLink = metadata.feedLinks[0];
112 | XCTAssertEqualObjects(feedLink.title, @"Iconfactory News Feed");
113 | XCTAssertEqualObjects(feedLink.type, @"application/rss+xml");
114 | }
115 |
116 |
117 | - (void)testFurboPerformance {
118 |
119 | RSXMLData *xmlData = [[self class] furboData];
120 |
121 | [self measureBlock:^{
122 | (void)[RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData];
123 | }];
124 | }
125 |
126 |
127 | - (void)testInessential {
128 |
129 | RSXMLData *xmlData = [[self class] inessentialData];
130 | RSHTMLMetadata *metadata = [RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData];
131 |
132 | XCTAssertNil(metadata.faviconLink);
133 |
134 | XCTAssertTrue(metadata.feedLinks.count == 1);
135 | RSHTMLMetadataFeedLink *feedLink = metadata.feedLinks[0];
136 | XCTAssertEqualObjects(feedLink.title, @"RSS");
137 | XCTAssertEqualObjects(feedLink.type, @"application/rss+xml");
138 | XCTAssertEqualObjects(feedLink.urlString, @"http://inessential.com/xml/rss.xml");
139 |
140 | XCTAssertEqual(metadata.appleTouchIcons.count, 0);
141 | }
142 |
143 |
144 | - (void)testInessentialPerformance {
145 |
146 | RSXMLData *xmlData = [[self class] inessentialData];
147 |
148 | [self measureBlock:^{
149 | (void)[RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData];
150 | }];
151 | }
152 |
153 |
154 | - (void)testSixcolors {
155 |
156 | RSXMLData *xmlData = [[self class] sixcolorsData];
157 | RSHTMLMetadata *metadata = [RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData];
158 |
159 | XCTAssertEqualObjects(metadata.faviconLink, @"https://sixcolors.com/images/favicon.ico");
160 |
161 | XCTAssertTrue(metadata.feedLinks.count == 1);
162 | RSHTMLMetadataFeedLink *feedLink = metadata.feedLinks[0];
163 | XCTAssertEqualObjects(feedLink.title, @"RSS");
164 | XCTAssertEqualObjects(feedLink.type, @"application/rss+xml");
165 | XCTAssertEqualObjects(feedLink.urlString, @"http://feedpress.me/sixcolors");
166 |
167 | XCTAssertEqual(metadata.appleTouchIcons.count, 6);
168 | RSHTMLMetadataAppleTouchIcon *icon = metadata.appleTouchIcons[3];
169 | XCTAssertEqualObjects(icon.rel, @"apple-touch-icon");
170 | XCTAssertEqualObjects(icon.sizes, @"120x120");
171 | XCTAssertEqualObjects(icon.urlString, @"https://sixcolors.com/apple-touch-icon-120.png");
172 | }
173 |
174 |
175 | - (void)testSixcolorsPerformance {
176 |
177 | RSXMLData *xmlData = [[self class] sixcolorsData];
178 |
179 | [self measureBlock:^{
180 | (void)[RSHTMLMetadataParser HTMLMetadataWithXMLData:xmlData];
181 | }];
182 | }
183 |
184 | #pragma mark - Links
185 |
186 | - (void)testSixColorsLinks {
187 |
188 | RSXMLData *xmlData = [[self class] sixcolorsData];
189 | NSArray *links = [RSHTMLLinkParser htmlLinksWithData:xmlData];
190 |
191 | NSString *linkToFind = @"https://www.theincomparable.com/theincomparable/290/index.php";
192 | NSString *textToFind = @"this week’s episode of The Incomparable";
193 |
194 | BOOL found = NO;
195 | for (RSHTMLLink *oneLink in links) {
196 |
197 | if ([oneLink.urlString isEqualToString:linkToFind] && [oneLink.text isEqualToString:textToFind]) {
198 | found = YES;
199 | break;
200 | }
201 | }
202 |
203 | XCTAssertTrue(found, @"Expected link should have been found.");
204 | XCTAssertEqual(links.count, 131, @"Expected 131 links.");
205 | }
206 |
207 |
208 | - (void)testSixColorsLinksPerformance {
209 |
210 | RSXMLData *xmlData = [[self class] sixcolorsData];
211 |
212 | [self measureBlock:^{
213 | (void)[RSHTMLLinkParser htmlLinksWithData:xmlData];
214 | }];
215 | }
216 |
217 | @end
218 |
219 |
--------------------------------------------------------------------------------
/RSXMLTests/RSOPMLTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSOPMLTests.m
3 | // RSXML
4 | //
5 | // Created by Brent Simmons on 2/28/16.
6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import
10 | @import RSXML;
11 |
12 | @interface RSOPMLTests : XCTestCase
13 |
14 | @end
15 |
16 | @implementation RSOPMLTests
17 |
18 | + (RSXMLData *)subsData {
19 |
20 | static RSXMLData *xmlData = nil;
21 |
22 | static dispatch_once_t onceToken;
23 | dispatch_once(&onceToken, ^{
24 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"Subs" ofType:@"opml"];
25 | NSData *d = [[NSData alloc] initWithContentsOfFile:s];
26 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://example.org/"];
27 | });
28 |
29 | return xmlData;
30 | }
31 |
32 | - (void)testNotOPML {
33 |
34 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"DaringFireball" ofType:@"rss"];
35 | NSData *d = [[NSData alloc] initWithContentsOfFile:s];
36 | RSXMLData *xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://example.org/"];
37 | RSOPMLParser *parser = [[RSOPMLParser alloc] initWithXMLData:xmlData];
38 | XCTAssertNotNil(parser.error);
39 |
40 | d = [[NSData alloc] initWithContentsOfFile:@"/System/Library/Kernels/kernel"];
41 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"/System/Library/Kernels/kernel"];
42 | parser = [[RSOPMLParser alloc] initWithXMLData:xmlData];
43 | XCTAssertNotNil(parser.error);
44 | }
45 |
46 |
47 | - (void)testSubsPerformance {
48 |
49 | RSXMLData *xmlData = [[self class] subsData];
50 |
51 | [self measureBlock:^{
52 | (void)[[RSOPMLParser alloc] initWithXMLData:xmlData];
53 | }];
54 | }
55 |
56 |
57 | - (void)testSubsStructure {
58 |
59 | RSXMLData *xmlData = [[self class] subsData];
60 |
61 | RSOPMLParser *parser = [[RSOPMLParser alloc] initWithXMLData:xmlData];
62 | XCTAssertNotNil(parser);
63 |
64 | RSOPMLDocument *document = parser.OPMLDocument;
65 | XCTAssertNotNil(document);
66 |
67 | [self checkStructureForOPMLItem:document];
68 | }
69 |
70 | - (void)checkStructureForOPMLItem:(RSOPMLItem *)item {
71 |
72 | RSOPMLFeedSpecifier *feedSpecifier = item.OPMLFeedSpecifier;
73 |
74 | if (![item isKindOfClass:[RSOPMLDocument class]]) {
75 | XCTAssertNotNil(item.attributes.opml_text);
76 | }
77 |
78 | // If it has no children, it should have a feed specifier. The converse is also true.
79 | BOOL isFolder = (item.children.count > 0);
80 | if (!isFolder && [item.attributes.opml_title isEqualToString:@"Skip"]) {
81 | isFolder = YES;
82 | }
83 |
84 | if (!isFolder) {
85 | XCTAssertNotNil(feedSpecifier.title);
86 | XCTAssertNotNil(feedSpecifier.feedURL);
87 | }
88 | else {
89 | XCTAssertNil(feedSpecifier);
90 | if (![item isKindOfClass:[RSOPMLDocument class]]) {
91 | XCTAssertNotNil(item.attributes.opml_title);
92 | }
93 | }
94 |
95 | if (item.children.count > 0) {
96 | for (RSOPMLItem *oneItem in item.children) {
97 | [self checkStructureForOPMLItem:oneItem];
98 | }
99 | }
100 | }
101 |
102 |
103 | @end
104 |
--------------------------------------------------------------------------------
/RSXMLTests/RSXMLTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // RSXMLTests.m
3 | // RSXMLTests
4 | //
5 | // Created by Brent Simmons on 7/12/15.
6 | // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
7 | //
8 |
9 | #import
10 | @import RSXML;
11 |
12 | @interface RSXMLTests : XCTestCase
13 |
14 | @end
15 |
16 | @implementation RSXMLTests
17 |
18 | + (RSXMLData *)oftData {
19 |
20 | static RSXMLData *xmlData = nil;
21 |
22 | static dispatch_once_t onceToken;
23 | dispatch_once(&onceToken, ^{
24 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"OneFootTsunami" ofType:@"atom"];
25 | NSData *d = [[NSData alloc] initWithContentsOfFile:s];
26 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://onefoottsunami.com/"];
27 | });
28 |
29 | return xmlData;
30 | }
31 |
32 |
33 | + (RSXMLData *)scriptingNewsData {
34 |
35 | static RSXMLData *xmlData = nil;
36 |
37 | static dispatch_once_t onceToken;
38 | dispatch_once(&onceToken, ^{
39 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"scriptingNews" ofType:@"rss"];
40 | NSData *d = [[NSData alloc] initWithContentsOfFile:s];
41 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://scripting.com/"];
42 | });
43 |
44 | return xmlData;
45 | }
46 |
47 |
48 | + (RSXMLData *)mantonData {
49 |
50 | static RSXMLData *xmlData = nil;
51 |
52 | static dispatch_once_t onceToken;
53 | dispatch_once(&onceToken, ^{
54 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"manton" ofType:@"rss"];
55 | NSData *d = [[NSData alloc] initWithContentsOfFile:s];
56 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://manton.org/"];
57 | });
58 |
59 | return xmlData;
60 | }
61 |
62 |
63 | + (RSXMLData *)daringFireballData {
64 |
65 | static RSXMLData *xmlData = nil;
66 |
67 | static dispatch_once_t onceToken;
68 | dispatch_once(&onceToken, ^{
69 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"DaringFireball" ofType:@"rss"];
70 | NSData *d = [[NSData alloc] initWithContentsOfFile:s];
71 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://daringfireball.net/"];
72 | });
73 |
74 | return xmlData;
75 | }
76 |
77 |
78 | + (RSXMLData *)katieFloydData {
79 |
80 | static RSXMLData *xmlData = nil;
81 |
82 | static dispatch_once_t onceToken;
83 | dispatch_once(&onceToken, ^{
84 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"KatieFloyd" ofType:@"rss"];
85 | NSData *d = [[NSData alloc] initWithContentsOfFile:s];
86 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"http://katiefloyd.com/"];
87 | });
88 |
89 | return xmlData;
90 | }
91 |
92 |
93 | + (RSXMLData *)eMarleyData {
94 |
95 | static RSXMLData *xmlData = nil;
96 |
97 | static dispatch_once_t onceToken;
98 | dispatch_once(&onceToken, ^{
99 | NSString *s = [[NSBundle bundleForClass:[self class]] pathForResource:@"EMarley" ofType:@"rss"];
100 | NSData *d = [[NSData alloc] initWithContentsOfFile:s];
101 | xmlData = [[RSXMLData alloc] initWithData:d urlString:@"https://medium.com/@emarley"];
102 | });
103 |
104 | return xmlData;
105 | }
106 |
107 |
108 | - (void)testOneFootTsunami {
109 |
110 | NSError *error = nil;
111 | RSXMLData *xmlData = [[self class] oftData];
112 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error);
113 | NSLog(@"parsedFeed: %@", parsedFeed);
114 | }
115 |
116 |
117 | - (void)testOFTPerformance {
118 |
119 | RSXMLData *xmlData = [[self class] oftData];
120 |
121 | [self measureBlock:^{
122 | NSError *error = nil;
123 | RSParseFeedSync(xmlData, &error);
124 | }];
125 | }
126 |
127 |
128 | - (void)testScriptingNews {
129 |
130 | NSError *error = nil;
131 | RSXMLData *xmlData = [[self class] scriptingNewsData];
132 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error);
133 | NSLog(@"parsedFeed: %@", parsedFeed);
134 | }
135 |
136 |
137 | - (void)testManton {
138 |
139 | NSError *error = nil;
140 | RSXMLData *xmlData = [[self class] mantonData];
141 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error);
142 | NSLog(@"parsedFeed: %@", parsedFeed);
143 | }
144 |
145 |
146 | - (void)testKatieFloyd {
147 |
148 | NSError *error = nil;
149 | RSXMLData *xmlData = [[self class] katieFloydData];
150 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error);
151 | XCTAssertEqualObjects(parsedFeed.title, @"Katie Floyd");
152 | }
153 |
154 |
155 | - (void)testEMarley {
156 |
157 | NSError *error = nil;
158 | RSXMLData *xmlData = [[self class] eMarleyData];
159 | RSParsedFeed *parsedFeed = RSParseFeedSync(xmlData, &error);
160 | XCTAssertEqualObjects(parsedFeed.title, @"Stories by Liz Marley on Medium");
161 | XCTAssertEqual(parsedFeed.articles.count, 10);
162 | }
163 |
164 |
165 | - (void)testScriptingNewsPerformance {
166 |
167 | RSXMLData *xmlData = [[self class] scriptingNewsData];
168 |
169 | [self measureBlock:^{
170 | NSError *error = nil;
171 | RSParseFeedSync(xmlData, &error);
172 | }];
173 |
174 | }
175 |
176 |
177 | - (void)testMantonPerformance {
178 |
179 | RSXMLData *xmlData = [[self class] mantonData];
180 |
181 | [self measureBlock:^{
182 | NSError *error = nil;
183 | RSParseFeedSync(xmlData, &error);
184 | }];
185 |
186 | }
187 |
188 |
189 | - (void)testDaringFireballPerformance {
190 |
191 | RSXMLData *xmlData = [[self class] daringFireballData];
192 |
193 | [self measureBlock:^{
194 | NSError *error = nil;
195 | RSParseFeedSync(xmlData, &error);
196 | }];
197 | }
198 |
199 |
200 | - (void)testCanParseFeedPerformance {
201 |
202 | RSXMLData *xmlData = [[self class] daringFireballData];
203 | // 0.379
204 | [self measureBlock:^{
205 | for (NSInteger i = 0; i < 100; i++) {
206 | RSCanParseFeed(xmlData);
207 | }
208 | }];
209 | }
210 |
211 | @end
212 |
--------------------------------------------------------------------------------
/RSXMLTests/Resources/EMarley.rss:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | https://medium.com/@emarley?source=rss-b4981c59ffa5------2
6 |
7 | https://d262ilb51hltx0.cloudfront.net/fit/c/150/150/0*I9s5OlzJw_En0NzC.jpg
8 | Stories by Liz Marley on Medium
9 | https://medium.com/@emarley?source=rss-b4981c59ffa5------2
10 |
11 | Medium
12 | Sun, 28 Aug 2016 17:27:51 GMT
13 |
14 |
15 |
16 | -
17 |
18 | Here’s a partial collection of links from my talk today…
Continue reading on »
]]>
19 | https://medium.com/@emarley/ui-automation-screenshots-c44a41af38d1?source=rss-b4981c59ffa5------2
20 | https://medium.com/p/c44a41af38d1
21 |
22 | Sat, 07 May 2016 23:53:30 GMT
23 |
24 | -
25 |
26 | “The [software developer tool] team clearly doesn’t use [that tool] themselves.”
Continue reading on »
]]>
27 | https://medium.com/@emarley/they-didn-t-3a4dab489f45?source=rss-b4981c59ffa5------2
28 | https://medium.com/p/3a4dab489f45
29 |
30 | Sat, 09 Jan 2016 15:29:25 GMT
31 |
32 | -
33 |
34 |
Continue reading on »
]]>
35 | https://medium.com/@emarley/side-quest-drawing-b959ded1a1a4?source=rss-b4981c59ffa5------2
36 | https://medium.com/p/b959ded1a1a4
37 |
38 | Wed, 09 Dec 2015 03:37:35 GMT
39 |
40 | -
41 |
42 | Continue reading on »
]]>
43 | https://medium.com/@emarley/and-if-i-somehow-lose-the-ipad-pro-i-can-find-that-with-find-my-iphone-e9aa43486521?source=rss-b4981c59ffa5------2
44 | https://medium.com/p/e9aa43486521
45 |
46 | Mon, 23 Nov 2015 19:38:20 GMT
47 |
48 | -
49 |
50 | Continue reading on »
]]>
51 | https://medium.com/@emarley/though-not-as-much-more-weight-as-you-might-expect-7b33fe989f6e?source=rss-b4981c59ffa5------2
52 | https://medium.com/p/7b33fe989f6e
53 |
54 | Mon, 23 Nov 2015 19:37:38 GMT
55 |
56 | -
57 |
58 | Continue reading on »
]]>
59 | https://medium.com/@emarley/i-avoided-art-classes-in-high-school-and-college-because-i-was-afraid-they-would-hurt-my-gpa-ab916601f2ad?source=rss-b4981c59ffa5------2
60 | https://medium.com/p/ab916601f2ad
61 |
62 | Mon, 23 Nov 2015 19:37:13 GMT
63 |
64 | -
65 |
66 | I lose things a lot. Sometimes they’re just misplaced, sometimes gone forever. I don’t know if I have ever run out of ink in a pen—there’s…
Continue reading on »
]]>
67 | https://medium.com/@emarley/finding-value-20a90bf5ebf?source=rss-b4981c59ffa5------2
68 | https://medium.com/p/20a90bf5ebf
69 |
70 | Mon, 23 Nov 2015 19:34:18 GMT
71 |
72 | -
73 |
74 | Keyboard shortcuts, and other little details may be programmatically simple to set up, but they are still an important part of an app’s…
Continue reading on »
]]>
75 | https://medium.com/@emarley/replaying-this-post-in-my-head-last-night-i-regret-this-word-d8ed0b43f0f9?source=rss-b4981c59ffa5------2
76 | https://medium.com/p/d8ed0b43f0f9
77 |
78 | Tue, 10 Nov 2015 18:08:19 GMT
79 |
80 | -
81 |
82 | I moved from Senior Test Pilot to Software Engineer last month.
Continue reading on »
]]>
83 | https://medium.com/@emarley/betterment-e0ef45fcd284?source=rss-b4981c59ffa5------2
84 | https://medium.com/p/e0ef45fcd284
85 |
86 | Tue, 10 Nov 2015 02:17:46 GMT
87 |
88 | -
89 |
90 |
This is only a test.
Continue reading on »
]]>
91 | https://medium.com/@emarley/this-is-a-test-6ab141a1c5b5?source=rss-b4981c59ffa5------2
92 | https://medium.com/p/6ab141a1c5b5
93 |
94 | Sun, 20 Sep 2015 07:00:44 GMT
95 |
96 |
97 |
--------------------------------------------------------------------------------
/RSXMLTests/Resources/TimerSearch.txt:
--------------------------------------------------------------------------------
1 | class SomeViewController: NSViewController {
2 |
3 | @IBOutlet weak var textField: NSTextField
4 | private var NSTimer: fetchDataTimer?
5 | private var currentText: String? {
6 | didSet {
7 | invalidateTimer()
8 | if currentText.length > 3 {
9 | restartTimer()
10 | }
11 | }
12 | }
13 |
14 | func textDidChange(notification: NSNotification) {
15 |
16 | currentText = textField.stringValue
17 | }
18 |
19 | func invalidateTimer() {
20 |
21 | if let timer = timer {
22 | if timer.isValid {
23 | timer.invalidate()
24 | }
25 | self.timer = nil
26 | }
27 | }
28 |
29 |
30 |
31 | }
--------------------------------------------------------------------------------
/RSXMLTests/Resources/inessential.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | inessential: weblog
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | by Brent Simmons
14 |
15 |
16 |
17 |
18 |
19 |
20 |
The Omni Group’s Liz Marley, who recently transitioned from testing to engineering, appears on the NSNorth 2016 podcast . She talks about…
21 |
22 |
…challenges in engineering school, working with office cats, making the transition from software engineering to testing to developing and how knitting, like code, has the ultimate undo.
23 |
24 |
Knitting is serious (though not somber) business here at Omni.
25 |
26 |
27 |
28 |
OmniOutliner 4.5 is up on Omni’s site, and should be in the Mac App Store within days.
29 |
30 |
With this release — see the release notes — I helped work on, of all things, printing bugs and features. This is the first time in my entire career where I worked on printing support that was more than just the most basic possible thing.
31 |
32 |
And that sounds weird for the year 2016, I realize. But here’s the thing: working on printing support is far from glamorous. You wouldn’t call it fun . But the people who need these features really do need them, and it’s a matter of respect for OmniOutliner users that we do a great job even with printing.
33 |
34 |
But I sure am glad to get it finished and shipping. And I’m proud of the work we did — more proud than I expected to be. It’s solid, and I think the people who print from OmniOutliner will be very pleased.
35 |
36 |
Now we’re on to other new features , including editing Markdown documents with OmniOutliner.
37 |
38 |
39 |
40 |
Omni is hiring a Mac/iOS developer !
41 |
42 |
We’re also hiring a web developer , graphic designer , and phone support humans .
43 |
44 |
I’ll let you try out my new beanbag chair.
45 |
46 |
47 |
48 |
We’re hiring a senior front-end web developer, a graphic designer, and support humans.
49 |
50 |
You should apply.
51 |
52 |
53 |
54 |
The South Carolina primary is where the establishment fixes the errors of Iowa and New Hampshire. It’s Lee Atwater’s firewall.
55 |
56 |
When Buchanan threatens Dole, South Carolina shuts it down. When McCain threatens Bush, South Carolina applies the kibosh.
57 |
58 |
But is there any hope that it will function that way this time?
59 |
60 |
I don’t think so. The establishment candidates are Bush, Rubio, and Kasich. They don’t have a shot. Nor does Cruz. Trump wins South Carolina.
61 |
62 |
If that’s true, then it’s all over. If South Carolina fails — if the very primary that’s designed to toss the ball back to the establishment fails — then there’s no hope at all.
63 |
64 |
Cruz will go on to win a few states, most notably Texas. But otherwise it’s going to be Trump. He’ll get the delegates he needs, and that will be that.
65 |
66 |
67 |
68 |
When I was in middle school in the late ’70s I struggled to get my hair to feather properly. It just didn’t want to do it.
69 |
70 |
Like many kids that age I was newly conscious of my appearance — and I naïvely thought that well-feathered hair was a necessary (though not sufficient) key to fitting in. (Which was probably true, by the way.)
71 |
72 |
Every morning I would find that my hair behaved, at least somewhat, or it didn’t. So I categorized each day as a “good hair day” and a “bad hair day.”
73 |
74 |
I told my friends about this categorization — including a neighborhood girl named Sarah. She ended up telling other kids at school.
75 |
76 |
And pretty soon those kids, even kids I didn’t really know, would stop me in the halls or at lunch and say, “Hey Brent — good hair day or bad hair day?” Not meanly. Teasingly. It was funny.
77 |
78 |
Years later I started hearing the phrase on TV, and I was surprised that my little middle-school thing had spread and become part of the culture .
79 |
80 |
* * *
81 |
82 |
83 |
Of course, it’s also possible that I picked it up from Jane Pauley . But for all these years I’ve believed — no joke — that it was me, that it was my phrase. Maybe Jane Pauley got it (indirectly) from me.
84 |
85 |
It’s highly unlikely — of course, I know this — that I’m the originator. But still, it had to be someone, right?
86 |
87 |
(Not necessarily. It’s kind of obvious and could have had many originators.)
88 |
89 |
* * *
90 |
91 |
92 |
I stopped categorizing good and bad hair days by the time I got to high school. And these days I’m just glad that I still have some hair.
93 |
94 |
95 |
96 |
River5 is Dave Winer’s river-of-news RSS aggregator .
97 |
98 |
It’s a Node app. You can run it on a public machine and access it anywhere, or run it on your desktop and just read your news there.
99 |
100 |
101 |
102 |
Some time last week my iPhone started prompting me frequently to re-enter my iCloud password. And then my Watch started doing the same, about once a minute — with a little tap on the wrist each time.
103 |
104 |
Obviously I did re-enter my password — and have done so a dozen or so times now — but it doesn’t seem to matter.
105 |
106 |
So I stopped wearing my Watch and have switched to a mid-sixties Hamilton that my Dad gave me. (He had gotten it as a high school graduation present.)
107 |
108 |
I’m no watch aficionado — but I do appreciate a good and attractive watch (which this is), and I appreciate even more an old watch that’s a family thing.
109 |
110 |
Here’s the thing, though: the Apple Watch contains a hundred miracles of engineering and design, surely, but serious problems with software and services can turn even the most incredible hardware into something you just sit on your desk and ignore.
111 |
112 |
113 |
114 |
The Atlantic, Norm Ornstein :
115 |
116 |
But is there any real evidence that there is a hidden “sleeper cell” of potential voters who are waiting for the signal to emerge and transform the electorate? No.
117 |
118 |
Pure candidates on both sides of the spectrum often claim that their purity will bring in the checked-out voters, because they’re just waiting for a real conservative or a real liberal.
119 |
120 |
It’s an enduring fairy tale with terrible consequences. To put faith in it is to lose to the other party.
121 |
122 |
123 |
124 |
Cesare Rocchi interviewed me for the latest CocoaConf Podcast on life before the App Store.
125 |
126 |
There was a life, by the way. It was fun! We could release software any time we wanted to.
127 |
128 |
129 |
Archive
130 |
131 |
132 |
133 |
147 |
148 |
149 |
164 |
165 |
169 |
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/RSXMLTests/Resources/manton.rss:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | Manton Reece
12 |
13 | http://www.manton.org
14 |
15 | Fri, 25 Sep 2015 14:26:40 +0000
16 | en-US
17 | hourly
18 | 1
19 | http://wordpress.org/?v=4.2.5
20 | -
21 |
22 | http://www.manton.org/2015/09/3071.html
23 | http://www.manton.org/2015/09/3071.html#comments
24 | Fri, 25 Sep 2015 14:26:40 +0000
25 |
26 |
27 |
28 | http://www.manton.org/?p=3071
29 |
30 | This week’s Core Intuition is out with a discussion about new and old iPhones, the latest rumors about an Apple Car, and a follow-up on WebKit for Apple TV.
31 | ]]>
32 | http://www.manton.org/2015/09/3071.html/feed
33 | 0
34 |
35 | -
36 |
37 | http://www.manton.org/2015/09/3069.html
38 | http://www.manton.org/2015/09/3069.html#comments
39 | Fri, 25 Sep 2015 00:38:25 +0000
40 |
41 |
42 |
43 | http://www.manton.org/?p=3069
44 |
45 | I probably shouldn’t have started installing watchOS 2.0 right before needing to leave the house. Taking… for… ev… er.
46 | ]]>
47 | http://www.manton.org/2015/09/3069.html/feed
48 | 0
49 |
50 | -
51 |
52 | http://www.manton.org/2015/09/3067.html
53 | http://www.manton.org/2015/09/3067.html#comments
54 | Thu, 24 Sep 2015 15:51:51 +0000
55 |
56 |
57 |
58 | http://www.manton.org/?p=3067
59 |
60 | Looking forward to NSDrinking tonight, 8pm at Radio Coffee & Beer.
61 | ]]>
62 | http://www.manton.org/2015/09/3067.html/feed
63 | 0
64 |
65 | -
66 |
Instagram hits 400 million users
67 | http://www.manton.org/2015/09/instagram-hits-400-million-users.html
68 | http://www.manton.org/2015/09/instagram-hits-400-million-users.html#comments
69 | Wed, 23 Sep 2015 14:34:12 +0000
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | http://www.manton.org/?p=3064
78 |
79 | From Graham Spencer at MacStories, commenting on the latest Instagram numbers and that the service is only 5 years old:
80 |
81 | “But I was really surprised to remember that Facebook acquired Instagram in April 2012 , when Instagram had ‘only’ 40 million users. If I recall correctly, a lot of people thought Facebook was crazy to buy Instagram for $1 billion. Well, I think Facebook got the last laugh on that one, and as Forbes points out , Instagram now has more monthly active users than Twitter (316 million).”
82 |
83 | Impressive growth, but it fits. Instagram has crafted a user experience that encourages thoughtful posts and never feels overwhelming in the way a Twitter or Facebook timeline can be. If Instagram was a paid product, I bet Instagram’s churn rate would be the lowest of any of the big social networks. They did it with a small team and weren’t afraid to grow slowly.
84 | ]]>
85 | http://www.manton.org/2015/09/instagram-hits-400-million-users.html/feed
86 | 0
87 |
88 | -
89 |
Complete mirror of this blog
90 | http://www.manton.org/2015/09/complete-mirror-of-this-blog.html
91 | http://www.manton.org/2015/09/complete-mirror-of-this-blog.html#comments
92 | Sun, 20 Sep 2015 19:00:33 +0000
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | http://www.manton.org/?p=3061
102 |
103 | I’ve been blogging here for 13 years. If you take any random post from that first year, the majority of the links to other web sites are broken. The default outcome for any site that isn’t maintained — including the one you’re reading right now — is for it to vanish. Permanence doesn’t exist on the web.
104 | We can solve this , but it will take time. For now I think mirroring our writing is a great solution, to guard against domain names expiring and other inevitable failures. But where to mirror to?
105 | Only 2 companies keep coming to mind: WordPress.com and GitHub. I believe both will last for decades, maybe even 100 years, and both embrace the open web in a way that most other centralized web sites do not.
106 | Even though I self-host this weblog on WordPress, I’ve chosen to mirror to GitHub because of their focus on simple, static publishing via GitHub Pages . It has the best chance of running for a long time without intervention.
107 | I exported all of manton.org with the httrack command-line tool and checked it into GitHub, with a CNAME for mirror.manton.org . It works perfectly. I still need to automate this process so that it updates regularly, but I’m very happy to finally have a complete mirror for the first time.
108 | ]]>
109 | http://www.manton.org/2015/09/complete-mirror-of-this-blog.html/feed
110 | 0
111 |
112 | -
113 |
Steve Jobs and ET
114 | http://www.manton.org/2015/09/steve-jobs-and-et.html
115 | http://www.manton.org/2015/09/steve-jobs-and-et.html#comments
116 | Sat, 19 Sep 2015 23:00:36 +0000
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | http://www.manton.org/?p=3059
126 |
127 | I watched two documentaries last week. The first was “Steve Jobs: The Man in the Machine” , which I somewhat regret paying $7 to rent. It had its moments, but also seemed to become more negative and dramatic the longer it went on. I guess we should all hope to be so lucky and famous to have people try to bring out the best and worst of us.
128 | The second documentary I watched was “Atari: Game Over” , which was free on Netflix. It was great, interspersing a history of the rise and fall of Atari with the effort to dig up the ET game cartridges supposedly buried in New Mexico. Highly recommended.
129 | ]]>
130 | http://www.manton.org/2015/09/steve-jobs-and-et.html/feed
131 | 0
132 |
133 | -
134 |
Peace, indies, and the App Store
135 | http://www.manton.org/2015/09/peace-indies-and-the-app-store.html
136 | http://www.manton.org/2015/09/peace-indies-and-the-app-store.html#comments
137 | Sat, 19 Sep 2015 15:53:31 +0000
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | http://www.manton.org/?p=3056
147 |
148 | You’ve probably heard that Marco Arment has pulled his content-blocking app Peace from the App Store. The app was extremely successful :
149 |
150 | “As I write this, Peace has been the number one paid app in the U.S. App Store for about 36 hours. It’s a massive achievement that should be the highlight of my professional career. If Overcast even broke the top 100, I’d be over the moon.”
151 |
152 | I’ve seen some comments asking why he didn’t think to do this sooner, before he even shipped the app. But we are just now starting to understand the impact of ad blockers in iOS 9. I don’t think it’s an exaggeration to say that the web is different than it was a few days ago, and so our choices — and Marco’s — are different too. As I mentioned yesterday , content blockers are one facet of an overall shake-up for the web.
153 | Brent Simmons writes that only indies can do what Marco did. Marco must have left a lot of money on the table with this decision. It will always look like the right call to me when someone goes with their gut feeling and not with profit.
154 | ]]>
155 | http://www.manton.org/2015/09/peace-indies-and-the-app-store.html/feed
156 | 0
157 |
158 | -
159 |
Core Intuition and ATP this week
160 | http://www.manton.org/2015/09/core-intuition-and-atp-this-week.html
161 | http://www.manton.org/2015/09/core-intuition-and-atp-this-week.html#comments
162 | Fri, 18 Sep 2015 20:25:57 +0000
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | http://www.manton.org/?p=3051
171 |
172 | On this week’s Core Intuition , Daniel and I spend the whole show talking about the Apple TV. The first half is about the Apple TV dev kit lottery, and the second half is about whether we need the web on our TVs.
173 | There’s also a good discussion on the Accidental Tech Podcast about this. Here’s an Overcast link about halfway into the episode .
174 | ]]>
175 | http://www.manton.org/2015/09/core-intuition-and-atp-this-week.html/feed
176 | 0
177 |
178 | -
179 |
Wrap-up thoughts on the TV web
180 | http://www.manton.org/2015/09/wrap-up-thoughts-on-the-tv-web.html
181 | http://www.manton.org/2015/09/wrap-up-thoughts-on-the-tv-web.html#comments
182 | Fri, 18 Sep 2015 14:58:39 +0000
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 | http://www.manton.org/?p=3048
192 |
193 | I’m going to mostly let John Gruber have the last word on the Apple TV vs. the web debate, because I could write about this every day and my readers would run away before I run out of material. I’m glad John addressed the Mac vs. the command-line argument, though, because it didn’t seem quite right to me either. He says:
194 |
195 | “The difference is that the command-line-less Mac was intended to replace command-line-based computers. The GUI relegated the command-line interface to a permanent tiny niche. Apple TV and Apple Watch aren’t like that at all — they’re not meant to replace any device you already use to access the open web.”
196 |
197 | This is the most hopeful part of the Apple ecosystem as it relates to the web. Apple’s other platforms really do have a great web experience. Remember when web sites were faster and worked better on a PC than a Mac? If anything, the opposite is true now.
198 | One of the themes I keep hearing is that a “web browser” on a TV will make for a poor user experience, so don’t bother. I tried to correct that misunderstanding in this post ; it’s not about standalone Safari, it’s about web technologies that could be used in native apps. But ignoring that, I think everyone too easily forgets what the mobile web was like before the iPhone.
199 | Steve Jobs, from the original iPhone introduction:
200 |
201 | “We wanted the best web browser in the world on our phone. Not a baby web browser or a WAP browser — a real browser. […] It is the first fully usable HTML browser on a phone.”
202 |
203 | That was a breakthrough. I believe the same evolution is possible on tvOS — to include parts of the open web and do it with a great user experience. You can start by weaving it together inside native apps. (I filed a bug with Apple yesterday with a suggestion. It was marked as a duplicate.)
204 | The web is at a fascinating, pivotal time right now. It has been shaken up by centralized publishing, closed platforms, and now content blockers. Users no longer value the concepts that made Web 2.0 special. The web can still have a strong future, but we have to try something , and we have to try it on every platform we can.
205 | ]]>
206 | http://www.manton.org/2015/09/wrap-up-thoughts-on-the-tv-web.html/feed
207 | 0
208 |
209 | -
210 |
211 | http://www.manton.org/2015/09/3046.html
212 | http://www.manton.org/2015/09/3046.html#comments
213 | Fri, 18 Sep 2015 13:43:21 +0000
214 |
215 |
216 |
217 | http://www.manton.org/?p=3046
218 |
219 | Expecting two packages today: the new Apple TV, and my new iPhone 5S (32 GB, space gray).
220 | ]]>
221 | http://www.manton.org/2015/09/3046.html/feed
222 | 0
223 |
224 |
225 |
226 |
227 |
228 |
229 |
--------------------------------------------------------------------------------
/RSXMLiOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | RSXML
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | FMWK
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------