├── .gitignore ├── CODE-OF-CONDUCT.md ├── LICENSE ├── Microsoft.SyndicationFeed.ReaderWriter.sln ├── README.md ├── build ├── package.nuspec └── sign.props ├── examples ├── AtomFeedReaderExample.cs ├── CreateSimpleRssFeedExample.cs ├── Microsoft.SyndicationFeed.ReaderWriter.Examples.csproj ├── ReadRssItemWithCustomFieldsExample.cs ├── RssReadFeedExample.cs └── RssWriteItemWithCustomElementExample.cs ├── src ├── Atom │ ├── AtomConstants.cs │ ├── AtomContributorTypes.cs │ ├── AtomElementNames.cs │ ├── AtomEntry.cs │ ├── AtomExtentions.cs │ ├── AtomFeedReader.cs │ ├── AtomFeedWriter.cs │ ├── AtomFormatter.cs │ ├── AtomImageTypes.cs │ ├── AtomLinkTypes.cs │ ├── AtomParser.cs │ └── IAtomEntry.cs ├── ISyndicationAttribute.cs ├── ISyndicationCategory.cs ├── ISyndicationContent.cs ├── ISyndicationFeedFormatter.cs ├── ISyndicationFeedParser.cs ├── ISyndicationFeedReader.cs ├── ISyndicationFeedWriter.cs ├── ISyndicationImage.cs ├── ISyndicationItem.cs ├── ISyndicationLink.cs ├── ISyndicationPerson.cs ├── Microsoft.SyndicationFeed.ReaderWriter.csproj ├── Rss │ ├── RssConstants.cs │ ├── RssContributorTypes.cs │ ├── RssElementNames.cs │ ├── RssFeedReader.cs │ ├── RssFeedWriter.cs │ ├── RssFormatter.cs │ ├── RssLinkTypes.cs │ └── RssParser.cs ├── SyndicationAttribute.cs ├── SyndicationCategory.cs ├── SyndicationContent.cs ├── SyndicationElementType.cs ├── SyndicationImage.cs ├── SyndicationItem.cs ├── SyndicationLink.cs ├── SyndicationPerson.cs ├── Utils │ ├── Converter.cs │ ├── DateTimeUtils.cs │ ├── UriUtils.cs │ ├── XmlExtentions.cs │ └── XmlUtils.cs ├── XmlFeedReader.cs └── XmlFeedWriter.cs └── tests ├── AtomReader.cs ├── AtomWriter.cs ├── Microsoft.SyndicationFeed.ReaderWriter.Tests.csproj ├── RssReader.cs ├── RssWriter.cs └── TestFeeds ├── CustomXml.xml ├── SimpleRssFeed.xml ├── internetRssFeed.xml ├── rss20-2items.xml ├── rss20.xml └── simpleAtomFeed.xml /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | ### VisualStudio ### 4 | 5 | # Tool Runtime Dir 6 | /[Tt]ools/ 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | x64/ 20 | x86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | msbuild.log 25 | msbuild.err 26 | msbuild.wrn 27 | 28 | # Cross building rootfs 29 | cross/rootfs/ 30 | 31 | # Visual Studio 2015 32 | .vs/ 33 | 34 | # Visual Studio 2015 Pre-CTP6 35 | *.sln.ide 36 | *.ide/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | #NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding addin-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | *.pubxml 145 | *.publishproj 146 | 147 | # NuGet Packages 148 | *.nuget.props 149 | *.nuget.targets 150 | *.nupkg 151 | **/packages/* 152 | 153 | # NuGet package restore lockfiles 154 | project.lock.json 155 | 156 | # Windows Azure Build Output 157 | csx/ 158 | *.build.csdef 159 | 160 | # Windows Store app package directory 161 | AppPackages/ 162 | 163 | # Others 164 | *.Cache 165 | ClientBin/ 166 | [Ss]tyle[Cc]op.* 167 | ~$* 168 | *.dbmdl 169 | *.dbproj.schemaview 170 | *.pfx 171 | *.publishsettings 172 | node_modules/ 173 | *.metaproj 174 | *.metaproj.tmp 175 | 176 | # RIA/Silverlight projects 177 | Generated_Code/ 178 | 179 | # Backup & report files from converting an old project file 180 | # to a newer Visual Studio version. Backup files are not needed, 181 | # because we have git ;-) 182 | _UpgradeReport_Files/ 183 | Backup*/ 184 | UpgradeLog*.XML 185 | UpgradeLog*.htm 186 | 187 | # SQL Server files 188 | *.mdf 189 | *.ldf 190 | 191 | # Business Intelligence projects 192 | *.rdl.data 193 | *.bim.layout 194 | *.bim_*.settings 195 | 196 | # Microsoft Fakes 197 | FakesAssemblies/ 198 | 199 | ### MonoDevelop ### 200 | 201 | *.pidb 202 | *.userprefs 203 | 204 | ### Windows ### 205 | 206 | # Windows image file caches 207 | Thumbs.db 208 | ehthumbs.db 209 | 210 | # Folder config file 211 | Desktop.ini 212 | 213 | # Recycle Bin used on file shares 214 | $RECYCLE.BIN/ 215 | 216 | # Windows Installer files 217 | *.cab 218 | *.msi 219 | *.msm 220 | *.msp 221 | 222 | # Windows shortcuts 223 | *.lnk 224 | 225 | ### Linux ### 226 | 227 | *~ 228 | 229 | # KDE directory preferences 230 | .directory 231 | 232 | ### OSX ### 233 | 234 | .DS_Store 235 | .AppleDouble 236 | .LSOverride 237 | 238 | # Icon must end with two \r 239 | Icon 240 | 241 | # Thumbnails 242 | ._* 243 | 244 | # Files that might appear on external disk 245 | .Spotlight-V100 246 | .Trashes 247 | 248 | # Directories potentially created on remote AFP share 249 | .AppleDB 250 | .AppleDesktop 251 | Network Trash Folder 252 | Temporary Items 253 | .apdisk 254 | 255 | # vim temporary files 256 | [._]*.s[a-w][a-z] 257 | [._]s[a-w][a-z] 258 | *.un~ 259 | Session.vim 260 | .netrwhist 261 | *~ 262 | 263 | # Visual Studio Code 264 | .vscode/ 265 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the code of conduct defined by the Contributor Covenant 4 | to clarify expected behavior in our community. 5 | 6 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 .NET Foundation 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 | -------------------------------------------------------------------------------- /Microsoft.SyndicationFeed.ReaderWriter.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SyndicationFeed.ReaderWriter", "src\Microsoft.SyndicationFeed.ReaderWriter.csproj", "{E8A15170-9B9F-4A6E-9FE8-8C694EB2B123}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SyndicationFeed.ReaderWriter.Tests", "tests\Microsoft.SyndicationFeed.ReaderWriter.Tests.csproj", "{CA320B78-D2F8-4A52-9EC3-F8D2D1F19B4F}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SyndicationFeed.ReaderWriter.Examples", "examples\Microsoft.SyndicationFeed.ReaderWriter.Examples.csproj", "{8ACF4B68-18C3-4835-9E21-812B4E109334}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {E8A15170-9B9F-4A6E-9FE8-8C694EB2B123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {E8A15170-9B9F-4A6E-9FE8-8C694EB2B123}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {E8A15170-9B9F-4A6E-9FE8-8C694EB2B123}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {E8A15170-9B9F-4A6E-9FE8-8C694EB2B123}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {CA320B78-D2F8-4A52-9EC3-F8D2D1F19B4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {CA320B78-D2F8-4A52-9EC3-F8D2D1F19B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {CA320B78-D2F8-4A52-9EC3-F8D2D1F19B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {CA320B78-D2F8-4A52-9EC3-F8D2D1F19B4F}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {8ACF4B68-18C3-4835-9E21-812B4E109334}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {8ACF4B68-18C3-4835-9E21-812B4E109334}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {8ACF4B68-18C3-4835-9E21-812B4E109334}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {8ACF4B68-18C3-4835-9E21-812B4E109334}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft.SyndicationFeed.ReaderWriter 2 | Microsoft.SyndicationFeed.ReaderWriter provides lightweight forward-only read/write APIs (similar to .NET XmlReader) to simplify operations with RSS 2.0 ([spec](http://cyber.harvard.edu/rss/rss.html)) and Atom ([spec](https://tools.ietf.org/html/rfc4287)) syndication feeds. It offers extensiblity to support custom feed elements and formatting. The workflow is async on demand, which enables this library to be used on syndication feeds of arbitrary size or stream latency. 3 | 4 | ### Requirements: 5 | * [Visual Studio 2017](https://www.visualstudio.com/vs/whatsnew/) 6 | 7 | ### Supports: 8 | * .NET Standard 1.3 9 | 10 | ### Building: 11 | * The solution will build in Visual Studio 2017 after cloning. 12 | 13 | ### Running Tests: 14 | * Open the solution in Visual Studio 2017. 15 | * Build the Tests project. 16 | * Open the Test Explorer and click "Run All" or run each test individually. 17 | 18 | # Examples 19 | Examples can be found [here](examples). 20 | 21 | ### Create an RssReader and Read a Feed ### 22 | ```cs 23 | using (var xmlReader = XmlReader.Create(filePath, new XmlReaderSettings() { Async = true })) 24 | { 25 | var feedReader = new RssFeedReader(xmlReader); 26 | 27 | while(await feedReader.Read()) 28 | { 29 | switch (feedReader.ElementType) 30 | { 31 | // Read category 32 | case SyndicationElementType.Category: 33 | ISyndicationCategory category = await feedReader.ReadCategory(); 34 | break; 35 | 36 | // Read Image 37 | case SyndicationElementType.Image: 38 | ISyndicationImage image = await feedReader.ReadImage(); 39 | break; 40 | 41 | // Read Item 42 | case SyndicationElementType.Item: 43 | ISyndicationItem item = await feedReader.ReadItem(); 44 | break; 45 | 46 | // Read link 47 | case SyndicationElementType.Link: 48 | ISyndicationLink link = await feedReader.ReadLink(); 49 | break; 50 | 51 | // Read Person 52 | case SyndicationElementType.Person: 53 | ISyndicationPerson person = await feedReader.ReadPerson(); 54 | break; 55 | 56 | // Read content 57 | default: 58 | ISyndicationContent content = await feedReader.ReadContent(); 59 | break; 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | ### Create an RssWriter and Write an Rss Item ### 66 | ```cs 67 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 68 | 69 | using (XmlWriter xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings() { Async = true, Indent = true })) 70 | { 71 | var writer = new RssFeedWriter(xmlWriter); 72 | 73 | // Create item 74 | var item = new SyndicationItem() 75 | { 76 | Title = "Rss Writer Avaliable", 77 | Description = "The new Rss Writer is now available as a NuGet Package!", 78 | Id = "https://www.nuget.org/packages/Microsoft.SyndicationFeed.ReaderWriter", 79 | Published = DateTimeOffset.UtcNow 80 | }; 81 | 82 | item.AddCategory(new SyndicationCategory("Technology")); 83 | item.AddContributor(new SyndicationPerson("test", "test@mail.com")); 84 | 85 | await writer.Write(item); 86 | xmlWriter.Flush(); 87 | } 88 | 89 | class StringWriterWithEncoding : StringWriter 90 | { 91 | private readonly Encoding _encoding; 92 | 93 | public StringWriterWithEncoding(Encoding encoding) 94 | { 95 | this._encoding = encoding; 96 | } 97 | 98 | public override Encoding Encoding { 99 | get { return _encoding; } 100 | } 101 | } 102 | ``` 103 | -------------------------------------------------------------------------------- /build/package.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.SyndicationFeed.ReaderWriter 5 | 1.0.2 6 | Microsoft 7 | Dotnet 8 | https://github.com/dotnet/SyndicationFeedReaderWriter/blob/master/LICENSE 9 | https://github.com/dotnet/SyndicationFeedReaderWriter 10 | false 11 | Microsoft.SyndicationFeed.ReaderWriter provides lightweight forward-only read/write APIs to simplify operations with RSS and Atom syndication feeds 12 | Microsoft.SyndicationFeed.ReaderWriter provides lightweight forward-only read/write APIs (similar to .NET XmlReader) to simplify operations with RSS and Atom syndication feeds. It offers extensiblity to support custom feed elements and formatting. The workflow is async on demand, which enables this library to be used on syndication feeds of arbitrary size or stream latency. 13 | Copyright 2017 14 | SyndicationFeed RSS Atom 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /build/sign.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0.2.0 6 | 7 | 8 | 9 | 10 | $(SigningIdentity) 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/AtomFeedReaderExample.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.SyndicationFeed; 6 | using Microsoft.SyndicationFeed.Atom; 7 | using System.Threading.Tasks; 8 | using System.Xml; 9 | 10 | /// 11 | /// Consumes an entire atom feed using the AtomFeedReader. 12 | /// 13 | class AtomFeedReaderExample 14 | { 15 | public static async Task ReadAtomFeed(string filePath) 16 | { 17 | // 18 | // Create an XmlReader from file 19 | // Example: ..\tests\TestFeeds\simpleAtomFeed.xml 20 | using (XmlReader xmlReader = XmlReader.Create(filePath, new XmlReaderSettings() { Async = true })) 21 | { 22 | // 23 | // Create an AtomFeedReader 24 | var reader = new AtomFeedReader(xmlReader); 25 | 26 | // 27 | // Read the feed 28 | while (await reader.Read()) 29 | { 30 | // 31 | // Check the type of the current element. 32 | switch (reader.ElementType) 33 | { 34 | // 35 | // Read category 36 | case SyndicationElementType.Category: 37 | ISyndicationCategory category = await reader.ReadCategory(); 38 | break; 39 | 40 | // 41 | // Read image 42 | case SyndicationElementType.Image: 43 | ISyndicationImage image = await reader.ReadImage(); 44 | break; 45 | 46 | // 47 | // Read entry 48 | case SyndicationElementType.Item: 49 | IAtomEntry entry = await reader.ReadEntry(); 50 | break; 51 | 52 | // 53 | // Read link 54 | case SyndicationElementType.Link: 55 | ISyndicationLink link = await reader.ReadLink(); 56 | break; 57 | 58 | // 59 | // Read person 60 | case SyndicationElementType.Person: 61 | ISyndicationPerson person = await reader.ReadPerson(); 62 | break; 63 | 64 | // 65 | // Read content 66 | default: 67 | ISyndicationContent content = await reader.ReadContent(); 68 | break; 69 | } 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /examples/CreateSimpleRssFeedExample.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.SyndicationFeed; 6 | using Microsoft.SyndicationFeed.Rss; 7 | using System; 8 | using System.IO; 9 | using System.Threading.Tasks; 10 | using System.Xml; 11 | 12 | /// 13 | /// Create an RSS 2.0 feed 14 | /// 15 | class CreateSimpleRssFeed 16 | { 17 | public static async Task WriteFeed() 18 | { 19 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 20 | 21 | using (XmlWriter xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings() { Async = true , Indent = true })) 22 | { 23 | var writer = new RssFeedWriter(xmlWriter); 24 | 25 | // 26 | // Add Title 27 | await writer.WriteTitle("Example of RssFeedWriter"); 28 | 29 | // 30 | // Add Description 31 | await writer.WriteDescription("Hello World, RSS 2.0!"); 32 | 33 | // 34 | // Add Link 35 | await writer.Write(new SyndicationLink(new Uri("https://github.com/dotnet/SyndicationFeedReaderWriter"))); 36 | 37 | // 38 | // Add managing editor 39 | await writer.Write(new SyndicationPerson("managingeditor", "managingeditor@contoso.com", RssContributorTypes.ManagingEditor)); 40 | 41 | // 42 | // Add publish date 43 | await writer.WritePubDate(DateTimeOffset.UtcNow); 44 | 45 | // 46 | // Add custom element 47 | var customElement = new SyndicationContent("customElement"); 48 | 49 | customElement.AddAttribute(new SyndicationAttribute("attr1", "true")); 50 | customElement.AddField(new SyndicationContent("Company", "Contoso")); 51 | 52 | await writer.Write(customElement); 53 | 54 | // 55 | // Add Items 56 | for (int i = 0; i < 5; ++i) 57 | { 58 | var item = new SyndicationItem() 59 | { 60 | Id = "https://www.nuget.org/packages/Microsoft.SyndicationFeed.ReaderWriter", 61 | Title = $"Item #{i + 1}", 62 | Description = "The new Microsoft.SyndicationFeed.ReaderWriter is now available as a NuGet package!", 63 | Published = DateTimeOffset.UtcNow 64 | }; 65 | 66 | item.AddLink(new SyndicationLink(new Uri("https://github.com/dotnet/SyndicationFeedReaderWriter"))); 67 | item.AddCategory(new SyndicationCategory("Technology")); 68 | item.AddContributor(new SyndicationPerson("user", "user@contoso.com")); 69 | 70 | await writer.Write(item); 71 | } 72 | 73 | // 74 | // Done 75 | xmlWriter.Flush(); 76 | } 77 | 78 | // 79 | // Ouput the feed 80 | Console.WriteLine(sw.ToString()); 81 | } 82 | 83 | class StringWriterWithEncoding : StringWriter 84 | { 85 | private readonly Encoding _encoding; 86 | 87 | public StringWriterWithEncoding(Encoding encoding) 88 | { 89 | this._encoding = encoding; 90 | } 91 | 92 | public override Encoding Encoding { 93 | get { return _encoding; } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /examples/Microsoft.SyndicationFeed.ReaderWriter.Examples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.3 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/ReadRssItemWithCustomFieldsExample.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.SyndicationFeed; 6 | using Microsoft.SyndicationFeed.Rss; 7 | using System; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using System.Xml; 11 | 12 | /// 13 | /// Reads RSS items with custom fields 14 | /// 15 | class ReadRssItemWithCustomFields 16 | { 17 | public static async Task ReadFeed(string filepath) 18 | { 19 | // 20 | // Create an XmlReader from file 21 | // Example: ..\tests\TestFeeds\rss20-2items.xml 22 | using (var xmlReader = XmlReader.Create(filepath, new XmlReaderSettings() { Async = true })) 23 | { 24 | var parser = new RssParser(); 25 | var feedReader = new RssFeedReader(xmlReader, parser); 26 | 27 | // 28 | // Read the feed 29 | while (await feedReader.Read()) 30 | { 31 | if (feedReader.ElementType == SyndicationElementType.Item) 32 | { 33 | // 34 | // Read the item as generic content 35 | ISyndicationContent content = await feedReader.ReadContent(); 36 | 37 | // 38 | // Parse the item if needed (unrecognized tags aren't available) 39 | // Utilize the existing parser 40 | ISyndicationItem item = parser.CreateItem(content); 41 | 42 | Console.WriteLine($"Item: {item.Title}"); 43 | 44 | // 45 | // Get field 46 | ISyndicationContent customElement = content.Fields.FirstOrDefault(f => f.Name == "example:customElement"); 47 | 48 | if (customElement != null) 49 | { 50 | Console.WriteLine($"{customElement.Name}: {customElement.Value}"); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/RssReadFeedExample.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.SyndicationFeed; 6 | using Microsoft.SyndicationFeed.Rss; 7 | using System.Threading.Tasks; 8 | using System.Xml; 9 | 10 | /// 11 | /// Rss 2.0 FeedReader that consumes an entire feed. 12 | /// 13 | class RssReadFeed 14 | { 15 | // Read an RssFeed 16 | public static async Task CreateRssFeedReaderExample(string filePath) 17 | { 18 | // Create an XmlReader 19 | // Example: ..\tests\TestFeeds\rss20-2items.xml 20 | using (var xmlReader = XmlReader.Create(filePath, new XmlReaderSettings() { Async = true })) 21 | { 22 | // Instantiate an Rss20FeedReader using the XmlReader. 23 | // This will assign as default an Rss20FeedParser as the parser. 24 | var feedReader = new RssFeedReader(xmlReader); 25 | 26 | // 27 | // Read the feed 28 | while (await feedReader.Read()) 29 | { 30 | switch (feedReader.ElementType) 31 | { 32 | // Read category 33 | case SyndicationElementType.Category: 34 | ISyndicationCategory category = await feedReader.ReadCategory(); 35 | break; 36 | 37 | // Read Image 38 | case SyndicationElementType.Image: 39 | ISyndicationImage image = await feedReader.ReadImage(); 40 | break; 41 | 42 | // Read Item 43 | case SyndicationElementType.Item: 44 | ISyndicationItem item = await feedReader.ReadItem(); 45 | break; 46 | 47 | // Read link 48 | case SyndicationElementType.Link: 49 | ISyndicationLink link = await feedReader.ReadLink(); 50 | break; 51 | 52 | // Read Person 53 | case SyndicationElementType.Person: 54 | ISyndicationPerson person = await feedReader.ReadPerson(); 55 | break; 56 | 57 | // Read content 58 | default: 59 | ISyndicationContent content = await feedReader.ReadContent(); 60 | break; 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/RssWriteItemWithCustomElementExample.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.SyndicationFeed; 6 | using Microsoft.SyndicationFeed.Rss; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Threading.Tasks; 11 | using System.Xml; 12 | 13 | 14 | /// 15 | /// Create a SyndicationItem and add a custom field. 16 | /// 17 | class RssWriteItemWithCustomElement 18 | { 19 | public static async Task WriteCustomItem() 20 | { 21 | const string ExampleNs = "http://contoso.com/syndication/feed/examples"; 22 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 23 | 24 | using (XmlWriter xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings() { Async = true, Indent = true })) 25 | { 26 | var attributes = new List() 27 | { 28 | new SyndicationAttribute("xmlns:example", ExampleNs) 29 | }; 30 | 31 | var formatter = new RssFormatter(attributes, xmlWriter.Settings); 32 | var writer = new RssFeedWriter(xmlWriter, attributes, formatter); 33 | 34 | // Create item 35 | var item = new SyndicationItem() 36 | { 37 | Title = "Rss Writer Available", 38 | Description = "The new RSS Writer is now available as a NuGet package!", 39 | Id = "https://www.nuget.org/packages/Microsoft.SyndicationFeed", 40 | Published = DateTimeOffset.UtcNow 41 | }; 42 | 43 | item.AddCategory(new SyndicationCategory("Technology")); 44 | item.AddContributor(new SyndicationPerson("test", "test@mail.com")); 45 | 46 | // 47 | // Format the item as SyndicationContent 48 | var content = new SyndicationContent(formatter.CreateContent(item)); 49 | 50 | // Add custom fields/attributes 51 | content.AddField(new SyndicationContent("customElement", ExampleNs, "Custom Value")); 52 | 53 | // Write 54 | await writer.Write(content); 55 | 56 | // Done 57 | xmlWriter.Flush(); 58 | } 59 | 60 | Console.WriteLine(sw.ToString()); 61 | } 62 | 63 | class StringWriterWithEncoding : StringWriter 64 | { 65 | private readonly Encoding _encoding; 66 | 67 | public StringWriterWithEncoding(Encoding encoding) 68 | { 69 | this._encoding = encoding; 70 | } 71 | 72 | public override Encoding Encoding { 73 | get { return _encoding; } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Atom/AtomConstants.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Atom 6 | { 7 | static class AtomConstants 8 | { 9 | public const string Atom10Namespace = "http://www.w3.org/2005/Atom"; 10 | public const string XhtmlNamespace = "http://www.w3.org/1999/xhtml"; 11 | public const string Atom10Prefix = "a10"; 12 | public const string PlainTextContentType = "text"; 13 | public const string XhtmlContentType = "xhtml"; 14 | public const string SpecificationLink = "http://atompub.org/2005/08/17/draft-ietf-atompub-format-11.html"; 15 | 16 | public const string Href = "href"; 17 | public const string Label = "label"; 18 | public const string Length = "length"; 19 | public const string Rel = "rel"; 20 | public const string Scheme = "scheme"; 21 | public const string Source = "src"; 22 | public const string Type = "type"; 23 | public const string Term = "term"; 24 | public const string Uri = "uri"; 25 | public const string Version = "version"; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Atom/AtomContributorTypes.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Atom 6 | { 7 | public static class AtomContributorTypes 8 | { 9 | public const string Author = "author"; 10 | public const string Contributor = "contributor"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Atom/AtomElementNames.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Atom 6 | { 7 | public static class AtomElementNames 8 | { 9 | public const string Category = "category"; 10 | public const string Content = "content"; 11 | public const string Email = "email"; 12 | public const string Entry = "entry"; 13 | public const string Feed = "feed"; 14 | public const string Generator = "generator"; 15 | public const string Icon = "icon"; 16 | public const string Id = "id"; 17 | public const string Link = "link"; 18 | public const string Logo = "logo"; 19 | public const string Name = "name"; 20 | public const string Published = "published"; 21 | public const string Rights = "rights"; 22 | public const string Source = "source"; 23 | public const string Subtitle = "subtitle"; 24 | public const string Summary = "summary"; 25 | public const string Title = "title"; 26 | public const string Updated = "updated"; 27 | public const string Uri = "uri"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Atom/AtomEntry.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Atom 6 | { 7 | public class AtomEntry : SyndicationItem, IAtomEntry 8 | { 9 | public AtomEntry() 10 | { 11 | } 12 | 13 | public AtomEntry(IAtomEntry item) 14 | : base(item) 15 | { 16 | ContentType = item.ContentType; 17 | Summary = item.Summary; 18 | Rights = item.Rights; 19 | } 20 | 21 | public string ContentType { get; set; } 22 | 23 | public string Summary { get; set; } 24 | 25 | public string Rights { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Atom/AtomExtentions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace Microsoft.SyndicationFeed.Atom 9 | { 10 | static class AtomAttributeExtentions 11 | { 12 | public static string GetAtom(this IEnumerable attributes, string name) 13 | { 14 | return attributes.FirstOrDefault(a => a.IsAtom(name))?.Value; 15 | } 16 | 17 | public static bool IsAtom(this ISyndicationAttribute attr, string name) 18 | { 19 | return attr.Name == name && (attr.Namespace == null || attr.Namespace == string.Empty || attr.Namespace == AtomConstants.Atom10Namespace); 20 | } 21 | } 22 | 23 | static class AtomContentExtentions 24 | { 25 | public static bool IsAtom(this ISyndicationContent content, string name) 26 | { 27 | return content.Name == name && (content.Namespace == null || content.Namespace == AtomConstants.Atom10Namespace); 28 | } 29 | 30 | public static bool IsAtom(this ISyndicationContent content) 31 | { 32 | return (content.Namespace == null || content.Namespace == AtomConstants.Atom10Namespace); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Atom/AtomFeedReader.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using System.Xml; 8 | 9 | namespace Microsoft.SyndicationFeed.Atom 10 | { 11 | public class AtomFeedReader : XmlFeedReader 12 | { 13 | private readonly XmlReader _reader; 14 | private bool _knownFeed; 15 | 16 | public AtomFeedReader(XmlReader reader) 17 | : this(reader, new AtomParser()) 18 | { 19 | } 20 | 21 | public AtomFeedReader(XmlReader reader, ISyndicationFeedParser parser) 22 | : base(reader, parser) 23 | { 24 | _reader = reader; 25 | } 26 | 27 | public override async Task Read() 28 | { 29 | if (!_knownFeed) 30 | { 31 | await InitRead(); 32 | _knownFeed = true; 33 | } 34 | 35 | return await base.Read(); 36 | } 37 | 38 | public virtual async Task ReadEntry() 39 | { 40 | IAtomEntry item = await base.ReadItem() as IAtomEntry; 41 | 42 | if (item == null) 43 | { 44 | throw new FormatException("Invalid Atom entry"); 45 | } 46 | 47 | return item; 48 | } 49 | 50 | protected override SyndicationElementType MapElementType(string elementName) 51 | { 52 | if (_reader.NamespaceURI != AtomConstants.Atom10Namespace) 53 | { 54 | return SyndicationElementType.Content; 55 | } 56 | 57 | switch (elementName) 58 | { 59 | case AtomElementNames.Entry: 60 | return SyndicationElementType.Item; 61 | 62 | case AtomElementNames.Link: 63 | return SyndicationElementType.Link; 64 | 65 | case AtomElementNames.Category: 66 | return SyndicationElementType.Category; 67 | 68 | case AtomElementNames.Logo: 69 | case AtomElementNames.Icon: 70 | return SyndicationElementType.Image; 71 | 72 | case AtomContributorTypes.Author: 73 | case AtomContributorTypes.Contributor: 74 | return SyndicationElementType.Person; 75 | 76 | default: 77 | return SyndicationElementType.Content; 78 | } 79 | } 80 | 81 | private async Task InitRead() 82 | { 83 | // Check 84 | 85 | if (_reader.IsStartElement(AtomElementNames.Feed, AtomConstants.Atom10Namespace)) 86 | { 87 | //Read 88 | await XmlUtils.ReadAsync(_reader); 89 | } 90 | else 91 | { 92 | throw new XmlException("Unknown Atom Feed"); 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Atom/AtomFeedWriter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using System.Xml; 10 | 11 | namespace Microsoft.SyndicationFeed.Atom 12 | { 13 | public class AtomFeedWriter : XmlFeedWriter 14 | { 15 | private readonly XmlWriter _writer; 16 | private readonly IEnumerable _attributes; 17 | private bool _feedStarted; 18 | 19 | public AtomFeedWriter(XmlWriter writer, IEnumerable attributes = null) 20 | : this(writer, attributes, null) 21 | { 22 | } 23 | 24 | public AtomFeedWriter(XmlWriter writer, IEnumerable attributes, ISyndicationFeedFormatter formatter) : 25 | this(writer, formatter, EnsureXmlNs(attributes ?? Enumerable.Empty())) 26 | { 27 | } 28 | 29 | private AtomFeedWriter(XmlWriter writer, ISyndicationFeedFormatter formatter, IEnumerable attributes) : 30 | base(writer, formatter ?? new AtomFormatter(attributes, writer.Settings)) 31 | { 32 | _writer = writer; 33 | _attributes = attributes; 34 | } 35 | 36 | public virtual Task WriteTitle(string value) 37 | { 38 | return WriteText(AtomElementNames.Title, value, null); 39 | } 40 | 41 | public virtual Task WriteSubtitle(string value) 42 | { 43 | return WriteText(AtomElementNames.Subtitle, value, null); 44 | } 45 | 46 | public virtual Task WriteId(string value) 47 | { 48 | if (value == null) 49 | { 50 | throw new ArgumentNullException(nameof(value)); 51 | } 52 | 53 | return WriteValue(AtomElementNames.Id, value); 54 | } 55 | 56 | public virtual Task WriteUpdated(DateTimeOffset dt) 57 | { 58 | if (dt == default(DateTimeOffset)) 59 | { 60 | throw new ArgumentException(nameof(dt)); 61 | } 62 | 63 | return WriteValue(AtomElementNames.Updated, dt); 64 | } 65 | 66 | public virtual Task WriteRights(string value) 67 | { 68 | return WriteText(AtomElementNames.Rights, value, null); 69 | } 70 | 71 | public virtual Task WriteGenerator(string value, string uri, string version) 72 | { 73 | if (value == null) 74 | { 75 | throw new ArgumentNullException(nameof(value)); 76 | } 77 | 78 | var generator = new SyndicationContent(AtomElementNames.Generator, value); 79 | 80 | if (!string.IsNullOrEmpty(uri)) 81 | { 82 | generator.AddAttribute(new SyndicationAttribute("uri", uri)); 83 | } 84 | 85 | if (!string.IsNullOrEmpty(version)) 86 | { 87 | generator.AddAttribute(new SyndicationAttribute("version", version)); 88 | } 89 | 90 | return Write(generator); 91 | } 92 | 93 | public virtual Task WriteText(string name, string value, string type) 94 | { 95 | if (string.IsNullOrEmpty(name)) 96 | { 97 | throw new ArgumentNullException(nameof(name)); 98 | } 99 | 100 | if (value == null) 101 | { 102 | throw new ArgumentNullException(nameof(value)); 103 | } 104 | 105 | var content = new SyndicationContent(name, value); 106 | 107 | if (!string.IsNullOrEmpty(type)) 108 | { 109 | content.AddAttribute(new SyndicationAttribute(AtomConstants.Type, type)); 110 | } 111 | 112 | return Write(content); 113 | } 114 | 115 | public override Task WriteRaw(string content) 116 | { 117 | if (!_feedStarted) 118 | { 119 | StartFeed(); 120 | } 121 | 122 | return XmlUtils.WriteRawAsync(_writer, content); 123 | } 124 | 125 | private void StartFeed() 126 | { 127 | ISyndicationAttribute xmlns = _attributes.FirstOrDefault(a => a.Name == "xmlns"); 128 | 129 | // Write 130 | if (xmlns != null) 131 | { 132 | _writer.WriteStartElement(AtomElementNames.Feed, xmlns.Value); 133 | } 134 | else 135 | { 136 | _writer.WriteStartElement(AtomElementNames.Feed); 137 | } 138 | 139 | // Add attributes 140 | foreach (var a in _attributes) 141 | { 142 | if (a != xmlns) 143 | { 144 | _writer.WriteSyndicationAttribute(a); 145 | } 146 | } 147 | 148 | _feedStarted = true; 149 | } 150 | 151 | private static IEnumerable EnsureXmlNs(IEnumerable attributes) 152 | { 153 | ISyndicationAttribute xmlnsAttr = attributes.FirstOrDefault(a => a.Name.StartsWith("xmlns") && a.Value == AtomConstants.Atom10Namespace); 154 | 155 | // 156 | // Insert Atom namespace if it doesn't already exist 157 | if (xmlnsAttr == null) 158 | { 159 | var list = new List(attributes); 160 | list.Insert(0, new SyndicationAttribute("xmlns", AtomConstants.Atom10Namespace)); 161 | 162 | attributes = list; 163 | } 164 | 165 | return attributes; 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /src/Atom/AtomImageTypes.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Atom 6 | { 7 | public static class AtomImageTypes 8 | { 9 | public const string Icon = "icon"; 10 | public const string Logo = "logo"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Atom/AtomLinkTypes.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Atom 6 | { 7 | public static class AtomLinkTypes 8 | { 9 | public const string Alternate = "alternate"; 10 | public const string Content = "content"; 11 | public const string Enclosure = "enclosure"; 12 | public const string Related = "related"; 13 | public const string Self = "self"; 14 | public const string Source = "source"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Atom/AtomParser.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Xml; 9 | 10 | namespace Microsoft.SyndicationFeed.Atom 11 | { 12 | public class AtomParser : ISyndicationFeedParser 13 | { 14 | public ISyndicationCategory ParseCategory(string value) 15 | { 16 | ISyndicationContent content = ParseContent(value); 17 | 18 | if (content.Name != AtomElementNames.Category) 19 | { 20 | throw new FormatException("Invalid Atom Category"); 21 | } 22 | 23 | return CreateCategory(content); 24 | } 25 | 26 | public ISyndicationImage ParseImage(string value) 27 | { 28 | ISyndicationContent content = ParseContent(value); 29 | 30 | if (content.Name != AtomElementNames.Logo && content.Name != AtomElementNames.Icon) 31 | { 32 | throw new FormatException("Invalid Atom Image"); 33 | } 34 | 35 | return CreateImage(content); 36 | } 37 | 38 | public ISyndicationItem ParseItem(string value) 39 | { 40 | return ParseEntry(value); 41 | } 42 | 43 | public IAtomEntry ParseEntry(string value) 44 | { 45 | ISyndicationContent content = ParseContent(value); 46 | 47 | if (content.Name != AtomElementNames.Entry) 48 | { 49 | throw new FormatException("Invalid Atom feed"); 50 | } 51 | 52 | return CreateEntry(content); 53 | } 54 | 55 | public ISyndicationLink ParseLink(string value) 56 | { 57 | ISyndicationContent content = ParseContent(value); 58 | 59 | if (content.Name != AtomElementNames.Link) 60 | { 61 | throw new FormatException("Invalid Atom Link"); 62 | } 63 | 64 | return CreateLink(content); 65 | } 66 | 67 | public ISyndicationPerson ParsePerson(string value) 68 | { 69 | ISyndicationContent content = ParseContent(value); 70 | 71 | if (content.Name != AtomContributorTypes.Author && content.Name != AtomContributorTypes.Contributor) 72 | { 73 | throw new FormatException("Invalid Atom person"); 74 | } 75 | 76 | return CreatePerson(content); 77 | } 78 | 79 | public ISyndicationContent ParseContent(string value) 80 | { 81 | if (string.IsNullOrEmpty(value)) 82 | { 83 | throw new ArgumentNullException(nameof(value)); 84 | } 85 | 86 | using (XmlReader reader = CreateXmlReader(value)) 87 | { 88 | reader.MoveToContent(); 89 | 90 | return ReadSyndicationContent(reader); 91 | } 92 | } 93 | 94 | public virtual bool TryParseValue(string value, out T result) 95 | { 96 | return Converter.TryParseValue(value, out result); 97 | } 98 | 99 | public virtual ISyndicationCategory CreateCategory(ISyndicationContent content) 100 | { 101 | if (content == null) 102 | { 103 | throw new ArgumentNullException(nameof(content)); 104 | } 105 | 106 | string term = content.Attributes.GetAtom(AtomConstants.Term); 107 | 108 | if (term == null) 109 | { 110 | throw new FormatException("Invalid Atom category, requires Term attribute"); 111 | } 112 | 113 | return new SyndicationCategory(term) 114 | { 115 | Scheme = content.Attributes.GetAtom(AtomConstants.Scheme), 116 | Label = content.Attributes.GetAtom(AtomConstants.Label) 117 | }; 118 | } 119 | 120 | public virtual ISyndicationImage CreateImage(ISyndicationContent content) 121 | { 122 | if (content == null) 123 | { 124 | throw new ArgumentNullException(nameof(content)); 125 | } 126 | 127 | if (!TryParseValue(content.Value, out Uri uri)) 128 | { 129 | throw new FormatException("Invalid Atom image url"); 130 | } 131 | 132 | return new SyndicationImage(uri, content.Name); 133 | } 134 | 135 | public virtual ISyndicationLink CreateLink(ISyndicationContent content) 136 | { 137 | if (content == null) 138 | { 139 | throw new ArgumentNullException(nameof(content)); 140 | } 141 | 142 | // 143 | // title 144 | string title = content.Attributes.GetAtom(AtomElementNames.Title); 145 | 146 | // type 147 | string type = content.Attributes.GetAtom(AtomConstants.Type); 148 | 149 | // 150 | // length 151 | long length = 0; 152 | TryParseValue(content.Attributes.GetAtom(AtomConstants.Length), out length); 153 | 154 | // 155 | // rel 156 | string rel = content.Attributes.GetAtom(AtomConstants.Rel) ?? ((content.Name == AtomElementNames.Link) ? AtomLinkTypes.Alternate : content.Name); 157 | 158 | // 159 | // href 160 | TryParseValue(content.Attributes.GetAtom(AtomConstants.Href), out Uri uri); 161 | 162 | // src 163 | if (uri == null) 164 | { 165 | TryParseValue(content.Attributes.GetAtom(AtomConstants.Source), out uri); 166 | } 167 | 168 | if (uri == null) 169 | { 170 | throw new FormatException("Invalid uri"); 171 | } 172 | 173 | return new SyndicationLink(uri, rel) 174 | { 175 | Title = title, 176 | Length = length, 177 | MediaType = type 178 | }; 179 | } 180 | 181 | public virtual ISyndicationPerson CreatePerson(ISyndicationContent content) 182 | { 183 | if (content == null) 184 | { 185 | throw new ArgumentNullException(nameof(content)); 186 | } 187 | 188 | string name = null; 189 | string email = null; 190 | string uri = null; 191 | 192 | foreach (var field in content.Fields) 193 | { 194 | // content does not contain atom's namespace. So if we receibe a different namespace we will ignore it. 195 | if (field.Namespace != AtomConstants.Atom10Namespace) 196 | { 197 | continue; 198 | } 199 | 200 | switch (field.Name) 201 | { 202 | // 203 | // Name 204 | case AtomElementNames.Name: 205 | name = field.Value; 206 | break; 207 | 208 | // 209 | // Email 210 | case AtomElementNames.Email: 211 | email = field.Value; 212 | break; 213 | 214 | // 215 | // Uri 216 | case AtomElementNames.Uri: 217 | uri = field.Value; 218 | break; 219 | // 220 | // Unrecognized field 221 | default: 222 | break; 223 | } 224 | } 225 | 226 | return new SyndicationPerson(name, email, content.Name) 227 | { 228 | Uri = uri 229 | }; 230 | } 231 | 232 | public virtual IAtomEntry CreateEntry(ISyndicationContent content) 233 | { 234 | if (content == null) 235 | { 236 | throw new ArgumentNullException(nameof(content)); 237 | } 238 | 239 | var item = new AtomEntry(); 240 | 241 | 242 | foreach (var field in content.Fields) 243 | { 244 | // content does not contain atom's namespace. So if we receibe a different namespace we will ignore it. 245 | if (field.Namespace != AtomConstants.Atom10Namespace) 246 | { 247 | continue; 248 | } 249 | 250 | switch (field.Name) 251 | { 252 | // 253 | // Category 254 | case AtomElementNames.Category: 255 | item.AddCategory(CreateCategory(field)); 256 | break; 257 | 258 | // 259 | // Content 260 | case AtomElementNames.Content: 261 | 262 | item.ContentType = field.Attributes.GetAtom(AtomConstants.Type) ?? AtomConstants.PlainTextContentType; 263 | 264 | if (field.Attributes.GetAtom(AtomConstants.Source) != null) 265 | { 266 | item.AddLink(CreateLink(field)); 267 | } 268 | else 269 | { 270 | item.Description = field.Value; 271 | } 272 | 273 | break; 274 | 275 | // 276 | // Author/Contributor 277 | case AtomContributorTypes.Author: 278 | case AtomContributorTypes.Contributor: 279 | item.AddContributor(CreatePerson(field)); 280 | break; 281 | 282 | // 283 | // Id 284 | case AtomElementNames.Id: 285 | item.Id = field.Value; 286 | break; 287 | 288 | // 289 | // Link 290 | case AtomElementNames.Link: 291 | item.AddLink(CreateLink(field)); 292 | break; 293 | 294 | // 295 | // Published 296 | case AtomElementNames.Published: 297 | if (TryParseValue(field.Value, out DateTimeOffset published)) 298 | { 299 | item.Published = published; 300 | } 301 | break; 302 | 303 | // 304 | // Rights 305 | case AtomElementNames.Rights: 306 | item.Rights = field.Value; 307 | break; 308 | 309 | // 310 | // Source 311 | case AtomElementNames.Source: 312 | item.AddLink(CreateSource(field)); 313 | break; 314 | // 315 | // Summary 316 | case AtomElementNames.Summary: 317 | item.Summary = field.Value; 318 | break; 319 | 320 | // 321 | // Title 322 | case AtomElementNames.Title: 323 | item.Title = field.Value; 324 | break; 325 | 326 | // 327 | // Updated 328 | case AtomElementNames.Updated: 329 | if (TryParseValue(field.Value, out DateTimeOffset updated)) 330 | { 331 | item.LastUpdated = updated; 332 | } 333 | break; 334 | 335 | // 336 | // Unrecognized tags 337 | default: 338 | break; 339 | } 340 | } 341 | 342 | 343 | return item; 344 | } 345 | 346 | public virtual ISyndicationLink CreateSource(ISyndicationContent content) 347 | { 348 | if (content == null) 349 | { 350 | throw new ArgumentNullException(nameof(content)); 351 | } 352 | 353 | Uri url = null; 354 | string title = null; 355 | DateTimeOffset lastUpdated; 356 | 357 | foreach (var field in content.Fields) 358 | { 359 | // content does not contain atom's namespace. So if we receibe a different namespace we will ignore it. 360 | if (field.Namespace != AtomConstants.Atom10Namespace) 361 | { 362 | continue; 363 | } 364 | 365 | switch (field.Name) 366 | { 367 | // 368 | // Id 369 | case AtomElementNames.Id: 370 | 371 | if (url == null) 372 | { 373 | TryParseValue(field.Value, out url); 374 | } 375 | 376 | break; 377 | 378 | // 379 | // Title 380 | case AtomElementNames.Title: 381 | title = field.Value; 382 | break; 383 | 384 | // 385 | // Updated 386 | case AtomElementNames.Updated: 387 | TryParseValue(field.Value, out lastUpdated); 388 | break; 389 | 390 | // 391 | // Link 392 | case AtomElementNames.Link: 393 | if(url == null) 394 | { 395 | url = CreateLink(field).Uri; 396 | } 397 | break; 398 | 399 | // 400 | // Unrecognized 401 | default: 402 | break; 403 | } 404 | } 405 | 406 | if (url == null) 407 | { 408 | throw new FormatException("Invalid source link"); 409 | } 410 | 411 | return new SyndicationLink(url, AtomLinkTypes.Source) 412 | { 413 | Title = title, 414 | LastUpdated = lastUpdated 415 | }; 416 | } 417 | 418 | private XmlReader CreateXmlReader(string value) 419 | { 420 | return XmlUtils.CreateXmlReader(value); 421 | } 422 | 423 | private static ISyndicationContent ReadSyndicationContent(XmlReader reader) 424 | { 425 | string type = null; 426 | 427 | var content = new SyndicationContent(reader.LocalName, reader.NamespaceURI, null); 428 | 429 | // 430 | // Attributes 431 | if (reader.HasAttributes) 432 | { 433 | while (reader.MoveToNextAttribute()) 434 | { 435 | ISyndicationAttribute attr = reader.ReadSyndicationAttribute(); 436 | 437 | if (attr != null) 438 | { 439 | if (type == null && attr.IsAtom(AtomConstants.Type)) 440 | { 441 | type = attr.Value; 442 | } 443 | 444 | content.AddAttribute(attr); 445 | } 446 | } 447 | 448 | reader.MoveToContent(); 449 | } 450 | 451 | // 452 | // Content 453 | if (!reader.IsEmptyElement) 454 | { 455 | // 456 | // Xml (applies to ) 457 | if (XmlUtils.IsXmlMediaType(type) && content.IsAtom(AtomElementNames.Content)) 458 | { 459 | if (reader.NodeType != XmlNodeType.Element) 460 | { 461 | throw new FormatException($"Invalid Xml element"); 462 | } 463 | 464 | content.Value = reader.ReadInnerXml(); 465 | } 466 | else 467 | { 468 | reader.ReadStartElement(); 469 | 470 | // 471 | // Xhtml 472 | if (XmlUtils.IsXhtmlMediaType(type) && content.IsAtom()) 473 | { 474 | if (reader.NamespaceURI != AtomConstants.XhtmlNamespace) 475 | { 476 | throw new FormatException($"Invalid Xhtml namespace"); 477 | } 478 | 479 | content.Value = reader.ReadInnerXml(); 480 | } 481 | // 482 | // Text/Html 483 | else if (reader.HasValue) 484 | { 485 | content.Value = reader.ReadContentAsString(); 486 | } 487 | // 488 | // Children 489 | else 490 | { 491 | while (reader.IsStartElement()) 492 | { 493 | content.AddField(ReadSyndicationContent(reader)); 494 | } 495 | } 496 | 497 | reader.ReadEndElement(); // end 498 | } 499 | } 500 | else 501 | { 502 | reader.Skip(); 503 | } 504 | 505 | return content; 506 | } 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /src/Atom/IAtomEntry.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed 6 | { 7 | public interface IAtomEntry : ISyndicationItem 8 | { 9 | string ContentType { get; } 10 | 11 | string Summary { get; } 12 | 13 | string Rights { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/ISyndicationAttribute.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed 6 | { 7 | public interface ISyndicationAttribute 8 | { 9 | string Name { get; } 10 | 11 | string Namespace { get; } 12 | 13 | string Value { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/ISyndicationCategory.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed 6 | { 7 | public interface ISyndicationCategory 8 | { 9 | string Name { get; } 10 | 11 | string Label { get; } 12 | 13 | string Scheme { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/ISyndicationContent.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public interface ISyndicationContent 10 | { 11 | string Name { get; } 12 | 13 | string Namespace { get; } 14 | 15 | string Value { get; } 16 | 17 | IEnumerable Fields { get; } 18 | 19 | IEnumerable Attributes { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/ISyndicationFeedFormatter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | 6 | namespace Microsoft.SyndicationFeed 7 | { 8 | public interface ISyndicationFeedFormatter 9 | { 10 | string Format(ISyndicationContent content); 11 | 12 | string Format(ISyndicationCategory category); 13 | 14 | string Format(ISyndicationImage image); 15 | 16 | string Format(ISyndicationItem item); 17 | 18 | string Format(ISyndicationPerson person); 19 | 20 | string Format(ISyndicationLink link); 21 | 22 | string FormatValue(T value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ISyndicationFeedParser.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed 6 | { 7 | public interface ISyndicationFeedParser 8 | { 9 | ISyndicationItem ParseItem(string value); 10 | 11 | ISyndicationLink ParseLink(string value); 12 | 13 | ISyndicationPerson ParsePerson(string value); 14 | 15 | ISyndicationCategory ParseCategory(string value); 16 | 17 | ISyndicationImage ParseImage(string value); 18 | 19 | ISyndicationContent ParseContent(string value); 20 | 21 | bool TryParseValue(string value, out T result); 22 | } 23 | } -------------------------------------------------------------------------------- /src/ISyndicationFeedReader.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Threading.Tasks; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public interface ISyndicationFeedReader 10 | { 11 | Task Read(); 12 | 13 | Task Skip(); 14 | 15 | SyndicationElementType ElementType { get; } 16 | 17 | string ElementName { get; } 18 | 19 | Task ReadItem(); 20 | 21 | Task ReadLink(); 22 | 23 | Task ReadPerson(); 24 | 25 | Task ReadImage(); 26 | 27 | Task ReadContent(); 28 | 29 | Task ReadCategory(); 30 | 31 | Task ReadValue(); 32 | 33 | Task ReadElementAsString(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ISyndicationFeedWriter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Threading.Tasks; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public interface ISyndicationFeedWriter 10 | { 11 | Task Write(ISyndicationContent content); 12 | 13 | Task Write(ISyndicationCategory category); 14 | 15 | Task Write(ISyndicationImage image); 16 | 17 | Task Write(ISyndicationItem item); 18 | 19 | Task Write(ISyndicationPerson person); 20 | 21 | Task Write(ISyndicationLink link); 22 | 23 | Task WriteValue(string name, T value); 24 | 25 | Task WriteRaw(string content); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ISyndicationImage.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information.using System; 4 | 5 | using System; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public interface ISyndicationImage 10 | { 11 | string Title { get; } 12 | 13 | Uri Url { get; } 14 | 15 | ISyndicationLink Link { get; } 16 | 17 | string RelationshipType { get; } 18 | 19 | string Description { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ISyndicationItem.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace Microsoft.SyndicationFeed 9 | { 10 | public interface ISyndicationItem 11 | { 12 | string Id { get; } 13 | 14 | string Title { get; } 15 | 16 | string Description { get; } 17 | 18 | IEnumerable Categories { get; } 19 | 20 | IEnumerable Contributors { get; } 21 | 22 | IEnumerable Links { get; } 23 | 24 | DateTimeOffset LastUpdated { get; } 25 | 26 | DateTimeOffset Published { get; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/ISyndicationLink.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public interface ISyndicationLink 10 | { 11 | Uri Uri { get; } 12 | 13 | string Title { get; } 14 | 15 | string MediaType { get; } 16 | 17 | string RelationshipType { get; } 18 | 19 | long Length { get; } 20 | 21 | DateTimeOffset LastUpdated { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/ISyndicationPerson.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed 6 | { 7 | public interface ISyndicationPerson 8 | { 9 | string Email { get; } 10 | 11 | string Name { get; } 12 | 13 | string Uri { get; } 14 | 15 | string RelationshipType { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Microsoft.SyndicationFeed.ReaderWriter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1.0.0 6 | netstandard1.3 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Rss/RssConstants.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Rss 6 | { 7 | static class RssConstants 8 | { 9 | public const string Rss20Namespace = ""; 10 | public const string SpecificationLink = "http://blogs.law.harvard.edu/tech/rss"; 11 | public const string Version = "2.0"; 12 | 13 | public const string IsPermaLink = "isPermaLink"; 14 | public const string Length = "length"; 15 | public const string Type = "type"; 16 | public const string Domain = "domain"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Rss/RssContributorTypes.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Rss 6 | { 7 | public static class RssContributorTypes 8 | { 9 | public const string Author = "author"; 10 | public const string ManagingEditor = "managingEditor"; 11 | public const string WebMaster = "webMaster"; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Rss/RssElementNames.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Rss 6 | { 7 | /// 8 | /// RSS 2.0 elements 9 | /// http://blogs.law.harvard.edu/tech/rss 10 | /// 11 | public static class RssElementNames 12 | { 13 | public const string Author = "author"; 14 | public const string Category = "category"; 15 | public const string Channel = "channel"; 16 | public const string Cloud = "cloud"; 17 | public const string Comments = "comments"; 18 | public const string Copyright = "copyright"; 19 | public const string Description = "description"; 20 | public const string Docs = "docs"; 21 | public const string Enclosure = "enclosure"; 22 | public const string Generator = "generator"; 23 | public const string Guid = "guid"; 24 | public const string Image = "image"; 25 | public const string Item = "item"; 26 | public const string Language = "language"; 27 | public const string LastBuildDate = "lastBuildDate"; 28 | public const string Link = "link"; 29 | public const string ManagingEditor = "managingEditor"; 30 | public const string PubDate = "pubDate"; 31 | public const string Rating = "rating"; 32 | public const string Rss = "rss"; 33 | public const string SkipDays = "skipDays"; 34 | public const string SkipHours = "skipHours"; 35 | public const string Source = "source"; 36 | public const string TextInput = "textInput"; 37 | public const string TimeToLive = "ttl"; 38 | public const string Title = "title"; 39 | public const string Url = "url"; 40 | public const string Version = "version"; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Rss/RssFeedReader.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Threading.Tasks; 6 | using System.Xml; 7 | 8 | namespace Microsoft.SyndicationFeed.Rss 9 | { 10 | public class RssFeedReader : XmlFeedReader 11 | { 12 | private readonly XmlReader _reader; 13 | private bool _knownFeed; 14 | 15 | public RssFeedReader(XmlReader reader) 16 | : this(reader, new RssParser()) 17 | { 18 | } 19 | 20 | public RssFeedReader(XmlReader reader, ISyndicationFeedParser parser) 21 | : base(reader, parser) 22 | { 23 | _reader = reader; 24 | } 25 | 26 | public override async Task Read() 27 | { 28 | if (!_knownFeed) 29 | { 30 | await InitRead(); 31 | _knownFeed = true; 32 | } 33 | 34 | return await base.Read(); 35 | } 36 | 37 | protected override SyndicationElementType MapElementType(string elementName) 38 | { 39 | if (_reader.NamespaceURI != RssConstants.Rss20Namespace) 40 | { 41 | return SyndicationElementType.Content; 42 | } 43 | 44 | switch (elementName) 45 | { 46 | case RssElementNames.Item: 47 | return SyndicationElementType.Item; 48 | 49 | case RssElementNames.Link: 50 | return SyndicationElementType.Link; 51 | 52 | case RssElementNames.Category: 53 | return SyndicationElementType.Category; 54 | 55 | case RssElementNames.Author: 56 | case RssElementNames.ManagingEditor: 57 | return SyndicationElementType.Person; 58 | 59 | case RssElementNames.Image: 60 | return SyndicationElementType.Image; 61 | 62 | default: 63 | return SyndicationElementType.Content; 64 | } 65 | } 66 | 67 | private async Task InitRead() 68 | { 69 | // Check 70 | bool knownFeed = _reader.IsStartElement(RssElementNames.Rss, RssConstants.Rss20Namespace) && 71 | _reader.GetAttribute(RssElementNames.Version).Equals(RssConstants.Version); 72 | 73 | if (knownFeed) 74 | { 75 | // Read 76 | await XmlUtils.ReadAsync(_reader); 77 | 78 | // Check 79 | knownFeed = _reader.IsStartElement(RssElementNames.Channel, RssConstants.Rss20Namespace); 80 | } 81 | 82 | if (!knownFeed) 83 | { 84 | throw new XmlException("Unknown Rss Feed"); 85 | } 86 | 87 | // Read 88 | await XmlUtils.ReadAsync(_reader); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/Rss/RssFeedWriter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Globalization; 8 | using System.Threading.Tasks; 9 | using System.Xml; 10 | 11 | namespace Microsoft.SyndicationFeed.Rss 12 | { 13 | public class RssFeedWriter : XmlFeedWriter 14 | { 15 | private readonly XmlWriter _writer; 16 | private bool _feedStarted; 17 | private readonly IEnumerable _attributes; 18 | 19 | public RssFeedWriter(XmlWriter writer, IEnumerable attributes = null) 20 | : this(writer, attributes, null) 21 | { 22 | } 23 | 24 | public RssFeedWriter(XmlWriter writer, IEnumerable attributes, ISyndicationFeedFormatter formatter) : 25 | base(writer, formatter ?? new RssFormatter(attributes, writer.Settings)) 26 | { 27 | _writer = writer; 28 | _attributes = attributes; 29 | } 30 | 31 | public virtual Task WriteTitle(string value) 32 | { 33 | if (value == null) 34 | { 35 | throw new ArgumentNullException(nameof(value)); 36 | } 37 | 38 | return WriteValue(RssElementNames.Title, value); 39 | } 40 | 41 | public virtual Task WriteDescription(string value) 42 | { 43 | if (value == null) 44 | { 45 | throw new ArgumentNullException(nameof(value)); 46 | } 47 | 48 | return WriteValue(RssElementNames.Description, value); 49 | } 50 | 51 | public virtual Task WriteLanguage(CultureInfo culture) 52 | { 53 | if (culture == null) 54 | { 55 | throw new ArgumentNullException(nameof(culture)); 56 | } 57 | 58 | return WriteValue(RssElementNames.Language, culture.Name); 59 | } 60 | 61 | public virtual Task WriteCopyright(string value) 62 | { 63 | if (value == null) 64 | { 65 | throw new ArgumentNullException(nameof(value)); 66 | } 67 | 68 | return WriteValue(RssElementNames.Copyright, value); 69 | } 70 | 71 | public virtual Task WritePubDate(DateTimeOffset dt) 72 | { 73 | if (dt == default(DateTimeOffset)) 74 | { 75 | throw new ArgumentException(nameof(dt)); 76 | } 77 | 78 | return WriteValue(RssElementNames.PubDate, dt); 79 | } 80 | 81 | public virtual Task WriteLastBuildDate(DateTimeOffset dt) 82 | { 83 | if (dt == default(DateTimeOffset)) 84 | { 85 | throw new ArgumentException(nameof(dt)); 86 | } 87 | 88 | return WriteValue(RssElementNames.LastBuildDate, dt); 89 | } 90 | 91 | public virtual Task WriteGenerator(string value) 92 | { 93 | if (value == null) 94 | { 95 | throw new ArgumentNullException(nameof(value)); 96 | } 97 | 98 | return WriteValue(RssElementNames.Generator, value); 99 | } 100 | 101 | public virtual Task WriteDocs() 102 | { 103 | return WriteValue(RssElementNames.Docs, RssConstants.SpecificationLink); 104 | } 105 | 106 | public virtual Task WriteCloud(Uri uri, string registerProcedure, string protocol) 107 | { 108 | if (uri == null) 109 | { 110 | throw new ArgumentNullException(nameof(uri)); 111 | } 112 | 113 | if (!uri.IsAbsoluteUri) 114 | { 115 | throw new ArgumentException("Absolute uri required"); 116 | } 117 | 118 | if (string.IsNullOrEmpty(registerProcedure)) 119 | { 120 | throw new ArgumentNullException(nameof(registerProcedure)); 121 | } 122 | 123 | var cloud = new SyndicationContent(RssElementNames.Cloud); 124 | 125 | cloud.AddAttribute(new SyndicationAttribute("domain", uri.GetComponents(UriComponents.Host, UriFormat.UriEscaped))); 126 | cloud.AddAttribute(new SyndicationAttribute("port", uri.GetComponents(UriComponents.StrongPort, UriFormat.UriEscaped))); 127 | cloud.AddAttribute(new SyndicationAttribute("path", uri.GetComponents(UriComponents.PathAndQuery, UriFormat.UriEscaped))); 128 | cloud.AddAttribute(new SyndicationAttribute("registerProcedure", registerProcedure)); 129 | cloud.AddAttribute(new SyndicationAttribute("protocol", protocol ?? "xml-rpc")); 130 | 131 | return Write(cloud); 132 | } 133 | 134 | public virtual Task WriteTimeToLive(TimeSpan ttl) 135 | { 136 | if (ttl == default(TimeSpan)) 137 | { 138 | throw new ArgumentException(nameof(ttl)); 139 | } 140 | 141 | return WriteValue(RssElementNames.TimeToLive, (long) Math.Max(1, Math.Ceiling(ttl.TotalMinutes))); 142 | } 143 | 144 | public virtual Task WriteSkipHours(IEnumerable hours) 145 | { 146 | if (hours == null) 147 | { 148 | throw new ArgumentNullException(nameof(hours)); 149 | } 150 | 151 | var skipHours = new SyndicationContent(RssElementNames.SkipHours); 152 | 153 | foreach (var h in hours) 154 | { 155 | if (h < 0 || h > 23) 156 | { 157 | throw new ArgumentOutOfRangeException("Hour value must be between 0 and 23"); 158 | } 159 | 160 | skipHours.AddField(new SyndicationContent("hour", Formatter.FormatValue(h))); 161 | } 162 | 163 | return Write(skipHours); 164 | } 165 | 166 | public virtual Task WriteSkipDays(IEnumerable days) 167 | { 168 | if (days == null) 169 | { 170 | throw new ArgumentNullException(nameof(days)); 171 | } 172 | 173 | var skipDays = new SyndicationContent(RssElementNames.SkipDays); 174 | 175 | foreach (var d in days) 176 | { 177 | skipDays.AddField(new SyndicationContent("day", Formatter.FormatValue(d))); 178 | } 179 | 180 | return Write(skipDays); 181 | } 182 | 183 | public override Task WriteRaw(string content) 184 | { 185 | if (!_feedStarted) 186 | { 187 | StartFeed(); 188 | } 189 | 190 | return XmlUtils.WriteRawAsync(_writer, content); 191 | } 192 | 193 | private void StartFeed() 194 | { 195 | // Write 196 | _writer.WriteStartElement(RssElementNames.Rss); 197 | 198 | // Write attributes if exist 199 | if (_attributes != null) 200 | { 201 | foreach (var a in _attributes) 202 | { 203 | _writer.WriteSyndicationAttribute(a); 204 | } 205 | } 206 | 207 | _writer.WriteAttributeString(RssElementNames.Version, RssConstants.Version); 208 | _writer.WriteStartElement(RssElementNames.Channel); 209 | _feedStarted = true; 210 | } 211 | } 212 | } -------------------------------------------------------------------------------- /src/Rss/RssFormatter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Xml; 9 | 10 | namespace Microsoft.SyndicationFeed.Rss 11 | { 12 | public class RssFormatter : ISyndicationFeedFormatter 13 | { 14 | private readonly XmlWriter _writer; 15 | private readonly StringBuilder _buffer; 16 | 17 | public RssFormatter() 18 | : this(null, null) 19 | { 20 | } 21 | 22 | public RssFormatter(IEnumerable knownAttributes, XmlWriterSettings settings) 23 | { 24 | _buffer = new StringBuilder(); 25 | _writer = XmlUtils.CreateXmlWriter(settings?.Clone() ?? new XmlWriterSettings(), knownAttributes, _buffer); 26 | } 27 | 28 | public bool UseCDATA { get; set; } 29 | 30 | public string Format(ISyndicationContent content) 31 | { 32 | if (content == null) 33 | { 34 | throw new ArgumentNullException(nameof(content)); 35 | } 36 | 37 | try 38 | { 39 | WriteSyndicationContent(content); 40 | 41 | _writer.Flush(); 42 | 43 | return _buffer.ToString(); 44 | } 45 | finally 46 | { 47 | _buffer.Clear(); 48 | } 49 | } 50 | 51 | public string Format(ISyndicationCategory category) 52 | { 53 | ISyndicationContent content = CreateContent(category); 54 | 55 | return Format(content); 56 | } 57 | 58 | public string Format(ISyndicationImage image) 59 | { 60 | ISyndicationContent content = CreateContent(image); 61 | 62 | return Format(content); 63 | } 64 | 65 | public string Format(ISyndicationPerson person) 66 | { 67 | ISyndicationContent content = CreateContent(person); 68 | 69 | return Format(content); 70 | } 71 | 72 | public string Format(ISyndicationItem item) 73 | { 74 | ISyndicationContent content = CreateContent(item); 75 | 76 | return Format(content); 77 | } 78 | 79 | public string Format(ISyndicationLink link) 80 | { 81 | ISyndicationContent content = CreateContent(link); 82 | 83 | return Format(content); 84 | } 85 | 86 | public virtual string FormatValue(T value) 87 | { 88 | if (value == null) 89 | { 90 | return null; 91 | } 92 | 93 | Type type = typeof(T); 94 | 95 | // 96 | // DateTimeOffset 97 | if (type == typeof(DateTimeOffset)) 98 | { 99 | return DateTimeUtils.ToRfc1123String((DateTimeOffset)(object)value); 100 | } 101 | 102 | // 103 | // DateTime 104 | if (type == typeof(DateTime)) 105 | { 106 | return DateTimeUtils.ToRfc1123String(new DateTimeOffset((DateTime)(object)value)); 107 | } 108 | 109 | return value.ToString(); 110 | 111 | } 112 | 113 | public virtual ISyndicationContent CreateContent(ISyndicationLink link) 114 | { 115 | if (link == null) 116 | { 117 | throw new ArgumentNullException(nameof(link)); 118 | } 119 | 120 | if (link.Uri == null) 121 | { 122 | throw new ArgumentNullException("Invalid link uri"); 123 | } 124 | 125 | switch (link.RelationshipType) 126 | { 127 | case RssElementNames.Enclosure: 128 | return CreateEnclosureContent(link); 129 | 130 | case RssElementNames.Comments: 131 | return CreateCommentsContent(link); 132 | 133 | case RssElementNames.Source: 134 | return CreateSourceContent(link); 135 | 136 | default: 137 | return CreateLinkContent(link); 138 | } 139 | } 140 | 141 | public virtual ISyndicationContent CreateContent(ISyndicationCategory category) 142 | { 143 | if (category == null) 144 | { 145 | throw new ArgumentNullException(nameof(category)); 146 | } 147 | 148 | if (string.IsNullOrEmpty(category.Name)) 149 | { 150 | throw new FormatException("Invalid category name"); 151 | } 152 | 153 | var content = new SyndicationContent(RssElementNames.Category, category.Name); 154 | 155 | if (category.Scheme != null) 156 | { 157 | content.AddAttribute(new SyndicationAttribute(RssConstants.Domain, category.Scheme)); 158 | } 159 | 160 | return content; 161 | } 162 | 163 | public virtual ISyndicationContent CreateContent(ISyndicationPerson person) 164 | { 165 | if (person == null) 166 | { 167 | throw new ArgumentNullException(nameof(person)); 168 | } 169 | 170 | // 171 | // RSS requires Email 172 | if (string.IsNullOrEmpty(person.Email)) 173 | { 174 | throw new ArgumentNullException("Invalid person Email"); 175 | } 176 | 177 | // 178 | // Real name recommended with RSS e-mail addresses 179 | // Ex: email@address.com (John Doe) 180 | 181 | string value = string.IsNullOrEmpty(person.Name) ? person.Email : $"{person.Email} ({person.Name})"; 182 | 183 | return new SyndicationContent(person.RelationshipType ?? RssElementNames.Author, value); 184 | } 185 | 186 | public virtual ISyndicationContent CreateContent(ISyndicationImage image) 187 | { 188 | if (image == null) 189 | { 190 | throw new ArgumentNullException(nameof(image)); 191 | } 192 | 193 | // Required URL - Title - Link 194 | if (string.IsNullOrEmpty(image.Title)) 195 | { 196 | throw new ArgumentNullException("Image requires a title"); 197 | } 198 | 199 | if (image.Link == null) 200 | { 201 | throw new ArgumentNullException("Image requires a link"); 202 | } 203 | 204 | if (image.Url == null) 205 | { 206 | throw new ArgumentNullException("Image requires an url"); 207 | } 208 | 209 | var content = new SyndicationContent(RssElementNames.Image); 210 | 211 | // Write required contents of image 212 | content.AddField(new SyndicationContent(RssElementNames.Url, FormatValue(image.Url))); 213 | content.AddField(new SyndicationContent(RssElementNames.Title, image.Title)); 214 | content.AddField(CreateContent(image.Link)); 215 | 216 | 217 | // Write optional elements 218 | if (!string.IsNullOrEmpty(image.Description)) 219 | { 220 | content.AddField(new SyndicationContent(RssElementNames.Description, image.Description)); 221 | } 222 | 223 | return content; 224 | } 225 | 226 | public virtual ISyndicationContent CreateContent(ISyndicationItem item) 227 | { 228 | if (item == null) 229 | { 230 | throw new ArgumentNullException(nameof(item)); 231 | } 232 | 233 | // Spec requires to have at least one title or description 234 | if (string.IsNullOrEmpty(item.Title) && string.IsNullOrEmpty(item.Description)) 235 | { 236 | throw new ArgumentNullException("RSS Item requires a title or a description"); 237 | } 238 | 239 | // Write tag 240 | var content = new SyndicationContent(RssElementNames.Item); 241 | 242 | // 243 | // Title 244 | if (!string.IsNullOrEmpty(item.Title)) 245 | { 246 | content.AddField(new SyndicationContent(RssElementNames.Title, item.Title)); 247 | } 248 | 249 | // 250 | // Links 251 | ISyndicationLink guidLink = null; 252 | 253 | if (item.Links != null) 254 | { 255 | foreach (var link in item.Links) 256 | { 257 | if (link.RelationshipType == RssElementNames.Guid) 258 | { 259 | guidLink = link; 260 | } 261 | 262 | content.AddField(CreateContent(link)); 263 | } 264 | } 265 | 266 | // 267 | // Description 268 | if (!string.IsNullOrEmpty(item.Description)) 269 | { 270 | content.AddField(new SyndicationContent(RssElementNames.Description, item.Description)); 271 | } 272 | 273 | // 274 | // Authors (persons) 275 | if (item.Contributors != null) 276 | { 277 | foreach (var person in item.Contributors) 278 | { 279 | content.AddField(CreateContent(person)); 280 | } 281 | } 282 | 283 | // 284 | // Cathegory 285 | if (item.Categories != null) 286 | { 287 | foreach (var category in item.Categories) 288 | { 289 | content.AddField(CreateContent(category)); 290 | } 291 | } 292 | 293 | // 294 | // Guid (id) 295 | if (guidLink == null && !string.IsNullOrEmpty(item.Id)) 296 | { 297 | var guid = new SyndicationContent(RssElementNames.Guid, item.Id); 298 | 299 | guid.AddAttribute(new SyndicationAttribute(RssConstants.IsPermaLink, "false")); 300 | 301 | content.AddField(guid); 302 | } 303 | 304 | // 305 | // PubDate 306 | if (item.Published != DateTimeOffset.MinValue) 307 | { 308 | content.AddField(new SyndicationContent(RssElementNames.PubDate, FormatValue(item.Published))); 309 | } 310 | 311 | return content; 312 | } 313 | 314 | 315 | private ISyndicationContent CreateEnclosureContent(ISyndicationLink link) 316 | { 317 | var content = new SyndicationContent(RssElementNames.Enclosure); 318 | 319 | // 320 | // Url 321 | content.AddAttribute(new SyndicationAttribute(RssElementNames.Url, FormatValue(link.Uri))); 322 | 323 | // 324 | // Length 325 | if (link.Length == 0) 326 | { 327 | throw new ArgumentException("Enclosure requires length attribute"); 328 | } 329 | 330 | content.AddAttribute(new SyndicationAttribute(RssConstants.Length, FormatValue(link.Length))); 331 | 332 | // 333 | // MediaType 334 | if (string.IsNullOrEmpty(link.MediaType)) 335 | { 336 | throw new ArgumentNullException("Enclosure requires a MediaType"); 337 | } 338 | 339 | content.AddAttribute(new SyndicationAttribute(RssConstants.Type, link.MediaType)); 340 | return content; 341 | } 342 | 343 | private ISyndicationContent CreateLinkContent(ISyndicationLink link) 344 | { 345 | SyndicationContent content; 346 | 347 | if (string.IsNullOrEmpty(link.RelationshipType) || 348 | link.RelationshipType == RssLinkTypes.Alternate) 349 | { 350 | // Regular 351 | content = new SyndicationContent(RssElementNames.Link); 352 | } 353 | else 354 | { 355 | // Custom 356 | content = new SyndicationContent(link.RelationshipType); 357 | } 358 | 359 | // 360 | // title 361 | if (!string.IsNullOrEmpty(link.Title)) 362 | { 363 | content.Value = link.Title; 364 | } 365 | 366 | // 367 | // url 368 | string url = FormatValue(link.Uri); 369 | 370 | if (content.Value != null) 371 | { 372 | content.AddAttribute(new SyndicationAttribute(RssElementNames.Url, url)); 373 | } 374 | else 375 | { 376 | content.Value = url; 377 | } 378 | 379 | // 380 | // Type 381 | if (!string.IsNullOrEmpty(link.MediaType)) 382 | { 383 | content.AddAttribute(new SyndicationAttribute(RssConstants.Type, link.MediaType)); 384 | } 385 | 386 | // 387 | // Lenght 388 | if (link.Length != 0) 389 | { 390 | content.AddAttribute(new SyndicationAttribute(RssConstants.Length, FormatValue(link.Length))); 391 | } 392 | 393 | return content; 394 | } 395 | 396 | private ISyndicationContent CreateCommentsContent(ISyndicationLink link) 397 | { 398 | return new SyndicationContent(link.RelationshipType) 399 | { 400 | Value = FormatValue(link.Uri) 401 | }; 402 | } 403 | 404 | private ISyndicationContent CreateSourceContent(ISyndicationLink link) 405 | { 406 | var content = new SyndicationContent(link.RelationshipType); 407 | 408 | // 409 | // Url 410 | string url = FormatValue(link.Uri); 411 | if (link.Title != url) 412 | { 413 | content.AddAttribute(new SyndicationAttribute(RssElementNames.Url, url)); 414 | } 415 | 416 | // 417 | // Title 418 | if (!string.IsNullOrEmpty(link.Title)) 419 | { 420 | content.Value = link.Title; 421 | } 422 | 423 | return content; 424 | } 425 | 426 | private void WriteSyndicationContent(ISyndicationContent content) 427 | { 428 | // 429 | // Write Start 430 | _writer.WriteStartSyndicationContent(content, null); 431 | 432 | // 433 | // Write attributes 434 | if (content.Attributes != null) 435 | { 436 | foreach (var a in content.Attributes) 437 | { 438 | _writer.WriteSyndicationAttribute(a); 439 | } 440 | } 441 | 442 | // 443 | // Write value 444 | if (content.Value != null) 445 | { 446 | _writer.WriteString(content.Value, UseCDATA); 447 | } 448 | // 449 | // Write Fields 450 | else 451 | { 452 | if (content.Fields != null) 453 | { 454 | foreach (var field in content.Fields) 455 | { 456 | WriteSyndicationContent(field); 457 | } 458 | } 459 | } 460 | 461 | // 462 | // Write End 463 | _writer.WriteEndElement(); 464 | } 465 | } 466 | } -------------------------------------------------------------------------------- /src/Rss/RssLinkTypes.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed.Rss 6 | { 7 | public static class RssLinkTypes 8 | { 9 | public const string Alternate = "alternate"; 10 | public const string Comments = "comments"; 11 | public const string Enclosure = "enclosure"; 12 | public const string Guid = "guid"; 13 | public const string Source = "source"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Rss/RssParser.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Xml; 9 | 10 | namespace Microsoft.SyndicationFeed.Rss 11 | { 12 | public class RssParser : ISyndicationFeedParser 13 | { 14 | public ISyndicationCategory ParseCategory(string value) 15 | { 16 | ISyndicationContent content = ParseContent(value); 17 | 18 | if (content.Name != RssElementNames.Category || 19 | content.Namespace != RssConstants.Rss20Namespace) 20 | { 21 | throw new FormatException("Invalid Rss category"); 22 | } 23 | 24 | return CreateCategory(content); 25 | } 26 | 27 | public ISyndicationItem ParseItem(string value) 28 | { 29 | ISyndicationContent content = ParseContent(value); 30 | 31 | if (content.Name != RssElementNames.Item || 32 | content.Namespace != RssConstants.Rss20Namespace) 33 | { 34 | throw new FormatException("Invalid Rss item"); 35 | } 36 | 37 | return CreateItem(content); 38 | } 39 | 40 | public ISyndicationLink ParseLink(string value) 41 | { 42 | ISyndicationContent content = ParseContent(value); 43 | 44 | if (content.Name != RssElementNames.Link || 45 | content.Namespace != RssConstants.Rss20Namespace) 46 | { 47 | throw new FormatException("Invalid Rss link"); 48 | } 49 | 50 | return CreateLink(content); 51 | } 52 | 53 | public ISyndicationPerson ParsePerson(string value) 54 | { 55 | ISyndicationContent content = ParseContent(value); 56 | 57 | if ((content.Name != RssElementNames.Author && 58 | content.Name != RssElementNames.ManagingEditor) || 59 | content.Namespace != RssConstants.Rss20Namespace) 60 | { 61 | throw new FormatException("Invalid Rss Person"); 62 | } 63 | 64 | return CreatePerson(content); 65 | } 66 | 67 | public ISyndicationImage ParseImage(string value) 68 | { 69 | ISyndicationContent content = ParseContent(value); 70 | 71 | if (content.Name != RssElementNames.Image || 72 | content.Namespace != RssConstants.Rss20Namespace) 73 | { 74 | throw new FormatException("Invalid Rss Image"); 75 | } 76 | 77 | return CreateImage(content); 78 | } 79 | 80 | public ISyndicationContent ParseContent(string value) 81 | { 82 | if (string.IsNullOrEmpty(value)) 83 | { 84 | throw new ArgumentNullException(nameof(value)); 85 | } 86 | 87 | using (XmlReader reader = XmlUtils.CreateXmlReader(value)) 88 | { 89 | reader.MoveToContent(); 90 | 91 | return ReadSyndicationContent(reader); 92 | } 93 | } 94 | 95 | public virtual bool TryParseValue(string value, out T result) 96 | { 97 | return Converter.TryParseValue(value, out result); 98 | } 99 | 100 | public virtual ISyndicationItem CreateItem(ISyndicationContent content) 101 | { 102 | if (content == null) 103 | { 104 | throw new ArgumentNullException(nameof(content)); 105 | } 106 | 107 | var item = new SyndicationItem(); 108 | 109 | foreach (var field in content.Fields) 110 | { 111 | if (field.Namespace != RssConstants.Rss20Namespace) 112 | { 113 | continue; 114 | } 115 | 116 | switch (field.Name) 117 | { 118 | // 119 | // Title 120 | case RssElementNames.Title: 121 | item.Title = field.Value; 122 | break; 123 | 124 | // 125 | // Link 126 | case RssElementNames.Link: 127 | item.AddLink(CreateLink(field)); 128 | break; 129 | 130 | // Description 131 | case RssElementNames.Description: 132 | item.Description = field.Value; 133 | break; 134 | 135 | // 136 | // Author 137 | case RssElementNames.Author: 138 | item.AddContributor(CreatePerson(field)); 139 | break; 140 | 141 | // 142 | // Category 143 | case RssElementNames.Category: 144 | item.AddCategory(CreateCategory(field)); 145 | break; 146 | 147 | // 148 | // Links 149 | case RssElementNames.Comments: 150 | case RssElementNames.Enclosure: 151 | case RssElementNames.Source: 152 | item.AddLink(CreateLink(field)); 153 | break; 154 | 155 | // 156 | // Guid 157 | case RssElementNames.Guid: 158 | item.Id = field.Value; 159 | 160 | // isPermaLink 161 | string isPermaLinkAttr = field.Attributes.GetRss(RssConstants.IsPermaLink); 162 | 163 | if ((isPermaLinkAttr == null || (TryParseValue(isPermaLinkAttr, out bool isPermalink) && isPermalink)) && 164 | TryParseValue(field.Value, out Uri permaLink)) 165 | { 166 | item.AddLink(new SyndicationLink(permaLink, RssLinkTypes.Guid)); 167 | } 168 | 169 | break; 170 | 171 | // 172 | // PubDate 173 | case RssElementNames.PubDate: 174 | if (TryParseValue(field.Value, out DateTimeOffset dt)) 175 | { 176 | item.Published = dt; 177 | } 178 | break; 179 | 180 | default: 181 | break; 182 | } 183 | } 184 | 185 | return item; 186 | } 187 | 188 | public virtual ISyndicationLink CreateLink(ISyndicationContent content) 189 | { 190 | if (content == null) 191 | { 192 | throw new ArgumentNullException(nameof(content)); 193 | } 194 | 195 | // 196 | // Title 197 | string title = content.Value; 198 | 199 | // 200 | // Url 201 | Uri uri = null; 202 | string url = content.Attributes.GetRss("url"); 203 | 204 | if (url != null) 205 | { 206 | if (!TryParseValue(url, out uri)) 207 | { 208 | throw new FormatException("Invalid url attribute"); 209 | } 210 | } 211 | else 212 | { 213 | if (!TryParseValue(content.Value, out uri)) 214 | { 215 | throw new FormatException("Invalid url"); 216 | } 217 | 218 | title = null; 219 | } 220 | 221 | // 222 | // Length 223 | long length = 0; 224 | TryParseValue(content.Attributes.GetRss("length"), out length); 225 | 226 | // 227 | // Type 228 | string type = content.Attributes.GetRss("type"); 229 | 230 | // 231 | // rel 232 | string rel = (content.Name == RssElementNames.Link) ? RssLinkTypes.Alternate : content.Name; 233 | 234 | return new SyndicationLink(uri, rel) 235 | { 236 | Title = title, 237 | Length = length, 238 | MediaType = type 239 | }; 240 | } 241 | 242 | public virtual ISyndicationPerson CreatePerson(ISyndicationContent content) 243 | { 244 | if (content == null) 245 | { 246 | throw new ArgumentNullException(nameof(content)); 247 | } 248 | 249 | if (string.IsNullOrEmpty(content.Value)) 250 | { 251 | throw new ArgumentNullException("Content value is required"); 252 | } 253 | 254 | // 255 | // Handle real name parsing 256 | // Ex: abc@def.com (John Doe) 257 | 258 | string email = content.Value; 259 | string name = null; 260 | 261 | int nameStart = content.Value.IndexOf('('); 262 | 263 | if (nameStart != -1) 264 | { 265 | int end = content.Value.IndexOf(')'); 266 | 267 | if (end == -1 || end - nameStart - 1 < 0) 268 | { 269 | throw new FormatException("Invalid Rss person"); 270 | } 271 | 272 | email = content.Value.Substring(0, nameStart).Trim(); 273 | 274 | name = content.Value.Substring(nameStart + 1, end - nameStart - 1); 275 | } 276 | 277 | return new SyndicationPerson(name, email, content.Name); 278 | } 279 | 280 | public virtual ISyndicationImage CreateImage(ISyndicationContent content) 281 | { 282 | if (content == null) 283 | { 284 | throw new ArgumentNullException(nameof(content)); 285 | } 286 | 287 | string title = null; 288 | string description = null; 289 | Uri url = null; 290 | ISyndicationLink link = null; 291 | 292 | foreach (var field in content.Fields) 293 | { 294 | if (field.Namespace != RssConstants.Rss20Namespace) 295 | { 296 | continue; 297 | } 298 | 299 | switch (field.Name) 300 | { 301 | // 302 | // Title 303 | case RssElementNames.Title: 304 | title = field.Value; 305 | break; 306 | 307 | // 308 | // Url 309 | case RssElementNames.Url: 310 | if (!TryParseValue(field.Value, out url)) 311 | { 312 | throw new FormatException($"Invalid image url '{field.Value}'"); 313 | } 314 | break; 315 | 316 | // 317 | // Link 318 | case RssElementNames.Link: 319 | link = CreateLink(field); 320 | break; 321 | 322 | // 323 | // Description 324 | case RssElementNames.Description: 325 | description = field.Value; 326 | break; 327 | 328 | default: 329 | break; 330 | } 331 | } 332 | 333 | if (url == null) 334 | { 335 | throw new FormatException("Image url not found"); 336 | } 337 | 338 | return new SyndicationImage(url, RssElementNames.Image) 339 | { 340 | Title = title, 341 | Description = description, 342 | Link = link 343 | }; 344 | } 345 | 346 | public virtual ISyndicationCategory CreateCategory(ISyndicationContent content) 347 | { 348 | if (content == null) 349 | { 350 | throw new ArgumentNullException(nameof(content)); 351 | } 352 | 353 | if (content.Value == null) 354 | { 355 | throw new FormatException("Invalid Rss category name"); 356 | } 357 | 358 | return new SyndicationCategory(content.Value) { 359 | Scheme = content.Attributes.GetRss(RssConstants.Domain) 360 | }; 361 | } 362 | 363 | private static ISyndicationContent ReadSyndicationContent(XmlReader reader) 364 | { 365 | var content = new SyndicationContent(reader.LocalName, reader.NamespaceURI, null); 366 | 367 | // 368 | // Attributes 369 | if (reader.HasAttributes) 370 | { 371 | while (reader.MoveToNextAttribute()) 372 | { 373 | ISyndicationAttribute attr = reader.ReadSyndicationAttribute(); 374 | 375 | if (attr != null) 376 | { 377 | content.AddAttribute(attr); 378 | } 379 | } 380 | 381 | reader.MoveToContent(); 382 | } 383 | 384 | // 385 | // Content 386 | if (!reader.IsEmptyElement) 387 | { 388 | reader.ReadStartElement(); 389 | 390 | // 391 | // Value 392 | if (reader.HasValue) 393 | { 394 | content.Value = reader.ReadContentAsString(); 395 | } 396 | // 397 | // Children 398 | else 399 | { 400 | while (reader.IsStartElement()) 401 | { 402 | content.AddField(ReadSyndicationContent(reader)); 403 | } 404 | } 405 | 406 | reader.ReadEndElement(); // end 407 | } 408 | else 409 | { 410 | reader.Skip(); 411 | } 412 | 413 | return content; 414 | } 415 | } 416 | 417 | static class RssAttributeExtentions 418 | { 419 | public static string GetRss(this IEnumerable attributes, string name) 420 | { 421 | return attributes.FirstOrDefault(a => a.Name == name && 422 | (a.Namespace == RssConstants.Rss20Namespace || a.Namespace == null))?.Value; 423 | } 424 | } 425 | } -------------------------------------------------------------------------------- /src/SyndicationAttribute.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public sealed class SyndicationAttribute : ISyndicationAttribute 10 | { 11 | public SyndicationAttribute(string name, string value) : 12 | this(name, null, value) 13 | { 14 | } 15 | 16 | public SyndicationAttribute(string name, string ns, string value) 17 | { 18 | Name = name ?? throw new ArgumentNullException(nameof(name)); 19 | Value = value ?? throw new ArgumentNullException(nameof(value)); 20 | Namespace = ns; 21 | } 22 | 23 | public string Name { get; private set; } 24 | public string Namespace { get; private set; } 25 | public string Value { get; private set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/SyndicationCategory.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public sealed class SyndicationCategory : ISyndicationCategory 10 | { 11 | public SyndicationCategory(string name) 12 | { 13 | Name = name ?? throw new ArgumentNullException(nameof(name)); 14 | } 15 | 16 | public string Name { get; private set; } 17 | 18 | public string Label { get; set; } 19 | 20 | public string Scheme { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/SyndicationContent.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace Microsoft.SyndicationFeed 10 | { 11 | public class SyndicationContent : ISyndicationContent 12 | { 13 | private ICollection _attributes; 14 | private ICollection _children; 15 | 16 | public SyndicationContent(string name, string value = null) 17 | : this(name, null, value) 18 | { 19 | } 20 | 21 | public SyndicationContent(string name, string ns, string value) 22 | { 23 | if (string.IsNullOrEmpty(name)) 24 | { 25 | throw new ArgumentNullException(nameof(name)); 26 | } 27 | 28 | 29 | Name = name; 30 | Value = value; 31 | Namespace = ns; 32 | 33 | _attributes = new List(); 34 | _children = new List(); 35 | } 36 | 37 | public SyndicationContent(ISyndicationContent content) 38 | { 39 | if (content == null) 40 | { 41 | throw new ArgumentNullException(nameof(content)); 42 | } 43 | 44 | Name = content.Name; 45 | Namespace = content.Namespace; 46 | Value = content.Value; 47 | 48 | // Copy collections only if needed 49 | _attributes = content.Attributes as ICollection ?? content.Attributes.ToList(); 50 | _children = content.Fields as ICollection ?? content.Fields.ToList(); 51 | } 52 | 53 | public string Name { get; private set; } 54 | 55 | public string Namespace { get; private set; } 56 | 57 | public string Value { get; set; } 58 | 59 | public IEnumerable Attributes 60 | { 61 | get 62 | { 63 | return _attributes; 64 | } 65 | } 66 | 67 | public IEnumerable Fields 68 | { 69 | get 70 | { 71 | return _children; 72 | } 73 | } 74 | 75 | public void AddAttribute(ISyndicationAttribute attribute) 76 | { 77 | if (attribute == null) 78 | { 79 | throw new ArgumentNullException(nameof(attribute)); 80 | } 81 | 82 | if (_attributes.IsReadOnly) 83 | { 84 | _attributes = _attributes.ToList(); 85 | } 86 | 87 | _attributes.Add(attribute); 88 | } 89 | 90 | public void AddField(ISyndicationContent field) 91 | { 92 | if (field == null) 93 | { 94 | throw new ArgumentNullException(nameof(field)); 95 | } 96 | 97 | if (_children.IsReadOnly) 98 | { 99 | _children = _children.ToList(); 100 | } 101 | 102 | _children.Add(field); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/SyndicationElementType.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.SyndicationFeed 6 | { 7 | public enum SyndicationElementType 8 | { 9 | None = 0, 10 | Item = 1, 11 | Person = 2, 12 | Link = 3, 13 | Content = 4, 14 | Category = 5, 15 | Image = 6 16 | } 17 | } -------------------------------------------------------------------------------- /src/SyndicationImage.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information.using System; 4 | 5 | using System; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public sealed class SyndicationImage : ISyndicationImage 10 | { 11 | public SyndicationImage(Uri url, string relationshipType = null) 12 | { 13 | Url = url ?? throw new ArgumentNullException(nameof(url)); 14 | RelationshipType = relationshipType; 15 | } 16 | 17 | public string Title { get; set; } 18 | 19 | public Uri Url { get; private set; } 20 | 21 | public ISyndicationLink Link { get; set; } 22 | 23 | public string RelationshipType { get; set; } 24 | 25 | public string Description { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/SyndicationItem.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace Microsoft.SyndicationFeed 10 | { 11 | public class SyndicationItem : ISyndicationItem 12 | { 13 | private ICollection _categories; 14 | private ICollection _contributors; 15 | private ICollection _links; 16 | 17 | public SyndicationItem() 18 | { 19 | _categories = new List(); 20 | _contributors = new List(); 21 | _links = new List(); 22 | } 23 | 24 | public SyndicationItem(ISyndicationItem item) 25 | { 26 | if (item == null) 27 | { 28 | throw new ArgumentNullException(nameof(item)); 29 | } 30 | 31 | Id = item.Id; 32 | Title = item.Title; 33 | Description = item.Description; 34 | LastUpdated = item.LastUpdated; 35 | Published = item.Published; 36 | 37 | // Copy collections only if needed 38 | _categories = item.Categories as ICollection ?? item.Categories.ToList(); 39 | _contributors = item.Contributors as ICollection ?? item.Contributors.ToList(); 40 | _links = item.Links as ICollection ?? item.Links.ToList(); 41 | } 42 | 43 | public string Id { get; set; } 44 | 45 | public string Title { get; set; } 46 | 47 | public string Description { get; set; } 48 | 49 | public IEnumerable Categories 50 | { 51 | get 52 | { 53 | return _categories; 54 | } 55 | } 56 | 57 | public IEnumerable Contributors 58 | { 59 | get 60 | { 61 | return _contributors; 62 | } 63 | } 64 | 65 | public IEnumerable Links 66 | { 67 | get { 68 | return _links; 69 | } 70 | } 71 | 72 | public DateTimeOffset LastUpdated { get; set; } 73 | 74 | public DateTimeOffset Published { get; set; } 75 | 76 | public void AddCategory(ISyndicationCategory category) 77 | { 78 | if (category == null) 79 | { 80 | throw new ArgumentNullException(nameof(category)); 81 | } 82 | 83 | if (_categories.IsReadOnly) 84 | { 85 | _categories = _categories.ToList(); 86 | } 87 | 88 | _categories.Add(category); 89 | } 90 | 91 | public void AddContributor(ISyndicationPerson person) 92 | { 93 | if (person == null) 94 | { 95 | throw new ArgumentNullException(nameof(person)); 96 | } 97 | 98 | if (_contributors.IsReadOnly) 99 | { 100 | _contributors = _contributors.ToList(); 101 | } 102 | 103 | _contributors.Add(person); 104 | } 105 | 106 | public void AddLink(ISyndicationLink link) 107 | { 108 | if (link == null) 109 | { 110 | throw new ArgumentNullException(nameof(link)); 111 | } 112 | 113 | if (_links.IsReadOnly) 114 | { 115 | _links = _links.ToList(); 116 | } 117 | 118 | _links.Add(link); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/SyndicationLink.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public sealed class SyndicationLink : ISyndicationLink 10 | { 11 | public SyndicationLink(Uri url, string relationshipType = null) 12 | { 13 | Uri = url ?? throw new ArgumentNullException(nameof(url)); 14 | RelationshipType = relationshipType; 15 | } 16 | 17 | public Uri Uri { get; private set; } 18 | 19 | public string Title { get; set; } 20 | 21 | public string MediaType { get; set; } 22 | 23 | public string RelationshipType { get; } 24 | 25 | public long Length { get; set; } 26 | 27 | public DateTimeOffset LastUpdated { get; set; } 28 | } 29 | } -------------------------------------------------------------------------------- /src/SyndicationPerson.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | public sealed class SyndicationPerson : ISyndicationPerson 10 | { 11 | public SyndicationPerson(string name, string email, string relationshipType = null) 12 | { 13 | if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(email)) 14 | { 15 | throw new ArgumentNullException("Valid name or email is required"); 16 | } 17 | 18 | Name = name; 19 | Email = email; 20 | RelationshipType = relationshipType; 21 | } 22 | 23 | public string Email { get; private set; } 24 | 25 | public string Name { get; private set; } 26 | 27 | public string Uri { get; set; } 28 | 29 | public string RelationshipType { get; set; } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Utils/Converter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | static class Converter 10 | { 11 | public static bool TryParseValue(string value, out T result) 12 | { 13 | result = default(T); 14 | 15 | Type type = typeof(T); 16 | 17 | // 18 | // String 19 | if (type == typeof(string)) 20 | { 21 | result = (T)(object)value; 22 | return true; 23 | } 24 | 25 | if (value == null) 26 | { 27 | return false; 28 | } 29 | 30 | // 31 | // DateTimeOffset 32 | if (type == typeof(DateTimeOffset)) 33 | { 34 | if (DateTimeUtils.TryParseDate(value, out DateTimeOffset dt)) 35 | { 36 | result = (T)(object)dt; 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | // 44 | // DateTime 45 | if (type == typeof(DateTime)) 46 | { 47 | if (DateTimeUtils.TryParseDate(value, out DateTimeOffset dt)) 48 | { 49 | result = (T)(object) dt.DateTime; 50 | return true; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | // 57 | // TODO: being added in netstandard 2.0 58 | //if (type.GetTypeInfo().IsEnum) 59 | //{ 60 | // if (Enum.TryParse(typeof(T), value, true, out T o)) { 61 | // result = (T)(object)o; 62 | // return true; 63 | // } 64 | //} 65 | 66 | // 67 | // Uri 68 | if (type == typeof(Uri)) 69 | { 70 | if (UriUtils.TryParse(value, out Uri uri)) 71 | { 72 | result = (T)(object)uri; 73 | return true; 74 | } 75 | 76 | return false; 77 | } 78 | 79 | // 80 | // Fall back default 81 | return (result = (T)Convert.ChangeType(value, typeof(T))) != null; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Utils/DateTimeUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text; 4 | 5 | namespace Microsoft.SyndicationFeed 6 | { 7 | static class DateTimeUtils 8 | { 9 | private const string Rfc3339LocalDateTimeFormat = "yyyy-MM-ddTHH:mm:sszzz"; 10 | private const string Rfc3339UTCDateTimeFormat = "yyyy-MM-ddTHH:mm:ssZ"; 11 | 12 | public static bool TryParseDate(string value, out DateTimeOffset result) 13 | { 14 | if(TryParseDateRfc3339(value, out result)) 15 | { 16 | return true; 17 | } 18 | 19 | if (TryParseDateRssSpec(value, out result)) 20 | { 21 | return true; 22 | } 23 | 24 | return false; 25 | } 26 | 27 | public static string ToRfc3339String(DateTimeOffset dto) 28 | { 29 | if (dto.Offset == TimeSpan.Zero) 30 | { 31 | return dto.ToUniversalTime().ToString(Rfc3339UTCDateTimeFormat, CultureInfo.InvariantCulture); 32 | } 33 | else 34 | { 35 | return dto.ToString(Rfc3339LocalDateTimeFormat, CultureInfo.InvariantCulture); 36 | } 37 | } 38 | 39 | public static string ToRfc1123String(DateTimeOffset dto) 40 | { 41 | return dto.ToString("r"); 42 | } 43 | 44 | private static bool TryParseDateRssSpec(string value, out DateTimeOffset result) 45 | { 46 | if (string.IsNullOrEmpty(value)) 47 | { 48 | return false; 49 | } 50 | 51 | StringBuilder sb = new StringBuilder(value.Trim()); 52 | 53 | if (sb.Length < 18) 54 | { 55 | return false; 56 | } 57 | 58 | if (sb[3] == ',') 59 | { 60 | // There is a leading (e.g.) "Tue, ", strip it off 61 | sb.Remove(0, 4); 62 | 63 | // There's supposed to be a space here but some implementations dont have one 64 | TrimStart(sb); 65 | } 66 | 67 | CollapseWhitespaces(sb); 68 | 69 | if (!char.IsDigit(sb[1])) 70 | { 71 | sb.Insert(0, '0'); 72 | } 73 | 74 | if (sb.Length < 19) 75 | { 76 | return false; 77 | } 78 | 79 | bool thereAreSeconds = (sb[17] == ':'); 80 | int timeZoneStartIndex = thereAreSeconds ? 21 : 18; 81 | 82 | string timeZoneSuffix = sb.ToString().Substring(timeZoneStartIndex); 83 | sb.Remove(timeZoneStartIndex, sb.Length - timeZoneStartIndex); 84 | 85 | bool isUtc; 86 | sb.Append(NormalizeTimeZone(timeZoneSuffix, out isUtc)); 87 | 88 | string wellFormattedString = sb.ToString(); 89 | 90 | string parseFormat = thereAreSeconds ? "dd MMM yyyy HH:mm:ss zzz" : "dd MMM yyyy HH:mm zzz"; 91 | 92 | return DateTimeOffset.TryParseExact(wellFormattedString, 93 | parseFormat, 94 | CultureInfo.InvariantCulture.DateTimeFormat, 95 | isUtc ? DateTimeStyles.AdjustToUniversal : DateTimeStyles.None, 96 | out result); 97 | } 98 | 99 | private static string NormalizeTimeZone(string rfc822TimeZone, out bool isUtc) 100 | { 101 | isUtc = false; 102 | // return a string in "-08:00" format 103 | if (rfc822TimeZone[0] == '+' || rfc822TimeZone[0] == '-') 104 | { 105 | // the time zone is supposed to be 4 digits but some feeds omit the initial 0 106 | StringBuilder result = new StringBuilder(rfc822TimeZone); 107 | if (result.Length == 4) 108 | { 109 | // the timezone is +/-HMM. Convert to +/-HHMM 110 | result.Insert(1, '0'); 111 | } 112 | result.Insert(3, ':'); 113 | return result.ToString(); 114 | } 115 | switch (rfc822TimeZone) 116 | { 117 | case "UT": 118 | case "Z": 119 | isUtc = true; 120 | return "-00:00"; 121 | case "GMT": 122 | return "-00:00"; 123 | case "A": 124 | return "-01:00"; 125 | case "B": 126 | return "-02:00"; 127 | case "C": 128 | return "-03:00"; 129 | case "D": 130 | case "EDT": 131 | return "-04:00"; 132 | case "E": 133 | case "EST": 134 | case "CDT": 135 | return "-05:00"; 136 | case "F": 137 | case "CST": 138 | case "MDT": 139 | return "-06:00"; 140 | case "G": 141 | case "MST": 142 | case "PDT": 143 | return "-07:00"; 144 | case "H": 145 | case "PST": 146 | return "-08:00"; 147 | case "I": 148 | return "-09:00"; 149 | case "K": 150 | return "-10:00"; 151 | case "L": 152 | return "-11:00"; 153 | case "M": 154 | return "-12:00"; 155 | case "N": 156 | return "+01:00"; 157 | case "O": 158 | return "+02:00"; 159 | case "P": 160 | return "+03:00"; 161 | case "Q": 162 | return "+04:00"; 163 | case "R": 164 | return "+05:00"; 165 | case "S": 166 | return "+06:00"; 167 | case "T": 168 | return "+07:00"; 169 | case "U": 170 | return "+08:00"; 171 | case "V": 172 | return "+09:00"; 173 | case "W": 174 | return "+10:00"; 175 | case "X": 176 | return "+11:00"; 177 | case "Y": 178 | return "+12:00"; 179 | default: 180 | return ""; 181 | } 182 | } 183 | 184 | private static void TrimStart(StringBuilder sb) 185 | { 186 | int i = 0; 187 | while (i < sb.Length) 188 | { 189 | if (!char.IsWhiteSpace(sb[i])) 190 | { 191 | break; 192 | } 193 | ++i; 194 | } 195 | if (i > 0) 196 | { 197 | sb.Remove(0, i); 198 | } 199 | } 200 | 201 | private static void CollapseWhitespaces(StringBuilder builder) 202 | { 203 | int index = 0; 204 | int whiteSpaceStart = -1; 205 | while (index < builder.Length) 206 | { 207 | if (char.IsWhiteSpace(builder[index])) 208 | { 209 | if (whiteSpaceStart < 0) 210 | { 211 | whiteSpaceStart = index; 212 | // normalize all white spaces to be ' ' so that the date time parsing works 213 | builder[index] = ' '; 214 | } 215 | } 216 | else if (whiteSpaceStart >= 0) 217 | { 218 | if (index > whiteSpaceStart + 1) 219 | { 220 | // there are at least 2 spaces... replace by 1 221 | builder.Remove(whiteSpaceStart, index - whiteSpaceStart - 1); 222 | index = whiteSpaceStart + 1; 223 | } 224 | whiteSpaceStart = -1; 225 | } 226 | ++index; 227 | } 228 | // we have already trimmed the start and end so there cannot be a trail of white spaces in the end 229 | //Fx.Assert(builder.Length == 0 || builder[builder.Length - 1] != ' ', "The string builder doesnt end in a white space"); 230 | } 231 | 232 | private static bool TryParseDateRfc3339(string dateTimeString, out DateTimeOffset result) 233 | { 234 | const string Rfc3339LocalDateTimeFormat = "yyyy-MM-ddTHH:mm:sszzz"; 235 | const string Rfc3339UTCDateTimeFormat = "yyyy-MM-ddTHH:mm:ssZ"; 236 | 237 | dateTimeString = dateTimeString.Trim(); 238 | 239 | if (dateTimeString[19] == '.') 240 | { 241 | // remove any fractional seconds, we choose to ignore them 242 | int i = 20; 243 | while (dateTimeString.Length > i && char.IsDigit(dateTimeString[i])) 244 | { 245 | ++i; 246 | } 247 | dateTimeString = dateTimeString.Substring(0, 19) + dateTimeString.Substring(i); 248 | } 249 | 250 | DateTimeOffset localTime; 251 | if (DateTimeOffset.TryParseExact(dateTimeString, Rfc3339LocalDateTimeFormat, 252 | CultureInfo.InvariantCulture.DateTimeFormat, 253 | DateTimeStyles.None, out localTime)) 254 | { 255 | result = localTime; 256 | return true; 257 | } 258 | DateTimeOffset utcTime; 259 | if (DateTimeOffset.TryParseExact(dateTimeString, Rfc3339UTCDateTimeFormat, 260 | CultureInfo.InvariantCulture.DateTimeFormat, 261 | DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out utcTime)) 262 | { 263 | result = utcTime; 264 | return true; 265 | } 266 | return false; 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/Utils/UriUtils.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | 7 | namespace Microsoft.SyndicationFeed 8 | { 9 | static class UriUtils 10 | { 11 | public static bool TryParse(string value, out Uri result) 12 | { 13 | return Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out result); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Utils/XmlExtentions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Xml; 7 | 8 | namespace Microsoft.SyndicationFeed 9 | { 10 | static class XmlExtentions 11 | { 12 | public static ISyndicationAttribute ReadSyndicationAttribute(this XmlReader reader) 13 | { 14 | if (reader.NodeType != XmlNodeType.Attribute) 15 | { 16 | throw new InvalidOperationException("Invalid Xml Attribute"); 17 | } 18 | 19 | string ns = reader.NamespaceURI; 20 | string name = reader.LocalName; 21 | 22 | if (XmlUtils.IsXmlns(name, ns) || XmlUtils.IsXmlSchemaType(name, ns)) 23 | { 24 | return null; 25 | } 26 | 27 | return new SyndicationAttribute(name, ns, reader.Value); 28 | } 29 | 30 | 31 | public static void WriteStartSyndicationContent(this XmlWriter writer, ISyndicationContent content, string defaultNs) 32 | { 33 | string ns = content.Namespace ?? defaultNs; 34 | 35 | if (ns != null) 36 | { 37 | XmlUtils.SplitName(content.Name, out string prefix, out string localName); 38 | 39 | prefix = writer.LookupPrefix(ns) ?? prefix; 40 | 41 | if (prefix != null) 42 | { 43 | writer.WriteStartElement(prefix, localName, ns); 44 | } 45 | else 46 | { 47 | writer.WriteStartElement(localName, ns); 48 | } 49 | } 50 | else 51 | { 52 | writer.WriteStartElement(content.Name); 53 | } 54 | } 55 | 56 | public static void WriteSyndicationAttribute(this XmlWriter writer, ISyndicationAttribute attr) 57 | { 58 | XmlUtils.SplitName(attr.Name, out string prefix, out string localName); 59 | 60 | writer.WriteAttribute(prefix, attr.Name, localName, attr.Namespace, attr.Value); 61 | } 62 | 63 | public static void WriteXmlFragment(this XmlWriter writer, string fragment, string defaultNs) 64 | { 65 | using (var reader = XmlUtils.CreateXmlReader(fragment)) 66 | { 67 | reader.MoveToContent(); 68 | 69 | while (!reader.EOF) 70 | { 71 | string ns = !string.IsNullOrEmpty(reader.NamespaceURI) ? reader.NamespaceURI : defaultNs; 72 | 73 | // 74 | // Start Element 75 | if (reader.NodeType == XmlNodeType.Element) 76 | { 77 | if (ns == null) 78 | { 79 | writer.WriteStartElement(reader.LocalName); 80 | } 81 | else 82 | { 83 | writer.WriteStartElement(reader.LocalName, ns); 84 | } 85 | 86 | if (reader.HasAttributes) 87 | { 88 | while (reader.MoveToNextAttribute()) 89 | { 90 | if (!XmlUtils.IsXmlns(reader.Name, reader.Value)) 91 | { 92 | writer.WriteAttribute(reader.Prefix, reader.Name, reader.LocalName, ns, reader.Value); 93 | } 94 | } 95 | 96 | reader.MoveToContent(); 97 | } 98 | 99 | if (reader.IsEmptyElement) 100 | { 101 | writer.WriteEndElement(); 102 | } 103 | 104 | reader.Read(); 105 | continue; 106 | } 107 | 108 | // 109 | // End Element 110 | if (reader.NodeType == XmlNodeType.EndElement) 111 | { 112 | writer.WriteEndElement(); 113 | reader.Read(); 114 | continue; 115 | } 116 | 117 | // 118 | // Copy Content 119 | writer.WriteNode(reader, false); 120 | } 121 | } 122 | } 123 | 124 | public static void WriteString(this XmlWriter writer, string value, bool useCDATA) 125 | { 126 | if (useCDATA && XmlUtils.NeedXmlEscape(value)) 127 | { 128 | writer.WriteCData(value); 129 | } 130 | else 131 | { 132 | writer.WriteString(value); 133 | } 134 | } 135 | 136 | private static void WriteAttribute(this XmlWriter writer, string prefix, string name, string localName, string ns, string value) 137 | { 138 | prefix = prefix ?? writer.LookupPrefix(ns ?? string.Empty); 139 | 140 | if (prefix == string.Empty) 141 | { 142 | writer.WriteStartAttribute(name); 143 | } 144 | else if (prefix != null) 145 | { 146 | writer.WriteStartAttribute(prefix, localName, ns); 147 | } 148 | else 149 | { 150 | writer.WriteStartAttribute(localName, ns); 151 | } 152 | 153 | writer.WriteString(value); 154 | writer.WriteEndAttribute(); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Utils/XmlUtils.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Xml; 11 | 12 | namespace Microsoft.SyndicationFeed 13 | { 14 | static class XmlUtils 15 | { 16 | public const string XmlNs = "http://www.w3.org/XML/1998/namespace"; 17 | 18 | public static string GetValue(string xmlNode) 19 | { 20 | using (XmlReader reader = XmlReader.Create(new StringReader(xmlNode))) 21 | { 22 | reader.MoveToContent(); 23 | return reader.ReadElementContentAsString(); 24 | } 25 | } 26 | 27 | public static Task ReadOuterXmlAsync(XmlReader reader) 28 | { 29 | if (reader.Settings.Async) 30 | { 31 | return reader.ReadOuterXmlAsync(); 32 | } 33 | 34 | return Task.FromResult(reader.ReadOuterXml()); 35 | } 36 | 37 | public static Task SkipAsync(XmlReader reader) 38 | { 39 | if (reader.Settings.Async) 40 | { 41 | return reader.SkipAsync(); 42 | } 43 | 44 | reader.Skip(); 45 | 46 | return Task.CompletedTask; 47 | } 48 | 49 | public static Task ReadAsync(XmlReader reader) 50 | { 51 | if (reader.Settings.Async) 52 | { 53 | return reader.ReadAsync(); 54 | } 55 | 56 | return Task.FromResult(reader.Read()); 57 | } 58 | 59 | public static XmlReader CreateXmlReader(string value) 60 | { 61 | return XmlReader.Create(new StringReader(value), 62 | new XmlReaderSettings() 63 | { 64 | ConformanceLevel = ConformanceLevel.Fragment, 65 | DtdProcessing = DtdProcessing.Ignore, 66 | IgnoreComments = true, 67 | IgnoreWhitespace = true 68 | }); 69 | } 70 | 71 | public static XmlWriter CreateXmlWriter(XmlWriterSettings settings, IEnumerable attributes, StringBuilder buffer) 72 | { 73 | settings.Async = false; 74 | settings.OmitXmlDeclaration = true; 75 | settings.ConformanceLevel = ConformanceLevel.Fragment; 76 | 77 | XmlWriter writer = XmlWriter.Create(buffer, settings); 78 | 79 | // 80 | // Apply attributes 81 | if (attributes != null && attributes.Count() > 0) 82 | { 83 | // 84 | // Create element wrapper 85 | ISyndicationAttribute xmlns = attributes.FirstOrDefault(a => a.Name == "xmlns"); 86 | 87 | if (xmlns != null) 88 | { 89 | writer.WriteStartElement("w", xmlns.Value); 90 | } 91 | else 92 | { 93 | writer.WriteStartElement("w"); 94 | } 95 | 96 | // 97 | // Write attributes 98 | foreach (var a in attributes) 99 | { 100 | if (a != xmlns) 101 | { 102 | writer.WriteSyndicationAttribute(a); 103 | } 104 | } 105 | 106 | writer.WriteStartElement("y"); 107 | writer.WriteEndElement(); 108 | 109 | writer.Flush(); 110 | buffer.Clear(); 111 | } 112 | 113 | return writer; 114 | } 115 | 116 | public static Task WriteRawAsync(XmlWriter writer, string content) 117 | { 118 | if (writer.Settings.Async) 119 | { 120 | return writer.WriteRawAsync(content); 121 | } 122 | 123 | writer.WriteRaw(content); 124 | 125 | return Task.CompletedTask; 126 | } 127 | 128 | public static Task FlushAsync(XmlWriter writer) 129 | { 130 | if (writer.Settings.Async) 131 | { 132 | return writer.FlushAsync(); 133 | } 134 | 135 | writer.Flush(); 136 | 137 | return Task.CompletedTask; 138 | } 139 | 140 | public static void SplitName(string name, out string prefix, out string localName) 141 | { 142 | int i = name.IndexOf(':'); 143 | if (i > 0) 144 | { 145 | prefix = name.Substring(0, i); 146 | localName = name.Substring(i + 1); 147 | } 148 | else 149 | { 150 | prefix = null; 151 | localName = name; 152 | } 153 | } 154 | 155 | public static bool IsXmlns(string name, string ns) 156 | { 157 | return name == "xmlns" || ns == "http://www.w3.org/2000/xmlns/"; 158 | } 159 | 160 | public static bool IsXmlSchemaType(string name, string ns) 161 | { 162 | return name == "type" && ns == "http://www.w3.org/2001/XMLSchema-instance"; 163 | } 164 | 165 | public static bool IsXmlMediaType(string value) 166 | { 167 | return value != null && (value == "xml" || value.EndsWith("/xml") || value.EndsWith("+xml")); 168 | } 169 | 170 | public static bool IsXhtmlMediaType(string value) 171 | { 172 | return value == "xhtml"; 173 | } 174 | 175 | public static bool NeedXmlEscape(string value) 176 | { 177 | if (string.IsNullOrEmpty(value)) 178 | { 179 | return false; 180 | } 181 | 182 | for (int i = 0; i < value.Length; ++i) 183 | { 184 | char ch = value[i]; 185 | 186 | if (ch == '<' || ch == '>' || ch == '&' || char.IsSurrogate(ch)) 187 | { 188 | return true; 189 | } 190 | } 191 | 192 | return false; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/XmlFeedReader.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using System.Xml; 8 | 9 | namespace Microsoft.SyndicationFeed 10 | { 11 | public abstract class XmlFeedReader : ISyndicationFeedReader 12 | { 13 | private readonly XmlReader _reader; 14 | private bool _currentSet; 15 | 16 | protected XmlFeedReader(XmlReader reader, ISyndicationFeedParser parser) 17 | { 18 | _reader = reader ?? throw new ArgumentNullException(nameof(reader)); 19 | Parser = parser ?? throw new ArgumentNullException(nameof(parser)); 20 | 21 | ElementType = SyndicationElementType.None; 22 | } 23 | 24 | public ISyndicationFeedParser Parser { get; private set; } 25 | 26 | public SyndicationElementType ElementType { get; private set; } 27 | 28 | public string ElementName { get; private set; } 29 | 30 | public virtual async Task Read() 31 | { 32 | if (_currentSet) { 33 | // 34 | // The reader is already advanced, return status 35 | _currentSet = false; 36 | return !_reader.EOF; 37 | } 38 | else { 39 | if (ElementType != SyndicationElementType.None) 40 | { 41 | await Skip(); 42 | return !_reader.EOF; 43 | } 44 | else 45 | { 46 | // 47 | // Advance the reader 48 | return await MoveNext(false); 49 | } 50 | } 51 | } 52 | 53 | public virtual async Task Skip() 54 | { 55 | await XmlUtils.SkipAsync(_reader); 56 | await MoveNext(false); 57 | } 58 | 59 | public virtual async Task ReadCategory() 60 | { 61 | if (ElementType == SyndicationElementType.None) 62 | { 63 | await Read(); 64 | } 65 | 66 | if (ElementType != SyndicationElementType.Category) 67 | { 68 | throw new InvalidOperationException("Unknown Category"); 69 | } 70 | 71 | return Parser.ParseCategory(await ReadElementAsString()); 72 | } 73 | 74 | public virtual async Task ReadContent() 75 | { 76 | if (ElementType == SyndicationElementType.None) 77 | { 78 | await Read(); 79 | } 80 | 81 | // 82 | // Any element can be read as ISyndicationContent 83 | if (ElementType == SyndicationElementType.None) 84 | { 85 | throw new InvalidOperationException("Unknown Content"); 86 | } 87 | 88 | return Parser.ParseContent(await ReadElementAsString()); 89 | } 90 | 91 | public virtual async Task ReadItem() 92 | { 93 | if (ElementType == SyndicationElementType.None) 94 | { 95 | await Read(); 96 | } 97 | 98 | if (ElementType != SyndicationElementType.Item) 99 | { 100 | throw new InvalidOperationException("Unknown Item"); 101 | } 102 | 103 | return Parser.ParseItem(await ReadElementAsString()); 104 | } 105 | 106 | public virtual async Task ReadLink() 107 | { 108 | if (ElementType == SyndicationElementType.None) 109 | { 110 | await Read(); 111 | } 112 | 113 | if (ElementType != SyndicationElementType.Link) 114 | { 115 | throw new InvalidOperationException("Unknown Link"); 116 | } 117 | 118 | return Parser.ParseLink(await ReadElementAsString()); 119 | } 120 | 121 | public virtual async Task ReadPerson() 122 | { 123 | if (ElementType == SyndicationElementType.None) 124 | { 125 | await Read(); 126 | } 127 | 128 | if (ElementType != SyndicationElementType.Person) 129 | { 130 | throw new InvalidOperationException("Unknown Person"); 131 | } 132 | 133 | return Parser.ParsePerson(await ReadElementAsString()); 134 | } 135 | 136 | public virtual async Task ReadImage() 137 | { 138 | if (ElementType == SyndicationElementType.None) 139 | { 140 | await Read(); 141 | } 142 | 143 | if (ElementType != SyndicationElementType.Image) 144 | { 145 | throw new InvalidOperationException("Unknown Image"); 146 | } 147 | 148 | return Parser.ParseImage(await ReadElementAsString()); 149 | } 150 | 151 | public virtual async Task ReadValue() 152 | { 153 | ISyndicationContent content = await ReadContent(); 154 | 155 | if (!Parser.TryParseValue(content.Value, out T value)) 156 | { 157 | throw new FormatException(); 158 | } 159 | 160 | return value; 161 | } 162 | 163 | public virtual async Task ReadElementAsString() 164 | { 165 | string result = await XmlUtils.ReadOuterXmlAsync(_reader); 166 | 167 | await MoveNext(); 168 | 169 | return result; 170 | } 171 | 172 | protected abstract SyndicationElementType MapElementType(string elementName); 173 | 174 | private async Task MoveNext(bool setCurrent = true) 175 | { 176 | do 177 | { 178 | if (_reader.NodeType == XmlNodeType.Element) 179 | { 180 | ElementType = MapElementType(_reader.LocalName); 181 | ElementName = _reader.LocalName; 182 | 183 | _currentSet = setCurrent; 184 | 185 | return true; 186 | } 187 | } 188 | while (await XmlUtils.ReadAsync(_reader)); 189 | 190 | // 191 | // Reset 192 | ElementType = SyndicationElementType.None; 193 | ElementName = null; 194 | _currentSet = false; 195 | 196 | return false; 197 | } 198 | } 199 | } -------------------------------------------------------------------------------- /src/XmlFeedWriter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | using System.Xml; 9 | 10 | namespace Microsoft.SyndicationFeed 11 | { 12 | public abstract class XmlFeedWriter : ISyndicationFeedWriter 13 | { 14 | private readonly XmlWriter _writer; 15 | 16 | protected XmlFeedWriter(XmlWriter writer, ISyndicationFeedFormatter formatter) 17 | { 18 | _writer = writer ?? throw new ArgumentNullException(nameof(writer)); 19 | Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); 20 | } 21 | 22 | public ISyndicationFeedFormatter Formatter { get; private set; } 23 | 24 | public virtual Task Write(ISyndicationContent content) 25 | { 26 | return WriteRaw(Formatter.Format(content ?? throw new ArgumentNullException(nameof(content)))); 27 | } 28 | 29 | public virtual Task Write(ISyndicationCategory category) 30 | { 31 | return WriteRaw(Formatter.Format(category ?? throw new ArgumentNullException(nameof(category)))); 32 | } 33 | 34 | public virtual Task Write(ISyndicationImage image) 35 | { 36 | return WriteRaw(Formatter.Format(image ?? throw new ArgumentNullException(nameof(image)))); 37 | } 38 | 39 | public virtual Task Write(ISyndicationItem item) 40 | { 41 | return WriteRaw(Formatter.Format(item ?? throw new ArgumentNullException(nameof(item)))); 42 | } 43 | 44 | public virtual Task Write(ISyndicationPerson person) 45 | { 46 | return WriteRaw(Formatter.Format(person ?? throw new ArgumentNullException(nameof(person)))); 47 | } 48 | 49 | public virtual Task Write(ISyndicationLink link) 50 | { 51 | return WriteRaw(Formatter.Format(link ?? throw new ArgumentNullException(nameof(link)))); 52 | } 53 | 54 | public virtual Task WriteValue(string name, T value) 55 | { 56 | if (string.IsNullOrEmpty(name)) 57 | { 58 | throw new ArgumentNullException(nameof(name)); 59 | } 60 | 61 | string valueString = Formatter.FormatValue(value); 62 | 63 | if (valueString == null) 64 | { 65 | throw new FormatException(nameof(value)); 66 | } 67 | 68 | return WriteRaw(Formatter.Format(new SyndicationContent(name, valueString))); 69 | } 70 | 71 | public virtual Task WriteRaw(string content) 72 | { 73 | return XmlUtils.WriteRawAsync(_writer, content); 74 | } 75 | 76 | public Task Flush() 77 | { 78 | return XmlUtils.FlushAsync(_writer); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/AtomReader.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.SyndicationFeed.Atom; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Diagnostics; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using System.Xml; 12 | using Xunit; 13 | 14 | namespace Microsoft.SyndicationFeed.Tests.Atom 15 | { 16 | public class AtomReader 17 | { 18 | [Fact] 19 | public async Task ReadPerson() 20 | { 21 | using (XmlReader xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\simpleAtomFeed.xml", new XmlReaderSettings { Async = true })) 22 | { 23 | var persons = new List(); 24 | var reader = new AtomFeedReader(xmlReader); 25 | while (await reader.Read()) 26 | { 27 | if(reader.ElementType == SyndicationElementType.Person) 28 | { 29 | ISyndicationPerson person = await reader.ReadPerson(); 30 | persons.Add(person); 31 | } 32 | } 33 | 34 | Assert.True(persons.Count() == 2); 35 | Assert.True(persons[0].Name == "Mark Pilgrim"); 36 | Assert.True(persons[0].Email == "f8dy@example.com"); 37 | Assert.True(persons[0].Uri == "http://example.org/"); 38 | Assert.True(persons[1].Name == "Sam Ruby"); 39 | 40 | } 41 | } 42 | 43 | [Fact] 44 | public async Task ReadImage() 45 | { 46 | using (XmlReader xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\simpleAtomFeed.xml", new XmlReaderSettings { Async = true })) 47 | { 48 | var reader = new AtomFeedReader(xmlReader); 49 | int imagesRead = 0; 50 | 51 | List contentsOfImages = new List(); 52 | 53 | while (await reader.Read()) 54 | { 55 | if (reader.ElementType == SyndicationElementType.Image) 56 | { 57 | ISyndicationImage image = await reader.ReadImage(); 58 | imagesRead++; 59 | contentsOfImages.Add(image.Url.OriginalString); 60 | } 61 | } 62 | Assert.True(imagesRead == 2); 63 | Assert.True(contentsOfImages[0] == "/icon.jpg"); 64 | Assert.True(contentsOfImages[1] == "/logo.jpg"); 65 | } 66 | } 67 | 68 | [Fact] 69 | public async Task ReadCategory() 70 | { 71 | using (XmlReader xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\simpleAtomFeed.xml", new XmlReaderSettings { Async = true })) 72 | { 73 | var reader = new AtomFeedReader(xmlReader); 74 | while (await reader.Read()) 75 | { 76 | if (reader.ElementType == SyndicationElementType.Category) 77 | { 78 | ISyndicationCategory category = await reader.ReadCategory(); 79 | Assert.True(category.Name == "sports"); 80 | Assert.True(category.Label == "testLabel"); 81 | Assert.True(category.Scheme == "testScheme"); 82 | } 83 | } 84 | } 85 | } 86 | 87 | [Fact] 88 | public async Task ReadLink() 89 | { 90 | using (XmlReader xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\simpleAtomFeed.xml", new XmlReaderSettings { Async = true })) 91 | { 92 | var reader = new AtomFeedReader(xmlReader); 93 | List hrefs = new List(); 94 | while (await reader.Read()) 95 | { 96 | if (reader.ElementType == SyndicationElementType.Link) 97 | { 98 | ISyndicationLink link = await reader.ReadLink(); 99 | hrefs.Add(link.Uri.OriginalString); 100 | } 101 | } 102 | Assert.True(hrefs[0] == "http://example.org/"); 103 | Assert.True(hrefs[1] == "http://example.org/feed.atom"); 104 | } 105 | } 106 | 107 | [Fact] 108 | public async Task ReadItem() 109 | { 110 | using (XmlReader xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\simpleAtomFeed.xml", new XmlReaderSettings { Async = true })) 111 | { 112 | var reader = new AtomFeedReader(xmlReader); 113 | while (await reader.Read()) 114 | { 115 | if (reader.ElementType == SyndicationElementType.Item) 116 | { 117 | IAtomEntry item = await reader.ReadEntry(); 118 | 119 | //Assert content of item 120 | Assert.True(item.Title == "Atom draft-07 snapshot"); 121 | Assert.True(item.Links.Count() == 3); 122 | Assert.True(item.Contributors.Count() == 3); 123 | Assert.True(item.Rights == "All rights Reserved. Contoso."); 124 | Assert.True(item.Id == "tag:example.org,2003:3.2397"); 125 | Assert.False(string.IsNullOrEmpty(item.Description)); 126 | } 127 | } 128 | } 129 | } 130 | 131 | [Fact] 132 | public async Task ReadItemContent() 133 | { 134 | using( XmlReader xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\simpleAtomFeed.xml", new XmlReaderSettings { Async = true })) 135 | { 136 | var reader = new AtomFeedReader(xmlReader); 137 | 138 | while (await reader.Read()) 139 | { 140 | if (reader.ElementType == SyndicationElementType.Item) 141 | { 142 | ISyndicationContent content = await reader.ReadContent(); 143 | 144 | var fields = content.Fields.ToArray(); 145 | 146 | Assert.True(fields.Length == 12); 147 | 148 | Assert.True(fields[0].Name == "title"); 149 | Assert.False(string.IsNullOrEmpty(fields[0].Value)); 150 | 151 | Assert.True(fields[1].Name == "link"); 152 | Assert.True(fields[1].Attributes.Count() > 0); 153 | 154 | Assert.True(fields[2].Name == "link"); 155 | Assert.True(fields[2].Attributes.Count() > 0); 156 | 157 | Assert.True(fields[3].Name == "id"); 158 | Assert.False(string.IsNullOrEmpty(fields[3].Value)); 159 | 160 | Assert.True(fields[4].Name == "updated"); 161 | Assert.False(string.IsNullOrEmpty(fields[4].Value)); 162 | 163 | Assert.True(fields[5].Name == "published"); 164 | Assert.False(string.IsNullOrEmpty(fields[5].Value)); 165 | 166 | Assert.True(fields[6].Name == "source"); 167 | Assert.True(fields[6].Fields.Count() > 0); 168 | 169 | Assert.True(fields[7].Name == "author"); 170 | Assert.True(fields[7].Fields.Count() > 0); 171 | 172 | Assert.True(fields[8].Name == "contributor"); 173 | Assert.True(fields[8].Fields.Count() > 0); 174 | 175 | Assert.True(fields[9].Name == "contributor"); 176 | Assert.True(fields[9].Fields.Count() > 0); 177 | 178 | Assert.True(fields[10].Name == "rights"); 179 | Assert.False(string.IsNullOrEmpty(fields[10].Value)); 180 | 181 | Assert.True(fields[11].Name == "content"); 182 | Assert.False(string.IsNullOrEmpty(fields[11].Value)); 183 | 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /tests/AtomWriter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.SyndicationFeed.Atom; 6 | using System; 7 | using System.Globalization; 8 | using System.IO; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using System.Xml; 12 | using Xunit; 13 | 14 | namespace Microsoft.SyndicationFeed.Tests.Atom 15 | { 16 | public class AtomWriter 17 | { 18 | [Fact] 19 | public async Task WriteCategory() 20 | { 21 | var category = new SyndicationCategory("Test Category"); 22 | 23 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 24 | 25 | using (var xmlWriter = XmlWriter.Create(sw)) 26 | { 27 | var writer = new AtomFeedWriter(xmlWriter); 28 | 29 | await writer.Write(category); 30 | await writer.Flush(); 31 | } 32 | 33 | string res = sw.ToString(); 34 | Assert.True(CheckResult(res, $"")); 35 | } 36 | 37 | [Fact] 38 | public async Task WritePerson() 39 | { 40 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 41 | 42 | var p1 = new SyndicationPerson("John Doe", "johndoe@contoso.com"); 43 | var p2 = new SyndicationPerson("Jane Doe", "janedoe@contoso.com", AtomContributorTypes.Contributor) 44 | { 45 | Uri = "www.contoso.com/janedoe" 46 | }; 47 | 48 | using (var xmlWriter = XmlWriter.Create(sw)) 49 | { 50 | var writer = new AtomFeedWriter(xmlWriter); 51 | 52 | await writer.Write(p1); 53 | await writer.Write(p2); 54 | 55 | await writer.Flush(); 56 | } 57 | 58 | string res = sw.ToString(); 59 | Assert.True(CheckResult(res, $"{p1.Name}{p1.Email}{p2.Name}{p2.Email}{p2.Uri}")); 60 | } 61 | 62 | [Fact] 63 | public async Task WriteImage() 64 | { 65 | var icon = new SyndicationImage(new Uri("http://contoso.com/icon.ico"), AtomImageTypes.Icon); 66 | var logo = new SyndicationImage(new Uri("http://contoso.com/logo.png"), AtomImageTypes.Logo); 67 | 68 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 69 | 70 | using (var xmlWriter = XmlWriter.Create(sw)) 71 | { 72 | var writer = new AtomFeedWriter(xmlWriter); 73 | 74 | await writer.Write(icon); 75 | await writer.Write(logo); 76 | 77 | await writer.Flush(); 78 | } 79 | 80 | string res = sw.ToString(); 81 | Assert.True(CheckResult(res, $"{icon.Url}{logo.Url}")); 82 | } 83 | 84 | 85 | [Fact] 86 | public async Task WriteLink() 87 | { 88 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 89 | 90 | var link = new SyndicationLink(new Uri("http://contoso.com")) 91 | { 92 | Title = "Test title", 93 | Length = 123, 94 | MediaType = "mp3/video" 95 | }; 96 | 97 | using (var xmlWriter = XmlWriter.Create(sw)) 98 | { 99 | var writer = new AtomFeedWriter(xmlWriter); 100 | 101 | await writer.Write(link); 102 | 103 | await writer.Flush(); 104 | } 105 | 106 | string res = sw.ToString(); 107 | Assert.True(CheckResult(res, $"")); 108 | } 109 | 110 | 111 | [Fact] 112 | public async Task WriteEntry() 113 | { 114 | var link = new SyndicationLink(new Uri("https://contoso.com/alternate")); 115 | var related = new SyndicationLink(new Uri("https://contoso.com/related"), AtomLinkTypes.Related); 116 | var self = new SyndicationLink(new Uri("https://contoso.com/28af09b3"), AtomLinkTypes.Self); 117 | var enclosure = new SyndicationLink(new Uri("https://contoso.com/podcast"), AtomLinkTypes.Enclosure) 118 | { 119 | Title = "Podcast", 120 | MediaType = "audio/mpeg", 121 | Length = 4123 122 | }; 123 | var source = new SyndicationLink(new Uri("https://contoso.com/source"), AtomLinkTypes.Source) 124 | { 125 | Title = "Blog", 126 | LastUpdated = DateTimeOffset.UtcNow.AddDays(-10) 127 | }; 128 | var author = new SyndicationPerson("John Doe", "johndoe@email.com"); 129 | var category = new SyndicationCategory("Lorem Category"); 130 | 131 | // 132 | // Construct entry 133 | var entry = new AtomEntry() 134 | { 135 | Id = "https://contoso.com/28af09b3", 136 | Title = "Lorem Ipsum", 137 | Description = "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit...", 138 | LastUpdated = DateTimeOffset.UtcNow, 139 | ContentType = "text/html", 140 | Summary = "Proin egestas sem in est feugiat, id laoreet massa dignissim", 141 | Rights = $"copyright (c) {DateTimeOffset.UtcNow.Year}" 142 | }; 143 | 144 | entry.AddLink(link); 145 | entry.AddLink(enclosure); 146 | entry.AddLink(related); 147 | entry.AddLink(source); 148 | entry.AddLink(self); 149 | 150 | entry.AddContributor(author); 151 | 152 | entry.AddCategory(category); 153 | 154 | // 155 | // Write 156 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 157 | 158 | using (var xmlWriter = XmlWriter.Create(sw)) 159 | { 160 | var writer = new AtomFeedWriter(xmlWriter); 161 | 162 | await writer.Write(entry); 163 | await writer.Flush(); 164 | } 165 | 166 | string res = sw.ToString(); 167 | Assert.True(CheckResult(res, $"{entry.Id}{entry.Title}{entry.LastUpdated.ToRfc3339()}{source.Title}{source.LastUpdated.ToRfc3339()}{author.Name}{author.Email}{entry.Description}{entry.Summary}{entry.Rights}")); 168 | } 169 | 170 | [Fact] 171 | public async Task WriteValue() 172 | { 173 | const string title = "Example Feed"; 174 | Guid id = Guid.NewGuid(); 175 | DateTimeOffset updated = DateTimeOffset.UtcNow.AddDays(-21); 176 | 177 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 178 | 179 | using (var xmlWriter = XmlWriter.Create(sw)) 180 | { 181 | var writer = new AtomFeedWriter(xmlWriter); 182 | 183 | await writer.WriteTitle(title); 184 | await writer.WriteId(id.ToString()); 185 | await writer.WriteUpdated(updated); 186 | 187 | await writer.Flush(); 188 | } 189 | 190 | string res = sw.ToString(); 191 | Assert.True(CheckResult(res, $"{title}{id}{updated.ToRfc3339()}")); 192 | } 193 | 194 | [Fact] 195 | public async Task WriteContent() 196 | { 197 | const string uri = "https://contoso.com/generator"; 198 | const string version = "1.0"; 199 | const string generator = "Example Toolkit"; 200 | 201 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 202 | 203 | using (var xmlWriter = XmlWriter.Create(sw)) 204 | { 205 | var writer = new AtomFeedWriter(xmlWriter); 206 | 207 | await writer.WriteGenerator(generator, uri, version); 208 | 209 | await writer.Flush(); 210 | } 211 | 212 | string res = sw.ToString(); 213 | Assert.True(CheckResult(res, $"{generator}")); 214 | } 215 | 216 | [Fact] 217 | public async Task WritePrefixedAtomNs() 218 | { 219 | const string title = "Example Feed"; 220 | const string uri = "https://contoso.com/generator"; 221 | const string generator = "Example Toolkit"; 222 | 223 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 224 | 225 | using (var xmlWriter = XmlWriter.Create(sw)) 226 | { 227 | var writer = new AtomFeedWriter(xmlWriter, 228 | new ISyndicationAttribute[] { new SyndicationAttribute("xmlns:atom", "http://www.w3.org/2005/Atom") }); 229 | 230 | await writer.WriteTitle(title); 231 | await writer.WriteGenerator(generator, uri, null); 232 | 233 | await writer.Flush(); 234 | } 235 | 236 | string res = sw.ToString(); 237 | Assert.True(CheckResult(res, $"{title}{generator}", "atom")); 238 | } 239 | 240 | [Fact] 241 | public async Task EmbededAtomInRssFeed() 242 | { 243 | var author = new SyndicationPerson("john doe", "johndoe@contoso.com"); 244 | var entry = new AtomEntry() 245 | { 246 | Id = "https://contoso.com/28af09b3", 247 | Title = "Atom Entry", 248 | Description = "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit...", 249 | LastUpdated = DateTimeOffset.UtcNow 250 | }; 251 | entry.AddContributor(author); 252 | 253 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 254 | 255 | using (var xmlWriter = XmlWriter.Create(sw)) 256 | { 257 | var attributes = new ISyndicationAttribute[] { new SyndicationAttribute("xmlns:atom", "http://www.w3.org/2005/Atom") }; 258 | var writer = new SyndicationFeed.Rss.RssFeedWriter(xmlWriter, attributes); 259 | var formatter = new AtomFormatter(attributes, xmlWriter.Settings); 260 | 261 | // 262 | // Write Rss elements 263 | await writer.WriteValue(SyndicationFeed.Rss.RssElementNames.Title, "Rss Title"); 264 | await writer.Write(author); 265 | await writer.Write(new SyndicationItem() 266 | { 267 | Title = "Rss Item", 268 | Id = "https://contoso.com/rss/28af09b3", 269 | Description = "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium", 270 | LastUpdated = DateTimeOffset.UtcNow 271 | }); 272 | 273 | // 274 | // Write atom entry 275 | await writer.WriteRaw(formatter.Format(entry)); 276 | 277 | await writer.Flush(); 278 | } 279 | 280 | string res = sw.ToString(); 281 | Assert.True(res.Contains($"{entry.Id}{entry.Title}{entry.LastUpdated.ToRfc3339()}{author.Name}{author.Email}{entry.Description}")); 282 | } 283 | 284 | [Fact] 285 | public async Task WriteXhtmlTextConstruct() 286 | { 287 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 288 | 289 | string content = "Heading"; 290 | 291 | using (var xmlWriter = XmlWriter.Create(sw)) 292 | { 293 | var writer = new AtomFeedWriter(xmlWriter); 294 | 295 | await writer.WriteText("title", content, "xhtml"); 296 | 297 | await writer.Flush(); 298 | } 299 | 300 | string res = sw.ToString(); 301 | Assert.True(CheckResult(res, $"{content}")); 302 | } 303 | 304 | [Fact] 305 | public async Task WriteXmlContent() 306 | { 307 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 308 | 309 | string content = "Heading"; 310 | 311 | using (var xmlWriter = XmlWriter.Create(sw)) 312 | { 313 | var writer = new AtomFeedWriter(xmlWriter); 314 | 315 | await writer.WriteText("content", content, "application/xml"); 316 | 317 | await writer.Flush(); 318 | } 319 | 320 | string res = sw.ToString(); 321 | Assert.True(CheckResult(res, $"{content}")); 322 | } 323 | 324 | [Fact] 325 | public async Task WriteCDATAValue() 326 | { 327 | var sw = new StringWriterWithEncoding(Encoding.UTF8); 328 | string title = "Title & Markup"; 329 | 330 | using (var xmlWriter = XmlWriter.Create(sw)) 331 | { 332 | var writer = new AtomFeedWriter(xmlWriter, null, new AtomFormatter() { UseCDATA = true }); 333 | 334 | await writer.WriteTitle(title); 335 | await writer.Flush(); 336 | } 337 | 338 | var res = sw.ToString(); 339 | Assert.True(CheckResult(res, $"")); 340 | } 341 | 342 | 343 | private static bool CheckResult(string result, string expected) 344 | { 345 | return result == $"{expected}"; 346 | } 347 | 348 | private static bool CheckResult(string result, string expected, string prefix) 349 | { 350 | return result == $"{expected}"; 351 | } 352 | } 353 | 354 | 355 | sealed class StringWriterWithEncoding : StringWriter 356 | { 357 | private readonly Encoding _encoding; 358 | 359 | public StringWriterWithEncoding(Encoding encoding) 360 | { 361 | this._encoding = encoding; 362 | } 363 | 364 | public override Encoding Encoding { 365 | get { return _encoding; } 366 | } 367 | } 368 | 369 | static class DateTimeOffsetExtentions 370 | { 371 | public static string ToRfc3339(this DateTimeOffset dto) 372 | { 373 | if (dto.Offset == TimeSpan.Zero) 374 | { 375 | return dto.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); 376 | } 377 | else 378 | { 379 | return dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture); 380 | } 381 | } 382 | } 383 | } -------------------------------------------------------------------------------- /tests/Microsoft.SyndicationFeed.ReaderWriter.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp1.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/RssReader.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.SyndicationFeed.Rss; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using System.Xml; 11 | using Xunit; 12 | 13 | namespace Microsoft.SyndicationFeed.Tests.Rss 14 | { 15 | public class RssReader 16 | { 17 | [Fact] 18 | public async Task ReadSequential() 19 | { 20 | using (var xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\rss20.xml", new XmlReaderSettings() { Async = true })) { 21 | var reader = new RssFeedReader(xmlReader); 22 | 23 | await reader.Read(); 24 | 25 | ISyndicationContent content = await reader.ReadContent(); 26 | content = await reader.ReadContent(); 27 | content = await reader.ReadContent(); 28 | } 29 | } 30 | 31 | [Fact] 32 | public async Task ReadItemAsContent() 33 | { 34 | using (var xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\rss20.xml", new XmlReaderSettings() { Async = true })) 35 | { 36 | var reader = new RssFeedReader(xmlReader); 37 | 38 | while (await reader.Read()) 39 | { 40 | if (reader.ElementType == SyndicationElementType.Item) 41 | { 42 | 43 | // Read as content 44 | ISyndicationContent content = await reader.ReadContent(); 45 | 46 | var fields = content.Fields.ToArray(); 47 | Assert.True(fields.Length >= 6); 48 | 49 | Assert.Equal("title", fields[0].Name); 50 | Assert.False(string.IsNullOrEmpty(fields[0].Value)); 51 | 52 | Assert.Equal("description", fields[1].Name); 53 | Assert.False(string.IsNullOrEmpty(fields[1].Value)); 54 | 55 | Assert.Equal("link", fields[2].Name); 56 | Assert.False(string.IsNullOrEmpty(fields[2].Value)); 57 | 58 | Assert.Equal("guid", fields[3].Name); 59 | Assert.Equal(fields[3].Attributes.Count(), 1); 60 | Assert.False(string.IsNullOrEmpty(fields[3].Value)); 61 | 62 | Assert.Equal("creator", fields[4].Name); 63 | Assert.Equal("http://purl.org/dc/elements/1.1/", fields[4].Namespace); 64 | Assert.False(string.IsNullOrEmpty(fields[4].Value)); 65 | 66 | Assert.Equal("pubDate", fields[5].Name); 67 | Assert.False(string.IsNullOrEmpty(fields[5].Value)); 68 | } 69 | } 70 | } 71 | } 72 | 73 | [Fact] 74 | public async Task ReadCategory() 75 | { 76 | using (var xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\rss20.xml", new XmlReaderSettings() { Async = true })) 77 | { 78 | var reader = new RssFeedReader(xmlReader); 79 | 80 | while (await reader.Read()) 81 | { 82 | if (reader.ElementType == SyndicationElementType.Category) 83 | { 84 | ISyndicationCategory category = await reader.ReadCategory(); 85 | 86 | Assert.True(category.Name == "Newspapers"); 87 | Assert.True(category.Scheme == "http://example.com/news"); 88 | } 89 | } 90 | } 91 | } 92 | 93 | [Fact] 94 | public async Task ReadItemCategory() 95 | { 96 | using (var xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\rss20.xml", new XmlReaderSettings() { Async = true })) 97 | { 98 | var reader = new RssFeedReader(xmlReader); 99 | 100 | while (await reader.Read()) 101 | { 102 | if (reader.ElementType == SyndicationElementType.Item) 103 | { 104 | ISyndicationItem item = await reader.ReadItem(); 105 | 106 | foreach (var c in item.Categories) 107 | { 108 | Assert.True(c.Name == "Newspapers"); 109 | Assert.True(c.Scheme == null || c.Scheme == "http://example.com/news/item"); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | [Fact] 117 | public async Task CountItems() 118 | { 119 | int itemCount = 0; 120 | 121 | using (var xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\rss20.xml", new XmlReaderSettings() { Async = true })) { 122 | var reader = new RssFeedReader(xmlReader); 123 | 124 | while (await reader.Read()) { 125 | if (reader.ElementType == SyndicationElementType.Item) { 126 | itemCount++; 127 | } 128 | } 129 | } 130 | 131 | Assert.Equal(itemCount, 10); 132 | } 133 | 134 | [Fact] 135 | private async Task ReadWhile() 136 | { 137 | using (var xmlReader = XmlReader.Create(@"..\..\..\TestFeeds\rss20.xml", new XmlReaderSettings() { Async = true })) 138 | { 139 | var reader = new RssFeedReader(xmlReader); 140 | 141 | while (await reader.Read()) 142 | { 143 | switch (reader.ElementType) 144 | { 145 | case SyndicationElementType.Link: 146 | ISyndicationLink link = await reader.ReadLink(); 147 | break; 148 | 149 | case SyndicationElementType.Item: 150 | ISyndicationItem item = await reader.ReadItem(); 151 | break; 152 | 153 | case SyndicationElementType.Person: 154 | ISyndicationPerson person = await reader.ReadPerson(); 155 | break; 156 | 157 | case SyndicationElementType.Image: 158 | ISyndicationImage image = await reader.ReadImage(); 159 | break; 160 | 161 | default: 162 | ISyndicationContent content = await reader.ReadContent(); 163 | break; 164 | } 165 | } 166 | } 167 | } 168 | 169 | [Fact] 170 | public static async Task ReadFeedElements() 171 | { 172 | var reader = XmlReader.Create(@"..\..\..\TestFeeds\rss20-2items.xml", new XmlReaderSettings() { Async = true }); 173 | await TestReadFeedElements(reader); 174 | } 175 | 176 | public static async Task TestReadFeedElements(XmlReader outerXmlReader) 177 | { 178 | using (var xmlReader = outerXmlReader) 179 | { 180 | var reader = new RssFeedReader(xmlReader); 181 | int items = 0; 182 | while (await reader.Read()) 183 | { 184 | switch (reader.ElementType) 185 | { 186 | case SyndicationElementType.Person: 187 | ISyndicationPerson person = await reader.ReadPerson(); 188 | Assert.True(person.Email == "John Smith"); 189 | break; 190 | 191 | case SyndicationElementType.Link: 192 | ISyndicationLink link = await reader.ReadLink(); 193 | Assert.True(link.Length == 123); 194 | Assert.True(link.MediaType == "testType"); 195 | Assert.True(link.Uri.OriginalString == "http://example.com/"); 196 | break; 197 | 198 | case SyndicationElementType.Image: 199 | ISyndicationImage image = await reader.ReadImage(); 200 | Assert.True(image.Title == "Microsoft News"); 201 | Assert.True(image.Description == "Test description"); 202 | Assert.True(image.Url.OriginalString == "http://2.bp.blogspot.com/-NA5Jb-64eUg/URx8CSdcj_I/AAAAAAAAAUo/eCx0irI0rq0/s1600/bg_Microsoft_logo3-20120824073001907469-620x349.jpg"); 203 | break; 204 | 205 | case SyndicationElementType.Item: 206 | items++; 207 | ISyndicationItem item = await reader.ReadItem(); 208 | 209 | if (items == 1) 210 | { 211 | Assert.True(item.Title == "Lorem ipsum 2017-07-06T20:25:00+00:00"); 212 | Assert.True(item.Description == "Exercitation sit dolore mollit et est eiusmod veniam aute officia veniam ipsum."); 213 | Assert.True(item.Links.Count() == 3); 214 | } 215 | else if(items == 2) 216 | { 217 | Assert.True(item.Title == "Lorem ipsum 2017-07-06T20:24:00+00:00"); 218 | Assert.True(item.Description == "Do ipsum dolore veniam minim est cillum aliqua ea."); 219 | Assert.True(item.Links.Count() == 3); 220 | } 221 | 222 | break; 223 | 224 | default: 225 | break; 226 | } 227 | } 228 | } 229 | } 230 | 231 | 232 | public static async Task> RssReadFeedContent(XmlReader xmlReader) 233 | { 234 | var list = new List(); 235 | 236 | using (XmlReader xReader = xmlReader) 237 | { 238 | var reader = new RssFeedReader(xReader); 239 | 240 | while(await reader.Read()) 241 | { 242 | list.Add(await reader.ReadContent()); 243 | } 244 | } 245 | 246 | return list; 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /tests/TestFeeds/CustomXml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | http://example.com/test/1499372700 9 | http://example.com/test/1499372700 10 | Thu, 06 Jul 2017 20:25:00 GMT 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/TestFeeds/SimpleRssFeed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft News 5 | http://www.microsoft.com/news 6 | My new category 7 | <div>Most recent news from Microsoft</div> 8 | jerry@microsoft.com 9 | Mon, 19 Jun 2017 11:52:39 -0700 10 | 11 | http://2.bp.blogspot.com/-NA5Jb-64eUg/URx8CSdcj_I/AAAAAAAAAUo/eCx0irI0rq0/s1600/bg_Microsoft_logo3-20120824073001907469-620x349.jpg 12 | Microsoft News 13 | http://www.microsoft.com/news 14 | 15 | 123FeedID 16 | asd 17 | 18 | http://microsoft.com/news/path 19 | SyndicationFeed released for .net Core 20 | A lot of text describing the release of .net core feature 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/TestFeeds/internetRssFeed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NDTV News - Latest 5 | http://www.ndtv.com/ 6 | August 03, 2017 02:50 AM 7 | 0.6 8 | GMT+05:30 9 | 200 10 | 11 | 12 | 13 | 14 | 15 | 1732763 16 | 17 | 18 | August 03, 2017 02:37 AM 19 | Thu, 03 Aug 2017 02:37:09 +0530 20 | 21 | 22 | 23 | 24 | 25 | NDTV 26 | 27 | 28 | 1732758 29 | 30 | 31 | August 03, 2017 01:33 AM 32 | Thu, 03 Aug 2017 01:33:51 +0530 33 | 34 | 35 | 36 | 37 | 38 | PTI 39 | 40 | 41 | 1732755 42 | 43 | 44 | August 03, 2017 01:30 AM 45 | Thu, 03 Aug 2017 01:30:16 +0530 46 | 47 | 48 | 49 | 50 | 51 | AFP 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/TestFeeds/rss20-2items.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | http://example.com/ 7 | 8 | http://2.bp.blogspot.com/-NA5Jb-64eUg/URx8CSdcj_I/AAAAAAAAAUo/eCx0irI0rq0/s1600/bg_Microsoft_logo3-20120824073001907469-620x349.jpg 9 | Microsoft News 10 | http://www.microsoft.com/news 11 | Test description 12 | 13 | RSS for Node 14 | Thu, 06 Jul 2017 20:25:17 GMT 15 | 16 | Thu, 06 Jul 2017 20:25:00 GMT 17 | 18 | 60 19 | 20 | 21 | 22 | 23 | http://example.com/test/1499372700 24 | http://example.com/test/1499372700 25 | 26 | Thu, 06 Jul 2017 20:25:00 GMT 27 | Testing Custom Elements 28 | 29 | 30 | 31 | 32 | http://example.com/test/1499372640 33 | http://example.com/test/1499372640 34 | 35 | Thu, 06 Jul 2017 20:24:00 GMT 36 | Ignored Text 37 | Testing Custom Elements 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/TestFeeds/rss20.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | abc@def.com (John Doe) 5 | 6 | 7 | http://example.com/ 8 | Newspapers 9 | 10 | http://2.bp.blogspot.com/-NA5Jb-64eUg/URx8CSdcj_I/AAAAAAAAAUo/eCx0irI0rq0/s1600/bg_Microsoft_logo3-20120824073001907469-620x349.jpg 11 | Microsoft News 12 | http://www.microsoft.com/news 13 | Test description 14 | 15 | RSS for Node 16 | Thu, 06 Jul 2017 20:25:17 GMT 17 | 18 | Thu, 06 Jul 2017 20:25:00 GMT 19 | 20 | 60 21 | 22 | 23 | 24 | http://example.com/test/1499372700 25 | http://example.com/test/1499372700 26 | 27 | Thu, 06 Jul 2017 20:25:00 GMT 28 | Newspapers 29 | 30 | 31 | 32 | 33 | http://example.com/test/1499372640 34 | http://example.com/test/1499372640 35 | 36 | Thu, 06 Jul 2017 20:24:00 GMT 37 | Newspapers 38 | 39 | 40 | 41 | 42 | http://example.com/test/1499372580 43 | http://example.com/test/1499372580 44 | 45 | Thu, 06 Jul 2017 20:23:00 GMT 46 | 47 | 48 | 49 | 50 | http://example.com/test/1499372520 51 | http://example.com/test/1499372520 52 | 53 | Thu, 06 Jul 2017 20:22:00 GMT 54 | 55 | 56 | 57 | 58 | http://example.com/test/1499372460 59 | http://example.com/test/1499372460 60 | 61 | Thu, 06 Jul 2017 20:21:00 GMT 62 | 63 | 64 | 65 | 66 | http://example.com/test/1499372400 67 | http://example.com/test/1499372400 68 | 69 | Thu, 06 Jul 2017 20:20:00 GMT 70 | 71 | 72 | 73 | 74 | http://example.com/test/1499372340 75 | http://example.com/test/1499372340 76 | 77 | Thu, 06 Jul 2017 20:19:00 GMT 78 | 79 | 80 | 81 | 82 | http://example.com/test/1499372280 83 | http://example.com/test/1499372280 84 | 85 | Thu, 06 Jul 2017 20:18:00 GMT 86 | 87 | 88 | 89 | 90 | http://example.com/test/1499372220 91 | http://example.com/test/1499372220 92 | 93 | Thu, 06 Jul 2017 20:17:00 GMT 94 | 95 | 96 | 97 | 98 | http://example.com/test/1499372160 99 | http://example.com/test/1499372160 100 | 101 | Thu, 06 Jul 2017 20:16:00 GMT 102 | 103 | 104 | -------------------------------------------------------------------------------- /tests/TestFeeds/simpleAtomFeed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | dive into mark 4 | 5 | A <em>lot</em> of effort 6 | went into making this effortless 7 | 8 | 9 | Mark Pilgrim 10 | http://example.org/ 11 | f8dy@example.com 12 | 13 | 14 | Sam Ruby 15 | 16 | 17 | 18 | 19 | /icon.jpg 20 | 21 | /logo.jpg 22 | 2005-07-31T12:29:29Z 23 | tag:example.org,2003:3 24 | 25 | 26 | 27 | 28 | 29 | 30 | Copyright (c) 2003, Mark Pilgrim 31 | 32 | Example Toolkit 33 | 34 | 35 | Atom draft-07 snapshot 36 | 37 | 39 | tag:example.org,2003:3.2397 40 | 2005-07-31T12:29:29Z 41 | 2003-12-13T08:29:29-04:00 42 | 43 | http://example.org/ 44 | Example, Inc. 45 | 2003-12-13T18:30:02Z 46 | 47 | 48 | Mark Pilgrim 49 | http://example.org/ 50 | f8dy@example.com 51 | 52 | 53 | Sam Ruby 54 | 55 | 56 | Joe Gregorio 57 | 58 | All rights Reserved. Contoso. 59 | 60 | 61 | 62 | [Update: The Atom draft is finished.] 63 | 64 | 65 | 66 | 67 | 68 | 69 | Atom draft-07 snapshot 70 | 71 | 73 | tag:example.org,2003:3.2397 74 | 2005-07-31T12:29:29Z 75 | 2003-12-13T08:29:29-04:00 76 | 77 | 10 78 | Example, Inc. 79 | 80 | 2003-12-13T18:30:02Z 81 | 82 | 83 | Mark Pilgrim 84 | http://example.org/ 85 | f8dy@example.com 86 | 87 | 88 | Sam Ruby 89 | 90 | 91 | Joe Gregorio 92 | 93 | All rights Reserved. Contoso. 94 | 95 | 96 | 97 | [Update: The Atom draft is finished.] 98 | 99 | 100 | 101 | 102 | [Update: The Atom draft is finished.] 103 | 104 | 105 | 106 | 107 | --------------------------------------------------------------------------------
62 | [Update: The Atom draft is finished.] 63 |
97 | [Update: The Atom draft is finished.] 98 |
102 | [Update: The Atom draft is finished.] 103 |