Skip to content

Commit 7cee6b7

Browse files
NetdocsCopilot
andcommitted
Fix CSS minifier dropping rules after quotes-in-comments
The CSS minifier protected string literals before stripping comments, so a quote or apostrophe inside a comment (e.g. `It's` or a "quoted" word) was mistaken for a string delimiter. That could swallow the rule immediately following the comment and drop it from the output. Strip CSS comments up front with a small string-aware scanner that skips over string literals (so `content: "/*"` is preserved) before the existing string-protection / whitespace passes run. Add regression tests for comments containing apostrophes/quotes and for comment-like tokens inside CSS strings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3a1f80d commit 7cee6b7

2 files changed

Lines changed: 74 additions & 4 deletions

File tree

src/Netdocs.Core/Optimization/CssJsMinifier.cs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,63 @@ public static string MinifyCss(string css)
1515
{
1616
if (string.IsNullOrEmpty(css)) return css;
1717

18+
// Strip comments with a string-aware scanner FIRST. A regex that protects string literals
19+
// before removing comments mis-reads quotes/apostrophes that live inside a comment (e.g.
20+
// "it's" or a quoted word), which can swallow the following rule. Removing comments up
21+
// front means the later string-protection pass only ever sees real CSS strings.
22+
css = StripCssComments(css);
23+
1824
var (withPlaceholders, strings) = ProtectStrings(css);
19-
withPlaceholders = CssComments().Replace(withPlaceholders, "");
2025
withPlaceholders = Whitespace().Replace(withPlaceholders, " ");
2126
// Tidy spaces around structural punctuation.
2227
withPlaceholders = CssAroundPunctuation().Replace(withPlaceholders, "$1");
2328
withPlaceholders = withPlaceholders.Replace(";}", "}").Trim();
2429
return RestoreStrings(withPlaceholders, strings);
2530
}
2631

32+
/// <summary>
33+
/// Removes <c>/* … */</c> comments while skipping over string literals so a <c>/*</c> that
34+
/// appears inside a CSS string (e.g. <c>content: "/*"</c>) is preserved, and quotes inside a
35+
/// comment are discarded with the comment rather than treated as string delimiters.
36+
/// </summary>
37+
private static string StripCssComments(string css)
38+
{
39+
var sb = new System.Text.StringBuilder(css.Length);
40+
int i = 0, n = css.Length;
41+
while (i < n)
42+
{
43+
var c = css[i];
44+
45+
if (c is '"' or '\'')
46+
{
47+
var quote = c;
48+
sb.Append(c);
49+
i++;
50+
while (i < n)
51+
{
52+
var d = css[i];
53+
sb.Append(d);
54+
if (d == '\\' && i + 1 < n) { sb.Append(css[i + 1]); i += 2; continue; }
55+
i++;
56+
if (d == quote) break;
57+
}
58+
continue;
59+
}
60+
61+
if (c == '/' && i + 1 < n && css[i + 1] == '*')
62+
{
63+
i += 2;
64+
while (i + 1 < n && !(css[i] == '*' && css[i + 1] == '/')) i++;
65+
i = Math.Min(i + 2, n); // skip closing */ (tolerate unterminated)
66+
continue;
67+
}
68+
69+
sb.Append(c);
70+
i++;
71+
}
72+
return sb.ToString();
73+
}
74+
2775
/// <summary>
2876
/// Minifies JavaScript conservatively: drops block and line comments and collapses runs of
2977
/// whitespace to a single space. Line breaks are preserved as spaces so automatic
@@ -59,9 +107,6 @@ private static string RestoreStrings(string text, List<string> strings) =>
59107
[GeneratedRegex(@"""(?:\\.|[^""\\])*""|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`")]
60108
private static partial Regex StringLiterals();
61109

62-
[GeneratedRegex(@"/\*[\s\S]*?\*/")]
63-
private static partial Regex CssComments();
64-
65110
[GeneratedRegex(@"/\*[\s\S]*?\*/")]
66111
private static partial Regex JsBlockComments();
67112

tests/Netdocs.Core.Tests/CssJsMinifierTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,31 @@ public void MinifyCss_PreservesStringContents()
3131
Assert.Contains("\" spaced value \"", min);
3232
}
3333

34+
[Fact]
35+
public void MinifyCss_CommentWithApostropheAndQuotes_DoesNotDropFollowingRule()
36+
{
37+
// Regression: a comment containing an apostrophe or quoted word used to make the
38+
// string-protection pass swallow the rule that followed it, dropping it from the output.
39+
var css = """
40+
/* It's a subtle "highlight" instead of an underline. */
41+
.md-tabs__item--active .md-tabs__link {
42+
font-weight: 700;
43+
}
44+
""";
45+
var min = CssJsMinifier.MinifyCss(css);
46+
47+
Assert.DoesNotContain("/*", min);
48+
Assert.Contains(".md-tabs__item--active .md-tabs__link{font-weight:700}", min);
49+
}
50+
51+
[Fact]
52+
public void MinifyCss_DoesNotTreatCommentTokenInsideString_AsComment()
53+
{
54+
var css = "a::before { content: \"/* not a comment */\"; }";
55+
var min = CssJsMinifier.MinifyCss(css);
56+
Assert.Contains("\"/* not a comment */\"", min);
57+
}
58+
3459
[Fact]
3560
public void MinifyJs_StripsCommentsAndCollapsesWhitespace()
3661
{

0 commit comments

Comments
 (0)