Skip to content

Commit

Permalink
Refactor ImportRuleCssSource and related tests for improved clarity a…
Browse files Browse the repository at this point in the history
…nd functionality
  • Loading branch information
martinnormark committed Jan 22, 2025
1 parent 30ae28d commit 6dbc26f
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 139 deletions.
72 changes: 64 additions & 8 deletions PreMailer.Net/PreMailer.Net.Tests/ImportRuleCssSourceTests.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,97 @@
using AngleSharp.Html.Parser;
using Xunit;
using Xunit;
using Moq;
using PreMailer.Net.Downloaders;
using PreMailer.Net.Sources;
using System;
using System.Text;
using System.IO;
using System.Collections.Generic;


namespace PreMailer.Net.Tests
{
public class ImportRuleCssSourceTests
{
private readonly Mock<IWebDownloader> _webDownloader = new Mock<IWebDownloader>();

public ImportRuleCssSourceTests()
{
WebDownloader.SharedDownloader = _webDownloader.Object;
}

[Fact]
public void FetchImportRules()
public void ItShould_DownloadAllImportedUrls_WhenCssContainsImportRules()
{
var baseUri = new Uri("https://a.com");
var urls = new List<string>() { "variables.css?v234", "/fonts.css", "https://fonts.google.com/css/test-font" };

var css = CreateCss(urls);
var contents = ImportRuleCssSource.FetchImportRules(baseUri, css);
var sut = new ImportRuleCssSource();

sut.GetCss(baseUri, css);

foreach (var url in urls)
{
_webDownloader.Verify(w => w.DownloadString(It.Is<Uri>(u => u.Equals(new Uri(baseUri, url)))));
}
}

[Fact]
public void ItShould_NotDownloadUrls_WhenLevelIsGreaterThanTwo()
{
var baseUri = new Uri("https://a.com");
var urls = new List<string>() { "variables.css?v234", "/fonts.css", "https://fonts.google.com/css/test-font" };

var css = CreateCss(urls);
var sut = new ImportRuleCssSource();

sut.GetCss(baseUri, css, 2);

_webDownloader.Verify(w => w.DownloadString(It.IsAny<Uri>()), Times.Never());
}

private string CreateCss(IEnumerable<string> imports)
[Fact]
public void ItShould_NotDownloadUrls_WhenCssIsEmpty()
{
WebDownloader.SharedDownloader = _webDownloader.Object;
var baseUri = new Uri("https://a.com");
var urls = new List<string>() { "variables.css?v234", "/fonts.css", "https://fonts.google.com/css/test-font" };

var css = CreateCss(urls);
var sut = new ImportRuleCssSource();

sut.GetCss(baseUri, string.Empty);

_webDownloader.Verify(w => w.DownloadString(It.IsAny<Uri>()), Times.Never());
}

[Fact]
public void ItShould_NotDownloadUrls_WhenCssIsNull()
{
var baseUri = new Uri("https://a.com");
var urls = new List<string>() { "variables.css?v234", "/fonts.css", "https://fonts.google.com/css/test-font" };

var css = CreateCss(urls);
var sut = new ImportRuleCssSource();

sut.GetCss(baseUri, null);

_webDownloader.Verify(w => w.DownloadString(It.IsAny<Uri>()), Times.Never());
}

[Fact]
public void ItShould_NotDownloadUrls_WhenCssDoesNotContainImportRules()
{
var baseUri = new Uri("https://a.com");
var urls = new List<string>() { "variables.css?v234", "/fonts.css", "https://fonts.google.com/css/test-font" };

var css = string.Join(Environment.NewLine, urls);
var sut = new ImportRuleCssSource();

sut.GetCss(baseUri, css);

_webDownloader.Verify(w => w.DownloadString(It.IsAny<Uri>()), Times.Never());
}

private string CreateCss(IEnumerable<string> imports)
{
var builder = new StringBuilder();
foreach (var import in imports)
{
Expand Down
167 changes: 40 additions & 127 deletions PreMailer.Net/PreMailer.Net/Sources/ImportRuleCssSource.cs
Original file line number Diff line number Diff line change
@@ -1,180 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using AngleSharp.Dom;
using PreMailer.Net.Downloaders;

namespace PreMailer.Net.Sources
{
/// <summary>
/// This class is used by the LinkTagCssSource class for donwloading/fetching import rules.
/// This class is used by the LinkTagCssSource class for downloading/fetching import rules.
/// </summary>
public class ImportRuleCssSource : ICssSource
public class ImportRuleCssSource
{
private readonly Uri _downloadUri;
private readonly int _level;
private Dictionary<Uri, string> _importList;

private Dictionary<Uri, string> _importList = new Dictionary<Uri, string>();
private static Regex _importRegex = new Regex("@import.*?[\"'](?<href>[^\"']+)[\"'].*?;", RegexOptions.Multiline | RegexOptions.IgnoreCase);

public ImportRuleCssSource(string importUri, Uri baseUri, int level, Dictionary<Uri, string> importList)
public IEnumerable<string> GetCss(Uri linkedStylesheetUrl, string contents, int level = 0)
{

if (Uri.IsWellFormedUriString(importUri, UriKind.Relative) && baseUri != null)
if (level >= 2 || string.IsNullOrEmpty(contents))
{
_downloadUri = new Uri(baseUri, importUri);
return _importList.Values;
}
else

var baseUri = GetBaseUri(linkedStylesheetUrl);
var matches = GetMatches(contents);

foreach (Match match in matches)
{
// Assume absolute
_downloadUri = new Uri(importUri);
}
var href = match.Groups["href"].Value;
Uri url = default;

_level = level;
_importList = importList;
}
if (Uri.IsWellFormedUriString(href, UriKind.Relative))
{
url = new Uri(baseUri, href);
}
else
{
url = new Uri(href);
}

public IEnumerable<string> GetCss()
{
Console.WriteLine($"{new string('\t', _level + 1)}GetCss scheme: {_downloadUri.Scheme}");
var content = DownloadContents(url);

if (IsSupported(_downloadUri.Scheme) && !_importList.ContainsKey(_downloadUri))
{
DownloadContents();
}
else if (_importList.ContainsKey(_downloadUri))
{
Console.WriteLine($"{new string('\t', _level + 1)}Already got import from '{_downloadUri}'");
}
_importList.Add(url, content);

// Everything is added to the _importList which is passed in with the constructor.
// So, we don't have to return anything here.
GetCss(url, content, level + 1);
}

return default;
return _importList.Values;
}

private void DownloadContents()
private string DownloadContents(Uri downloadUri)
{
string contents;
var indent = new string('\t', _level + 1);

try
{
Console.WriteLine($"{indent}Will download import from '{_downloadUri}'");

contents = WebDownloader.SharedDownloader.DownloadString(_downloadUri);
contents = WebDownloader.SharedDownloader.DownloadString(downloadUri);
}
catch (WebException ex)
{
Console.WriteLine($"Download failed with: {ex}");
throw new WebException($"PreMailer.Net is unable to download the requested URL: {_downloadUri}", ex);
throw new WebException($"PreMailer.Net is unable to download the requested URL: {downloadUri}", ex);
}

if (_level < 2 && contents != null) // Stop processing imports at level 2
{
FetchImportRules(_downloadUri, contents, _level + 1, ref _importList);
}

// Prevent a recursive import of the same url
if (!_importList.ContainsKey( _downloadUri))
{
_importList.Add(_downloadUri, contents);
}
else
{
Console.WriteLine($"{indent}An import added {_downloadUri} in the meantime!");
}

}

private static bool IsSupported(string scheme)
{
return
scheme == "http" ||
scheme == "https";
return contents;
}

private static bool IsSupported(string scheme) => scheme == "http" || scheme == "https";

/// <summary>
/// This is the entry point for the LinkTagCssSource class when fetching its content.
/// </summary>
/// <param name="downloadUri"></param>
/// <param name="contents"></param>
/// <returns></returns>
public static IList<string> FetchImportRules(Uri downloadUri, string contents)
private static MatchCollection GetMatches(string contents)
{
if (contents == null) // Is the case with testing
{
return null;
}


var importList = new Dictionary<Uri, string>();

// First fetch the content of any import rule
FetchImportRules(downloadUri, contents, 0, ref importList);

return importList.Values.ToList();

}

/// <summary>
/// This methods gets recursively called from within this class.
/// </summary>
/// <param name="downloadUri"></param>
/// <param name="contents"></param>
/// <param name="level"></param>
/// <param name="contentBuilder"></param>
/// <param name="importList"></param>
private static void FetchImportRules(Uri downloadUri, string contents, int level, ref Dictionary<Uri, string> importList)
{
string indent = level > 0 ? new string('\t', level) : string.Empty;
int cnt = 0;

var matches = GetMatches(contents);
if (matches.Count > 0)
if (string.IsNullOrEmpty(contents))
{
var baseUri = GetBaseUri(downloadUri);

Console.WriteLine($"{indent}Found {matches.Count} import declarations");

foreach (Match match in matches)
{
var url = match.Groups["href"].Value;

Console.WriteLine($"{indent}{++cnt}: {url}");

var importRuleSource = new ImportRuleCssSource(url, baseUri, level, importList);
importRuleSource.GetCss();
}
return default;
}

}

/// <summary>
/// Extracted as a separate method for testing purposes.
/// </summary>
/// <param name="contents"></param>
/// <returns></returns>
public static MatchCollection GetMatches(string contents)
{
return _importRegex.Matches(contents);
}

private static Uri GetBaseUri(Uri downloadUri)
{
var baseUrl = new UriBuilder(downloadUri)
{
Port = -1 /* Excludes the port number */,
Query = string.Empty
var baseUrl = new UriBuilder(downloadUri)
{
Port = -1 /* Excludes the port number */,
Query = string.Empty
};

// Strip of the css file segment
var path = baseUrl.Path;
baseUrl.Path = path.Substring(0, path.LastIndexOf('/') + 1);

return baseUrl.Uri;
}
}
Expand Down
14 changes: 10 additions & 4 deletions PreMailer.Net/PreMailer.Net/Sources/LinkTagCssSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
using AngleSharp.Dom;
using PreMailer.Net.Downloaders;

Expand All @@ -13,9 +11,15 @@ public class LinkTagCssSource : ICssSource
{
private readonly Uri _downloadUri;
private List<string> _cssContents;
private ImportRuleCssSource _importRuleCssSource;
public LinkTagCssSource(IElement node, Uri baseUri) : this(node, baseUri, new ImportRuleCssSource())
{
}

public LinkTagCssSource(IElement node, Uri baseUri)
public LinkTagCssSource(IElement node, Uri baseUri, ImportRuleCssSource importRuleCssSource)
{
_importRuleCssSource = importRuleCssSource;

// There must be an href
var href = node.Attributes.First(a => a.Name.Equals("href", StringComparison.OrdinalIgnoreCase)).Value;

Expand Down Expand Up @@ -59,11 +63,13 @@ private List<string> DownloadContents()
}

// Fetch possible import rules
var imports = ImportRuleCssSource.FetchImportRules(_downloadUri, content);
var imports = _importRuleCssSource.GetCss(_downloadUri, content);

if (imports != null)
{
_cssContents.AddRange(imports);
}

_cssContents.Add(content);

return _cssContents;
Expand Down

0 comments on commit 6dbc26f

Please sign in to comment.