diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index a25ebe0..0000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-github: [RobThree]
-custom: ["https://paypal.me/robiii"]
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 0000000..b9a6ec9
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,35 @@
+# This workflow will build a .NET project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
+
+name: .NET Build and test
+
+on:
+ push:
+ branches: [ "trunk" ]
+ pull_request:
+ branches: [ "trunk" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: Firenza/secrets-to-env@v1.3.0
+ name: Set secrets as environment variables
+ with:
+ secrets: ${{ toJSON(secrets) }}
+ secret_filter_regex: ENV_*
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+ - name: Restore dependencies
+ run: dotnet restore src
+ - name: Build
+ run: dotnet build src --no-restore
+ - name: Test
+ run: dotnet test src --no-build --verbosity normal
+ - name: Publish
+ run: dotnet nuget push **/*.nupkg --api-key ${{ secrets.PACKAGE_N2 }} --source https://api.nuget.org/v3/index.json
diff --git a/Help/SimpleFeedReaderHelp.shfbproj b/Help/SimpleFeedReaderHelp.shfbproj
deleted file mode 100644
index f7f0a78..0000000
--- a/Help/SimpleFeedReaderHelp.shfbproj
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
-
- Debug
- AnyCPU
- 2.0
- {d6a78785-3434-4544-a495-b201afbd0964}
- 2017.9.26.0
-
- Documentation
- Documentation
- Documentation
-
- .\Output\
- Documentation
- en-US
- http://robiii.nl
- %28C%29 2014-2018 Devcorner.nl
- rob%40devcorner.nl
- Rob Janssen
- SimpleFeedReader documentation
- True
- SimpleFeedReader documentation
- False
- False
-
-
-
-
- <%3bpara>%3bThe SimpleFeedReader library is used for retrieving syndication feeds in a quick and easy manner abstracting details and normalization of items even further than the .Net'%3bs System.Services.Syndication does.<%3b/para>%3b
- 1.0.8.0
- 2
- False
- Standard
- Blank
- VS2013
- Guid
- AboveNamespaces
-
- <para>Contains classes and interfaces for the SimpleFeedReader library.</para>
-
- ms.vsipcc+, ms.vsexpresscc+
- Hierarchical
- Msdn
- True
- HtmlHelp1
- Cross-platform (.NET Core/.NET Standard)
- True
- True
- False
- False
- OnlyWarningsAndErrors
- 100
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- SimpleFeedReader
- {b057867d-0d85-4eec-87c2-fba8020ed31d}
- True
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index e788348..9086d29 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# ![Logo](https://raw.githubusercontent.com/RobThree/SimpleFeedReader/master/Gfx/icon.png) SimpleFeedReader
+# ![Logo](https://raw.githubusercontent.com/gjkaal/SimpleFeedReader/refs/heads/trunk/src/Gfx/icon.png) SimpleFeedReader
-Easy to use, simple, Syndication feed reader (Atom / RSS). Available as [Nuget Package](https://www.nuget.org/packages/SimpleFeedReader/).
+Easy to use, simple, Syndication feed reader (Atom / RSS). Available as [Nuget Package](https://www.nuget.org/packages/N2.FeedReader/).
## Usage
@@ -25,14 +25,29 @@ Output:
## Notes
-By default the `FeedReader` suppresses exceptions (since feeds tend to go down occasionally, they contain invalid XML from time-to-time and have all other sorts of problems). However, you can tell the `FeedReader` to throw exceptions simply by setting the `throwOnError` argument of the `FeedReader`'s constructor to true.
+This is a fork of [SimpleFeedReader](https://github.com/RobThree/SimpleFeedReader) by [RobIII](https://github.com/RobThree).
+The original project is no longer maintained.
+In this fork, the code is converted to Net8.0 and .NET Standard 2.0.
-The `FeedReader` also accepts an optional `FeedNormalizer` (needs to implement the `IFeedItemNormalizer` interface). This "normalizer" can transform or otherwise affect the way [`SyndicationItem`](http://msdn.microsoft.com/en-us/library/system.servicemodel.syndication.syndicationitem.aspx)s are transformed into `FeedItem`s. The `FeedItem` is the basic object retrieved from feeds and, for simplicity, contains only a few simple properties and methods. It has `Title`, `Summary`, `Content`, `Uri` , `PublishDate`, `LastUpdatedDate`, `Images` and `Categories` properties and that's about it. The default `DefaultFeedItemNormalizer` strips and decodes any HTML in the `Title`, `Summary`, `Content` to (try to) reliably return plain text only. The `Date` property will be populated with whatever the `SyndicationItem`'s latest date is: the `PublishDate` or `LastUpdatedTime`.
+By default the `FeedReader` suppresses exceptions (since feeds tend to go down occasionally,
+they contain invalid XML from time-to-time and have all other sorts of problems). However,
+you can tell the `FeedReader` to throw exceptions simply by setting the `throwOnError`
+argument of the `FeedReader`'s constructor to true.
-You can implement your own `IFeedItemNormalizer` (see the [UnitTest project](https://github.com/RobThree/SimpleFeedReader/tree/master/SimpleFeedReaderTests) for examples) to handle 'normalization' differently to your desire. The `FeedReader` has some convienience methods like `RetrieveFeeds()` that retrieve more than one feed.
+The `FeedReader` also accepts an optional `FeedNormalizer` (needs to implement the `IFeedItemNormalizer` interface).
+This "normalizer" can transform or otherwise affect the way [`SyndicationItem`](http://msdn.microsoft.com/en-us/library/system.servicemodel.syndication.syndicationitem.aspx)s
+are transformed into `FeedItem`s. The `FeedItem` is the basic object retrieved from feeds and, for simplicity,
+contains only a few simple properties and methods. It has `Title`, `Summary`, `Content`, `Uri` , `PublishDate`, `LastUpdatedDate`, `Images`
+and `Categories` properties and that's about it. The default `DefaultFeedItemNormalizer` strips and decodes any
+HTML in the `Title`, `Summary`, `Content` to (try to) reliably return plain text only. The `Date` property will be
+populated with whatever the `SyndicationItem`'s latest date is: the `PublishDate` or `LastUpdatedTime`.
-Included in the project is a [Sandcastle Helpfile Builder project](https://github.com/RobThree/SimpleFeedReader/tree/master/Help) that creates a helpfile for easy to use documentation.
+You can implement your own `IFeedItemNormalizer` (see the [UnitTest project](https://github.com/RobThree/SimpleFeedReader/tree/master/SimpleFeedReaderTests) for examples)
+to handle 'normalization' differently to your desire. The `FeedReader` has some convienience methods like `RetrieveFeeds()` that retrieve more than one feed.
-The project is aimed at easy, don't-make-me-think, retrieval of syndication feeds' entries. It is by no means intended as full-fledged feedreader. It is, however, easily extensible for your purposes (again, see the [UnitTest project](https://github.com/RobThree/SimpleFeedReader/tree/master/SimpleFeedReaderTests) for examples; the `ExtendedFeedItem` and `ExtendedFeedItemNormalizer` are nice concrete examples of this idea).
+The project is aimed at easy, don't-make-me-think, retrieval of syndication feeds' entries. It is by no means intended as full-fledged feedreader.
+It is, however, easily extensible for your purposes (again, see the [UnitTest project](https://github.com/RobThree/SimpleFeedReader/tree/master/SimpleFeedReaderTests)
+for examples; the `ExtendedFeedItem` and `ExtendedFeedItemNormalizer` are nice concrete examples of this idea).
-[![Build status](https://ci.appveyor.com/api/projects/status/wy2swaddwhukotg8)](https://ci.appveyor.com/project/RobIII/simplefeedreader)
+[![Build status](https://github.com/gjkaal/SimpleFeedReader/actions/workflows/dotnet.yml/badge.svg)](https://github.com/gjkaal/SimpleFeedReader/actions/workflows/dotnet.yml/badge.svg)
+
diff --git a/SimpleFeedReader.sln b/SimpleFeedReader.sln
deleted file mode 100644
index 69192a8..0000000
--- a/SimpleFeedReader.sln
+++ /dev/null
@@ -1,45 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30011.22
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{35CEBD40-E6BE-4671-B270-89C7E72CA980}"
- ProjectSection(SolutionItems) = preProject
- LICENSE = LICENSE
- README.md = README.md
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleFeedReader", "SimpleFeedReader\SimpleFeedReader.csproj", "{B057867D-0D85-4EEC-87C2-FBA8020ED31D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleFeedReaderTests", "SimpleFeedReaderTests\SimpleFeedReaderTests.csproj", "{F89D25D8-DDE6-4533-85BE-EBF105269C31}"
-EndProject
-Project("{7CF6DF6D-3B04-46F8-A40B-537D21BCA0B4}") = "SimpleFeedReaderHelp", "Help\SimpleFeedReaderHelp.shfbproj", "{D6A78785-3434-4544-A495-B201AFBD0964}"
- ProjectSection(ProjectDependencies) = postProject
- {B057867D-0D85-4EEC-87C2-FBA8020ED31D} = {B057867D-0D85-4EEC-87C2-FBA8020ED31D}
- EndProjectSection
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {B057867D-0D85-4EEC-87C2-FBA8020ED31D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B057867D-0D85-4EEC-87C2-FBA8020ED31D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B057867D-0D85-4EEC-87C2-FBA8020ED31D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B057867D-0D85-4EEC-87C2-FBA8020ED31D}.Release|Any CPU.Build.0 = Release|Any CPU
- {F89D25D8-DDE6-4533-85BE-EBF105269C31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F89D25D8-DDE6-4533-85BE-EBF105269C31}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F89D25D8-DDE6-4533-85BE-EBF105269C31}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F89D25D8-DDE6-4533-85BE-EBF105269C31}.Release|Any CPU.Build.0 = Release|Any CPU
- {D6A78785-3434-4544-A495-B201AFBD0964}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D6A78785-3434-4544-A495-B201AFBD0964}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D6A78785-3434-4544-A495-B201AFBD0964}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {F7595307-AFC6-4349-B557-BD6B7927DA66}
- EndGlobalSection
-EndGlobal
diff --git a/SimpleFeedReaderTests/SimpleFeedReaderTests.csproj b/SimpleFeedReaderTests/SimpleFeedReaderTests.csproj
deleted file mode 100644
index 709a06d..0000000
--- a/SimpleFeedReaderTests/SimpleFeedReaderTests.csproj
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
- netcoreapp3.1
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
-
-
-
\ No newline at end of file
diff --git a/Gfx/icon.png b/src/Gfx/icon.png
similarity index 100%
rename from Gfx/icon.png
rename to src/Gfx/icon.png
diff --git a/Gfx/icon.psd b/src/Gfx/icon.psd
similarity index 100%
rename from Gfx/icon.psd
rename to src/Gfx/icon.psd
diff --git a/SimpleFeedReaderTests/FeedReaderTests.cs b/src/N2.RssAtomFeedReader.UnitTests/FeedReaderTests.cs
similarity index 68%
rename from SimpleFeedReaderTests/FeedReaderTests.cs
rename to src/N2.RssAtomFeedReader.UnitTests/FeedReaderTests.cs
index e4a9fcc..56a892e 100644
--- a/SimpleFeedReaderTests/FeedReaderTests.cs
+++ b/src/N2.RssAtomFeedReader.UnitTests/FeedReaderTests.cs
@@ -1,281 +1,295 @@
-using System;
-using System.Linq;
-using System.ServiceModel.Syndication;
-using System.IO;
-using System.Net;
-using System.Xml;
-using System.Text.RegularExpressions;
-using System.Globalization;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using SimpleFeedReader;
-
-namespace SimpleFeedReaderTests
-{
- [TestClass]
- public class FeedReaderTests
- {
- [TestMethod]
- public void BasicRSSFeedTest()
- {
- var target = new FeedReader(true);
- var items = target.RetrieveFeed(@"TestFeeds\basic.rss").ToArray();
-
- Assert.AreEqual(2, items.Length);
-
- Assert.AreEqual("http://example.org/foo/bar/1", items[0].Uri.ToString());
- Assert.AreEqual("Title 1", items[0].Title);
- Assert.IsTrue(items[0].Summary.StartsWith("Lorem ipsum dolor sit"));
- Assert.IsNull(items[0].Content);
- Assert.AreEqual("tag:example.org,1999:blog-123456789123456789123456789.post-987564321987654231", items[0].Id);
- Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].PublishDate);
- Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].LastUpdatedDate);
-
- Assert.IsNull(items[1].Title);
- Assert.IsNull(items[1].Summary);
- Assert.IsNull(items[1].Content);
- Assert.IsNull(items[1].Uri);
- Assert.IsNull(items[1].Id);
- Assert.AreEqual(DateTimeOffset.MinValue, items[1].PublishDate);
- Assert.AreEqual(DateTimeOffset.MinValue, items[1].LastUpdatedDate);
-
- Assert.IsTrue(items[0].GetContent().StartsWith("Lorem ipsum dolor sit"));
- Assert.IsTrue(items[0].GetSummary().StartsWith("Lorem ipsum dolor sit"));
-
-#pragma warning disable 0618
- Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].Date);
- Assert.AreEqual(DateTimeOffset.MinValue, items[1].Date);
-#pragma warning restore 0618
- }
-
- [TestMethod]
- public void BasicRSSWithImageFeedTest()
- {
- var target = new FeedReader(true);
- var items = target.RetrieveFeed(@"TestFeeds\basic_image.rss").ToArray();
-
- Assert.AreEqual(2, items.Length);
-
- Assert.AreEqual("http://example.org/foo/bar/1", items[0].Uri.ToString());
- Assert.AreEqual("Title 1", items[0].Title);
- Assert.IsTrue(items[0].Summary.StartsWith("Lorem ipsum dolor sit"));
- Assert.IsNull(items[0].Content);
- Assert.AreEqual("tag:example.org,1999:blog-123456789123456789123456789.post-987564321987654231", items[0].Id);
- Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].PublishDate);
- Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].LastUpdatedDate);
-
- Assert.IsNull(items[1].Title);
- Assert.IsNull(items[1].Summary);
- Assert.IsNull(items[1].Content);
- Assert.IsNull(items[1].Uri);
- Assert.IsNull(items[1].Id);
- Assert.AreEqual(DateTimeOffset.MinValue, items[1].PublishDate);
- Assert.AreEqual(DateTimeOffset.MinValue, items[1].LastUpdatedDate);
-
- Assert.IsTrue(items[0].GetContent().StartsWith("Lorem ipsum dolor sit"));
- Assert.IsTrue(items[0].GetSummary().StartsWith("Lorem ipsum dolor sit"));
-
-#pragma warning disable 0618
- Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].Date);
- Assert.AreEqual(DateTimeOffset.MinValue, items[1].Date);
-#pragma warning restore 0618
-
- Assert.IsTrue(items[0].Images.Count() == 2);
- Assert.AreEqual(items[0].Images.ElementAt(0).ToString(), "http://example.org/foo/bar/123abc.png");
- Assert.AreEqual(items[0].Images.ElementAt(1).ToString(), "http://example.org/foo/bar/123abc_2.png");
- }
-
- [TestMethod]
- [ExpectedException(typeof(FileNotFoundException))]
- public void ThrowsWhenRequiredFeedTest1()
- {
- var target = new FeedReader(true);
- var items = target.RetrieveFeed(@"TestFeeds\non_existing.rss");
- }
-
- [TestMethod]
- [ExpectedException(typeof(WebException))]
- public void ThrowsWhenRequiredFeedTest2()
- {
- var target = new FeedReader(true);
- var items = target.RetrieveFeed(@"http://example.org/non_existing.rss");
- }
-
- [TestMethod]
- public void SuppressesExceptionsWhenRequiredFeedTest()
- {
- var target = new FeedReader(false);
- var items = target.RetrieveFeed(@"TestFeeds\non_existing.rss");
- }
-
- [TestMethod]
- public void DefaultNormalizationTest()
- {
- var target = new FeedReader(true);
- var items = target.RetrieveFeed(@"TestFeeds\decoding.rss").ToArray();
-
- var i0 = items[0];
- Assert.IsNull(i0.Summary);
- Assert.IsNull(i0.Title);
-
- var i1 = items[1];
- Assert.AreEqual("foo&bar", i1.Summary);
- Assert.AreEqual("foo&bar", i1.Title);
-
- var i2 = items[2];
- Assert.AreEqual(i2.Title, i2.Content);
- }
-
- [TestMethod]
- public void ExtendedNormalizerRSSFeedTest()
- {
- var target = new FeedReader(new ExtendedFeedItemNormalizer(), true);
- var items = target.RetrieveFeed(@"TestFeeds\basic.rss").ToArray();
-
- Assert.IsInstanceOfType(items[0], typeof(ExtendedFeedItem));
- Assert.AreEqual(1, ((ExtendedFeedItem)items[0]).Authors.Length);
- Assert.AreEqual("noreply1@example.org (John Doe 1)", ((ExtendedFeedItem)items[0]).Authors[0]);
- }
-
- [TestMethod]
- [ExpectedException(typeof(XmlException))]
- public void DTDInjectionTest1()
- {
- var target = new FeedReader(true); //We want to check the exception so don't suppress it
- var items = target.RetrieveFeed(@"TestFeeds\xml_injection1.rss");
- }
-
- [TestMethod]
- [ExpectedException(typeof(XmlException))]
- public void DTDInjectionTest2()
- {
- var target = new FeedReader(true); //We want to check the exception so don't suppress it
- var items = target.RetrieveFeed(@"TestFeeds\xml_injection2.rss");
- }
-
- [TestMethod]
- public void BasicAtomFeedTest()
- {
- var target = new FeedReader(true);
- var items = target.RetrieveFeed(@"TestFeeds\basic.atom").ToArray();
-
- Assert.AreEqual(2, items.Length);
-
- Assert.AreEqual("http://example.org/foo/bar/1", items[0].Uri.ToString());
- Assert.AreEqual("Summary1", items[0].Summary);
- Assert.AreEqual("Test1", items[0].Title);
- Assert.AreEqual("HTML content", items[0].Content);
- Assert.AreEqual("urn:uuid:0ea6c57b-4546-4264-8b96-13434c349d87", items[0].Id);
-
- Assert.AreEqual(DateTimeOffset.Parse("2013-03-13T13:37:31.0000000+00:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].PublishDate);
- Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+00:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].LastUpdatedDate);
-
- Assert.AreEqual("http://example.org/foo/bar/2", items[1].Uri.ToString());
- Assert.AreEqual("Summary2", items[1].Summary);
- Assert.AreEqual("Test2", items[1].Title);
- Assert.AreEqual("Text content", items[1].Content);
- Assert.AreEqual("urn:uuid:d58672c4-f62e-483e-ab00-ff0940113e29", items[1].Id);
- Assert.AreEqual(DateTimeOffset.MinValue, items[1].PublishDate);
- Assert.AreEqual(DateTimeOffset.MinValue, items[1].LastUpdatedDate);
-
-#pragma warning disable 0618
- Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+00:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].Date);
- Assert.AreEqual(DateTimeOffset.MinValue, items[1].Date);
-#pragma warning restore 0618
- }
-
- [TestMethod]
- public void BasicActualRSSFeedTest()
- {
- var target = new FeedReader(new GoogleFeedItemNormalizer(), true);
- var items = target.RetrieveFeed(@"TestFeeds\google_snapshot.rss").ToArray();
-
- Assert.AreEqual(10, items.Length);
- Assert.IsTrue(items[0].GetContent().StartsWith("(CNN) -- Rescue boats"));
- Assert.IsTrue(items[1].GetContent().StartsWith("Pro-Russian troops guard"));
- Assert.IsTrue(items[2].GetContent().StartsWith("(CNN) -- Former New York"));
- Assert.IsTrue(items[3].GetContent().StartsWith("Two blasts near the"));
- Assert.IsTrue(items[4].GetContent().StartsWith("A three-year-old boy"));
- }
-
- [TestMethod]
- public void BasicActualAtomFeedTest()
- {
- var target = new FeedReader(new GoogleFeedItemNormalizer(), true);
- var items = target.RetrieveFeed(@"TestFeeds\google_snapshot.atom").ToArray();
-
- Assert.AreEqual(10, items.Length);
- Assert.IsTrue(items[0].GetContent().StartsWith("(CNN) -- Rescue boats"));
- Assert.IsTrue(items[1].GetContent().StartsWith("Pro-Russian troops guard"));
- Assert.IsTrue(items[2].GetContent().StartsWith("(CNN) -- Former New York"));
- Assert.IsTrue(items[3].GetContent().StartsWith("Two blasts near the"));
- Assert.IsTrue(items[4].GetContent().StartsWith("A three-year-old boy"));
- }
-
- [TestMethod]
- public void BasicRSSCategoriesTest()
- {
- var target = new FeedReader();
- var items = target.RetrieveFeed(@"TestFeeds\categories.rss").ToArray();
- Assert.AreEqual(items[0].Categories.ElementAt(0), "NEWS");
- Assert.AreEqual(items[0].Categories.ElementAt(1), "TEST");
- }
-
- [TestMethod]
- public void BasicAtomCategoriesTest()
- {
- var target = new FeedReader();
- var items = target.RetrieveFeed(@"TestFeeds\categories.atom").ToArray();
- Assert.AreEqual(items[0].Categories.ElementAt(0), "a");
- Assert.AreEqual(items[1].Categories.ElementAt(0), "b");
- Assert.AreEqual(items[1].Categories.ElementAt(1), "c");
- }
-
- #region TestClasses
- private class ExtendedFeedItem : FeedItem
- {
- public string[] Authors { get; set; }
-
- public ExtendedFeedItem() { }
- public ExtendedFeedItem(FeedItem item)
- : base(item) { }
- }
-
- private class ExtendedFeedItemNormalizer : DefaultFeedItemNormalizer, IFeedItemNormalizer
- {
- public override FeedItem Normalize(SyndicationFeed feed, SyndicationItem item)
- {
- return new ExtendedFeedItem(base.Normalize(feed, item))
- {
- Authors = item.Authors.Select(i => i.Name ?? i.Email).ToArray()
- };
- }
- }
-
- ///
- /// Simple sample class (not to be taken TOO seriously) to demonstrate extracting "interesting" stuff from a bunch of HTML crap in the feed
- ///
- private class GoogleFeedItemNormalizer : DefaultFeedItemNormalizer, IFeedItemNormalizer
- {
- ///
- /// Some people, when confronted with a problem, think “I know, I'll use regular expressions.”
- /// Now they have two problems. — Jamie Zawinski
- ///
- private static Regex _getlines = new Regex("(.*?)", RegexOptions.Compiled);
-
- public override FeedItem Normalize(SyndicationFeed feed, SyndicationItem item)
- {
- if (item.Content != null)
- item.Content = new TextSyndicationContent(GetLines(((TextSyndicationContent)item.Content).Text)[2]);
- if (item.Summary != null)
- item.Summary = new TextSyndicationContent(GetLines(item.Summary.Text)[2]);
- return base.Normalize(feed, item);
- }
-
- private static string[] GetLines(string value)
- {
- return _getlines.Matches(value).Cast().ToArray().Select(m => m.Value).ToArray();
- }
- }
- #endregion
- }
-}
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using N2.RssAtomFeedReader;
+using System.Globalization;
+using System.ServiceModel.Syndication;
+using System.Text.RegularExpressions;
+using System.Xml;
+
+namespace SimpleFeedReaderTests
+{
+ [TestClass]
+ public partial class FeedReaderTests
+ {
+ [TestMethod]
+ public void BasicRSSFeedTest()
+ {
+ var target = new FeedReader(true);
+ var items = target.RetrieveFeed(@"TestFeeds/basic.rss").ToArray();
+
+ Assert.AreEqual(2, items.Length);
+
+ Assert.AreEqual("http://example.org/foo/bar/1", items[0].Uri.ToString());
+ Assert.AreEqual("Title 1", items[0].Title);
+ Assert.IsTrue(items[0].Summary!.StartsWith("Lorem ipsum dolor sit"));
+ Assert.IsNull(items[0].Content);
+ Assert.AreEqual("tag:example.org,1999:blog-123456789123456789123456789.post-987564321987654231", items[0].Id);
+ Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].PublishDate);
+ Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].LastUpdatedDate);
+
+ Assert.IsNull(items[1].Title);
+ Assert.IsNull(items[1].Summary);
+ Assert.IsNull(items[1].Content);
+ Assert.IsNull(items[1].Uri);
+ Assert.IsNull(items[1].Id);
+ Assert.AreEqual(DateTimeOffset.MinValue, items[1].PublishDate);
+ Assert.AreEqual(DateTimeOffset.MinValue, items[1].LastUpdatedDate);
+
+ Assert.IsTrue(items[0].GetContent()!.StartsWith("Lorem ipsum dolor sit"));
+ Assert.IsTrue(items[0].GetSummary()!.StartsWith("Lorem ipsum dolor sit"));
+ }
+
+ [TestMethod]
+ public void BasicRSSWithImageFeedTest()
+ {
+ var target = new FeedReader(true);
+ var items = target.RetrieveFeed(@"TestFeeds/basic_image.rss").ToArray();
+
+ Assert.AreEqual(2, items.Length);
+
+ Assert.AreEqual("http://example.org/foo/bar/1", items[0].Uri.ToString());
+ Assert.AreEqual("Title 1", items[0].Title);
+ Assert.IsTrue(items[0].Summary!.StartsWith("Lorem ipsum dolor sit"));
+ Assert.IsNull(items[0].Content);
+ Assert.AreEqual("tag:example.org,1999:blog-123456789123456789123456789.post-987564321987654231", items[0].Id);
+ Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].PublishDate);
+ Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+02:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].LastUpdatedDate);
+
+ Assert.IsNull(items[1].Title);
+ Assert.IsNull(items[1].Summary);
+ Assert.IsNull(items[1].Content);
+ Assert.IsNull(items[1].Uri);
+ Assert.IsNull(items[1].Id);
+ Assert.AreEqual(DateTimeOffset.MinValue, items[1].PublishDate);
+ Assert.AreEqual(DateTimeOffset.MinValue, items[1].LastUpdatedDate);
+
+ Assert.IsTrue(items[0].GetContent()!.StartsWith("Lorem ipsum dolor sit"));
+ Assert.IsTrue(items[0].GetSummary()!.StartsWith("Lorem ipsum dolor sit"));
+
+ Assert.IsTrue(items[0].Images.Count() == 2);
+ Assert.AreEqual(items[0].Images.ElementAt(0).ToString(), "http://example.org/foo/bar/123abc.png");
+ Assert.AreEqual(items[0].Images.ElementAt(1).ToString(), "http://example.org/foo/bar/123abc_2.png");
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(FileNotFoundException))]
+ public void ThrowsWhenRequiredFeedTest1()
+ {
+ var target = new FeedReader(true);
+ _ = target.RetrieveFeed(@"TestFeeds/non_existing.rss");
+ }
+
+ //https://feeds.nos.nl/nosnieuwsalgemeen
+ [TestMethod]
+ public void OpenActualFeedTest1()
+ {
+ var target = new FeedReader(true);
+ var items = target.RetrieveFeed(@"https://feeds.nos.nl/nosnieuwsalgemeen");
+ Assert.IsTrue(items.Any());
+ foreach (var item in items)
+ {
+ Assert.IsNotNull(item.Title);
+ Assert.IsNotNull(item.GetContent());
+ Assert.IsNotNull(item.GetSummary());
+ Assert.IsNotNull(item.Uri);
+ Assert.IsNotNull(item.Id);
+ Assert.AreNotEqual(DateTimeOffset.MinValue, item.PublishDate);
+ Assert.AreNotEqual(DateTimeOffset.MinValue, item.LastUpdatedDate);
+ Console.WriteLine(item.Title);
+ Console.WriteLine(item.GetContent());
+ }
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(HttpRequestException))]
+ public void ThrowsWhenRequiredFeedTest2()
+ {
+ var target = new FeedReader(true);
+ _ = target.RetrieveFeed(@"http://example.org/non_existing.rss");
+ }
+
+ [TestMethod]
+ public void SuppressesExceptionsWhenRequiredFeedTest()
+ {
+ var target = new FeedReader(false);
+ _ = target.RetrieveFeed(@"TestFeeds/non_existing.rss");
+ }
+
+ [TestMethod]
+ public void DefaultNormalizationTest()
+ {
+ var target = new FeedReader(true);
+ var items = target.RetrieveFeed(@"TestFeeds/decoding.rss").ToArray();
+
+ var i0 = items[0];
+ Assert.IsNull(i0.Summary);
+ Assert.IsNull(i0.Title);
+
+ var i1 = items[1];
+ Assert.AreEqual("foo&bar", i1.Summary);
+ Assert.AreEqual("foo&bar", i1.Title);
+
+ var i2 = items[2];
+ Assert.AreEqual(i2.Title, i2.Content);
+ }
+
+ [TestMethod]
+ public void ExtendedNormalizerRSSFeedTest()
+ {
+ var target = new FeedReader(new ExtendedFeedItemNormalizer(), true);
+ var items = target.RetrieveFeed(@"TestFeeds/basic.rss").ToArray();
+
+ Assert.IsInstanceOfType(items[0], typeof(ExtendedFeedItem));
+ Assert.AreEqual(1, ((ExtendedFeedItem)items[0]).Authors.Length);
+ Assert.AreEqual("noreply1@example.org (John Doe 1)", ((ExtendedFeedItem)items[0]).Authors[0]);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(XmlException))]
+ public void DTDInjectionTest1()
+ {
+ var target = new FeedReader(true); //We want to check the exception so don't suppress it
+ _ = target.RetrieveFeed(@"TestFeeds/xml_injection1.rss");
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(XmlException))]
+ public void DTDInjectionTest2()
+ {
+ var target = new FeedReader(true); //We want to check the exception so don't suppress it
+ _ = target.RetrieveFeed(@"TestFeeds/xml_injection2.rss");
+ }
+
+ [TestMethod]
+ public void BasicAtomFeedTest()
+ {
+ var target = new FeedReader(true);
+ var items = target.RetrieveFeed(@"TestFeeds/basic.atom").ToArray();
+
+ Assert.AreEqual(2, items.Length);
+
+ Assert.AreEqual("http://example.org/foo/bar/1", items[0].Uri.ToString());
+ Assert.AreEqual("Summary1", items[0].Summary);
+ Assert.AreEqual("Test1", items[0].Title);
+ Assert.AreEqual("HTML content", items[0].Content);
+ Assert.AreEqual("urn:uuid:0ea6c57b-4546-4264-8b96-13434c349d87", items[0].Id);
+
+ Assert.AreEqual(DateTimeOffset.Parse("2013-03-13T13:37:31.0000000+00:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].PublishDate);
+ Assert.AreEqual(DateTimeOffset.Parse("2014-04-16T13:57:35.0000000+00:00", CultureInfo.InvariantCulture, DateTimeStyles.None), items[0].LastUpdatedDate);
+
+ Assert.AreEqual("http://example.org/foo/bar/2", items[1].Uri.ToString());
+ Assert.AreEqual("Summary2", items[1].Summary);
+ Assert.AreEqual("Test2", items[1].Title);
+ Assert.AreEqual("Text content", items[1].Content);
+ Assert.AreEqual("urn:uuid:d58672c4-f62e-483e-ab00-ff0940113e29", items[1].Id);
+ Assert.AreEqual(DateTimeOffset.MinValue, items[1].PublishDate);
+ Assert.AreEqual(DateTimeOffset.MinValue, items[1].LastUpdatedDate);
+ }
+
+ [TestMethod]
+ public void BasicActualRSSFeedTest()
+ {
+ var target = new FeedReader(new GoogleFeedItemNormalizer(), true);
+ var items = target.RetrieveFeed(@"TestFeeds/google_snapshot.rss").ToArray();
+
+ Assert.AreEqual(10, items.Length);
+ Assert.IsTrue(items[0].GetContent()!.StartsWith("(CNN) -- Rescue boats"));
+ Assert.IsTrue(items[1].GetContent()!.StartsWith("Pro-Russian troops guard"));
+ Assert.IsTrue(items[2].GetContent()!.StartsWith("(CNN) -- Former New York"));
+ Assert.IsTrue(items[3].GetContent()!.StartsWith("Two blasts near the"));
+ Assert.IsTrue(items[4].GetContent()!.StartsWith("A three-year-old boy"));
+ }
+
+ [TestMethod]
+ public void BasicActualAtomFeedTest()
+ {
+ var target = new FeedReader(new GoogleFeedItemNormalizer(), true);
+ var items = target.RetrieveFeed(@"TestFeeds/google_snapshot.atom").ToArray();
+
+ Assert.AreEqual(10, items.Length);
+ Assert.IsTrue(items[0].GetContent()!.StartsWith("(CNN) -- Rescue boats"));
+ Assert.IsTrue(items[1].GetContent()!.StartsWith("Pro-Russian troops guard"));
+ Assert.IsTrue(items[2].GetContent()!.StartsWith("(CNN) -- Former New York"));
+ Assert.IsTrue(items[3].GetContent()!.StartsWith("Two blasts near the"));
+ Assert.IsTrue(items[4].GetContent()!.StartsWith("A three-year-old boy"));
+ }
+
+ [TestMethod]
+ public void BasicRSSCategoriesTest()
+ {
+ var target = new FeedReader();
+ var items = target.RetrieveFeed(@"TestFeeds/categories.rss").ToArray();
+ Assert.AreEqual(items[0].Categories.ElementAt(0), "NEWS");
+ Assert.AreEqual(items[0].Categories.ElementAt(1), "TEST");
+ }
+
+ [TestMethod]
+ public void BasicAtomCategoriesTest()
+ {
+ var target = new FeedReader();
+ var items = target.RetrieveFeed(@"TestFeeds/categories.atom").ToArray();
+ Assert.AreEqual(items[0].Categories.ElementAt(0), "a");
+ Assert.AreEqual(items[1].Categories.ElementAt(0), "b");
+ Assert.AreEqual(items[1].Categories.ElementAt(1), "c");
+ }
+
+ #region TestClasses
+
+ private class ExtendedFeedItem : FeedItem
+ {
+ public string[] Authors { get; set; }
+
+ public ExtendedFeedItem()
+ {
+ Authors = [];
+ }
+
+ public ExtendedFeedItem(FeedItem item)
+ : base(item)
+ {
+ Authors = [];
+ }
+ }
+
+ private class ExtendedFeedItemNormalizer : DefaultFeedItemNormalizer, IFeedItemNormalizer
+ {
+ public override FeedItem Normalize(SyndicationFeed feed, SyndicationItem item)
+ {
+ return new ExtendedFeedItem(base.Normalize(feed, item))
+ {
+ Authors = item.Authors.Select(i => i.Name ?? i.Email).ToArray()
+ };
+ }
+ }
+
+ ///
+ /// Simple sample class (not to be taken TOO seriously) to demonstrate extracting "interesting" stuff from a bunch of HTML crap in the feed
+ ///
+ private partial class GoogleFeedItemNormalizer : DefaultFeedItemNormalizer, IFeedItemNormalizer
+ {
+ ///
+ /// Some people, when confronted with a problem, think “I know, I'll use regular expressions.”
+ /// Now they have two problems. — Jamie Zawinski
+ ///
+ private static readonly Regex getlines = RegExGetRssLines();
+
+ public override FeedItem Normalize(SyndicationFeed feed, SyndicationItem item)
+ {
+ if (item.Content != null)
+ item.Content = new TextSyndicationContent(GetLines(((TextSyndicationContent)item.Content).Text)[2]);
+ if (item.Summary != null)
+ item.Summary = new TextSyndicationContent(GetLines(item.Summary.Text)[2]);
+ return base.Normalize(feed, item);
+ }
+
+ private static string[] GetLines(string value)
+ {
+ return getlines.Matches(value).Cast().ToArray().Select(m => m.Value).ToArray();
+ }
+
+ [GeneratedRegex("(.*?)", RegexOptions.Compiled)]
+ private static partial Regex RegExGetRssLines();
+ }
+
+ #endregion TestClasses
+ }
+}
\ No newline at end of file
diff --git a/src/N2.RssAtomFeedReader.UnitTests/N2.RssAtomFeedReader.UnitTests.csproj b/src/N2.RssAtomFeedReader.UnitTests/N2.RssAtomFeedReader.UnitTests.csproj
new file mode 100644
index 0000000..d35e105
--- /dev/null
+++ b/src/N2.RssAtomFeedReader.UnitTests/N2.RssAtomFeedReader.UnitTests.csproj
@@ -0,0 +1,71 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SimpleFeedReaderTests/TestFeeds/basic.atom b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/basic.atom
similarity index 96%
rename from SimpleFeedReaderTests/TestFeeds/basic.atom
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/basic.atom
index 3173e38..7cce3d8 100644
--- a/SimpleFeedReaderTests/TestFeeds/basic.atom
+++ b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/basic.atom
@@ -1,29 +1,29 @@
-
-
- Example Feed
-
- 2014-04-16T13:57:35Z
-
- John Doe
-
- urn:uuid:abf81fc9-76db-4e46-a83d-e05ec7f45ccb
-
-
- Test1
-
- urn:uuid:0ea6c57b-4546-4264-8b96-13434c349d87
- 2013-03-13T13:37:31Z
- 2014-04-16T13:57:35Z
- Summary1
-
- <p>HTML content</p>
-
-
-
- Test2
-
- urn:uuid:d58672c4-f62e-483e-ab00-ff0940113e29
- Summary2
- Text content
-
+
+
+ Example Feed
+
+ 2014-04-16T13:57:35Z
+
+ John Doe
+
+ urn:uuid:abf81fc9-76db-4e46-a83d-e05ec7f45ccb
+
+
+ Test1
+
+ urn:uuid:0ea6c57b-4546-4264-8b96-13434c349d87
+ 2013-03-13T13:37:31Z
+ 2014-04-16T13:57:35Z
+ Summary1
+
+ <p>HTML content</p>
+
+
+
+ Test2
+
+ urn:uuid:d58672c4-f62e-483e-ab00-ff0940113e29
+ Summary2
+ Text content
+
\ No newline at end of file
diff --git a/SimpleFeedReaderTests/TestFeeds/basic.rss b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/basic.rss
similarity index 97%
rename from SimpleFeedReaderTests/TestFeeds/basic.rss
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/basic.rss
index fdb913a..53e151a 100644
--- a/SimpleFeedReaderTests/TestFeeds/basic.rss
+++ b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/basic.rss
@@ -1,63 +1,63 @@
-
-
-
-
-
- Test RSS feed
- http://example.org/
-
- en
- noreply@example.org (John Doe)
- Wed, 16 Apr 2014 13:57:35 +0200
- FooBar http://www.example.org
-
- 10
-
-
- 1
-
-
- 25
-
-
- -
- Title 1
-
- http://example.org/foo/bar/1
-
- noreply1@example.org (John Doe 1)
- Wed, 16 Apr 2014 13:57:35 +0200
-
- tag:example.org,1999:blog-123456789123456789123456789.post-987564321987654231
-
-
- <a onblur="try {parent.foo.bar();} catch(e)
- {}"
- href="http://example.org/foo/bar/xyz.png"><img
- style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width:
- 232px; height: 108px;"
- src="http://example.org/foo/bar/xyz.png"
- alt="" id="BLOGGER_PHOTO_ID_987564321987654231fedcba" border="0"
- /></a><br />Lorem ipsum dolor sit amet.<br />
- <br />Nunc lacinia facilisis rutrum. Morbi vulputate faucibus mi
- <a href="http://www.example.org/foo/bar/abcdef.html?id=123&x=abc"
- >link</a> quis pellentesque. Vivamus vel mi eget est aliquet.
-
- <br /><br />
-
-
-
- 0
-
-
-
-
-
+
+
+
+
+
+ Test RSS feed
+ http://example.org/
+
+ en
+ noreply@example.org (John Doe)
+ Wed, 16 Apr 2014 13:57:35 +0200
+ FooBar http://www.example.org
+
+ 10
+
+
+ 1
+
+
+ 25
+
+
+ -
+ Title 1
+
+ http://example.org/foo/bar/1
+
+ noreply1@example.org (John Doe 1)
+ Wed, 16 Apr 2014 13:57:35 +0200
+
+ tag:example.org,1999:blog-123456789123456789123456789.post-987564321987654231
+
+
+ <a onblur="try {parent.foo.bar();} catch(e)
+ {}"
+ href="http://example.org/foo/bar/xyz.png"><img
+ style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width:
+ 232px; height: 108px;"
+ src="http://example.org/foo/bar/xyz.png"
+ alt="" id="BLOGGER_PHOTO_ID_987564321987654231fedcba" border="0"
+ /></a><br />Lorem ipsum dolor sit amet.<br />
+ <br />Nunc lacinia facilisis rutrum. Morbi vulputate faucibus mi
+ <a href="http://www.example.org/foo/bar/abcdef.html?id=123&x=abc"
+ >link</a> quis pellentesque. Vivamus vel mi eget est aliquet.
+
+ <br /><br />
+
+
+
+ 0
+
+
+
+
+
\ No newline at end of file
diff --git a/SimpleFeedReaderTests/TestFeeds/basic_image.rss b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/basic_image.rss
similarity index 100%
rename from SimpleFeedReaderTests/TestFeeds/basic_image.rss
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/basic_image.rss
diff --git a/SimpleFeedReaderTests/TestFeeds/categories.atom b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/categories.atom
similarity index 100%
rename from SimpleFeedReaderTests/TestFeeds/categories.atom
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/categories.atom
diff --git a/SimpleFeedReaderTests/TestFeeds/categories.rss b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/categories.rss
similarity index 100%
rename from SimpleFeedReaderTests/TestFeeds/categories.rss
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/categories.rss
diff --git a/SimpleFeedReaderTests/TestFeeds/decoding.rss b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/decoding.rss
similarity index 96%
rename from SimpleFeedReaderTests/TestFeeds/decoding.rss
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/decoding.rss
index c95da11..5fa9f64 100644
--- a/SimpleFeedReaderTests/TestFeeds/decoding.rss
+++ b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/decoding.rss
@@ -1,23 +1,23 @@
-
-
-
- -
- foo&amp;amp;amp;amp;bar
- foo&amp;amp;amp;amp;bar
-
- foo&amp;amp;amp;amp;bar
-
-
- -
- foo&amp;amp;amp;bar
- foo&amp;amp;amp;bar
-
- foo&amp;amp;amp;bar
-
-
- -
- ế
- ế
-
-
+
+
+
+ -
+ foo&amp;amp;amp;amp;bar
+ foo&amp;amp;amp;amp;bar
+
+ foo&amp;amp;amp;amp;bar
+
+
+ -
+ foo&amp;amp;amp;bar
+ foo&amp;amp;amp;bar
+
+ foo&amp;amp;amp;bar
+
+
+ -
+ ế
+ ế
+
+
\ No newline at end of file
diff --git a/SimpleFeedReaderTests/TestFeeds/google_snapshot.atom b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/google_snapshot.atom
similarity index 100%
rename from SimpleFeedReaderTests/TestFeeds/google_snapshot.atom
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/google_snapshot.atom
diff --git a/SimpleFeedReaderTests/TestFeeds/google_snapshot.rss b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/google_snapshot.rss
similarity index 100%
rename from SimpleFeedReaderTests/TestFeeds/google_snapshot.rss
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/google_snapshot.rss
diff --git a/SimpleFeedReaderTests/TestFeeds/xml_injection1.rss b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/xml_injection1.rss
similarity index 95%
rename from SimpleFeedReaderTests/TestFeeds/xml_injection1.rss
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/xml_injection1.rss
index 92f82f4..9d4f92f 100644
--- a/SimpleFeedReaderTests/TestFeeds/xml_injection1.rss
+++ b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/xml_injection1.rss
@@ -1,15 +1,15 @@
-
-
-]>
-
-
- -
- &test;
- &test;
-
- &test;
-
-
-
+
+
+]>
+
+
+ -
+ &test;
+ &test;
+
+ &test;
+
+
+
\ No newline at end of file
diff --git a/SimpleFeedReaderTests/TestFeeds/xml_injection2.rss b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/xml_injection2.rss
similarity index 95%
rename from SimpleFeedReaderTests/TestFeeds/xml_injection2.rss
rename to src/N2.RssAtomFeedReader.UnitTests/TestFeeds/xml_injection2.rss
index a2c974d..c671d45 100644
--- a/SimpleFeedReaderTests/TestFeeds/xml_injection2.rss
+++ b/src/N2.RssAtomFeedReader.UnitTests/TestFeeds/xml_injection2.rss
@@ -1,15 +1,15 @@
-
-
-]>
-
-
- -
- &test;
- &test;
-
- &test;
-
-
-
+
+
+]>
+
+
+ -
+ &test;
+ &test;
+
+ &test;
+
+
+
\ No newline at end of file
diff --git a/src/N2.RssAtomFeedReader/BuilderConfiguration.cs b/src/N2.RssAtomFeedReader/BuilderConfiguration.cs
new file mode 100644
index 0000000..1806ab6
--- /dev/null
+++ b/src/N2.RssAtomFeedReader/BuilderConfiguration.cs
@@ -0,0 +1,21 @@
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Reflection;
+
+[assembly: AssemblyVersion("8.0.9")]
+
+namespace N2.RssAtomFeedReader;
+
+public static class BuilderConfiguration
+{
+ ///
+ /// Add the feed reader service to your application.
+ ///
+ public static IServiceCollection AddFeedReader(this IServiceCollection services, Func options)
+ {
+ var setup = options?.Invoke() ?? new FeedReaderOptions();
+
+ services.AddSingleton(new FeedReader(setup));
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/SimpleFeedReader/DefaultFeedItemNormalizer.cs b/src/N2.RssAtomFeedReader/DefaultFeedItemNormalizer.cs
similarity index 70%
rename from SimpleFeedReader/DefaultFeedItemNormalizer.cs
rename to src/N2.RssAtomFeedReader/DefaultFeedItemNormalizer.cs
index 34e4b5a..b19d28d 100644
--- a/SimpleFeedReader/DefaultFeedItemNormalizer.cs
+++ b/src/N2.RssAtomFeedReader/DefaultFeedItemNormalizer.cs
@@ -1,114 +1,140 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.ServiceModel.Syndication;
-using System.Text.RegularExpressions;
-using System.Xml.Linq;
-
-namespace SimpleFeedReader
-{
- ///
- /// The normalizes ,
- /// and of s to the point where
- /// they no longer contain any HTML, redundant whitespace, un-normalized unicode chars and other control chars like
- /// tabs, newlines or backspaces. The 's property will contain
- /// whichever date is latest; the or .
- ///
- ///
- /// You can implement a normalizer yourself by implementing the interface.
- ///
- public class DefaultFeedItemNormalizer : IFeedItemNormalizer
- {
- private static Regex _htmlRegex = new Regex(@"<[^>]*>", RegexOptions.Compiled); //@"<(.|\n)*?>"
- private static Regex _controlCodesRegex = new Regex(@"[\x00-\x1F\x7f]", RegexOptions.Compiled);
- private static Regex _whiteSpaceRegex = new Regex(@"\s{2,}", RegexOptions.Compiled);
-
- ///
- /// Normalizes a SyndicationItem into a FeedItem.
- ///
- /// The on which the item was retrieved.
- /// A to normalize into a .
- /// Returns a normalized .
- public virtual FeedItem Normalize(SyndicationFeed feed, SyndicationItem item)
- {
- var alternatelink = item.Links.FirstOrDefault(l => l.RelationshipType == null || l.RelationshipType.Equals("alternate", StringComparison.OrdinalIgnoreCase));
-
- Uri itemuri = null;
- if (alternatelink == null && !Uri.TryCreate(item.Id, UriKind.Absolute, out Uri parsed))
- {
- itemuri = parsed;
- }
- else
- {
- itemuri = alternatelink.GetAbsoluteUri();
- }
-
- return new FeedItem
- {
- Id = string.IsNullOrEmpty(item.Id) ? null : item.Id.Trim(),
- Title = item.Title == null ? null : Normalize(item.Title.Text),
- Content = item.Content == null ? null : Normalize(((TextSyndicationContent)item.Content).Text),
- Summary = item.Summary == null ? null : Normalize(item.Summary.Text),
- PublishDate = item.PublishDate,
- LastUpdatedDate = item.LastUpdatedTime == DateTimeOffset.MinValue ? item.PublishDate : item.LastUpdatedTime,
- Uri = itemuri,
- Images = GetFeedItemImages(item),
- Categories = item.Categories.Select(c => c.Name)
- };
- }
-
- private static IEnumerable GetFeedItemImages(SyndicationItem item)
- {
- return item.ElementExtensions
- .Where(p => p.OuterName.Equals("image"))
- .Select(p => new Uri(p.GetObject().Value));
- }
-
- private static string Normalize(string value)
- {
- if (!string.IsNullOrEmpty(value))
- {
- value = HtmlDecode(value);
- if (string.IsNullOrEmpty(value))
- return value;
-
- value = StripHTML(value);
- value = StripDoubleOrMoreWhiteSpace(RemoveControlChars(value));
- value = value.Normalize().Trim();
- }
- return value;
- }
-
- private static string RemoveControlChars(string value)
- {
- return _controlCodesRegex.Replace(value, " ");
- }
-
- private static string StripDoubleOrMoreWhiteSpace(string value)
- {
- return _whiteSpaceRegex.Replace(value, " ");
- }
-
- private static string StripHTML(string value)
- {
- return _htmlRegex.Replace(value, " ");
- }
-
- private static string HtmlDecode(string value, int threshold = 5)
- {
- int c = 0;
- string newvalue = WebUtility.HtmlDecode(value);
- while (!newvalue.Equals(value) && c < threshold) //Keep decoding (if a string is double/triple/... encoded; we want the original)
- {
- c++;
- value = newvalue;
- newvalue = WebUtility.HtmlDecode(value);
- }
- if (c >= threshold) //Decoding threshold exceeded?
- return null;
-
- return newvalue;
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.ServiceModel.Syndication;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+
+namespace N2.RssAtomFeedReader
+{
+ ///
+ /// The normalizes ,
+ /// and of s to the point where
+ /// they no longer contain any HTML, redundant whitespace, un-normalized unicode chars and other control chars like
+ /// tabs, newlines or backspaces. The 's property will contain
+ /// whichever date is latest; the or .
+ ///
+ ///
+ /// You can implement a normalizer yourself by implementing the interface.
+ ///
+ public partial class DefaultFeedItemNormalizer : IFeedItemNormalizer
+ {
+ private static readonly Regex controlCodesRegex = RegExControlCodes();
+ private static readonly Regex htmlRegex = RegExXmlElement(); //@"<(.|\n)*?>"
+ private static readonly Regex whiteSpaceRegex = RegExWhiteSpace();
+
+ ///
+ /// Normalizes a SyndicationItem into a FeedItem.
+ ///
+ /// The on which the item was retrieved.
+ /// A to normalize into a .
+ /// Returns a normalized .
+ public virtual FeedItem Normalize(SyndicationFeed feed, SyndicationItem item)
+ {
+ if(item==null) throw new ArgumentNullException(nameof(item));
+
+ var alternatelink = item.Links.FirstOrDefault(l => l.RelationshipType == null || l.RelationshipType.Equals("alternate", StringComparison.OrdinalIgnoreCase));
+
+ Uri? itemuri = null;
+ if (alternatelink == null && !Uri.TryCreate(item.Id, UriKind.Absolute, out Uri? parsed))
+ {
+ itemuri = parsed;
+ }
+ else
+ {
+ itemuri = alternatelink?.GetAbsoluteUri();
+ }
+
+ return new FeedItem
+ {
+ Id = string.IsNullOrEmpty(item.Id) ? null : item.Id.Trim(),
+ Title = item.Title == null ? null : Normalize(item.Title.Text),
+ Content = item.Content == null ? null : Normalize(((TextSyndicationContent)item.Content).Text),
+ Summary = item.Summary == null ? null : Normalize(item.Summary.Text),
+ PublishDate = item.PublishDate,
+ LastUpdatedDate = item.LastUpdatedTime == DateTimeOffset.MinValue ? item.PublishDate : item.LastUpdatedTime,
+ Uri = itemuri,
+ Images = GetFeedItemImages(item),
+ Categories = item.Categories.Select(c => c.Name)
+ };
+ }
+
+ private static IEnumerable GetFeedItemImages(SyndicationItem item)
+ {
+ return item.ElementExtensions
+ .Where(p => p.OuterName.Equals("image"))
+ .Select(p => new Uri(p.GetObject().Value));
+ }
+
+ private static string? HtmlDecode(string value, int threshold = 5)
+ {
+ int c = 0;
+ string newvalue = WebUtility.HtmlDecode(value);
+ while (!newvalue.Equals(value) && c < threshold) //Keep decoding (if a string is double/triple/... encoded; we want the original)
+ {
+ c++;
+ value = newvalue;
+ newvalue = WebUtility.HtmlDecode(value);
+ }
+ if (c >= threshold) //Decoding threshold exceeded?
+ return null;
+
+ return newvalue;
+ }
+
+ private static string? Normalize(string? value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ value = HtmlDecode(value);
+ if (string.IsNullOrEmpty(value))
+ return default;
+
+ value = StripHTML(value);
+ value = StripDoubleOrMoreWhiteSpace(RemoveControlChars(value));
+ value = value.Normalize().Trim();
+ }
+ return value;
+ }
+
+ private static string RemoveControlChars(string value)
+ {
+ return controlCodesRegex.Replace(value, " ");
+ }
+
+ private static string StripDoubleOrMoreWhiteSpace(string value)
+ {
+ return whiteSpaceRegex.Replace(value, " ");
+ }
+
+ private static string StripHTML(string value)
+ {
+ return htmlRegex.Replace(value, " ");
+ }
+#if NETSTANDARD
+ private static Regex RegExXmlElement()
+ {
+ return new Regex(@"<[^>]*>", RegexOptions.Compiled);
+ }
+ private static Regex RegExControlCodes()
+ {
+ return new Regex(@"[\x00-\x1F\x7f]", RegexOptions.Compiled);
+ }
+ private static Regex RegExWhiteSpace()
+ {
+ return new Regex(@"\s{2,}", RegexOptions.Compiled);
+ }
+#else
+ [GeneratedRegex(@"[\x00-\x1F\x7f]", RegexOptions.Compiled)]
+ private static partial Regex RegExControlCodes();
+
+ [GeneratedRegex(@"\s{2,}", RegexOptions.Compiled)]
+ private static partial Regex RegExWhiteSpace();
+
+ [GeneratedRegex(@"<[^>]*>", RegexOptions.Compiled)]
+ private static partial Regex RegExXmlElement();
+#endif
+
+ }
+}
diff --git a/SimpleFeedReader/FeedItem.cs b/src/N2.RssAtomFeedReader/FeedItem.cs
similarity index 79%
rename from SimpleFeedReader/FeedItem.cs
rename to src/N2.RssAtomFeedReader/FeedItem.cs
index b90ec90..64da662 100644
--- a/SimpleFeedReader/FeedItem.cs
+++ b/src/N2.RssAtomFeedReader/FeedItem.cs
@@ -1,111 +1,102 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.ServiceModel.Syndication;
-
-namespace SimpleFeedReader
-{
- ///
- /// Represents an item from a .
- ///
- public class FeedItem
- {
- ///
- /// The Id of the .
- ///
- public string Id { get; set; }
-
- ///
- /// The Title of the .
- ///
- public string Title { get; set; }
-
- ///
- /// The Content of the .
- ///
- public string Content { get; set; }
-
- ///
- /// The Summary of the .
- ///
- public string Summary { get; set; }
-
- ///
- /// The Uri of the .
- ///
- public Uri Uri { get; set; }
-
- ///
- /// The images of the .
- ///
- public IEnumerable Images { get; set; }
-
- ///
- /// The vategories of the .
- ///
- public IEnumerable Categories { get; set; }
-
- ///
- /// The Date of the .
- ///
- [Obsolete("Split into PublishDate and LastUpdatedDate")]
- public DateTimeOffset Date { get { return new[] { PublishDate, LastUpdatedDate }.Max(); } }
-
- ///
- /// The publication date of the .
- ///
- public DateTimeOffset PublishDate { get; set; }
-
- ///
- /// The date when the feeditem was last updated .
- ///
- public DateTimeOffset LastUpdatedDate { get; set; }
-
- ///
- /// Initializes a new .
- ///
- public FeedItem()
- {
- Images = new List();
- Categories = new List();
- }
-
- ///
- /// Initializes a new by copying the passed item's properties into the new instance.
- ///
- /// The to copy.
- /// This is a copy-constructor.
- public FeedItem(FeedItem item)
- : this()
- {
- Title = item.Title;
- Content = item.Content;
- Summary = item.Summary;
- Uri = item.Uri;
- PublishDate = item.PublishDate;
- LastUpdatedDate = item.LastUpdatedDate;
- Images = item.Images;
- Categories = item.Categories;
- }
-
- ///
- /// Returns content, if any, otherwise returns the summary as content.
- ///
- /// Returns content, if any, otherwise returns the summary as content.
- /// This method is intended as conveinience-method.
- public string GetContent()
- {
- return !string.IsNullOrEmpty(Content) ? Content : Summary;
- }
-
- ///
- /// Returns the summary, if any, otherwise returns the content as the summary.
- ///
- /// Returns the summary, if any, otherwise returns the content as the summary.
- /// This method is intended as conveinience-method.
- public string GetSummary()
- {
- return !string.IsNullOrEmpty(Summary) ? Summary : Content;
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.ServiceModel.Syndication;
+
+namespace N2.RssAtomFeedReader
+{
+ ///
+ /// Represents an item from a .
+ ///
+ public class FeedItem
+ {
+ ///
+ /// Initializes a new .
+ ///
+ public FeedItem()
+ {
+ Images = [];
+ Categories = [];
+ }
+
+ ///
+ /// Initializes a new by copying the passed item's properties into the new instance.
+ ///
+ /// The to copy.
+ /// This is a copy-constructor.
+ public FeedItem(FeedItem item)
+ : this()
+ {
+ Title = item.Title;
+ Content = item.Content;
+ Summary = item.Summary;
+ Uri = item.Uri;
+ PublishDate = item.PublishDate;
+ LastUpdatedDate = item.LastUpdatedDate;
+ Images = item.Images;
+ Categories = item.Categories;
+ }
+
+ ///
+ /// The vategories of the .
+ ///
+ public IEnumerable Categories { get; set; }
+
+ ///
+ /// The Content of the .
+ ///
+ public string? Content { get; set; }
+
+ ///
+ /// The Id of the .
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// The images of the .
+ ///
+ public IEnumerable Images { get; set; }
+
+ ///
+ /// The date when the feeditem was last updated .
+ ///
+ public DateTimeOffset LastUpdatedDate { get; set; }
+
+ ///
+ /// The publication date of the .
+ ///
+ public DateTimeOffset PublishDate { get; set; }
+
+ ///
+ /// The Summary of the .
+ ///
+ public string? Summary { get; set; }
+
+ ///
+ /// The Title of the .
+ ///
+ public string? Title { get; set; }
+ ///
+ /// The Uri of the .
+ ///
+ public Uri? Uri { get; set; }
+ ///
+ /// Returns content, if any, otherwise returns the summary as content.
+ ///
+ /// Returns content, if any, otherwise returns the summary as content.
+ /// This method is intended as conveinience-method.
+ public string? GetContent()
+ {
+ return !string.IsNullOrEmpty(Content) ? Content : Summary;
+ }
+
+ ///
+ /// Returns the summary, if any, otherwise returns the content as the summary.
+ ///
+ /// Returns the summary, if any, otherwise returns the content as the summary.
+ /// This method is intended as conveinience-method.
+ public string? GetSummary()
+ {
+ return !string.IsNullOrEmpty(Summary) ? Summary : Content;
+ }
+ }
+}
\ No newline at end of file
diff --git a/SimpleFeedReader/FeedReader.cs b/src/N2.RssAtomFeedReader/FeedReader.cs
similarity index 81%
rename from SimpleFeedReader/FeedReader.cs
rename to src/N2.RssAtomFeedReader/FeedReader.cs
index ea6051d..86eb93a 100644
--- a/SimpleFeedReader/FeedReader.cs
+++ b/src/N2.RssAtomFeedReader/FeedReader.cs
@@ -1,179 +1,210 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.ServiceModel.Syndication;
-using System.Xml;
-
-namespace SimpleFeedReader
-{
- ///
- /// Retrieves s and normalizes the items from the feed into s.
- ///
- public class FeedReader
- {
- ///
- /// Gets the default FeedItemNormalizer the will use when normalizing
- /// s.
- ///
- public IFeedItemNormalizer DefaultNormalizer { get; private set; }
-
- ///
- /// Gets wether the FeedReader will throw on exceptions or suppress exceptions and return empty results on
- /// errors.
- ///
- public bool ThrowOnError { get; private set; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- public FeedReader()
- : this(new DefaultFeedItemNormalizer()) { }
-
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// When true, the will throw on errors, when false the will
- /// suppress exceptions and return empty results.
- ///
- public FeedReader(bool throwOnError)
- : this(new DefaultFeedItemNormalizer(), throwOnError) { }
-
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// The to use when normalizing s.
- ///
- public FeedReader(IFeedItemNormalizer defaultFeedItemNormalizer)
- : this(defaultFeedItemNormalizer, false) { }
-
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// The to use when normalizing s.
- ///
- ///
- /// When true, the will throw on errors, when false the will
- /// suppress exceptions and return empty results.
- ///
- public FeedReader(IFeedItemNormalizer defaultFeedItemNormalizer, bool throwOnError)
- {
- DefaultNormalizer = defaultFeedItemNormalizer ?? throw new ArgumentNullException("defaultFeedItemNormalizer");
- ThrowOnError = throwOnError;
- }
-
- ///
- /// Retrieves the specified feeds.
- ///
- /// The uri's of the feeds to retrieve.
- ///
- /// Returns an of retrieved s.
- ///
- /// This is a convenience method.
- public IEnumerable RetrieveFeeds(IEnumerable uris)
- {
- return RetrieveFeeds(uris, DefaultNormalizer);
- }
-
- ///
- /// Retrieves the specified feeds.
- ///
- /// The uri's of the feeds to retrieve.
- ///
- /// The to use when normalizing s.
- ///
- ///
- /// Returns an of retrieved s.
- ///
- /// This is a convenience method.
- public IEnumerable RetrieveFeeds(IEnumerable uris, IFeedItemNormalizer normalizer)
- {
- List items = new List();
- foreach (var u in uris)
- items.AddRange(RetrieveFeed(u, normalizer));
- return items;
- }
-
- ///
- /// Retrieves the specified feed.
- ///
- /// The uri of the feed to retrieve.
- ///
- /// Returns an of retrieved s.
- ///
- public IEnumerable RetrieveFeed(string uri)
- {
- return RetrieveFeed(uri, DefaultNormalizer);
- }
-
- ///
- /// Retrieves the specified feed.
- ///
- /// The uri of the feed to retrieve.
- ///
- /// The to use when normalizing s.
- ///
- ///
- /// Returns an of retrieved s.
- ///
- public IEnumerable RetrieveFeed(string uri, IFeedItemNormalizer normalizer)
- {
- try
- {
- return RetrieveFeed(XmlReader.Create(uri), normalizer);
- }
- catch
- {
- if (ThrowOnError)
- throw;
- }
- return Enumerable.Empty();
- }
-
- ///
- /// Retrieves the specified feed.
- ///
- /// The to use to read the items from.
- ///
- /// Returns an of retrieved s.
- ///
- public IEnumerable RetrieveFeed(XmlReader xmlReader)
- {
- return RetrieveFeed(xmlReader, DefaultNormalizer);
- }
-
- ///
- /// Retrieves the specified feed.
- ///
- /// The to use to read the items from.
- ///
- /// The to use when normalizing s.
- ///
- ///
- /// Returns an of retrieved s.
- ///
- public IEnumerable RetrieveFeed(XmlReader xmlReader, IFeedItemNormalizer normalizer)
- {
- if (xmlReader == null)
- throw new ArgumentNullException("xmlReader");
- if (normalizer == null)
- throw new ArgumentNullException("normalizer");
-
- var items = new List();
- try
- {
- var feed = SyndicationFeed.Load(xmlReader);
- foreach (var item in feed.Items)
- items.Add(normalizer.Normalize(feed, item));
- }
- catch
- {
- if (ThrowOnError)
- throw;
- }
- return items;
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.ServiceModel.Syndication;
+using System.Xml;
+
+namespace N2.RssAtomFeedReader
+{
+ ///
+ /// Retrieves s and normalizes the items from the feed into s.
+ ///
+ public class FeedReader : IFeedReader
+ {
+ ///
+ /// The default feeds for the .
+ ///
+ private readonly Dictionary feeds = [];
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FeedReader()
+ : this(new DefaultFeedItemNormalizer()) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Initialization options.
+ public FeedReader(FeedReaderOptions options)
+ {
+ DefaultNormalizer = options.DefaultNormalizer;
+ ThrowOnError = options.ThrowOnError;
+ feeds.Clear();
+ foreach (var feed in options.Feeds)
+ {
+ feeds.Add(feed.Key, feed.Value);
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// When true, the will throw on errors, when false the will
+ /// suppress exceptions and return empty results.
+ ///
+ public FeedReader(bool throwOnError)
+ : this(new DefaultFeedItemNormalizer(), throwOnError) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The to use when normalizing s.
+ ///
+ public FeedReader(IFeedItemNormalizer defaultFeedItemNormalizer)
+ : this(defaultFeedItemNormalizer, false) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The to use when normalizing s.
+ ///
+ ///
+ /// When true, the will throw on errors, when false the will
+ /// suppress exceptions and return empty results.
+ ///
+ public FeedReader(IFeedItemNormalizer defaultFeedItemNormalizer, bool throwOnError)
+ {
+ DefaultNormalizer = defaultFeedItemNormalizer ?? throw new ArgumentNullException(nameof(defaultFeedItemNormalizer));
+ ThrowOnError = throwOnError;
+ }
+
+ ///
+ /// Gets the default FeedItemNormalizer the will use when normalizing
+ /// s.
+ ///
+ public IFeedItemNormalizer DefaultNormalizer { get; private set; }
+
+ ///
+ /// Gets wether the FeedReader will throw on exceptions or suppress exceptions and return empty results on
+ /// errors.
+ ///
+ public bool ThrowOnError { get; private set; }
+
+ ///
+ /// Retrieves the specified feed.
+ ///
+ /// The uri of the feed to retrieve.
+ ///
+ /// Returns an of retrieved s.
+ ///
+ public IEnumerable RetrieveFeed(string uri)
+ {
+ if (feeds.TryGetValue(uri, out var value))
+ {
+ return RetrieveFeed(value, DefaultNormalizer);
+ }
+ return RetrieveFeed(uri, DefaultNormalizer);
+ }
+
+ ///
+ /// Retrieves the specified feed.
+ ///
+ /// The uri of the feed to retrieve.
+ ///
+ /// The to use when normalizing s.
+ ///
+ ///
+ /// Returns an of retrieved s.
+ ///
+ public IEnumerable RetrieveFeed(string uri, IFeedItemNormalizer normalizer)
+ {
+ try
+ {
+ using var reader = XmlReader.Create(uri);
+ return RetrieveFeed(reader, normalizer);
+ }
+ catch
+ {
+ if (ThrowOnError)
+ throw;
+ }
+ return [];
+ }
+
+ ///
+ /// Retrieves the specified feed.
+ ///
+ /// The to use to read the items from.
+ ///
+ /// Returns an of retrieved s.
+ ///
+ public IEnumerable RetrieveFeed(XmlReader xmlReader)
+ {
+ return RetrieveFeed(xmlReader, DefaultNormalizer);
+ }
+
+ ///
+ /// Retrieves the specified feed.
+ ///
+ /// The to use to read the items from.
+ ///
+ /// The to use when normalizing s.
+ ///
+ ///
+ /// Returns an of retrieved s.
+ ///
+ public IEnumerable RetrieveFeed(XmlReader xmlReader, IFeedItemNormalizer normalizer)
+ {
+ if (xmlReader == null)
+ {
+ throw new ArgumentNullException(nameof(xmlReader));
+ }
+
+ if (normalizer == null)
+ {
+ throw new ArgumentNullException(nameof(normalizer));
+ }
+
+ var items = new List();
+ try
+ {
+ var feed = SyndicationFeed.Load(xmlReader);
+ foreach (var item in feed.Items)
+ items.Add(normalizer.Normalize(feed, item));
+ }
+ catch
+ {
+ if (ThrowOnError)
+ throw;
+ }
+ return items;
+ }
+
+ ///
+ /// Retrieves the specified feeds.
+ ///
+ /// The uri's of the feeds to retrieve.
+ ///
+ /// Returns an of retrieved s.
+ ///
+ /// This is a convenience method.
+ public IEnumerable RetrieveFeeds(IEnumerable uris)
+ {
+ return RetrieveFeeds(uris, DefaultNormalizer);
+ }
+
+ ///
+ /// Retrieves the specified feeds.
+ ///
+ /// The uri's of the feeds to retrieve.
+ ///
+ /// The to use when normalizing s.
+ ///
+ ///
+ /// Returns an of retrieved s.
+ ///
+ /// This is a convenience method.
+ public IEnumerable RetrieveFeeds(IEnumerable uris, IFeedItemNormalizer normalizer)
+ {
+ List items = [];
+ foreach (var u in uris)
+ {
+ items.AddRange(RetrieveFeed(u, normalizer));
+ }
+ return items;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/N2.RssAtomFeedReader/FeedReaderOptions.cs b/src/N2.RssAtomFeedReader/FeedReaderOptions.cs
new file mode 100644
index 0000000..0439232
--- /dev/null
+++ b/src/N2.RssAtomFeedReader/FeedReaderOptions.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace N2.RssAtomFeedReader;
+
+///
+/// Options to initialize a feed reader.
+///
+public class FeedReaderOptions
+{
+ ///
+ /// The default normalizer to use when normalizing s.
+ ///
+ public IFeedItemNormalizer DefaultNormalizer { get; set; } = new DefaultFeedItemNormalizer();
+ ///
+ /// When true, the will throw on errors.
+ ///
+ public bool ThrowOnError { get; set; } = false;
+ ///
+ /// A default set of feeds that can be retrieved using the key.
+ ///
+ public Dictionary Feeds { get; set; } = [];
+}
\ No newline at end of file
diff --git a/SimpleFeedReader/IFeedItemNormalizer.cs b/src/N2.RssAtomFeedReader/IFeedItemNormalizer.cs
similarity index 93%
rename from SimpleFeedReader/IFeedItemNormalizer.cs
rename to src/N2.RssAtomFeedReader/IFeedItemNormalizer.cs
index de237d0..3cfce5f 100644
--- a/SimpleFeedReader/IFeedItemNormalizer.cs
+++ b/src/N2.RssAtomFeedReader/IFeedItemNormalizer.cs
@@ -1,18 +1,18 @@
-using System.ServiceModel.Syndication;
-
-namespace SimpleFeedReader
-{
- ///
- /// Provides the base FeedItemNormalizer interface for s.
- ///
- public interface IFeedItemNormalizer
- {
- ///
- /// Normalizes a into a .
- ///
- /// The on which the item was retrieved.
- /// The to normalize.
- /// Returns a normalized .
- FeedItem Normalize(SyndicationFeed feed, SyndicationItem item);
- }
-}
+using System.ServiceModel.Syndication;
+
+namespace N2.RssAtomFeedReader
+{
+ ///
+ /// Provides the base FeedItemNormalizer interface for s.
+ ///
+ public interface IFeedItemNormalizer
+ {
+ ///
+ /// Normalizes a into a .
+ ///
+ /// The on which the item was retrieved.
+ /// The to normalize.
+ /// Returns a normalized .
+ FeedItem Normalize(SyndicationFeed feed, SyndicationItem item);
+ }
+}
diff --git a/src/N2.RssAtomFeedReader/IFeedReader.cs b/src/N2.RssAtomFeedReader/IFeedReader.cs
new file mode 100644
index 0000000..fd00d69
--- /dev/null
+++ b/src/N2.RssAtomFeedReader/IFeedReader.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace N2.RssAtomFeedReader;
+
+public interface IFeedReader
+{
+ ///
+ /// Retrieves a feed from the specified URI.
+ ///
+ /// A valid uri specification
+ /// A list of feed items.
+ IEnumerable RetrieveFeed(string uri);
+}
\ No newline at end of file
diff --git a/SimpleFeedReader/SimpleFeedReader.csproj b/src/N2.RssAtomFeedReader/N2.RssAtomFeedReader.csproj
similarity index 54%
rename from SimpleFeedReader/SimpleFeedReader.csproj
rename to src/N2.RssAtomFeedReader/N2.RssAtomFeedReader.csproj
index 9069a37..41c8f69 100644
--- a/SimpleFeedReader/SimpleFeedReader.csproj
+++ b/src/N2.RssAtomFeedReader/N2.RssAtomFeedReader.csproj
@@ -1,41 +1,38 @@
-
-
-
- net4;netstandard20
- 1.0.9.0
- 1.0.9.0
- 1.0.9.0
- true
- RobIII
- Easy to use, simple, Syndication feed reader (Atom / RSS).
- Provides classes and methods for consuming syndication feeds.
- SimpleFeedReader
- SimpleFeedReader
- SimpleFeedReader
- https://github.com/RobThree/SimpleFeedReader
- https://github.com/RobThree/SimpleFeedReader
- rss atom syndication feed
- * Added netcore support
- false
- icon.png
- (c) 2014 - 2020 Devcorner.nl
- MIT
- true
-
-
-
-
- bin\Release\SimpleFeedReader.xml
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ net8.0;NetStandard2.1
+ 12.0
+ 8.0.9.0
+ 8.0.9.3
+ enable
+ true
+ Easy to use, simple, Syndication feed reader (Atom / RSS).
+ Provides classes and methods for consuming syndication feeds.
+ N2.FeedReader
+ N2.RssAtomFeedReader
+ N2.RssAtomFeedReader
+ rss atom syndication feed
+ * Forked from SimpleFeedReader
+ false
+ icon.png
+ MIT
+ true
+
+
+
+
+ bin\Release\SimpleFeedReader.xml
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SimpleFeedReader.sln b/src/SimpleFeedReader.sln
new file mode 100644
index 0000000..c69ea90
--- /dev/null
+++ b/src/SimpleFeedReader.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35514.174
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{35CEBD40-E6BE-4671-B270-89C7E72CA980}"
+ ProjectSection(SolutionItems) = preProject
+ ..\LICENSE = ..\LICENSE
+ ..\README.md = ..\README.md
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "N2.RssAtomFeedReader", "N2.RssAtomFeedReader\N2.RssAtomFeedReader.csproj", "{003A8F2F-F910-4FD4-AF49-C8C7D2042668}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "N2.RssAtomFeedReader.UnitTests", "N2.RssAtomFeedReader.UnitTests\N2.RssAtomFeedReader.UnitTests.csproj", "{B1836759-F532-4A22-AA0A-D0CA3E2B2715}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {003A8F2F-F910-4FD4-AF49-C8C7D2042668}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {003A8F2F-F910-4FD4-AF49-C8C7D2042668}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {003A8F2F-F910-4FD4-AF49-C8C7D2042668}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {003A8F2F-F910-4FD4-AF49-C8C7D2042668}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B1836759-F532-4A22-AA0A-D0CA3E2B2715}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B1836759-F532-4A22-AA0A-D0CA3E2B2715}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B1836759-F532-4A22-AA0A-D0CA3E2B2715}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B1836759-F532-4A22-AA0A-D0CA3E2B2715}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {F7595307-AFC6-4349-B557-BD6B7927DA66}
+ EndGlobalSection
+EndGlobal