feat: regex converter
This commit is contained in:
@@ -13,6 +13,10 @@
|
|||||||
541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 541C67C22255470B004D2CE6 /* SettingsAppearance.m */; };
|
541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 541C67C22255470B004D2CE6 /* SettingsAppearance.m */; };
|
||||||
54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; };
|
54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; };
|
||||||
54229F552E02491A0019ACB0 /* TinySVG.m in Sources */ = {isa = PBXBuildFile; fileRef = 54229F542E02491A0019ACB0 /* TinySVG.m */; };
|
54229F552E02491A0019ACB0 /* TinySVG.m in Sources */ = {isa = PBXBuildFile; fileRef = 54229F542E02491A0019ACB0 /* TinySVG.m */; };
|
||||||
|
54253C7F2C47303A00742695 /* RegexConverter+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54253C7E2C47303A00742695 /* RegexConverter+Ext.m */; };
|
||||||
|
54253C932C49BFCD00742695 /* RegexConverterModal.m in Sources */ = {isa = PBXBuildFile; fileRef = 54253C8A2C49A92400742695 /* RegexConverterModal.m */; };
|
||||||
|
54253C942C49BFDC00742695 /* RegexConverterController.m in Sources */ = {isa = PBXBuildFile; fileRef = 54253C872C49A6A800742695 /* RegexConverterController.m */; };
|
||||||
|
54253C952C49BFE400742695 /* RegexConverterView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54253C842C47369000742695 /* RegexConverterView.m */; };
|
||||||
544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; };
|
544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; };
|
||||||
544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; };
|
544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; };
|
||||||
54501010230E9C8600F0B165 /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 5450100F230E9C8600F0B165 /* FeedDownload.m */; };
|
54501010230E9C8600F0B165 /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 5450100F230E9C8600F0B165 /* FeedDownload.m */; };
|
||||||
@@ -45,6 +49,7 @@
|
|||||||
54B749E0220636200022CC6D /* FeedArticle+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B749DF220635CD0022CC6D /* FeedArticle+Ext.m */; };
|
54B749E0220636200022CC6D /* FeedArticle+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B749DF220635CD0022CC6D /* FeedArticle+Ext.m */; };
|
||||||
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54BB048821FD2AB500C303A5 /* NSDate+Ext.m */; };
|
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54BB048821FD2AB500C303A5 /* NSDate+Ext.m */; };
|
||||||
54BF444A22D0F4F300660096 /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 54BF444922D0F4F300660096 /* AppIcon.icns */; };
|
54BF444A22D0F4F300660096 /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 54BF444922D0F4F300660096 /* AppIcon.icns */; };
|
||||||
|
54D10DDB2C6E930F0008F621 /* RegexFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D10DDA2C6E930F0008F621 /* RegexFeed.m */; };
|
||||||
54D55D7322E624CD00057B98 /* SettingsFeeds+DragDrop.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D55D7222E624CD00057B98 /* SettingsFeeds+DragDrop.m */; };
|
54D55D7322E624CD00057B98 /* SettingsFeeds+DragDrop.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D55D7222E624CD00057B98 /* SettingsFeeds+DragDrop.m */; };
|
||||||
54D857CE227C5785001BA1C8 /* RefreshStatisticsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D857CD227C5785001BA1C8 /* RefreshStatisticsView.m */; };
|
54D857CE227C5785001BA1C8 /* RefreshStatisticsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D857CD227C5785001BA1C8 /* RefreshStatisticsView.m */; };
|
||||||
54DD9F1323D1D6B000B1EAA6 /* NSColor+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54DD9F1223D1D6B000B1EAA6 /* NSColor+Ext.m */; };
|
54DD9F1323D1D6B000B1EAA6 /* NSColor+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54DD9F1223D1D6B000B1EAA6 /* NSColor+Ext.m */; };
|
||||||
@@ -130,6 +135,14 @@
|
|||||||
54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = "<group>"; };
|
54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = "<group>"; };
|
||||||
54229F532E02491A0019ACB0 /* TinySVG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TinySVG.h; sourceTree = "<group>"; };
|
54229F532E02491A0019ACB0 /* TinySVG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TinySVG.h; sourceTree = "<group>"; };
|
||||||
54229F542E02491A0019ACB0 /* TinySVG.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TinySVG.m; sourceTree = "<group>"; };
|
54229F542E02491A0019ACB0 /* TinySVG.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TinySVG.m; sourceTree = "<group>"; };
|
||||||
|
54253C7A2C47303A00742695 /* RegexConverter+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RegexConverter+Ext.h"; sourceTree = "<group>"; };
|
||||||
|
54253C7E2C47303A00742695 /* RegexConverter+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "RegexConverter+Ext.m"; sourceTree = "<group>"; };
|
||||||
|
54253C832C47368F00742695 /* RegexConverterView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RegexConverterView.h; sourceTree = "<group>"; };
|
||||||
|
54253C842C47369000742695 /* RegexConverterView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RegexConverterView.m; sourceTree = "<group>"; };
|
||||||
|
54253C872C49A6A800742695 /* RegexConverterController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RegexConverterController.m; sourceTree = "<group>"; };
|
||||||
|
54253C882C49A6A800742695 /* RegexConverterController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RegexConverterController.h; sourceTree = "<group>"; };
|
||||||
|
54253C8A2C49A92400742695 /* RegexConverterModal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RegexConverterModal.m; sourceTree = "<group>"; };
|
||||||
|
54253C8B2C49A92400742695 /* RegexConverterModal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RegexConverterModal.h; sourceTree = "<group>"; };
|
||||||
544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; };
|
544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; };
|
||||||
544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = "<group>"; };
|
544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = "<group>"; };
|
||||||
544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = "<group>"; };
|
544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = "<group>"; };
|
||||||
@@ -190,6 +203,8 @@
|
|||||||
54BB048721FD2AB500C303A5 /* NSDate+Ext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+Ext.h"; sourceTree = "<group>"; };
|
54BB048721FD2AB500C303A5 /* NSDate+Ext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+Ext.h"; sourceTree = "<group>"; };
|
||||||
54BB048821FD2AB500C303A5 /* NSDate+Ext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+Ext.m"; sourceTree = "<group>"; };
|
54BB048821FD2AB500C303A5 /* NSDate+Ext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+Ext.m"; sourceTree = "<group>"; };
|
||||||
54BF444922D0F4F300660096 /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = "<group>"; };
|
54BF444922D0F4F300660096 /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = "<group>"; };
|
||||||
|
54D10DD92C6E930F0008F621 /* RegexFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RegexFeed.h; sourceTree = "<group>"; };
|
||||||
|
54D10DDA2C6E930F0008F621 /* RegexFeed.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RegexFeed.m; sourceTree = "<group>"; };
|
||||||
54D55D7122E624CD00057B98 /* SettingsFeeds+DragDrop.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SettingsFeeds+DragDrop.h"; sourceTree = "<group>"; };
|
54D55D7122E624CD00057B98 /* SettingsFeeds+DragDrop.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SettingsFeeds+DragDrop.h"; sourceTree = "<group>"; };
|
||||||
54D55D7222E624CD00057B98 /* SettingsFeeds+DragDrop.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SettingsFeeds+DragDrop.m"; sourceTree = "<group>"; };
|
54D55D7222E624CD00057B98 /* SettingsFeeds+DragDrop.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SettingsFeeds+DragDrop.m"; sourceTree = "<group>"; };
|
||||||
54D857CC227C5785001BA1C8 /* RefreshStatisticsView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RefreshStatisticsView.h; sourceTree = "<group>"; };
|
54D857CC227C5785001BA1C8 /* RefreshStatisticsView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RefreshStatisticsView.h; sourceTree = "<group>"; };
|
||||||
@@ -240,6 +255,21 @@
|
|||||||
path = "Status Bar Menu";
|
path = "Status Bar Menu";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
54253C862C49A5A900742695 /* Regex Editor */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
54253C8B2C49A92400742695 /* RegexConverterModal.h */,
|
||||||
|
54253C8A2C49A92400742695 /* RegexConverterModal.m */,
|
||||||
|
54253C882C49A6A800742695 /* RegexConverterController.h */,
|
||||||
|
54253C872C49A6A800742695 /* RegexConverterController.m */,
|
||||||
|
54253C832C47368F00742695 /* RegexConverterView.h */,
|
||||||
|
54253C842C47369000742695 /* RegexConverterView.m */,
|
||||||
|
54D10DD92C6E930F0008F621 /* RegexFeed.h */,
|
||||||
|
54D10DDA2C6E930F0008F621 /* RegexFeed.m */,
|
||||||
|
);
|
||||||
|
path = "Regex Editor";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
544936F721F1E51E00DEE9AA /* NSCategories */ = {
|
544936F721F1E51E00DEE9AA /* NSCategories */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -298,6 +328,8 @@
|
|||||||
5477D34D21233C62002BA27F /* FeedGroup+Ext.m */,
|
5477D34D21233C62002BA27F /* FeedGroup+Ext.m */,
|
||||||
540F704321B6C16C0022E69D /* FeedMeta+Ext.h */,
|
540F704321B6C16C0022E69D /* FeedMeta+Ext.h */,
|
||||||
540F704421B6C16C0022E69D /* FeedMeta+Ext.m */,
|
540F704421B6C16C0022E69D /* FeedMeta+Ext.m */,
|
||||||
|
54253C7A2C47303A00742695 /* RegexConverter+Ext.h */,
|
||||||
|
54253C7E2C47303A00742695 /* RegexConverter+Ext.m */,
|
||||||
54B749DE220635BE0022CC6D /* FeedArticle+Ext.h */,
|
54B749DE220635BE0022CC6D /* FeedArticle+Ext.h */,
|
||||||
54B749DF220635CD0022CC6D /* FeedArticle+Ext.m */,
|
54B749DF220635CD0022CC6D /* FeedArticle+Ext.m */,
|
||||||
);
|
);
|
||||||
@@ -344,6 +376,7 @@
|
|||||||
541A90EF21257D4F002680A6 /* Status Bar Menu */,
|
541A90EF21257D4F002680A6 /* Status Bar Menu */,
|
||||||
54A07A8322105E0800082C51 /* Core Data */,
|
54A07A8322105E0800082C51 /* Core Data */,
|
||||||
54AD4E04230084FD000AE386 /* Feed Import */,
|
54AD4E04230084FD000AE386 /* Feed Import */,
|
||||||
|
54253C862C49A5A900742695 /* Regex Editor */,
|
||||||
546FC44D2118B357007CC3A3 /* Preferences */,
|
546FC44D2118B357007CC3A3 /* Preferences */,
|
||||||
54ACC28A21061B3C0020715F /* Info.plist */,
|
54ACC28A21061B3C0020715F /* Info.plist */,
|
||||||
54F7101322EE0DDA006985D1 /* Artwork */,
|
54F7101322EE0DDA006985D1 /* Artwork */,
|
||||||
@@ -606,8 +639,10 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
54253C932C49BFCD00742695 /* RegexConverterModal.m in Sources */,
|
||||||
54B51704226DC339006C1B29 /* ModalFeedEditView.m in Sources */,
|
54B51704226DC339006C1B29 /* ModalFeedEditView.m in Sources */,
|
||||||
54AD4E0C2301853D000AE386 /* NSString+Ext.m in Sources */,
|
54AD4E0C2301853D000AE386 /* NSString+Ext.m in Sources */,
|
||||||
|
54D10DDB2C6E930F0008F621 /* RegexFeed.m in Sources */,
|
||||||
546A6A2C22C584AF0034E806 /* SettingsAppearanceView.m in Sources */,
|
546A6A2C22C584AF0034E806 /* SettingsAppearanceView.m in Sources */,
|
||||||
54E9CF32225914300023696F /* SettingsAbout.m in Sources */,
|
54E9CF32225914300023696F /* SettingsAbout.m in Sources */,
|
||||||
54B749E0220636200022CC6D /* FeedArticle+Ext.m in Sources */,
|
54B749E0220636200022CC6D /* FeedArticle+Ext.m in Sources */,
|
||||||
@@ -619,6 +654,7 @@
|
|||||||
54DD9F1323D1D6B000B1EAA6 /* NSColor+Ext.m in Sources */,
|
54DD9F1323D1D6B000B1EAA6 /* NSColor+Ext.m in Sources */,
|
||||||
54ACC29521061E270020715F /* UpdateScheduler.m in Sources */,
|
54ACC29521061E270020715F /* UpdateScheduler.m in Sources */,
|
||||||
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */,
|
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */,
|
||||||
|
54253C7F2C47303A00742695 /* RegexConverter+Ext.m in Sources */,
|
||||||
5477D34E21233C62002BA27F /* FeedGroup+Ext.m in Sources */,
|
5477D34E21233C62002BA27F /* FeedGroup+Ext.m in Sources */,
|
||||||
54B6F14A231551B3002C94C9 /* FaviconDownload.m in Sources */,
|
54B6F14A231551B3002C94C9 /* FaviconDownload.m in Sources */,
|
||||||
54E4446C2329AE0600BBF481 /* NSError+Ext.m in Sources */,
|
54E4446C2329AE0600BBF481 /* NSError+Ext.m in Sources */,
|
||||||
@@ -642,11 +678,13 @@
|
|||||||
541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */,
|
541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */,
|
||||||
54B749DA2204A85C0022CC6D /* BarStatusItem.m in Sources */,
|
54B749DA2204A85C0022CC6D /* BarStatusItem.m in Sources */,
|
||||||
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */,
|
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */,
|
||||||
|
54253C952C49BFE400742695 /* RegexConverterView.m in Sources */,
|
||||||
548C6D0A230C33DE003A1AAF /* NSURL+Ext.m in Sources */,
|
548C6D0A230C33DE003A1AAF /* NSURL+Ext.m in Sources */,
|
||||||
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */,
|
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */,
|
||||||
54195883218A061100581B79 /* Feed+Ext.m in Sources */,
|
54195883218A061100581B79 /* Feed+Ext.m in Sources */,
|
||||||
54501010230E9C8600F0B165 /* FeedDownload.m in Sources */,
|
54501010230E9C8600F0B165 /* FeedDownload.m in Sources */,
|
||||||
54209E942117325100F3B5EF /* DrawImage.m in Sources */,
|
54209E942117325100F3B5EF /* DrawImage.m in Sources */,
|
||||||
|
54253C942C49BFDC00742695 /* RegexConverterController.m in Sources */,
|
||||||
54FE73D021220DEC003EAC65 /* StoreCoordinator.m in Sources */,
|
54FE73D021220DEC003EAC65 /* StoreCoordinator.m in Sources */,
|
||||||
54A07A7F220E04CF00082C51 /* NSFetchRequest+Ext.m in Sources */,
|
54A07A7F220E04CF00082C51 /* NSFetchRequest+Ext.m in Sources */,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -89,6 +89,8 @@
|
|||||||
[localSet removeObject:stored];
|
[localSet removeObject:stored];
|
||||||
if (stored.sortIndex != currentIndex)
|
if (stored.sortIndex != currentIndex)
|
||||||
stored.sortIndex = currentIndex; // Ensures block of ascending indices
|
stored.sortIndex = currentIndex; // Ensures block of ascending indices
|
||||||
|
// replace local values with remote changes (if any)
|
||||||
|
[stored updateArticleIfChanged:article];
|
||||||
} else {
|
} else {
|
||||||
FeedArticle *newArticle = [FeedArticle newArticle:article inContext:self.managedObjectContext];
|
FeedArticle *newArticle = [FeedArticle newArticle:article inContext:self.managedObjectContext];
|
||||||
newArticle.sortIndex = currentIndex;
|
newArticle.sortIndex = currentIndex;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
@interface FeedArticle (Ext)
|
@interface FeedArticle (Ext)
|
||||||
+ (instancetype)newArticle:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)moc;
|
+ (instancetype)newArticle:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)moc;
|
||||||
|
- (void)updateArticleIfChanged:(RSParsedArticle*)entry;
|
||||||
- (NSMenuItem*)newMenuItem;
|
- (NSMenuItem*)newMenuItem;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,16 @@
|
|||||||
return fa;
|
return fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)updateArticleIfChanged:(RSParsedArticle*)entry {
|
||||||
|
[self setGuidIfChanged:entry.guid];
|
||||||
|
[self setTitleIfChanged:entry.title];
|
||||||
|
[self setAuthorIfChanged:entry.author];
|
||||||
|
[self setAbstractIfChanged:(entry.abstract.length > 0) ? [entry.abstract htmlToPlainText] : nil];
|
||||||
|
[self setBodyIfChanged:(entry.body.length > 0) ? [entry.body htmlToPlainText] : nil];
|
||||||
|
[self setLinkIfChanged:(entry.link.length > 0) ? entry.link : entry.guid];
|
||||||
|
[self setPublishedIfChanged:entry.datePublished ? entry.datePublished : entry.dateModified];
|
||||||
|
}
|
||||||
|
|
||||||
/// @return Full or truncated article title, based on user preference in settings.
|
/// @return Full or truncated article title, based on user preference in settings.
|
||||||
- (NSString*)shortArticleName {
|
- (NSString*)shortArticleName {
|
||||||
NSString *title = self.title;
|
NSString *title = self.title;
|
||||||
@@ -71,4 +81,78 @@
|
|||||||
[moc reset];
|
[moc reset];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark - Setter -
|
||||||
|
|
||||||
|
|
||||||
|
/// Set @c guid attribute but only if value differs.
|
||||||
|
- (void)setGuidIfChanged:(nullable NSString*)guid {
|
||||||
|
if (guid.length == 0) {
|
||||||
|
if (self.guid.length > 0)
|
||||||
|
self.guid = nil; // nullify empty strings
|
||||||
|
} else if (![self.guid isEqualToString: guid]) {
|
||||||
|
self.guid = guid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c link attribute but only if value differs.
|
||||||
|
- (void)setLinkIfChanged:(nullable NSString*)link {
|
||||||
|
if (link.length == 0) {
|
||||||
|
if (self.link.length > 0)
|
||||||
|
self.link = nil; // nullify empty strings
|
||||||
|
} else if (![self.link isEqualToString: link]) {
|
||||||
|
self.link = link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c title attribute but only if value differs.
|
||||||
|
- (void)setTitleIfChanged:(nullable NSString*)title {
|
||||||
|
if (title.length == 0) {
|
||||||
|
if (self.title.length > 0)
|
||||||
|
self.title = nil; // nullify empty strings
|
||||||
|
} else if (![self.title isEqualToString: title]) {
|
||||||
|
self.title = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c abstract attribute but only if value differs.
|
||||||
|
- (void)setAbstractIfChanged:(nullable NSString*)abstract {
|
||||||
|
if (abstract.length == 0) {
|
||||||
|
if (self.abstract.length > 0)
|
||||||
|
self.abstract = nil; // nullify empty strings
|
||||||
|
} else if (![self.abstract isEqualToString: abstract]) {
|
||||||
|
self.abstract = abstract;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c body attribute but only if value differs.
|
||||||
|
- (void)setBodyIfChanged:(nullable NSString*)body {
|
||||||
|
if (body.length == 0) {
|
||||||
|
if (self.body.length > 0)
|
||||||
|
self.body = nil; // nullify empty strings
|
||||||
|
} else if (![self.body isEqualToString: body]) {
|
||||||
|
self.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c author attribute but only if value differs.
|
||||||
|
- (void)setAuthorIfChanged:(nullable NSString*)author {
|
||||||
|
if (author.length == 0) {
|
||||||
|
if (self.author.length > 0)
|
||||||
|
self.author = nil; // nullify empty strings
|
||||||
|
} else if (![self.author isEqualToString: author]) {
|
||||||
|
self.author = author;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c published attribute but only if value differs.
|
||||||
|
- (void)setPublishedIfChanged:(nullable NSDate*)published {
|
||||||
|
if (!published) {
|
||||||
|
if (self.published)
|
||||||
|
self.published = nil; // nullify empty date
|
||||||
|
} else if (![self.published isEqualToDate: published]) {
|
||||||
|
self.published = published;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
16
baRSS/Core Data/RegexConverter+Ext.h
Normal file
16
baRSS/Core Data/RegexConverter+Ext.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@import Cocoa;
|
||||||
|
#import "RegexConverter+CoreDataClass.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface RegexConverter (Ext)
|
||||||
|
+ (instancetype)newInContext:(NSManagedObjectContext*)moc;
|
||||||
|
- (void)setEntryIfChanged:(nullable NSString*)pattern;
|
||||||
|
- (void)setHrefIfChanged:(nullable NSString*)pattern;
|
||||||
|
- (void)setTitleIfChanged:(nullable NSString*)pattern;
|
||||||
|
- (void)setDescIfChanged:(nullable NSString*)pattern;
|
||||||
|
- (void)setDateIfChanged:(nullable NSString*)pattern;
|
||||||
|
- (void)setDateFormatIfChanged:(nullable NSString*)pattern;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
70
baRSS/Core Data/RegexConverter+Ext.m
Normal file
70
baRSS/Core Data/RegexConverter+Ext.m
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#import "RegexConverter+Ext.h"
|
||||||
|
|
||||||
|
@implementation RegexConverter (Ext)
|
||||||
|
|
||||||
|
/// Create new instance
|
||||||
|
+ (instancetype)newInContext:(NSManagedObjectContext*)moc {
|
||||||
|
return [[RegexConverter alloc] initWithEntity:[RegexConverter entity] insertIntoManagedObjectContext:moc];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c entry attribute but only if value differs.
|
||||||
|
- (void)setEntryIfChanged:(nullable NSString*)pattern {
|
||||||
|
if (pattern.length == 0) {
|
||||||
|
if (self.entry.length > 0)
|
||||||
|
self.entry = nil; // nullify empty strings
|
||||||
|
} else if (![self.entry isEqualToString: pattern]) {
|
||||||
|
self.entry = pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c href attribute but only if value differs.
|
||||||
|
- (void)setHrefIfChanged:(nullable NSString*)pattern {
|
||||||
|
if (pattern.length == 0) {
|
||||||
|
if (self.href.length > 0)
|
||||||
|
self.href = nil; // nullify empty strings
|
||||||
|
} else if (![self.href isEqualToString: pattern]) {
|
||||||
|
self.href = pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c title attribute but only if value differs.
|
||||||
|
- (void)setTitleIfChanged:(nullable NSString*)pattern {
|
||||||
|
if (pattern.length == 0) {
|
||||||
|
if (self.title.length > 0)
|
||||||
|
self.title = nil; // nullify empty strings
|
||||||
|
} else if (![self.title isEqualToString: pattern]) {
|
||||||
|
self.title = pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c desc attribute but only if value differs.
|
||||||
|
- (void)setDescIfChanged:(nullable NSString*)pattern {
|
||||||
|
if (pattern.length == 0) {
|
||||||
|
if (self.desc.length > 0)
|
||||||
|
self.desc = nil; // nullify empty strings
|
||||||
|
} else if (![self.desc isEqualToString: pattern]) {
|
||||||
|
self.desc = pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c date attribute but only if value differs.
|
||||||
|
- (void)setDateIfChanged:(nullable NSString*)pattern {
|
||||||
|
if (pattern.length == 0) {
|
||||||
|
if (self.date.length > 0)
|
||||||
|
self.date = nil; // nullify empty strings
|
||||||
|
} else if (![self.date isEqualToString: pattern]) {
|
||||||
|
self.date = pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set @c dateFormat attribute but only if value differs.
|
||||||
|
- (void)setDateFormatIfChanged:(nullable NSString*)pattern {
|
||||||
|
if (pattern.length == 0) {
|
||||||
|
if (self.dateFormat.length > 0)
|
||||||
|
self.dateFormat = nil; // nullify empty strings
|
||||||
|
} else if (![self.dateFormat isEqualToString: pattern]) {
|
||||||
|
self.dateFormat = pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -1,52 +1,63 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="17G8030" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1.0.0">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="19H2026" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1.0.0">
|
||||||
<entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
|
<entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="indexPath" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="indexPath" optional="YES" attributeType="String"/>
|
||||||
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="link" optional="YES" attributeType="String"/>
|
||||||
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||||
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="title" optional="YES" attributeType="String"/>
|
||||||
<relationship name="articles" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedArticle" inverseName="feed" inverseEntity="FeedArticle" syncable="YES"/>
|
<relationship name="articles" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedArticle" inverseName="feed" inverseEntity="FeedArticle"/>
|
||||||
<relationship name="group" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="feed" inverseEntity="FeedGroup" syncable="YES"/>
|
<relationship name="group" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="feed" inverseEntity="FeedGroup"/>
|
||||||
<relationship name="meta" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="FeedMeta" inverseName="feed" inverseEntity="FeedMeta" syncable="YES"/>
|
<relationship name="meta" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="FeedMeta" inverseName="feed" inverseEntity="FeedMeta"/>
|
||||||
|
<relationship name="regex" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="RegexConverter" inverseName="feed" inverseEntity="RegexConverter"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="FeedArticle" representedClassName="FeedArticle" syncable="YES" codeGenerationType="class">
|
<entity name="FeedArticle" representedClassName="FeedArticle" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="abstract" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="abstract" optional="YES" attributeType="String"/>
|
||||||
<attribute name="author" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="author" optional="YES" attributeType="String"/>
|
||||||
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="body" optional="YES" attributeType="String"/>
|
||||||
<attribute name="guid" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="guid" optional="YES" attributeType="String"/>
|
||||||
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="link" optional="YES" attributeType="String"/>
|
||||||
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO" customClassName="NSArray" syncable="YES"/>
|
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO" customClassName="NSArray"/>
|
||||||
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="title" optional="YES" attributeType="String"/>
|
||||||
<attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="articles" inverseEntity="Feed" syncable="YES"/>
|
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="articles" inverseEntity="Feed"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="FeedGroup" representedClassName="FeedGroup" syncable="YES" codeGenerationType="class">
|
<entity name="FeedGroup" representedClassName="FeedGroup" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="name" optional="YES" attributeType="String"/>
|
||||||
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES"/>
|
||||||
<relationship name="children" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedGroup" inverseName="parent" inverseEntity="FeedGroup" syncable="YES"/>
|
<relationship name="children" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedGroup" inverseName="parent" inverseEntity="FeedGroup"/>
|
||||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Feed" inverseName="group" inverseEntity="Feed" syncable="YES"/>
|
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Feed" inverseName="group" inverseEntity="Feed"/>
|
||||||
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="children" inverseEntity="FeedGroup" syncable="YES"/>
|
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="children" inverseEntity="FeedGroup"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="FeedMeta" representedClassName="FeedMeta" syncable="YES" codeGenerationType="class">
|
<entity name="FeedMeta" representedClassName="FeedMeta" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="errorCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="errorCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="etag" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="etag" optional="YES" attributeType="String"/>
|
||||||
<attribute name="modified" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="modified" optional="YES" attributeType="String"/>
|
||||||
<attribute name="refresh" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="refresh" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES"/>
|
||||||
<attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="url" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="url" optional="YES" attributeType="String"/>
|
||||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="meta" inverseEntity="Feed" syncable="YES"/>
|
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="meta" inverseEntity="Feed"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Options" representedClassName="Options" syncable="YES" codeGenerationType="class">
|
<entity name="Options" representedClassName="Options" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="key" optional="YES" attributeType="String"/>
|
||||||
<attribute name="value" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="value" optional="YES" attributeType="String"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="RegexConverter" representedClassName="RegexConverter" syncable="YES" codeGenerationType="class">
|
||||||
|
<attribute name="date" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="dateFormat" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="desc" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="entry" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="href" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="title" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="regex" inverseEntity="Feed"/>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="150"/>
|
<element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="163"/>
|
||||||
<element name="FeedArticle" positionX="-96.77734375" positionY="-113.83984375" width="128" height="195"/>
|
<element name="FeedArticle" positionX="-96.77734375" positionY="-113.83984375" width="128" height="195"/>
|
||||||
<element name="FeedGroup" positionX="-460.37890625" positionY="-111.62890625" width="130.52734375" height="135"/>
|
<element name="FeedGroup" positionX="-460.37890625" positionY="-111.62890625" width="130.52734375" height="135"/>
|
||||||
<element name="FeedMeta" positionX="-348.02734375" positionY="136.89453125" width="128" height="150"/>
|
<element name="FeedMeta" positionX="-456.265625" positionY="62.41015625" width="128" height="150"/>
|
||||||
<element name="Options" positionX="-279" positionY="36" width="128" height="75"/>
|
<element name="Options" positionX="-279.09375" positionY="91.4609375" width="128" height="75"/>
|
||||||
|
<element name="RegexConverter" positionX="-115.984375" positionY="93.1796875" width="128" height="148"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
@import Cocoa;
|
@import Cocoa;
|
||||||
@class RSParsedFeed, RSHTMLMetadataFeedLink, Feed, FaviconDownload;
|
@class RSParsedFeed, RSHTMLMetadataFeedLink, Feed, FaviconDownload, RegexConverter;
|
||||||
@protocol FeedDownloadDelegate;
|
@protocol FeedDownloadDelegate;
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property (readonly, nullable) RSParsedFeed *xmlfeed;
|
@property (readonly, nullable) RSParsedFeed *xmlfeed;
|
||||||
@property (readonly, nullable) NSError *error;
|
@property (readonly, nullable) NSError *error;
|
||||||
@property (readonly, nullable) NSString *faviconURL;
|
@property (readonly, nullable) NSString *faviconURL;
|
||||||
|
@property (readonly, nullable) NSData *rawData;
|
||||||
|
|
||||||
typedef void (^FeedDownloadBlock)(FeedDownload *sender);
|
typedef void (^FeedDownloadBlock)(FeedDownload *sender);
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ typedef void (^FeedDownloadBlock)(FeedDownload *sender);
|
|||||||
+ (instancetype)withURL:(NSString*)url;
|
+ (instancetype)withURL:(NSString*)url;
|
||||||
+ (instancetype)withFeed:(Feed*)feed forced:(BOOL)flag;
|
+ (instancetype)withFeed:(Feed*)feed forced:(BOOL)flag;
|
||||||
// Actions
|
// Actions
|
||||||
|
- (instancetype)withRegex:(nullable RegexConverter *)converter enforce:(BOOL)flag;
|
||||||
- (instancetype)startWithDelegate:(id<FeedDownloadDelegate>)delegate;
|
- (instancetype)startWithDelegate:(id<FeedDownloadDelegate>)delegate;
|
||||||
- (instancetype)startWithBlock:(nonnull FeedDownloadBlock)block;
|
- (instancetype)startWithBlock:(nonnull FeedDownloadBlock)block;
|
||||||
- (void)cancel;
|
- (void)cancel;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
#import "FeedMeta+Ext.h"
|
#import "FeedMeta+Ext.h"
|
||||||
#import "NSError+Ext.h"
|
#import "NSError+Ext.h"
|
||||||
#import "NSURLRequest+Ext.h"
|
#import "NSURLRequest+Ext.h"
|
||||||
|
#import "RegexFeed.h"
|
||||||
|
#import "RegexConverter+Ext.h"
|
||||||
|
|
||||||
|
|
||||||
@interface FeedDownload()
|
@interface FeedDownload()
|
||||||
@property (nonatomic, assign) BOOL respondToSelectFeed, respondToRedirect, respondToEnd;
|
@property (nonatomic, assign) BOOL respondToSelectFeed, respondToRedirect, respondToEnd;
|
||||||
@@ -20,6 +23,9 @@
|
|||||||
@property (nonatomic, strong) RSParsedFeed *xmlfeed;
|
@property (nonatomic, strong) RSParsedFeed *xmlfeed;
|
||||||
@property (nonatomic, strong) NSError *error;
|
@property (nonatomic, strong) NSError *error;
|
||||||
@property (nonatomic, strong) NSString *faviconURL;
|
@property (nonatomic, strong) NSString *faviconURL;
|
||||||
|
@property (nonatomic, strong) NSData *rawData;
|
||||||
|
@property (nonatomic, strong) RegexConverter *regexConverter;
|
||||||
|
@property (nonatomic, assign) BOOL regexEnforce;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation FeedDownload
|
@implementation FeedDownload
|
||||||
@@ -51,13 +57,20 @@
|
|||||||
FeedDownload *this = [FeedDownload new];
|
FeedDownload *this = [FeedDownload new];
|
||||||
this.assertIsFeedURL = YES;
|
this.assertIsFeedURL = YES;
|
||||||
this.request = req;
|
this.request = req;
|
||||||
return this;
|
return [this withRegex:feed.regex enforce:false];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// | MARK: - Getter & Setter
|
// | MARK: - Getter & Setter
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Set @c .regexConverter for html-processed feeds.
|
||||||
|
- (instancetype)withRegex:(RegexConverter *)converter enforce:(BOOL)flag {
|
||||||
|
self.regexConverter = converter;
|
||||||
|
self.regexEnforce = flag;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
/// Set delegate and check what methods are implemented.
|
/// Set delegate and check what methods are implemented.
|
||||||
- (void)setDelegate:(id<FeedDownloadDelegate>)observer {
|
- (void)setDelegate:(id<FeedDownloadDelegate>)observer {
|
||||||
_delegate = observer;
|
_delegate = observer;
|
||||||
@@ -134,10 +147,16 @@
|
|||||||
self.currentDownload = [request dataTask:^(NSData * _Nullable data, NSError * _Nullable error, NSHTTPURLResponse *response) {
|
self.currentDownload = [request dataTask:^(NSData * _Nullable data, NSError * _Nullable error, NSHTTPURLResponse *response) {
|
||||||
self.error = error;
|
self.error = error;
|
||||||
self.response = response;
|
self.response = response;
|
||||||
|
self.rawData = data;
|
||||||
if (!data) { // data = nil if (error || 304)
|
if (!data) { // data = nil if (error || 304)
|
||||||
[self performSelectorOnMainThread:@selector(finishAndNotify) withObject:nil waitUntilDone:NO];
|
[self performSelectorOnMainThread:@selector(finishAndNotify) withObject:nil waitUntilDone:NO];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// if regex is used, no further processing
|
||||||
|
if (self.regexConverter || self.regexEnforce) {
|
||||||
|
[self processWithRegexConverter:self.regexConverter data:data];
|
||||||
|
return;
|
||||||
|
}
|
||||||
RSXMLData *xml = [[RSXMLData alloc] initWithData:data url:response.URL];
|
RSXMLData *xml = [[RSXMLData alloc] initWithData:data url:response.URL];
|
||||||
if (!self.assertIsFeedURL && [xml.parserClass isHTMLParser])
|
if (!self.assertIsFeedURL && [xml.parserClass isHTMLParser])
|
||||||
[self processXMLDataHTML:xml]; // HTML source handling
|
[self processXMLDataHTML:xml]; // HTML source handling
|
||||||
@@ -146,6 +165,30 @@
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The downloaded source is HTML data and will be parsed with @c RegexConverter
|
||||||
|
- (void)processWithRegexConverter:(RegexConverter *)converter data:(NSData *)rawData {
|
||||||
|
NSError *err = nil;
|
||||||
|
if (converter) {
|
||||||
|
NSString *theData = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding];
|
||||||
|
NSArray<RegexFeedEntry*> *matches = [[RegexFeed from:converter] process:theData error:&err];
|
||||||
|
|
||||||
|
RSParsedFeed *feed = [[RSParsedFeed alloc] initWithURL:self.request.URL];
|
||||||
|
feed.link = self.request.URL.absoluteString; // needed for group-menu-item-open
|
||||||
|
for (RegexFeedEntry *rxEntry in matches) {
|
||||||
|
RSParsedArticle *article = [feed appendNewArticle];
|
||||||
|
article.link = rxEntry.href;
|
||||||
|
article.title = rxEntry.title;
|
||||||
|
article.body = rxEntry.desc;
|
||||||
|
article.datePublished = rxEntry.date;
|
||||||
|
}
|
||||||
|
self.xmlfeed = feed;
|
||||||
|
} else {
|
||||||
|
self.xmlfeed = nil;
|
||||||
|
}
|
||||||
|
self.error = err;
|
||||||
|
[self performSelectorOnMainThread:@selector(finishAndNotify) withObject:nil waitUntilDone:NO];
|
||||||
|
}
|
||||||
|
|
||||||
/// The downloaded source seems to be HTML data, lets parse it with @c RSXML @c RSHTMLMetadataParser
|
/// The downloaded source seems to be HTML data, lets parse it with @c RSXML @c RSHTMLMetadataParser
|
||||||
- (void)processXMLDataHTML:(RSXMLData*)xml {
|
- (void)processXMLDataHTML:(RSXMLData*)xml {
|
||||||
RSHTMLMetadataParser *parser = [RSHTMLMetadataParser parserWithXMLData:xml];
|
RSHTMLMetadataParser *parser = [RSHTMLMetadataParser parserWithXMLData:xml];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#import "OpmlFile.h"
|
#import "OpmlFile.h"
|
||||||
#import "FeedMeta+Ext.h"
|
#import "FeedMeta+Ext.h"
|
||||||
#import "FeedGroup+Ext.h"
|
#import "FeedGroup+Ext.h"
|
||||||
|
#import "RegexConverter+Ext.h"
|
||||||
#import "StoreCoordinator.h"
|
#import "StoreCoordinator.h"
|
||||||
#import "Constants.h"
|
#import "Constants.h"
|
||||||
#import "NSDate+Ext.h"
|
#import "NSDate+Ext.h"
|
||||||
@@ -120,6 +121,24 @@ static NSInteger RadioGroupSelection(NSView *view) {
|
|||||||
|
|
||||||
newFeed.feed.meta.url = [item attributeForKey:OPMLXMLURLKey];
|
newFeed.feed.meta.url = [item attributeForKey:OPMLXMLURLKey];
|
||||||
newFeed.feed.meta.refresh = interval;
|
newFeed.feed.meta.refresh = interval;
|
||||||
|
|
||||||
|
// baRSS specific
|
||||||
|
NSString *rxEntry = [item attributeForKey:@"rxEntry"];
|
||||||
|
NSString *rxHref = [item attributeForKey:@"rxHref"];
|
||||||
|
NSString *rxTitle = [item attributeForKey:@"rxTitle"];
|
||||||
|
NSString *rxDesc = [item attributeForKey:@"rxDesc"];
|
||||||
|
NSString *rxDate = [item attributeForKey:@"rxDate"];
|
||||||
|
NSString *rxDateFormat = [item attributeForKey:@"rxDateFormat"];
|
||||||
|
if (rxEntry || rxHref || rxTitle || rxDesc || rxDate || rxDateFormat) {
|
||||||
|
RegexConverter *rx = [RegexConverter newInContext:moc];
|
||||||
|
rx.entry = rxEntry;
|
||||||
|
rx.href = rxHref;
|
||||||
|
rx.title = rxTitle;
|
||||||
|
rx.desc = rxDesc;
|
||||||
|
rx.date = rxDate;
|
||||||
|
rx.dateFormat = rxDateFormat;
|
||||||
|
newFeed.feed.regex = rx;
|
||||||
|
}
|
||||||
} else { // GROUP
|
} else { // GROUP
|
||||||
for (NSUInteger i = 0; i < item.children.count; i++) {
|
for (NSUInteger i = 0; i < item.children.count; i++) {
|
||||||
[self importFeed:item.children[i] parent:newFeed index:(int32_t)i inContext:moc];
|
[self importFeed:item.children[i] parent:newFeed index:(int32_t)i inContext:moc];
|
||||||
@@ -279,6 +298,21 @@ static NSInteger RadioGroupSelection(NSView *view) {
|
|||||||
[outline addAttribute:[NSXMLNode attributeWithName:OPMLTypeKey stringValue:@"rss"]];
|
[outline addAttribute:[NSXMLNode attributeWithName:OPMLTypeKey stringValue:@"rss"]];
|
||||||
NSString *intervalStr = [NSString stringWithFormat:@"%d", item.feed.meta.refresh];
|
NSString *intervalStr = [NSString stringWithFormat:@"%d", item.feed.meta.refresh];
|
||||||
[outline addAttribute:[NSXMLNode attributeWithName:@"refreshInterval" stringValue:intervalStr]]; // baRSS specific
|
[outline addAttribute:[NSXMLNode attributeWithName:@"refreshInterval" stringValue:intervalStr]]; // baRSS specific
|
||||||
|
RegexConverter *rx = item.feed.regex;
|
||||||
|
if (rx) { // baRSS specific
|
||||||
|
if (rx.entry)
|
||||||
|
[outline addAttribute:[NSXMLNode attributeWithName:@"rxEntry" stringValue:rx.entry]];
|
||||||
|
if (rx.href)
|
||||||
|
[outline addAttribute:[NSXMLNode attributeWithName:@"rxHref" stringValue:rx.href]];
|
||||||
|
if (rx.title)
|
||||||
|
[outline addAttribute:[NSXMLNode attributeWithName:@"rxTitle" stringValue:rx.title]];
|
||||||
|
if (rx.desc)
|
||||||
|
[outline addAttribute:[NSXMLNode attributeWithName:@"rxDesc" stringValue:rx.desc]];
|
||||||
|
if (rx.date)
|
||||||
|
[outline addAttribute:[NSXMLNode attributeWithName:@"rxDate" stringValue:rx.date]];
|
||||||
|
if (rx.dateFormat)
|
||||||
|
[outline addAttribute:[NSXMLNode attributeWithName:@"rxDateFormat" stringValue:rx.dateFormat]];
|
||||||
|
}
|
||||||
// TODO: option to export unread state?
|
// TODO: option to export unread state?
|
||||||
}
|
}
|
||||||
parent = outline;
|
parent = outline;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
@interface ModalFeedEdit : ModalEditDialog <NSTextFieldDelegate>
|
@interface ModalFeedEdit : ModalEditDialog <NSTextFieldDelegate>
|
||||||
- (void)didClickWarningButton:(NSButton*)sender;
|
- (void)didClickWarningButton:(NSButton*)sender;
|
||||||
|
- (void)openRegexConverter;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface ModalGroupEdit : ModalEditDialog
|
@interface ModalGroupEdit : ModalEditDialog
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
#import "NSView+Ext.h"
|
#import "NSView+Ext.h"
|
||||||
#import "NSDate+Ext.h"
|
#import "NSDate+Ext.h"
|
||||||
#import "NSURL+Ext.h"
|
#import "NSURL+Ext.h"
|
||||||
|
#import "RegexConverterController.h"
|
||||||
|
#import "RegexConverterModal.h"
|
||||||
|
#import "RegexConverter+Ext.h"
|
||||||
|
|
||||||
// ################################################################
|
// ################################################################
|
||||||
// #
|
// #
|
||||||
@@ -59,6 +62,8 @@
|
|||||||
@property (strong) FeedDownload *memFeed;
|
@property (strong) FeedDownload *memFeed;
|
||||||
@property (weak) FaviconDownload *memIcon;
|
@property (weak) FaviconDownload *memIcon;
|
||||||
@property (strong) RefreshStatisticsView *statisticsView;
|
@property (strong) RefreshStatisticsView *statisticsView;
|
||||||
|
@property (nonatomic, assign) BOOL openRegexAfterDownload;
|
||||||
|
@property (weak) id eventMonitor;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation ModalFeedEdit
|
@implementation ModalFeedEdit
|
||||||
@@ -71,6 +76,13 @@
|
|||||||
self.view.refreshNum.intValue = 30;
|
self.view.refreshNum.intValue = 30;
|
||||||
[NSDate populateUnitsMenu:self.view.refreshUnit selected:TimeUnitMinutes];
|
[NSDate populateUnitsMenu:self.view.refreshUnit selected:TimeUnitMinutes];
|
||||||
[self populateTextFields:self.feedGroup];
|
[self populateTextFields:self.feedGroup];
|
||||||
|
|
||||||
|
// removed in windowShouldClose:
|
||||||
|
self.eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskFlagsChanged handler:^(NSEvent *event) {
|
||||||
|
BOOL optionKeyActive = ((event.modifierFlags & NSEventModifierFlagOption) != 0);
|
||||||
|
self.view.regexConverterButton.hidden = !self.feedGroup.feed.regex && !optionKeyActive;
|
||||||
|
return event;
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pre-fill UI control field values with @c FeedGroup properties.
|
/// Pre-fill UI control field values with @c FeedGroup properties.
|
||||||
@@ -81,6 +93,7 @@
|
|||||||
self.view.url.objectValue = fg.feed.meta.url;
|
self.view.url.objectValue = fg.feed.meta.url;
|
||||||
self.previousURL = self.view.url.stringValue;
|
self.previousURL = self.view.url.stringValue;
|
||||||
self.view.favicon.image = [fg.feed iconImage16];
|
self.view.favicon.image = [fg.feed iconImage16];
|
||||||
|
self.view.regexConverterButton.hidden = !fg.feed.regex;
|
||||||
[NSDate setInterval:fg.feed.meta.refresh forPopup:self.view.refreshUnit andField:self.view.refreshNum animate:NO];
|
[NSDate setInterval:fg.feed.meta.refresh forPopup:self.view.refreshUnit andField:self.view.refreshNum animate:NO];
|
||||||
[self statsForCoreDataObject];
|
[self statsForCoreDataObject];
|
||||||
}
|
}
|
||||||
@@ -131,7 +144,9 @@
|
|||||||
self.view.name.placeholderString = NSLocalizedString(@"Loading …", nil);
|
self.view.name.placeholderString = NSLocalizedString(@"Loading …", nil);
|
||||||
}
|
}
|
||||||
self.previousURL = self.view.url.stringValue;
|
self.previousURL = self.view.url.stringValue;
|
||||||
self.memFeed = [[FeedDownload withURL:self.previousURL] startWithDelegate:self];
|
self.memFeed = [[[FeedDownload withURL:self.previousURL]
|
||||||
|
withRegex:self.feedGroup.feed.regex enforce:self.openRegexAfterDownload]
|
||||||
|
startWithDelegate:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,7 +225,42 @@
|
|||||||
- (void)downloadComplete {
|
- (void)downloadComplete {
|
||||||
[self.view.spinnerURL stopAnimation:nil];
|
[self.view.spinnerURL stopAnimation:nil];
|
||||||
[self.modalSheet setDoneEnabled:YES];
|
[self.modalSheet setDoneEnabled:YES];
|
||||||
|
|
||||||
|
if (self.openRegexAfterDownload) {
|
||||||
|
[self openRegexConverter];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark - Regex Converter
|
||||||
|
|
||||||
|
- (void)openRegexConverter {
|
||||||
|
if (!self.openRegexAfterDownload) {
|
||||||
|
self.openRegexAfterDownload = YES;
|
||||||
|
[self downloadRSS];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.openRegexAfterDownload = NO;
|
||||||
|
|
||||||
|
// shrink FeedEdit modal size to effectively hide it behind new modal
|
||||||
|
NSRect previous = self.modalSheet.frame;
|
||||||
|
CGFloat minWidthDiff = previous.size.width - self.modalSheet.minSize.width;
|
||||||
|
[self.modalSheet setFrame:NSInsetRect(previous, minWidthDiff / 2.0, 0) display:NO];
|
||||||
|
|
||||||
|
Feed *feed = self.feedGroup.feed;
|
||||||
|
RegexConverterController *c = [RegexConverterController withData:self.memFeed.rawData andConverter:feed.regex];
|
||||||
|
[self.modalSheet.sheetParent beginCriticalSheet:[c getModalSheet] completionHandler:^(NSModalResponse returnCode) {
|
||||||
|
// reset previous size
|
||||||
|
[self.modalSheet setFrame:previous display:NO];
|
||||||
|
|
||||||
|
if (returnCode == NSModalResponseOK) {
|
||||||
|
[c applyChanges:feed];
|
||||||
|
self.view.regexConverterButton.hidden = !feed.regex;
|
||||||
|
[self downloadRSS];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Feed Statistics
|
#pragma mark - Feed Statistics
|
||||||
|
|
||||||
@@ -264,6 +314,7 @@
|
|||||||
[[NSNotificationCenter defaultCenter] postNotificationName:NSControlTextDidEndEditingNotification object:self.view.url];
|
[[NSNotificationCenter defaultCenter] postNotificationName:NSControlTextDidEndEditingNotification object:self.view.url];
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
[NSEvent removeMonitor:self.eventMonitor];
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property NSPopover *warningPopover;
|
@property NSPopover *warningPopover;
|
||||||
@property (strong) IBOutlet NSTextField *warningText;
|
@property (strong) IBOutlet NSTextField *warningText;
|
||||||
@property (strong) IBOutlet NSButton *warningReload;
|
@property (strong) IBOutlet NSButton *warningReload;
|
||||||
|
@property (strong) IBOutlet NSButton *regexConverterButton;
|
||||||
|
|
||||||
- (instancetype)initWithController:(ModalFeedEdit*)controller NS_DESIGNATED_INITIALIZER;
|
- (instancetype)initWithController:(ModalFeedEdit*)controller NS_DESIGNATED_INITIALIZER;
|
||||||
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
|
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#import "ModalFeedEditView.h"
|
#import "ModalFeedEditView.h"
|
||||||
#import "ModalFeedEdit.h"
|
#import "ModalFeedEdit.h"
|
||||||
#import "NSView+Ext.h"
|
#import "NSView+Ext.h"
|
||||||
|
#import "Constants.h"
|
||||||
|
|
||||||
@interface StrictUIntFormatter : NSFormatter
|
@interface StrictUIntFormatter : NSFormatter
|
||||||
@end
|
@end
|
||||||
@@ -25,7 +26,8 @@
|
|||||||
self.url = [[[NSView inputField:@"https://example.org/feed.rss" width:0] placeIn:self x:x yTop:0] sizeToRight:PAD_S + 18];
|
self.url = [[[NSView inputField:@"https://example.org/feed.rss" width:0] placeIn:self x:x yTop:0] sizeToRight:PAD_S + 18];
|
||||||
self.spinnerURL = [[NSView activitySpinner] placeIn:self xRight:1 yTop:2.5];
|
self.spinnerURL = [[NSView activitySpinner] placeIn:self xRight:1 yTop:2.5];
|
||||||
self.favicon = [[[NSView imageView:nil size:18] tooltip:NSLocalizedString(@"Favicon", nil)] placeIn:self xRight:0 yTop:1.5];
|
self.favicon = [[[NSView imageView:nil size:18] tooltip:NSLocalizedString(@"Favicon", nil)] placeIn:self xRight:0 yTop:1.5];
|
||||||
self.warningButton = [[[[NSView buttonIcon:NSImageNameCaution size:18] action:@selector(didClickWarningButton:) target:nil] // up the responder chain
|
self.warningButton = [[[[NSView buttonIcon:NSImageNameCaution size:18]
|
||||||
|
action:@selector(didClickWarningButton:) target:nil] // up the responder chain
|
||||||
tooltip:NSLocalizedString(@"Click here to show failure reason", nil)]
|
tooltip:NSLocalizedString(@"Click here to show failure reason", nil)]
|
||||||
placeIn:self xRight:0 yTop:1.5];
|
placeIn:self xRight:0 yTop:1.5];
|
||||||
// 2. row
|
// 2. row
|
||||||
@@ -34,6 +36,10 @@
|
|||||||
// 3. row
|
// 3. row
|
||||||
self.refreshNum = [[NSView inputField:@"30" width:85] placeIn:self x:x yTop:2*rowHeight];
|
self.refreshNum = [[NSView inputField:@"30" width:85] placeIn:self x:x yTop:2*rowHeight];
|
||||||
self.refreshUnit = [[NSView popupButton:120] placeIn:self x:NSMaxX(self.refreshNum.frame) + PAD_M yTop:2*rowHeight];
|
self.refreshUnit = [[NSView popupButton:120] placeIn:self x:NSMaxX(self.refreshNum.frame) + PAD_M yTop:2*rowHeight];
|
||||||
|
self.regexConverterButton = [[[[NSView buttonIcon:RSSImageRegexIcon size:19]
|
||||||
|
action:@selector(openRegexConverter) target:controller]
|
||||||
|
tooltip:NSLocalizedString(@"Regex converter", nil)]
|
||||||
|
placeIn:self xRight:0 yTop:2*rowHeight + 1];
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
self.url.accessibilityLabel = lbls[0];
|
self.url.accessibilityLabel = lbls[0];
|
||||||
@@ -41,6 +47,7 @@
|
|||||||
self.refreshNum.accessibilityLabel = NSLocalizedString(@"Refresh interval", nil);
|
self.refreshNum.accessibilityLabel = NSLocalizedString(@"Refresh interval", nil);
|
||||||
self.url.delegate = controller;
|
self.url.delegate = controller;
|
||||||
self.warningButton.hidden = YES;
|
self.warningButton.hidden = YES;
|
||||||
|
self.regexConverterButton.hidden = YES;
|
||||||
self.refreshNum.formatter = [StrictUIntFormatter new]; // see below ...
|
self.refreshNum.formatter = [StrictUIntFormatter new]; // see below ...
|
||||||
[self prepareWarningPopover];
|
[self prepareWarningPopover];
|
||||||
return self;
|
return self;
|
||||||
|
|||||||
12
baRSS/Regex Editor/RegexConverterController.h
Normal file
12
baRSS/Regex Editor/RegexConverterController.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@import Cocoa;
|
||||||
|
@class RegexConverter, RegexConverterModal, Feed;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface RegexConverterController : NSViewController <NSTextFieldDelegate>
|
||||||
|
+ (instancetype)withData:(NSData *)data andConverter:(nullable RegexConverter*)converter;
|
||||||
|
- (RegexConverterModal*)getModalSheet;
|
||||||
|
- (void)applyChanges:(Feed *)feed;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
125
baRSS/Regex Editor/RegexConverterController.m
Normal file
125
baRSS/Regex Editor/RegexConverterController.m
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#import "RegexConverterController.h"
|
||||||
|
#import "RegexConverterView.h"
|
||||||
|
#import "RegexConverter+Ext.h"
|
||||||
|
#import "RegexConverterModal.h"
|
||||||
|
#import "RegexFeed.h"
|
||||||
|
#import "Feed+Ext.h"
|
||||||
|
#import "NSURLRequest+Ext.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ################################################################
|
||||||
|
// #
|
||||||
|
// # MARK: - RegexConverterController -
|
||||||
|
// #
|
||||||
|
// ################################################################
|
||||||
|
|
||||||
|
@interface RegexConverterController() <NSWindowDelegate>
|
||||||
|
@property (strong) RegexConverter *converter;
|
||||||
|
@property (strong) RegexConverterModal *modalSheet;
|
||||||
|
@property (strong) IBOutlet RegexConverterView *view; // override
|
||||||
|
|
||||||
|
@property (strong) NSString *theData; // not "copy" because generated in initializer
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RegexConverterController
|
||||||
|
@dynamic view;
|
||||||
|
|
||||||
|
/// Dedicated initializer
|
||||||
|
+ (instancetype)withData:(NSData *)data andConverter:(RegexConverter*)converter {
|
||||||
|
RegexConverterController *diag = [self new];
|
||||||
|
diag.theData = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]: @"";
|
||||||
|
diag.converter = converter;
|
||||||
|
return diag;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (RegexConverterModal *)getModalSheet {
|
||||||
|
if (!self.modalSheet) {
|
||||||
|
self.modalSheet = [[RegexConverterModal alloc] initWithView:self.view];
|
||||||
|
self.modalSheet.delegate = self;
|
||||||
|
}
|
||||||
|
return self.modalSheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)loadView {
|
||||||
|
self.view = [[RegexConverterView alloc] initWithController:self];
|
||||||
|
[self populateTextFields:self.converter];
|
||||||
|
[self updateOutput:self.theData];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pre-fill UI control field values with @c RegexConverter properties.
|
||||||
|
- (void)populateTextFields:(RegexConverter*)converter {
|
||||||
|
if (converter) {
|
||||||
|
self.view.entry.objectValue = converter.entry;
|
||||||
|
self.view.href.objectValue = converter.href;
|
||||||
|
self.view.title.objectValue = converter.title;
|
||||||
|
self.view.desc.objectValue = converter.desc;
|
||||||
|
self.view.date.objectValue = converter.date;
|
||||||
|
self.view.dateFormat.objectValue = converter.dateFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Update CoreData
|
||||||
|
|
||||||
|
- (void)applyChanges:(Feed *)feed {
|
||||||
|
BOOL shouldDelete = self.view.entry.stringValue.length == 0;
|
||||||
|
|
||||||
|
if (shouldDelete) {
|
||||||
|
if (feed.regex) {
|
||||||
|
[feed.managedObjectContext deleteObject:feed.regex];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!feed.regex) {
|
||||||
|
feed.regex = [RegexConverter newInContext:feed.managedObjectContext];
|
||||||
|
}
|
||||||
|
|
||||||
|
[feed.regex setEntryIfChanged:self.view.entry.stringValue];
|
||||||
|
[feed.regex setHrefIfChanged:self.view.href.stringValue];
|
||||||
|
[feed.regex setTitleIfChanged:self.view.title.stringValue];
|
||||||
|
[feed.regex setDescIfChanged:self.view.desc.stringValue];
|
||||||
|
[feed.regex setDateIfChanged:self.view.date.stringValue];
|
||||||
|
[feed.regex setDateFormatIfChanged:self.view.dateFormat.stringValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - NSTextField Delegate
|
||||||
|
|
||||||
|
- (RegexFeed*)regexParser {
|
||||||
|
RegexFeed *tmp = [RegexFeed new];
|
||||||
|
tmp.rxEntry = self.view.entry.stringValue;
|
||||||
|
tmp.rxHref = self.view.href.stringValue;
|
||||||
|
tmp.rxTitle = self.view.title.stringValue;
|
||||||
|
tmp.rxDesc = self.view.desc.stringValue;
|
||||||
|
tmp.rxDate = self.view.date.stringValue;
|
||||||
|
tmp.dateFormat = self.view.dateFormat.stringValue;
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)controlTextDidEndEditing:(NSNotification*)obj {
|
||||||
|
if (self.view.entry.stringValue.length == 0) {
|
||||||
|
[self updateOutput:self.theData];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *err = nil;
|
||||||
|
NSArray<RegexFeedEntry*> *matches = [[self regexParser] process:self.theData error:&err];
|
||||||
|
if (err) {
|
||||||
|
[self updateOutput:[NSString stringWithFormat:@"%@\n––––\n%@",
|
||||||
|
err.localizedDescription, err.localizedRecoverySuggestion]];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableString *rv = [NSMutableString new];
|
||||||
|
for (RegexFeedEntry *entry in matches) {
|
||||||
|
[rv appendFormat:@"%@\n\n$_href: %@\n$_title: %@\n$_date: %@ -> %@\n$_description: %@\n\n----------\n\n",
|
||||||
|
entry.rawMatch, entry.href, entry.title, entry.dateString, entry.date, entry.desc];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self updateOutput:rv];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateOutput:(NSString *)text {
|
||||||
|
[self.view.output.textStorage setAttributedString:[[NSAttributedString alloc] initWithString:text]];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
12
baRSS/Regex Editor/RegexConverterModal.h
Normal file
12
baRSS/Regex Editor/RegexConverterModal.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@import Cocoa;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface RegexConverterModal : NSPanel
|
||||||
|
@property (readonly) BOOL didTapCancel;
|
||||||
|
|
||||||
|
- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)backingStoreType defer:(BOOL)flag NS_UNAVAILABLE;
|
||||||
|
- (instancetype)initWithView:(NSView*)content NS_DESIGNATED_INITIALIZER;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
60
baRSS/Regex Editor/RegexConverterModal.m
Normal file
60
baRSS/Regex Editor/RegexConverterModal.m
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#import "RegexConverterModal.h"
|
||||||
|
#import "UserPrefs.h"
|
||||||
|
#import "NSView+Ext.h"
|
||||||
|
|
||||||
|
@interface RegexConverterModal()
|
||||||
|
@property (assign) BOOL respondToShouldClose;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RegexConverterModal
|
||||||
|
|
||||||
|
/// Designated initializer. 'Done' and 'Cancel' buttons will be added automatically.
|
||||||
|
- (instancetype)initWithView:(NSView*)content {
|
||||||
|
static CGFloat const contentOffsetY = PAD_WIN + HEIGHT_BUTTON + PAD_L;
|
||||||
|
|
||||||
|
CGSize sz = content.frame.size;
|
||||||
|
sz.width += 2 * (NSInteger)PAD_WIN;
|
||||||
|
sz.height += PAD_WIN + contentOffsetY; // the second PAD_WIN is already in contentOffsetY
|
||||||
|
|
||||||
|
NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView;
|
||||||
|
self = [super initWithContentRect:NSMakeRect(0, 0, sz.width, sz.height) styleMask:style backing:NSBackingStoreBuffered defer:NO];
|
||||||
|
[content placeIn:self.contentView x:PAD_WIN y:contentOffsetY];
|
||||||
|
|
||||||
|
self.minSize = sz;
|
||||||
|
|
||||||
|
// Add default interaction buttons
|
||||||
|
NSButton *btnDone = [self createButton:NSLocalizedString(@"Done", nil) atX:PAD_WIN];
|
||||||
|
NSButton *btnCancel = [self createButton:NSLocalizedString(@"Cancel", nil) atX:sz.width - NSMinX(btnDone.frame) + PAD_M];
|
||||||
|
btnDone.tag = 42; // mark 'Done' button
|
||||||
|
//btnDone.keyEquivalent = @"\r"; // Enter / Return
|
||||||
|
btnCancel.keyEquivalent = [NSString stringWithFormat:@"%c", 0x1b]; // ESC
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to create bottom-right aligned button.
|
||||||
|
- (NSButton*)createButton:(NSString*)text atX:(CGFloat)x {
|
||||||
|
return [[[NSView button:text] action:@selector(didTapButton:) target:self] placeIn:self.contentView xRight:x y:PAD_WIN];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets bool for future usage
|
||||||
|
- (void)setDelegate:(id<NSWindowDelegate>)delegate {
|
||||||
|
[super setDelegate:delegate];
|
||||||
|
self.respondToShouldClose = [delegate respondsToSelector:@selector(windowShouldClose:)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Called after user has clicked the 'Done' (Return) or 'Cancel' (Esc) button.
|
||||||
|
In the later case set @c .didTapCancel @c = @c YES
|
||||||
|
*/
|
||||||
|
- (void)didTapButton:(NSButton*)sender {
|
||||||
|
BOOL successful = (sender.tag == 42); // 'Done' button
|
||||||
|
_didTapCancel = !successful;
|
||||||
|
if (self.respondToShouldClose && ![self.delegate windowShouldClose:self]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Remove subviews to avoid _NSKeyboardFocusClipView issues
|
||||||
|
[self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||||
|
[self.sheetParent endSheet:self returnCode:(successful ? NSModalResponseOK : NSModalResponseCancel)];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
20
baRSS/Regex Editor/RegexConverterView.h
Normal file
20
baRSS/Regex Editor/RegexConverterView.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
@import Cocoa;
|
||||||
|
@class RegexConverter, RegexConverterController;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface RegexConverterView : NSView
|
||||||
|
@property (strong) IBOutlet NSTextField *entry;
|
||||||
|
@property (strong) IBOutlet NSTextField *href;
|
||||||
|
@property (strong) IBOutlet NSTextField *title;
|
||||||
|
@property (strong) IBOutlet NSTextField *date;
|
||||||
|
@property (strong) IBOutlet NSTextField *dateFormat;
|
||||||
|
@property (strong) IBOutlet NSTextField *desc;
|
||||||
|
@property (strong) IBOutlet NSTextView *output;
|
||||||
|
|
||||||
|
- (instancetype)initWithController:(RegexConverterController*)controller NS_DESIGNATED_INITIALIZER;
|
||||||
|
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
|
||||||
|
- (nullable instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
141
baRSS/Regex Editor/RegexConverterView.m
Normal file
141
baRSS/Regex Editor/RegexConverterView.m
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#import "RegexConverterView.h"
|
||||||
|
#import "RegexConverterController.h"
|
||||||
|
#import "RegexConverter+Ext.h"
|
||||||
|
#import "NSDate+Ext.h"
|
||||||
|
#import "NSView+Ext.h"
|
||||||
|
|
||||||
|
@interface RegexConverterView()
|
||||||
|
@property NSPopover *infoPopover;
|
||||||
|
@property (strong) IBOutlet NSTextField *popoverText;
|
||||||
|
@property (strong) IBOutlet NSButton *infoButtonEntry;
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
@implementation RegexConverterView
|
||||||
|
|
||||||
|
static CGFloat const heightHowTo = 2 * HEIGHT_LABEL_SMALL;
|
||||||
|
static CGFloat const heightOutput = 150;
|
||||||
|
static CGFloat const heightRow = PAD_S + HEIGHT_INPUTFIELD;
|
||||||
|
|
||||||
|
- (instancetype)initWithController:(RegexConverterController*)controller {
|
||||||
|
NSArray *lbls = @[
|
||||||
|
NSLocalizedString(@"Entries", nil),
|
||||||
|
NSLocalizedString(@"Link", nil),
|
||||||
|
NSLocalizedString(@"Title", nil),
|
||||||
|
NSLocalizedString(@"Description", nil),
|
||||||
|
NSLocalizedString(@"Date", nil),
|
||||||
|
NSLocalizedString(@"Date Format", nil),
|
||||||
|
];
|
||||||
|
NSView *labels = [NSView labelColumn:lbls rowHeight:HEIGHT_INPUTFIELD padding:PAD_S];
|
||||||
|
|
||||||
|
self = [super initWithFrame:NSMakeRect(0, 0, 420, heightHowTo + PAD_L + NSHeight(labels.frame) + PAD_L + heightOutput)];
|
||||||
|
self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||||
|
|
||||||
|
[self makeHowTo];
|
||||||
|
|
||||||
|
CGFloat x = NSWidth(labels.frame) + PAD_S;
|
||||||
|
[labels placeIn:self x:0 yTop:heightHowTo + PAD_L];
|
||||||
|
|
||||||
|
self.entry = [self inputAndExamples:0 x:x delegate:controller];
|
||||||
|
self.href = [self inputAndExamples:1 x:x delegate:controller];
|
||||||
|
self.title = [self inputAndExamples:2 x:x delegate:controller];
|
||||||
|
self.desc = [self inputAndExamples:3 x:x delegate:controller];
|
||||||
|
self.date = [self inputAndExamples:4 x:x delegate:controller];
|
||||||
|
self.dateFormat = [self inputAndExamples:5 x:x delegate:controller];
|
||||||
|
|
||||||
|
// output text field
|
||||||
|
self.output = [self makeOutput];
|
||||||
|
|
||||||
|
// prepare info popover
|
||||||
|
self.infoPopover = [NSView popover: NSMakeSize(400, 100)];
|
||||||
|
NSView *content = self.infoPopover.contentViewController.view;
|
||||||
|
self.popoverText = [[[[[NSView label:@""] selectable] sizableWidthAndHeight]
|
||||||
|
multiline:NSMakeSize(384, 92)] placeIn:content x:8 y:4];
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSTextView *)makeHowTo {
|
||||||
|
NSTextView *tv = [[NSTextView new] sizableWidthAndHeight];
|
||||||
|
tv.editable = NO; // but selectable
|
||||||
|
tv.drawsBackground = NO;
|
||||||
|
tv.textContainer.textView.string = NSLocalizedString(@"DIY regex converter. Press enter to confirm. For help, refer to online tools (e.g., regex101 with options: global + single-line)", nil);
|
||||||
|
NSScrollView *scroll = [self wrapContent:tv inScrollView:NSMakeRect(-1, NSHeight(self.frame) - heightHowTo, NSWidth(self.frame) + 2, heightHowTo)];
|
||||||
|
scroll.drawsBackground = NO;
|
||||||
|
scroll.borderType = NSNoBorder;
|
||||||
|
scroll.verticalScrollElasticity = NSScrollElasticityNone;
|
||||||
|
scroll.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable;
|
||||||
|
return tv;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSTextView *)makeOutput {
|
||||||
|
NSTextView *tv = [[NSTextView new] sizableWidthAndHeight];
|
||||||
|
tv.editable = NO; // but selectable
|
||||||
|
tv.backgroundColor = NSColor.whiteColor;
|
||||||
|
[self wrapContent:tv inScrollView:NSMakeRect(-1, 0, NSWidth(self.frame) + 2, heightOutput)];
|
||||||
|
return tv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to create input field with help button showing regex examples
|
||||||
|
- (NSTextField *)inputAndExamples:(NSInteger)row x:(CGFloat)x delegate:(id<NSTextFieldDelegate>)delegate {
|
||||||
|
CGFloat yOffset = heightHowTo + PAD_L + row * heightRow;
|
||||||
|
NSTextField *input = [[[NSView inputField:@"" width:0] placeIn:self x:x yTop:yOffset]
|
||||||
|
sizeToRight:PAD_S + HEIGHT_BUTTON]; // width of the helpButton
|
||||||
|
input.delegate = delegate;
|
||||||
|
|
||||||
|
NSInteger tag = 700 + row;
|
||||||
|
NSArray<NSString *> *examples = [self examplesFor:tag];
|
||||||
|
if (examples.count > 0) {
|
||||||
|
[[[[NSView helpButton] action:@selector(didClickExamplesButton:) target:self]
|
||||||
|
tooltip:NSLocalizedString(@"Click here to show examples", nil)]
|
||||||
|
placeIn:self xRight:0 yTop:yOffset].tag = tag;
|
||||||
|
|
||||||
|
input.placeholderString = [examples firstObject];
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Example to be displayed in help button
|
||||||
|
- (NSArray<NSString *> *)examplesFor:(NSInteger)tag {
|
||||||
|
switch (tag) {
|
||||||
|
case 700: return @[ // entries
|
||||||
|
@"<dt[ >].*?<\\/dd>",
|
||||||
|
];
|
||||||
|
case 701: return @[ // link
|
||||||
|
@"href=\"([^\"]*)\"",
|
||||||
|
];
|
||||||
|
case 702: return @[ // title
|
||||||
|
@"title=\"([^\"]*)\"",
|
||||||
|
@">([^\\s<]*?)<\\/span>"
|
||||||
|
];
|
||||||
|
case 703: return @[ // description
|
||||||
|
@"<dd[^>]*>(.*?)<\\/dd>",
|
||||||
|
];
|
||||||
|
case 704: return @[ // date matcher
|
||||||
|
@"(\\d{2}.\\d{2}.\\d{4})",
|
||||||
|
];
|
||||||
|
case 705: return @[ // date format
|
||||||
|
@"dd.MM.yyyy",
|
||||||
|
@"dd. MMM yyyy",
|
||||||
|
@"yyyy-MM-dd'T'HH:mm:ssZZZZZ",
|
||||||
|
];
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return @[];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didClickExamplesButton:(NSButton*)sender {
|
||||||
|
NSString *examples = [[self examplesFor:sender.tag] componentsJoinedByString:@"\n"];
|
||||||
|
|
||||||
|
// TODO: clickable entries
|
||||||
|
self.popoverText.stringValue = [NSString stringWithFormat:@"%@", examples];
|
||||||
|
|
||||||
|
NSSize newSize = self.popoverText.fittingSize; // width is limited by the textfield's preferred width
|
||||||
|
newSize.width += 2 * self.popoverText.frame.origin.x; // the padding
|
||||||
|
newSize.height += 2 * self.popoverText.frame.origin.y;
|
||||||
|
|
||||||
|
// apply fitting size and display
|
||||||
|
self.infoPopover.contentSize = newSize;
|
||||||
|
[self.infoPopover showRelativeToRect:NSZeroRect ofView:sender preferredEdge:NSRectEdgeMinY];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
30
baRSS/Regex Editor/RegexFeed.h
Normal file
30
baRSS/Regex Editor/RegexFeed.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@import Cocoa;
|
||||||
|
@class RegexConverter;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface RegexFeedEntry : NSObject
|
||||||
|
@property (nullable, readonly) NSString *href;
|
||||||
|
@property (nullable, readonly) NSString *title;
|
||||||
|
@property (nullable, readonly) NSString *desc;
|
||||||
|
@property (nullable, readonly) NSString *dateString;
|
||||||
|
@property (nullable, readonly) NSDate *date;
|
||||||
|
|
||||||
|
@property (nullable, readonly) NSString *rawMatch;
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
@interface RegexFeed : NSObject
|
||||||
|
@property (nullable, copy) NSString *rxEntry;
|
||||||
|
@property (nullable, copy) NSString *rxHref;
|
||||||
|
@property (nullable, copy) NSString *rxTitle;
|
||||||
|
@property (nullable, copy) NSString *rxDesc;
|
||||||
|
@property (nullable, copy) NSString *rxDate;
|
||||||
|
@property (nullable, copy) NSString *dateFormat;
|
||||||
|
|
||||||
|
+ (RegexFeed *)from:(RegexConverter*)regex;
|
||||||
|
|
||||||
|
- (NSArray<RegexFeedEntry*>*)process:(NSString*)rawData error:(NSError * __autoreleasing *)err;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
86
baRSS/Regex Editor/RegexFeed.m
Normal file
86
baRSS/Regex Editor/RegexFeed.m
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#import "RegexFeed.h"
|
||||||
|
#import "RegexConverter+Ext.h"
|
||||||
|
|
||||||
|
@interface RegexFeedEntry()
|
||||||
|
@property (nullable, copy) NSString *href;
|
||||||
|
@property (nullable, copy) NSString *title;
|
||||||
|
@property (nullable, copy) NSString *desc;
|
||||||
|
@property (nullable, copy) NSString *dateString;
|
||||||
|
@property (nullable, retain) NSDate *date;
|
||||||
|
|
||||||
|
@property (nullable, copy) NSString *rawMatch;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RegexFeedEntry
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
@implementation RegexFeed
|
||||||
|
|
||||||
|
+ (RegexFeed *)from:(RegexConverter*)regex {
|
||||||
|
RegexFeed *x = [RegexFeed new];
|
||||||
|
x.rxEntry = regex.entry;
|
||||||
|
x.rxHref = regex.href;
|
||||||
|
x.rxTitle = regex.title;
|
||||||
|
x.rxDesc = regex.desc;
|
||||||
|
x.rxDate = regex.date;
|
||||||
|
x.dateFormat = regex.dateFormat;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<RegexFeedEntry*>*)process:(NSString*)rawData error:(NSError * __autoreleasing *)err {
|
||||||
|
NSRegularExpression *re_entries = [self regex:_rxEntry error:err];
|
||||||
|
if (!re_entries) {
|
||||||
|
return @[];
|
||||||
|
}
|
||||||
|
NSDateFormatter *dateFormatter = [NSDateFormatter new];
|
||||||
|
[dateFormatter setDateFormat:_dateFormat];
|
||||||
|
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
|
||||||
|
// TODO: we probably need to handle locale. Especially for "d. MMM" like "3. Dec"
|
||||||
|
|
||||||
|
NSMutableArray<RegexFeedEntry*> *rv = [NSMutableArray new];
|
||||||
|
NSRegularExpression *re4 = [self regex:_rxDate error:err];
|
||||||
|
NSRegularExpression *re3 = [self regex:_rxDesc error:err];
|
||||||
|
NSRegularExpression *re2 = [self regex:_rxTitle error:err];
|
||||||
|
NSRegularExpression *re1 = [self regex:_rxHref error:err];
|
||||||
|
NSArray<NSTextCheckingResult*> *matches = [re_entries matchesInString:rawData options:0 range:NSMakeRange(0, rawData.length)];
|
||||||
|
|
||||||
|
for (NSTextCheckingResult *match in matches) {
|
||||||
|
NSString *subdata = [rawData substringWithRange:match.range];
|
||||||
|
RegexFeedEntry *entry = [[RegexFeedEntry alloc] init];
|
||||||
|
entry.rawMatch = subdata;
|
||||||
|
entry.href = [self firstMatch:subdata re:re1];
|
||||||
|
entry.title = [self firstMatch:subdata re:re2];
|
||||||
|
entry.desc = [self firstMatch:subdata re:re3];
|
||||||
|
entry.dateString = [self firstMatch:subdata re:re4];
|
||||||
|
entry.date = (_dateFormat.length && entry.dateString.length) ? [dateFormatter dateFromString:entry.dateString] : nil;
|
||||||
|
[rv addObject:entry];
|
||||||
|
};
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nullable NSRegularExpression*)regex:(NSString*)pattern error:(NSError * __autoreleasing *)err {
|
||||||
|
if (pattern.length == 0) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSRegularExpression *re = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionDotMatchesLineSeparators error:err];
|
||||||
|
if (*err) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
return re;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nonnull NSString*)firstMatch:(NSString*)str re:(NSRegularExpression*)re {
|
||||||
|
NSTextCheckingResult *match = [[re matchesInString:str options:0 range:NSMakeRange(0, str.length)] firstObject];
|
||||||
|
if (match) {
|
||||||
|
if (match.numberOfRanges < 2) {
|
||||||
|
return NSLocalizedString(@"Regex error: Missing match-group? ('outer(.*?)text')", nil);
|
||||||
|
}else if (match.numberOfRanges > 2) {
|
||||||
|
return NSLocalizedString(@"Regex error: Multiple match-groups found", nil);
|
||||||
|
}
|
||||||
|
return [str substringWithRange:[match rangeAtIndex:1]];
|
||||||
|
}
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
Reference in New Issue
Block a user