using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace BilibiliEvolved.Build { partial class ProjectBuilder { public ProjectBuilder BuildResources() { new CssMinifier().Build(this); new HtmlMinifier().Build(this); new JavascriptMinifier().Build(this); return this; } } abstract class ResourceMinifier { public abstract Predicate FileFilter { get; } public abstract string ResourceType { get; } public abstract string Minify(string input); public static string GetMinimizedFileName(string path) { var fileInfo = new FileInfo(path); return "min/" + fileInfo.Name.Insert(fileInfo.Name.LastIndexOf("."), ".min"); } public static IEnumerable GetFiles(Predicate filter) { string getRelativePath(string fullPath) { var currentFolder = Environment.CurrentDirectory; var path = fullPath.Replace(currentFolder, ""); if (path.StartsWith(Path.DirectorySeparatorChar)) { return path.Remove(path.IndexOf(Path.DirectorySeparatorChar), 1); } else { return path; } } IEnumerable getFiles(Predicate predicate, string path) { var list = new List(); var currentDirectory = new DirectoryInfo(path); list.AddRange(currentDirectory.EnumerateFiles() .Where(file => { var fullName = file.FullName; return predicate(file) && !fullName.Contains(@".vs\") && !fullName.Contains(@".vscode\") && !fullName.Contains(@"build-scripts\") && !fullName.Contains(@"node_modules\") && !fullName.Contains(@".backup.") && !fullName.Contains(@"extras"); }) .Select(file => getRelativePath(file.FullName))); foreach (var subDir in currentDirectory.EnumerateDirectories()) { list.AddRange(getFiles(filter, subDir.FullName)); } return list; } var directory = Environment.CurrentDirectory; if (!directory.EndsWith(Path.DirectorySeparatorChar)) { directory = directory + Path.DirectorySeparatorChar; } return getFiles(filter, directory); } public virtual ProjectBuilder Build(ProjectBuilder builder) { var files = GetFiles(FileFilter); using (var cache = new BuildCache()) { var changedFiles = files.Where(file => !cache.Contains(file)); Parallel.ForEach(changedFiles, path => { builder.WriteInfo($"{ResourceType} minify: {path}"); var text = File.ReadAllText(path); var result = Minify(text); var outputPath = GetMinimizedFileName(path); File.WriteAllText(outputPath, result); cache.AddCache(path); builder.WriteHint($"\t=> {outputPath}"); // builder.WriteHint($"\t=> {outputPath.PadRight(48)}{(100.0 * result.Length / text.Length):0.##}%"); }); cache.SaveCache(); } files.ForEach(file => { builder.OriginalResourceLength += new FileInfo(file).Length; builder.MinimizedResourceLength += new FileInfo(GetMinimizedFileName(file)).Length; }); builder.WriteSuccess($"{ResourceType} minify complete."); return builder; } } sealed class CssMinifier : ResourceMinifier { public override Predicate FileFilter { get; } = file => { return !file.FullName.Contains(".min") && file.Name != "dark.css" && file.Name != "dark-template.css" && (file.Extension == ".css"); }; public override string ResourceType { get; } = "CSS"; public override string Minify(string input) { return new UglifyCss().Run(input); } public override ProjectBuilder Build(ProjectBuilder builder) { base.Build(builder); return builder.BuildDarkStyles(); } } sealed class JavascriptMinifier : ResourceMinifier { public override Predicate FileFilter { get; } = file => { return file.FullName.Contains("src" + Path.DirectorySeparatorChar) && !file.FullName.Contains("client" + Path.DirectorySeparatorChar) && file.Extension == ".js"; }; public override string ResourceType { get; } = "JavaScript"; public override string Minify(string input) { if (!input.StartsWith("(() =>")) { Func convertToRuntimeSource = source => { var index = source.LastIndexOf("/"); if (index != -1) { source = source.Remove(1, index); } return source; }; input = RegexReplacer.Replace(input, @"import (.*) from ([^\r\n;]*)", match => { var imported = match.Groups[1].Value.Replace(" as ", ":"); var source = convertToRuntimeSource(match.Groups[2].Value); return $"const {imported} = resources.import({source})"; }); input = RegexReplacer.Replace(input, @" import\((.*)\)", match => { var source = convertToRuntimeSource(match.Groups[1].Value); return $" resources.importAsync({source})"; }); input = @"(() => { return (settings, resources) => { " + input.Replace("export default ", "return ").Replace("export ", "") + @" }; })();"; } // Console.WriteLine(input); return new UglifyJs().Run(input); } } sealed class HtmlMinifier : ResourceMinifier { public override Predicate FileFilter { get; } = file => { return !file.FullName.Contains(".min") && file.Extension == ".html"; }; public override string ResourceType { get; } = "HTML"; static HtmlMinifier() { var patterns = new string[] { @"([^\0]*?)<\/category>", @"([^\0]*?)<\/checkbox>", @"([^\0]*?)<\/dropdown>", @"([^\0]*?)<\/textbox>", }; var replacements = new string[] { @"
  • $2
  • ", @"
  • ", @"
  • ", @"
  • ", }; regices = patterns.Zip(replacements, (pattern, replacement) => { return (new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase), replacement); }); } private static readonly IEnumerable<(Regex, string)> regices; public override string Minify(string input) { foreach (var (regex, replacement) in regices) { input = regex.Replace(input, replacement); } return new UglifyHtml().Run(input); } } }