330 lines
9.9 KiB
C#
330 lines
9.9 KiB
C#
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
|
||
public static class HtmlMinifier
|
||
{
|
||
public static string MinifyHtml(string html)
|
||
{
|
||
|
||
var htmlMinifier = new WebMarkupMin.Core.HtmlMinifier();
|
||
var result = htmlMinifier.Minify(html, generateStatistics: true);
|
||
return result.MinifiedContent;
|
||
|
||
if (string.IsNullOrEmpty(html))
|
||
return html;
|
||
|
||
// Вырезаем чувствительные теги
|
||
var placeholders = new Dictionary<string, string>();
|
||
html = ExtractTag(html, "pre", placeholders);
|
||
html = ExtractTag(html, "code", placeholders);
|
||
html = ExtractTag(html, "textarea", placeholders);
|
||
|
||
// Минификация CSS и JS
|
||
html = MinifyCssInHtml(html);
|
||
html = MinifyJavaScriptInHtml(html);
|
||
|
||
// Удаление HTML комментариев
|
||
html = Regex.Replace(html,
|
||
@"<!--(?!\[if|\s*\[endif).*?-->",
|
||
"",
|
||
RegexOptions.Singleline | RegexOptions.Compiled);
|
||
|
||
// Удаление лишних пробелов между тегами
|
||
html = Regex.Replace(html, @">\s+<", "><");
|
||
|
||
// Collapse whitespace
|
||
html = Regex.Replace(html, @"\s{2,}", " ");
|
||
|
||
// Возвращаем чувствительные блоки
|
||
foreach (var kv in placeholders)
|
||
html = html.Replace(kv.Key, kv.Value);
|
||
|
||
return html.Trim();
|
||
}
|
||
private static string ExtractTag(string html, string tag, Dictionary<string, string> dict)
|
||
{
|
||
return Regex.Replace(html,
|
||
$@"<{tag}[^>]*>[\s\S]*?<\/{tag}>",
|
||
m =>
|
||
{
|
||
var key = $"__PLACEHOLDER_{dict.Count}__";
|
||
dict[key] = m.Value;
|
||
return key;
|
||
},
|
||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||
}
|
||
|
||
private static string MinifyCssInHtml(string html)
|
||
{
|
||
// Находим все теги <style>
|
||
var styleRegex = new Regex(@"<style[^>]*>([\s\S]*?)</style>",
|
||
RegexOptions.Compiled);
|
||
|
||
return styleRegex.Replace(html, match =>
|
||
{
|
||
var css = match.Groups[1].Value;
|
||
var minifiedCss = MinifyCss(css);
|
||
return $"<style>{minifiedCss}</style>";
|
||
});
|
||
}
|
||
|
||
public static string MinifyCss(string css)
|
||
{
|
||
if (string.IsNullOrEmpty(css))
|
||
return css;
|
||
|
||
// 1. Удаление комментариев
|
||
css = Regex.Replace(css, @"/\*[\s\S]*?\*/", "",
|
||
RegexOptions.Compiled);
|
||
|
||
// 2. Удаление лишних пробелов и переносов
|
||
css = Regex.Replace(css, @"\s+", " ",
|
||
RegexOptions.Compiled);
|
||
css = Regex.Replace(css, @"\s*{\s*", "{",
|
||
RegexOptions.Compiled);
|
||
css = Regex.Replace(css, @"\s*}\s*", "}",
|
||
RegexOptions.Compiled);
|
||
css = Regex.Replace(css, @"\s*:\s*", ":",
|
||
RegexOptions.Compiled);
|
||
css = Regex.Replace(css, @"\s*;\s*", ";",
|
||
RegexOptions.Compiled);
|
||
css = Regex.Replace(css, @"\s*,\s*", ",",
|
||
RegexOptions.Compiled);
|
||
|
||
// 3. Удаление последней точки с запятой перед }
|
||
css = Regex.Replace(css, @";}", "}",
|
||
RegexOptions.Compiled);
|
||
|
||
// 4. Удаление пробелов вокруг селекторов
|
||
css = Regex.Replace(css, @"\s*>\s*", ">",
|
||
RegexOptions.Compiled);
|
||
css = Regex.Replace(css, @"\s*\+\s*", "+",
|
||
RegexOptions.Compiled);
|
||
css = Regex.Replace(css, @"\s*~\s*", "~",
|
||
RegexOptions.Compiled);
|
||
|
||
// 5. Удаление пробелов в значениях
|
||
css = Regex.Replace(css, @"(\d)\s+(px|em|rem|%|pt|pc|in|cm|mm|ex|ch|vw|vh|vmin|vmax)",
|
||
"$1$2", RegexOptions.Compiled);
|
||
|
||
// 6. Удаление ведущих нулей
|
||
css = Regex.Replace(css, @"(?<=[ :\(,])0?\.(\d+)", ".$1",
|
||
RegexOptions.Compiled);
|
||
|
||
css = Regex.Replace(css, @"url\(\s*(.*?)\s*\)", "url($1)");
|
||
|
||
return css.Trim();
|
||
}
|
||
|
||
private static string MinifyJavaScriptInHtml(string html)
|
||
{
|
||
// Находим все теги <script>
|
||
var scriptRegex = new Regex(@"<script[^>]*>([\s\S]*?)</script>",
|
||
RegexOptions.Compiled);
|
||
|
||
return scriptRegex.Replace(html, match =>
|
||
{
|
||
var scriptContent = match.Groups[1].Value;
|
||
|
||
// Пропускаем script с type="application/json" или src
|
||
var tag = match.Value;
|
||
if (tag.Contains("type=\"application/json\"") ||
|
||
tag.Contains("src=") ||
|
||
scriptContent.Trim().Length == 0)
|
||
return match.Value;
|
||
|
||
var minifiedJs = MinifyJavaScript(scriptContent);
|
||
|
||
if (tag.Contains("type=\"module\""))
|
||
return $"<script type=\"module\">{minifiedJs}</script>";
|
||
else
|
||
return $"<script>{minifiedJs}</script>";
|
||
});
|
||
}
|
||
|
||
public static string MinifyJavaScript(string js)
|
||
{
|
||
if (string.IsNullOrEmpty(js))
|
||
return js;
|
||
|
||
var sb = new StringBuilder(js.Length);
|
||
int i = 0;
|
||
int len = js.Length;
|
||
|
||
bool inSingle = false;
|
||
bool inDouble = false;
|
||
bool inTemplate = false;
|
||
bool inLineComment = false;
|
||
bool inBlockComment = false;
|
||
|
||
while (i < len)
|
||
{
|
||
char c = js[i];
|
||
char next = i + 1 < len ? js[i + 1] : '\0';
|
||
|
||
// -----------------------------
|
||
// LINE COMMENT //
|
||
// -----------------------------
|
||
if (inLineComment)
|
||
{
|
||
if (c == '\n' || c == '\r')
|
||
{
|
||
inLineComment = false;
|
||
sb.Append(' ');
|
||
}
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
// -----------------------------
|
||
// BLOCK COMMENT /* ... */ //
|
||
// -----------------------------
|
||
if (inBlockComment)
|
||
{
|
||
if (c == '*' && next == '/')
|
||
{
|
||
inBlockComment = false;
|
||
i += 2;
|
||
}
|
||
else
|
||
{
|
||
i++;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// -----------------------------
|
||
// STRING: '...' //
|
||
// -----------------------------
|
||
if (inSingle)
|
||
{
|
||
sb.Append(c);
|
||
if (c == '\\')
|
||
{
|
||
if (i + 1 < len) sb.Append(js[i + 1]);
|
||
i += 2;
|
||
continue;
|
||
}
|
||
if (c == '\'') inSingle = false;
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
// -----------------------------
|
||
// STRING: "..." //
|
||
// -----------------------------
|
||
if (inDouble)
|
||
{
|
||
sb.Append(c);
|
||
if (c == '\\')
|
||
{
|
||
if (i + 1 < len) sb.Append(js[i + 1]);
|
||
i += 2;
|
||
continue;
|
||
}
|
||
if (c == '"') inDouble = false;
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
// -----------------------------
|
||
// TEMPLATE: `...` //
|
||
// -----------------------------
|
||
if (inTemplate)
|
||
{
|
||
sb.Append(c);
|
||
if (c == '\\')
|
||
{
|
||
if (i + 1 < len) sb.Append(js[i + 1]);
|
||
i += 2;
|
||
continue;
|
||
}
|
||
if (c == '`') inTemplate = false;
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
// -----------------------------
|
||
// NORMAL CODE //
|
||
// -----------------------------
|
||
|
||
// Start of line comment
|
||
if (c == '/' && next == '/')
|
||
{
|
||
inLineComment = true;
|
||
i += 2;
|
||
continue;
|
||
}
|
||
|
||
// Start of block comment
|
||
if (c == '/' && next == '*')
|
||
{
|
||
inBlockComment = true;
|
||
i += 2;
|
||
continue;
|
||
}
|
||
|
||
// Start of strings
|
||
if (c == '\'')
|
||
{
|
||
inSingle = true;
|
||
sb.Append(c);
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
if (c == '"')
|
||
{
|
||
inDouble = true;
|
||
sb.Append(c);
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
if (c == '`')
|
||
{
|
||
inTemplate = true;
|
||
sb.Append(c);
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
// Collapse whitespace in code
|
||
if (char.IsWhiteSpace(c))
|
||
{
|
||
sb.Append(' ');
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
sb.Append(c);
|
||
i++;
|
||
}
|
||
|
||
// -----------------------------
|
||
// FINAL WHITESPACE MINIFICATION
|
||
// -----------------------------
|
||
var result = sb.ToString();
|
||
|
||
// Удаляем повторяющиеся пробелы
|
||
result = Regex.Replace(result, @"\s+", " ");
|
||
|
||
// Убираем пробелы вокруг операторов
|
||
result = Regex.Replace(result, @"\s*([=+\-*/%&|^<>!?:,;{}()\[\]])\s*", "$1");
|
||
|
||
return result.Trim();
|
||
}
|
||
|
||
public static string CompressJson(string json)
|
||
{
|
||
if (string.IsNullOrEmpty(json))
|
||
return json;
|
||
|
||
// Удаление пробелов и переносов из JSON
|
||
json = Regex.Replace(json, @"(""[^""\\]*(?:\\.[^""\\]*)*"")|\s+",
|
||
match => match.Groups[1].Success ? match.Groups[1].Value : "",
|
||
RegexOptions.Compiled);
|
||
|
||
return json;
|
||
}
|
||
} |