Skip to content

Commit

Permalink
Merge pull request #45 from AndyBoot/updates/our-svg
Browse files Browse the repository at this point in the history
UPDATE: <our-svg> - Caching + Enforcing Viewbox
  • Loading branch information
Warren Buckley authored Oct 17, 2022
2 parents bc6df56 + 4bd085d commit 1611f8c
Show file tree
Hide file tree
Showing 8 changed files with 355 additions and 29 deletions.
39 changes: 29 additions & 10 deletions Our.Umbraco.TagHelpers.Tests/InlineSvgTagHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Our.Umbraco.TagHelpers.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
Expand All @@ -20,6 +21,7 @@ public class InlineSvgTagHelperTests
{
private TagHelperContext _context = null!;
private TagHelperOutput _output = null!;
private IOptions<OurUmbracoTagHelpersConfiguration> _settings = null!;

[SetUp]
public void Setup()
Expand All @@ -36,12 +38,25 @@ public void Setup()
content.SetContent("Something else");
return Task.FromResult<TagHelperContent>(content);
});

var settings = new OurUmbracoTagHelpersConfiguration()
{
OurSVG =
{
Cache = false,
EnsureViewBox = false,
CacheMinutes = 180
}
};
_settings = Options.Create(settings);

}

[Test]
public void NoOutputIfNoMediaOrFileSet()
{
var tagHelper = new InlineSvgTagHelper(null, null, null);

var tagHelper = new InlineSvgTagHelper(null, null, null, _settings, null);

tagHelper.Process(_context, _output);

Expand All @@ -52,7 +67,7 @@ public void NoOutputIfNoMediaOrFileSet()
public void NoOutputIfBothMediaAndFileSet()
{
var umbContent = Mock.Of<IPublishedContent>(c => c.ContentType.ItemType == PublishedItemType.Media);
var tagHelper = new InlineSvgTagHelper(null, null, null)
var tagHelper = new InlineSvgTagHelper(null, null, null, _settings, null)
{
FileSource = "test.svg",
MediaItem = umbContent
Expand All @@ -66,7 +81,7 @@ public void NoOutputIfBothMediaAndFileSet()
[Test]
public void NoOutputIfFileNotSvg()
{
var tagHelper = new InlineSvgTagHelper(null, null, null)
var tagHelper = new InlineSvgTagHelper(null, null, null, _settings, null)
{
FileSource = "test.notsvg"
};
Expand All @@ -82,7 +97,7 @@ public void NoOutputIfFileNotFound()
var fileProvider = new Mock<IFileProvider>();
fileProvider.Setup(p => p.GetFileInfo(It.IsAny<string>())).Returns(Mock.Of<IFileInfo>(f => !f.Exists));
var hostEnv = Mock.Of<IWebHostEnvironment>(e => e.WebRootFileProvider == fileProvider.Object);
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null)
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null, _settings, null)
{
FileSource = "test.svg"
};
Expand All @@ -98,7 +113,7 @@ public void ExpectedOutputIfValidFile()
var fileProvider = new Mock<IFileProvider>();
fileProvider.Setup(p => p.GetFileInfo(It.IsAny<string>())).Returns(Mock.Of<IFileInfo>(f => f.Exists && f.CreateReadStream() == new MemoryStream(Encoding.UTF8.GetBytes("test svg"))));
var hostEnv = Mock.Of<IWebHostEnvironment>(e => e.WebRootFileProvider == fileProvider.Object);
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null)
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null, _settings, null)
{
FileSource = "test.svg"
};
Expand All @@ -116,7 +131,7 @@ public void NoOutputIfMediaUrlNull()
{
var urlProvider = new Mock<IPublishedUrlProvider>();
urlProvider.Setup(p => p.GetMediaUrl(It.IsAny<IPublishedContent>(), It.IsAny<UrlMode>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns((string)null!);
var tagHelper = new InlineSvgTagHelper(null, null, urlProvider.Object)
var tagHelper = new InlineSvgTagHelper(null, null, urlProvider.Object, _settings, null)
{
MediaItem = Mock.Of<IPublishedContent>(c => c.ContentType.ItemType == PublishedItemType.Media)
};
Expand All @@ -132,7 +147,7 @@ public void NoOutputIfMediaNotSvg()
var umbContent = Mock.Of<IPublishedContent>(c => c.ContentType.ItemType == PublishedItemType.Media);
var urlProvider = new Mock<IPublishedUrlProvider>();
urlProvider.Setup(p => p.GetMediaUrl(umbContent, It.IsAny<UrlMode>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns("test.notsvg");
var tagHelper = new InlineSvgTagHelper(null, null, urlProvider.Object)
var tagHelper = new InlineSvgTagHelper(null, null, urlProvider.Object, _settings, null)
{
MediaItem = umbContent
};
Expand All @@ -152,7 +167,9 @@ public void NoOutputIfMediaNotFound()
var tagHelper = new InlineSvgTagHelper(
new MediaFileManager(fileSystem, null, null, null, null, Mock.Of<IOptions<ContentSettings>>()),
null,
urlProvider.Object)
urlProvider.Object,
_settings,
null)
{
MediaItem = umbContent
};
Expand All @@ -172,7 +189,9 @@ public void ExpectedOutputIfValidMedia()
var tagHelper = new InlineSvgTagHelper(
new MediaFileManager(fileSystem, null, null, null, null, Mock.Of<IOptions<ContentSettings>>()),
null,
urlProvider.Object)
urlProvider.Object,
_settings,
null)
{
MediaItem = umbContent
};
Expand All @@ -193,7 +212,7 @@ public void SanitizesJavascript()
.Setup(p => p.GetFileInfo(It.IsAny<string>()))
.Returns(Mock.Of<IFileInfo>(f => f.Exists && f.CreateReadStream() == new MemoryStream(Encoding.UTF8.GetBytes("<a xlink:href=\"javascript:alert('test');\">Click here</a><script attr=\"test\">test</script>end"))));
var hostEnv = Mock.Of<IWebHostEnvironment>(e => e.WebRootFileProvider == fileProvider.Object);
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null)
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null, _settings, null)
{
FileSource = "test.svg"
};
Expand Down
20 changes: 20 additions & 0 deletions Our.Umbraco.TagHelpers/Composing/InlineSvgTagHelperComposer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Our.Umbraco.TagHelpers.Notifications;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;

namespace Our.Umbraco.TagHelpers.Composing
{
public class InlineSvgTagHelperComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddNotificationHandler<MediaSavedNotification, InlineSvgTagHelperNotifications>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Our.Umbraco.TagHelpers.Configuration
{
public class OurUmbracoTagHelpersConfiguration
{
public InlineSvgTagHelperConfiguration OurSVG { get; set; } = new InlineSvgTagHelperConfiguration();
}

public class InlineSvgTagHelperConfiguration
{
public bool EnsureViewBox { get; set; } = false;
public bool Cache { get; set; } = false;
public int CacheMinutes { get; set; } = 180;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Our.Umbraco.TagHelpers.Services;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;

namespace Our.Umbraco.TagHelpers.Configuration
{
public class OurUmbracoTagHelpersConfigurationComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddOptions<OurUmbracoTagHelpersConfiguration>()
.Bind(builder.Config.GetSection("Our.Umbraco.TagHelpers"));
}
}
}
138 changes: 119 additions & 19 deletions Our.Umbraco.TagHelpers/InlineSvgTagHelper.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
using Microsoft.AspNetCore.Hosting;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Options;
using Our.Umbraco.TagHelpers.Configuration;
using Our.Umbraco.TagHelpers.Utils;
using System;
using System.IO;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
Expand All @@ -20,12 +25,16 @@ public class InlineSvgTagHelper : TagHelper
private MediaFileManager _mediaFileManager;
private IWebHostEnvironment _webHostEnvironment;
private IPublishedUrlProvider _urlProvider;
private OurUmbracoTagHelpersConfiguration _globalSettings;
private AppCaches _appCaches;

public InlineSvgTagHelper(MediaFileManager mediaFileManager, IWebHostEnvironment webHostEnvironment, IPublishedUrlProvider urlProvider)
public InlineSvgTagHelper(MediaFileManager mediaFileManager, IWebHostEnvironment webHostEnvironment, IPublishedUrlProvider urlProvider, IOptions<OurUmbracoTagHelpersConfiguration> globalSettings, AppCaches appCaches)
{
_mediaFileManager = mediaFileManager;
_webHostEnvironment = webHostEnvironment;
_urlProvider = urlProvider;
_globalSettings = globalSettings.Value;
_appCaches = appCaches;
}

/// <summary>
Expand All @@ -42,6 +51,40 @@ public InlineSvgTagHelper(MediaFileManager mediaFileManager, IWebHostEnvironment
[HtmlAttributeName("media-item")]
public IPublishedContent? MediaItem { get; set; }

/// <summary>
/// A classic CSS class property to apply/append a CSS class or classes.
/// </summary>
[HtmlAttributeName("class")]
public string? CssClass { get; set; }

/// <summary>
/// A boolean to ensure a viewbox is present within the SVG tag to ensure the vector is always responsive.
/// NOTE: Use the appsettings configuration to apply this globally (e.g. "Our.Umbraco.TagHelpers": { "InlineSvgTagHelper": { "EnsureViewBox": true } } ).
/// </summary>
[HtmlAttributeName("ensure-viewbox")]
public bool EnsureViewBox { get; set; }

/// <summary>
/// A boolean to cache the SVG contents rather than performing the operation on each page load.
/// NOTE: Use the appsettings configuration to apply this globally (e.g. "Our.Umbraco.TagHelpers": { "InlineSvgTagHelper": { "Cache": true } } ).
/// </summary>
[HtmlAttributeName("cache")]
public bool Cache { get; set; }

/// <summary>
/// An integer to set the cache minutes. Default: 180 minutes.
/// NOTE: Use the appsettings configuration to apply this globally (e.g. "Our.Umbraco.TagHelpers": { "InlineSvgTagHelper": { "CacheMinutes": 180 } } ).
/// </summary>
[HtmlAttributeName("cache-minutes")]
public int CacheMinutes { get; set; }

/// <summary>
/// A boolean to ignore the appsettings.
/// NOTE: Applies to 'ensure-viewbox' & 'cache' only
/// </summary>
[HtmlAttributeName("ignore-appsettings")]
public bool IgnoreAppSettings { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)
{
// Can only use media-item OR src
Expand All @@ -55,40 +98,79 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
return;
}

string? cleanedFileContents = null;

if(Cache || (_globalSettings.OurSVG.Cache && !IgnoreAppSettings))
{
var cacheName = string.Empty;
var cacheMins = CacheMinutes > 0 ? CacheMinutes : _globalSettings.OurSVG.CacheMinutes;

if (MediaItem is not null)
{
cacheName = string.Concat("MediaItem-SvgContents (", MediaItem.Key.ToString(), ")");
}
else if (string.IsNullOrWhiteSpace(FileSource) == false)
{
cacheName = string.Concat("File-SvgContents (", FileSource, ")");
}

cleanedFileContents = _appCaches.RuntimeCache.GetCacheItem(cacheName, () =>
{
return GetFileContents();
}, TimeSpan.FromMinutes(cacheMins));
}
else
{
cleanedFileContents = GetFileContents();
}

if (string.IsNullOrEmpty(cleanedFileContents))
{
output.SuppressOutput();
return;
}

// Remove the src attribute or media-item from the <svg>
output.Attributes.RemoveAll("src");
output.Attributes.RemoveAll("media-item");

output.TagName = null; // Remove <our-svg>
output.Content.SetHtmlContent(cleanedFileContents);
}

private string? GetFileContents()
{
// SVG fileContents to render to DOM
var fileContents = string.Empty;

if(MediaItem is not null)
if (MediaItem is not null)
{
// Check Umbraco Media Item that is picked/used
// has a file that uses a .svg file extension
var mediaItemPath = MediaItem.Url(_urlProvider);
if (mediaItemPath?.EndsWith(".svg", StringComparison.InvariantCultureIgnoreCase) != true)
{
output.SuppressOutput();
return;
return null;
}

// Ensure the file actually exists on disk, Azure blob provider or ...
// Anywhere else defined by IFileSystem to fetch & store files
if (_mediaFileManager.FileSystem.FileExists(mediaItemPath) == false)
{
output.SuppressOutput();
return;
return null;
}

// Read its contents (get its stream)
var fileStream = _mediaFileManager.FileSystem.OpenFile(mediaItemPath);
using var reader = new StreamReader(fileStream);
fileContents = reader.ReadToEnd();
}
else if(string.IsNullOrWhiteSpace(FileSource) == false)
else if (string.IsNullOrWhiteSpace(FileSource) == false)
{
// Check string src filepath ends with .svg
if (FileSource.EndsWith(".svg", StringComparison.InvariantCultureIgnoreCase) == false)
{
output.SuppressOutput();
return;
return null;
}

// Get file from wwwRoot using a path such as
Expand All @@ -98,10 +180,9 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
var file = webRoot.GetFileInfo(FileSource);

// Ensure file exists in wwwroot path
if(file.Exists == false)
if (file.Exists == false)
{
output.SuppressOutput();
return;
return null;
}

using var reader = new StreamReader(file.CreateReadStream());
Expand All @@ -120,12 +201,31 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
@"syntax:error:",
RegexOptions.IgnoreCase | RegexOptions.Singleline);

// Remove the src attribute or media-item from the <svg>
output.Attributes.RemoveAll("src");
output.Attributes.RemoveAll("media-item");
if ((EnsureViewBox || (_globalSettings.OurSVG.EnsureViewBox && !IgnoreAppSettings)) || !string.IsNullOrEmpty(CssClass))
{
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(cleanedFileContents);
var svgs = doc.DocumentNode.SelectNodes("//svg");
foreach (var svgNode in svgs)
{
if (!string.IsNullOrEmpty(CssClass))
{
svgNode.AddClass(CssClass);
}
if ((EnsureViewBox || (_globalSettings.OurSVG.EnsureViewBox && !IgnoreAppSettings)) && svgNode.Attributes.Contains("width") && svgNode.Attributes.Contains("height") && !svgNode.Attributes.Contains("viewbox"))
{
var width = StringUtils.GetDecimal(svgNode.GetAttributeValue("width", "0"));
var height = StringUtils.GetDecimal(svgNode.GetAttributeValue("height", "0"));
svgNode.SetAttributeValue("viewbox", $"0 0 {width} {height}");

svgNode.Attributes.Remove("width");
svgNode.Attributes.Remove("height");
}
}
cleanedFileContents = doc.DocumentNode.OuterHtml;
}

output.TagName = null; // Remove <our-svg>
output.Content.SetHtmlContent(cleanedFileContents);
}
return cleanedFileContents;
}
}
}
Loading

0 comments on commit 1611f8c

Please sign in to comment.