Skip to content

Commit 1611f8c

Browse files
author
Warren Buckley
authored
Merge pull request #45 from AndyBoot/updates/our-svg
UPDATE: <our-svg> - Caching + Enforcing Viewbox
2 parents bc6df56 + 4bd085d commit 1611f8c

File tree

8 files changed

+355
-29
lines changed

8 files changed

+355
-29
lines changed

Our.Umbraco.TagHelpers.Tests/InlineSvgTagHelperTests.cs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.Extensions.Options;
55
using Moq;
66
using NUnit.Framework;
7+
using Our.Umbraco.TagHelpers.Configuration;
78
using System;
89
using System.Collections.Generic;
910
using System.IO;
@@ -20,6 +21,7 @@ public class InlineSvgTagHelperTests
2021
{
2122
private TagHelperContext _context = null!;
2223
private TagHelperOutput _output = null!;
24+
private IOptions<OurUmbracoTagHelpersConfiguration> _settings = null!;
2325

2426
[SetUp]
2527
public void Setup()
@@ -36,12 +38,25 @@ public void Setup()
3638
content.SetContent("Something else");
3739
return Task.FromResult<TagHelperContent>(content);
3840
});
41+
42+
var settings = new OurUmbracoTagHelpersConfiguration()
43+
{
44+
OurSVG =
45+
{
46+
Cache = false,
47+
EnsureViewBox = false,
48+
CacheMinutes = 180
49+
}
50+
};
51+
_settings = Options.Create(settings);
52+
3953
}
4054

4155
[Test]
4256
public void NoOutputIfNoMediaOrFileSet()
4357
{
44-
var tagHelper = new InlineSvgTagHelper(null, null, null);
58+
59+
var tagHelper = new InlineSvgTagHelper(null, null, null, _settings, null);
4560

4661
tagHelper.Process(_context, _output);
4762

@@ -52,7 +67,7 @@ public void NoOutputIfNoMediaOrFileSet()
5267
public void NoOutputIfBothMediaAndFileSet()
5368
{
5469
var umbContent = Mock.Of<IPublishedContent>(c => c.ContentType.ItemType == PublishedItemType.Media);
55-
var tagHelper = new InlineSvgTagHelper(null, null, null)
70+
var tagHelper = new InlineSvgTagHelper(null, null, null, _settings, null)
5671
{
5772
FileSource = "test.svg",
5873
MediaItem = umbContent
@@ -66,7 +81,7 @@ public void NoOutputIfBothMediaAndFileSet()
6681
[Test]
6782
public void NoOutputIfFileNotSvg()
6883
{
69-
var tagHelper = new InlineSvgTagHelper(null, null, null)
84+
var tagHelper = new InlineSvgTagHelper(null, null, null, _settings, null)
7085
{
7186
FileSource = "test.notsvg"
7287
};
@@ -82,7 +97,7 @@ public void NoOutputIfFileNotFound()
8297
var fileProvider = new Mock<IFileProvider>();
8398
fileProvider.Setup(p => p.GetFileInfo(It.IsAny<string>())).Returns(Mock.Of<IFileInfo>(f => !f.Exists));
8499
var hostEnv = Mock.Of<IWebHostEnvironment>(e => e.WebRootFileProvider == fileProvider.Object);
85-
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null)
100+
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null, _settings, null)
86101
{
87102
FileSource = "test.svg"
88103
};
@@ -98,7 +113,7 @@ public void ExpectedOutputIfValidFile()
98113
var fileProvider = new Mock<IFileProvider>();
99114
fileProvider.Setup(p => p.GetFileInfo(It.IsAny<string>())).Returns(Mock.Of<IFileInfo>(f => f.Exists && f.CreateReadStream() == new MemoryStream(Encoding.UTF8.GetBytes("test svg"))));
100115
var hostEnv = Mock.Of<IWebHostEnvironment>(e => e.WebRootFileProvider == fileProvider.Object);
101-
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null)
116+
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null, _settings, null)
102117
{
103118
FileSource = "test.svg"
104119
};
@@ -116,7 +131,7 @@ public void NoOutputIfMediaUrlNull()
116131
{
117132
var urlProvider = new Mock<IPublishedUrlProvider>();
118133
urlProvider.Setup(p => p.GetMediaUrl(It.IsAny<IPublishedContent>(), It.IsAny<UrlMode>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns((string)null!);
119-
var tagHelper = new InlineSvgTagHelper(null, null, urlProvider.Object)
134+
var tagHelper = new InlineSvgTagHelper(null, null, urlProvider.Object, _settings, null)
120135
{
121136
MediaItem = Mock.Of<IPublishedContent>(c => c.ContentType.ItemType == PublishedItemType.Media)
122137
};
@@ -132,7 +147,7 @@ public void NoOutputIfMediaNotSvg()
132147
var umbContent = Mock.Of<IPublishedContent>(c => c.ContentType.ItemType == PublishedItemType.Media);
133148
var urlProvider = new Mock<IPublishedUrlProvider>();
134149
urlProvider.Setup(p => p.GetMediaUrl(umbContent, It.IsAny<UrlMode>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns("test.notsvg");
135-
var tagHelper = new InlineSvgTagHelper(null, null, urlProvider.Object)
150+
var tagHelper = new InlineSvgTagHelper(null, null, urlProvider.Object, _settings, null)
136151
{
137152
MediaItem = umbContent
138153
};
@@ -152,7 +167,9 @@ public void NoOutputIfMediaNotFound()
152167
var tagHelper = new InlineSvgTagHelper(
153168
new MediaFileManager(fileSystem, null, null, null, null, Mock.Of<IOptions<ContentSettings>>()),
154169
null,
155-
urlProvider.Object)
170+
urlProvider.Object,
171+
_settings,
172+
null)
156173
{
157174
MediaItem = umbContent
158175
};
@@ -172,7 +189,9 @@ public void ExpectedOutputIfValidMedia()
172189
var tagHelper = new InlineSvgTagHelper(
173190
new MediaFileManager(fileSystem, null, null, null, null, Mock.Of<IOptions<ContentSettings>>()),
174191
null,
175-
urlProvider.Object)
192+
urlProvider.Object,
193+
_settings,
194+
null)
176195
{
177196
MediaItem = umbContent
178197
};
@@ -193,7 +212,7 @@ public void SanitizesJavascript()
193212
.Setup(p => p.GetFileInfo(It.IsAny<string>()))
194213
.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"))));
195214
var hostEnv = Mock.Of<IWebHostEnvironment>(e => e.WebRootFileProvider == fileProvider.Object);
196-
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null)
215+
var tagHelper = new InlineSvgTagHelper(null, hostEnv, null, _settings, null)
197216
{
198217
FileSource = "test.svg"
199218
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Our.Umbraco.TagHelpers.Notifications;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using Umbraco.Cms.Core.Composing;
8+
using Umbraco.Cms.Core.DependencyInjection;
9+
using Umbraco.Cms.Core.Notifications;
10+
11+
namespace Our.Umbraco.TagHelpers.Composing
12+
{
13+
public class InlineSvgTagHelperComposer : IComposer
14+
{
15+
public void Compose(IUmbracoBuilder builder)
16+
{
17+
builder.AddNotificationHandler<MediaSavedNotification, InlineSvgTagHelperNotifications>();
18+
}
19+
}
20+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Our.Umbraco.TagHelpers.Configuration
2+
{
3+
public class OurUmbracoTagHelpersConfiguration
4+
{
5+
public InlineSvgTagHelperConfiguration OurSVG { get; set; } = new InlineSvgTagHelperConfiguration();
6+
}
7+
8+
public class InlineSvgTagHelperConfiguration
9+
{
10+
public bool EnsureViewBox { get; set; } = false;
11+
public bool Cache { get; set; } = false;
12+
public int CacheMinutes { get; set; } = 180;
13+
}
14+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Our.Umbraco.TagHelpers.Services;
3+
using Umbraco.Cms.Core.Composing;
4+
using Umbraco.Cms.Core.DependencyInjection;
5+
6+
namespace Our.Umbraco.TagHelpers.Configuration
7+
{
8+
public class OurUmbracoTagHelpersConfigurationComposer : IComposer
9+
{
10+
public void Compose(IUmbracoBuilder builder)
11+
{
12+
builder.Services.AddOptions<OurUmbracoTagHelpersConfiguration>()
13+
.Bind(builder.Config.GetSection("Our.Umbraco.TagHelpers"));
14+
}
15+
}
16+
}

Our.Umbraco.TagHelpers/InlineSvgTagHelper.cs

Lines changed: 119 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
using Microsoft.AspNetCore.Hosting;
1+
using HtmlAgilityPack;
2+
using Microsoft.AspNetCore.Hosting;
23
using Microsoft.AspNetCore.Razor.TagHelpers;
4+
using Microsoft.Extensions.Options;
5+
using Our.Umbraco.TagHelpers.Configuration;
6+
using Our.Umbraco.TagHelpers.Utils;
37
using System;
48
using System.IO;
59
using System.Text.RegularExpressions;
10+
using Umbraco.Cms.Core.Cache;
611
using Umbraco.Cms.Core.IO;
712
using Umbraco.Cms.Core.Models.PublishedContent;
813
using Umbraco.Cms.Core.Routing;
@@ -20,12 +25,16 @@ public class InlineSvgTagHelper : TagHelper
2025
private MediaFileManager _mediaFileManager;
2126
private IWebHostEnvironment _webHostEnvironment;
2227
private IPublishedUrlProvider _urlProvider;
28+
private OurUmbracoTagHelpersConfiguration _globalSettings;
29+
private AppCaches _appCaches;
2330

24-
public InlineSvgTagHelper(MediaFileManager mediaFileManager, IWebHostEnvironment webHostEnvironment, IPublishedUrlProvider urlProvider)
31+
public InlineSvgTagHelper(MediaFileManager mediaFileManager, IWebHostEnvironment webHostEnvironment, IPublishedUrlProvider urlProvider, IOptions<OurUmbracoTagHelpersConfiguration> globalSettings, AppCaches appCaches)
2532
{
2633
_mediaFileManager = mediaFileManager;
2734
_webHostEnvironment = webHostEnvironment;
2835
_urlProvider = urlProvider;
36+
_globalSettings = globalSettings.Value;
37+
_appCaches = appCaches;
2938
}
3039

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

54+
/// <summary>
55+
/// A classic CSS class property to apply/append a CSS class or classes.
56+
/// </summary>
57+
[HtmlAttributeName("class")]
58+
public string? CssClass { get; set; }
59+
60+
/// <summary>
61+
/// A boolean to ensure a viewbox is present within the SVG tag to ensure the vector is always responsive.
62+
/// NOTE: Use the appsettings configuration to apply this globally (e.g. "Our.Umbraco.TagHelpers": { "InlineSvgTagHelper": { "EnsureViewBox": true } } ).
63+
/// </summary>
64+
[HtmlAttributeName("ensure-viewbox")]
65+
public bool EnsureViewBox { get; set; }
66+
67+
/// <summary>
68+
/// A boolean to cache the SVG contents rather than performing the operation on each page load.
69+
/// NOTE: Use the appsettings configuration to apply this globally (e.g. "Our.Umbraco.TagHelpers": { "InlineSvgTagHelper": { "Cache": true } } ).
70+
/// </summary>
71+
[HtmlAttributeName("cache")]
72+
public bool Cache { get; set; }
73+
74+
/// <summary>
75+
/// An integer to set the cache minutes. Default: 180 minutes.
76+
/// NOTE: Use the appsettings configuration to apply this globally (e.g. "Our.Umbraco.TagHelpers": { "InlineSvgTagHelper": { "CacheMinutes": 180 } } ).
77+
/// </summary>
78+
[HtmlAttributeName("cache-minutes")]
79+
public int CacheMinutes { get; set; }
80+
81+
/// <summary>
82+
/// A boolean to ignore the appsettings.
83+
/// NOTE: Applies to 'ensure-viewbox' & 'cache' only
84+
/// </summary>
85+
[HtmlAttributeName("ignore-appsettings")]
86+
public bool IgnoreAppSettings { get; set; }
87+
4588
public override void Process(TagHelperContext context, TagHelperOutput output)
4689
{
4790
// Can only use media-item OR src
@@ -55,40 +98,79 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
5598
return;
5699
}
57100

101+
string? cleanedFileContents = null;
102+
103+
if(Cache || (_globalSettings.OurSVG.Cache && !IgnoreAppSettings))
104+
{
105+
var cacheName = string.Empty;
106+
var cacheMins = CacheMinutes > 0 ? CacheMinutes : _globalSettings.OurSVG.CacheMinutes;
107+
108+
if (MediaItem is not null)
109+
{
110+
cacheName = string.Concat("MediaItem-SvgContents (", MediaItem.Key.ToString(), ")");
111+
}
112+
else if (string.IsNullOrWhiteSpace(FileSource) == false)
113+
{
114+
cacheName = string.Concat("File-SvgContents (", FileSource, ")");
115+
}
116+
117+
cleanedFileContents = _appCaches.RuntimeCache.GetCacheItem(cacheName, () =>
118+
{
119+
return GetFileContents();
120+
}, TimeSpan.FromMinutes(cacheMins));
121+
}
122+
else
123+
{
124+
cleanedFileContents = GetFileContents();
125+
}
126+
127+
if (string.IsNullOrEmpty(cleanedFileContents))
128+
{
129+
output.SuppressOutput();
130+
return;
131+
}
132+
133+
// Remove the src attribute or media-item from the <svg>
134+
output.Attributes.RemoveAll("src");
135+
output.Attributes.RemoveAll("media-item");
136+
137+
output.TagName = null; // Remove <our-svg>
138+
output.Content.SetHtmlContent(cleanedFileContents);
139+
}
140+
141+
private string? GetFileContents()
142+
{
58143
// SVG fileContents to render to DOM
59144
var fileContents = string.Empty;
60145

61-
if(MediaItem is not null)
146+
if (MediaItem is not null)
62147
{
63148
// Check Umbraco Media Item that is picked/used
64149
// has a file that uses a .svg file extension
65150
var mediaItemPath = MediaItem.Url(_urlProvider);
66151
if (mediaItemPath?.EndsWith(".svg", StringComparison.InvariantCultureIgnoreCase) != true)
67152
{
68-
output.SuppressOutput();
69-
return;
153+
return null;
70154
}
71155

72156
// Ensure the file actually exists on disk, Azure blob provider or ...
73157
// Anywhere else defined by IFileSystem to fetch & store files
74158
if (_mediaFileManager.FileSystem.FileExists(mediaItemPath) == false)
75159
{
76-
output.SuppressOutput();
77-
return;
160+
return null;
78161
}
79162

80163
// Read its contents (get its stream)
81164
var fileStream = _mediaFileManager.FileSystem.OpenFile(mediaItemPath);
82165
using var reader = new StreamReader(fileStream);
83166
fileContents = reader.ReadToEnd();
84167
}
85-
else if(string.IsNullOrWhiteSpace(FileSource) == false)
168+
else if (string.IsNullOrWhiteSpace(FileSource) == false)
86169
{
87170
// Check string src filepath ends with .svg
88171
if (FileSource.EndsWith(".svg", StringComparison.InvariantCultureIgnoreCase) == false)
89172
{
90-
output.SuppressOutput();
91-
return;
173+
return null;
92174
}
93175

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

100182
// Ensure file exists in wwwroot path
101-
if(file.Exists == false)
183+
if (file.Exists == false)
102184
{
103-
output.SuppressOutput();
104-
return;
185+
return null;
105186
}
106187

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

123-
// Remove the src attribute or media-item from the <svg>
124-
output.Attributes.RemoveAll("src");
125-
output.Attributes.RemoveAll("media-item");
204+
if ((EnsureViewBox || (_globalSettings.OurSVG.EnsureViewBox && !IgnoreAppSettings)) || !string.IsNullOrEmpty(CssClass))
205+
{
206+
HtmlDocument doc = new HtmlDocument();
207+
doc.LoadHtml(cleanedFileContents);
208+
var svgs = doc.DocumentNode.SelectNodes("//svg");
209+
foreach (var svgNode in svgs)
210+
{
211+
if (!string.IsNullOrEmpty(CssClass))
212+
{
213+
svgNode.AddClass(CssClass);
214+
}
215+
if ((EnsureViewBox || (_globalSettings.OurSVG.EnsureViewBox && !IgnoreAppSettings)) && svgNode.Attributes.Contains("width") && svgNode.Attributes.Contains("height") && !svgNode.Attributes.Contains("viewbox"))
216+
{
217+
var width = StringUtils.GetDecimal(svgNode.GetAttributeValue("width", "0"));
218+
var height = StringUtils.GetDecimal(svgNode.GetAttributeValue("height", "0"));
219+
svgNode.SetAttributeValue("viewbox", $"0 0 {width} {height}");
220+
221+
svgNode.Attributes.Remove("width");
222+
svgNode.Attributes.Remove("height");
223+
}
224+
}
225+
cleanedFileContents = doc.DocumentNode.OuterHtml;
226+
}
126227

127-
output.TagName = null; // Remove <our-svg>
128-
output.Content.SetHtmlContent(cleanedFileContents);
129-
}
228+
return cleanedFileContents;
229+
}
130230
}
131231
}

0 commit comments

Comments
 (0)