diff --git a/simple-css-nesting.sln b/simple-css-nesting.sln index 0446a06..4ca0aa6 100644 --- a/simple-css-nesting.sln +++ b/simple-css-nesting.sln @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.002.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cascadium", "src\Cascadium.csproj", "{ACD2DBA9-BF07-4F18-95ED-BC69DCC59A3C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cascadium-Utility", "tool\Cascadium-Utility.csproj", "{CB7E8644-3094-44DB-94C9-068CB12C4993}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{650184C6-E955-4D15-B478-15BB312B79AF}" @@ -13,16 +11,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{EF0FD356 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CssTests", "tests\build\CssTests\CssTests.csproj", "{9C391FB9-77C4-420D-88DC-2863B759AA64}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CascadiumLexer", "src\CascadiumLexer.csproj", "{7043E166-FDAC-4ACB-B44E-1EE018CADAE8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {ACD2DBA9-BF07-4F18-95ED-BC69DCC59A3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ACD2DBA9-BF07-4F18-95ED-BC69DCC59A3C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ACD2DBA9-BF07-4F18-95ED-BC69DCC59A3C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ACD2DBA9-BF07-4F18-95ED-BC69DCC59A3C}.Release|Any CPU.Build.0 = Release|Any CPU {CB7E8644-3094-44DB-94C9-068CB12C4993}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CB7E8644-3094-44DB-94C9-068CB12C4993}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB7E8644-3094-44DB-94C9-068CB12C4993}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -31,6 +27,10 @@ Global {9C391FB9-77C4-420D-88DC-2863B759AA64}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C391FB9-77C4-420D-88DC-2863B759AA64}.Release|Any CPU.ActiveCfg = Release|Any CPU {9C391FB9-77C4-420D-88DC-2863B759AA64}.Release|Any CPU.Build.0 = Release|Any CPU + {7043E166-FDAC-4ACB-B44E-1EE018CADAE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7043E166-FDAC-4ACB-B44E-1EE018CADAE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7043E166-FDAC-4ACB-B44E-1EE018CADAE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7043E166-FDAC-4ACB-B44E-1EE018CADAE8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/CascadiumCompiler.cs b/src/CascadiumCompiler.cs index a96d233..87f307e 100644 --- a/src/CascadiumCompiler.cs +++ b/src/CascadiumCompiler.cs @@ -11,29 +11,17 @@ namespace Cascadium; /// -/// Provides a CSS compiler that compiles higher-level code with single-line comments, nesting into a legacy CSS file. +/// Provides a compiler that compiles Cascadium code into an legacy CSS file. /// -public sealed partial class CascadiumCompiler +public static class CascadiumCompiler { /// - /// Compiles a top-module CSS stylesheet to legacy CSS, with the code already minified. + /// Compiles an Cascadium stylesheet to legacy CSS. /// - /// The byte array including the top module CSS code. - /// The encoder which will be used to decode the CSS output. - /// Optional options and parameters to the compilation. - /// The compiled and minified UTF-8 CSS. - public static string Compile(ReadOnlySpan xcss, Encoding encoder, CascadiumOptions? options = null) - { - return Compile(encoder.GetString(xcss), options); - } - - /// - /// Compiles a top-module CSS stylesheet to legacy CSS, with the code already minified. - /// - /// The top module CSS code. - /// Optional options and parameters to the compilation. + /// The Cascadium source code. + /// Optional. Options and parameters to the compiler. /// The compiled and minified CSS. - public static string Compile(string xcss, CascadiumOptions? options = null) + public static CssStylesheet Parse(string xcss, CascadiumOptions? options = null) { CascadiumOptions _options = options ?? new CascadiumOptions(); @@ -55,13 +43,48 @@ public static string Compile(string xcss, CascadiumOptions? options = null) //// build the css body, assembling the flat rules into an valid //// css string CssStylesheet css = new Assembler(_options).AssemblyCss(flattenStylesheet, _options); + css.Options = _options; //// apply cascadium extensions if (_options.UseVarShortcut) ValueHandler.TransformVarShortcuts(css); if (_options.AtRulesRewrites.Count > 0) MediaRewriter.ApplyRewrites(css, _options); if (_options.Converters.Count > 0) Converter.ConvertAll(css, _options); - //// export the css into an string - return css.Export(_options); + return css; + } + + /// + /// Compiles an Cascadium stylesheet to legacy CSS. + /// + /// The Cascadium source code. + /// Optional. Options and parameters to the compiler. + /// The compiled and minified CSS. + public static string Compile(string xcss, CascadiumOptions? options = null) + { + CascadiumOptions _options = options ?? new CascadiumOptions(); + return Parse(xcss, _options).Export(); + } + + /// + /// Compiles an Cascadium stylesheet to legacy CSS. + /// + /// The byte array including the Cascadium source code. + /// Optional. Options and parameters to the compiler. + /// The compiled UTF-8 CSS. + public static string Compile(ReadOnlySpan xcss, CascadiumOptions? options = null) + { + return Compile(xcss, Encoding.UTF8, options); + } + + /// + /// Compiles an Cascadium stylesheet to legacy CSS. + /// + /// The byte array including the Cascadium source code. + /// The encoder which will be used to decode the CSS output. + /// Optional. Options and parameters to the compiler. + /// The compiled CSS. + public static string Compile(ReadOnlySpan xcss, Encoding encoder, CascadiumOptions? options = null) + { + return Compile(encoder.GetString(xcss), options); } } \ No newline at end of file diff --git a/src/CascadiumLexer.csproj b/src/CascadiumLexer.csproj index 391a1d2..0fe2a0c 100644 --- a/src/CascadiumLexer.csproj +++ b/src/CascadiumLexer.csproj @@ -1,44 +1,10 @@ - - net6.0 - Cascadium - Cascadium.Core - disable - enable - True - True - - Cascadium Compiler - Cascadium.Compiler - - CypherPotato - Project Principium - Cascadium - Provides a CSS compiler that compiles higher-level code with single-line comments, nesting and more into a legacy CSS file. - README.md - https://github.com/CypherPotato/cascadium - css,scss,sass,less - git - - 0.2.0 - 0.2.0 - 0.2.0 - - en - True - LICENSE.txt - - - - - True - \ - - - True - \ - - - - \ No newline at end of file + + net6.0 + enable + enable + Cascadium + + + diff --git a/src/Compiler/Assembler.cs b/src/Compiler/Assembler.cs index 7192faf..1079042 100644 --- a/src/Compiler/Assembler.cs +++ b/src/Compiler/Assembler.cs @@ -36,11 +36,11 @@ public CssStylesheet AssemblyCss(FlatStylesheet flatStylesheet, CascadiumOptions { cssRule = new CssRule() { - Declarations = rule.Declarations, - Selector = rule.Selectors[0][0], - Order = ++ruleIndex + _declarations = rule.Declarations, + Selector = BuildCssSelector(rule.Selectors), + _order = ++ruleIndex }; - result.Rules.Add(cssRule); + result._rules.Add(cssRule); } else { @@ -49,29 +49,29 @@ public CssStylesheet AssemblyCss(FlatStylesheet flatStylesheet, CascadiumOptions cssRule = new CssRule() { - Declarations = rule.Declarations, + _declarations = rule.Declarations, Selector = selector, - Order = ++ruleIndex + _order = ++ruleIndex }; if (atRule != null) { CssStylesheet atRuleStylesheet = result.GetOrCreateStylesheet(atRule, options.Merge.HasFlag(MergeOption.AtRules)); - atRuleStylesheet.Rules.Add(cssRule); + atRuleStylesheet._rules.Add(cssRule); } else { - result.Rules.Add(cssRule); + result._rules.Add(cssRule); } } } - result.Statements.AddRange(flatStylesheet.Statements); + result._statements.AddRange(flatStylesheet.Statements); if (options.Merge != MergeOption.None) { Merge(result, options); - foreach(CssStylesheet subCss in result.Stylesheets) + foreach(CssStylesheet subCss in result._stylesheets) { Merge(subCss, options); } @@ -166,7 +166,7 @@ public void Merge(CssStylesheet stylesheet, CascadiumOptions options) { List newRules = new List(); - foreach (CssRule rule in stylesheet.Rules) + foreach (CssRule rule in stylesheet._rules) { CssRule? existingRule = newRules .FirstOrDefault(r => Helper.IsSelectorsEqual(r.Selector, rule.Selector)); @@ -177,20 +177,20 @@ public void Merge(CssStylesheet stylesheet, CascadiumOptions options) } else { - foreach (var prop in rule.Declarations) + foreach (var prop in rule._declarations) { - existingRule.Declarations[prop.Key] = prop.Value; + existingRule._declarations[prop.Key] = prop.Value; if (options.MergeOrderPriority == MergeOrderPriority.PreserveLast) { - if (rule.Order > existingRule.Order) - existingRule.Order = rule.Order; + if (rule._order > existingRule._order) + existingRule._order = rule._order; } } } } - stylesheet.Rules = newRules; + stylesheet._rules = newRules; } if (options.Merge.HasFlag(MergeOption.Declarations)) @@ -198,7 +198,7 @@ public void Merge(CssStylesheet stylesheet, CascadiumOptions options) // merge top-level only List newRules = new List(); - foreach (CssRule rule in stylesheet.Rules) + foreach (CssRule rule in stylesheet._rules) { CssRule? existingRule = newRules .FirstOrDefault(r => r.GetHashCode() == rule.GetHashCode()); @@ -213,7 +213,7 @@ public void Merge(CssStylesheet stylesheet, CascadiumOptions options) } } - stylesheet.Rules = newRules; + stylesheet._rules = newRules; } } } diff --git a/src/Compiler/Helper.cs b/src/Compiler/Helper.cs index 5c00763..e0250de 100644 --- a/src/Compiler/Helper.cs +++ b/src/Compiler/Helper.cs @@ -9,6 +9,55 @@ namespace Cascadium.Compiler; internal static class Helper { + public static int SafeCountIncidences(string value, char op) + { + int incidences = 0; + bool inSingleString = false; + bool inDoubleString = false; + int expressionIndex = 0, groupIndex = 0; + + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + char b = i > 0 ? value[i - 1] : '\0'; + + if (c == '\'' && b != '\\' && !inDoubleString) + { + inSingleString = !inSingleString; + } + else if (c == '"' && b != '\\' && !inSingleString) + { + inDoubleString = !inDoubleString; + } + else if (c == '(' && !(inDoubleString || inSingleString)) + { + expressionIndex++; + } + else if (c == ')' && !(inDoubleString || inSingleString)) + { + expressionIndex--; + } + else if (c == '[' && !(inDoubleString || inSingleString)) + { + groupIndex++; + } + else if (c == ']' && !(inDoubleString || inSingleString)) + { + groupIndex--; + } + + if ((inDoubleString || inSingleString) == false && expressionIndex == 0 && groupIndex == 0) + { + if (c == op) + { + incidences++; + } + } + } + + return incidences; + } + public static string[] SafeSplit(string? value, char op) { if (value == null) return Array.Empty(); diff --git a/src/Compiler/TextInterpreter.cs b/src/Compiler/TextInterpreter.cs index a49ca46..20c8940 100644 --- a/src/Compiler/TextInterpreter.cs +++ b/src/Compiler/TextInterpreter.cs @@ -1,5 +1,4 @@ -using System; -using System.Text; +using System.Text; using Cascadium.Object; namespace Cascadium.Compiler; diff --git a/src/Compiler/Tokenizer.cs b/src/Compiler/Tokenizer.cs index debcf18..901bafa 100644 --- a/src/Compiler/Tokenizer.cs +++ b/src/Compiler/Tokenizer.cs @@ -93,9 +93,13 @@ IEnumerable ReadCurrentDeclaration(string declaration) string property = declaration.Substring(0, dotPos).Trim(); string value = declaration.Substring(dotPos + 1).Trim(); - if (property == "") + if (!Token.IsValidPropertyName(property)) { - throw new CascadiumException(Interpreter.TakeSnapshot(declaration), "syntax error: property name cannot be empty"); + throw new CascadiumException(Interpreter.TakeSnapshot(declaration), "syntax error: invalid property name"); + } + else if (Token.IsPropertyValueUnescapedDoubleDots(value)) + { + throw new CascadiumException(Interpreter.TakeSnapshot(declaration), "syntax error: unclosed declaration"); } else { diff --git a/src/Entity/CssRule.cs b/src/Entity/CssRule.cs index 1a15c32..82e9c18 100644 --- a/src/Entity/CssRule.cs +++ b/src/Entity/CssRule.cs @@ -6,12 +6,28 @@ namespace Cascadium.Entity; -internal class CssRule +/// +/// Represents an CSS rule. +/// +public class CssRule { - public string Selector { get; set; } = ""; - public Dictionary Declarations { get; set; } = new Dictionary(); - public int Order { get; set; } = 0; + internal Dictionary _declarations { get; set; } = new Dictionary(); + internal int _order = 0; + /// + /// Gets the rule selector. + /// + public string Selector { get; internal set; } = ""; + + /// + /// Gets the declarations defined in this . + /// + public IDictionary Declarations { get => _declarations; } + + /// + /// Gets the hash code for this . + /// + /// A 32-bit signed integer hash code. public override int GetHashCode() { int n = 0, j = 1; @@ -26,12 +42,26 @@ public override int GetHashCode() // bar: foo // foo: bar - foreach (var kp in Declarations) + foreach (var kp in _declarations) { n += (kp.Key.GetHashCode() + kp.Value.GetHashCode()) / 2; n *= j; j++; } - return n / Declarations.Count; + return n / _declarations.Count; + } + + /// + /// Determines if the specified objects are and are equals. + /// + /// The another . + /// A boolean indicating if this object is equals to the other one. + public override bool Equals(object? obj) + { + if (obj is CssRule r) + { + return this.GetHashCode() == r.GetHashCode(); + } + return false; } } diff --git a/src/Entity/CssStylesheet.cs b/src/Entity/CssStylesheet.cs index 373697a..5e8bd5a 100644 --- a/src/Entity/CssStylesheet.cs +++ b/src/Entity/CssStylesheet.cs @@ -8,19 +8,57 @@ namespace Cascadium.Entity; -class CssStylesheet +/// +/// Represents an CSS stylesheet. +/// +public class CssStylesheet { - public string? AtRuleDeclaration { get; set; } - public List Statements { get; set; } = new List(); - public List Stylesheets { get; set; } = new List(); - public List Rules { get; set; } = new List(); + internal List _statements { get; set; } = new List(); + internal List _stylesheets { get; set; } = new List(); + internal List _rules { get; set; } = new List(); - public CssStylesheet GetOrCreateStylesheet(string atRuleDeclaration, bool canMerge) + /// + /// Gets the @-rule declaration that holds this stylesheet. + /// + public string? AtRuleDeclaration { get; internal set; } + + /// + /// Gets the individual statements of this stylesheet. + /// + public string[] Statements { get => _statements.ToArray(); } + + /// + /// Gets the children of this stylesheet. + /// + public CssStylesheet[] Stylesheets { get => _stylesheets.ToArray(); } + + /// + /// Gets an array of of this stylesheet. + /// + public CssRule[] Rules { get => _rules.ToArray(); } + + /// + /// Gets the used used to compile this CSS stylesheet. + /// + public CascadiumOptions Options { get; internal set; } = null!; + + /// + /// Exports this to an CSS representation string, using the + /// parameter. + /// + /// An CSS string. + public string Export() + { + return Export(Options); + } + + + internal CssStylesheet GetOrCreateStylesheet(string atRuleDeclaration, bool canMerge) { if (canMerge) { string sanitized = Helper.RemoveSpaces(atRuleDeclaration); - foreach (CssStylesheet subStylesheet in Stylesheets) + foreach (CssStylesheet subStylesheet in _stylesheets) { if (Helper.RemoveSpaces(subStylesheet.AtRuleDeclaration ?? "") == sanitized) { @@ -32,7 +70,7 @@ public CssStylesheet GetOrCreateStylesheet(string atRuleDeclaration, bool canMer { AtRuleDeclaration = atRuleDeclaration }; - Stylesheets.Add(newStylesheet); + _stylesheets.Add(newStylesheet); return newStylesheet; } else @@ -41,12 +79,12 @@ public CssStylesheet GetOrCreateStylesheet(string atRuleDeclaration, bool canMer { AtRuleDeclaration = atRuleDeclaration }; - Stylesheets.Add(newStylesheet); + _stylesheets.Add(newStylesheet); return newStylesheet; } } - public string Export(CascadiumOptions options) + string Export(CascadiumOptions options) { StringBuilder sb = new StringBuilder(); @@ -76,7 +114,7 @@ void ExportStylesheet(CssStylesheet css, int indentLevel) void ExportRules(CssStylesheet css, int indentLevel) { - foreach (var rule in css.Rules.OrderBy(r => r.Order)) + foreach (var rule in css._rules.OrderBy(r => r._order)) { if (options.Pretty) sb.Append(new string(' ', indentLevel * 4)); sb.Append(rule.Selector); @@ -84,7 +122,7 @@ void ExportRules(CssStylesheet css, int indentLevel) sb.Append('{'); if (options.Pretty) sb.Append('\n'); - foreach (KeyValuePair property in rule.Declarations) + foreach (KeyValuePair property in rule._declarations) { if (options.Pretty) sb.Append(new string(' ', (indentLevel + 1) * 4)); sb.Append(property.Key); @@ -103,17 +141,17 @@ void ExportRules(CssStylesheet css, int indentLevel) } } - foreach (string decl in Statements) + foreach (string decl in _statements) { sb.Append(decl); sb.Append(';'); if (options.Pretty) sb.AppendLine(); } - if (options.Pretty && Statements.Count > 0) sb.AppendLine(); + if (options.Pretty && _statements.Count > 0) sb.AppendLine(); ExportRules(this, 0); - foreach (CssStylesheet stylesheet in Stylesheets) + foreach (CssStylesheet stylesheet in _stylesheets) { ExportStylesheet(stylesheet, 0); } diff --git a/src/Extensions/Converter.cs b/src/Extensions/Converter.cs index e2b6e50..4a82b6e 100644 --- a/src/Extensions/Converter.cs +++ b/src/Extensions/Converter.cs @@ -13,9 +13,9 @@ internal class Converter { public static void ConvertAll(CssStylesheet css, CascadiumOptions options) { - foreach (var rule in css.Rules) + foreach (var rule in css._rules) { - foreach (KeyValuePair declaration in rule.Declarations.ToArray()) + foreach (KeyValuePair declaration in rule._declarations.ToArray()) { foreach (CSSConverter converter in options.Converters) { @@ -23,19 +23,19 @@ public static void ConvertAll(CssStylesheet css, CascadiumOptions options) { NameValueCollection output = new NameValueCollection(); converter.Convert(declaration.Value, output); - rule.Declarations.Remove(declaration.Key); + rule._declarations.Remove(declaration.Key); foreach (string nprop in output) { string? value = output[nprop]; if (string.IsNullOrEmpty(value)) continue; - rule.Declarations[nprop] = value; + rule._declarations[nprop] = value; } } } } } - foreach (var subcss in css.Stylesheets) + foreach (var subcss in css._stylesheets) { ConvertAll(subcss, options); } diff --git a/src/Extensions/MediaRewriter.cs b/src/Extensions/MediaRewriter.cs index 25c5506..4540fb3 100644 --- a/src/Extensions/MediaRewriter.cs +++ b/src/Extensions/MediaRewriter.cs @@ -12,7 +12,7 @@ internal class MediaRewriter { public static void ApplyRewrites(CssStylesheet cssStylesheet, CascadiumOptions options) { - foreach (var subcss in cssStylesheet.Stylesheets) + foreach (var subcss in cssStylesheet._stylesheets) { if (subcss.AtRuleDeclaration == null) continue; foreach (string rewrite in options.AtRulesRewrites) diff --git a/src/Extensions/ValueHandler.cs b/src/Extensions/ValueHandler.cs index 42240fa..2fdc984 100644 --- a/src/Extensions/ValueHandler.cs +++ b/src/Extensions/ValueHandler.cs @@ -12,20 +12,20 @@ internal class ValueHandler { public static void TransformVarShortcuts(CssStylesheet css) { - foreach (var rule in css.Rules) + foreach (var rule in css._rules) { - foreach(string key in rule.Declarations.Keys) + foreach (string key in rule._declarations.Keys) { - rule.Declarations[key] = ApplyVarShortcuts(rule.Declarations[key]); + rule._declarations[key] = ApplyVarShortcuts(rule._declarations[key], css); } } - foreach (var subcss in css.Stylesheets) + foreach (var subcss in css._stylesheets) { TransformVarShortcuts(subcss); } } - static string ApplyVarShortcuts(string value) + static string ApplyVarShortcuts(string value, CssStylesheet stylesheet) { StringBuilder output = new StringBuilder(); char[] chars = value.ToCharArray(); @@ -37,6 +37,7 @@ static string ApplyVarShortcuts(string value) { char c = chars[i]; char b = i > 0 ? chars[i - 1] : '\0'; + char n = i < chars.Length - 1 ? chars[i + 1] : '\0'; output.Append(c); @@ -51,7 +52,8 @@ static string ApplyVarShortcuts(string value) if ((inSingleString || inDoubleString) == false) { - if (c == '-' && b == '-' && output.Length >= 2 && !output.ToString().EndsWith("var(--")) + string tmpOut = output.ToString(); + if (c == '-' && b == '-' && n != '-' && output.Length >= 2 && !tmpOut.EndsWith("var(--") && !tmpOut.EndsWith("---")) { isParsingVarname = true; output.Length -= 2; diff --git a/src/Object/Token.cs b/src/Object/Token.cs index d609e62..9c85d42 100644 --- a/src/Object/Token.cs +++ b/src/Object/Token.cs @@ -32,6 +32,17 @@ struct Token public static readonly char Ch_Semicolon = ';'; public static readonly char Ch_DoubleDots = ':'; + public static bool IsPropertyValueUnescapedDoubleDots(string propertyValue) + { + return Helper.SafeCountIncidences(propertyValue, ':') > 0; + } + + public static bool IsValidPropertyName(string propertyName) + { + if (propertyName.Length == 0) return false; + return propertyName.All(c => char.IsLetterOrDigit(c) || c == '-' || c == '$' || c == '%'); // $ and % is an special token for converters + } + public static bool IsIdentifierChr(char c) { return Char.IsLetter(c) || Char.IsDigit(c) || c == '_' || c == '-'; diff --git a/tool/Cascadium-Utility.csproj b/tool/Cascadium-Utility.csproj index af593ad..433475e 100644 --- a/tool/Cascadium-Utility.csproj +++ b/tool/Cascadium-Utility.csproj @@ -7,7 +7,7 @@ enable cascadium true - + 0.1.2.4 0.1.2.4 cascadiumtool @@ -18,7 +18,7 @@ - + diff --git a/tool/Compiler.cs b/tool/Compiler.cs index e0ad437..4d4919e 100644 --- a/tool/Compiler.cs +++ b/tool/Compiler.cs @@ -1,6 +1,7 @@ using Cascadium; using Microsoft.VisualBasic; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -9,14 +10,17 @@ using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; namespace cascadiumtool; internal class Compiler { - public static int RunCompiler(CommandLineArguments args) + public static async Task RunCompiler(CommandLineArguments args) { + Stopwatch sw = new Stopwatch(); + sw.Start(); string? stdin = null; bool anyCompiled = false; @@ -99,47 +103,55 @@ public static int RunCompiler(CommandLineArguments args) if (inputFiles.Count > 0) { anyCompiled = true; - StringBuilder resultCss = new StringBuilder(); - - long compiledLength = 0, totalLength = 0; + long compiledLength = 0; int smallInputLength = inputFiles.Select(Path.GetDirectoryName).Min(i => i?.Length ?? 0); + ConcurrentBag resultCss = new ConcurrentBag(); - foreach (string file in inputFiles) - { - string contents = ReadFile(file); - string result; - totalLength += contents.Length; - - try - { - result = CascadiumCompiler.Compile(contents, options); - compiledLength += result.Length; + CancellationTokenSource errorCanceller = new CancellationTokenSource(); - if (options.Pretty) + try + { + await Parallel.ForEachAsync(inputFiles, + new ParallelOptions() { - resultCss.AppendLine(result + "\n"); - } - else + MaxDegreeOfParallelism = 4, + CancellationToken = errorCanceller.Token + }, (file, ct) => { - resultCss.Append(result); - } - } - catch (CascadiumException cex) - { - Console.WriteLine($"error at file {file.Substring(smallInputLength + 1)}, line {cex.Line}, col. {cex.Column}:"); - Console.WriteLine(); - Console.WriteLine($"\t{cex.LineText}"); - Console.WriteLine($"\t{new string(' ', cex.Column - 1)}^"); - Console.WriteLine($"\t{cex.Message}"); - return 5; - } + if (ct.IsCancellationRequested) + return ValueTask.FromCanceled(ct); + + try + { + string result = CompileFile(file, options); + Interlocked.Add(ref compiledLength, result.Length); + resultCss.Add(result); + } + catch (CascadiumException cex) + { + Console.WriteLine($"error at file {file.Substring(smallInputLength + 1)}, line {cex.Line}, col. {cex.Column}:"); + Console.WriteLine(); + Console.WriteLine($"\t{cex.LineText}"); + Console.WriteLine($"\t{new string(' ', cex.Column - 1)}^"); + Console.WriteLine($"\t{cex.Message}"); + errorCanceller.Cancel(); + } + + return ValueTask.CompletedTask; + }); + } + catch (TaskCanceledException) + { + ; } if (outputFile != null) { - File.WriteAllText(outputFile, resultCss.ToString()); + string css = string.Join(options.Pretty ? "\n" : "", resultCss); + File.WriteAllText(outputFile, css); + compiledLength = new FileInfo(outputFile).Length; - Log.Info($"{inputFiles.Count} file(s) -> {Path.GetFileName(args.OutputFile)} [{PathUtils.FileSize(totalLength)} -> {PathUtils.FileSize(compiledLength)}]"); + Log.Info($"{inputFiles.Count} file(s) -> {Path.GetFileName(args.OutputFile)} ({PathUtils.FileSize(compiledLength)}) in {sw.ElapsedMilliseconds:N0}ms"); } else { @@ -156,8 +168,17 @@ public static int RunCompiler(CommandLineArguments args) return 0; } - static string ReadFile(string file) + static string CompileFile(string file, CascadiumOptions options) { - return System.IO.File.ReadAllText(file); + string result; + + lock (Program.CompilerCache) + if (!Program.CompilerCache.TryGetValue(file, out result!)) + { + result = CascadiumCompiler.Compile(File.ReadAllText(file), options); + Program.CompilerCache.Add(file, result); + } + + return result; } } diff --git a/tool/Log.cs b/tool/Log.cs index a829c26..c515983 100644 --- a/tool/Log.cs +++ b/tool/Log.cs @@ -14,7 +14,7 @@ public static int ErrorKill(string message) Write("error", message); return 1; } - public static int Info(string message, bool force=false) + public static int Info(string message, bool force = false) { if (!force && !LoggingEnabled) return 0; Write("info", message); diff --git a/tool/Program.cs b/tool/Program.cs index 1d60b95..b89a14c 100644 --- a/tool/Program.cs +++ b/tool/Program.cs @@ -2,18 +2,20 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading.Tasks; namespace cascadiumtool; internal class Program { - public const string VersionLabel = "v.0.2-alpha-4"; - public static string CurrentDirectory { get; } = Directory.GetCurrentDirectory(); + public const string VersionLabel = "v.0.3"; + public static string CurrentDirectory { get; set; } = Directory.GetCurrentDirectory(); public static bool HasRootConfiguration { get; private set; } public static JsonCssCompilerOptions? CompilerOptions { get; set; } + public static Dictionary CompilerCache { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CommandLineArguments))] - static int Main(string[] args) + static async Task Main(string[] args) { CommandLineParser.TryParse(args, out var result, out var errors); @@ -21,29 +23,31 @@ static int Main(string[] args) { CommandLineParser.PrintHelp($"Cascadium [{VersionLabel}]", "Distributed under MIT License", errors); return 0; - } else + } + else { - RunParsed(result, out int errorcode); - return errorcode; + return await RunParsed(result); } } - public static void RunParsed(CommandLineArguments args, out int errorcode) + public static async Task RunParsed(CommandLineArguments args) { if (args.ConfigFile != null) { - CompilerOptions = JsonCssCompilerOptions.Create(args.ConfigFile); + string fullPath = PathUtils.ResolvePath(args.ConfigFile); + CompilerOptions = JsonCssCompilerOptions.Create(fullPath); + Program.CurrentDirectory = Path.GetDirectoryName(fullPath)!; } args.Import(CompilerOptions); if (args.Watch) { - errorcode = Watcher.Watch(args); + return await Watcher.Watch(args); } else { - errorcode = Compiler.RunCompiler(args); + return await Compiler.RunCompiler(args); } } } diff --git a/tool/Watcher.cs b/tool/Watcher.cs index cbf6e07..8be61ee 100644 --- a/tool/Watcher.cs +++ b/tool/Watcher.cs @@ -14,8 +14,9 @@ internal static class Watcher private static FileSystemWatcher fsWatcher = new FileSystemWatcher(); private static CommandLineArguments watchArgs = null!; private static string[] watchingDirectories = Array.Empty(); + private static bool IsCompilingFile = false; - public static int Watch(CommandLineArguments args) + public static async Task Watch(CommandLineArguments args) { watchArgs = args; HashSet paths = new HashSet(); @@ -40,7 +41,7 @@ public static int Watch(CommandLineArguments args) watchingDirectories = paths.ToArray(); foreach (string p in paths) { - if(!Directory.Exists(p)) + if (!Directory.Exists(p)) { return Log.ErrorKill("the detected directory path at " + p + " does not exists."); } @@ -58,20 +59,25 @@ public static int Watch(CommandLineArguments args) fsWatcher.IncludeSubdirectories = true; fsWatcher.EnableRaisingEvents = true; + await Compiler.RunCompiler(args); Log.Info("cascadium is watching for file changes"); - Log.LoggingEnabled = false; - - Compiler.RunCompiler(args); + //Log.LoggingEnabled = false; Thread.Sleep(-1); return 0; } - [MethodImpl(MethodImplOptions.Synchronized)] - private static void FsWatcher_Changed(Object sender, FileSystemEventArgs e) + private static async void FsWatcher_Changed(Object sender, FileSystemEventArgs e) { + if (IsCompilingFile) + { + return; + } + + IsCompilingFile = true; + string outFile = PathUtils.ResolvePath(watchArgs.OutputFile); - if(outFile == e.FullPath) + if (outFile == e.FullPath) { // avoid compiling the out file return; @@ -87,12 +93,17 @@ private static void FsWatcher_Changed(Object sender, FileSystemEventArgs e) foreach (string includedDir in watchingDirectories) isDirIncluded |= file.StartsWith(includedDir); + if (!isDirIncluded) return; + Program.CompilerCache.Remove(file); + try { - Compiler.RunCompiler(watchArgs); + Thread.Sleep(300); // prevent the below error giving time to the time to write + await Compiler.RunCompiler(watchArgs); + ; } catch (System.IO.IOException) { @@ -101,7 +112,8 @@ private static void FsWatcher_Changed(Object sender, FileSystemEventArgs e) } finally { - Thread.Sleep(500); + IsCompilingFile = false; + Thread.Sleep(150); } } } diff --git a/tool/etc/build.bat b/tool/etc/build.bat index 8f3283b..da45f34 100644 --- a/tool/etc/build.bat +++ b/tool/etc/build.bat @@ -12,4 +12,4 @@ dotnet publish "%~dp0/../Cascadium-Utility.csproj" --nologo ^ -p:PublishReadyToRun=true ^ -p:PublishTrimmed=true ^ -p:PublishSingleFile=true ^ - -o "%~dp0bin/Build/%NAME%/" \ No newline at end of file + -o "%~dp0bin/Build/cascadium-v0.3-%NAME%/" \ No newline at end of file