From 077da21937043cfd49f5919e14aafbf38f80e1f8 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 10 Jun 2022 17:12:43 -0400 Subject: [PATCH] Initial Commit. --- .gitattributes | 63 + .gitignore | 363 ++++++ HtmlFactory.sln | 25 + HtmlFactory/AttributesComparer.cs | 33 + HtmlFactory/Enums.cs | 25 + HtmlFactory/HtmlFactory.csproj | 12 + HtmlFactory/HtmlTag.cs | 1147 +++++++++++++++++ .../HtmlTagExtensions/AttributeShortHands.cs | 124 ++ HtmlFactory/HtmlTagExtensions/Styles.cs | 107 ++ HtmlFactory/HtmlTagExtensions/Text.cs | 39 + .../HtmlTagExtensions/TogglableAttributes.cs | 69 + HtmlFactory/HtmlTags.cs | 281 ++++ HtmlFactory/HtmlText.cs | 100 ++ .../IHtmlContentExtensions.ToHtmlTag.cs | 45 + HtmlFactory/Interfaces.cs | 15 + HtmlFactory/Used Nuget Commands.txt | 11 + 16 files changed, 2459 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 HtmlFactory.sln create mode 100644 HtmlFactory/AttributesComparer.cs create mode 100644 HtmlFactory/Enums.cs create mode 100644 HtmlFactory/HtmlFactory.csproj create mode 100644 HtmlFactory/HtmlTag.cs create mode 100644 HtmlFactory/HtmlTagExtensions/AttributeShortHands.cs create mode 100644 HtmlFactory/HtmlTagExtensions/Styles.cs create mode 100644 HtmlFactory/HtmlTagExtensions/Text.cs create mode 100644 HtmlFactory/HtmlTagExtensions/TogglableAttributes.cs create mode 100644 HtmlFactory/HtmlTags.cs create mode 100644 HtmlFactory/HtmlText.cs create mode 100644 HtmlFactory/IHtmlContentExtensions.ToHtmlTag.cs create mode 100644 HtmlFactory/Interfaces.cs create mode 100644 HtmlFactory/Used Nuget Commands.txt diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/HtmlFactory.sln b/HtmlFactory.sln new file mode 100644 index 0000000..342659c --- /dev/null +++ b/HtmlFactory.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32519.379 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlFactory", "HtmlFactory\HtmlFactory.csproj", "{537C8BD1-E339-4231-BCDC-3B769A6091A2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {537C8BD1-E339-4231-BCDC-3B769A6091A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {537C8BD1-E339-4231-BCDC-3B769A6091A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {537C8BD1-E339-4231-BCDC-3B769A6091A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {537C8BD1-E339-4231-BCDC-3B769A6091A2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C712CC4B-2F5D-4E9A-836C-1096F6191379} + EndGlobalSection +EndGlobal diff --git a/HtmlFactory/AttributesComparer.cs b/HtmlFactory/AttributesComparer.cs new file mode 100644 index 0000000..8a1dad3 --- /dev/null +++ b/HtmlFactory/AttributesComparer.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; + +namespace HtmlFactory +{ + internal static class AttributesComparer + { + public static bool Equals(IEnumerable> leftAttributes, + IEnumerable> rightAttributes, params string[] keysToExclude) + { + return Equals(leftAttributes, rightAttributes, EqualityComparer.Default, keysToExclude); + } + + private static bool Equals(IEnumerable> leftAttributes, + IEnumerable> rightAttributes, + IEqualityComparer equalityComparer, + params string[] keysToExclude) + { + var leftDictionary = leftAttributes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var rightDictionary = rightAttributes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + if (leftDictionary.Count != rightDictionary.Count) return false; + + keysToExclude = keysToExclude ?? new string[0]; + foreach (var keyValuePair in leftDictionary.Where(kvp => !keysToExclude.Contains(kvp.Key))) + { + if (!rightDictionary.TryGetValue(keyValuePair.Key, out var value)) return false; + if (!equalityComparer.Equals(keyValuePair.Value, value)) return false; + } + + return true; + } + } +} diff --git a/HtmlFactory/Enums.cs b/HtmlFactory/Enums.cs new file mode 100644 index 0000000..172fdde --- /dev/null +++ b/HtmlFactory/Enums.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HtmlFactory +{ + internal class Enums + { + } +} + + + + + +namespace HtmlFactory.TagRenderCompat +{ + public enum TagRenderMode + { + Normal = 0, + StartTag = 1, + EndTag = 2, + SelfClosing = 3 + } +} diff --git a/HtmlFactory/HtmlFactory.csproj b/HtmlFactory/HtmlFactory.csproj new file mode 100644 index 0000000..2ca954a --- /dev/null +++ b/HtmlFactory/HtmlFactory.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/HtmlFactory/HtmlTag.cs b/HtmlFactory/HtmlTag.cs new file mode 100644 index 0000000..5ba85f0 --- /dev/null +++ b/HtmlFactory/HtmlTag.cs @@ -0,0 +1,1147 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.IO; +using System.Text.Encodings.Web; +using System.Web; + +using Microsoft.AspNetCore.Html; + +using HtmlFactory.TagRenderCompat; + +using HtmlAgilityPack; +using System.Collections; + + + +namespace HtmlFactory +{ + /// + /// Represents an html tag that can have a parent, children, attributes, etc. + /// + public class HtmlTag : IHtmlElement, IDictionary + { + /// + /// The inner tag type ex. div / span + /// + private readonly string _tagName; + string IHtmlElement.Tagname { get { return _tagName; } } + + /// + /// The attributes of this tag + /// + private Dictionary _attributes = new Dictionary(); + Dictionary IHtmlElement.Attributes + { + get { return _attributes; } + set { _attributes = value; } + } + + /// + /// The inner list of contents + /// + private List _contents = new List(); + List IHtmlElement.Contents + { + get { return _contents; } + set { _contents = value; } + } + + /// + /// The inner + /// + private TagRenderMode? _tagRenderMode; + + /// + /// Initializes a new instance of + /// + /// The tag name + public HtmlTag(string tagName) + { + _tagName = tagName ?? throw new ArgumentNullException(nameof(tagName)); + } + + public HtmlTag(string tagName, Dictionary attributes, List contents, TagRenderMode? tagRenderMode) + { + _tagName = tagName; + _attributes = attributes; + _contents = contents; + _tagRenderMode = tagRenderMode; + } + + + + /// + /// Gets the tag name + /// + public string TagName => _tagName; + + /// + /// Gets the attributes as a dictionary + /// + public Dictionary Attributes => _attributes; + + /// + /// Gets the contents. + /// This property is very similar to the property, save for the fact that instead of + /// just a string + /// this is now a collection of elements. This allows for more extensive manipulation and DOM traversal similar to what + /// can be done with jQuery. + /// + public List Contents => _contents; + + #region Attributes that can be toggled + + /// + /// Triggers an attribute on this tag. Common examples include "checked", "selected", "disabled", ... + /// + /// The name of the attribute + /// A value indicating whether this attribute should be set on this tag or not. + /// This + public HtmlTag ToggleAttribute(string attribute, bool value) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + return value ? Attribute(attribute, attribute) : RemoveAttribute(attribute); + } + + #endregion + + #region ToString + + /// + public override string ToString() + { + var tag = TagName; + var attributes = string.Join(" ", Attributes.Select(kvp => $"{kvp.Key}=\"{kvp.Value}\"")); + var selfClosing = _tagRenderMode == TagRenderMode.SelfClosing ? " /" : ""; + return $"<{tag} {attributes}{selfClosing}>"; + } + +#endregion + +#region DOM Traversal + + /// + /// Gets the children in the order that they were added. + ///
WARNING: Text nodes () do not count as children and will not be + /// included in this property. + /// See if you want the text nodes to be included. + ///
+ public IEnumerable Children => Contents.OfType(); + + /// + /// Finds the children or the children of those children, etc. that match the + /// + /// The filter that specifies the conditions that each subnode must satisfy + /// The sub elements that satisfied the filter + public IEnumerable Find(Func filter) => Children.Where(filter).Concat(Children.SelectMany(c => c.Find(filter))); + + /// + /// Prepends to the + /// + /// + /// The html contents that will be inserted at the beginning of the contents of this tag, before all other content + /// + /// this + public HtmlTag Prepend(params IHtmlContent[] htmlContents) => htmlContents == null ? this : Prepend(htmlContents.AsEnumerable()); + + /// + /// Prepends to the + /// + /// + /// The html contents that will be inserted at the beginning of the contents of this tag, before all other content + /// + /// this + public HtmlTag Prepend(IEnumerable htmlContents) => htmlContents == null ? this : Prepend(htmlContents.SelectMany(htmlContent => ParseAll(htmlContent))); + + /// + /// Prepends an to the + /// + /// + /// The elements that will be inserted at the beginning of the contents of this tag, before all + /// other content elements + /// + /// this + public HtmlTag Prepend(params IHtmlElement[] elements) => Insert(0, elements); + + /// + /// Prepends an to the + /// + /// + /// The elements that will be inserted at the beginning of the contents of this tag, before all + /// other content elements + /// + /// this + public HtmlTag Prepend(IEnumerable elements) => Insert(0, elements); + + /// + /// Prepends an to the + /// + /// + /// The text that will be inserted as a at the beginning of the contents of this + /// tag, before all other content elements + /// + /// this + public HtmlTag Prepend(string text) => Insert(0, new HtmlText(text)); + + /// + /// Inserts an to the at the given + /// + /// The index at which the should be inserted + /// + /// The elements that will be inserted at the specifix of the contents of + /// this tag + /// + /// this + public HtmlTag Insert(int index, params IHtmlElement[] elements) => elements == null ? this : Insert(index, elements.AsEnumerable()); + + /// + /// Inserts an to the at the given + /// + /// The index at which the should be inserted + /// + /// The elements that will be inserted at the specifix of the contents of + /// this tag + /// + /// this + public HtmlTag Insert(int index, IEnumerable elements) + { + if (elements == null) + { + return this; + } + + if (index < 0 || index > _contents.Count) + { + throw new IndexOutOfRangeException( + $"Cannot insert anything at index '{index}', content elements count = {Contents.Count()}"); + } + + _contents.InsertRange(index, elements.Where(e => e != null)); + + return this; + } + + /// + /// Appends to the + /// + /// + /// The html contents that will be inserted at the end of the contents of this tag, after all other + /// content elements + /// + /// this + public HtmlTag Append(params IHtmlContent[] htmlContents) => htmlContents == null ? this : Append(htmlContents.AsEnumerable()); + + /// + /// Appends to the + /// + /// + /// The html contents that will be inserted at the end of the contents of this tag, after all other content + /// + /// this + public HtmlTag Append(IEnumerable htmlContents) => htmlContents == null ? this : Append(htmlContents.SelectMany(htmlContent => ParseAll(htmlContent))); + + /// + /// Inserts an to the at the given + /// + /// The index at which the should be inserted + /// + /// The text that will be inserted as a at the specifix + /// of the contents of this tag + /// + /// this + public HtmlTag Insert(int index, string text) => text == null ? this : Insert(index, new HtmlText(text)); + + /// + /// Appends an to the + /// + /// + /// The elements that will be inserted at the end of the contents of this tag, after all other + /// content elements + /// + /// this + public HtmlTag Append(params IHtmlElement[] elements) => elements == null ? this : Append(elements.AsEnumerable()); + + /// + /// Appends an to the + /// + /// + /// The elements that will be inserted at the end of the contents of this tag, after all other + /// content elements + /// + /// this + public HtmlTag Append(IEnumerable elements) + { + _contents.AddRange(elements.Where(e => e != null)); + + return this; + } + + + /// + /// Appends an to the + /// + /// + /// The text that will be inserted as a at the end of the contents of this tag, + /// after all other content elements + /// + /// this + public HtmlTag Append(string text) => Append(new HtmlText(text)); + + + + + +#endregion + + + + + + +#region Attribute + + /// + /// Safely reads an attribute or returns null when the attribute is absent + /// + public string this[string attribute] => _attributes.TryGetValue(attribute, out var value) ? value : null; + + /// + /// True when the attribute is known by this HtmlTag + /// + /// The attribute + /// True if the attribute was present in the attributes dictionary or false otherwise + public bool HasAttribute(string attribute) + { + return _attributes.ContainsKey(attribute); + } + + /// + /// Sets an attribute on this tag + /// + /// The attribute to set + /// The value to set + /// + /// A value indicating whether the should override the existing value for the + /// , if any. + /// + /// This + public HtmlTag Attribute(string attribute, string value, bool replaceExisting = true) + { + if (string.IsNullOrEmpty(attribute)) + { + throw new ArgumentNullException(nameof(attribute)); + } + + if (replaceExisting || !_attributes.ContainsKey(attribute)) + { + _attributes[attribute] = value; + } + else + { + _attributes.Add(value, attribute); + } + + return this; + } + + /// + /// Removes an attribute from this tag + /// + /// The attribute to remove + /// This + public HtmlTag RemoveAttribute(string attribute) + { + if (string.IsNullOrEmpty(attribute)) + { + throw new ArgumentNullException(nameof(attribute)); + } + + if (_attributes.ContainsKey(attribute)) + { + _attributes.Remove(attribute); + } + + return this; + } + + #endregion + + #region Data Attributes + + /// + /// Sets a data attribute. This method will automatically prepend 'data-' to the attribute name if the attribute name + /// does not start with 'data-'. + /// + /// The name of the attribute + /// The value + /// + /// A value indicating whether the existing data attribute, if any, should have its value + /// replace by the provided. + /// + /// This + public HtmlTag Data(string attribute, string value, bool replaceExisting = true) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + return Attribute(attribute.StartsWith("data-") ? attribute : "data-" + attribute, value, replaceExisting); + } + + ///// + ///// Sets a data attribute. This method will automatically prepend 'data-' to the attribute name if the attribute name + ///// does not start with 'data-'. + ///// + ///// The anonymous data object containing properties that should be set as data attributes + ///// + ///// A value indicating whether the existing data attributes, if any, should have their values + ///// replaced by the values found in + ///// + ///// + ///// + ///// // results in <a data-index-url="/index" data-about-url="/about"><a> + ///// new HtmlTag('a').Data(new { index_url = "/index", about_url = "/about"}).ToHtml() + ///// + ///// + ///// This + //public HtmlTag Data(object data, bool replaceExisting = true) + //{ + // if (data == null) + // { + // throw new ArgumentNullException(nameof(data)); + // } + + // var newAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(data) + // .Select(entry => new { + // Attribute = entry.Key.StartsWith("data-") ? entry.Key : "data-" + entry.Key, + // Value = Convert.ToString(entry.Value) + // }); + + // return newAttributes.Aggregate(this, (htmlTag, next) => htmlTag.Attribute(next.Attribute, next.Value, replaceExisting)); + //} + + + + public HtmlTag Data(string data, bool replaceExisting = true) + { + if (string.IsNullOrEmpty(data)) + { + throw new ArgumentNullException(nameof(data)); + } + + throw new NotImplementedException(); + + return this; + } + +#endregion + +#region Styles + + + + /// + /// Gets the 'style' attribute of this . + /// Note that this is a utility method that parses the 'style' attribute from a string into a + /// + /// + public IReadOnlyDictionary Styles + { + get + { + if (!_attributes.TryGetValue("style", out string styles)) + { + return new Dictionary(); + } + + var styleRulesSplit = styles.Split(';'); + var styleRuleStep1 = + styleRulesSplit.Select(styleRule => new { StyleRule = styleRule, SeparatorIndex = styleRule.IndexOf(':') }) + .ToArray(); + var styleRuleStep2 = styleRuleStep1.Select(a => + new { + StyleKey = a.StyleRule.Substring(0, a.SeparatorIndex), + StyleValue = a.StyleRule.Substring(a.SeparatorIndex + 1, a.StyleRule.Length - a.SeparatorIndex - 1) + }).ToArray(); + + + return styleRuleStep2.ToDictionary(a => a.StyleKey, a => a.StyleValue); + } + set + { + if (value.Count == 0) + { + Remove("style"); + } + else + { + var newStyle = string.Join(";", value.Select(s => string.Format("{0}:{1}", s.Key, s.Value))); + Attribute("style", newStyle); + } + } + } + + /// + /// Sets a css style and on the 'style' attribute. + /// + /// The type of the style (width, height, margin, padding, ...) + /// The value of the style (any valid css value for the given ) + /// + /// A value indicating whether the existing value for the given + /// should be updated or not, if such a key is already present in the 'style' attribute. + /// + /// This Htmltag + public HtmlTag Style(string key, string value, bool replaceExisting = true) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (key.Contains(";")) + { + throw new ArgumentException($"Style key cannot contain ';'! Key was '{key}'"); + } + + if (value.Contains(";")) + { + throw new ArgumentException($"Style value cannot contain ';'! Value was '{key}'"); + } + + + + var tStyles = Styles.ToDictionary(S => S.Key, s => s.Value); + + if (replaceExisting || !tStyles.ContainsKey(key)) + { + tStyles[key] = value; + } + Styles = tStyles; + + return this; + } + + /// + /// Removes a from the , if such a key is present. + /// + /// The key to remove from the style + /// + public HtmlTag RemoveStyle(string key) + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + + if (HasAttribute("style")) + { + RemoveAttribute("style"); + } + + return this; + } + +#endregion + +#region Class + + + + /// + /// Gets or sets the classes of this + /// This is a utility method to easily manipulate the 'class' attribute + /// + public IEnumerable Classes + { + get + { + string classes; + return TryGetValue("class", out classes) ? classes.Split(' ') : Enumerable.Empty(); + } + set + { + if (!value.Any()) + { + RemoveAttribute("class"); + } + else + { + Attribute("class", string.Join(" ", value)); + } + } + } + + public ICollection Keys => ((IDictionary)_attributes).Keys; + + public ICollection Values => ((IDictionary)_attributes).Values; + + public int Count => ((ICollection>)_attributes).Count; + + public bool IsReadOnly => ((ICollection>)_attributes).IsReadOnly; + + string IDictionary.this[string key] { get => ((IDictionary)_attributes)[key]; set => ((IDictionary)_attributes)[key] = value; } + + + + + /// + /// Returns true if this has the or false otherwise + /// + /// The class + /// True if this has the or false otherwise + public bool HasClass(string @class) + { + return Classes.Any(c => string.Equals(c, @class)); + } + + /// + /// Adds a class to this tag. + /// + /// The class(es) to add + /// This + public HtmlTag Class(string @class) + { + if (@class == null) + { + return this; + } + var classesToAdd = @class.Split(' '); + Classes = Classes.Concat(classesToAdd).Distinct(); + return this; + } + + /// + /// Removes one or more classes from this tag. + /// + /// The class(es) to remove + /// + /// This + public HtmlTag RemoveClass(string @class) + { + if (@class == null) + { + throw new ArgumentNullException("class"); + } + var classesToRemove = @class.Split(' '); + Classes = Classes.Where(c => !classesToRemove.Contains(c)); + return this; + } + +#endregion + +#region To Html + + /// + /// Sets the that will be used when rendering this . + /// This is a convenient way to build up an tree, configure the for + /// each node in the tree, + /// and then render it entirely in one go. + /// + /// The tag render mode + /// This + public HtmlTag Render(TagRenderMode tagRenderMode) + { + _tagRenderMode = tagRenderMode; + return this; + } + + + + /// + public virtual void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + var tTagRenderMode = _tagRenderMode ?? TagRenderMode.Normal; + + StringBuilder sb = new StringBuilder(); + + switch (tTagRenderMode) + { + case TagRenderMode.StartTag: + + sb.Append('<').Append(TagName); + AppendAttributes(sb); + sb.Append('>'); + break; + case TagRenderMode.EndTag: + sb.Append("'); + break; + case TagRenderMode.SelfClosing: + if (Contents.Any()) + { + throw new InvalidOperationException( + "Cannot render this tag with the self closing TagRenderMode because this tag has inner contents: " + this); + } + sb.Append('<') + .Append(TagName); + AppendAttributes(sb); + sb.Append(" />"); + break; + default: + sb.Append('<') + .Append(TagName); + AppendAttributes(sb); + sb.Append('>'); + + using (var writer2 = new StringWriter()) + { + foreach (var content in Contents) + { + content.WriteTo(writer2, encoder); + } + sb.Append(writer2.ToString()); + } + + sb.Append("'); + break; + } + + writer.Write(sb.ToString()); + } + + #endregion + + #region TagBuilder Methods + + private void AppendAttributes(StringBuilder sb) + { + foreach (var attribute in Attributes) + { + string key = attribute.Key; + if (string.Equals(key, "id", StringComparison.Ordinal) && string.IsNullOrEmpty(attribute.Value)) + { + continue; + } + string value = HttpUtility.HtmlAttributeEncode(attribute.Value); + sb.Append(' ') + .Append(key) + .Append("=\"") + .Append(value) + .Append('"'); + } + } + + + + + + #endregion + + + + #region Factory methods + + /// + /// Parses an from the given + /// + /// The html + /// A value indicating whether the html should be checked for syntax errors. + /// A new that is an object representation of the + /// + /// If is true and syntax errors are + /// encountered in the + /// + public static HtmlTag Parse(string html, bool validateSyntax = false) + { + if (html == null) + { + throw new ArgumentNullException(nameof(html)); + } + + return Parse(new StringReader(HtmlEntity.DeEntitize(html)), validateSyntax); + } + + + /// + /// Parses an from the given + /// + /// The text reader + /// A value indicating whether the html should be checked for syntax errors. + /// A new that is an object representation of the + /// + /// If is true and syntax errors are + /// encountered in the + /// + public static HtmlTag Parse(TextReader textReader, bool validateSyntax = false) + { + if (textReader == null) + { + throw new ArgumentNullException(nameof(textReader)); + } + + var htmlDocument = new HtmlDocument { OptionCheckSyntax = validateSyntax }; + HtmlNode.ElementsFlags.Remove("option"); + htmlDocument.Load(textReader); + return Parse(htmlDocument, validateSyntax); + } + + /// + /// Parses an from the given + /// + /// The html document containing the html + /// A value indicating whether the html should be checked for syntax errors. + /// + /// Multiple s that is an object representation of the + /// + /// + /// If is true and syntax errors are + /// encountered in the + /// + public static HtmlTag Parse(HtmlDocument htmlDocument, bool validateSyntax = false) + { + if (htmlDocument.ParseErrors.Any() && validateSyntax) + { + var readableErrors = + htmlDocument.ParseErrors.Select( + e => $"Code = {e.Code}, SourceText = {e.SourceText}, Reason = {e.Reason}"); + throw new InvalidOperationException($"Parse errors found: \n{string.Join("\n", readableErrors)}"); + } + + if (htmlDocument.DocumentNode.ChildNodes.Count != 1) + { + throw new ArgumentException( + "Html contains more than one element. The parse method can only be used for single html tags! Input was : \n" + + htmlDocument.DocumentNode.OuterHtml); + } + + htmlDocument.OptionWriteEmptyNodes = true; + return ParseHtmlTag(htmlDocument.DocumentNode.ChildNodes.Single()); + } + + /// + /// Parses multiple s from the given + /// + /// The html content + /// A value indicating whether the html should be checked for syntax errors. + /// A collection of + /// + /// If is true and syntax errors are + /// encountered in the + /// + public static IEnumerable ParseAll(IHtmlContent htmlContent, bool validateSyntax = false) + { + if (htmlContent == null) + { + throw new ArgumentNullException(nameof(htmlContent)); + } + // special case: html content is already an HtmlTag! + if (htmlContent is HtmlTag alreadyHtmlTag) + { + return new[] { alreadyHtmlTag }; + } + //// special case: string that may contain HTML but must be encoded when writing + //if (htmlContent is StringHtmlContent s) + //{ + // return new[] { new HtmlText(s) }; + //} + //// special case: TagBuilder + //if (htmlContent is TagBuilder tagBuilder) + //{ + // var htmlTag = new HtmlTag(tagBuilder.TagName) + // .WithTagRenderMode(tagBuilder.TagRenderMode); + + // if (tagBuilder.Attributes.Any()) + // { + // htmlTag = tagBuilder.Attributes + // .Aggregate(htmlTag, + // (tag, attribute) => tag.Attribute(attribute.Key, HtmlEntity.DeEntitize(attribute.Value))); + // } + + // if (tagBuilder.HasInnerHtml) + // htmlTag = htmlTag.WithContents(ParseAll(tagBuilder.InnerHtml, validateSyntax).ToImmutableList()); + + // return new[] { htmlTag }; + //} + return ParseAll(htmlContent.ToHtmlString(), validateSyntax); + } + + + + + /// + /// Parses multiple s from the given + /// + /// The html + /// A value indicating whether the html should be checked for syntax errors. + /// A collection of + /// + /// If is true and syntax errors are + /// encountered in the + /// + public static IEnumerable ParseAll(string html, bool validateSyntax = false) + { + if (html == null) + { + throw new ArgumentNullException(nameof(html)); + } + + using (var reader = new StringReader(HtmlEntity.DeEntitize(html))) + { + return ParseAll(reader, validateSyntax); + } + } + + /// + /// Parses multiple s from the given + /// + /// The text reader + /// A value indicating whether the html should be checked for syntax errors. + /// A collection of + /// + /// If is true and syntax errors are + /// encountered in the + /// + public static IEnumerable ParseAll(TextReader textReader, bool validateSyntax = false) + { + if (textReader == null) + { + throw new ArgumentNullException(nameof(textReader)); + } + + var htmlDocument = new HtmlDocument { OptionCheckSyntax = validateSyntax }; + HtmlNode.ElementsFlags.Remove("option"); + htmlDocument.Load(textReader); + return ParseAll(htmlDocument, validateSyntax); + } + + /// + /// Parses multiple s from the given + /// + /// The html document + /// A value indicating whether the html should be checked for syntax errors. + /// A collection of + /// + /// If is true and syntax errors are + /// encountered in the + /// + public static IEnumerable ParseAll(HtmlDocument htmlDocument, bool validateSyntax = false) + { + if (htmlDocument.ParseErrors.Any() && validateSyntax) + { + var readableErrors = + htmlDocument.ParseErrors.Select( + e => $"Code = {e.Code}, SourceText = {e.SourceText}, Reason = {e.Reason}"); + throw new InvalidOperationException($"Parse errors found: \n{string.Join("\n", readableErrors)}"); + } + + foreach (var childNode in htmlDocument.DocumentNode.ChildNodes) + { + if (childNode.NodeType == HtmlNodeType.Text) yield return ParseHtmlText(childNode); + if (childNode.NodeType == HtmlNodeType.Element) + { + if (string.IsNullOrEmpty(childNode.Name)) + continue; + yield return ParseHtmlTag(childNode); + + } + } + } + + private static HtmlTag ParseHtmlTag(HtmlNode htmlNode) + { + var htmlTag = new HtmlTag(htmlNode.Name); + if (htmlNode.Closed && !htmlNode.ChildNodes.Any()) + { + htmlTag = htmlTag.Render(TagRenderMode.SelfClosing); + + // fix: self closing I tags (used by icon fonts) have rendering issues if they do not have content + if (htmlTag.TagName.Equals(HtmlTags.I.TagName)) + { + htmlTag = htmlTag.Render(TagRenderMode.Normal).Append(" "); + } + } + + foreach (var attribute in htmlNode.Attributes) + { + htmlTag = htmlTag.Attribute(attribute.Name, attribute.Value); + } + + foreach (var childNode in htmlNode.ChildNodes) + { + IHtmlElement childElement = null; + switch (childNode.NodeType) + { + case HtmlNodeType.Element: + childElement = ParseHtmlTag(childNode); + break; + case HtmlNodeType.Text: + childElement = ParseHtmlText(childNode); + break; + } + + if (childElement != null) + { + htmlTag = htmlTag.Append(childElement); + } + } + + return htmlTag; + } + + private static HtmlText ParseHtmlText(HtmlNode htmlNode) + { + return new HtmlText(htmlNode.InnerText); + } + + #endregion + + #region Equality + + private bool Equals(HtmlTag other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (!string.Equals(TagName, other.TagName)) + { + return false; + } + + if (_attributes.Count != other._attributes.Count) + { + return false; + } + + if (!AttributesComparer.Equals(_attributes, other._attributes, "class", "style")) + { + return false; + } + + if (!AttributesComparer.Equals(Styles, other.Styles)) + { + return false; + } + + return Classes.OrderBy(c => c).SequenceEqual(other.Classes.OrderBy(c => c)) + && Contents.SequenceEqual(other.Contents); + } + + /// + /// Returns true if this is equivalent to . If any of the attributes or + /// the children are different, + /// this method will return false. It is important to note that the order in which styles and classes appear will not + /// affect the equality in any way. + /// However, the order of the does matter. + /// As a rule of thumb, if one would have the same display presentation and behavior in a browser + /// as another , they are considered equal. + /// + /// + /// + public override bool Equals(object other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return other.GetType() == GetType() && Equals((HtmlTag)other); + } + + /// + public override int GetHashCode() + { + var hash = 17; + hash = hash * 23 + TagName.GetHashCode(); + foreach ( + var attribute in _attributes.Where(attribute => !string.Equals(attribute.Key, "style") && !string.Equals(attribute.Key, "class")) + .OrderBy(attribute => attribute.Key)) + { + hash = hash * 23 + attribute.Key.GetHashCode(); + hash = hash * 23 + attribute.Value.GetHashCode(); + } + + foreach (var style in Styles.OrderBy(style => style.Key)) + { + hash = hash * 23 + style.Key.GetHashCode(); + hash = hash * 23 + style.Value.GetHashCode(); + } + + hash = Classes.OrderBy(c => c).Aggregate(hash, (current, @class) => current * 23 + @class.GetHashCode()); + return Contents.Aggregate(hash, (current, content) => current * 23 + content.GetHashCode()); + } + + public void Add(string key, string value) + { + ((IDictionary)_attributes).Add(key, value); + } + + public bool ContainsKey(string key) + { + return ((IDictionary)_attributes).ContainsKey(key); + } + + public bool Remove(string key) + { + return ((IDictionary)_attributes).Remove(key); + } + + public bool TryGetValue(string key, out string value) + { + return ((IDictionary)_attributes).TryGetValue(key, out value); + } + + public void Add(KeyValuePair item) + { + ((ICollection>)_attributes).Add(item); + } + + public void Clear() + { + ((ICollection>)_attributes).Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((ICollection>)_attributes).Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((ICollection>)_attributes).CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return ((ICollection>)_attributes).Remove(item); + } + + public IEnumerator> GetEnumerator() + { + return ((IEnumerable>)_attributes).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_attributes).GetEnumerator(); + } + + #endregion + } +} diff --git a/HtmlFactory/HtmlTagExtensions/AttributeShortHands.cs b/HtmlFactory/HtmlTagExtensions/AttributeShortHands.cs new file mode 100644 index 0000000..c285115 --- /dev/null +++ b/HtmlFactory/HtmlTagExtensions/AttributeShortHands.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HtmlFactory +{ + /// + /// Contains a whole series of useful methods to manipulate common HTML attributes of an HtmlTag + /// + public static partial class HtmlTagExtensions + { + /// + /// Sets the name property. This is a shorthand for the method with 'name' as the + /// attribute parameter value. + /// + /// This + /// The value for the 'name' attribute + /// + /// A value indicating whether the existing attribute, if any, should have its value replaced + /// by the provided. + /// + /// This + public static HtmlTag Name(this HtmlTag htmlTag, string name, bool replaceExisting = true) + { + return htmlTag.Attribute("name", name, replaceExisting); + } + + /// + /// Sets the title property. This is a shorthand for the method with 'title' as the + /// attribute parameter value. + /// + /// This + /// The value for the 'title' attribute + /// + /// A value indicating whether the existing attribute, if any, should have its value replaced + /// by the provided. + /// + /// This + public static HtmlTag Title(this HtmlTag htmlTag, string title, bool replaceExisting = true) + { + return htmlTag.Attribute("title", title, replaceExisting); + } + + /// + /// Sets the id property. This is a shorthand for the method with 'id' as the attribute + /// parameter value. + /// + /// This + /// The value for the 'id' attribute + /// + /// A value indicating whether the existing attribute, if any, should have its value replaced + /// by the provided. + /// + /// This + public static HtmlTag Id(this HtmlTag htmlTag, string id, bool replaceExisting = true) + { + return htmlTag.Attribute("id", id, replaceExisting); + } + + /// + /// Sets the type property. This is a shorthand for the method with 'type' as the + /// attribute parameter value. + /// + /// This + /// The value for the 'type' attribute + /// + /// A value indicating whether the existing attribute, if any, should have its value replaced + /// by the provided. + /// + /// This + public static HtmlTag Type(this HtmlTag htmlTag, string type, bool replaceExisting = true) + { + return htmlTag.Attribute("type", type, replaceExisting); + } + + /// + /// Sets the value property. This is a shorthand for the method with 'value' as the + /// attribute parameter value. + /// + /// This + /// The value for the 'value' attribute + /// + /// A value indicating whether the existing attribute, if any, should have its value replaced + /// by the provided. + /// + /// This + public static HtmlTag Value(this HtmlTag htmlTag, string value, bool replaceExisting = true) + { + return htmlTag.Attribute("value", value, replaceExisting); + } + + /// + /// Sets the href property. This is a shorthand for the method with 'href' as the + /// attribute parameter value. + /// + /// This + /// The value for the 'href' attribute + /// + /// A value indicating whether the existing attribute, if any, should have its value replaced + /// by the provided. + /// + /// This + public static HtmlTag Href(this HtmlTag htmlTag, string href, bool replaceExisting = true) + { + return htmlTag.Attribute("href", href, replaceExisting); + } + + /// + /// Sets the src property. This is a shorthand for the method with 'src' as the + /// attribute parameter value. + /// + /// This + /// The value for the 'src' attribute + /// + /// A value indicating whether the existing attribute, if any, should have its value replaced + /// by the provided. + /// + /// This + public static HtmlTag Src(this HtmlTag htmlTag, string src, bool replaceExisting = true) + { + return htmlTag.Attribute("src", src, replaceExisting); + } + } +} diff --git a/HtmlFactory/HtmlTagExtensions/Styles.cs b/HtmlFactory/HtmlTagExtensions/Styles.cs new file mode 100644 index 0000000..548a77b --- /dev/null +++ b/HtmlFactory/HtmlTagExtensions/Styles.cs @@ -0,0 +1,107 @@ +using System; + +namespace HtmlFactory +{ + public static partial class HtmlTagExtensions + { + /// + /// Sets the width style. This is a shorthand for calling the method with the 'width' key + /// + /// This + /// The width. This can be any valid css value for 'width' + /// A value indicating whether the existing width, if any, should be overriden or not + /// This + public static HtmlTag Width(this HtmlTag htmlTag, string width, bool replaceExisting = true) + { + if (width == null) + throw new ArgumentNullException(nameof(width)); + return htmlTag.Style("width", width, replaceExisting); + } + + /// + /// Sets the height style. This is a shorthand for calling the method with the 'height' key + /// + /// This + /// The height. This can be any valid css value for 'height' + /// A value indicating whether the existing height, if any, should be overriden or not + /// This + public static HtmlTag Height(this HtmlTag htmlTag, string height, bool replaceExisting = true) + { + if (height == null) + throw new ArgumentNullException(nameof(height)); + return htmlTag.Style("height", height, replaceExisting); + } + + /// + /// Sets the margin style. This is a shorthand for calling the method with the 'margin' key + /// + /// This + /// The margin. This can be any valid css value for 'margin' + /// A value indicating whether the existing margin, if any, should be overriden or not + /// This + public static HtmlTag Margin(this HtmlTag htmlTag, string margin, bool replaceExisting = true) + { + if (margin == null) + throw new ArgumentNullException(nameof(margin)); + return htmlTag.Style("margin", margin, replaceExisting); + } + + /// + /// Sets the padding style. This is a shorthand for calling the method with the 'padding' + /// key + /// + /// This + /// The padding. This can be any valid css value for 'padding' + /// A value indicating whether the existing padding, if any, should be overriden or not + /// This + public static HtmlTag Padding(this HtmlTag htmlTag, string padding, bool replaceExisting = true) + { + if (padding == null) + throw new ArgumentNullException(nameof(padding)); + return htmlTag.Style("padding", padding, replaceExisting); + } + + /// + /// Sets the color style. This is a shorthand for calling the method with the 'color' key + /// + /// This + /// The color. This can be any valid css value for 'color' + /// A value indicating whether the existing color, if any, should be overriden or not + /// This + public static HtmlTag Color(this HtmlTag htmlTag, string color, bool replaceExisting = true) + { + if (color == null) + throw new ArgumentNullException(nameof(color)); + return htmlTag.Style("color", color, replaceExisting); + } + + /// + /// Sets the text-align style. This is a shorthand for calling the method with the + /// 'text-align' key + /// + /// This + /// The text alignment. This can be any valid css value for 'text-align' + /// A value indicating whether the existing text-align, if any, should be overriden or not + /// This + public static HtmlTag TextAlign(this HtmlTag htmlTag, string textAlign, bool replaceExisting = true) + { + if (textAlign == null) + throw new ArgumentNullException(nameof(textAlign)); + return htmlTag.Style("text-align", textAlign, replaceExisting); + } + + /// + /// Sets the border style. This is a shorthand for calling the method with the 'border' key + /// + /// This + /// The border. This can be any valid css value for 'border' + /// A value indicating whether the existing border, if any, should be overriden or not + /// This + public static HtmlTag Border(this HtmlTag htmlTag, string border, bool replaceExisting = true) + { + if (border == null) + throw new ArgumentNullException(nameof(border)); + return htmlTag.Style("border", border, replaceExisting); + } + } +} diff --git a/HtmlFactory/HtmlTagExtensions/Text.cs b/HtmlFactory/HtmlTagExtensions/Text.cs new file mode 100644 index 0000000..0d9eb08 --- /dev/null +++ b/HtmlFactory/HtmlTagExtensions/Text.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.Text.Encodings.Web; + +namespace HtmlFactory +{ + public static partial class HtmlTagExtensions + { + /// + /// Gets the HTML stripped content of this + /// + /// This + /// The HTML stripped contents. + public static string Text(this HtmlTag htmlTag) + { + if (htmlTag == null) + return string.Empty; + using (var writer = new StringWriter()) + { + Write(htmlTag, writer, HtmlEncoder.Default); + return writer.ToString(); + } + } + + private static void Write(HtmlTag htmlTag, TextWriter writer, HtmlEncoder encoder) + { + foreach (var content in htmlTag.Contents) + { + if (content is HtmlText text) + { + text.WriteTo(writer, encoder); + } + else if (content is HtmlTag tag) + { + Write(tag, writer, encoder); + } + } + } + } +} diff --git a/HtmlFactory/HtmlTagExtensions/TogglableAttributes.cs b/HtmlFactory/HtmlTagExtensions/TogglableAttributes.cs new file mode 100644 index 0000000..d82a400 --- /dev/null +++ b/HtmlFactory/HtmlTagExtensions/TogglableAttributes.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HtmlFactory +{ + public static partial class HtmlTagExtensions + { + /// + /// Sets the 'checked' attribute to 'checked' if is true or removes the attribute if + /// is false + /// + /// This + /// + /// A value indicating whether this tag should have the attribute 'checked' with value 'checked' or + /// not. + /// + /// This + public static HtmlTag Checked(this HtmlTag htmlTag, bool @checked) + { + return htmlTag.ToggleAttribute("checked", @checked); + } + + /// + /// Sets the 'disabled' attribute to 'disabled' if is true or removes the attribute if + /// is false + /// + /// This + /// + /// A value indicating whether this tag should have the attribute 'disabled' with value 'disabled' + /// or not. + /// + /// This + public static HtmlTag Disabled(this HtmlTag htmlTag, bool disabled) + { + return htmlTag.ToggleAttribute("disabled", disabled); + } + + /// + /// Sets the 'selected' attribute to 'selected' if is true or removes the attribute if + /// is false + /// + /// This + /// + /// A value indicating whether this tag should have the attribute 'selected' with value 'selected' + /// or not. + /// + /// This + public static HtmlTag Selected(this HtmlTag htmlTag, bool selected) + { + return htmlTag.ToggleAttribute("selected", selected); + } + + /// + /// Sets the 'readonly' attribute to 'readonly' if is true or removes the attribute if + /// is false + /// + /// This + /// + /// A value indicating whether this tag should have the attribute 'readonly' with value 'readonly' + /// or not. + /// + /// This + public static HtmlTag Readonly(this HtmlTag htmlTag, bool @readonly) + { + return htmlTag.ToggleAttribute("readonly", @readonly); + } + } +} diff --git a/HtmlFactory/HtmlTags.cs b/HtmlFactory/HtmlTags.cs new file mode 100644 index 0000000..e3e34f1 --- /dev/null +++ b/HtmlFactory/HtmlTags.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using HtmlFactory.TagRenderCompat; + +namespace HtmlFactory +{ + /// + /// Provides convenience properties to create instances of + /// + public static class HtmlTags + { + public static HtmlTag A => new HtmlTag("a"); + + public static HtmlTag Abbr => new HtmlTag("abbr"); + + public static HtmlTag Address => new HtmlTag("address"); + + public static HtmlTag Area => new HtmlTag("area").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Article => new HtmlTag("article"); + + public static HtmlTag Aside => new HtmlTag("aside"); + + public static HtmlTag Audio => new HtmlTag("audio"); + + public static HtmlTag B => new HtmlTag("b"); + + public static HtmlTag Base => new HtmlTag("base").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Bdi => new HtmlTag("bdi"); + + public static HtmlTag Bdo => new HtmlTag("bdo"); + + public static HtmlTag BlockQuote => new HtmlTag("blockquote"); + + public static HtmlTag Body => new HtmlTag("body"); + + public static HtmlTag Br => new HtmlTag("br").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Button => new HtmlTag("button"); + + public static HtmlTag Canvas => new HtmlTag("canvas"); + + public static HtmlTag Caption => new HtmlTag("caption"); + + public static HtmlTag Cite => new HtmlTag("cite"); + + public static HtmlTag Code => new HtmlTag("code"); + + public static HtmlTag Col => new HtmlTag("col").Render(TagRenderMode.SelfClosing); + + public static HtmlTag ColGroup => new HtmlTag("colgroup"); + + public static HtmlTag Data => new HtmlTag("data"); + + public static HtmlTag DataList => new HtmlTag("datalist"); + + public static HtmlTag Dd => new HtmlTag("dd"); + + public static HtmlTag Del => new HtmlTag("del"); + + public static HtmlTag Details => new HtmlTag("details"); + + public static HtmlTag Dfn => new HtmlTag("dfn"); + + public static HtmlTag Div => new HtmlTag("div"); + + public static HtmlTag Dl => new HtmlTag("dl"); + + public static HtmlTag Dt => new HtmlTag("dt"); + + public static HtmlTag Em => new HtmlTag("em"); + + public static HtmlTag Embed => new HtmlTag("embed").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Fieldset => new HtmlTag("fieldset"); + + public static HtmlTag FigCaption => new HtmlTag("figcaption"); + + public static HtmlTag Figure => new HtmlTag("figure"); + + public static HtmlTag Footer => new HtmlTag("footer"); + + public static HtmlTag Form => new HtmlTag("form"); + + public static HtmlTag H1 => new HtmlTag("h1"); + + public static HtmlTag H2 => new HtmlTag("h2"); + + public static HtmlTag H3 => new HtmlTag("h3"); + + public static HtmlTag H4 => new HtmlTag("h4"); + + public static HtmlTag H5 => new HtmlTag("h5"); + + public static HtmlTag H6 => new HtmlTag("h6"); + + public static HtmlTag Head => new HtmlTag("head"); + + public static HtmlTag Header => new HtmlTag("header"); + + public static HtmlTag Hr => new HtmlTag("hr").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Html => new HtmlTag("html"); + + public static HtmlTag I => new HtmlTag("i"); + + public static HtmlTag Iframe => new HtmlTag("iframe"); + + public static HtmlTag Img => new HtmlTag("img").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Ins => new HtmlTag("ins"); + + public static HtmlTag Kbd => new HtmlTag("kbd"); + + public static HtmlTag Keygen => new HtmlTag("keygen"); + + public static HtmlTag Label => new HtmlTag("label"); + + public static HtmlTag Legend => new HtmlTag("legend"); + + public static HtmlTag Li => new HtmlTag("li"); + + public static HtmlTag Link => new HtmlTag("link").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Main => new HtmlTag("main"); + + public static HtmlTag Map => new HtmlTag("map"); + + public static HtmlTag Mark => new HtmlTag("mark"); + + public static HtmlTag Menu => new HtmlTag("menu"); + + public static HtmlTag MenuItem => new HtmlTag("menuitem"); + + public static HtmlTag Meta => new HtmlTag("meta").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Meter => new HtmlTag("meter"); + + public static HtmlTag Nav => new HtmlTag("nav"); + + public static HtmlTag NoScript => new HtmlTag("noscript"); + + public static HtmlTag Object => new HtmlTag("object"); + + public static HtmlTag Ol => new HtmlTag("ol"); + + public static HtmlTag OptGroup => new HtmlTag("optgroup"); + + public static HtmlTag Option => new HtmlTag("option"); + + public static HtmlTag Output => new HtmlTag("output"); + + public static HtmlTag P => new HtmlTag("p"); + + public static HtmlTag Param => new HtmlTag("param").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Pre => new HtmlTag("pre"); + + public static HtmlTag Progress => new HtmlTag("progress"); + + public static HtmlTag Q => new HtmlTag("q"); + + public static HtmlTag Rp => new HtmlTag("rp"); + + public static HtmlTag Rt => new HtmlTag("rt"); + + public static HtmlTag Ruby => new HtmlTag("ruby"); + + public static HtmlTag S => new HtmlTag("s"); + + public static HtmlTag Samp => new HtmlTag("samp"); + + public static HtmlTag Script => new HtmlTag("script"); + + public static HtmlTag Section => new HtmlTag("section"); + + public static HtmlTag Select => new HtmlTag("select"); + + public static HtmlTag Small => new HtmlTag("small"); + + public static HtmlTag Source => new HtmlTag("source").Render(TagRenderMode.SelfClosing); + + public static HtmlTag Span => new HtmlTag("span"); + + public static HtmlTag Strong => new HtmlTag("strong"); + + public static HtmlTag Style => new HtmlTag("style"); + + public static HtmlTag Sub => new HtmlTag("sub"); + + public static HtmlTag Summary => new HtmlTag("summary"); + + public static HtmlTag Sup => new HtmlTag("sup"); + + public static HtmlTag Table => new HtmlTag("table"); + + public static HtmlTag Tbody => new HtmlTag("tbody"); + + public static HtmlTag Td => new HtmlTag("td"); + + public static HtmlTag Template => new HtmlTag("template"); + + public static HtmlTag Textarea => new HtmlTag("textarea"); + + public static HtmlTag Tfoot => new HtmlTag("tfoot"); + + public static HtmlTag Th => new HtmlTag("th"); + + public static HtmlTag Thead => new HtmlTag("thead"); + + public static HtmlTag Time => new HtmlTag("time"); + + public static HtmlTag Title => new HtmlTag("title"); + + public static HtmlTag Tr => new HtmlTag("tr"); + + public static HtmlTag Track => new HtmlTag("track").Render(TagRenderMode.SelfClosing); + + public static HtmlTag U => new HtmlTag("u"); + + public static HtmlTag Ul => new HtmlTag("ul"); + + public static HtmlTag Var => new HtmlTag("var"); + + public static HtmlTag Video => new HtmlTag("video"); + + public static HtmlTag Wbr => new HtmlTag("wbr").Render(TagRenderMode.SelfClosing); + + public static class Input + { + public static HtmlTag Button => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("button"); + + public static HtmlTag CheckBox => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("checkbox"); + + public static HtmlTag Color => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("color"); + + public static HtmlTag Date => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("date"); + + public static HtmlTag DateTime => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("datetime"); + + public static HtmlTag DateTimeLocal => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("datetime-local"); + + public static HtmlTag Email => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("email"); + + public static HtmlTag File => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("file"); + + public static HtmlTag Hidden => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("hidden"); + + public static HtmlTag Image => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("image"); + + public static HtmlTag Month => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("month"); + + public static HtmlTag Number => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("number"); + + public static HtmlTag Password => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("password"); + + public static HtmlTag Radio => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("radio"); + + public static HtmlTag Range => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("range"); + + public static HtmlTag Reset => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("reset"); + + public static HtmlTag Search => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("search"); + + public static HtmlTag Submit => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("submit"); + + public static HtmlTag Tel => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("tel"); + + public static HtmlTag Text => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("text"); + + public static HtmlTag Time => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("time"); + + public static HtmlTag Url => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("url"); + + public static HtmlTag Week => new HtmlTag("input").Render(TagRenderMode.SelfClosing).Type("week"); + } + } +} diff --git a/HtmlFactory/HtmlText.cs b/HtmlFactory/HtmlText.cs new file mode 100644 index 0000000..b0ddf79 --- /dev/null +++ b/HtmlFactory/HtmlText.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Encodings.Web; + +using Microsoft.AspNetCore.Html; + + + +namespace HtmlFactory +{ + /// + /// Represents a text node that has an optional parent and some text + /// + public class HtmlText : IHtmlElement + { + private readonly IHtmlContent _content = null; + private string _rawinput; + private bool IsEncoded = false; + + public string Tagname => null; + + public Dictionary Attributes { get => null; set { } } + public List Contents { get => null; set { } } + + + /// + /// Initializes a new instance of + /// + /// The text that still needs to be encoded + public HtmlText(string text, bool inToBeEncoded = true) + { + _rawinput = text; + IsEncoded = !inToBeEncoded; + } + + /// + /// Initializes a new instance of + /// + /// The already encoded HTML string + public HtmlText(HtmlString htmlString) + { + _content = htmlString ?? new HtmlString(string.Empty); + } + + + /// + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + if (_content == null && !string.IsNullOrEmpty(_rawinput)) + { + if (IsEncoded) + { + writer.Write(_rawinput); + } + else + { + encoder.Encode(writer, _rawinput); + } + } + else + { + _content.WriteTo(writer, encoder); + } + + } + + /// + public override string ToString() + { + using (var writer = new StringWriter()) + { + WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } + } + + private bool Equals(HtmlText other) + { + return string.Equals(ToString(), other.ToString()); + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((HtmlText)obj); + } + + /// + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + } +} diff --git a/HtmlFactory/IHtmlContentExtensions.ToHtmlTag.cs b/HtmlFactory/IHtmlContentExtensions.ToHtmlTag.cs new file mode 100644 index 0000000..9e9debb --- /dev/null +++ b/HtmlFactory/IHtmlContentExtensions.ToHtmlTag.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Encodings.Web; + +using Microsoft.AspNetCore.Html; + + +namespace HtmlFactory +{ + /// + /// Contains utility methods to convert into other things, such as an . + /// + public static class IHtmlContentExtensions + { + /// + /// Renders this html content to an HTML string using the default HTML encoder + /// + /// The HTML content to render + /// A string representation of the HTML content + public static string ToHtmlString(this IHtmlContent htmlContent) + { + using (var writer = new StringWriter()) + { + htmlContent.WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } + } + + /// + /// Parses the provided HTML content and tries to extract an HTML tag from it. + /// + /// + /// + /// + /// + public static HtmlTag ToHtmlTag(this IHtmlContent htmlContent) + { + var htmlTags = HtmlTag.ParseAll(htmlContent).OfType().ToList(); + if (htmlTags.Count > 1) throw new ArgumentException($"Multiple tags parsed from html: {Environment.NewLine}" + + $"{string.Join(Environment.NewLine, htmlTags.Select(t => t.ToString()))}"); + return htmlTags.SingleOrDefault(); + } + } +} diff --git a/HtmlFactory/Interfaces.cs b/HtmlFactory/Interfaces.cs new file mode 100644 index 0000000..9b955cf --- /dev/null +++ b/HtmlFactory/Interfaces.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.AspNetCore.Html; + +namespace HtmlFactory +{ + public interface IHtmlElement : IHtmlContent + { + string Tagname { get; } + Dictionary Attributes { get; set; } + List Contents { get; set; } + } +} diff --git a/HtmlFactory/Used Nuget Commands.txt b/HtmlFactory/Used Nuget Commands.txt new file mode 100644 index 0000000..cbe7ccf --- /dev/null +++ b/HtmlFactory/Used Nuget Commands.txt @@ -0,0 +1,11 @@ + + + + + + + + + +Install-Package Microsoft.AspNetCore.Html.Abstractions -Version 2.2.0 +Install-Package HtmlAgilityPack -Version 1.11.43 \ No newline at end of file