From 69a74a8b050858010cf56cbe379efba1a059b61b Mon Sep 17 00:00:00 2001 From: CypherPotato Date: Thu, 25 Jan 2024 18:16:01 -0300 Subject: [PATCH] v0.2, new lexer and interpreter --- src/CascadiumCompiler.cs | 48 ++- src/CascadiumException.cs | 22 ++ ...Cascadium.csproj => CascadiumLexer.csproj} | 10 +- src/CascadiumOptions.cs | 4 + src/Changelog.md | 5 - src/Compiler/Assembler.cs | 219 +++++++++++ src/Compiler/CompilerContext.cs | 41 --- src/Compiler/CompilerModule.cs | 19 - src/Compiler/Exporter.cs | 102 ------ src/Compiler/Flattener.cs | 49 +++ src/Compiler/Helper.cs | 172 +++++++++ src/Compiler/Merger.cs | 100 ----- src/Compiler/Parser.cs | 346 ++++-------------- src/Compiler/Preparers.cs | 227 ------------ src/Compiler/Sanitizer.cs | 67 ++++ src/Compiler/Split.cs | 69 ---- src/Compiler/TextInterpreter.cs | 181 +++++++++ src/Compiler/Tokenizer.cs | 122 ++++++ src/Compiler/Utils.cs | 31 -- src/Compiler/_README.txt | 10 + src/Converters/CSSConverter.cs | 7 +- src/Converters/StaticCSSConverter.cs | 1 - src/Entity/CssRule.cs | 37 ++ src/Entity/CssStylesheet.cs | 123 +++++++ src/Entity/FlatRule.cs | 40 ++ src/Entity/FlatStylesheet.cs | 13 + src/Entity/IRuleContainer.cs | 12 + src/Entity/NestedRule.cs | 16 + src/Entity/NestedStylesheet.cs | 13 + src/Extensions/Converter.cs | 43 +++ src/Extensions/MediaRewriter.cs | 27 ++ src/Extensions/ValueHandler.cs | 75 ++++ src/Object/Token.cs | 74 ++++ src/Object/TokenCollection.cs | 52 +++ src/Rule.cs | 38 -- tool/Cascadium-Utility.csproj | 8 +- tool/CommandLineArguments.cs | 6 - tool/Compiler.cs | 49 ++- tool/JsonCompilerOptions.cs | 50 +-- tool/Program.cs | 2 +- tool/etc/build.bat | 10 +- 41 files changed, 1569 insertions(+), 971 deletions(-) create mode 100644 src/CascadiumException.cs rename src/{Cascadium.csproj => CascadiumLexer.csproj} (89%) delete mode 100644 src/Changelog.md create mode 100644 src/Compiler/Assembler.cs delete mode 100644 src/Compiler/CompilerContext.cs delete mode 100644 src/Compiler/CompilerModule.cs delete mode 100644 src/Compiler/Exporter.cs create mode 100644 src/Compiler/Flattener.cs create mode 100644 src/Compiler/Helper.cs delete mode 100644 src/Compiler/Merger.cs delete mode 100644 src/Compiler/Preparers.cs create mode 100644 src/Compiler/Sanitizer.cs delete mode 100644 src/Compiler/Split.cs create mode 100644 src/Compiler/TextInterpreter.cs create mode 100644 src/Compiler/Tokenizer.cs delete mode 100644 src/Compiler/Utils.cs create mode 100644 src/Compiler/_README.txt create mode 100644 src/Entity/CssRule.cs create mode 100644 src/Entity/CssStylesheet.cs create mode 100644 src/Entity/FlatRule.cs create mode 100644 src/Entity/FlatStylesheet.cs create mode 100644 src/Entity/IRuleContainer.cs create mode 100644 src/Entity/NestedRule.cs create mode 100644 src/Entity/NestedStylesheet.cs create mode 100644 src/Extensions/Converter.cs create mode 100644 src/Extensions/MediaRewriter.cs create mode 100644 src/Extensions/ValueHandler.cs create mode 100644 src/Object/Token.cs create mode 100644 src/Object/TokenCollection.cs delete mode 100644 src/Rule.cs diff --git a/src/CascadiumCompiler.cs b/src/CascadiumCompiler.cs index 66aa38d..a96d233 100644 --- a/src/CascadiumCompiler.cs +++ b/src/CascadiumCompiler.cs @@ -1,5 +1,12 @@ -using System; +using Cascadium.Compiler; +using Cascadium.Entity; +using Cascadium.Extensions; +using Cascadium.Object; +using System; +using System.Collections.Generic; +using System.Linq; using System.Text; +using System.Threading.Tasks; namespace Cascadium; @@ -8,10 +15,6 @@ namespace Cascadium; /// public sealed partial class CascadiumCompiler { - internal static char[] combinators = new[] { '>', '~', '+' }; - - private CascadiumCompiler() { } - /// /// Compiles a top-module CSS stylesheet to legacy CSS, with the code already minified. /// @@ -32,12 +35,33 @@ public static string Compile(ReadOnlySpan xcss, Encoding encoder, Cascadiu /// The compiled and minified CSS. public static string Compile(string xcss, CascadiumOptions? options = null) { - var context = new Compiler.CompilerContext() - { - Options = options - }; - string prepared = context.Preparers.PrepareCssInput(xcss); - context.Parser.ParseXCss(prepared); - return context.Exporter.Export().Trim(); + CascadiumOptions _options = options ?? new CascadiumOptions(); + + //// strip comments and trim the input + string sanitizedInput = Sanitizer.SanitizeInput(xcss); + + //// tokenizes the sanitized input, which is the lexical analyzer + //// and convert the code into tokens + TokenCollection resultTokens = new Tokenizer(sanitizedInput).Tokenize(); + + //// parses the produced tokens and produce an nested stylesheet from + //// the token. this also applies the semantic and syntax checking + NestedStylesheet nestedStylesheet = Parser.ParseSpreadsheet(resultTokens); + + //// flatten the stylesheet, which removes the nesting and places all + //// rules in the top level of the stylesheet. + FlatStylesheet flattenStylesheet = Flattener.FlattenStylesheet(nestedStylesheet); + + //// build the css body, assembling the flat rules into an valid + //// css string + CssStylesheet css = new Assembler(_options).AssemblyCss(flattenStylesheet, _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); } } \ No newline at end of file diff --git a/src/CascadiumException.cs b/src/CascadiumException.cs new file mode 100644 index 0000000..bd0b3da --- /dev/null +++ b/src/CascadiumException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Cascadium.Object; + +namespace Cascadium; + +public class CascadiumException : Exception +{ + public int Line { get; private set; } + public int Column { get; private set; } + public string LineText { get; private set; } + + internal CascadiumException(TokenDebugInfo snapshot, string message) : base(message) + { + Line = snapshot.Line; + Column = snapshot.Column; + LineText = snapshot.LineText; + } +} diff --git a/src/Cascadium.csproj b/src/CascadiumLexer.csproj similarity index 89% rename from src/Cascadium.csproj rename to src/CascadiumLexer.csproj index 732fcbd..391a1d2 100644 --- a/src/Cascadium.csproj +++ b/src/CascadiumLexer.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -21,9 +21,9 @@ css,scss,sass,less git - 0.1.2 - 0.1.2 - 0.1.2 + 0.2.0 + 0.2.0 + 0.2.0 en True @@ -41,4 +41,4 @@ - + \ No newline at end of file diff --git a/src/CascadiumOptions.cs b/src/CascadiumOptions.cs index f991b35..489d5d3 100644 --- a/src/CascadiumOptions.cs +++ b/src/CascadiumOptions.cs @@ -2,6 +2,9 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace Cascadium; @@ -47,6 +50,7 @@ public class CascadiumOptions public MergeOrderPriority MergeOrderPriority { get; set; } = MergeOrderPriority.PreserveLast; } + /// /// Represents the order priority used at an , only appliable to . /// diff --git a/src/Changelog.md b/src/Changelog.md deleted file mode 100644 index 9d2a748..0000000 --- a/src/Changelog.md +++ /dev/null @@ -1,5 +0,0 @@ -## v.0.1.0 - -Project renamed from SimpleCSSCompiler to Cascadium. - -The project is taking shape and becoming more stable over time. Documentation, an RFC and more information should come soon. \ No newline at end of file diff --git a/src/Compiler/Assembler.cs b/src/Compiler/Assembler.cs new file mode 100644 index 0000000..7192faf --- /dev/null +++ b/src/Compiler/Assembler.cs @@ -0,0 +1,219 @@ +using Cascadium.Entity; +using Cascadium.Object; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Compiler; + +class Assembler +{ + public CascadiumOptions Options { get; set; } + + public Assembler(CascadiumOptions options) + { + Options = options; + } + + public CssStylesheet AssemblyCss(FlatStylesheet flatStylesheet, CascadiumOptions options) + { + CssStylesheet result = new CssStylesheet(); + int ruleIndex = 0; + + foreach (FlatRule rule in flatStylesheet.Rules) + { + if (rule.Declarations.Count == 0) + { + // skip empty rules + continue; + } + + CssRule cssRule; + if (rule.IsRuleAtRule()) // checks if the rule is an @ rule, like @font-face + { + cssRule = new CssRule() + { + Declarations = rule.Declarations, + Selector = rule.Selectors[0][0], + Order = ++ruleIndex + }; + result.Rules.Add(cssRule); + } + else + { + string? atRule = rule.PopAtRule(); + string selector = BuildCssSelector(rule.Selectors); + + cssRule = new CssRule() + { + Declarations = rule.Declarations, + Selector = selector, + Order = ++ruleIndex + }; + + if (atRule != null) + { + CssStylesheet atRuleStylesheet = result.GetOrCreateStylesheet(atRule, options.Merge.HasFlag(MergeOption.AtRules)); + atRuleStylesheet.Rules.Add(cssRule); + } + else + { + result.Rules.Add(cssRule); + } + } + } + + result.Statements.AddRange(flatStylesheet.Statements); + + if (options.Merge != MergeOption.None) + { + Merge(result, options); + foreach(CssStylesheet subCss in result.Stylesheets) + { + Merge(subCss, options); + } + } + + return result; + } + + string BuildCssSelector(IList selectors) + { + int flatCount = selectors.Count; + + if (flatCount == 0) + { + return ""; + } + if (flatCount == 1) + { + return BuildCssSelector(selectors[0], Array.Empty()); + } + else + { + string carry = BuildCssSelector(selectors[0], Array.Empty()); + for (int i = 1; i < flatCount; i++) + { + string[] current = selectors[i]; + if (current.Length == 0) continue; + carry = BuildCssSelector(current, Helper.SafeSplit(carry, ',')); + } + return carry; + } + } + + string BuildCssSelector(string[] cSelectors, string[] bSelectors) + { + StringBuilder sb = new StringBuilder(); + if (bSelectors.Length == 0) + { + foreach (string cSelector in cSelectors) + { + string prepared = Helper.PrepareSelectorUnit(cSelector, Options.KeepNestingSpace, Options.Pretty); + sb.Append(prepared); + sb.Append(','); + if (Options.Pretty) + sb.Append(' '); + } + goto finish; + } + foreach (string C in cSelectors) + { + string c = C.Trim(); + foreach (string B in bSelectors) + { + string b = B.Trim(); + string s; + if (c.StartsWith('&')) + { + sb.Append(b); + s = c.Substring(1); + if (!Options.KeepNestingSpace) + { + s = s.TrimStart(); + } + } + else + { + sb.Append(b); + if (c.Length != 0 && !Token.Sel_Combinators.Contains(c[0])) + { + sb.Append(' '); + } + s = c; + } + + s = Helper.PrepareSelectorUnit(s, Options.KeepNestingSpace, Options.Pretty); + sb.Append(s); + sb.Append(','); + if (Options.Pretty) + sb.Append(' '); + } + } + + finish: + if (sb.Length > 0) sb.Length--; + if (sb.Length > 0 && Options.Pretty) sb.Length--; + return sb.ToString(); + } + + public void Merge(CssStylesheet stylesheet, CascadiumOptions options) + { + if (options.Merge.HasFlag(MergeOption.Selectors)) + { + List newRules = new List(); + + foreach (CssRule rule in stylesheet.Rules) + { + CssRule? existingRule = newRules + .FirstOrDefault(r => Helper.IsSelectorsEqual(r.Selector, rule.Selector)); + + if (existingRule == null) + { + newRules.Add(rule); + } + else + { + foreach (var prop in rule.Declarations) + { + existingRule.Declarations[prop.Key] = prop.Value; + + if (options.MergeOrderPriority == MergeOrderPriority.PreserveLast) + { + if (rule.Order > existingRule.Order) + existingRule.Order = rule.Order; + } + } + } + } + + stylesheet.Rules = newRules; + } + + if (options.Merge.HasFlag(MergeOption.Declarations)) + { + // merge top-level only + List newRules = new List(); + + foreach (CssRule rule in stylesheet.Rules) + { + CssRule? existingRule = newRules + .FirstOrDefault(r => r.GetHashCode() == rule.GetHashCode()); + + if (existingRule == null) + { + newRules.Add(rule); + } + else + { + existingRule.Selector = Helper.CombineSelectors(existingRule.Selector, rule.Selector, options.Pretty); + } + } + + stylesheet.Rules = newRules; + } + } +} diff --git a/src/Compiler/CompilerContext.cs b/src/Compiler/CompilerContext.cs deleted file mode 100644 index 192a47b..0000000 --- a/src/Compiler/CompilerContext.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cascadium.Compiler; - -internal class CompilerContext -{ - public string? AtRule { get; set; } - public List Rules { get; set; } = new List(); - public List Childrens { get; set; } = new List(); - public List Declarations { get; set; } = new List(); - public int StackOrder { get; set; } = 0; - public CascadiumOptions? Options { get; set; } - - public readonly Merger Merger; - public readonly Utils Utils; - public readonly Exporter Exporter; - public readonly Parser Parser; - public readonly Preparers Preparers; - public readonly Split Split; - - public CompilerContext() - { - Merger = new Merger(this); - Utils = new Utils(this); - Exporter = new Exporter(this); - Parser = new Parser(this); - Preparers = new Preparers(this); - Split = new Split(this); - } - - internal CascadiumOptions EnsureOptionsNotNull() - { - if (Options == null) - Options = new CascadiumOptions(); - return Options; - } -} diff --git a/src/Compiler/CompilerModule.cs b/src/Compiler/CompilerModule.cs deleted file mode 100644 index df5183f..0000000 --- a/src/Compiler/CompilerModule.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cascadium.Compiler; - -internal abstract class CompilerModule -{ - public CompilerContext Context { get; private set; } - - public CascadiumOptions? Options { get => Context.Options; } - - public CompilerModule(CompilerContext context) - { - this.Context = context; - } -} diff --git a/src/Compiler/Exporter.cs b/src/Compiler/Exporter.cs deleted file mode 100644 index 3f77590..0000000 --- a/src/Compiler/Exporter.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml.Linq; - -namespace Cascadium.Compiler; -internal class Exporter : CompilerModule -{ - public Exporter(CompilerContext context) : base(context) - { - } - - public string Export() - { - StringBuilder sb = new StringBuilder(); - - if (this.Options?.Merge != MergeOption.None) - { - this.Context.Merger.Merge( - mergeSelectors: this.Options?.Merge.HasFlag(MergeOption.Selectors) == true, - mergeAtRules: this.Options?.Merge.HasFlag(MergeOption.AtRules) == true, - mergeDeclarations: this.Options?.Merge.HasFlag(MergeOption.Declarations) == true - ); - } - - foreach (string decl in Context.Declarations) - { - sb.Append(decl); - if (Options?.Pretty == true) sb.Append("\n"); - } - if (Options?.Pretty == true) sb.Append("\n"); - - ExportRules(sb, this.Context, 0); - - foreach (CompilerContext stylesheet in Context.Childrens) - { - stylesheet.Options = this.Options; - ExportStylesheet(sb, stylesheet, 0); - } - - if (Options?.Pretty == true) this.Context.Utils.TrimEndSb(sb); - return sb.ToString(); - } - - public void ExportStylesheet(StringBuilder sb, CompilerContext css, int indentLevel) - { - if (css.AtRule != "") - { - if (css.Options?.Pretty == true) sb.Append(new string(' ', indentLevel * 4)); - sb.Append(css.AtRule?.Trim()); - if (css.Options?.Pretty == true) sb.Append(' '); - sb.Append('{'); - if (css.Options?.Pretty == true) sb.Append('\n'); - } - foreach (string decl in css.Declarations) - { - if (css.Options?.Pretty == true) sb.Append(new string(' ', indentLevel + 1 * 4)); - sb.Append(decl); - if (css.Options?.Pretty == true) sb.Append("\n"); - } - ExportRules(sb, css, indentLevel + 1); - if (css.Options?.Pretty == true) this.Context.Utils.TrimEndSb(sb); - if (css.AtRule != "") - { - if (css.Options?.Pretty == true) sb.Append('\n'); - if (css.Options?.Pretty == true) sb.Append(new string(' ', indentLevel * 4)); - sb.Append('}'); - if (css.Options?.Pretty == true) sb.Append("\n\n"); - } - } - - private void ExportRules(StringBuilder sb, CompilerContext css, int indentLevel) - { - foreach (var rule in css.Rules.Where(r => r.Exported).OrderBy(r => r.Order)) - { - if (css.Options?.Pretty == true) sb.Append(new string(' ', indentLevel * 4)); - sb.Append(rule.Selector); - if (css.Options?.Pretty == true) sb.Append(' '); - sb.Append('{'); - if (css.Options?.Pretty == true) sb.Append('\n'); - - foreach (KeyValuePair property in rule.Properties) - { - if (css.Options?.Pretty == true) sb.Append(new string(' ', (indentLevel + 1) * 4)); - sb.Append(property.Key); - sb.Append(':'); - if (css.Options?.Pretty == true) sb.Append(' '); - sb.Append(property.Value); - sb.Append(';'); - if (css.Options?.Pretty == true) sb.Append('\n'); - } - - sb.Length--; // remove the last ; - if (css.Options?.Pretty == true) sb.Append('\n'); - if (css.Options?.Pretty == true) sb.Append(new string(' ', indentLevel * 4)); - sb.Append('}'); - if (css.Options?.Pretty == true) sb.Append("\n\n"); - } - } -} diff --git a/src/Compiler/Flattener.cs b/src/Compiler/Flattener.cs new file mode 100644 index 0000000..adbefce --- /dev/null +++ b/src/Compiler/Flattener.cs @@ -0,0 +1,49 @@ +using Cascadium.Entity; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Compiler; + +internal class Flattener +{ + public static FlatStylesheet FlattenStylesheet(NestedStylesheet nestedStylesheet) + { + FlatStylesheet output = new FlatStylesheet(); + + void CreateRules(NestedRule rule, IEnumerable parents) + { + List selectors = new List(); + + foreach (NestedRule parent in parents) + { + selectors.Add(parent.Selectors.ToArray()); + } + + selectors.Add(rule.Selectors.ToArray()); + + var frule = new FlatRule() + { + Selectors = selectors, + Declarations = rule.Declarations + }; + output.Rules.Add(frule); + + foreach (NestedRule r in rule.Rules) + { + CreateRules(r, parents.Concat(new[] { rule })); + } + } + + foreach (NestedRule r in nestedStylesheet.Rules) + { + CreateRules(r, Array.Empty()); + } + + output.Statements.AddRange(nestedStylesheet.Statements); + + return output; + } +} diff --git a/src/Compiler/Helper.cs b/src/Compiler/Helper.cs new file mode 100644 index 0000000..5c00763 --- /dev/null +++ b/src/Compiler/Helper.cs @@ -0,0 +1,172 @@ +using Cascadium.Object; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Compiler; + +internal static class Helper +{ + public static string[] SafeSplit(string? value, char op) + { + if (value == null) return Array.Empty(); + List output = new List(); + StringBuilder mounting = new StringBuilder(); + 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'; + mounting.Append(c); + + 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) + { + mounting.Length--; + output.Add(mounting.ToString()); + mounting.Clear(); + } + } + } + + if (mounting.Length > 0) + output.Add(mounting.ToString()); + + return output.ToArray(); + } + + public static bool InvariantCompare(string? a, string? b) + { + return string.Compare(RemoveSpaces(a ?? ""), RemoveSpaces(b ?? "")) == 0; + } + + public static string RemoveSpaces(string s) + => new String(s.ToCharArray().Where(c => !char.IsWhiteSpace(c)).ToArray()); + + public static bool IsSelectorsEqual(string? a, string? b) + { + if (a == null || b == null) return a == b; + return PrepareSelectorUnit(a) == PrepareSelectorUnit(b); + } + + public static string PrepareSelectorUnit(string s, bool keepNestingSpace = false, bool pretty = false) + { + StringBuilder output = new StringBuilder(); + char[] chars; + + bool ignoreTrailingSpace = keepNestingSpace == false; + + if (ignoreTrailingSpace) + { + chars = s.Trim().ToCharArray(); + } + else + { + chars = s.TrimEnd().ToCharArray(); + } + + bool inSingleString = false; + bool inDoubleString = false; + char lastCombinator = '\0'; + + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + char b = i > 0 ? chars[i - 1] : '\0'; + + if (c == '\'' && b != '\\' && !inDoubleString) + { + inSingleString = !inSingleString; + } + else if (c == '"' && b != '\\' && !inSingleString) + { + inDoubleString = !inDoubleString; + } + + if (inDoubleString || inSingleString) + { + output.Append(c); + continue; + } + + if (i == 0 && ignoreTrailingSpace && char.IsWhiteSpace(c)) + { + output.Append(c); + } + + if (Token.Sel_Combinators.Contains(c) || char.IsWhiteSpace(c)) + { + if (char.IsWhiteSpace(lastCombinator) || lastCombinator == '\0') + lastCombinator = c; + } + else + { + bool prettySpace = pretty == true && !char.IsWhiteSpace(lastCombinator) && lastCombinator != '\0'; + if (prettySpace) output.Append(' '); + + if (lastCombinator != '\0') + output.Append(lastCombinator); + + if (prettySpace) output.Append(' '); + output.Append(c); + lastCombinator = '\0'; + } + } + + return output.ToString(); + } + + public static string CombineSelectors(string a, string b, bool pretty) + { + string[] A = SafeSplit(a, ',').Select(a => a.Trim()).ToArray(); + string[] B = SafeSplit(b, ',').Select(b => b.Trim()).ToArray(); + + List output = new List(); + + foreach (string n in A.Concat(B)) + { + if (!output.Contains(n)) + output.Add(n); + } + + if (pretty == true) + { + return String.Join(", ", output.OrderByDescending(o => o.Length)); + } + else + { + return String.Join(',', output); + } + } +} diff --git a/src/Compiler/Merger.cs b/src/Compiler/Merger.cs deleted file mode 100644 index 553568b..0000000 --- a/src/Compiler/Merger.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cascadium.Compiler; - -internal class Merger : CompilerModule -{ - public Merger(CompilerContext context) : base(context) - { - } - - public void Merge(bool mergeSelectors, bool mergeAtRules, bool mergeDeclarations) - { - if (mergeSelectors) - { - List newRules = new List(); - - foreach (Rule rule in this.Context.Rules) - { - Rule? existingRule = newRules - .FirstOrDefault(r => Context.Preparers.IsSelectorsEqual(r.Selector, rule.Selector)); - - if (existingRule == null) - { - newRules.Add(rule); - } - else - { - foreach (var prop in rule.Properties) - { - existingRule.Properties[prop.Key] = prop.Value; - - if (this.Context.Options?.MergeOrderPriority == MergeOrderPriority.PreserveLast) - { - if (rule.Order > existingRule.Order) - existingRule.Order = rule.Order; - } - } - } - } - - this.Context.Rules = newRules; - } - - if (mergeAtRules) - { - List stylesheets = new List(); - - foreach (CompilerContext css in this.Context.Childrens) - { - if (css.AtRule == null) continue; - - CompilerContext? existingCss = stylesheets - .FirstOrDefault(r => Context.Utils.RemoveSpaces(r.AtRule ?? "") == Context.Utils.RemoveSpaces(css.AtRule)); - - if (existingCss == null) - { - stylesheets.Add(css); - } - else - { - existingCss.Rules.AddRange(css.Rules); - } - } - - foreach (CompilerContext css in stylesheets) - { - css.Merger.Merge(mergeSelectors, mergeAtRules, mergeDeclarations); - } - - this.Context.Childrens = stylesheets; - } - - if (mergeDeclarations) - { - // merge top-level only - List newRules = new List(); - - foreach (Rule rule in this.Context.Rules) - { - Rule? existingRule = newRules - .FirstOrDefault(r => r.GetPropertiesHashCode() == rule.GetPropertiesHashCode()); - - if (existingRule == null) - { - newRules.Add(rule); - } - else - { - existingRule.Selector = Context.Preparers.CombineSelectors(existingRule.Selector, rule.Selector); - } - } - - this.Context.Rules = newRules; - } - } -} diff --git a/src/Compiler/Parser.cs b/src/Compiler/Parser.cs index d8ceb3c..9208576 100644 --- a/src/Compiler/Parser.cs +++ b/src/Compiler/Parser.cs @@ -1,328 +1,134 @@ -using Cascadium.Converters; +using Cascadium.Entity; +using Cascadium.Object; using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Data; using System.Linq; -using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; namespace Cascadium.Compiler; -internal class Parser : CompilerModule -{ - public Parser(CompilerContext context) : base(context) - { - } - public bool ParseXCss(string css) +class Parser +{ + public static NestedStylesheet ParseSpreadsheet(TokenCollection tokens) { - bool anyParsed = false; - bool selectorStarted = false; - bool isAtRule = false; - int keyLevel = 0; + NestedStylesheet spreadsheet = new NestedStylesheet(); - StringBuilder sb = new StringBuilder(); - char[] chars = css.ToCharArray(); + readRule: + List selectors = new List(); - bool inSingleString = false; - bool inDoubleString = false; - - for (int i = 0; i < chars.Length; i++) + readRule__nextSelector: { - char c = chars[i], b = i > 0 ? chars[i - 1] : '\0'; ; - sb.Append(c); + var next = tokens.Read(out Token result); - if (!char.IsWhiteSpace(c)) + if (!next) { - selectorStarted = true; + goto finish; } - - if (c == '\'' && b != '\\' && !inDoubleString) + else if (result.Type == TokenType.Em_Selector) { - inSingleString = !inSingleString; + if (result.Content == "") + throw new CascadiumException(result.DebugInfo, "empty selectors are not allowed"); + + selectors.Add(result.Content); + goto readRule__nextSelector; } - else if (c == '"' && b != '\\' && !inSingleString) + else if (result.Type == TokenType.Em_RuleStart) { - inDoubleString = !inDoubleString; - } + if (selectors.Count == 0) + throw new CascadiumException(result.DebugInfo, "selector expected"); - if (inSingleString || inDoubleString) - continue; + ReadRule(tokens, spreadsheet, selectors.ToArray()); + selectors.Clear(); - if (c == '@' && keyLevel == 0 && selectorStarted) - { - isAtRule = true; + goto readRule__nextSelector; } - else if (c == ';' && isAtRule && keyLevel == 0) + else if (result.Type == TokenType.Em_RuleEnd) { - ParseAtRule(sb.ToString()); - isAtRule = false; - sb.Clear(); + goto readRule; } - else if (c == '{') + else if (result.Type == TokenType.Em_Statement) { - keyLevel++; + spreadsheet.Statements.Add(result.Content); + goto readRule__nextSelector; } - else if (c == '}') + else { - keyLevel--; - if (keyLevel == 0) - { - if (isAtRule) - { - ParseAtRule(sb.ToString()); - isAtRule = false; - } - else - { - ParseRule(sb.ToString(), ""); - } - anyParsed = true; - sb.Clear(); - } - selectorStarted = false; + throw new CascadiumException(result.DebugInfo, "unexpected token"); } } - return anyParsed; + + finish: + return spreadsheet; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ParseAtRule(in string atRuleStr, string? baseSelector = null) + static void ReadRule(TokenCollection tokens, IRuleContainer container, string[] externSelectors) { - string ruleStr = atRuleStr.Trim(); - int openingTagIndex = ruleStr.IndexOf('{'); + int ruleIndex = 0; + List buildingSelectors = new List(); + NestedRule buildingRule = new NestedRule(); - if (ruleStr.Length == 0) return; // empty @ rule - - if (baseSelector != null) + readRule__nextItem: { - ruleStr = ruleStr.Substring(0, openingTagIndex + 1) - + baseSelector + '{' + ruleStr.Substring(openingTagIndex + 1); - - ruleStr += '}'; - } + var next = tokens.Read(out Token result); - if (openingTagIndex >= 0) - { - CompilerContext css = new CompilerContext() + if (result.Type == TokenType.Em_RuleEnd) { - Options = this.Options - }; - - css.AtRule = ruleStr.Substring(0, openingTagIndex); - - if (Options != null) - { - string sanitized = css.AtRule.Trim().TrimStart('@'); - foreach (string atRule in Options.AtRulesRewrites) - { - if (string.Compare(atRule, sanitized, true) == 0) - { - css.AtRule = "@" + Options.AtRulesRewrites[atRule]; - } - } - } - - string body = ruleStr - .Substring(openingTagIndex + 1, ruleStr.Length - openingTagIndex - 2) - .Trim(); - - bool parseResult = css.Parser.ParseXCss(body); - if (parseResult) - { - // body was interpreted as an css stylesheet - this.Context.Childrens.Add(css); + goto readRule__finish; } - else - { - // is an at-rule, but not identified as an stylesheet inside it - // try to interpret as a rule - ParseRule(ruleStr, ""); - } - } - else - { - this.Context.Declarations.Add(ruleStr); - } - } - - private void ParseRule(string ruleStr, string baseSelector) - { - ruleStr = ruleStr.Trim(); - Rule rule = new Rule() - { - Order = Context.StackOrder++ - }; - - string mounting = ""; - - void SetDeclaration() - { - string declaration = mounting.Substring(0, mounting.Length - 1).Trim(); - int sepIndex = declaration.IndexOf(':'); - if (sepIndex > 0) + else if (result.Type == TokenType.Em_RuleStart) { - bool wasConverted = false; - string propKey = declaration.Substring(0, sepIndex).Trim(); - string? propValue = declaration.Substring(sepIndex + 1).Trim(); - propValue = Context.Preparers.PrepareValue(propValue); - - if (Options?.Converters != null) - { - foreach (CSSConverter converter in Options.Converters) - { - if (converter.CanConvert(propKey, propValue)) - { - wasConverted = true; - NameValueCollection result = new NameValueCollection(); - converter.Convert(propValue, result); - foreach (string rKey in result) - { - string rValue = result[rKey]!; - rule.Properties[rKey] = rValue; - } - } - } - } - if (!wasConverted) - { - if (propValue == "") - { - mounting = ""; - return; // do not add empty declarations - } - else - { - rule.Properties[propKey] = propValue; - } - } - - mounting = ""; - } - } - - int openingTagIndex = ruleStr.IndexOf('{'); - - rule.Selector = ruleStr.Substring(0, openingTagIndex).Trim(); - rule.Selector = JoinSelector(rule.Selector, baseSelector); - - int keyState = 0; + if (buildingSelectors.Count == 0) + throw new CascadiumException(result.DebugInfo, "selector expected"); - bool inSingleString = false; - bool inDoubleString = false; + ruleIndex++; + ReadRule(tokens, buildingRule, buildingSelectors.ToArray()); - string body = ruleStr.Substring(openingTagIndex + 1); - for (int i = 0; i < body.Length; i++) - { - char c = body[i]; - char b = i > 0 ? body[i - 1] : '\0'; - mounting += c; + buildingSelectors.Clear(); - if (c == '\'' && b != '\\' && !inDoubleString) - { - inSingleString = !inSingleString; + goto readRule__nextItem; } - else if (c == '"' && b != '\\' && !inSingleString) + else if (result.Type == TokenType.Em_PropertyName) { - inDoubleString = !inDoubleString; - } + tokens.Read(out Token valueToken); - if (inSingleString || inDoubleString) - continue; + if (valueToken.Type != TokenType.Em_PropertyValue) + throw new CascadiumException(tokens.Last.DebugInfo, "property value expected"); + if (string.IsNullOrWhiteSpace(valueToken.Content)) + goto readRule__nextItem; // skip empty declarations - if (c == '{') - { - keyState++; + buildingRule.Declarations[result.Content] = valueToken.Content; + goto readRule__nextItem; } - else if (c == '}') + else if (result.Type == TokenType.Em_Selector) { - keyState--; - if (keyState == 0) - { - if (mounting.TrimStart().StartsWith('@')) - { - // at rule inside rule - ParseAtRule(mounting, rule.Selector); - } - else - { - ParseRule(mounting, rule.Selector); - } - mounting = ""; - continue; - } + if (result.Content == "") + throw new CascadiumException(result.DebugInfo, "empty selectors are not allowed"); + if (IsSelectorInvalidRuleset(result.Content)) + throw new CascadiumException(result.DebugInfo, "; expected"); + + buildingSelectors.Add(result.Content); + goto readRule__nextItem; } - else if (c == ';' && keyState == 0) + else { - SetDeclaration(); + throw new CascadiumException(result.DebugInfo, "unexpected token"); } } - if (mounting.Length > 0 && mounting.Contains(':') && mounting.EndsWith('}')) - { - SetDeclaration(); - } - - if (rule.Properties.Count > 0) - Context.Rules.Add(rule); + readRule__finish: + buildingRule.Selectors.AddRange(externSelectors); + container.Rules.Add(buildingRule); } - private string JoinSelector(string current, string before) + static bool IsSelectorInvalidRuleset(string content) { - StringBuilder sb = new StringBuilder(); - string[] cSelectors = Context.Split.SafeSplit(current, ','); - string[] bSelectors = Context.Split.SafeSplit(before, ','); - - if (before.Length == 0) + int nlIndex = content.IndexOfAny(new char[] { '\n', '\r' }); + if (nlIndex >= 0) { - foreach (string cSelector in cSelectors) - { - string prepared = Context.Preparers.PrepareSelectorUnit(cSelector); - sb.Append(prepared); - sb.Append(','); - if (Options?.Pretty == true) - sb.Append(' '); - } - goto finish; + return content.Substring(0, nlIndex).IndexOf(':') >= 0; } - foreach (string C in cSelectors) - { - string c = C.Trim(); - foreach (string B in bSelectors) - { - string b = B.Trim(); - string s; - if (c.StartsWith('&')) - { - sb.Append(b); - s = c.Substring(1); - if (Options == null || Options.KeepNestingSpace == false) - { - s = s.TrimStart(); - } - } - else - { - sb.Append(b); - if (c.Length != 0 && !CascadiumCompiler.combinators.Contains(c[0])) - { - sb.Append(' '); - } - s = c; - } - s = Context.Preparers.PrepareSelectorUnit(s); - sb.Append(s); - sb.Append(','); - if (Options?.Pretty == true) - sb.Append(' '); - } - } - - finish: - if (sb.Length > 0) sb.Length--; - if (sb.Length > 0 && Options?.Pretty == true) sb.Length--; - return sb.ToString(); - } + return false; + } } diff --git a/src/Compiler/Preparers.cs b/src/Compiler/Preparers.cs deleted file mode 100644 index ae33264..0000000 --- a/src/Compiler/Preparers.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cascadium.Compiler; - -internal class Preparers : CompilerModule -{ - public Preparers(CompilerContext context) : base(context) - { - } - - public string PrepareCssInput(string input) - { - StringBuilder output = new StringBuilder(); - - char[] inputChars = input.ToCharArray(); - - bool inSingleString = false; - bool inDoubleString = false; - bool inSinglelineComment = false; - bool inMultilineComment = false; - - var inString = () => inSingleString || inDoubleString; - var inComment = () => inMultilineComment || inSinglelineComment; - - for (int i = 0; i < inputChars.Length; i++) - { - char current = inputChars[i]; - char before = i > 0 ? inputChars[i - 1] : '\0'; - - if (current == '\'' && before != '\\' && !inDoubleString && !inComment()) - { - inSingleString = !inSingleString; - } - else if (current == '"' && before != '\\' && !inSingleString && !inComment()) - { - inDoubleString = !inDoubleString; - } - else if (current == '*' && before == '/' && !inString() && !inSinglelineComment) - { - inMultilineComment = true; - if (output.Length > 0) output.Length--; - } - else if (current == '/' && before == '*' && !inString() && !inSinglelineComment) - { - inMultilineComment = false; - if (output.Length > 0) output.Length--; - continue; - } - else if (current == '/' && before == '/' && !inString() && !inMultilineComment) - { - inSinglelineComment = true; - if (output.Length > 0) output.Length--; - } - else if (current == '\n' || current == '\r' && !inString() && inSinglelineComment) - { - inSinglelineComment = false; - } - - if (!inComment()) - { - output.Append(current); - } - } - - return output.ToString().Trim(); - } - - public string? CombineSelectors(string? a, string? b) - { - if (a == null) return b; - if (b == null) return a; - if (a == null && b == null) return null; - - string[] A = Context.Split.SafeSplit(a, ',').Select(a => a.Trim()).ToArray(); - string[] B = Context.Split.SafeSplit(b, ',').Select(b => b.Trim()).ToArray(); - - List output = new List(); - - foreach (string n in A.Concat(B)) - { - if (!output.Contains(n)) - output.Add(n); - } - - if (Context.Options?.Pretty == true) - { - return String.Join(", ", output.OrderByDescending(o => o.Length)); - } - else - { - return String.Join(',', output); - } - } - - public string PrepareValue(string value) - { - if (Options?.UseVarShortcut == true) - { - StringBuilder output = new StringBuilder(); - char[] chars = value.ToCharArray(); - bool inSingleString = false; - bool inDoubleString = false; - bool isParsingVarname = false; - - for (int i = 0; i < chars.Length; i++) - { - char c = chars[i]; - char b = i > 0 ? chars[i - 1] : '\0'; - - output.Append(c); - - if (c == '\'' && b != '\\' && !inDoubleString) - { - inSingleString = !inSingleString; - } - else if (c == '"' && b != '\\' && !inSingleString) - { - inDoubleString = !inDoubleString; - } - - if ((inSingleString || inDoubleString) == false) - { - if (c == '-' && b == '-' && output.Length >= 2 && !output.ToString().EndsWith("var(--")) - { - isParsingVarname = true; - output.Length -= 2; - output.Append("var(--"); - } - else if (!Context.Utils.IsNameChar(c) && isParsingVarname) - { - output.Length--; - output.Append(')'); - output.Append(c); - isParsingVarname = false; - } - } - } - - if (isParsingVarname) - output.Append(')'); - - return output.ToString(); - } - else - { - return value; - } - } - - public bool IsSelectorsEqual(string? a, string? b) - { - if (a == null || b == null) return a == b; - return PrepareSelectorUnit(a) == PrepareSelectorUnit(b); - } - - public string PrepareSelectorUnit(string s) - { - StringBuilder output = new StringBuilder(); - char[] chars; - - bool ignoreTrailingSpace = Options == null || Options.KeepNestingSpace == false; - - if (ignoreTrailingSpace) - { - chars = s.Trim().ToCharArray(); - } - else - { - chars = s.TrimEnd().ToCharArray(); - } - - bool inSingleString = false; - bool inDoubleString = false; - char lastCombinator = '\0'; - - for (int i = 0; i < chars.Length; i++) - { - char c = chars[i]; - char b = i > 0 ? chars[i - 1] : '\0'; - - if (c == '\'' && b != '\\' && !inDoubleString) - { - inSingleString = !inSingleString; - } - else if (c == '"' && b != '\\' && !inSingleString) - { - inDoubleString = !inDoubleString; - } - - if (inDoubleString || inSingleString) - { - output.Append(c); - continue; - } - - if (i == 0 && ignoreTrailingSpace && char.IsWhiteSpace(c)) - { - output.Append(c); - } - - if (CascadiumCompiler.combinators.Contains(c) || char.IsWhiteSpace(c)) - { - if (char.IsWhiteSpace(lastCombinator) || lastCombinator == '\0') - lastCombinator = c; - } - else - { - bool prettySpace = Options?.Pretty == true && !char.IsWhiteSpace(lastCombinator) && lastCombinator != '\0'; - if (prettySpace) output.Append(' '); - - if (lastCombinator != '\0') - output.Append(lastCombinator); - - if (prettySpace) output.Append(' '); - output.Append(c); - lastCombinator = '\0'; - } - } - - return output.ToString(); - } -} diff --git a/src/Compiler/Sanitizer.cs b/src/Compiler/Sanitizer.cs new file mode 100644 index 0000000..96561e0 --- /dev/null +++ b/src/Compiler/Sanitizer.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Compiler; + +class Sanitizer +{ + public static string SanitizeInput(string input) + { + StringBuilder output = new StringBuilder(); + + char[] inputChars = input.ToCharArray(); + + bool inSingleString = false; + bool inDoubleString = false; + bool inSinglelineComment = false; + bool inMultilineComment = false; + + var inString = () => inSingleString || inDoubleString; + var inComment = () => inMultilineComment || inSinglelineComment; + + for (int i = 0; i < inputChars.Length; i++) + { + char current = inputChars[i]; + char before = i > 0 ? inputChars[i - 1] : '\0'; + + if (current == '\'' && before != '\\' && !inDoubleString && !inComment()) + { + inSingleString = !inSingleString; + } + else if (current == '"' && before != '\\' && !inSingleString && !inComment()) + { + inDoubleString = !inDoubleString; + } + else if (current == '*' && before == '/' && !inString() && !inSinglelineComment) + { + inMultilineComment = true; + if (output.Length > 0) output.Length--; + } + else if (current == '/' && before == '*' && !inString() && !inSinglelineComment) + { + inMultilineComment = false; + if (output.Length > 0) output.Length--; + continue; + } + else if (current == '/' && before == '/' && !inString() && !inMultilineComment) + { + inSinglelineComment = true; + if (output.Length > 0) output.Length--; + } + else if (current == '\n' || current == '\r' && !inString() && inSinglelineComment) + { + inSinglelineComment = false; + } + + if (!inComment()) + { + output.Append(current); + } + } + + return output.ToString(); + } +} diff --git a/src/Compiler/Split.cs b/src/Compiler/Split.cs deleted file mode 100644 index 1c1a24e..0000000 --- a/src/Compiler/Split.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cascadium.Compiler; -internal class Split : CompilerModule -{ - internal static Split Shared = new Split(null!); - - public Split(CompilerContext context) : base(context) - { - } - - internal static string[] StSafeSplit(string? value, char op) - { - return Shared.SafeSplit(value, op); - } - - public string[] SafeSplit(string? value, char op) - { - if (value == null) return Array.Empty(); - List output = new List(); - StringBuilder mounting = new StringBuilder(); - bool inSingleString = false; - bool inDoubleString = false; - int expressionIndex = 0; - - for (int i = 0; i < value.Length; i++) - { - char c = value[i]; - char b = i > 0 ? value[i - 1] : '\0'; - mounting.Append(c); - - 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--; - } - - if ((inDoubleString || inSingleString) == false && expressionIndex == 0) - { - if (c == op) - { - mounting.Length--; - output.Add(mounting.ToString()); - mounting.Clear(); - } - } - } - - if (mounting.Length > 0) - output.Add(mounting.ToString()); - - return output.ToArray(); - } -} diff --git a/src/Compiler/TextInterpreter.cs b/src/Compiler/TextInterpreter.cs new file mode 100644 index 0000000..a49ca46 --- /dev/null +++ b/src/Compiler/TextInterpreter.cs @@ -0,0 +1,181 @@ +using System; +using System.Text; +using Cascadium.Object; + +namespace Cascadium.Compiler; + +class TextInterpreter +{ + public string InputString { get; private set; } + public int Position { get; private set; } = 0; + public int Length { get; private set; } + public int Line + { + get + { + int ocurrences = 1; // line start at 1 + for (int i = 0; i < Position; i++) + { + if (InputString[i] == '\n') + { + ocurrences++; + } + } + return ocurrences; + } + } + + public int Column + { + get + { + int col = 1; + for (int n = 0; n < Position; n++) + { + if (InputString[n] == '\n') + { + col = 0; + } + col++; + } + return col; + } + } + + public string CurrentLine + { + get + { + return InputString.Split('\n')[Line - 1]; + } + } + + public TextInterpreter(string s) + { + InputString = s; + Length = InputString.Length; + } + + public TokenDebugInfo TakeSnapshot(string text) + { + int textIndex = InputString.Substring(0, Position).LastIndexOf(text.Trim()); + return TakeSnapshot(-(Position - textIndex)); + } + + public TokenDebugInfo TakeSnapshot(int offset = 0) + { + Move(offset); + var snapshot = new TokenDebugInfo() + { + Column = Column, + Line = Line, + LineText = CurrentLine + }; + Move(offset * -1); + + return snapshot; + } + + public bool CanRead() + { + return Position < InputString.Length - 1; + } + + public void Move(int count) + { + Position = Math.Min(Math.Max(Position + count, 0), InputString.Length); + } + + public int Read(out char c) + { + if (InputString.Length <= Position) + { + c = '\0'; + return -1; + } + c = InputString[Position]; + Position++; + return 1; + } + + public string ReadAtLeast(int count) + { + StringBuilder sb = new StringBuilder(); + + int n = 0; + while (n < count) + { + int j = Read(out char c); + if (j >= 0) + { + sb.Append(c); + n++; + } + else break; + } + + return sb.ToString(); + } + + public char ReadUntil(Span untilChars, bool wrapStringToken, out string result) + { + char hit = '\0'; + StringBuilder sb = new StringBuilder(); + + bool inDoubleString = false; + bool inSingleString = false; + char b = '\0'; + + while (Read(out char c) > 0) + { + if (wrapStringToken && !inSingleString && c == Token.Ch_DoubleStringQuote && b != Token.Ch_CharEscape) + { + inDoubleString = !inDoubleString; + } + else if (wrapStringToken && !inDoubleString && c == Token.Ch_SingleStringQuote && b != Token.Ch_CharEscape) + { + inSingleString = !inSingleString; + } + + if (inDoubleString || inSingleString) + { + sb.Append(c); + b = c; + continue; + } + + if (untilChars.Contains(c)) + { + hit = c; + break; + } + + sb.Append(c); + b = c; + } + + result = sb.ToString(); + return hit; + } + + public void SkipIgnoreTokens() + { + bool skipping = true; + while (skipping) + { + if (Read(out char c) > 0) + { + if (Token.IsWhitespaceChr(c)) + { + continue; // whitespace + } + else + { + Move(-1); + break; + } + } + else break; + } + } +} diff --git a/src/Compiler/Tokenizer.cs b/src/Compiler/Tokenizer.cs new file mode 100644 index 0000000..debcf18 --- /dev/null +++ b/src/Compiler/Tokenizer.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Cascadium.Entity; +using Cascadium.Object; + +namespace Cascadium.Compiler; + +internal class Tokenizer +{ + public TextInterpreter Interpreter { get; } + + public Tokenizer(string code) + { + Interpreter = new TextInterpreter(code); + } + + public TokenCollection Tokenize() + { + char[] hitChars = new char[] { Token.Ch_BraceOpen, Token.Ch_BraceClose, Token.Ch_Semicolon }; + List tokens = new List(); + int opennedRules = 0; + TokenDebugInfo lastOpennedRule = default; + + string result; + char hit; + while ((hit = Interpreter.ReadUntil(hitChars, true, out result)) != '\0') + { + if (hit == Token.Ch_Semicolon) + { + tokens.AddRange(ReadCurrentDeclaration(result)); + continue; + } + else if (hit == Token.Ch_BraceOpen) + { + tokens.AddRange(ReadSelectors(result)); + tokens.Add(new Token(TokenType.Em_RuleStart, "", Interpreter)); + + opennedRules++; + lastOpennedRule = Interpreter.TakeSnapshot(-1); + + continue; + } + else if (hit == Token.Ch_BraceClose) + { + if (!string.IsNullOrWhiteSpace(result)) + { + // remaining declaration + tokens.AddRange(ReadCurrentDeclaration(result)); + } + + tokens.Add(new Token(TokenType.Em_RuleEnd, "", Interpreter)); + opennedRules--; + continue; + } + } + + if (hit == '\0') + { + if (!string.IsNullOrWhiteSpace(result)) + { + throw new CascadiumException(Interpreter.TakeSnapshot(result), "syntax error: unexpected token"); + } + } + + if (opennedRules != 0) + { + throw new CascadiumException(lastOpennedRule, "syntax error: unclosed rule"); + } + + return new TokenCollection(tokens.ToArray()); + } + + IEnumerable ReadCurrentDeclaration(string declaration) + { + if (declaration.TrimStart().StartsWith('@')) + { + // its an statement + yield return new Token(TokenType.Em_Statement, declaration.Trim(), Interpreter); + yield break; + } + + int dotPos = declaration.IndexOf(Token.Ch_DoubleDots); + if (dotPos == -1) + { + throw new CascadiumException(Interpreter.TakeSnapshot(declaration), "syntax error: unexpected token \"" + declaration.Trim() + "\""); + } + + string property = declaration.Substring(0, dotPos).Trim(); + string value = declaration.Substring(dotPos + 1).Trim(); + + if (property == "") + { + throw new CascadiumException(Interpreter.TakeSnapshot(declaration), "syntax error: property name cannot be empty"); + } + else + { + yield return new Token(TokenType.Em_PropertyName, property, Interpreter); + yield return new Token(TokenType.Em_PropertyValue, value, Interpreter); + } + } + + IEnumerable ReadSelectors(string selectorCode) + { + if (selectorCode.IndexOf(',') < 0) + { + yield return new Token(TokenType.Em_Selector, selectorCode.Trim(), Interpreter); + } + else + { + string[] selectors = Helper.SafeSplit(selectorCode, ','); + foreach (string s in selectors) + { + yield return new Token(TokenType.Em_Selector, s.Trim(), Interpreter); + } + } + } +} diff --git a/src/Compiler/Utils.cs b/src/Compiler/Utils.cs deleted file mode 100644 index c60c126..0000000 --- a/src/Compiler/Utils.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cascadium.Compiler; -internal class Utils : CompilerModule -{ - public Utils(CompilerContext context) : base(context) - { - } - - public void TrimEndSb(StringBuilder sb) - { - int i = sb.Length - 1; - - for (; i >= 0; i--) - if (!char.IsWhiteSpace(sb[i])) - break; - - if (i < sb.Length - 1) - sb.Length = i + 1; - } - - public bool IsNameChar(char c) - => Char.IsLetter(c) || Char.IsDigit(c) || c == '_' || c == '-'; - - public string RemoveSpaces(string s) - => new String(s.ToCharArray().Where(c => !char.IsWhiteSpace(c)).ToArray()); -} diff --git a/src/Compiler/_README.txt b/src/Compiler/_README.txt new file mode 100644 index 0000000..e55f1cd --- /dev/null +++ b/src/Compiler/_README.txt @@ -0,0 +1,10 @@ +Compiler steps: + +-- Sanitizer.cs + Sanitizes the code, stripping comments. + +-- Tokenizer.cs + The lexical analyzer. Reads the code into tokens. + +-- Parser.cs + The syntax analyzer. Reads the tokens into assembly. \ No newline at end of file diff --git a/src/Converters/CSSConverter.cs b/src/Converters/CSSConverter.cs index 825e71a..d8e43f1 100644 --- a/src/Converters/CSSConverter.cs +++ b/src/Converters/CSSConverter.cs @@ -1,6 +1,9 @@ -using Cascadium.Compiler; +using System; +using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using System.Text; +using System.Threading.Tasks; namespace Cascadium.Converters; @@ -29,6 +32,6 @@ public abstract class CSSConverter /// The raw CSS value. public string[] SafeSplit(string? value) { - return Split.StSafeSplit(value, ' '); + return Compiler.Helper.SafeSplit(value, ' '); } } diff --git a/src/Converters/StaticCSSConverter.cs b/src/Converters/StaticCSSConverter.cs index 04a0fd7..4b52251 100644 --- a/src/Converters/StaticCSSConverter.cs +++ b/src/Converters/StaticCSSConverter.cs @@ -3,7 +3,6 @@ using System.Collections.Specialized; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Cascadium.Converters; diff --git a/src/Entity/CssRule.cs b/src/Entity/CssRule.cs new file mode 100644 index 0000000..1a15c32 --- /dev/null +++ b/src/Entity/CssRule.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Entity; + +internal class CssRule +{ + public string Selector { get; set; } = ""; + public Dictionary Declarations { get; set; } = new Dictionary(); + public int Order { get; set; } = 0; + + public override int GetHashCode() + { + int n = 0, j = 1; + + // the property order should impact on the hash code + // so + // foo: bar + // bar: foo + // + // is different than + // + // bar: foo + // foo: bar + + foreach (var kp in Declarations) + { + n += (kp.Key.GetHashCode() + kp.Value.GetHashCode()) / 2; + n *= j; + j++; + } + return n / Declarations.Count; + } +} diff --git a/src/Entity/CssStylesheet.cs b/src/Entity/CssStylesheet.cs new file mode 100644 index 0000000..373697a --- /dev/null +++ b/src/Entity/CssStylesheet.cs @@ -0,0 +1,123 @@ +using Cascadium.Compiler; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Entity; + +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(); + + public CssStylesheet GetOrCreateStylesheet(string atRuleDeclaration, bool canMerge) + { + if (canMerge) + { + string sanitized = Helper.RemoveSpaces(atRuleDeclaration); + foreach (CssStylesheet subStylesheet in Stylesheets) + { + if (Helper.RemoveSpaces(subStylesheet.AtRuleDeclaration ?? "") == sanitized) + { + return subStylesheet; + } + } + + CssStylesheet newStylesheet = new CssStylesheet() + { + AtRuleDeclaration = atRuleDeclaration + }; + Stylesheets.Add(newStylesheet); + return newStylesheet; + } + else + { + CssStylesheet newStylesheet = new CssStylesheet() + { + AtRuleDeclaration = atRuleDeclaration + }; + Stylesheets.Add(newStylesheet); + return newStylesheet; + } + } + + public string Export(CascadiumOptions options) + { + StringBuilder sb = new StringBuilder(); + + void ExportStylesheet(CssStylesheet css, int indentLevel) + { + if (css.AtRuleDeclaration != null) + { + if (options.Pretty) sb.Append(new string(' ', indentLevel * 4)); + sb.Append(css.AtRuleDeclaration.Trim()); + if (options.Pretty) sb.Append(' '); + sb.Append('{'); + if (options.Pretty) sb.Append('\n'); + } + ExportRules(css, indentLevel + 1); + if (options.Pretty) + { + sb = new StringBuilder(sb.ToString().TrimEnd()); + } + if (css.AtRuleDeclaration != null) + { + if (options.Pretty) sb.Append('\n'); + if (options.Pretty) sb.Append(new string(' ', indentLevel * 4)); + sb.Append('}'); + if (options.Pretty) sb.Append("\n\n"); + } + } + + void ExportRules(CssStylesheet css, int indentLevel) + { + foreach (var rule in css.Rules.OrderBy(r => r.Order)) + { + if (options.Pretty) sb.Append(new string(' ', indentLevel * 4)); + sb.Append(rule.Selector); + if (options.Pretty) sb.Append(' '); + sb.Append('{'); + if (options.Pretty) sb.Append('\n'); + + foreach (KeyValuePair property in rule.Declarations) + { + if (options.Pretty) sb.Append(new string(' ', (indentLevel + 1) * 4)); + sb.Append(property.Key); + sb.Append(':'); + if (options.Pretty) sb.Append(' '); + sb.Append(property.Value); + sb.Append(';'); + if (options.Pretty) sb.Append('\n'); + } + + sb.Length--; // remove the last ; + if (options.Pretty) sb.Append('\n'); + if (options.Pretty) sb.Append(new string(' ', indentLevel * 4)); + sb.Append('}'); + if (options.Pretty) sb.Append("\n\n"); + } + } + + foreach (string decl in Statements) + { + sb.Append(decl); + sb.Append(';'); + if (options.Pretty) sb.AppendLine(); + } + if (options.Pretty && Statements.Count > 0) sb.AppendLine(); + + ExportRules(this, 0); + + foreach (CssStylesheet stylesheet in Stylesheets) + { + ExportStylesheet(stylesheet, 0); + } + + return sb.ToString().Trim(); + } +} diff --git a/src/Entity/FlatRule.cs b/src/Entity/FlatRule.cs new file mode 100644 index 0000000..49c4127 --- /dev/null +++ b/src/Entity/FlatRule.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Entity; + +class FlatRule +{ + public List Selectors { get; set; } = new List(); + public Dictionary Declarations { get; set; } = new Dictionary(); + + public override string ToString() => $"{string.Join(" ", Selectors.Select(s => string.Join(",", s)))} Declarations={Declarations.Count}"; + + public bool IsRuleAtRule() + { + return Selectors.Count == 1 && Selectors[0].Length == 1 && Selectors[0][0].StartsWith('@'); + } + + public string? PopAtRule() + { + string? s = null; + foreach (string[] group in Selectors) + { + foreach (string selector in group) + { + if (selector.StartsWith('@')) + { + s = selector; + } + } + } + + Selectors = Selectors.Select(group => group.Where(ss => ss != s).ToArray()).ToList(); + + return s; + } +} diff --git a/src/Entity/FlatStylesheet.cs b/src/Entity/FlatStylesheet.cs new file mode 100644 index 0000000..c7227ee --- /dev/null +++ b/src/Entity/FlatStylesheet.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Entity; + +class FlatStylesheet +{ + public List Statements { get; set; } = new(); + public List Rules { get; set; } = new(); +} diff --git a/src/Entity/IRuleContainer.cs b/src/Entity/IRuleContainer.cs new file mode 100644 index 0000000..94d9374 --- /dev/null +++ b/src/Entity/IRuleContainer.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Entity; + +interface IRuleContainer +{ + public List Rules { get; set; } +} diff --git a/src/Entity/NestedRule.cs b/src/Entity/NestedRule.cs new file mode 100644 index 0000000..35cf33a --- /dev/null +++ b/src/Entity/NestedRule.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Entity; + +class NestedRule : IRuleContainer +{ + public List Selectors { get; set; } = new List(); + public Dictionary Declarations { get; set; } = new Dictionary(); + public List Rules { get; set; } = new List(); + + public override string ToString() => $"{string.Join(",", Selectors)} Declarations={Declarations.Count} Rules={Rules.Count}"; +} diff --git a/src/Entity/NestedStylesheet.cs b/src/Entity/NestedStylesheet.cs new file mode 100644 index 0000000..d9701fa --- /dev/null +++ b/src/Entity/NestedStylesheet.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Entity; + +internal class NestedStylesheet : IRuleContainer +{ + public List Statements { get; set; } = new List(); + public List Rules { get; set; } = new List(); +} diff --git a/src/Extensions/Converter.cs b/src/Extensions/Converter.cs new file mode 100644 index 0000000..e2b6e50 --- /dev/null +++ b/src/Extensions/Converter.cs @@ -0,0 +1,43 @@ +using Cascadium.Converters; +using Cascadium.Entity; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Extensions; + +internal class Converter +{ + public static void ConvertAll(CssStylesheet css, CascadiumOptions options) + { + foreach (var rule in css.Rules) + { + foreach (KeyValuePair declaration in rule.Declarations.ToArray()) + { + foreach (CSSConverter converter in options.Converters) + { + if (converter.CanConvert(declaration.Key, declaration.Value)) + { + NameValueCollection output = new NameValueCollection(); + converter.Convert(declaration.Value, output); + rule.Declarations.Remove(declaration.Key); + + foreach (string nprop in output) + { + string? value = output[nprop]; + if (string.IsNullOrEmpty(value)) continue; + rule.Declarations[nprop] = value; + } + } + } + } + } + foreach (var subcss in css.Stylesheets) + { + ConvertAll(subcss, options); + } + } +} diff --git a/src/Extensions/MediaRewriter.cs b/src/Extensions/MediaRewriter.cs new file mode 100644 index 0000000..25c5506 --- /dev/null +++ b/src/Extensions/MediaRewriter.cs @@ -0,0 +1,27 @@ +using Cascadium.Compiler; +using Cascadium.Entity; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Extensions; + +internal class MediaRewriter +{ + public static void ApplyRewrites(CssStylesheet cssStylesheet, CascadiumOptions options) + { + foreach (var subcss in cssStylesheet.Stylesheets) + { + if (subcss.AtRuleDeclaration == null) continue; + foreach (string rewrite in options.AtRulesRewrites) + { + if (Helper.InvariantCompare(subcss.AtRuleDeclaration?.TrimStart('@'), rewrite.TrimStart('@'))) + { + subcss.AtRuleDeclaration = '@' + options.AtRulesRewrites[rewrite]!.TrimStart('@'); + } + } + } + } +} diff --git a/src/Extensions/ValueHandler.cs b/src/Extensions/ValueHandler.cs new file mode 100644 index 0000000..42240fa --- /dev/null +++ b/src/Extensions/ValueHandler.cs @@ -0,0 +1,75 @@ +using Cascadium.Entity; +using Cascadium.Object; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Extensions; + +internal class ValueHandler +{ + public static void TransformVarShortcuts(CssStylesheet css) + { + foreach (var rule in css.Rules) + { + foreach(string key in rule.Declarations.Keys) + { + rule.Declarations[key] = ApplyVarShortcuts(rule.Declarations[key]); + } + } + foreach (var subcss in css.Stylesheets) + { + TransformVarShortcuts(subcss); + } + } + + static string ApplyVarShortcuts(string value) + { + StringBuilder output = new StringBuilder(); + char[] chars = value.ToCharArray(); + bool inSingleString = false; + bool inDoubleString = false; + bool isParsingVarname = false; + + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + char b = i > 0 ? chars[i - 1] : '\0'; + + output.Append(c); + + if (c == '\'' && b != '\\' && !inDoubleString) + { + inSingleString = !inSingleString; + } + else if (c == '"' && b != '\\' && !inSingleString) + { + inDoubleString = !inDoubleString; + } + + if ((inSingleString || inDoubleString) == false) + { + if (c == '-' && b == '-' && output.Length >= 2 && !output.ToString().EndsWith("var(--")) + { + isParsingVarname = true; + output.Length -= 2; + output.Append("var(--"); + } + else if (!Token.IsIdentifierChr(c) && isParsingVarname) + { + output.Length--; + output.Append(')'); + output.Append(c); + isParsingVarname = false; + } + } + } + + if (isParsingVarname) + output.Append(')'); + + return output.ToString(); + } +} diff --git a/src/Object/Token.cs b/src/Object/Token.cs new file mode 100644 index 0000000..d609e62 --- /dev/null +++ b/src/Object/Token.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Cascadium.Compiler; + +namespace Cascadium.Object; + +struct TokenDebugInfo +{ + public int Line; + public int Column; + public string LineText; +} + +struct Token +{ + public static readonly char[] Sel_Combinators = new[] { '>', '~', '+' }; + + public static readonly char Ch_DoubleStringQuote = '"'; + public static readonly char Ch_SingleStringQuote = '\''; + public static readonly char Ch_CharEscape = '\\'; + + public static readonly char Ch_BraceOpen = '{'; + public static readonly char Ch_BraceClose = '}'; + + public static readonly char Ch_ParentesisOpen = '('; + public static readonly char Ch_ParentesisClose = ')'; + + public static readonly char Ch_Comma = ','; + public static readonly char Ch_Semicolon = ';'; + public static readonly char Ch_DoubleDots = ':'; + + public static bool IsIdentifierChr(char c) + { + return Char.IsLetter(c) || Char.IsDigit(c) || c == '_' || c == '-'; + } + + public static bool IsWhitespaceChr(char c) + { + return c == ' ' || c == '\t' || c == '\r' || c == '\t' || c == '\n'; + } + + public string Content { get; } + public TokenType Type { get; } = TokenType.Empty; + public TokenDebugInfo DebugInfo { get; } + + public Token(TokenType type, string content, TextInterpreter raiser) + { + Type = type; + Content = content; + DebugInfo = raiser.TakeSnapshot(-content.Length); + } + + public override string ToString() => $"{{{Type}}} \"{Content}\""; +} + +enum TokenType +{ + Empty = -1, + + Em_Comma = 21, + Em_Semicolon = 22, + Em_DoubleDots = 23, + + Em_Selector = 27, + Em_PropertyName = 30, + Em_PropertyValue = 31, + Em_Statement = 35, + + Em_RuleEnd = 0, + Em_RuleStart = 1 +} diff --git a/src/Object/TokenCollection.cs b/src/Object/TokenCollection.cs new file mode 100644 index 0000000..c521c10 --- /dev/null +++ b/src/Object/TokenCollection.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cascadium.Object; + +internal class TokenCollection +{ + private Token[] tokens; + + public int Position { get; set; } + public Token Last { get => tokens[tokens.Length - 1]; } + public Token Current { get => tokens[Math.Min(Position, tokens.Length - 1)]; } + + public TokenCollection(Token[] tokens) + { + this.tokens = tokens; + } + + public bool Expect(TokenType type, out Token result) + { + bool state = Read(out result); + if (!state) + { + return false; + } + if (result.Type != type) + { + throw new CascadiumException(result.DebugInfo, $"expected {type}. got {result.Type} instead."); + } + return true; + } + + public bool Read(out Token token) + { + bool state; + if (Position < tokens.Length) + { + token = tokens[Position]; + state = true; + } + else + { + token = default; + state = false; + } + Position++; + return state; + } +} diff --git a/src/Rule.cs b/src/Rule.cs deleted file mode 100644 index f15f2a7..0000000 --- a/src/Rule.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Cascadium; - -internal class Rule -{ - public string? Selector { get; set; } - public Dictionary Properties { get; set; } = new Dictionary(); - public override String ToString() => $"{Selector} [{Properties.Count}]"; - public int Order { get; set; } - - internal bool Exported { get; set; } = true; - - internal Int32 GetPropertiesHashCode() - { - int n = 0, j = 1; - - // the property order should impact on the hash code - // so - // foo: bar - // bar: foo - // - // is different than - // - // bar: foo - // foo: bar - - foreach (var kp in Properties) - { - n += (kp.Key.GetHashCode() + kp.Value.GetHashCode()) / 2; - n *= j; - j++; - } - return n / Properties.Count; - } - -} \ No newline at end of file diff --git a/tool/Cascadium-Utility.csproj b/tool/Cascadium-Utility.csproj index 102a17b..af593ad 100644 --- a/tool/Cascadium-Utility.csproj +++ b/tool/Cascadium-Utility.csproj @@ -6,7 +6,6 @@ disable enable cascadium - true true 0.1.2.4 @@ -15,8 +14,11 @@ - - + + + + + diff --git a/tool/CommandLineArguments.cs b/tool/CommandLineArguments.cs index 431f7ff..8fe02ee 100644 --- a/tool/CommandLineArguments.cs +++ b/tool/CommandLineArguments.cs @@ -45,12 +45,6 @@ internal class CommandLineArguments [Option("p:KeepNestingSpace", Group = "Compiler settings", Order = 3, HelpText = "Specifies whether the compiler should keep spaces after the & operator.")] public BoolType KeepNestingSpace { get; set; } = BoolType.False; - [Option("p:Merge", Group = "Compiler settings", Order = 3, HelpText = "Specifies whether the compiler should merge rules and at-rules.")] - public MergeOption Merge { get; set; } = MergeOption.None; - - [Option("p:MergeOrderPriority", Group = "Compiler settings", Order = 3, HelpText = "Specifies how the merge will prioritize the order of the rules as it finds them.")] - public MergeOrderPriority MergeOrderPriority { get; set; } = MergeOrderPriority.PreserveFirst; - [Option("watch", Group = "Other", Order = 4, HelpText = "Specifies if the compiler should watch for file changes and rebuild on each save.")] public bool Watch { get; set; } = false; diff --git a/tool/Compiler.cs b/tool/Compiler.cs index 06098aa..e0ad437 100644 --- a/tool/Compiler.cs +++ b/tool/Compiler.cs @@ -32,9 +32,7 @@ public static int RunCompiler(CommandLineArguments args) { Pretty = args.Pretty == BoolType.True, UseVarShortcut = args.UseVarShortcuts == BoolType.True, - KeepNestingSpace = args.KeepNestingSpace == BoolType.True, - Merge = args.Merge, - MergeOrderPriority = args.MergeOrderPriority + KeepNestingSpace = args.KeepNestingSpace == BoolType.True }; Program.CompilerOptions?.ApplyConfiguration(options); @@ -101,21 +99,51 @@ public static int RunCompiler(CommandLineArguments args) if (inputFiles.Count > 0) { anyCompiled = true; + StringBuilder resultCss = new StringBuilder(); - string rawCss = string.Join("\n", inputFiles.Select(File.ReadAllText)); + long compiledLength = 0, totalLength = 0; + int smallInputLength = inputFiles.Select(Path.GetDirectoryName).Min(i => i?.Length ?? 0); - long compiledLength = 0, totalLength = rawCss.Length; - string result = Cascadium.CascadiumCompiler.Compile(rawCss, options); + foreach (string file in inputFiles) + { + string contents = ReadFile(file); + string result; + totalLength += contents.Length; + + try + { + result = CascadiumCompiler.Compile(contents, options); + compiledLength += result.Length; + + if (options.Pretty) + { + resultCss.AppendLine(result + "\n"); + } + else + { + 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 (outputFile != null) { - File.WriteAllText(outputFile, result); + File.WriteAllText(outputFile, resultCss.ToString()); compiledLength = new FileInfo(outputFile).Length; Log.Info($"{inputFiles.Count} file(s) -> {Path.GetFileName(args.OutputFile)} [{PathUtils.FileSize(totalLength)} -> {PathUtils.FileSize(compiledLength)}]"); } else { - Console.Write(result.ToString()); + Console.Write(resultCss.ToString()); } } } @@ -127,4 +155,9 @@ public static int RunCompiler(CommandLineArguments args) return 0; } + + static string ReadFile(string file) + { + return System.IO.File.ReadAllText(file); + } } diff --git a/tool/JsonCompilerOptions.cs b/tool/JsonCompilerOptions.cs index 1748500..e04a0cc 100644 --- a/tool/JsonCompilerOptions.cs +++ b/tool/JsonCompilerOptions.cs @@ -22,8 +22,6 @@ internal class JsonCssCompilerOptions public bool? KeepNestingSpace { get; set; } public bool? Pretty { get; set; } public bool? UseVarShortcut { get; set; } - public string? Merge { get; set; } - public string? MergeOrderPriority { get; set; } public IEnumerable Converters { get; set; } = Array.Empty(); public IDictionary AtRulesRewrites { get; set; } = new Dictionary(); @@ -41,32 +39,27 @@ public static JsonCssCompilerOptions Create(string configFile) string contents = File.ReadAllText(pathToConfigFile); - JsonOptions.ThrowOnInvalidCast = true; JsonOptions.PropertyNameCaseInsensitive = true; JsonOptions.Mappers.Add(new StaticCSSConverterMapper()); JsonOptions.Mappers.Add(new DictionaryMapper()); - JsonObject jsonObj = JsonValue.Parse(contents).AsJsonObject!; + JsonObject jsonObj = JsonValue.Parse(contents).GetJsonObject(); JsonCssCompilerOptions compilerConfig = new JsonCssCompilerOptions(); - compilerConfig.InputFiles = new List(jsonObj["InputFiles"].MaybeNull()?.AsJsonArray!.Select(i => i.AsString!) ?? Array.Empty()); - compilerConfig.InputDirectories = new List(jsonObj["InputDirectories"].MaybeNull()?.AsJsonArray!.Select(i => i.AsString!) ?? Array.Empty()); - compilerConfig.OutputFile = jsonObj["OutputFile"].MaybeNull()?.AsString; - compilerConfig.KeepNestingSpace = jsonObj["KeepNestingSpace"].MaybeNull()?.AsBoolean; - compilerConfig.Pretty = jsonObj["pretty"].MaybeNull()?.AsBoolean; - compilerConfig.UseVarShortcut = jsonObj["useVarShortcut"].MaybeNull()?.AsBoolean; - compilerConfig.Merge = jsonObj["merge"].MaybeNull()?.AsString; - compilerConfig.MergeOrderPriority = jsonObj["MergeOrderPriority"].MaybeNull()?.AsString; - compilerConfig.Converters = jsonObj["Converters"].MaybeNull()?.AsJsonArray!.EveryAs() ?? Array.Empty(); - compilerConfig.AtRulesRewrites = jsonObj["AtRulesRewrites"].MaybeNull()?.As>() ?? new Dictionary(); - compilerConfig.Extensions = jsonObj["Extensions"].MaybeNull()?.AsJsonArray!.Select(s => s.AsString!) ?? Array.Empty(); - compilerConfig.ExcludePatterns = jsonObj["ExcludePatterns"].MaybeNull()?.AsJsonArray!.Select(s => s.AsString!) ?? Array.Empty(); + compilerConfig.InputFiles = new List(jsonObj["InputFiles"].MaybeNull()?.GetJsonArray().Select(i => i.GetString()) ?? Array.Empty()); + compilerConfig.InputDirectories = new List(jsonObj["InputDirectories"].MaybeNull()?.GetJsonArray().Select(i => i.GetString()) ?? Array.Empty()); + compilerConfig.OutputFile = jsonObj["OutputFile"].MaybeNull()?.GetString(); + compilerConfig.KeepNestingSpace = jsonObj["KeepNestingSpace"].MaybeNull()?.GetBoolean(); + compilerConfig.Pretty = jsonObj["pretty"].MaybeNull()?.GetBoolean(); + compilerConfig.UseVarShortcut = jsonObj["useVarShortcut"].MaybeNull()?.GetBoolean(); + compilerConfig.Converters = jsonObj["Converters"].MaybeNull()?.GetJsonArray().Select(s => s.Get()) ?? Array.Empty(); + compilerConfig.AtRulesRewrites = jsonObj["AtRulesRewrites"].MaybeNull()?.Get>() ?? new Dictionary(); + compilerConfig.Extensions = jsonObj["Extensions"].MaybeNull()?.GetJsonArray().Select(s => s.GetString()) ?? Array.Empty(); + compilerConfig.ExcludePatterns = jsonObj["ExcludePatterns"].MaybeNull()?.GetJsonArray().Select(s => s.GetString()) ?? Array.Empty(); return compilerConfig; } - [DynamicDependency("MergeOption")] - [DynamicDependency("MergeOrderPriority")] public void ApplyConfiguration(CascadiumOptions compilerOptions) { compilerOptions.Converters.AddRange(Converters); @@ -75,8 +68,6 @@ public void ApplyConfiguration(CascadiumOptions compilerOptions) if (this.UseVarShortcut != null) compilerOptions.UseVarShortcut = this.UseVarShortcut.Value; if (this.Pretty != null) compilerOptions.Pretty = this.Pretty.Value; if (this.KeepNestingSpace != null) compilerOptions.KeepNestingSpace = this.KeepNestingSpace.Value; - if (this.Merge != null) compilerOptions.Merge = Enum.Parse(this.Merge, true); - if (this.MergeOrderPriority != null) compilerOptions.MergeOrderPriority = Enum.Parse(this.MergeOrderPriority, true); foreach (KeyValuePair mediaRw in this.AtRulesRewrites) { @@ -85,23 +76,22 @@ public void ApplyConfiguration(CascadiumOptions compilerOptions) } } -public class DictionaryMapper : JsonSerializerMapper +public class DictionaryMapper : LightJson.Converters.JsonConverter { public override Boolean CanSerialize(Type obj) { return obj == typeof(IDictionary); } - public override Object Deserialize(JsonValue value) + public override Object Deserialize(JsonValue value, Type requestedType) { var dict = new Dictionary(); - value.EnsureType(JsonValueType.Object); - var obj = value.AsJsonObject!; + var obj = value.GetJsonObject(); foreach (var kvp in obj.Properties) { - dict.Add(kvp.Key, kvp.Value.AsString!); + dict.Add(kvp.Key, kvp.Value.GetString()); } return dict; @@ -113,20 +103,20 @@ public override JsonValue Serialize(Object value) } } -public class StaticCSSConverterMapper : JsonSerializerMapper +public class StaticCSSConverterMapper : LightJson.Converters.JsonConverter { public override Boolean CanSerialize(Type obj) { return obj == typeof(StaticCSSConverter); } - public override Object Deserialize(JsonValue value) + public override Object Deserialize(JsonValue value, Type requestedType) { return new StaticCSSConverter() { - ArgumentCount = (int)value["ArgumentCount"].AsNumber, - MatchProperty = value["MatchProperty"].AsString, - Output = value["Output"].As>() + ArgumentCount = value["ArgumentCount"].MaybeNull()?.GetInteger(), + MatchProperty = value["MatchProperty"].GetString(), + Output = value["Output"].Get>() }; } diff --git a/tool/Program.cs b/tool/Program.cs index 66a3fc7..1d60b95 100644 --- a/tool/Program.cs +++ b/tool/Program.cs @@ -7,7 +7,7 @@ namespace cascadiumtool; internal class Program { - public const string VersionLabel = "v.0.1.1-alpha-3"; + public const string VersionLabel = "v.0.2-alpha-4"; public static string CurrentDirectory { get; } = Directory.GetCurrentDirectory(); public static bool HasRootConfiguration { get; private set; } public static JsonCssCompilerOptions? CompilerOptions { get; set; } diff --git a/tool/etc/build.bat b/tool/etc/build.bat index 541d89f..8f3283b 100644 --- a/tool/etc/build.bat +++ b/tool/etc/build.bat @@ -4,4 +4,12 @@ SET NAME=%1 ECHO Building %NAME%... -dotnet publish "%~dp0/../Cascadium-Utility.csproj" --nologo -v quiet -r %NAME% -c Release -o "%~dp0bin/Build/%NAME%/" \ No newline at end of file +dotnet publish "%~dp0/../Cascadium-Utility.csproj" --nologo ^ + -v quiet ^ + -r %NAME% ^ + -c Release ^ + --self-contained true ^ + -p:PublishReadyToRun=true ^ + -p:PublishTrimmed=true ^ + -p:PublishSingleFile=true ^ + -o "%~dp0bin/Build/%NAME%/" \ No newline at end of file