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 - - - - - &lt%3bpara&gt%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&#39%3bs System.Services.Syndication does.&lt%3b/para&gt%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) NuGet version +[![Build status](https://github.com/gjkaal/SimpleFeedReader/actions/workflows/dotnet.yml/badge.svg)](https://github.com/gjkaal/SimpleFeedReader/actions/workflows/dotnet.yml/badge.svg) +NuGet version 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;amp;bar - foo&amp;amp;amp;amp;amp;bar - - foo&amp;amp;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;amp;amp;bar + foo&amp;amp;amp;amp;amp;bar + + foo&amp;amp;amp;amp;amp;bar + + + + foo&amp;amp;amp;amp;bar + foo&amp;amp;amp;amp;bar + + foo&amp;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