├── src ├── BlazorRssReader │ ├── Pages │ │ ├── _ViewImports.cshtml │ │ ├── Details │ │ │ ├── Details.cshtml.cs │ │ │ └── Details.cshtml │ │ ├── Index.cshtml │ │ ├── Listing │ │ │ ├── TitleView.cshtml │ │ │ ├── MagazineView.cshtml │ │ │ ├── CardsView.cshtml │ │ │ ├── ArticleView.cshtml │ │ │ ├── Index.cshtml │ │ │ └── Index.cshtml.cs │ │ ├── Index.cshtml.cs │ │ ├── Organize │ │ │ ├── Edit.cshtml │ │ │ ├── Index.cshtml.cs │ │ │ ├── Edit.cshtml.cs │ │ │ └── Index.cshtml │ │ └── Add │ │ │ ├── AddFeed.cshtml.cs │ │ │ └── AddFeed.cshtml │ ├── wwwroot │ │ ├── images │ │ │ ├── buffer.png │ │ │ └── hootsuite.png │ │ ├── css │ │ │ └── site.css │ │ └── index.html │ ├── App.cshtml │ ├── package.json │ ├── Services │ │ ├── NotificationService.cs │ │ ├── Session.cs │ │ └── FeedService.cs │ ├── _ViewImports.cshtml │ ├── RssParser │ │ ├── SchemaBase.cs │ │ ├── RssType.cs │ │ ├── IParser.cs │ │ ├── RssParser.cs │ │ ├── RssSchema.cs │ │ ├── BaseRssParser.cs │ │ ├── AtomParser.cs │ │ ├── StringExtensions.cs │ │ ├── Rss2Parser.cs │ │ └── RssHelper.cs │ ├── Shared │ │ ├── HtmlView.cshtml │ │ ├── MainLayout.cshtml │ │ ├── MainLayout.cshtml.cs │ │ ├── NavMenu.cshtml │ │ └── ShareButtons.cshtml │ ├── Models │ │ ├── FeedItem.cs │ │ └── Feed.cs │ ├── BlazorRssReader.csproj │ └── Program.cs └── BlazorRssReader.sln ├── assets ├── AddFeed.png ├── Splash.png ├── CardsView.png ├── EditFeed.png ├── FeedsView.png ├── Organize.png ├── TitleView.png ├── ArticleView.png ├── FeedItemView.png └── MagazineView.png ├── LICENSE ├── README.md └── .gitignore /src/BlazorRssReader/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @layout MainLayout 2 | -------------------------------------------------------------------------------- /assets/AddFeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/AddFeed.png -------------------------------------------------------------------------------- /assets/Splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/Splash.png -------------------------------------------------------------------------------- /assets/CardsView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/CardsView.png -------------------------------------------------------------------------------- /assets/EditFeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/EditFeed.png -------------------------------------------------------------------------------- /assets/FeedsView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/FeedsView.png -------------------------------------------------------------------------------- /assets/Organize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/Organize.png -------------------------------------------------------------------------------- /assets/TitleView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/TitleView.png -------------------------------------------------------------------------------- /assets/ArticleView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/ArticleView.png -------------------------------------------------------------------------------- /assets/FeedItemView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/FeedItemView.png -------------------------------------------------------------------------------- /assets/MagazineView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/assets/MagazineView.png -------------------------------------------------------------------------------- /src/BlazorRssReader/wwwroot/images/buffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/src/BlazorRssReader/wwwroot/images/buffer.png -------------------------------------------------------------------------------- /src/BlazorRssReader/wwwroot/images/hootsuite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-rss-reader/HEAD/src/BlazorRssReader/wwwroot/images/hootsuite.png -------------------------------------------------------------------------------- /src/BlazorRssReader/App.cshtml: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /src/BlazorRssReader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blazorrssreader", 3 | "version": "1.0.0", 4 | "description": "Blazor RSS Reader", 5 | "author": "kashyapa", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "surge": "0.20.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Details/Details.cshtml.cs: -------------------------------------------------------------------------------- 1 | using BlazorRssReader.Services; 2 | using Microsoft.AspNetCore.Blazor.Components; 3 | 4 | namespace BlazorRssReader.Pages.Details 5 | { 6 | public class DetailsModel : BlazorComponent 7 | { 8 | [Parameter] protected string EntryId { get; set; } 9 | [Inject] protected Session Session { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/BlazorRssReader/Services/NotificationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace BlazorRssReader.Services 5 | { 6 | public class NotificationService 7 | { 8 | public event Func OnFeedUpdated; 9 | public async Task NotifyFeedChange() 10 | { 11 | await OnFeedUpdated?.Invoke(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/BlazorRssReader/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Blazor.Layouts 3 | @using Microsoft.AspNetCore.Blazor.Routing 4 | @using Microsoft.AspNetCore.Blazor.Components 5 | @using Microsoft.Toolkit.Parsers 6 | @using BlazorRssReader 7 | @using BlazorRssReader.Shared 8 | @using BlazorRssReader.Pages 9 | @using BlazorRssReader.Models 10 | @using Microsoft.JSInterop -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @inherits IndexModel 3 | 4 |
5 |
6 |
7 |
8 |
9 |
10 |

11 |

No entries.

12 | Click "Add Feed" to add feed(s). 13 |

14 |
15 |
16 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Services/Session.cs: -------------------------------------------------------------------------------- 1 | using BlazorRssReader.Models; 2 | using Microsoft.Toolkit.Parsers; 3 | using System.Collections.Generic; 4 | 5 | namespace BlazorRssReader.Services 6 | { 7 | public class Session 8 | { 9 | public Feed SelectedFeed { get; set; } = null; 10 | public List SelectedFeedItems { get; set; } = null; 11 | public RssSchema SelectedEntry { get; set; } = null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/SchemaBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Microsoft.Toolkit.Parsers 6 | { 7 | /// 8 | /// Strong typed schema base class. 9 | /// 10 | public abstract class SchemaBase 11 | { 12 | /// 13 | /// Gets or sets identifier for strong typed record. 14 | /// 15 | public string InternalID { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Shared/HtmlView.cshtml: -------------------------------------------------------------------------------- 1 |
2 | 3 | @functions{ 4 | 5 | [Parameter] string Content { get; set; } 6 | 7 | private ElementRef HtmlContentDiv; 8 | 9 | protected override Task OnAfterRenderAsync() 10 | { 11 | if (!string.IsNullOrEmpty(Content)) 12 | { 13 | return JSRuntime.Current.InvokeAsync("blazorRssReaderFunctions.RawHtml", HtmlContentDiv, Content); 14 | } 15 | return Task.FromResult(false); 16 | } 17 | } -------------------------------------------------------------------------------- /src/BlazorRssReader/Models/FeedItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BlazorRssReader.Models 4 | { 5 | public class FeedItem 6 | { 7 | public string Id { get; set; } 8 | public string Title { get; set; } 9 | public string Descriiption { get; set; } 10 | public string Url { get; set; } 11 | public DateTime Published { get; set; } 12 | public string Creator { get; set; } 13 | public string Category { get; set; } 14 | public string Encoded { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/RssType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Microsoft.Toolkit.Parsers 6 | { 7 | /// 8 | /// Type of Rss. 9 | /// 10 | internal enum RssType 11 | { 12 | /// 13 | /// Atom 14 | /// 15 | Atom, 16 | /// 17 | /// RSS 18 | /// 19 | Rss, 20 | /// 21 | /// Unknown 22 | /// 23 | Unknown 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Models/Feed.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BlazorRssReader.Models 5 | { 6 | public class Feed 7 | { 8 | public Feed() 9 | { 10 | Id = Guid.NewGuid(); 11 | } 12 | public Guid Id { get; set; } 13 | public string Title { get; set; } 14 | public string Description { get; set; } 15 | public string Url { get; set; } 16 | public string WebsiteUrl { get; set; } 17 | public string ImageUrl { get; set; } = null; 18 | public DateTime LastUpdate { get; set; } 19 | public List Items { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/BlazorRssReader/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | .github-corner { 2 | z-index: 10000; 3 | } 4 | 5 | .github-corner:hover .octo-arm { 6 | animation: octocat-wave 560ms ease-in-out 7 | } 8 | 9 | @keyframes octocat-wave { 10 | 0%,100% { 11 | transform: rotate(0) 12 | } 13 | 14 | 20%,60% { 15 | transform: rotate(-25deg) 16 | } 17 | 18 | 40%,80% { 19 | transform: rotate(10deg) 20 | } 21 | } 22 | 23 | @media (max-width:500px) { 24 | .github-corner:hover .octo-arm { 25 | animation: none 26 | } 27 | 28 | .github-corner .octo-arm { 29 | animation: octocat-wave 560ms ease-in-out 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/IParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Microsoft.Toolkit.Parsers 6 | { 7 | /// 8 | /// Parser interface. 9 | /// 10 | /// Type to parse into. 11 | public interface IParser 12 | where T : SchemaBase 13 | { 14 | /// 15 | /// Parse method which all classes must implement. 16 | /// 17 | /// Data to parse. 18 | /// Strong typed parsed data. 19 | IEnumerable Parse(string data); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Listing/TitleView.cshtml: -------------------------------------------------------------------------------- 1 | @foreach (var item in Items) 2 | { 3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | @item.Title @item.Summary 11 |
12 |
13 | @item.PublishDate.ToString("MMM dd") 14 |
15 |
16 | } 17 | 18 | @functions{ 19 | [Parameter] List Items { get; set; } 20 | 21 | [Parameter] Action OnItemClicked { get; set; } 22 | 23 | void OnClick(RssSchema item) 24 | { 25 | OnItemClicked?.Invoke(item); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using BlazorRssReader.Services; 2 | using Microsoft.AspNetCore.Blazor.Components; 3 | using Microsoft.AspNetCore.Blazor.Services; 4 | using Microsoft.Extensions.Logging; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorRssReader.Pages 9 | { 10 | public class IndexModel : BlazorComponent 11 | { 12 | [Inject] private ILogger Logger { get; set; } 13 | [Inject] private FeedService FeedService { get; set; } 14 | [Inject] private IUriHelper UriHelper { get; set; } 15 | 16 | protected override async Task OnInitAsync() 17 | { 18 | List feeds = await FeedService.GetFeeds(); 19 | if (feeds.Count > 0) 20 | { 21 | UriHelper.NavigateTo($"/feed/{feeds[0].Id}"); 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/BlazorRssReader/BlazorRssReader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | dotnet 6 | blazor serve 7 | 7.3 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Always 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Shared/MainLayout.cshtml: -------------------------------------------------------------------------------- 1 | @inherits MainLayoutModel 2 | 3 | 22 | 23 |
24 | 25 |
26 | @Body 27 | 28 |
29 |
-------------------------------------------------------------------------------- /src/BlazorRssReader/Program.cs: -------------------------------------------------------------------------------- 1 | using Blazor.Extensions.Logging; 2 | using Blazored.Storage; 3 | using BlazorRssReader.Services; 4 | using Microsoft.AspNetCore.Blazor.Browser.Rendering; 5 | using Microsoft.AspNetCore.Blazor.Browser.Services; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace BlazorRssReader 10 | { 11 | public class Program 12 | { 13 | static void Main(string[] args) 14 | { 15 | var serviceProvider = new BrowserServiceProvider(services => 16 | { 17 | services.AddLocalStorage(); 18 | services.AddSingleton(); 19 | services.AddSingleton(); 20 | services.AddSingleton(); 21 | 22 | services.AddLogging(builder => builder 23 | .AddBrowserConsole() 24 | .SetMinimumLevel(LogLevel.Information) 25 | ); 26 | 27 | 28 | }); 29 | 30 | new BrowserRenderer(serviceProvider).AddComponent("app"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lohith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Listing/MagazineView.cshtml: -------------------------------------------------------------------------------- 1 | @foreach (var item in Items) 2 | { 3 |
4 |
5 | @if (!string.IsNullOrEmpty(item.ImageUrl)) 6 | { 7 | 8 | } 9 |
10 |
11 |
@item.Title
12 |
@item.PublishDate.ToString("dd MMM")
13 |
@item.Summary.Truncate(230)
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | } 25 | 26 | @functions{ 27 | [Parameter] List Items { get; set; } 28 | 29 | [Parameter] Action OnItemClicked { get; set; } 30 | 31 | void OnClick(RssSchema item) 32 | { 33 | OnItemClicked?.Invoke(item); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/RssParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Xml.Linq; 5 | 6 | namespace Microsoft.Toolkit.Parsers 7 | { 8 | public class RssParser : IParser 9 | { 10 | 11 | /// 12 | /// Parse an RSS content string into RSS Schema. 13 | /// 14 | /// Input string. 15 | /// Strong type. 16 | 17 | public IEnumerable Parse(string data) 18 | { 19 | if (string.IsNullOrEmpty(data)) 20 | { 21 | return null; 22 | } 23 | var doc = XDocument.Parse(data); 24 | var type = BaseRssParser.GetFeedType(doc); 25 | BaseRssParser rssParser; 26 | if (type == RssType.Rss) 27 | { 28 | rssParser = new Rss2Parser(); 29 | } 30 | else 31 | { 32 | rssParser = new AtomParser(); 33 | } 34 | return rssParser.LoadFeed(doc); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Listing/CardsView.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Toolkit.Parsers 2 | 3 |
4 | @foreach (var item in Items) 5 | { 6 |
7 |
8 |
9 | @if (!string.IsNullOrEmpty(item.ImageUrl)) 10 | { 11 | @item.Title 12 | } 13 |
14 | @item.Title.Truncate(75) 15 |

@item.Summary.Truncate(127)

16 |

@item.PublishDate.ToString("dd MMM")

17 |
18 |
19 | } 20 |
21 | 22 | @functions{ 23 | [Parameter] List Items { get; set; } 24 | 25 | [Parameter] Action OnItemClicked { get; set; } 26 | 27 | void OnClick(RssSchema item) 28 | { 29 | OnItemClicked?.Invoke(item); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/BlazorRssReader.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27729.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorRssReader", "BlazorRssReader\BlazorRssReader.csproj", "{54BA9B2F-2F66-4736-9944-BF59EFB94A1A}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {54BA9B2F-2F66-4736-9944-BF59EFB94A1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {54BA9B2F-2F66-4736-9944-BF59EFB94A1A}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {54BA9B2F-2F66-4736-9944-BF59EFB94A1A}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {54BA9B2F-2F66-4736-9944-BF59EFB94A1A}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {5A2B32D4-29CC-42BF-93F3-FFAD0D9DD6CC} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Shared/MainLayout.cshtml.cs: -------------------------------------------------------------------------------- 1 | using BlazorRssReader.Models; 2 | using BlazorRssReader.Services; 3 | using Microsoft.AspNetCore.Blazor.Components; 4 | using Microsoft.AspNetCore.Blazor.Layouts; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | namespace BlazorRssReader.Shared 10 | { 11 | public class MainLayoutModel : BlazorLayoutComponent, IDisposable 12 | { 13 | [Inject] private NotificationService NotificationService { get; set; } 14 | [Inject] private FeedService FeedService { get; set; } 15 | public List MenuItems { get; set; } 16 | 17 | protected override async Task OnInitAsync() 18 | { 19 | NotificationService.OnFeedUpdated += UpdateFeeds; 20 | await LoadMenuItems(); 21 | } 22 | 23 | private async Task UpdateFeeds() 24 | { 25 | await LoadMenuItems(); 26 | StateHasChanged(); 27 | } 28 | 29 | private async Task LoadMenuItems() 30 | { 31 | MenuItems = await FeedService.GetFeeds(); 32 | } 33 | 34 | public void Dispose() 35 | { 36 | NotificationService.OnFeedUpdated -= UpdateFeeds; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Organize/Edit.cshtml: -------------------------------------------------------------------------------- 1 | @page "/organize/edit/{FeedId}" 2 | @inherits EditModel 3 | @if (IsBusy) 4 | { 5 |

Loading feed ...

6 | } 7 | else 8 | { 9 | 17 |
18 |
19 |

Edit feed

20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | Cancel 34 |
35 |
36 |
37 |
38 |
39 |
40 | } -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Organize/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using BlazorRssReader.Models; 2 | using BlazorRssReader.Services; 3 | using Microsoft.AspNetCore.Blazor.Components; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | namespace BlazorRssReader.Pages.Organize 10 | { 11 | public class IndexModel : BlazorComponent 12 | { 13 | [Inject] NotificationService NotificationService { get; set; } 14 | [Inject] FeedService Service { get; set; } 15 | [Inject] ILogger Logger { get; set; } 16 | 17 | public bool IsBusy { get; set; } 18 | 19 | public List Feeds { get; set; } 20 | 21 | protected override async Task OnInitAsync() 22 | { 23 | IsBusy = true; 24 | await LoadFeedsAsync(); 25 | IsBusy = false; 26 | } 27 | 28 | private async Task LoadFeedsAsync() 29 | { 30 | await Task.Run(async () => 31 | { 32 | Feeds = await Service.GetFeeds(); 33 | }); 34 | } 35 | 36 | public async Task OnUnfollowFeed(Guid feedId) 37 | { 38 | Logger.LogInformation("feed id {feedId}", feedId); 39 | await Service.DeleteFeed(feedId); 40 | await NotificationService.NotifyFeedChange(); 41 | await LoadFeedsAsync(); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Details/Details.cshtml: -------------------------------------------------------------------------------- 1 | @page "/entry/{EntryId}" 2 | @inherits DetailsModel 3 | 4 | 10 | 11 |
12 |
13 |

@Session.SelectedEntry.Title

14 | 15 | @Session.SelectedFeed.Title by @Session.SelectedEntry.Author 16 | / 17 | @Session.SelectedEntry.PublishDate.ToString("dd MMM") 18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | VISIT WEBSITE 37 |
38 |
39 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Organize/Edit.cshtml.cs: -------------------------------------------------------------------------------- 1 | using BlazorRssReader.Models; 2 | using BlazorRssReader.Services; 3 | using Microsoft.AspNetCore.Blazor.Components; 4 | using Microsoft.AspNetCore.Blazor.Services; 5 | using Microsoft.Extensions.Logging; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorRssReader.Pages.Organize 9 | { 10 | public class EditModel : BlazorComponent 11 | { 12 | [Parameter] protected string FeedId { get; set; } 13 | 14 | [Inject] private NotificationService NotificationService { get; set; } 15 | [Inject] private IUriHelper UriHelper { get; set; } 16 | [Inject] private ILogger Logger { get; set; } 17 | 18 | protected bool IsBusy { get; set; } = false; 19 | 20 | [Inject] private FeedService FeedService { get; set; } 21 | 22 | public string FeedTitle { get; set; } 23 | 24 | private Feed feed; 25 | 26 | protected override async Task OnInitAsync() 27 | { 28 | IsBusy = true; 29 | await LoadFeed(); 30 | IsBusy = false; 31 | } 32 | 33 | private async Task LoadFeed() 34 | { 35 | feed = await FeedService.GetFeedDetails(FeedId); 36 | Logger.LogInformation("Feed {0}",feed); 37 | FeedTitle = feed.Title; 38 | } 39 | 40 | public async Task OnSaveClick() 41 | { 42 | await FeedService.UpdateFeed(FeedId, FeedTitle); 43 | await NotificationService.NotifyFeedChange(); 44 | UriHelper.NavigateTo("/organize"); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Add/AddFeed.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Blazor.Components; 2 | using System.Threading.Tasks; 3 | using BlazorRssReader.Models; 4 | using BlazorRssReader.Services; 5 | using Microsoft.AspNetCore.Blazor.Services; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace BlazorRssReader.Pages.Add 9 | { 10 | public class AddFeedModel : BlazorComponent 11 | { 12 | [Inject] NotificationService NotificationService { get; set; } 13 | [Inject] FeedService FeedService { get; set; } 14 | [Inject] IUriHelper UriHelper { get; set; } 15 | [Inject] ILogger Logger { get; set; } 16 | 17 | public string FeedUrl { get; set; } 18 | public bool IsBusy { get; set; } 19 | public bool IsError { get; set; } 20 | public Feed Feed { get; set; } = null; 21 | public async Task OnCheckFeed() 22 | { 23 | if (string.IsNullOrEmpty(FeedUrl)) return; 24 | 25 | IsError = false; 26 | IsBusy = true; 27 | Feed = null; 28 | try 29 | { 30 | Feed = await FeedService.GetFeedMetadata(FeedUrl); 31 | } 32 | catch 33 | { 34 | IsError = true; 35 | } 36 | finally 37 | { 38 | IsBusy=false; 39 | } 40 | } 41 | 42 | public async Task OnFollowFeed() 43 | { 44 | await FeedService.AddFeed(Feed); 45 | await NotificationService.NotifyFeedChange(); 46 | UriHelper.NavigateTo($"/feed/{Feed.Id}"); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/RssSchema.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Microsoft.Toolkit.Parsers 6 | { 7 | /// 8 | /// Implementation of the RssSchema class. 9 | /// 10 | public class RssSchema : SchemaBase 11 | { 12 | /// 13 | /// Gets or sets title. 14 | /// 15 | public string Title { get; set; } 16 | 17 | /// 18 | /// Gets or sets summary. 19 | /// 20 | public string Summary { get; set; } 21 | 22 | /// 23 | /// Gets or sets content. 24 | /// 25 | public string Content { get; set; } 26 | 27 | 28 | /// 29 | /// Gets or sets image Url. 30 | /// 31 | public string ImageUrl { get; set; } 32 | 33 | /// 34 | /// Gets or sets extra Image Url. 35 | /// 36 | public string ExtraImageUrl { get; set; } 37 | 38 | /// 39 | /// Gets or sets media Url. 40 | /// 41 | public string MediaUrl { get; set; } 42 | 43 | /// 44 | /// Gets or sets feed Url. 45 | /// 46 | public string FeedUrl { get; set; } 47 | 48 | /// 49 | /// Gets or sets author. 50 | /// 51 | public string Author { get; set; } 52 | 53 | /// 54 | /// Gets or sets publish Date. 55 | /// 56 | public DateTime PublishDate { get; set; } 57 | 58 | /// 59 | /// Gets or sets item's categories. 60 | /// 61 | public IEnumerable Categories { get; set; } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Shared/NavMenu.cshtml: -------------------------------------------------------------------------------- 1 | 47 | 48 | @functions{ 49 | [Parameter] protected List Feeds { get; set; } 50 | } 51 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Listing/ArticleView.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Toolkit.Parsers 2 | 3 | @foreach (var item in Items) 4 | { 5 |
6 |
7 |
8 |
9 |
10 | @item.Title 11 |
12 |
13 | 14 | @if (!string.IsNullOrEmpty(item.Author)) 15 | { 16 | @item.Author / 17 | } 18 | @item.PublishDate.ToString("dd MMM yyyy") 19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 | @if (!string.IsNullOrEmpty(item.ExtraImageUrl)) 27 | { 28 |
29 | @item.Title 30 |
31 | } 32 |
33 | 34 |
35 |
36 |
37 | VISIT WEBSITE 38 |
39 |
40 |
41 |
42 | } 43 | 44 | @functions{ 45 | [Parameter] List Items { get; set; } 46 | } 47 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Shared/ShareButtons.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @functions{ 27 | 28 | [Parameter] string Url { get; set; } 29 | 30 | [Parameter] string Title { get; set; } 31 | 32 | private ElementRef CopyToClipButton; 33 | 34 | Task CopyToClipboard() 35 | { 36 | return JSRuntime.Current.InvokeAsync("blazorRssReaderFunctions.CopyToClip", CopyToClipButton, null); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/BaseRssParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Xml.Linq; 5 | 6 | namespace Microsoft.Toolkit.Parsers 7 | { 8 | /// 9 | /// Base class for Rss Parser(s). 10 | /// 11 | internal abstract class BaseRssParser 12 | { 13 | /// 14 | /// Retrieve feed type from XDocument. 15 | /// 16 | /// XDocument doc. 17 | /// Return feed type. 18 | public static RssType GetFeedType(XDocument doc) 19 | { 20 | if (doc.Root == null) 21 | { 22 | return RssType.Unknown; 23 | } 24 | 25 | XNamespace defaultNamespace = doc.Root.GetDefaultNamespace(); 26 | return defaultNamespace.NamespaceName.EndsWith("Atom") ? RssType.Atom : RssType.Rss; 27 | } 28 | 29 | /// 30 | /// Abstract method to be override by specific implementations of the reader. 31 | /// 32 | /// XDocument doc. 33 | /// Returns list of strongly typed results. 34 | public abstract IEnumerable LoadFeed(XDocument doc); 35 | 36 | /// 37 | /// Fix up the HTML content. 38 | /// 39 | /// Content to be fixed up. 40 | /// Fixed up content. 41 | protected internal static string ProcessHtmlContent(string htmlContent) 42 | { 43 | return htmlContent.FixHtml().SanitizeString(); 44 | } 45 | 46 | /// 47 | /// Create a summary of the HTML content. 48 | /// 49 | /// Content to be processed. 50 | /// Summary of the content. 51 | protected internal static string ProcessHtmlSummary(string htmlContent) 52 | { 53 | return htmlContent.DecodeHtml().Trim().Truncate(500).SanitizeString(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Add/AddFeed.cshtml: -------------------------------------------------------------------------------- 1 | @page "/add" 2 | @inherits AddFeedModel 3 | 4 |
5 |
6 |
7 |

Add Feed

8 |
9 |
10 |
11 |
12 |
What feed do you want to follow ?
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 | @if (IsBusy) 31 | { 32 |
33 |
34 | Loading... 35 |
36 |
37 | } 38 | @if (IsError) 39 | { 40 |
41 |
42 |
Error parsing feed. Please try again
43 |
44 |
45 | } 46 | @if (Feed != null) 47 | { 48 |
49 |
50 |
Feed source details:
51 |
52 |
53 |
54 | @if (!string.IsNullOrEmpty(Feed.ImageUrl)) 55 | { 56 |
57 | 58 |
59 | } 60 |
61 |

@Feed.Title

62 |

63 |

@Feed.Description
64 |

65 |

66 | @foreach (var item in Feed.Items) 67 | { 68 | @item.Title
69 | } 70 |

71 |
72 | 73 |
74 |
75 |
76 | 77 |
78 |
79 | } 80 |
-------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Organize/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page "/organize" 2 | @inherits IndexModel 3 | 4 | 5 | @if (IsBusy) 6 | { 7 |

Loading feeds ...

8 | } 9 | else 10 | { 11 | 16 |
17 |
18 |

Organize Sources

19 |
20 |
21 |
22 |
23 | @if (Feeds.Count > 0) 24 | { 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | @foreach (var feed in Feeds) 35 | { 36 | 37 | 50 | 53 | 59 | 60 | } 61 | 62 |
TitleWebsiteAction
38 | @if (feed.ImageUrl != null) 39 | { 40 | 41 | } 42 | else 43 | { 44 |
45 | @feed.Title.Substring(0, 1) 46 |
47 | } 48 | @feed.Title 49 |
51 | @feed.WebsiteUrl 52 | 54 |
55 | 56 | 57 |
58 |
63 | } 64 | else 65 | { 66 |

No entries.

67 | } 68 |
69 |
70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazor RSS Reader 2 | A simple RSS reader sample application built using Blazor V0.4.0 (https://blazor.net). Note: Blazor is a experimental tech from Microsoft 3 | 4 | # Live Site 5 | Blazor RSS Reader is hosted on Surge and the here is the link to Live Demo - [https://blazorrssreader.surge.sh](https://blazorrssreader.surge.sh) 6 | 7 | # Screen Shots 8 | 9 | ##### Splash Screen 10 | Splash Screen 11 | 12 | ##### Home Page 13 | Home Page 14 | 15 | ##### Feed Title View 16 | Feed Title View 17 | 18 | ##### Feed Magazine View 19 | Feed Magazine View 20 | 21 | ##### Feed Cards View 22 | Feed Cards View 23 | 24 | ##### Feed Article View 25 | Feed Article View 26 | 27 | ##### Reading Post 28 | Reading Post 29 | 30 | ##### Add Feed 31 | Add Feed 32 | 33 | ##### Organize Sources 34 | Organize Sources 35 | 36 | ##### Edit Feed Title 37 | Edit Feed 38 | 39 | # Working with the code base 40 | 41 | ## 1. Pre-Requisites: 42 | 43 | ### Get Blazor 0.7.0 44 | To get setup with Blazor 0.7.0: 45 | 1. Install the [.NET Core 2.1 SDK](https://go.microsoft.com/fwlink/?linkid=873092) (2.1.500 or later). 46 | 2. Install [Visual Studio 2017](https://go.microsoft.com/fwlink/?linkid=873093) (15.9 or later) with the ASP.NET and web development workload selected. 47 | 3. Install the latest [Blazor Language Services extension](https://go.microsoft.com/fwlink/?linkid=870389) from the Visual Studio Marketplace. 48 | 4. To install the Blazor templates on the command-line: 49 | ``` 50 | 51 | dotnet new -i Microsoft.AspNetCore.Blazor.Templates 52 | 53 | ``` 54 | 55 | You can find getting started instructions, docs, and tutorials for Blazor at https://blazor.net. 56 | 57 | ## 2. Clone source code 58 | 1. Clone the source code on your machine. 59 | 2. Open `src\BlazorRssReader.sln` in Visual Studio. 60 | 3. Start hacking. 61 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Listing/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page "/feed/{FeedId}" 2 | @inherits IndexModel 3 | 4 | @if (IsBusy) 5 | { 6 |

Loading feed ...

7 | } 8 | else 9 | { 10 | @if (Feed != null) 11 | { 12 | 17 |
18 |
19 |

@Feed.Title

20 | @Feed.Description 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 | 31 | 34 | 37 | 40 |
41 |
42 |
43 |
44 | } 45 |
46 |
47 | 48 | @if (FeedItems != null) 49 | { 50 | @if (ViewType == "Title") 51 | { 52 | 53 | } 54 | @if (ViewType == "Magazine") 55 | { 56 | 57 | } 58 | @if (ViewType == "Cards") 59 | { 60 | 61 | } 62 | @if (ViewType == "Article") 63 | { 64 | 65 | } 66 | } 67 |
68 |
69 | } 70 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Pages/Listing/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Blazored.Storage; 2 | using BlazorRssReader.Models; 3 | using BlazorRssReader.Services; 4 | using Microsoft.AspNetCore.Blazor.Components; 5 | using Microsoft.AspNetCore.Blazor.Services; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Toolkit.Parsers; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | using System.Web; 11 | 12 | namespace BlazorRssReader.Pages.Listing 13 | { 14 | public class IndexModel : BlazorComponent 15 | { 16 | [Parameter] protected string FeedId { get; set; } = ""; 17 | [Inject] ILogger Logger { get; set; } 18 | [Inject] FeedService FeedService { get; set; } 19 | [Inject] IUriHelper UriHelper { get; set; } 20 | [Inject] Session Session { get; set; } 21 | [Inject] ILocalStorage Storage { get; set; } 22 | 23 | public Feed Feed { get; set; } = null; 24 | public List FeedItems { get; set; } = null; 25 | public bool IsBusy { get; set; } = false; 26 | public string ViewType { get; set; } 27 | 28 | protected override async Task OnParametersSetAsync() 29 | { 30 | if (string.IsNullOrEmpty(FeedId)) return; 31 | 32 | IsBusy = true; 33 | ClearFeedItems(); 34 | await SetViewType(); 35 | await LoadFeedDetails(); 36 | await LoadFeedItems(); 37 | IsBusy = false; 38 | } 39 | 40 | private void ClearFeedItems() 41 | { 42 | FeedItems = null; 43 | Session.SelectedFeedItems = null; 44 | } 45 | 46 | private async Task SetViewType() 47 | { 48 | var viewType = await Storage.GetItem("blazor.rss.viewtype"); 49 | ViewType = string.IsNullOrEmpty(viewType) ? "Title" : viewType; 50 | } 51 | 52 | public void OnArticleViewClick() 53 | { 54 | Storage.SetItem("blazor.rss.viewtype","Article"); 55 | ViewType = "Article"; 56 | } 57 | 58 | public void OnTitleViewClick() 59 | { 60 | Storage.SetItem("blazor.rss.viewtype", "Title"); 61 | 62 | ViewType = "Title"; 63 | } 64 | 65 | public void OnCardsViewClick() 66 | { 67 | Storage.SetItem("blazor.rss.viewtype", "Cards"); 68 | 69 | ViewType = "Cards"; 70 | } 71 | 72 | public void OnMagazineViewClick() 73 | { 74 | Storage.SetItem("blazor.rss.viewtype", "Magazine"); 75 | 76 | ViewType = "Magazine"; 77 | } 78 | 79 | public void OnFeedItemClick(RssSchema item) 80 | { 81 | Session.SelectedEntry = item; 82 | Session.SelectedFeed = Feed; 83 | Session.SelectedFeedItems = FeedItems; 84 | UriHelper.NavigateTo($"/entry/{HttpUtility.UrlEncode(item.InternalID)}"); 85 | } 86 | 87 | public async Task OnRefreshFeed() 88 | { 89 | IsBusy = true; 90 | await LoadFeedItems(); 91 | IsBusy = false; 92 | } 93 | 94 | private async Task LoadFeedItems() 95 | { 96 | if(Session.SelectedFeedItems != null) 97 | { 98 | FeedItems = Session.SelectedFeedItems; 99 | } 100 | else 101 | { 102 | FeedItems = await FeedService.GetFeedItems(Feed); 103 | } 104 | } 105 | 106 | private async Task LoadFeedDetails() 107 | { 108 | Feed = await FeedService.GetFeedDetails(FeedId); 109 | } 110 | 111 | 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/BlazorRssReader/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BlazorRssReader 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Blazor RSS Reader 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |
26 |

Blazor RSS Reader

27 |

RSS reader built with Blazor

28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/AtomParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Xml.Linq; 7 | 8 | namespace Microsoft.Toolkit.Parsers 9 | { 10 | /// 11 | /// Parser for Atom endpoints. 12 | /// 13 | internal class AtomParser : BaseRssParser 14 | { 15 | /// 16 | /// Atom reader implementation to parse Atom content. 17 | /// 18 | /// XDocument to parse. 19 | /// Strong typed response. 20 | public override IEnumerable LoadFeed(XDocument doc) 21 | { 22 | Collection feed = new Collection(); 23 | 24 | if (doc.Root == null) 25 | { 26 | return feed; 27 | } 28 | 29 | var items = doc.Root.Elements(doc.Root.GetDefaultNamespace() + "entry").Select(item => GetRssSchema(item)).ToList(); 30 | 31 | feed = new Collection(items); 32 | 33 | return feed; 34 | } 35 | 36 | /// 37 | /// Retieves strong type for passed item. 38 | /// 39 | /// XElement to parse. 40 | /// Strong typed object. 41 | private static RssSchema GetRssSchema(XElement item) 42 | { 43 | RssSchema rssItem = new RssSchema 44 | { 45 | Author = GetItemAuthor(item), 46 | Title = item.GetSafeElementString("title").Trim().DecodeHtml(), 47 | ImageUrl = GetItemImage(item), 48 | PublishDate = item.GetSafeElementDate("published"), 49 | FeedUrl = item.GetLink("alternate"), 50 | }; 51 | 52 | var content = GetItemContent(item); 53 | 54 | // Removes scripts from html 55 | if (!string.IsNullOrEmpty(content)) 56 | { 57 | rssItem.Summary = ProcessHtmlSummary(content); 58 | rssItem.Content = ProcessHtmlContent(content); 59 | } 60 | 61 | string id = item.GetSafeElementString("guid").Trim(); 62 | if (string.IsNullOrEmpty(id)) 63 | { 64 | id = item.GetSafeElementString("id").Trim(); 65 | if (string.IsNullOrEmpty(id)) 66 | { 67 | id = rssItem.FeedUrl; 68 | } 69 | } 70 | 71 | rssItem.InternalID = id; 72 | return rssItem; 73 | } 74 | 75 | /// 76 | /// Retrieves item author from XElement. 77 | /// 78 | /// XElement item. 79 | /// String of Item Author. 80 | private static string GetItemAuthor(XElement item) 81 | { 82 | var content = string.Empty; 83 | 84 | if (item != null && item.Element(item.GetDefaultNamespace() + "author") != null) 85 | { 86 | content = item.Element(item.GetDefaultNamespace() + "author").GetSafeElementString("name"); 87 | } 88 | 89 | if (string.IsNullOrEmpty(content)) 90 | { 91 | content = item.GetSafeElementString("author"); 92 | } 93 | 94 | return content; 95 | } 96 | 97 | /// 98 | /// Returns item image from XElement item. 99 | /// 100 | /// XElement item. 101 | /// String pointing to item image. 102 | private static string GetItemImage(XElement item) 103 | { 104 | if (!string.IsNullOrEmpty(item.GetSafeElementString("image"))) 105 | { 106 | return item.GetSafeElementString("image"); 107 | } 108 | 109 | return item.GetImage(); 110 | } 111 | 112 | /// 113 | /// Returns item content from XElement item. 114 | /// 115 | /// XElement item. 116 | /// String of item content. 117 | private static string GetItemContent(XElement item) 118 | { 119 | var content = item.GetSafeElementString("description"); 120 | if (string.IsNullOrEmpty(content)) 121 | { 122 | content = item.GetSafeElementString("content"); 123 | } 124 | 125 | if (string.IsNullOrEmpty(content)) 126 | { 127 | content = item.GetSafeElementString("summary"); 128 | } 129 | 130 | return content; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/BlazorRssReader/Services/FeedService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.ServiceModel.Syndication; 7 | using System.Threading.Tasks; 8 | using System.Xml; 9 | using System.Xml.Linq; 10 | using System.Xml.XPath; 11 | using BlazorRssReader.Models; 12 | using System.Collections.Generic; 13 | using Blazored.Storage; 14 | using Microsoft.Toolkit.Parsers; 15 | 16 | namespace BlazorRssReader.Services 17 | { 18 | public class FeedService 19 | { 20 | readonly HttpClient _client; 21 | readonly ILogger _logger; 22 | readonly ILocalStorage _localStorage; 23 | public FeedService(HttpClient httpClient, ILocalStorage localStorage, ILogger logger) 24 | { 25 | _client = httpClient; 26 | _logger = logger; 27 | _localStorage = localStorage; 28 | } 29 | 30 | public async Task> GetFeeds() 31 | { 32 | var feeds = await _localStorage.GetItem>("blazor.rss.feeds") ?? new List(); 33 | return feeds; 34 | } 35 | 36 | public async Task GetFeedDetails(string feedId) 37 | { 38 | var feeds = await GetFeeds(); 39 | var feed = feeds.SingleOrDefault(f => f.Id.ToString() == feedId); 40 | return feed; 41 | } 42 | public async Task GetFeedMetadata(string feedUrl) 43 | { 44 | try 45 | { 46 | Feed feed = null; 47 | SyndicationFeed syndicationFeed = await GetSyndicationFeed(feedUrl); 48 | if (syndicationFeed != null) 49 | { 50 | feed = new Feed 51 | { 52 | Id = Guid.NewGuid(), 53 | Description = syndicationFeed.Description?.Text, 54 | ImageUrl = syndicationFeed.ImageUrl?.AbsoluteUri, 55 | Items = syndicationFeed.Items.Take(3).Select(i => new FeedItem 56 | { 57 | Title = i.Title.Text, 58 | Descriiption = i.Summary.Text, 59 | Url = i.Links[0].Uri.AbsoluteUri 60 | }).ToList(), 61 | Title = syndicationFeed.Title?.Text, 62 | Url = feedUrl, 63 | WebsiteUrl = syndicationFeed.Links.SingleOrDefault(l => l.RelationshipType == "alternate")?.Uri.AbsoluteUri, 64 | LastUpdate = syndicationFeed.LastUpdatedTime.DateTime 65 | }; 66 | } 67 | return feed; 68 | } 69 | catch (Exception ex) 70 | { 71 | _logger.LogCritical(ex, "Error Parsing Feed", null); 72 | throw; 73 | } 74 | } 75 | 76 | public async Task> GetFeedItems(Feed selectedFeed) 77 | { 78 | try 79 | { 80 | var rawContent = await GetRawContent(selectedFeed.Url); 81 | var feedContent = GetFeedContent(rawContent); 82 | List feedItems = new RssParser().Parse(feedContent).ToList(); 83 | return feedItems; 84 | } 85 | catch (Exception ex) 86 | { 87 | _logger.LogCritical(ex, "Error Parsing Feed", null); 88 | throw; 89 | } 90 | } 91 | 92 | public async Task AddFeed(Feed feed) 93 | { 94 | await Task.Run(async () => 95 | { 96 | feed.Items = null; 97 | var existingFeeds = await GetFeeds(); 98 | existingFeeds.Add(feed); 99 | await _localStorage.SetItem("blazor.rss.feeds", existingFeeds); 100 | }).ConfigureAwait(false); 101 | } 102 | 103 | public async Task UpdateFeed(string feedId, string feedTitle) 104 | { 105 | var feeds = await GetFeeds(); 106 | var feed = feeds.SingleOrDefault(f => f.Id.ToString() == feedId); 107 | feed.Title = feedTitle; 108 | await _localStorage.SetItem("blazor.rss.feeds", feeds); 109 | } 110 | 111 | public async Task DeleteFeed(Guid feedId) 112 | { 113 | await Task.Run(async () => 114 | { 115 | var existingFeeds = await GetFeeds(); 116 | var feedToDelete = existingFeeds.SingleOrDefault(f => f.Id == feedId); 117 | existingFeeds.Remove(feedToDelete); 118 | await _localStorage.SetItem("blazor.rss.feeds", existingFeeds); 119 | }).ConfigureAwait(false); 120 | } 121 | 122 | private async Task GetSyndicationFeed(string feedUrl) 123 | { 124 | var rawContent = await GetRawContent(feedUrl); 125 | var feedContent = GetFeedContent(rawContent); 126 | var syndicationFeed = SyndicationFeed.Load(XmlReader.Create(new StringReader(feedContent))); 127 | return syndicationFeed; 128 | } 129 | 130 | private string GetFeedContent(string rawContent) 131 | { 132 | var xDoc = XDocument.Load(new StringReader(rawContent)); 133 | var node = xDoc.XPathSelectElement("//query/results/*[1]"); 134 | var reader = node.CreateReader(); 135 | reader.MoveToContent(); 136 | var feedString = reader.ReadOuterXml(); 137 | return feedString; 138 | } 139 | 140 | private async Task GetRawContent(string feedUrl) 141 | { 142 | var url = $"https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20xml%20where%20url%20%3D%20'{feedUrl}'&format=xml"; 143 | var rawContent = await _client.GetStringAsync(url); 144 | return rawContent; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Net; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace Microsoft.Toolkit.Parsers 9 | { 10 | /// 11 | /// All common string extensions should go here 12 | /// 13 | public static class StringExtensions 14 | { 15 | internal const string PhoneNumberRegex = @"^\s*\+?\s*([0-9][\s-]*){9,}$"; 16 | internal const string CharactersRegex = "^[A-Za-z]+$"; 17 | internal const string EmailRegex = "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"; 18 | 19 | /// 20 | /// Regular expression of HTML tags to remove. 21 | /// 22 | private const string RemoveHtmlTagsRegex = @"(?>(?:[^>'""]+|'[^']*'|""[^""]*"")*)>"; 23 | 24 | /// 25 | /// Regular expression for removing comments. 26 | /// 27 | private static readonly Regex RemoveHtmlCommentsRegex = new Regex("", RegexOptions.Singleline); 28 | 29 | /// 30 | /// Regular expression for removing scripts. 31 | /// 32 | private static readonly Regex RemoveHtmlScriptsRegex = new Regex(@"(?s)|)", RegexOptions.Singleline | RegexOptions.IgnoreCase); 33 | 34 | /// 35 | /// Regular expression for removing styles. 36 | /// 37 | private static readonly Regex RemoveHtmlStylesRegex = new Regex(@"(?s)|)", RegexOptions.Singleline | RegexOptions.IgnoreCase); 38 | 39 | /// 40 | /// Returns whether said string is a valid email or not. 41 | /// Uses general Email Regex (RFC 5322 Official Standard) from emailregex.com 42 | /// 43 | /// string value. 44 | /// true for valid email.false otherwise 45 | public static bool IsEmail(this string str) 46 | { 47 | return Regex.IsMatch(str, EmailRegex); 48 | } 49 | 50 | /// 51 | /// Returns whether said string is a valid decimal number or not. 52 | /// 53 | /// true for valid decimal number.false otherwise 54 | public static bool IsDecimal(this string str) 55 | { 56 | return decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal _decimal); 57 | } 58 | 59 | /// 60 | /// Returns whether said string is a valid integer or not. 61 | /// 62 | /// string value. 63 | /// true for valid integer.false otherwise 64 | public static bool IsNumeric(this string str) 65 | { 66 | return int.TryParse(str, out int _integer); 67 | } 68 | 69 | /// 70 | /// Returns whether said string is a valid phonenumber or not. 71 | /// 72 | /// string value. 73 | /// true for valid phonenumber.false otherwise 74 | public static bool IsPhoneNumber(this string str) 75 | { 76 | return Regex.IsMatch(str, PhoneNumberRegex); 77 | } 78 | 79 | /// 80 | /// Returns whether said string contains only letters or not. 81 | /// 82 | /// string value. 83 | /// true for valid Character.false otherwise 84 | public static bool IsCharacterString(this string str) 85 | { 86 | return Regex.IsMatch(str, CharactersRegex); 87 | } 88 | 89 | /// 90 | /// Converts object into string. 91 | /// 92 | /// Object value. 93 | /// Returns string value. 94 | public static string ToSafeString(this object value) 95 | { 96 | return value?.ToString(); 97 | } 98 | 99 | /// 100 | /// Decode HTML string. 101 | /// 102 | /// HTML string. 103 | /// Returns decoded HTML string. 104 | public static string DecodeHtml(this string htmlText) 105 | { 106 | if (htmlText == null) 107 | { 108 | return null; 109 | } 110 | 111 | var ret = htmlText.FixHtml(); 112 | 113 | // Remove html tags 114 | ret = new Regex(RemoveHtmlTagsRegex).Replace(ret, string.Empty); 115 | 116 | return WebUtility.HtmlDecode(ret); 117 | } 118 | 119 | /// 120 | /// Applies regular expressions to string of HTML to remove comments, scripts, styles. 121 | /// 122 | /// HTML string to fix 123 | /// Fixed HTML string 124 | public static string FixHtml(this string html) 125 | { 126 | // Remove comments 127 | var withoutComments = RemoveHtmlCommentsRegex.Replace(html, string.Empty); 128 | 129 | // Remove scripts 130 | var withoutScripts = RemoveHtmlScriptsRegex.Replace(withoutComments, string.Empty); 131 | 132 | // Remove styles 133 | var withoutStyles = RemoveHtmlStylesRegex.Replace(withoutScripts, string.Empty); 134 | 135 | return withoutStyles; 136 | } 137 | 138 | /// 139 | /// Trims and Truncates the specified string to the specified length. 140 | /// 141 | /// The string to be truncated. 142 | /// The maximum length. 143 | /// Truncated string. 144 | public static string Truncate(this string value, int length) 145 | { 146 | return Truncate(value, length, false); 147 | } 148 | 149 | /// 150 | /// Trims and Truncates the specified string to the specified length. 151 | /// 152 | /// The string to be truncated. 153 | /// The maximum length. 154 | /// if set to true add a text ellipsis. 155 | /// Truncated string. 156 | public static string Truncate(this string value, int length, bool ellipsis) 157 | { 158 | if (!string.IsNullOrEmpty(value)) 159 | { 160 | value = value.Trim(); 161 | if (value.Length > length) 162 | { 163 | if (ellipsis) 164 | { 165 | return value.Substring(0, length) + "..."; 166 | } 167 | 168 | return value.Substring(0, length); 169 | } 170 | } 171 | 172 | return value ?? string.Empty; 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/Rss2Parser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Xml.Linq; 7 | 8 | namespace Microsoft.Toolkit.Parsers 9 | { 10 | /// 11 | /// Rss reader implementation to parse Rss content. 12 | /// 13 | internal class Rss2Parser : BaseRssParser 14 | { 15 | /// 16 | /// RDF Namespace Uri. 17 | /// 18 | private static readonly XNamespace NsRdfNamespaceUri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; 19 | 20 | /// 21 | /// RDF Elements Namespace Uri. 22 | /// 23 | private static readonly XNamespace NsRdfElementsNamespaceUri = "http://purl.org/dc/elements/1.1/"; 24 | 25 | /// 26 | /// RDF Content Namespace Uri. 27 | /// 28 | private static readonly XNamespace NsRdfContentNamespaceUri = "http://purl.org/rss/1.0/modules/content/"; 29 | 30 | /// 31 | /// This override load and parses the document and return a list of RssSchema values. 32 | /// 33 | /// XDocument to be loaded. 34 | /// Strongly typed list of feeds. 35 | public override IEnumerable LoadFeed(XDocument doc) 36 | { 37 | bool isRDF = false; 38 | var feed = new Collection(); 39 | XNamespace defaultNamespace = string.Empty; 40 | 41 | if (doc.Root != null) 42 | { 43 | isRDF = doc.Root.Name == (NsRdfNamespaceUri + "RDF"); 44 | defaultNamespace = doc.Root.GetDefaultNamespace(); 45 | } 46 | 47 | foreach (var item in doc.Descendants(defaultNamespace + "item")) 48 | { 49 | var rssItem = isRDF ? ParseRDFItem(item) : ParseRssItem(item); 50 | feed.Add(rssItem); 51 | } 52 | 53 | return feed; 54 | } 55 | 56 | /// 57 | /// Parses XElement item into strong typed object. 58 | /// 59 | /// XElement item to parse. 60 | /// Strong typed object. 61 | private static RssSchema ParseItem(XElement item) 62 | { 63 | var rssItem = new RssSchema(); 64 | rssItem.Title = item.GetSafeElementString("title").Trim().DecodeHtml(); 65 | rssItem.FeedUrl = item.GetSafeElementString("link"); 66 | 67 | rssItem.Author = GetItemAuthor(item); 68 | 69 | string content = item.GetSafeElementString("encoded", NsRdfContentNamespaceUri); 70 | if (string.IsNullOrEmpty(content)) 71 | { 72 | content = item.GetSafeElementString("description"); 73 | if (string.IsNullOrEmpty(content)) 74 | { 75 | content = item.GetSafeElementString("content"); 76 | } 77 | } 78 | 79 | var summary = item.GetSafeElementString("description"); 80 | if (string.IsNullOrEmpty(summary)) 81 | { 82 | summary = item.GetSafeElementString("encoded", NsRdfContentNamespaceUri); 83 | } 84 | 85 | // Removes scripts from html 86 | if (!string.IsNullOrEmpty(summary)) 87 | { 88 | rssItem.Summary = ProcessHtmlSummary(summary); 89 | } 90 | 91 | if (!string.IsNullOrEmpty(content)) 92 | { 93 | rssItem.Content = ProcessHtmlContent(content); 94 | } 95 | 96 | string id = item.GetSafeElementString("guid").Trim(); 97 | if (string.IsNullOrEmpty(id)) 98 | { 99 | id = item.GetSafeElementString("id").Trim(); 100 | if (string.IsNullOrEmpty(id)) 101 | { 102 | id = rssItem.FeedUrl; 103 | } 104 | } 105 | 106 | rssItem.InternalID = id; 107 | 108 | return rssItem; 109 | } 110 | 111 | /// 112 | /// Parses RSS version 1.0 objects. 113 | /// 114 | /// XElement item. 115 | /// Strong typed object. 116 | private static RssSchema ParseRDFItem(XElement item) 117 | { 118 | XNamespace ns = "http://search.yahoo.com/mrss/"; 119 | var rssItem = ParseItem(item); 120 | 121 | rssItem.PublishDate = item.GetSafeElementDate("date", NsRdfElementsNamespaceUri); 122 | 123 | string image = item.GetSafeElementString("image"); 124 | if (string.IsNullOrEmpty(image) && item.Elements(ns + "thumbnail").LastOrDefault() != null) 125 | { 126 | var element = item.Elements(ns + "thumbnail").Last(); 127 | image = element.Attribute("url").Value; 128 | } 129 | 130 | if (string.IsNullOrEmpty(image) && item.ToString().Contains("thumbnail")) 131 | { 132 | image = item.GetSafeElementString("thumbnail"); 133 | } 134 | 135 | if (string.IsNullOrEmpty(image)) 136 | { 137 | image = item.GetImage(); 138 | } 139 | 140 | rssItem.ImageUrl = image; 141 | 142 | return rssItem; 143 | } 144 | 145 | /// 146 | /// Parses RSS version 2.0 objects. 147 | /// 148 | /// XElement item. 149 | /// Strong typed object. 150 | private static RssSchema ParseRssItem(XElement item) 151 | { 152 | XNamespace ns = "http://search.yahoo.com/mrss/"; 153 | var rssItem = ParseItem(item); 154 | 155 | rssItem.PublishDate = item.GetSafeElementDate("pubDate"); 156 | 157 | string image = item.GetSafeElementString("image"); 158 | if (string.IsNullOrEmpty(image)) 159 | { 160 | image = item.GetImageFromEnclosure(); 161 | } 162 | 163 | if (string.IsNullOrEmpty(image) && item.Elements(ns + "content").LastOrDefault() != null) 164 | { 165 | var element = item.Elements(ns + "content").Last(); 166 | if (element.Attribute("type") != null && element.Attribute("type").Value.Contains("image/")) 167 | { 168 | image = element.Attribute("url").Value; 169 | } 170 | } 171 | 172 | if (string.IsNullOrEmpty(image) && item.Elements(ns + "thumbnail").LastOrDefault() != null) 173 | { 174 | var element = item.Elements(ns + "thumbnail").Last(); 175 | image = element.Attribute("url").Value; 176 | } 177 | 178 | if (string.IsNullOrEmpty(image) && item.ToString().Contains("thumbnail")) 179 | { 180 | image = item.GetSafeElementString("thumbnail"); 181 | } 182 | 183 | if (string.IsNullOrEmpty(image)) 184 | { 185 | image = item.GetImage(); 186 | } 187 | 188 | rssItem.Categories = item.GetSafeElementsString("category"); 189 | 190 | rssItem.ImageUrl = image; 191 | 192 | return rssItem; 193 | } 194 | 195 | /// 196 | /// Retrieve item author from item. 197 | /// 198 | /// XElement item. 199 | /// String of item author. 200 | private static string GetItemAuthor(XElement item) 201 | { 202 | var content = item.GetSafeElementString("creator", NsRdfElementsNamespaceUri).Trim(); 203 | if (string.IsNullOrEmpty(content)) 204 | { 205 | content = item.GetSafeElementString("author"); 206 | } 207 | 208 | return content; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/BlazorRssReader/RssParser/RssHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using System.Xml.Linq; 9 | 10 | namespace Microsoft.Toolkit.Parsers 11 | { 12 | /// 13 | /// Class with utilities for Rss related works. 14 | /// 15 | internal static class RssHelper 16 | { 17 | /// 18 | /// String for regular expression for image pattern. 19 | /// 20 | private const string ImagePattern = @""; 21 | 22 | /// 23 | /// String for regular xpression for hyperlink pattern. 24 | /// 25 | private const string HiperlinkPattern = @"]*?\s+)?href=""([^ ""]*)"""; 26 | 27 | /// 28 | /// String for regular expression for height pattern. 29 | /// 30 | private const string HeightPattern = @"height=(?:(['""])(?(?:(?!\1).)*)\1|(?\S+))"; 31 | 32 | /// 33 | /// String for regular expression for width pattern. 34 | /// 35 | private const string WidthPattern = @"width=(?:(['""])(?(?:(?!\1).)*)\1|(?\S+))"; 36 | 37 | /// 38 | /// Regular expression for image pattern. 39 | /// 40 | private static readonly Regex RegexImages = new Regex(ImagePattern, RegexOptions.IgnoreCase); 41 | 42 | /// 43 | /// Regular expression for hyperlink pattern. 44 | /// 45 | private static readonly Regex RegexLinks = new Regex(HiperlinkPattern, RegexOptions.IgnoreCase); 46 | 47 | /// 48 | /// Regular expression for height pattern. 49 | /// 50 | private static readonly Regex RegexHeight = new Regex(HeightPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline); 51 | 52 | /// 53 | /// Regular expression for width pattern. 54 | /// 55 | private static readonly Regex RegexWidth = new Regex(WidthPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline); 56 | 57 | /// 58 | /// Removes \t characters in the string and trim additional space and carriage returns. 59 | /// 60 | /// Text string. 61 | /// Sanitized string. 62 | public static string SanitizeString(this string text) 63 | { 64 | if (string.IsNullOrEmpty(text)) 65 | { 66 | return string.Empty; 67 | } 68 | 69 | var textArray = text.Split(new[] { "\t" }, StringSplitOptions.RemoveEmptyEntries); 70 | string sanitizedText = string.Empty; 71 | foreach (var item in textArray.ToList()) 72 | { 73 | sanitizedText += item.Trim(); 74 | } 75 | 76 | sanitizedText = string.Join(" ", Regex.Split(sanitizedText, @"(?:\r\n|\n|\r)")); 77 | 78 | return sanitizedText; 79 | } 80 | 81 | /// 82 | /// Get item date from xelement and element name. 83 | /// 84 | /// XElement item. 85 | /// Name of element. 86 | /// Item date. 87 | public static DateTime GetSafeElementDate(this XElement item, string elementName) 88 | { 89 | return GetSafeElementDate(item, elementName, item.GetDefaultNamespace()); 90 | } 91 | 92 | /// 93 | /// Get item date from xelement, element name and namespace. 94 | /// 95 | /// XElement item. 96 | /// Name of element. 97 | /// XNamespace namespace. 98 | /// Item date. 99 | public static DateTime GetSafeElementDate(this XElement item, string elementName, XNamespace xNamespace) 100 | { 101 | DateTime date; 102 | XElement element = item.Element(xNamespace + elementName); 103 | if (element == null) 104 | { 105 | return DateTime.Now; 106 | } 107 | 108 | if (TryParseDateTime(element.Value, out date)) 109 | { 110 | return date; 111 | } 112 | 113 | return DateTime.Now; 114 | } 115 | 116 | /// 117 | /// Get item string value for xelement and element name. 118 | /// 119 | /// XElement item. 120 | /// Name of element. 121 | /// Safe string. 122 | public static string GetSafeElementString(this XElement item, string elementName) 123 | { 124 | if (item == null) 125 | { 126 | return string.Empty; 127 | } 128 | 129 | return GetSafeElementString(item, elementName, item.GetDefaultNamespace()); 130 | } 131 | 132 | /// 133 | /// Get item string values for xelement and element name. 134 | /// 135 | /// XElement item. 136 | /// Name of the element. 137 | /// Safe list of string values. 138 | public static IEnumerable GetSafeElementsString(this XElement item, string elementName) 139 | { 140 | return GetSafeElementsString(item, elementName, item.GetDefaultNamespace()); 141 | } 142 | 143 | /// 144 | /// Get item string values for xelement, element name and namespace. 145 | /// 146 | /// XELement item. 147 | /// Name of element. 148 | /// XNamespace namespace. 149 | /// Safe list of string values. 150 | public static IEnumerable GetSafeElementsString(this XElement item, string elementName, XNamespace xNamespace) 151 | { 152 | if (item != null) 153 | { 154 | IEnumerable values = item.Elements(xNamespace + elementName); 155 | return values.Where(f => !string.IsNullOrEmpty(f.Value)) 156 | .Select(f => f.Value); 157 | } 158 | 159 | return Enumerable.Empty(); 160 | } 161 | 162 | /// 163 | /// Get item string value for xelement, element name and namespace. 164 | /// 165 | /// XElement item. 166 | /// Name of element. 167 | /// XNamespace namespace. 168 | /// Safe string. 169 | public static string GetSafeElementString(this XElement item, string elementName, XNamespace xNamespace) 170 | { 171 | if (item == null) 172 | { 173 | return string.Empty; 174 | } 175 | 176 | XElement value = item.Element(xNamespace + elementName); 177 | if (value != null) 178 | { 179 | return value.Value; 180 | } 181 | 182 | return string.Empty; 183 | } 184 | 185 | /// 186 | /// Get feed url to see full original information. 187 | /// 188 | /// XElement item. 189 | /// rel attribute value. 190 | /// String link. 191 | public static string GetLink(this XElement item, string rel) 192 | { 193 | IEnumerable links = item.Elements(item.GetDefaultNamespace() + "link"); 194 | var xElements = links as XElement[] ?? links.ToArray(); 195 | IEnumerable link = from l in xElements 196 | let xAttribute = l.Attribute("rel") 197 | where xAttribute != null && xAttribute.Value == rel 198 | let attribute = l.Attribute("href") 199 | where attribute != null 200 | select attribute.Value; 201 | var enumerable = link as string[] ?? link.ToArray(); 202 | if (!enumerable.Any() && xElements.Any()) 203 | { 204 | return xElements.FirstOrDefault().Attributes().First().Value; 205 | } 206 | 207 | return enumerable.FirstOrDefault(); 208 | } 209 | 210 | /// 211 | /// Get feed image. 212 | /// 213 | /// XElement item. 214 | /// Feed data image. 215 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The general catch is intended to avoid breaking the Data Provider by a Html decode exception")] 216 | public static string GetImage(this XElement item) 217 | { 218 | string feedDataImage = null; 219 | try 220 | { 221 | feedDataImage = GetImagesInHTMLString(item.Value).FirstOrDefault(); 222 | if (!string.IsNullOrEmpty(feedDataImage) && feedDataImage.EndsWith("'")) 223 | { 224 | feedDataImage = feedDataImage.Remove(feedDataImage.Length - 1); 225 | } 226 | } 227 | catch (Exception ex) 228 | { 229 | Debug.WriteLine(ex); 230 | } 231 | 232 | return feedDataImage; 233 | } 234 | 235 | /// 236 | /// Get the item image from the enclosure element http://www.w3schools.com/rss/rss_tag_enclosure.asp 237 | /// 238 | /// XElement item. 239 | /// Feed data image. 240 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The general catch is intended to avoid breaking the Data Provider by a Html decode exception")] 241 | public static string GetImageFromEnclosure(this XElement item) 242 | { 243 | string feedDataImage = null; 244 | try 245 | { 246 | XElement element = item.Element(item.GetDefaultNamespace() + "enclosure"); 247 | if (element == null) 248 | { 249 | return string.Empty; 250 | } 251 | 252 | var typeAttribute = element.Attribute("type"); 253 | if (!string.IsNullOrEmpty(typeAttribute?.Value) && typeAttribute.Value.StartsWith("image")) 254 | { 255 | var urlAttribute = element.Attribute("url"); 256 | feedDataImage = (!string.IsNullOrEmpty(urlAttribute?.Value)) ? 257 | urlAttribute.Value : string.Empty; 258 | } 259 | } 260 | catch (Exception ex) 261 | { 262 | Debug.WriteLine(ex); 263 | } 264 | 265 | return feedDataImage; 266 | } 267 | 268 | /// 269 | /// Tries to parse the original string to a datetime format. 270 | /// 271 | /// Input string. 272 | /// Parsed datetime. 273 | /// True if success 274 | public static bool TryParseDateTime(string s, out DateTime result) 275 | { 276 | if (DateTime.TryParse(s, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AllowWhiteSpaces, out result)) 277 | { 278 | return true; 279 | } 280 | 281 | int tzIndex = s.LastIndexOf(" "); 282 | if (tzIndex >= 0) 283 | { 284 | string tz = s.Substring(tzIndex, s.Length - tzIndex); 285 | string offset = TimeZoneToOffset(tz); 286 | if (offset != null) 287 | { 288 | string offsetDate = string.Format("{0} {1}", s.Substring(0, tzIndex), offset); 289 | return TryParseDateTime(offsetDate, out result); 290 | } 291 | } 292 | 293 | result = default(DateTime); 294 | return false; 295 | } 296 | 297 | /// 298 | /// Calculate and return timezone. 299 | /// 300 | /// Input string. 301 | /// Parsed timezone. 302 | public static string TimeZoneToOffset(string tz) 303 | { 304 | if (tz == null) 305 | { 306 | return null; 307 | } 308 | 309 | tz = tz.ToUpper().Trim(); 310 | 311 | if (timeZones.ContainsKey(tz)) 312 | { 313 | return timeZones[tz].First(); 314 | } 315 | 316 | return null; 317 | } 318 | 319 | /// 320 | /// Retrieve images from HTML string. 321 | /// 322 | /// String of HTML. 323 | /// List of images. 324 | private static IEnumerable GetImagesInHTMLString(string htmlString) 325 | { 326 | var images = new List(); 327 | foreach (Match match in RegexImages.Matches(htmlString)) 328 | { 329 | bool include = true; 330 | string tag = match.Value; 331 | 332 | // Ignores images with low size 333 | var matchHeight = RegexHeight.Match(tag); 334 | if (matchHeight.Success) 335 | { 336 | var heightValue = matchHeight.Groups["height"].Value; 337 | int size = 0; 338 | if (int.TryParse(heightValue, out size) && size < 10) 339 | { 340 | include = false; 341 | } 342 | } 343 | 344 | var matchWidth = RegexWidth.Match(tag); 345 | if (matchWidth.Success) 346 | { 347 | var widthValue = matchWidth.Groups["width"].Value; 348 | int size = 0; 349 | if (int.TryParse(widthValue, out size) && size < 10) 350 | { 351 | include = false; 352 | } 353 | } 354 | 355 | if (include) 356 | { 357 | images.Add(match.Groups[1].Value); 358 | } 359 | } 360 | 361 | foreach (Match match in RegexLinks.Matches(htmlString)) 362 | { 363 | var value = match.Groups[1].Value; 364 | if (value.Contains(".jpg") || value.Contains(".png")) 365 | { 366 | images.Add(value); 367 | } 368 | } 369 | 370 | return images; 371 | } 372 | 373 | /// 374 | /// Dictionary of timezones. 375 | /// 376 | private static Dictionary timeZones = new Dictionary 377 | { 378 | { "ACDT", new[] { "-1030", "Australian Central Daylight" } }, 379 | { "ACST", new[] { "-0930", "Australian Central Standard" } }, 380 | { "ADT", new[] { "+0300", "(US) Atlantic Daylight" } }, 381 | { "AEDT", new[] { "-1100", "Australian East Daylight" } }, 382 | { "AEST", new[] { "-1000", "Australian East Standard" } }, 383 | { "AHDT", new[] { "+0900", string.Empty } }, 384 | { "AHST", new[] { "+1000", string.Empty } }, 385 | { "AST", new[] { "+0400", "(US) Atlantic Standard" } }, 386 | { "AT", new[] { "+0200", "Azores" } }, 387 | { "AWDT", new[] { "-0900", "Australian West Daylight" } }, 388 | { "AWST", new[] { "-0800", "Australian West Standard" } }, 389 | { "BAT", new[] { "-0300", "Bhagdad" } }, 390 | { "BDST", new[] { "-0200", "British Double Summer" } }, 391 | { "BET", new[] { "+1100", "Bering Standard" } }, 392 | { "BST", new[] { "+0300", "Brazil Standard" } }, 393 | { "BT", new[] { "-0300", "Baghdad" } }, 394 | { "BZT2", new[] { "+0300", "Brazil Zone 2" } }, 395 | { "CADT", new[] { "-1030", "Central Australian Daylight" } }, 396 | { "CAST", new[] { "-0930", "Central Australian Standard" } }, 397 | { "CAT", new[] { "+1000", "Central Alaska" } }, 398 | { "CCT", new[] { "-0800", "China Coast" } }, 399 | { "CDT", new[] { "+0500", "(US) Central Daylight" } }, 400 | { "CED", new[] { "-0200", "Central European Daylight" } }, 401 | { "CET", new[] { "-0100", "Central European" } }, 402 | { "CST", new[] { "+0600", "(US) Central Standard" } }, 403 | { "EAST", new[] { "-1000", "Eastern Australian Standard" } }, 404 | { "EDT", new[] { "+0400", "(US) Eastern Daylight" } }, 405 | { "EED", new[] { "-0300", "Eastern European Daylight" } }, 406 | { "EET", new[] { "-0200", "Eastern Europe" } }, 407 | { "EEST", new[] { "-0300", "Eastern Europe Summer" } }, 408 | { "EST", new[] { "+0500", "(US) Eastern Standard" } }, 409 | { "FST", new[] { "-0200", "French Summer" } }, 410 | { "FWT", new[] { "-0100", "French Winter" } }, 411 | { "GMT", new[] { "+0000", "Greenwich Mean" } }, 412 | { "GST", new[] { "-1000", "Guam Standard" } }, 413 | { "HDT", new[] { "+0900", "Hawaii Daylight" } }, 414 | { "HST", new[] { "+1000", "Hawaii Standard" } }, 415 | { "IDLE", new[] { "-1200", "Internation Date Line East" } }, 416 | { "IDLW", new[] { "+1200", "Internation Date Line West" } }, 417 | { "IST", new[] { "-0530", "Indian Standard" } }, 418 | { "IT", new[] { "-0330", "Iran" } }, 419 | { "JST", new[] { "-0900", "Japan Standard" } }, 420 | { "JT", new[] { "-0700", "Java" } }, 421 | { "MDT", new[] { "+0600", "(US) Mountain Daylight" } }, 422 | { "MED", new[] { "-0200", "Middle European Daylight" } }, 423 | { "MET", new[] { "-0100", "Middle European" } }, 424 | { "MEST", new[] { "-0200", "Middle European Summer" } }, 425 | { "MEWT", new[] { "-0100", "Middle European Winter" } }, 426 | { "MST", new[] { "+0700", "(US) Mountain Standard" } }, 427 | { "MT", new[] { "-0800", "Moluccas" } }, 428 | { "NDT", new[] { "+0230", "Newfoundland Daylight" } }, 429 | { "NFT", new[] { "+0330", "Newfoundland" } }, 430 | { "NT", new[] { "+1100", "Nome" } }, 431 | { "NST", new[] { "-0630", "North Sumatra" } }, 432 | { "NZ", new[] { "-1100", "New Zealand " } }, 433 | { "NZST", new[] { "-1200", "New Zealand Standard" } }, 434 | { "NZDT", new[] { "-1300", "New Zealand Daylight" } }, 435 | { "NZT", new[] { "-1200", "New Zealand" } }, 436 | { "PDT", new[] { "+0700", "(US) Pacific Daylight" } }, 437 | { "PST", new[] { "+0800", "(US) Pacific Standard" } }, 438 | { "ROK", new[] { "-0900", "Republic of Korea" } }, 439 | { "SAD", new[] { "-1000", "South Australia Daylight" } }, 440 | { "SAST", new[] { "-0900", "South Australia Standard" } }, 441 | { "SAT", new[] { "-0900", "South Australia Standard" } }, 442 | { "SDT", new[] { "-1000", "South Australia Daylight" } }, 443 | { "SST", new[] { "-0200", "Swedish Summer" } }, 444 | { "SWT", new[] { "-0100", "Swedish Winter" } }, 445 | { "USZ3", new[] { "-0400", "Volga Time (Russia)" } }, 446 | { "USZ4", new[] { "-0500", "Ural Time (Russia)" } }, 447 | { "USZ5", new[] { "-0600", "West-Siberian Time (Russia) " } }, 448 | { "USZ6", new[] { "-0700", "Yenisei Time (Russia)" } }, 449 | { "UT", new[] { "+0000", "Universal Coordinated" } }, 450 | { "UTC", new[] { "+0000", "Universal Coordinated" } }, 451 | { "UZ10", new[] { "-1100", "Okhotsk Time (Russia)" } }, 452 | { "WAT", new[] { "+0100", "West Africa" } }, 453 | { "WET", new[] { "+0000", "West European" } }, 454 | { "WST", new[] { "-0800", "West Australian Standard" } }, 455 | { "YDT", new[] { "+0800", "Yukon Daylight" } }, 456 | { "YST", new[] { "+0900", "Yukon Standard" } } 457 | }; 458 | } 459 | } 460 | --------------------------------------------------------------------------------