Skip to content

Caching SVG doesn't cache CSS class name and Viewbox settings #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 79 additions & 27 deletions Our.Umbraco.TagHelpers/InlineSvgTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,31 @@ public InlineSvgTagHelper(MediaFileManager mediaFileManager, IWebHostEnvironment
[HtmlAttributeName("ignore-appsettings")]
public bool IgnoreAppSettings { get; set; }



private string CalculateSvgContentCacheKey() {
if (MediaItem is not null)
{
return string.Concat("MediaItem-SvgContents (", MediaItem.Key.ToString(), ")");
}

if (string.IsNullOrWhiteSpace(FileSource) == false)
{
return string.Concat("File-SvgContents (", FileSource, ")");
}

return string.Empty;
}

private string CalculateSetAttrsCacheKey (string cleanedFileContents)
=> $"SvgWithAttributes ({CssClass?.GetHashCode()}_{cleanedFileContents.GetHashCode()})";

Comment on lines +105 to +106
Copy link
Preview

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using GetHashCode() for cache keys can cause collisions since GetHashCode() is not guaranteed to be unique or consistent across application restarts. Consider using a more deterministic approach like combining the actual values or using a cryptographic hash.

Suggested change
=> $"SvgWithAttributes ({CssClass?.GetHashCode()}_{cleanedFileContents.GetHashCode()})";
{
// Use actual values or a deterministic hash for the cache key
var cssClassValue = CssClass ?? string.Empty;
var combined = cssClassValue + "|" + cleanedFileContents;
var hash = ComputeSHA256Hash(combined);
return $"SvgWithAttributes ({hash})";
}
private static string ComputeSHA256Hash(string input)
{
using (var sha256 = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(input);
var hashBytes = sha256.ComputeHash(bytes);
var sb = new StringBuilder();
foreach (var b in hashBytes)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString();
}
}

Copilot uses AI. Check for mistakes.

private int FetchCacheMinutes()
=> CacheMinutes > 0 ? CacheMinutes : _globalSettings.OurSVG.CacheMinutes;

private bool FetchEnsureViewBoxStatus () =>
EnsureViewBox || (_globalSettings.OurSVG.EnsureViewBox && !IgnoreAppSettings);

public override void Process(TagHelperContext context, TagHelperOutput output)
{
// Can only use media-item OR src
Expand All @@ -99,23 +124,14 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
}

string? cleanedFileContents = null;

if(Cache || (_globalSettings.OurSVG.Cache && !IgnoreAppSettings))
var doCache = Cache || (_globalSettings.OurSVG.Cache && !IgnoreAppSettings);
if (doCache)
{
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, ")");
}
var cacheKey = CalculateSvgContentCacheKey();
var cacheMins = FetchCacheMinutes();

cleanedFileContents = _appCaches.RuntimeCache.GetCacheItem(cacheName, () =>
{
cleanedFileContents = _appCaches.RuntimeCache.GetCacheItem(cacheKey, () =>
{
return GetFileContents();
}, TimeSpan.FromMinutes(cacheMins));
}
Expand All @@ -130,12 +146,28 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
return;
}

string svgWithAttributes;
if (doCache)
{
var cacheKey = CalculateSetAttrsCacheKey(cleanedFileContents);
var cacheMins = FetchCacheMinutes();

svgWithAttributes = _appCaches.RuntimeCache.GetCacheItem(cacheKey, () =>
{
return ParseAndSetAttrs(cleanedFileContents);
}, TimeSpan.FromMinutes(cacheMins));
}
else
{
svgWithAttributes = ParseAndSetAttrs(cleanedFileContents);
}

// 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);
output.Content.SetHtmlContent(svgWithAttributes);
}

private string? GetFileContents()
Expand Down Expand Up @@ -201,18 +233,38 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
@"syntax:error:",
RegexOptions.IgnoreCase | RegexOptions.Singleline);

if ((EnsureViewBox || (_globalSettings.OurSVG.EnsureViewBox && !IgnoreAppSettings)) || !string.IsNullOrEmpty(CssClass))
return cleanedFileContents;
}

/// <summary>
/// Set CSS Class, Viewbox, and/or width/height
/// </summary>
/// <param name="cleanedFileContents">SVG file contents</param>
/// <returns>SVG file contents with attributes</returns>
private string ParseAndSetAttrs(string cleanedFileContents) {
var doEnsureViewBox = FetchEnsureViewBoxStatus();
var hasCssClass = !string.IsNullOrEmpty(CssClass);

// No work to do (save the unnecessary LoadHtml)
if (!doEnsureViewBox && !hasCssClass)
{
return cleanedFileContents;
}

HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(cleanedFileContents);
var svgs = doc.DocumentNode.SelectNodes("//svg");
foreach (var svgNode in svgs)
{
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(cleanedFileContents);
var svgs = doc.DocumentNode.SelectNodes("//svg");
foreach (var svgNode in svgs)
if (hasCssClass)
{
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"))
svgNode.AddClass(CssClass);
}

if (doEnsureViewBox)
{
var hasDimensionsAndNoViewbox = svgNode.Attributes.Contains("width") && svgNode.Attributes.Contains("height") && !svgNode.Attributes.Contains("viewbox");
if (hasDimensionsAndNoViewbox)
{
var width = StringUtils.GetDecimal(svgNode.GetAttributeValue("width", "0"));
var height = StringUtils.GetDecimal(svgNode.GetAttributeValue("height", "0"));
Expand All @@ -222,8 +274,8 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
svgNode.Attributes.Remove("height");
}
}
cleanedFileContents = doc.DocumentNode.OuterHtml;
}
cleanedFileContents = doc.DocumentNode.OuterHtml;

return cleanedFileContents;
}
Expand Down