Refactored OPML
This commit is contained in:
@@ -552,12 +552,6 @@ static const NSInteger kSelfLength = 5;
|
||||
}
|
||||
|
||||
|
||||
static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
|
||||
|
||||
return memcmp(bytes1, bytes2, length) == 0;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length {
|
||||
|
||||
static const NSUInteger alternateLength = kAlternateLength - 1;
|
||||
@@ -569,35 +563,35 @@ static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length
|
||||
static const NSUInteger textLength = kTextLength - 1;
|
||||
static const NSUInteger selfLength = kSelfLength - 1;
|
||||
|
||||
if (length == alternateLength && equalBytes(bytes, kAlternate, alternateLength)) {
|
||||
if (length == alternateLength && RSSAXEqualBytes(bytes, kAlternate, alternateLength)) {
|
||||
return kAlternateValue;
|
||||
}
|
||||
|
||||
if (length == textHTMLLength && equalBytes(bytes, kTextHTML, textHTMLLength)) {
|
||||
if (length == textHTMLLength && RSSAXEqualBytes(bytes, kTextHTML, textHTMLLength)) {
|
||||
return kTextHTMLValue;
|
||||
}
|
||||
|
||||
if (length == relatedLength && equalBytes(bytes, kRelated, relatedLength)) {
|
||||
if (length == relatedLength && RSSAXEqualBytes(bytes, kRelated, relatedLength)) {
|
||||
return kRelatedValue;
|
||||
}
|
||||
|
||||
if (length == shortURLLength && equalBytes(bytes, kShortURL, shortURLLength)) {
|
||||
if (length == shortURLLength && RSSAXEqualBytes(bytes, kShortURL, shortURLLength)) {
|
||||
return kShortURLValue;
|
||||
}
|
||||
|
||||
if (length == htmlLength && equalBytes(bytes, kHTML, htmlLength)) {
|
||||
if (length == htmlLength && RSSAXEqualBytes(bytes, kHTML, htmlLength)) {
|
||||
return kHTMLValue;
|
||||
}
|
||||
|
||||
if (length == enLength && equalBytes(bytes, kEn, enLength)) {
|
||||
if (length == enLength && RSSAXEqualBytes(bytes, kEn, enLength)) {
|
||||
return kEnValue;
|
||||
}
|
||||
|
||||
if (length == textLength && equalBytes(bytes, kText, textLength)) {
|
||||
if (length == textLength && RSSAXEqualBytes(bytes, kText, textLength)) {
|
||||
return kTextValue;
|
||||
}
|
||||
|
||||
if (length == selfLength && equalBytes(bytes, kSelf, selfLength)) {
|
||||
if (length == selfLength && RSSAXEqualBytes(bytes, kSelf, selfLength)) {
|
||||
return kSelfValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
static NSString *kLIBXMLParserErrorDomain = @"LIBXMLParserErrorDomain";
|
||||
static NSString *kRSXMLParserErrorDomain = @"RSXMLParserErrorDomain";
|
||||
|
||||
BOOL RSCanParseFeed(RSXMLData *xmlData);
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright (c) 2015 Ranchero Software LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import <libxml/xmlerror.h>
|
||||
#import "RSXMLError.h"
|
||||
#import "RSFeedParser.h"
|
||||
#import "FeedParser.h"
|
||||
#import "RSXMLData.h"
|
||||
@@ -49,43 +49,10 @@ static BOOL dataHasLeftCaret(const char *bytes, NSUInteger numberOfBytes);
|
||||
static const NSUInteger maxNumberOfBytesToSearch = 4096;
|
||||
static const NSUInteger minNumberOfBytesToSearch = 20;
|
||||
|
||||
typedef enum {
|
||||
RSXMLErrorNoData = 100,
|
||||
RSXMLErrorMissingLeftCaret,
|
||||
RSXMLErrorProbablyHTML,
|
||||
RSXMLErrorContainsXMLErrorsTag,
|
||||
RSXMLErrorNoSuitableParser
|
||||
} RSXMLError;
|
||||
|
||||
static void setError(NSError **error, RSXMLError code) {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
NSString *msg = @"";
|
||||
switch (code) { // switch statement will warn if an enum value is missing
|
||||
case RSXMLErrorNoData:
|
||||
msg = @"Couldn't parse feed. No data available.";
|
||||
break;
|
||||
case RSXMLErrorMissingLeftCaret:
|
||||
msg = @"Couldn't parse feed. Missing left caret character ('<').";
|
||||
break;
|
||||
case RSXMLErrorProbablyHTML:
|
||||
msg = @"Couldn't parse feed. Expecting XML data but found html data.";
|
||||
break;
|
||||
case RSXMLErrorContainsXMLErrorsTag:
|
||||
msg = @"Couldn't parse feed. XML contains 'errors' tag.";
|
||||
break;
|
||||
case RSXMLErrorNoSuitableParser:
|
||||
msg = @"Couldn't parse feed. No suitable parser found. XML document not well-formed.";
|
||||
break;
|
||||
}
|
||||
*error = [NSError errorWithDomain:kRSXMLParserErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: msg}];
|
||||
}
|
||||
|
||||
static Class parserClassForXMLData(RSXMLData *xmlData, NSError **error) {
|
||||
|
||||
if (!feedMayBeParseable(xmlData)) {
|
||||
setError(error, RSXMLErrorNoData);
|
||||
RSXMLSetError(error, RSXMLErrorNoData, nil);
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -101,7 +68,7 @@ static Class parserClassForXMLData(RSXMLData *xmlData, NSError **error) {
|
||||
}
|
||||
|
||||
if (!dataHasLeftCaret(bytes, numberOfBytes)) {
|
||||
setError(error, RSXMLErrorMissingLeftCaret);
|
||||
RSXMLSetError(error, RSXMLErrorMissingLeftCaret, nil);
|
||||
return nil;
|
||||
}
|
||||
if (optimisticCanParseRSSData(bytes, numberOfBytes)) {
|
||||
@@ -114,11 +81,11 @@ static Class parserClassForXMLData(RSXMLData *xmlData, NSError **error) {
|
||||
return [RSRSSParser class]; //TODO: parse RDF feeds, using RSS parser so far ...
|
||||
}
|
||||
if (dataIsProbablyHTML(bytes, numberOfBytes)) {
|
||||
setError(error, RSXMLErrorProbablyHTML);
|
||||
RSXMLSetError(error, RSXMLErrorProbablyHTML, nil);
|
||||
return nil;
|
||||
}
|
||||
if (dataIsSomeWeirdException(bytes, numberOfBytes)) {
|
||||
setError(error, RSXMLErrorContainsXMLErrorsTag);
|
||||
RSXMLSetError(error, RSXMLErrorContainsXMLErrorsTag, nil);
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
@@ -130,7 +97,7 @@ static Class parserClassForXMLData(RSXMLData *xmlData, NSError **error) {
|
||||
}
|
||||
}
|
||||
// Try RSS anyway? libxml would return a parsing error
|
||||
setError(error, RSXMLErrorNoSuitableParser);
|
||||
RSXMLSetError(error, RSXMLErrorNoSuitableParser, nil);
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -250,19 +217,11 @@ RSParsedFeed *RSParseFeedSync(RSXMLData *xmlData, NSError **error) {
|
||||
xmlResetLastError();
|
||||
id<FeedParser> parser = parserForXMLData(xmlData, error);
|
||||
if (error && *error) {
|
||||
//printf("ERROR in parserForXMLData(): %s\n", [[*error localizedDescription] UTF8String]);
|
||||
return nil;
|
||||
}
|
||||
RSParsedFeed *parsedResult = [parser parseFeed];
|
||||
|
||||
xmlErrorPtr err = xmlGetLastError();
|
||||
if (err && error) {
|
||||
int errCode = err->code;
|
||||
char * msg = err->message;
|
||||
//if (err->level == XML_ERR_FATAL)
|
||||
NSString *errMsg = [[NSString stringWithFormat:@"%s", msg] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
*error = [NSError errorWithDomain:kLIBXMLParserErrorDomain code:errCode userInfo:@{NSLocalizedDescriptionKey: errMsg}];
|
||||
//printf("ERROR in [parseFeed] (%d): %s\n", err->level, [[*error localizedDescription] UTF8String]);
|
||||
if (error) {
|
||||
*error = RSXMLMakeErrorFromLIBXMLError(xmlGetLastError());
|
||||
xmlResetLastError();
|
||||
}
|
||||
return parsedResult;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// RSOPMLAttributes.h
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
// OPML allows for arbitrary attributes.
|
||||
// These are the common attributes in OPML files used as RSS subscription lists.
|
||||
|
||||
extern NSString *OPMLTextKey; //text
|
||||
extern NSString *OPMLTitleKey; //title
|
||||
extern NSString *OPMLDescriptionKey; //description
|
||||
extern NSString *OPMLTypeKey; //type
|
||||
extern NSString *OPMLVersionKey; //version
|
||||
extern NSString *OPMLHMTLURLKey; //htmlUrl
|
||||
extern NSString *OPMLXMLURLKey; //xmlUrl
|
||||
|
||||
|
||||
@interface NSDictionary (RSOPMLAttributes)
|
||||
|
||||
// A frequent error in OPML files is to mess up the capitalization,
|
||||
// so these do a case-insensitive lookup.
|
||||
|
||||
@property (nonatomic, readonly) NSString *opml_text;
|
||||
@property (nonatomic, readonly) NSString *opml_title;
|
||||
@property (nonatomic, readonly) NSString *opml_description;
|
||||
@property (nonatomic, readonly) NSString *opml_type;
|
||||
@property (nonatomic, readonly) NSString *opml_version;
|
||||
@property (nonatomic, readonly) NSString *opml_htmlUrl;
|
||||
@property (nonatomic, readonly) NSString *opml_xmlUrl;
|
||||
|
||||
@end
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// RSOPMLAttributes.m
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSOPMLAttributes.h"
|
||||
#import "RSXMLInternal.h"
|
||||
|
||||
|
||||
NSString *OPMLTextKey = @"text";
|
||||
NSString *OPMLTitleKey = @"title";
|
||||
NSString *OPMLDescriptionKey = @"description";
|
||||
NSString *OPMLTypeKey = @"type";
|
||||
NSString *OPMLVersionKey = @"version";
|
||||
NSString *OPMLHMTLURLKey = @"htmlUrl";
|
||||
NSString *OPMLXMLURLKey = @"xmlUrl";
|
||||
|
||||
|
||||
@implementation NSDictionary (RSOPMLAttributes)
|
||||
|
||||
- (NSString *)opml_text {
|
||||
|
||||
return [self rsxml_objectForCaseInsensitiveKey:OPMLTextKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_title {
|
||||
|
||||
return [self rsxml_objectForCaseInsensitiveKey:OPMLTitleKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_description {
|
||||
|
||||
return [self rsxml_objectForCaseInsensitiveKey:OPMLDescriptionKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_type {
|
||||
|
||||
return [self rsxml_objectForCaseInsensitiveKey:OPMLTypeKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_version {
|
||||
|
||||
return [self rsxml_objectForCaseInsensitiveKey:OPMLVersionKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_htmlUrl {
|
||||
|
||||
return [self rsxml_objectForCaseInsensitiveKey:OPMLHMTLURLKey];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)opml_xmlUrl {
|
||||
|
||||
return [self rsxml_objectForCaseInsensitiveKey:OPMLXMLURLKey];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// RSOPMLDocument.h
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
#import "RSOPMLItem.h"
|
||||
|
||||
|
||||
@interface RSOPMLDocument : RSOPMLItem
|
||||
|
||||
@property (nonatomic) NSString *title;
|
||||
|
||||
@end
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// RSOPMLDocument.m
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSOPMLDocument.h"
|
||||
|
||||
@implementation RSOPMLDocument
|
||||
|
||||
@end
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// RSOPMLFeedSpecifier.h
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
|
||||
@interface RSOPMLFeedSpecifier : NSObject
|
||||
|
||||
|
||||
- (instancetype)initWithTitle:(NSString *)title feedDescription:(NSString *)feedDescription homePageURL:(NSString *)homePageURL feedURL:(NSString *)feedURL;
|
||||
|
||||
@property (nonatomic, readonly) NSString *title;
|
||||
@property (nonatomic, readonly) NSString *feedDescription;
|
||||
@property (nonatomic, readonly) NSString *homePageURL;
|
||||
@property (nonatomic, readonly) NSString *feedURL;
|
||||
|
||||
|
||||
@end
|
||||
@@ -1,50 +0,0 @@
|
||||
//
|
||||
// RSOPMLFeedSpecifier.m
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSOPMLFeedSpecifier.h"
|
||||
#import "RSXMLInternal.h"
|
||||
|
||||
|
||||
@implementation RSOPMLFeedSpecifier
|
||||
|
||||
- (instancetype)initWithTitle:(NSString *)title feedDescription:(NSString *)feedDescription homePageURL:(NSString *)homePageURL feedURL:(NSString *)feedURL {
|
||||
|
||||
NSParameterAssert(!RSXMLIsEmpty(feedURL));
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (RSXMLIsEmpty(title)) {
|
||||
_title = nil;
|
||||
}
|
||||
else {
|
||||
_title = title;
|
||||
}
|
||||
|
||||
if (RSXMLIsEmpty(feedDescription)) {
|
||||
_feedDescription = nil;
|
||||
}
|
||||
else {
|
||||
_feedDescription = feedDescription;
|
||||
}
|
||||
|
||||
if (RSXMLIsEmpty(homePageURL)) {
|
||||
_homePageURL = nil;
|
||||
}
|
||||
else {
|
||||
_homePageURL = homePageURL;
|
||||
}
|
||||
|
||||
_feedURL = feedURL;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,26 +1,27 @@
|
||||
//
|
||||
// RSOPMLItem.h
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
@class RSOPMLFeedSpecifier;
|
||||
// OPML allows for arbitrary attributes.
|
||||
// These are the common attributes in OPML files used as RSS subscription lists.
|
||||
|
||||
extern NSString *OPMLTextKey; //text
|
||||
extern NSString *OPMLTitleKey; //title
|
||||
extern NSString *OPMLDescriptionKey; //description
|
||||
extern NSString *OPMLTypeKey; //type
|
||||
extern NSString *OPMLVersionKey; //version
|
||||
extern NSString *OPMLHMTLURLKey; //htmlUrl
|
||||
extern NSString *OPMLXMLURLKey; //xmlUrl
|
||||
|
||||
|
||||
@interface RSOPMLItem : NSObject
|
||||
|
||||
@property (nonatomic) NSArray<RSOPMLItem*> *children;
|
||||
@property (nonatomic) NSDictionary *attributes;
|
||||
@property (nonatomic) NSArray *children;
|
||||
@property (nonatomic, readonly) BOOL isFolder; // true if children.count > 0
|
||||
@property (nonatomic, readonly) NSString *displayName; //May be nil.
|
||||
|
||||
- (void)addChild:(RSOPMLItem *)child;
|
||||
- (void)setAttribute:(id)value forKey:(NSString *)key;
|
||||
- (id)attributeForKey:(NSString *)key;
|
||||
|
||||
@property (nonatomic, readonly) RSOPMLFeedSpecifier *OPMLFeedSpecifier; //May be nil.
|
||||
|
||||
@property (nonatomic, readonly) NSString *titleFromAttributes; //May be nil.
|
||||
@property (nonatomic, readonly) BOOL isFolder;
|
||||
|
||||
- (NSString *)recursiveDescription;
|
||||
@end
|
||||
|
||||
@@ -1,86 +1,99 @@
|
||||
//
|
||||
// RSOPMLItem.m
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSOPMLItem.h"
|
||||
#import "RSOPMLAttributes.h"
|
||||
#import "RSOPMLFeedSpecifier.h"
|
||||
#import "RSXMLInternal.h"
|
||||
|
||||
|
||||
NSString *OPMLTextKey = @"text";
|
||||
NSString *OPMLTitleKey = @"title";
|
||||
NSString *OPMLDescriptionKey = @"description";
|
||||
NSString *OPMLTypeKey = @"type";
|
||||
NSString *OPMLVersionKey = @"version";
|
||||
NSString *OPMLHMTLURLKey = @"htmlUrl";
|
||||
NSString *OPMLXMLURLKey = @"xmlUrl";
|
||||
|
||||
|
||||
@interface RSOPMLItem ()
|
||||
|
||||
@property (nonatomic) NSMutableArray *mutableChildren;
|
||||
|
||||
@property (nonatomic) NSMutableArray<RSOPMLItem*> *mutableChildren;
|
||||
@property (nonatomic) NSMutableDictionary *mutableAttributes;
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSOPMLItem
|
||||
|
||||
@synthesize children = _children;
|
||||
@synthesize OPMLFeedSpecifier = _OPMLFeedSpecifier;
|
||||
|
||||
|
||||
- (NSArray *)children {
|
||||
|
||||
return [self.mutableChildren copy];
|
||||
}
|
||||
|
||||
|
||||
- (void)setChildren:(NSArray *)children {
|
||||
|
||||
_children = children;
|
||||
self.mutableChildren = [_children mutableCopy];
|
||||
- (void)setChildren:(NSArray<RSOPMLItem*>*)children {
|
||||
self.mutableChildren = [children mutableCopy];
|
||||
}
|
||||
|
||||
|
||||
- (void)addChild:(RSOPMLItem *)child {
|
||||
|
||||
if (!self.mutableChildren) {
|
||||
self.mutableChildren = [NSMutableArray new];
|
||||
}
|
||||
|
||||
[self.mutableChildren addObject:child];
|
||||
- (NSDictionary *)attributes {
|
||||
return [self.mutableAttributes copy];
|
||||
}
|
||||
|
||||
|
||||
- (RSOPMLFeedSpecifier *)OPMLFeedSpecifier {
|
||||
|
||||
if (_OPMLFeedSpecifier) {
|
||||
return _OPMLFeedSpecifier;
|
||||
}
|
||||
|
||||
NSString *feedURL = self.attributes.opml_xmlUrl;
|
||||
if (RSXMLIsEmpty(feedURL)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_OPMLFeedSpecifier = [[RSOPMLFeedSpecifier alloc] initWithTitle:self.attributes.opml_title feedDescription:self.attributes.opml_description homePageURL:self.attributes.opml_htmlUrl feedURL:feedURL];
|
||||
|
||||
return _OPMLFeedSpecifier;
|
||||
}
|
||||
|
||||
- (NSString *)titleFromAttributes {
|
||||
|
||||
NSString *title = self.attributes.opml_title;
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
title = self.attributes.opml_text;
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return nil;
|
||||
- (void)setAttributes:(NSDictionary *)attributes {
|
||||
self.mutableAttributes = [attributes mutableCopy];
|
||||
}
|
||||
|
||||
- (BOOL)isFolder {
|
||||
|
||||
return self.mutableChildren.count > 0;
|
||||
}
|
||||
|
||||
- (NSString *)displayName {
|
||||
NSString *title = [self attributeForKey:OPMLTitleKey];
|
||||
if (!title) {
|
||||
title = [self attributeForKey:OPMLTextKey];
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
- (void)addChild:(RSOPMLItem *)child {
|
||||
if (!self.mutableChildren) {
|
||||
self.mutableChildren = [NSMutableArray new];
|
||||
}
|
||||
[self.mutableChildren addObject:child];
|
||||
}
|
||||
|
||||
- (void)setAttribute:(id)value forKey:(NSString *)key {
|
||||
if (!self.mutableAttributes) {
|
||||
self.mutableAttributes = [NSMutableDictionary new];
|
||||
}
|
||||
[self.mutableAttributes setValue:value forKey:key];
|
||||
}
|
||||
|
||||
- (id)attributeForKey:(NSString *)key {
|
||||
if (self.attributes.count > 0 && !RSXMLStringIsEmpty(key)) {
|
||||
return [self.attributes rsxml_objectForCaseInsensitiveKey:key];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - Printing
|
||||
|
||||
- (NSString *)description {
|
||||
NSMutableString *str = [NSMutableString stringWithFormat:@"<%@ group: %d", [self class], self.isFolder];
|
||||
for (NSString *key in _mutableAttributes) {
|
||||
[str appendFormat:@", %@: '%@'", key, _mutableAttributes[key]];
|
||||
}
|
||||
[str appendString:@">"];
|
||||
return str;
|
||||
}
|
||||
|
||||
- (void)appendStringRecursive:(NSMutableString *)str indent:(NSString *)prefix {
|
||||
[str appendFormat:@"%@%@\n", prefix, self];
|
||||
if (self.isFolder) {
|
||||
for (RSOPMLItem *child in self.children) {
|
||||
[child appendStringRecursive:str indent:[prefix stringByAppendingString:@" "]];
|
||||
}
|
||||
[str appendFormat:@"%@</group>\n", prefix];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)recursiveDescription {
|
||||
NSMutableString *mStr = [NSMutableString new];
|
||||
[self appendStringRecursive:mStr indent:@""];
|
||||
return mStr;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
|
||||
|
||||
@class RSXMLData;
|
||||
@class RSOPMLDocument;
|
||||
@class RSOPMLItem;
|
||||
|
||||
|
||||
typedef void (^RSParsedOPMLBlock)(RSOPMLDocument *OPMLDocument, NSError *error);
|
||||
typedef void (^RSParsedOPMLBlock)(RSOPMLItem *opmlDocument, NSError *error);
|
||||
|
||||
void RSParseOPML(RSXMLData *xmlData, RSParsedOPMLBlock callback); //async; calls back on main thread.
|
||||
|
||||
@@ -22,7 +22,7 @@ void RSParseOPML(RSXMLData *xmlData, RSParsedOPMLBlock callback); //async; calls
|
||||
|
||||
- (instancetype)initWithXMLData:(RSXMLData *)xmlData;
|
||||
|
||||
@property (nonatomic, readonly) RSOPMLDocument *OPMLDocument;
|
||||
@property (nonatomic, readonly) RSOPMLItem *opmlDocument;
|
||||
@property (nonatomic, readonly) NSError *error;
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
#import "RSXMLData.h"
|
||||
#import "RSSAXParser.h"
|
||||
#import "RSOPMLItem.h"
|
||||
#import "RSOPMLDocument.h"
|
||||
#import "RSOPMLAttributes.h"
|
||||
#import "RSXMLError.h"
|
||||
|
||||
|
||||
@@ -27,7 +25,7 @@ void RSParseOPML(RSXMLData *xmlData, RSParsedOPMLBlock callback) {
|
||||
|
||||
RSOPMLParser *parser = [[RSOPMLParser alloc] initWithXMLData:xmlData];
|
||||
|
||||
RSOPMLDocument *document = parser.OPMLDocument;
|
||||
RSOPMLItem *document = parser.opmlDocument;
|
||||
NSError *error = parser.error;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@@ -41,9 +39,9 @@ void RSParseOPML(RSXMLData *xmlData, RSParsedOPMLBlock callback) {
|
||||
|
||||
@interface RSOPMLParser () <RSSAXParserDelegate>
|
||||
|
||||
@property (nonatomic, readwrite) RSOPMLDocument *OPMLDocument;
|
||||
@property (nonatomic, readwrite) RSOPMLItem *opmlDocument;
|
||||
@property (nonatomic, readwrite) NSError *error;
|
||||
@property (nonatomic) NSMutableArray *itemStack;
|
||||
@property (nonatomic) NSMutableArray<RSOPMLItem*> *itemStack;
|
||||
|
||||
@end
|
||||
|
||||
@@ -72,31 +70,28 @@ void RSParseOPML(RSXMLData *xmlData, RSParsedOPMLBlock callback) {
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
if (![self canParseData:XMLData.data]) {
|
||||
|
||||
if ([self canParseData:XMLData.data]) {
|
||||
RSSAXParser *parser = [[RSSAXParser alloc] initWithDelegate:self];
|
||||
|
||||
self.itemStack = [NSMutableArray new];
|
||||
self.opmlDocument = [RSOPMLItem new];
|
||||
[self.itemStack addObject:self.opmlDocument];
|
||||
|
||||
[parser parseData:XMLData.data];
|
||||
[parser finishParsing];
|
||||
|
||||
} else {
|
||||
|
||||
NSString *filename = nil;
|
||||
NSURL *url = [NSURL URLWithString:XMLData.urlString];
|
||||
if (url && url.isFileURL) {
|
||||
filename = url.path.lastPathComponent;
|
||||
}
|
||||
if ([XMLData.urlString hasPrefix:@"http"]) {
|
||||
filename = XMLData.urlString;
|
||||
}
|
||||
if (!filename) {
|
||||
filename = XMLData.urlString;
|
||||
}
|
||||
self.error = RSOPMLWrongFormatError(filename);
|
||||
return;
|
||||
self.error = RSXMLMakeError(RSXMLErrorFileNotOPML, filename);
|
||||
}
|
||||
|
||||
RSSAXParser *parser = [[RSSAXParser alloc] initWithDelegate:self];
|
||||
|
||||
self.itemStack = [NSMutableArray new];
|
||||
self.OPMLDocument = [RSOPMLDocument new];
|
||||
[self pushItem:self.OPMLDocument];
|
||||
|
||||
[parser parseData:XMLData.data];
|
||||
[parser finishParsing];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,12 +117,12 @@ void RSParseOPML(RSXMLData *xmlData, RSParsedOPMLBlock callback) {
|
||||
}
|
||||
|
||||
NSRange opmlRange = [s rangeOfString:@"<opml" options:NSCaseInsensitiveSearch range:rangeToSearch];
|
||||
if (opmlRange.length < 1) {
|
||||
if (opmlRange.location == NSNotFound) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSRange outlineRange = [s rangeOfString:@"<outline" options:NSLiteralSearch range:rangeToSearch];
|
||||
if (outlineRange.length < 1) {
|
||||
if (outlineRange.location == NSNotFound) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -139,11 +134,6 @@ void RSParseOPML(RSXMLData *xmlData, RSParsedOPMLBlock callback) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)pushItem:(RSOPMLItem *)item {
|
||||
|
||||
[self.itemStack addObject:item];
|
||||
}
|
||||
|
||||
|
||||
- (void)popItem {
|
||||
|
||||
@@ -158,28 +148,27 @@ void RSParseOPML(RSXMLData *xmlData, RSParsedOPMLBlock callback) {
|
||||
}
|
||||
|
||||
|
||||
- (RSOPMLItem *)currentItem {
|
||||
|
||||
return self.itemStack.lastObject;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - RSSAXParserDelegate
|
||||
|
||||
static const char *kOutline = "outline";
|
||||
static const char kOutlineLength = 8;
|
||||
static const char *kHead = "head";
|
||||
static const char kHeadLength = 5;
|
||||
static BOOL isHead = NO;
|
||||
|
||||
- (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 {
|
||||
|
||||
if (!RSSAXEqualTags(localName, kOutline, kOutlineLength)) {
|
||||
return;
|
||||
if (RSSAXEqualTags(localName, kOutline, kOutlineLength)) {
|
||||
RSOPMLItem *item = [RSOPMLItem new];
|
||||
item.attributes = [SAXParser attributesDictionary:attributes numberOfAttributes:numberOfAttributes];
|
||||
|
||||
[self.itemStack.lastObject addChild:item];
|
||||
[self.itemStack addObject:item];
|
||||
} else if (RSSAXEqualTags(localName, kHead, kHeadLength)) {
|
||||
isHead = YES;
|
||||
} else if (isHead) {
|
||||
[SAXParser beginStoringCharacters];
|
||||
}
|
||||
|
||||
RSOPMLItem *item = [RSOPMLItem new];
|
||||
item.attributes = [SAXParser attributesDictionary:attributes numberOfAttributes:numberOfAttributes];
|
||||
|
||||
[[self currentItem] addChild:item];
|
||||
[self pushItem:item];
|
||||
}
|
||||
|
||||
|
||||
@@ -187,31 +176,15 @@ static const char kOutlineLength = 8;
|
||||
|
||||
if (RSSAXEqualTags(localName, kOutline, kOutlineLength)) {
|
||||
[self popItem];
|
||||
} else if (RSSAXEqualTags(localName, kHead, kHeadLength)) {
|
||||
isHead = NO;
|
||||
} else if (isHead) {
|
||||
NSString *key = [NSString stringWithFormat:@"%s", localName];
|
||||
[self.itemStack.lastObject setAttribute:[SAXParser currentString] forKey:key];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const char *kText = "text";
|
||||
static const NSInteger kTextLength = 5;
|
||||
|
||||
static const char *kTitle = "title";
|
||||
static const NSInteger kTitleLength = 6;
|
||||
|
||||
static const char *kDescription = "description";
|
||||
static const NSInteger kDescriptionLength = 12;
|
||||
|
||||
static const char *kType = "type";
|
||||
static const NSInteger kTypeLength = 5;
|
||||
|
||||
static const char *kVersion = "version";
|
||||
static const NSInteger kVersionLength = 8;
|
||||
|
||||
static const char *kHTMLURL = "htmlUrl";
|
||||
static const NSInteger kHTMLURLLength = 8;
|
||||
|
||||
static const char *kXMLURL = "xmlUrl";
|
||||
static const NSInteger kXMLURLLength = 7;
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForName:(const xmlChar *)name prefix:(const xmlChar *)prefix {
|
||||
|
||||
if (prefix) {
|
||||
@@ -219,77 +192,37 @@ static const NSInteger kXMLURLLength = 7;
|
||||
}
|
||||
|
||||
size_t nameLength = strlen((const char *)name);
|
||||
|
||||
if (nameLength == kTextLength - 1) {
|
||||
if (RSSAXEqualTags(name, kText, kTextLength)) {
|
||||
return OPMLTextKey;
|
||||
}
|
||||
if (RSSAXEqualTags(name, kType, kTypeLength)) {
|
||||
return OPMLTypeKey;
|
||||
}
|
||||
switch (nameLength) {
|
||||
case 4:
|
||||
if (RSSAXEqualTags(name, "text", 5)) return OPMLTextKey;
|
||||
if (RSSAXEqualTags(name, "type", 5)) return OPMLTypeKey;
|
||||
break;
|
||||
case 5:
|
||||
if (RSSAXEqualTags(name, "title", 6)) return OPMLTitleKey;
|
||||
break;
|
||||
case 6:
|
||||
if (RSSAXEqualTags(name, "xmlUrl", 7)) return OPMLXMLURLKey;
|
||||
break;
|
||||
case 7:
|
||||
if (RSSAXEqualTags(name, "version", 8)) return OPMLVersionKey;
|
||||
if (RSSAXEqualTags(name, "htmlUrl", 8)) return OPMLHMTLURLKey;
|
||||
break;
|
||||
case 11:
|
||||
if (RSSAXEqualTags(name, "description", 12)) return OPMLDescriptionKey;
|
||||
break;
|
||||
}
|
||||
|
||||
else if (nameLength == kTitleLength - 1) {
|
||||
if (RSSAXEqualTags(name, kTitle, kTitleLength)) {
|
||||
return OPMLTitleKey;
|
||||
}
|
||||
}
|
||||
|
||||
else if (nameLength == kXMLURLLength - 1) {
|
||||
if (RSSAXEqualTags(name, kXMLURL, kXMLURLLength)) {
|
||||
return OPMLXMLURLKey;
|
||||
}
|
||||
}
|
||||
|
||||
else if (nameLength == kVersionLength - 1) {
|
||||
if (RSSAXEqualTags(name, kVersion, kVersionLength)) {
|
||||
return OPMLVersionKey;
|
||||
}
|
||||
if (RSSAXEqualTags(name, kHTMLURL, kHTMLURLLength)) {
|
||||
return OPMLHMTLURLKey;
|
||||
}
|
||||
}
|
||||
|
||||
else if (nameLength == kDescriptionLength - 1) {
|
||||
if (RSSAXEqualTags(name, kDescription, kDescriptionLength)) {
|
||||
return OPMLDescriptionKey;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
static const char *kRSSUppercase = "RSS";
|
||||
static const char *kRSSLowercase = "rss";
|
||||
static const NSUInteger kRSSLength = 3;
|
||||
static NSString *RSSUppercaseValue = @"RSS";
|
||||
static NSString *RSSLowercaseValue = @"rss";
|
||||
static NSString *emptyString = @"";
|
||||
|
||||
static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
|
||||
|
||||
return memcmp(bytes1, bytes2, length) == 0;
|
||||
}
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length {
|
||||
|
||||
|
||||
if (length < 1) {
|
||||
return emptyString;
|
||||
return @"";
|
||||
} else if (length == 3) {
|
||||
if (RSSAXEqualBytes(bytes, "RSS", 3)) return @"RSS";
|
||||
if (RSSAXEqualBytes(bytes, "rss", 3)) return @"rss";
|
||||
}
|
||||
|
||||
if (length == kRSSLength) {
|
||||
|
||||
if (equalBytes(bytes, kRSSUppercase, kRSSLength)) {
|
||||
return RSSUppercaseValue;
|
||||
}
|
||||
else if (equalBytes(bytes, kRSSLowercase, kRSSLength)) {
|
||||
return RSSLowercaseValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
@@ -446,22 +446,16 @@ static const NSInteger kTrueLength = 5;
|
||||
}
|
||||
|
||||
|
||||
static BOOL equalBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
|
||||
|
||||
return memcmp(bytes1, bytes2, length) == 0;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)saxParser:(RSSAXParser *)SAXParser internedStringForValue:(const void *)bytes length:(NSUInteger)length {
|
||||
|
||||
static const NSUInteger falseLength = kFalseLength - 1;
|
||||
static const NSUInteger trueLength = kTrueLength - 1;
|
||||
|
||||
if (length == falseLength && equalBytes(bytes, kFalse, falseLength)) {
|
||||
if (length == falseLength && RSSAXEqualBytes(bytes, kFalse, falseLength)) {
|
||||
return kFalseValue;
|
||||
}
|
||||
|
||||
if (length == trueLength && equalBytes(bytes, kTrue, trueLength)) {
|
||||
if (length == trueLength && RSSAXEqualBytes(bytes, kTrue, trueLength)) {
|
||||
return kTrueValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ void RSSAXInitLibXMLParser(void); // Needed by RSSAXHTMLParser.
|
||||
/*For use by delegate.*/
|
||||
|
||||
BOOL RSSAXEqualTags(const unsigned char *localName, const char *tag, NSInteger tagLength);
|
||||
BOOL RSSAXEqualBytes(const void *bytes1, const void *bytes2, NSUInteger length);
|
||||
|
||||
|
||||
@interface RSSAXParser : NSObject
|
||||
|
||||
@@ -225,6 +225,11 @@ BOOL RSSAXEqualTags(const xmlChar *localName, const char *tag, NSInteger tagLeng
|
||||
return !strncmp((const char *)localName, tag, (size_t)tagLength);
|
||||
}
|
||||
|
||||
BOOL RSSAXEqualBytes(const void *bytes1, const void *bytes2, NSUInteger length) {
|
||||
|
||||
return memcmp(bytes1, bytes2, length) == 0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Callbacks
|
||||
|
||||
|
||||
@@ -20,10 +20,7 @@
|
||||
#import <RSXML/RSParsedArticle.h>
|
||||
|
||||
#import <RSXML/RSOPMLParser.h>
|
||||
#import <RSXML/RSOPMLDocument.h>
|
||||
#import <RSXML/RSOPMLItem.h>
|
||||
#import <RSXML/RSOPMLAttributes.h>
|
||||
#import <RSXML/RSOPMLFeedSpecifier.h>
|
||||
|
||||
#import <RSXML/RSXMLError.h>
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
//
|
||||
// RSXMLError.h
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
#import <libxml/xmlerror.h>
|
||||
|
||||
extern NSString *RSXMLErrorDomain;
|
||||
extern NSErrorDomain kLIBXMLParserErrorDomain;
|
||||
extern NSErrorDomain kRSXMLParserErrorDomain;
|
||||
|
||||
|
||||
typedef NS_ENUM(NSInteger, RSXMLErrorCode) {
|
||||
RSXMLErrorCodeDataIsWrongFormat = 1024
|
||||
/// Error codes for RSXML error domain @c (kRSXMLParserErrorDomain)
|
||||
typedef NS_ENUM(NSInteger, RSXMLError) {
|
||||
/// Error codes
|
||||
RSXMLErrorNoData = 100,
|
||||
RSXMLErrorMissingLeftCaret = 110,
|
||||
RSXMLErrorProbablyHTML = 120,
|
||||
RSXMLErrorContainsXMLErrorsTag = 130,
|
||||
RSXMLErrorNoSuitableParser = 140,
|
||||
RSXMLErrorFileNotOPML = 1024 // original value
|
||||
};
|
||||
|
||||
|
||||
NSError *RSOPMLWrongFormatError(NSString *fileName);
|
||||
void RSXMLSetError(NSError **error, RSXMLError code, NSString *filename);
|
||||
NSError * RSXMLMakeError(RSXMLError code, NSString *filename);
|
||||
NSError * RSXMLMakeErrorFromLIBXMLError(xmlErrorPtr err);
|
||||
|
||||
@@ -1,22 +1,48 @@
|
||||
//
|
||||
// RSXMLError.m
|
||||
// RSXML
|
||||
//
|
||||
// Created by Brent Simmons on 2/28/16.
|
||||
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSXMLError.h"
|
||||
|
||||
NSString *RSXMLErrorDomain = @"com.ranchero.RSXML";
|
||||
NSErrorDomain kLIBXMLParserErrorDomain = @"LIBXMLParserErrorDomain";
|
||||
NSErrorDomain kRSXMLParserErrorDomain = @"RSXMLParserErrorDomain";
|
||||
|
||||
NSError *RSOPMLWrongFormatError(NSString *fileName) {
|
||||
|
||||
NSString *localizedDescriptionFormatString = NSLocalizedString(@"The file ‘%@’ can’t be parsed because it’s not an OPML file.", @"OPML wrong format");
|
||||
NSString *localizedDescription = [NSString stringWithFormat:localizedDescriptionFormatString, fileName];
|
||||
|
||||
NSString *localizedFailureString = NSLocalizedString(@"The file is not an OPML file.", @"OPML wrong format");
|
||||
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: localizedDescription, NSLocalizedFailureReasonErrorKey: localizedFailureString};
|
||||
|
||||
return [[NSError alloc] initWithDomain:RSXMLErrorDomain code:RSXMLErrorCodeDataIsWrongFormat userInfo:userInfo];
|
||||
NSString * getErrorMessageForRSXMLError(RSXMLError code, id paramA);
|
||||
NSString * getErrorMessageForRSXMLError(RSXMLError code, id paramA) {
|
||||
switch (code) { // switch statement will warn if an enum value is missing
|
||||
case RSXMLErrorNoData:
|
||||
return @"Couldn't parse feed. No data available.";
|
||||
case RSXMLErrorMissingLeftCaret:
|
||||
return @"Couldn't parse feed. Missing left caret character ('<').";
|
||||
case RSXMLErrorProbablyHTML:
|
||||
return @"Couldn't parse feed. Expecting XML data but found html data.";
|
||||
case RSXMLErrorContainsXMLErrorsTag:
|
||||
return @"Couldn't parse feed. XML contains 'errors' tag.";
|
||||
case RSXMLErrorNoSuitableParser:
|
||||
return @"Couldn't parse feed. No suitable parser found. XML document not well-formed.";
|
||||
case RSXMLErrorFileNotOPML:
|
||||
if (paramA) {
|
||||
return [NSString stringWithFormat:@"The file ‘%@’ can't be parsed because it's not an OPML file.", paramA];
|
||||
}
|
||||
return @"The file can't be parsed because it's not an OPML file.";
|
||||
}
|
||||
}
|
||||
|
||||
void RSXMLSetError(NSError **error, RSXMLError code, NSString *filename) {
|
||||
if (error) {
|
||||
*error = RSXMLMakeError(code, filename);
|
||||
}
|
||||
}
|
||||
|
||||
NSError * RSXMLMakeError(RSXMLError code, NSString *filename) {
|
||||
return [NSError errorWithDomain:kRSXMLParserErrorDomain code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey: getErrorMessageForRSXMLError(code, nil)}];
|
||||
}
|
||||
|
||||
NSError * RSXMLMakeErrorFromLIBXMLError(xmlErrorPtr err) {
|
||||
if (err) {
|
||||
int errCode = err->code;
|
||||
char * msg = err->message;
|
||||
//if (err->level == XML_ERR_FATAL)
|
||||
NSString *errMsg = [[NSString stringWithFormat:@"%s", msg] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
return [NSError errorWithDomain:kLIBXMLParserErrorDomain code:errCode userInfo:@{NSLocalizedDescriptionKey: errMsg}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user