From f9661e2bf1b799c68396bb38fa4d942516c9f16a Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 1 Dec 2015 14:58:47 -0800 Subject: [PATCH] Initial commit --- .gitattributes | 50 ++ .gitignore | 27 ++ .travis.yml | 18 + HtmlAbstractions.sln | 55 +++ NuGet.config | 7 + NuGetPackageVerifier.json | 25 + appveyor.yml | 7 + build.cmd | 40 ++ build.sh | 43 ++ global.json | 3 + makefile.shade | 10 + .../HtmlContentBuilder.cs | 138 ++++++ .../HtmlContentBuilderExtensions.cs | 324 +++++++++++++ .../HtmlEncodedString.cs | 49 ++ src/Microsoft.AspNet.Html/HtmlTextWriter.cs | 49 ++ src/Microsoft.AspNet.Html/IHtmlContent.cs | 22 + .../IHtmlContentBuilder.cs | 40 ++ .../Microsoft.AspNet.Html.xproj | 19 + .../Properties/AssemblyInfo.cs | 10 + src/Microsoft.AspNet.Html/project.json | 29 ++ .../HtmlContentBuilderExtensionsTest.cs | 436 ++++++++++++++++++ .../HtmlContentBuilderTest.cs | 145 ++++++ .../Microsoft.AspNet.Html.Test.xproj | 21 + test/Microsoft.AspNet.Html.Test/project.json | 19 + tools/Key.snk | Bin 0 -> 596 bytes 25 files changed, 1586 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 HtmlAbstractions.sln create mode 100644 NuGet.config create mode 100644 NuGetPackageVerifier.json create mode 100644 appveyor.yml create mode 100644 build.cmd create mode 100755 build.sh create mode 100644 global.json create mode 100644 makefile.shade create mode 100644 src/Microsoft.AspNet.Html/HtmlContentBuilder.cs create mode 100644 src/Microsoft.AspNet.Html/HtmlContentBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Html/HtmlEncodedString.cs create mode 100644 src/Microsoft.AspNet.Html/HtmlTextWriter.cs create mode 100644 src/Microsoft.AspNet.Html/IHtmlContent.cs create mode 100644 src/Microsoft.AspNet.Html/IHtmlContentBuilder.cs create mode 100644 src/Microsoft.AspNet.Html/Microsoft.AspNet.Html.xproj create mode 100644 src/Microsoft.AspNet.Html/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.Html/project.json create mode 100644 test/Microsoft.AspNet.Html.Test/HtmlContentBuilderExtensionsTest.cs create mode 100644 test/Microsoft.AspNet.Html.Test/HtmlContentBuilderTest.cs create mode 100644 test/Microsoft.AspNet.Html.Test/Microsoft.AspNet.Html.Test.xproj create mode 100644 test/Microsoft.AspNet.Html.Test/project.json create mode 100644 tools/Key.snk diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..bdaa5ba982 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,50 @@ +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +*.jpg binary +*.png binary +*.gif binary + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..ac82da7568 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*net451.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +*.sln.ide +project.lock.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..c0befaffcf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: csharp +sudo: required +dist: trusty +addons: + apt: + packages: + - gettext + - libcurl4-openssl-dev + - libicu-dev + - libssl-dev + - libunwind8 + - zlib1g +env: + - KOREBUILD_DNU_RESTORE_CORECLR=true KOREBUILD_TEST_DNXCORE=true +mono: + - 4.0.5 +script: + - ./build.sh --quiet verify diff --git a/HtmlAbstractions.sln b/HtmlAbstractions.sln new file mode 100644 index 0000000000..1d1f5118f3 --- /dev/null +++ b/HtmlAbstractions.sln @@ -0,0 +1,55 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.24711.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A5A15F1C-885A-452A-A731-B0173DDBD913}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F31FF137-390C-49BF-A3BD-7C6ED3597C21}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Html", "src\Microsoft.AspNet.Html.Abstractions\Microsoft.AspNet.Html.xproj", "{68A28E4A-3ADE-4187-9625-4FF185887CB3}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Html.Test", "test\Microsoft.AspNet.Html.Abstractions.Test\Microsoft.AspNet.Html.Test.xproj", "{2D187B88-94BD-4A39-AC97-F8F8B9363301}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Debug|x86.ActiveCfg = Debug|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Debug|x86.Build.0 = Debug|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Release|Any CPU.Build.0 = Release|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Release|x86.ActiveCfg = Release|Any CPU + {68A28E4A-3ADE-4187-9625-4FF185887CB3}.Release|x86.Build.0 = Release|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Debug|x86.ActiveCfg = Debug|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Debug|x86.Build.0 = Debug|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Release|Any CPU.Build.0 = Release|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Release|x86.ActiveCfg = Release|Any CPU + {2D187B88-94BD-4A39-AC97-F8F8B9363301}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {68A28E4A-3ADE-4187-9625-4FF185887CB3} = {A5A15F1C-885A-452A-A731-B0173DDBD913} + {2D187B88-94BD-4A39-AC97-F8F8B9363301} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21} + EndGlobalSection +EndGlobal diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000000..03704957e8 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json new file mode 100644 index 0000000000..c57b2afb17 --- /dev/null +++ b/NuGetPackageVerifier.json @@ -0,0 +1,25 @@ +{ + "adx": { // Packages written by the ADX team and that ship on NuGet.org + "rules": [ + "AssemblyHasDocumentFileRule", + "AssemblyHasVersionAttributesRule", + "AssemblyHasServicingAttributeRule", + "AssemblyHasNeutralResourcesLanguageAttributeRule", + "SatellitePackageRule", + "StrictSemanticVersionValidationRule" + ], + "packages": { + "Microsoft.AspNet.Html.Abstractions": { } + } + }, + "Default": { // Rules to run for packages not listed in any other set. + "rules": [ + "AssemblyHasDocumentFileRule", + "AssemblyHasVersionAttributesRule", + "AssemblyHasServicingAttributeRule", + "AssemblyHasNeutralResourcesLanguageAttributeRule", + "SatellitePackageRule", + "StrictSemanticVersionValidationRule" + ] + } +} \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..636a7618d3 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,7 @@ +init: + - git config --global core.autocrlf true +build_script: + - build.cmd --quiet verify +clone_depth: 1 +test: off +deploy: off \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..553e3929a0 --- /dev/null +++ b/build.cmd @@ -0,0 +1,40 @@ +@echo off +cd %~dp0 + +SETLOCAL +SET NUGET_VERSION=latest +SET CACHED_NUGET=%LocalAppData%\NuGet\nuget.%NUGET_VERSION%.exe +SET BUILDCMD_KOREBUILD_VERSION= +SET BUILDCMD_DNX_VERSION= + +IF EXIST %CACHED_NUGET% goto copynuget +echo Downloading latest version of NuGet.exe... +IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet +@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/%NUGET_VERSION%/nuget.exe' -OutFile '%CACHED_NUGET%'" + +:copynuget +IF EXIST .nuget\nuget.exe goto restore +md .nuget +copy %CACHED_NUGET% .nuget\nuget.exe > nul + +:restore +IF EXIST packages\Sake goto getdnx +IF "%BUILDCMD_KOREBUILD_VERSION%"=="" ( + .nuget\nuget.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre +) ELSE ( + .nuget\nuget.exe install KoreBuild -version %BUILDCMD_KOREBUILD_VERSION% -ExcludeVersion -o packages -nocache -pre +) +.nuget\NuGet.exe install Sake -ExcludeVersion -Source https://www.nuget.org/api/v2/ -Out packages + +:getdnx +IF "%BUILDCMD_DNX_VERSION%"=="" ( + SET BUILDCMD_DNX_VERSION=latest +) +IF "%SKIP_DNX_INSTALL%"=="" ( + CALL packages\KoreBuild\build\dnvm install %BUILDCMD_DNX_VERSION% -runtime CoreCLR -arch x86 -alias default + CALL packages\KoreBuild\build\dnvm install default -runtime CLR -arch x86 -alias default +) ELSE ( + CALL packages\KoreBuild\build\dnvm use default -runtime CLR -arch x86 +) + +packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* diff --git a/build.sh b/build.sh new file mode 100755 index 0000000000..da4e3fcd1c --- /dev/null +++ b/build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +if test `uname` = Darwin; then + cachedir=~/Library/Caches/KBuild +else + if [ -z $XDG_DATA_HOME ]; then + cachedir=$HOME/.local/share + else + cachedir=$XDG_DATA_HOME; + fi +fi +mkdir -p $cachedir +nugetVersion=latest +cachePath=$cachedir/nuget.$nugetVersion.exe + +url=https://dist.nuget.org/win-x86-commandline/$nugetVersion/nuget.exe + +if test ! -f $cachePath; then + wget -O $cachePath $url 2>/dev/null || curl -o $cachePath --location $url /dev/null +fi + +if test ! -e .nuget; then + mkdir .nuget + cp $cachePath .nuget/nuget.exe +fi + +if test ! -d packages/Sake; then + mono .nuget/nuget.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre + mono .nuget/nuget.exe install Sake -ExcludeVersion -Source https://www.nuget.org/api/v2/ -Out packages +fi + +if ! type dnvm > /dev/null 2>&1; then + source packages/KoreBuild/build/dnvm.sh +fi + +if ! type dnx > /dev/null 2>&1 || [ -z "$SKIP_DNX_INSTALL" ]; then + dnvm install latest -runtime coreclr -alias default + dnvm install default -runtime mono -alias default +else + dnvm use default -runtime mono +fi + +mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" diff --git a/global.json b/global.json new file mode 100644 index 0000000000..983ba0401e --- /dev/null +++ b/global.json @@ -0,0 +1,3 @@ +{ + "projects": ["src"] +} diff --git a/makefile.shade b/makefile.shade new file mode 100644 index 0000000000..d5e473d558 --- /dev/null +++ b/makefile.shade @@ -0,0 +1,10 @@ + +var VERSION='0.1' +var FULL_VERSION='0.1' +var AUTHORS='Microsoft Open Technologies, Inc.' + +use-standard-lifecycle +k-standard-goals + +#xml-docs-test .clean .build-compile description='Check generated XML documentation files for errors' target='package' + k-xml-docs-test diff --git a/src/Microsoft.AspNet.Html/HtmlContentBuilder.cs b/src/Microsoft.AspNet.Html/HtmlContentBuilder.cs new file mode 100644 index 0000000000..34e59d3318 --- /dev/null +++ b/src/Microsoft.AspNet.Html/HtmlContentBuilder.cs @@ -0,0 +1,138 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNet.Html +{ + /// + /// An implementation using an in memory list. + /// + public class HtmlContentBuilder : IHtmlContentBuilder + { + /// + /// Creates a new . + /// + public HtmlContentBuilder() + : this(new List()) + { + } + + /// + /// Creates a new with the given initial capacity. + /// + /// The initial capacity of the backing store. + public HtmlContentBuilder(int capacity) + : this(new List(capacity)) + { + } + + /// + /// Creates a new with the given list of entries. + /// + /// + /// The list of entries. The will use this list without making a copy. + /// + public HtmlContentBuilder(IList entries) + { + if (entries == null) + { + throw new ArgumentNullException(nameof(entries)); + } + + Entries = entries; + } + + // This is not List because that would lead to wrapping all strings to IHtmlContent + // which is not space performant. + // + // In general unencoded strings are added here. We're optimizing for that case, and allocating + // a wrapper when encoded strings are used. + // + // internal for testing. + internal IList Entries { get; } + + /// + public IHtmlContentBuilder Append(string unencoded) + { + if (!string.IsNullOrEmpty(unencoded)) + { + Entries.Add(unencoded); + } + + return this; + } + + /// + public IHtmlContentBuilder Append(IHtmlContent htmlContent) + { + if (htmlContent == null) + { + return this; + } + + Entries.Add(htmlContent); + return this; + } + + /// + public IHtmlContentBuilder AppendHtml(string encoded) + { + if (!string.IsNullOrEmpty(encoded)) + { + Entries.Add(new HtmlEncodedString(encoded)); + } + + return this; + } + + /// + public IHtmlContentBuilder Clear() + { + Entries.Clear(); + return this; + } + + /// + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + for (var i = 0; i < Entries.Count; i++) + { + var entry = Entries[i]; + + var entryAsString = entry as string; + if (entryAsString != null) + { + encoder.Encode(writer, entryAsString); + } + else + { + // Only string, IHtmlContent values can be added to the buffer. + ((IHtmlContent)entry).WriteTo(writer, encoder); + } + } + } + + private string DebuggerToString() + { + using (var writer = new StringWriter()) + { + WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Html/HtmlContentBuilderExtensions.cs b/src/Microsoft.AspNet.Html/HtmlContentBuilderExtensions.cs new file mode 100644 index 0000000000..eabfafbd12 --- /dev/null +++ b/src/Microsoft.AspNet.Html/HtmlContentBuilderExtensions.cs @@ -0,0 +1,324 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNet.Html +{ + /// + /// Extension methods for . + /// + public static class HtmlContentBuilderExtensions + { + /// + /// Appends the specified to the existing content after replacing each format + /// item with the HTML encoded representation of the corresponding item in the + /// array. + /// + /// The . + /// + /// The composite format (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx). + /// The format string is assumed to be HTML encoded as-provided, and no further encoding will be performed. + /// + /// + /// The object array to format. Each element in the array will be formatted and then HTML encoded. + /// + /// A reference to this instance after the append operation has completed. + public static IHtmlContentBuilder AppendFormat( + this IHtmlContentBuilder builder, + string format, + params object[] args) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (format == null) + { + throw new ArgumentNullException(nameof(format)); + } + + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + builder.Append(new HtmlFormatString(format, args)); + return builder; + } + + /// + /// Appends the specified to the existing content with information from the + /// after replacing each format item with the HTML encoded + /// representation of the corresponding item in the array. + /// + /// The . + /// An object that supplies culture-specific formatting information. + /// + /// The composite format (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx). + /// The format string is assumed to be HTML encoded as-provided, and no further encoding will be performed. + /// + /// + /// The object array to format. Each element in the array will be formatted and then HTML encoded. + /// + /// A reference to this instance after the append operation has completed. + public static IHtmlContentBuilder AppendFormat( + this IHtmlContentBuilder builder, + IFormatProvider formatProvider, + string format, + params object[] args) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (format == null) + { + throw new ArgumentNullException(nameof(format)); + } + + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + builder.Append(new HtmlFormatString(formatProvider, format, args)); + return builder; + } + + /// + /// Appends an . + /// + /// The . + /// The . + public static IHtmlContentBuilder AppendLine(this IHtmlContentBuilder builder) + { + builder.Append(HtmlEncodedString.NewLine); + return builder; + } + + /// + /// Appends an after appending the value. + /// The value is treated as unencoded as-provided, and will be HTML encoded before writing to output. + /// + /// The . + /// The to append. + /// The . + public static IHtmlContentBuilder AppendLine(this IHtmlContentBuilder builder, string unencoded) + { + builder.Append(unencoded); + builder.Append(HtmlEncodedString.NewLine); + return builder; + } + + /// + /// Appends an after appending the value. + /// + /// The . + /// The to append. + /// The . + public static IHtmlContentBuilder AppendLine(this IHtmlContentBuilder builder, IHtmlContent content) + { + builder.Append(content); + builder.Append(HtmlEncodedString.NewLine); + return builder; + } + + /// + /// Appends an after appending the value. + /// The value is treated as HTML encoded as-provided, and no further encoding will be performed. + /// + /// The . + /// The HTML encoded to append. + /// The . + public static IHtmlContentBuilder AppendHtmlLine(this IHtmlContentBuilder builder, string encoded) + { + builder.AppendHtml(encoded); + builder.Append(HtmlEncodedString.NewLine); + return builder; + } + + /// + /// Sets the content to the value. The value is treated as unencoded as-provided, + /// and will be HTML encoded before writing to output. + /// + /// The . + /// The value that replaces the content. + /// The . + public static IHtmlContentBuilder SetContent(this IHtmlContentBuilder builder, string unencoded) + { + builder.Clear(); + builder.Append(unencoded); + return builder; + } + + /// + /// Sets the content to the value. + /// + /// The . + /// The value that replaces the content. + /// The . + public static IHtmlContentBuilder SetContent(this IHtmlContentBuilder builder, IHtmlContent content) + { + builder.Clear(); + builder.Append(content); + return builder; + } + + /// + /// Sets the content to the value. The value is treated as HTML encoded as-provided, and + /// no further encoding will be performed. + /// + /// The . + /// The HTML encoded that replaces the content. + /// The . + public static IHtmlContentBuilder SetHtmlContent(this IHtmlContentBuilder builder, string encoded) + { + builder.Clear(); + builder.AppendHtml(encoded); + return builder; + } + + [DebuggerDisplay("{DebuggerToString()}")] + private class HtmlFormatString : IHtmlContent + { + private readonly IFormatProvider _formatProvider; + private readonly string _format; + private readonly object[] _args; + + public HtmlFormatString(string format, object[] args) + : this(null, format, args) + { + } + + public HtmlFormatString(IFormatProvider formatProvider, string format, object[] args) + { + Debug.Assert(format != null); + Debug.Assert(args != null); + + _formatProvider = formatProvider ?? CultureInfo.CurrentCulture; + _format = format; + _args = args; + } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + var formatProvider = new EncodingFormatProvider(_formatProvider, encoder); + writer.Write(string.Format(formatProvider, _format, _args)); + } + + private string DebuggerToString() + { + using (var writer = new StringWriter()) + { + WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } + } + } + + // This class implements Html encoding via an ICustomFormatter. Passing an instance of this + // class into a string.Format method or anything similar will evaluate arguments implementing + // IHtmlContent without HTML encoding them, and will give other arguments the standard + // composite format string treatment, and then HTML encode the result. + // + // Plenty of examples of ICustomFormatter and the interactions with string.Format here: + // https://msdn.microsoft.com/en-us/library/system.string.format(v=vs.110).aspx#Format6_Example + private class EncodingFormatProvider : IFormatProvider, ICustomFormatter + { + private readonly HtmlEncoder _encoder; + private readonly IFormatProvider _formatProvider; + + public EncodingFormatProvider(IFormatProvider formatProvider, HtmlEncoder encoder) + { + Debug.Assert(formatProvider != null); + Debug.Assert(encoder != null); + + _formatProvider = formatProvider; + _encoder = encoder; + } + + public string Format(string format, object arg, IFormatProvider formatProvider) + { + // This is the case we need to special case. We trust the IHtmlContent instance to do the + // right thing with encoding. + var htmlContent = arg as IHtmlContent; + if (htmlContent != null) + { + using (var writer = new StringWriter()) + { + htmlContent.WriteTo(writer, _encoder); + return writer.ToString(); + } + } + + // If we get here then 'arg' is not an IHtmlContent, and we want to handle it the way a normal + // string.Format would work, but then HTML encode the result. + // + // First check for an ICustomFormatter - if the IFormatProvider is a CultureInfo, then it's likely + // that ICustomFormatter will be null. + var customFormatter = (ICustomFormatter)_formatProvider.GetFormat(typeof(ICustomFormatter)); + if (customFormatter != null) + { + var result = customFormatter.Format(format, arg, _formatProvider); + if (result != null) + { + return _encoder.Encode(result); + } + } + + // Next check if 'arg' is an IFormattable (DateTime is an example). + // + // An IFormattable will likely call back into the IFormatterProvider and ask for more information + // about how to format itself. This is the typical case when IFormatterProvider is a CultureInfo. + var formattable = arg as IFormattable; + if (formattable != null) + { + var result = formattable.ToString(format, _formatProvider); + if (result != null) + { + return _encoder.Encode(result); + } + } + + // If we get here then there's nothing really smart left to try. + if (arg != null) + { + var result = arg.ToString(); + if (result != null) + { + return _encoder.Encode(result); + } + } + + return string.Empty; + } + + public object GetFormat(Type formatType) + { + if (formatType == typeof(ICustomFormatter)) + { + return this; + } + + return null; + } + } + } +} diff --git a/src/Microsoft.AspNet.Html/HtmlEncodedString.cs b/src/Microsoft.AspNet.Html/HtmlEncodedString.cs new file mode 100644 index 0000000000..348588800f --- /dev/null +++ b/src/Microsoft.AspNet.Html/HtmlEncodedString.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNet.Html +{ + /// + /// An impelementation that wraps an HTML encoded . + /// + [DebuggerDisplay("{DebuggerToString()}")] + public class HtmlEncodedString : IHtmlContent + { + /// + /// An instance for . + /// + public static readonly IHtmlContent NewLine = new HtmlEncodedString(Environment.NewLine); + + private readonly string _value; + + /// + /// Creates a new . + /// + /// The HTML encoded value. + public HtmlEncodedString(string value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _value = value; + } + + /// + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + writer.Write(_value); + } + + private string DebuggerToString() + { + return _value; + } + } +} diff --git a/src/Microsoft.AspNet.Html/HtmlTextWriter.cs b/src/Microsoft.AspNet.Html/HtmlTextWriter.cs new file mode 100644 index 0000000000..56633f527d --- /dev/null +++ b/src/Microsoft.AspNet.Html/HtmlTextWriter.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; + +namespace Microsoft.AspNet.Html +{ + /// + /// A which supports special processing of . + /// + public abstract class HtmlTextWriter : TextWriter + { + /// + /// Writes an value. + /// + /// The value. + public abstract void Write(IHtmlContent value); + + /// + public override void Write(object value) + { + var htmlContent = value as IHtmlContent; + if (htmlContent == null) + { + base.Write(value); + } + else + { + Write(htmlContent); + } + } + + /// + public override void WriteLine(object value) + { + var htmlContent = value as IHtmlContent; + if (htmlContent == null) + { + base.Write(value); + } + else + { + Write(htmlContent); + } + + base.WriteLine(); + } + } +} diff --git a/src/Microsoft.AspNet.Html/IHtmlContent.cs b/src/Microsoft.AspNet.Html/IHtmlContent.cs new file mode 100644 index 0000000000..0e21889d54 --- /dev/null +++ b/src/Microsoft.AspNet.Html/IHtmlContent.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNet.Html +{ + /// + /// HTML content which can be written to a TextWriter. + /// + public interface IHtmlContent + { + /// + /// Writes the content by encoding it with the specified + /// to the specified . + /// + /// The to which the content is written. + /// The which encodes the content to be written. + void WriteTo(TextWriter writer, HtmlEncoder encoder); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Html/IHtmlContentBuilder.cs b/src/Microsoft.AspNet.Html/IHtmlContentBuilder.cs new file mode 100644 index 0000000000..de4b9ebd90 --- /dev/null +++ b/src/Microsoft.AspNet.Html/IHtmlContentBuilder.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Html +{ + /// + /// A builder for HTML content. + /// + public interface IHtmlContentBuilder : IHtmlContent + { + /// + /// Appends an instance. + /// + /// The to append. + /// The . + IHtmlContentBuilder Append(IHtmlContent content); + + /// + /// Appends a value. The value is treated as unencoded as-provided, and will be HTML + /// encoded before writing to output. + /// + /// The to append. + /// The . + IHtmlContentBuilder Append(string unencoded); + + /// + /// Appends an HTML encoded value. The value is treated as HTML encoded as-provided, and + /// no further encoding will be performed. + /// + /// The HTML encoded to append. + /// The . + IHtmlContentBuilder AppendHtml(string encoded); + + /// + /// Clears the content. + /// + /// The . + IHtmlContentBuilder Clear(); + } +} diff --git a/src/Microsoft.AspNet.Html/Microsoft.AspNet.Html.xproj b/src/Microsoft.AspNet.Html/Microsoft.AspNet.Html.xproj new file mode 100644 index 0000000000..564c800504 --- /dev/null +++ b/src/Microsoft.AspNet.Html/Microsoft.AspNet.Html.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 68a28e4a-3ade-4187-9625-4ff185887cb3 + Microsoft.AspNet.Html.Abstractions + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Html/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Html/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1951d644af --- /dev/null +++ b/src/Microsoft.AspNet.Html/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] +[assembly: InternalsVisibleTo("Microsoft.AspNet.Html.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Html/project.json b/src/Microsoft.AspNet.Html/project.json new file mode 100644 index 0000000000..55cc30d9b1 --- /dev/null +++ b/src/Microsoft.AspNet.Html/project.json @@ -0,0 +1,29 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 HTML content interface.", + "repository": { + "type": "git", + "url": "git://github.com/aspnet/htmlabstractions" + }, + "compilationOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "dependencies": { + "System.Text.Encodings.Web": "4.0.0-*" + }, + "frameworks": { + "net451": { + "frameworkAssemblies": { + "System.IO": "", + "System.Runtime": "" + } + }, + "dotnet5.4": { + "dependencies": { + "System.Collections": "4.0.11-*", + "System.Resources.ResourceManager": "4.0.1-*" + } + } + } +} diff --git a/test/Microsoft.AspNet.Html.Test/HtmlContentBuilderExtensionsTest.cs b/test/Microsoft.AspNet.Html.Test/HtmlContentBuilderExtensionsTest.cs new file mode 100644 index 0000000000..6253bf9be3 --- /dev/null +++ b/test/Microsoft.AspNet.Html.Test/HtmlContentBuilderExtensionsTest.cs @@ -0,0 +1,436 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.Encodings.Web; +using Microsoft.AspNet.Testing; +using Microsoft.Extensions.WebEncoders.Testing; +using Xunit; + +namespace Microsoft.AspNet.Html.Test +{ + public class HtmlContentBuilderExtensionsTest + { + [Fact] + public void Builder_AppendLine_Empty() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendLine(); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Equal(Environment.NewLine, HtmlContentToString(entry))); + } + + [Fact] + public void Builder_AppendLine_String() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendLine("Hi"); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Equal("Hi", Assert.IsType(entry).Value), + entry => Assert.Equal(Environment.NewLine, HtmlContentToString(entry))); + } + + [Fact] + public void Builder_AppendLine_IHtmlContent() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + var content = new OtherHtmlContent("Hi"); + + // Act + builder.AppendLine(content); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Same(content, entry), + entry => Assert.Equal(Environment.NewLine, HtmlContentToString(entry))); + } + + [Fact] + public void Builder_AppendHtmlLine_String() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendHtmlLine("Hi"); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Equal("Hi", Assert.IsType(entry).Value), + entry => Assert.Equal(Environment.NewLine, HtmlContentToString(entry))); + } + + [Fact] + public void Builder_SetContent_String() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + builder.Append("Existing Content. Will be Cleared."); + + // Act + builder.SetContent("Hi"); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Equal("Hi", Assert.IsType(entry).Value)); + } + + [Fact] + public void Builder_SetContent_IHtmlContent() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + builder.Append("Existing Content. Will be Cleared."); + + var content = new OtherHtmlContent("Hi"); + + // Act + builder.SetContent(content); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Same(content, entry)); + } + + [Fact] + public void Builder_SetHtmlContent_String() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + builder.Append("Existing Content. Will be Cleared."); + + // Act + builder.SetHtmlContent("Hi"); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Equal("Hi", Assert.IsType(entry).Value)); + } + + [Fact] + public void Builder_AppendFormat() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat("{0} {1} {2} {3}!", "First", "Second", "Third", "Fourth"); + + // Assert + Assert.Equal( + "HtmlEncode[[First]] HtmlEncode[[Second]] HtmlEncode[[Third]] HtmlEncode[[Fourth]]!", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormat_HtmlContent() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat("{0}!", new EncodedString("First")); + + // Assert + Assert.Equal( + "First!", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormatContent_With1Argument() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat("0x{0:X} - hex equivalent for 50.", 50); + + // Assert + Assert.Equal( + "0xHtmlEncode[[32]] - hex equivalent for 50.", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormatContent_With2Arguments() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat("0x{0:X} - hex equivalent for {1}.", 50, 50); + + // Assert + Assert.Equal( + "0xHtmlEncode[[32]] - hex equivalent for HtmlEncode[[50]].", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormatContent_With3Arguments() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat("0x{0:X} - {1} equivalent for {2}.", 50, "hex", 50); + + // Assert + Assert.Equal( + "0xHtmlEncode[[32]] - HtmlEncode[[hex]] equivalent for HtmlEncode[[50]].", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormat_WithAlignmentComponent() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat("{0, -25} World!", "Hello"); + + // Assert + Assert.Equal( + "HtmlEncode[[Hello]] World!", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormat_WithFormatStringComponent() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat("0x{0:X}", 50); + + // Assert + Assert.Equal("0xHtmlEncode[[32]]", HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormat_WithCulture() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat( + CultureInfo.InvariantCulture, + "Numbers in InvariantCulture - {0, -5:N} {1} {2} {3}!", + 1.1, + 2.98, + 145.82, + 32.86); + + // Assert + Assert.Equal( + "Numbers in InvariantCulture - HtmlEncode[[1.10]] HtmlEncode[[2.98]] " + + "HtmlEncode[[145.82]] HtmlEncode[[32.86]]!", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormat_WithCulture_1Argument() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat( + CultureInfo.InvariantCulture, + "Numbers in InvariantCulture - {0:N}!", + 1.1); + + // Assert + Assert.Equal( + "Numbers in InvariantCulture - HtmlEncode[[1.10]]!", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormat_WithCulture_2Arguments() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat( + CultureInfo.InvariantCulture, + "Numbers in InvariantCulture - {0:N} {1}!", + 1.1, + 2.98); + + // Assert + Assert.Equal( + "Numbers in InvariantCulture - HtmlEncode[[1.10]] HtmlEncode[[2.98]]!", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormat_WithCulture_3Arguments() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat( + CultureInfo.InvariantCulture, + "Numbers in InvariantCulture - {0:N} {1} {2}!", + 1.1, + 2.98, + 3.12); + + // Assert + Assert.Equal( + "Numbers in InvariantCulture - HtmlEncode[[1.10]] HtmlEncode[[2.98]] HtmlEncode[[3.12]]!", + HtmlContentToString(builder)); + } + + [Fact] + public void Builder_AppendFormat_WithDifferentCulture() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + var culture = new CultureInfo("fr-FR"); + + // Act + builder.AppendFormat(culture, "{0} in french!", 1.21); + + // Assert + Assert.Equal( + "HtmlEncode[[1,21]] in french!", + HtmlContentToString(builder)); + } + + [Fact] + [ReplaceCulture("de-DE", "de-DE")] + public void Builder_AppendFormat_WithDifferentCurrentCulture() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat(CultureInfo.CurrentCulture, "{0:D}", DateTime.Parse("01/02/2015")); + + // Assert + Assert.Equal( + "HtmlEncode[[Sonntag, 1. Februar 2015]]", + HtmlContentToString(builder)); + } + + private static string HtmlContentToString(IHtmlContent content) + { + using (var writer = new StringWriter()) + { + content.WriteTo(writer, new HtmlTestEncoder()); + return writer.ToString(); + } + } + + private class TestHtmlContentBuilder : IHtmlContentBuilder + { + public List Entries { get; } = new List(); + + public IHtmlContentBuilder Append(string unencoded) + { + Entries.Add(new UnencodedString(unencoded)); + return this; + } + + public IHtmlContentBuilder Append(IHtmlContent content) + { + Entries.Add(content); + return this; + } + + public IHtmlContentBuilder AppendHtml(string encoded) + { + Entries.Add(new EncodedString(encoded)); + return this; + } + + public IHtmlContentBuilder Clear() + { + Entries.Clear(); + return this; + } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + foreach (var entry in Entries) + { + entry.WriteTo(writer, encoder); + } + } + } + + private class EncodedString : IHtmlContent + { + public EncodedString(string value) + { + Value = value; + } + + public string Value { get; } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + writer.Write(Value); + } + } + + private class UnencodedString : IHtmlContent + { + public UnencodedString(string value) + { + Value = value; + } + + public string Value { get; } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + encoder.Encode(writer, Value); + } + } + + private class OtherHtmlContent : IHtmlContent + { + public OtherHtmlContent(string value) + { + Value = value; + } + + public string Value { get; } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNet.Html.Test/HtmlContentBuilderTest.cs b/test/Microsoft.AspNet.Html.Test/HtmlContentBuilderTest.cs new file mode 100644 index 0000000000..4eee28e219 --- /dev/null +++ b/test/Microsoft.AspNet.Html.Test/HtmlContentBuilderTest.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Text.Encodings.Web; +using Microsoft.AspNet.Html; +using Microsoft.Extensions.WebEncoders.Testing; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class HtmlContentBuilderTest + { + [Fact] + public void AppendString_AppendsAString() + { + // Arrange + var content = new HtmlContentBuilder(); + + // Act + content.Append("Hello"); + + // Assert + var result = Assert.Single(content.Entries); + Assert.IsType(result); + } + + [Fact] + public void AppendString_WrittenAsEncoded() + { + // Arrange + var content = new HtmlContentBuilder(); + content.Append("Hello"); + + var writer = new StringWriter(); + + // Act + content.WriteTo(writer, new HtmlTestEncoder()); + + // Assert + Assert.Equal("HtmlEncode[[Hello]]", writer.ToString()); + } + + [Fact] + public void AppendHtml_DoesNotGetWrittenAsEncoded() + { + // Arrange + var content = new HtmlContentBuilder(); + content.AppendHtml("Hello"); + + var writer = new StringWriter(); + + // Act + content.WriteTo(writer, new HtmlTestEncoder()); + + // Assert + Assert.Equal("Hello", writer.ToString()); + } + + [Fact] + public void AppendIHtmlContent_AppendsAsIs() + { + // Arrange + var content = new HtmlContentBuilder(); + var writer = new StringWriter(); + + // Act + content.Append(new TestHtmlContent("Hello")); + + // Assert + var result = Assert.Single(content.Entries); + var testHtmlContent = Assert.IsType(result); + testHtmlContent.WriteTo(writer, new HtmlTestEncoder()); + Assert.Equal("Written from TestHtmlContent: Hello", writer.ToString()); + } + + [Fact] + public void CanAppendMultipleItems() + { + // Arrange + var content = new HtmlContentBuilder(); + + // Act + content.Append(new TestHtmlContent("hello")); + content.Append("Test"); + + // Assert + Assert.Equal(2, content.Entries.Count); + Assert.Equal("Written from TestHtmlContent: hello", content.Entries[0].ToString()); + Assert.Equal("Test", content.Entries[1]); + } + + [Fact] + public void Clear_DeletesAllItems() + { + // Arrange + var content = new HtmlContentBuilder(); + content.Append(new TestHtmlContent("hello")); + content.Append("Test"); + + // Act + content.Clear(); + + // Assert + Assert.Equal(0, content.Entries.Count); + } + + [Fact] + public void WriteTo_WritesAllItems() + { + // Arrange + var content = new HtmlContentBuilder(); + var writer = new StringWriter(); + content.Append(new TestHtmlContent("Hello")); + content.Append("Test"); + + // Act + content.WriteTo(writer, new HtmlTestEncoder()); + + // Assert + Assert.Equal(2, content.Entries.Count); + Assert.Equal("Written from TestHtmlContent: HelloHtmlEncode[[Test]]", writer.ToString()); + } + + private class TestHtmlContent : IHtmlContent + { + private string _content; + + public TestHtmlContent(string content) + { + _content = content; + } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + writer.Write(ToString()); + } + + public override string ToString() + { + return "Written from TestHtmlContent: " + _content; + } + } + } +} diff --git a/test/Microsoft.AspNet.Html.Test/Microsoft.AspNet.Html.Test.xproj b/test/Microsoft.AspNet.Html.Test/Microsoft.AspNet.Html.Test.xproj new file mode 100644 index 0000000000..4a03567677 --- /dev/null +++ b/test/Microsoft.AspNet.Html.Test/Microsoft.AspNet.Html.Test.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2d187b88-94bd-4a39-ac97-f8f8b9363301 + Microsoft.AspNet.Html.Abstractions.Test + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Html.Test/project.json b/test/Microsoft.AspNet.Html.Test/project.json new file mode 100644 index 0000000000..f5ea4ca7e6 --- /dev/null +++ b/test/Microsoft.AspNet.Html.Test/project.json @@ -0,0 +1,19 @@ +{ + "compilationOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "dependencies": { + "Microsoft.AspNet.Html": "1.0.0-*", + "Microsoft.AspNet.Testing": "1.0.0-*", + "Microsoft.Extensions.WebEncoders": "1.0.0-*", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +} diff --git a/tools/Key.snk b/tools/Key.snk new file mode 100644 index 0000000000000000000000000000000000000000..e10e4889c125d3120cd9e81582243d70f7cbb806 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098=Iw=HCsnz~#iVhm& zj%TU(_THUee?3yHBjk$37ysB?i5#7WD$={H zV4B!OxRPrb|8)HPg~A}8P>^=#y<)56#=E&NzcjOtPK~<4n6GHt=K$ro*T(lhby_@U zEk(hLzk1H)0yXj{A_5>fk-TgNoP|q6(tP2xo8zt8i%212CWM#AeCd?`hS|4~L({h~Moo(~vy&3Z z1uI}`fd^*>o=rwbAGymj6RM^pZm(*Kfhs+Y1#`-2JPWZMK8@;ZWCk2+9bX4YP);~fj-BU*R zQPvWv$89!{Rl9wM+zR>_TSkn^voYxA?2G iKnV#iZ6Ah`K>b=@=IjYJXrxL124zR(38)nxe+&q_$QXwJ literal 0 HcmV?d00001