Skip to content

Commit 0f8f8c2

Browse files
committed
UPDATE: <our-svg> - Caching + Enforcing Viewbox
1 parent bc6df56 commit 0f8f8c2

File tree

6 files changed

+243
-19
lines changed

6 files changed

+243
-19
lines changed
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
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using Microsoft.Extensions.Logging;
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.Cache;
8+
using Umbraco.Cms.Core.Events;
9+
using Umbraco.Cms.Core.Notifications;
10+
using Umbraco.Extensions;
11+
12+
namespace Our.Umbraco.TagHelpers.Notifications
13+
{
14+
public class InlineSvgTagHelperNotifications : INotificationHandler<MediaSavedNotification>
15+
{
16+
private readonly ILogger<InlineSvgTagHelperNotifications> _logger;
17+
private readonly AppCaches _appCaches;
18+
19+
public InlineSvgTagHelperNotifications(ILogger<InlineSvgTagHelperNotifications> logger, AppCaches appCaches)
20+
{
21+
_logger = logger;
22+
_appCaches = appCaches;
23+
}
24+
25+
public void Handle(MediaSavedNotification notification)
26+
{
27+
foreach (var mediaItem in notification.SavedEntities)
28+
{
29+
if (mediaItem.ContentType.Alias.Equals("umbracoMediaVectorGraphics", StringComparison.InvariantCultureIgnoreCase))
30+
{
31+
var cacheKey = string.Concat("MediaItem-SvgContents (", mediaItem.Key.ToString(), ")");
32+
if (_appCaches.RuntimeCache.SearchByKey(cacheKey).Any())
33+
{
34+
_appCaches.RuntimeCache.ClearByKey(cacheKey);
35+
_logger.LogDebug($"Removed {mediaItem.Name} from RuntimeCache");
36+
}
37+
}
38+
}
39+
}
40+
}
41+
}

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,44 @@ If you do not specify a template and use `<our-lang-switcher />` it will use the
182182
This tag helper element `<our-svg>` will read the file contents of an SVG file and output it as an inline SVG in the DOM.
183183
It can be used in one of two ways, either by specifying the `src` attribute to a physcial static file served from wwwRoot or by specifying the `media-item` attribute to use a picked IPublishedContent Media Item.
184184

185+
### Basic usage:
185186
```cshtml
186187
<our-svg src="/assets/icon.svg" />
187188
<our-svg media-item="@Model.Logo" />
188189
```
189190

191+
### Advanced usage: *(as of version 1.x.x)
192+
193+
Additional properties have been added to cache the output and also to ensure the SVG contains a viewbox property instead of the width & height properties to aid in making the vector image responsive within a parent HTML element.
194+
```cshtml
195+
<our-svg src="/assets/icon.svg" class="my-css-class" ensure-viewbox="true" cache="true" cache-minutes="180" ignore-appsettings="true" />
196+
<our-svg media-item="@Model.Logo" class="my-css-class" ensure-viewbox="true" cache="true" cache-minutes="180" ignore-appsettings="true" />
197+
```
198+
199+
- `class` - Allows for a CSS class upon the SVG element. This is a `string` value.
200+
- `ensure-viewbox` - Enables the feature to "fix" the output SVG which always ensures the SVG utilises a viewbox rather than width & height. This is a `boolean` value.
201+
- `cache` - Enables the feature to cache the output at runtime level. This is a `boolean` value.
202+
- `cache-minutes` - Defines the amount of time (in minutes) to cache the output. To be used in conjunction with the `cache` property. This is an `integer` value.
203+
- `ignore-appsettings` - When enabled, the all settings appropiate to this tag helper which are defined within `appsettings.json` are completely ignored. For example, if global caching is enabled we can simply disable caching of individual elements (unless the `cache` property is `true`). This is a `boolean` value.
204+
205+
### Global settings via appsettings.json
206+
207+
Applying any of the below configurations within your `appsettings.json` file will apply global settings to all elements using this tag helper. See the `ignore-appsettings` to override these global settings at element level. The values shown below are the hard-coded default values.
208+
209+
{
210+
"Our.Umbraco.TagHelpers": {
211+
"OurSVG": {
212+
"EnsureViewBox": false,
213+
"Cache": false,
214+
"CacheMinutes": 180
215+
}
216+
}
217+
}
218+
219+
> **Note:** SVG caches are cleared on application restart, or by resaving the media in the media library.
220+
221+
222+
190223
## `<our-fallback>`
191224
This tag helper element `<our-fallback>` uses the same fallback mode logic that is only available on the `Value()` method of the `IPublishedContent` interface that uses a string for the property name to lookup. In addition if the fallback value from a language or ancestors is not available we are still able to fallback to the content inside the tag.
192225

0 commit comments

Comments
 (0)